Skip to content

Commit c17a85d

Browse files
committed
feat(diff): add diff view modes and focused context rendering
1 parent 0e7934c commit c17a85d

11 files changed

Lines changed: 586 additions & 325 deletions

File tree

anycode-base/src/diff.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { diffChars, diffLines, Change } from 'diff';
21
import * as JsDiff from 'diff';
32

43
export enum EditKind {

anycode-base/src/editor.ts

Lines changed: 137 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import './styles.css';
2626
import { Search } from "./search";
2727
import { computeGitChanges, DiffInfo } from "./diff";
28+
import { getGapElementData } from "./renderer/DiffRenderer";
2829

2930
export interface EditorSettings {
3031
lineHeight: number;
@@ -36,6 +37,8 @@ export interface EditorOptions {
3637
column?: number;
3738
theme?: any;
3839
readOnly?: boolean;
40+
focusedDiffEnabled?: boolean;
41+
focusedDiffContextLines?: number;
3942
}
4043

4144
export interface EditorState {
@@ -89,6 +92,8 @@ export class AnycodeEditor {
8992
private search: Search = new Search();
9093

9194
private diffEnabled: boolean = false;
95+
private focusedDiffEnabled: boolean;
96+
private focusedDiffContextLines: number;
9297
private originalCode?: string;
9398
private diffs?: Map<number, DiffInfo>;
9499
private readonly readOnly: boolean;
@@ -101,6 +106,8 @@ export class AnycodeEditor {
101106
) {
102107
this.code = new Code(initialText, filename, language);
103108
this.readOnly = options.readOnly ?? false;
109+
this.focusedDiffEnabled = options.focusedDiffEnabled ?? false;
110+
this.focusedDiffContextLines = Math.max(0, options.focusedDiffContextLines ?? 3);
104111
// Set initial cursor position
105112
if (options.line !== undefined && options.column !== undefined) {
106113
this.offset = this.code.getOffset(options.line, options.column);
@@ -122,6 +129,7 @@ export class AnycodeEditor {
122129
addCssToDocument(css, 'anyeditor-theme');
123130
this.createDomElements();
124131
this.renderer = new Renderer(this.container, this.buttonsColumn, this.gutter, this.codeContent);
132+
this.renderer.setFocusedDiffMode(this.focusedDiffEnabled, this.focusedDiffContextLines);
125133
}
126134

127135
private createDomElements() {
@@ -173,11 +181,7 @@ export class AnycodeEditor {
173181

174182
public setText(newText: string) {
175183
this.code.setContent(newText);
176-
if (this.diffEnabled && this.originalCode !== undefined) {
177-
this.diffs = computeGitChanges(this.originalCode, newText);
178-
} else {
179-
this.diffs = undefined;
180-
}
184+
this.recomputeDiffs();
181185
}
182186

183187
public updateTextIncremental(newText: string) {
@@ -220,12 +224,7 @@ export class AnycodeEditor {
220224
this.selection = null;
221225
this.offset = Math.min(this.offset, this.code.getContentLength());
222226

223-
if (this.diffEnabled && this.originalCode !== undefined) {
224-
const updatedText = this.code.getContent();
225-
this.diffs = computeGitChanges(this.originalCode, updatedText);
226-
} else {
227-
this.diffs = undefined;
228-
}
227+
this.recomputeDiffs();
229228

230229
if (this.search.isActive()) {
231230
const matches = this.code.search(this.search.getPattern());
@@ -395,6 +394,7 @@ export class AnycodeEditor {
395394

396395
this.handleClick = this.handleClick.bind(this);
397396
this.codeContent.addEventListener('click', this.handleClick);
397+
this.gutter.addEventListener('click', this.handleClick);
398398

399399
this.handleKeydown = this.handleKeydown.bind(this);
400400
this.codeContent.addEventListener('keydown', this.handleKeydown);
@@ -427,6 +427,7 @@ export class AnycodeEditor {
427427
private removeEventListeners() {
428428
this.container.removeEventListener("scroll", this.handleScroll);
429429
this.codeContent.removeEventListener('click', this.handleClick);
430+
this.gutter.removeEventListener('click', this.handleClick);
430431
this.codeContent.removeEventListener('keydown', this.handleKeydown);
431432
this.codeContent.removeEventListener('paste', this.handlePasteEvent);
432433
this.container.removeEventListener('beforeinput', this.handleBeforeInput);
@@ -493,11 +494,53 @@ export class AnycodeEditor {
493494
this.renderer.renderCursorOrSelection(this.getEditorState());
494495
}
495496

497+
private getDiffGapTarget(target: EventTarget | null): HTMLElement | null {
498+
if (!(target instanceof Element)) {
499+
return null;
500+
}
501+
const match = target.closest('.diff-gap, .diff-gap-expand-btn');
502+
return match instanceof HTMLElement ? match : null;
503+
}
504+
505+
private handleDiffGapExpandClick(e: MouseEvent): boolean {
506+
const gapTarget = this.getDiffGapTarget(e.target);
507+
if (!gapTarget) return false;
508+
509+
e.preventDefault();
510+
e.stopPropagation();
511+
512+
const gapData = getGapElementData(gapTarget);
513+
if (!gapData || gapData.hiddenStart < 0 || gapData.hiddenEnd < gapData.hiddenStart) {
514+
return true;
515+
}
516+
517+
const prevScrollTop = this.container.scrollTop;
518+
const expanded = this.renderer.expandFocusedHiddenRange(
519+
gapData.hiddenStart,
520+
gapData.hiddenEnd,
521+
gapData.expandStep,
522+
gapData.expandDirection,
523+
);
524+
if (expanded) {
525+
this.renderer.render(this.getEditorState(), this.search);
526+
this.container.scrollTop = prevScrollTop;
527+
if (!this.readOnly) {
528+
this.codeContent.focus({ preventScroll: true });
529+
}
530+
}
531+
532+
return true;
533+
}
534+
496535
private handleClick(e: MouseEvent): void {
497536
console.log("click", e);
498537
this.clearPendingHover();
499538
this.closeHover();
500539

540+
if (this.handleDiffGapExpandClick(e)) {
541+
return;
542+
}
543+
501544
const oldCursor = this.code.getPosition(this.offset);
502545

503546
if (this.selection && this.selection.nonEmpty()) { return; }
@@ -612,6 +655,11 @@ export class AnycodeEditor {
612655

613656
private handleMouseDown(e: MouseEvent) {
614657
if (e.button !== 0) return;
658+
if (this.getDiffGapTarget(e.target)) {
659+
e.preventDefault();
660+
e.stopPropagation();
661+
return;
662+
}
615663
if (isInsideDiagnostic(e.target as Node)) return;
616664
e.preventDefault();
617665
this.clearPendingHover();
@@ -937,6 +985,7 @@ export class AnycodeEditor {
937985
};
938986

939987
const result = await executeAction(action, ctx);
988+
this.adjustFocusedDiffNavigationOffset(result, action);
940989
this.applyEditResult(result);
941990

942991
if (this.isCompletionOpen) {
@@ -950,6 +999,60 @@ export class AnycodeEditor {
950999
}
9511000
}
9521001

1002+
private adjustFocusedDiffNavigationOffset(result: ActionResult, action: Action): void {
1003+
if (!this.focusedDiffEnabled) return;
1004+
if (
1005+
action !== Action.ARROW_LEFT
1006+
&& action !== Action.ARROW_RIGHT
1007+
&& action !== Action.ARROW_LEFT_ALT
1008+
&& action !== Action.ARROW_RIGHT_ALT
1009+
&& action !== Action.ARROW_UP
1010+
&& action !== Action.ARROW_DOWN
1011+
) {
1012+
return;
1013+
}
1014+
1015+
const visibleLines = this.renderer.getVisibleRealLineIndices();
1016+
if (visibleLines.size === 0) return;
1017+
const visibleLineList = Array.from(visibleLines);
1018+
1019+
const pos = result.ctx.code.getPosition(result.ctx.offset);
1020+
if (visibleLines.has(pos.line)) return;
1021+
1022+
const preferNext =
1023+
action === Action.ARROW_RIGHT
1024+
|| action === Action.ARROW_RIGHT_ALT
1025+
|| action === Action.ARROW_DOWN;
1026+
1027+
let targetLine: number | null = null;
1028+
if (preferNext) {
1029+
targetLine = visibleLineList.find((line) => line > pos.line) ?? null;
1030+
if (targetLine === null) {
1031+
targetLine = visibleLineList[visibleLineList.length - 1] ?? null;
1032+
}
1033+
} else {
1034+
for (let i = visibleLineList.length - 1; i >= 0; i--) {
1035+
if (visibleLineList[i] < pos.line) {
1036+
targetLine = visibleLineList[i];
1037+
break;
1038+
}
1039+
}
1040+
if (targetLine === null) {
1041+
targetLine = visibleLineList[0] ?? null;
1042+
}
1043+
}
1044+
1045+
if (targetLine === null) return;
1046+
1047+
const targetColumn = Math.min(pos.column, result.ctx.code.lineLength(targetLine));
1048+
const targetOffset = result.ctx.code.getOffset(targetLine, targetColumn);
1049+
result.ctx.offset = targetOffset;
1050+
1051+
if (result.ctx.selection && result.ctx.event?.shiftKey) {
1052+
result.ctx.selection = result.ctx.selection.fromCursor(targetOffset);
1053+
}
1054+
}
1055+
9531056
private getActionFromKey(event: KeyboardEvent): Action | null {
9541057
const { key, altKey, ctrlKey, metaKey, shiftKey } = event;
9551058

@@ -1021,13 +1124,7 @@ export class AnycodeEditor {
10211124

10221125
if (textChanged) {
10231126
this.code = result.ctx.code;
1024-
// calculate diff when text changes
1025-
if (this.diffEnabled && this.originalCode !== undefined) {
1026-
const currentText = this.code.getContent();
1027-
this.diffs = computeGitChanges(this.originalCode, currentText);
1028-
} else {
1029-
this.diffs = undefined;
1030-
}
1127+
this.recomputeDiffs();
10311128
}
10321129
if (offsetChanged) this.offset = result.ctx.offset;
10331130
if (selectionChanged) this.selection = result.ctx.selection || null;
@@ -1392,12 +1489,7 @@ export class AnycodeEditor {
13921489
this.code.setStateAfter(this.offset, this.selection || undefined);
13931490
this.code.commit();
13941491

1395-
if (this.diffEnabled && this.originalCode !== undefined) {
1396-
const currentText = this.code.getContent();
1397-
this.diffs = computeGitChanges(this.originalCode, currentText);
1398-
} else {
1399-
this.diffs = undefined;
1400-
}
1492+
this.recomputeDiffs();
14011493

14021494
this.renderer.renderChanges(this.getEditorState(), this.search);
14031495
this.verifyDiffRendering();
@@ -1411,29 +1503,44 @@ export class AnycodeEditor {
14111503
this.originalCode = this.code.getContent();
14121504
}
14131505

1414-
if (enabled && this.originalCode !== undefined) {
1415-
const currentText = this.code.getContent();
1416-
this.diffs = computeGitChanges(this.originalCode, currentText);
1417-
}
1506+
this.recomputeDiffs();
14181507

14191508
if (!enabled) {
14201509
this.renderer.clearAllDiffs();
1510+
this.renderer.render(this.getEditorState(), this.search);
14211511
} else {
14221512
this.renderer.render(this.getEditorState(), this.search);
14231513
this.verifyDiffRendering();
14241514
}
14251515
}
14261516

1517+
public setFocusedDiffMode(enabled: boolean, contextLines: number = 3): void {
1518+
this.focusedDiffEnabled = enabled;
1519+
this.focusedDiffContextLines = Math.max(0, contextLines);
1520+
this.renderer.setFocusedDiffMode(this.focusedDiffEnabled, this.focusedDiffContextLines);
1521+
this.renderer.render(this.getEditorState(), this.search);
1522+
if (this.diffEnabled) {
1523+
this.verifyDiffRendering();
1524+
}
1525+
}
1526+
14271527
public setOriginalCode(content: string): void {
14281528
this.originalCode = content;
14291529
if (this.diffEnabled) {
1430-
const currentText = this.code.getContent();
1431-
this.diffs = computeGitChanges(this.originalCode, currentText);
1530+
this.recomputeDiffs();
14321531
this.renderer.render(this.getEditorState(), this.search);
14331532
this.verifyDiffRendering();
14341533
}
14351534
}
14361535

1536+
private recomputeDiffs(): void {
1537+
if (this.diffEnabled && this.originalCode !== undefined) {
1538+
this.diffs = computeGitChanges(this.originalCode, this.code.getContent());
1539+
} else {
1540+
this.diffs = undefined;
1541+
}
1542+
}
1543+
14371544
private verifyDiffRendering(): void {
14381545
if (!this.diffEnabled || this.diffs === undefined) {
14391546
return;

0 commit comments

Comments
 (0)