Add CodeMirror file viewer highlighting

This commit is contained in:
Federico Jaramillo Martinez
2026-05-07 23:28:31 +02:00
parent ba8f74bee2
commit 9a80503b72
5 changed files with 472 additions and 3 deletions
+112
View File
@@ -0,0 +1,112 @@
import { EditorState, type Extension } from "@codemirror/state";
import { EditorView, keymap, lineNumbers } from "@codemirror/view";
import { defaultKeymap } from "@codemirror/commands";
import { syntaxHighlighting, defaultHighlightStyle } from "@codemirror/language";
import { javascript } from "@codemirror/lang-javascript";
import { json } from "@codemirror/lang-json";
import { markdown } from "@codemirror/lang-markdown";
import { css as cssLang } from "@codemirror/lang-css";
import { html as htmlLang } from "@codemirror/lang-html";
import { python } from "@codemirror/lang-python";
import { rust } from "@codemirror/lang-rust";
import { go } from "@codemirror/lang-go";
import { LitElement, css, html } from "lit";
import { customElement, property, query } from "lit/decorators.js";
@customElement("code-viewer")
export class CodeViewer extends LitElement {
@property() content = "";
@property() language: string | undefined;
@query(".host") private editorHost?: HTMLDivElement;
private view: EditorView | undefined;
override firstUpdated(): void {
this.recreateEditor();
}
override updated(changed: Map<string, unknown>): void {
if (changed.has("content") || changed.has("language")) this.recreateEditor();
}
override disconnectedCallback(): void {
this.view?.destroy();
this.view = undefined;
super.disconnectedCallback();
}
override render() {
return html`<div class="host"></div>`;
}
private recreateEditor(): void {
if (!this.editorHost) return;
this.view?.destroy();
this.view = new EditorView({
parent: this.editorHost,
state: EditorState.create({
doc: this.content,
extensions: [
lineNumbers(),
keymap.of(defaultKeymap),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
EditorState.readOnly.of(true),
EditorView.editable.of(false),
EditorView.lineWrapping,
viewerTheme,
...languageExtensions(this.language),
],
}),
});
}
static override styles = css`
:host { display: block; min-height: 0; height: 100%; }
.host { height: 100%; min-height: 0; overflow: auto; }
`;
}
const viewerTheme = EditorView.theme({
"&": {
height: "100%",
color: "#e6edf3",
backgroundColor: "#0d1117",
fontSize: "12px",
},
".cm-scroller": {
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
lineHeight: "1.45",
},
".cm-gutters": {
backgroundColor: "#0d1117",
color: "#6e7681",
borderRight: "1px solid #21262d",
},
".cm-activeLineGutter": {
backgroundColor: "transparent",
},
".cm-activeLine": {
backgroundColor: "transparent",
},
".cm-content": {
caretColor: "transparent",
},
"&.cm-focused": {
outline: "none",
},
});
function languageExtensions(language: string | undefined): Extension[] {
switch (language) {
case "typescript": return [javascript({ typescript: true })];
case "javascript": return [javascript()];
case "json": return [json()];
case "markdown": return [markdown()];
case "css": return [cssLang()];
case "html": return [htmlLang()];
case "python": return [python()];
case "rust": return [rust()];
case "go": return [go()];
default: return [];
}
}
+3 -2
View File
@@ -2,6 +2,7 @@ import { LitElement, html, type TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import type { FileContentResponse, FileTreeEntry, GitDiffResponse, GitStatusResponse, Workspace } from "../api";
import { workspacePanelStyles } from "./shared";
import "./CodeViewer";
@customElement("workspace-panel")
export class WorkspacePanel extends LitElement {
@@ -73,7 +74,7 @@ export class WorkspacePanel extends LitElement {
if (file.binary) return html`<p class="muted">Binary file: ${file.path}</p>`;
return html`
<div class="viewer-header"><strong>${file.path}</strong><small>${file.language ?? "text"}${file.truncated ? " · truncated" : ""}</small></div>
<pre><code>${file.content}</code></pre>
<code-viewer .content=${file.content} .language=${file.language}></code-viewer>
`;
}
@@ -110,7 +111,7 @@ export class WorkspacePanel extends LitElement {
if (!diff) return html`<p class="muted">Loading diff…</p>`;
return html`
<div class="viewer-header"><strong>${diff.path ?? "diff"}</strong><small>${diff.staged ? "staged" : "unstaged"}${diff.truncated ? " · truncated" : ""}</small></div>
<pre><code>${diff.diff || "No unstaged diff."}</code></pre>
<code-viewer .content=${diff.diff || "No unstaged diff."}></code-viewer>
`;
}
+2 -1
View File
@@ -71,9 +71,10 @@ export const workspacePanelStyles = css`
.row:hover, .row.selected { background: #0d2847; }
.row span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.summary { margin: 4px 6px 8px; color: #8b949e; }
.viewer { min-height: 0; overflow: auto; }
.viewer { min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
.viewer-header { position: sticky; top: 0; display: flex; justify-content: space-between; gap: 8px; padding: 8px; border-bottom: 1px solid #21262d; background: #0d1117; }
.viewer-header strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
code-viewer { flex: 1 1 auto; min-height: 0; }
pre { margin: 0; padding: 10px; overflow: auto; font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere; }
p { margin: 10px; }
`;