feat(editor): Add JS method aliases (#21799)

This commit is contained in:
yehorkardash
2025-12-15 15:36:03 +01:00
committed by GitHub
parent 31cc49ee2d
commit 1b256ccc52
12 changed files with 551 additions and 41 deletions
@@ -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');
});
});
});
@@ -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);
@@ -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';
@@ -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',
+1 -1
View File
@@ -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 '" }],