feat(editor): Introduce proxy completions to expressions (#5075)
* ⚡ Introduce proxy completions to expressions * 🧪 Add tests * ⚡ Replace snippet with alphabetic char completions * ⚡ Tighten `DateTime` check * 🧹 Clean up `n8nLang` * 🔥 Remove duplicate * 👕 Remove non-null assertion * ⚡ Confirm that `overlay` is needed * 🔥 Remove comment * 🔥 Remove more unneeded code * 🔥 Remove unneded Pinia setup * ⚡ Simplify syntax
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
||||
lineNumbers,
|
||||
} from '@codemirror/view';
|
||||
import { bracketMatching, foldGutter, indentOnInput } from '@codemirror/language';
|
||||
import { acceptCompletion, closeBrackets } from '@codemirror/autocomplete';
|
||||
import { acceptCompletion } from '@codemirror/autocomplete';
|
||||
import {
|
||||
history,
|
||||
indentWithTab,
|
||||
@@ -16,11 +16,8 @@ import {
|
||||
toggleComment,
|
||||
} from '@codemirror/commands';
|
||||
import { lintGutter } from '@codemirror/lint';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
|
||||
import { customInputHandler } from './inputHandler';
|
||||
|
||||
const [_, bracketState] = closeBrackets() as readonly Extension[];
|
||||
import { codeInputHandler } from '@/plugins/codemirror/inputHandlers/code.inputHandler';
|
||||
|
||||
export const baseExtensions = [
|
||||
lineNumbers(),
|
||||
@@ -29,7 +26,7 @@ export const baseExtensions = [
|
||||
history(),
|
||||
foldGutter(),
|
||||
lintGutter(),
|
||||
[customInputHandler, bracketState],
|
||||
codeInputHandler(),
|
||||
dropCursor(),
|
||||
indentOnInput(),
|
||||
bracketMatching(),
|
||||
|
||||
@@ -54,7 +54,7 @@ export const completerExtension = mixins(
|
||||
// luxon
|
||||
this.todayCompletions,
|
||||
this.nowCompletions,
|
||||
this.dateTimeCompltions,
|
||||
this.dateTimeCompletions,
|
||||
|
||||
// item index
|
||||
this.inputCompletions,
|
||||
@@ -174,7 +174,7 @@ export const completerExtension = mixins(
|
||||
|
||||
if (value === '$now') return this.nowCompletions(context, variable);
|
||||
if (value === '$today') return this.todayCompletions(context, variable);
|
||||
if (value === 'DateTime') return this.dateTimeCompltions(context, variable);
|
||||
if (value === 'DateTime') return this.dateTimeCompletions(context, variable);
|
||||
|
||||
// item index
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import Vue from 'vue';
|
||||
import { isAllowedInDotNotation, escape, toVariableOption } from '../utils';
|
||||
import { escape, toVariableOption } from '../utils';
|
||||
import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
|
||||
import type { IDataObject, IPinData, IRunData } from 'n8n-workflow';
|
||||
import type { CodeNodeEditorMixin } from '../types';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useWorkflowsStore } from '@/stores/workflows';
|
||||
import { useNDVStore } from '@/stores/ndv';
|
||||
import { isAllowedInDotNotation } from '@/plugins/codemirror/completions/utils';
|
||||
|
||||
export const jsonFieldCompletions = (Vue as CodeNodeEditorMixin).extend({
|
||||
computed: {
|
||||
|
||||
@@ -76,7 +76,7 @@ export const luxonCompletions = (Vue as CodeNodeEditorMixin).extend({
|
||||
/**
|
||||
* Complete `DateTime` with luxon `DateTime` static methods.
|
||||
*/
|
||||
dateTimeCompltions(context: CompletionContext, matcher = 'DateTime'): CompletionResult | null {
|
||||
dateTimeCompletions(context: CompletionContext, matcher = 'DateTime'): CompletionResult | null {
|
||||
const pattern = new RegExp(`${escape(matcher)}\..*`);
|
||||
|
||||
const preCursor = context.matchBefore(pattern);
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { completionStatus, insertBracket } from '@codemirror/autocomplete';
|
||||
import { codePointAt, codePointSize } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
|
||||
/**
|
||||
* Customized input handler to prevent token autoclosing in certain cases.
|
||||
*
|
||||
* Based on: https://github.com/codemirror/closebrackets/blob/0a56edfaf2c6d97bc5e88f272de0985b4f41e37a/src/closebrackets.ts#L79
|
||||
*/
|
||||
export const customInputHandler = EditorView.inputHandler.of((view, from, to, insert) => {
|
||||
if (view.composing || view.state.readOnly) return false;
|
||||
|
||||
// customization: do not autoclose tokens during autocompletion
|
||||
if (completionStatus(view.state) !== null) return false;
|
||||
|
||||
const selection = view.state.selection.main;
|
||||
|
||||
// customization: do not autoclose square brackets prior to `.json`
|
||||
if (
|
||||
insert === '[' &&
|
||||
view.state.doc.toString().slice(selection.from - '.json'.length, selection.to) === '.json'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
insert.length > 2 ||
|
||||
(insert.length === 2 && codePointSize(codePointAt(insert, 0)) === 1) ||
|
||||
from !== selection.from ||
|
||||
to !== selection.to
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const transaction = insertBracket(view.state, insert);
|
||||
|
||||
if (!transaction) return false;
|
||||
|
||||
view.dispatch(transaction);
|
||||
|
||||
return true;
|
||||
});
|
||||
@@ -29,12 +29,6 @@ export function walk<T extends RangeNode>(
|
||||
return found as T[];
|
||||
}
|
||||
|
||||
export const isAllowedInDotNotation = (str: string) => {
|
||||
const DOT_NOTATION_BANNED_CHARS = /^(\d)|[\\ `!@#$%^&*()_+\-=[\]{};':"\\|,.<>?~]/g;
|
||||
|
||||
return !DOT_NOTATION_BANNED_CHARS.test(str);
|
||||
};
|
||||
|
||||
export const escape = (str: string) =>
|
||||
str
|
||||
.replace('$', '\\$')
|
||||
|
||||
@@ -10,13 +10,14 @@ import { history } from '@codemirror/commands';
|
||||
|
||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
import { expressionManager } from '@/mixins/expressionManager';
|
||||
import { doubleBraceHandler } from '@/plugins/codemirror/doubleBraceHandler';
|
||||
import { n8nLanguageSupport } from '@/plugins/codemirror/n8nLanguageSupport';
|
||||
import { expressionInputHandler } from '@/plugins/codemirror/inputHandlers/expression.inputHandler';
|
||||
import { n8nLang } from '@/plugins/codemirror/n8nLang';
|
||||
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
||||
import { inputTheme } from './theme';
|
||||
|
||||
import type { IVariableItemSelected } from '@/Interface';
|
||||
import { forceParse } from '@/utils/forceParse';
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
|
||||
export default mixins(expressionManager, workflowHelpers).extend({
|
||||
name: 'ExpressionEditorModalInput',
|
||||
@@ -36,9 +37,10 @@ export default mixins(expressionManager, workflowHelpers).extend({
|
||||
mounted() {
|
||||
const extensions = [
|
||||
inputTheme(),
|
||||
n8nLanguageSupport(),
|
||||
autocompletion(),
|
||||
n8nLang(),
|
||||
history(),
|
||||
doubleBraceHandler(),
|
||||
expressionInputHandler(),
|
||||
EditorView.lineWrapping,
|
||||
EditorState.readOnly.of(this.isReadOnly),
|
||||
EditorView.domEventHandlers({ scroll: forceParse }),
|
||||
|
||||
@@ -13,9 +13,10 @@ import { useNDVStore } from '@/stores/ndv';
|
||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
import { expressionManager } from '@/mixins/expressionManager';
|
||||
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
||||
import { n8nLanguageSupport } from '@/plugins/codemirror/n8nLanguageSupport';
|
||||
import { doubleBraceHandler } from '@/plugins/codemirror/doubleBraceHandler';
|
||||
import { expressionInputHandler } from '@/plugins/codemirror/inputHandlers/expression.inputHandler';
|
||||
import { inputTheme } from './theme';
|
||||
import { autocompletion, ifIn } from '@codemirror/autocomplete';
|
||||
import { n8nLang } from '@/plugins/codemirror/n8nLang';
|
||||
|
||||
export default mixins(expressionManager, workflowHelpers).extend({
|
||||
name: 'InlineExpressionEditorInput',
|
||||
@@ -39,35 +40,19 @@ export default mixins(expressionManager, workflowHelpers).extend({
|
||||
},
|
||||
watch: {
|
||||
value(newValue) {
|
||||
const payload: Record<string, unknown> = {
|
||||
const isInternalChange = newValue === this.editor?.state.doc.toString();
|
||||
|
||||
if (isInternalChange) return;
|
||||
|
||||
// manual update on external change, e.g. from expression modal or mapping drop
|
||||
|
||||
this.editor?.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: this.editor?.state.doc.length,
|
||||
insert: newValue,
|
||||
},
|
||||
selection: { anchor: this.cursorPosition, head: this.cursorPosition },
|
||||
};
|
||||
|
||||
/**
|
||||
* If completion from selection, preserve selection.
|
||||
*/
|
||||
if (this.editor) {
|
||||
const [range] = this.editor.state.selection.ranges;
|
||||
|
||||
const isBraceAutoinsertion =
|
||||
this.editor.state.sliceDoc(range.from - 1, range.from) === '{' &&
|
||||
this.editor.state.sliceDoc(range.to, range.to + 1) === '}';
|
||||
|
||||
if (isBraceAutoinsertion) {
|
||||
payload.selection = { anchor: range.from, head: range.to };
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this.editor?.dispatch(payload);
|
||||
} catch (_) {
|
||||
// ignore out-of-range selection error on drop
|
||||
}
|
||||
});
|
||||
},
|
||||
ndvInputData() {
|
||||
this.editor?.dispatch({
|
||||
@@ -92,9 +77,10 @@ export default mixins(expressionManager, workflowHelpers).extend({
|
||||
mounted() {
|
||||
const extensions = [
|
||||
inputTheme({ isSingleLine: this.isSingleLine }),
|
||||
n8nLanguageSupport(),
|
||||
autocompletion(),
|
||||
n8nLang(),
|
||||
history(),
|
||||
doubleBraceHandler(),
|
||||
expressionInputHandler(),
|
||||
EditorView.lineWrapping,
|
||||
EditorView.editable.of(!this.isReadOnly),
|
||||
EditorView.domEventHandlers({
|
||||
|
||||
Reference in New Issue
Block a user