Improved autocompletion!

This commit is contained in:
Gregory Schier
2023-03-02 11:14:51 -08:00
parent 0ccceaac77
commit 548aa4c7cd
8 changed files with 89 additions and 64 deletions

View File

@@ -13,30 +13,24 @@
} }
.cm-editor { .cm-editor {
width: 100%; @apply w-full block;
display: block;
} }
.cm-singleline .cm-scroller { .cm-singleline .cm-scroller {
overflow: hidden !important;; overflow: hidden !important;;
} }
.cm-editor .cm-tooltip {
color: gray;
}
.cm-editor .placeholder-widget { .cm-editor .placeholder-widget {
background-color: hsl(var(--color-blue-400)); @apply text-xs text-white bg-blue-400 py-[1px] px-1 mx-[1px] rounded border border-gray-50 cursor-pointer;
text-shadow: 0 0 0.2em black; text-shadow: 0 0 0.2em black;
padding: 0.05em 0.3em;
border-radius: 0.2em;
color: hsl(var(--color-blue-900));
cursor: pointer;
} }
.cm-editor .cm-scroller { .cm-editor .cm-scroller {
border-radius: var(--border-radius-lg); @apply rounded-lg bg-gray-50;
background-color: hsl(var(--color-gray-50)); }
.cm-multiline .cm-editor .cm-scroller {
padding-bottom: 300px;
} }
.cm-editor.cm-focused { .cm-editor.cm-focused {
@@ -137,7 +131,7 @@
/* <-- */ /* <-- */
.cm-editor .cm-tooltip { .cm-editor .cm-tooltip {
@apply shadow-lg border-0 bg-background rounded overflow-hidden text-gray-900; @apply shadow-lg bg-background rounded overflow-hidden text-gray-900 border border-gray-100/70;
} }
.cm-editor .cm-tooltip * { .cm-editor .cm-tooltip * {

View File

@@ -119,7 +119,6 @@ function getExtensions({
? [ ? [
EditorView.domEventHandlers({ EditorView.domEventHandlers({
keydown: (e) => { keydown: (e) => {
console.log('KEYDOWN', e);
if (e.key === 'Enter') onSubmit?.(); if (e.key === 'Enter') onSubmit?.();
}, },
}), }),

View File

@@ -1,38 +0,0 @@
import type { CompletionContext } from '@codemirror/autocomplete';
const openTag = '${[ ';
const closeTag = ' ]}';
const variables = [
{ name: 'DOMAIN' },
{ name: 'BASE_URL' },
{ name: 'TOKEN' },
{ name: 'PROJECT_ID' },
{ name: 'DUMMY' },
{ name: 'DUMMY_2' },
];
export function myCompletions(context: CompletionContext) {
const toStartOfName = context.explicit ? context.matchBefore(/\w*/) : context.matchBefore(/\w+/);
const toStartOfVariable = context.matchBefore(/\$\{?\[?\s*\w*/);
const toMatch = toStartOfVariable ?? toStartOfName ?? null;
if (toMatch === null) {
return null;
}
// Match a minimum of two characters when typing a variable ${[...]} to prevent it
// from opening on "$"
if (toStartOfVariable !== null && toMatch.to - toMatch.from < 2 && !context.explicit) {
return null;
}
return {
from: toMatch.from,
options: variables.map((v) => ({
label: toStartOfVariable ? `${openTag}${v.name}${closeTag}` : v.name,
apply: `${openTag}${v.name}${closeTag}`,
type: 'variable',
})),
};
}

View File

@@ -107,7 +107,7 @@ export const baseExtensions = [
drawSelection(), drawSelection(),
dropCursor(), dropCursor(),
bracketMatching(), bracketMatching(),
autocompletion({ activateOnTyping: false, closeOnBlur: true }), autocompletion({ closeOnBlur: true }),
syntaxHighlighting(myHighlightStyle), syntaxHighlighting(myHighlightStyle),
EditorState.allowMultipleSelections.of(true), EditorState.allowMultipleSelections.of(true),
]; ];

View File

@@ -0,0 +1,55 @@
import type { CompletionContext } from '@codemirror/autocomplete';
import { match } from 'assert';
const openTag = '${[ ';
const closeTag = ' ]}';
const variables = [
{ name: 'DOMAIN' },
{ name: 'BASE_URL' },
{ name: 'TOKEN' },
{ name: 'PROJECT_ID' },
{ name: 'DUMMY' },
{ name: 'DUMMY_2' },
{ name: 'STRIPE_PUB_KEY' },
{ name: 'RAILWAY_TOKEN' },
{ name: 'SECRET' },
{ name: 'PORT' },
];
const MIN_MATCH_VAR = 2;
const MIN_MATCH_NAME = 2;
export 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;
const matchLen = toMatch.to - toMatch.from;
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: variables
.map((v) => ({
label: toStartOfVariable ? `${openTag}${v.name}${closeTag}` : v.name,
apply: `${openTag}${v.name}${closeTag}`,
type: 'variable',
}))
// Filter out exact matches
.filter((o) => o.label !== toMatch.text),
};
}

View File

@@ -1,18 +1,18 @@
import { LanguageSupport, LRLanguage } from '@codemirror/language'; import { LanguageSupport, LRLanguage } from '@codemirror/language';
import { parseMixed } from '@lezer/common'; import { parseMixed } from '@lezer/common';
import { myCompletions } from '../completion/completion'; import { completions } from './completion';
import { placeholders } from '../widgets'; import { placeholders } from '../widgets';
import { parser as twigParser } from './twig'; import { parser as twigParser } from './twig';
export function twig(base?: LanguageSupport) { export function twig(base?: LanguageSupport) {
const language = mixedOrPlainLanguage(base); const language = mixedOrPlainLanguage(base);
const completion = language.data.of({ const completion = language.data.of({
autocomplete: myCompletions, autocomplete: completions,
}); });
const languageSupport = new LanguageSupport(language, [completion]); const languageSupport = new LanguageSupport(language, [completion]);
if (base) { if (base) {
const completion2 = base.language.data.of({ autocomplete: myCompletions }); const completion2 = base.language.data.of({ autocomplete: completions });
const languageSupport2 = new LanguageSupport(base.language, [completion2]); const languageSupport2 = new LanguageSupport(base.language, [completion2]);
return [languageSupport, languageSupport2, placeholders, base.support]; return [languageSupport, languageSupport2, placeholders, base.support];
} else { } else {

View File

@@ -0,0 +1,19 @@
import type { CompletionContext } from '@codemirror/autocomplete';
const options = [
{ label: 'http://', type: 'constant' },
{ label: 'https://', type: 'constant' },
];
const MIN_MATCH = 1;
export function completions(context: CompletionContext) {
const toMatch = context.matchBefore(/^[\w:/]*/);
if (toMatch === null) return null;
const matchedMinimumLength = toMatch.to - toMatch.from >= MIN_MATCH;
if (!matchedMinimumLength && !context.explicit) return null;
const optionsWithoutExactMatches = options.filter((o) => o.label !== toMatch.text);
return { from: toMatch.from, options: optionsWithoutExactMatches };
}

View File

@@ -1,5 +1,6 @@
import { completeFromList } from '@codemirror/autocomplete'; import { completeFromList } from '@codemirror/autocomplete';
import { LanguageSupport, LRLanguage } from '@codemirror/language'; import { LanguageSupport, LRLanguage } from '@codemirror/language';
import { completions } from './completion';
import { parser } from './url'; import { parser } from './url';
const urlLanguage = LRLanguage.define({ const urlLanguage = LRLanguage.define({
@@ -7,13 +8,8 @@ const urlLanguage = LRLanguage.define({
languageData: {}, languageData: {},
}); });
const exampleCompletion = urlLanguage.data.of({ const completion = urlLanguage.data.of({ autocomplete: completions });
autocomplete: completeFromList([
{ label: 'http://', type: 'constant' },
{ label: 'https://', type: 'constant' },
]),
});
export function url() { export function url() {
return new LanguageSupport(urlLanguage, [exampleCompletion]); return new LanguageSupport(urlLanguage, [completion]);
} }