mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 04:04:06 -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).| }}', () => {
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
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', () => {
|
||||
const resolveParameterSpy = vi.spyOn(workflowHelpers, 'resolveParameter');
|
||||
const { $input } = mockProxy;
|
||||
|
|
|
@ -5,11 +5,16 @@ import { resolveParameter } from '@/mixins/workflowHelpers';
|
|||
import { useNDVStore } from '@/stores/ndv';
|
||||
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).
|
||||
*/
|
||||
export function splitBaseTail(userInput: string): [string, string] {
|
||||
const parts = userInput.split('.');
|
||||
const processedInput = extractSubExpression(userInput);
|
||||
const parts = processedInput.split('.');
|
||||
const tail = parts.pop() ?? '';
|
||||
|
||||
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) =>
|
||||
first.startsWith(second) && first !== second;
|
||||
|
||||
|
|
Loading…
Reference in a new issue