@vemjs/renderer-vecto 0.1.0 → 0.1.2

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.
@@ -12,6 +12,8 @@ export class VemEditorEntity extends UIComponent {
12
12
  private charWidth = 8.4;
13
13
  private lineHeight = 20;
14
14
  private scrollY = 0; // scroll offset in lines
15
+ private autocompleteItems: { label: string; detail?: string }[] = [];
16
+ private selectedAutocompleteIndex = 0;
15
17
 
16
18
  constructor(editorState: VemEditorState) {
17
19
  super();
@@ -55,28 +57,46 @@ export class VemEditorEntity extends UIComponent {
55
57
  const buffer = this.editorState.getBuffer();
56
58
  const cursor = this.editorState.getCursor();
57
59
  const lineCount = buffer.getLineCount();
60
+ const theme = this.editorState.theme;
61
+ const layout = this.editorState.layoutConfig;
58
62
 
59
63
  // 1. Calculate gutter width dynamically
60
64
  const maxLineDigits = Math.max(2, lineCount.toString().length);
61
65
  const gutterWidth = maxLineDigits * this.charWidth + 15;
62
66
 
63
- // 2. Set line numbers text
67
+ // 2. Sync theme colors
68
+ this.gutterText.color = theme.gutterFg;
69
+ this.bodyText.color = theme.fg;
70
+
71
+ // 3. Set line numbers text
64
72
  const lineNums: string[] = [];
65
73
  for (let i = 1; i <= lineCount; i++) {
66
74
  lineNums.push(i.toString().padStart(maxLineDigits, ' '));
67
75
  }
68
76
  this.gutterText.setText(lineNums.join('\n'));
69
- this.gutterText.setPosition(5, 5);
70
77
 
71
- // 3. Set editor body text
72
- const spans = buffer.getLines().map((line, idx) => {
78
+ // 4. Set editor body text
79
+ const spans: any[] = [];
80
+ const lines = buffer.getLines();
81
+ lines.forEach((line, idx) => {
73
82
  const suffix = idx === lineCount - 1 ? '' : '\n';
74
- return { text: line + suffix };
83
+ const highlight = (this.editorState as any).highlightLine;
84
+ if (highlight) {
85
+ const lineSpans = highlight(line, idx);
86
+ if (lineSpans.length > 0) {
87
+ const lastSpan = { ...lineSpans[lineSpans.length - 1] };
88
+ lastSpan.text += suffix;
89
+ spans.push(...lineSpans.slice(0, -1), lastSpan);
90
+ } else {
91
+ spans.push({ text: suffix });
92
+ }
93
+ } else {
94
+ spans.push({ text: line + suffix });
95
+ }
75
96
  });
76
97
  this.bodyText.setSpans(spans);
77
- this.bodyText.setPosition(gutterWidth + 5, 5);
78
98
 
79
- // 4. Handle viewport scrolling to keep cursor visible
99
+ // 5. Handle viewport scrolling to keep cursor visible
80
100
  const visibleLines = Math.floor((this.height - 35) / this.lineHeight); // reserve 35px for status bar
81
101
  if (cursor.line >= this.scrollY + visibleLines) {
82
102
  this.scrollY = cursor.line - visibleLines + 1;
@@ -84,13 +104,9 @@ export class VemEditorEntity extends UIComponent {
84
104
  this.scrollY = cursor.line;
85
105
  }
86
106
 
87
- // Update children scroll positions
88
- const scrollOffsetY = -this.scrollY * this.lineHeight;
89
- this.gutterText.setPosition(5, 5 + scrollOffsetY);
90
- this.bodyText.setPosition(gutterWidth + 5, 5 + scrollOffsetY);
91
-
92
- // 5. Handle CommandBar visibility
93
- if (this.editorState.getMode() === 'COMMAND') {
107
+ // 6. Position and layout based on statusBarPosition
108
+ const hasCommandBar = this.editorState.getMode() === 'COMMAND';
109
+ if (hasCommandBar) {
94
110
  if (!this.children.includes(this.commandBar)) {
95
111
  this.add(this.commandBar);
96
112
  }
@@ -100,9 +116,58 @@ export class VemEditorEntity extends UIComponent {
100
116
  this.remove(this.commandBar);
101
117
  }
102
118
  }
119
+
120
+ const scrollOffsetY = -this.scrollY * this.lineHeight;
121
+ if (layout.statusBarPosition === 'top') {
122
+ this.commandBar.setPosition(0, 0);
123
+ this.gutterText.setPosition(5, 5 + scrollOffsetY + 30);
124
+ this.bodyText.setPosition(gutterWidth + 5, 5 + scrollOffsetY + 30);
125
+ } else {
126
+ this.commandBar.setPosition(0, this.height - 30);
127
+ this.gutterText.setPosition(5, 5 + scrollOffsetY);
128
+ this.bodyText.setPosition(gutterWidth + 5, 5 + scrollOffsetY);
129
+ }
130
+ }
131
+
132
+ public setAutocompleteItems(items: { label: string; detail?: string }[]): void {
133
+ this.autocompleteItems = items;
134
+ this.selectedAutocompleteIndex = 0;
135
+ }
136
+
137
+ public getAutocompleteItems(): { label: string; detail?: string }[] {
138
+ return this.autocompleteItems;
139
+ }
140
+
141
+ public selectNextAutocomplete(): void {
142
+ if (this.autocompleteItems.length > 0) {
143
+ this.selectedAutocompleteIndex =
144
+ (this.selectedAutocompleteIndex + 1) % this.autocompleteItems.length;
145
+ }
146
+ }
147
+
148
+ public selectPrevAutocomplete(): void {
149
+ if (this.autocompleteItems.length > 0) {
150
+ this.selectedAutocompleteIndex =
151
+ (this.selectedAutocompleteIndex - 1 + this.autocompleteItems.length) %
152
+ this.autocompleteItems.length;
153
+ }
154
+ }
155
+
156
+ public getSelectedAutocomplete(): { label: string; detail?: string } | null {
157
+ if (this.autocompleteItems.length > 0) {
158
+ return this.autocompleteItems[this.selectedAutocompleteIndex];
159
+ }
160
+ return null;
161
+ }
162
+
163
+ public clearAutocomplete(): void {
164
+ this.autocompleteItems = [];
103
165
  }
104
166
 
105
167
  public render(r: IRenderer): void {
168
+ const theme = this.editorState.theme;
169
+ const layout = this.editorState.layoutConfig;
170
+
106
171
  // 1. Draw editor background
107
172
  r.beginPath();
108
173
  r.moveTo(0, 0);
@@ -110,7 +175,7 @@ export class VemEditorEntity extends UIComponent {
110
175
  r.lineTo(this.width, this.height);
111
176
  r.lineTo(0, this.height);
112
177
  r.closePath();
113
- r.fill('#0f172a'); // slate-900
178
+ r.fill(theme.bg);
114
179
 
115
180
  const lineCount = this.editorState.getBuffer().getLineCount();
116
181
  const maxLineDigits = Math.max(2, lineCount.toString().length);
@@ -123,11 +188,28 @@ export class VemEditorEntity extends UIComponent {
123
188
  r.lineTo(gutterWidth, this.height);
124
189
  r.lineTo(0, this.height);
125
190
  r.closePath();
126
- r.fill('#1e293b'); // slate-800
191
+ r.fill(theme.gutterBg);
192
+
193
+ // Apply scrolling transformation (with offset if statusBar is top)
194
+ const contentOffsetY = layout.statusBarPosition === 'top' ? 30 : 0;
127
195
 
128
- // Apply scrolling transformation for cursor and selections
129
196
  r.save();
130
- r.translate(0, -this.scrollY * this.lineHeight);
197
+ r.translate(0, -this.scrollY * this.lineHeight + contentOffsetY);
198
+
199
+ // 2.5. Draw Gutter Decorations (Git diff signs)
200
+ const decs = (this.editorState as any).gutterDecorations;
201
+ if (decs && decs.size > 0) {
202
+ for (let l = 0; l < lineCount; l++) {
203
+ const dec = decs.get(l);
204
+ if (dec) {
205
+ const decY = 5 + l * this.lineHeight;
206
+ r.beginPath();
207
+ r.roundRect(1, decY + 2, 3, this.lineHeight - 4, 1.5);
208
+ r.closePath();
209
+ r.fill(dec.color);
210
+ }
211
+ }
212
+ }
131
213
 
132
214
  // 3. Draw Visual Mode selections
133
215
  const selection = this.editorState.getVisualSelection();
@@ -154,7 +236,7 @@ export class VemEditorEntity extends UIComponent {
154
236
  r.lineTo(x + w, y + h);
155
237
  r.lineTo(x, y + h);
156
238
  r.closePath();
157
- r.fill('rgba(56, 189, 248, 0.3)'); // sky-400 opacity
239
+ r.fill(theme.accent + '44'); // Theme accent with 25% opacity
158
240
  };
159
241
 
160
242
  if (type === 'line') {
@@ -183,6 +265,32 @@ export class VemEditorEntity extends UIComponent {
183
265
  }
184
266
  }
185
267
 
268
+ // 3.5. Draw LSP Diagnostics (wavy underline)
269
+ const diagnostics = this.editorState.getDiagnostics();
270
+ for (const diag of diagnostics) {
271
+ const lineText = this.editorState.getBuffer().getLine(diag.line);
272
+ const startChar = diag.startCharacter;
273
+ // If endCharacter is same or not specified, underline at least 1 character width
274
+ const endChar = Math.max(startChar + 1, Math.min(diag.endCharacter, lineText.length));
275
+
276
+ const startX = gutterWidth + 5 + startChar * this.charWidth;
277
+ const y = 5 + diag.line * this.lineHeight + this.lineHeight - 2;
278
+ const length = (endChar - startChar) * this.charWidth;
279
+
280
+ let color = '#ef4444'; // default error red
281
+ if (diag.severity === 'warning') color = '#f97316'; // orange
282
+ else if (diag.severity === 'info') color = '#3b82f6'; // blue
283
+ else if (diag.severity === 'hint') color = '#10b981'; // emerald green
284
+
285
+ r.beginPath();
286
+ r.moveTo(startX, y);
287
+ for (let offset = 0; offset <= length; offset += 2) {
288
+ const waveY = y + (offset % 4 === 0 ? 1 : -1);
289
+ r.lineTo(startX + offset, waveY);
290
+ }
291
+ r.stroke(color, 1.2);
292
+ }
293
+
186
294
  // 4. Draw Vim cursor
187
295
  const cursor = this.editorState.getCursor();
188
296
  const cursorX = gutterWidth + 5 + cursor.character * this.charWidth;
@@ -196,40 +304,288 @@ export class VemEditorEntity extends UIComponent {
196
304
  r.lineTo(cursorX + 2, cursorY + this.lineHeight);
197
305
  r.lineTo(cursorX, cursorY + this.lineHeight);
198
306
  r.closePath();
199
- r.fill('#f43f5e'); // rose-500
307
+ r.fill(theme.accent);
200
308
  } else {
201
309
  r.moveTo(cursorX, cursorY);
202
310
  r.lineTo(cursorX + this.charWidth, cursorY);
203
311
  r.lineTo(cursorX + this.charWidth, cursorY + this.lineHeight);
204
312
  r.lineTo(cursorX, cursorY + this.lineHeight);
205
313
  r.closePath();
206
- r.fill('rgba(56, 189, 248, 0.7)'); // sky-400 opacity
314
+ r.fill(theme.accent + '88'); // 50% opacity accent
207
315
  }
208
316
 
209
317
  r.restore(); // Restore scroll transform
210
318
 
211
- // 5. Draw status bar at the bottom
319
+ // 5. Draw status bar
212
320
  const statusBarHeight = 30;
213
- const statusY = this.height - statusBarHeight;
321
+ const statusY = layout.statusBarPosition === 'top' ? 0 : this.height - statusBarHeight;
214
322
  r.beginPath();
215
323
  r.moveTo(0, statusY);
216
324
  r.lineTo(this.width, statusY);
217
- r.lineTo(this.width, this.height);
218
- r.lineTo(0, this.height);
325
+ r.lineTo(this.width, statusY + statusBarHeight);
326
+ r.lineTo(0, statusY + statusBarHeight);
219
327
  r.closePath();
220
- r.fill('#1e293b'); // slate-800
328
+ r.fill(theme.statusBarBg);
329
+
330
+ const sl = this.editorState.statuslineLayout;
331
+ if (
332
+ mode !== 'COMMAND' &&
333
+ ((sl.left && sl.left.length > 0) || (sl.right && sl.right.length > 0))
334
+ ) {
335
+ // Custom statusline layout (lualine-like)
336
+ let startX = 0;
337
+ if (sl.left) {
338
+ for (const segment of sl.left) {
339
+ const textWidth = (r as any).measureText
340
+ ? (r as any).measureText(segment.text).width
341
+ : segment.text.length * 8;
342
+ const font = segment.bold ? 'bold 12px monospace' : '12px monospace';
343
+ const color = segment.color || theme.statusBarFg;
344
+
345
+ if (segment.bg) {
346
+ const blockWidth = textWidth + 20;
347
+ r.beginPath();
348
+ r.moveTo(startX, statusY);
349
+ r.lineTo(startX + blockWidth, statusY);
350
+ r.lineTo(startX + blockWidth, statusY + statusBarHeight);
351
+ r.lineTo(startX, statusY + statusBarHeight);
352
+ r.closePath();
353
+ r.fill(segment.bg);
354
+
355
+ r.fillText(segment.text, startX + 10, statusY + 18, font, color);
356
+ startX += blockWidth;
357
+ } else {
358
+ r.fillText(segment.text, startX + 10, statusY + 18, font, color);
359
+ startX += textWidth + 15;
360
+ }
361
+ }
362
+ }
221
363
 
222
- if (mode !== 'COMMAND') {
364
+ let endX = this.width;
365
+ if (sl.right) {
366
+ for (const segment of sl.right) {
367
+ const textWidth = (r as any).measureText
368
+ ? (r as any).measureText(segment.text).width
369
+ : segment.text.length * 8;
370
+ const font = segment.bold ? 'bold 12px monospace' : '12px monospace';
371
+ const color = segment.color || theme.statusBarFg;
372
+
373
+ if (segment.bg) {
374
+ const blockWidth = textWidth + 20;
375
+ endX -= blockWidth;
376
+ r.beginPath();
377
+ r.moveTo(endX, statusY);
378
+ r.lineTo(endX + blockWidth, statusY);
379
+ r.lineTo(endX + blockWidth, statusY + statusBarHeight);
380
+ r.lineTo(endX, statusY + statusBarHeight);
381
+ r.closePath();
382
+ r.fill(segment.bg);
383
+
384
+ r.fillText(segment.text, endX + 10, statusY + 18, font, color);
385
+ } else {
386
+ endX -= textWidth + 15;
387
+ r.fillText(segment.text, endX + 10, statusY + 18, font, color);
388
+ }
389
+ }
390
+ }
391
+ } else if (mode !== 'COMMAND') {
392
+ // Default fallback status bar
223
393
  const modeText = `-- ${mode} --`;
224
394
  const posText = `${cursor.line + 1}:${cursor.character + 1}`;
225
395
  const pendingKeys = this.editorState.getPendingKeys();
226
396
  const pendingText = pendingKeys.length > 0 ? pendingKeys.join('') : '';
227
397
 
228
- r.fillText(modeText, 10, statusY + 18, 'bold 12px monospace', '#38bdf8');
398
+ r.fillText(modeText, 10, statusY + 18, 'bold 12px monospace', theme.accent);
229
399
  if (pendingText) {
230
- r.fillText(pendingText, 120, statusY + 18, '12px monospace', '#e2e8f0');
400
+ r.fillText(pendingText, 120, statusY + 18, '12px monospace', theme.statusBarFg);
401
+ }
402
+ r.fillText(posText, this.width - 60, statusY + 18, '12px monospace', theme.statusBarFg);
403
+ }
404
+
405
+ // 6. Draw autocomplete popup menu
406
+ if (this.autocompleteItems.length > 0) {
407
+ // Calculate popup dimensions
408
+ const maxLabelLen = Math.max(
409
+ ...this.autocompleteItems.map(
410
+ (item) => item.label.length + (item.detail ? item.detail.length + 3 : 0),
411
+ ),
412
+ );
413
+ const popupWidth = Math.max(160, maxLabelLen * 7.5 + 20);
414
+ const popupHeight = Math.min(200, this.autocompleteItems.length * 18 + 6);
415
+
416
+ // Translate coordinates from buffer space to screen space
417
+ let popupX = cursorX;
418
+ let popupY = cursorY + this.lineHeight - this.scrollY * this.lineHeight + contentOffsetY;
419
+
420
+ // Adjust positioning if it overflows screen boundaries
421
+ if (popupX + popupWidth > this.width) {
422
+ popupX = Math.max(gutterWidth + 5, this.width - popupWidth - 5);
423
+ }
424
+ if (layout.statusBarPosition === 'bottom' && popupY + popupHeight > statusY) {
425
+ popupY = cursorY - this.scrollY * this.lineHeight - popupHeight;
426
+ } else if (layout.statusBarPosition === 'top' && popupY < 30) {
427
+ popupY = cursorY + this.lineHeight - this.scrollY * this.lineHeight + 30;
428
+ }
429
+
430
+ // Draw background panel
431
+ r.beginPath();
432
+ r.roundRect(popupX, popupY, popupWidth, popupHeight, 4);
433
+ r.closePath();
434
+ r.fill(theme.statusBarBg);
435
+ r.stroke(theme.accent + '88', 1.2);
436
+
437
+ // Draw menu items
438
+ r.save();
439
+ r.clip(popupX, popupY, popupWidth, popupHeight);
440
+ for (let i = 0; i < this.autocompleteItems.length; i++) {
441
+ const item = this.autocompleteItems[i];
442
+ const itemY = popupY + 3 + i * 18;
443
+ if (itemY + 18 < popupY || itemY > popupY + popupHeight) continue;
444
+
445
+ // Draw active row selection background
446
+ if (i === this.selectedAutocompleteIndex) {
447
+ r.beginPath();
448
+ r.moveTo(popupX + 2, itemY);
449
+ r.lineTo(popupX + popupWidth - 2, itemY);
450
+ r.lineTo(popupX + popupWidth - 2, itemY + 18);
451
+ r.lineTo(popupX + 2, itemY + 18);
452
+ r.closePath();
453
+ r.fill(theme.accent + '44');
454
+ }
455
+
456
+ const labelColor = i === this.selectedAutocompleteIndex ? theme.accent : theme.fg;
457
+ r.fillText(item.label, popupX + 8, itemY + 13, '12px monospace', labelColor);
458
+ if (item.detail) {
459
+ r.fillText(
460
+ ` ${item.detail}`,
461
+ popupX + 8 + item.label.length * 7.5,
462
+ itemY + 13,
463
+ '10px monospace',
464
+ theme.gutterFg,
465
+ );
466
+ }
467
+ }
468
+ r.restore();
469
+ }
470
+
471
+ // 7. Draw centered Floating Popup Picker Modal (Telescope-like)
472
+ const popup = this.editorState.activePopup;
473
+ if (popup) {
474
+ const modalW = Math.min(550, this.width - 40);
475
+ const modalH = Math.min(380, this.height - 80);
476
+ const modalX = (this.width - modalW) / 2;
477
+ const modalY = (this.height - modalH) / 2;
478
+
479
+ // Draw modal backdrop shade
480
+ r.beginPath();
481
+ r.roundRect(0, 0, this.width, this.height, 0);
482
+ r.closePath();
483
+ r.fill('rgba(15, 23, 42, 0.6)'); // Translucent dark overlay
484
+
485
+ // Draw main panel
486
+ r.beginPath();
487
+ r.roundRect(modalX, modalY, modalW, modalH, 6);
488
+ r.closePath();
489
+ r.fill(theme.bg);
490
+ r.stroke(theme.accent, 1.5);
491
+
492
+ // Title
493
+ r.fillText(
494
+ popup.title.toUpperCase(),
495
+ modalX + 15,
496
+ modalY + 28,
497
+ 'bold 13px Outfit, monospace',
498
+ theme.accent,
499
+ );
500
+
501
+ // Input Search Bar
502
+ r.beginPath();
503
+ r.roundRect(modalX + 15, modalY + 42, modalW - 30, 28, 4);
504
+ r.closePath();
505
+ r.fill(theme.statusBarBg);
506
+ r.stroke(theme.accent + '44', 1);
507
+
508
+ const queryText = `> ${this.editorState.popupFilterText}`;
509
+ r.fillText(queryText, modalX + 25, modalY + 60, '13px monospace', theme.fg);
510
+
511
+ // Draw flashing block cursor in search bar
512
+ const cursorOffset = (r as any).measureText
513
+ ? (r as any).measureText(queryText).width
514
+ : queryText.length * 8;
515
+ r.beginPath();
516
+ r.roundRect(modalX + 25 + cursorOffset + 2, modalY + 48, 8, 15, 0);
517
+ r.closePath();
518
+ r.fill(theme.accent);
519
+
520
+ // Divider line
521
+ r.beginPath();
522
+ r.moveTo(modalX + 15, modalY + 82);
523
+ r.lineTo(modalX + modalW - 15, modalY + 82);
524
+ r.closePath();
525
+ r.stroke(theme.gutterBg, 1.2);
526
+
527
+ // Render filtered list items
528
+ const items = this.editorState.getFilteredPopupItems();
529
+ const listStartY = modalY + 95;
530
+ const itemH = 22;
531
+ const maxVisibleItems = 11;
532
+ const startIndex = Math.max(
533
+ 0,
534
+ Math.min(
535
+ this.editorState.activePopupIndex - Math.floor(maxVisibleItems / 2),
536
+ items.length - maxVisibleItems,
537
+ ),
538
+ );
539
+
540
+ r.save();
541
+ r.clip(modalX + 15, modalY + 85, modalW - 30, modalH - 100);
542
+
543
+ for (let i = 0; i < Math.min(maxVisibleItems, items.length); i++) {
544
+ const itemIdx = startIndex + i;
545
+ const item = items[itemIdx];
546
+ if (!item) break;
547
+
548
+ const rowY = listStartY + i * itemH;
549
+
550
+ if (itemIdx === this.editorState.activePopupIndex) {
551
+ // Draw active row highlight background
552
+ r.beginPath();
553
+ r.roundRect(modalX + 15, rowY, modalW - 30, itemH, 4);
554
+ r.closePath();
555
+ r.fill(theme.accent + '33');
556
+ }
557
+
558
+ const labelColor = itemIdx === this.editorState.activePopupIndex ? theme.accent : theme.fg;
559
+ r.fillText(
560
+ item.label,
561
+ modalX + 25,
562
+ rowY + 15,
563
+ itemIdx === this.editorState.activePopupIndex ? 'bold 12px monospace' : '12px monospace',
564
+ labelColor,
565
+ );
566
+
567
+ if (item.detail) {
568
+ const detailX = modalX + modalW - 35 - item.detail.length * 7.5;
569
+ r.fillText(
570
+ item.detail,
571
+ Math.max(modalX + 250, detailX),
572
+ rowY + 15,
573
+ '10px monospace',
574
+ theme.gutterFg,
575
+ );
576
+ }
231
577
  }
232
- r.fillText(posText, this.width - 60, statusY + 18, '12px monospace', '#94a3b8');
578
+ r.restore();
579
+
580
+ // Draw item counts indicator
581
+ const countText = `${this.editorState.activePopupIndex + 1}/${items.length}`;
582
+ r.fillText(
583
+ countText,
584
+ modalX + modalW - 20 - countText.length * 7.5,
585
+ modalY + 28,
586
+ '11px monospace',
587
+ theme.gutterFg,
588
+ );
233
589
  }
234
590
  }
235
591
  }
@@ -11,6 +11,10 @@ export class WorkspaceExplorer extends UIComponent {
11
11
  private treeView: TreeView | null = null;
12
12
  private openBtn: Button;
13
13
  private fsHandler: FileSystemHandler;
14
+ private openDirectoryCallbacks: ((
15
+ files: any[],
16
+ fsHandler: FileSystemHandler,
17
+ ) => void | Promise<void>)[] = [];
14
18
 
15
19
  constructor(width: number, height: number, initialText?: string) {
16
20
  super();
@@ -53,6 +57,28 @@ export class WorkspaceExplorer extends UIComponent {
53
57
  return this.workspace;
54
58
  }
55
59
 
60
+ public onDidOpenDirectory(
61
+ cb: (files: any[], fsHandler: FileSystemHandler) => void | Promise<void>,
62
+ ): void {
63
+ this.openDirectoryCallbacks.push(cb);
64
+ }
65
+
66
+ private flattenFiles(nodes: any[]): string[] {
67
+ const list: string[] = [];
68
+ const recurse = (nodeList: any[], prefix: string) => {
69
+ for (const node of nodeList) {
70
+ const path = prefix ? `${prefix}/${node.label}` : node.label;
71
+ if (node.children && node.children.length > 0) {
72
+ recurse(node.children, path);
73
+ } else if (!node.children || node.children.length === 0) {
74
+ list.push(path);
75
+ }
76
+ }
77
+ };
78
+ recurse(nodes, '');
79
+ return list;
80
+ }
81
+
56
82
  private async handleOpenFolder(): Promise<void> {
57
83
  if (typeof window === 'undefined' || !(window as any).showDirectoryPicker) {
58
84
  console.warn('File System Access API is not supported in this environment.');
@@ -63,6 +89,13 @@ export class WorkspaceExplorer extends UIComponent {
63
89
  const rootHandle = await (window as any).showDirectoryPicker();
64
90
  const nodes = await this.fsHandler.readDirectory(rootHandle);
65
91
 
92
+ // Cache all file paths for search plugins (like Telescope)
93
+ const fileList = this.flattenFiles(nodes);
94
+ const activeState = this.getActiveEditorState();
95
+ if (activeState) {
96
+ activeState.projectFiles = fileList;
97
+ }
98
+
66
99
  this.treeView = new TreeView({
67
100
  nodes,
68
101
  width: this.leftPanel.width,
@@ -82,14 +115,82 @@ export class WorkspaceExplorer extends UIComponent {
82
115
 
83
116
  this.leftPanel.remove(this.openBtn);
84
117
  this.leftPanel.add(this.treeView);
118
+
119
+ // Trigger directory opened callbacks
120
+ for (const cb of this.openDirectoryCallbacks) {
121
+ try {
122
+ cb(nodes, this.fsHandler);
123
+ } catch (e) {
124
+ console.error('Error executing openDirectory callback:', e);
125
+ }
126
+ }
85
127
  } catch (err) {
86
128
  console.error('Error selecting directory:', err);
87
129
  }
88
130
  }
89
131
 
132
+ public getActiveEditorState(): any | null {
133
+ return this.workspace.getActiveLayout()?.getActiveState() || null;
134
+ }
135
+
136
+ private lastSidebarPosition: 'left' | 'right' | 'hidden' = 'left';
137
+ private lastSidebarWidth = 240;
138
+
139
+ public syncLayout(activeState: any): void {
140
+ const layout = activeState.layoutConfig;
141
+
142
+ this.remove(this.panelGroup);
143
+
144
+ this.panelGroup = new PanelGroup({
145
+ direction: 'horizontal',
146
+ width: this.width,
147
+ height: this.height,
148
+ });
149
+
150
+ this.leftPanel = new Panel({
151
+ minSize: 150,
152
+ defaultSize: layout.sidebarWidth / Math.max(1, this.width),
153
+ });
154
+ this.rightPanel = new Panel({ minSize: 300 });
155
+
156
+ if (layout.sidebarPosition === 'left') {
157
+ this.leftPanel.add(this.openBtn);
158
+ if (this.treeView) this.leftPanel.add(this.treeView);
159
+ this.rightPanel.add(this.workspace);
160
+
161
+ this.panelGroup.addPanel(this.leftPanel);
162
+ this.panelGroup.addPanel(this.rightPanel);
163
+ } else if (layout.sidebarPosition === 'right') {
164
+ this.leftPanel.add(this.openBtn);
165
+ if (this.treeView) this.leftPanel.add(this.treeView);
166
+ this.rightPanel.add(this.workspace);
167
+
168
+ this.panelGroup.addPanel(this.rightPanel);
169
+ this.panelGroup.addPanel(this.leftPanel);
170
+ } else {
171
+ this.rightPanel.add(this.workspace);
172
+ this.panelGroup.addPanel(this.rightPanel);
173
+ }
174
+
175
+ this.add(this.panelGroup);
176
+ }
177
+
90
178
  public update(dt: number, time: number): void {
91
179
  super.update(dt, time);
92
180
 
181
+ const activeState = this.getActiveEditorState();
182
+ if (activeState) {
183
+ const layout = activeState.layoutConfig;
184
+ if (
185
+ layout.sidebarPosition !== this.lastSidebarPosition ||
186
+ layout.sidebarWidth !== this.lastSidebarWidth
187
+ ) {
188
+ this.lastSidebarPosition = layout.sidebarPosition;
189
+ this.lastSidebarWidth = layout.sidebarWidth;
190
+ this.syncLayout(activeState);
191
+ }
192
+ }
193
+
93
194
  if (this.panelGroup.width !== this.width || this.panelGroup.height !== this.height) {
94
195
  this.panelGroup.width = this.width;
95
196
  this.panelGroup.height = this.height;
@@ -112,13 +213,34 @@ export class WorkspaceExplorer extends UIComponent {
112
213
  }
113
214
  }
114
215
 
115
- public render(_r: IRenderer): void {
116
- _r.beginPath();
117
- _r.moveTo(0, 0);
118
- _r.lineTo(this.leftPanel.width, 0);
119
- _r.lineTo(this.leftPanel.width, this.height);
120
- _r.lineTo(0, this.height);
121
- _r.closePath();
122
- _r.fill('#090d16'); // deep slate sidebar background
216
+ public render(r: IRenderer): void {
217
+ const activeState = this.getActiveEditorState();
218
+ if (!activeState) return;
219
+
220
+ const theme = activeState.theme;
221
+ const layout = activeState.layoutConfig;
222
+
223
+ // Apply button styling
224
+ this.openBtn.bg = theme.statusBarBg;
225
+ this.openBtn.hoverBg = theme.statusBarBg;
226
+ this.openBtn.color = theme.fg;
227
+
228
+ if (this.treeView) {
229
+ /* eslint-disable-next-line no-underscore-dangle */
230
+ (this.treeView as any)._color = theme.fg;
231
+ /* eslint-disable-next-line no-underscore-dangle */
232
+ (this.treeView as any)._selColor = theme.accent + '33';
233
+ }
234
+
235
+ if (layout.sidebarPosition !== 'hidden') {
236
+ const startX = layout.sidebarPosition === 'left' ? 0 : this.width - this.leftPanel.width;
237
+ r.beginPath();
238
+ r.moveTo(startX, 0);
239
+ r.lineTo(startX + this.leftPanel.width, 0);
240
+ r.lineTo(startX + this.leftPanel.width, this.height);
241
+ r.lineTo(startX, this.height);
242
+ r.closePath();
243
+ r.fill(theme.sidebarBg);
244
+ }
123
245
  }
124
246
  }