mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 21:07:28 -08:00
fix(editor): Fix autocomplete for complex expresions (#5695)
* ✨ Fixing autocomplete for expressions as function arguments * ✅ Added more autocomplete tests * ⚡ Improving autocomplete for complex expressions * ⚡ Handling complex operation expressions in autocomplete
This commit is contained in:
parent
541850f95f
commit
11bf260bf1
|
@ -121,6 +121,15 @@ describe('Resolution-based completions', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should properly handle string that contain dollar signs', () => {
|
||||||
|
// @ts-expect-error Spied function is mistyped
|
||||||
|
resolveParameterSpy.mockReturnValueOnce('"You \'owe\' me 200$"');
|
||||||
|
|
||||||
|
expect(completions('{{ "You \'owe\' me 200$".| }}')).toHaveLength(
|
||||||
|
natives('string').length + extensions('string').length,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('should return completions for number literal: {{ (123).| }}', () => {
|
test('should return completions for number literal: {{ (123).| }}', () => {
|
||||||
// @ts-expect-error Spied function is mistyped
|
// @ts-expect-error Spied function is mistyped
|
||||||
resolveParameterSpy.mockReturnValueOnce(123);
|
resolveParameterSpy.mockReturnValueOnce(123);
|
||||||
|
@ -161,6 +170,61 @@ describe('Resolution-based completions', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('complex expression completions', () => {
|
||||||
|
const resolveParameterSpy = vi.spyOn(workflowHelpers, 'resolveParameter');
|
||||||
|
const { $input } = mockProxy;
|
||||||
|
|
||||||
|
test('should return completions when $input is used as a function parameter', () => {
|
||||||
|
resolveParameterSpy.mockReturnValue($input.item.json.num);
|
||||||
|
const found = completions('{{ Math.abs($input.item.json.num1).| }}');
|
||||||
|
if (!found) throw new Error('Expected to find completions');
|
||||||
|
expect(found).toHaveLength(extensions('number').length + natives('number').length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return completions when node reference is used as a function parameter', () => {
|
||||||
|
const initialState = { workflows: { workflow: { nodes: mockNodes } } };
|
||||||
|
|
||||||
|
setActivePinia(createTestingPinia({ initialState }));
|
||||||
|
|
||||||
|
expect(completions('{{ new Date($(|) }}')).toHaveLength(mockNodes.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return completions for complex expression: {{ $now.diff($now.diff($now.|)) }}', () => {
|
||||||
|
expect(completions('{{ $now.diff($now.diff($now.|)) }}')).toHaveLength(
|
||||||
|
natives('date').length + extensions('object').length,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return completions for complex expression: {{ $execution.resumeUrl.includes($json.) }}', () => {
|
||||||
|
resolveParameterSpy.mockReturnValue($input.item.json);
|
||||||
|
const { $json } = mockProxy;
|
||||||
|
const found = completions('{{ $execution.resumeUrl.includes($json.|) }}');
|
||||||
|
|
||||||
|
if (!found) throw new Error('Expected to find completions');
|
||||||
|
expect(found).toHaveLength(Object.keys($json).length + natives('object').length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return completions for operation expression: {{ $now.day + $json. }}', () => {
|
||||||
|
resolveParameterSpy.mockReturnValue($input.item.json);
|
||||||
|
const { $json } = mockProxy;
|
||||||
|
const found = completions('{{ $now.day + $json.| }}');
|
||||||
|
|
||||||
|
if (!found) throw new Error('Expected to find completions');
|
||||||
|
|
||||||
|
expect(found).toHaveLength(Object.keys($json).length + natives('object').length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return completions for operation expression: {{ Math.abs($now.day) >= 10 ? $now : Math.abs($json.). }}', () => {
|
||||||
|
resolveParameterSpy.mockReturnValue($input.item.json);
|
||||||
|
const { $json } = mockProxy;
|
||||||
|
const found = completions('{{ Math.abs($now.day) >= 10 ? $now : Math.abs($json.|) }}');
|
||||||
|
|
||||||
|
if (!found) throw new Error('Expected to find completions');
|
||||||
|
|
||||||
|
expect(found).toHaveLength(Object.keys($json).length + natives('object').length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('bracket-aware completions', () => {
|
describe('bracket-aware completions', () => {
|
||||||
const resolveParameterSpy = vi.spyOn(workflowHelpers, 'resolveParameter');
|
const resolveParameterSpy = vi.spyOn(workflowHelpers, 'resolveParameter');
|
||||||
const { $input } = mockProxy;
|
const { $input } = mockProxy;
|
||||||
|
|
|
@ -5,11 +5,16 @@ import { resolveParameter } from '@/mixins/workflowHelpers';
|
||||||
import { useNDVStore } from '@/stores/ndv';
|
import { useNDVStore } from '@/stores/ndv';
|
||||||
import type { Completion, CompletionContext } from '@codemirror/autocomplete';
|
import type { Completion, CompletionContext } from '@codemirror/autocomplete';
|
||||||
|
|
||||||
|
// String literal expression is everything enclosed in single, double or tick quotes following a dot
|
||||||
|
const stringLiteralRegex = /^"[^"]+"|^'[^']+'|^`[^`]+`\./;
|
||||||
|
// JavaScript operands
|
||||||
|
const operandsRegex = /[+\-*/><<==>**!=?]/;
|
||||||
/**
|
/**
|
||||||
* Split user input into base (to resolve) and tail (to filter).
|
* Split user input into base (to resolve) and tail (to filter).
|
||||||
*/
|
*/
|
||||||
export function splitBaseTail(userInput: string): [string, string] {
|
export function splitBaseTail(userInput: string): [string, string] {
|
||||||
const parts = userInput.split('.');
|
const processedInput = extractSubExpression(userInput);
|
||||||
|
const parts = processedInput.split('.');
|
||||||
const tail = parts.pop() ?? '';
|
const tail = parts.pop() ?? '';
|
||||||
|
|
||||||
return [parts.join('.'), tail];
|
return [parts.join('.'), tail];
|
||||||
|
@ -31,6 +36,30 @@ export function longestCommonPrefix(...strings: string[]) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process user input if expressions are used as part of complex expression
|
||||||
|
// i.e. as a function parameter or an operation expression
|
||||||
|
// this function will extract expression that is currently typed so autocomplete
|
||||||
|
// suggestions can be matched based on it.
|
||||||
|
function extractSubExpression(userInput: string): string {
|
||||||
|
const dollarSignIndex = userInput.indexOf('$');
|
||||||
|
// If it's not a dollar sign expression just strip parentheses
|
||||||
|
if (dollarSignIndex === -1) {
|
||||||
|
userInput = userInput.replace(/^.+(\(|\[|{)/, '');
|
||||||
|
} else if (!stringLiteralRegex.test(userInput)) {
|
||||||
|
// If there is a dollar sign in the input and input is not a string literal,
|
||||||
|
// extract part of following the last $
|
||||||
|
const expressionParts = userInput.split('$');
|
||||||
|
userInput = `$${expressionParts[expressionParts.length - 1]}`;
|
||||||
|
// If input is part of a complex operation expression and extract last operand
|
||||||
|
const operationPart = userInput.split(operandsRegex).pop()?.trim() || '';
|
||||||
|
const lastOperand = operationPart.split(' ').pop();
|
||||||
|
if (lastOperand) {
|
||||||
|
userInput = lastOperand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return userInput;
|
||||||
|
}
|
||||||
|
|
||||||
export const prefixMatch = (first: string, second: string) =>
|
export const prefixMatch = (first: string, second: string) =>
|
||||||
first.startsWith(second) && first !== second;
|
first.startsWith(second) && first !== second;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue