@joinezco/codeblock 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/assets/fs.worker-DfanUHpQ.js +21 -0
  2. package/dist/assets/{index-MGle_v2x.js → index-BAnLzvMk.js} +1 -1
  3. package/dist/assets/{index-as7ELo0J.js → index-BBC9WDX6.js} +1 -1
  4. package/dist/assets/{index-Dx_VuNNd.js → index-BEXYxRro.js} +1 -1
  5. package/dist/assets/{index-pGm0qkrJ.js → index-BfYmUKH9.js} +1 -1
  6. package/dist/assets/{index-CXFONXS8.js → index-BhaTNAWE.js} +1 -1
  7. package/dist/assets/{index-D5Z27j1C.js → index-CCbYDSng.js} +1 -1
  8. package/dist/assets/{index-Dvu-FFzd.js → index-CIi8tLT6.js} +1 -1
  9. package/dist/assets/{index-C-QhPFHP.js → index-CaANcgI2.js} +1 -1
  10. package/dist/assets/index-CkWzFNzm.js +208 -0
  11. package/dist/assets/{index-N-GE7HTU.js → index-D_XGv9QZ.js} +1 -1
  12. package/dist/assets/{index-DWOBdRjn.js → index-DkmiPfkD.js} +1 -1
  13. package/dist/assets/{index-CGx5MZO7.js → index-DmNlLMQ4.js} +1 -1
  14. package/dist/assets/{index-I0dlv-r3.js → index-DmX_vI7D.js} +1 -1
  15. package/dist/assets/{index-9HdhmM_Y.js → index-DogEEevD.js} +1 -1
  16. package/dist/assets/{index-aEsF5o-7.js → index-DsDl5qZV.js} +1 -1
  17. package/dist/assets/{index-gUUzXNuP.js → index-gAy5mDg-.js} +1 -1
  18. package/dist/assets/{index-CIuq3uTk.js → index-i5qJLB2h.js} +1 -1
  19. package/dist/assets/javascript.worker-ClsyHOLi.js +552 -0
  20. package/dist/e2e/editor.spec.d.ts +1 -0
  21. package/dist/e2e/editor.spec.js +309 -0
  22. package/dist/editor.d.ts +9 -3
  23. package/dist/editor.js +83 -15
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.html +2 -3
  26. package/dist/index.js +3 -0
  27. package/dist/lsps/typescript.d.ts +3 -1
  28. package/dist/lsps/typescript.js +8 -17
  29. package/dist/panels/footer.d.ts +16 -0
  30. package/dist/panels/footer.js +258 -0
  31. package/dist/panels/toolbar.d.ts +15 -2
  32. package/dist/panels/toolbar.js +528 -115
  33. package/dist/panels/toolbar.test.js +20 -14
  34. package/dist/rpc/transport.d.ts +2 -11
  35. package/dist/rpc/transport.js +19 -35
  36. package/dist/themes/index.js +181 -14
  37. package/dist/themes/vscode.js +3 -2
  38. package/dist/utils/fs.d.ts +15 -3
  39. package/dist/utils/fs.js +85 -6
  40. package/dist/utils/lsp.d.ts +26 -15
  41. package/dist/utils/lsp.js +79 -44
  42. package/dist/utils/search.d.ts +2 -0
  43. package/dist/utils/search.js +13 -4
  44. package/dist/utils/typescript-defaults.d.ts +57 -0
  45. package/dist/utils/typescript-defaults.js +208 -0
  46. package/dist/utils/typescript-defaults.test.d.ts +1 -0
  47. package/dist/utils/typescript-defaults.test.js +197 -0
  48. package/dist/workers/fs.worker.js +14 -18
  49. package/dist/workers/javascript.worker.js +11 -9
  50. package/package.json +4 -3
  51. package/dist/assets/fs.worker-BwEqZcql.ts +0 -109
  52. package/dist/assets/index-C3BnE2cG.js +0 -222
  53. package/dist/assets/javascript.worker-C1zGArKk.js +0 -527
  54. package/dist/snapshot.bin +0 -0
@@ -1,12 +1,39 @@
1
1
  import { StateEffect, StateField } from "@codemirror/state";
2
- import { CodeblockFacet, openFileEffect, currentFileField } from "../editor";
2
+ import { CodeblockFacet, openFileEffect, currentFileField, setThemeEffect } from "../editor";
3
3
  import { extOrLanguageToLanguageId } from "../lsps";
4
+ import { LSP, LspLog, FileChangeType } from "../utils/lsp";
5
+ import { Seti } from "@m234/nerd-fonts/fs";
6
+ import { settingsField, resolveThemeDark, createSettingsOverlay } from "./footer";
7
+ // Browser-safe file icon lookup (avoids node:path.parse used by Seti.fromPath)
8
+ const FALLBACK_ICON = { value: '\ue64e', hexCode: 0xe64e }; // nf-seti-text
9
+ function setiIconForPath(filePath) {
10
+ const base = filePath.split('/').pop() || filePath;
11
+ // Check exact basename match first (e.g. Dockerfile, Makefile)
12
+ const byBase = Seti.byBaseSeti.get(base);
13
+ if (byBase)
14
+ return byBase;
15
+ // Walk extensions from longest to shortest (e.g. .spec.ts → .ts)
16
+ let dot = base.indexOf('.');
17
+ if (dot < 0)
18
+ return FALLBACK_ICON;
19
+ let ext = base.slice(dot);
20
+ for (;;) {
21
+ const byExt = Seti.byExtensionSeti.get(ext);
22
+ if (byExt)
23
+ return byExt;
24
+ dot = ext.indexOf('.', 1);
25
+ if (dot === -1)
26
+ break;
27
+ ext = ext.slice(dot);
28
+ }
29
+ return FALLBACK_ICON;
30
+ }
4
31
  // Type guards
5
32
  function isCommandResult(result) {
6
- return 'type' in result;
33
+ return 'type' in result && result.query !== undefined;
7
34
  }
8
- function isSearchResult(result) {
9
- return 'score' in result;
35
+ function isBrowseEntry(result) {
36
+ return 'type' in result && ('fullPath' in result);
10
37
  }
11
38
  // Search results state - now handles both commands and search results
12
39
  export const setSearchResults = StateEffect.define();
@@ -40,89 +67,21 @@ function isValidProgrammingLanguage(query) {
40
67
  return Object.keys(extOrLanguageToLanguageId).some(key => key.toLowerCase() === lowerQuery ||
41
68
  extOrLanguageToLanguageId[key].toLowerCase() === lowerQuery);
42
69
  }
43
- // Get appropriate icon for language/extension
70
+ // Icons
71
+ const SEARCH_ICON = '\uf002'; // nf-fa-search (magnifying glass)
72
+ const DEFAULT_FILE_ICON = '\ue64e'; // nf-seti-text
73
+ const COG_ICON = '\uf013'; // nf-fa-cog
74
+ const FOLDER_ICON = '\ue613'; // nf-seti-folder
75
+ const FOLDER_OPEN_ICON = '\ue614'; // nf-seti-folder (open variant)
76
+ const PARENT_DIR_ICON = '\uf112'; // nf-fa-reply (back/up arrow)
77
+ // Get nerd font icon for a file path
78
+ function getFileIcon(path) {
79
+ const result = setiIconForPath(path);
80
+ return { glyph: result.value, color: result.color || '' };
81
+ }
82
+ // Get icon for a language/extension query (used for create-file commands)
44
83
  function getLanguageIcon(query) {
45
- const lowerQuery = query.toLowerCase();
46
- // Language/extension icons matching extOrLanguageToLanguageId
47
- const iconMap = {
48
- // JavaScript/TypeScript family
49
- 'javascript': '🟨',
50
- 'js': '🟨',
51
- 'typescript': '🔷',
52
- 'ts': '🔷',
53
- 'jsx': '⚛️',
54
- 'tsx': '⚛️',
55
- // Python
56
- 'python': '🐍',
57
- 'py': '🐍',
58
- // Ruby
59
- 'ruby': '💎',
60
- 'rb': '💎',
61
- // PHP
62
- 'php': '🐘',
63
- // Java
64
- 'java': '☕',
65
- // C/C++
66
- 'cpp': '⚙️',
67
- 'c': '⚙️',
68
- // C#
69
- 'csharp': '🔷',
70
- 'cs': '🔷',
71
- // Go
72
- 'go': '🐹',
73
- // Swift
74
- 'swift': '🦉',
75
- // Kotlin
76
- 'kotlin': '🟣',
77
- 'kt': '🟣',
78
- // Rust
79
- 'rust': '🦀',
80
- 'rs': '🦀',
81
- // Scala
82
- 'scala': '🔴',
83
- // Visual Basic
84
- 'vb': '🔵',
85
- // Haskell
86
- 'haskell': '🎭',
87
- 'hs': '🎭',
88
- // Lua
89
- 'lua': '🌙',
90
- // Perl
91
- 'perl': '🐪',
92
- 'pl': '🐪',
93
- // Shell/Bash
94
- 'bash': '🐚',
95
- 'shell': '🐚',
96
- 'sh': '🐚',
97
- 'zsh': '🐚',
98
- // SQL
99
- 'mysql': '🗃️',
100
- 'sql': '🗃️',
101
- // Web technologies
102
- 'html': '🌐',
103
- 'css': '🎨',
104
- 'scss': '🎨',
105
- 'less': '🎨',
106
- // Data formats
107
- 'json': '📋',
108
- 'yaml': '⚙️',
109
- 'yml': '⚙️',
110
- 'xml': '📄',
111
- 'toml': '⚙️',
112
- 'ini': '⚙️',
113
- 'conf': '⚙️',
114
- 'log': '📄',
115
- 'env': '🔧',
116
- // Documentation
117
- 'markdown': '📝',
118
- 'md': '📝',
119
- // Docker/Build
120
- 'dockerfile': '🐳',
121
- 'makefile': '🔨',
122
- 'dockerignore': '🐳',
123
- 'gitignore': '📝'
124
- };
125
- return iconMap[lowerQuery] || '📄';
84
+ return getFileIcon(`file.${query}`);
126
85
  }
127
86
  // Create command results for the first section
128
87
  function createCommandResults(query, view, searchResults) {
@@ -136,10 +95,12 @@ function createCommandResults(query, view, searchResults) {
136
95
  if (query.trim()) {
137
96
  // Create new file command (only if query doesn't match existing file)
138
97
  if (!hasExactFileMatch) {
98
+ const langIcon = isLanguageQuery ? getLanguageIcon(query) : null;
139
99
  const createFileCommand = {
140
100
  id: isLanguageQuery ? "Create new file" : `Create new file "${query}"`,
141
101
  type: 'create-file',
142
- icon: isLanguageQuery ? getLanguageIcon(query) : '📄',
102
+ icon: langIcon ? langIcon.glyph : DEFAULT_FILE_ICON,
103
+ iconColor: langIcon?.color,
143
104
  query,
144
105
  requiresInput: isLanguageQuery
145
106
  };
@@ -150,23 +111,90 @@ function createCommandResults(query, view, searchResults) {
150
111
  const renameCommand = {
151
112
  id: `Rename to "${query}"`,
152
113
  type: 'rename-file',
153
- icon: '✏️',
114
+ icon: '\uf044', // nf-fa-pencil_square_o (edit icon)
154
115
  query
155
116
  };
156
117
  commands.push(renameCommand);
157
118
  }
158
119
  }
120
+ // Open file (filesystem browser) — always shown
121
+ commands.push({
122
+ id: 'Open file',
123
+ type: 'open-file',
124
+ icon: FOLDER_OPEN_ICON,
125
+ query: '',
126
+ });
127
+ // Import commands — always shown
128
+ commands.push({
129
+ id: 'Import file(s)',
130
+ type: 'import-local-files',
131
+ icon: '\uf15b', // nf-fa-file
132
+ query: '',
133
+ });
134
+ commands.push({
135
+ id: 'Import folder(s)',
136
+ type: 'import-local-folder',
137
+ icon: FOLDER_ICON,
138
+ query: '',
139
+ });
159
140
  return commands;
160
141
  }
142
+ async function importFiles(files, view) {
143
+ const { fs, index } = view.state.facet(CodeblockFacet);
144
+ for (const file of files) {
145
+ const path = file.webkitRelativePath || file.name;
146
+ const dir = path.substring(0, path.lastIndexOf('/'));
147
+ if (dir)
148
+ await fs.mkdir(dir, { recursive: true });
149
+ await fs.writeFile(path, await file.text());
150
+ if (index)
151
+ index.add(path);
152
+ LSP.notifyFileChanged(path, FileChangeType.Created);
153
+ }
154
+ if (index?.savePath)
155
+ await index.save(fs, index.savePath);
156
+ // Open first imported file
157
+ if (files.length > 0) {
158
+ const first = files[0].webkitRelativePath || files[0].name;
159
+ safeDispatch(view, { effects: openFileEffect.of({ path: first }) });
160
+ }
161
+ }
162
+ // Create an LSP log overlay element
163
+ function createLspLogOverlay() {
164
+ const overlay = document.createElement("div");
165
+ overlay.className = "cm-settings-overlay";
166
+ // Log content
167
+ const content = document.createElement("div");
168
+ content.className = "cm-lsp-log-content";
169
+ overlay.appendChild(content);
170
+ function render() {
171
+ const entries = LspLog.entries();
172
+ const fragment = document.createDocumentFragment();
173
+ for (const entry of entries) {
174
+ const div = document.createElement("div");
175
+ div.className = `cm-lsp-log-entry cm-lsp-log-${entry.level}`;
176
+ const time = new Date(entry.timestamp).toLocaleTimeString();
177
+ div.textContent = `[${time}] [${entry.level}] ${entry.message}`;
178
+ fragment.appendChild(div);
179
+ }
180
+ content.replaceChildren(fragment);
181
+ content.scrollTop = content.scrollHeight;
182
+ }
183
+ render();
184
+ const unsub = LspLog.subscribe(render);
185
+ overlay._lspLogUnsub = unsub;
186
+ return overlay;
187
+ }
188
+ const MIN_LOADING_MS = 400;
161
189
  // Toolbar Panel
162
190
  export const toolbarPanel = (view) => {
163
191
  let { filepath, language, index } = view.state.facet(CodeblockFacet);
164
192
  const dom = document.createElement("div");
165
193
  dom.className = "cm-toolbar-panel";
166
- // Create state icon (left side)
194
+ // Create state icon (left side) — magnifying glass at rest
167
195
  const stateIcon = document.createElement("div");
168
196
  stateIcon.className = "cm-toolbar-state-icon";
169
- stateIcon.textContent = "📄"; // Default file icon
197
+ stateIcon.textContent = SEARCH_ICON;
170
198
  // Create container for state icon to help with alignment
171
199
  const stateIconContainer = document.createElement("div");
172
200
  stateIconContainer.className = "cm-toolbar-state-icon-container";
@@ -181,21 +209,149 @@ export const toolbarPanel = (view) => {
181
209
  input.value = filepath || language || "";
182
210
  input.className = "cm-toolbar-input";
183
211
  inputContainer.appendChild(input);
212
+ // LSP log button (shows file-type icon of current file, hidden when lspLogEnabled is false)
213
+ const lspLogBtn = document.createElement("button");
214
+ lspLogBtn.className = "cm-toolbar-lsp-log";
215
+ lspLogBtn.style.fontFamily = 'var(--cm-icon-font-family)';
216
+ function updateLspLogIcon() {
217
+ const filePath = view.state.field(currentFileField).path;
218
+ if (filePath) {
219
+ const icon = getFileIcon(filePath);
220
+ lspLogBtn.textContent = icon.glyph;
221
+ lspLogBtn.style.color = icon.color || '';
222
+ }
223
+ else {
224
+ lspLogBtn.textContent = DEFAULT_FILE_ICON;
225
+ lspLogBtn.style.color = '';
226
+ }
227
+ }
228
+ function updateLspLogVisibility() {
229
+ const enabled = view.state.field(settingsField).lspLogEnabled;
230
+ lspLogBtn.style.display = enabled ? '' : 'none';
231
+ }
232
+ updateLspLogIcon();
233
+ updateLspLogVisibility();
234
+ // Settings cog button (far right of toolbar)
235
+ const settingsCog = document.createElement("button");
236
+ settingsCog.className = "cm-toolbar-settings-cog";
237
+ settingsCog.style.fontFamily = 'var(--cm-icon-font-family)';
238
+ settingsCog.textContent = COG_ICON;
239
+ // Overlay management
240
+ let activeOverlay = null;
241
+ let activeOverlayType = null;
242
+ let savedInputValue = null;
243
+ const overlayLabels = {
244
+ 'settings': 'settings',
245
+ 'lsp-log': 'lsp.log',
246
+ };
247
+ function updateCogAppearance() {
248
+ if (activeOverlayType) {
249
+ settingsCog.textContent = '\u2715'; // ✕
250
+ settingsCog.style.fontFamily = '';
251
+ settingsCog.classList.add('cm-cog-active');
252
+ }
253
+ else {
254
+ settingsCog.textContent = COG_ICON;
255
+ settingsCog.style.fontFamily = 'var(--cm-icon-font-family)';
256
+ settingsCog.classList.remove('cm-cog-active');
257
+ }
258
+ }
259
+ function showOverlay(overlay) {
260
+ const panelsTop = view.dom.querySelector('.cm-panels-top');
261
+ if (panelsTop) {
262
+ overlay.style.top = `${panelsTop.getBoundingClientRect().height}px`;
263
+ }
264
+ view.dom.appendChild(overlay);
265
+ }
266
+ function openOverlay(type, overlay) {
267
+ // Save current input and show overlay label
268
+ savedInputValue = input.value;
269
+ input.value = overlayLabels[type];
270
+ activeOverlay = overlay;
271
+ activeOverlayType = type;
272
+ showOverlay(overlay);
273
+ updateCogAppearance();
274
+ }
275
+ function closeOverlay() {
276
+ if (activeOverlay) {
277
+ // Clean up LSP log subscription if applicable
278
+ if (activeOverlay._lspLogUnsub) {
279
+ activeOverlay._lspLogUnsub();
280
+ }
281
+ activeOverlay.remove();
282
+ activeOverlay = null;
283
+ activeOverlayType = null;
284
+ // Restore input text
285
+ if (savedInputValue !== null) {
286
+ input.value = savedInputValue;
287
+ savedInputValue = null;
288
+ }
289
+ updateCogAppearance();
290
+ }
291
+ }
292
+ settingsCog.addEventListener("click", () => {
293
+ if (activeOverlayType) {
294
+ closeOverlay();
295
+ }
296
+ else {
297
+ openOverlay('settings', createSettingsOverlay(view));
298
+ }
299
+ });
300
+ lspLogBtn.addEventListener("click", () => {
301
+ if (activeOverlayType === 'lsp-log') {
302
+ closeOverlay();
303
+ }
304
+ else {
305
+ closeOverlay();
306
+ openOverlay('lsp-log', createLspLogOverlay());
307
+ }
308
+ });
309
+ dom.appendChild(lspLogBtn);
310
+ dom.appendChild(settingsCog);
184
311
  const resultsList = document.createElement("ul");
185
312
  resultsList.className = "cm-search-results";
186
313
  dom.appendChild(resultsList);
187
314
  let selectedIndex = 0;
188
315
  let namingMode = { active: false, type: 'create-file', originalQuery: '' };
316
+ let browseMode = { active: false, currentPath: '/', filter: '' };
317
+ let loadingStartTime = null;
318
+ // System theme media query listener
319
+ const systemThemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
320
+ function handleSystemThemeChange() {
321
+ const settings = view.state.field(settingsField);
322
+ if (settings.theme === 'system') {
323
+ safeDispatch(view, {
324
+ effects: setThemeEffect.of({ dark: systemThemeQuery.matches })
325
+ });
326
+ }
327
+ }
328
+ systemThemeQuery.addEventListener('change', handleSystemThemeChange);
329
+ // Apply initial settings (font size, font family, theme)
330
+ function applySettings() {
331
+ const settings = view.state.field(settingsField);
332
+ view.dom.style.setProperty('--cm-font-size', `${settings.fontSize}px`);
333
+ if (settings.fontFamily) {
334
+ view.dom.style.setProperty('--cm-font-family', settings.fontFamily);
335
+ }
336
+ else {
337
+ view.dom.style.removeProperty('--cm-font-family');
338
+ }
339
+ }
340
+ applySettings();
341
+ // Apply initial theme
342
+ const initialSettings = view.state.field(settingsField);
343
+ const initialDark = resolveThemeDark(initialSettings.theme);
344
+ view.dom.setAttribute('data-theme', initialDark ? 'dark' : 'light');
189
345
  // Tracks gutter width for toolbar alignment
190
346
  function updateGutterWidthVariables() {
191
347
  const gutters = view.dom.querySelector('.cm-gutters');
192
348
  if (gutters) {
193
349
  const gutterWidth = gutters.getBoundingClientRect().width;
194
- dom.style.setProperty('--cm-gutter-width', `${gutterWidth}px`);
350
+ view.dom.style.setProperty('--cm-gutter-width', `${gutterWidth}px`);
195
351
  const numberGutter = gutters.querySelector('.cm-lineNumbers');
196
352
  if (numberGutter) {
197
353
  const numberGutterWidth = numberGutter.getBoundingClientRect().width;
198
- dom.style.setProperty('--cm-gutter-lineno-width', `${numberGutterWidth}px`);
354
+ view.dom.style.setProperty('--cm-gutter-lineno-width', `${numberGutterWidth}px`);
199
355
  }
200
356
  }
201
357
  }
@@ -215,12 +371,33 @@ export const toolbarPanel = (view) => {
215
371
  setupGutterObserver();
216
372
  const renderItem = (result, i) => {
217
373
  const li = document.createElement("li");
218
- li.className = `cm-search-result ${isCommandResult(result) ? 'cm-command-result' : 'cm-file-result'}`;
374
+ let resultClass = 'cm-file-result';
375
+ if (isCommandResult(result))
376
+ resultClass = 'cm-command-result';
377
+ else if (isBrowseEntry(result))
378
+ resultClass = result.type === 'browse-file' ? 'cm-file-result' : 'cm-browse-dir-result';
379
+ li.className = `cm-search-result ${resultClass}`;
219
380
  const resultIconContainer = document.createElement("div");
220
381
  resultIconContainer.className = "cm-search-result-icon-container";
221
382
  const resultIcon = document.createElement("div");
222
383
  resultIcon.className = "cm-search-result-icon";
223
- resultIcon.textContent = isCommandResult(result) ? result.icon : '📄';
384
+ resultIcon.style.fontFamily = 'var(--cm-icon-font-family)';
385
+ if (isBrowseEntry(result)) {
386
+ resultIcon.textContent = result.icon;
387
+ if (result.iconColor)
388
+ resultIcon.style.color = result.iconColor;
389
+ }
390
+ else if (isCommandResult(result)) {
391
+ resultIcon.textContent = result.icon;
392
+ if (result.iconColor)
393
+ resultIcon.style.color = result.iconColor;
394
+ }
395
+ else {
396
+ const icon = getFileIcon(result.id);
397
+ resultIcon.textContent = icon.glyph;
398
+ if (icon.color)
399
+ resultIcon.style.color = icon.color;
400
+ }
224
401
  resultIconContainer.appendChild(resultIcon);
225
402
  li.appendChild(resultIconContainer);
226
403
  const resultLabel = document.createElement("div");
@@ -238,24 +415,17 @@ export const toolbarPanel = (view) => {
238
415
  function updateDropdown() {
239
416
  const results = view.state.field(searchResultsField);
240
417
  const children = [];
241
- // Separate commands from search results
242
- const commands = results.filter(isCommandResult);
243
- const searchResults = results.filter(isSearchResult);
244
- let currentIndex = 0;
245
- // Render commands section
246
- commands.forEach((command) => {
247
- children.push(renderItem(command, currentIndex));
248
- currentIndex++;
249
- });
250
- // Render search results section
251
- searchResults.forEach((result) => {
252
- children.push(renderItem(result, currentIndex));
253
- currentIndex++;
418
+ // Render items in state array order (search results first, commands second)
419
+ results.forEach((result, i) => {
420
+ children.push(renderItem(result, i));
254
421
  });
255
422
  resultsList.replaceChildren(...children);
256
423
  }
257
424
  function selectResult(result) {
258
- if (isCommandResult(result)) {
425
+ if (isBrowseEntry(result)) {
426
+ navigateBrowse(result);
427
+ }
428
+ else if (isCommandResult(result)) {
259
429
  handleCommandResult(result);
260
430
  }
261
431
  else {
@@ -264,10 +434,10 @@ export const toolbarPanel = (view) => {
264
434
  }
265
435
  function updateStateIcon() {
266
436
  if (namingMode.active) {
267
- stateIcon.textContent = namingMode.type === 'create-file' ? '📄' : '✏️';
437
+ stateIcon.textContent = namingMode.type === 'create-file' ? DEFAULT_FILE_ICON : '\uf044';
268
438
  }
269
439
  else {
270
- stateIcon.textContent = '📄'; // Default file icon
440
+ stateIcon.textContent = SEARCH_ICON;
271
441
  }
272
442
  }
273
443
  function enterNamingMode(type, originalQuery, languageExtension) {
@@ -286,8 +456,125 @@ export const toolbarPanel = (view) => {
286
456
  updateStateIcon();
287
457
  input.placeholder = '';
288
458
  }
459
+ // --- Browse mode functions ---
460
+ async function enterBrowseMode(startPath) {
461
+ const { cwd } = view.state.facet(CodeblockFacet);
462
+ const browsePath = startPath || cwd || '/';
463
+ browseMode = { active: true, currentPath: browsePath, filter: '' };
464
+ // Update state icon to folder
465
+ stateIcon.textContent = FOLDER_OPEN_ICON;
466
+ input.value = browsePath.endsWith('/') ? browsePath : browsePath + '/';
467
+ input.placeholder = '';
468
+ input.focus();
469
+ await refreshBrowseEntries();
470
+ }
471
+ async function refreshBrowseEntries() {
472
+ if (!browseMode.active)
473
+ return;
474
+ const { fs } = view.state.facet(CodeblockFacet);
475
+ const dir = browseMode.currentPath;
476
+ try {
477
+ const entries = await fs.readDir(dir);
478
+ const browseResults = [];
479
+ // Add parent directory entry if not at root
480
+ if (dir !== '/' && dir !== '') {
481
+ const parentPath = dir.split('/').slice(0, -1).join('/') || '/';
482
+ browseResults.push({
483
+ id: '..',
484
+ type: 'browse-parent',
485
+ icon: PARENT_DIR_ICON,
486
+ fullPath: parentPath,
487
+ });
488
+ }
489
+ // Separate directories and files, sort each alphabetically
490
+ const dirs = [];
491
+ const files = [];
492
+ for (const [name, fileType] of entries) {
493
+ // Skip hidden/internal files
494
+ if (name.startsWith('.'))
495
+ continue;
496
+ const fullPath = dir === '/' ? `${name}` : `${dir}/${name}`;
497
+ // FileType.Directory = 2
498
+ if (fileType === 2) {
499
+ dirs.push({
500
+ id: name + '/',
501
+ type: 'browse-directory',
502
+ icon: FOLDER_ICON,
503
+ fullPath,
504
+ });
505
+ }
506
+ else {
507
+ const icon = getFileIcon(name);
508
+ files.push({
509
+ id: name,
510
+ type: 'browse-file',
511
+ icon: icon.glyph,
512
+ iconColor: icon.color,
513
+ fullPath,
514
+ });
515
+ }
516
+ }
517
+ dirs.sort((a, b) => a.id.localeCompare(b.id));
518
+ files.sort((a, b) => a.id.localeCompare(b.id));
519
+ // Apply filter
520
+ const filter = browseMode.filter.toLowerCase();
521
+ const filtered = [...browseResults, ...dirs, ...files].filter(entry => entry.type === 'browse-parent' || entry.id.toLowerCase().includes(filter));
522
+ selectedIndex = 0;
523
+ safeDispatch(view, { effects: setSearchResults.of(filtered) });
524
+ }
525
+ catch (e) {
526
+ console.error('Failed to read directory:', e);
527
+ exitBrowseMode();
528
+ }
529
+ }
530
+ async function navigateBrowse(entry) {
531
+ if (entry.type === 'browse-file') {
532
+ // Open the file and exit browse mode
533
+ const path = entry.fullPath;
534
+ exitBrowseMode();
535
+ input.value = path;
536
+ safeDispatch(view, {
537
+ effects: [setSearchResults.of([]), openFileEffect.of({ path })]
538
+ });
539
+ }
540
+ else {
541
+ // Navigate into directory (or parent)
542
+ browseMode.currentPath = entry.fullPath;
543
+ browseMode.filter = '';
544
+ const displayPath = entry.fullPath === '/' ? '/' : entry.fullPath + '/';
545
+ input.value = displayPath;
546
+ selectedIndex = 0;
547
+ await refreshBrowseEntries();
548
+ }
549
+ }
550
+ function exitBrowseMode() {
551
+ browseMode = { active: false, currentPath: '/', filter: '' };
552
+ updateStateIcon();
553
+ input.placeholder = '';
554
+ }
555
+ function triggerFileImport(folder) {
556
+ safeDispatch(view, { effects: setSearchResults.of([]) });
557
+ const fileInput = document.createElement('input');
558
+ fileInput.type = 'file';
559
+ if (folder) {
560
+ fileInput.setAttribute('webkitdirectory', '');
561
+ }
562
+ else {
563
+ fileInput.multiple = true;
564
+ }
565
+ fileInput.addEventListener('change', () => {
566
+ if (fileInput.files?.length) {
567
+ importFiles(fileInput.files, view);
568
+ }
569
+ });
570
+ fileInput.click();
571
+ }
289
572
  function handleCommandResult(command) {
290
- if (command.type === 'create-file') {
573
+ if (command.type === 'open-file') {
574
+ enterBrowseMode();
575
+ return;
576
+ }
577
+ else if (command.type === 'create-file') {
291
578
  if (command.requiresInput) {
292
579
  // Enter naming mode for language-specific file
293
580
  enterNamingMode('create-file', command.query, command.query);
@@ -314,6 +601,12 @@ export const toolbarPanel = (view) => {
314
601
  });
315
602
  }
316
603
  }
604
+ else if (command.type === 'import-local-files') {
605
+ triggerFileImport(false);
606
+ }
607
+ else if (command.type === 'import-local-folder') {
608
+ triggerFileImport(true);
609
+ }
317
610
  }
318
611
  function handleSearchResult(result) {
319
612
  input.value = result.id;
@@ -348,10 +641,17 @@ export const toolbarPanel = (view) => {
348
641
  }
349
642
  exitNamingMode();
350
643
  }
644
+ function resetInputToCurrentFile() {
645
+ const currentFile = view.state.field(currentFileField);
646
+ input.value = currentFile.path || '';
647
+ }
351
648
  // Close dropdown when clicking outside
352
649
  function handleClickOutside(event) {
353
650
  if (!dom.contains(event.target)) {
651
+ if (browseMode.active)
652
+ exitBrowseMode();
354
653
  safeDispatch(view, { effects: setSearchResults.of([]) });
654
+ resetInputToCurrentFile();
355
655
  }
356
656
  }
357
657
  input.addEventListener("click", () => {
@@ -362,10 +662,15 @@ export const toolbarPanel = (view) => {
362
662
  if (query.trim()) {
363
663
  // Get regular search results from index first
364
664
  const searchResults = (index?.search(query) || []).slice(0, 100);
365
- // Add command results first (passing search results to check for existing files)
665
+ // Add command results (passing search results to check for existing files)
366
666
  const commands = createCommandResults(query, view, searchResults);
667
+ // Search results first, then commands
367
668
  results = searchResults.concat(commands);
368
669
  }
670
+ else {
671
+ // Show import commands when dropdown opens with empty query
672
+ results = createCommandResults('', view, []);
673
+ }
369
674
  safeDispatch(view, { effects: setSearchResults.of(results) });
370
675
  // Add click-outside listener when dropdown opens
371
676
  document.addEventListener("click", handleClickOutside);
@@ -378,15 +683,27 @@ export const toolbarPanel = (view) => {
378
683
  if (namingMode.active) {
379
684
  return;
380
685
  }
686
+ // If in browse mode, filter the directory entries
687
+ if (browseMode.active) {
688
+ // Extract filter text after the directory path prefix
689
+ const prefix = browseMode.currentPath === '/' ? '/' : browseMode.currentPath + '/';
690
+ browseMode.filter = query.startsWith(prefix) ? query.slice(prefix.length) : query;
691
+ refreshBrowseEntries();
692
+ return;
693
+ }
381
694
  let results = [];
382
695
  if (query.trim()) {
383
696
  // Get regular search results from index first
384
697
  const searchResults = (index?.search(query) || []).slice(0, 1000);
385
- // Add command results first (passing search results to check for existing files)
698
+ // Add command results (passing search results to check for existing files)
386
699
  const commands = createCommandResults(query, view, searchResults);
387
- results.push(...commands);
388
- // Add search results
700
+ // Search results first, then commands
389
701
  results.push(...searchResults);
702
+ results.push(...commands);
703
+ }
704
+ else {
705
+ // Show import commands even with empty query
706
+ results = createCommandResults('', view, []);
390
707
  }
391
708
  safeDispatch(view, { effects: setSearchResults.of(results) });
392
709
  });
@@ -404,6 +721,47 @@ export const toolbarPanel = (view) => {
404
721
  }
405
722
  return;
406
723
  }
724
+ // Browse mode keyboard handling
725
+ if (browseMode.active) {
726
+ const results = view.state.field(searchResultsField);
727
+ if (event.key === "ArrowDown") {
728
+ event.preventDefault();
729
+ if (results.length) {
730
+ selectedIndex = mod(selectedIndex + 1, results.length);
731
+ updateDropdown();
732
+ }
733
+ }
734
+ else if (event.key === "ArrowUp") {
735
+ event.preventDefault();
736
+ if (results.length) {
737
+ selectedIndex = mod(selectedIndex - 1, results.length);
738
+ updateDropdown();
739
+ }
740
+ }
741
+ else if (event.key === "Enter" && results.length && selectedIndex >= 0) {
742
+ event.preventDefault();
743
+ selectResult(results[selectedIndex]);
744
+ }
745
+ else if (event.key === "Backspace") {
746
+ // If filter is empty and backspace pressed, go up a directory
747
+ if (browseMode.filter === '' && browseMode.currentPath !== '/') {
748
+ event.preventDefault();
749
+ const parentPath = browseMode.currentPath.split('/').slice(0, -1).join('/') || '/';
750
+ browseMode.currentPath = parentPath;
751
+ const displayPath = parentPath === '/' ? '/' : parentPath + '/';
752
+ input.value = displayPath;
753
+ refreshBrowseEntries();
754
+ }
755
+ }
756
+ else if (event.key === "Escape") {
757
+ event.preventDefault();
758
+ exitBrowseMode();
759
+ safeDispatch(view, { effects: setSearchResults.of([]) });
760
+ resetInputToCurrentFile();
761
+ input.blur();
762
+ }
763
+ return;
764
+ }
407
765
  // Normal search mode
408
766
  const results = view.state.field(searchResultsField);
409
767
  if (event.key === "ArrowDown") {
@@ -412,6 +770,10 @@ export const toolbarPanel = (view) => {
412
770
  selectedIndex = mod(selectedIndex + 1, results.length);
413
771
  updateDropdown();
414
772
  }
773
+ else {
774
+ // No dropdown open — move cursor to editor body
775
+ view.focus();
776
+ }
415
777
  }
416
778
  else if (event.key === "ArrowUp") {
417
779
  event.preventDefault();
@@ -424,6 +786,12 @@ export const toolbarPanel = (view) => {
424
786
  event.preventDefault();
425
787
  selectResult(results[selectedIndex]);
426
788
  }
789
+ else if (event.key === "Escape") {
790
+ event.preventDefault();
791
+ safeDispatch(view, { effects: setSearchResults.of([]) });
792
+ resetInputToCurrentFile();
793
+ input.blur();
794
+ }
427
795
  });
428
796
  return {
429
797
  dom,
@@ -439,10 +807,55 @@ export const toolbarPanel = (view) => {
439
807
  document.removeEventListener("click", handleClickOutside);
440
808
  }
441
809
  }
810
+ // Apply settings when they change
811
+ const prevSettings = update.startState.field(settingsField);
812
+ const nextSettings = update.state.field(settingsField);
813
+ if (prevSettings.fontSize !== nextSettings.fontSize || prevSettings.fontFamily !== nextSettings.fontFamily) {
814
+ applySettings();
815
+ }
816
+ if (prevSettings.lspLogEnabled !== nextSettings.lspLogEnabled) {
817
+ updateLspLogVisibility();
818
+ // Close the log overlay if the user disables it
819
+ if (!nextSettings.lspLogEnabled && activeOverlayType === 'lsp-log') {
820
+ closeOverlay();
821
+ }
822
+ }
823
+ // Update loading indicator with minimum animation duration
824
+ const prevFile = update.startState.field(currentFileField);
825
+ const nextFile = update.state.field(currentFileField);
826
+ if (prevFile.loading !== nextFile.loading) {
827
+ if (nextFile.loading) {
828
+ loadingStartTime = Date.now();
829
+ stateIcon.textContent = ''; // clear glyph; CSS border spinner handles the visual
830
+ stateIcon.classList.add('cm-loading');
831
+ }
832
+ else {
833
+ const elapsed = loadingStartTime ? Date.now() - loadingStartTime : Infinity;
834
+ const remaining = Math.max(0, MIN_LOADING_MS - elapsed);
835
+ loadingStartTime = null;
836
+ setTimeout(() => {
837
+ if (!view.state.field(currentFileField).loading) {
838
+ stateIcon.textContent = SEARCH_ICON;
839
+ stateIcon.classList.remove('cm-loading');
840
+ }
841
+ }, remaining);
842
+ }
843
+ }
844
+ // Update LSP log icon when file changes
845
+ if (prevFile.path !== nextFile.path) {
846
+ updateLspLogIcon();
847
+ }
848
+ // Sync input value when file path changes (unless overlay is open or user is naming)
849
+ if (prevFile.path !== nextFile.path && !namingMode.active && !activeOverlayType) {
850
+ input.value = nextFile.path || '';
851
+ }
442
852
  },
443
853
  destroy() {
444
854
  // Clean up event listeners when panel is destroyed
445
855
  document.removeEventListener("click", handleClickOutside);
856
+ systemThemeQuery.removeEventListener('change', handleSystemThemeChange);
857
+ // Clean up overlay
858
+ closeOverlay();
446
859
  // Clean up ResizeObserver
447
860
  if (gutterObserver) {
448
861
  gutterObserver.disconnect();