@fresh-editor/fresh-editor 0.1.4

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.
Files changed (38) hide show
  1. package/.gitignore +2 -0
  2. package/LICENSE +117 -0
  3. package/README.md +54 -0
  4. package/binary-install.js +212 -0
  5. package/binary.js +126 -0
  6. package/install.js +4 -0
  7. package/npm-shrinkwrap.json +900 -0
  8. package/package.json +100 -0
  9. package/plugins/README.md +121 -0
  10. package/plugins/clangd_support.md +20 -0
  11. package/plugins/clangd_support.ts +323 -0
  12. package/plugins/color_highlighter.ts +302 -0
  13. package/plugins/diagnostics_panel.ts +308 -0
  14. package/plugins/examples/README.md +245 -0
  15. package/plugins/examples/async_demo.ts +165 -0
  16. package/plugins/examples/bookmarks.ts +329 -0
  17. package/plugins/examples/buffer_query_demo.ts +110 -0
  18. package/plugins/examples/git_grep.ts +262 -0
  19. package/plugins/examples/hello_world.ts +93 -0
  20. package/plugins/examples/virtual_buffer_demo.ts +116 -0
  21. package/plugins/find_references.ts +357 -0
  22. package/plugins/git_find_file.ts +298 -0
  23. package/plugins/git_grep.ts +188 -0
  24. package/plugins/git_log.ts +1283 -0
  25. package/plugins/lib/fresh.d.ts +849 -0
  26. package/plugins/lib/index.ts +24 -0
  27. package/plugins/lib/navigation-controller.ts +214 -0
  28. package/plugins/lib/panel-manager.ts +218 -0
  29. package/plugins/lib/types.ts +72 -0
  30. package/plugins/lib/virtual-buffer-factory.ts +158 -0
  31. package/plugins/manual_help.ts +243 -0
  32. package/plugins/markdown_compose.ts +1207 -0
  33. package/plugins/merge_conflict.ts +1811 -0
  34. package/plugins/path_complete.ts +163 -0
  35. package/plugins/search_replace.ts +481 -0
  36. package/plugins/todo_highlighter.ts +204 -0
  37. package/plugins/welcome.ts +74 -0
  38. package/run-fresh.js +4 -0
@@ -0,0 +1,1283 @@
1
+ /// <reference path="../types/fresh.d.ts" />
2
+
3
+ /**
4
+ * Git Log Plugin - Magit-style Git Log Interface
5
+ *
6
+ * Provides an interactive git log view with:
7
+ * - Syntax highlighting for hash, author, date, subject
8
+ * - Cursor navigation between commits
9
+ * - Enter to open commit details in a virtual buffer
10
+ *
11
+ * Architecture designed for future magit-style features.
12
+ */
13
+
14
+ // =============================================================================
15
+ // Types and Interfaces
16
+ // =============================================================================
17
+
18
+ interface GitCommit {
19
+ hash: string;
20
+ shortHash: string;
21
+ author: string;
22
+ authorEmail: string;
23
+ date: string;
24
+ relativeDate: string;
25
+ subject: string;
26
+ body: string;
27
+ refs: string; // Branch/tag refs
28
+ graph: string; // Graph characters
29
+ }
30
+
31
+ interface GitLogOptions {
32
+ showGraph: boolean;
33
+ showRefs: boolean;
34
+ maxCommits: number;
35
+ }
36
+
37
+ interface GitLogState {
38
+ isOpen: boolean;
39
+ bufferId: number | null;
40
+ splitId: number | null; // The split where git log is displayed
41
+ sourceBufferId: number | null; // The buffer that was open before git log (to restore on close)
42
+ commits: GitCommit[];
43
+ options: GitLogOptions;
44
+ cachedContent: string; // Store content for highlighting (getBufferText doesn't work for virtual buffers)
45
+ }
46
+
47
+ interface GitCommitDetailState {
48
+ isOpen: boolean;
49
+ bufferId: number | null;
50
+ splitId: number | null;
51
+ commit: GitCommit | null;
52
+ cachedContent: string; // Store content for highlighting
53
+ }
54
+
55
+ interface GitFileViewState {
56
+ isOpen: boolean;
57
+ bufferId: number | null;
58
+ splitId: number | null;
59
+ filePath: string | null;
60
+ commitHash: string | null;
61
+ }
62
+
63
+ // =============================================================================
64
+ // State Management
65
+ // =============================================================================
66
+
67
+ const gitLogState: GitLogState = {
68
+ isOpen: false,
69
+ bufferId: null,
70
+ splitId: null,
71
+ sourceBufferId: null,
72
+ commits: [],
73
+ options: {
74
+ showGraph: false, // Disabled by default - graph interferes with format parsing
75
+ showRefs: true,
76
+ maxCommits: 100,
77
+ },
78
+ cachedContent: "",
79
+ };
80
+
81
+ const commitDetailState: GitCommitDetailState = {
82
+ isOpen: false,
83
+ bufferId: null,
84
+ splitId: null,
85
+ commit: null,
86
+ cachedContent: "",
87
+ };
88
+
89
+ const fileViewState: GitFileViewState = {
90
+ isOpen: false,
91
+ bufferId: null,
92
+ splitId: null,
93
+ filePath: null,
94
+ commitHash: null,
95
+ };
96
+
97
+ // =============================================================================
98
+ // Color Definitions (for syntax highlighting)
99
+ // =============================================================================
100
+
101
+ const colors = {
102
+ hash: [255, 180, 50] as [number, number, number], // Yellow/Orange
103
+ author: [100, 200, 255] as [number, number, number], // Cyan
104
+ date: [150, 255, 150] as [number, number, number], // Green
105
+ subject: [255, 255, 255] as [number, number, number], // White
106
+ header: [255, 200, 100] as [number, number, number], // Gold
107
+ separator: [100, 100, 100] as [number, number, number], // Gray
108
+ selected: [80, 80, 120] as [number, number, number], // Selection background
109
+ diffAdd: [100, 255, 100] as [number, number, number], // Green for additions
110
+ diffDel: [255, 100, 100] as [number, number, number], // Red for deletions
111
+ diffHunk: [150, 150, 255] as [number, number, number], // Blue for hunk headers
112
+ branch: [255, 150, 255] as [number, number, number], // Magenta for branches
113
+ tag: [255, 255, 100] as [number, number, number], // Yellow for tags
114
+ remote: [255, 130, 100] as [number, number, number], // Orange for remotes
115
+ graph: [150, 150, 150] as [number, number, number], // Gray for graph
116
+ // Syntax highlighting colors
117
+ syntaxKeyword: [200, 120, 220] as [number, number, number], // Purple for keywords
118
+ syntaxString: [180, 220, 140] as [number, number, number], // Light green for strings
119
+ syntaxComment: [120, 120, 120] as [number, number, number], // Gray for comments
120
+ syntaxNumber: [220, 180, 120] as [number, number, number], // Orange for numbers
121
+ syntaxFunction: [100, 180, 255] as [number, number, number], // Blue for functions
122
+ syntaxType: [80, 200, 180] as [number, number, number], // Teal for types
123
+ };
124
+
125
+ // =============================================================================
126
+ // Mode Definitions
127
+ // =============================================================================
128
+
129
+ // Define git-log mode with minimal keybindings
130
+ // Navigation uses normal cursor movement (arrows, j/k work naturally via parent mode)
131
+ editor.defineMode(
132
+ "git-log",
133
+ "normal", // inherit from normal mode for cursor movement
134
+ [
135
+ ["Return", "git_log_show_commit"],
136
+ ["Tab", "git_log_show_commit"],
137
+ ["q", "git_log_close"],
138
+ ["Escape", "git_log_close"],
139
+ ["r", "git_log_refresh"],
140
+ ["y", "git_log_copy_hash"],
141
+ ],
142
+ true // read-only
143
+ );
144
+
145
+ // Define git-commit-detail mode for viewing commit details
146
+ // Inherits from normal mode for natural cursor movement
147
+ editor.defineMode(
148
+ "git-commit-detail",
149
+ "normal", // inherit from normal mode for cursor movement
150
+ [
151
+ ["Return", "git_commit_detail_open_file"],
152
+ ["q", "git_commit_detail_close"],
153
+ ["Escape", "git_commit_detail_close"],
154
+ ],
155
+ true // read-only
156
+ );
157
+
158
+ // Define git-file-view mode for viewing files at a specific commit
159
+ editor.defineMode(
160
+ "git-file-view",
161
+ "normal", // inherit from normal mode for cursor movement
162
+ [
163
+ ["q", "git_file_view_close"],
164
+ ["Escape", "git_file_view_close"],
165
+ ],
166
+ true // read-only
167
+ );
168
+
169
+ // =============================================================================
170
+ // Git Command Execution
171
+ // =============================================================================
172
+
173
+ async function fetchGitLog(): Promise<GitCommit[]> {
174
+ // Use record separator to reliably split commits
175
+ // Format: hash, short hash, author, email, date, relative date, refs, subject, body
176
+ const format = "%H%x00%h%x00%an%x00%ae%x00%ai%x00%ar%x00%d%x00%s%x00%b%x1e";
177
+
178
+ const args = [
179
+ "log",
180
+ `--format=${format}`,
181
+ `-n${gitLogState.options.maxCommits}`,
182
+ ];
183
+
184
+ const cwd = editor.getCwd();
185
+ const result = await editor.spawnProcess("git", args, cwd);
186
+
187
+ if (result.exit_code !== 0) {
188
+ editor.setStatus(`Git log error: ${result.stderr}`);
189
+ return [];
190
+ }
191
+
192
+ const commits: GitCommit[] = [];
193
+ // Split by record separator (0x1e)
194
+ const records = result.stdout.split("\x1e");
195
+
196
+ for (const record of records) {
197
+ if (!record.trim()) continue;
198
+
199
+ const parts = record.split("\x00");
200
+ if (parts.length >= 8) {
201
+ commits.push({
202
+ hash: parts[0].trim(),
203
+ shortHash: parts[1].trim(),
204
+ author: parts[2].trim(),
205
+ authorEmail: parts[3].trim(),
206
+ date: parts[4].trim(),
207
+ relativeDate: parts[5].trim(),
208
+ refs: parts[6].trim(),
209
+ subject: parts[7].trim(),
210
+ body: parts[8] ? parts[8].trim() : "",
211
+ graph: "", // Graph is handled separately if needed
212
+ });
213
+ }
214
+ }
215
+
216
+ return commits;
217
+ }
218
+
219
+ async function fetchCommitDiff(hash: string): Promise<string> {
220
+ const cwd = editor.getCwd();
221
+ const result = await editor.spawnProcess("git", [
222
+ "show",
223
+ "--stat",
224
+ "--patch",
225
+ hash,
226
+ ], cwd);
227
+
228
+ if (result.exit_code !== 0) {
229
+ return `Error fetching diff: ${result.stderr}`;
230
+ }
231
+
232
+ return result.stdout;
233
+ }
234
+
235
+ // =============================================================================
236
+ // Git Log View
237
+ // =============================================================================
238
+
239
+ function formatCommitRow(commit: GitCommit): string {
240
+ // Build a structured line for consistent parsing and highlighting
241
+ // Format: shortHash (author, relativeDate) subject [refs]
242
+ let line = commit.shortHash;
243
+
244
+ // Add author in parentheses
245
+ line += " (" + commit.author + ", " + commit.relativeDate + ")";
246
+
247
+ // Add subject
248
+ line += " " + commit.subject;
249
+
250
+ // Add refs at the end if present and enabled
251
+ if (gitLogState.options.showRefs && commit.refs) {
252
+ line += " " + commit.refs;
253
+ }
254
+
255
+ return line + "\n";
256
+ }
257
+
258
+ // Helper to extract content string from entries (for highlighting)
259
+ function entriesToContent(entries: TextPropertyEntry[]): string {
260
+ return entries.map(e => e.text).join("");
261
+ }
262
+
263
+ function buildGitLogEntries(): TextPropertyEntry[] {
264
+ const entries: TextPropertyEntry[] = [];
265
+
266
+ // Magit-style header
267
+ entries.push({
268
+ text: "Commits:\n",
269
+ properties: { type: "section-header" },
270
+ });
271
+
272
+ if (gitLogState.commits.length === 0) {
273
+ entries.push({
274
+ text: " No commits found\n",
275
+ properties: { type: "empty" },
276
+ });
277
+ } else {
278
+ // Add each commit
279
+ for (let i = 0; i < gitLogState.commits.length; i++) {
280
+ const commit = gitLogState.commits[i];
281
+ entries.push({
282
+ text: formatCommitRow(commit),
283
+ properties: {
284
+ type: "commit",
285
+ index: i,
286
+ hash: commit.hash,
287
+ shortHash: commit.shortHash,
288
+ author: commit.author,
289
+ date: commit.relativeDate,
290
+ subject: commit.subject,
291
+ refs: commit.refs,
292
+ graph: commit.graph,
293
+ },
294
+ });
295
+ }
296
+ }
297
+
298
+ // Footer with help
299
+ entries.push({
300
+ text: "\n",
301
+ properties: { type: "blank" },
302
+ });
303
+ entries.push({
304
+ text: `${gitLogState.commits.length} commits | ↑/↓/j/k: navigate | RET: show | y: yank hash | r: refresh | q: quit\n`,
305
+ properties: { type: "footer" },
306
+ });
307
+
308
+ return entries;
309
+ }
310
+
311
+ function applyGitLogHighlighting(): void {
312
+ if (gitLogState.bufferId === null) return;
313
+
314
+ const bufferId = gitLogState.bufferId;
315
+
316
+ // Clear existing overlays
317
+ editor.clearNamespace(bufferId, "gitlog");
318
+
319
+ // Use cached content (getBufferText doesn't work for virtual buffers)
320
+ const content = gitLogState.cachedContent;
321
+ if (!content) return;
322
+ const lines = content.split("\n");
323
+
324
+ // Get cursor line to highlight current row (1-indexed from API)
325
+ const cursorLine = editor.getCursorLine();
326
+ const headerLines = 1; // Just "Commits:" header
327
+
328
+ let byteOffset = 0;
329
+
330
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
331
+ const line = lines[lineIdx];
332
+ const lineStart = byteOffset;
333
+ const lineEnd = byteOffset + line.length;
334
+
335
+ // Highlight section header
336
+ if (line === "Commits:") {
337
+ editor.addOverlay(
338
+ bufferId,
339
+ "gitlog",
340
+ lineStart,
341
+ lineEnd,
342
+ colors.header[0],
343
+ colors.header[1],
344
+ colors.header[2],
345
+ true, // underline
346
+ true, // bold
347
+ false // italic
348
+ );
349
+ byteOffset += line.length + 1;
350
+ continue;
351
+ }
352
+
353
+ const commitIndex = lineIdx - headerLines;
354
+ if (commitIndex < 0 || commitIndex >= gitLogState.commits.length) {
355
+ byteOffset += line.length + 1;
356
+ continue;
357
+ }
358
+
359
+ const commit = gitLogState.commits[commitIndex];
360
+ // cursorLine is 1-indexed, lineIdx is 0-indexed
361
+ const isCurrentLine = (lineIdx + 1) === cursorLine;
362
+
363
+ // Highlight entire line if cursor is on it (using selected color with underline)
364
+ if (isCurrentLine) {
365
+ editor.addOverlay(
366
+ bufferId,
367
+ "gitlog",
368
+ lineStart,
369
+ lineEnd,
370
+ colors.selected[0],
371
+ colors.selected[1],
372
+ colors.selected[2],
373
+ true, // underline to make it visible
374
+ true, // bold
375
+ false // italic
376
+ );
377
+ }
378
+
379
+ // Parse the line format: "shortHash (author, relativeDate) subject [refs]"
380
+ // Highlight hash (first 7+ chars until space)
381
+ const hashEnd = commit.shortHash.length;
382
+ editor.addOverlay(
383
+ bufferId,
384
+ "gitlog",
385
+ lineStart,
386
+ lineStart + hashEnd,
387
+ colors.hash[0],
388
+ colors.hash[1],
389
+ colors.hash[2],
390
+ false, // underline
391
+ false, // bold
392
+ false // italic
393
+ );
394
+
395
+ // Highlight author name (inside parentheses)
396
+ const authorPattern = "(" + commit.author + ",";
397
+ const authorStartInLine = line.indexOf(authorPattern);
398
+ if (authorStartInLine >= 0) {
399
+ const authorStart = lineStart + authorStartInLine + 1; // skip "("
400
+ const authorEnd = authorStart + commit.author.length;
401
+ editor.addOverlay(
402
+ bufferId,
403
+ "gitlog",
404
+ authorStart,
405
+ authorEnd,
406
+ colors.author[0],
407
+ colors.author[1],
408
+ colors.author[2],
409
+ false, // underline
410
+ false, // bold
411
+ false // italic
412
+ );
413
+ }
414
+
415
+ // Highlight relative date
416
+ const datePattern = ", " + commit.relativeDate + ")";
417
+ const dateStartInLine = line.indexOf(datePattern);
418
+ if (dateStartInLine >= 0) {
419
+ const dateStart = lineStart + dateStartInLine + 2; // skip ", "
420
+ const dateEnd = dateStart + commit.relativeDate.length;
421
+ editor.addOverlay(
422
+ bufferId,
423
+ "gitlog",
424
+ dateStart,
425
+ dateEnd,
426
+ colors.date[0],
427
+ colors.date[1],
428
+ colors.date[2],
429
+ false, // underline
430
+ false, // bold
431
+ false // italic
432
+ );
433
+ }
434
+
435
+ // Highlight refs (branches/tags) at end of line if present
436
+ if (gitLogState.options.showRefs && commit.refs) {
437
+ const refsStartInLine = line.lastIndexOf(commit.refs);
438
+ if (refsStartInLine >= 0) {
439
+ const refsStart = lineStart + refsStartInLine;
440
+ const refsEnd = refsStart + commit.refs.length;
441
+
442
+ // Determine color based on ref type
443
+ let refColor = colors.branch;
444
+ if (commit.refs.includes("tag:")) {
445
+ refColor = colors.tag;
446
+ } else if (commit.refs.includes("origin/") || commit.refs.includes("remote")) {
447
+ refColor = colors.remote;
448
+ }
449
+
450
+ editor.addOverlay(
451
+ bufferId,
452
+ "gitlog",
453
+ refsStart,
454
+ refsEnd,
455
+ refColor[0],
456
+ refColor[1],
457
+ refColor[2],
458
+ false, // underline
459
+ true, // bold (make refs stand out)
460
+ false // italic
461
+ );
462
+ }
463
+ }
464
+
465
+ byteOffset += line.length + 1;
466
+ }
467
+ }
468
+
469
+ function updateGitLogView(): void {
470
+ if (gitLogState.bufferId !== null) {
471
+ const entries = buildGitLogEntries();
472
+ gitLogState.cachedContent = entriesToContent(entries);
473
+ editor.setVirtualBufferContent(gitLogState.bufferId, entries);
474
+ applyGitLogHighlighting();
475
+ }
476
+ }
477
+
478
+ // =============================================================================
479
+ // Commit Detail View
480
+ // =============================================================================
481
+
482
+ // Parse diff line to extract file and line information
483
+ interface DiffContext {
484
+ currentFile: string | null;
485
+ currentHunkNewStart: number;
486
+ currentHunkNewLine: number; // Current line within the new file
487
+ }
488
+
489
+ function buildCommitDetailEntries(commit: GitCommit, showOutput: string): TextPropertyEntry[] {
490
+ const entries: TextPropertyEntry[] = [];
491
+ const lines = showOutput.split("\n");
492
+
493
+ // Track diff context for file/line navigation
494
+ const diffContext: DiffContext = {
495
+ currentFile: null,
496
+ currentHunkNewStart: 0,
497
+ currentHunkNewLine: 0,
498
+ };
499
+
500
+ for (const line of lines) {
501
+ let lineType = "text";
502
+ const properties: Record<string, unknown> = { type: lineType };
503
+
504
+ // Detect diff file header: diff --git a/path b/path
505
+ const diffHeaderMatch = line.match(/^diff --git a\/(.+) b\/(.+)$/);
506
+ if (diffHeaderMatch) {
507
+ diffContext.currentFile = diffHeaderMatch[2]; // Use the 'b' (new) file path
508
+ diffContext.currentHunkNewStart = 0;
509
+ diffContext.currentHunkNewLine = 0;
510
+ lineType = "diff-header";
511
+ properties.type = lineType;
512
+ properties.file = diffContext.currentFile;
513
+ }
514
+ // Detect +++ line (new file path)
515
+ else if (line.startsWith("+++ b/")) {
516
+ diffContext.currentFile = line.slice(6);
517
+ lineType = "diff-header";
518
+ properties.type = lineType;
519
+ properties.file = diffContext.currentFile;
520
+ }
521
+ // Detect hunk header: @@ -old,count +new,count @@
522
+ else if (line.startsWith("@@")) {
523
+ lineType = "diff-hunk";
524
+ const hunkMatch = line.match(/@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
525
+ if (hunkMatch) {
526
+ diffContext.currentHunkNewStart = parseInt(hunkMatch[1], 10);
527
+ diffContext.currentHunkNewLine = diffContext.currentHunkNewStart;
528
+ }
529
+ properties.type = lineType;
530
+ properties.file = diffContext.currentFile;
531
+ properties.line = diffContext.currentHunkNewStart;
532
+ }
533
+ // Addition line
534
+ else if (line.startsWith("+") && !line.startsWith("+++")) {
535
+ lineType = "diff-add";
536
+ properties.type = lineType;
537
+ properties.file = diffContext.currentFile;
538
+ properties.line = diffContext.currentHunkNewLine;
539
+ diffContext.currentHunkNewLine++;
540
+ }
541
+ // Deletion line
542
+ else if (line.startsWith("-") && !line.startsWith("---")) {
543
+ lineType = "diff-del";
544
+ properties.type = lineType;
545
+ properties.file = diffContext.currentFile;
546
+ // Deletion lines don't advance the new file line counter
547
+ }
548
+ // Context line (unchanged)
549
+ else if (line.startsWith(" ") && diffContext.currentFile && diffContext.currentHunkNewLine > 0) {
550
+ lineType = "diff-context";
551
+ properties.type = lineType;
552
+ properties.file = diffContext.currentFile;
553
+ properties.line = diffContext.currentHunkNewLine;
554
+ diffContext.currentHunkNewLine++;
555
+ }
556
+ // Other diff header lines
557
+ else if (line.startsWith("index ") || line.startsWith("--- ")) {
558
+ lineType = "diff-header";
559
+ properties.type = lineType;
560
+ }
561
+ // Commit header lines
562
+ else if (line.startsWith("commit ")) {
563
+ lineType = "header";
564
+ properties.type = lineType;
565
+ const hashMatch = line.match(/^commit ([a-f0-9]+)/);
566
+ if (hashMatch) {
567
+ properties.hash = hashMatch[1];
568
+ }
569
+ }
570
+ else if (line.startsWith("Author:")) {
571
+ lineType = "meta";
572
+ properties.type = lineType;
573
+ properties.field = "author";
574
+ }
575
+ else if (line.startsWith("Date:")) {
576
+ lineType = "meta";
577
+ properties.type = lineType;
578
+ properties.field = "date";
579
+ }
580
+
581
+ entries.push({
582
+ text: `${line}\n`,
583
+ properties: properties,
584
+ });
585
+ }
586
+
587
+ // Footer with help
588
+ entries.push({
589
+ text: "\n",
590
+ properties: { type: "blank" },
591
+ });
592
+ entries.push({
593
+ text: `↑/↓/j/k: navigate | RET: open file at line | q: back to log\n`,
594
+ properties: { type: "footer" },
595
+ });
596
+
597
+ return entries;
598
+ }
599
+
600
+ function applyCommitDetailHighlighting(): void {
601
+ if (commitDetailState.bufferId === null) return;
602
+
603
+ const bufferId = commitDetailState.bufferId;
604
+
605
+ // Clear existing overlays
606
+ editor.clearNamespace(bufferId, "gitdetail");
607
+
608
+ // Use cached content (getBufferText doesn't work for virtual buffers)
609
+ const content = commitDetailState.cachedContent;
610
+ if (!content) return;
611
+ const lines = content.split("\n");
612
+
613
+ let byteOffset = 0;
614
+
615
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
616
+ const line = lines[lineIdx];
617
+ const lineStart = byteOffset;
618
+ const lineEnd = byteOffset + line.length;
619
+
620
+ // Highlight diff additions (green)
621
+ if (line.startsWith("+") && !line.startsWith("+++")) {
622
+ editor.addOverlay(
623
+ bufferId,
624
+ "gitdetail",
625
+ lineStart,
626
+ lineEnd,
627
+ colors.diffAdd[0],
628
+ colors.diffAdd[1],
629
+ colors.diffAdd[2],
630
+ false, // underline
631
+ false, // bold
632
+ false // italic
633
+ );
634
+ }
635
+ // Highlight diff deletions (red)
636
+ else if (line.startsWith("-") && !line.startsWith("---")) {
637
+ editor.addOverlay(
638
+ bufferId,
639
+ "gitdetail",
640
+ lineStart,
641
+ lineEnd,
642
+ colors.diffDel[0],
643
+ colors.diffDel[1],
644
+ colors.diffDel[2],
645
+ false, // underline
646
+ false, // bold
647
+ false // italic
648
+ );
649
+ }
650
+ // Highlight hunk headers (cyan/blue)
651
+ else if (line.startsWith("@@")) {
652
+ editor.addOverlay(
653
+ bufferId,
654
+ "gitdetail",
655
+ lineStart,
656
+ lineEnd,
657
+ colors.diffHunk[0],
658
+ colors.diffHunk[1],
659
+ colors.diffHunk[2],
660
+ false, // underline
661
+ true, // bold
662
+ false // italic
663
+ );
664
+ }
665
+ // Highlight commit hash in "commit <hash>" line (git show format)
666
+ else if (line.startsWith("commit ")) {
667
+ const hashMatch = line.match(/^commit ([a-f0-9]+)/);
668
+ if (hashMatch) {
669
+ const hashStart = lineStart + 7; // "commit " is 7 chars
670
+ editor.addOverlay(
671
+ bufferId,
672
+ "gitdetail",
673
+ hashStart,
674
+ hashStart + hashMatch[1].length,
675
+ colors.hash[0],
676
+ colors.hash[1],
677
+ colors.hash[2],
678
+ false, // underline
679
+ true, // bold
680
+ false // italic
681
+ );
682
+ }
683
+ }
684
+ // Highlight author line
685
+ else if (line.startsWith("Author:")) {
686
+ editor.addOverlay(
687
+ bufferId,
688
+ "gitdetail",
689
+ lineStart + 8, // "Author: " is 8 chars
690
+ lineEnd,
691
+ colors.author[0],
692
+ colors.author[1],
693
+ colors.author[2],
694
+ false, // underline
695
+ false, // bold
696
+ false // italic
697
+ );
698
+ }
699
+ // Highlight date line
700
+ else if (line.startsWith("Date:")) {
701
+ editor.addOverlay(
702
+ bufferId,
703
+ "gitdetail",
704
+ lineStart + 6, // "Date: " is 6 chars (with trailing spaces it's 8)
705
+ lineEnd,
706
+ colors.date[0],
707
+ colors.date[1],
708
+ colors.date[2],
709
+ false, // underline
710
+ false, // bold
711
+ false // italic
712
+ );
713
+ }
714
+ // Highlight diff file headers
715
+ else if (line.startsWith("diff --git")) {
716
+ editor.addOverlay(
717
+ bufferId,
718
+ "gitdetail",
719
+ lineStart,
720
+ lineEnd,
721
+ colors.header[0],
722
+ colors.header[1],
723
+ colors.header[2],
724
+ false, // underline
725
+ true, // bold
726
+ false // italic
727
+ );
728
+ }
729
+
730
+ byteOffset += line.length + 1;
731
+ }
732
+ }
733
+
734
+ // =============================================================================
735
+ // Public Commands - Git Log
736
+ // =============================================================================
737
+
738
+ globalThis.show_git_log = async function(): Promise<void> {
739
+ if (gitLogState.isOpen) {
740
+ editor.setStatus("Git log already open");
741
+ return;
742
+ }
743
+
744
+ editor.setStatus("Loading git log...");
745
+
746
+ // Store the current split ID and buffer ID before opening git log
747
+ gitLogState.splitId = editor.getActiveSplitId();
748
+ gitLogState.sourceBufferId = editor.getActiveBufferId();
749
+
750
+ // Fetch commits
751
+ gitLogState.commits = await fetchGitLog();
752
+
753
+ if (gitLogState.commits.length === 0) {
754
+ editor.setStatus("No commits found or not a git repository");
755
+ gitLogState.splitId = null;
756
+ return;
757
+ }
758
+
759
+ // Build entries and cache content for highlighting
760
+ const entries = buildGitLogEntries();
761
+ gitLogState.cachedContent = entriesToContent(entries);
762
+
763
+ // Create virtual buffer in the current split (replacing current buffer)
764
+ const bufferId = await editor.createVirtualBufferInExistingSplit({
765
+ name: "*Git Log*",
766
+ mode: "git-log",
767
+ read_only: true,
768
+ entries: entries,
769
+ split_id: gitLogState.splitId!,
770
+ show_line_numbers: false,
771
+ show_cursors: true,
772
+ editing_disabled: true,
773
+ });
774
+
775
+ if (bufferId !== null) {
776
+ gitLogState.isOpen = true;
777
+ gitLogState.bufferId = bufferId;
778
+
779
+ // Apply syntax highlighting
780
+ applyGitLogHighlighting();
781
+
782
+ editor.setStatus(`Git log: ${gitLogState.commits.length} commits | ↑/↓: navigate | RET: show | q: quit`);
783
+ editor.debug("Git log panel opened");
784
+ } else {
785
+ gitLogState.splitId = null;
786
+ editor.setStatus("Failed to open git log panel");
787
+ }
788
+ };
789
+
790
+ globalThis.git_log_close = function(): void {
791
+ if (!gitLogState.isOpen) {
792
+ return;
793
+ }
794
+
795
+ // Restore the original buffer in the split
796
+ if (gitLogState.splitId !== null && gitLogState.sourceBufferId !== null) {
797
+ editor.setSplitBuffer(gitLogState.splitId, gitLogState.sourceBufferId);
798
+ }
799
+
800
+ // Close the git log buffer (it's no longer displayed)
801
+ if (gitLogState.bufferId !== null) {
802
+ editor.closeBuffer(gitLogState.bufferId);
803
+ }
804
+
805
+ gitLogState.isOpen = false;
806
+ gitLogState.bufferId = null;
807
+ gitLogState.splitId = null;
808
+ gitLogState.sourceBufferId = null;
809
+ gitLogState.commits = [];
810
+ editor.setStatus("Git log closed");
811
+ };
812
+
813
+ // Cursor moved handler for git log - update highlighting and status
814
+ globalThis.on_git_log_cursor_moved = function(data: {
815
+ buffer_id: number;
816
+ cursor_id: number;
817
+ old_position: number;
818
+ new_position: number;
819
+ }): void {
820
+ // Only handle cursor movement in our git log buffer
821
+ if (gitLogState.bufferId === null || data.buffer_id !== gitLogState.bufferId) {
822
+ return;
823
+ }
824
+
825
+ // Re-apply highlighting to update cursor line highlight
826
+ applyGitLogHighlighting();
827
+
828
+ // Get cursor line to show status
829
+ const cursorLine = editor.getCursorLine();
830
+ const headerLines = 1;
831
+ const commitIndex = cursorLine - headerLines;
832
+
833
+ if (commitIndex >= 0 && commitIndex < gitLogState.commits.length) {
834
+ editor.setStatus(`Commit ${commitIndex + 1}/${gitLogState.commits.length}`);
835
+ }
836
+ };
837
+
838
+ // Register cursor movement handler
839
+ editor.on("cursor_moved", "on_git_log_cursor_moved");
840
+
841
+ globalThis.git_log_refresh = async function(): Promise<void> {
842
+ if (!gitLogState.isOpen) return;
843
+
844
+ editor.setStatus("Refreshing git log...");
845
+ gitLogState.commits = await fetchGitLog();
846
+ updateGitLogView();
847
+ editor.setStatus(`Git log refreshed: ${gitLogState.commits.length} commits`);
848
+ };
849
+
850
+ // Helper function to get commit at current cursor position
851
+ function getCommitAtCursor(): GitCommit | null {
852
+ if (gitLogState.bufferId === null) return null;
853
+
854
+ // Use text properties to find which commit the cursor is on
855
+ // This is more reliable than line number calculation
856
+ const props = editor.getTextPropertiesAtCursor(gitLogState.bufferId);
857
+
858
+ if (props.length > 0) {
859
+ const prop = props[0];
860
+ // Check if cursor is on a commit line (has type "commit" and index)
861
+ if (prop.type === "commit" && typeof prop.index === "number") {
862
+ const index = prop.index as number;
863
+ if (index >= 0 && index < gitLogState.commits.length) {
864
+ return gitLogState.commits[index];
865
+ }
866
+ }
867
+ // Also support finding commit by hash (alternative lookup)
868
+ if (prop.hash && typeof prop.hash === "string") {
869
+ return gitLogState.commits.find(c => c.hash === prop.hash) || null;
870
+ }
871
+ }
872
+
873
+ return null;
874
+ }
875
+
876
+ globalThis.git_log_show_commit = async function(): Promise<void> {
877
+ if (!gitLogState.isOpen || gitLogState.commits.length === 0) return;
878
+ if (gitLogState.splitId === null) return;
879
+
880
+ const commit = getCommitAtCursor();
881
+ if (!commit) {
882
+ editor.setStatus("Move cursor to a commit line");
883
+ return;
884
+ }
885
+
886
+ editor.setStatus(`Loading commit ${commit.shortHash}...`);
887
+
888
+ // Fetch full commit info using git show (includes header and diff)
889
+ const showOutput = await fetchCommitDiff(commit.hash);
890
+
891
+ // Build entries using raw git show output
892
+ const entries = buildCommitDetailEntries(commit, showOutput);
893
+
894
+ // Cache content for highlighting (getBufferText doesn't work for virtual buffers)
895
+ commitDetailState.cachedContent = entriesToContent(entries);
896
+
897
+ // Create virtual buffer in the current split (replacing git log view)
898
+ const bufferId = await editor.createVirtualBufferInExistingSplit({
899
+ name: `*Commit: ${commit.shortHash}*`,
900
+ mode: "git-commit-detail",
901
+ read_only: true,
902
+ entries: entries,
903
+ split_id: gitLogState.splitId!,
904
+ show_line_numbers: false, // Disable line numbers for cleaner diff view
905
+ show_cursors: true,
906
+ editing_disabled: true,
907
+ });
908
+
909
+ if (bufferId !== null) {
910
+ commitDetailState.isOpen = true;
911
+ commitDetailState.bufferId = bufferId;
912
+ commitDetailState.splitId = gitLogState.splitId;
913
+ commitDetailState.commit = commit;
914
+
915
+ // Apply syntax highlighting
916
+ applyCommitDetailHighlighting();
917
+
918
+ editor.setStatus(`Commit ${commit.shortHash} | ↑/↓: navigate | RET: open file | q: back`);
919
+ } else {
920
+ editor.setStatus("Failed to open commit details");
921
+ }
922
+ };
923
+
924
+ globalThis.git_log_copy_hash = function(): void {
925
+ if (!gitLogState.isOpen || gitLogState.commits.length === 0) return;
926
+
927
+ const commit = getCommitAtCursor();
928
+ if (!commit) {
929
+ editor.setStatus("Move cursor to a commit line");
930
+ return;
931
+ }
932
+
933
+ // Use spawn to copy to clipboard (works on most systems)
934
+ // Try xclip first (Linux), then pbcopy (macOS), then xsel
935
+ editor.spawnProcess("sh", ["-c", `echo -n "${commit.hash}" | xclip -selection clipboard 2>/dev/null || echo -n "${commit.hash}" | pbcopy 2>/dev/null || echo -n "${commit.hash}" | xsel --clipboard 2>/dev/null`])
936
+ .then(() => {
937
+ editor.setStatus(`Copied: ${commit.shortHash} (${commit.hash})`);
938
+ })
939
+ .catch(() => {
940
+ // If all clipboard commands fail, just show the hash
941
+ editor.setStatus(`Hash: ${commit.hash}`);
942
+ });
943
+ };
944
+
945
+ // =============================================================================
946
+ // Public Commands - Commit Detail
947
+ // =============================================================================
948
+
949
+ globalThis.git_commit_detail_close = function(): void {
950
+ if (!commitDetailState.isOpen) {
951
+ return;
952
+ }
953
+
954
+ // Go back to the git log view by restoring the git log buffer
955
+ if (commitDetailState.splitId !== null && gitLogState.bufferId !== null) {
956
+ editor.setSplitBuffer(commitDetailState.splitId, gitLogState.bufferId);
957
+ // Re-apply highlighting since we're switching back
958
+ applyGitLogHighlighting();
959
+ }
960
+
961
+ // Close the commit detail buffer (it's no longer displayed)
962
+ if (commitDetailState.bufferId !== null) {
963
+ editor.closeBuffer(commitDetailState.bufferId);
964
+ }
965
+
966
+ commitDetailState.isOpen = false;
967
+ commitDetailState.bufferId = null;
968
+ commitDetailState.splitId = null;
969
+ commitDetailState.commit = null;
970
+
971
+ editor.setStatus(`Git log: ${gitLogState.commits.length} commits | ↑/↓: navigate | RET: show | q: quit`);
972
+ };
973
+
974
+ // Close file view and go back to commit detail
975
+ globalThis.git_file_view_close = function(): void {
976
+ if (!fileViewState.isOpen) {
977
+ return;
978
+ }
979
+
980
+ // Go back to the commit detail view by restoring the commit detail buffer
981
+ if (fileViewState.splitId !== null && commitDetailState.bufferId !== null) {
982
+ editor.setSplitBuffer(fileViewState.splitId, commitDetailState.bufferId);
983
+ // Re-apply highlighting since we're switching back
984
+ applyCommitDetailHighlighting();
985
+ }
986
+
987
+ // Close the file view buffer (it's no longer displayed)
988
+ if (fileViewState.bufferId !== null) {
989
+ editor.closeBuffer(fileViewState.bufferId);
990
+ }
991
+
992
+ fileViewState.isOpen = false;
993
+ fileViewState.bufferId = null;
994
+ fileViewState.splitId = null;
995
+ fileViewState.filePath = null;
996
+ fileViewState.commitHash = null;
997
+
998
+ if (commitDetailState.commit) {
999
+ editor.setStatus(`Commit ${commitDetailState.commit.shortHash} | ↑/↓: navigate | RET: open file | q: back`);
1000
+ }
1001
+ };
1002
+
1003
+ // Fetch file content at a specific commit
1004
+ async function fetchFileAtCommit(commitHash: string, filePath: string): Promise<string | null> {
1005
+ const cwd = editor.getCwd();
1006
+ const result = await editor.spawnProcess("git", [
1007
+ "show",
1008
+ `${commitHash}:${filePath}`,
1009
+ ], cwd);
1010
+
1011
+ if (result.exit_code !== 0) {
1012
+ return null;
1013
+ }
1014
+
1015
+ return result.stdout;
1016
+ }
1017
+
1018
+ // Get language type from file extension
1019
+ function getLanguageFromPath(filePath: string): string {
1020
+ const ext = editor.pathExtname(filePath).toLowerCase();
1021
+ const extMap: Record<string, string> = {
1022
+ ".rs": "rust",
1023
+ ".ts": "typescript",
1024
+ ".tsx": "typescript",
1025
+ ".js": "javascript",
1026
+ ".jsx": "javascript",
1027
+ ".py": "python",
1028
+ ".go": "go",
1029
+ ".c": "c",
1030
+ ".cpp": "cpp",
1031
+ ".h": "c",
1032
+ ".hpp": "cpp",
1033
+ ".java": "java",
1034
+ ".rb": "ruby",
1035
+ ".sh": "shell",
1036
+ ".bash": "shell",
1037
+ ".zsh": "shell",
1038
+ ".toml": "toml",
1039
+ ".yaml": "yaml",
1040
+ ".yml": "yaml",
1041
+ ".json": "json",
1042
+ ".md": "markdown",
1043
+ ".css": "css",
1044
+ ".html": "html",
1045
+ ".xml": "xml",
1046
+ };
1047
+ return extMap[ext] || "text";
1048
+ }
1049
+
1050
+ // Keywords for different languages
1051
+ const languageKeywords: Record<string, string[]> = {
1052
+ rust: ["fn", "let", "mut", "const", "pub", "use", "mod", "struct", "enum", "impl", "trait", "for", "while", "loop", "if", "else", "match", "return", "async", "await", "move", "self", "Self", "super", "crate", "where", "type", "static", "unsafe", "extern", "ref", "dyn", "as", "in", "true", "false"],
1053
+ typescript: ["function", "const", "let", "var", "class", "interface", "type", "extends", "implements", "import", "export", "from", "async", "await", "return", "if", "else", "for", "while", "do", "switch", "case", "break", "continue", "new", "this", "super", "null", "undefined", "true", "false", "try", "catch", "finally", "throw", "typeof", "instanceof", "void", "delete", "in", "of", "static", "readonly", "private", "public", "protected", "abstract", "enum"],
1054
+ javascript: ["function", "const", "let", "var", "class", "extends", "import", "export", "from", "async", "await", "return", "if", "else", "for", "while", "do", "switch", "case", "break", "continue", "new", "this", "super", "null", "undefined", "true", "false", "try", "catch", "finally", "throw", "typeof", "instanceof", "void", "delete", "in", "of", "static"],
1055
+ python: ["def", "class", "if", "elif", "else", "for", "while", "try", "except", "finally", "with", "as", "import", "from", "return", "yield", "raise", "pass", "break", "continue", "and", "or", "not", "in", "is", "lambda", "None", "True", "False", "global", "nonlocal", "async", "await", "self"],
1056
+ go: ["func", "var", "const", "type", "struct", "interface", "map", "chan", "if", "else", "for", "range", "switch", "case", "default", "break", "continue", "return", "go", "defer", "select", "import", "package", "nil", "true", "false", "make", "new", "len", "cap", "append", "copy", "delete", "panic", "recover"],
1057
+ };
1058
+
1059
+ // Apply basic syntax highlighting to file view
1060
+ function applyFileViewHighlighting(bufferId: number, content: string, filePath: string): void {
1061
+ const language = getLanguageFromPath(filePath);
1062
+ const keywords = languageKeywords[language] || [];
1063
+ const lines = content.split("\n");
1064
+
1065
+ // Clear existing overlays
1066
+ editor.clearNamespace(bufferId, "syntax");
1067
+
1068
+ let byteOffset = 0;
1069
+ let inMultilineComment = false;
1070
+ let inMultilineString = false;
1071
+
1072
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
1073
+ const line = lines[lineIdx];
1074
+ const lineStart = byteOffset;
1075
+
1076
+ // Skip empty lines
1077
+ if (line.trim() === "") {
1078
+ byteOffset += line.length + 1;
1079
+ continue;
1080
+ }
1081
+
1082
+ // Check for multiline comment start/end
1083
+ if (language === "rust" || language === "c" || language === "cpp" || language === "java" || language === "javascript" || language === "typescript" || language === "go") {
1084
+ if (line.includes("/*") && !line.includes("*/")) {
1085
+ inMultilineComment = true;
1086
+ }
1087
+ if (inMultilineComment) {
1088
+ editor.addOverlay(bufferId, "syntax", lineStart, lineStart + line.length, colors.syntaxComment[0], colors.syntaxComment[1], colors.syntaxComment[2], false, false, true);
1089
+ if (line.includes("*/")) {
1090
+ inMultilineComment = false;
1091
+ }
1092
+ byteOffset += line.length + 1;
1093
+ continue;
1094
+ }
1095
+ }
1096
+
1097
+ // Python multiline strings
1098
+ if (language === "python" && (line.includes('"""') || line.includes("'''"))) {
1099
+ const tripleQuote = line.includes('"""') ? '"""' : "'''";
1100
+ const firstIdx = line.indexOf(tripleQuote);
1101
+ const secondIdx = line.indexOf(tripleQuote, firstIdx + 3);
1102
+ if (firstIdx >= 0 && secondIdx < 0) {
1103
+ inMultilineString = !inMultilineString;
1104
+ }
1105
+ }
1106
+ if (inMultilineString) {
1107
+ editor.addOverlay(bufferId, "syntax", lineStart, lineStart + line.length, colors.syntaxString[0], colors.syntaxString[1], colors.syntaxString[2], false, false, false);
1108
+ byteOffset += line.length + 1;
1109
+ continue;
1110
+ }
1111
+
1112
+ // Single-line comment detection
1113
+ let commentStart = -1;
1114
+ if (language === "rust" || language === "c" || language === "cpp" || language === "java" || language === "javascript" || language === "typescript" || language === "go") {
1115
+ commentStart = line.indexOf("//");
1116
+ } else if (language === "python" || language === "shell" || language === "ruby" || language === "yaml" || language === "toml") {
1117
+ commentStart = line.indexOf("#");
1118
+ }
1119
+
1120
+ if (commentStart >= 0) {
1121
+ editor.addOverlay(bufferId, "syntax", lineStart + commentStart, lineStart + line.length, colors.syntaxComment[0], colors.syntaxComment[1], colors.syntaxComment[2], false, false, true);
1122
+ }
1123
+
1124
+ // String highlighting (simple: find "..." and '...')
1125
+ let i = 0;
1126
+ let stringCount = 0;
1127
+ while (i < line.length) {
1128
+ const ch = line[i];
1129
+ if (ch === '"' || ch === "'") {
1130
+ const quote = ch;
1131
+ const start = i;
1132
+ i++;
1133
+ while (i < line.length && line[i] !== quote) {
1134
+ if (line[i] === '\\') i++; // Skip escaped chars
1135
+ i++;
1136
+ }
1137
+ if (i < line.length) i++; // Include closing quote
1138
+ const end = i;
1139
+ if (commentStart < 0 || start < commentStart) {
1140
+ editor.addOverlay(bufferId, "syntax", lineStart + start, lineStart + end, colors.syntaxString[0], colors.syntaxString[1], colors.syntaxString[2], false, false, false);
1141
+ }
1142
+ } else {
1143
+ i++;
1144
+ }
1145
+ }
1146
+
1147
+ // Keyword highlighting
1148
+ for (const keyword of keywords) {
1149
+ const regex = new RegExp(`\\b${keyword}\\b`, "g");
1150
+ let match;
1151
+ while ((match = regex.exec(line)) !== null) {
1152
+ const kwStart = match.index;
1153
+ const kwEnd = kwStart + keyword.length;
1154
+ // Don't highlight if inside comment
1155
+ if (commentStart < 0 || kwStart < commentStart) {
1156
+ editor.addOverlay(bufferId, "syntax", lineStart + kwStart, lineStart + kwEnd, colors.syntaxKeyword[0], colors.syntaxKeyword[1], colors.syntaxKeyword[2], false, true, false);
1157
+ }
1158
+ }
1159
+ }
1160
+
1161
+ // Number highlighting
1162
+ const numberRegex = /\b\d+(\.\d+)?\b/g;
1163
+ let numMatch;
1164
+ while ((numMatch = numberRegex.exec(line)) !== null) {
1165
+ const numStart = numMatch.index;
1166
+ const numEnd = numStart + numMatch[0].length;
1167
+ if (commentStart < 0 || numStart < commentStart) {
1168
+ editor.addOverlay(bufferId, "syntax", lineStart + numStart, lineStart + numEnd, colors.syntaxNumber[0], colors.syntaxNumber[1], colors.syntaxNumber[2], false, false, false);
1169
+ }
1170
+ }
1171
+
1172
+ byteOffset += line.length + 1;
1173
+ }
1174
+ }
1175
+
1176
+ // Open file at the current diff line position - shows file as it was at that commit
1177
+ globalThis.git_commit_detail_open_file = async function(): Promise<void> {
1178
+ if (!commitDetailState.isOpen || commitDetailState.bufferId === null) {
1179
+ return;
1180
+ }
1181
+
1182
+ const commit = commitDetailState.commit;
1183
+ if (!commit) {
1184
+ editor.setStatus("No commit context available");
1185
+ return;
1186
+ }
1187
+
1188
+ // Get text properties at cursor position to find file/line info
1189
+ const props = editor.getTextPropertiesAtCursor(commitDetailState.bufferId);
1190
+
1191
+ if (props.length > 0) {
1192
+ const file = props[0].file as string | undefined;
1193
+ const line = props[0].line as number | undefined;
1194
+
1195
+ if (file) {
1196
+ editor.setStatus(`Loading ${file} at ${commit.shortHash}...`);
1197
+
1198
+ // Fetch file content at this commit
1199
+ const content = await fetchFileAtCommit(commit.hash, file);
1200
+
1201
+ if (content === null) {
1202
+ editor.setStatus(`File ${file} not found at commit ${commit.shortHash}`);
1203
+ return;
1204
+ }
1205
+
1206
+ // Build entries for the virtual buffer - one entry per line for proper line tracking
1207
+ const lines = content.split("\n");
1208
+ const entries: TextPropertyEntry[] = [];
1209
+
1210
+ for (let i = 0; i < lines.length; i++) {
1211
+ entries.push({
1212
+ text: lines[i] + (i < lines.length - 1 ? "\n" : ""),
1213
+ properties: { type: "content", line: i + 1 },
1214
+ });
1215
+ }
1216
+
1217
+ // Create a read-only virtual buffer with the file content
1218
+ const bufferId = await editor.createVirtualBufferInExistingSplit({
1219
+ name: `${file} @ ${commit.shortHash}`,
1220
+ mode: "git-file-view",
1221
+ read_only: true,
1222
+ entries: entries,
1223
+ split_id: commitDetailState.splitId!,
1224
+ show_line_numbers: true,
1225
+ show_cursors: true,
1226
+ editing_disabled: true,
1227
+ });
1228
+
1229
+ if (bufferId !== null) {
1230
+ // Track file view state so we can navigate back
1231
+ fileViewState.isOpen = true;
1232
+ fileViewState.bufferId = bufferId;
1233
+ fileViewState.splitId = commitDetailState.splitId;
1234
+ fileViewState.filePath = file;
1235
+ fileViewState.commitHash = commit.hash;
1236
+
1237
+ // Apply syntax highlighting based on file type
1238
+ applyFileViewHighlighting(bufferId, content, file);
1239
+
1240
+ const targetLine = line || 1;
1241
+ editor.setStatus(`${file} @ ${commit.shortHash} (read-only) | Target: line ${targetLine} | q: back`);
1242
+ } else {
1243
+ editor.setStatus(`Failed to open ${file}`);
1244
+ }
1245
+ } else {
1246
+ editor.setStatus("Move cursor to a diff line with file context");
1247
+ }
1248
+ } else {
1249
+ editor.setStatus("Move cursor to a diff line");
1250
+ }
1251
+ };
1252
+
1253
+ // =============================================================================
1254
+ // Command Registration
1255
+ // =============================================================================
1256
+
1257
+ editor.registerCommand(
1258
+ "Git Log",
1259
+ "Show git log in magit-style interface",
1260
+ "show_git_log",
1261
+ "normal"
1262
+ );
1263
+
1264
+ editor.registerCommand(
1265
+ "Git Log: Close",
1266
+ "Close the git log panel",
1267
+ "git_log_close",
1268
+ "normal"
1269
+ );
1270
+
1271
+ editor.registerCommand(
1272
+ "Git Log: Refresh",
1273
+ "Refresh the git log",
1274
+ "git_log_refresh",
1275
+ "normal"
1276
+ );
1277
+
1278
+ // =============================================================================
1279
+ // Plugin Initialization
1280
+ // =============================================================================
1281
+
1282
+ editor.setStatus("Git Log plugin loaded (magit-style)");
1283
+ editor.debug("Git Log plugin initialized - Use 'Git Log' command to open");