diff --git a/src/components/config/DiffModal.module.scss b/src/components/config/DiffModal.module.scss index 020ac1c..7868ad2 100644 --- a/src/components/config/DiffModal.module.scss +++ b/src/components/config/DiffModal.module.scss @@ -70,7 +70,7 @@ .diffColumnHeader { display: flex; - align-items: center; + align-items: baseline; justify-content: space-between; gap: $spacing-sm; padding: 8px 10px; @@ -81,23 +81,66 @@ background: var(--bg-secondary); } +.lineMeta { + display: inline-flex; + align-items: center; + gap: 8px; +} + .lineRange { + font-size: 11px; + color: var(--text-secondary); + font-family: 'Consolas', 'Monaco', 'Menlo', monospace; +} + +.contextRange { font-size: 11px; color: var(--text-tertiary); font-family: 'Consolas', 'Monaco', 'Menlo', monospace; } -.codeBlock { - margin: 0; - padding: 10px; +.codeList { + overflow: auto; + max-height: 280px; + font-family: 'Consolas', 'Monaco', 'Menlo', monospace; +} + +.codeLine { + display: grid; + grid-template-columns: 52px minmax(0, 1fr); + align-items: start; + border-top: 1px solid color-mix(in srgb, var(--border-color) 55%, transparent); +} + +.codeLine:first-child { + border-top: none; +} + +.codeLineChanged { + background: color-mix(in srgb, var(--primary-color) 8%, transparent); +} + +.codeLineNumber { + padding: 7px 10px 7px 8px; + text-align: right; + font-size: 11px; + color: var(--text-tertiary); + border-right: 1px solid color-mix(in srgb, var(--border-color) 55%, transparent); + background: color-mix(in srgb, var(--bg-secondary) 90%, transparent); + font-variant-numeric: tabular-nums; + user-select: none; + box-sizing: border-box; +} + +.codeLineText { + padding: 7px 10px; font-size: 12px; - line-height: 1.5; + line-height: 1.45; color: var(--text-primary); white-space: pre-wrap; word-break: break-word; - font-family: 'Consolas', 'Monaco', 'Menlo', monospace; - overflow: auto; - max-height: 240px; + display: block; + box-sizing: border-box; } @include mobile { @@ -109,4 +152,25 @@ .diffColumns { grid-template-columns: 1fr; } + + .lineMeta { + flex-direction: column; + align-items: flex-end; + gap: 1px; + } + + .codeLine { + grid-template-columns: 44px minmax(0, 1fr); + } + + .codeLineNumber { + padding: 6px 6px 6px 4px; + font-size: 10px; + } + + .codeLineText { + padding: 6px 8px; + font-size: 11px; + line-height: 1.4; + } } diff --git a/src/components/config/DiffModal.tsx b/src/components/config/DiffModal.tsx index f771323..bbcb19e 100644 --- a/src/components/config/DiffModal.tsx +++ b/src/components/config/DiffModal.tsx @@ -17,31 +17,63 @@ type DiffModalProps = { type DiffChunkCard = { id: string; - currentLines: string; - modifiedLines: string; - currentText: string; - modifiedText: string; + current: DiffSide; + modified: DiffSide; }; +type LineRange = { + start: number; + end: number; +}; + +type DiffSideLine = { + lineNumber: number; + text: string; + changed: boolean; +}; + +type DiffSide = { + changedRangeLabel: string; + contextRangeLabel: string; + lines: DiffSideLine[]; +}; + +const DIFF_CONTEXT_LINES = 2; + const clampPos = (doc: Text, pos: number) => Math.max(0, Math.min(pos, doc.length)); -const getLineRangeLabel = (doc: Text, from: number, to: number): string => { +const getLineRangeLabel = (range: LineRange): string => { + return range.start === range.end ? String(range.start) : `${range.start}-${range.end}`; +}; + +const getChangedLineRange = (doc: Text, from: number, to: number): LineRange => { const start = clampPos(doc, from); const end = clampPos(doc, to); if (start === end) { const linePos = Math.min(start, doc.length); - return String(doc.lineAt(linePos).number); + const anchorLine = doc.lineAt(linePos).number; + return { start: anchorLine, end: anchorLine }; } const startLine = doc.lineAt(start).number; const endLine = doc.lineAt(Math.max(start, end - 1)).number; - return startLine === endLine ? String(startLine) : `${startLine}-${endLine}`; + return { start: startLine, end: endLine }; }; -const getChunkText = (doc: Text, from: number, to: number): string => { - const start = clampPos(doc, from); - const end = clampPos(doc, to); - if (start >= end) return ''; - return doc.sliceString(start, end).trimEnd(); +const expandContextRange = (doc: Text, range: LineRange): LineRange => ({ + start: Math.max(1, range.start - DIFF_CONTEXT_LINES), + end: Math.min(doc.lines, range.end + DIFF_CONTEXT_LINES) +}); + +const buildSideLines = (doc: Text, contextRange: LineRange, changedRange: LineRange): DiffSideLine[] => { + const lines: DiffSideLine[] = []; + for (let lineNumber = contextRange.start; lineNumber <= contextRange.end; lineNumber += 1) { + lines.push({ + lineNumber, + text: doc.line(lineNumber).text, + changed: lineNumber >= changedRange.start && lineNumber <= changedRange.end + }); + } + return lines; }; export function DiffModal({ @@ -59,13 +91,26 @@ export function DiffModal({ const modifiedDoc = Text.of(modified.split('\n')); const chunks = Chunk.build(currentDoc, modifiedDoc); - return chunks.map((chunk, index) => ({ - id: `${index}-${chunk.fromA}-${chunk.toA}-${chunk.fromB}-${chunk.toB}`, - currentLines: getLineRangeLabel(currentDoc, chunk.fromA, chunk.toA), - modifiedLines: getLineRangeLabel(modifiedDoc, chunk.fromB, chunk.toB), - currentText: getChunkText(currentDoc, chunk.fromA, chunk.toA), - modifiedText: getChunkText(modifiedDoc, chunk.fromB, chunk.toB) - })); + return chunks.map((chunk, index) => { + const currentChangedRange = getChangedLineRange(currentDoc, chunk.fromA, chunk.toA); + const modifiedChangedRange = getChangedLineRange(modifiedDoc, chunk.fromB, chunk.toB); + const currentContextRange = expandContextRange(currentDoc, currentChangedRange); + const modifiedContextRange = expandContextRange(modifiedDoc, modifiedChangedRange); + + return { + id: `${index}-${chunk.fromA}-${chunk.toA}-${chunk.fromB}-${chunk.toB}`, + current: { + changedRangeLabel: getLineRangeLabel(currentChangedRange), + contextRangeLabel: getLineRangeLabel(currentContextRange), + lines: buildSideLines(currentDoc, currentContextRange, currentChangedRange) + }, + modified: { + changedRangeLabel: getLineRangeLabel(modifiedChangedRange), + contextRangeLabel: getLineRangeLabel(modifiedContextRange), + lines: buildSideLines(modifiedDoc, modifiedContextRange, modifiedChangedRange) + } + }; + }); }, [modified, original]); return ( @@ -99,16 +144,46 @@ export function DiffModal({
{t('config_management.diff.current')} - L{card.currentLines} + + L{card.current.changedRangeLabel} + + ±{DIFF_CONTEXT_LINES}: L{card.current.contextRangeLabel} + +
-
{card.currentText || '-'}
+
+ {card.current.lines.map((line) => ( +
+ {line.lineNumber} + {line.text || ' '} +
+ ))} +
{t('config_management.diff.modified')} - L{card.modifiedLines} + + L{card.modified.changedRangeLabel} + + ±{DIFF_CONTEXT_LINES}: L{card.modified.contextRangeLabel} + +
-
{card.modifiedText || '-'}
+
+ {card.modified.lines.map((line) => ( +
+ {line.lineNumber} + {line.text || ' '} +
+ ))} +