fix(config): show diff chunks with line numbers and context

This commit is contained in:
Supra4E8C
2026-02-17 00:01:54 +08:00
parent 32bf103f15
commit 7d4c400084
2 changed files with 170 additions and 31 deletions

View File

@@ -70,7 +70,7 @@
.diffColumnHeader { .diffColumnHeader {
display: flex; display: flex;
align-items: center; align-items: baseline;
justify-content: space-between; justify-content: space-between;
gap: $spacing-sm; gap: $spacing-sm;
padding: 8px 10px; padding: 8px 10px;
@@ -81,23 +81,66 @@
background: var(--bg-secondary); background: var(--bg-secondary);
} }
.lineMeta {
display: inline-flex;
align-items: center;
gap: 8px;
}
.lineRange { .lineRange {
font-size: 11px;
color: var(--text-secondary);
font-family: 'Consolas', 'Monaco', 'Menlo', monospace;
}
.contextRange {
font-size: 11px; font-size: 11px;
color: var(--text-tertiary); color: var(--text-tertiary);
font-family: 'Consolas', 'Monaco', 'Menlo', monospace; font-family: 'Consolas', 'Monaco', 'Menlo', monospace;
} }
.codeBlock { .codeList {
margin: 0; overflow: auto;
padding: 10px; 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; font-size: 12px;
line-height: 1.5; line-height: 1.45;
color: var(--text-primary); color: var(--text-primary);
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
font-family: 'Consolas', 'Monaco', 'Menlo', monospace; display: block;
overflow: auto; box-sizing: border-box;
max-height: 240px;
} }
@include mobile { @include mobile {
@@ -109,4 +152,25 @@
.diffColumns { .diffColumns {
grid-template-columns: 1fr; 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;
}
} }

View File

@@ -17,31 +17,63 @@ type DiffModalProps = {
type DiffChunkCard = { type DiffChunkCard = {
id: string; id: string;
currentLines: string; current: DiffSide;
modifiedLines: string; modified: DiffSide;
currentText: string;
modifiedText: string;
}; };
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 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 start = clampPos(doc, from);
const end = clampPos(doc, to); const end = clampPos(doc, to);
if (start === end) { if (start === end) {
const linePos = Math.min(start, doc.length); 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 startLine = doc.lineAt(start).number;
const endLine = doc.lineAt(Math.max(start, end - 1)).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 expandContextRange = (doc: Text, range: LineRange): LineRange => ({
const start = clampPos(doc, from); start: Math.max(1, range.start - DIFF_CONTEXT_LINES),
const end = clampPos(doc, to); end: Math.min(doc.lines, range.end + DIFF_CONTEXT_LINES)
if (start >= end) return ''; });
return doc.sliceString(start, end).trimEnd();
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({ export function DiffModal({
@@ -59,13 +91,26 @@ export function DiffModal({
const modifiedDoc = Text.of(modified.split('\n')); const modifiedDoc = Text.of(modified.split('\n'));
const chunks = Chunk.build(currentDoc, modifiedDoc); const chunks = Chunk.build(currentDoc, modifiedDoc);
return chunks.map((chunk, index) => ({ return chunks.map((chunk, index) => {
id: `${index}-${chunk.fromA}-${chunk.toA}-${chunk.fromB}-${chunk.toB}`, const currentChangedRange = getChangedLineRange(currentDoc, chunk.fromA, chunk.toA);
currentLines: getLineRangeLabel(currentDoc, chunk.fromA, chunk.toA), const modifiedChangedRange = getChangedLineRange(modifiedDoc, chunk.fromB, chunk.toB);
modifiedLines: getLineRangeLabel(modifiedDoc, chunk.fromB, chunk.toB), const currentContextRange = expandContextRange(currentDoc, currentChangedRange);
currentText: getChunkText(currentDoc, chunk.fromA, chunk.toA), const modifiedContextRange = expandContextRange(modifiedDoc, modifiedChangedRange);
modifiedText: getChunkText(modifiedDoc, chunk.fromB, chunk.toB)
})); 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]); }, [modified, original]);
return ( return (
@@ -99,16 +144,46 @@ export function DiffModal({
<section className={styles.diffColumn}> <section className={styles.diffColumn}>
<header className={styles.diffColumnHeader}> <header className={styles.diffColumnHeader}>
<span>{t('config_management.diff.current')}</span> <span>{t('config_management.diff.current')}</span>
<span className={styles.lineRange}>L{card.currentLines}</span> <span className={styles.lineMeta}>
<span className={styles.lineRange}>L{card.current.changedRangeLabel}</span>
<span className={styles.contextRange}>
±{DIFF_CONTEXT_LINES}: L{card.current.contextRangeLabel}
</span>
</span>
</header> </header>
<pre className={styles.codeBlock}>{card.currentText || '-'}</pre> <div className={styles.codeList}>
{card.current.lines.map((line) => (
<div
key={`${card.id}-a-${line.lineNumber}`}
className={`${styles.codeLine} ${line.changed ? styles.codeLineChanged : ''}`}
>
<span className={styles.codeLineNumber}>{line.lineNumber}</span>
<code className={styles.codeLineText}>{line.text || ' '}</code>
</div>
))}
</div>
</section> </section>
<section className={styles.diffColumn}> <section className={styles.diffColumn}>
<header className={styles.diffColumnHeader}> <header className={styles.diffColumnHeader}>
<span>{t('config_management.diff.modified')}</span> <span>{t('config_management.diff.modified')}</span>
<span className={styles.lineRange}>L{card.modifiedLines}</span> <span className={styles.lineMeta}>
<span className={styles.lineRange}>L{card.modified.changedRangeLabel}</span>
<span className={styles.contextRange}>
±{DIFF_CONTEXT_LINES}: L{card.modified.contextRangeLabel}
</span>
</span>
</header> </header>
<pre className={styles.codeBlock}>{card.modifiedText || '-'}</pre> <div className={styles.codeList}>
{card.modified.lines.map((line) => (
<div
key={`${card.id}-b-${line.lineNumber}`}
className={`${styles.codeLine} ${line.changed ? styles.codeLineChanged : ''}`}
>
<span className={styles.codeLineNumber}>{line.lineNumber}</span>
<code className={styles.codeLineText}>{line.text || ' '}</code>
</div>
))}
</div>
</section> </section>
</div> </div>
</article> </article>