diff --git a/src-web/components/core/Editor/extensions.ts b/src-web/components/core/Editor/extensions.ts index a8b8295e..d15e15c6 100644 --- a/src-web/components/core/Editor/extensions.ts +++ b/src-web/components/core/Editor/extensions.ts @@ -68,7 +68,7 @@ const syntaxExtensions: Record = { '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(), diff --git a/src-web/components/core/Editor/twig/extension.ts b/src-web/components/core/Editor/twig/extension.ts index ab76966e..67643cdb 100644 --- a/src-web/components/core/Editor/twig/extension.ts +++ b/src-web/components/core/Editor/twig/extension.ts @@ -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) }), diff --git a/src-web/components/core/Editor/twig/templateTags.ts b/src-web/components/core/Editor/twig/templateTags.ts index ff1f2634..cb8ab004 100644 --- a/src-web/components/core/Editor/twig/templateTags.ts +++ b/src-web/components/core/Editor/twig/templateTags.ts @@ -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[] = []; + 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)); + }, + }, }, ); }