Beginnings of autocomplete for headers

This commit is contained in:
Gregory Schier
2023-03-17 16:51:20 -07:00
parent 10616001df
commit e33085a7b4
17 changed files with 155 additions and 48 deletions

View File

@@ -29,7 +29,7 @@
@apply bg-transparent;
}
&.cm-focused .cm-selectionBackground {
@apply bg-gray-400;
@apply bg-violet-500/20;
}
/* Style gutters */
@@ -155,7 +155,7 @@
&.cm-tooltip-autocomplete {
& > ul {
@apply p-1 max-h-[40vh];
@apply p-1 max-h-[20rem];
}
& > ul > li {

View File

@@ -9,6 +9,7 @@ import { useUnmount } from 'react-use';
import { IconButton } from '../IconButton';
import './Editor.css';
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
import type { GenericCompletionOption } from './genericCompletion';
import { singleLineExt } from './singleLine';
export interface _EditorProps {
@@ -26,6 +27,7 @@ export interface _EditorProps {
onFocus?: () => void;
singleLine?: boolean;
format?: (v: string) => string;
autocompleteOptions?: GenericCompletionOption[];
}
export function _Editor({
@@ -41,6 +43,7 @@ export function _Editor({
className,
singleLine,
format,
autocompleteOptions,
}: _EditorProps) {
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
const wrapperRef = useRef<HTMLDivElement | null>(null);
@@ -77,16 +80,16 @@ export function _Editor({
useEffect(() => {
if (cm.current === null) return;
const { view, languageCompartment } = cm.current;
const ext = getLanguageExtension({ contentType, useTemplating });
const ext = getLanguageExtension({ contentType, useTemplating, autocompleteOptions });
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
}, [contentType]);
}, [contentType, JSON.stringify(autocompleteOptions)]);
// Initialize the editor when ref mounts
useEffect(() => {
if (wrapperRef.current === null || cm.current !== null) return;
try {
const languageCompartment = new Compartment();
const langExt = getLanguageExtension({ contentType, useTemplating });
const langExt = getLanguageExtension({ contentType, useTemplating, autocompleteOptions });
const state = EditorState.create({
doc: `${defaultValue ?? ''}`,
extensions: [

View File

@@ -33,6 +33,8 @@ import {
} from '@codemirror/view';
import { tags as t } from '@lezer/highlight';
import { graphqlLanguageSupport } from 'cm6-graphql';
import type { GenericCompletionOption } from './genericCompletion';
import { text } from './text/extension';
import { twig } from './twig/extension';
import { url } from './url/extension';
@@ -93,17 +95,19 @@ const syntaxExtensions: Record<string, LanguageSupport> = {
export function getLanguageExtension({
contentType,
useTemplating = false,
autocompleteOptions,
}: {
contentType?: string;
useTemplating?: boolean;
autocompleteOptions?: GenericCompletionOption[];
}) {
const justContentType = contentType?.split(';')[0] ?? contentType ?? '';
const base = syntaxExtensions[justContentType] ?? json();
const base = syntaxExtensions[justContentType] ?? text();
if (!useTemplating) {
return [base];
return base ? base : [];
}
return twig(base);
return twig(base, autocompleteOptions);
}
export const baseExtensions = [
@@ -115,7 +119,7 @@ export const baseExtensions = [
// TODO: Figure out how to debounce showing of autocomplete in a good way
// debouncedAutocompletionDisplay({ millis: 1000 }),
// autocompletion({ closeOnBlur: true, interactionDelay: 200, activateOnTyping: false }),
autocompletion({ closeOnBlur: true, interactionDelay: 300 }),
autocompletion({ closeOnBlur: true, interactionDelay: 200 }),
syntaxHighlighting(myHighlightStyle),
EditorState.allowMultipleSelections.of(true),
];

View File

@@ -0,0 +1,25 @@
import type { CompletionContext } from '@codemirror/autocomplete';
export interface GenericCompletionOption {
label: string;
type: 'constant' | 'variable';
}
export function genericCompletion({
options,
minMatch = 1,
}: {
options: GenericCompletionOption[];
minMatch?: number;
}) {
return function completions(context: CompletionContext) {
const toMatch = context.matchBefore(/^[\w:/]*/);
if (toMatch === null) return null;
const matchedMinimumLength = toMatch.to - toMatch.from >= minMatch;
if (!matchedMinimumLength && !context.explicit) return null;
const optionsWithoutExactMatches = options.filter((o) => o.label !== toMatch.text);
return { from: toMatch.from, options: optionsWithoutExactMatches };
};
}

View File

@@ -0,0 +1,11 @@
import { LanguageSupport, LRLanguage } from '@codemirror/language';
import { parser } from './text';
const textLanguage = LRLanguage.define({
parser,
languageData: {},
});
export function text() {
return new LanguageSupport(textLanguage);
}

View File

@@ -0,0 +1,5 @@
@top Template { Text }
@tokens {
Text { ![]+ }
}

View File

@@ -0,0 +1,4 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
Template = 1,
Text = 2

View File

@@ -0,0 +1,16 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
export const parser = LRParser.deserialize({
version: 14,
states: "[OQOPOOQOOOOO",
stateData: "V~OQPO~O",
goto: "QPP",
nodeNames: "⚠ Template Text",
maxTerm: 3,
skippedNodes: [0],
repeatNodeCount: 0,
tokenData: "p~RRO;'S[;'S;=`j<%lO[~aRQ~O;'S[;'S;=`j<%lO[~mP;=`<%l[",
tokenizers: [0],
topRules: {"Template":[0,1]},
tokenPrec: 0
})

View File

@@ -6,6 +6,7 @@ const closeTag = ' ]}';
const variables = [
{ name: 'DOMAIN' },
{ name: 'BASE_URL' },
{ name: 'CONTENT_THINGY' },
{ name: 'TOKEN' },
{ name: 'PROJECT_ID' },
{ name: 'DUMMY' },
@@ -17,7 +18,7 @@ const variables = [
];
const MIN_MATCH_VAR = 2;
const MIN_MATCH_NAME = 4;
const MIN_MATCH_NAME = 3;
export function completions(context: CompletionContext) {
const toStartOfName = context.matchBefore(/\w*/);

View File

@@ -1,15 +1,21 @@
import { LanguageSupport, LRLanguage } from '@codemirror/language';
import { parseMixed } from '@lezer/common';
import { completions } from './completion';
import type { GenericCompletionOption } from '../genericCompletion';
import { genericCompletion } from '../genericCompletion';
import { placeholders } from '../widgets';
import { completions } from './completion';
import { parser as twigParser } from './twig';
export function twig(base?: LanguageSupport) {
export function twig(base?: LanguageSupport, autocompleteOptions?: GenericCompletionOption[]) {
const language = mixedOrPlainLanguage(base);
const additionalCompletion =
autocompleteOptions && base
? [language.data.of({ autocomplete: genericCompletion({ options: autocompleteOptions }) })]
: [];
const completion = language.data.of({
autocomplete: completions,
});
const languageSupport = new LanguageSupport(language, [completion]);
const languageSupport = new LanguageSupport(language, [completion, ...additionalCompletion]);
if (base) {
const completion2 = base.language.data.of({ autocomplete: completions });
@@ -23,18 +29,15 @@ export function twig(base?: LanguageSupport) {
function mixedOrPlainLanguage(base?: LanguageSupport): LRLanguage {
const name = 'twig';
if (base == null) {
if (!base) {
return LRLanguage.define({ name, parser: twigParser });
}
const parser = twigParser.configure({
wrap: parseMixed((node) => {
if (!node.type.isTop) return null;
return {
parser: base.language.parser,
overlay: (node) => node.type.name === 'Text',
};
}),
wrap: parseMixed(() => ({
parser: base.language.parser,
overlay: (node) => node.type.name === 'Text' || node.type.name === 'Template',
})),
});
return LRLanguage.define({ name, parser });

View File

@@ -1,19 +1,9 @@
import type { CompletionContext } from '@codemirror/autocomplete';
import { genericCompletion } from '../genericCompletion';
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 };
}
export const completions = genericCompletion({
options: [
{ label: 'http://', type: 'constant' },
{ label: 'https://', type: 'constant' },
],
minMatch: 1,
});