diff --git a/.changeset/odin-highlighting.md b/.changeset/odin-highlighting.md new file mode 100644 index 00000000..f19aa004 --- /dev/null +++ b/.changeset/odin-highlighting.md @@ -0,0 +1,5 @@ +--- +"hunkdiff": patch +--- + +Highlight Odin source files in diff views. diff --git a/bun.lock b/bun.lock index 89cb794c..f457285f 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "hunk", "dependencies": { "@pierre/diffs": "1.2.2", + "@shikijs/langs": "4.0.2", "bun": "^1.3.14", "chokidar": "^4.0.3", "commander": "^14.0.3", @@ -215,13 +216,13 @@ "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], - "@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], + "@shikijs/langs": ["@shikijs/langs@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg=="], "@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], "@shikijs/transformers": ["@shikijs/transformers@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="], - "@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + "@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="], "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], @@ -497,6 +498,16 @@ "@opentui/core/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "@shikijs/core/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + + "@shikijs/engine-javascript/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + + "@shikijs/engine-oniguruma/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + + "@shikijs/themes/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + + "@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + "cli-truncate/string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="], "ghostty-opentui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], @@ -511,6 +522,10 @@ "restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], + + "shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + "string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], diff --git a/package.json b/package.json index 2023fe6c..e32acab0 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ }, "dependencies": { "@pierre/diffs": "1.2.2", + "@shikijs/langs": "4.0.2", "bun": "^1.3.14", "chokidar": "^4.0.3", "commander": "^14.0.3", diff --git a/src/core/fileLanguage.test.ts b/src/core/fileLanguage.test.ts index 490fe41a..d30e5bf3 100644 --- a/src/core/fileLanguage.test.ts +++ b/src/core/fileLanguage.test.ts @@ -8,6 +8,11 @@ describe("custom file language registration", () => { expect(getFiletypeFromFileName("src/nested/foo.mts")).toBe("typescript"); }); + test("maps Odin source files to odin", () => { + expect(getFiletypeFromFileName("main.odin")).toBe("odin"); + expect(getFiletypeFromFileName("src/nested/main.odin")).toBe("odin"); + }); + test("preserves Pierre's built-in extension detection", () => { expect(getFiletypeFromFileName("foo.ts")).toBe("typescript"); expect(getFiletypeFromFileName("foo.tsx")).toBe("tsx"); diff --git a/src/core/fileLanguage.ts b/src/core/fileLanguage.ts index 25f1dd3a..e64106cc 100644 --- a/src/core/fileLanguage.ts +++ b/src/core/fileLanguage.ts @@ -1,10 +1,14 @@ import { getFiletypeFromFileName, + registerCustomLanguage, setCustomExtension, type SupportedLanguages, } from "@pierre/diffs"; -// Pierre omits these TypeScript extensions, so register them before lookups or rendering. +// Shiki 3 does not bundle Odin, so load its current TextMate grammar through Pierre's custom path. +registerCustomLanguage("odin", () => import("@shikijs/langs/odin"), ["odin"]); + +// Register extensions Pierre omits before performing language lookups or rendering. const HUNK_CUSTOM_EXTENSIONS: Record = { mts: "typescript", cts: "typescript", diff --git a/src/ui/diff/pierre.test.ts b/src/ui/diff/pierre.test.ts index 644df05f..22501762 100644 --- a/src/ui/diff/pierre.test.ts +++ b/src/ui/diff/pierre.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; import { parseDiffFromFile } from "@pierre/diffs"; +import "../../core/fileLanguage"; import type { DiffFile } from "../../core/types"; import { buildSplitRows, @@ -742,6 +743,43 @@ describe("Pierre diff rows", () => { ).toBe(true); }); + test("highlights Odin source with the registered custom grammar", async () => { + const metadata = parseDiffFromFile( + { + name: "main.odin", + contents: "package main\nmain :: proc() {}\n", + cacheKey: "odin-before", + }, + { + name: "main.odin", + contents: 'package main\nmain :: proc() {\n message := "hello"\n}\n', + cacheKey: "odin-after", + }, + { context: 3 }, + true, + ); + const file: DiffFile = { + id: "odin-syntax", + path: "main.odin", + patch: "", + language: "odin", + stats: { additions: 2, deletions: 1 }, + metadata, + agent: null, + }; + const theme = resolveTheme("github-dark-default", null); + const highlighted = await loadHighlightedDiff(file, theme); + const spans = buildStackRows(file, highlighted, theme) + .filter( + (row): row is Extract => + row.type === "stack-line" && row.cell.kind === "addition", + ) + .flatMap((row) => row.cell.spans); + + expect(spans.find((span) => span.text === " :: proc")?.fg).toBeDefined(); + expect(spans.find((span) => span.text.includes('"hello"'))?.fg).toBeDefined(); + }); + test("uses Shiki's bundled Catppuccin theme for Catppuccin syntax", async () => { const metadata = parseDiffFromFile( { name: "syntax.ts", contents: "const a = 1;\n", cacheKey: "catppuccin-before" },