mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-10 19:26:49 +02:00
Node syntaxTree to parse template tags
This commit is contained in:
@@ -68,7 +68,7 @@ const syntaxExtensions: Record<string, LanguageSupport> = {
|
|||||||
'application/graphql': graphqlLanguageSupport(),
|
'application/graphql': graphqlLanguageSupport(),
|
||||||
'application/json': json(),
|
'application/json': json(),
|
||||||
'application/javascript': javascript(),
|
'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(),
|
'application/xml': xml(),
|
||||||
'text/xml': xml(),
|
'text/xml': xml(),
|
||||||
url: url(),
|
url: url(),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { genericCompletion } from '../genericCompletion';
|
|||||||
import { textLanguageName } from '../text/extension';
|
import { textLanguageName } from '../text/extension';
|
||||||
import type { TwigCompletionOption } from './completion';
|
import type { TwigCompletionOption } from './completion';
|
||||||
import { twigCompletion } from './completion';
|
import { twigCompletion } from './completion';
|
||||||
import { templateTags } from './templateTags';
|
import { templateTagsPlugin } from './templateTags';
|
||||||
import { parser as twigParser } from './twig';
|
import { parser as twigParser } from './twig';
|
||||||
|
|
||||||
export function twig({
|
export function twig({
|
||||||
@@ -62,7 +62,7 @@ export function twig({
|
|||||||
return [
|
return [
|
||||||
language,
|
language,
|
||||||
base.support,
|
base.support,
|
||||||
templateTags(options, onClickMissingVariable),
|
templateTagsPlugin(options, onClickMissingVariable),
|
||||||
language.data.of({ autocomplete: completions }),
|
language.data.of({ autocomplete: completions }),
|
||||||
base.language.data.of({ autocomplete: completions }),
|
base.language.data.of({ autocomplete: completions }),
|
||||||
language.data.of({ autocomplete: genericCompletion(autocomplete) }),
|
language.data.of({ autocomplete: genericCompletion(autocomplete) }),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { syntaxTree } from '@codemirror/language';
|
||||||
|
import type { Range } from '@codemirror/state';
|
||||||
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
||||||
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
|
import { Decoration, ViewPlugin, WidgetType } from '@codemirror/view';
|
||||||
import { BetterMatchDecorator } from '../BetterMatchDecorator';
|
import { EditorView } from 'codemirror';
|
||||||
import type { TwigCompletionOption } from './completion';
|
import type { TwigCompletionOption } from './completion';
|
||||||
|
|
||||||
class TemplateTagWidget extends WidgetType {
|
class TemplateTagWidget extends WidgetType {
|
||||||
@@ -22,7 +24,8 @@ class TemplateTagWidget extends WidgetType {
|
|||||||
this.option.name === other.option.name &&
|
this.option.name === other.option.name &&
|
||||||
this.option.type === other.option.type &&
|
this.option.type === other.option.type &&
|
||||||
this.option.value === other.option.value &&
|
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[],
|
options: TwigCompletionOption[],
|
||||||
onClickMissingVariable: (name: string, rawTag: string, startPos: number) => void,
|
onClickMissingVariable: (name: string, rawTag: string, startPos: number) => void,
|
||||||
) {
|
): DecorationSet {
|
||||||
const templateTagMatcher = new BetterMatchDecorator({
|
const widgets: Range<Decoration>[] = [];
|
||||||
regexp: /\$\{\[\s*(.+)(?!]})\s*]}/g,
|
for (const { from, to } of view.visibleRanges) {
|
||||||
decoration(match, view, matchStartPos) {
|
syntaxTree(view.state).iterate({
|
||||||
const matchEndPos = matchStartPos + match[0].length - 1;
|
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
|
const rawTag = view.state.doc.sliceString(node.from, node.to);
|
||||||
for (const r of view.state.selection.ranges) {
|
|
||||||
if (r.from > matchStartPos && r.to <= matchEndPos) {
|
// TODO: Search `node.tree` instead of using Regex here
|
||||||
return Decoration.replace({});
|
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) {
|
return Decoration.set(widgets);
|
||||||
// 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),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
export function templateTagsPlugin(
|
||||||
|
options: TwigCompletionOption[],
|
||||||
|
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void,
|
||||||
|
) {
|
||||||
return ViewPlugin.fromClass(
|
return ViewPlugin.fromClass(
|
||||||
class {
|
class {
|
||||||
decorations: DecorationSet;
|
decorations: DecorationSet;
|
||||||
|
|
||||||
constructor(view: EditorView) {
|
constructor(view: EditorView) {
|
||||||
this.decorations = templateTagMatcher.createDeco(view);
|
this.decorations = templateTags(view, options, onClickMissingVariable);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(update: ViewUpdate) {
|
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) =>
|
provide: (plugin) =>
|
||||||
EditorView.atomicRanges.of((view) => {
|
EditorView.atomicRanges.of((view) => {
|
||||||
return view.plugin(plugin)?.decorations || Decoration.none;
|
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));
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user