From 883c1bde675bb6926fa49a7c4a444589975c4dff Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 19:42:52 +0200 Subject: [PATCH] fix(editor): comment JSX children with JSX comments Cmd+/ on a JSX child line in a .jsx or .tsx file inserted `//`, which JSX renders as text. JSX nodes now resolve to `{/* */}` block tokens, while JavaScript inside a JSX escape keeps `//`. --- .../app/src/file-pane/editor/language.test.ts | 53 ++++++++++++++++--- packages/highlight/src/parsers.ts | 21 ++++++-- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/packages/app/src/file-pane/editor/language.test.ts b/packages/app/src/file-pane/editor/language.test.ts index e51a587ae3..1f1bb09fe6 100644 --- a/packages/app/src/file-pane/editor/language.test.ts +++ b/packages/app/src/file-pane/editor/language.test.ts @@ -15,8 +15,51 @@ test.each([ ])("toggles the comment and restores the source in %s", (filename, doc, commented) => { const language = getLanguageForFile(filename); if (!language) throw new Error(`Missing language for ${filename}`); - let state = EditorState.create({ doc, extensions: [language] }); - const target = { + const target = editorTarget(EditorState.create({ doc, extensions: [language] })); + expect(toggleComment(target)).toBe(true); + expect(target.state.doc.toString()).toBe(commented); + expect(toggleComment(target)).toBe(true); + expect(target.state.doc.toString()).toBe(doc); +}); + +const jsxView = [ + "const view = (", + "
", + " text", + " {value}", + " ", + " {items.map(() => {", + " const item = 1;", + " })}", + "
", + ");", +].join("\n"); + +test.each([ + ["view.jsx", "text", "{/* text */}"], + ["view.tsx", "text", "{/* text */}"], + ["view.tsx", "{value}", "{/* {value} */}"], + ["view.tsx", "", "{/* */}"], + ["view.tsx", "const item = 1;", "// const item = 1;"], +])("toggles %s line %s as %s", (filename, line, commented) => { + const language = getLanguageForFile(filename); + if (!language) throw new Error(`Missing language for ${filename}`); + const target = editorTarget( + EditorState.create({ + doc: jsxView, + extensions: [language], + selection: { anchor: jsxView.indexOf(line) }, + }), + ); + expect(toggleComment(target)).toBe(true); + expect(target.state.doc.toString()).toBe(jsxView.replace(line, commented)); + expect(toggleComment(target)).toBe(true); + expect(target.state.doc.toString()).toBe(jsxView); +}); + +function editorTarget(initial: EditorState) { + let state = initial; + return { get state() { return state; }, @@ -24,11 +67,7 @@ test.each([ state = transaction.state; }, }; - expect(toggleComment(target)).toBe(true); - expect(state.doc.toString()).toBe(commented); - expect(toggleComment(target)).toBe(true); - expect(state.doc.toString()).toBe(doc); -}); +} test("keeps JSON comment-free", () => { const language = getLanguageForFile("data.json"); diff --git a/packages/highlight/src/parsers.ts b/packages/highlight/src/parsers.ts index dcc5f8ccf1..8b7fae7c59 100644 --- a/packages/highlight/src/parsers.ts +++ b/packages/highlight/src/parsers.ts @@ -3,6 +3,8 @@ import { Language, languageDataProp, StreamLanguage, + sublanguageProp, + type Sublanguage, } from "@codemirror/language"; import { dart } from "@codemirror/legacy-modes/mode/clike"; import { swift } from "@codemirror/legacy-modes/mode/swift"; @@ -33,15 +35,28 @@ const hashComments = { commentTokens: { line: "#" } }; const cssComments = { commentTokens: { block: { open: "/*", close: "*/" } } }; const markupComments = { commentTokens: { block: { open: "" } } }; +// JSX children are text, so a `//` there renders on the page instead of commenting the line out. +const jsxComments: Sublanguage = { + // The `{` opening a JSX escape sits on a child line too, so judge it by its escape. + test: (node) => (node.name === "{" && node.parent ? node.parent : node).name.startsWith("JSX"), + facet: defineLanguageFacet({ commentTokens: { block: { open: "{/*", close: "*/}" } } }), +}; + function language( parser: LRParser | MarkdownParser, languageData: Parameters[0] = {}, + sublanguages: Sublanguage[] = [], ): Language { const data = defineLanguageFacet(languageData); return new Language( data, parser.configure({ - props: [languageDataProp.add((node) => (node.isTop ? data : undefined))], + props: [ + languageDataProp.add((node) => (node.isTop ? data : undefined)), + sublanguageProp.add((node) => + node.isTop && sublanguages.length ? sublanguages : undefined, + ), + ], }), ); } @@ -49,9 +64,9 @@ function language( const languagesByExtension: Record = { // JavaScript/TypeScript js: language(jsParser, cStyleComments), - jsx: language(jsParser.configure({ dialect: "jsx" }), cStyleComments), + jsx: language(jsParser.configure({ dialect: "jsx" }), cStyleComments, [jsxComments]), ts: language(jsParser.configure({ dialect: "ts" }), cStyleComments), - tsx: language(jsParser.configure({ dialect: "ts jsx" }), cStyleComments), + tsx: language(jsParser.configure({ dialect: "ts jsx" }), cStyleComments, [jsxComments]), mjs: language(jsParser, cStyleComments), cjs: language(jsParser, cStyleComments), // C / C++ / Objective-C