Skip to content

Commit

Permalink
fix(editor): Fix autocomplete for complex expresions (#5695)
Browse files Browse the repository at this point in the history
* ✨ Fixing autocomplete for expressions as function arguments

* ✅ Added more autocomplete tests

* ⚡ Improving autocomplete for complex expressions

* ⚡ Handling complex operation expressions in autocomplete
  • Loading branch information
MiloradFilipovic authored Mar 16, 2023
1 parent 541850f commit 11bf260
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
31 changes: 30 additions & 1 deletion packages/editor-ui/src/plugins/codemirror/completions/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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;

Expand Down

0 comments on commit 11bf260

Please sign in to comment.