generated from nhcarrigan/template
feat: fix git window and add pretty diff viewer (#178)
## Summary - **Fix git window "Not a git repository" error** — The working directory received from Claude Code is a WSL Linux path (e.g. `/home/naomi/...`), but git commands were being run as native Windows processes with `.current_dir()`. Windows can't resolve WSL paths, causing `git rev-parse --git-dir` to fail. Fixed by routing git commands through `wsl -- git -C <path>` when the working directory starts with `/`. - **Add syntax highlighting and line numbers to diff view** — Replaced the raw `<pre>` block with a proper `DiffViewer` component featuring: - Old/new line number columns with correct tracking across hunks - Colour-coded gutter (`+`/`-`) with green/red row backgrounds - Syntax highlighting via `highlight.js` using the detected file language, respecting all app themes via `--hljs-*` CSS variables - Styled hunk headers and file headers ## New files - `src/lib/utils/diffParser.ts` — pure diff parsing logic - `src/lib/utils/diffParser.test.ts` — 30 tests covering all line types, line number tracking, and language detection - `src/lib/components/DiffViewer.svelte` — the pretty diff viewer component ✨ This pull request was created with help from Hikari~ 🌸 Reviewed-on: #178 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #178.
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
<script lang="ts">
|
||||
import hljs from "highlight.js";
|
||||
import { parseDiff, detectLanguage } from "$lib/utils/diffParser";
|
||||
|
||||
export let diffContent: string;
|
||||
export let filePath: string;
|
||||
|
||||
$: lines = diffContent ? parseDiff(diffContent) : [];
|
||||
$: language = detectLanguage(filePath);
|
||||
|
||||
function highlightCode(code: string): string {
|
||||
if (!code) return "";
|
||||
try {
|
||||
return hljs.highlight(code, { language }).value;
|
||||
} catch {
|
||||
return hljs.highlightAuto(code).value;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if lines.length === 0}
|
||||
<div class="empty-diff">No changes</div>
|
||||
{:else}
|
||||
<table class="diff-table">
|
||||
<tbody>
|
||||
{#each lines as line, i (i)}
|
||||
{#if line.type === "file-header"}
|
||||
<tr class="line-file-header">
|
||||
<td class="line-num" colspan="2"></td>
|
||||
<td class="line-gutter"></td>
|
||||
<td class="line-code">{line.content}</td>
|
||||
</tr>
|
||||
{:else if line.type === "hunk-header"}
|
||||
<tr class="line-hunk-header">
|
||||
<td class="line-num" colspan="2"></td>
|
||||
<td class="line-gutter">⋯</td>
|
||||
<td class="line-code">{line.content}</td>
|
||||
</tr>
|
||||
{:else if line.type === "no-newline"}
|
||||
<tr class="line-no-newline">
|
||||
<td class="line-num" colspan="2"></td>
|
||||
<td class="line-gutter"></td>
|
||||
<td class="line-code">{line.content}</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr class="line-{line.type}">
|
||||
<td class="line-num">{line.oldLineNumber ?? ""}</td>
|
||||
<td class="line-num">{line.newLineNumber ?? ""}</td>
|
||||
<td class="line-gutter">
|
||||
{line.type === "added" ? "+" : line.type === "removed" ? "-" : ""}
|
||||
</td>
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -- Syntax highlighting requires @html; content is from trusted git diff output -->
|
||||
<td class="line-code">{@html highlightCode(line.content)}</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.empty-diff {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.diff-table {
|
||||
border-collapse: collapse;
|
||||
min-width: 100%;
|
||||
width: max-content;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.diff-table tr {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.diff-table td {
|
||||
padding: 0;
|
||||
white-space: pre;
|
||||
vertical-align: top;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.line-num {
|
||||
width: 3.5rem;
|
||||
min-width: 3.5rem;
|
||||
color: var(--text-secondary);
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
border-right: 1px solid var(--border-color);
|
||||
opacity: 0.6;
|
||||
font-size: 0.75rem;
|
||||
padding: 0 0.4rem;
|
||||
}
|
||||
|
||||
.line-gutter {
|
||||
width: 1.5rem;
|
||||
min-width: 1.5rem;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
font-weight: bold;
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
.line-code {
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
|
||||
/* Added lines */
|
||||
.line-added {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.line-added .line-num {
|
||||
background: rgba(34, 197, 94, 0.08);
|
||||
color: rgba(34, 197, 94, 0.7);
|
||||
}
|
||||
|
||||
.line-added .line-gutter {
|
||||
color: #22c55e;
|
||||
background: rgba(34, 197, 94, 0.18);
|
||||
}
|
||||
|
||||
/* Removed lines */
|
||||
.line-removed {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.line-removed .line-num {
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
color: rgba(239, 68, 68, 0.7);
|
||||
}
|
||||
|
||||
.line-removed .line-gutter {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.18);
|
||||
}
|
||||
|
||||
/* Hunk header */
|
||||
.line-hunk-header {
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
}
|
||||
|
||||
.line-hunk-header .line-code {
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.line-hunk-header .line-gutter {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* File header */
|
||||
.line-file-header {
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.line-file-header .line-code {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
padding: 0.15rem 0.75rem;
|
||||
}
|
||||
|
||||
/* No newline */
|
||||
.line-no-newline .line-code {
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Syntax highlighting — scoped to this component's table */
|
||||
.diff-table :global(.hljs-keyword),
|
||||
.diff-table :global(.hljs-selector-tag),
|
||||
.diff-table :global(.hljs-built_in),
|
||||
.diff-table :global(.hljs-name) {
|
||||
color: var(--hljs-keyword);
|
||||
}
|
||||
|
||||
.diff-table :global(.hljs-string),
|
||||
.diff-table :global(.hljs-attr),
|
||||
.diff-table :global(.hljs-symbol),
|
||||
.diff-table :global(.hljs-bullet) {
|
||||
color: var(--hljs-string);
|
||||
}
|
||||
|
||||
.diff-table :global(.hljs-number),
|
||||
.diff-table :global(.hljs-literal) {
|
||||
color: var(--hljs-number);
|
||||
}
|
||||
|
||||
.diff-table :global(.hljs-comment),
|
||||
.diff-table :global(.hljs-quote) {
|
||||
color: var(--hljs-comment);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.diff-table :global(.hljs-function),
|
||||
.diff-table :global(.hljs-title) {
|
||||
color: var(--hljs-function);
|
||||
}
|
||||
|
||||
.diff-table :global(.hljs-type),
|
||||
.diff-table :global(.hljs-class) {
|
||||
color: var(--hljs-type);
|
||||
}
|
||||
|
||||
.diff-table :global(.hljs-variable),
|
||||
.diff-table :global(.hljs-template-variable) {
|
||||
color: var(--hljs-variable);
|
||||
}
|
||||
|
||||
.diff-table :global(.hljs-meta) {
|
||||
color: var(--hljs-meta);
|
||||
}
|
||||
|
||||
.diff-table :global(.hljs-tag) {
|
||||
color: var(--hljs-keyword);
|
||||
}
|
||||
|
||||
.diff-table :global(.hljs-attribute) {
|
||||
color: var(--hljs-function);
|
||||
}
|
||||
|
||||
.diff-table :global(.hljs-params) {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user