@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,642 @@
1
+ /// <reference path="./lib/fresh.d.ts" />
2
+ const editor = getEditor();
3
+
4
+
5
+ /**
6
+ * Diagnostics Panel Plugin
7
+ *
8
+ * Interactive diagnostics panel showing LSP diagnostics with:
9
+ * - Real-time updates when diagnostics change
10
+ * - Filter by current file or show all files
11
+ * - Cursor navigation with highlighting
12
+ * - Enter to jump to diagnostic location
13
+ */
14
+
15
+ // =============================================================================
16
+ // Types and Interfaces
17
+ // =============================================================================
18
+
19
+ interface DiagnosticLocation {
20
+ file: string;
21
+ line: number;
22
+ column: number;
23
+ }
24
+
25
+ interface DiagnosticLineMapping {
26
+ panelLine: number; // 1-based line in panel
27
+ location: DiagnosticLocation;
28
+ }
29
+
30
+ interface DiagnosticsState {
31
+ isOpen: boolean;
32
+ bufferId: number | null;
33
+ splitId: number | null;
34
+ sourceSplitId: number | null; // The split that was active when panel opened
35
+ sourceBufferId: number | null;
36
+ showAllFiles: boolean;
37
+ cachedContent: string;
38
+ // Maps panel line numbers to diagnostic locations for sync
39
+ lineMappings: DiagnosticLineMapping[];
40
+ // Current cursor line in the panel (1-indexed)
41
+ panelCursorLine: number;
42
+ }
43
+
44
+ // =============================================================================
45
+ // State Management
46
+ // =============================================================================
47
+
48
+ const state: DiagnosticsState = {
49
+ isOpen: false,
50
+ bufferId: null,
51
+ splitId: null,
52
+ sourceSplitId: null,
53
+ sourceBufferId: null,
54
+ showAllFiles: false, // Default to filtering by current file
55
+ cachedContent: "",
56
+ lineMappings: [],
57
+ panelCursorLine: 1,
58
+ };
59
+
60
+ // =============================================================================
61
+ // Color Definitions
62
+ // =============================================================================
63
+
64
+ const colors = {
65
+ error: [255, 100, 100] as [number, number, number],
66
+ warning: [255, 200, 100] as [number, number, number],
67
+ info: [100, 200, 255] as [number, number, number],
68
+ hint: [150, 150, 150] as [number, number, number],
69
+ file: [180, 180, 255] as [number, number, number],
70
+ location: [150, 255, 150] as [number, number, number],
71
+ header: [255, 200, 100] as [number, number, number],
72
+ selected: [80, 80, 120] as [number, number, number],
73
+ };
74
+
75
+ // =============================================================================
76
+ // Keybindings
77
+ // =============================================================================
78
+
79
+ const keybindings = {
80
+ goto: "Enter",
81
+ gotoAlt: "Tab",
82
+ toggleAll: "a",
83
+ refresh: "r",
84
+ close: "q",
85
+ closeAlt: "Escape",
86
+ // These are global keybindings, not part of the mode
87
+ nextDiag: "F8",
88
+ prevDiag: "Shift+F8",
89
+ };
90
+
91
+ // =============================================================================
92
+ // Mode Definition
93
+ // =============================================================================
94
+
95
+ editor.defineMode(
96
+ "diagnostics-list",
97
+ "normal",
98
+ [
99
+ ["Return", "diagnostics_goto"],
100
+ [keybindings.gotoAlt, "diagnostics_goto"],
101
+ [keybindings.toggleAll, "diagnostics_toggle_all"],
102
+ [keybindings.refresh, "diagnostics_refresh"],
103
+ [keybindings.close, "diagnostics_close"],
104
+ [keybindings.closeAlt, "diagnostics_close"],
105
+ ],
106
+ true
107
+ );
108
+
109
+ // =============================================================================
110
+ // Helpers
111
+ // =============================================================================
112
+
113
+ function severityIcon(severity: number): string {
114
+ switch (severity) {
115
+ case 1: return "[E]";
116
+ case 2: return "[W]";
117
+ case 3: return "[I]";
118
+ case 4: return "[H]";
119
+ default: return "[?]";
120
+ }
121
+ }
122
+
123
+ function uriToPath(uri: string): string {
124
+ if (uri.startsWith("file://")) {
125
+ return uri.slice(7);
126
+ }
127
+ return uri;
128
+ }
129
+
130
+ function getActiveFileUri(): string | null {
131
+ const bufferId = state.sourceBufferId ?? editor.getActiveBufferId();
132
+ const path = editor.getBufferPath(bufferId);
133
+ if (!path) return null;
134
+ return "file://" + path;
135
+ }
136
+
137
+ function entriesToContent(entries: TextPropertyEntry[]): string {
138
+ return entries.map(e => e.text).join("");
139
+ }
140
+
141
+ // =============================================================================
142
+ // Panel Content Building
143
+ // =============================================================================
144
+
145
+ function buildPanelEntries(): TextPropertyEntry[] {
146
+ const entries: TextPropertyEntry[] = [];
147
+ const diagnostics = editor.getAllDiagnostics();
148
+
149
+ // Clear and rebuild line mappings
150
+ state.lineMappings = [];
151
+
152
+ const activeUri = getActiveFileUri();
153
+ const filterUri = state.showAllFiles ? null : activeUri;
154
+
155
+ // Filter diagnostics
156
+ const filtered = filterUri
157
+ ? diagnostics.filter(d => d.uri === filterUri)
158
+ : diagnostics;
159
+
160
+ // Group by file
161
+ const byFile = new Map<string, TsDiagnostic[]>();
162
+ for (const diag of filtered) {
163
+ const existing = byFile.get(diag.uri) || [];
164
+ existing.push(diag);
165
+ byFile.set(diag.uri, existing);
166
+ }
167
+
168
+ // Sort files, with active file first if filtering
169
+ const files = Array.from(byFile.keys()).sort((a, b) => {
170
+ if (activeUri) {
171
+ if (a === activeUri) return -1;
172
+ if (b === activeUri) return 1;
173
+ }
174
+ // Simple string comparison (localeCompare has ICU issues in Deno)
175
+ if (a < b) return -1;
176
+ if (a > b) return 1;
177
+ return 0;
178
+ });
179
+
180
+ // Help line (line 1)
181
+ const helpText = `${keybindings.goto}:goto ${keybindings.close}:close ${keybindings.toggleAll}:toggle all ${keybindings.refresh}:refresh ${keybindings.nextDiag}/${keybindings.prevDiag}:next/prev\n`;
182
+ entries.push({
183
+ text: helpText,
184
+ properties: { type: "help" },
185
+ });
186
+
187
+ // Header (line 2)
188
+ let filterLabel: string;
189
+ if (state.showAllFiles) {
190
+ filterLabel = editor.t("panel.all_files");
191
+ } else if (activeUri) {
192
+ const fileName = editor.pathBasename(uriToPath(activeUri));
193
+ filterLabel = fileName;
194
+ } else {
195
+ filterLabel = editor.t("panel.current_file");
196
+ }
197
+ entries.push({
198
+ text: editor.t("panel.header", { filter: filterLabel }) + "\n",
199
+ properties: { type: "header" },
200
+ });
201
+
202
+ let currentPanelLine = 3; // Start after help + header
203
+
204
+ if (filtered.length === 0) {
205
+ entries.push({
206
+ text: " " + editor.t("panel.no_diagnostics") + "\n",
207
+ properties: { type: "empty" },
208
+ });
209
+ currentPanelLine++;
210
+ } else {
211
+ let diagIndex = 0;
212
+ for (const uri of files) {
213
+ const fileDiags = byFile.get(uri) || [];
214
+ const filePath = uriToPath(uri);
215
+ const fileName = editor.pathBasename(filePath);
216
+
217
+ // File header (blank line + filename)
218
+ entries.push({
219
+ text: `\n${fileName}:\n`,
220
+ properties: { type: "file-header", uri },
221
+ });
222
+ currentPanelLine += 2; // blank line + file header
223
+
224
+ // Sort diagnostics by line, then severity
225
+ fileDiags.sort((a, b) => {
226
+ const lineDiff = a.range.start.line - b.range.start.line;
227
+ if (lineDiff !== 0) return lineDiff;
228
+ return a.severity - b.severity;
229
+ });
230
+
231
+ for (const diag of fileDiags) {
232
+ const icon = severityIcon(diag.severity);
233
+ const line = diag.range.start.line + 1;
234
+ const col = diag.range.start.character + 1;
235
+ const msg = diag.message.split("\n")[0]; // First line only
236
+
237
+ const location: DiagnosticLocation = {
238
+ file: filePath,
239
+ line: line,
240
+ column: col,
241
+ };
242
+
243
+ // Track mapping for cursor sync
244
+ state.lineMappings.push({
245
+ panelLine: currentPanelLine,
246
+ location: location,
247
+ });
248
+
249
+ entries.push({
250
+ text: ` ${icon} ${line}:${col} ${msg}\n`,
251
+ properties: {
252
+ type: "diagnostic",
253
+ index: diagIndex,
254
+ severity: diag.severity,
255
+ location: location,
256
+ },
257
+ });
258
+ diagIndex++;
259
+ currentPanelLine++;
260
+ }
261
+ }
262
+ }
263
+
264
+ // Summary
265
+ const errorCount = filtered.filter(d => d.severity === 1).length;
266
+ const warningCount = filtered.filter(d => d.severity === 2).length;
267
+ const infoCount = filtered.filter(d => d.severity === 3).length;
268
+
269
+ entries.push({
270
+ text: "\n",
271
+ properties: { type: "blank" },
272
+ });
273
+ entries.push({
274
+ text: `${errorCount}E ${warningCount}W ${infoCount}I | a: toggle filter | r: refresh | RET: goto | q: close\n`,
275
+ properties: { type: "footer" },
276
+ });
277
+
278
+ return entries;
279
+ }
280
+
281
+ // =============================================================================
282
+ // Highlighting
283
+ // =============================================================================
284
+
285
+ function applyHighlighting(): void {
286
+ if (state.bufferId === null) return;
287
+
288
+ const bufferId = state.bufferId;
289
+ editor.clearNamespace(bufferId, "diag");
290
+
291
+ const content = state.cachedContent;
292
+ if (!content) return;
293
+
294
+ const lines = content.split("\n");
295
+ const cursorLine = state.panelCursorLine;
296
+
297
+ let byteOffset = 0;
298
+
299
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
300
+ const line = lines[lineIdx];
301
+ const lineStart = byteOffset;
302
+ const lineEnd = byteOffset + line.length;
303
+ const isCurrentLine = (lineIdx + 1) === cursorLine;
304
+ const isDiagnosticLine = line.trim().startsWith("[");
305
+
306
+ // Highlight current line if it's a diagnostic line (entire line gets background)
307
+ if (isCurrentLine && isDiagnosticLine) {
308
+ editor.addOverlay(
309
+ bufferId, "diag", lineStart, lineEnd,
310
+ colors.selected[0], colors.selected[1], colors.selected[2],
311
+ true, true, false
312
+ );
313
+ }
314
+
315
+ // Help line highlighting (dimmed)
316
+ if (line.startsWith("Enter:")) {
317
+ editor.addOverlay(
318
+ bufferId, "diag", lineStart, lineEnd,
319
+ colors.hint[0], colors.hint[1], colors.hint[2],
320
+ false, true, false
321
+ );
322
+ }
323
+
324
+ // Header highlighting
325
+ if (line.startsWith("Diagnostics")) {
326
+ editor.addOverlay(
327
+ bufferId, "diag", lineStart, lineEnd,
328
+ colors.header[0], colors.header[1], colors.header[2],
329
+ true, true, false
330
+ );
331
+ }
332
+
333
+ // File header highlighting
334
+ if (line.endsWith(":") && !line.startsWith("Diagnostics") && !line.startsWith(" ")) {
335
+ editor.addOverlay(
336
+ bufferId, "diag", lineStart, lineEnd,
337
+ colors.file[0], colors.file[1], colors.file[2],
338
+ false, true, false
339
+ );
340
+ }
341
+
342
+ // Severity icon highlighting
343
+ const iconMatch = line.match(/^\s+\[([EWIH?])\]/);
344
+ if (iconMatch) {
345
+ const iconStart = lineStart + line.indexOf("[");
346
+ const iconEnd = iconStart + 3;
347
+
348
+ let color: [number, number, number];
349
+ switch (iconMatch[1]) {
350
+ case "E": color = colors.error; break;
351
+ case "W": color = colors.warning; break;
352
+ case "I": color = colors.info; break;
353
+ case "H": color = colors.hint; break;
354
+ default: color = colors.hint;
355
+ }
356
+
357
+ editor.addOverlay(
358
+ bufferId, "diag", iconStart, iconEnd,
359
+ color[0], color[1], color[2],
360
+ false, true, false
361
+ );
362
+
363
+ // Location highlighting (line:col after icon)
364
+ const locMatch = line.match(/\[.\]\s+(\d+:\d+)/);
365
+ if (locMatch && locMatch.index !== undefined) {
366
+ const locStart = lineStart + line.indexOf(locMatch[1]);
367
+ const locEnd = locStart + locMatch[1].length;
368
+ editor.addOverlay(
369
+ bufferId, "diag", locStart, locEnd,
370
+ colors.location[0], colors.location[1], colors.location[2],
371
+ false, false, false
372
+ );
373
+ }
374
+ }
375
+
376
+ byteOffset += line.length + 1;
377
+ }
378
+ }
379
+
380
+ function updatePanel(): void {
381
+ if (state.bufferId === null) return;
382
+
383
+ const entries = buildPanelEntries();
384
+ state.cachedContent = entriesToContent(entries);
385
+ editor.setVirtualBufferContent(state.bufferId, entries);
386
+ applyHighlighting();
387
+ }
388
+
389
+ // =============================================================================
390
+ // Commands
391
+ // =============================================================================
392
+
393
+ globalThis.show_diagnostics_panel = async function(): Promise<void> {
394
+ if (state.isOpen) {
395
+ // If already open, just focus the panel
396
+ if (state.splitId !== null) {
397
+ editor.focusSplit(state.splitId);
398
+ }
399
+ return;
400
+ }
401
+
402
+ state.sourceSplitId = editor.getActiveSplitId();
403
+ state.sourceBufferId = editor.getActiveBufferId();
404
+
405
+ const entries = buildPanelEntries();
406
+ state.cachedContent = entriesToContent(entries);
407
+
408
+ // Create a horizontal split below the current buffer
409
+ const result = await editor.createVirtualBufferInSplit({
410
+ name: "*Diagnostics*",
411
+ mode: "diagnostics-list",
412
+ read_only: true,
413
+ entries: entries,
414
+ ratio: 0.7, // Source keeps 70%, panel takes 30%
415
+ direction: "horizontal", // Split below
416
+ panel_id: "diagnostics", // Enable idempotent updates
417
+ show_line_numbers: false,
418
+ show_cursors: true,
419
+ editing_disabled: true,
420
+ });
421
+
422
+ if (result.buffer_id !== null) {
423
+ state.isOpen = true;
424
+ state.bufferId = result.buffer_id;
425
+ state.splitId = result.split_id ?? null;
426
+ applyHighlighting();
427
+
428
+ const diagnostics = editor.getAllDiagnostics();
429
+ editor.setStatus(editor.t("status.diagnostics_count", { count: String(diagnostics.length) }));
430
+ } else {
431
+ state.sourceSplitId = null;
432
+ state.sourceBufferId = null;
433
+ editor.setStatus(editor.t("status.failed_to_open"));
434
+ }
435
+ };
436
+
437
+ globalThis.diagnostics_close = function(): void {
438
+ if (!state.isOpen) return;
439
+
440
+ // Capture values before clearing state
441
+ const splitId = state.splitId;
442
+ const sourceSplitId = state.sourceSplitId;
443
+ const sourceBufferId = state.sourceBufferId;
444
+ const bufferId = state.bufferId;
445
+
446
+ // Clear state FIRST to prevent event handlers from trying to update
447
+ state.isOpen = false;
448
+ state.bufferId = null;
449
+ state.splitId = null;
450
+ state.sourceSplitId = null;
451
+ state.sourceBufferId = null;
452
+ state.cachedContent = "";
453
+
454
+ // Try to close the split first
455
+ let splitClosed = false;
456
+ if (splitId !== null) {
457
+ splitClosed = editor.closeSplit(splitId);
458
+ }
459
+
460
+ // If split couldn't be closed (only split), switch buffer back to source
461
+ if (!splitClosed && splitId !== null && sourceBufferId !== null) {
462
+ editor.setSplitBuffer(splitId, sourceBufferId);
463
+ }
464
+
465
+ // Always delete the virtual buffer completely (removes from all splits)
466
+ if (bufferId !== null) {
467
+ editor.closeBuffer(bufferId);
468
+ }
469
+
470
+ // Focus back on the source split
471
+ if (sourceSplitId !== null) {
472
+ editor.focusSplit(sourceSplitId);
473
+ }
474
+
475
+ editor.setStatus(editor.t("status.closed"));
476
+ };
477
+
478
+ globalThis.diagnostics_goto = function(): void {
479
+ if (!state.isOpen || state.bufferId === null) return;
480
+
481
+ const props = editor.getTextPropertiesAtCursor(state.bufferId);
482
+
483
+ if (props.length > 0) {
484
+ const location = props[0].location as { file: string; line: number; column: number } | undefined;
485
+ if (location) {
486
+ const file = location.file;
487
+ const line = location.line;
488
+ const col = location.column;
489
+
490
+ // Focus back on the source split and navigate to the location
491
+ if (state.sourceSplitId !== null) {
492
+ editor.focusSplit(state.sourceSplitId);
493
+ }
494
+ editor.openFile(file, line, col);
495
+ editor.setStatus(editor.t("status.jumped_to", { file: editor.pathBasename(file), line: String(line) }));
496
+ return;
497
+ }
498
+ }
499
+
500
+ editor.setStatus(editor.t("status.move_to_diagnostic"));
501
+ };
502
+
503
+ globalThis.diagnostics_toggle_all = function(): void {
504
+ if (!state.isOpen) return;
505
+
506
+ state.showAllFiles = !state.showAllFiles;
507
+ updatePanel();
508
+
509
+ const label = state.showAllFiles ? editor.t("panel.all_files") : editor.t("panel.current_file");
510
+ editor.setStatus(editor.t("status.showing", { label }));
511
+ };
512
+
513
+ globalThis.diagnostics_refresh = function(): void {
514
+ if (!state.isOpen) return;
515
+
516
+ updatePanel();
517
+ editor.setStatus(editor.t("status.refreshed"));
518
+ };
519
+
520
+ globalThis.toggle_diagnostics_panel = function(): void {
521
+ if (state.isOpen) {
522
+ globalThis.diagnostics_close();
523
+ } else {
524
+ globalThis.show_diagnostics_panel();
525
+ }
526
+ };
527
+
528
+ // =============================================================================
529
+ // Event Handlers
530
+ // =============================================================================
531
+
532
+ // Find the panel line that matches a source file location
533
+ function findPanelLineForLocation(file: string, sourceLine: number): number | null {
534
+ // Find the first diagnostic on this source line for this file
535
+ for (const mapping of state.lineMappings) {
536
+ if (mapping.location.file === file && mapping.location.line === sourceLine) {
537
+ return mapping.panelLine;
538
+ }
539
+ }
540
+ return null;
541
+ }
542
+
543
+ // Convert a 1-based line number to byte offset in the cached content
544
+ function lineToByteOffset(lineNumber: number): number {
545
+ const lines = state.cachedContent.split("\n");
546
+ let offset = 0;
547
+ for (let i = 0; i < lineNumber - 1 && i < lines.length; i++) {
548
+ offset += lines[i].length + 1; // +1 for newline
549
+ }
550
+ return offset;
551
+ }
552
+
553
+ // Sync the panel cursor to match a source location
554
+ function syncPanelCursorToSourceLine(file: string, sourceLine: number): void {
555
+ if (state.bufferId === null) return;
556
+
557
+ const panelLine = findPanelLineForLocation(file, sourceLine);
558
+ if (panelLine !== null) {
559
+ // Convert panel line number to byte offset and move cursor
560
+ const byteOffset = lineToByteOffset(panelLine);
561
+ state.panelCursorLine = panelLine;
562
+ editor.setBufferCursor(state.bufferId, byteOffset);
563
+ applyHighlighting();
564
+ }
565
+ }
566
+
567
+ globalThis.on_diagnostics_cursor_moved = function(data: {
568
+ buffer_id: number;
569
+ cursor_id: number;
570
+ old_position: number;
571
+ new_position: number;
572
+ line: number;
573
+ }): void {
574
+ if (!state.isOpen || state.bufferId === null) return;
575
+
576
+ // If cursor moved in the diagnostics panel, update the tracked line and highlighting
577
+ if (data.buffer_id === state.bufferId) {
578
+ state.panelCursorLine = data.line;
579
+ applyHighlighting();
580
+ return;
581
+ }
582
+
583
+ // Cursor moved in a non-panel buffer - sync the panel cursor to match
584
+ // This handles F8/Shift+F8 jumps and normal cursor movement in source buffers
585
+ const path = editor.getBufferPath(data.buffer_id);
586
+ if (path) {
587
+ syncPanelCursorToSourceLine(path, data.line);
588
+ }
589
+ };
590
+
591
+ globalThis.on_diagnostics_updated = function(_data: {
592
+ uri: string;
593
+ count: number;
594
+ }): void {
595
+ if (!state.isOpen) return;
596
+ updatePanel();
597
+ };
598
+
599
+ globalThis.on_diagnostics_buffer_activated = function(data: {
600
+ buffer_id: number;
601
+ }): void {
602
+ if (!state.isOpen) return;
603
+
604
+ // If the diagnostics panel itself became active, don't update source tracking
605
+ if (data.buffer_id === state.bufferId) {
606
+ return;
607
+ }
608
+
609
+ // A different buffer became active - update source buffer and refresh the panel
610
+ state.sourceBufferId = data.buffer_id;
611
+ updatePanel();
612
+ };
613
+
614
+ // Register event handlers
615
+ editor.on("cursor_moved", "on_diagnostics_cursor_moved");
616
+ editor.on("diagnostics_updated", "on_diagnostics_updated");
617
+ editor.on("buffer_activated", "on_diagnostics_buffer_activated");
618
+
619
+ // =============================================================================
620
+ // Command Registration
621
+ // =============================================================================
622
+
623
+ editor.registerCommand(
624
+ "%cmd.show_diagnostics_panel",
625
+ "%cmd.show_diagnostics_panel_desc",
626
+ "show_diagnostics_panel",
627
+ "normal"
628
+ );
629
+
630
+ editor.registerCommand(
631
+ "%cmd.toggle_diagnostics_panel",
632
+ "%cmd.toggle_diagnostics_panel_desc",
633
+ "toggle_diagnostics_panel",
634
+ "normal"
635
+ );
636
+
637
+ // =============================================================================
638
+ // Initialization
639
+ // =============================================================================
640
+
641
+ editor.setStatus(editor.t("status.loaded"));
642
+ editor.debug("Diagnostics Panel plugin initialized");