mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-18 07:12:35 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f77692fefb | ||
|
|
883c1bde67 | ||
|
|
5b4c57a301 | ||
|
|
5807cf817f | ||
|
|
e092bd9147 | ||
|
|
6ae609bce3 |
@@ -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);
|
||||
});
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user