fix(editor): Fix code node displays lint messages in wrong location (#13664)

This commit is contained in:
Elias Meire
2025-03-04 14:59:30 +01:00
committed by GitHub
parent be441fb91f
commit d3ead68059
6 changed files with 174 additions and 45 deletions

View File

@@ -94,11 +94,11 @@
"xss": "catalog:"
},
"devDependencies": {
"@faker-js/faker": "^8.0.2",
"@iconify/json": "^2.2.228",
"@n8n/eslint-config": "workspace:*",
"@n8n/typescript-config": "workspace:*",
"@n8n/vitest-config": "workspace:*",
"@faker-js/faker": "^8.0.2",
"@iconify/json": "^2.2.228",
"@pinia/testing": "^0.1.6",
"@types/dateformat": "^3.0.0",
"@types/file-saver": "^2.0.1",
@@ -111,6 +111,7 @@
"@vitejs/plugin-vue": "catalog:frontend",
"@vitest/coverage-v8": "catalog:frontend",
"browserslist-to-esbuild": "^2.1.1",
"fake-indexeddb": "^6.0.0",
"miragejs": "^0.1.48",
"unplugin-icons": "^0.19.0",
"unplugin-vue-components": "^0.27.2",

View File

@@ -1,4 +1,5 @@
import '@testing-library/jest-dom';
import 'fake-indexeddb/auto';
import { configure } from '@testing-library/vue';
import 'core-js/proposals/set-methods-v2';
@@ -64,20 +65,21 @@ Object.defineProperty(window, 'matchMedia', {
});
class Worker {
onmessage: (message: string) => void;
onmessage = vi.fn();
url: string;
constructor(url: string) {
this.url = url;
this.onmessage = () => {};
}
postMessage(message: string) {
postMessage = vi.fn((message: string) => {
this.onmessage(message);
}
});
addEventListener() {}
addEventListener = vi.fn();
terminate = vi.fn();
}
Object.defineProperty(window, 'Worker', {

View File

@@ -14,7 +14,7 @@ import { Text, type Extension } from '@codemirror/state';
import { EditorView, hoverTooltip } from '@codemirror/view';
import * as Comlink from 'comlink';
import { NodeConnectionType, type CodeExecutionMode, type INodeExecutionData } from 'n8n-workflow';
import { ref, toRef, toValue, watch, type MaybeRefOrGetter } from 'vue';
import { onBeforeUnmount, ref, toRef, toValue, watch, type MaybeRefOrGetter } from 'vue';
import type { LanguageServiceWorker, RemoteLanguageServiceWorkerInit } from '../types';
import { typescriptCompletionSource } from './completions';
import { typescriptWorkerFacet } from './facet';
@@ -33,11 +33,13 @@ export function useTypescript(
const { debounce } = useDebounce();
const activeNodeName = ndvStore.activeNodeName;
const worker = ref<Comlink.Remote<LanguageServiceWorker>>();
const webWorker = ref<Worker>();
async function createWorker(): Promise<Extension> {
const { init } = Comlink.wrap<RemoteLanguageServiceWorkerInit>(
new Worker(new URL('../worker/typescript.worker.ts', import.meta.url), { type: 'module' }),
);
webWorker.value = new Worker(new URL('../worker/typescript.worker.ts', import.meta.url), {
type: 'module',
});
const { init } = Comlink.wrap<RemoteLanguageServiceWorkerInit>(webWorker.value);
worker.value = await init(
{
id: toValue(id),
@@ -125,6 +127,10 @@ export function useTypescript(
forceParse(editor);
});
onBeforeUnmount(() => {
if (webWorker.value) webWorker.value.terminate();
});
return {
createWorker,
};

View File

@@ -0,0 +1,118 @@
import type { WorkerInitOptions } from '../types';
import { worker } from './typescript.worker';
import { type ChangeSet, EditorState } from '@codemirror/state';
async function createWorker({
doc,
options,
}: { doc?: string; options?: Partial<WorkerInitOptions> } = {}) {
const defaultDoc = `
function myFunction(){
if (true){
const myObj = {test: "value"}
}
}
return $input.all();`;
const state = EditorState.create({ doc: doc ?? defaultDoc });
const tsWorker = worker.init(
{
allNodeNames: [],
content: state.doc.toJSON(),
id: 'id',
inputNodeNames: [],
mode: 'runOnceForAllItems',
variables: [],
...options,
},
async () => ({
json: { path: '', type: 'string', value: '' },
binary: [],
params: { path: '', type: 'string', value: '' },
}),
);
return await tsWorker;
}
describe('Typescript Worker', () => {
it('should return diagnostics', async () => {
const tsWorker = await createWorker();
expect(tsWorker.getDiagnostics()).toEqual([
{
from: 10,
markClass: 'cm-faded',
message: "'myFunction' is declared but its value is never read.",
severity: 'warning',
to: 20,
},
{
from: 47,
markClass: 'cm-faded',
message: "'myObj' is declared but its value is never read.",
severity: 'warning',
to: 52,
},
]);
});
it('should accept updates from the client and buffer them', async () => {
const tsWorker = await createWorker();
// Add if statement and remove indentation
const changes = [
[75, [0, '', ''], 22],
[76, [0, '', ''], 22],
[77, [0, ' if (true){', ' const myObj = {test: "value"}', ' }'], 22],
[77, [1], 13, [2], 30, [2], 23],
];
vi.useFakeTimers({ toFake: ['setTimeout', 'queueMicrotask', 'nextTick'] });
for (const change of changes) {
tsWorker.updateFile(change as unknown as ChangeSet);
}
expect(tsWorker.getDiagnostics()).toHaveLength(2);
vi.advanceTimersByTime(1000);
vi.runAllTicks();
expect(tsWorker.getDiagnostics()).toHaveLength(3);
expect(tsWorker.getDiagnostics()).toEqual([
{
from: 10,
markClass: 'cm-faded',
message: "'myFunction' is declared but its value is never read.",
severity: 'warning',
to: 20,
},
{
from: 47,
markClass: 'cm-faded',
message: "'myObj' is declared but its value is never read.",
severity: 'warning',
to: 52,
},
{
from: 96,
markClass: 'cm-faded',
message: "'myObj' is declared but its value is never read.",
severity: 'warning',
to: 101,
},
]);
});
it('should return completions', async () => {
const doc = 'return $input.';
const tsWorker = await createWorker({ doc });
const completionResult = await tsWorker.getCompletionsAtPos(doc.length);
assert(completionResult !== null);
const completionLabels = completionResult.result.options.map((c) => c.label);
expect(completionLabels).toContain('all()');
expect(completionLabels).toContain('first()');
});
});

View File

@@ -28,7 +28,7 @@ import { until } from '@vueuse/core';
self.process = { env: {} } as NodeJS.Process;
const worker: LanguageServiceWorkerInit = {
export const worker: LanguageServiceWorkerInit = {
async init(options, nodeDataFetcher) {
const loadedNodeTypesMap: Map<string, { type: string; typeName: string }> = reactive(new Map());
@@ -157,11 +157,11 @@ const worker: LanguageServiceWorkerInit = {
});
const applyChangesToCode = bufferChangeSets((bufferedChanges) => {
bufferedChanges.iterChanges((start, end, _fromNew, _toNew, text) => {
bufferedChanges.iterChanges((start, end, fromNew, _toNew, text) => {
const length = end - start;
env.updateFile(codeFileName, text.toString(), {
start: editorPositionToTypescript(start),
start: editorPositionToTypescript(fromNew),
length,
});
});