A bit more chaining cleanup

This commit is contained in:
Gregory Schier
2024-08-19 16:38:28 -07:00
parent 96125a0741
commit dbfe2dc93d
16 changed files with 173 additions and 51 deletions

View File

@@ -180,7 +180,30 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
);
const onClickVariable = useCallback(
async (v: EnvironmentVariable, tagValue: string, startPos: number) => {
async (_v: EnvironmentVariable, tagValue: string, startPos: number) => {
const initialTokens = await parseTemplate(tagValue);
dialog.show({
size: 'dynamic',
id: 'template-variable',
title: 'Configure Variable',
render: ({ hide }) => (
<TemplateVariableDialog
hide={hide}
initialTokens={initialTokens}
onChange={(insert) => {
cm.current?.view.dispatch({
changes: [{ from: startPos, to: startPos + tagValue.length, insert }],
});
}}
/>
),
});
},
[dialog],
);
const onClickMissingVariable = useCallback(
async (_name: string, tagValue: string, startPos: number) => {
const initialTokens = await parseTemplate(tagValue);
dialog.show({
size: 'dynamic',
@@ -188,7 +211,6 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
title: 'Configure Variable',
render: ({ hide }) => (
<TemplateVariableDialog
definition={v}
hide={hide}
initialTokens={initialTokens}
onChange={(insert) => {
@@ -215,6 +237,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
templateFunctions,
onClickFunction,
onClickVariable,
onClickMissingVariable,
});
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
}, [
@@ -225,6 +248,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
templateFunctions,
onClickFunction,
onClickVariable,
onClickMissingVariable,
]);
// Initialize the editor when ref mounts
@@ -247,6 +271,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
templateFunctions,
onClickVariable,
onClickFunction,
onClickMissingVariable,
});
const state = EditorState.create({
@@ -358,7 +383,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
justifyContent="end"
className={classNames(
'absolute bottom-2 left-0 right-0',
'pointer-events-none', // No pointer events so we don't block the editor
'pointer-events-none', // No pointer events, so we don't block the editor
)}
>
{decoratedActions}

View File

@@ -31,10 +31,9 @@ import {
rectangularSelection,
} from '@codemirror/view';
import { tags as t } from '@lezer/highlight';
import type { EnvironmentVariable } from '@yaakapp/api';
import type { EnvironmentVariable, TemplateFunction } from '@yaakapp/api';
import { graphql, graphqlLanguageSupport } from 'cm6-graphql';
import { EditorView } from 'codemirror';
import type { TemplateFunction } from '../../../hooks/useTemplateFunctions';
import type { EditorProps } from './index';
import { pairs } from './pairs/extension';
import { text } from './text/extension';
@@ -84,11 +83,13 @@ export function getLanguageExtension({
templateFunctions,
onClickVariable,
onClickFunction,
onClickMissingVariable,
}: {
environmentVariables: EnvironmentVariable[];
templateFunctions: TemplateFunction[];
onClickFunction: (option: TemplateFunction, tagValue: string, startPos: number) => void;
onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void;
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void;
} & Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete'>) {
const justContentType = contentType?.split(';')[0] ?? contentType ?? '';
if (justContentType === 'application/graphql') {
@@ -106,6 +107,7 @@ export function getLanguageExtension({
autocomplete,
onClickFunction,
onClickVariable,
onClickMissingVariable,
});
}

View File

@@ -3,13 +3,22 @@ import type { CompletionContext } from '@codemirror/autocomplete';
const openTag = '${[ ';
const closeTag = ' ]}';
export interface TwigCompletionOption {
export type TwigCompletionOptionVariable = {
type: 'variable';
};
export type TwigCompletionOptionFunction = {
args: { name: string }[];
type: 'function';
};
export type TwigCompletionOption = (TwigCompletionOptionFunction | TwigCompletionOptionVariable) & {
name: string;
label: string;
type: 'function' | 'variable' | 'unknown';
onClick: (rawTag: string, startPos: number) => void;
value: string | null;
onClick?: (rawTag: string, startPos: number) => void;
}
invalid?: boolean;
};
export interface TwigCompletionConfig {
options: TwigCompletionOption[];
@@ -46,10 +55,9 @@ export function twigCompletion({ options }: TwigCompletionConfig) {
options: options
.filter((v) => v.name.trim())
.map((v) => {
const innerLabel = v.type === 'function' ? `${v.name}()` : v.name;
const tagSyntax = openTag + innerLabel + closeTag;
const tagSyntax = openTag + v.label + closeTag;
return {
label: innerLabel,
label: v.label,
apply: tagSyntax,
type: v.type === 'variable' ? 'variable' : 'function',
matchLen: matchLen,

View File

@@ -1,8 +1,7 @@
import type { LanguageSupport } from '@codemirror/language';
import { LRLanguage } from '@codemirror/language';
import { parseMixed } from '@lezer/common';
import type { EnvironmentVariable } from '@yaakapp/api';
import type { TemplateFunction } from '../../../../hooks/useTemplateFunctions';
import type { EnvironmentVariable, TemplateFunction } from '@yaakapp/api';
import type { GenericCompletionConfig } from '../genericCompletion';
import { genericCompletion } from '../genericCompletion';
import { textLanguageName } from '../text/extension';
@@ -18,6 +17,7 @@ export function twig({
autocomplete,
onClickFunction,
onClickVariable,
onClickMissingVariable,
}: {
base: LanguageSupport;
environmentVariables: EnvironmentVariable[];
@@ -25,6 +25,7 @@ export function twig({
autocomplete?: GenericCompletionConfig;
onClickFunction: (option: TemplateFunction, tagValue: string, startPos: number) => void;
onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void;
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void;
}) {
const language = mixLanguage(base);
@@ -35,14 +36,23 @@ export function twig({
label: v.name,
onClick: (rawTag: string, startPos: number) => onClickVariable(v, rawTag, startPos),
})) ?? [];
const functionOptions: TwigCompletionOption[] =
templateFunctions.map((fn) => ({
name: fn.name,
type: 'function',
value: null,
label: fn.name + '(' + fn.args.length + ')',
onClick: (rawTag: string, startPos: number) => onClickFunction(fn, rawTag, startPos),
})) ?? [];
templateFunctions.map((fn) => {
const shortArgs =
fn.args
.slice(0, 2)
.map((a) => a.name)
.join(', ') + (fn.args.length > 2 ? ', …' : '');
return {
name: fn.name,
type: 'function',
args: fn.args.map((a) => ({ name: a.name })),
value: null,
label: `${fn.name}(${shortArgs})`,
onClick: (rawTag: string, startPos: number) => onClickFunction(fn, rawTag, startPos),
};
}) ?? [];
const options = [...variableOptions, ...functionOptions];
@@ -51,7 +61,7 @@ export function twig({
return [
language,
base.support,
templateTags(options),
templateTags(options, onClickMissingVariable),
language.data.of({ autocomplete: completions }),
base.language.data.of({ autocomplete: completions }),
language.data.of({ autocomplete: genericCompletion(autocomplete) }),

View File

@@ -1,11 +1,8 @@
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
import { truncate } from '../../../../lib/truncate';
import { BetterMatchDecorator } from '../BetterMatchDecorator';
import type { TwigCompletionOption } from './completion';
const TAG_TRUNCATE_LEN = 30;
class TemplateTagWidget extends WidgetType {
readonly #clickListenerCallback: () => void;
@@ -32,17 +29,15 @@ class TemplateTagWidget extends WidgetType {
toDOM() {
const elt = document.createElement('span');
elt.className = `x-theme-templateTag template-tag ${
this.option.type === 'unknown'
this.option.invalid
? 'x-theme-templateTag--danger'
: this.option.type === 'variable'
? 'x-theme-templateTag--primary'
: 'x-theme-templateTag--info'
}`;
elt.title = this.option.type === 'unknown' ? '__NOT_FOUND__' : this.option.value ?? '';
elt.textContent = truncate(
this.rawTag.replace('${[', '').replace(']}', '').trim(),
TAG_TRUNCATE_LEN,
);
elt.title = this.option.invalid ? 'Not Found' : this.option.value ?? '';
elt.setAttribute('data-tag-type', this.option.type);
elt.textContent = this.option.label;
elt.addEventListener('click', this.#clickListenerCallback);
return elt;
}
@@ -57,7 +52,10 @@ class TemplateTagWidget extends WidgetType {
}
}
export function templateTags(options: TwigCompletionOption[]) {
export function templateTags(
options: TwigCompletionOption[],
onClickMissingVariable: (name: string, rawTag: string, startPos: number) => void,
) {
const templateTagMatcher = new BetterMatchDecorator({
regexp: /\$\{\[\s*(.+)(?!]})\s*]}/g,
decoration(match, view, matchStartPos) {
@@ -82,7 +80,14 @@ export function templateTags(options: TwigCompletionOption[]) {
let option = options.find((v) => v.name === name);
if (option == null) {
option = { type: 'unknown', name: innerTagMatch, value: null, label: innerTagMatch };
option = {
invalid: true,
type: 'variable',
name: innerTagMatch,
value: null,
label: innerTagMatch,
onClick: () => onClickMissingVariable(name, match[0], matchStartPos),
};
}
return Decoration.replace({