Skip to content

Commit

Permalink
feat(editor): Show expression infobox on hover and cursor position (#…
Browse files Browse the repository at this point in the history
…9507)

Co-authored-by: Giulio Andreini <andreini@netseven.it>
  • Loading branch information
elsmr and gandreini committed May 28, 2024
1 parent ac4e0fb commit ec0373f
Show file tree
Hide file tree
Showing 14 changed files with 657 additions and 136 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ function useJsonFieldCompletions() {
* - Complete `$input.item.json[` to `['field']`.
*/
const inputJsonFieldCompletions = (context: CompletionContext): CompletionResult | null => {
console.log('🚀 ~ inputJsonFieldCompletions ~ context:', context);
const patterns = {
first: /\$input\.first\(\)\.json(\[|\.).*/,
last: /\$input\.last\(\)\.json(\[|\.).*/,
Expand Down Expand Up @@ -158,7 +157,6 @@ function useJsonFieldCompletions() {

if (name === 'all') {
const regexMatch = preCursor.text.match(regex);
console.log('🚀 ~ selectorJsonFieldCompletions ~ regexMatch:', regexMatch);
if (!regexMatch?.groups?.index) continue;

const { index } = regexMatch.groups;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from '@/plugins/codemirror/keymap';
import type { Segment } from '@/types/expressions';
import { removeExpressionPrefix } from '@/utils/expressions';
import { infoBoxTooltips } from '@/plugins/codemirror/tooltips/InfoBoxTooltip';
type Props = {
modelValue: string;
Expand Down Expand Up @@ -68,6 +69,7 @@ const extensions = computed(() => [
expressionInputHandler(),
EditorView.lineWrapping,
EditorView.domEventHandlers({ scroll: forceParse }),
infoBoxTooltips(),
]);
const editorValue = ref<string>(removeExpressionPrefix(props.modelValue));
const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { removeExpressionPrefix } from '@/utils/expressions';
import { createEventBus, type EventBus } from 'n8n-design-system/utils';
import type { IDataObject } from 'n8n-workflow';
import { inputTheme } from './theme';
import { infoBoxTooltips } from '@/plugins/codemirror/tooltips/InfoBoxTooltip';
type Props = {
modelValue: string;
Expand Down Expand Up @@ -56,6 +57,7 @@ const extensions = computed(() => [
history(),
expressionInputHandler(),
EditorView.lineWrapping,
infoBoxTooltips(),
]);
const editorValue = ref<string>(removeExpressionPrefix(props.modelValue));
const {
Expand Down Expand Up @@ -138,7 +140,12 @@ onBeforeUnmount(() => {
</script>

<template>
<div ref="root" :class="$style.editor" data-test-id="inline-expression-editor-input"></div>
<div
ref="root"
title=""
:class="$style.editor"
data-test-id="inline-expression-editor-input"
></div>
</template>

<style lang="scss" module>
Expand Down
16 changes: 9 additions & 7 deletions packages/editor-ui/src/composables/useExpressionEditor.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {
computed,
type MaybeRefOrGetter,
onBeforeUnmount,
onMounted,
ref,
watchEffect,
type Ref,
toValue,
watch,
onMounted,
watchEffect,
type MaybeRefOrGetter,
type Ref,
} from 'vue';

import { ensureSyntaxTree } from '@codemirror/language';
Expand All @@ -19,6 +19,8 @@ import { useNDVStore } from '@/stores/ndv.store';

import type { TargetItem } from '@/Interface';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
import { closeCursorInfoBox } from '@/plugins/codemirror/tooltips/InfoBoxTooltip';
import type { Html, Plaintext, RawSegment, Resolvable, Segment } from '@/types/expressions';
import {
getExpressionErrorMessage,
Expand All @@ -28,16 +30,15 @@ import {
import { closeCompletion, completionStatus } from '@codemirror/autocomplete';
import {
Compartment,
EditorSelection,
EditorState,
type SelectionRange,
type Extension,
EditorSelection,
type SelectionRange,
} from '@codemirror/state';
import { EditorView, type ViewUpdate } from '@codemirror/view';
import { debounce, isEqual } from 'lodash-es';
import { useRouter } from 'vue-router';
import { useI18n } from '../composables/useI18n';
import { highlighter } from '../plugins/codemirror/resolvableHighlighter';
import { useWorkflowsStore } from '../stores/workflows.store';
import { useAutocompleteTelemetry } from './useAutocompleteTelemetry';

Expand Down Expand Up @@ -163,6 +164,7 @@ export const useExpressionEditor = ({
if (editor.value) {
editor.value.contentDOM.blur();
closeCompletion(editor.value);
closeCursorInfoBox(editor.value);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -735,8 +735,8 @@ describe('Resolution-based completions', () => {
const result = completions('{{ $json.obj.| }}');
expect(result).toContainEqual(expect.objectContaining({ label: 'str', detail: 'string' }));
expect(result).toContainEqual(expect.objectContaining({ label: 'empty', detail: 'null' }));
expect(result).toContainEqual(expect.objectContaining({ label: 'arr', detail: 'array' }));
expect(result).toContainEqual(expect.objectContaining({ label: 'obj', detail: 'object' }));
expect(result).toContainEqual(expect.objectContaining({ label: 'arr', detail: 'Array' }));
expect(result).toContainEqual(expect.objectContaining({ label: 'obj', detail: 'Object' }));
});

test('should display type information for: {{ $input.item.json.| }}', () => {
Expand All @@ -750,8 +750,8 @@ describe('Resolution-based completions', () => {
const result = completions('{{ $json.item.json.| }}');
expect(result).toContainEqual(expect.objectContaining({ label: 'str', detail: 'string' }));
expect(result).toContainEqual(expect.objectContaining({ label: 'empty', detail: 'null' }));
expect(result).toContainEqual(expect.objectContaining({ label: 'arr', detail: 'array' }));
expect(result).toContainEqual(expect.objectContaining({ label: 'obj', detail: 'object' }));
expect(result).toContainEqual(expect.objectContaining({ label: 'arr', detail: 'Array' }));
expect(result).toContainEqual(expect.objectContaining({ label: 'obj', detail: 'Object' }));
});

test('should display type information for: {{ $("My Node").item.json.| }}', () => {
Expand All @@ -765,8 +765,8 @@ describe('Resolution-based completions', () => {
const result = completions('{{ $("My Node").item.json.| }}');
expect(result).toContainEqual(expect.objectContaining({ label: 'str', detail: 'string' }));
expect(result).toContainEqual(expect.objectContaining({ label: 'empty', detail: 'null' }));
expect(result).toContainEqual(expect.objectContaining({ label: 'arr', detail: 'array' }));
expect(result).toContainEqual(expect.objectContaining({ label: 'obj', detail: 'object' }));
expect(result).toContainEqual(expect.objectContaining({ label: 'arr', detail: 'Array' }));
expect(result).toContainEqual(expect.objectContaining({ label: 'obj', detail: 'Object' }));
});

test('should not display type information for other completions', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const ROOT_DOLLAR_COMPLETIONS: Completion[] = [
section: RECOMMENDED_SECTION,
info: createInfoBoxRenderer({
name: '$json',
returnType: 'object',
returnType: 'Object',
description: i18n.baseText('codeNodeEditor.completer.json'),
docURL: 'https://docs.n8n.io/data/data-structure/',
}),
Expand All @@ -64,7 +64,7 @@ export const ROOT_DOLLAR_COMPLETIONS: Completion[] = [
section: RECOMMENDED_SECTION,
info: createInfoBoxRenderer({
name: '$binary',
returnType: 'object',
returnType: 'Object',
description: i18n.baseText('codeNodeEditor.completer.binary'),
}),
},
Expand Down Expand Up @@ -170,7 +170,7 @@ export const ROOT_DOLLAR_COMPLETIONS: Completion[] = [
section: METADATA_SECTION,
info: createInfoBoxRenderer({
name: '$input',
returnType: 'object',
returnType: 'Object',
description: i18n.baseText('codeNodeEditor.completer.$input'),
}),
},
Expand All @@ -179,7 +179,7 @@ export const ROOT_DOLLAR_COMPLETIONS: Completion[] = [
section: METADATA_SECTION,
info: createInfoBoxRenderer({
name: '$parameter',
returnType: 'object',
returnType: 'Object',
description: i18n.baseText('codeNodeEditor.completer.$parameter'),
}),
},
Expand Down Expand Up @@ -215,7 +215,7 @@ export const ROOT_DOLLAR_COMPLETIONS: Completion[] = [
section: METADATA_SECTION,
info: createInfoBoxRenderer({
name: '$vars',
returnType: 'object',
returnType: 'Object',
description: i18n.baseText('codeNodeEditor.completer.$vars'),
}),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ import { luxonInstanceDocs } from './nativesAutocompleteDocs/luxon.instance.docs
import { luxonStaticDocs } from './nativesAutocompleteDocs/luxon.static.docs';
import type { AutocompleteInput, ExtensionTypeName, FnToDoc, Resolved } from './types';
import {
applyBracketAccess,
applyBracketAccessCompletion,
applyCompletion,
getDefaultArgs,
getDisplayType,
hasNoParams,
hasRequiredArgs,
insertDefaultArgs,
Expand Down Expand Up @@ -181,7 +181,7 @@ export const natives = ({
typeName: ExtensionTypeName;
transformLabel?: (label: string) => string;
}): Completion[] => {
const nativeDocs: NativeDoc = NativeMethods.find((ee) => ee.typeName.toLowerCase() === typeName);
const nativeDocs = NativeMethods.find((ee) => ee.typeName.toLowerCase() === typeName);

if (!nativeDocs) return [];

Expand Down Expand Up @@ -231,12 +231,6 @@ export const extensions = ({
return toOptions({ fnToDoc, isFunction: true, includeHidden, transformLabel });
};

export const getType = (value: unknown): string => {
if (Array.isArray(value)) return 'array';
if (value === null) return 'null';
return (typeof value).toLocaleLowerCase();
};

export const isInputData = (base: string): boolean => {
return (
/^\$input\..*\.json]/.test(base) || /^\$json/.test(base) || /^\$\(.*\)\..*\.json/.test(base)
Expand All @@ -258,7 +252,7 @@ export const isBinary = (input: AutocompleteInput<IDataObject>): boolean => {
};

export const getDetail = (base: string, value: unknown): string | undefined => {
const type = getType(value);
const type = getDisplayType(value);
if (!isInputData(base) || type === 'function') return undefined;
return type;
};
Expand Down Expand Up @@ -382,17 +376,6 @@ const objectOptions = (input: AutocompleteInput<IDataObject>): Completion[] => {
detail: getDetail(base, resolvedProp),
};

const infoName = needsBracketAccess ? applyBracketAccess(key) : key;
option.info = createCompletionOption({
name: infoName,
doc: {
name: infoName,
returnType: isFunction ? 'any' : getType(resolvedProp),
},
isFunction,
transformLabel,
}).info;

return option;
});

Expand Down Expand Up @@ -821,7 +804,7 @@ export const customDataOptions = () => {
},
{
name: 'getAll',
returnType: 'object',
returnType: 'Object',
docURL: 'https://docs.n8n.io/workflows/executions/custom-executions-data/',
description: i18n.baseText('codeNodeEditor.completer.$execution.customData.getAll'),
examples: [
Expand Down Expand Up @@ -1046,13 +1029,13 @@ export const itemOptions = () => {
return [
{
name: 'json',
returnType: 'object',
returnType: 'Object',
docURL: 'https://docs.n8n.io/data/data-structure/',
description: i18n.baseText('codeNodeEditor.completer.item.json'),
},
{
name: 'binary',
returnType: 'object',
returnType: 'Object',
docURL: 'https://docs.n8n.io/data/data-structure/',
description: i18n.baseText('codeNodeEditor.completer.item.binary'),
},
Expand Down Expand Up @@ -1161,7 +1144,7 @@ export const secretProvidersOptions = () => {
name: provider,
doc: {
name: provider,
returnType: 'object',
returnType: 'Object',
description: i18n.baseText('codeNodeEditor.completer.$secrets.provider'),
docURL: i18n.baseText('settings.externalSecrets.docs'),
},
Expand Down Expand Up @@ -1296,7 +1279,7 @@ export const objectGlobalOptions = () => {
evaluated: "{ id: 1, name: 'Banana' }",
},
],
returnType: 'object',
returnType: 'Object',
docURL:
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ import {
import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
import { escapeMappingString } from '@/utils/mappingUtils';
import { PREVIOUS_NODES_SECTION, RECOMMENDED_SECTION, ROOT_DOLLAR_COMPLETIONS } from './constants';
import {
METADATA_SECTION,
PREVIOUS_NODES_SECTION,
RECOMMENDED_SECTION,
ROOT_DOLLAR_COMPLETIONS,
} from './constants';
import { createInfoBoxRenderer } from './infoBoxRenderer';

/**
Expand Down Expand Up @@ -79,7 +84,7 @@ export function dollarOptions(): Completion[] {
section: RECOMMENDED_SECTION,
info: createInfoBoxRenderer({
name: '$request',
returnType: 'object',
returnType: 'Object',
docURL: 'https://docs.n8n.io/code/builtin/http-node-variables/',
description: i18n.baseText('codeNodeEditor.completer.$request'),
}),
Expand All @@ -91,12 +96,22 @@ export function dollarOptions(): Completion[] {
return useExternalSecretsStore().isEnterpriseExternalSecretsEnabled
? [
{
label: '$secrets',
type: 'keyword',
label: '$vars',
section: METADATA_SECTION,
info: createInfoBoxRenderer({
name: '$vars',
returnType: 'Object',
description: i18n.baseText('codeNodeEditor.completer.$vars'),
}),
},
{
label: '$vars',
type: 'keyword',
label: '$secrets',
section: METADATA_SECTION,
info: createInfoBoxRenderer({
name: '$secrets',
returnType: 'Object',
description: i18n.baseText('codeNodeEditor.completer.$secrets'),
}),
},
]
: [];
Expand All @@ -114,7 +129,7 @@ export function dollarOptions(): Completion[] {
label,
info: createInfoBoxRenderer({
name: label,
returnType: 'object',
returnType: 'Object',
description: i18n.baseText('codeNodeEditor.completer.$()', { interpolate: { nodeName } }),
}),
section: PREVIOUS_NODES_SECTION,
Expand Down
Loading

0 comments on commit ec0373f

Please sign in to comment.