@joinezco/codeblock 0.0.8 → 0.0.10
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/dist/editor.d.ts +30 -3
- package/dist/editor.js +416 -43
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/lsps/index.d.ts +5 -0
- package/dist/lsps/index.js +9 -2
- package/dist/lsps/typescript.d.ts +3 -1
- package/dist/lsps/typescript.js +8 -17
- package/dist/panels/settings.d.ts +22 -0
- package/dist/panels/settings.js +267 -0
- package/dist/panels/terminal.d.ts +3 -0
- package/dist/panels/terminal.js +76 -0
- package/dist/panels/toolbar.d.ts +53 -3
- package/dist/panels/toolbar.js +1336 -164
- package/dist/panels/toolbar.test.js +20 -14
- package/dist/rpc/transport.d.ts +2 -11
- package/dist/rpc/transport.js +19 -35
- package/dist/themes/index.js +226 -13
- package/dist/themes/vscode.js +3 -2
- package/dist/types.d.ts +5 -0
- package/dist/utils/fs.d.ts +22 -3
- package/dist/utils/fs.js +126 -21
- package/dist/utils/lsp.d.ts +26 -15
- package/dist/utils/lsp.js +79 -44
- package/dist/utils/search.d.ts +2 -0
- package/dist/utils/search.js +13 -4
- package/dist/utils/typescript-defaults.d.ts +57 -0
- package/dist/utils/typescript-defaults.js +208 -0
- package/dist/utils/typescript-defaults.test.d.ts +1 -0
- package/dist/utils/typescript-defaults.test.js +197 -0
- package/dist/workers/fs.worker.d.ts +4 -8
- package/dist/workers/fs.worker.js +30 -60
- package/dist/workers/javascript.worker.js +11 -9
- package/package.json +8 -4
- package/dist/assets/clike-C8IJ2oj_.js +0 -1
- package/dist/assets/cmake-BQqOBYOt.js +0 -1
- package/dist/assets/dockerfile-C_y-rIpk.js +0 -1
- package/dist/assets/fs.worker-BwEqZcql.ts +0 -109
- package/dist/assets/go-CTD25R5P.js +0 -1
- package/dist/assets/haskell-BWDZoCOh.js +0 -1
- package/dist/assets/index-9HdhmM_Y.js +0 -1
- package/dist/assets/index-C-QhPFHP.js +0 -3
- package/dist/assets/index-C3BnE2cG.js +0 -222
- package/dist/assets/index-CGx5MZO7.js +0 -6
- package/dist/assets/index-CIuq3uTk.js +0 -1
- package/dist/assets/index-CXFONXS8.js +0 -1
- package/dist/assets/index-D5Z27j1C.js +0 -1
- package/dist/assets/index-DWOBdRjn.js +0 -1
- package/dist/assets/index-Dvu-FFzd.js +0 -1
- package/dist/assets/index-Dx_VuNNd.js +0 -1
- package/dist/assets/index-I0dlv-r3.js +0 -1
- package/dist/assets/index-MGle_v2x.js +0 -1
- package/dist/assets/index-N-GE7HTU.js +0 -1
- package/dist/assets/index-aEsF5o-7.js +0 -2
- package/dist/assets/index-as7ELo0J.js +0 -1
- package/dist/assets/index-gUUzXNuP.js +0 -1
- package/dist/assets/index-pGm0qkrJ.js +0 -13
- package/dist/assets/javascript.worker-C1zGArKk.js +0 -527
- package/dist/assets/lua-BgMRiT3U.js +0 -1
- package/dist/assets/perl-CdXCOZ3F.js +0 -1
- package/dist/assets/process-Dw9K5EnD.js +0 -1357
- package/dist/assets/properties-C78fOPTZ.js +0 -1
- package/dist/assets/ruby-B2Rjki9n.js +0 -1
- package/dist/assets/shell-CjFT_Tl9.js +0 -1
- package/dist/assets/swift-BzpIVaGY.js +0 -1
- package/dist/assets/toml-BXUEaScT.js +0 -1
- package/dist/assets/vb-CmGdzxic.js +0 -1
- package/dist/e2e/example.spec.d.ts +0 -5
- package/dist/e2e/example.spec.js +0 -44
- package/dist/index.html +0 -16
- package/dist/resources/config.json +0 -13
- package/dist/snapshot.bin +0 -0
- package/dist/styles.css +0 -7
package/dist/panels/toolbar.js
CHANGED
|
@@ -1,12 +1,50 @@
|
|
|
1
|
+
import { EditorView } from "@codemirror/view";
|
|
1
2
|
import { StateEffect, StateField } from "@codemirror/state";
|
|
2
|
-
import { CodeblockFacet, openFileEffect, currentFileField } from "../editor";
|
|
3
|
+
import { CodeblockFacet, openFileEffect, fileLoadedEffect, currentFileField, setThemeEffect, lineWrappingCompartment, lineNumbersCompartment, foldGutterCompartment } from "../editor";
|
|
4
|
+
import { lineNumbers, highlightActiveLineGutter } from "@codemirror/view";
|
|
5
|
+
import { foldGutter } from "@codemirror/language";
|
|
3
6
|
import { extOrLanguageToLanguageId } from "../lsps";
|
|
7
|
+
import { LSP, LspLog, FileChangeType } from "../utils/lsp";
|
|
8
|
+
import { Seti } from "@m234/nerd-fonts/fs";
|
|
9
|
+
import { settingsField, resolveThemeDark, updateSettingsEffect } from "./settings";
|
|
10
|
+
// Browser-safe file icon lookup (avoids node:path.parse used by Seti.fromPath)
|
|
11
|
+
const FALLBACK_ICON = { value: '\ue64e', hexCode: 0xe64e }; // nf-seti-text
|
|
12
|
+
function setiIconForPath(filePath) {
|
|
13
|
+
const base = filePath.split('/').pop() || filePath;
|
|
14
|
+
// Check exact basename match first (e.g. Dockerfile, Makefile)
|
|
15
|
+
const byBase = Seti.byBaseSeti.get(base);
|
|
16
|
+
if (byBase)
|
|
17
|
+
return byBase;
|
|
18
|
+
// Walk extensions from longest to shortest (e.g. .spec.ts → .ts)
|
|
19
|
+
let dot = base.indexOf('.');
|
|
20
|
+
if (dot < 0)
|
|
21
|
+
return FALLBACK_ICON;
|
|
22
|
+
let ext = base.slice(dot);
|
|
23
|
+
for (;;) {
|
|
24
|
+
const byExt = Seti.byExtensionSeti.get(ext);
|
|
25
|
+
if (byExt)
|
|
26
|
+
return byExt;
|
|
27
|
+
dot = ext.indexOf('.', 1);
|
|
28
|
+
if (dot === -1)
|
|
29
|
+
break;
|
|
30
|
+
ext = ext.slice(dot);
|
|
31
|
+
}
|
|
32
|
+
return FALLBACK_ICON;
|
|
33
|
+
}
|
|
34
|
+
const fileActionRegistry = [];
|
|
35
|
+
/** Register a command that appears when files with matching extensions are open. */
|
|
36
|
+
export function registerFileAction(entry) {
|
|
37
|
+
fileActionRegistry.push(entry);
|
|
38
|
+
}
|
|
4
39
|
// Type guards
|
|
5
40
|
function isCommandResult(result) {
|
|
6
|
-
return 'type' in result;
|
|
41
|
+
return 'type' in result && result.query !== undefined;
|
|
7
42
|
}
|
|
8
|
-
function
|
|
9
|
-
return '
|
|
43
|
+
function isBrowseEntry(result) {
|
|
44
|
+
return 'type' in result && ('fullPath' in result);
|
|
45
|
+
}
|
|
46
|
+
function isSettingsEntry(result) {
|
|
47
|
+
return 'type' in result && ('settingKey' in result);
|
|
10
48
|
}
|
|
11
49
|
// Search results state - now handles both commands and search results
|
|
12
50
|
export const setSearchResults = StateEffect.define();
|
|
@@ -40,133 +78,244 @@ function isValidProgrammingLanguage(query) {
|
|
|
40
78
|
return Object.keys(extOrLanguageToLanguageId).some(key => key.toLowerCase() === lowerQuery ||
|
|
41
79
|
extOrLanguageToLanguageId[key].toLowerCase() === lowerQuery);
|
|
42
80
|
}
|
|
43
|
-
//
|
|
44
|
-
function
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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': '📝'
|
|
81
|
+
// Map language names to canonical file extensions
|
|
82
|
+
function languageToFileExtension(langOrExt) {
|
|
83
|
+
const nameToExt = {
|
|
84
|
+
javascript: 'js',
|
|
85
|
+
typescript: 'ts',
|
|
86
|
+
python: 'py',
|
|
87
|
+
ruby: 'rb',
|
|
88
|
+
csharp: 'cs',
|
|
89
|
+
kotlin: 'kt',
|
|
90
|
+
rust: 'rs',
|
|
91
|
+
haskell: 'hs',
|
|
92
|
+
perl: 'pl',
|
|
93
|
+
bash: 'sh',
|
|
94
|
+
shell: 'sh',
|
|
95
|
+
mysql: 'sql',
|
|
96
|
+
markdown: 'md',
|
|
124
97
|
};
|
|
125
|
-
return
|
|
98
|
+
return nameToExt[langOrExt.toLowerCase()] || langOrExt.toLowerCase();
|
|
126
99
|
}
|
|
100
|
+
// Icons
|
|
101
|
+
const SEARCH_ICON = '\uf002'; // nf-fa-search (magnifying glass)
|
|
102
|
+
const DEFAULT_FILE_ICON = '\ue64e'; // nf-seti-text
|
|
103
|
+
const COG_ICON = '\uf013'; // nf-fa-cog
|
|
104
|
+
const FOLDER_ICON = '\ue613'; // nf-seti-folder
|
|
105
|
+
const FOLDER_OPEN_ICON = '\ue614'; // nf-seti-folder (open variant)
|
|
106
|
+
const PARENT_DIR_ICON = '\uf112'; // nf-fa-reply (back/up arrow)
|
|
107
|
+
// const TERMINAL_ICON = '\uf120'; // nf-fa-terminal — awaiting WASM VM/shim
|
|
108
|
+
// Get nerd font icon for a file path
|
|
109
|
+
function getFileIcon(path) {
|
|
110
|
+
const result = setiIconForPath(path);
|
|
111
|
+
return { glyph: result.value, color: result.color || '' };
|
|
112
|
+
}
|
|
113
|
+
// Get icon for a language/extension query (used for create-file commands)
|
|
114
|
+
function getLanguageIcon(query) {
|
|
115
|
+
return getFileIcon(`file.${query}`);
|
|
116
|
+
}
|
|
117
|
+
// Theme cycle values and icons
|
|
118
|
+
const themeCycleValues = ['light', 'dark', 'system'];
|
|
119
|
+
const themeIcons = {
|
|
120
|
+
light: '\u2600\uFE0F', // ☀️
|
|
121
|
+
dark: '\uD83C\uDF19', // 🌙
|
|
122
|
+
system: '\uD83C\uDF13', // 🌓
|
|
123
|
+
};
|
|
124
|
+
// Font family cycle values
|
|
125
|
+
const fontFamilyCycleValues = ['', '"UbuntuMono NF", monospace'];
|
|
126
|
+
const fontFamilyLabels = {
|
|
127
|
+
'': 'System default',
|
|
128
|
+
'"UbuntuMono NF", monospace': 'UbuntuMono NF',
|
|
129
|
+
};
|
|
127
130
|
// Create command results for the first section
|
|
128
131
|
function createCommandResults(query, view, searchResults) {
|
|
129
132
|
const commands = [];
|
|
130
133
|
const currentFile = view.state.field(currentFileField);
|
|
131
134
|
const hasValidFile = currentFile.path && !currentFile.loading;
|
|
135
|
+
const hasContent = view.state.doc.length > 0;
|
|
132
136
|
const isLanguageQuery = isValidProgrammingLanguage(query);
|
|
133
|
-
// TODO: fix language ext for new file with full language names, "typescript" -> "file.ts"
|
|
134
137
|
// Check if query matches an existing file (first search result with exact match)
|
|
135
138
|
const hasExactFileMatch = searchResults.length > 0 && searchResults[0].id === query;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (!
|
|
139
|
-
|
|
140
|
-
id:
|
|
141
|
-
type: '
|
|
142
|
-
icon:
|
|
139
|
+
// "Save as" — shown when the editor has content or a file is open
|
|
140
|
+
if (hasContent || hasValidFile) {
|
|
141
|
+
if (!query.trim()) {
|
|
142
|
+
commands.push({
|
|
143
|
+
id: 'Save as',
|
|
144
|
+
type: 'save-as',
|
|
145
|
+
icon: DEFAULT_FILE_ICON,
|
|
146
|
+
query: '',
|
|
147
|
+
requiresInput: true,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
else if (!hasExactFileMatch) {
|
|
151
|
+
const langIcon = isLanguageQuery ? getLanguageIcon(query) : null;
|
|
152
|
+
commands.push({
|
|
153
|
+
id: isLanguageQuery ? "Save as" : `Save as "${query}"`,
|
|
154
|
+
type: 'save-as',
|
|
155
|
+
icon: langIcon ? langIcon.glyph : DEFAULT_FILE_ICON,
|
|
156
|
+
iconColor: langIcon?.color,
|
|
143
157
|
query,
|
|
144
|
-
requiresInput: isLanguageQuery
|
|
145
|
-
};
|
|
146
|
-
commands.push(createFileCommand);
|
|
158
|
+
requiresInput: isLanguageQuery,
|
|
159
|
+
});
|
|
147
160
|
}
|
|
161
|
+
}
|
|
162
|
+
// "Create new file" — always available, creates a blank file
|
|
163
|
+
if (!query.trim()) {
|
|
164
|
+
commands.push({
|
|
165
|
+
id: 'Create new file',
|
|
166
|
+
type: 'create-file',
|
|
167
|
+
icon: DEFAULT_FILE_ICON,
|
|
168
|
+
query: '',
|
|
169
|
+
requiresInput: true,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
else if (!hasExactFileMatch) {
|
|
173
|
+
const langIcon = isLanguageQuery ? getLanguageIcon(query) : null;
|
|
174
|
+
commands.push({
|
|
175
|
+
id: isLanguageQuery ? "Create new file" : `Create new file "${query}"`,
|
|
176
|
+
type: 'create-file',
|
|
177
|
+
icon: langIcon ? langIcon.glyph : DEFAULT_FILE_ICON,
|
|
178
|
+
iconColor: langIcon?.color,
|
|
179
|
+
query,
|
|
180
|
+
requiresInput: isLanguageQuery,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (query.trim()) {
|
|
148
184
|
// Rename file command (only if file is open, query is not a language, and doesn't match current file)
|
|
149
185
|
if (hasValidFile && !isLanguageQuery && !hasExactFileMatch) {
|
|
150
186
|
const renameCommand = {
|
|
151
187
|
id: `Rename to "${query}"`,
|
|
152
188
|
type: 'rename-file',
|
|
153
|
-
icon: '
|
|
189
|
+
icon: '\uf044', // nf-fa-pencil_square_o (edit icon)
|
|
154
190
|
query
|
|
155
191
|
};
|
|
156
192
|
commands.push(renameCommand);
|
|
157
193
|
}
|
|
158
194
|
}
|
|
195
|
+
// Open file (filesystem browser) — always shown
|
|
196
|
+
commands.push({
|
|
197
|
+
id: 'Open file',
|
|
198
|
+
type: 'open-file',
|
|
199
|
+
icon: FOLDER_OPEN_ICON,
|
|
200
|
+
query: '',
|
|
201
|
+
});
|
|
202
|
+
// Open terminal — awaiting a backing WASM VM/shim for implementation
|
|
203
|
+
// commands.push({
|
|
204
|
+
// id: 'Open terminal',
|
|
205
|
+
// type: 'open-terminal',
|
|
206
|
+
// icon: TERMINAL_ICON,
|
|
207
|
+
// query: '',
|
|
208
|
+
// });
|
|
209
|
+
// Import commands — always shown
|
|
210
|
+
commands.push({
|
|
211
|
+
id: 'Import file(s)',
|
|
212
|
+
type: 'import-local-files',
|
|
213
|
+
icon: '\uf15b', // nf-fa-file
|
|
214
|
+
query: '',
|
|
215
|
+
});
|
|
216
|
+
commands.push({
|
|
217
|
+
id: 'Import folder',
|
|
218
|
+
type: 'import-local-folder',
|
|
219
|
+
icon: FOLDER_ICON,
|
|
220
|
+
query: '',
|
|
221
|
+
});
|
|
222
|
+
// File-extension-specific commands — shown when a matching file is open
|
|
223
|
+
if (hasValidFile && currentFile.path) {
|
|
224
|
+
const ext = currentFile.path.split('.').pop()?.toLowerCase() || '';
|
|
225
|
+
for (const entry of fileActionRegistry) {
|
|
226
|
+
if (entry.extensions.includes(ext)) {
|
|
227
|
+
commands.push({
|
|
228
|
+
id: entry.label,
|
|
229
|
+
type: 'file-action',
|
|
230
|
+
icon: entry.icon,
|
|
231
|
+
query: '',
|
|
232
|
+
action: entry.action,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Settings command — always shown
|
|
238
|
+
commands.push({
|
|
239
|
+
id: 'Settings',
|
|
240
|
+
type: 'settings',
|
|
241
|
+
icon: COG_ICON,
|
|
242
|
+
query: '',
|
|
243
|
+
});
|
|
159
244
|
return commands;
|
|
160
245
|
}
|
|
246
|
+
const BINARY_IMAGE_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'ico', 'avif']);
|
|
247
|
+
function fileToDataUrl(file) {
|
|
248
|
+
return new Promise((resolve, reject) => {
|
|
249
|
+
const reader = new FileReader();
|
|
250
|
+
reader.onload = () => resolve(reader.result);
|
|
251
|
+
reader.onerror = reject;
|
|
252
|
+
reader.readAsDataURL(file);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
async function importFiles(files, view) {
|
|
256
|
+
const { fs, index } = view.state.facet(CodeblockFacet);
|
|
257
|
+
for (const file of files) {
|
|
258
|
+
const path = file.webkitRelativePath || file.name;
|
|
259
|
+
const dir = path.substring(0, path.lastIndexOf('/'));
|
|
260
|
+
if (dir)
|
|
261
|
+
await fs.mkdir(dir, { recursive: true });
|
|
262
|
+
// Store binary images as data URLs so they can be rendered
|
|
263
|
+
const ext = path.split('.').pop()?.toLowerCase() || '';
|
|
264
|
+
if (BINARY_IMAGE_EXTS.has(ext)) {
|
|
265
|
+
const dataUrl = await fileToDataUrl(file);
|
|
266
|
+
await fs.writeFile(path, dataUrl);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
await fs.writeFile(path, await file.text());
|
|
270
|
+
}
|
|
271
|
+
if (index)
|
|
272
|
+
index.add(path);
|
|
273
|
+
LSP.notifyFileChanged(path, FileChangeType.Created);
|
|
274
|
+
}
|
|
275
|
+
if (index?.savePath)
|
|
276
|
+
await index.save(fs, index.savePath);
|
|
277
|
+
// Open first imported file
|
|
278
|
+
if (files.length > 0) {
|
|
279
|
+
const first = files[0].webkitRelativePath || files[0].name;
|
|
280
|
+
safeDispatch(view, { effects: openFileEffect.of({ path: first }) });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Create an LSP log overlay element
|
|
284
|
+
function createLspLogOverlay() {
|
|
285
|
+
const overlay = document.createElement("div");
|
|
286
|
+
overlay.className = "cm-settings-overlay";
|
|
287
|
+
// Log content
|
|
288
|
+
const content = document.createElement("div");
|
|
289
|
+
content.className = "cm-lsp-log-content";
|
|
290
|
+
overlay.appendChild(content);
|
|
291
|
+
function render() {
|
|
292
|
+
const entries = LspLog.entries();
|
|
293
|
+
const fragment = document.createDocumentFragment();
|
|
294
|
+
for (const entry of entries) {
|
|
295
|
+
const div = document.createElement("div");
|
|
296
|
+
div.className = `cm-lsp-log-entry cm-lsp-log-${entry.level}`;
|
|
297
|
+
const time = new Date(entry.timestamp).toLocaleTimeString();
|
|
298
|
+
div.textContent = `[${time}] [${entry.level}] ${entry.message}`;
|
|
299
|
+
fragment.appendChild(div);
|
|
300
|
+
}
|
|
301
|
+
content.replaceChildren(fragment);
|
|
302
|
+
content.scrollTop = content.scrollHeight;
|
|
303
|
+
}
|
|
304
|
+
render();
|
|
305
|
+
const unsub = LspLog.subscribe(render);
|
|
306
|
+
overlay._lspLogUnsub = unsub;
|
|
307
|
+
return overlay;
|
|
308
|
+
}
|
|
309
|
+
const SPINNER_FADE_MS = 150;
|
|
161
310
|
// Toolbar Panel
|
|
162
311
|
export const toolbarPanel = (view) => {
|
|
163
312
|
let { filepath, language, index } = view.state.facet(CodeblockFacet);
|
|
164
313
|
const dom = document.createElement("div");
|
|
165
314
|
dom.className = "cm-toolbar-panel";
|
|
166
|
-
// Create state icon (left side)
|
|
315
|
+
// Create state icon (left side) — magnifying glass at rest
|
|
167
316
|
const stateIcon = document.createElement("div");
|
|
168
317
|
stateIcon.className = "cm-toolbar-state-icon";
|
|
169
|
-
stateIcon.textContent =
|
|
318
|
+
stateIcon.textContent = SEARCH_ICON;
|
|
170
319
|
// Create container for state icon to help with alignment
|
|
171
320
|
const stateIconContainer = document.createElement("div");
|
|
172
321
|
stateIconContainer.className = "cm-toolbar-state-icon-container";
|
|
@@ -181,23 +330,146 @@ export const toolbarPanel = (view) => {
|
|
|
181
330
|
input.value = filepath || language || "";
|
|
182
331
|
input.className = "cm-toolbar-input";
|
|
183
332
|
inputContainer.appendChild(input);
|
|
333
|
+
// LSP log button (shows file-type icon of current file, hidden when lspLogEnabled is false)
|
|
334
|
+
const lspLogBtn = document.createElement("button");
|
|
335
|
+
lspLogBtn.className = "cm-toolbar-lsp-log";
|
|
336
|
+
lspLogBtn.style.fontFamily = 'var(--cm-icon-font-family)';
|
|
337
|
+
function updateLspLogIcon() {
|
|
338
|
+
const filePath = view.state.field(currentFileField).path;
|
|
339
|
+
if (filePath) {
|
|
340
|
+
const icon = getFileIcon(filePath);
|
|
341
|
+
lspLogBtn.textContent = icon.glyph;
|
|
342
|
+
lspLogBtn.style.color = icon.color || '';
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
lspLogBtn.textContent = DEFAULT_FILE_ICON;
|
|
346
|
+
lspLogBtn.style.color = '';
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function updateLspLogVisibility() {
|
|
350
|
+
const enabled = view.state.field(settingsField).lspLogEnabled;
|
|
351
|
+
lspLogBtn.style.display = enabled ? '' : 'none';
|
|
352
|
+
}
|
|
353
|
+
updateLspLogIcon();
|
|
354
|
+
updateLspLogVisibility();
|
|
355
|
+
// LSP log overlay management (dedicated to LSP log only)
|
|
356
|
+
let lspLogOverlay = null;
|
|
357
|
+
let lspLogSavedInputValue = null;
|
|
358
|
+
function showLspLogOverlay(overlay) {
|
|
359
|
+
const panelsTop = view.dom.querySelector('.cm-panels-top');
|
|
360
|
+
if (panelsTop) {
|
|
361
|
+
overlay.style.top = `${panelsTop.getBoundingClientRect().height}px`;
|
|
362
|
+
}
|
|
363
|
+
view.dom.appendChild(overlay);
|
|
364
|
+
}
|
|
365
|
+
function openLspLogOverlay() {
|
|
366
|
+
lspLogSavedInputValue = input.value;
|
|
367
|
+
input.value = 'lsp.log';
|
|
368
|
+
lspLogOverlay = createLspLogOverlay();
|
|
369
|
+
showLspLogOverlay(lspLogOverlay);
|
|
370
|
+
}
|
|
371
|
+
function closeLspLogOverlay() {
|
|
372
|
+
if (lspLogOverlay) {
|
|
373
|
+
if (lspLogOverlay._lspLogUnsub) {
|
|
374
|
+
lspLogOverlay._lspLogUnsub();
|
|
375
|
+
}
|
|
376
|
+
lspLogOverlay.remove();
|
|
377
|
+
lspLogOverlay = null;
|
|
378
|
+
if (lspLogSavedInputValue !== null) {
|
|
379
|
+
input.value = lspLogSavedInputValue;
|
|
380
|
+
lspLogSavedInputValue = null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
lspLogBtn.addEventListener("click", () => {
|
|
385
|
+
if (lspLogOverlay) {
|
|
386
|
+
closeLspLogOverlay();
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
openLspLogOverlay();
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
dom.appendChild(lspLogBtn);
|
|
184
393
|
const resultsList = document.createElement("ul");
|
|
185
394
|
resultsList.className = "cm-search-results";
|
|
186
395
|
dom.appendChild(resultsList);
|
|
187
396
|
let selectedIndex = 0;
|
|
188
397
|
let namingMode = { active: false, type: 'create-file', originalQuery: '' };
|
|
189
|
-
|
|
398
|
+
let browseMode = { active: false, currentPath: '/', filter: '' };
|
|
399
|
+
let settingsMode = { active: false, filter: '', editing: null };
|
|
400
|
+
let deleteMode = { active: false, filePath: '' };
|
|
401
|
+
let overwriteMode = { active: false, filePath: '', action: 'create-file' };
|
|
402
|
+
// System theme media query listener
|
|
403
|
+
const systemThemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
404
|
+
function handleSystemThemeChange() {
|
|
405
|
+
const settings = view.state.field(settingsField);
|
|
406
|
+
if (settings.theme === 'system') {
|
|
407
|
+
safeDispatch(view, {
|
|
408
|
+
effects: setThemeEffect.of({ dark: systemThemeQuery.matches })
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
systemThemeQuery.addEventListener('change', handleSystemThemeChange);
|
|
413
|
+
// Apply initial settings (font size, font family, theme)
|
|
414
|
+
function applySettings() {
|
|
415
|
+
const settings = view.state.field(settingsField);
|
|
416
|
+
view.dom.style.setProperty('--cm-font-size', `${settings.fontSize}px`);
|
|
417
|
+
if (settings.fontFamily) {
|
|
418
|
+
view.dom.style.setProperty('--cm-font-family', settings.fontFamily);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
view.dom.style.removeProperty('--cm-font-family');
|
|
422
|
+
}
|
|
423
|
+
// Max visible lines: set max-height on the scroller
|
|
424
|
+
const scroller = view.dom.querySelector('.cm-scroller');
|
|
425
|
+
if (scroller) {
|
|
426
|
+
if (settings.maxVisibleLines > 0) {
|
|
427
|
+
const lineHeight = settings.fontSize * 1.5; // approximate
|
|
428
|
+
scroller.style.maxHeight = `${settings.maxVisibleLines * lineHeight}px`;
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
scroller.style.maxHeight = '';
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
applySettings();
|
|
436
|
+
// Apply initial theme and auto-hide
|
|
437
|
+
const initialSettings = view.state.field(settingsField);
|
|
438
|
+
const initialDark = resolveThemeDark(initialSettings.theme);
|
|
439
|
+
view.dom.setAttribute('data-theme', initialDark ? 'dark' : 'light');
|
|
440
|
+
// Auto-hide is enabled after the panel is mounted (needs parent ref).
|
|
441
|
+
// Defer to first update cycle.
|
|
442
|
+
let autoHidePendingInit = initialSettings.autoHideToolbar;
|
|
443
|
+
// Tracks gutter width for toolbar alignment.
|
|
444
|
+
// Sets CSS variables used by icon containers and alignment:
|
|
445
|
+
// --cm-gutter-width: total width of all gutters combined
|
|
446
|
+
// --cm-gutter-lineno-width: width of the line numbers gutter
|
|
447
|
+
// --cm-icon-col-width: character-width-based minimum for icon column
|
|
190
448
|
function updateGutterWidthVariables() {
|
|
449
|
+
// Character width for ch-based sizing (2 columns: icon occupies ~0.5ch advance,
|
|
450
|
+
// but we want the container to be a clean multiple of character width)
|
|
451
|
+
const chWidth = view.defaultCharacterWidth;
|
|
452
|
+
const iconColWidth = Math.ceil(2 * chWidth); // 2 character columns minimum
|
|
453
|
+
view.dom.style.setProperty('--cm-icon-col-width', `${iconColWidth}px`);
|
|
191
454
|
const gutters = view.dom.querySelector('.cm-gutters');
|
|
192
455
|
if (gutters) {
|
|
193
456
|
const gutterWidth = gutters.getBoundingClientRect().width;
|
|
194
|
-
dom.style.setProperty('--cm-gutter-width', `${gutterWidth}px`);
|
|
457
|
+
view.dom.style.setProperty('--cm-gutter-width', `${gutterWidth}px`);
|
|
195
458
|
const numberGutter = gutters.querySelector('.cm-lineNumbers');
|
|
196
459
|
if (numberGutter) {
|
|
197
460
|
const numberGutterWidth = numberGutter.getBoundingClientRect().width;
|
|
198
|
-
dom.style.setProperty('--cm-gutter-lineno-width', `${numberGutterWidth}px`);
|
|
461
|
+
view.dom.style.setProperty('--cm-gutter-lineno-width', `${numberGutterWidth}px`);
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
// Line numbers hidden — icon column sized to character-width multiple
|
|
465
|
+
view.dom.style.setProperty('--cm-gutter-lineno-width', `${iconColWidth}px`);
|
|
199
466
|
}
|
|
200
467
|
}
|
|
468
|
+
else {
|
|
469
|
+
// No gutters at all — icon column sized to character-width multiple
|
|
470
|
+
view.dom.style.setProperty('--cm-gutter-width', `${iconColWidth}px`);
|
|
471
|
+
view.dom.style.setProperty('--cm-gutter-lineno-width', `${iconColWidth}px`);
|
|
472
|
+
}
|
|
201
473
|
}
|
|
202
474
|
// Set up ResizeObserver to watch gutter width changes
|
|
203
475
|
let gutterObserver = null;
|
|
@@ -213,14 +485,466 @@ export const toolbarPanel = (view) => {
|
|
|
213
485
|
// Initial width setup and observer
|
|
214
486
|
updateGutterWidthVariables();
|
|
215
487
|
setupGutterObserver();
|
|
488
|
+
// --- Auto-hide toolbar management ---
|
|
489
|
+
// When enabled, the toolbar retracts (height → 0). It expands when:
|
|
490
|
+
// - the mouse enters the first visible code line (top of scroller), OR
|
|
491
|
+
// - the toolbar input receives focus.
|
|
492
|
+
// It retracts when:
|
|
493
|
+
// - the mouse leaves the toolbar AND the input doesn't have focus.
|
|
494
|
+
let autoHideEnabled = false;
|
|
495
|
+
let panelsTopEl = null;
|
|
496
|
+
function getPanelsTop() {
|
|
497
|
+
if (!panelsTopEl)
|
|
498
|
+
panelsTopEl = dom.parentElement;
|
|
499
|
+
return panelsTopEl;
|
|
500
|
+
}
|
|
501
|
+
function retractToolbar() {
|
|
502
|
+
const pt = getPanelsTop();
|
|
503
|
+
if (pt && autoHideEnabled)
|
|
504
|
+
pt.classList.add('cm-toolbar-retracted');
|
|
505
|
+
}
|
|
506
|
+
function expandToolbar() {
|
|
507
|
+
const pt = getPanelsTop();
|
|
508
|
+
if (pt)
|
|
509
|
+
pt.classList.remove('cm-toolbar-retracted');
|
|
510
|
+
}
|
|
511
|
+
function isToolbarInteractive() {
|
|
512
|
+
return dom.contains(document.activeElement);
|
|
513
|
+
}
|
|
514
|
+
// Single mousemove handler on the editor root checks whether the mouse
|
|
515
|
+
// is inside the toolbar region (panels-top + any overflow like dropdowns)
|
|
516
|
+
// or inside the first code line (trigger zone). This is more reliable than
|
|
517
|
+
// mouseleave on cm-panels-top, which misses absolutely-positioned children.
|
|
518
|
+
function handleEditorMouseMove(e) {
|
|
519
|
+
if (!autoHideEnabled)
|
|
520
|
+
return;
|
|
521
|
+
const pt = getPanelsTop();
|
|
522
|
+
if (!pt)
|
|
523
|
+
return;
|
|
524
|
+
// Check if mouse is inside the toolbar panel or its dropdown
|
|
525
|
+
// (dropdown overflows below cm-panels-top, so check dom directly)
|
|
526
|
+
if (dom.contains(e.target) || pt.contains(e.target)) {
|
|
527
|
+
return; // still in toolbar region — stay expanded
|
|
528
|
+
}
|
|
529
|
+
// Check if mouse is within the first code line (trigger to expand)
|
|
530
|
+
const scroller = view.dom.querySelector('.cm-scroller');
|
|
531
|
+
if (scroller) {
|
|
532
|
+
const scrollRect = scroller.getBoundingClientRect();
|
|
533
|
+
if (e.clientY >= scrollRect.top && e.clientY < scrollRect.top + view.defaultLineHeight) {
|
|
534
|
+
expandToolbar();
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// Mouse is elsewhere in the editor — retract if not interactive
|
|
539
|
+
if (!isToolbarInteractive())
|
|
540
|
+
retractToolbar();
|
|
541
|
+
}
|
|
542
|
+
function handleEditorMouseLeave() {
|
|
543
|
+
if (!autoHideEnabled)
|
|
544
|
+
return;
|
|
545
|
+
if (!isToolbarInteractive())
|
|
546
|
+
retractToolbar();
|
|
547
|
+
}
|
|
548
|
+
function handleInputBlur() {
|
|
549
|
+
if (!autoHideEnabled)
|
|
550
|
+
return;
|
|
551
|
+
// Delay slightly so click-to-focus-another-element events settle
|
|
552
|
+
setTimeout(() => {
|
|
553
|
+
if (autoHideEnabled && !isToolbarInteractive()) {
|
|
554
|
+
retractToolbar();
|
|
555
|
+
}
|
|
556
|
+
}, 100);
|
|
557
|
+
}
|
|
558
|
+
function enableAutoHide() {
|
|
559
|
+
autoHideEnabled = true;
|
|
560
|
+
view.dom.addEventListener('mousemove', handleEditorMouseMove);
|
|
561
|
+
view.dom.addEventListener('mouseleave', handleEditorMouseLeave);
|
|
562
|
+
input.addEventListener('blur', handleInputBlur);
|
|
563
|
+
retractToolbar();
|
|
564
|
+
}
|
|
565
|
+
function disableAutoHide() {
|
|
566
|
+
autoHideEnabled = false;
|
|
567
|
+
view.dom.removeEventListener('mousemove', handleEditorMouseMove);
|
|
568
|
+
view.dom.removeEventListener('mouseleave', handleEditorMouseLeave);
|
|
569
|
+
input.removeEventListener('blur', handleInputBlur);
|
|
570
|
+
expandToolbar();
|
|
571
|
+
}
|
|
572
|
+
// --- Settings mode functions ---
|
|
573
|
+
function buildSettingsEntries(filter) {
|
|
574
|
+
const settings = view.state.field(settingsField);
|
|
575
|
+
const entries = [];
|
|
576
|
+
// Theme: cycle through light/dark/system
|
|
577
|
+
entries.push({
|
|
578
|
+
id: `Theme: ${settings.theme}`,
|
|
579
|
+
settingKey: 'theme',
|
|
580
|
+
type: 'settings-cycle',
|
|
581
|
+
icon: themeIcons[settings.theme],
|
|
582
|
+
currentValue: settings.theme,
|
|
583
|
+
});
|
|
584
|
+
// Font size: input
|
|
585
|
+
entries.push({
|
|
586
|
+
id: `Font size: ${settings.fontSize}px`,
|
|
587
|
+
settingKey: 'fontSize',
|
|
588
|
+
type: 'settings-input',
|
|
589
|
+
icon: 'Aa',
|
|
590
|
+
currentValue: String(settings.fontSize),
|
|
591
|
+
});
|
|
592
|
+
// Font family: cycle
|
|
593
|
+
const fontLabel = fontFamilyLabels[settings.fontFamily] || settings.fontFamily || 'System default';
|
|
594
|
+
entries.push({
|
|
595
|
+
id: `Font family: ${fontLabel}`,
|
|
596
|
+
settingKey: 'fontFamily',
|
|
597
|
+
type: 'settings-cycle',
|
|
598
|
+
icon: 'Aa',
|
|
599
|
+
currentValue: settings.fontFamily,
|
|
600
|
+
});
|
|
601
|
+
// Autosave: toggle
|
|
602
|
+
entries.push({
|
|
603
|
+
id: `Autosave: ${settings.autosave ? 'on' : 'off'}`,
|
|
604
|
+
settingKey: 'autosave',
|
|
605
|
+
type: 'settings-toggle',
|
|
606
|
+
icon: settings.autosave ? '\u2713' : '\u2717', // ✓ or ✗
|
|
607
|
+
currentValue: String(settings.autosave),
|
|
608
|
+
});
|
|
609
|
+
// Line wrap: toggle
|
|
610
|
+
entries.push({
|
|
611
|
+
id: `Line wrap: ${settings.lineWrap ? 'on' : 'off'}`,
|
|
612
|
+
settingKey: 'lineWrap',
|
|
613
|
+
type: 'settings-toggle',
|
|
614
|
+
icon: settings.lineWrap ? '\u2713' : '\u2717',
|
|
615
|
+
currentValue: String(settings.lineWrap),
|
|
616
|
+
});
|
|
617
|
+
// LSP log: toggle
|
|
618
|
+
entries.push({
|
|
619
|
+
id: `LSP log: ${settings.lspLogEnabled ? 'on' : 'off'}`,
|
|
620
|
+
settingKey: 'lspLogEnabled',
|
|
621
|
+
type: 'settings-toggle',
|
|
622
|
+
icon: settings.lspLogEnabled ? '\u2713' : '\u2717',
|
|
623
|
+
currentValue: String(settings.lspLogEnabled),
|
|
624
|
+
});
|
|
625
|
+
// Max visible lines: input
|
|
626
|
+
entries.push({
|
|
627
|
+
id: `Max lines: ${settings.maxVisibleLines || 'unlimited'}`,
|
|
628
|
+
settingKey: 'maxVisibleLines',
|
|
629
|
+
type: 'settings-input',
|
|
630
|
+
icon: '\u2195', // ↕
|
|
631
|
+
currentValue: String(settings.maxVisibleLines || ''),
|
|
632
|
+
});
|
|
633
|
+
// Line numbers: toggle
|
|
634
|
+
entries.push({
|
|
635
|
+
id: `Line numbers: ${settings.showLineNumbers ? 'on' : 'off'}`,
|
|
636
|
+
settingKey: 'showLineNumbers',
|
|
637
|
+
type: 'settings-toggle',
|
|
638
|
+
icon: settings.showLineNumbers ? '\u2713' : '\u2717',
|
|
639
|
+
currentValue: String(settings.showLineNumbers),
|
|
640
|
+
});
|
|
641
|
+
// Fold gutter: toggle
|
|
642
|
+
entries.push({
|
|
643
|
+
id: `Fold gutter: ${settings.showFoldGutter ? 'on' : 'off'}`,
|
|
644
|
+
settingKey: 'showFoldGutter',
|
|
645
|
+
type: 'settings-toggle',
|
|
646
|
+
icon: settings.showFoldGutter ? '\u2713' : '\u2717',
|
|
647
|
+
currentValue: String(settings.showFoldGutter),
|
|
648
|
+
});
|
|
649
|
+
// Auto-hide toolbar: toggle
|
|
650
|
+
entries.push({
|
|
651
|
+
id: `Auto-hide toolbar: ${settings.autoHideToolbar ? 'on' : 'off'}`,
|
|
652
|
+
settingKey: 'autoHideToolbar',
|
|
653
|
+
type: 'settings-toggle',
|
|
654
|
+
icon: settings.autoHideToolbar ? '\u2713' : '\u2717',
|
|
655
|
+
currentValue: String(settings.autoHideToolbar),
|
|
656
|
+
});
|
|
657
|
+
// Filter entries
|
|
658
|
+
if (filter) {
|
|
659
|
+
const lowerFilter = filter.toLowerCase();
|
|
660
|
+
return entries.filter(e => e.id.toLowerCase().includes(lowerFilter));
|
|
661
|
+
}
|
|
662
|
+
return entries;
|
|
663
|
+
}
|
|
664
|
+
function refreshSettingsEntries() {
|
|
665
|
+
if (!settingsMode.active)
|
|
666
|
+
return;
|
|
667
|
+
const entries = buildSettingsEntries(settingsMode.filter);
|
|
668
|
+
selectedIndex = Math.min(selectedIndex, Math.max(0, entries.length - 1));
|
|
669
|
+
safeDispatch(view, { effects: setSearchResults.of(entries) });
|
|
670
|
+
}
|
|
671
|
+
function enterSettingsMode() {
|
|
672
|
+
settingsMode = { active: true, filter: '', editing: null };
|
|
673
|
+
stateIcon.textContent = COG_ICON;
|
|
674
|
+
input.value = 'settings/';
|
|
675
|
+
input.placeholder = '';
|
|
676
|
+
input.focus();
|
|
677
|
+
selectedIndex = 0;
|
|
678
|
+
const entries = buildSettingsEntries('');
|
|
679
|
+
safeDispatch(view, { effects: setSearchResults.of(entries) });
|
|
680
|
+
// Ensure click-outside listener is active
|
|
681
|
+
document.addEventListener("click", handleClickOutside);
|
|
682
|
+
}
|
|
683
|
+
function exitSettingsMode() {
|
|
684
|
+
settingsMode = { active: false, filter: '', editing: null };
|
|
685
|
+
updateStateIcon();
|
|
686
|
+
input.placeholder = '';
|
|
687
|
+
}
|
|
688
|
+
function handleSettingsEntry(entry) {
|
|
689
|
+
const settings = view.state.field(settingsField);
|
|
690
|
+
if (entry.type === 'settings-toggle') {
|
|
691
|
+
const key = entry.settingKey;
|
|
692
|
+
const newValue = !settings[key];
|
|
693
|
+
const effects = [updateSettingsEffect.of({ [key]: newValue })];
|
|
694
|
+
// Special handling for lineWrap: reconfigure compartment
|
|
695
|
+
if (key === 'lineWrap') {
|
|
696
|
+
effects.push(lineWrappingCompartment.reconfigure(newValue ? EditorView.lineWrapping : []));
|
|
697
|
+
}
|
|
698
|
+
// Special handling for showLineNumbers: reconfigure compartment
|
|
699
|
+
if (key === 'showLineNumbers') {
|
|
700
|
+
effects.push(lineNumbersCompartment.reconfigure(newValue ? [lineNumbers(), highlightActiveLineGutter()] : []));
|
|
701
|
+
}
|
|
702
|
+
// Special handling for showFoldGutter: reconfigure compartment
|
|
703
|
+
if (key === 'showFoldGutter') {
|
|
704
|
+
effects.push(foldGutterCompartment.reconfigure(newValue ? [foldGutter()] : []));
|
|
705
|
+
}
|
|
706
|
+
// Special handling for autoHideToolbar: JS-managed retract/expand
|
|
707
|
+
if (key === 'autoHideToolbar') {
|
|
708
|
+
if (newValue)
|
|
709
|
+
enableAutoHide();
|
|
710
|
+
else
|
|
711
|
+
disableAutoHide();
|
|
712
|
+
}
|
|
713
|
+
safeDispatch(view, { effects });
|
|
714
|
+
}
|
|
715
|
+
else if (entry.type === 'settings-cycle') {
|
|
716
|
+
if (entry.settingKey === 'theme') {
|
|
717
|
+
const currentIdx = themeCycleValues.indexOf(settings.theme);
|
|
718
|
+
const nextTheme = themeCycleValues[(currentIdx + 1) % themeCycleValues.length];
|
|
719
|
+
safeDispatch(view, {
|
|
720
|
+
effects: [
|
|
721
|
+
updateSettingsEffect.of({ theme: nextTheme }),
|
|
722
|
+
setThemeEffect.of({ dark: resolveThemeDark(nextTheme) }),
|
|
723
|
+
]
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
else if (entry.settingKey === 'fontFamily') {
|
|
727
|
+
const currentIdx = fontFamilyCycleValues.indexOf(settings.fontFamily);
|
|
728
|
+
const nextIdx = (currentIdx + 1) % fontFamilyCycleValues.length;
|
|
729
|
+
safeDispatch(view, {
|
|
730
|
+
effects: [updateSettingsEffect.of({ fontFamily: fontFamilyCycleValues[nextIdx] })]
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
else if (entry.type === 'settings-input') {
|
|
735
|
+
// Enter editing mode: show the current value in the input for inline editing
|
|
736
|
+
settingsMode.editing = entry.settingKey;
|
|
737
|
+
input.value = entry.currentValue;
|
|
738
|
+
input.select();
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
function confirmSettingsEdit() {
|
|
742
|
+
if (!settingsMode.editing)
|
|
743
|
+
return;
|
|
744
|
+
const key = settingsMode.editing;
|
|
745
|
+
const rawValue = input.value.trim();
|
|
746
|
+
if (key === 'fontSize') {
|
|
747
|
+
const size = Number(rawValue);
|
|
748
|
+
if (!isNaN(size) && size >= 1 && size <= 128) {
|
|
749
|
+
safeDispatch(view, { effects: [updateSettingsEffect.of({ fontSize: size })] });
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
else if (key === 'maxVisibleLines') {
|
|
753
|
+
const lines = rawValue === '' ? 0 : Number(rawValue);
|
|
754
|
+
if (!isNaN(lines) && lines >= 0) {
|
|
755
|
+
safeDispatch(view, { effects: [updateSettingsEffect.of({ maxVisibleLines: Math.floor(lines) })] });
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
settingsMode.editing = null;
|
|
759
|
+
input.value = 'settings/' + settingsMode.filter;
|
|
760
|
+
// Refresh entries after a microtask so the state update has landed
|
|
761
|
+
queueMicrotask(() => refreshSettingsEntries());
|
|
762
|
+
}
|
|
763
|
+
function cancelSettingsEdit() {
|
|
764
|
+
settingsMode.editing = null;
|
|
765
|
+
input.value = 'settings/' + settingsMode.filter;
|
|
766
|
+
}
|
|
767
|
+
// --- Delete mode functions ---
|
|
768
|
+
function enterDeleteMode(filePath) {
|
|
769
|
+
deleteMode = { active: true, filePath };
|
|
770
|
+
stateIcon.textContent = '\u2717'; // ✗
|
|
771
|
+
input.value = '';
|
|
772
|
+
input.placeholder = `Delete "${filePath}"? (Enter to confirm, Esc to cancel)`;
|
|
773
|
+
input.focus();
|
|
774
|
+
safeDispatch(view, { effects: setSearchResults.of([]) });
|
|
775
|
+
}
|
|
776
|
+
function exitDeleteMode() {
|
|
777
|
+
deleteMode = { active: false, filePath: '' };
|
|
778
|
+
updateStateIcon();
|
|
779
|
+
input.placeholder = '';
|
|
780
|
+
}
|
|
781
|
+
async function confirmDelete() {
|
|
782
|
+
if (!deleteMode.active)
|
|
783
|
+
return;
|
|
784
|
+
const path = deleteMode.filePath;
|
|
785
|
+
const { fs, index } = view.state.facet(CodeblockFacet);
|
|
786
|
+
try {
|
|
787
|
+
const currentFile = view.state.field(currentFileField);
|
|
788
|
+
const wasOpen = currentFile.path === path;
|
|
789
|
+
// Delete from VFS
|
|
790
|
+
await fs.unlink(path).catch((e) => console.warn('VFS unlink failed:', e));
|
|
791
|
+
// Remove from search index
|
|
792
|
+
if (index) {
|
|
793
|
+
try {
|
|
794
|
+
index.index.discard(path);
|
|
795
|
+
}
|
|
796
|
+
catch { /* not in index */ }
|
|
797
|
+
if (index.savePath)
|
|
798
|
+
index.save(fs, index.savePath);
|
|
799
|
+
}
|
|
800
|
+
// Notify LSP
|
|
801
|
+
LSP.notifyFileChanged(path, FileChangeType.Deleted);
|
|
802
|
+
exitDeleteMode();
|
|
803
|
+
safeDispatch(view, { effects: setSearchResults.of([]) });
|
|
804
|
+
resetInputToCurrentFile();
|
|
805
|
+
// If the deleted file was currently open, clear the editor
|
|
806
|
+
if (wasOpen) {
|
|
807
|
+
safeDispatch(view, {
|
|
808
|
+
changes: { from: 0, to: view.state.doc.length, insert: '' },
|
|
809
|
+
effects: [
|
|
810
|
+
fileLoadedEffect.of({ path: '', content: '', language: null }),
|
|
811
|
+
]
|
|
812
|
+
});
|
|
813
|
+
input.value = '';
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
catch (e) {
|
|
817
|
+
console.error('Failed to delete file:', e);
|
|
818
|
+
exitDeleteMode();
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
// --- Overwrite confirmation mode ---
|
|
822
|
+
function enterOverwriteMode(filePath, action, oldPath) {
|
|
823
|
+
overwriteMode = { active: true, filePath, action, oldPath };
|
|
824
|
+
stateIcon.textContent = '\u26A0'; // ⚠
|
|
825
|
+
input.value = '';
|
|
826
|
+
input.placeholder = `"${filePath}" exists. Overwrite? (Enter/Esc)`;
|
|
827
|
+
input.focus();
|
|
828
|
+
safeDispatch(view, { effects: setSearchResults.of([]) });
|
|
829
|
+
}
|
|
830
|
+
function exitOverwriteMode() {
|
|
831
|
+
overwriteMode = { active: false, filePath: '', action: 'create-file' };
|
|
832
|
+
updateStateIcon();
|
|
833
|
+
input.placeholder = '';
|
|
834
|
+
}
|
|
835
|
+
async function confirmOverwrite() {
|
|
836
|
+
if (!overwriteMode.active)
|
|
837
|
+
return;
|
|
838
|
+
const { filePath, action, oldPath } = overwriteMode;
|
|
839
|
+
exitOverwriteMode();
|
|
840
|
+
if (action === 'save-as') {
|
|
841
|
+
input.value = filePath;
|
|
842
|
+
await createAndOpenFile(filePath);
|
|
843
|
+
}
|
|
844
|
+
else if (action === 'create-file') {
|
|
845
|
+
input.value = filePath;
|
|
846
|
+
await createBlankFile(filePath);
|
|
847
|
+
}
|
|
848
|
+
else if (action === 'rename' && oldPath) {
|
|
849
|
+
input.value = filePath;
|
|
850
|
+
await performRename(oldPath, filePath);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
/** Check if file exists before executing; enters overwrite mode if it does. */
|
|
854
|
+
async function checkOverwriteAndExecute(path, action, execute, oldPath) {
|
|
855
|
+
const { fs } = view.state.facet(CodeblockFacet);
|
|
856
|
+
const exists = await fs.exists(path);
|
|
857
|
+
if (exists) {
|
|
858
|
+
enterOverwriteMode(path, action, oldPath);
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
await execute();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
/** Create a blank (empty) file in the VFS and open it. */
|
|
865
|
+
async function createBlankFile(pathToOpen) {
|
|
866
|
+
const { fs, index } = view.state.facet(CodeblockFacet);
|
|
867
|
+
const dir = pathToOpen.substring(0, pathToOpen.lastIndexOf('/'));
|
|
868
|
+
if (dir)
|
|
869
|
+
await fs.mkdir(dir, { recursive: true }).catch(() => { });
|
|
870
|
+
await fs.writeFile(pathToOpen, '').catch(console.error);
|
|
871
|
+
if (index) {
|
|
872
|
+
index.add(pathToOpen);
|
|
873
|
+
if (index.savePath)
|
|
874
|
+
index.save(fs, index.savePath);
|
|
875
|
+
}
|
|
876
|
+
safeDispatch(view, {
|
|
877
|
+
effects: [setSearchResults.of([]), openFileEffect.of({ path: pathToOpen })]
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
/** Rename a file: save current content to newPath, delete oldPath. */
|
|
881
|
+
async function performRename(oldPath, newPath) {
|
|
882
|
+
const { fs, index } = view.state.facet(CodeblockFacet);
|
|
883
|
+
const content = view.state.doc.toString();
|
|
884
|
+
// Write content to new path
|
|
885
|
+
const dir = newPath.substring(0, newPath.lastIndexOf('/'));
|
|
886
|
+
if (dir)
|
|
887
|
+
await fs.mkdir(dir, { recursive: true }).catch(() => { });
|
|
888
|
+
await fs.writeFile(newPath, content).catch(console.error);
|
|
889
|
+
// Delete old path
|
|
890
|
+
await fs.unlink(oldPath).catch((e) => console.warn('VFS unlink failed during rename:', e));
|
|
891
|
+
// Update search index
|
|
892
|
+
if (index) {
|
|
893
|
+
try {
|
|
894
|
+
index.index.discard(oldPath);
|
|
895
|
+
}
|
|
896
|
+
catch { /* not in index */ }
|
|
897
|
+
index.add(newPath);
|
|
898
|
+
if (index.savePath)
|
|
899
|
+
index.save(fs, index.savePath);
|
|
900
|
+
}
|
|
901
|
+
// Notify LSP
|
|
902
|
+
LSP.notifyFileChanged(oldPath, FileChangeType.Deleted);
|
|
903
|
+
LSP.notifyFileChanged(newPath, FileChangeType.Created);
|
|
904
|
+
// Open the new file — skipSave prevents handleOpen's save-on-switch
|
|
905
|
+
// from re-creating the old file we just deleted
|
|
906
|
+
safeDispatch(view, {
|
|
907
|
+
effects: [setSearchResults.of([]), openFileEffect.of({ path: newPath, skipSave: true })]
|
|
908
|
+
});
|
|
909
|
+
}
|
|
216
910
|
const renderItem = (result, i) => {
|
|
217
911
|
const li = document.createElement("li");
|
|
218
|
-
|
|
912
|
+
let resultClass = 'cm-file-result';
|
|
913
|
+
if (isSettingsEntry(result))
|
|
914
|
+
resultClass = 'cm-command-result';
|
|
915
|
+
else if (isCommandResult(result))
|
|
916
|
+
resultClass = 'cm-command-result';
|
|
917
|
+
else if (isBrowseEntry(result))
|
|
918
|
+
resultClass = result.type === 'browse-file' ? 'cm-file-result' : 'cm-browse-dir-result';
|
|
919
|
+
li.className = `cm-search-result ${resultClass}`;
|
|
219
920
|
const resultIconContainer = document.createElement("div");
|
|
220
921
|
resultIconContainer.className = "cm-search-result-icon-container";
|
|
221
922
|
const resultIcon = document.createElement("div");
|
|
222
923
|
resultIcon.className = "cm-search-result-icon";
|
|
223
|
-
|
|
924
|
+
if (isSettingsEntry(result)) {
|
|
925
|
+
// Settings entries use emoji or text icons, not nerd fonts
|
|
926
|
+
resultIcon.style.fontFamily = '';
|
|
927
|
+
resultIcon.textContent = result.icon;
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
resultIcon.style.fontFamily = 'var(--cm-icon-font-family)';
|
|
931
|
+
if (isBrowseEntry(result)) {
|
|
932
|
+
resultIcon.textContent = result.icon;
|
|
933
|
+
if (result.iconColor)
|
|
934
|
+
resultIcon.style.color = result.iconColor;
|
|
935
|
+
}
|
|
936
|
+
else if (isCommandResult(result)) {
|
|
937
|
+
resultIcon.textContent = result.icon;
|
|
938
|
+
if (result.iconColor)
|
|
939
|
+
resultIcon.style.color = result.iconColor;
|
|
940
|
+
}
|
|
941
|
+
else {
|
|
942
|
+
const icon = getFileIcon(result.id);
|
|
943
|
+
resultIcon.textContent = icon.glyph;
|
|
944
|
+
if (icon.color)
|
|
945
|
+
resultIcon.style.color = icon.color;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
224
948
|
resultIconContainer.appendChild(resultIcon);
|
|
225
949
|
li.appendChild(resultIconContainer);
|
|
226
950
|
const resultLabel = document.createElement("div");
|
|
@@ -232,30 +956,34 @@ export const toolbarPanel = (view) => {
|
|
|
232
956
|
li.addEventListener("mousedown", (ev) => {
|
|
233
957
|
ev.preventDefault();
|
|
234
958
|
});
|
|
235
|
-
li.addEventListener("click", () =>
|
|
959
|
+
li.addEventListener("click", (ev) => {
|
|
960
|
+
ev.stopPropagation();
|
|
961
|
+
selectResult(result);
|
|
962
|
+
});
|
|
236
963
|
return li;
|
|
237
964
|
};
|
|
238
965
|
function updateDropdown() {
|
|
239
966
|
const results = view.state.field(searchResultsField);
|
|
240
967
|
const children = [];
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
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++;
|
|
968
|
+
// Render items in state array order (search results first, commands second)
|
|
969
|
+
results.forEach((result, i) => {
|
|
970
|
+
children.push(renderItem(result, i));
|
|
254
971
|
});
|
|
255
972
|
resultsList.replaceChildren(...children);
|
|
973
|
+
// Scroll the selected item into view
|
|
974
|
+
const selected = resultsList.querySelector('.selected');
|
|
975
|
+
if (selected) {
|
|
976
|
+
selected.scrollIntoView({ block: 'nearest' });
|
|
977
|
+
}
|
|
256
978
|
}
|
|
257
979
|
function selectResult(result) {
|
|
258
|
-
if (
|
|
980
|
+
if (isSettingsEntry(result)) {
|
|
981
|
+
handleSettingsEntry(result);
|
|
982
|
+
}
|
|
983
|
+
else if (isBrowseEntry(result)) {
|
|
984
|
+
navigateBrowse(result);
|
|
985
|
+
}
|
|
986
|
+
else if (isCommandResult(result)) {
|
|
259
987
|
handleCommandResult(result);
|
|
260
988
|
}
|
|
261
989
|
else {
|
|
@@ -264,10 +992,13 @@ export const toolbarPanel = (view) => {
|
|
|
264
992
|
}
|
|
265
993
|
function updateStateIcon() {
|
|
266
994
|
if (namingMode.active) {
|
|
267
|
-
stateIcon.textContent = namingMode.type === 'create-file'
|
|
995
|
+
stateIcon.textContent = (namingMode.type === 'create-file' || namingMode.type === 'save-as') ? DEFAULT_FILE_ICON : '\uf044';
|
|
996
|
+
}
|
|
997
|
+
else if (settingsMode.active) {
|
|
998
|
+
stateIcon.textContent = COG_ICON;
|
|
268
999
|
}
|
|
269
1000
|
else {
|
|
270
|
-
stateIcon.textContent =
|
|
1001
|
+
stateIcon.textContent = SEARCH_ICON;
|
|
271
1002
|
}
|
|
272
1003
|
}
|
|
273
1004
|
function enterNamingMode(type, originalQuery, languageExtension) {
|
|
@@ -286,34 +1017,175 @@ export const toolbarPanel = (view) => {
|
|
|
286
1017
|
updateStateIcon();
|
|
287
1018
|
input.placeholder = '';
|
|
288
1019
|
}
|
|
1020
|
+
// --- Browse mode functions ---
|
|
1021
|
+
async function enterBrowseMode(startPath) {
|
|
1022
|
+
const { cwd } = view.state.facet(CodeblockFacet);
|
|
1023
|
+
const browsePath = startPath || cwd || '/';
|
|
1024
|
+
browseMode = { active: true, currentPath: browsePath, filter: '' };
|
|
1025
|
+
// Update state icon to folder
|
|
1026
|
+
stateIcon.textContent = FOLDER_OPEN_ICON;
|
|
1027
|
+
input.value = browsePath.endsWith('/') ? browsePath : browsePath + '/';
|
|
1028
|
+
input.placeholder = '';
|
|
1029
|
+
input.focus();
|
|
1030
|
+
await refreshBrowseEntries();
|
|
1031
|
+
// Ensure click-outside listener is active
|
|
1032
|
+
document.addEventListener("click", handleClickOutside);
|
|
1033
|
+
}
|
|
1034
|
+
async function refreshBrowseEntries() {
|
|
1035
|
+
if (!browseMode.active)
|
|
1036
|
+
return;
|
|
1037
|
+
const { fs } = view.state.facet(CodeblockFacet);
|
|
1038
|
+
const dir = browseMode.currentPath;
|
|
1039
|
+
try {
|
|
1040
|
+
const entries = await fs.readDir(dir);
|
|
1041
|
+
const browseResults = [];
|
|
1042
|
+
// Add parent directory entry if not at root
|
|
1043
|
+
if (dir !== '/' && dir !== '') {
|
|
1044
|
+
const parentPath = dir.split('/').slice(0, -1).join('/') || '/';
|
|
1045
|
+
browseResults.push({
|
|
1046
|
+
id: '..',
|
|
1047
|
+
type: 'browse-parent',
|
|
1048
|
+
icon: PARENT_DIR_ICON,
|
|
1049
|
+
fullPath: parentPath,
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
// Separate directories and files, sort each alphabetically
|
|
1053
|
+
const dirs = [];
|
|
1054
|
+
const files = [];
|
|
1055
|
+
for (const [name, fileType] of entries) {
|
|
1056
|
+
// Skip hidden/internal files
|
|
1057
|
+
if (name.startsWith('.'))
|
|
1058
|
+
continue;
|
|
1059
|
+
const fullPath = dir === '/' ? `${name}` : `${dir}/${name}`;
|
|
1060
|
+
// FileType.Directory = 2
|
|
1061
|
+
if (fileType === 2) {
|
|
1062
|
+
dirs.push({
|
|
1063
|
+
id: name + '/',
|
|
1064
|
+
type: 'browse-directory',
|
|
1065
|
+
icon: FOLDER_ICON,
|
|
1066
|
+
fullPath,
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
else {
|
|
1070
|
+
const icon = getFileIcon(name);
|
|
1071
|
+
files.push({
|
|
1072
|
+
id: name,
|
|
1073
|
+
type: 'browse-file',
|
|
1074
|
+
icon: icon.glyph,
|
|
1075
|
+
iconColor: icon.color,
|
|
1076
|
+
fullPath,
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
dirs.sort((a, b) => a.id.localeCompare(b.id));
|
|
1081
|
+
files.sort((a, b) => a.id.localeCompare(b.id));
|
|
1082
|
+
// Apply filter
|
|
1083
|
+
const filter = browseMode.filter.toLowerCase();
|
|
1084
|
+
const filtered = [...browseResults, ...dirs, ...files].filter(entry => entry.type === 'browse-parent' || entry.id.toLowerCase().includes(filter));
|
|
1085
|
+
selectedIndex = 0;
|
|
1086
|
+
safeDispatch(view, { effects: setSearchResults.of(filtered) });
|
|
1087
|
+
}
|
|
1088
|
+
catch (e) {
|
|
1089
|
+
console.error('Failed to read directory:', e);
|
|
1090
|
+
exitBrowseMode();
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
async function navigateBrowse(entry) {
|
|
1094
|
+
if (entry.type === 'browse-file') {
|
|
1095
|
+
// Open the file and exit browse mode
|
|
1096
|
+
const path = entry.fullPath;
|
|
1097
|
+
exitBrowseMode();
|
|
1098
|
+
input.value = path;
|
|
1099
|
+
safeDispatch(view, {
|
|
1100
|
+
effects: [setSearchResults.of([]), openFileEffect.of({ path })]
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
else {
|
|
1104
|
+
// Navigate into directory (or parent)
|
|
1105
|
+
browseMode.currentPath = entry.fullPath;
|
|
1106
|
+
browseMode.filter = '';
|
|
1107
|
+
const displayPath = entry.fullPath === '/' ? '/' : entry.fullPath + '/';
|
|
1108
|
+
input.value = displayPath;
|
|
1109
|
+
selectedIndex = 0;
|
|
1110
|
+
await refreshBrowseEntries();
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
function exitBrowseMode() {
|
|
1114
|
+
browseMode = { active: false, currentPath: '/', filter: '' };
|
|
1115
|
+
updateStateIcon();
|
|
1116
|
+
input.placeholder = '';
|
|
1117
|
+
}
|
|
1118
|
+
function triggerFileImport(folder) {
|
|
1119
|
+
safeDispatch(view, { effects: setSearchResults.of([]) });
|
|
1120
|
+
const fileInput = document.createElement('input');
|
|
1121
|
+
fileInput.type = 'file';
|
|
1122
|
+
if (folder) {
|
|
1123
|
+
fileInput.setAttribute('webkitdirectory', '');
|
|
1124
|
+
}
|
|
1125
|
+
else {
|
|
1126
|
+
fileInput.multiple = true;
|
|
1127
|
+
}
|
|
1128
|
+
fileInput.addEventListener('change', () => {
|
|
1129
|
+
if (fileInput.files?.length) {
|
|
1130
|
+
importFiles(fileInput.files, view);
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
fileInput.click();
|
|
1134
|
+
}
|
|
289
1135
|
function handleCommandResult(command) {
|
|
290
|
-
if (command.type === '
|
|
1136
|
+
if (command.type === 'settings') {
|
|
1137
|
+
enterSettingsMode();
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
else if (command.type === 'open-file') {
|
|
1141
|
+
enterBrowseMode();
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
else if (command.type === 'open-terminal') {
|
|
1145
|
+
safeDispatch(view, { effects: setSearchResults.of([]) });
|
|
1146
|
+
import('./terminal').then(({ openTerminal }) => openTerminal(view));
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
else if (command.type === 'save-as') {
|
|
291
1150
|
if (command.requiresInput) {
|
|
292
|
-
|
|
293
|
-
enterNamingMode('
|
|
1151
|
+
const ext = command.query ? languageToFileExtension(command.query) : undefined;
|
|
1152
|
+
enterNamingMode('save-as', command.query, ext);
|
|
294
1153
|
}
|
|
295
1154
|
else {
|
|
296
|
-
// Create file directly and populate toolbar
|
|
297
1155
|
const pathToOpen = command.query.includes('.') ? command.query : `${command.query}.txt`;
|
|
298
1156
|
input.value = pathToOpen;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
1157
|
+
checkOverwriteAndExecute(pathToOpen, 'save-as', () => createAndOpenFile(pathToOpen));
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
else if (command.type === 'create-file') {
|
|
1161
|
+
if (command.requiresInput) {
|
|
1162
|
+
const ext = command.query ? languageToFileExtension(command.query) : undefined;
|
|
1163
|
+
enterNamingMode('create-file', command.query, ext);
|
|
1164
|
+
}
|
|
1165
|
+
else {
|
|
1166
|
+
const pathToOpen = command.query.includes('.') ? command.query : `${command.query}.txt`;
|
|
1167
|
+
input.value = pathToOpen;
|
|
1168
|
+
checkOverwriteAndExecute(pathToOpen, 'create-file', () => createBlankFile(pathToOpen));
|
|
302
1169
|
}
|
|
303
1170
|
}
|
|
304
1171
|
else if (command.type === 'rename-file') {
|
|
305
|
-
// Rename file directly since the new name is provided by the query
|
|
306
1172
|
const currentFile = view.state.field(currentFileField);
|
|
307
1173
|
if (currentFile.path) {
|
|
308
1174
|
const newPath = command.query.includes('.') ? command.query : `${command.query}.txt`;
|
|
309
1175
|
input.value = newPath;
|
|
310
|
-
|
|
311
|
-
console.log(`Rename ${currentFile.path} to ${newPath}`);
|
|
312
|
-
safeDispatch(view, {
|
|
313
|
-
effects: [setSearchResults.of([]), openFileEffect.of({ path: newPath })]
|
|
314
|
-
});
|
|
1176
|
+
checkOverwriteAndExecute(newPath, 'rename', () => performRename(currentFile.path, newPath), currentFile.path);
|
|
315
1177
|
}
|
|
316
1178
|
}
|
|
1179
|
+
else if (command.type === 'import-local-files') {
|
|
1180
|
+
triggerFileImport(false);
|
|
1181
|
+
}
|
|
1182
|
+
else if (command.type === 'import-local-folder') {
|
|
1183
|
+
triggerFileImport(true);
|
|
1184
|
+
}
|
|
1185
|
+
else if (command.type === 'file-action' && command.action) {
|
|
1186
|
+
safeDispatch(view, { effects: setSearchResults.of([]) });
|
|
1187
|
+
command.action(view);
|
|
1188
|
+
}
|
|
317
1189
|
}
|
|
318
1190
|
function handleSearchResult(result) {
|
|
319
1191
|
input.value = result.id;
|
|
@@ -321,76 +1193,174 @@ export const toolbarPanel = (view) => {
|
|
|
321
1193
|
effects: [setSearchResults.of([]), openFileEffect.of({ path: result.id })]
|
|
322
1194
|
});
|
|
323
1195
|
}
|
|
1196
|
+
/** Save the current editor content to a new file path in the VFS, then open it. */
|
|
1197
|
+
async function createAndOpenFile(pathToOpen) {
|
|
1198
|
+
const { fs } = view.state.facet(CodeblockFacet);
|
|
1199
|
+
const content = view.state.doc.toString();
|
|
1200
|
+
const dir = pathToOpen.substring(0, pathToOpen.lastIndexOf('/'));
|
|
1201
|
+
if (dir)
|
|
1202
|
+
await fs.mkdir(dir, { recursive: true }).catch(() => { });
|
|
1203
|
+
await fs.writeFile(pathToOpen, content).catch(console.error);
|
|
1204
|
+
// Add to search index
|
|
1205
|
+
const { index } = view.state.facet(CodeblockFacet);
|
|
1206
|
+
if (index) {
|
|
1207
|
+
index.add(pathToOpen);
|
|
1208
|
+
if (index.savePath)
|
|
1209
|
+
index.save(fs, index.savePath);
|
|
1210
|
+
}
|
|
1211
|
+
safeDispatch(view, {
|
|
1212
|
+
effects: [setSearchResults.of([]), openFileEffect.of({ path: pathToOpen })]
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
324
1215
|
function executeNamingMode(filename) {
|
|
325
1216
|
if (!namingMode.active || !filename.trim())
|
|
326
1217
|
return;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
1218
|
+
const resolvePath = (fn) => namingMode.languageExtension && !fn.includes('.')
|
|
1219
|
+
? `${fn}.${namingMode.languageExtension}`
|
|
1220
|
+
: fn;
|
|
1221
|
+
if (namingMode.type === 'save-as') {
|
|
1222
|
+
const pathToOpen = resolvePath(filename);
|
|
331
1223
|
input.value = pathToOpen;
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
1224
|
+
exitNamingMode();
|
|
1225
|
+
checkOverwriteAndExecute(pathToOpen, 'save-as', () => createAndOpenFile(pathToOpen));
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
else if (namingMode.type === 'create-file') {
|
|
1229
|
+
const pathToOpen = resolvePath(filename);
|
|
1230
|
+
input.value = pathToOpen;
|
|
1231
|
+
exitNamingMode();
|
|
1232
|
+
checkOverwriteAndExecute(pathToOpen, 'create-file', () => createBlankFile(pathToOpen));
|
|
1233
|
+
return;
|
|
336
1234
|
}
|
|
337
1235
|
else if (namingMode.type === 'rename-file') {
|
|
338
1236
|
const currentFile = view.state.field(currentFileField);
|
|
339
1237
|
if (currentFile.path) {
|
|
340
1238
|
const newPath = filename.includes('.') ? filename : `${filename}.txt`;
|
|
341
1239
|
input.value = newPath;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
effects: [setSearchResults.of([]), openFileEffect.of({ path: newPath })]
|
|
346
|
-
});
|
|
1240
|
+
exitNamingMode();
|
|
1241
|
+
checkOverwriteAndExecute(newPath, 'rename', () => performRename(currentFile.path, newPath), currentFile.path);
|
|
1242
|
+
return;
|
|
347
1243
|
}
|
|
348
1244
|
}
|
|
349
1245
|
exitNamingMode();
|
|
350
1246
|
}
|
|
1247
|
+
function resetInputToCurrentFile() {
|
|
1248
|
+
const currentFile = view.state.field(currentFileField);
|
|
1249
|
+
const cfg = view.state.facet(CodeblockFacet);
|
|
1250
|
+
input.value = currentFile.path || cfg.language || '';
|
|
1251
|
+
}
|
|
351
1252
|
// Close dropdown when clicking outside
|
|
352
1253
|
function handleClickOutside(event) {
|
|
353
1254
|
if (!dom.contains(event.target)) {
|
|
1255
|
+
if (settingsMode.active)
|
|
1256
|
+
exitSettingsMode();
|
|
1257
|
+
if (browseMode.active)
|
|
1258
|
+
exitBrowseMode();
|
|
354
1259
|
safeDispatch(view, { effects: setSearchResults.of([]) });
|
|
1260
|
+
resetInputToCurrentFile();
|
|
355
1261
|
}
|
|
356
1262
|
}
|
|
357
1263
|
input.addEventListener("click", () => {
|
|
1264
|
+
// Don't interfere when in a special mode
|
|
1265
|
+
if (namingMode.active || settingsMode.active || browseMode.active) {
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
358
1268
|
// Open dropdown when input is clicked
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
safeDispatch(view, { effects: setSearchResults.of(results) });
|
|
370
|
-
// Add click-outside listener when dropdown opens
|
|
371
|
-
document.addEventListener("click", handleClickOutside);
|
|
1269
|
+
const query = input.value;
|
|
1270
|
+
let results = [];
|
|
1271
|
+
if (query.trim()) {
|
|
1272
|
+
// Get regular search results from index first
|
|
1273
|
+
const searchResults = (index?.search(query) || []).slice(0, 100);
|
|
1274
|
+
// Add command results (passing search results to check for existing files)
|
|
1275
|
+
const commands = createCommandResults(query, view, searchResults);
|
|
1276
|
+
// Search results first, then commands
|
|
1277
|
+
results = searchResults.concat(commands);
|
|
372
1278
|
}
|
|
1279
|
+
else {
|
|
1280
|
+
// Show import commands when dropdown opens with empty query
|
|
1281
|
+
results = createCommandResults('', view, []);
|
|
1282
|
+
}
|
|
1283
|
+
safeDispatch(view, { effects: setSearchResults.of(results) });
|
|
1284
|
+
// Add click-outside listener when dropdown opens
|
|
1285
|
+
document.addEventListener("click", handleClickOutside);
|
|
373
1286
|
});
|
|
374
1287
|
input.addEventListener("input", (event) => {
|
|
375
1288
|
const query = event.target.value;
|
|
376
1289
|
selectedIndex = 0;
|
|
1290
|
+
// Block input during delete/overwrite confirmation
|
|
1291
|
+
if (deleteMode.active || overwriteMode.active) {
|
|
1292
|
+
input.value = '';
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
377
1295
|
// If in naming mode, don't show search results
|
|
378
1296
|
if (namingMode.active) {
|
|
379
1297
|
return;
|
|
380
1298
|
}
|
|
1299
|
+
// If editing a settings value, don't interfere
|
|
1300
|
+
if (settingsMode.active && settingsMode.editing) {
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
// If in settings mode, filter the settings entries
|
|
1304
|
+
if (settingsMode.active) {
|
|
1305
|
+
const prefix = 'settings/';
|
|
1306
|
+
settingsMode.filter = query.startsWith(prefix) ? query.slice(prefix.length) : query;
|
|
1307
|
+
refreshSettingsEntries();
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
// If in browse mode, filter the directory entries
|
|
1311
|
+
if (browseMode.active) {
|
|
1312
|
+
// Extract filter text after the directory path prefix
|
|
1313
|
+
const prefix = browseMode.currentPath === '/' ? '/' : browseMode.currentPath + '/';
|
|
1314
|
+
browseMode.filter = query.startsWith(prefix) ? query.slice(prefix.length) : query;
|
|
1315
|
+
refreshBrowseEntries();
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
381
1318
|
let results = [];
|
|
382
1319
|
if (query.trim()) {
|
|
383
1320
|
// Get regular search results from index first
|
|
384
1321
|
const searchResults = (index?.search(query) || []).slice(0, 1000);
|
|
385
|
-
// Add command results
|
|
1322
|
+
// Add command results (passing search results to check for existing files)
|
|
386
1323
|
const commands = createCommandResults(query, view, searchResults);
|
|
387
|
-
results
|
|
388
|
-
// Add search results
|
|
1324
|
+
// Search results first, then commands
|
|
389
1325
|
results.push(...searchResults);
|
|
1326
|
+
results.push(...commands);
|
|
1327
|
+
}
|
|
1328
|
+
else {
|
|
1329
|
+
// Show import commands even with empty query
|
|
1330
|
+
results = createCommandResults('', view, []);
|
|
390
1331
|
}
|
|
391
1332
|
safeDispatch(view, { effects: setSearchResults.of(results) });
|
|
392
1333
|
});
|
|
393
1334
|
input.addEventListener("keydown", (event) => {
|
|
1335
|
+
// Overwrite confirmation mode
|
|
1336
|
+
if (overwriteMode.active) {
|
|
1337
|
+
if (event.key === "Enter") {
|
|
1338
|
+
event.preventDefault();
|
|
1339
|
+
confirmOverwrite();
|
|
1340
|
+
}
|
|
1341
|
+
else if (event.key === "Escape") {
|
|
1342
|
+
event.preventDefault();
|
|
1343
|
+
exitOverwriteMode();
|
|
1344
|
+
resetInputToCurrentFile();
|
|
1345
|
+
}
|
|
1346
|
+
event.preventDefault();
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
// Delete confirmation mode
|
|
1350
|
+
if (deleteMode.active) {
|
|
1351
|
+
if (event.key === "Enter") {
|
|
1352
|
+
event.preventDefault();
|
|
1353
|
+
confirmDelete();
|
|
1354
|
+
}
|
|
1355
|
+
else if (event.key === "Escape") {
|
|
1356
|
+
event.preventDefault();
|
|
1357
|
+
exitDeleteMode();
|
|
1358
|
+
resetInputToCurrentFile();
|
|
1359
|
+
}
|
|
1360
|
+
// Block all other keys in delete mode
|
|
1361
|
+
event.preventDefault();
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
394
1364
|
if (namingMode.active) {
|
|
395
1365
|
// Handle naming mode
|
|
396
1366
|
if (event.key === "Enter") {
|
|
@@ -404,6 +1374,107 @@ export const toolbarPanel = (view) => {
|
|
|
404
1374
|
}
|
|
405
1375
|
return;
|
|
406
1376
|
}
|
|
1377
|
+
// Settings mode keyboard handling
|
|
1378
|
+
if (settingsMode.active) {
|
|
1379
|
+
const results = view.state.field(searchResultsField);
|
|
1380
|
+
// If currently editing a settings value
|
|
1381
|
+
if (settingsMode.editing) {
|
|
1382
|
+
if (event.key === "Enter") {
|
|
1383
|
+
event.preventDefault();
|
|
1384
|
+
confirmSettingsEdit();
|
|
1385
|
+
}
|
|
1386
|
+
else if (event.key === "Escape") {
|
|
1387
|
+
event.preventDefault();
|
|
1388
|
+
cancelSettingsEdit();
|
|
1389
|
+
}
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
if (event.key === "ArrowDown") {
|
|
1393
|
+
event.preventDefault();
|
|
1394
|
+
if (results.length) {
|
|
1395
|
+
selectedIndex = mod(selectedIndex + 1, results.length);
|
|
1396
|
+
updateDropdown();
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
else if (event.key === "ArrowUp") {
|
|
1400
|
+
event.preventDefault();
|
|
1401
|
+
if (results.length) {
|
|
1402
|
+
selectedIndex = mod(selectedIndex - 1, results.length);
|
|
1403
|
+
updateDropdown();
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
else if (event.key === "Enter" && results.length && selectedIndex >= 0) {
|
|
1407
|
+
event.preventDefault();
|
|
1408
|
+
selectResult(results[selectedIndex]);
|
|
1409
|
+
}
|
|
1410
|
+
else if (event.key === "Backspace") {
|
|
1411
|
+
// If filter is empty and not editing, exit settings mode
|
|
1412
|
+
if (settingsMode.filter === '') {
|
|
1413
|
+
event.preventDefault();
|
|
1414
|
+
exitSettingsMode();
|
|
1415
|
+
safeDispatch(view, { effects: setSearchResults.of([]) });
|
|
1416
|
+
resetInputToCurrentFile();
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
else if (event.key === "Escape") {
|
|
1420
|
+
event.preventDefault();
|
|
1421
|
+
exitSettingsMode();
|
|
1422
|
+
safeDispatch(view, { effects: setSearchResults.of([]) });
|
|
1423
|
+
resetInputToCurrentFile();
|
|
1424
|
+
input.blur();
|
|
1425
|
+
}
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
// Browse mode keyboard handling
|
|
1429
|
+
if (browseMode.active) {
|
|
1430
|
+
const results = view.state.field(searchResultsField);
|
|
1431
|
+
if (event.key === "ArrowDown") {
|
|
1432
|
+
event.preventDefault();
|
|
1433
|
+
if (results.length) {
|
|
1434
|
+
selectedIndex = mod(selectedIndex + 1, results.length);
|
|
1435
|
+
updateDropdown();
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
else if (event.key === "ArrowUp") {
|
|
1439
|
+
event.preventDefault();
|
|
1440
|
+
if (results.length) {
|
|
1441
|
+
selectedIndex = mod(selectedIndex - 1, results.length);
|
|
1442
|
+
updateDropdown();
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
else if (event.key === "Enter" && results.length && selectedIndex >= 0) {
|
|
1446
|
+
event.preventDefault();
|
|
1447
|
+
selectResult(results[selectedIndex]);
|
|
1448
|
+
}
|
|
1449
|
+
else if (event.key === "Backspace") {
|
|
1450
|
+
// If filter is empty and backspace pressed, go up a directory
|
|
1451
|
+
if (browseMode.filter === '' && browseMode.currentPath !== '/') {
|
|
1452
|
+
event.preventDefault();
|
|
1453
|
+
const parentPath = browseMode.currentPath.split('/').slice(0, -1).join('/') || '/';
|
|
1454
|
+
browseMode.currentPath = parentPath;
|
|
1455
|
+
const displayPath = parentPath === '/' ? '/' : parentPath + '/';
|
|
1456
|
+
input.value = displayPath;
|
|
1457
|
+
refreshBrowseEntries();
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
else if (event.key === "Delete" && results.length && selectedIndex >= 0) {
|
|
1461
|
+
// Delete a file from the browse dropdown
|
|
1462
|
+
const result = results[selectedIndex];
|
|
1463
|
+
if (isBrowseEntry(result) && result.type === 'browse-file') {
|
|
1464
|
+
event.preventDefault();
|
|
1465
|
+
exitBrowseMode();
|
|
1466
|
+
enterDeleteMode(result.fullPath);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
else if (event.key === "Escape") {
|
|
1470
|
+
event.preventDefault();
|
|
1471
|
+
exitBrowseMode();
|
|
1472
|
+
safeDispatch(view, { effects: setSearchResults.of([]) });
|
|
1473
|
+
resetInputToCurrentFile();
|
|
1474
|
+
input.blur();
|
|
1475
|
+
}
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
407
1478
|
// Normal search mode
|
|
408
1479
|
const results = view.state.field(searchResultsField);
|
|
409
1480
|
if (event.key === "ArrowDown") {
|
|
@@ -412,6 +1483,10 @@ export const toolbarPanel = (view) => {
|
|
|
412
1483
|
selectedIndex = mod(selectedIndex + 1, results.length);
|
|
413
1484
|
updateDropdown();
|
|
414
1485
|
}
|
|
1486
|
+
else {
|
|
1487
|
+
// No dropdown open — move cursor to editor body
|
|
1488
|
+
view.focus();
|
|
1489
|
+
}
|
|
415
1490
|
}
|
|
416
1491
|
else if (event.key === "ArrowUp") {
|
|
417
1492
|
event.preventDefault();
|
|
@@ -424,6 +1499,20 @@ export const toolbarPanel = (view) => {
|
|
|
424
1499
|
event.preventDefault();
|
|
425
1500
|
selectResult(results[selectedIndex]);
|
|
426
1501
|
}
|
|
1502
|
+
else if (event.key === "Delete" && results.length && selectedIndex >= 0) {
|
|
1503
|
+
// Delete key on a highlighted file result → enter delete confirmation
|
|
1504
|
+
const result = results[selectedIndex];
|
|
1505
|
+
if (!isCommandResult(result) && !isBrowseEntry(result) && !isSettingsEntry(result)) {
|
|
1506
|
+
event.preventDefault();
|
|
1507
|
+
enterDeleteMode(result.id);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
else if (event.key === "Escape") {
|
|
1511
|
+
event.preventDefault();
|
|
1512
|
+
safeDispatch(view, { effects: setSearchResults.of([]) });
|
|
1513
|
+
resetInputToCurrentFile();
|
|
1514
|
+
input.blur();
|
|
1515
|
+
}
|
|
427
1516
|
});
|
|
428
1517
|
return {
|
|
429
1518
|
dom,
|
|
@@ -439,10 +1528,93 @@ export const toolbarPanel = (view) => {
|
|
|
439
1528
|
document.removeEventListener("click", handleClickOutside);
|
|
440
1529
|
}
|
|
441
1530
|
}
|
|
1531
|
+
// Apply settings when they change
|
|
1532
|
+
const prevSettings = update.startState.field(settingsField);
|
|
1533
|
+
const nextSettings = update.state.field(settingsField);
|
|
1534
|
+
if (prevSettings.fontSize !== nextSettings.fontSize || prevSettings.fontFamily !== nextSettings.fontFamily || prevSettings.maxVisibleLines !== nextSettings.maxVisibleLines) {
|
|
1535
|
+
applySettings();
|
|
1536
|
+
}
|
|
1537
|
+
if (prevSettings.lspLogEnabled !== nextSettings.lspLogEnabled) {
|
|
1538
|
+
updateLspLogVisibility();
|
|
1539
|
+
// Close the log overlay if the user disables it
|
|
1540
|
+
if (!nextSettings.lspLogEnabled && lspLogOverlay) {
|
|
1541
|
+
closeLspLogOverlay();
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
// Sync autoHideToolbar via JS event handlers
|
|
1545
|
+
if (prevSettings.autoHideToolbar !== nextSettings.autoHideToolbar) {
|
|
1546
|
+
if (nextSettings.autoHideToolbar)
|
|
1547
|
+
enableAutoHide();
|
|
1548
|
+
else
|
|
1549
|
+
disableAutoHide();
|
|
1550
|
+
}
|
|
1551
|
+
// Deferred auto-hide init (needs parent element to be mounted)
|
|
1552
|
+
if (autoHidePendingInit && getPanelsTop()) {
|
|
1553
|
+
autoHidePendingInit = false;
|
|
1554
|
+
enableAutoHide();
|
|
1555
|
+
}
|
|
1556
|
+
// Refresh gutter width variables when gutter-related settings change
|
|
1557
|
+
if (prevSettings.showLineNumbers !== nextSettings.showLineNumbers || prevSettings.showFoldGutter !== nextSettings.showFoldGutter) {
|
|
1558
|
+
queueMicrotask(() => updateGutterWidthVariables());
|
|
1559
|
+
}
|
|
1560
|
+
// Refresh settings dropdown when settings change and settings mode is active
|
|
1561
|
+
if (settingsMode.active && prevSettings !== nextSettings) {
|
|
1562
|
+
refreshSettingsEntries();
|
|
1563
|
+
}
|
|
1564
|
+
// Loading spinner — separate element so the container keeps
|
|
1565
|
+
// tracking gutter width while the spinner has fixed dimensions.
|
|
1566
|
+
const prevFile = update.startState.field(currentFileField);
|
|
1567
|
+
const nextFile = update.state.field(currentFileField);
|
|
1568
|
+
if (prevFile.loading !== nextFile.loading) {
|
|
1569
|
+
if (nextFile.loading) {
|
|
1570
|
+
// Immediately swap icon → spinner (no delay to avoid race conditions)
|
|
1571
|
+
stateIcon.style.display = 'none';
|
|
1572
|
+
stateIcon.style.opacity = '0';
|
|
1573
|
+
if (!stateIconContainer.querySelector('.cm-loading')) {
|
|
1574
|
+
const spinner = document.createElement('div');
|
|
1575
|
+
spinner.className = 'cm-loading';
|
|
1576
|
+
stateIconContainer.appendChild(spinner);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
else {
|
|
1580
|
+
// Fade spinner out, then crossfade icon in
|
|
1581
|
+
const spinner = stateIconContainer.querySelector('.cm-loading');
|
|
1582
|
+
if (spinner) {
|
|
1583
|
+
spinner.style.opacity = '0';
|
|
1584
|
+
setTimeout(() => {
|
|
1585
|
+
spinner.remove();
|
|
1586
|
+
if (!view.state.field(currentFileField).loading) {
|
|
1587
|
+
stateIcon.style.display = '';
|
|
1588
|
+
// Force reflow so the browser sees opacity:0 before transitioning to 1
|
|
1589
|
+
stateIcon.offsetHeight;
|
|
1590
|
+
stateIcon.style.opacity = '1';
|
|
1591
|
+
}
|
|
1592
|
+
}, SPINNER_FADE_MS);
|
|
1593
|
+
}
|
|
1594
|
+
else {
|
|
1595
|
+
stateIcon.style.display = '';
|
|
1596
|
+
stateIcon.style.opacity = '1';
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
// Update LSP log icon when file changes
|
|
1601
|
+
if (prevFile.path !== nextFile.path) {
|
|
1602
|
+
updateLspLogIcon();
|
|
1603
|
+
}
|
|
1604
|
+
// Sync input value when file path changes (unless overlay/mode is active or user is naming)
|
|
1605
|
+
if (prevFile.path !== nextFile.path && !namingMode.active && !lspLogOverlay && !settingsMode.active) {
|
|
1606
|
+
input.value = nextFile.path || '';
|
|
1607
|
+
}
|
|
442
1608
|
},
|
|
443
1609
|
destroy() {
|
|
444
1610
|
// Clean up event listeners when panel is destroyed
|
|
445
1611
|
document.removeEventListener("click", handleClickOutside);
|
|
1612
|
+
systemThemeQuery.removeEventListener('change', handleSystemThemeChange);
|
|
1613
|
+
// Clean up auto-hide
|
|
1614
|
+
if (autoHideEnabled)
|
|
1615
|
+
disableAutoHide();
|
|
1616
|
+
// Clean up LSP log overlay
|
|
1617
|
+
closeLspLogOverlay();
|
|
446
1618
|
// Clean up ResizeObserver
|
|
447
1619
|
if (gutterObserver) {
|
|
448
1620
|
gutterObserver.disconnect();
|