diff --git a/e2e-tests/code-editing-and-ast-interaction.test.ts b/e2e-tests/code-editing-and-ast-interaction.test.ts index ee2df7de..af28de72 100644 --- a/e2e-tests/code-editing-and-ast-interaction.test.ts +++ b/e2e-tests/code-editing-and-ast-interaction.test.ts @@ -196,6 +196,54 @@ test(`should not crash when an ESQuery selector matches a node with an empty ran expect(consoleErrors).toStrictEqual([]); }); +test(`should keep ESQuery highlights when switching languages`, async ({ + page, +}) => { + const consoleErrors: string[] = []; + page.on("console", message => { + if (message.type() === "error") { + consoleErrors.push(message.text()); + } + }); + + await page.goto("/"); + + const codeEditor = page.getByRole("textbox", { + name: "Code Editor", + exact: true, + }); + const highlight = codeEditor.locator(".bg-editorHighlightedRangeColor"); + const languageOptions = page.getByRole("button", { + name: "Language Options", + }); + + await codeEditor.fill('foo("bar");'); + + await page + .getByRole("textbox", { name: "ESQuery Selector" }) + .fill("Literal"); + + await expect(highlight).toHaveText(['"bar"']); + + await languageOptions.click(); + await setComboboxOption(page, "Language", "JSON"); + await page.keyboard.press("Escape"); + + await codeEditor.fill("[]"); + + await expect( + page + .getByRole("listitem") + .filter({ hasText: "bodyArray{type, elements, loc, ...}" }), + ).toBeVisible(); + + await languageOptions.click(); + await setComboboxOption(page, "Language", "JavaScript"); + + await expect(highlight).toHaveText(['"bar"']); + expect(consoleErrors).toStrictEqual([]); +}); + test.describe("AST node expansion", () => { test.beforeEach(async ({ page }) => { await page.goto("/"); diff --git a/src/components/editor.tsx b/src/components/editor.tsx index 4e567d36..6b323a5a 100644 --- a/src/components/editor.tsx +++ b/src/components/editor.tsx @@ -39,7 +39,7 @@ const basicSetup: BasicSetupOptions = { type EditorProperties = { ariaLabel: string; readOnly?: boolean; - value?: string; + value: string; highlightedRanges?: SourceRange[]; onChange?: (value: string) => void; }; @@ -70,9 +70,9 @@ export const Editor: FC = ({ EditorView.contentAttributes.of({ "aria-label": ariaLabel }), ESLintPlaygroundTheme, ESLintPlaygroundHighlightStyle, - highlightedRangesExtension(highlightedRanges), + highlightedRangesExtension(highlightedRanges, value), ], - [activeLanguageExtension, wrap, ariaLabel, highlightedRanges], + [activeLanguageExtension, wrap, ariaLabel, highlightedRanges, value], ); const debouncedOnChange = useMemo( diff --git a/src/utils/highlighted-ranges.ts b/src/utils/highlighted-ranges.ts index 23972880..c1cbecc1 100644 --- a/src/utils/highlighted-ranges.ts +++ b/src/utils/highlighted-ranges.ts @@ -16,18 +16,31 @@ function createHighlightedRangeDecorations(ranges: SourceRange[]) { ); } -export const highlightedRangesExtension = (ranges: SourceRange[]) => +export const highlightedRangesExtension = ( + ranges: SourceRange[], + code: string, +) => ViewPlugin.define( - () => ({ - decorations: createHighlightedRangeDecorations(ranges), + view => ({ + decorations: + view.state.doc.toString() === code + ? createHighlightedRangeDecorations(ranges) + : null, update(update: ViewUpdate) { - if (update.docChanged) { + if (!update.docChanged) { + return; + } + + if (this.decorations) { this.decorations = this.decorations.map(update.changes); + } else if (update.state.doc.toString() === code) { + this.decorations = + createHighlightedRangeDecorations(ranges); } }, }), { - decorations: value => value.decorations, + decorations: value => value.decorations ?? Decoration.none, }, );