mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
feat(editor): Add JS method aliases (#21799)
This commit is contained in:
+379
@@ -0,0 +1,379 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { sortCompletionsByInput } from './datatype.completions';
|
||||
import type { AliasCompletion } from 'n8n-workflow';
|
||||
|
||||
describe('sortCompletionsByInput', () => {
|
||||
describe('prefix match on label (order 3)', () => {
|
||||
test('should assign order 3 when input matches label prefix', () => {
|
||||
const options: AliasCompletion[] = [
|
||||
{ label: 'push()', type: 'method' },
|
||||
{ label: 'pop()', type: 'method' },
|
||||
];
|
||||
|
||||
const result = sortCompletionsByInput(options, 'pu');
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
option: options[0],
|
||||
order: 3,
|
||||
alias: undefined,
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
option: options[1],
|
||||
order: 0,
|
||||
alias: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('should assign order 3 for exact label match', () => {
|
||||
const options: AliasCompletion[] = [{ label: 'push()', type: 'method' }];
|
||||
|
||||
const result = sortCompletionsByInput(options, 'push()');
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
option: options[0],
|
||||
order: 3,
|
||||
alias: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('should be case-insensitive for prefix matching', () => {
|
||||
const options: AliasCompletion[] = [{ label: 'toLowerCase()', type: 'method' }];
|
||||
|
||||
const result = sortCompletionsByInput(options, 'tolower');
|
||||
|
||||
expect(result[0].order).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exact alias match (order 2)', () => {
|
||||
test('should assign order 2 when input exactly matches an alias with mode "exact"', () => {
|
||||
const options: AliasCompletion[] = [
|
||||
{
|
||||
label: 'push()',
|
||||
type: 'method',
|
||||
alias: [{ label: 'append', mode: 'exact' }],
|
||||
},
|
||||
];
|
||||
|
||||
const result = sortCompletionsByInput(options, 'append');
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
option: options[0],
|
||||
order: 2,
|
||||
alias: { label: 'append', mode: 'exact' },
|
||||
});
|
||||
});
|
||||
|
||||
test('should prefer exact alias match over prefix label match', () => {
|
||||
const options: AliasCompletion[] = [
|
||||
{
|
||||
label: 'append()',
|
||||
type: 'method',
|
||||
alias: [{ label: 'push', mode: 'exact' }],
|
||||
},
|
||||
{
|
||||
label: 'push()',
|
||||
type: 'method',
|
||||
},
|
||||
];
|
||||
|
||||
const result = sortCompletionsByInput(options, 'push');
|
||||
|
||||
// First should be exact alias match (order 2) even though second has prefix match (order 3)
|
||||
// Wait, order 3 is higher than order 2, so prefix match wins
|
||||
expect(result[0].option.label).toBe('push()');
|
||||
expect(result[0].order).toBe(3);
|
||||
expect(result[1].option.label).toBe('append()');
|
||||
expect(result[1].order).toBe(2);
|
||||
});
|
||||
|
||||
test('should not match if alias mode is not "exact"', () => {
|
||||
const options: AliasCompletion[] = [
|
||||
{
|
||||
label: 'push()',
|
||||
type: 'method',
|
||||
alias: [{ label: 'append', mode: 'prefix' }],
|
||||
},
|
||||
];
|
||||
|
||||
const result = sortCompletionsByInput(options, 'append');
|
||||
|
||||
// Should fall through to prefix alias match, not exact
|
||||
expect(result[0].order).toBe(1);
|
||||
});
|
||||
|
||||
test('should handle multiple aliases and match the exact one', () => {
|
||||
const options: AliasCompletion[] = [
|
||||
{
|
||||
label: 'toUpperCase()',
|
||||
type: 'method',
|
||||
alias: [
|
||||
{ label: 'upper', mode: 'prefix' },
|
||||
{ label: 'UPPER', mode: 'exact' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = sortCompletionsByInput(options, 'UPPER');
|
||||
|
||||
expect(result[0].order).toBe(2);
|
||||
expect(result[0].alias).toEqual({ label: 'UPPER', mode: 'exact' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('prefix alias match (order 1)', () => {
|
||||
test('should assign order 1 when input matches alias prefix and mode is not "exact"', () => {
|
||||
const options: AliasCompletion[] = [
|
||||
{
|
||||
label: 'push()',
|
||||
type: 'method',
|
||||
alias: [{ label: 'append', mode: 'prefix' }],
|
||||
},
|
||||
];
|
||||
|
||||
const result = sortCompletionsByInput(options, 'app');
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
option: options[0],
|
||||
order: 1,
|
||||
alias: { label: 'append', mode: 'prefix' },
|
||||
});
|
||||
});
|
||||
|
||||
test('should match when alias has no mode specified (defaults to prefix)', () => {
|
||||
const options: AliasCompletion[] = [
|
||||
{
|
||||
label: 'push()',
|
||||
type: 'method',
|
||||
alias: [{ label: 'append' }],
|
||||
},
|
||||
];
|
||||
|
||||
const result = sortCompletionsByInput(options, 'app');
|
||||
|
||||
expect(result[0].order).toBe(1);
|
||||
expect(result[0].alias).toEqual({ label: 'append' });
|
||||
});
|
||||
|
||||
test('should match first prefix alias when multiple exist', () => {
|
||||
const options: AliasCompletion[] = [
|
||||
{
|
||||
label: 'push()',
|
||||
type: 'method',
|
||||
alias: [{ label: 'append' }, { label: 'add' }],
|
||||
},
|
||||
];
|
||||
|
||||
const result = sortCompletionsByInput(options, 'app');
|
||||
|
||||
expect(result[0].order).toBe(1);
|
||||
expect(result[0].alias).toEqual({ label: 'append' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('no match (order 0)', () => {
|
||||
test('should assign order 0 when no matches found', () => {
|
||||
const options: AliasCompletion[] = [
|
||||
{ label: 'push()', type: 'method' },
|
||||
{ label: 'pop()', type: 'method' },
|
||||
];
|
||||
|
||||
const result = sortCompletionsByInput(options, 'xyz');
|
||||
|
||||
expect(result[0].order).toBe(0);
|
||||
expect(result[1].order).toBe(0);
|
||||
});
|
||||
|
||||
test('should handle empty alias array', () => {
|
||||
const options: AliasCompletion[] = [
|
||||
{
|
||||
label: 'push()',
|
||||
type: 'method',
|
||||
alias: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = sortCompletionsByInput(options, 'app');
|
||||
|
||||
expect(result[0].order).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sorting behavior', () => {
|
||||
test('should sort by order descending (highest first)', () => {
|
||||
const options: AliasCompletion[] = [
|
||||
{
|
||||
label: 'slice()',
|
||||
type: 'method',
|
||||
},
|
||||
{
|
||||
label: 'append()',
|
||||
type: 'method',
|
||||
alias: [{ label: 'push', mode: 'exact' }],
|
||||
},
|
||||
{
|
||||
label: 'push()',
|
||||
type: 'method',
|
||||
alias: [{ label: 'add' }],
|
||||
},
|
||||
{
|
||||
label: 'map()',
|
||||
type: 'method',
|
||||
},
|
||||
];
|
||||
|
||||
const result = sortCompletionsByInput(options, 'push');
|
||||
|
||||
// push() has prefix match on label (order 3) - should be first
|
||||
// append() has exact alias match (order 2) - should be second
|
||||
// slice() and map() have no match (order 0) - should be last
|
||||
expect(result[0].option.label).toBe('push()'); // order 3
|
||||
expect(result[0].order).toBe(3);
|
||||
expect(result[1].option.label).toBe('append()'); // order 2
|
||||
expect(result[1].order).toBe(2);
|
||||
expect(result[2].option.label).toBe('slice()'); // order 0
|
||||
expect(result[2].order).toBe(0);
|
||||
expect(result[3].option.label).toBe('map()'); // order 0
|
||||
expect(result[3].order).toBe(0);
|
||||
});
|
||||
|
||||
test('should maintain order for items with same priority', () => {
|
||||
const options: AliasCompletion[] = [
|
||||
{ label: 'pop()', type: 'method' },
|
||||
{ label: 'pow()', type: 'method' },
|
||||
];
|
||||
|
||||
const result = sortCompletionsByInput(options, 'po');
|
||||
|
||||
expect(result[0].order).toBe(3);
|
||||
expect(result[1].order).toBe(3);
|
||||
// Original order should be preserved for same priority
|
||||
expect(result[0].option.label).toBe('pop()');
|
||||
expect(result[1].option.label).toBe('pow()');
|
||||
});
|
||||
});
|
||||
|
||||
describe('complex scenarios', () => {
|
||||
test('should handle all order types together', () => {
|
||||
const options: AliasCompletion[] = [
|
||||
{
|
||||
label: 'splice()',
|
||||
type: 'method',
|
||||
alias: [{ label: 'remove', mode: 'exact' }],
|
||||
},
|
||||
{
|
||||
label: 'split()',
|
||||
type: 'method',
|
||||
alias: [{ label: 'explode' }],
|
||||
},
|
||||
{
|
||||
label: 'slice()',
|
||||
type: 'method',
|
||||
},
|
||||
{
|
||||
label: 'concat()',
|
||||
type: 'method',
|
||||
},
|
||||
];
|
||||
|
||||
const result = sortCompletionsByInput(options, 'spl');
|
||||
|
||||
// split() and splice() have prefix match on label (order 3)
|
||||
// Others have no match (order 0)
|
||||
expect(result[0].order).toBe(3);
|
||||
expect(result[1].order).toBe(3);
|
||||
expect(result[2].order).toBe(0);
|
||||
expect(result[3].order).toBe(0);
|
||||
});
|
||||
|
||||
test('should prioritize label over alias', () => {
|
||||
const options: AliasCompletion[] = [
|
||||
{
|
||||
label: 'append()',
|
||||
type: 'method',
|
||||
alias: [{ label: 'push' }],
|
||||
},
|
||||
{
|
||||
label: 'push()',
|
||||
type: 'method',
|
||||
alias: [{ label: 'append' }],
|
||||
},
|
||||
];
|
||||
|
||||
const result = sortCompletionsByInput(options, 'push');
|
||||
|
||||
// push() has prefix match on label (order 3) - should be first
|
||||
// append() has prefix alias match (order 1) - should be second
|
||||
expect(result[0].option.label).toBe('push()');
|
||||
expect(result[0].order).toBe(3);
|
||||
expect(result[1].option.label).toBe('append()');
|
||||
expect(result[1].order).toBe(1);
|
||||
});
|
||||
|
||||
test('should handle empty input string', () => {
|
||||
const options: AliasCompletion[] = [
|
||||
{ label: 'push()', type: 'method' },
|
||||
{ label: 'pop()', type: 'method' },
|
||||
];
|
||||
|
||||
const result = sortCompletionsByInput(options, '');
|
||||
|
||||
// Empty string should match all as prefix
|
||||
expect(result[0].order).toBe(3);
|
||||
expect(result[1].order).toBe(3);
|
||||
});
|
||||
|
||||
test('should handle empty options array', () => {
|
||||
const options: AliasCompletion[] = [];
|
||||
|
||||
const result = sortCompletionsByInput(options, 'test');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test('should handle options without type property', () => {
|
||||
const options: AliasCompletion[] = [
|
||||
{
|
||||
label: 'push()',
|
||||
alias: [{ label: 'append' }],
|
||||
},
|
||||
];
|
||||
|
||||
const result = sortCompletionsByInput(options, 'app');
|
||||
|
||||
expect(result[0].order).toBe(1);
|
||||
});
|
||||
|
||||
test('should work with real-world array method aliases', () => {
|
||||
const options: AliasCompletion[] = [
|
||||
{
|
||||
label: 'push()',
|
||||
type: 'method',
|
||||
alias: [{ label: 'append' }, { label: 'add' }],
|
||||
},
|
||||
{
|
||||
label: 'pop()',
|
||||
type: 'method',
|
||||
alias: [{ label: 'remove' }],
|
||||
},
|
||||
{
|
||||
label: 'concat()',
|
||||
type: 'method',
|
||||
alias: [{ label: 'extend' }],
|
||||
},
|
||||
{
|
||||
label: 'includes()',
|
||||
type: 'method',
|
||||
alias: [{ label: 'contains' }],
|
||||
},
|
||||
];
|
||||
|
||||
const result = sortCompletionsByInput(options, 'app');
|
||||
|
||||
// push() should match via its 'append' alias (order 1)
|
||||
expect(result[0].option.label).toBe('push()');
|
||||
expect(result[0].order).toBe(1);
|
||||
expect(result[0].alias?.label).toBe('append');
|
||||
});
|
||||
});
|
||||
});
|
||||
+60
-10
@@ -2,7 +2,6 @@ import { VALID_EMAIL_REGEX } from '@/app/constants';
|
||||
import { i18n } from '@n8n/i18n';
|
||||
import { useEnvironmentsStore } from '@/features/settings/environments.ee/environments.store';
|
||||
import { useExternalSecretsStore } from '@/features/integrations/externalSecrets.ee/externalSecrets.ee.store';
|
||||
|
||||
import type {
|
||||
Completion,
|
||||
CompletionContext,
|
||||
@@ -11,8 +10,14 @@ import type {
|
||||
} from '@codemirror/autocomplete';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import { DateTime } from 'luxon';
|
||||
import type { DocMetadata, IDataObject, NativeDoc } from 'n8n-workflow';
|
||||
import { Expression, ExpressionExtensions, NativeMethods, validateFieldType } from 'n8n-workflow';
|
||||
import type { Alias, DocMetadata, IDataObject, NativeDoc } from 'n8n-workflow';
|
||||
import {
|
||||
Expression,
|
||||
ExpressionExtensions,
|
||||
NativeMethods,
|
||||
validateFieldType,
|
||||
type AliasCompletion,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
ARRAY_NUMBER_ONLY_METHODS,
|
||||
ARRAY_RECOMMENDED_OPTIONS,
|
||||
@@ -74,7 +79,7 @@ export function datatypeCompletions(context: CompletionContext): CompletionResul
|
||||
const syntaxTree = javascriptLanguage.parser.parse(word.text);
|
||||
const [base, tail] = splitBaseTail(syntaxTree, word.text);
|
||||
|
||||
let options: Completion[] = [];
|
||||
let options: AliasCompletion[] = [];
|
||||
|
||||
const isCredential = isCredentialsModalOpen();
|
||||
|
||||
@@ -116,7 +121,7 @@ export function datatypeCompletions(context: CompletionContext): CompletionResul
|
||||
}
|
||||
|
||||
if (tail !== '') {
|
||||
options = options.filter((o) => prefixMatch(o.label, tail));
|
||||
options = filterOptions(options, tail);
|
||||
}
|
||||
|
||||
let from = word.to - tail.length;
|
||||
@@ -142,10 +147,54 @@ export function datatypeCompletions(context: CompletionContext): CompletionResul
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a sorted array of completions by the input string.
|
||||
* Each completion is returned with its order.
|
||||
* 0 if no match
|
||||
* 1 if prefix alias match
|
||||
* 2 if exact alias match
|
||||
* 3 if prefix label match
|
||||
*/
|
||||
export function sortCompletionsByInput(options: AliasCompletion[], input: string) {
|
||||
return options
|
||||
.map((o) => {
|
||||
let order = 0;
|
||||
let alias: Alias | undefined = undefined;
|
||||
if (prefixMatch(o.label, input)) {
|
||||
order = 3;
|
||||
} else {
|
||||
const exactAliasMatch = o.alias?.find((a) => a.mode === 'exact' && a.label === input);
|
||||
const prefixAliasMatch = o.alias?.find(
|
||||
(a) => a.mode !== 'exact' && prefixMatch(a.label, input),
|
||||
);
|
||||
if (exactAliasMatch) {
|
||||
alias = exactAliasMatch;
|
||||
order = 2;
|
||||
} else if (prefixAliasMatch) {
|
||||
alias = prefixAliasMatch;
|
||||
order = 1;
|
||||
}
|
||||
}
|
||||
return {
|
||||
option: o,
|
||||
order,
|
||||
alias,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.order - a.order);
|
||||
}
|
||||
|
||||
function filterOptions(options: AliasCompletion[], tail: string): AliasCompletion[] {
|
||||
const matches = sortCompletionsByInput(options, tail)
|
||||
.filter((m) => m.order > 0)
|
||||
.map((m) => m.option);
|
||||
return matches;
|
||||
}
|
||||
|
||||
function explicitDataTypeOptions(
|
||||
expression: string,
|
||||
targetNodeParameterContext?: TargetNodeParameterContext,
|
||||
): Completion[] {
|
||||
): AliasCompletion[] {
|
||||
return attempt(
|
||||
() => {
|
||||
const resolved = resolveAutocompleteExpression(
|
||||
@@ -163,7 +212,7 @@ function explicitDataTypeOptions(
|
||||
);
|
||||
}
|
||||
|
||||
function datatypeOptions(input: AutocompleteInput): Completion[] {
|
||||
function datatypeOptions(input: AutocompleteInput): AliasCompletion[] {
|
||||
const { resolved } = input;
|
||||
|
||||
if (resolved === null) return [];
|
||||
@@ -211,7 +260,7 @@ export const natives = ({
|
||||
}: {
|
||||
typeName: ExtensionTypeName;
|
||||
transformLabel?: (label: string) => string;
|
||||
}): Completion[] => {
|
||||
}): AliasCompletion[] => {
|
||||
const nativeDocs = NativeMethods.find((ee) => ee.typeName.toLowerCase() === typeName);
|
||||
|
||||
if (!nativeDocs) return [];
|
||||
@@ -324,9 +373,9 @@ const createCompletionOption = ({
|
||||
isFunction?: boolean;
|
||||
transformLabel?: (label: string) => string;
|
||||
type?: 'strikethrough';
|
||||
}): Completion => {
|
||||
}): AliasCompletion => {
|
||||
const label = isFunction ? name + '()' : name;
|
||||
const option: Completion = {
|
||||
const option: AliasCompletion = {
|
||||
label,
|
||||
section: doc?.section,
|
||||
apply: applyCompletion({
|
||||
@@ -334,6 +383,7 @@ const createCompletionOption = ({
|
||||
defaultArgs: getDefaultArgs(doc),
|
||||
transformLabel,
|
||||
}),
|
||||
alias: doc?.aliases?.map((alias) => ({ label: alias, mode: doc.aliasMode })),
|
||||
type,
|
||||
};
|
||||
option.info = createInfoBoxRenderer(doc, isFunction);
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ export type Resolved = unknown;
|
||||
|
||||
export type ExtensionTypeName = 'number' | 'string' | 'date' | 'array' | 'object' | 'boolean';
|
||||
|
||||
export type FnToDoc = { [fnName: string]: { doc?: DocMetadata } };
|
||||
export type FnToDoc = { [fnName: string]: { doc?: DocMetadata; aliases?: string[] } };
|
||||
|
||||
export type FunctionOptionType = 'native-function' | 'extension-function';
|
||||
export type KeywordOptionType = 'keyword';
|
||||
|
||||
+75
-29
@@ -6,14 +6,12 @@ import {
|
||||
type Completion,
|
||||
type CompletionSource,
|
||||
} from '@codemirror/autocomplete';
|
||||
import {
|
||||
autocompletableNodeNames,
|
||||
longestCommonPrefix,
|
||||
prefixMatch,
|
||||
} from '../../completions/utils';
|
||||
import { autocompletableNodeNames, longestCommonPrefix } from '../../completions/utils';
|
||||
import { typescriptWorkerFacet } from './facet';
|
||||
import { blockCommentSnippet, snippets } from './snippets';
|
||||
import { TARGET_NODE_PARAMETER_FACET } from '../../completions/constants';
|
||||
import type { AliasCompletion, Alias } from 'n8n-workflow';
|
||||
import { sortCompletionsByInput } from '../../completions/datatype.completions';
|
||||
|
||||
const START_CHARACTERS = ['"', "'", '(', '.', '@'];
|
||||
const START_CHARACTERS_REGEX = /[\.\(\'\"\@]/;
|
||||
@@ -48,7 +46,49 @@ export const typescriptCompletionSource: CompletionSource = async (context) => {
|
||||
|
||||
const { result, isGlobal } = completionResult;
|
||||
|
||||
let options = [...result.options];
|
||||
let options: AliasCompletion[] = [...result.options];
|
||||
|
||||
const addAlias = (label: string, alias: Alias[]) => {
|
||||
const completionIndex = options.findIndex((opt) => opt.label === label);
|
||||
if (completionIndex !== -1) {
|
||||
options[completionIndex].alias = alias;
|
||||
}
|
||||
};
|
||||
|
||||
// array aliases
|
||||
addAlias('push()', [
|
||||
{
|
||||
label: 'append()',
|
||||
info: 'Use push() to add one or more elements to the end of an array',
|
||||
},
|
||||
]);
|
||||
addAlias('pop()', [
|
||||
{ label: 'remove()', info: 'Use pop() to remove the last element from an array' },
|
||||
]);
|
||||
addAlias('splice()', [
|
||||
{
|
||||
label: 'insertAt()',
|
||||
info: 'Use splice(index, 0, value) to insert elements at a specified index in an array',
|
||||
},
|
||||
{
|
||||
label: 'removeAt()',
|
||||
info: 'Use splice(indexOf(itemToRemove), 1) to remove elements from an array',
|
||||
},
|
||||
]);
|
||||
addAlias('concat()', [{ label: 'extend()', info: 'Use concat() to merge two or more arrays' }]);
|
||||
addAlias('includes()', [
|
||||
{ label: 'contains()', info: 'Use includes() to check if an array contains a value' },
|
||||
{ label: 'has()', info: 'Use includes() to check if an array contains a value' },
|
||||
]);
|
||||
|
||||
// string aliases
|
||||
addAlias('toLowerCase()', [{ label: 'lower()' }]);
|
||||
addAlias('toUpperCase()', [{ label: 'upper()' }]);
|
||||
|
||||
addAlias('length', [
|
||||
{ label: 'size', mode: 'exact' },
|
||||
{ label: 'count', mode: 'exact' },
|
||||
]);
|
||||
|
||||
if (isGlobal) {
|
||||
options = options
|
||||
@@ -67,6 +107,22 @@ export const typescriptCompletionSource: CompletionSource = async (context) => {
|
||||
.concat(snippets);
|
||||
}
|
||||
|
||||
const filteredOptions: AliasCompletion[] = [];
|
||||
for (const { option, order, alias } of sortCompletionsByInput(options, word.text)) {
|
||||
const isEmpty = word.text === '';
|
||||
const isStartCharacter = START_CHARACTERS.includes(word.text);
|
||||
const isMatch = order > 0;
|
||||
|
||||
// replace the default description with the alias description
|
||||
if (alias?.info) {
|
||||
option.info = alias.info;
|
||||
}
|
||||
|
||||
if (isEmpty || isStartCharacter || isMatch) {
|
||||
filteredOptions.push(option);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
from: word ? (START_CHARACTERS.includes(word.text) ? word.to : word.from) : context.pos,
|
||||
filter: false,
|
||||
@@ -74,28 +130,18 @@ export const typescriptCompletionSource: CompletionSource = async (context) => {
|
||||
const lcp = longestCommonPrefix(completion.label, word.text);
|
||||
return [0, lcp.length];
|
||||
},
|
||||
options: options
|
||||
.filter(
|
||||
(option) =>
|
||||
word.text === '' ||
|
||||
START_CHARACTERS.includes(word.text) ||
|
||||
prefixMatch(
|
||||
option.label.replace(START_CHARACTERS_REGEX, ''),
|
||||
word.text.replace(START_CHARACTERS_REGEX, ''),
|
||||
),
|
||||
)
|
||||
.map((completion) => {
|
||||
if (completion.label.endsWith('()')) {
|
||||
completion.apply = (view, _, from, to) => {
|
||||
const cursorPosition = from + completion.label.length - 1;
|
||||
view.dispatch({
|
||||
...insertCompletionText(view.state, completion.label, from, to),
|
||||
annotations: pickedCompletion.of(completion),
|
||||
selection: { anchor: cursorPosition, head: cursorPosition },
|
||||
});
|
||||
};
|
||||
}
|
||||
return completion;
|
||||
}),
|
||||
options: filteredOptions.map((completion) => {
|
||||
if (completion.label.endsWith('()')) {
|
||||
completion.apply = (view, _, from, to) => {
|
||||
const cursorPosition = from + completion.label.length - 1;
|
||||
view.dispatch({
|
||||
...insertCompletionText(view.state, completion.label, from, to),
|
||||
annotations: pickedCompletion.of(completion),
|
||||
selection: { anchor: cursorPosition, head: cursorPosition },
|
||||
});
|
||||
};
|
||||
}
|
||||
return completion;
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -334,6 +334,7 @@ export function toDateTime() {
|
||||
|
||||
average.doc = {
|
||||
name: 'average',
|
||||
aliases: ['mean'],
|
||||
description:
|
||||
'Returns the average of the numbers in the array. Throws an error if there are any non-numbers.',
|
||||
examples: [{ example: '[12, 1, 5].average()', evaluated: '6' }],
|
||||
@@ -343,6 +344,7 @@ average.doc = {
|
||||
|
||||
compact.doc = {
|
||||
name: 'compact',
|
||||
aliases: ['removeEmpty'],
|
||||
description:
|
||||
'Removes any empty values from the array. <code>null</code>, <code>""</code> and <code>undefined</code> count as empty.',
|
||||
examples: [{ example: '[2, null, 1, ""].compact()', evaluated: '[2, 1]' }],
|
||||
@@ -374,6 +376,7 @@ isNotEmpty.doc = {
|
||||
|
||||
first.doc = {
|
||||
name: 'first',
|
||||
aliases: ['head'],
|
||||
description: 'Returns the first element of the array',
|
||||
examples: [{ example: "['quick', 'brown', 'fox'].first()", evaluated: "'quick'" }],
|
||||
returnType: 'any',
|
||||
@@ -382,6 +385,7 @@ first.doc = {
|
||||
|
||||
last.doc = {
|
||||
name: 'last',
|
||||
aliases: ['tail'],
|
||||
description: 'Returns the last element of the array',
|
||||
examples: [{ example: "['quick', 'brown', 'fox'].last()", evaluated: "'fox'" }],
|
||||
returnType: 'any',
|
||||
@@ -640,6 +644,7 @@ toJsonString.doc = {
|
||||
|
||||
append.doc = {
|
||||
name: 'append',
|
||||
aliases: ['push'],
|
||||
description:
|
||||
'Adds new elements to the end of the array. Similar to <code>push()</code>, but returns the modified array. Consider using spread syntax instead (see examples).',
|
||||
examples: [
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import type { Completion } from '@codemirror/autocomplete';
|
||||
|
||||
export type Alias = { label: string; info?: string; mode?: 'prefix' | 'exact' };
|
||||
export interface AliasCompletion extends Completion {
|
||||
alias?: Alias[];
|
||||
}
|
||||
|
||||
export interface ExtensionMap {
|
||||
typeName: string;
|
||||
functions: Record<string, Extension>;
|
||||
@@ -35,6 +42,7 @@ export type DocMetadata = {
|
||||
section?: string;
|
||||
hidden?: boolean;
|
||||
aliases?: string[];
|
||||
aliasMode?: 'prefix' | 'exact';
|
||||
args?: DocMetadataArgument[];
|
||||
examples?: DocMetadataExample[];
|
||||
docURL?: string;
|
||||
|
||||
@@ -14,3 +14,8 @@ export type {
|
||||
DocMetadataArgument,
|
||||
DocMetadataExample,
|
||||
} from './extensions';
|
||||
|
||||
export type {
|
||||
Alias,
|
||||
AliasCompletion,
|
||||
} from './extensions';
|
||||
|
||||
@@ -179,6 +179,7 @@ hasField.doc = {
|
||||
|
||||
removeField.doc = {
|
||||
name: 'removeField',
|
||||
aliases: ['delete'],
|
||||
description: "Removes a field from the Object. The same as JavaScript's <code>delete</code>.",
|
||||
examples: [
|
||||
{
|
||||
|
||||
@@ -821,6 +821,7 @@ quote.doc = {
|
||||
|
||||
parseJson.doc = {
|
||||
name: 'parseJson',
|
||||
aliases: ['fromJson'],
|
||||
description:
|
||||
"Returns the JavaScript value or object represented by the string, or <code>undefined</code> if the string isn't valid JSON. Single-quoted JSON is not supported.",
|
||||
section: 'cast',
|
||||
@@ -836,6 +837,7 @@ parseJson.doc = {
|
||||
|
||||
base64Encode.doc = {
|
||||
name: 'base64Encode',
|
||||
aliases: ['toBase64'],
|
||||
description: 'Converts plain text to a base64-encoded string',
|
||||
examples: [{ example: '"hello".base64Encode()', evaluated: '"aGVsbG8="' }],
|
||||
section: 'edit',
|
||||
@@ -846,6 +848,7 @@ base64Encode.doc = {
|
||||
|
||||
base64Decode.doc = {
|
||||
name: 'base64Decode',
|
||||
aliases: ['fromBase64'],
|
||||
description: 'Converts a base64-encoded string to plain text',
|
||||
examples: [{ example: '"aGVsbG8=".base64Decode()', evaluated: '"hello"' }],
|
||||
section: 'edit',
|
||||
|
||||
@@ -75,7 +75,7 @@ export {
|
||||
type ExtractableSubgraphData,
|
||||
type IConnectionAdjacencyList as AdjacencyList,
|
||||
} from './graph/graph-utils';
|
||||
export { ExpressionExtensions } from './extensions';
|
||||
export { ExpressionExtensions, type Alias, type AliasCompletion } from './extensions';
|
||||
export * as ExpressionParser from './extensions/expression-parser';
|
||||
export { NativeMethods } from './native-methods';
|
||||
export * from './node-parameters/filter-parameter';
|
||||
|
||||
@@ -6,6 +6,8 @@ export const arrayMethods: NativeDoc = {
|
||||
length: {
|
||||
doc: {
|
||||
name: 'length',
|
||||
aliases: ['size', 'count'],
|
||||
aliasMode: 'exact',
|
||||
description: 'The number of elements in the array',
|
||||
examples: [{ example: "['Bob', 'Bill', 'Nat'].length", evaluated: '3' }],
|
||||
docURL:
|
||||
@@ -18,6 +20,7 @@ export const arrayMethods: NativeDoc = {
|
||||
concat: {
|
||||
doc: {
|
||||
name: 'concat',
|
||||
aliases: ['extend'],
|
||||
description: 'Joins one or more arrays onto the end of the base array',
|
||||
examples: [
|
||||
{
|
||||
@@ -239,6 +242,7 @@ export const arrayMethods: NativeDoc = {
|
||||
includes: {
|
||||
doc: {
|
||||
name: 'includes',
|
||||
aliases: ['contains', 'has'],
|
||||
description: 'Returns <code>true</code> if the array contains the specified element',
|
||||
examples: [
|
||||
{ example: "['Bob', 'Bill', 'Nat'].includes('Nat')", evaluated: 'true' },
|
||||
@@ -418,6 +422,7 @@ export const arrayMethods: NativeDoc = {
|
||||
description:
|
||||
'Returns a portion of the array, from the <code>start</code> index up to (but not including) the <code>end</code> index. Indexes start at 0.',
|
||||
examples: [
|
||||
{ example: '[1, 2, 3, 4, 5].slice(0, -1)', evaluated: '[1, 2, 3, 4]' },
|
||||
{ example: '[1, 2, 3, 4, 5].slice(2, 4)', evaluated: '[3, 4]' },
|
||||
{ example: '[1, 2, 3, 4, 5].slice(2)', evaluated: '[3, 4, 5]' },
|
||||
{ example: '[1, 2, 3, 4, 5].slice(-2)', evaluated: '[4, 5]' },
|
||||
@@ -536,6 +541,7 @@ export const arrayMethods: NativeDoc = {
|
||||
toSpliced: {
|
||||
doc: {
|
||||
name: 'toSpliced',
|
||||
aliases: ['insertAt', 'removeAt'],
|
||||
description:
|
||||
'Adds and/or removes array elements at a given position. \n\nSee also <code>slice()</code> and <code>append()</code>.',
|
||||
examples: [
|
||||
|
||||
@@ -6,6 +6,8 @@ export const stringMethods: NativeDoc = {
|
||||
length: {
|
||||
doc: {
|
||||
name: 'length',
|
||||
aliases: ['size', 'count'],
|
||||
aliasMode: 'exact',
|
||||
description: 'The number of characters in the string',
|
||||
examples: [{ example: '"hello".length', evaluated: '5' }],
|
||||
section: 'query',
|
||||
@@ -190,6 +192,7 @@ export const stringMethods: NativeDoc = {
|
||||
includes: {
|
||||
doc: {
|
||||
name: 'includes',
|
||||
aliases: ['contains'],
|
||||
description:
|
||||
'Returns <code>true</code> if the string contains the <code>searchString</code>. Case-sensitive.',
|
||||
section: 'query',
|
||||
@@ -483,6 +486,7 @@ export const stringMethods: NativeDoc = {
|
||||
toLowerCase: {
|
||||
doc: {
|
||||
name: 'toLowerCase',
|
||||
aliases: ['lower'],
|
||||
description: 'Converts all letters in the string to lower case',
|
||||
section: 'case',
|
||||
docURL:
|
||||
@@ -494,6 +498,7 @@ export const stringMethods: NativeDoc = {
|
||||
toUpperCase: {
|
||||
doc: {
|
||||
name: 'toUpperCase',
|
||||
aliases: ['upper'],
|
||||
description: 'Converts all letters in the string to upper case (capitals)',
|
||||
examples: [{ example: '"I\'m not angry".toUpperCase()', evaluated: '"I\'M NOT ANGRY"' }],
|
||||
section: 'case',
|
||||
@@ -517,6 +522,7 @@ export const stringMethods: NativeDoc = {
|
||||
trimEnd: {
|
||||
doc: {
|
||||
name: 'trimEnd',
|
||||
aliases: ['trimRight'],
|
||||
description:
|
||||
'Removes whitespace from the end of a string and returns a new string. Whitespace includes new lines, tabs, spaces, etc.',
|
||||
examples: [{ example: "' lonely '.trimEnd()", evaluated: "' lonely'" }],
|
||||
@@ -529,6 +535,7 @@ export const stringMethods: NativeDoc = {
|
||||
trimStart: {
|
||||
doc: {
|
||||
name: 'trimStart',
|
||||
aliases: ['trimLeft'],
|
||||
description:
|
||||
'Removes whitespace from the beginning of a string and returns a new string. Whitespace includes new lines, tabs, spaces, etc.',
|
||||
examples: [{ example: "' lonely '.trimStart()", evaluated: "'lonely '" }],
|
||||
|
||||
Reference in New Issue
Block a user