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