Node syntaxTree to parse template tags

This commit is contained in:
Gregory Schier
2024-08-26 11:30:10 -07:00
parent 124fb35dcd
commit 24a4e3494e
3 changed files with 64 additions and 49 deletions

View File

@@ -68,7 +68,7 @@ const syntaxExtensions: Record<string, LanguageSupport> = {
'application/graphql': graphqlLanguageSupport(),
'application/json': json(),
'application/javascript': javascript(),
'text/html': xml(), // HTML as xml because HTML is oddly slow
'text/html': xml(), // HTML as XML because HTML is oddly slow
'application/xml': xml(),
'text/xml': xml(),
url: url(),

View File

@@ -7,7 +7,7 @@ import { genericCompletion } from '../genericCompletion';
import { textLanguageName } from '../text/extension';
import type { TwigCompletionOption } from './completion';
import { twigCompletion } from './completion';
import { templateTags } from './templateTags';
import { templateTagsPlugin } from './templateTags';
import { parser as twigParser } from './twig';
export function twig({
@@ -62,7 +62,7 @@ export function twig({
return [
language,
base.support,
templateTags(options, onClickMissingVariable),
templateTagsPlugin(options, onClickMissingVariable),
language.data.of({ autocomplete: completions }),
base.language.data.of({ autocomplete: completions }),
language.data.of({ autocomplete: genericCompletion(autocomplete) }),

View File

@@ -1,6 +1,8 @@
import { syntaxTree } from '@codemirror/language';
import type { Range } from '@codemirror/state';
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
import { BetterMatchDecorator } from '../BetterMatchDecorator';
import { Decoration, ViewPlugin, WidgetType } from '@codemirror/view';
import { EditorView } from 'codemirror';
import type { TwigCompletionOption } from './completion';
class TemplateTagWidget extends WidgetType {
@@ -22,7 +24,8 @@ class TemplateTagWidget extends WidgetType {
this.option.name === other.option.name &&
this.option.type === other.option.type &&
this.option.value === other.option.value &&
this.rawTag === other.rawTag
this.rawTag === other.rawTag &&
this.startPos === other.startPos
);
}
@@ -55,69 +58,81 @@ class TemplateTagWidget extends WidgetType {
}
}
export function templateTags(
function templateTags(
view: EditorView,
options: TwigCompletionOption[],
onClickMissingVariable: (name: string, rawTag: string, startPos: number) => void,
) {
const templateTagMatcher = new BetterMatchDecorator({
regexp: /\$\{\[\s*(.+)(?!]})\s*]}/g,
decoration(match, view, matchStartPos) {
const matchEndPos = matchStartPos + match[0].length - 1;
): DecorationSet {
const widgets: Range<Decoration>[] = [];
for (const { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({
from,
to,
enter: (node) => {
if (node.name == 'Tag') {
// Don't decorate if the cursor is inside the match
for (const r of view.state.selection.ranges) {
if (r.from > node.from && r.to < node.to) {
return;
}
}
// Don't decorate if the cursor is inside the match
for (const r of view.state.selection.ranges) {
if (r.from > matchStartPos && r.to <= matchEndPos) {
return Decoration.replace({});
const rawTag = view.state.doc.sliceString(node.from, node.to);
// TODO: Search `node.tree` instead of using Regex here
const inner = rawTag.replace(/^\$\{\[\s*/, '').replace(/\s*]}$/, '');
const name = inner.match(/(\w+)[(]/)?.[1] ?? inner;
let option = options.find((v) => v.name === name);
if (option == null) {
option = {
invalid: true,
type: 'variable',
name: inner,
value: null,
label: inner,
onClick: () => onClickMissingVariable(name, rawTag, node.from),
};
}
const widget = new TemplateTagWidget(option, rawTag, node.from);
const deco = Decoration.replace({ widget, inclusive: true });
widgets.push(deco.range(node.from, node.to));
}
}
const innerTagMatch = match[1];
if (innerTagMatch == null) {
// Should never happen, but make TS happy
console.warn('Group match was empty', match);
return Decoration.replace({});
}
// TODO: Replace this hacky match with a proper template parser
const name = innerTagMatch.match(/\s*(\w+)[(\s]*/)?.[1] ?? innerTagMatch;
let option = options.find((v) => v.name === name);
if (option == null) {
option = {
invalid: true,
type: 'variable',
name: innerTagMatch,
value: null,
label: innerTagMatch,
onClick: () => onClickMissingVariable(name, match[0], matchStartPos),
};
}
return Decoration.replace({
inclusive: true,
widget: new TemplateTagWidget(option, match[0], matchStartPos),
});
},
});
},
});
}
return Decoration.set(widgets);
}
export function templateTagsPlugin(
options: TwigCompletionOption[],
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void,
) {
return ViewPlugin.fromClass(
class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = templateTagMatcher.createDeco(view);
this.decorations = templateTags(view, options, onClickMissingVariable);
}
update(update: ViewUpdate) {
this.decorations = templateTagMatcher.updateDeco(update, this.decorations);
this.decorations = templateTags(update.view, options, onClickMissingVariable);
}
},
{
decorations: (instance) => instance.decorations,
decorations: (v) => v.decorations,
provide: (plugin) =>
EditorView.atomicRanges.of((view) => {
return view.plugin(plugin)?.decorations || Decoration.none;
}),
eventHandlers: {
mousedown: (e) => {
const target = e.target as HTMLElement;
if (target.classList.contains('template-tag')) console.log('CLICKED TEMPLATE TAG');
// return toggleBoolean(view, view.posAtDOM(target));
},
},
},
);
}