@involvex/fresh-editor 0.1.76 → 0.1.78

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