@fresh-editor/fresh-editor 0.1.67 → 0.1.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +2 -0
- package/package.json +1 -1
- package/plugins/audit_mode.i18n.json +380 -0
- package/plugins/audit_mode.ts +836 -68
- package/plugins/buffer_modified.i18n.json +32 -0
- package/plugins/buffer_modified.ts +5 -3
- package/plugins/calculator.i18n.json +44 -0
- package/plugins/calculator.ts +6 -4
- package/plugins/clangd-lsp.ts +2 -0
- package/plugins/clangd_support.i18n.json +104 -0
- package/plugins/clangd_support.ts +18 -16
- package/plugins/color_highlighter.i18n.json +68 -0
- package/plugins/color_highlighter.ts +12 -10
- package/plugins/config-schema.json +25 -140
- package/plugins/csharp-lsp.ts +2 -0
- package/plugins/csharp_support.i18n.json +38 -0
- package/plugins/csharp_support.ts +6 -4
- package/plugins/css-lsp.ts +2 -0
- package/plugins/diagnostics_panel.i18n.json +110 -0
- package/plugins/diagnostics_panel.ts +19 -17
- package/plugins/find_references.i18n.json +128 -0
- package/plugins/find_references.ts +22 -20
- package/plugins/git_blame.i18n.json +230 -0
- package/plugins/git_blame.ts +39 -37
- package/plugins/git_find_file.i18n.json +146 -0
- package/plugins/git_find_file.ts +24 -22
- package/plugins/git_grep.i18n.json +80 -0
- package/plugins/git_grep.ts +15 -13
- package/plugins/git_gutter.i18n.json +44 -0
- package/plugins/git_gutter.ts +7 -5
- package/plugins/git_log.i18n.json +224 -0
- package/plugins/git_log.ts +41 -39
- package/plugins/go-lsp.ts +2 -0
- package/plugins/html-lsp.ts +2 -0
- package/plugins/json-lsp.ts +2 -0
- package/plugins/lib/fresh.d.ts +53 -13
- package/plugins/lib/index.ts +1 -1
- package/plugins/lib/navigation-controller.ts +3 -3
- package/plugins/lib/panel-manager.ts +15 -13
- package/plugins/lib/virtual-buffer-factory.ts +84 -112
- package/plugins/live_grep.i18n.json +80 -0
- package/plugins/live_grep.ts +15 -13
- package/plugins/markdown_compose.i18n.json +104 -0
- package/plugins/markdown_compose.ts +17 -15
- package/plugins/merge_conflict.i18n.json +380 -0
- package/plugins/merge_conflict.ts +72 -73
- package/plugins/path_complete.i18n.json +38 -0
- package/plugins/path_complete.ts +6 -4
- package/plugins/python-lsp.ts +2 -0
- package/plugins/rust-lsp.ts +2 -0
- package/plugins/search_replace.i18n.json +188 -0
- package/plugins/search_replace.ts +31 -29
- package/plugins/test_i18n.i18n.json +12 -0
- package/plugins/test_i18n.ts +18 -0
- package/plugins/theme_editor.i18n.json +1417 -0
- package/plugins/theme_editor.ts +73 -69
- package/plugins/todo_highlighter.i18n.json +86 -0
- package/plugins/todo_highlighter.ts +15 -13
- package/plugins/typescript-lsp.ts +2 -0
- package/plugins/vi_mode.i18n.json +716 -0
- package/plugins/vi_mode.ts +1195 -78
- package/plugins/welcome.i18n.json +110 -0
- package/plugins/welcome.ts +18 -16
package/plugins/audit_mode.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Review Diff Plugin
|
|
2
2
|
// Provides a unified workflow for reviewing code changes (diffs, conflicts, AI outputs).
|
|
3
|
+
const editor = getEditor();
|
|
3
4
|
|
|
4
5
|
/// <reference path="./lib/fresh.d.ts" />
|
|
5
6
|
/// <reference path="./lib/types.ts" />
|
|
6
7
|
/// <reference path="./lib/virtual-buffer-factory.ts" />
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
+
import { createVirtualBufferFactory } from "./lib/virtual-buffer-factory.ts";
|
|
10
|
+
const VirtualBufferFactory = createVirtualBufferFactory(editor);
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Hunk status for staging
|
|
@@ -210,6 +212,7 @@ interface HighlightTask {
|
|
|
210
212
|
bg?: [number, number, number];
|
|
211
213
|
bold?: boolean;
|
|
212
214
|
italic?: boolean;
|
|
215
|
+
extend_to_line_end?: boolean;
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
/**
|
|
@@ -222,31 +225,31 @@ async function renderReviewStream(): Promise<{ entries: TextPropertyEntry[], hig
|
|
|
222
225
|
let currentByte = 0;
|
|
223
226
|
|
|
224
227
|
// Add help header with keybindings at the TOP
|
|
225
|
-
const helpHeader = "
|
|
228
|
+
const helpHeader = "╔" + "═".repeat(74) + "╗\n";
|
|
226
229
|
const helpLen0 = getByteLength(helpHeader);
|
|
227
230
|
entries.push({ text: helpHeader, properties: { type: "help" } });
|
|
228
231
|
highlights.push({ range: [currentByte, currentByte + helpLen0], fg: STYLE_COMMENT_BORDER });
|
|
229
232
|
currentByte += helpLen0;
|
|
230
233
|
|
|
231
|
-
const helpLine1 = "║
|
|
234
|
+
const helpLine1 = "║ " + editor.t("panel.help_review").padEnd(72) + " ║\n";
|
|
232
235
|
const helpLen1 = getByteLength(helpLine1);
|
|
233
236
|
entries.push({ text: helpLine1, properties: { type: "help" } });
|
|
234
237
|
highlights.push({ range: [currentByte, currentByte + helpLen1], fg: STYLE_COMMENT });
|
|
235
238
|
currentByte += helpLen1;
|
|
236
239
|
|
|
237
|
-
const helpLine2 = "║
|
|
240
|
+
const helpLine2 = "║ " + editor.t("panel.help_stage").padEnd(72) + " ║\n";
|
|
238
241
|
const helpLen2 = getByteLength(helpLine2);
|
|
239
242
|
entries.push({ text: helpLine2, properties: { type: "help" } });
|
|
240
243
|
highlights.push({ range: [currentByte, currentByte + helpLen2], fg: STYLE_COMMENT });
|
|
241
244
|
currentByte += helpLen2;
|
|
242
245
|
|
|
243
|
-
const helpLine3 = "║
|
|
246
|
+
const helpLine3 = "║ " + editor.t("panel.help_export").padEnd(72) + " ║\n";
|
|
244
247
|
const helpLen3 = getByteLength(helpLine3);
|
|
245
248
|
entries.push({ text: helpLine3, properties: { type: "help" } });
|
|
246
249
|
highlights.push({ range: [currentByte, currentByte + helpLen3], fg: STYLE_COMMENT });
|
|
247
250
|
currentByte += helpLen3;
|
|
248
251
|
|
|
249
|
-
const helpFooter = "
|
|
252
|
+
const helpFooter = "╚" + "═".repeat(74) + "╝\n\n";
|
|
250
253
|
const helpLen4 = getByteLength(helpFooter);
|
|
251
254
|
entries.push({ text: helpFooter, properties: { type: "help" } });
|
|
252
255
|
highlights.push({ range: [currentByte, currentByte + helpLen4], fg: STYLE_COMMENT_BORDER });
|
|
@@ -509,7 +512,7 @@ async function renderReviewStream(): Promise<{ entries: TextPropertyEntry[], hig
|
|
|
509
512
|
}
|
|
510
513
|
|
|
511
514
|
if (entries.length === 0) {
|
|
512
|
-
entries.push({ text: "
|
|
515
|
+
entries.push({ text: editor.t("panel.no_changes") + "\n", properties: {} });
|
|
513
516
|
} else {
|
|
514
517
|
// Add help footer with keybindings
|
|
515
518
|
const helpSeparator = "\n" + "─".repeat(70) + "\n";
|
|
@@ -518,19 +521,19 @@ async function renderReviewStream(): Promise<{ entries: TextPropertyEntry[], hig
|
|
|
518
521
|
highlights.push({ range: [currentByte, currentByte + helpLen1], fg: STYLE_BORDER });
|
|
519
522
|
currentByte += helpLen1;
|
|
520
523
|
|
|
521
|
-
const helpLine1 = "
|
|
524
|
+
const helpLine1 = editor.t("panel.help_review_footer") + "\n";
|
|
522
525
|
const helpLen2 = getByteLength(helpLine1);
|
|
523
526
|
entries.push({ text: helpLine1, properties: { type: "help" } });
|
|
524
527
|
highlights.push({ range: [currentByte, currentByte + helpLen2], fg: STYLE_COMMENT });
|
|
525
528
|
currentByte += helpLen2;
|
|
526
529
|
|
|
527
|
-
const helpLine2 = "
|
|
530
|
+
const helpLine2 = editor.t("panel.help_stage_footer") + "\n";
|
|
528
531
|
const helpLen3 = getByteLength(helpLine2);
|
|
529
532
|
entries.push({ text: helpLine2, properties: { type: "help" } });
|
|
530
533
|
highlights.push({ range: [currentByte, currentByte + helpLen3], fg: STYLE_COMMENT });
|
|
531
534
|
currentByte += helpLen3;
|
|
532
535
|
|
|
533
|
-
const helpLine3 = "
|
|
536
|
+
const helpLine3 = editor.t("panel.help_export_footer") + "\n";
|
|
534
537
|
const helpLen4 = getByteLength(helpLine3);
|
|
535
538
|
entries.push({ text: helpLine3, properties: { type: "help" } });
|
|
536
539
|
highlights.push({ range: [currentByte, currentByte + helpLen4], fg: STYLE_COMMENT });
|
|
@@ -572,13 +575,13 @@ async function updateReviewUI() {
|
|
|
572
575
|
async function refreshReviewData() {
|
|
573
576
|
if (isUpdating) return;
|
|
574
577
|
isUpdating = true;
|
|
575
|
-
editor.setStatus("
|
|
578
|
+
editor.setStatus(editor.t("status.refreshing"));
|
|
576
579
|
try {
|
|
577
580
|
const newHunks = await getGitDiff();
|
|
578
581
|
newHunks.forEach(h => h.status = state.hunkStatus[h.id] || 'pending');
|
|
579
582
|
state.hunks = newHunks;
|
|
580
583
|
await updateReviewUI();
|
|
581
|
-
editor.setStatus(
|
|
584
|
+
editor.setStatus(editor.t("status.updated", { count: String(state.hunks.length) }));
|
|
582
585
|
} catch (e) {
|
|
583
586
|
editor.debug(`ReviewDiff Error: ${e}`);
|
|
584
587
|
} finally {
|
|
@@ -641,12 +644,371 @@ globalThis.review_refresh = () => { refreshReviewData(); };
|
|
|
641
644
|
|
|
642
645
|
let activeDiffViewState: { lSplit: number, rSplit: number } | null = null;
|
|
643
646
|
|
|
647
|
+
/**
|
|
648
|
+
* Find line number for a given byte offset using binary search
|
|
649
|
+
*/
|
|
650
|
+
function findLineForByte(lineByteOffsets: number[], topByte: number): number {
|
|
651
|
+
let low = 0;
|
|
652
|
+
let high = lineByteOffsets.length - 1;
|
|
653
|
+
while (low < high) {
|
|
654
|
+
const mid = Math.floor((low + high + 1) / 2);
|
|
655
|
+
if (lineByteOffsets[mid] <= topByte) {
|
|
656
|
+
low = mid;
|
|
657
|
+
} else {
|
|
658
|
+
high = mid - 1;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return low;
|
|
662
|
+
}
|
|
663
|
+
|
|
644
664
|
globalThis.on_viewport_changed = (data: any) => {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
665
|
+
// This handler is now a no-op - scroll sync is handled by the core
|
|
666
|
+
// using the anchor-based ScrollSyncGroup system.
|
|
667
|
+
// Keeping the handler for backward compatibility if core sync fails.
|
|
668
|
+
if (!activeDiffViewState || !activeSideBySideState) return;
|
|
669
|
+
|
|
670
|
+
// Skip if core scroll sync is active (we have a scrollSyncGroupId)
|
|
671
|
+
if (activeSideBySideState.scrollSyncGroupId !== null) return;
|
|
672
|
+
|
|
673
|
+
const { oldSplitId, newSplitId, oldLineByteOffsets, newLineByteOffsets } = activeSideBySideState;
|
|
674
|
+
|
|
675
|
+
if (data.split_id === oldSplitId && newLineByteOffsets.length > 0) {
|
|
676
|
+
// OLD pane scrolled - find which line it's on and sync NEW pane to same line
|
|
677
|
+
const lineNum = findLineForByte(oldLineByteOffsets, data.top_byte);
|
|
678
|
+
const targetByte = newLineByteOffsets[Math.min(lineNum, newLineByteOffsets.length - 1)];
|
|
679
|
+
(editor as any).setSplitScroll(newSplitId, targetByte);
|
|
680
|
+
} else if (data.split_id === newSplitId && oldLineByteOffsets.length > 0) {
|
|
681
|
+
// NEW pane scrolled - find which line it's on and sync OLD pane to same line
|
|
682
|
+
const lineNum = findLineForByte(newLineByteOffsets, data.top_byte);
|
|
683
|
+
const targetByte = oldLineByteOffsets[Math.min(lineNum, oldLineByteOffsets.length - 1)];
|
|
684
|
+
(editor as any).setSplitScroll(oldSplitId, targetByte);
|
|
685
|
+
}
|
|
648
686
|
};
|
|
649
687
|
|
|
688
|
+
/**
|
|
689
|
+
* Represents an aligned line pair for side-by-side diff display
|
|
690
|
+
*/
|
|
691
|
+
interface AlignedLine {
|
|
692
|
+
oldLine: string | null; // null means filler line
|
|
693
|
+
newLine: string | null; // null means filler line
|
|
694
|
+
oldLineNum: number | null;
|
|
695
|
+
newLineNum: number | null;
|
|
696
|
+
changeType: 'unchanged' | 'added' | 'removed' | 'modified';
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Parse git diff and compute fully aligned line pairs for side-by-side display.
|
|
701
|
+
* Shows the complete files with proper alignment through all hunks.
|
|
702
|
+
*/
|
|
703
|
+
function computeFullFileAlignedDiff(oldContent: string, newContent: string, hunks: Hunk[]): AlignedLine[] {
|
|
704
|
+
const oldLines = oldContent.split('\n');
|
|
705
|
+
const newLines = newContent.split('\n');
|
|
706
|
+
const aligned: AlignedLine[] = [];
|
|
707
|
+
|
|
708
|
+
// Build a map of changes from all hunks for this file
|
|
709
|
+
// Key: old line number (1-based), Value: { type, newLineNum, content }
|
|
710
|
+
interface ChangeInfo {
|
|
711
|
+
type: 'removed' | 'added' | 'modified' | 'context';
|
|
712
|
+
oldContent?: string;
|
|
713
|
+
newContent?: string;
|
|
714
|
+
newLineNum?: number;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Parse all hunks for this file
|
|
718
|
+
const allHunkChanges: { oldStart: number, newStart: number, changes: { type: 'add' | 'remove' | 'context', content: string }[] }[] = [];
|
|
719
|
+
for (const hunk of hunks) {
|
|
720
|
+
const changes: { type: 'add' | 'remove' | 'context', content: string }[] = [];
|
|
721
|
+
for (const line of hunk.lines) {
|
|
722
|
+
if (line.startsWith('+')) {
|
|
723
|
+
changes.push({ type: 'add', content: line.substring(1) });
|
|
724
|
+
} else if (line.startsWith('-')) {
|
|
725
|
+
changes.push({ type: 'remove', content: line.substring(1) });
|
|
726
|
+
} else if (line.startsWith(' ')) {
|
|
727
|
+
changes.push({ type: 'context', content: line.substring(1) });
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
allHunkChanges.push({
|
|
731
|
+
oldStart: hunk.oldRange.start,
|
|
732
|
+
newStart: hunk.range.start,
|
|
733
|
+
changes
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Sort hunks by old line start
|
|
738
|
+
allHunkChanges.sort((a, b) => a.oldStart - b.oldStart);
|
|
739
|
+
|
|
740
|
+
// Process the file line by line
|
|
741
|
+
let oldIdx = 0; // 0-based index into oldLines
|
|
742
|
+
let newIdx = 0; // 0-based index into newLines
|
|
743
|
+
let hunkIdx = 0;
|
|
744
|
+
|
|
745
|
+
while (oldIdx < oldLines.length || newIdx < newLines.length || hunkIdx < allHunkChanges.length) {
|
|
746
|
+
// Check if we're at a hunk boundary
|
|
747
|
+
const currentHunk = hunkIdx < allHunkChanges.length ? allHunkChanges[hunkIdx] : null;
|
|
748
|
+
|
|
749
|
+
if (currentHunk && oldIdx + 1 === currentHunk.oldStart) {
|
|
750
|
+
// Process this hunk
|
|
751
|
+
let changeIdx = 0;
|
|
752
|
+
while (changeIdx < currentHunk.changes.length) {
|
|
753
|
+
const change = currentHunk.changes[changeIdx];
|
|
754
|
+
|
|
755
|
+
if (change.type === 'context') {
|
|
756
|
+
aligned.push({
|
|
757
|
+
oldLine: oldLines[oldIdx],
|
|
758
|
+
newLine: newLines[newIdx],
|
|
759
|
+
oldLineNum: oldIdx + 1,
|
|
760
|
+
newLineNum: newIdx + 1,
|
|
761
|
+
changeType: 'unchanged'
|
|
762
|
+
});
|
|
763
|
+
oldIdx++;
|
|
764
|
+
newIdx++;
|
|
765
|
+
changeIdx++;
|
|
766
|
+
} else if (change.type === 'remove') {
|
|
767
|
+
// Look ahead to see if next is an 'add' (modification)
|
|
768
|
+
if (changeIdx + 1 < currentHunk.changes.length &&
|
|
769
|
+
currentHunk.changes[changeIdx + 1].type === 'add') {
|
|
770
|
+
// Modified line
|
|
771
|
+
aligned.push({
|
|
772
|
+
oldLine: oldLines[oldIdx],
|
|
773
|
+
newLine: newLines[newIdx],
|
|
774
|
+
oldLineNum: oldIdx + 1,
|
|
775
|
+
newLineNum: newIdx + 1,
|
|
776
|
+
changeType: 'modified'
|
|
777
|
+
});
|
|
778
|
+
oldIdx++;
|
|
779
|
+
newIdx++;
|
|
780
|
+
changeIdx += 2;
|
|
781
|
+
} else {
|
|
782
|
+
// Pure removal
|
|
783
|
+
aligned.push({
|
|
784
|
+
oldLine: oldLines[oldIdx],
|
|
785
|
+
newLine: null,
|
|
786
|
+
oldLineNum: oldIdx + 1,
|
|
787
|
+
newLineNum: null,
|
|
788
|
+
changeType: 'removed'
|
|
789
|
+
});
|
|
790
|
+
oldIdx++;
|
|
791
|
+
changeIdx++;
|
|
792
|
+
}
|
|
793
|
+
} else if (change.type === 'add') {
|
|
794
|
+
// Pure addition
|
|
795
|
+
aligned.push({
|
|
796
|
+
oldLine: null,
|
|
797
|
+
newLine: newLines[newIdx],
|
|
798
|
+
oldLineNum: null,
|
|
799
|
+
newLineNum: newIdx + 1,
|
|
800
|
+
changeType: 'added'
|
|
801
|
+
});
|
|
802
|
+
newIdx++;
|
|
803
|
+
changeIdx++;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
hunkIdx++;
|
|
807
|
+
} else if (oldIdx < oldLines.length && newIdx < newLines.length) {
|
|
808
|
+
// Not in a hunk - add unchanged line
|
|
809
|
+
aligned.push({
|
|
810
|
+
oldLine: oldLines[oldIdx],
|
|
811
|
+
newLine: newLines[newIdx],
|
|
812
|
+
oldLineNum: oldIdx + 1,
|
|
813
|
+
newLineNum: newIdx + 1,
|
|
814
|
+
changeType: 'unchanged'
|
|
815
|
+
});
|
|
816
|
+
oldIdx++;
|
|
817
|
+
newIdx++;
|
|
818
|
+
} else if (oldIdx < oldLines.length) {
|
|
819
|
+
// Only old lines left (shouldn't happen normally)
|
|
820
|
+
aligned.push({
|
|
821
|
+
oldLine: oldLines[oldIdx],
|
|
822
|
+
newLine: null,
|
|
823
|
+
oldLineNum: oldIdx + 1,
|
|
824
|
+
newLineNum: null,
|
|
825
|
+
changeType: 'removed'
|
|
826
|
+
});
|
|
827
|
+
oldIdx++;
|
|
828
|
+
} else if (newIdx < newLines.length) {
|
|
829
|
+
// Only new lines left
|
|
830
|
+
aligned.push({
|
|
831
|
+
oldLine: null,
|
|
832
|
+
newLine: newLines[newIdx],
|
|
833
|
+
oldLineNum: null,
|
|
834
|
+
newLineNum: newIdx + 1,
|
|
835
|
+
changeType: 'added'
|
|
836
|
+
});
|
|
837
|
+
newIdx++;
|
|
838
|
+
} else {
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return aligned;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Generate virtual buffer content with diff highlighting for one side.
|
|
848
|
+
* Returns entries, highlight tasks, and line byte offsets for scroll sync.
|
|
849
|
+
*/
|
|
850
|
+
function generateDiffPaneContent(
|
|
851
|
+
alignedLines: AlignedLine[],
|
|
852
|
+
side: 'old' | 'new'
|
|
853
|
+
): { entries: TextPropertyEntry[], highlights: HighlightTask[], lineByteOffsets: number[] } {
|
|
854
|
+
const entries: TextPropertyEntry[] = [];
|
|
855
|
+
const highlights: HighlightTask[] = [];
|
|
856
|
+
const lineByteOffsets: number[] = [];
|
|
857
|
+
let currentByte = 0;
|
|
858
|
+
|
|
859
|
+
for (const line of alignedLines) {
|
|
860
|
+
lineByteOffsets.push(currentByte);
|
|
861
|
+
const content = side === 'old' ? line.oldLine : line.newLine;
|
|
862
|
+
const lineNum = side === 'old' ? line.oldLineNum : line.newLineNum;
|
|
863
|
+
const isFiller = content === null;
|
|
864
|
+
|
|
865
|
+
// Format: "│ NNN │ content" or "│ │ ~~~~~~~~" for filler
|
|
866
|
+
let lineNumStr: string;
|
|
867
|
+
if (lineNum !== null) {
|
|
868
|
+
lineNumStr = lineNum.toString().padStart(4, ' ');
|
|
869
|
+
} else {
|
|
870
|
+
lineNumStr = ' ';
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Gutter marker based on change type
|
|
874
|
+
let gutterMarker = ' ';
|
|
875
|
+
if (line.changeType === 'added' && side === 'new') gutterMarker = '+';
|
|
876
|
+
else if (line.changeType === 'removed' && side === 'old') gutterMarker = '-';
|
|
877
|
+
else if (line.changeType === 'modified') gutterMarker = '~';
|
|
878
|
+
|
|
879
|
+
let lineText: string;
|
|
880
|
+
if (isFiller) {
|
|
881
|
+
// Filler line for alignment
|
|
882
|
+
lineText = `│${gutterMarker}${lineNumStr} │ ${"░".repeat(40)}\n`;
|
|
883
|
+
} else {
|
|
884
|
+
lineText = `│${gutterMarker}${lineNumStr} │ ${content}\n`;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const lineLen = getByteLength(lineText);
|
|
888
|
+
const prefixLen = getByteLength(`│${gutterMarker}${lineNumStr} │ `);
|
|
889
|
+
|
|
890
|
+
entries.push({
|
|
891
|
+
text: lineText,
|
|
892
|
+
properties: {
|
|
893
|
+
type: 'diff-line',
|
|
894
|
+
changeType: line.changeType,
|
|
895
|
+
lineNum: lineNum,
|
|
896
|
+
side: side
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// Apply colors based on change type
|
|
901
|
+
// Border color
|
|
902
|
+
highlights.push({ range: [currentByte, currentByte + 1], fg: STYLE_BORDER });
|
|
903
|
+
highlights.push({ range: [currentByte + prefixLen - 3, currentByte + prefixLen - 1], fg: STYLE_BORDER });
|
|
904
|
+
|
|
905
|
+
// Line number color
|
|
906
|
+
highlights.push({
|
|
907
|
+
range: [currentByte + 2, currentByte + 6],
|
|
908
|
+
fg: [120, 120, 120] // Gray line numbers
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
if (isFiller) {
|
|
912
|
+
// Filler styling - extend to full line width
|
|
913
|
+
highlights.push({
|
|
914
|
+
range: [currentByte + prefixLen, currentByte + lineLen - 1],
|
|
915
|
+
fg: [60, 60, 60],
|
|
916
|
+
bg: [30, 30, 30],
|
|
917
|
+
extend_to_line_end: true
|
|
918
|
+
});
|
|
919
|
+
} else if (line.changeType === 'added' && side === 'new') {
|
|
920
|
+
// Added line (green) - extend to full line width
|
|
921
|
+
highlights.push({ range: [currentByte + 1, currentByte + 2], fg: STYLE_ADD_TEXT, bold: true }); // gutter marker
|
|
922
|
+
highlights.push({
|
|
923
|
+
range: [currentByte + prefixLen, currentByte + lineLen - 1],
|
|
924
|
+
fg: STYLE_ADD_TEXT,
|
|
925
|
+
bg: [30, 50, 30],
|
|
926
|
+
extend_to_line_end: true
|
|
927
|
+
});
|
|
928
|
+
} else if (line.changeType === 'removed' && side === 'old') {
|
|
929
|
+
// Removed line (red) - extend to full line width
|
|
930
|
+
highlights.push({ range: [currentByte + 1, currentByte + 2], fg: STYLE_REMOVE_TEXT, bold: true }); // gutter marker
|
|
931
|
+
highlights.push({
|
|
932
|
+
range: [currentByte + prefixLen, currentByte + lineLen - 1],
|
|
933
|
+
fg: STYLE_REMOVE_TEXT,
|
|
934
|
+
bg: [50, 30, 30],
|
|
935
|
+
extend_to_line_end: true
|
|
936
|
+
});
|
|
937
|
+
} else if (line.changeType === 'modified') {
|
|
938
|
+
// Modified line - show word-level diff
|
|
939
|
+
const oldText = line.oldLine || '';
|
|
940
|
+
const newText = line.newLine || '';
|
|
941
|
+
const diffParts = diffStrings(oldText, newText);
|
|
942
|
+
|
|
943
|
+
let offset = currentByte + prefixLen;
|
|
944
|
+
if (side === 'old') {
|
|
945
|
+
highlights.push({ range: [currentByte + 1, currentByte + 2], fg: STYLE_REMOVE_TEXT, bold: true });
|
|
946
|
+
// Highlight removed parts in old line
|
|
947
|
+
for (const part of diffParts) {
|
|
948
|
+
const partLen = getByteLength(part.text);
|
|
949
|
+
if (part.type === 'removed') {
|
|
950
|
+
highlights.push({
|
|
951
|
+
range: [offset, offset + partLen],
|
|
952
|
+
fg: STYLE_REMOVE_TEXT,
|
|
953
|
+
bg: STYLE_REMOVE_BG,
|
|
954
|
+
bold: true
|
|
955
|
+
});
|
|
956
|
+
} else if (part.type === 'unchanged') {
|
|
957
|
+
highlights.push({
|
|
958
|
+
range: [offset, offset + partLen],
|
|
959
|
+
fg: STYLE_REMOVE_TEXT
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
if (part.type !== 'added') {
|
|
963
|
+
offset += partLen;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
} else {
|
|
967
|
+
highlights.push({ range: [currentByte + 1, currentByte + 2], fg: STYLE_ADD_TEXT, bold: true });
|
|
968
|
+
// Highlight added parts in new line
|
|
969
|
+
for (const part of diffParts) {
|
|
970
|
+
const partLen = getByteLength(part.text);
|
|
971
|
+
if (part.type === 'added') {
|
|
972
|
+
highlights.push({
|
|
973
|
+
range: [offset, offset + partLen],
|
|
974
|
+
fg: STYLE_ADD_TEXT,
|
|
975
|
+
bg: STYLE_ADD_BG,
|
|
976
|
+
bold: true
|
|
977
|
+
});
|
|
978
|
+
} else if (part.type === 'unchanged') {
|
|
979
|
+
highlights.push({
|
|
980
|
+
range: [offset, offset + partLen],
|
|
981
|
+
fg: STYLE_ADD_TEXT
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
if (part.type !== 'removed') {
|
|
985
|
+
offset += partLen;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
currentByte += lineLen;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return { entries, highlights, lineByteOffsets };
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// State for active side-by-side diff view
|
|
998
|
+
interface SideBySideDiffState {
|
|
999
|
+
oldSplitId: number;
|
|
1000
|
+
newSplitId: number;
|
|
1001
|
+
oldBufferId: number;
|
|
1002
|
+
newBufferId: number;
|
|
1003
|
+
alignedLines: AlignedLine[];
|
|
1004
|
+
oldLineByteOffsets: number[];
|
|
1005
|
+
newLineByteOffsets: number[];
|
|
1006
|
+
scrollSyncGroupId: number | null; // Core scroll sync group ID
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
let activeSideBySideState: SideBySideDiffState | null = null;
|
|
1010
|
+
let nextScrollSyncGroupId = 1;
|
|
1011
|
+
|
|
650
1012
|
globalThis.review_drill_down = async () => {
|
|
651
1013
|
const bid = editor.getActiveBufferId();
|
|
652
1014
|
const props = editor.getTextPropertiesAtCursor(bid);
|
|
@@ -654,39 +1016,201 @@ globalThis.review_drill_down = async () => {
|
|
|
654
1016
|
const id = props[0].hunkId as string;
|
|
655
1017
|
const h = state.hunks.find(x => x.id === id);
|
|
656
1018
|
if (!h) return;
|
|
1019
|
+
|
|
1020
|
+
editor.setStatus(editor.t("status.loading_diff"));
|
|
1021
|
+
|
|
1022
|
+
// Get all hunks for this file
|
|
1023
|
+
const fileHunks = state.hunks.filter(hunk => hunk.file === h.file);
|
|
1024
|
+
|
|
1025
|
+
// Get git root to construct absolute path
|
|
1026
|
+
const gitRootResult = await editor.spawnProcess("git", ["rev-parse", "--show-toplevel"]);
|
|
1027
|
+
if (gitRootResult.exit_code !== 0) {
|
|
1028
|
+
editor.setStatus(editor.t("status.not_git_repo"));
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
const gitRoot = gitRootResult.stdout.trim();
|
|
1032
|
+
const absoluteFilePath = editor.pathJoin(gitRoot, h.file);
|
|
1033
|
+
|
|
1034
|
+
// Get old (HEAD) and new (working) file content
|
|
657
1035
|
const gitShow = await editor.spawnProcess("git", ["show", `HEAD:${h.file}`]);
|
|
658
|
-
if (gitShow.exit_code !== 0)
|
|
1036
|
+
if (gitShow.exit_code !== 0) {
|
|
1037
|
+
editor.setStatus(editor.t("status.failed_old_version"));
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
const oldContent = gitShow.stdout;
|
|
1041
|
+
|
|
1042
|
+
// Read new file content (use absolute path for readFile)
|
|
1043
|
+
let newContent: string;
|
|
1044
|
+
try {
|
|
1045
|
+
newContent = await editor.readFile(absoluteFilePath);
|
|
1046
|
+
} catch (e) {
|
|
1047
|
+
editor.setStatus(editor.t("status.failed_new_version"));
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Close the Review Diff buffer to make room for side-by-side view
|
|
1052
|
+
// Store the review buffer ID so we can restore it later
|
|
1053
|
+
const reviewBufferId = bid;
|
|
659
1054
|
|
|
660
|
-
//
|
|
661
|
-
|
|
1055
|
+
// Compute aligned diff for the FULL file with all hunks
|
|
1056
|
+
const alignedLines = computeFullFileAlignedDiff(oldContent, newContent, fileHunks);
|
|
662
1057
|
|
|
663
|
-
//
|
|
664
|
-
|
|
665
|
-
const
|
|
1058
|
+
// Generate content for both panes
|
|
1059
|
+
const oldPane = generateDiffPaneContent(alignedLines, 'old');
|
|
1060
|
+
const newPane = generateDiffPaneContent(alignedLines, 'new');
|
|
666
1061
|
|
|
667
|
-
//
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1062
|
+
// Close any existing side-by-side views
|
|
1063
|
+
if (activeSideBySideState) {
|
|
1064
|
+
try {
|
|
1065
|
+
// Remove scroll sync group first
|
|
1066
|
+
if (activeSideBySideState.scrollSyncGroupId !== null) {
|
|
1067
|
+
(editor as any).removeScrollSyncGroup(activeSideBySideState.scrollSyncGroupId);
|
|
1068
|
+
}
|
|
1069
|
+
editor.closeBuffer(activeSideBySideState.oldBufferId);
|
|
1070
|
+
editor.closeBuffer(activeSideBySideState.newBufferId);
|
|
1071
|
+
} catch {}
|
|
1072
|
+
activeSideBySideState = null;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Get the current split ID before closing the Review Diff buffer
|
|
1076
|
+
const currentSplitId = (editor as any).getActiveSplitId();
|
|
1077
|
+
|
|
1078
|
+
// Create OLD buffer in the CURRENT split (replaces Review Diff)
|
|
1079
|
+
const oldBufferId = await editor.createVirtualBufferInExistingSplit({
|
|
1080
|
+
name: `[OLD] ${h.file}`,
|
|
1081
|
+
mode: "diff-view",
|
|
672
1082
|
read_only: true,
|
|
673
1083
|
editing_disabled: true,
|
|
674
|
-
entries:
|
|
1084
|
+
entries: oldPane.entries,
|
|
1085
|
+
split_id: currentSplitId,
|
|
1086
|
+
show_line_numbers: false,
|
|
1087
|
+
line_wrap: false
|
|
1088
|
+
});
|
|
1089
|
+
const oldSplitId = currentSplitId;
|
|
1090
|
+
|
|
1091
|
+
// Close the Review Diff buffer after showing OLD
|
|
1092
|
+
editor.closeBuffer(reviewBufferId);
|
|
1093
|
+
|
|
1094
|
+
// Apply highlights to old pane
|
|
1095
|
+
editor.clearNamespace(oldBufferId, "diff-view");
|
|
1096
|
+
for (const hl of oldPane.highlights) {
|
|
1097
|
+
const bg = hl.bg || [-1, -1, -1];
|
|
1098
|
+
editor.addOverlay(
|
|
1099
|
+
oldBufferId, "diff-view",
|
|
1100
|
+
hl.range[0], hl.range[1],
|
|
1101
|
+
hl.fg[0], hl.fg[1], hl.fg[2],
|
|
1102
|
+
false, hl.bold || false, false,
|
|
1103
|
+
bg[0], bg[1], bg[2],
|
|
1104
|
+
hl.extend_to_line_end || false
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Step 2: Create NEW pane in a vertical split (RIGHT of OLD)
|
|
1109
|
+
const newRes = await editor.createVirtualBufferInSplit({
|
|
1110
|
+
name: `[NEW] ${h.file}`,
|
|
1111
|
+
mode: "diff-view",
|
|
1112
|
+
read_only: true,
|
|
1113
|
+
editing_disabled: true,
|
|
1114
|
+
entries: newPane.entries,
|
|
675
1115
|
ratio: 0.5,
|
|
676
1116
|
direction: "vertical",
|
|
677
|
-
show_line_numbers:
|
|
1117
|
+
show_line_numbers: false,
|
|
1118
|
+
line_wrap: false
|
|
678
1119
|
});
|
|
679
|
-
const
|
|
1120
|
+
const newBufferId = newRes.buffer_id;
|
|
1121
|
+
const newSplitId = newRes.split_id!;
|
|
1122
|
+
|
|
1123
|
+
// Apply highlights to new pane
|
|
1124
|
+
editor.clearNamespace(newBufferId, "diff-view");
|
|
1125
|
+
for (const hl of newPane.highlights) {
|
|
1126
|
+
const bg = hl.bg || [-1, -1, -1];
|
|
1127
|
+
editor.addOverlay(
|
|
1128
|
+
newBufferId, "diff-view",
|
|
1129
|
+
hl.range[0], hl.range[1],
|
|
1130
|
+
hl.fg[0], hl.fg[1], hl.fg[2],
|
|
1131
|
+
false, hl.bold || false, false,
|
|
1132
|
+
bg[0], bg[1], bg[2],
|
|
1133
|
+
hl.extend_to_line_end || false
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
680
1136
|
|
|
681
|
-
// Focus
|
|
682
|
-
(editor as any).focusSplit(
|
|
1137
|
+
// Focus OLD pane (left) - convention is to start on old side
|
|
1138
|
+
(editor as any).focusSplit(oldSplitId);
|
|
1139
|
+
|
|
1140
|
+
// Set up core-handled scroll sync using the new anchor-based API
|
|
1141
|
+
// This replaces the old viewport_changed hook approach
|
|
1142
|
+
let scrollSyncGroupId: number | null = null;
|
|
1143
|
+
try {
|
|
1144
|
+
// Generate a unique group ID
|
|
1145
|
+
scrollSyncGroupId = nextScrollSyncGroupId++;
|
|
1146
|
+
(editor as any).createScrollSyncGroup(scrollSyncGroupId, oldSplitId, newSplitId);
|
|
1147
|
+
|
|
1148
|
+
// Compute sync anchors from aligned lines
|
|
1149
|
+
// Each aligned line is a sync point - we map line indices to anchors
|
|
1150
|
+
// For the new core sync, we use line numbers (not byte offsets)
|
|
1151
|
+
const anchors: [number, number][] = [];
|
|
1152
|
+
for (let i = 0; i < alignedLines.length; i++) {
|
|
1153
|
+
// Add anchors at meaningful boundaries: start of file, and at change boundaries
|
|
1154
|
+
const line = alignedLines[i];
|
|
1155
|
+
const prevLine = i > 0 ? alignedLines[i - 1] : null;
|
|
1156
|
+
|
|
1157
|
+
// Add anchor at start of file
|
|
1158
|
+
if (i === 0) {
|
|
1159
|
+
anchors.push([0, 0]);
|
|
1160
|
+
}
|
|
683
1161
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
1162
|
+
// Add anchor at change boundaries (when change type changes)
|
|
1163
|
+
if (prevLine && prevLine.changeType !== line.changeType) {
|
|
1164
|
+
anchors.push([i, i]);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Add anchor at end
|
|
1169
|
+
if (alignedLines.length > 0) {
|
|
1170
|
+
anchors.push([alignedLines.length, alignedLines.length]);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
(editor as any).setScrollSyncAnchors(scrollSyncGroupId, anchors);
|
|
1174
|
+
} catch (e) {
|
|
1175
|
+
editor.debug(`Failed to create scroll sync group: ${e}`);
|
|
1176
|
+
scrollSyncGroupId = null;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Store state for synchronized scrolling
|
|
1180
|
+
activeSideBySideState = {
|
|
1181
|
+
oldSplitId,
|
|
1182
|
+
newSplitId,
|
|
1183
|
+
oldBufferId,
|
|
1184
|
+
newBufferId,
|
|
1185
|
+
alignedLines,
|
|
1186
|
+
oldLineByteOffsets: oldPane.lineByteOffsets,
|
|
1187
|
+
newLineByteOffsets: newPane.lineByteOffsets,
|
|
1188
|
+
scrollSyncGroupId
|
|
1189
|
+
};
|
|
1190
|
+
activeDiffViewState = { lSplit: oldSplitId, rSplit: newSplitId };
|
|
1191
|
+
|
|
1192
|
+
const addedLines = alignedLines.filter(l => l.changeType === 'added').length;
|
|
1193
|
+
const removedLines = alignedLines.filter(l => l.changeType === 'removed').length;
|
|
1194
|
+
const modifiedLines = alignedLines.filter(l => l.changeType === 'modified').length;
|
|
1195
|
+
editor.setStatus(editor.t("status.diff_summary", { added: String(addedLines), removed: String(removedLines), modified: String(modifiedLines) }));
|
|
687
1196
|
}
|
|
688
1197
|
};
|
|
689
1198
|
|
|
1199
|
+
// Define the diff-view mode with navigation keys
|
|
1200
|
+
editor.defineMode("diff-view", "special", [
|
|
1201
|
+
["q", "close_buffer"],
|
|
1202
|
+
["j", "move_down"],
|
|
1203
|
+
["k", "move_up"],
|
|
1204
|
+
["g", "move_document_start"],
|
|
1205
|
+
["G", "move_document_end"],
|
|
1206
|
+
["Ctrl+d", "move_page_down"],
|
|
1207
|
+
["Ctrl+u", "move_page_up"],
|
|
1208
|
+
["Down", "move_down"],
|
|
1209
|
+
["Up", "move_up"],
|
|
1210
|
+
["PageDown", "move_page_down"],
|
|
1211
|
+
["PageUp", "move_page_up"],
|
|
1212
|
+
], true);
|
|
1213
|
+
|
|
690
1214
|
// --- Review Comment Actions ---
|
|
691
1215
|
|
|
692
1216
|
function getCurrentHunkId(): string | null {
|
|
@@ -728,7 +1252,7 @@ let pendingCommentInfo: PendingCommentInfo | null = null;
|
|
|
728
1252
|
globalThis.review_add_comment = async () => {
|
|
729
1253
|
const info = getCurrentLineInfo();
|
|
730
1254
|
if (!info) {
|
|
731
|
-
editor.setStatus("
|
|
1255
|
+
editor.setStatus(editor.t("status.no_hunk_selected"));
|
|
732
1256
|
return;
|
|
733
1257
|
}
|
|
734
1258
|
pendingCommentInfo = info;
|
|
@@ -744,7 +1268,7 @@ globalThis.review_add_comment = async () => {
|
|
|
744
1268
|
} else if (info.oldLine) {
|
|
745
1269
|
lineRef = `L${info.oldLine}`;
|
|
746
1270
|
}
|
|
747
|
-
editor.startPrompt(
|
|
1271
|
+
editor.startPrompt(editor.t("prompt.comment", { line: lineRef }), "review-comment");
|
|
748
1272
|
};
|
|
749
1273
|
|
|
750
1274
|
// Prompt event handlers
|
|
@@ -776,7 +1300,7 @@ globalThis.on_review_prompt_confirm = (args: { prompt_type: string; input: strin
|
|
|
776
1300
|
} else if (comment.old_line) {
|
|
777
1301
|
lineRef = `line ${comment.old_line}`;
|
|
778
1302
|
}
|
|
779
|
-
editor.setStatus(
|
|
1303
|
+
editor.setStatus(editor.t("status.comment_added", { line: lineRef }));
|
|
780
1304
|
}
|
|
781
1305
|
pendingCommentInfo = null;
|
|
782
1306
|
return true;
|
|
@@ -785,7 +1309,7 @@ globalThis.on_review_prompt_confirm = (args: { prompt_type: string; input: strin
|
|
|
785
1309
|
globalThis.on_review_prompt_cancel = (args: { prompt_type: string }): boolean => {
|
|
786
1310
|
if (args.prompt_type === "review-comment") {
|
|
787
1311
|
pendingCommentInfo = null;
|
|
788
|
-
editor.setStatus("
|
|
1312
|
+
editor.setStatus(editor.t("status.comment_cancelled"));
|
|
789
1313
|
}
|
|
790
1314
|
return true;
|
|
791
1315
|
};
|
|
@@ -801,7 +1325,7 @@ globalThis.review_approve_hunk = async () => {
|
|
|
801
1325
|
if (h) {
|
|
802
1326
|
h.reviewStatus = 'approved';
|
|
803
1327
|
await updateReviewUI();
|
|
804
|
-
editor.setStatus(
|
|
1328
|
+
editor.setStatus(editor.t("status.hunk_approved"));
|
|
805
1329
|
}
|
|
806
1330
|
};
|
|
807
1331
|
|
|
@@ -812,7 +1336,7 @@ globalThis.review_reject_hunk = async () => {
|
|
|
812
1336
|
if (h) {
|
|
813
1337
|
h.reviewStatus = 'rejected';
|
|
814
1338
|
await updateReviewUI();
|
|
815
|
-
editor.setStatus(
|
|
1339
|
+
editor.setStatus(editor.t("status.hunk_rejected"));
|
|
816
1340
|
}
|
|
817
1341
|
};
|
|
818
1342
|
|
|
@@ -823,7 +1347,7 @@ globalThis.review_needs_changes = async () => {
|
|
|
823
1347
|
if (h) {
|
|
824
1348
|
h.reviewStatus = 'needs_changes';
|
|
825
1349
|
await updateReviewUI();
|
|
826
|
-
editor.setStatus(
|
|
1350
|
+
editor.setStatus(editor.t("status.hunk_needs_changes"));
|
|
827
1351
|
}
|
|
828
1352
|
};
|
|
829
1353
|
|
|
@@ -834,7 +1358,7 @@ globalThis.review_question_hunk = async () => {
|
|
|
834
1358
|
if (h) {
|
|
835
1359
|
h.reviewStatus = 'question';
|
|
836
1360
|
await updateReviewUI();
|
|
837
|
-
editor.setStatus(
|
|
1361
|
+
editor.setStatus(editor.t("status.hunk_question"));
|
|
838
1362
|
}
|
|
839
1363
|
};
|
|
840
1364
|
|
|
@@ -845,15 +1369,15 @@ globalThis.review_clear_status = async () => {
|
|
|
845
1369
|
if (h) {
|
|
846
1370
|
h.reviewStatus = 'pending';
|
|
847
1371
|
await updateReviewUI();
|
|
848
|
-
editor.setStatus(
|
|
1372
|
+
editor.setStatus(editor.t("status.hunk_status_cleared"));
|
|
849
1373
|
}
|
|
850
1374
|
};
|
|
851
1375
|
|
|
852
1376
|
globalThis.review_set_overall_feedback = async () => {
|
|
853
|
-
const text = await editor.prompt("
|
|
1377
|
+
const text = await editor.prompt(editor.t("prompt.overall_feedback"), state.overallFeedback || "");
|
|
854
1378
|
if (text !== null) {
|
|
855
1379
|
state.overallFeedback = text.trim();
|
|
856
|
-
editor.setStatus(
|
|
1380
|
+
editor.setStatus(text.trim() ? editor.t("status.feedback_set") : editor.t("status.feedback_cleared"));
|
|
857
1381
|
}
|
|
858
1382
|
};
|
|
859
1383
|
|
|
@@ -861,10 +1385,7 @@ globalThis.review_export_session = async () => {
|
|
|
861
1385
|
const cwd = editor.getCwd();
|
|
862
1386
|
const reviewDir = editor.pathJoin(cwd, ".review");
|
|
863
1387
|
|
|
864
|
-
//
|
|
865
|
-
await editor.spawnProcess("mkdir", ["-p", reviewDir]);
|
|
866
|
-
|
|
867
|
-
// Generate markdown content
|
|
1388
|
+
// Generate markdown content (writeFile creates parent directories)
|
|
868
1389
|
let md = `# Code Review Session\n`;
|
|
869
1390
|
md += `Date: ${new Date().toISOString()}\n\n`;
|
|
870
1391
|
|
|
@@ -930,13 +1451,13 @@ globalThis.review_export_session = async () => {
|
|
|
930
1451
|
// Write file
|
|
931
1452
|
const filePath = editor.pathJoin(reviewDir, "session.md");
|
|
932
1453
|
await editor.writeFile(filePath, md);
|
|
933
|
-
editor.setStatus(
|
|
1454
|
+
editor.setStatus(editor.t("status.exported", { path: filePath }));
|
|
934
1455
|
};
|
|
935
1456
|
|
|
936
1457
|
globalThis.review_export_json = async () => {
|
|
937
1458
|
const cwd = editor.getCwd();
|
|
938
1459
|
const reviewDir = editor.pathJoin(cwd, ".review");
|
|
939
|
-
|
|
1460
|
+
// writeFile creates parent directories
|
|
940
1461
|
|
|
941
1462
|
const session = {
|
|
942
1463
|
version: "1.0",
|
|
@@ -966,11 +1487,11 @@ globalThis.review_export_json = async () => {
|
|
|
966
1487
|
|
|
967
1488
|
const filePath = editor.pathJoin(reviewDir, "session.json");
|
|
968
1489
|
await editor.writeFile(filePath, JSON.stringify(session, null, 2));
|
|
969
|
-
editor.setStatus(
|
|
1490
|
+
editor.setStatus(editor.t("status.exported", { path: filePath }));
|
|
970
1491
|
};
|
|
971
1492
|
|
|
972
1493
|
globalThis.start_review_diff = async () => {
|
|
973
|
-
editor.setStatus("
|
|
1494
|
+
editor.setStatus(editor.t("status.generating"));
|
|
974
1495
|
editor.setContext("review-mode", true);
|
|
975
1496
|
|
|
976
1497
|
// Initial data fetch
|
|
@@ -985,7 +1506,7 @@ globalThis.start_review_diff = async () => {
|
|
|
985
1506
|
state.reviewBufferId = bufferId;
|
|
986
1507
|
await updateReviewUI(); // Apply initial highlights
|
|
987
1508
|
|
|
988
|
-
editor.setStatus(
|
|
1509
|
+
editor.setStatus(editor.t("status.review_summary", { count: String(state.hunks.length) }));
|
|
989
1510
|
editor.on("buffer_activated", "on_review_buffer_activated");
|
|
990
1511
|
editor.on("buffer_closed", "on_review_buffer_closed");
|
|
991
1512
|
};
|
|
@@ -995,9 +1516,10 @@ globalThis.stop_review_diff = () => {
|
|
|
995
1516
|
editor.setContext("review-mode", false);
|
|
996
1517
|
editor.off("buffer_activated", "on_review_buffer_activated");
|
|
997
1518
|
editor.off("buffer_closed", "on_review_buffer_closed");
|
|
998
|
-
editor.setStatus("
|
|
1519
|
+
editor.setStatus(editor.t("status.stopped"));
|
|
999
1520
|
};
|
|
1000
1521
|
|
|
1522
|
+
|
|
1001
1523
|
globalThis.on_review_buffer_activated = (data: any) => {
|
|
1002
1524
|
if (data.buffer_id === state.reviewBufferId) refreshReviewData();
|
|
1003
1525
|
};
|
|
@@ -1006,21 +1528,267 @@ globalThis.on_review_buffer_closed = (data: any) => {
|
|
|
1006
1528
|
if (data.buffer_id === state.reviewBufferId) stop_review_diff();
|
|
1007
1529
|
};
|
|
1008
1530
|
|
|
1531
|
+
// Side-by-side diff for current file (can be called directly without Review Diff mode)
|
|
1532
|
+
globalThis.side_by_side_diff_current_file = async () => {
|
|
1533
|
+
const bid = editor.getActiveBufferId();
|
|
1534
|
+
const absolutePath = editor.getBufferPath(bid);
|
|
1535
|
+
|
|
1536
|
+
if (!absolutePath) {
|
|
1537
|
+
editor.setStatus(editor.t("status.no_file_open"));
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
editor.setStatus(editor.t("status.loading_diff"));
|
|
1542
|
+
|
|
1543
|
+
// Get the file's directory and name for running git commands
|
|
1544
|
+
const fileDir = editor.pathDirname(absolutePath);
|
|
1545
|
+
const fileName = editor.pathBasename(absolutePath);
|
|
1546
|
+
|
|
1547
|
+
// Run git commands from the file's directory to avoid path format issues on Windows
|
|
1548
|
+
const gitRootResult = await editor.spawnProcess("git", ["-C", fileDir, "rev-parse", "--show-toplevel"]);
|
|
1549
|
+
if (gitRootResult.exit_code !== 0) {
|
|
1550
|
+
editor.setStatus(editor.t("status.not_git_repo"));
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
const gitRoot = gitRootResult.stdout.trim();
|
|
1554
|
+
|
|
1555
|
+
// Get relative path from git root using git itself (handles Windows paths correctly)
|
|
1556
|
+
const relPathResult = await editor.spawnProcess("git", ["-C", fileDir, "ls-files", "--full-name", fileName]);
|
|
1557
|
+
let filePath: string;
|
|
1558
|
+
if (relPathResult.exit_code === 0 && relPathResult.stdout.trim()) {
|
|
1559
|
+
filePath = relPathResult.stdout.trim();
|
|
1560
|
+
} else {
|
|
1561
|
+
// File might be untracked, compute relative path manually
|
|
1562
|
+
// Normalize paths: replace backslashes with forward slashes for comparison
|
|
1563
|
+
const normAbsPath = absolutePath.replace(/\\/g, '/');
|
|
1564
|
+
const normGitRoot = gitRoot.replace(/\\/g, '/');
|
|
1565
|
+
if (normAbsPath.toLowerCase().startsWith(normGitRoot.toLowerCase())) {
|
|
1566
|
+
filePath = normAbsPath.substring(normGitRoot.length + 1);
|
|
1567
|
+
} else {
|
|
1568
|
+
// Fallback to just the filename
|
|
1569
|
+
filePath = fileName;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// Get hunks for this specific file (use -C gitRoot since filePath is relative to git root)
|
|
1574
|
+
const result = await editor.spawnProcess("git", ["-C", gitRoot, "diff", "HEAD", "--unified=3", "--", filePath]);
|
|
1575
|
+
if (result.exit_code !== 0) {
|
|
1576
|
+
editor.setStatus(editor.t("status.failed_git_diff"));
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// Parse hunks from diff output
|
|
1581
|
+
const lines = result.stdout.split('\n');
|
|
1582
|
+
const fileHunks: Hunk[] = [];
|
|
1583
|
+
let currentHunk: Hunk | null = null;
|
|
1584
|
+
|
|
1585
|
+
for (const line of lines) {
|
|
1586
|
+
if (line.startsWith('@@')) {
|
|
1587
|
+
const match = line.match(/@@ -(\d+),?\d* \+(\d+),?\d* @@(.*)/);
|
|
1588
|
+
if (match) {
|
|
1589
|
+
const oldStart = parseInt(match[1]);
|
|
1590
|
+
const newStart = parseInt(match[2]);
|
|
1591
|
+
currentHunk = {
|
|
1592
|
+
id: `${filePath}:${newStart}`,
|
|
1593
|
+
file: filePath,
|
|
1594
|
+
range: { start: newStart, end: newStart },
|
|
1595
|
+
oldRange: { start: oldStart, end: oldStart },
|
|
1596
|
+
type: 'modify',
|
|
1597
|
+
lines: [],
|
|
1598
|
+
status: 'pending',
|
|
1599
|
+
reviewStatus: 'pending',
|
|
1600
|
+
contextHeader: match[3]?.trim() || "",
|
|
1601
|
+
byteOffset: 0
|
|
1602
|
+
};
|
|
1603
|
+
fileHunks.push(currentHunk);
|
|
1604
|
+
}
|
|
1605
|
+
} else if (currentHunk && (line.startsWith('+') || line.startsWith('-') || line.startsWith(' '))) {
|
|
1606
|
+
if (!line.startsWith('---') && !line.startsWith('+++')) {
|
|
1607
|
+
currentHunk.lines.push(line);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
if (fileHunks.length === 0) {
|
|
1613
|
+
editor.setStatus(editor.t("status.no_changes"));
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// Get old (HEAD) and new (working) file content (use -C gitRoot since filePath is relative to git root)
|
|
1618
|
+
const gitShow = await editor.spawnProcess("git", ["-C", gitRoot, "show", `HEAD:${filePath}`]);
|
|
1619
|
+
if (gitShow.exit_code !== 0) {
|
|
1620
|
+
editor.setStatus(editor.t("status.failed_old_new_file"));
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
const oldContent = gitShow.stdout;
|
|
1624
|
+
|
|
1625
|
+
// Read new file content (use absolute path for readFile)
|
|
1626
|
+
let newContent: string;
|
|
1627
|
+
try {
|
|
1628
|
+
newContent = await editor.readFile(absolutePath);
|
|
1629
|
+
} catch (e) {
|
|
1630
|
+
editor.setStatus(editor.t("status.failed_new_version"));
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// Compute aligned diff for the FULL file
|
|
1635
|
+
const alignedLines = computeFullFileAlignedDiff(oldContent, newContent, fileHunks);
|
|
1636
|
+
|
|
1637
|
+
// Generate content for both panes
|
|
1638
|
+
const oldPane = generateDiffPaneContent(alignedLines, 'old');
|
|
1639
|
+
const newPane = generateDiffPaneContent(alignedLines, 'new');
|
|
1640
|
+
|
|
1641
|
+
// Close any existing side-by-side views
|
|
1642
|
+
if (activeSideBySideState) {
|
|
1643
|
+
try {
|
|
1644
|
+
if (activeSideBySideState.scrollSyncGroupId !== null) {
|
|
1645
|
+
(editor as any).removeScrollSyncGroup(activeSideBySideState.scrollSyncGroupId);
|
|
1646
|
+
}
|
|
1647
|
+
editor.closeBuffer(activeSideBySideState.oldBufferId);
|
|
1648
|
+
editor.closeBuffer(activeSideBySideState.newBufferId);
|
|
1649
|
+
} catch {}
|
|
1650
|
+
activeSideBySideState = null;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// Get the current split ID
|
|
1654
|
+
const currentSplitId = (editor as any).getActiveSplitId();
|
|
1655
|
+
|
|
1656
|
+
// Create OLD buffer in the CURRENT split
|
|
1657
|
+
const oldBufferId = await editor.createVirtualBufferInExistingSplit({
|
|
1658
|
+
name: `[OLD] ${filePath}`,
|
|
1659
|
+
mode: "diff-view",
|
|
1660
|
+
read_only: true,
|
|
1661
|
+
editing_disabled: true,
|
|
1662
|
+
entries: oldPane.entries,
|
|
1663
|
+
split_id: currentSplitId,
|
|
1664
|
+
show_line_numbers: false,
|
|
1665
|
+
line_wrap: false
|
|
1666
|
+
});
|
|
1667
|
+
const oldSplitId = currentSplitId;
|
|
1668
|
+
|
|
1669
|
+
// Apply highlights to old pane
|
|
1670
|
+
editor.clearNamespace(oldBufferId, "diff-view");
|
|
1671
|
+
for (const hl of oldPane.highlights) {
|
|
1672
|
+
const bg = hl.bg || [-1, -1, -1];
|
|
1673
|
+
editor.addOverlay(
|
|
1674
|
+
oldBufferId, "diff-view",
|
|
1675
|
+
hl.range[0], hl.range[1],
|
|
1676
|
+
hl.fg[0], hl.fg[1], hl.fg[2],
|
|
1677
|
+
false, hl.bold || false, false,
|
|
1678
|
+
bg[0], bg[1], bg[2],
|
|
1679
|
+
hl.extend_to_line_end || false
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// Create NEW pane in a vertical split (RIGHT of OLD)
|
|
1684
|
+
const newRes = await editor.createVirtualBufferInSplit({
|
|
1685
|
+
name: `[NEW] ${filePath}`,
|
|
1686
|
+
mode: "diff-view",
|
|
1687
|
+
read_only: true,
|
|
1688
|
+
editing_disabled: true,
|
|
1689
|
+
entries: newPane.entries,
|
|
1690
|
+
ratio: 0.5,
|
|
1691
|
+
direction: "vertical",
|
|
1692
|
+
show_line_numbers: false,
|
|
1693
|
+
line_wrap: false
|
|
1694
|
+
});
|
|
1695
|
+
const newBufferId = newRes.buffer_id;
|
|
1696
|
+
const newSplitId = newRes.split_id!;
|
|
1697
|
+
|
|
1698
|
+
// Apply highlights to new pane
|
|
1699
|
+
editor.clearNamespace(newBufferId, "diff-view");
|
|
1700
|
+
for (const hl of newPane.highlights) {
|
|
1701
|
+
const bg = hl.bg || [-1, -1, -1];
|
|
1702
|
+
editor.addOverlay(
|
|
1703
|
+
newBufferId, "diff-view",
|
|
1704
|
+
hl.range[0], hl.range[1],
|
|
1705
|
+
hl.fg[0], hl.fg[1], hl.fg[2],
|
|
1706
|
+
false, hl.bold || false, false,
|
|
1707
|
+
bg[0], bg[1], bg[2],
|
|
1708
|
+
hl.extend_to_line_end || false
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// Focus OLD pane (left)
|
|
1713
|
+
(editor as any).focusSplit(oldSplitId);
|
|
1714
|
+
|
|
1715
|
+
// Set up scroll sync
|
|
1716
|
+
let scrollSyncGroupId: number | null = null;
|
|
1717
|
+
try {
|
|
1718
|
+
scrollSyncGroupId = nextScrollSyncGroupId++;
|
|
1719
|
+
(editor as any).createScrollSyncGroup(scrollSyncGroupId, oldSplitId, newSplitId);
|
|
1720
|
+
|
|
1721
|
+
const anchors: [number, number][] = [];
|
|
1722
|
+
for (let i = 0; i < alignedLines.length; i++) {
|
|
1723
|
+
const line = alignedLines[i];
|
|
1724
|
+
const prevLine = i > 0 ? alignedLines[i - 1] : null;
|
|
1725
|
+
if (i === 0) anchors.push([0, 0]);
|
|
1726
|
+
if (prevLine && prevLine.changeType !== line.changeType) {
|
|
1727
|
+
anchors.push([i, i]);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
if (alignedLines.length > 0) {
|
|
1731
|
+
anchors.push([alignedLines.length, alignedLines.length]);
|
|
1732
|
+
}
|
|
1733
|
+
(editor as any).setScrollSyncAnchors(scrollSyncGroupId, anchors);
|
|
1734
|
+
} catch (e) {
|
|
1735
|
+
editor.debug(`Failed to create scroll sync group: ${e}`);
|
|
1736
|
+
scrollSyncGroupId = null;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// Store state
|
|
1740
|
+
activeSideBySideState = {
|
|
1741
|
+
oldSplitId,
|
|
1742
|
+
newSplitId,
|
|
1743
|
+
oldBufferId,
|
|
1744
|
+
newBufferId,
|
|
1745
|
+
alignedLines,
|
|
1746
|
+
oldLineByteOffsets: oldPane.lineByteOffsets,
|
|
1747
|
+
newLineByteOffsets: newPane.lineByteOffsets,
|
|
1748
|
+
scrollSyncGroupId
|
|
1749
|
+
};
|
|
1750
|
+
activeDiffViewState = { lSplit: oldSplitId, rSplit: newSplitId };
|
|
1751
|
+
|
|
1752
|
+
const addedLines = alignedLines.filter(l => l.changeType === 'added').length;
|
|
1753
|
+
const removedLines = alignedLines.filter(l => l.changeType === 'removed').length;
|
|
1754
|
+
const modifiedLines = alignedLines.filter(l => l.changeType === 'modified').length;
|
|
1755
|
+
editor.setStatus(editor.t("status.diff_summary", { added: String(addedLines), removed: String(removedLines), modified: String(modifiedLines) }));
|
|
1756
|
+
};
|
|
1757
|
+
|
|
1009
1758
|
// Register Modes and Commands
|
|
1010
|
-
editor.registerCommand("
|
|
1011
|
-
editor.registerCommand("
|
|
1012
|
-
editor.registerCommand("
|
|
1759
|
+
editor.registerCommand("%cmd.review_diff", "%cmd.review_diff_desc", "start_review_diff", "global");
|
|
1760
|
+
editor.registerCommand("%cmd.stop_review_diff", "%cmd.stop_review_diff_desc", "stop_review_diff", "review-mode");
|
|
1761
|
+
editor.registerCommand("%cmd.refresh_review_diff", "%cmd.refresh_review_diff_desc", "review_refresh", "review-mode");
|
|
1762
|
+
editor.registerCommand("%cmd.side_by_side_diff", "%cmd.side_by_side_diff_desc", "side_by_side_diff_current_file", "global");
|
|
1013
1763
|
|
|
1014
1764
|
// Review Comment Commands
|
|
1015
|
-
editor.registerCommand("
|
|
1016
|
-
editor.registerCommand("
|
|
1017
|
-
editor.registerCommand("
|
|
1018
|
-
editor.registerCommand("
|
|
1019
|
-
editor.registerCommand("
|
|
1020
|
-
editor.registerCommand("
|
|
1021
|
-
editor.registerCommand("
|
|
1022
|
-
editor.registerCommand("
|
|
1023
|
-
editor.registerCommand("
|
|
1765
|
+
editor.registerCommand("%cmd.add_comment", "%cmd.add_comment_desc", "review_add_comment", "review-mode");
|
|
1766
|
+
editor.registerCommand("%cmd.approve_hunk", "%cmd.approve_hunk_desc", "review_approve_hunk", "review-mode");
|
|
1767
|
+
editor.registerCommand("%cmd.reject_hunk", "%cmd.reject_hunk_desc", "review_reject_hunk", "review-mode");
|
|
1768
|
+
editor.registerCommand("%cmd.needs_changes", "%cmd.needs_changes_desc", "review_needs_changes", "review-mode");
|
|
1769
|
+
editor.registerCommand("%cmd.question", "%cmd.question_desc", "review_question_hunk", "review-mode");
|
|
1770
|
+
editor.registerCommand("%cmd.clear_status", "%cmd.clear_status_desc", "review_clear_status", "review-mode");
|
|
1771
|
+
editor.registerCommand("%cmd.overall_feedback", "%cmd.overall_feedback_desc", "review_set_overall_feedback", "review-mode");
|
|
1772
|
+
editor.registerCommand("%cmd.export_markdown", "%cmd.export_markdown_desc", "review_export_session", "review-mode");
|
|
1773
|
+
editor.registerCommand("%cmd.export_json", "%cmd.export_json_desc", "review_export_json", "review-mode");
|
|
1774
|
+
|
|
1775
|
+
// Handler for when buffers are closed - cleans up scroll sync groups
|
|
1776
|
+
globalThis.on_buffer_closed = (data: any) => {
|
|
1777
|
+
// If one of the diff view buffers is closed, clean up the scroll sync group
|
|
1778
|
+
if (activeSideBySideState) {
|
|
1779
|
+
if (data.buffer_id === activeSideBySideState.oldBufferId ||
|
|
1780
|
+
data.buffer_id === activeSideBySideState.newBufferId) {
|
|
1781
|
+
// Remove scroll sync group
|
|
1782
|
+
if (activeSideBySideState.scrollSyncGroupId !== null) {
|
|
1783
|
+
try {
|
|
1784
|
+
(editor as any).removeScrollSyncGroup(activeSideBySideState.scrollSyncGroupId);
|
|
1785
|
+
} catch {}
|
|
1786
|
+
}
|
|
1787
|
+
activeSideBySideState = null;
|
|
1788
|
+
activeDiffViewState = null;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1024
1792
|
|
|
1025
1793
|
editor.on("buffer_closed", "on_buffer_closed");
|
|
1026
1794
|
|
|
@@ -1042,4 +1810,4 @@ editor.defineMode("review-mode", "normal", [
|
|
|
1042
1810
|
["E", "review_export_session"],
|
|
1043
1811
|
], true);
|
|
1044
1812
|
|
|
1045
|
-
editor.debug("Review Diff plugin loaded with review comments support");
|
|
1813
|
+
editor.debug("Review Diff plugin loaded with review comments support");
|