Compare commits

...
Author SHA1 Message Date
paseo-bot[bot]and3ae3ae f77692fefb fix(editor): keep // on JSX attribute lines and use {/* */} in MDX
Cmd+/ on an attribute line of a multi-line JSX tag wrapped it in {/* */},
which TypeScript rejects. Only lines that start a JSX child (text, the {
of a child escape, or the < of a nested element) take {/* */} now, and
attribute lines keep //.

.mdx files got <!-- -->, which MDX 2 and later fail to compile. They use
{/* */} now.

Co-authored-by: 3ae3ae <83870598+3ae3ae@users.noreply.github.com>
2026-10-05 03:54:11 +02:00
paseo-bot[bot] 883c1bde67 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 `//`.
2026-10-04 19:42:52 +02:00
paseo-bot[bot]andBae Seongmin 5b4c57a301 Merge main into the editor comment-token fix
Co-authored-by: Bae Seongmin <83870598+3ae3ae@users.noreply.github.com>
2026-10-04 19:25:39 +02:00
Bae Seongmin 5807cf817f Merge branch 'main' into fix/editor-language-comment-tokens 2026-09-29 23:25:24 +09:00
3ae3ae e092bd9147 Merge main into fix/editor-language-comment-tokens 2026-09-29 16:04:18 +09:00
3ae3ae 6ae609bce3 fix(editor): register comment tokens for Lezer languages 2026-09-29 15:42:30 +09:00
3 changed files with 219 additions and 38 deletions
@@ -96,6 +96,37 @@ async function seedAgentWithFileLink(input: LinkedFile) {
}
test.describe("CodeMirror workspace file editing", () => {
test("toggles comments using each file language's tokens", async ({ page, withWorkspace }) => {
const workspace = await withWorkspace({ prefix: "file-comment-shortcut-" });
const sources = [
{ filename: "comment.ts", content: "const value = 1;", commented: "// const value = 1;" },
{ filename: "comment.py", content: "value = 1", commented: "# value = 1" },
{ filename: "comment.css", content: "body {}", commented: "/* body {} */" },
];
for (const source of sources) {
await writeFile(path.join(workspace.repoPath, source.filename), source.content, "utf8");
}
await workspace.navigateTo();
for (const source of sources) {
await openWorkspaceFile(page, source.filename);
await editor(page).click();
await editor(page).press("ControlOrMeta+/");
await expect(editor(page)).toHaveText(source.commented);
await expect
.poll(() => readFile(path.join(workspace.repoPath, source.filename), "utf8"))
.toBe(source.commented);
await editor(page).press("ControlOrMeta+/");
await expect(editor(page)).toHaveText(source.content);
await expect
.poll(() => readFile(path.join(workspace.repoPath, source.filename), "utf8"))
.toBe(source.content);
}
await test.info().attach("comment-shortcut", {
body: await page.screenshot(),
contentType: "image/png",
});
});
test("jumps to each relative reference to the same file", async ({ page }) => {
const session = await seedMockAgentWorkspace({
repoPrefix: "relative-reference-qa-",
@@ -0,0 +1,98 @@
import { EditorState, type Transaction } from "@codemirror/state";
import { toggleComment } from "@codemirror/commands";
import { getLanguageForFile } from "@getpaseo/highlight";
import { expect, test } from "vitest";
test.each([
["source.ts", "const value = 1;", "// const value = 1;"],
["source.py", "value = 1", "# value = 1"],
["source.css", "body {}", "/* body {} */"],
["source.html", "<p>text</p>", "<!-- <p>text</p> -->"],
["source.md", "text", "<!-- text -->"],
["source.mdx", "text", "{/* text */}"],
["source.ex", "value = 1", "# value = 1"],
["source.cs", "var value = 1;", "// var value = 1;"],
["source.nix", "1", "# 1"],
])("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}`);
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 = (",
" <div",
' className="x"',
" onClick={() => go()}",
" >",
" text",
" {value}",
" <span />",
"{flag}",
"<br />",
" <Foo",
' a="1"',
" />",
" {items.map(() => {",
" const item = 1;",
" })}",
" </div>",
");",
].join("\n");
test.each([
["view.jsx", "text", "{/* text */}"],
["view.tsx", "text", "{/* text */}"],
["view.tsx", "{value}", "{/* {value} */}"],
["view.tsx", "<span />", "{/* <span /> */}"],
["view.tsx", "{flag}", "{/* {flag} */}"],
["view.tsx", "<br />", "{/* <br /> */}"],
["view.tsx", 'className="x"', '// className="x"'],
["view.tsx", "onClick={() => go()}", "// onClick={() => go()}"],
["view.tsx", 'a="1"', '// a="1"'],
["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;
},
dispatch(transaction: Transaction) {
state = transaction.state;
},
};
}
test("keeps JSON comment-free", () => {
const language = getLanguageForFile("data.json");
if (!language) throw new Error("Missing JSON language");
const state = EditorState.create({ doc: "{}", extensions: [language] });
expect(
toggleComment({
state,
dispatch() {
throw new Error("JSON must not be edited");
},
}),
).toBe(false);
});
+90 -38
View File
@@ -1,4 +1,11 @@
import { defineLanguageFacet, Language, StreamLanguage } from "@codemirror/language";
import {
defineLanguageFacet,
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";
import { parser as jsParser } from "@lezer/javascript";
@@ -9,68 +16,113 @@ import { parser as goParser } from "@lezer/go";
import { parser as htmlParser } from "@lezer/html";
import { parser as javaParser } from "@lezer/java";
import { parser as pythonParser } from "@lezer/python";
import { parser as markdownParser } from "@lezer/markdown";
import { parser as markdownParser, type MarkdownParser } from "@lezer/markdown";
import { parser as phpParser } from "@lezer/php";
import { parser as rustParser } from "@lezer/rust";
import { parser as xmlParser } from "@lezer/xml";
import { parser as yamlParser } from "@lezer/yaml";
import { parser as elixirParser } from "lezer-elixir";
import type { Parser } from "@lezer/common";
import type { Parser, SyntaxNode } from "@lezer/common";
import type { LRParser } from "@lezer/lr";
import { csharpLanguage } from "./csharp/language.js";
import { astroParser } from "./astro/parser.js";
import { nixLanguage } from "./nix/language.js";
import { parser as svelteBaseParser } from "./svelte/parser.js";
import { configureNesting, defaultNesting } from "./svelte/nesting.js";
function language(parser: Parser): Language {
return new Language(defineLanguageFacet(), parser);
const cStyleComments = { commentTokens: { line: "//", block: { open: "/*", close: "*/" } } };
const hashComments = { commentTokens: { line: "#" } };
const cssComments = { commentTokens: { block: { open: "/*", close: "*/" } } };
const markupComments = { commentTokens: { block: { open: "<!--", close: "-->" } } };
const jsxChildComments = { commentTokens: { block: { open: "{/*", close: "*/}" } } };
// JSX children are text, so a `//` there renders on the page instead of commenting the line out.
// Attribute lines of a multi-line tag are not children and keep `//`.
const jsxComments: Sublanguage = {
test: isJsxChild,
facet: defineLanguageFacet(jsxChildComments),
};
// A line starts on a child as text, as the `{` of an escape, or as the `<` of a nested element.
function isJsxChild(node: SyntaxNode): boolean {
return childAt(node)?.parent?.name === "JSXElement";
}
function childAt(node: SyntaxNode): SyntaxNode | null {
if (node.name === "JSXText") return node;
if (node.name === "{") return node.parent;
if (node.name === "JSXStartTag") return node.parent?.parent ?? null;
return null;
}
function language(
parser: LRParser | MarkdownParser,
languageData: Parameters<typeof defineLanguageFacet>[0] = {},
sublanguages: Sublanguage[] = [],
): Language {
const data = defineLanguageFacet(languageData);
return new Language(
data,
parser.configure({
props: [
languageDataProp.add((node) => (node.isTop ? data : undefined)),
sublanguageProp.add((node) =>
node.isTop && sublanguages.length ? sublanguages : undefined,
),
],
}),
);
}
const languagesByExtension: Record<string, Language> = {
// JavaScript/TypeScript
js: language(jsParser),
jsx: language(jsParser.configure({ dialect: "jsx" })),
ts: language(jsParser.configure({ dialect: "ts" })),
tsx: language(jsParser.configure({ dialect: "ts jsx" })),
mjs: language(jsParser),
cjs: language(jsParser),
js: language(jsParser, cStyleComments),
jsx: language(jsParser.configure({ dialect: "jsx" }), cStyleComments, [jsxComments]),
ts: language(jsParser.configure({ dialect: "ts" }), cStyleComments),
tsx: language(jsParser.configure({ dialect: "ts jsx" }), cStyleComments, [jsxComments]),
mjs: language(jsParser, cStyleComments),
cjs: language(jsParser, cStyleComments),
// C / C++ / Objective-C
c: language(cppParser),
h: language(cppParser),
cc: language(cppParser),
cpp: language(cppParser),
cxx: language(cppParser),
hpp: language(cppParser),
hxx: language(cppParser),
m: language(cppParser),
mm: language(cppParser),
c: language(cppParser, cStyleComments),
h: language(cppParser, cStyleComments),
cc: language(cppParser, cStyleComments),
cpp: language(cppParser, cStyleComments),
cxx: language(cppParser, cStyleComments),
hpp: language(cppParser, cStyleComments),
hxx: language(cppParser, cStyleComments),
m: language(cppParser, cStyleComments),
mm: language(cppParser, cStyleComments),
// JSON
json: language(jsonParser),
// CSS
css: language(cssParser),
scss: language(cssParser),
css: language(cssParser, cssComments),
scss: language(cssParser, cStyleComments),
// HTML
html: language(htmlParser),
htm: language(htmlParser),
html: language(htmlParser, markupComments),
htm: language(htmlParser, markupComments),
// Svelte
svelte: language(svelteBaseParser.configure({ wrap: configureNesting(defaultNesting) })),
svelte: language(
svelteBaseParser.configure({ wrap: configureNesting(defaultNesting) }),
markupComments,
),
// Astro
astro: language(astroParser),
astro: new Language(defineLanguageFacet(), astroParser),
// XML
xml: language(xmlParser),
xml: language(xmlParser, markupComments),
// Java
java: language(javaParser),
java: language(javaParser, cStyleComments),
// Python
py: language(pythonParser),
py: language(pythonParser, hashComments),
// Go
go: language(goParser),
go: language(goParser, cStyleComments),
// PHP
php: language(phpParser),
php: language(phpParser, cStyleComments),
// YAML
yaml: language(yamlParser),
yml: language(yamlParser),
yaml: language(yamlParser, hashComments),
yml: language(yamlParser, hashComments),
// Rust
rs: language(rustParser),
rs: language(rustParser, cStyleComments),
// Swift
swift: StreamLanguage.define(swift),
// Dart
@@ -80,11 +132,11 @@ const languagesByExtension: Record<string, Language> = {
// Nix
nix: nixLanguage,
// Elixir
ex: language(elixirParser),
exs: language(elixirParser),
ex: language(elixirParser, hashComments),
exs: language(elixirParser, hashComments),
// Markdown
md: language(markdownParser),
mdx: language(markdownParser),
md: language(markdownParser, markupComments),
mdx: language(markdownParser, jsxChildComments),
};
export function getLanguageForFile(filename: string): Language | null {