diff --git a/packages/app/e2e/browser/file-editing.spec.ts b/packages/app/e2e/browser/file-editing.spec.ts index d426a03fb4..df332c8278 100644 --- a/packages/app/e2e/browser/file-editing.spec.ts +++ b/packages/app/e2e/browser/file-editing.spec.ts @@ -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("shows an absolute POSIX assistant file link relative to the workspace on hover", async ({ page, }) => { diff --git a/packages/app/src/file-pane/editor/language.test.ts b/packages/app/src/file-pane/editor/language.test.ts new file mode 100644 index 0000000000..e51a587ae3 --- /dev/null +++ b/packages/app/src/file-pane/editor/language.test.ts @@ -0,0 +1,45 @@ +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", "
text
", ""], + ["source.md", "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}`); + let state = EditorState.create({ doc, extensions: [language] }); + const target = { + get state() { + return state; + }, + dispatch(transaction: Transaction) { + 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"); + 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); +}); diff --git a/packages/highlight/src/parsers.ts b/packages/highlight/src/parsers.ts index 9ccb3129be..dcc5f8ccf1 100644 --- a/packages/highlight/src/parsers.ts +++ b/packages/highlight/src/parsers.ts @@ -1,4 +1,9 @@ -import { defineLanguageFacet, Language, StreamLanguage } from "@codemirror/language"; +import { + defineLanguageFacet, + Language, + languageDataProp, + StreamLanguage, +} 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 +14,86 @@ 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 { 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: "" } } }; + +function language( + parser: LRParser | MarkdownParser, + languageData: Parameters