@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.
- package/bin/CHANGELOG.md +1017 -0
- package/bin/LICENSE +117 -0
- package/bin/README.md +248 -0
- package/bin/fresh.exe +0 -0
- package/bin/plugins/README.md +71 -0
- package/bin/plugins/audit_mode.i18n.json +821 -0
- package/bin/plugins/audit_mode.ts +1810 -0
- package/bin/plugins/buffer_modified.i18n.json +67 -0
- package/bin/plugins/buffer_modified.ts +281 -0
- package/bin/plugins/calculator.i18n.json +93 -0
- package/bin/plugins/calculator.ts +770 -0
- package/bin/plugins/clangd-lsp.ts +168 -0
- package/bin/plugins/clangd_support.i18n.json +223 -0
- package/bin/plugins/clangd_support.md +20 -0
- package/bin/plugins/clangd_support.ts +325 -0
- package/bin/plugins/color_highlighter.i18n.json +145 -0
- package/bin/plugins/color_highlighter.ts +304 -0
- package/bin/plugins/config-schema.json +768 -0
- package/bin/plugins/csharp-lsp.ts +147 -0
- package/bin/plugins/csharp_support.i18n.json +80 -0
- package/bin/plugins/csharp_support.ts +170 -0
- package/bin/plugins/css-lsp.ts +143 -0
- package/bin/plugins/diagnostics_panel.i18n.json +236 -0
- package/bin/plugins/diagnostics_panel.ts +642 -0
- package/bin/plugins/examples/README.md +85 -0
- package/bin/plugins/examples/async_demo.ts +165 -0
- package/bin/plugins/examples/bookmarks.ts +329 -0
- package/bin/plugins/examples/buffer_query_demo.ts +110 -0
- package/bin/plugins/examples/git_grep.ts +262 -0
- package/bin/plugins/examples/hello_world.ts +93 -0
- package/bin/plugins/examples/virtual_buffer_demo.ts +116 -0
- package/bin/plugins/find_references.i18n.json +275 -0
- package/bin/plugins/find_references.ts +359 -0
- package/bin/plugins/git_blame.i18n.json +496 -0
- package/bin/plugins/git_blame.ts +707 -0
- package/bin/plugins/git_find_file.i18n.json +314 -0
- package/bin/plugins/git_find_file.ts +300 -0
- package/bin/plugins/git_grep.i18n.json +171 -0
- package/bin/plugins/git_grep.ts +191 -0
- package/bin/plugins/git_gutter.i18n.json +93 -0
- package/bin/plugins/git_gutter.ts +477 -0
- package/bin/plugins/git_log.i18n.json +481 -0
- package/bin/plugins/git_log.ts +1285 -0
- package/bin/plugins/go-lsp.ts +143 -0
- package/bin/plugins/html-lsp.ts +145 -0
- package/bin/plugins/json-lsp.ts +145 -0
- package/bin/plugins/lib/fresh.d.ts +1321 -0
- package/bin/plugins/lib/index.ts +24 -0
- package/bin/plugins/lib/navigation-controller.ts +214 -0
- package/bin/plugins/lib/panel-manager.ts +220 -0
- package/bin/plugins/lib/types.ts +72 -0
- package/bin/plugins/lib/virtual-buffer-factory.ts +130 -0
- package/bin/plugins/live_grep.i18n.json +171 -0
- package/bin/plugins/live_grep.ts +422 -0
- package/bin/plugins/markdown_compose.i18n.json +223 -0
- package/bin/plugins/markdown_compose.ts +630 -0
- package/bin/plugins/merge_conflict.i18n.json +821 -0
- package/bin/plugins/merge_conflict.ts +1810 -0
- package/bin/plugins/path_complete.i18n.json +80 -0
- package/bin/plugins/path_complete.ts +165 -0
- package/bin/plugins/python-lsp.ts +162 -0
- package/bin/plugins/rust-lsp.ts +166 -0
- package/bin/plugins/search_replace.i18n.json +405 -0
- package/bin/plugins/search_replace.ts +484 -0
- package/bin/plugins/test_i18n.i18n.json +67 -0
- package/bin/plugins/test_i18n.ts +18 -0
- package/bin/plugins/theme_editor.i18n.json +3746 -0
- package/bin/plugins/theme_editor.ts +2063 -0
- package/bin/plugins/todo_highlighter.i18n.json +184 -0
- package/bin/plugins/todo_highlighter.ts +206 -0
- package/bin/plugins/typescript-lsp.ts +167 -0
- package/bin/plugins/vi_mode.i18n.json +1549 -0
- package/bin/plugins/vi_mode.ts +2747 -0
- package/bin/plugins/welcome.i18n.json +236 -0
- package/bin/plugins/welcome.ts +76 -0
- package/bin/themes/dark.json +102 -0
- package/bin/themes/dracula.json +62 -0
- package/bin/themes/high-contrast.json +102 -0
- package/bin/themes/light.json +102 -0
- package/bin/themes/nord.json +62 -0
- package/bin/themes/nostalgia.json +102 -0
- package/bin/themes/solarized-dark.json +62 -0
- package/binary-install.js +1 -1
- package/dist/bin/fresh.js +9 -0
- package/dist/binary-install.js +149 -0
- package/dist/binary.js +30 -0
- package/dist/fresh-6yhknp07.exe +0 -0
- package/dist/install.js +158 -0
- package/dist/run-fresh.js +43 -0
- 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");
|