Skip to content

Commit

Permalink
fix(editor): Fix Code node bug erasing and overwriting code when swit…
Browse files Browse the repository at this point in the history
…ching between nodes (#12637)

Co-authored-by: Elias Meire <[email protected]>
  • Loading branch information
alexgrozav and elsmr authored Jan 16, 2025
1 parent 0b0f532 commit 02d953d
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 5 deletions.
22 changes: 22 additions & 0 deletions packages/editor-ui/src/__tests__/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,25 @@ Object.defineProperty(window, 'matchMedia', {
dispatchEvent: vi.fn(),
})),
});

class Worker {
onmessage: (message: string) => void;

url: string;

constructor(url: string) {
this.url = url;
this.onmessage = () => {};
}

postMessage(message: string) {
this.onmessage(message);
}

addEventListener() {}
}

Object.defineProperty(window, 'Worker', {
writable: true,
value: Worker,
});
103 changes: 103 additions & 0 deletions packages/editor-ui/src/composables/useCodeEditor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { renderComponent } from '@/__tests__/render';
import { EditorView } from '@codemirror/view';
import { createTestingPinia } from '@pinia/testing';
import { waitFor } from '@testing-library/vue';
import { setActivePinia } from 'pinia';
import { beforeEach, describe, vi } from 'vitest';
import { defineComponent, h, ref, toValue } from 'vue';
import { useCodeEditor } from './useCodeEditor';
import userEvent from '@testing-library/user-event';

describe('useCodeEditor', () => {
const defaultOptions: Omit<Parameters<typeof useCodeEditor>[0], 'editorRef'> = {
language: 'javaScript',
};

const renderCodeEditor = async (options: Partial<typeof defaultOptions> = defaultOptions) => {
let codeEditor!: ReturnType<typeof useCodeEditor>;
const renderResult = renderComponent(
defineComponent({
setup() {
const root = ref<HTMLElement>();
codeEditor = useCodeEditor({ ...defaultOptions, ...options, editorRef: root });

return () => h('div', { ref: root, 'data-test-id': 'editor-root' });
},
}),
{ props: { options } },
);
expect(renderResult.getByTestId('editor-root')).toBeInTheDocument();
await waitFor(() => toValue(codeEditor.editor));
return { renderResult, codeEditor };
};

beforeEach(() => {
setActivePinia(createTestingPinia());
});

afterEach(() => {
vi.clearAllMocks();
vi.useRealTimers();
});

it('should create an editor', async () => {
const { codeEditor } = await renderCodeEditor();

await waitFor(() => expect(toValue(codeEditor.editor)).toBeInstanceOf(EditorView));
});

it('should focus editor', async () => {
const { renderResult, codeEditor } = await renderCodeEditor({});

const root = renderResult.getByTestId('editor-root');
const input = root.querySelector('.cm-line') as HTMLDivElement;

await userEvent.click(input);

expect(codeEditor.editor.value?.hasFocus).toBe(true);
});

it('should emit changes', async () => {
vi.useFakeTimers();

const user = userEvent.setup({
advanceTimers: vi.advanceTimersByTime,
});

const onChange = vi.fn();
const { renderResult } = await renderCodeEditor({
onChange,
});

const root = renderResult.getByTestId('editor-root');
const input = root.querySelector('.cm-line') as HTMLDivElement;

await user.type(input, 'test');

vi.advanceTimersByTime(300);

expect(onChange.mock.calls[0][0].state.doc.toString()).toEqual('test');
});

it('should emit debounced changes before unmount', async () => {
vi.useFakeTimers();

const user = userEvent.setup({
advanceTimers: vi.advanceTimersByTime,
});

const onChange = vi.fn();
const { renderResult } = await renderCodeEditor({
onChange,
});

const root = renderResult.getByTestId('editor-root');
const input = root.querySelector('.cm-line') as HTMLDivElement;

await user.type(input, 'test');

renderResult.unmount();

expect(onChange.mock.calls[0][0].state.doc.toString()).toEqual('test');
});
});
18 changes: 13 additions & 5 deletions packages/editor-ui/src/composables/useCodeEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import {
} from '@codemirror/view';
import { indentationMarkers } from '@replit/codemirror-indentation-markers';
import { html } from 'codemirror-lang-html-n8n';
import { debounce } from 'lodash-es';
import { jsonParse, type CodeExecutionMode, type IDataObject } from 'n8n-workflow';
import { v4 as uuid } from 'uuid';
import {
Expand All @@ -47,6 +46,8 @@ import {
import { useCompleter } from '../components/CodeNodeEditor/completer';
import { mappingDropCursor } from '../plugins/codemirror/dragAndDrop';
import { languageFacet, type CodeEditorLanguage } from '../plugins/codemirror/format';
import { debounce } from 'lodash-es';
import { ignoreUpdateAnnotation } from '../utils/forceParse';

export type CodeEditorLanguageParamsMap = {
json: {};
Expand Down Expand Up @@ -85,7 +86,6 @@ export const useCodeEditor = <L extends CodeEditorLanguage>({
const editor = ref<EditorView>();
const hasFocus = ref(false);
const hasChanges = ref(false);
const lastChange = ref<ViewUpdate>();
const selection = ref<SelectionRange>(EditorSelection.cursor(0)) as Ref<SelectionRange>;
const customExtensions = ref<Compartment>(new Compartment());
const readOnlyExtensions = ref<Compartment>(new Compartment());
Expand Down Expand Up @@ -157,14 +157,19 @@ export const useCodeEditor = <L extends CodeEditorLanguage>({
const emitChanges = debounce((update: ViewUpdate) => {
onChange(update);
}, 300);
const lastChange = ref<ViewUpdate>();

function onEditorUpdate(update: ViewUpdate) {
autocompleteStatus.value = completionStatus(update.view.state);
updateSelection(update);

if (update.docChanged) {
hasChanges.value = true;
const shouldIgnoreUpdate = update.transactions.some((tr) =>
tr.annotation(ignoreUpdateAnnotation),
);

if (update.docChanged && !shouldIgnoreUpdate) {
lastChange.value = update;
hasChanges.value = true;
emitChanges(update);
}
}
Expand Down Expand Up @@ -369,7 +374,10 @@ export const useCodeEditor = <L extends CodeEditorLanguage>({
// Code is too large, localStorage quota exceeded
localStorage.removeItem(storedStateId.value);
}
if (lastChange.value) onChange(lastChange.value);

if (lastChange.value) {
onChange(lastChange.value);
}
editor.value.destroy();
}
});
Expand Down
5 changes: 5 additions & 0 deletions packages/editor-ui/src/utils/forceParse.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { Annotation } from '@codemirror/state';
import type { EditorView } from '@codemirror/view';

export const ignoreUpdateAnnotation = Annotation.define<boolean>();

/**
* Simulate user action to force parser to catch up during scroll.
*/
export function forceParse(view: EditorView) {
view.dispatch({
changes: { from: view.viewport.to, insert: '_' },
annotations: [ignoreUpdateAnnotation.of(true)],
});

view.dispatch({
changes: { from: view.viewport.to - 1, to: view.viewport.to, insert: '' },
annotations: [ignoreUpdateAnnotation.of(true)],
});
}

0 comments on commit 02d953d

Please sign in to comment.