feat(Code Node): create Code node (#3965)

* Introduce node deprecation (#3930)

 Introduce node deprecation

* 🚧 Scaffold out Code node

* 👕 Fix lint

* 📘 Create types file

* 🚚 Rename theme

* 🔥 Remove unneeded prop

*  Override keybindings

*  Expand lintings

*  Create editor content getter

* 🚚 Ensure all helpers use `$`

*  Add autocompletion

*  Filter out welcome note node

*  Convey error line number

*  Highlight error line

*  Restore logging from node

*  More autocompletions

*  Streamline completions

* ✏️ Update placeholders

*  Update linter to new methods

* 🔥 Remove `$nodeItem` completions

*  Re-update placeholders

* 🎨 Fix formatting

* 📦 Update `package-lock.json`

*  Refresh with multi-line empty string

*  Account for syntax errors

* 🔥 Remove unneeded variant

*  Minor improvements

*  Add more autocompletions

* 🚚 Rename extension

* 🔥 Remove outdated comments

* 🚚 Rename field

*  More autocompletions

*  Fix up error display when empty text

* 🔥 Remove logging

*  More error validation

* 🐛 Fix `pairedItem` to `pairedItem()`

*  Add item to validation info

* 📦 Update `package-lock.json`

*  Leftover fixes

*  Set `insertNewlineAndIndent`

* 📦 Update `package-lock.json`

* 📦 Re-update `package-lock.json`

* 👕 Add lint exception

* 📘 Add type to mixin type

* Clean up comment

*  Refactor completion per new requirements

*  Adjust placeholders

*  Add `json` autocompletions for `$input`

* 🎨 Set border

*  Restore local completion source

*  Implement autocompletion for imports

*  Add `.*` to follow user typing on autocompletion

* 📘 Fix typings in autocompletions

* 👕 Add linting for use of `item()`

* 📦 Update `package-lock.json`

* 🐛 Fix for `$items(nodeName)[0]`

*  Filter down built-in modules list

*  Refactor error handling

*  Linter and validation improvements

*  Apply review feedback

* ♻️ More general refactorings

*  Add dot notation utility

* Customize input handler

*  Support `.json.` completions

*  Adjust placeholder

*  Sort imports

* 🔥 Remove blank rows addition

*  Add more error validation

* 📦 Update `package-lock.json`

*  Make date logging consistent

* 🔧 Adjust linting highlight range

*  Add line numbers to each item mode errors

*  Allow for links in error descriptions

*  More input validation

*  Expand linting to loops

*  Deprecate Function and Function Item nodes

* 🐛 Fix placeholder syntax

* 📘 Narrow down type

* 🚚 Rename using kebab-case

* 🔥 Remove `mapGetters`

* ✏️ Fix casing

*  Adjust import for type

* ✏️ Fix quotes

* 🐛 Fix `activeNode` reference

*  Use constant

* 🔥 Remove logging

* ✏️ Fix typo

*  Add missing `notice`

* ✏️ Add tags

* ✏️ Fix alias

* ✏️ Update copy

* 🔥 Remove wrong linting

* ✏️ Update copy

*  Add validation for `null`

*  Add validation for non-object and non-array

*  Add validation for non-array with json

* ✏️ Intentionally use wrong spelling

*  More validation

* ✏️ More copy updates

* ✏️ Placeholder updates

*  Restore spelling

*  Fix var name

* ✏️ More copy updates

*  Add luxon autocompletions

*  Make scrollable

*  Fix comma from merge conflict resolution

* 📦 Update `package-lock.json`

* 👕 Fix lint detail

* 🎨 Set font family

*  Bring in expressions fix

* ♻️ Address feedback

*  Exclude codemirror packages from render chunks

* 🐛 Fix placeholder not showing on first load

* feat(editor-ui): Replace `lezer` with `esprima` in client linter (#4192)

* 🔥 Remove addition from misresolved conflict

*  Replace `lezer` with `esprima` in client linter

*  Add missing key

* 📦 Update `package-lock.json`

*  Match dependencies

* 📦 Update `package-lock.json`

* 📦 Re-update `package-lock.json`

*  Match whitespace

* 🐛 Fix selection

*  Expand validation

* 🔥 Remove validation

* ✏️ Update copy

* 🚚 Move to constants

*  More `null` validation

*  Support `all()` with index to access item

*  Gloss over n8n syntax error

* 🎨 Re-style diagnostic button

* 🔥 Remove `item` as `itemAlias`

*  Add linting for `item.json` in single item mode

*  Refactor to add label info descriptions

*  More autocompletions

* 👕 Fix lint

*  Simplify typings

* feat(nodes-base): Multiline autocompletion for `code-node-editor` (#4220)

*  Simplify typings

*  Consolidate helpers in utils

*  Multiline autocompletion for standalone vars

* 🔥 Remove unneeded mixins

* ✏️ Update copy

* ✏️ Prep TODOs

*  Multiline completion for `$input.method` + `$input.item`

* 🔥 Remove unused method

* 🔥 Remove another unused method

* 🚚 Move luxon strings to helpers

*  Multiline autocompletion for methods output

*  Refactor to use optional chaining

* 👕 Fix lint

* ✏️ Update TODOs

*  Multiline autocompletion for `json` fields

* 📘 Add typings

*  De-duplicate callback to forEach

* 🐛 Fix autocompletions not working with leading whitespace

* 🌐 Apply i18n

* 👕 Fix lint

* :constructor: Second-period var usage completions

* 👕 Fix lint

* 👕 Add exception

*  Add completion telemetry

* 📘 Add typing

*  Major refactoring to organize

* 🐛 Fix multiline `.all()[index]`

* 🐛 Do not autoclose square brackets prior to `.json`

* 🐛 Fix accessor for multiline `jsonField` completions

*  Add completions for half-assignments

* 🐛 Fix `jsonField` completions for `x.json`

* ✏️ Improve comments

* 🐛 Fix `.json[field]` for multiline matches

*  Cleanup

* 📦 Update `package-lock.json`

* 👕 Fix lint

* 🐛 Rely on original value for custom matcher

*  Create `customMatcherJsonFieldCompletions` to simplify setup

* 🐛 Include selector in `customMatcherJsonField` completions

* ✏️ Make naming consistent

* ✏️ Add docline

*  Finish self-review cleanup

* 🔥 Remove outdated comment

* 📌 Pin luxon to major-minor

* ✏️ Fix typo

* 📦 Update `package-lock.json`

* 📦 Update `package-lock.json`

* 📦 Re-update `package-lock.json`

*  Add `luxon` for Gmail node

* 📦 Update `package-lock.json`

*  Replace Function with Code in suggested nodes

* 🐛 Fix `$prevNode` completions

* ✏️ Update `$execution.mode` copy

*  Separate luxon getters from methods

*  Adjusting linter to tolerate `.binary`

*  Adjust top-level item keys check

*  Anticipate user expecting `item` to pre-exist

*  Add linting for legacy item access

*  Add hint for attempted `items` access

*  Add keybinding for toggling comments

* ✏️ Update copy of `all`, `first`, `last` and `itemMatching`

* 🐛 Make `input.all()` etc act on copies

* 📦 Update `package-lock.json`

* 🐛 Fix guard in `$input.last()`

* ♻️ Address Jan's feedback

* ⬆️ Upgrade `eslint-plugin-n8n-nodes-base`

* 📦 Update `package-lock.json`

* 🔥 Remove unneeded exceptions

*  Restore placeholder logic

*  Add placeholders to client

*  Account for shadow item

* ✏️ More completion info labels

* 👕 Fix lint

* ✏️ Update copy

* ✏️ Update copy

* ✏️ More copy updates

* 📦 Update `package-lock.json`

*  Add more validation

*  Add placheolder on first load

* Replace `Cmd` with `Mod`

* 📦 Update `package-lock.json`
This commit is contained in:
Iván Ovejero
2022-10-13 14:28:02 +02:00
committed by GitHub
parent 12e821528b
commit 1db4fa2bf8
54 changed files with 5127 additions and 1400 deletions

View File

@@ -0,0 +1,274 @@
import Vue from 'vue';
import mixins from 'vue-typed-mixins';
import { autocompletion } from '@codemirror/autocomplete';
import { localCompletionSource } from '@codemirror/lang-javascript';
import { baseCompletions } from './completions/base.completions';
import { jsSnippets } from './completions/js.snippets';
import { requireCompletions } from './completions/require.completions';
import { executionCompletions } from './completions/execution.completions';
import { workflowCompletions } from './completions/workflow.completions';
import { prevNodeCompletions } from './completions/prevNode.completions';
import { luxonCompletions } from './completions/luxon.completions';
import { itemIndexCompletions } from './completions/itemIndex.completions';
import { itemFieldCompletions } from './completions/itemField.completions';
import { jsonFieldCompletions } from './completions/jsonField.completions';
import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
import type { Extension } from '@codemirror/state';
import type { CodeNodeEditorMixin } from './types';
export const completerExtension = mixins(
Vue as CodeNodeEditorMixin,
baseCompletions,
requireCompletions,
executionCompletions,
workflowCompletions,
prevNodeCompletions,
luxonCompletions,
itemIndexCompletions,
itemFieldCompletions,
jsonFieldCompletions,
).extend({
methods: {
autocompletionExtension(): Extension {
return autocompletion({
compareCompletions: (a: Completion, b: Completion) => {
if (/\.json$|id$|id['"]\]$/.test(a.label)) return 0;
return a.label.localeCompare(b.label);
},
override: [
jsSnippets,
localCompletionSource,
// core
this.baseCompletions,
this.requireCompletions,
this.nodeSelectorCompletions,
this.prevNodeCompletions,
this.workflowCompletions,
this.executionCompletions,
// luxon
this.todayCompletions,
this.nowCompletions,
this.dateTimeCompltions,
// item index
this.inputCompletions,
this.selectorCompletions,
// item field
this.inputMethodCompletions,
this.selectorMethodCompletions,
// item json field
this.inputJsonFieldCompletions,
this.selectorJsonFieldCompletions,
// multiline
this.multilineCompletions,
],
});
},
/**
* Complete uses of variables to any of the supported completions.
*/
multilineCompletions(context: CompletionContext): CompletionResult | null {
if (!this.editor) return null;
let variablesToValues: Record<string, string> = {};
try {
variablesToValues = this.variablesToValues();
} catch (_) {
return null;
}
if (Object.keys(variablesToValues).length === 0) return null;
/**
* Complete uses of extended variables, i.e. variables having
* one or more dotted segments already.
*
* const x = $input;
* x.first(). -> .json
* x.first().json. -> .field
*/
const docLines = this.editor.state.doc.toString().split('\n');
const varNames = Object.keys(variablesToValues);
const uses = this.extendedUses(docLines, varNames);
for (const use of uses.itemField) {
const matcher = use.replace(/\.$/, '');
const completions = this.matcherItemFieldCompletions(context, matcher, variablesToValues);
if (completions) return completions;
}
for (const use of uses.jsonField) {
const matcher = use.replace(/(\.|\[)$/, '');
const completions = this.matcherJsonFieldCompletions(context, matcher, variablesToValues);
if (completions) return completions;
}
/**
* Complete uses of unextended variables, i.e. variables having
* no dotted segment already.
*
* const x = $input;
* x. -> .first()
*
* const x = $input.first();
* x. -> .json
*
* const x = $input.first().json;
* x. -> .field
*/
const SELECTOR_REGEX = /^\$\((?<quotedNodeName>['"][\w\s]+['"])\)$/; // $('nodeName')
const INPUT_METHOD_REGEXES = Object.values({
first: /\$input\.first\(\)$/,
last: /\$input\.last\(\)$/,
item: /\$input\.item$/,
all: /\$input\.all\(\)\[(?<index>\w+)\]$/,
});
const SELECTOR_METHOD_REGEXES = Object.values({
first: /\$\((?<quotedNodeName>['"][\w\s]+['"])\)\.first\(\)$/,
last: /\$\((?<quotedNodeName>['"][\w\s]+['"])\)\.last\(\)$/,
item: /\$\((?<quotedNodeName>['"][\w\s]+['"])\)\.item$/,
all: /\$\((?<quotedNodeName>['"][\w\s]+['"])\)\.all\(\)\[(?<index>\w+)\]$/,
});
const INPUT_JSON_REGEXES = Object.values({
first: /\$input\.first\(\)\.json$/,
last: /\$input\.last\(\)\.json$/,
item: /\$input\.item\.json$/,
all: /\$input\.all\(\)\[(?<index>\w+)\]\.json$/,
});
const SELECTOR_JSON_REGEXES = Object.values({
first: /\$\((?<quotedNodeName>['"][\w\s]+['"])\)\.first\(\)\.json$/,
last: /\$\((?<quotedNodeName>['"][\w\s]+['"])\)\.last\(\)\.json$/,
item: /\$\((?<quotedNodeName>['"][\w\s]+['"])\)\.item\.json$/,
all: /\$\((?<quotedNodeName>['"][\w\s]+['"])\)\.all\(\)\[(?<index>\w+)\]\.json$/,
});
for (const [variable, value] of Object.entries(variablesToValues)) {
// core
if (value === '$execution') return this.executionCompletions(context, variable);
if (value === '$workflow') return this.workflowCompletions(context, variable);
if (value === '$prevNode') return this.prevNodeCompletions(context, variable);
// luxon
if (value === '$now') return this.nowCompletions(context, variable);
if (value === '$today') return this.todayCompletions(context, variable);
if (value === 'DateTime') return this.dateTimeCompltions(context, variable);
// item index
if (value === '$input') return this.inputCompletions(context, variable);
if (SELECTOR_REGEX.test(value)) return this.selectorCompletions(context, variable);
// json field
const inputJsonMatched = INPUT_JSON_REGEXES.some((regex) => regex.test(value));
const selectorJsonMatched = SELECTOR_JSON_REGEXES.some((regex) => regex.test(value));
if (inputJsonMatched || selectorJsonMatched) {
return this.matcherJsonFieldCompletions(context, variable, variablesToValues);
}
// item field
const inputMethodMatched = INPUT_METHOD_REGEXES.some((regex) => regex.test(value));
const selectorMethodMatched = SELECTOR_METHOD_REGEXES.some((regex) => regex.test(value));
if (inputMethodMatched || selectorMethodMatched) {
return this.matcherItemFieldCompletions(context, variable, variablesToValues);
}
}
return null;
},
// ----------------------------------
// helpers
// ----------------------------------
/**
* Create a map of variables and the values they point to.
*/
variablesToValues() {
return this.variableDeclarationLines().reduce<Record<string, string>>((acc, line) => {
const [left, right] = line.split('=');
const varName = left.replace(/(var|let|const)/, '').trim();
const varValue = right.replace(/;/, '').trim();
acc[varName] = varValue;
return acc;
}, {});
},
variableDeclarationLines() {
if (!this.editor) return [];
const docLines = this.editor.state.doc.toString().split('\n');
const isVariableDeclarationLine = (line: string) =>
['var', 'const', 'let'].some((varType) => line.startsWith(varType));
return docLines.filter(isVariableDeclarationLine);
},
/**
* Collect uses of variables pointing to n8n syntax if they have been extended.
*
* x.first().
* x.first().json.
* x.json.
*/
extendedUses(docLines: string[], varNames: string[]) {
return docLines.reduce<{ itemField: string[]; jsonField: string[] }>(
(acc, cur) => {
varNames.forEach((varName) => {
const accessorPattern = `(${varName}.first\\(\\)|${varName}.last\\(\\)|${varName}.item|${varName}.all\\(\\)\\[\\w+\\]).*`;
const methodMatch = cur.match(new RegExp(accessorPattern));
if (methodMatch) {
if (/json(\.|\[)$/.test(methodMatch[0])) {
acc.jsonField.push(methodMatch[0]);
} else {
acc.itemField.push(methodMatch[0]);
}
}
const jsonPattern = `^${varName}\\.json(\\.|\\[)$`;
const jsonMatch = cur.match(new RegExp(jsonPattern));
if (jsonMatch) {
acc.jsonField.push(jsonMatch[0]);
}
});
return acc;
},
{ itemField: [], jsonField: [] },
);
},
},
});