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:
Milorad FIlipović 2023-03-16 12:13:02 +01:00 committed by GitHub
parent 541850f95f
commit 11bf260bf1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 94 additions and 1 deletions

View file

@ -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;

View file

@ -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;