Download apps/desktop/src/components/chat/diff-lines.tsx from SaylorTwift/hermes-agent: direct link, hf CLI and curl.
- Browser
- Download file 22.2 kB
-
https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/apps/desktop/src/components/chat/diff-lines.tsx
- Command line
-
hf download hf://SaylorTwift/hermes-agent/apps/desktop/src/components/chat/diff-lines.tsx
-
curl -L -o diff-lines.tsx https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/apps/desktop/src/components/chat/diff-lines.tsx
22.2 kB
| 'use client' | |
| import * as React from 'react' | |
| import type { BundledLanguage, ShikiTransformer, ThemedToken } from 'shiki' | |
| import { chunkLines, type LineChunk, useFixedRowWindow } from '@/components/chat/fixed-row-window' | |
| import { exceedsHighlightBudget, SHIKI_THEME } from '@/components/chat/shiki-highlighter' | |
| import { ErrorBoundary } from '@/components/error-boundary' | |
| import { shikiLanguageForFilename } from '@/lib/markdown-code' | |
| import { cn } from '@/lib/utils' | |
| /** | |
| * Renders a unified diff for a tool's file edit. Two paths share one parse: | |
| * - `SyntaxDiff` highlights the change *content* in the file's language via | |
| * Shiki, then a per-line transformer paints the add/remove tint on top. | |
| * - `DiffLines` is the color-only fallback (no language, over budget, or while | |
| * Shiki loads). | |
| * Both drop git file-headers + `@@` hunk noise and the `+/-` gutter so changes | |
| * read by color + a 2px gutter accent, the way Cursor does. | |
| */ | |
| type DiffKind = 'add' | 'context' | 'remove' | |
| export interface DiffLine { | |
| kind: DiffKind | |
| text: string | |
| /** 1-based line number in the old/new file (absent on the "other" side of an | |
| * add/remove, and on hunk-separator blanks). Only used when line numbers are | |
| * shown (the preview's full diff). */ | |
| newNo?: number | |
| oldNo?: number | |
| } | |
| interface ParsedHunk { | |
| lines: Array<{ kind: DiffKind; text: string }> | |
| newStart: number | |
| oldStart: number | |
| } | |
| // Tint + 2px gutter accent per change kind. Text color is included for the | |
| // plain renderer; the Shiki path omits it so syntax colors win, layering only | |
| // the background + border. | |
| const DIFF_KIND_TINT: Record<DiffKind, string> = { | |
| add: 'border-(--ui-diff-add-border) bg-(--ui-diff-add-background)', | |
| context: 'border-transparent', | |
| remove: 'border-(--ui-diff-remove-border) bg-(--ui-diff-remove-background)' | |
| } | |
| const DIFF_KIND_TEXT: Record<DiffKind, string> = { | |
| add: 'text-(--ui-diff-add-foreground)', | |
| context: '', | |
| remove: 'text-(--ui-diff-remove-foreground)' | |
| } | |
| const DIFF_LINE_BASE = 'block min-w-max whitespace-pre border-l-2 px-2.5 py-px' | |
| const PREVIEW_DIFF_LINE_BASE = 'block h-5 min-w-max whitespace-pre px-2.5 leading-5' | |
| const PREVIEW_CHUNK_LINES = 200 | |
| const PREVIEW_LINE_PX = 20 | |
| const PREVIEW_OVERSCAN_LINES = 400 | |
| // Bleed out of the tool-card body's `p-1.5` so tints/borders run flush to the | |
| // card edges (rounded corners clip via the card's overflow); compact height | |
| // with internal scroll like a code block. | |
| // `overscroll-y-auto` so reaching the box's top/bottom hands the wheel back to | |
| // the page (no scroll-trap); `overscroll-x-contain` keeps a trackpad's sideways | |
| // overscroll on long code lines from firing browser back/forward navigation. | |
| const DIFF_BOX_CLASS = | |
| '-mx-1.5 -mb-1.5 max-h-[12rem] max-w-none min-w-0 overflow-auto overscroll-x-contain overscroll-y-auto font-mono text-[0.7rem] leading-relaxed text-(--ui-text-secondary)' | |
| function diffKind(line: string): DiffKind { | |
| if (line.startsWith('+') && !line.startsWith('+++')) { | |
| return 'add' | |
| } | |
| if (line.startsWith('-') && !line.startsWith('---')) { | |
| return 'remove' | |
| } | |
| return 'context' | |
| } | |
| // Drop the leading +/-/space gutter so changes read by color alone, keeping the | |
| // rest of the indentation intact. | |
| function stripDiffMarker(line: string): string { | |
| if (diffKind(line) !== 'context' || line.startsWith(' ')) { | |
| return line.slice(1) | |
| } | |
| return line | |
| } | |
| // Git-style unified diffs arrive with a file-header preamble — `diff --git`, | |
| // `index …`, `--- a/path`, `+++ b/path`, and Hermes' own `a/path → b/path` | |
| // arrow line. That preamble just repeats the path (which the tool row already | |
| // shows) and reads especially badly for absolute paths (`a//Users/…`). Strip | |
| // the leading header zone up to the first hunk. | |
| const DIFF_HEADER_PREFIXES = [ | |
| 'diff --git', | |
| 'index ', | |
| '--- ', | |
| '+++ ', | |
| 'similarity ', | |
| 'rename ', | |
| 'new file', | |
| 'deleted file' | |
| ] | |
| function isArrowHeaderLine(line: string): boolean { | |
| const trimmed = line.trim() | |
| return trimmed.includes('→') && /^\S.*→\s*\S+$/.test(trimmed) && !/^[+\-@]/.test(trimmed) | |
| } | |
| /** Exported for tests. */ | |
| export function stripDiffFileHeaders(diff: string): string { | |
| const lines = diff.split('\n') | |
| let start = 0 | |
| for (; start < lines.length; start += 1) { | |
| const line = lines[start] | |
| if (line.startsWith('@@')) { | |
| break | |
| } | |
| if (line.trim() === '' || isArrowHeaderLine(line) || DIFF_HEADER_PREFIXES.some(prefix => line.startsWith(prefix))) { | |
| continue | |
| } | |
| break | |
| } | |
| return lines.slice(start).join('\n') | |
| } | |
| function parseHunks(diff: string): ParsedHunk[] { | |
| const hunks: ParsedHunk[] = [] | |
| let active: null | ParsedHunk = null | |
| for (const line of stripDiffFileHeaders(diff).split('\n')) { | |
| if (line.startsWith('@@')) { | |
| const match = /@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line) | |
| if (!match) { | |
| active = null | |
| continue | |
| } | |
| active = { oldStart: Number(match[1]), newStart: Number(match[2]), lines: [] } | |
| hunks.push(active) | |
| continue | |
| } | |
| if (!active || line.startsWith('\\')) { | |
| continue | |
| } | |
| active.lines.push({ kind: diffKind(line), text: stripDiffMarker(line) }) | |
| } | |
| return hunks | |
| } | |
| // Cleaned diff → renderable lines: file-headers + `@@` hunks dropped (a blank | |
| // separator kept between hunks), markers stripped, kind recorded. Old/new line | |
| // numbers are tracked from each `@@ -a,b +c,d @@` header so a caller that wants | |
| // a gutter (the preview) can render them; the blank separator carries none. | |
| function parseDiff(diff: string): DiffLine[] { | |
| const hunks = parseHunks(diff) | |
| if (hunks.length === 0) { | |
| // Fallback for unexpected non-hunk payloads. | |
| return stripDiffFileHeaders(diff) | |
| .split('\n') | |
| .map(line => ({ kind: diffKind(line), text: stripDiffMarker(line) })) | |
| } | |
| const out: DiffLine[] = [] | |
| let emitted = false | |
| let oldNo = 1 | |
| let newNo = 1 | |
| for (const hunk of hunks) { | |
| oldNo = hunk.oldStart | |
| newNo = hunk.newStart | |
| if (emitted) { | |
| out.push({ kind: 'context', text: '' }) | |
| } | |
| for (const line of hunk.lines) { | |
| const entry: DiffLine = { kind: line.kind, text: line.text } | |
| if (line.kind === 'add') { | |
| entry.newNo = newNo++ | |
| } else if (line.kind === 'remove') { | |
| entry.oldNo = oldNo++ | |
| } else { | |
| entry.oldNo = oldNo++ | |
| entry.newNo = newNo++ | |
| } | |
| out.push(entry) | |
| emitted = true | |
| } | |
| } | |
| return out | |
| } | |
| // Build a full-file diff view anchored to the CURRENT file text. Every current | |
| // line is emitted from `fullText` with its real new-file line number; hunks only | |
| // mark those rows as added and insert deleted rows between them. That keeps the | |
| // preview's SOURCE and DIFF views on the same line map even when git returns | |
| // compact hunks or removed-only rows. | |
| function parseFullFileDiff(diff: string, fullText: string): DiffLine[] { | |
| const hunks = parseHunks(diff) | |
| const fullLines = fullText.split('\n') | |
| if (hunks.length === 0) { | |
| return fullLines.map((text, index) => ({ kind: 'context', newNo: index + 1, oldNo: index + 1, text })) | |
| } | |
| const added = new Set<number>() | |
| const oldNoByNewNo = new Map<number, number>() | |
| const removalsByNewNo = new Map<number, DiffLine[]>() | |
| const out: DiffLine[] = [] | |
| for (const hunk of hunks) { | |
| let oldNo = hunk.oldStart | |
| let newNo = hunk.newStart | |
| for (const line of hunk.lines) { | |
| if (line.kind === 'add') { | |
| added.add(newNo) | |
| newNo += 1 | |
| } else if (line.kind === 'remove') { | |
| const anchor = Math.max(1, Math.min(newNo, fullLines.length + 1)) | |
| const bucket = removalsByNewNo.get(anchor) ?? [] | |
| bucket.push({ kind: 'remove', oldNo, text: line.text }) | |
| removalsByNewNo.set(anchor, bucket) | |
| oldNo += 1 | |
| } else { | |
| oldNoByNewNo.set(newNo, oldNo) | |
| oldNo += 1 | |
| newNo += 1 | |
| } | |
| } | |
| } | |
| for (let index = 0; index < fullLines.length; index += 1) { | |
| const newNo = index + 1 | |
| const removals = removalsByNewNo.get(newNo) | |
| if (removals) { | |
| out.push(...removals) | |
| } | |
| out.push({ | |
| kind: added.has(newNo) ? 'add' : 'context', | |
| newNo, | |
| oldNo: oldNoByNewNo.get(newNo), | |
| text: fullLines[index] ?? '' | |
| }) | |
| } | |
| const trailingRemovals = removalsByNewNo.get(fullLines.length + 1) | |
| if (trailingRemovals) { | |
| out.push(...trailingRemovals) | |
| } | |
| return out | |
| } | |
| /** Exported for the lazily-loaded SyntaxDiff (syntax-diff.tsx). */ | |
| export function DiffBody({ lines, syntax }: { lines: DiffLine[]; syntax?: boolean }) { | |
| return ( | |
| <> | |
| {lines.map((line, index) => ( | |
| <span | |
| className={cn(DIFF_LINE_BASE, DIFF_KIND_TINT[line.kind], !syntax && DIFF_KIND_TEXT[line.kind])} | |
| key={`${index}-${line.text}`} | |
| > | |
| {line.text || ' '} | |
| </span> | |
| ))} | |
| </> | |
| ) | |
| } | |
| // shiki FontStyle is a bitmask: Italic=1, Bold=2, Underline=4. | |
| function tokenStyle({ bgColor, color, fontStyle = 0 }: ThemedToken): React.CSSProperties | undefined { | |
| if (!color && !bgColor && !fontStyle) { | |
| return undefined | |
| } | |
| return { | |
| backgroundColor: bgColor, | |
| color, | |
| fontStyle: fontStyle & 1 ? 'italic' : undefined, | |
| fontWeight: fontStyle & 2 ? 700 : undefined, | |
| textDecorationLine: fontStyle & 4 ? 'underline' : undefined | |
| } | |
| } | |
| function useThemeName() { | |
| const current = () => (document.documentElement.classList.contains('dark') ? SHIKI_THEME.dark : SHIKI_THEME.light) | |
| const [theme, setTheme] = React.useState(current) | |
| React.useEffect(() => { | |
| const observer = new MutationObserver(() => setTheme(current())) | |
| observer.observe(document.documentElement, { attributeFilter: ['class'], attributes: true }) | |
| return () => observer.disconnect() | |
| }, []) | |
| return theme | |
| } | |
| function PreviewDiffRows({ | |
| afterLines = 0, | |
| beforeLines = 0, | |
| chunks, | |
| tokens | |
| }: { | |
| afterLines?: number | |
| beforeLines?: number | |
| chunks: Array<LineChunk<DiffLine>> | |
| tokens?: ThemedToken[][] | null | |
| }) { | |
| return ( | |
| <> | |
| {beforeLines > 0 && <div aria-hidden style={{ height: beforeLines * PREVIEW_LINE_PX }} />} | |
| {chunks.map(chunk => ( | |
| <div className="block" key={chunk.start}> | |
| {chunk.lines.map((line, offset) => { | |
| const index = chunk.start + offset | |
| const rowTokens = tokens?.[index] ?? [] | |
| return ( | |
| <span className={cn(PREVIEW_DIFF_LINE_BASE, DIFF_KIND_TINT[line.kind])} key={`${index}-${line.text}`}> | |
| {rowTokens.length > 0 | |
| ? rowTokens.map((token, tokenIndex) => ( | |
| <span key={`${tokenIndex}-${token.offset}`} style={tokenStyle(token)}> | |
| {token.content} | |
| </span> | |
| )) | |
| : line.text || ' '} | |
| </span> | |
| ) | |
| })} | |
| </div> | |
| ))} | |
| {afterLines > 0 && <div aria-hidden style={{ height: afterLines * PREVIEW_LINE_PX }} />} | |
| </> | |
| ) | |
| } | |
| function TokenizedDiffBody({ | |
| afterLines, | |
| beforeLines, | |
| chunked = false, | |
| chunks, | |
| language, | |
| lines | |
| }: { | |
| afterLines?: number | |
| beforeLines?: number | |
| chunked?: boolean | |
| chunks?: Array<LineChunk<DiffLine>> | |
| language: string | |
| lines: DiffLine[] | |
| }) { | |
| const code = React.useMemo(() => lines.map(line => line.text).join('\n'), [lines]) | |
| const theme = useThemeName() | |
| const [tokens, setTokens] = React.useState<ThemedToken[][] | null>(null) | |
| React.useEffect(() => { | |
| let cancelled = false | |
| setTokens(null) | |
| // Dynamic import so the multi-MB shiki chunk stays off the cold-start | |
| // path — this effect only runs once a highlightable diff is on screen. | |
| void import('shiki') | |
| .then(({ codeToTokens }) => codeToTokens(code, { lang: language as BundledLanguage, theme })) | |
| .then(result => { | |
| if (!cancelled) { | |
| setTokens(result.tokens) | |
| } | |
| }) | |
| .catch(() => { | |
| if (!cancelled) { | |
| setTokens([]) | |
| } | |
| }) | |
| return () => { | |
| cancelled = true | |
| } | |
| }, [code, language, theme]) | |
| if (!tokens) { | |
| return chunked ? ( | |
| <PreviewDiffRows | |
| afterLines={afterLines} | |
| beforeLines={beforeLines} | |
| chunks={chunks ?? chunkLines(lines, PREVIEW_CHUNK_LINES)} | |
| /> | |
| ) : ( | |
| <DiffBody lines={lines} /> | |
| ) | |
| } | |
| if (chunked) { | |
| return ( | |
| <PreviewDiffRows | |
| afterLines={afterLines} | |
| beforeLines={beforeLines} | |
| chunks={chunks ?? chunkLines(lines, PREVIEW_CHUNK_LINES)} | |
| tokens={tokens} | |
| /> | |
| ) | |
| } | |
| return ( | |
| <> | |
| {lines.map((line, index) => { | |
| const rowTokens = tokens[index] ?? [] | |
| return ( | |
| <span className={cn(PREVIEW_DIFF_LINE_BASE, DIFF_KIND_TINT[line.kind])} key={`${index}-${line.text}`}> | |
| {rowTokens.length > 0 | |
| ? rowTokens.map((token, tokenIndex) => ( | |
| <span key={`${tokenIndex}-${token.offset}`} style={tokenStyle(token)}> | |
| {token.content} | |
| </span> | |
| )) | |
| : line.text || ' '} | |
| </span> | |
| ) | |
| })} | |
| </> | |
| ) | |
| } | |
| // Shiki transformer: tag each `.line` with the diff tint for its kind, so the | |
| // syntax-highlighted output keeps add/remove backgrounds + the gutter accent. | |
| // Exported for the lazily-loaded SyntaxDiff (syntax-diff.tsx). | |
| export function diffLineTransformer(kinds: DiffKind[]): ShikiTransformer { | |
| return { | |
| line(node, line) { | |
| const kind = kinds[line - 1] ?? 'context' | |
| const existing = Array.isArray(node.properties.className) | |
| ? (node.properties.className as string[]) | |
| : node.properties.className | |
| ? [String(node.properties.className)] | |
| : [] | |
| node.properties.className = [...existing, DIFF_LINE_BASE, DIFF_KIND_TINT[kind]] | |
| } | |
| } | |
| } | |
| function SyntaxDiff({ language, lines }: { language: string; lines: DiffLine[] }) { | |
| // The Shiki hook lives in a lazily-loaded module (syntax-diff.tsx) so the | |
| // multi-MB shiki chunk stays off the cold-start path. Until it (and the | |
| // highlight itself) resolves, show the plain colored diff — no flash. | |
| // | |
| // A rejected dynamic import (e.g. a packaged app whose renderer window is | |
| // pointed at the asar copy of dist/ while the chunk only exists in | |
| // app.asar.unpacked, #93479) throws past Suspense, which only covers the | |
| // pending state. Without a local boundary that throw reaches the workspace | |
| // ContribBoundary and blanks the whole pane instead of just this diff. | |
| return ( | |
| <ErrorBoundary fallback={() => <DiffBody lines={lines} />} label="syntax-diff"> | |
| <React.Suspense fallback={<DiffBody lines={lines} />}> | |
| <LazySyntaxDiff language={language} lines={lines} /> | |
| </React.Suspense> | |
| </ErrorBoundary> | |
| ) | |
| } | |
| const LazySyntaxDiff = React.lazy(() => import('./syntax-diff')) | |
| interface DiffLinesProps extends Omit<React.ComponentProps<'pre'>, 'children'> { | |
| text: string | |
| } | |
| export function DiffLines({ className, text, ...props }: DiffLinesProps) { | |
| const lines = React.useMemo(() => parseDiff(text), [text]) | |
| return ( | |
| <pre className={cn(DIFF_BOX_CLASS, className)} data-slot="diff-lines" {...props}> | |
| <DiffBody lines={lines} /> | |
| </pre> | |
| ) | |
| } | |
| // Coalesce consecutive same-kind changed rows into runs, each placed by line | |
| // fraction (no DOM measurement). Context rows produce no tick. | |
| function overviewRuns(lines: DiffLine[]): { kind: 'add' | 'remove'; sizePct: number; startPct: number }[] { | |
| const total = lines.length || 1 | |
| const runs: { kind: 'add' | 'remove'; sizePct: number; startPct: number }[] = [] | |
| for (let i = 0; i < lines.length;) { | |
| const kind = lines[i].kind | |
| if (kind === 'context') { | |
| i += 1 | |
| continue | |
| } | |
| let j = i + 1 | |
| while (j < lines.length && lines[j].kind === kind) { | |
| j += 1 | |
| } | |
| runs.push({ kind, sizePct: ((j - i) / total) * 100, startPct: (i / total) * 100 }) | |
| i = j | |
| } | |
| return runs | |
| } | |
| // VS Code-style overview ruler: a thin strip pinned to the diff's right edge with | |
| // a green/red tick per change, positioned by line fraction. Pinned to the | |
| // viewport (not the scrolled content) by living as an absolute sibling of the | |
| // scroller inside a relative wrapper — so no scroll listener or measurement. | |
| function DiffOverviewRuler({ lines }: { lines: DiffLine[] }) { | |
| const runs = React.useMemo(() => overviewRuns(lines), [lines]) | |
| if (runs.length === 0) { | |
| return null | |
| } | |
| return ( | |
| <div aria-hidden className="pointer-events-none absolute top-0 right-0 bottom-0 w-1.5 opacity-80"> | |
| {/* Cap the tick field to the diff's natural height (rows × line px) so a | |
| short diff renders thin, line-aligned ticks instead of stretching a few | |
| changes into gross full-height blocks. A long diff hits the 100% cap and | |
| compresses into a true overview. */} | |
| <div className="relative w-full" style={{ height: `min(100%, ${lines.length * PREVIEW_LINE_PX}px)` }}> | |
| {runs.map((run, index) => ( | |
| <div | |
| className={cn( | |
| 'absolute inset-x-0', | |
| run.kind === 'add' ? 'bg-(--ui-diff-add-border)' : 'bg-(--ui-diff-remove-border)' | |
| )} | |
| key={index} | |
| style={{ height: `max(0.125rem, ${run.sizePct}%)`, top: `${run.startPct}%` }} | |
| /> | |
| ))} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| interface FileDiffPanelProps { | |
| /** Override the default (tool-card) box styling — the full-height preview | |
| * cancels the bleed/clamp so the diff fills its pane. */ | |
| className?: string | |
| diff: string | |
| /** Current file text. When provided, the panel expands hunked diffs into a | |
| * full-file view so unchanged lines are preserved between hunks. */ | |
| fullText?: string | |
| path?: string | |
| /** Render an old/new line-number gutter (the full preview diff). The compact | |
| * tool-card + inline review diff leave this off. */ | |
| showLineNumbers?: boolean | |
| /** Window the rows (fixed-row virtualization) WITHOUT a gutter — for a large | |
| * diff in a scrolling pane (the review panel), so only visible rows mount | |
| * instead of highlighting every line. `showLineNumbers` implies windowing. */ | |
| virtualized?: boolean | |
| } | |
| export function FileDiffPanel({ | |
| className, | |
| diff, | |
| fullText, | |
| path, | |
| showLineNumbers = false, | |
| virtualized = false | |
| }: FileDiffPanelProps) { | |
| const lines = React.useMemo( | |
| () => (fullText != null ? parseFullFileDiff(diff, fullText) : parseDiff(diff)), | |
| [diff, fullText] | |
| ) | |
| const lineChunks = React.useMemo(() => chunkLines(lines, PREVIEW_CHUNK_LINES), [lines]) | |
| const { afterRows, beforeRows, endChunk, onScroll, scrollerRef, startChunk } = useFixedRowWindow({ | |
| overscanRows: PREVIEW_OVERSCAN_LINES, | |
| rowPx: PREVIEW_LINE_PX, | |
| rowsPerChunk: PREVIEW_CHUNK_LINES, | |
| totalRows: lines.length | |
| }) | |
| const visibleLineChunks = lineChunks.slice(startChunk, endChunk + 1) | |
| const language = shikiLanguageForFilename(path) | |
| const canHighlight = Boolean(language) && !exceedsHighlightBudget(fullText ?? diff) | |
| const windowed = showLineNumbers || virtualized | |
| // Windowed: we own fixed-height rows and render only the visible chunks, so a | |
| // large diff never mounts (or Shiki-highlights) every line. Compact tool cards | |
| // are small/clamped, so they let Shiki own the rows (SyntaxDiff). | |
| const windowedBody = canHighlight ? ( | |
| <TokenizedDiffBody | |
| afterLines={afterRows} | |
| beforeLines={beforeRows} | |
| chunked | |
| chunks={visibleLineChunks} | |
| language={language} | |
| lines={lines} | |
| /> | |
| ) : ( | |
| <PreviewDiffRows afterLines={afterRows} beforeLines={beforeRows} chunks={visibleLineChunks} /> | |
| ) | |
| const compactBody = !canHighlight ? ( | |
| <DiffBody lines={lines} /> | |
| ) : fullText != null ? ( | |
| <TokenizedDiffBody language={language} lines={lines} /> | |
| ) : ( | |
| <SyntaxDiff language={language} lines={lines} /> | |
| ) | |
| if (!windowed) { | |
| return ( | |
| <div className={cn(DIFF_BOX_CLASS, className)} data-slot="file-diff-panel"> | |
| {compactBody} | |
| </div> | |
| ) | |
| } | |
| // Windowed: a fixed-row scroller renders only the visible rows (killing the | |
| // full-Shiki-of-every-line freeze on large diffs). With `showLineNumbers` a | |
| // VS Code-style gutter (new number for context/adds, old for removals) sits in | |
| // a left column; the scroller owns scroll so the overview ruler (an absolute | |
| // sibling) stays viewport-fixed. | |
| return ( | |
| <div className={cn(DIFF_BOX_CLASS, 'relative overflow-hidden', className)} data-slot="file-diff-panel"> | |
| <div | |
| className={cn('absolute inset-0 overflow-auto', showLineNumbers && 'pr-2.5')} | |
| onScroll={onScroll} | |
| ref={scrollerRef} | |
| > | |
| {showLineNumbers ? ( | |
| <div className="grid min-w-max grid-cols-[auto_minmax(0,1fr)]"> | |
| <div | |
| className="sticky left-0 z-1 select-none bg-(--ui-editor-surface-background) py-3 text-muted-foreground/55" | |
| // Masks the code scrolling horizontally beneath it, so it has to | |
| // stay opaque when window glass thins the field. See | |
| // `[data-glass-opaque]` in styles.css. | |
| data-glass-opaque="" | |
| > | |
| {beforeRows > 0 && <div aria-hidden style={{ height: beforeRows * PREVIEW_LINE_PX }} />} | |
| {visibleLineChunks.map(chunk => ( | |
| <div className="block" key={chunk.start}> | |
| {chunk.lines.map((line, offset) => { | |
| const index = chunk.start + offset | |
| return ( | |
| <div | |
| className="h-5 w-9 pr-2 text-right leading-5 tabular-nums" | |
| key={`${index}-${line.oldNo}-${line.newNo}`} | |
| > | |
| {line.newNo ?? ''} | |
| </div> | |
| ) | |
| })} | |
| </div> | |
| ))} | |
| {afterRows > 0 && <div aria-hidden style={{ height: afterRows * PREVIEW_LINE_PX }} />} | |
| </div> | |
| <div className="min-w-0">{windowedBody}</div> | |
| </div> | |
| ) : ( | |
| <div className="min-w-0">{windowedBody}</div> | |
| )} | |
| </div> | |
| <DiffOverviewRuler lines={lines} /> | |
| </div> | |
| ) | |
| } | |