Skip to content

Commit 1e2a0f8

Browse files
authored
widen diff language aliases (#474)
1 parent 1b29190 commit 1e2a0f8

9 files changed

Lines changed: 111 additions & 27 deletions

apps/web/src/components/RightPanelHeader.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ const TABS: readonly {
1414
{ id: "diffs", label: "Diffs", icon: GitCompareIcon },
1515
];
1616

17-
export const RightPanelHeader = memo(function RightPanelHeader() {
17+
export const RightPanelHeader = memo(function RightPanelHeader(props: { hasDiffs: boolean }) {
1818
const activeTab = useRightPanelStore((s) => s.activeTab);
1919
const setActiveTab = useRightPanelStore((s) => s.setActiveTab);
2020
const close = useRightPanelStore((s) => s.close);
21+
const visibleTabs = props.hasDiffs ? TABS : TABS.filter((tab) => tab.id !== "diffs");
2122

2223
return (
2324
<div
@@ -27,7 +28,7 @@ export const RightPanelHeader = memo(function RightPanelHeader() {
2728
)}
2829
>
2930
<div className="flex items-center gap-0.5 [-webkit-app-region:no-drag]">
30-
{TABS.map((tab) => {
31+
{visibleTabs.map((tab) => {
3132
const Icon = tab.icon;
3233
const isActive = activeTab === tab.id;
3334
return (
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, expect, it } from "vitest";
2+
import type { FileDiffMetadata } from "@pierre/diffs/react";
3+
4+
import { resolveFileDiffLanguage } from "./pr-review-utils";
5+
6+
describe("resolveFileDiffLanguage", () => {
7+
it("normalizes common VS Code language ids into diff highlighter languages", () => {
8+
expect(resolveFileDiffLanguage({ lang: "typescriptreact" } as FileDiffMetadata)).toBe("tsx");
9+
expect(resolveFileDiffLanguage({ lang: "javascriptreact" } as FileDiffMetadata)).toBe("jsx");
10+
expect(resolveFileDiffLanguage({ lang: "manifest-yaml" } as FileDiffMetadata)).toBe("yaml");
11+
expect(resolveFileDiffLanguage({ lang: "esphome" } as FileDiffMetadata)).toBe("yaml");
12+
expect(resolveFileDiffLanguage({ lang: "django-html" } as FileDiffMetadata)).toBe("html");
13+
expect(resolveFileDiffLanguage({ lang: "cfmhtml" } as FileDiffMetadata)).toBe("html");
14+
expect(resolveFileDiffLanguage({ lang: "restructuredtext" } as FileDiffMetadata)).toBe("rst");
15+
expect(resolveFileDiffLanguage({ lang: "json-tmlanguage" } as FileDiffMetadata)).toBe("json");
16+
expect(resolveFileDiffLanguage({ lang: "plaintext" } as FileDiffMetadata)).toBe("text");
17+
expect(resolveFileDiffLanguage({ lang: "go.mod" } as FileDiffMetadata)).toBe("go");
18+
expect(resolveFileDiffLanguage({ lang: "swagger" } as FileDiffMetadata)).toBe("yaml");
19+
});
20+
21+
it("infers normalized languages from file paths", () => {
22+
expect(resolveFileDiffLanguage({ name: "checkbox.tsx" } as FileDiffMetadata)).toBe("tsx");
23+
expect(resolveFileDiffLanguage({ name: "entrypoint.sh" } as FileDiffMetadata)).toBe(
24+
"shellscript",
25+
);
26+
expect(resolveFileDiffLanguage({ name: "config.yml" } as FileDiffMetadata)).toBe("yaml");
27+
});
28+
});

apps/web/src/components/pr-review/pr-review-utils.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,6 @@ describe("withInferredFileDiffLanguage", () => {
3030
isPartial: true,
3131
} as never;
3232

33-
expect(withInferredFileDiffLanguage(fileDiff).lang).toBe("typescriptreact");
33+
expect(withInferredFileDiffLanguage(fileDiff).lang).toBe("tsx");
3434
});
3535
});

apps/web/src/components/pr-review/pr-review-utils.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from "~/lib/renderablePatch";
1818
import { ensureNativeApi } from "~/nativeApi";
1919
import { inferLanguageIdForPath } from "~/vscode-icons";
20+
import { normalizeLanguageIdForHighlighting } from "~/lib/languageIds";
2021

2122
export { parseRenderablePatch, resolveFileDiffPath, summarizeFileDiffStats };
2223

@@ -151,11 +152,11 @@ export function buildFileDiffRenderKey(fileDiff: FileDiffMetadata): string {
151152

152153
export function resolveFileDiffLanguage(fileDiff: FileDiffMetadata): SupportedLanguages | null {
153154
if (fileDiff.lang != null) {
154-
return fileDiff.lang;
155+
return normalizeLanguageIdForHighlighting(fileDiff.lang) as SupportedLanguages;
155156
}
156157
const path = resolveFileDiffPath(fileDiff);
157158
const languageId = inferLanguageIdForPath(path);
158-
return languageId ? (languageId as SupportedLanguages) : null;
159+
return languageId ? (normalizeLanguageIdForHighlighting(languageId) as SupportedLanguages) : null;
159160
}
160161

161162
export function withInferredFileDiffLanguage(fileDiff: FileDiffMetadata): FileDiffMetadata {

apps/web/src/lib/languageIds.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { bundledLanguages, bundledLanguagesAlias } from "shiki";
2+
3+
const LANGUAGE_ID_OVERRIDES: Record<string, string> = {
4+
"bun.lockb": "text",
5+
"code-text-binary": "text",
6+
cfmhtml: "html",
7+
csharp: "c#",
8+
"django-html": "html",
9+
esphome: "yaml",
10+
"go.mod": "go",
11+
"go.work": "go",
12+
"json-tmlanguage": "json",
13+
javascriptreact: "jsx",
14+
"manifest-yaml": "yaml",
15+
plaintext: "text",
16+
proto3: "protobuf",
17+
rmd: "md",
18+
restructuredtext: "rst",
19+
swagger: "yaml",
20+
typescriptreact: "tsx",
21+
"yaml-tmlanguage": "yaml",
22+
};
23+
24+
export function normalizeLanguageIdForHighlighting(languageId: string): string {
25+
const normalized = languageId.toLowerCase();
26+
if (
27+
Object.hasOwn(bundledLanguages, normalized) ||
28+
Object.hasOwn(bundledLanguagesAlias, normalized)
29+
) {
30+
return normalized;
31+
}
32+
return LANGUAGE_ID_OVERRIDES[normalized] ?? normalized;
33+
}

apps/web/src/lib/syntaxHighlighting.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,10 @@ import { DiffsHighlighter, getSharedHighlighter, SupportedLanguages } from "@pie
22

33
import { LRUCache } from "./lruCache";
44
import { fnv1a32, resolveDiffThemeName, type DiffThemeName } from "./diffRendering";
5+
import { normalizeLanguageIdForHighlighting } from "./languageIds";
56

67
const CODE_FENCE_LANGUAGE_REGEX = /(?:^|\s)language-([^\s]+)/;
78

8-
/**
9-
* Map VSCode language identifiers that don't match Shiki's bundled language names.
10-
* VSCode uses e.g. "typescriptreact" / "javascriptreact" while Shiki expects "tsx" / "jsx".
11-
*/
12-
const VSCODE_TO_SHIKI_LANG: Record<string, string> = {
13-
typescriptreact: "tsx",
14-
javascriptreact: "jsx",
15-
};
16-
17-
/** Normalise a language identifier so Shiki can resolve it. */
18-
function normalizeLanguage(language: string): string {
19-
return VSCODE_TO_SHIKI_LANG[language] ?? language;
20-
}
21-
229
const MAX_HIGHLIGHT_CACHE_ENTRIES = 500;
2310
const MAX_HIGHLIGHT_CACHE_MEMORY_BYTES = 50 * 1024 * 1024;
2411
const highlightedCodeCache = new LRUCache<string>(
@@ -76,7 +63,7 @@ export function setCachedHighlightedHtml(
7663
}
7764

7865
export function getHighlighterPromise(language: string): Promise<DiffsHighlighter> {
79-
const normalized = normalizeLanguage(language);
66+
const normalized = normalizeLanguageIdForHighlighting(language);
8067
const cached = highlighterPromiseCache.get(normalized);
8168
if (cached) return cached;
8269

@@ -104,7 +91,7 @@ export function renderHighlightedCodeHtml(
10491
language: string,
10592
themeName: DiffThemeName,
10693
): string {
107-
const normalized = normalizeLanguage(language);
94+
const normalized = normalizeLanguageIdForHighlighting(language);
10895
try {
10996
return highlighter.codeToHtml(code, { lang: normalized, theme: themeName });
11097
} catch (error) {

apps/web/src/rightPanelStore.test.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { describe, expect, it } from "vitest";
2-
import { normalizeRightPanelTab } from "./rightPanelStore";
1+
import { afterEach, describe, expect, it } from "vitest";
2+
import { useRightPanelStore, normalizeRightPanelTab } from "./rightPanelStore";
33

44
describe("normalizeRightPanelTab", () => {
55
it("maps legacy files and editor tabs into the workspace tab", () => {
@@ -14,3 +14,24 @@ describe("normalizeRightPanelTab", () => {
1414
expect(normalizeRightPanelTab(null)).toBeNull();
1515
});
1616
});
17+
18+
describe("useRightPanelStore setActiveTab", () => {
19+
afterEach(() => {
20+
useRightPanelStore.setState({
21+
isOpen: false,
22+
activeTab: "workspace",
23+
});
24+
});
25+
26+
it("can retarget the active tab without opening the panel", () => {
27+
useRightPanelStore.setState({
28+
isOpen: false,
29+
activeTab: "diffs",
30+
});
31+
32+
useRightPanelStore.getState().setActiveTab("workspace", false);
33+
34+
expect(useRightPanelStore.getState().activeTab).toBe("workspace");
35+
expect(useRightPanelStore.getState().isOpen).toBe(false);
36+
});
37+
});

apps/web/src/rightPanelStore.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ interface RightPanelState {
77
activeTab: RightPanelTab;
88
open: (tab?: RightPanelTab) => void;
99
close: () => void;
10-
setActiveTab: (tab: RightPanelTab) => void;
10+
setActiveTab: (tab: RightPanelTab, open?: boolean) => void;
1111
}
1212

1313
const STORAGE_KEY = "okcode:right-panel-tab:v1";
@@ -62,8 +62,8 @@ export const useRightPanelStore = create<RightPanelState>((set) => ({
6262

6363
close: () => set({ isOpen: false }),
6464

65-
setActiveTab: (tab) => {
65+
setActiveTab: (tab, open = true) => {
6666
persistTab(tab);
67-
set({ activeTab: tab, isOpen: true });
67+
set((state) => ({ activeTab: tab, isOpen: open ? true : state.isOpen }));
6868
},
6969
}));

apps/web/src/routes/_chat.$threadId.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ function ChatThreadRouteView() {
200200
const rightPanelTab = useRightPanelStore((s) => s.activeTab);
201201
const openRightPanel = useRightPanelStore((s) => s.open);
202202
const closeRightPanel = useRightPanelStore((s) => s.close);
203+
const setRightPanelTab = useRightPanelStore((s) => s.setActiveTab);
203204

204205
// ── Code viewer state ─────────────────────────────────────────────
205206
const codeViewerOpen = useCodeViewerStore((state) => state.isOpen);
@@ -223,6 +224,12 @@ function ChatThreadRouteView() {
223224
if (!project) return null;
224225
return thread?.worktreePath ?? draftThread?.worktreePath ?? project.cwd;
225226
});
227+
const hasThreadDiffs = useStore(
228+
(store) =>
229+
store.threads
230+
.find((t) => t.id === threadId)
231+
?.turnDiffSummaries.some((summary) => summary.files.length > 0) ?? false,
232+
);
226233

227234
// ── Keep-alive flags so lazy content doesn't unmount on tab switch ─
228235
const [hasOpenedSimulation, setHasOpenedSimulation] = useState(simulationOpen);
@@ -295,6 +302,12 @@ function ChatThreadRouteView() {
295302
}
296303
}, [diffViewerOpen, openRightPanel]);
297304

305+
useEffect(() => {
306+
if (!hasThreadDiffs && rightPanelTab === "diffs") {
307+
setRightPanelTab("workspace", false);
308+
}
309+
}, [hasThreadDiffs, rightPanelTab, setRightPanelTab]);
310+
298311
// ── Sync right panel close → close sub-panels ─────────────────────
299312
const prevRightPanelOpenRef = useRef(rightPanelOpen);
300313
useEffect(() => {
@@ -335,7 +348,7 @@ function ChatThreadRouteView() {
335348
// ── Right panel content (shared between desktop sidebar & mobile sheet) ──
336349
const rightPanelContent = (
337350
<div className="flex min-h-0 flex-1 flex-col bg-background">
338-
<RightPanelHeader />
351+
<RightPanelHeader hasDiffs={hasThreadDiffs} />
339352
<div className="relative flex-1 overflow-hidden">
340353
{rightPanelTab === "workspace" ? (
341354
<WorkspacePanel

0 commit comments

Comments
 (0)