From 83e2cab1b6c2cce9727d14bbc9eb51bfc81e604c Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 23 Oct 2023 10:31:21 -0700 Subject: [PATCH] Hacky implementation of variable autocomplete --- src-web/components/WorkspaceHeader.tsx | 7 +- src-web/components/core/Editor/Editor.tsx | 40 ++++++----- src-web/components/core/Editor/extensions.ts | 6 +- .../core/Editor/genericCompletion.ts | 2 +- src-web/components/core/Editor/singleLine.ts | 9 +-- .../components/core/Editor/twig/completion.ts | 70 ++++++++++--------- .../components/core/Editor/twig/extension.ts | 10 ++- 7 files changed, 83 insertions(+), 61 deletions(-) diff --git a/src-web/components/WorkspaceHeader.tsx b/src-web/components/WorkspaceHeader.tsx index 4eb610a3..860d46fb 100644 --- a/src-web/components/WorkspaceHeader.tsx +++ b/src-web/components/WorkspaceHeader.tsx @@ -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 + } }} /> diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index c9333b43..c90c1125 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -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(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(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(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 diff --git a/src-web/components/core/Editor/extensions.ts b/src-web/components/core/Editor/extensions.ts index 703d7bbf..c1118d3d 100644 --- a/src-web/components/core/Editor/extensions.ts +++ b/src-web/components/core/Editor/extensions.ts @@ -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 = { export function getLanguageExtension({ contentType, useTemplating = false, + environment, autocomplete, -}: Pick) { +}: { environment: Environment | null } & Pick) { 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 = [ diff --git a/src-web/components/core/Editor/genericCompletion.ts b/src-web/components/core/Editor/genericCompletion.ts index af6366da..487e6283 100644 --- a/src-web/components/core/Editor/genericCompletion.ts +++ b/src-web/components/core/Editor/genericCompletion.ts @@ -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', }; }; } diff --git a/src-web/components/core/Editor/singleLine.ts b/src-web/components/core/Editor/singleLine.ts index 1a166b59..a9a0de00 100644 --- a/src-web/components/core/Editor/singleLine.ts +++ b/src-web/components/core/Editor/singleLine.ts @@ -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; }, ); } diff --git a/src-web/components/core/Editor/twig/completion.ts b/src-web/components/core/Editor/twig/completion.ts index b98861e0..3ac457a5 100644 --- a/src-web/components/core/Editor/twig/completion.ts +++ b/src-web/components/core/Editor/twig/completion.ts @@ -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), - }; } diff --git a/src-web/components/core/Editor/twig/extension.ts b/src-web/components/core/Editor/twig/extension.ts index 2fbc7789..56949767 100644 --- a/src-web/components/core/Editor/twig/extension.ts +++ b/src-web/components/core/Editor/twig/extension.ts @@ -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 });