fix(highlight): highlight Svelte components (#3487)
* fix(highlight): highlight Svelte components `.svelte` had no entry in the extension table, so `highlightCode` fell through to one unstyled token per line. That is the same table the file pane, the diff tokens, markdown fences and the web editor all resolve through, so a component rendered as plain text everywhere in the app. Register `@replit/codemirror-lang-svelte`. Its `svelteLanguage` already wires the mixed parsing for the `<script>` and `<style>` blocks, so the script gets JS/TS roles, the style block gets CSS roles, and the template gets tag, directive and mustache roles. It comes from the same publisher as the `@replit/codemirror-lang-csharp` entry already in this package, and only the grammar itself is declared: its CodeMirror peers resolve the same way the C# grammar's peers already do. * test(server): cover Svelte diff highlighting The review surface in the issue is the diff, and the daemon is what tokenizes it (`diff-highlighter.ts` calls `highlightCode`), so the regression belongs here too: script, template block and markup roles on a `.svelte` hunk.
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
|||||||
sha256-Ep6Z+J0urY1L66cXGe9bB6tQr8T3TY73GkLrvhf8FmA=
|
sha256-WblXeNn1wUUaxElDGNXHcWdvvHJhqp7LySanhLDv5lQ=
|
||||||
|
|||||||
Generated
+80
@@ -2573,6 +2573,54 @@
|
|||||||
"@lezer/common": "^1.1.0"
|
"@lezer/common": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@codemirror/lang-css": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@lezer/common": "^1.0.2",
|
||||||
|
"@lezer/css": "^1.1.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lang-html": {
|
||||||
|
"version": "6.4.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.12.tgz",
|
||||||
|
"integrity": "sha512-pw2ReWKUqSkbvh76RAT4NYxiogRu+PWkR2ukAwO9uOgrm8uipkzjtKKtNpyeAQwHOqxEeSvAXZ6vr3AfyB9y/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/lang-css": "^6.0.0",
|
||||||
|
"@codemirror/lang-javascript": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.4.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.17.0",
|
||||||
|
"@lezer/common": "^1.0.0",
|
||||||
|
"@lezer/css": "^1.1.0",
|
||||||
|
"@lezer/html": "^1.3.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lang-javascript": {
|
||||||
|
"version": "6.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz",
|
||||||
|
"integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.6.0",
|
||||||
|
"@codemirror/lint": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.17.0",
|
||||||
|
"@lezer/common": "^1.0.0",
|
||||||
|
"@lezer/javascript": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@codemirror/language": {
|
"node_modules/@codemirror/language": {
|
||||||
"version": "6.12.4",
|
"version": "6.12.4",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
|
||||||
@@ -2596,6 +2644,18 @@
|
|||||||
"@codemirror/language": "^6.0.0"
|
"@codemirror/language": "^6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@codemirror/lint": {
|
||||||
|
"version": "6.9.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz",
|
||||||
|
"integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.42.0",
|
||||||
|
"crelt": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@codemirror/search": {
|
"node_modules/@codemirror/search": {
|
||||||
"version": "6.7.1",
|
"version": "6.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz",
|
||||||
@@ -11169,6 +11229,25 @@
|
|||||||
"@lezer/lr": "^1.0.0"
|
"@lezer/lr": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@replit/codemirror-lang-svelte": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@replit/codemirror-lang-svelte/-/codemirror-lang-svelte-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-U2OqqgMM6jKelL0GNWbAmqlu1S078zZNoBqlJBW+retTc5M4Mha6/Y2cf4SVg6ddgloJvmcSpt4hHrVoM4ePRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/lang-css": "^6.0.1",
|
||||||
|
"@codemirror/lang-html": "^6.2.0",
|
||||||
|
"@codemirror/lang-javascript": "^6.1.1",
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0",
|
||||||
|
"@lezer/common": "^1.0.0",
|
||||||
|
"@lezer/highlight": "^1.0.0",
|
||||||
|
"@lezer/javascript": "^1.2.0",
|
||||||
|
"@lezer/lr": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@replit/codemirror-vim": {
|
"node_modules/@replit/codemirror-vim": {
|
||||||
"version": "6.3.0",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@replit/codemirror-vim/-/codemirror-vim-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@replit/codemirror-vim/-/codemirror-vim-6.3.0.tgz",
|
||||||
@@ -38597,6 +38676,7 @@
|
|||||||
"@lezer/yaml": "^1.0.4",
|
"@lezer/yaml": "^1.0.4",
|
||||||
"@replit/codemirror-lang-csharp": "^6.2.0",
|
"@replit/codemirror-lang-csharp": "^6.2.0",
|
||||||
"@replit/codemirror-lang-nix": "^6.0.1",
|
"@replit/codemirror-lang-nix": "^6.0.1",
|
||||||
|
"@replit/codemirror-lang-svelte": "^6.0.0",
|
||||||
"lezer-elixir": "^1.1.2"
|
"lezer-elixir": "^1.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
"@lezer/yaml": "^1.0.4",
|
"@lezer/yaml": "^1.0.4",
|
||||||
"@replit/codemirror-lang-csharp": "^6.2.0",
|
"@replit/codemirror-lang-csharp": "^6.2.0",
|
||||||
"@replit/codemirror-lang-nix": "^6.0.1",
|
"@replit/codemirror-lang-nix": "^6.0.1",
|
||||||
|
"@replit/codemirror-lang-svelte": "^6.0.0",
|
||||||
"lezer-elixir": "^1.1.2"
|
"lezer-elixir": "^1.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -86,6 +86,38 @@ describe("highlightCode", () => {
|
|||||||
expect(stringToken?.style).toBe("string");
|
expect(stringToken?.style).toBe("string");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("highlights Svelte components across script, markup, and style", () => {
|
||||||
|
const code = [
|
||||||
|
'<script lang="ts">',
|
||||||
|
" let count: number = 1;",
|
||||||
|
"</script>",
|
||||||
|
"",
|
||||||
|
"{#if count > 0}",
|
||||||
|
' <span class="badge">{count}</span>',
|
||||||
|
"{/if}",
|
||||||
|
"",
|
||||||
|
"<style>",
|
||||||
|
" .badge {",
|
||||||
|
" color: red;",
|
||||||
|
" }",
|
||||||
|
"</style>",
|
||||||
|
].join("\n");
|
||||||
|
const result = highlightCode(code, "Counter.svelte");
|
||||||
|
|
||||||
|
expect(result[1].find((t) => t.text === "let")?.style).toBe("keyword");
|
||||||
|
expect(result[1].find((t) => t.text === "number")?.style).toBe("type");
|
||||||
|
|
||||||
|
expect(result[4].find((t) => t.text === "if")?.style).toBe("keyword");
|
||||||
|
expect(result[4].find((t) => t.text === "count")?.style).toBe("variable");
|
||||||
|
|
||||||
|
expect(result[5].find((t) => t.text === "span")?.style).toBe("tag");
|
||||||
|
expect(result[5].find((t) => t.text === "class")?.style).toBe("attribute");
|
||||||
|
expect(result[5].find((t) => t.text === "count")?.style).toBe("variable");
|
||||||
|
|
||||||
|
expect(result[9].find((t) => t.text === "badge")?.style).toBe("class");
|
||||||
|
expect(result[10].find((t) => t.text === "color")?.style).toBe("property");
|
||||||
|
});
|
||||||
|
|
||||||
it("highlights TSX code with correct dialect", () => {
|
it("highlights TSX code with correct dialect", () => {
|
||||||
const code = 'const el = <div className="test">hello</div>;';
|
const code = 'const el = <div className="test">hello</div>;';
|
||||||
const result = highlightCode(code, "test.tsx");
|
const result = highlightCode(code, "test.tsx");
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ describe("isLanguageSupported", () => {
|
|||||||
expect(isLanguageSupported("test.cs")).toBe(true);
|
expect(isLanguageSupported("test.cs")).toBe(true);
|
||||||
expect(isLanguageSupported("test.nix")).toBe(true);
|
expect(isLanguageSupported("test.nix")).toBe(true);
|
||||||
expect(isLanguageSupported("test.ex")).toBe(true);
|
expect(isLanguageSupported("test.ex")).toBe(true);
|
||||||
|
expect(isLanguageSupported("Counter.svelte")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns false for unsupported file extensions", () => {
|
it("returns false for unsupported file extensions", () => {
|
||||||
@@ -63,6 +64,7 @@ describe("getSupportedExtensions", () => {
|
|||||||
expect(extensions).toContain("cs");
|
expect(extensions).toContain("cs");
|
||||||
expect(extensions).toContain("nix");
|
expect(extensions).toContain("nix");
|
||||||
expect(extensions).toContain("json");
|
expect(extensions).toContain("json");
|
||||||
|
expect(extensions).toContain("svelte");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { parser as xmlParser } from "@lezer/xml";
|
|||||||
import { parser as yamlParser } from "@lezer/yaml";
|
import { parser as yamlParser } from "@lezer/yaml";
|
||||||
import { csharpLanguage } from "@replit/codemirror-lang-csharp";
|
import { csharpLanguage } from "@replit/codemirror-lang-csharp";
|
||||||
import { nixLanguage } from "@replit/codemirror-lang-nix";
|
import { nixLanguage } from "@replit/codemirror-lang-nix";
|
||||||
|
import { svelteLanguage } from "@replit/codemirror-lang-svelte";
|
||||||
import { parser as elixirParser } from "lezer-elixir";
|
import { parser as elixirParser } from "lezer-elixir";
|
||||||
import type { Parser } from "@lezer/common";
|
import type { Parser } from "@lezer/common";
|
||||||
|
|
||||||
@@ -49,6 +50,8 @@ const languagesByExtension: Record<string, Language> = {
|
|||||||
// HTML
|
// HTML
|
||||||
html: language(htmlParser),
|
html: language(htmlParser),
|
||||||
htm: language(htmlParser),
|
htm: language(htmlParser),
|
||||||
|
// Svelte
|
||||||
|
svelte: svelteLanguage,
|
||||||
// XML
|
// XML
|
||||||
xml: language(xmlParser),
|
xml: language(xmlParser),
|
||||||
// Java
|
// Java
|
||||||
|
|||||||
@@ -160,6 +160,21 @@ index 1234567..abcdefg 100644
|
|||||||
</config>
|
</config>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const SVELTE_DIFF = `diff --git a/Counter.svelte b/Counter.svelte
|
||||||
|
index 1234567..abcdefg 100644
|
||||||
|
--- a/Counter.svelte
|
||||||
|
+++ b/Counter.svelte
|
||||||
|
@@ -1,7 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
- let count: number = 1;
|
||||||
|
+ let count: number = 2;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if count > 0}
|
||||||
|
<span class="badge">{count}</span>
|
||||||
|
{/if}
|
||||||
|
`;
|
||||||
|
|
||||||
describe("parseDiff", () => {
|
describe("parseDiff", () => {
|
||||||
it("parses a simple diff with one hunk", () => {
|
it("parses a simple diff with one hunk", () => {
|
||||||
const files = parseDiff(SIMPLE_DIFF);
|
const files = parseDiff(SIMPLE_DIFF);
|
||||||
@@ -471,6 +486,33 @@ describe("highlightDiffFromHunks", () => {
|
|||||||
expect(addedLine!.tokens!.some((t) => t.text === "2" && t.style === "number")).toBe(true);
|
expect(addedLine!.tokens!.some((t) => t.text === "2" && t.style === "number")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds syntax highlighting tokens to Svelte components", () => {
|
||||||
|
const files = parseDiff(SVELTE_DIFF);
|
||||||
|
const highlighted = highlightDiffFromHunks(files[0]);
|
||||||
|
const hunk = highlighted.hunks[0];
|
||||||
|
|
||||||
|
const scriptLine = hunk.lines[1];
|
||||||
|
expect(scriptLine.tokens).toBeDefined();
|
||||||
|
expect(scriptLine.tokens!.some((t) => t.text === "script" && t.style === "tag")).toBe(true);
|
||||||
|
|
||||||
|
const addedLine = hunk.lines.find(
|
||||||
|
(line) => line.type === "add" && line.content.includes("count"),
|
||||||
|
);
|
||||||
|
expect(addedLine?.tokens).toBeDefined();
|
||||||
|
expect(addedLine!.tokens!.some((t) => t.text === "let" && t.style === "keyword")).toBe(true);
|
||||||
|
expect(addedLine!.tokens!.some((t) => t.text === "number" && t.style === "type")).toBe(true);
|
||||||
|
expect(addedLine!.tokens!.some((t) => t.text === "2" && t.style === "number")).toBe(true);
|
||||||
|
|
||||||
|
const blockLine = hunk.lines.find((line) => line.content.includes("{#if"));
|
||||||
|
expect(blockLine?.tokens).toBeDefined();
|
||||||
|
expect(blockLine!.tokens!.some((t) => t.text === "if" && t.style === "keyword")).toBe(true);
|
||||||
|
expect(blockLine!.tokens!.some((t) => t.text === "count" && t.style === "variable")).toBe(true);
|
||||||
|
|
||||||
|
const markupLine = hunk.lines.find((line) => line.content.includes("<span"));
|
||||||
|
expect(markupLine?.tokens).toBeDefined();
|
||||||
|
expect(markupLine!.tokens!.some((t) => t.text === "span" && t.style === "tag")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("adds syntax highlighting tokens to Objective-C file extensions", () => {
|
it("adds syntax highlighting tokens to Objective-C file extensions", () => {
|
||||||
const files = parseDiff(OBJECTIVE_C_DIFF);
|
const files = parseDiff(OBJECTIVE_C_DIFF);
|
||||||
const highlighted = highlightDiffFromHunks(files[0]);
|
const highlighted = highlightDiffFromHunks(files[0]);
|
||||||
|
|||||||
Reference in New Issue
Block a user