@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.
- package/CHANGELOG.md +29 -2
- package/dist/CommandBar.d.ts.map +1 -1
- package/dist/CommandBar.js +6 -1
- package/dist/CommandBar.js.map +1 -1
- package/dist/VemEditorEntity.d.ts +17 -0
- package/dist/VemEditorEntity.d.ts.map +1 -1
- package/dist/VemEditorEntity.js +313 -29
- package/dist/VemEditorEntity.js.map +1 -1
- package/dist/WorkspaceExplorer.d.ts +9 -1
- package/dist/WorkspaceExplorer.d.ts.map +1 -1
- package/dist/WorkspaceExplorer.js +111 -8
- package/dist/WorkspaceExplorer.js.map +1 -1
- package/dist/WorkspaceLayout.d.ts.map +1 -1
- package/dist/WorkspaceLayout.js +7 -0
- package/dist/WorkspaceLayout.js.map +1 -1
- package/package.json +1 -1
- package/src/CommandBar.ts +8 -1
- package/src/VemEditorEntity.test.ts +55 -0
- package/src/VemEditorEntity.ts +386 -30
- package/src/WorkspaceExplorer.ts +130 -8
- package/src/WorkspaceLayout.ts +9 -0
package/src/VemEditorEntity.ts
CHANGED
|
@@ -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.
|
|
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
|
-
//
|
|
72
|
-
const spans =
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
88
|
-
const
|
|
89
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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,
|
|
218
|
-
r.lineTo(0,
|
|
325
|
+
r.lineTo(this.width, statusY + statusBarHeight);
|
|
326
|
+
r.lineTo(0, statusY + statusBarHeight);
|
|
219
327
|
r.closePath();
|
|
220
|
-
r.fill(
|
|
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
|
-
|
|
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',
|
|
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',
|
|
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.
|
|
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
|
}
|
package/src/WorkspaceExplorer.ts
CHANGED
|
@@ -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(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
}
|