Skip to content

Commit 286cf51

Browse files
committed
refactor(diff): add syntax-highlighted ghost rows and streamline renderer flow
1 parent b41978c commit 286cf51

7 files changed

Lines changed: 136 additions & 116 deletions

File tree

anycode-base/src/diff.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type ChangeType = 'added' | 'modified' | 'deleted';
1616

1717
export type DiffInfo = {
1818
changeType: ChangeType;
19-
oldLines?: string[];
19+
oldLineNumbers?: number[];
2020
ghostAnchorLine?: number;
2121
hunkId: number;
2222
};
@@ -40,7 +40,9 @@ export function computeGitChanges(
4040
if (line.startsWith('@@ -')) {
4141
const headerMatch = line.match(/ \+(\d+)(?:,(\d+))?/);
4242
if (headerMatch) {
43+
const oldHeaderMatch = line.match(/@@ -(\d+)(?:,(\d+))?/);
4344
let newLine = parseInt(headerMatch[1], 10);
45+
let oldLine = oldHeaderMatch ? parseInt(oldHeaderMatch[1], 10) : 1;
4446
i++;
4547
lastChangeWasConsecutive = false;
4648

@@ -60,11 +62,12 @@ export function computeGitChanges(
6062
}
6163

6264
if (currentLine.startsWith('-') || currentLine.startsWith('+')) {
63-
const deletedLines: string[] = [];
65+
const deletedLineNumbers: number[] = [];
6466
const addedLineNumbers: number[] = [];
6567

6668
while (i < lines.length && lines[i].startsWith('-')) {
67-
deletedLines.push(lines[i].slice(1));
69+
deletedLineNumbers.push(oldLine);
70+
oldLine++;
6871
i++;
6972
}
7073

@@ -82,11 +85,11 @@ export function computeGitChanges(
8285
i++;
8386
}
8487

85-
if (deletedLines.length > 0 && addedLineNumbers.length > 0) {
88+
if (deletedLineNumbers.length > 0 && addedLineNumbers.length > 0) {
8689
for (const lineNum of addedLineNumbers) {
8790
changes.set(lineNum, {
8891
changeType: 'modified',
89-
oldLines: deletedLines,
92+
oldLineNumbers: deletedLineNumbers,
9093
hunkId: hunkId,
9194
});
9295
}
@@ -98,18 +101,17 @@ export function computeGitChanges(
98101
hunkId: hunkId,
99102
});
100103
}
101-
} else if (deletedLines.length > 0) {
104+
} else if (deletedLineNumbers.length > 0) {
102105
// deleted
103106
// JsDiff can emit +0 for deletions before the first line.
104107
// ghostAnchorLine is the line BEFORE which ghost lines appear.
105108
// markerLine is the line where the deletion marker appears in gutter
106-
// (the last real line before the deletion, i.e. anchorLine - 1).
109+
// (aligned with the ghost anchor line in our renderer model).
107110
const ghostAnchorLine = Math.max(1, newLine + 1);
108-
// Marker should be on the line BEFORE the ghosts
109-
const markerLine = Math.max(1, Math.min(ghostAnchorLine - 1, currentLineCount));
111+
const markerLine = Math.max(1, Math.min(ghostAnchorLine, currentLineCount));
110112
changes.set(markerLine, {
111113
changeType: 'deleted',
112-
oldLines: deletedLines,
114+
oldLineNumbers: deletedLineNumbers,
113115
ghostAnchorLine,
114116
hunkId: hunkId,
115117
});
@@ -122,6 +124,7 @@ export function computeGitChanges(
122124
hunkId++;
123125
lastChangeWasConsecutive = false;
124126
}
127+
oldLine++;
125128
newLine++;
126129
i++;
127130
} else {

anycode-base/src/editor.ts

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export interface EditorOptions {
4343

4444
export interface EditorState {
4545
code: Code;
46+
originalCode?: Code;
4647
offset: number;
4748
selection: Selection | null;
4849
runLines: number[];
@@ -94,7 +95,7 @@ export class AnycodeEditor {
9495
private diffEnabled: boolean = false;
9596
private focusedDiffEnabled: boolean;
9697
private focusedDiffContextLines: number;
97-
private originalCode?: string;
98+
private originalCode?: Code;
9899
private diffs?: Map<number, DiffInfo>;
99100
private readonly readOnly: boolean;
100101

@@ -118,12 +119,6 @@ export class AnycodeEditor {
118119

119120
this.settings = { lineHeight: 20, buffer: 30 };
120121

121-
if (this.diffEnabled) {
122-
this.originalCode = initialText;
123-
const currentText = this.code.getContent();
124-
this.diffs = computeGitChanges(this.originalCode, currentText);
125-
}
126-
127122
const theme = options.theme || vesper;
128123
const css = generateCssClasses(theme);
129124
addCssToDocument(css, 'anyeditor-theme');
@@ -261,6 +256,33 @@ export class AnycodeEditor {
261256
this.setupEventListeners();
262257
}
263258

259+
private async initOriginalCode(content: string): Promise<boolean> {
260+
if (this.originalCode?.getContent() === content) {
261+
return false;
262+
}
263+
const originalCode = new Code(
264+
content,
265+
this.code.filename,
266+
this.code.language ?? 'text',
267+
);
268+
this.originalCode = originalCode;
269+
270+
try {
271+
await originalCode.init();
272+
// Ignore stale async completion if a newer baseline replaced this instance.
273+
if (this.originalCode !== originalCode) return false;
274+
this.originalCode = originalCode;
275+
return true;
276+
} catch (error) {
277+
// Don't wipe newer baseline on stale failure.
278+
if (this.originalCode === originalCode) {
279+
this.originalCode = undefined;
280+
}
281+
console.warn('Failed to initialize original code for diff rendering', error);
282+
return false;
283+
}
284+
}
285+
264286
private setupReadOnlyEventListeners() {
265287
this.handleScroll = this.handleScroll.bind(this);
266288
this.container.addEventListener("scroll", this.handleScroll);
@@ -472,6 +494,7 @@ export class AnycodeEditor {
472494
private getEditorState(): EditorState {
473495
return {
474496
code: this.code,
497+
originalCode: this.originalCode,
475498
offset: this.offset,
476499
selection: this.selection,
477500
runLines: this.runLines,
@@ -911,7 +934,6 @@ export class AnycodeEditor {
911934
}
912935

913936
private async handleKeydown(event: KeyboardEvent) {
914-
console.log('keydown', event);
915937
this.clearPendingHover();
916938
this.closeHover();
917939

@@ -1497,8 +1519,14 @@ export class AnycodeEditor {
14971519
this.diffEnabled = enabled;
14981520
this.renderer.setDiffEnabled(enabled);
14991521

1500-
if (enabled && this.originalCode === undefined) {
1501-
this.originalCode = this.code.getContent();
1522+
if (enabled) {
1523+
const baseline = this.originalCode?.getContent() ?? this.code.getContent();
1524+
void this.initOriginalCode(baseline).then((updated) => {
1525+
if (!this.diffEnabled || !updated) return;
1526+
this.recomputeDiffs();
1527+
this.renderer.render(this.getEditorState(), this.search);
1528+
this.verifyDiffRendering();
1529+
});
15021530
}
15031531

15041532
this.recomputeDiffs();
@@ -1523,17 +1551,17 @@ export class AnycodeEditor {
15231551
}
15241552

15251553
public setOriginalCode(content: string): void {
1526-
this.originalCode = content;
1527-
if (this.diffEnabled) {
1554+
void this.initOriginalCode(content).then((updated) => {
1555+
if (!this.diffEnabled || !updated) return;
15281556
this.recomputeDiffs();
15291557
this.renderer.render(this.getEditorState(), this.search);
15301558
this.verifyDiffRendering();
1531-
}
1559+
});
15321560
}
15331561

15341562
private recomputeDiffs(): void {
1535-
if (this.diffEnabled && this.originalCode !== undefined) {
1536-
this.diffs = computeGitChanges(this.originalCode, this.code.getContent());
1563+
if (this.diffEnabled && this.originalCode) {
1564+
this.diffs = computeGitChanges(this.originalCode.getContent(), this.code.getContent());
15371565
} else {
15381566
this.diffs = undefined;
15391567
}

anycode-base/src/renderer/DiffRenderer.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { AnycodeLine } from "../utils";
22
import { EditorSettings } from "../editor";
33
import { DiffInfo, ChangeType } from "../diff";
4+
import { HighlighedNode } from "../code";
45
import type { GhostRow, SeparatorRow, VisualRow } from "./Renderer";
56

67
export type ExpandDirection = 'up' | 'down' | 'both' | 'all';
@@ -73,15 +74,26 @@ export class DiffRenderer {
7374
// ========== Ghost Lines ==========
7475

7576
private createDeletedGhostLine(
76-
text: string, settings: EditorSettings, hunkId: number
77+
text: string,
78+
settings: EditorSettings,
79+
hunkId: number,
80+
nodes?: HighlighedNode[]
7781
): HTMLDivElement {
7882
const ghostLine = document.createElement('div');
7983
ghostLine.className = "line line-deleted-ghost";
8084
ghostLine.style.lineHeight = `${settings.lineHeight}px`;
8185
ghostLine.setAttribute('data-ghost', 'true');
8286
ghostLine.setAttribute('data-hunk-id', hunkId.toString());
8387

84-
if (text === '') {
88+
if (nodes && nodes.length > 0) {
89+
for (const { name, text: nodeText } of nodes) {
90+
const span = document.createElement('span');
91+
if (name) span.className = name;
92+
if (!name && nodeText === '\t') span.className = 'indent';
93+
span.textContent = nodeText;
94+
ghostLine.appendChild(span);
95+
}
96+
} else if (text === '') {
8597
ghostLine.textContent = '\u00A0'; // non-breaking space
8698
} else {
8799
ghostLine.textContent = text;
@@ -96,11 +108,13 @@ export class DiffRenderer {
96108
*/
97109
public createGhostRowElements(
98110
ghostRow: GhostRow,
99-
settings: EditorSettings
111+
settings: EditorSettings,
112+
originalText: string,
113+
originalNodes?: HighlighedNode[]
100114
): GhostLine {
101-
const { hunkId, text } = ghostRow;
115+
const { hunkId } = ghostRow;
102116

103-
const ghostLine = this.createDeletedGhostLine(text, settings, hunkId);
117+
const ghostLine = this.createDeletedGhostLine(originalText, settings, hunkId, originalNodes);
104118

105119
const emptyGutter = document.createElement('div');
106120
emptyGutter.className = 'ln';
@@ -387,14 +401,12 @@ export class DiffRenderer {
387401

388402
public createGapRowElements(
389403
row: SeparatorRow,
390-
visualIndex: number,
391404
settings: EditorSettings
392405
): { code: HTMLElement; gutter: HTMLElement; btn: HTMLElement } {
393406
const code = document.createElement('div');
394407
code.className = 'line diff-gap';
395408
code.style.lineHeight = `${settings.lineHeight}px`;
396409
code.style.height = `${settings.lineHeight}px`;
397-
code.setAttribute('data-visual-index', visualIndex.toString());
398410
setGapElementData(code, {
399411
hiddenStart: row.hiddenStart,
400412
hiddenEnd: row.hiddenEnd,
@@ -417,7 +429,6 @@ export class DiffRenderer {
417429
const gutter = document.createElement('div');
418430
gutter.className = 'ln diff-gap-gutter';
419431
gutter.style.height = `${settings.lineHeight}px`;
420-
gutter.setAttribute('data-visual-index', visualIndex.toString());
421432

422433
const upBtn = document.createElement('button');
423434
upBtn.className = 'diff-gap-expand-btn diff-gap-gutter-btn diff-gap-gutter-btn-up';
@@ -446,7 +457,6 @@ export class DiffRenderer {
446457
const btn = document.createElement('div');
447458
btn.className = 'bt diff-gap-btn';
448459
btn.style.height = `${settings.lineHeight}px`;
449-
btn.setAttribute('data-visual-index', visualIndex.toString());
450460

451461
return { code, gutter, btn };
452462
}

anycode-base/src/renderer/LineRenderer.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,20 @@ export class LineRenderer {
127127
return div;
128128
}
129129

130+
public createLineElements(
131+
lineNumber: number,
132+
nodes: HighlighedNode[],
133+
errorLines: Map<number, string>,
134+
settings: EditorSettings,
135+
diffs: Map<number, DiffInfo> | undefined,
136+
runLines: number[],
137+
): { code: AnycodeLine; gutter: HTMLDivElement; btn: HTMLDivElement } {
138+
const code = this.createLineWrapper(lineNumber, nodes, errorLines, settings, diffs);
139+
const gutter = this.createLineNumber(lineNumber, settings, diffs);
140+
const btn = this.createLineButtons(lineNumber, runLines, errorLines, settings);
141+
return { code, gutter, btn };
142+
}
143+
130144
/**
131145
* Creates a spacer element for virtual scrolling
132146
*/

0 commit comments

Comments
 (0)