@fresh-editor/fresh-editor 0.1.76 → 0.1.83

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.
@@ -1,914 +0,0 @@
1
- /// <reference path="./fresh.d.ts" />
2
-
3
- import type { Location, RGB } from "./types.ts";
4
-
5
- /**
6
- * ResultsPanel v2 - VS Code-inspired Provider Pattern
7
- *
8
- * Key architecture principles (lessons from VS Code's TreeDataProvider):
9
- * 1. Don't pass arrays, pass Providers - creates a live data channel
10
- * 2. Standardize the Item shape - Core handles sync automatically
11
- * 3. Event-driven updates - Provider emits events, Panel refreshes
12
- *
13
- * @example
14
- * ```typescript
15
- * // Create a provider that emits change events
16
- * class MyProvider implements ResultsProvider<ResultItem> {
17
- * private items: ResultItem[] = [];
18
- * private emitter = new EventEmitter<void>();
19
- * readonly onDidChangeResults = this.emitter.event;
20
- *
21
- * updateItems(newItems: ResultItem[]) {
22
- * this.items = newItems;
23
- * this.emitter.fire(); // Notify panel to refresh
24
- * }
25
- *
26
- * provideResults() {
27
- * return this.items;
28
- * }
29
- * }
30
- *
31
- * const provider = new MyProvider();
32
- * const panel = new ResultsPanel(editor, "references", provider, {
33
- * title: "References",
34
- * syncWithEditor: true,
35
- * onSelect: (item) => {
36
- * if (item.location) {
37
- * panel.openInSource(item.location.file, item.location.line, item.location.column);
38
- * }
39
- * },
40
- * });
41
- *
42
- * // Later: update data
43
- * provider.updateItems(newReferences);
44
- * ```
45
- */
46
-
47
- // ============================================================================
48
- // Event System (Simplified VS Code-style)
49
- // ============================================================================
50
-
51
- /**
52
- * A function that can be called to unsubscribe from an event
53
- */
54
- export type Disposable = () => void;
55
-
56
- /**
57
- * An event that can be subscribed to
58
- */
59
- export type Event<T> = (listener: (e: T) => void) => Disposable;
60
-
61
- /**
62
- * Simple event emitter for Provider → Panel communication
63
- */
64
- export class EventEmitter<T> {
65
- private listeners: Array<(e: T) => void> = [];
66
-
67
- /**
68
- * The event that others can subscribe to
69
- */
70
- readonly event: Event<T> = (listener) => {
71
- this.listeners.push(listener);
72
- return () => {
73
- const index = this.listeners.indexOf(listener);
74
- if (index >= 0) {
75
- this.listeners.splice(index, 1);
76
- }
77
- };
78
- };
79
-
80
- /**
81
- * Fire the event, notifying all listeners
82
- */
83
- fire(data: T): void {
84
- for (const listener of this.listeners) {
85
- try {
86
- listener(data);
87
- } catch (e) {
88
- // Don't let one listener break others
89
- console.error("Event listener error:", e);
90
- }
91
- }
92
- }
93
-
94
- /**
95
- * Fire without data (for void events)
96
- */
97
- fireVoid(): void {
98
- this.fire(undefined as T);
99
- }
100
- }
101
-
102
- // ============================================================================
103
- // Core Interfaces
104
- // ============================================================================
105
-
106
- /**
107
- * Standard shape for any item in a results list.
108
- *
109
- * By enforcing a standard `location` property, the Core can implement
110
- * "Sync Cursor" logic once, globally, rather than asking every plugin
111
- * to write custom sync callbacks.
112
- */
113
- export interface ResultItem {
114
- /** Unique identifier for this item (used for reveal/selection) */
115
- id: string;
116
-
117
- /** Primary text shown for this item */
118
- label: string;
119
-
120
- /** Secondary text (e.g., code preview) */
121
- description?: string;
122
-
123
- /**
124
- * Location in source file - CRITICAL for Core-managed features:
125
- * - Bidirectional cursor sync (syncWithEditor)
126
- * - Navigation (Enter to jump)
127
- */
128
- location?: Location;
129
-
130
- /** Severity for visual styling (error/warning/info badge) */
131
- severity?: "error" | "warning" | "info" | "hint";
132
-
133
- /** Custom data attached to this item */
134
- metadata?: unknown;
135
- }
136
-
137
- /**
138
- * The Provider acts as the bridge between Plugin Logic and UI.
139
- *
140
- * This matches VS Code's TreeDataProvider pattern where:
141
- * - Provider owns the data and business logic
142
- * - Panel owns the UI rendering and interaction
143
- */
144
- export interface ResultsProvider<T extends ResultItem = ResultItem> {
145
- /**
146
- * Data Retrieval: Core calls this when it needs the current items.
147
- * Can be sync or async.
148
- */
149
- provideResults(): T[] | Promise<T[]>;
150
-
151
- /**
152
- * Reactivity: Plugin fires this to tell Core "My data changed, please refresh".
153
- * Matches VS Code's 'onDidChangeTreeData' pattern.
154
- *
155
- * If omitted, the panel won't auto-refresh; you must call panel.refresh() manually.
156
- */
157
- onDidChangeResults?: Event<void>;
158
-
159
- /**
160
- * Optional filtering logic for complex custom filtering.
161
- * If omitted, Core does simple substring matching on label.
162
- */
163
- filter?(item: T, query: string): boolean;
164
- }
165
-
166
- // ============================================================================
167
- // Panel Options
168
- // ============================================================================
169
-
170
- /**
171
- * Options for creating a ResultsPanel
172
- */
173
- export interface ResultsPanelOptions<T extends ResultItem = ResultItem> {
174
- /** Title shown at top of panel */
175
- title: string;
176
-
177
- /**
178
- * Bidirectional Sync: If true, Core automatically highlights items
179
- * matching the active editor's cursor position, based on the item's
180
- * `location` property. No custom callback needed.
181
- */
182
- syncWithEditor?: boolean;
183
-
184
- /**
185
- * Grouping strategy for items.
186
- * - 'file': Group by file path (default for location-based items)
187
- * - 'severity': Group by severity level
188
- * - 'none': Flat list, no grouping
189
- */
190
- groupBy?: "file" | "severity" | "none";
191
-
192
- /** Split ratio (default 0.7 = source keeps 70%) */
193
- ratio?: number;
194
-
195
- /** Called when user presses Enter on an item */
196
- onSelect?: (item: T, index: number) => void;
197
-
198
- /** Called when user presses Escape */
199
- onClose?: () => void;
200
-
201
- /** Called when cursor moves to a new item (for preview updates) */
202
- onCursorMove?: (item: T, index: number) => void;
203
- }
204
-
205
- // ============================================================================
206
- // Colors
207
- // ============================================================================
208
-
209
- const colors = {
210
- selected: [80, 80, 120] as RGB,
211
- location: [150, 255, 150] as RGB,
212
- help: [150, 150, 150] as RGB,
213
- title: [200, 200, 255] as RGB,
214
- error: [255, 100, 100] as RGB,
215
- warning: [255, 200, 100] as RGB,
216
- info: [100, 200, 255] as RGB,
217
- hint: [150, 150, 150] as RGB,
218
- fileHeader: [180, 180, 255] as RGB,
219
- };
220
-
221
- // ============================================================================
222
- // Internal State
223
- // ============================================================================
224
-
225
- interface PanelState<T extends ResultItem> {
226
- isOpen: boolean;
227
- bufferId: number | null;
228
- splitId: number | null;
229
- sourceSplitId: number | null;
230
- cachedContent: string;
231
- cursorLine: number;
232
- items: T[];
233
- // Maps panel line -> item index (for sync)
234
- lineToItemIndex: Map<number, number>;
235
- }
236
-
237
- // ============================================================================
238
- // ResultsPanel Class
239
- // ============================================================================
240
-
241
- /**
242
- * ResultsPanel - manages a results list panel with Provider pattern
243
- */
244
- export class ResultsPanel<T extends ResultItem = ResultItem> {
245
- private state: PanelState<T> = {
246
- isOpen: false,
247
- bufferId: null,
248
- splitId: null,
249
- sourceSplitId: null,
250
- cachedContent: "",
251
- cursorLine: 1,
252
- items: [],
253
- lineToItemIndex: new Map(),
254
- };
255
-
256
- private readonly modeName: string;
257
- private readonly panelName: string;
258
- private readonly namespace: string;
259
- private readonly handlerPrefix: string;
260
-
261
- private providerDisposable: Disposable | null = null;
262
- private cursorSyncDisposable: Disposable | null = null;
263
-
264
- /**
265
- * Create a new ResultsPanel with a Provider
266
- *
267
- * @param editor - The editor API instance
268
- * @param id - Unique identifier for this panel (e.g., "references", "diagnostics")
269
- * @param provider - The data provider
270
- * @param options - Panel configuration
271
- */
272
- constructor(
273
- private readonly editor: EditorAPI,
274
- private readonly id: string,
275
- private readonly provider: ResultsProvider<T>,
276
- private readonly options: ResultsPanelOptions<T>
277
- ) {
278
- this.modeName = `${id}-results`;
279
- this.panelName = `*${id.charAt(0).toUpperCase() + id.slice(1)}*`;
280
- this.namespace = id;
281
- this.handlerPrefix = `_results_panel_${id}`;
282
-
283
- // Define mode with minimal keybindings (navigation inherited from "normal")
284
- editor.defineMode(
285
- this.modeName,
286
- "normal",
287
- [
288
- ["Return", `${this.handlerPrefix}_select`],
289
- ["Escape", `${this.handlerPrefix}_close`],
290
- ],
291
- true
292
- );
293
-
294
- // Register global handlers
295
- this.registerHandlers();
296
-
297
- // Auto-subscribe to provider changes (the "VS Code Way")
298
- if (this.provider.onDidChangeResults) {
299
- this.providerDisposable = this.provider.onDidChangeResults(() => {
300
- if (this.state.isOpen) {
301
- this.refresh();
302
- }
303
- });
304
- }
305
- }
306
-
307
- // ==========================================================================
308
- // Public API
309
- // ==========================================================================
310
-
311
- get isOpen(): boolean {
312
- return this.state.isOpen;
313
- }
314
-
315
- get bufferId(): number | null {
316
- return this.state.bufferId;
317
- }
318
-
319
- get sourceSplitId(): number | null {
320
- return this.state.sourceSplitId;
321
- }
322
-
323
- /**
324
- * Show the panel, fetching items from the provider
325
- */
326
- async show(): Promise<void> {
327
- // Save source context if not already open
328
- if (!this.state.isOpen) {
329
- this.state.sourceSplitId = this.editor.getActiveSplitId();
330
- }
331
-
332
- // Fetch items from provider
333
- this.state.items = await Promise.resolve(this.provider.provideResults());
334
-
335
- // Build entries
336
- const entries = this.buildEntries();
337
- this.state.cachedContent = entries.map((e) => e.text).join("");
338
- this.state.cursorLine = this.findFirstItemLine();
339
-
340
- try {
341
- const result = await this.editor.createVirtualBufferInSplit({
342
- name: this.panelName,
343
- mode: this.modeName,
344
- read_only: true,
345
- entries: entries,
346
- ratio: this.options.ratio ?? 0.7,
347
- direction: "horizontal",
348
- panel_id: this.id,
349
- show_line_numbers: false,
350
- show_cursors: true,
351
- editing_disabled: true,
352
- });
353
-
354
- if (result.buffer_id !== null) {
355
- this.state.bufferId = result.buffer_id;
356
- this.state.splitId = result.split_id ?? null;
357
- this.state.isOpen = true;
358
- this.applyHighlighting();
359
-
360
- // Enable bidirectional cursor sync if requested
361
- if (this.options.syncWithEditor) {
362
- this.enableCursorSync();
363
- }
364
-
365
- const count = this.state.items.length;
366
- this.editor.setStatus(
367
- `${this.options.title}: ${count} item${count !== 1 ? "s" : ""}`
368
- );
369
- } else {
370
- this.editor.setStatus(`Failed to open ${this.panelName}`);
371
- }
372
- } catch (error) {
373
- const msg = error instanceof Error ? error.message : String(error);
374
- this.editor.setStatus(`Failed to open panel: ${msg}`);
375
- this.editor.debug(`ResultsPanel error: ${msg}`);
376
- }
377
- }
378
-
379
- /**
380
- * Refresh the panel by re-fetching from the provider
381
- */
382
- async refresh(): Promise<void> {
383
- if (!this.state.isOpen || this.state.bufferId === null) {
384
- return;
385
- }
386
-
387
- this.state.items = await Promise.resolve(this.provider.provideResults());
388
-
389
- const entries = this.buildEntries();
390
- this.state.cachedContent = entries.map((e) => e.text).join("");
391
-
392
- this.editor.setVirtualBufferContent(this.state.bufferId, entries);
393
- this.applyHighlighting();
394
-
395
- const count = this.state.items.length;
396
- this.editor.setStatus(
397
- `${this.options.title}: ${count} item${count !== 1 ? "s" : ""}`
398
- );
399
- }
400
-
401
- /**
402
- * Close the panel and clean up
403
- */
404
- close(): void {
405
- if (!this.state.isOpen) {
406
- return;
407
- }
408
-
409
- // Capture values before clearing
410
- const splitId = this.state.splitId;
411
- const bufferId = this.state.bufferId;
412
- const sourceSplitId = this.state.sourceSplitId;
413
-
414
- // Disable cursor sync
415
- if (this.cursorSyncDisposable) {
416
- this.cursorSyncDisposable();
417
- this.cursorSyncDisposable = null;
418
- }
419
-
420
- // Clear state
421
- this.state.isOpen = false;
422
- this.state.bufferId = null;
423
- this.state.splitId = null;
424
- this.state.sourceSplitId = null;
425
- this.state.cachedContent = "";
426
- this.state.cursorLine = 1;
427
- this.state.items = [];
428
- this.state.lineToItemIndex.clear();
429
-
430
- // Close split and buffer
431
- if (splitId !== null) {
432
- this.editor.closeSplit(splitId);
433
- }
434
- if (bufferId !== null) {
435
- this.editor.closeBuffer(bufferId);
436
- }
437
-
438
- // Focus source
439
- if (sourceSplitId !== null) {
440
- this.editor.focusSplit(sourceSplitId);
441
- }
442
-
443
- // Call user callback
444
- if (this.options.onClose) {
445
- this.options.onClose();
446
- }
447
-
448
- this.editor.setStatus(`${this.panelName} closed`);
449
- }
450
-
451
- /**
452
- * Reveal an item by ID (scroll to and highlight)
453
- */
454
- reveal(itemId: string, options?: { focus?: boolean; select?: boolean }): void {
455
- if (!this.state.isOpen || this.state.bufferId === null) return;
456
-
457
- const index = this.state.items.findIndex((item) => item.id === itemId);
458
- if (index === -1) return;
459
-
460
- // Find the panel line for this item
461
- for (const [line, idx] of this.state.lineToItemIndex) {
462
- if (idx === index) {
463
- this.state.cursorLine = line;
464
-
465
- // Move cursor to this line
466
- const byteOffset = this.lineToByteOffset(line);
467
- this.editor.setBufferCursor(this.state.bufferId, byteOffset);
468
- this.applyHighlighting();
469
-
470
- if (options?.focus) {
471
- this.focusPanel();
472
- }
473
- break;
474
- }
475
- }
476
- }
477
-
478
- /**
479
- * Open a file in the source split and jump to location
480
- */
481
- openInSource(file: string, line: number, column: number): void {
482
- if (this.state.sourceSplitId === null) return;
483
-
484
- this.editor.focusSplit(this.state.sourceSplitId);
485
- this.editor.openFile(file, line, column);
486
- }
487
-
488
- /**
489
- * Focus the source split
490
- */
491
- focusSource(): void {
492
- if (this.state.sourceSplitId !== null) {
493
- this.editor.focusSplit(this.state.sourceSplitId);
494
- }
495
- }
496
-
497
- /**
498
- * Focus the panel split
499
- */
500
- focusPanel(): void {
501
- if (this.state.splitId !== null) {
502
- this.editor.focusSplit(this.state.splitId);
503
- }
504
- }
505
-
506
- /**
507
- * Get the currently selected item
508
- */
509
- getSelectedItem(): T | null {
510
- const index = this.state.lineToItemIndex.get(this.state.cursorLine);
511
- if (index !== undefined && index < this.state.items.length) {
512
- return this.state.items[index];
513
- }
514
- return null;
515
- }
516
-
517
- /**
518
- * Dispose the panel and all subscriptions
519
- */
520
- dispose(): void {
521
- this.close();
522
- if (this.providerDisposable) {
523
- this.providerDisposable();
524
- this.providerDisposable = null;
525
- }
526
- }
527
-
528
- // ==========================================================================
529
- // Private Methods
530
- // ==========================================================================
531
-
532
- private registerHandlers(): void {
533
- const self = this;
534
-
535
- // Select handler (Enter)
536
- (globalThis as Record<string, unknown>)[`${this.handlerPrefix}_select`] =
537
- function (): void {
538
- if (!self.state.isOpen) return;
539
-
540
- const item = self.getSelectedItem();
541
- if (item && self.options.onSelect) {
542
- const index = self.state.items.indexOf(item);
543
- self.options.onSelect(item, index);
544
- } else if (!item) {
545
- self.editor.setStatus("No item selected");
546
- }
547
- };
548
-
549
- // Close handler (Escape)
550
- (globalThis as Record<string, unknown>)[`${this.handlerPrefix}_close`] =
551
- function (): void {
552
- self.close();
553
- };
554
-
555
- // Panel cursor movement handler
556
- (globalThis as Record<string, unknown>)[
557
- `${this.handlerPrefix}_cursor_moved`
558
- ] = function (data: {
559
- buffer_id: number;
560
- cursor_id: number;
561
- old_position: number;
562
- new_position: number;
563
- line: number;
564
- }): void {
565
- if (!self.state.isOpen || self.state.bufferId === null) return;
566
- if (data.buffer_id !== self.state.bufferId) return;
567
-
568
- self.state.cursorLine = data.line;
569
- self.applyHighlighting();
570
-
571
- // Get the item at this line
572
- const itemIndex = self.state.lineToItemIndex.get(data.line);
573
- if (itemIndex !== undefined && itemIndex < self.state.items.length) {
574
- const item = self.state.items[itemIndex];
575
- self.editor.setStatus(`Item ${itemIndex + 1}/${self.state.items.length}`);
576
-
577
- if (self.options.onCursorMove) {
578
- self.options.onCursorMove(item, itemIndex);
579
- }
580
- }
581
- };
582
-
583
- // Register cursor movement handler
584
- this.editor.on("cursor_moved", `${this.handlerPrefix}_cursor_moved`);
585
- }
586
-
587
- /**
588
- * Enable bidirectional cursor sync with source files
589
- */
590
- private enableCursorSync(): void {
591
- const self = this;
592
- const handlerName = `${this.handlerPrefix}_source_cursor`;
593
-
594
- // Handler for cursor movement in SOURCE files
595
- (globalThis as Record<string, unknown>)[handlerName] = function (data: {
596
- buffer_id: number;
597
- cursor_id: number;
598
- old_position: number;
599
- new_position: number;
600
- line: number;
601
- }): void {
602
- if (!self.state.isOpen || self.state.bufferId === null) return;
603
-
604
- // Ignore cursor moves in the panel itself
605
- if (data.buffer_id === self.state.bufferId) return;
606
-
607
- // Get the file path for this buffer
608
- const filePath = self.editor.getBufferPath(data.buffer_id);
609
- if (!filePath) return;
610
-
611
- // Find an item that matches this file and line
612
- const matchingIndex = self.state.items.findIndex((item) => {
613
- if (!item.location) return false;
614
- return (
615
- item.location.file === filePath && item.location.line === data.line
616
- );
617
- });
618
-
619
- if (matchingIndex >= 0) {
620
- const item = self.state.items[matchingIndex];
621
- // Reveal this item in the panel (without stealing focus)
622
- self.reveal(item.id, { focus: false, select: true });
623
- }
624
- };
625
-
626
- // Register the handler
627
- this.editor.on("cursor_moved", handlerName);
628
-
629
- // Store disposable to unregister later
630
- this.cursorSyncDisposable = () => {
631
- // Note: Fresh doesn't have an "off" method, so we just make the handler a no-op
632
- (globalThis as Record<string, unknown>)[handlerName] = () => {};
633
- };
634
- }
635
-
636
- private buildEntries(): TextPropertyEntry[] {
637
- const entries: TextPropertyEntry[] = [];
638
- this.state.lineToItemIndex.clear();
639
-
640
- let currentLine = 1;
641
-
642
- // Title line
643
- entries.push({
644
- text: `${this.options.title}\n`,
645
- properties: { type: "title" },
646
- });
647
- currentLine++;
648
-
649
- if (this.state.items.length === 0) {
650
- entries.push({
651
- text: " No results\n",
652
- properties: { type: "empty" },
653
- });
654
- currentLine++;
655
- } else if (this.options.groupBy === "file") {
656
- // Group by file
657
- const byFile = new Map<string, Array<{ item: T; index: number }>>();
658
-
659
- for (let i = 0; i < this.state.items.length; i++) {
660
- const item = this.state.items[i];
661
- const file = item.location?.file ?? "(no file)";
662
- if (!byFile.has(file)) {
663
- byFile.set(file, []);
664
- }
665
- byFile.get(file)!.push({ item, index: i });
666
- }
667
-
668
- for (const [file, itemsInFile] of byFile) {
669
- // File header
670
- const fileName = file.split("/").pop() ?? file;
671
- entries.push({
672
- text: `\n${fileName}:\n`,
673
- properties: { type: "file-header", file },
674
- });
675
- currentLine += 2;
676
-
677
- // Items in this file
678
- for (const { item, index } of itemsInFile) {
679
- entries.push(this.buildItemEntry(item, index));
680
- this.state.lineToItemIndex.set(currentLine, index);
681
- currentLine++;
682
- }
683
- }
684
- } else {
685
- // Flat list
686
- for (let i = 0; i < this.state.items.length; i++) {
687
- const item = this.state.items[i];
688
- entries.push(this.buildItemEntry(item, i));
689
- this.state.lineToItemIndex.set(currentLine, i);
690
- currentLine++;
691
- }
692
- }
693
-
694
- // Help footer
695
- entries.push({
696
- text: "\n",
697
- properties: { type: "blank" },
698
- });
699
- entries.push({
700
- text: "Enter:select | Esc:close\n",
701
- properties: { type: "help" },
702
- });
703
-
704
- return entries;
705
- }
706
-
707
- private buildItemEntry(item: T, _index: number): TextPropertyEntry {
708
- const severityIcon =
709
- item.severity === "error"
710
- ? "[E]"
711
- : item.severity === "warning"
712
- ? "[W]"
713
- : item.severity === "info"
714
- ? "[I]"
715
- : item.severity === "hint"
716
- ? "[H]"
717
- : "";
718
-
719
- const prefix = severityIcon ? `${severityIcon} ` : " ";
720
- const desc = item.description ? ` ${item.description}` : "";
721
-
722
- let line = `${prefix}${item.label}${desc}`;
723
- const maxLen = 100;
724
- if (line.length > maxLen) {
725
- line = line.slice(0, maxLen - 3) + "...";
726
- }
727
-
728
- return {
729
- text: `${line}\n`,
730
- properties: {
731
- type: "item",
732
- id: item.id,
733
- location: item.location,
734
- severity: item.severity,
735
- metadata: item.metadata,
736
- },
737
- };
738
- }
739
-
740
- private findFirstItemLine(): number {
741
- // Find the first line that has an item
742
- for (const [line] of this.state.lineToItemIndex) {
743
- return line;
744
- }
745
- return 2; // Default to line after title
746
- }
747
-
748
- private lineToByteOffset(lineNumber: number): number {
749
- const lines = this.state.cachedContent.split("\n");
750
- let offset = 0;
751
- for (let i = 0; i < lineNumber - 1 && i < lines.length; i++) {
752
- offset += lines[i].length + 1;
753
- }
754
- return offset;
755
- }
756
-
757
- private applyHighlighting(): void {
758
- if (this.state.bufferId === null) return;
759
-
760
- const bufferId = this.state.bufferId;
761
- this.editor.clearNamespace(bufferId, this.namespace);
762
-
763
- if (!this.state.cachedContent) return;
764
-
765
- const lines = this.state.cachedContent.split("\n");
766
- let byteOffset = 0;
767
-
768
- for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
769
- const line = lines[lineIdx];
770
- const lineStart = byteOffset;
771
- const lineEnd = byteOffset + line.length;
772
- const lineNumber = lineIdx + 1;
773
- const isCurrentLine = lineNumber === this.state.cursorLine;
774
- const isItemLine = this.state.lineToItemIndex.has(lineNumber);
775
-
776
- // Highlight current line if it's an item line
777
- if (isCurrentLine && isItemLine && line.trim() !== "") {
778
- this.editor.addOverlay(
779
- bufferId,
780
- this.namespace,
781
- lineStart,
782
- lineEnd,
783
- colors.selected[0],
784
- colors.selected[1],
785
- colors.selected[2],
786
- true,
787
- true,
788
- false
789
- );
790
- }
791
-
792
- // Title line
793
- if (lineNumber === 1) {
794
- this.editor.addOverlay(
795
- bufferId,
796
- this.namespace,
797
- lineStart,
798
- lineEnd,
799
- colors.title[0],
800
- colors.title[1],
801
- colors.title[2],
802
- true,
803
- true,
804
- false
805
- );
806
- }
807
-
808
- // File header (ends with : but isn't title)
809
- if (line.endsWith(":") && lineNumber > 1 && !line.startsWith(" ")) {
810
- this.editor.addOverlay(
811
- bufferId,
812
- this.namespace,
813
- lineStart,
814
- lineEnd,
815
- colors.fileHeader[0],
816
- colors.fileHeader[1],
817
- colors.fileHeader[2],
818
- false,
819
- true,
820
- false
821
- );
822
- }
823
-
824
- // Severity icon highlighting
825
- const iconMatch = line.match(/^\[([EWIH])\]/);
826
- if (iconMatch) {
827
- const iconEnd = lineStart + 3;
828
- let color: RGB;
829
- switch (iconMatch[1]) {
830
- case "E":
831
- color = colors.error;
832
- break;
833
- case "W":
834
- color = colors.warning;
835
- break;
836
- case "I":
837
- color = colors.info;
838
- break;
839
- case "H":
840
- color = colors.hint;
841
- break;
842
- default:
843
- color = colors.hint;
844
- }
845
-
846
- this.editor.addOverlay(
847
- bufferId,
848
- this.namespace,
849
- lineStart,
850
- iconEnd,
851
- color[0],
852
- color[1],
853
- color[2],
854
- false,
855
- true,
856
- false
857
- );
858
- }
859
-
860
- // Help line (dimmed)
861
- if (line.startsWith("Enter:") || line.includes("|")) {
862
- this.editor.addOverlay(
863
- bufferId,
864
- this.namespace,
865
- lineStart,
866
- lineEnd,
867
- colors.help[0],
868
- colors.help[1],
869
- colors.help[2],
870
- false,
871
- true,
872
- false
873
- );
874
- }
875
-
876
- byteOffset += line.length + 1;
877
- }
878
- }
879
- }
880
-
881
- // ============================================================================
882
- // Utility Functions
883
- // ============================================================================
884
-
885
- /**
886
- * Get the relative path for display
887
- */
888
- export function getRelativePath(editor: EditorAPI, filePath: string): string {
889
- const cwd = editor.getCwd();
890
- if (filePath.startsWith(cwd)) {
891
- return filePath.slice(cwd.length + 1);
892
- }
893
- return filePath;
894
- }
895
-
896
- /**
897
- * Create a simple static provider from an array of items.
898
- * Useful for one-shot results like "Find References".
899
- */
900
- export function createStaticProvider<T extends ResultItem>(
901
- initialItems: T[] = []
902
- ): ResultsProvider<T> & { updateItems: (items: T[]) => void } {
903
- let items = initialItems;
904
- const emitter = new EventEmitter<void>();
905
-
906
- return {
907
- provideResults: () => items,
908
- onDidChangeResults: emitter.event,
909
- updateItems: (newItems: T[]) => {
910
- items = newItems;
911
- emitter.fireVoid();
912
- },
913
- };
914
- }