Hacky implementation of variable autocomplete

This commit is contained in:
Gregory Schier
2023-10-23 10:31:21 -07:00
parent 4ad5d7f291
commit 83e2cab1b6
7 changed files with 83 additions and 61 deletions

View File

@@ -20,7 +20,6 @@ interface Props {
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
const environments = useEnvironments();
const updateEnvironment = useUpdateEnvironment();
const activeRequest = useActiveRequest();
const dialog = useDialog();
@@ -82,7 +81,11 @@ const EnvironmentList = function({ environment }: EnvironmentListProps) {
className='w-full h-[400px] !bg-gray-50'
defaultValue={JSON.stringify(environment.data, null, 2)}
onChange={data => {
updateEnvironment.mutate({ data: JSON.parse(data) });
try {
updateEnvironment.mutate({ data: JSON.parse(data) });
} catch (err) {
// That's okay
}
}}
/>
</div>

View File

@@ -12,6 +12,7 @@ import './Editor.css';
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
import type { GenericCompletionConfig } from './genericCompletion';
import { singleLineExt } from './singleLine';
import { useEnvironments } from '../../../hooks/useEnvironments';
// Export some things so all the code-split parts are in this file
export { buildClientSchema, getIntrospectionQuery } from 'graphql/utilities';
@@ -64,6 +65,9 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
}: EditorProps,
ref,
) {
const environments = useEnvironments();
const environment = environments[0] ?? null;
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
useImperativeHandle(ref, () => cm.current?.view);
@@ -108,9 +112,9 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
useEffect(() => {
if (cm.current === null) return;
const { view, languageCompartment } = cm.current;
const ext = getLanguageExtension({ contentType, useTemplating, autocomplete });
const ext = getLanguageExtension({ contentType, environment, useTemplating, autocomplete });
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
}, [contentType, autocomplete, useTemplating]);
}, [contentType, autocomplete, useTemplating, environment]);
useEffect(() => {
if (cm.current === null) return;
@@ -131,7 +135,7 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
let view: EditorView;
try {
const languageCompartment = new Compartment();
const langExt = getLanguageExtension({ contentType, useTemplating, autocomplete });
const langExt = getLanguageExtension({ contentType, useTemplating, autocomplete, environment });
const state = EditorState.create({
doc: `${defaultValue ?? ''}`,
@@ -238,21 +242,21 @@ function getExtensions({
: []),
...(singleLine
? [
EditorView.domEventHandlers({
focus: (e, view) => {
// select all text on focus, like a regular input does
view.dispatch({ selection: { anchor: 0, head: view.state.doc.length } });
},
keydown: (e) => {
// Submit nearest form on enter if there is one
if (e.key === 'Enter') {
const el = e.currentTarget as HTMLElement;
const form = el.closest('form');
form?.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
}
},
}),
]
EditorView.domEventHandlers({
focus: (_, view) => {
// select all text on focus, like a regular input does
view.dispatch({ selection: { anchor: 0, head: view.state.doc.length } });
},
keydown: (e) => {
// Submit nearest form on enter if there is one
if (e.key === 'Enter') {
const el = e.currentTarget as HTMLElement;
const form = el.closest('form');
form?.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
}
},
}),
]
: []),
// Handle onFocus

View File

@@ -37,6 +37,7 @@ import type { EditorProps } from './index';
import { text } from './text/extension';
import { twig } from './twig/extension';
import { url } from './url/extension';
import type { Environment } from '../../../lib/models';
export const myHighlightStyle = HighlightStyle.define([
{
@@ -95,8 +96,9 @@ const syntaxExtensions: Record<string, LanguageSupport> = {
export function getLanguageExtension({
contentType,
useTemplating = false,
environment,
autocomplete,
}: Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete'>) {
}: { environment: Environment | null } & Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete'>) {
if (contentType === 'application/graphql') {
return graphql();
}
@@ -106,7 +108,7 @@ export function getLanguageExtension({
return base ? base : [];
}
return twig(base, autocomplete);
return twig(base, environment, autocomplete);
}
export const baseExtensions = [

View File

@@ -24,6 +24,6 @@ export function genericCompletion({ options, minMatch = 1 }: GenericCompletionCo
if (!matchedMinimumLength && !context.explicit) return null;
const optionsWithoutExactMatches = options.filter((o) => o.label !== toMatch.text);
return { from: toMatch.from, options: optionsWithoutExactMatches, info: 'hello' };
return { from: toMatch.from, options: optionsWithoutExactMatches, info: 'hello', };
};
}

View File

@@ -6,8 +6,8 @@ export function singleLineExt() {
(tr: Transaction): TransactionSpec | TransactionSpec[] => {
if (!tr.isUserEvent('input')) return tr;
const trs: TransactionSpec[] = [];
tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
const specs: TransactionSpec[] = [];
tr.changes.iterChanges((_, toA, fromB, toB, inserted) => {
let insert = '';
let newlinesRemoved = 0;
for (const line of inserted) {
@@ -21,9 +21,10 @@ export function singleLineExt() {
const selection = EditorSelection.create([cursor], 0);
const changes = [{ from: fromB, to: toA, insert }];
trs.push({ ...tr, selection, changes });
specs.push({ ...tr, selection, changes });
});
return trs;
return specs;
},
);
}

View File

@@ -3,44 +3,50 @@ import type { CompletionContext } from '@codemirror/autocomplete';
const openTag = '${[ ';
const closeTag = ' ]}';
const variables: { name: string }[] = [
{ name: 'foo' }
];
interface TwigCompletionOption {
name: string;
}
export interface TwigCompletionConfig {
options: TwigCompletionOption[];
}
const MIN_MATCH_VAR = 2;
const MIN_MATCH_NAME = 3;
export function completions(context: CompletionContext) {
const toStartOfName = context.matchBefore(/\w*/);
const toStartOfVariable = context.matchBefore(/\$\{?\[?\s*\w*/);
const toMatch = toStartOfVariable ?? toStartOfName ?? null;
export function twigCompletion({ options }: TwigCompletionConfig) {
return function completions(context: CompletionContext) {
const toStartOfName = context.matchBefore(/\w*/);
const toStartOfVariable = context.matchBefore(/\$\{?\[?\s*\w*/);
const toMatch = toStartOfVariable ?? toStartOfName ?? null;
if (toMatch === null) return null;
if (toMatch === null) return null;
const matchLen = toMatch.to - toMatch.from;
const matchLen = toMatch.to - toMatch.from;
const failedVarLen = toStartOfVariable !== null && matchLen < MIN_MATCH_VAR;
if (failedVarLen && !context.explicit) {
return null;
const failedVarLen = toStartOfVariable !== null && matchLen < MIN_MATCH_VAR;
if (failedVarLen && !context.explicit) {
return null;
}
const failedNameLen = toStartOfVariable === null && matchLen < MIN_MATCH_NAME;
if (failedNameLen && !context.explicit) {
return null;
}
// TODO: Figure out how to make autocomplete stay open if opened explicitly. It sucks when you explicitly
// open it, then it closes when you type the next character.
return {
from: toMatch.from,
options: options
.map((v) => ({
label: toStartOfVariable ? `${openTag}${v.name}${closeTag}` : v.name,
apply: `${openTag}${v.name}${closeTag}`,
type: 'variable',
matchLen,
}))
// Filter out exact matches
.filter((o) => o.label !== toMatch.text),
};
}
const failedNameLen = toStartOfVariable === null && matchLen < MIN_MATCH_NAME;
if (failedNameLen && !context.explicit) {
return null;
}
// TODO: Figure out how to make autocomplete stay open if opened explicitly. It sucks when you explicitly
// open it, then it closes when you type the next character.
return {
from: toMatch.from,
options: variables
.map((v) => ({
label: toStartOfVariable ? `${openTag}${v.name}${closeTag}` : v.name,
apply: `${openTag}${v.name}${closeTag}`,
type: 'variable',
matchLen,
}))
// Filter out exact matches
.filter((o) => o.label !== toMatch.text),
};
}

View File

@@ -5,10 +5,16 @@ import type { GenericCompletionConfig } from '../genericCompletion';
import { genericCompletion } from '../genericCompletion';
import { placeholders } from '../placeholder';
import { textLanguageName } from '../text/extension';
import { completions } from './completion';
import { twigCompletion } from './completion';
import { parser as twigParser } from './twig';
import type { Environment } from '../../../../lib/models';
export function twig(base: LanguageSupport, environment: Environment | null, autocomplete?: GenericCompletionConfig) {
// TODO: fill variables here
const data = environment?.data ?? {};
const options = Object.keys(data).map(key => ({ name: key }));
const completions = twigCompletion({ options });
export function twig(base: LanguageSupport, autocomplete?: GenericCompletionConfig) {
const language = mixLanguage(base);
const completion = language.data.of({ autocomplete: completions });
const completionBase = base.language.data.of({ autocomplete: completions });