From 6a6869ddae3eb863bfaa373c6d1e22360e096cf3 Mon Sep 17 00:00:00 2001 From: RealDewKJ Date: Fri, 9 Oct 2026 18:16:51 +0700 Subject: [PATCH 1/3] Highlight Vue, Svelte, SCSS, Sass and Less files Map .vue, .svelte, .scss, .sass and .less to their CodeMirror languages in languageForPath so the file editor and diff view color them instead of showing plain text. Closes #695 --- package-lock.json | 74 +++++++++++++++++++ package.json | 4 + .../files/editor/editorLanguage.test.ts | 13 ++++ src/features/files/editor/editorLanguage.ts | 16 ++++ .../files/editor/syntaxTokens.test.ts | 22 ++++++ 5 files changed, 129 insertions(+) diff --git a/package-lock.json b/package-lock.json index f6c2856c10..fe9f38c149 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,14 @@ "@codemirror/lang-java": "^6.0.2", "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-less": "^6.0.2", "@codemirror/lang-markdown": "^6.5.2", "@codemirror/lang-php": "^6.0.2", "@codemirror/lang-python": "^6.2.1", "@codemirror/lang-rust": "^6.0.2", + "@codemirror/lang-sass": "^6.0.2", "@codemirror/lang-sql": "^6.10.0", + "@codemirror/lang-vue": "^0.1.3", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.3", "@codemirror/legacy-modes": "^6.5.4", @@ -28,6 +31,7 @@ "@hugeicons/core-free-icons": "^4.3.0", "@hugeicons/react": "^1.1.10", "@lezer/highlight": "^1.2.3", + "@replit/codemirror-lang-svelte": "^6.0.0", "@streamdown/code": "^1.1.1", "@streamdown/mermaid": "^1.0.2", "@tauri-apps/api": "^2.7.0", @@ -470,6 +474,19 @@ "@lezer/json": "^1.0.0" } }, + "node_modules/@codemirror/lang-less": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-less/-/lang-less-6.0.2.tgz", + "integrity": "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@codemirror/lang-markdown": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.2.tgz", @@ -521,6 +538,19 @@ "@lezer/rust": "^1.0.0" } }, + "node_modules/@codemirror/lang-sass": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sass/-/lang-sass-6.0.2.tgz", + "integrity": "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/sass": "^1.0.0" + } + }, "node_modules/@codemirror/lang-sql": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz", @@ -535,6 +565,20 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@codemirror/lang-vue": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-vue/-/lang-vue-0.1.3.tgz", + "integrity": "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.1" + } + }, "node_modules/@codemirror/lang-xml": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", @@ -1300,6 +1344,17 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@lezer/sass": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lezer/sass/-/sass-1.1.0.tgz", + "integrity": "sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@lezer/xml": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", @@ -1370,6 +1425,25 @@ "node": ">=20" } }, + "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/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", diff --git a/package.json b/package.json index 4fc8ebc8ff..5565839695 100644 --- a/package.json +++ b/package.json @@ -35,11 +35,14 @@ "@codemirror/lang-java": "^6.0.2", "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-less": "^6.0.2", "@codemirror/lang-markdown": "^6.5.2", "@codemirror/lang-php": "^6.0.2", "@codemirror/lang-python": "^6.2.1", "@codemirror/lang-rust": "^6.0.2", + "@codemirror/lang-sass": "^6.0.2", "@codemirror/lang-sql": "^6.10.0", + "@codemirror/lang-vue": "^0.1.3", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.3", "@codemirror/legacy-modes": "^6.5.4", @@ -49,6 +52,7 @@ "@hugeicons/core-free-icons": "^4.3.0", "@hugeicons/react": "^1.1.10", "@lezer/highlight": "^1.2.3", + "@replit/codemirror-lang-svelte": "^6.0.0", "@streamdown/code": "^1.1.1", "@streamdown/mermaid": "^1.0.2", "@tauri-apps/api": "^2.7.0", diff --git a/src/features/files/editor/editorLanguage.test.ts b/src/features/files/editor/editorLanguage.test.ts index 289e7c64c3..e1db146ec2 100644 --- a/src/features/files/editor/editorLanguage.test.ts +++ b/src/features/files/editor/editorLanguage.test.ts @@ -51,10 +51,23 @@ describe("languageForPath", () => { "messages.proto", "Dockerfile", "settings.jsonc", + "Counter.svelte", ])("loads highlighting for additional mainstream file %s", async (path) => { await expect(languageForPath(path)).resolves.not.toBeNull(); }); + it.each([ + ["App.vue", "vue"], + ["theme.scss", "sass"], + ["theme.sass", "sass"], + ["theme.less", "less"], + ])("maps component and stylesheet file %s to %s", async (path, name) => { + const extension = await languageForPath(path); + + expect(extension).not.toBeNull(); + expect(languageName(extension!)).toBe(name); + }); + it("leaves unknown file types as plain text", async () => { await expect(languageForPath("notes.unknown")).resolves.toBeNull(); }); diff --git a/src/features/files/editor/editorLanguage.ts b/src/features/files/editor/editorLanguage.ts index f5c1dd718e..e777097018 100644 --- a/src/features/files/editor/editorLanguage.ts +++ b/src/features/files/editor/editorLanguage.ts @@ -167,10 +167,26 @@ export async function languageForPath(path: string): Promise { const { css } = await import("@codemirror/lang-css"); return css(); } + if ([".scss", ".sass"].includes(extension)) { + const { sass } = await import("@codemirror/lang-sass"); + return sass({ indented: extension === ".sass" }); + } + if (extension === ".less") { + const { less } = await import("@codemirror/lang-less"); + return less(); + } if ([".html", ".htm"].includes(extension)) { const { html } = await import("@codemirror/lang-html"); return html(); } + if (extension === ".vue") { + const { vue } = await import("@codemirror/lang-vue"); + return vue(); + } + if (extension === ".svelte") { + const { svelte } = await import("@replit/codemirror-lang-svelte"); + return svelte(); + } if ([".md", ".mdx", ".markdown"].includes(extension)) { const { markdown } = await import("@codemirror/lang-markdown"); return markdown(); diff --git a/src/features/files/editor/syntaxTokens.test.ts b/src/features/files/editor/syntaxTokens.test.ts index 0de01b5df0..fbadcb3c90 100644 --- a/src/features/files/editor/syntaxTokens.test.ts +++ b/src/features/files/editor/syntaxTokens.test.ts @@ -29,6 +29,28 @@ describe("highlightSource", () => { expect(token(lines[1], "// note")?.color).toBe(COMMENT_DARK); }); + it("colors script blocks in Vue and Svelte components", async () => { + for (const path of ["App.vue", "Counter.svelte"]) { + const language = await languageForPath(path); + const lines = highlightSource( + '', + language, + "dark", + ); + expect(token(lines[1], "const")?.color).toBe(KEYWORD_DARK); + expect(token(lines[1], '"agent"')?.color).toBe(STRING_DARK); + } + }); + + it.each(["theme.scss", "theme.less"])( + "colors comments in %s stylesheets", + async (path) => { + const language = await languageForPath(path); + const lines = highlightSource("// note\na { color: red; }", language, "dark"); + expect(token(lines[0], "// note")?.color).toBe(COMMENT_DARK); + }, + ); + it("leaves unknown languages unstyled", () => { const lines = highlightSource("plain text", null, "dark"); expect(lines).toEqual([[{ text: "plain text" }]]); From adc1de586448451b6fcd8a20a3e8e26c8b735b37 Mon Sep 17 00:00:00 2001 From: RealDewKJ Date: Fri, 9 Oct 2026 18:28:07 +0700 Subject: [PATCH 2/3] Test indented Sass declaration highlighting --- src/features/files/editor/syntaxTokens.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/features/files/editor/syntaxTokens.test.ts b/src/features/files/editor/syntaxTokens.test.ts index fbadcb3c90..0275fb5b94 100644 --- a/src/features/files/editor/syntaxTokens.test.ts +++ b/src/features/files/editor/syntaxTokens.test.ts @@ -10,6 +10,7 @@ import { const KEYWORD_DARK = "#ff8ffd"; const STRING_DARK = "#b4fa72"; const COMMENT_DARK = "#fefdc2"; +const PROPERTY_DARK = "#d0d1fe"; describe("highlightSource", () => { it("colors TypeScript keywords, strings, and comments", () => { @@ -51,6 +52,12 @@ describe("highlightSource", () => { }, ); + it("colors declarations in indented Sass stylesheets", async () => { + const language = await languageForPath("theme.sass"); + const lines = highlightSource(".card\n color: red", language, "dark"); + expect(token(lines[1], "color")?.color).toBe(PROPERTY_DARK); + }); + it("leaves unknown languages unstyled", () => { const lines = highlightSource("plain text", null, "dark"); expect(lines).toEqual([[{ text: "plain text" }]]); From 306046cf7f1e7e5ae0d541cda83925dc63f029b6 Mon Sep 17 00:00:00 2001 From: Nick <60738984+hardbeat920@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:02:44 +0100 Subject: [PATCH 3/3] Resolve highlighting test conflicts with main Reorder the added component and stylesheet tests so the dotenv tests on main can merge without overlapping insertions. Preserve all highlighting behavior and existing test assertions. --- .../files/editor/editorLanguage.test.ts | 24 ++++++------- .../files/editor/syntaxTokens.test.ts | 34 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/features/files/editor/editorLanguage.test.ts b/src/features/files/editor/editorLanguage.test.ts index e1db146ec2..68c56dda79 100644 --- a/src/features/files/editor/editorLanguage.test.ts +++ b/src/features/files/editor/editorLanguage.test.ts @@ -11,6 +11,18 @@ function languageName(extension: Extension): string | null { } describe("languageForPath", () => { + it.each([ + ["App.vue", "vue"], + ["theme.scss", "sass"], + ["theme.sass", "sass"], + ["theme.less", "less"], + ])("maps component and stylesheet file %s to %s", async (path, name) => { + const extension = await languageForPath(path); + + expect(extension).not.toBeNull(); + expect(languageName(extension!)).toBe(name); + }); + it.each([ "index.php", "Program.cs", @@ -56,18 +68,6 @@ describe("languageForPath", () => { await expect(languageForPath(path)).resolves.not.toBeNull(); }); - it.each([ - ["App.vue", "vue"], - ["theme.scss", "sass"], - ["theme.sass", "sass"], - ["theme.less", "less"], - ])("maps component and stylesheet file %s to %s", async (path, name) => { - const extension = await languageForPath(path); - - expect(extension).not.toBeNull(); - expect(languageName(extension!)).toBe(name); - }); - it("leaves unknown file types as plain text", async () => { await expect(languageForPath("notes.unknown")).resolves.toBeNull(); }); diff --git a/src/features/files/editor/syntaxTokens.test.ts b/src/features/files/editor/syntaxTokens.test.ts index 0275fb5b94..b1dabdb8e7 100644 --- a/src/features/files/editor/syntaxTokens.test.ts +++ b/src/features/files/editor/syntaxTokens.test.ts @@ -13,23 +13,6 @@ const COMMENT_DARK = "#fefdc2"; const PROPERTY_DARK = "#d0d1fe"; describe("highlightSource", () => { - it("colors TypeScript keywords, strings, and comments", () => { - const lines = highlightSource( - 'const name = "agent";\n// note', - javascript({ typescript: true }), - "dark", - ); - expect(token(lines[0], "const")?.color).toBe(KEYWORD_DARK); - expect(token(lines[0], '"agent"')?.color).toBe(STRING_DARK); - expect(token(lines[1], "// note")?.color).toBe(COMMENT_DARK); - }); - - it("colors comments in JSONC files", async () => { - const language = await languageForPath("settings.jsonc"); - const lines = highlightSource('{\n // note\n "a": 1\n}', language, "dark"); - expect(token(lines[1], "// note")?.color).toBe(COMMENT_DARK); - }); - it("colors script blocks in Vue and Svelte components", async () => { for (const path of ["App.vue", "Counter.svelte"]) { const language = await languageForPath(path); @@ -58,6 +41,23 @@ describe("highlightSource", () => { expect(token(lines[1], "color")?.color).toBe(PROPERTY_DARK); }); + it("colors TypeScript keywords, strings, and comments", () => { + const lines = highlightSource( + 'const name = "agent";\n// note', + javascript({ typescript: true }), + "dark", + ); + expect(token(lines[0], "const")?.color).toBe(KEYWORD_DARK); + expect(token(lines[0], '"agent"')?.color).toBe(STRING_DARK); + expect(token(lines[1], "// note")?.color).toBe(COMMENT_DARK); + }); + + it("colors comments in JSONC files", async () => { + const language = await languageForPath("settings.jsonc"); + const lines = highlightSource('{\n // note\n "a": 1\n}', language, "dark"); + expect(token(lines[1], "// note")?.color).toBe(COMMENT_DARK); + }); + it("leaves unknown languages unstyled", () => { const lines = highlightSource("plain text", null, "dark"); expect(lines).toEqual([[{ text: "plain text" }]]);