@joinezco/codeblock 0.0.9 → 0.0.11
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 +23 -2
- package/dist/editor.js +346 -41
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/lsps/index.d.ts +5 -0
- package/dist/lsps/index.js +9 -2
- package/dist/panels/{footer.d.ts → settings.d.ts} +7 -1
- package/dist/panels/{footer.js → settings.js} +12 -3
- package/dist/panels/terminal.d.ts +3 -0
- package/dist/panels/terminal.js +76 -0
- package/dist/panels/toolbar.d.ts +40 -3
- package/dist/panels/toolbar.js +919 -160
- package/dist/themes/index.js +63 -17
- package/dist/types.d.ts +5 -0
- package/dist/utils/fs.d.ts +7 -0
- package/dist/utils/fs.js +41 -15
- package/dist/workers/fs.worker.d.ts +4 -8
- package/dist/workers/fs.worker.js +27 -53
- package/package.json +14 -12
- 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-DfanUHpQ.js +0 -21
- package/dist/assets/go-CTD25R5P.js +0 -1
- package/dist/assets/haskell-BWDZoCOh.js +0 -1
- package/dist/assets/index-BAnLzvMk.js +0 -1
- package/dist/assets/index-BBC9WDX6.js +0 -1
- package/dist/assets/index-BEXYxRro.js +0 -1
- package/dist/assets/index-BfYmUKH9.js +0 -13
- package/dist/assets/index-BhaTNAWE.js +0 -1
- package/dist/assets/index-CCbYDSng.js +0 -1
- package/dist/assets/index-CIi8tLT6.js +0 -1
- package/dist/assets/index-CaANcgI2.js +0 -3
- package/dist/assets/index-CkWzFNzm.js +0 -208
- package/dist/assets/index-D_XGv9QZ.js +0 -1
- package/dist/assets/index-DkmiPfkD.js +0 -1
- package/dist/assets/index-DmNlLMQ4.js +0 -6
- package/dist/assets/index-DmX_vI7D.js +0 -1
- package/dist/assets/index-DogEEevD.js +0 -1
- package/dist/assets/index-DsDl5qZV.js +0 -2
- package/dist/assets/index-gAy5mDg-.js +0 -1
- package/dist/assets/index-i5qJLB2h.js +0 -1
- package/dist/assets/javascript.worker-ClsyHOLi.js +0 -552
- 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/editor.spec.d.ts +0 -1
- package/dist/e2e/editor.spec.js +0 -309
- package/dist/e2e/example.spec.d.ts +0 -5
- package/dist/e2e/example.spec.js +0 -44
- package/dist/index.html +0 -15
- package/dist/resources/config.json +0 -13
- package/dist/styles.css +0 -7
package/dist/panels/toolbar.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { EditorView } from "@codemirror/view";
|
|
1
2
|
import { StateEffect, StateField } from "@codemirror/state";
|
|
2
|
-
import { CodeblockFacet, openFileEffect, currentFileField, setThemeEffect } 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";
|
|
4
7
|
import { LSP, LspLog, FileChangeType } from "../utils/lsp";
|
|
5
8
|
import { Seti } from "@m234/nerd-fonts/fs";
|
|
6
|
-
import { settingsField, resolveThemeDark,
|
|
9
|
+
import { settingsField, resolveThemeDark, updateSettingsEffect } from "./settings";
|
|
7
10
|
// Browser-safe file icon lookup (avoids node:path.parse used by Seti.fromPath)
|
|
8
11
|
const FALLBACK_ICON = { value: '\ue64e', hexCode: 0xe64e }; // nf-seti-text
|
|
9
12
|
function setiIconForPath(filePath) {
|
|
@@ -28,6 +31,11 @@ function setiIconForPath(filePath) {
|
|
|
28
31
|
}
|
|
29
32
|
return FALLBACK_ICON;
|
|
30
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
|
+
}
|
|
31
39
|
// Type guards
|
|
32
40
|
function isCommandResult(result) {
|
|
33
41
|
return 'type' in result && result.query !== undefined;
|
|
@@ -35,6 +43,9 @@ function isCommandResult(result) {
|
|
|
35
43
|
function isBrowseEntry(result) {
|
|
36
44
|
return 'type' in result && ('fullPath' in result);
|
|
37
45
|
}
|
|
46
|
+
function isSettingsEntry(result) {
|
|
47
|
+
return 'type' in result && ('settingKey' in result);
|
|
48
|
+
}
|
|
38
49
|
// Search results state - now handles both commands and search results
|
|
39
50
|
export const setSearchResults = StateEffect.define();
|
|
40
51
|
export const searchResultsField = StateField.define({
|
|
@@ -67,6 +78,25 @@ function isValidProgrammingLanguage(query) {
|
|
|
67
78
|
return Object.keys(extOrLanguageToLanguageId).some(key => key.toLowerCase() === lowerQuery ||
|
|
68
79
|
extOrLanguageToLanguageId[key].toLowerCase() === lowerQuery);
|
|
69
80
|
}
|
|
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',
|
|
97
|
+
};
|
|
98
|
+
return nameToExt[langOrExt.toLowerCase()] || langOrExt.toLowerCase();
|
|
99
|
+
}
|
|
70
100
|
// Icons
|
|
71
101
|
const SEARCH_ICON = '\uf002'; // nf-fa-search (magnifying glass)
|
|
72
102
|
const DEFAULT_FILE_ICON = '\ue64e'; // nf-seti-text
|
|
@@ -74,6 +104,7 @@ const COG_ICON = '\uf013'; // nf-fa-cog
|
|
|
74
104
|
const FOLDER_ICON = '\ue613'; // nf-seti-folder
|
|
75
105
|
const FOLDER_OPEN_ICON = '\ue614'; // nf-seti-folder (open variant)
|
|
76
106
|
const PARENT_DIR_ICON = '\uf112'; // nf-fa-reply (back/up arrow)
|
|
107
|
+
// const TERMINAL_ICON = '\uf120'; // nf-fa-terminal — awaiting WASM VM/shim
|
|
77
108
|
// Get nerd font icon for a file path
|
|
78
109
|
function getFileIcon(path) {
|
|
79
110
|
const result = setiIconForPath(path);
|
|
@@ -83,29 +114,73 @@ function getFileIcon(path) {
|
|
|
83
114
|
function getLanguageIcon(query) {
|
|
84
115
|
return getFileIcon(`file.${query}`);
|
|
85
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
|
+
};
|
|
86
130
|
// Create command results for the first section
|
|
87
131
|
function createCommandResults(query, view, searchResults) {
|
|
88
132
|
const commands = [];
|
|
89
133
|
const currentFile = view.state.field(currentFileField);
|
|
90
134
|
const hasValidFile = currentFile.path && !currentFile.loading;
|
|
135
|
+
const hasContent = view.state.doc.length > 0;
|
|
91
136
|
const isLanguageQuery = isValidProgrammingLanguage(query);
|
|
92
|
-
// TODO: fix language ext for new file with full language names, "typescript" -> "file.ts"
|
|
93
137
|
// Check if query matches an existing file (first search result with exact match)
|
|
94
138
|
const hasExactFileMatch = searchResults.length > 0 && searchResults[0].id === query;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (!
|
|
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) {
|
|
98
151
|
const langIcon = isLanguageQuery ? getLanguageIcon(query) : null;
|
|
99
|
-
|
|
100
|
-
id: isLanguageQuery ? "
|
|
101
|
-
type: '
|
|
152
|
+
commands.push({
|
|
153
|
+
id: isLanguageQuery ? "Save as" : `Save as "${query}"`,
|
|
154
|
+
type: 'save-as',
|
|
102
155
|
icon: langIcon ? langIcon.glyph : DEFAULT_FILE_ICON,
|
|
103
156
|
iconColor: langIcon?.color,
|
|
104
157
|
query,
|
|
105
|
-
requiresInput: isLanguageQuery
|
|
106
|
-
};
|
|
107
|
-
commands.push(createFileCommand);
|
|
158
|
+
requiresInput: isLanguageQuery,
|
|
159
|
+
});
|
|
108
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()) {
|
|
109
184
|
// Rename file command (only if file is open, query is not a language, and doesn't match current file)
|
|
110
185
|
if (hasValidFile && !isLanguageQuery && !hasExactFileMatch) {
|
|
111
186
|
const renameCommand = {
|
|
@@ -124,6 +199,13 @@ function createCommandResults(query, view, searchResults) {
|
|
|
124
199
|
icon: FOLDER_OPEN_ICON,
|
|
125
200
|
query: '',
|
|
126
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
|
+
// });
|
|
127
209
|
// Import commands — always shown
|
|
128
210
|
commands.push({
|
|
129
211
|
id: 'Import file(s)',
|
|
@@ -132,13 +214,44 @@ function createCommandResults(query, view, searchResults) {
|
|
|
132
214
|
query: '',
|
|
133
215
|
});
|
|
134
216
|
commands.push({
|
|
135
|
-
id: 'Import folder
|
|
217
|
+
id: 'Import folder',
|
|
136
218
|
type: 'import-local-folder',
|
|
137
219
|
icon: FOLDER_ICON,
|
|
138
220
|
query: '',
|
|
139
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
|
+
});
|
|
140
244
|
return commands;
|
|
141
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
|
+
}
|
|
142
255
|
async function importFiles(files, view) {
|
|
143
256
|
const { fs, index } = view.state.facet(CodeblockFacet);
|
|
144
257
|
for (const file of files) {
|
|
@@ -146,7 +259,15 @@ async function importFiles(files, view) {
|
|
|
146
259
|
const dir = path.substring(0, path.lastIndexOf('/'));
|
|
147
260
|
if (dir)
|
|
148
261
|
await fs.mkdir(dir, { recursive: true });
|
|
149
|
-
|
|
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
|
+
}
|
|
150
271
|
if (index)
|
|
151
272
|
index.add(path);
|
|
152
273
|
LSP.notifyFileChanged(path, FileChangeType.Created);
|
|
@@ -185,7 +306,7 @@ function createLspLogOverlay() {
|
|
|
185
306
|
overlay._lspLogUnsub = unsub;
|
|
186
307
|
return overlay;
|
|
187
308
|
}
|
|
188
|
-
const
|
|
309
|
+
const SPINNER_FADE_MS = 150;
|
|
189
310
|
// Toolbar Panel
|
|
190
311
|
export const toolbarPanel = (view) => {
|
|
191
312
|
let { filepath, language, index } = view.state.facet(CodeblockFacet);
|
|
@@ -231,90 +352,53 @@ export const toolbarPanel = (view) => {
|
|
|
231
352
|
}
|
|
232
353
|
updateLspLogIcon();
|
|
233
354
|
updateLspLogVisibility();
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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) {
|
|
355
|
+
// LSP log overlay management (dedicated to LSP log only)
|
|
356
|
+
let lspLogOverlay = null;
|
|
357
|
+
let lspLogSavedInputValue = null;
|
|
358
|
+
function showLspLogOverlay(overlay) {
|
|
260
359
|
const panelsTop = view.dom.querySelector('.cm-panels-top');
|
|
261
360
|
if (panelsTop) {
|
|
262
361
|
overlay.style.top = `${panelsTop.getBoundingClientRect().height}px`;
|
|
263
362
|
}
|
|
264
363
|
view.dom.appendChild(overlay);
|
|
265
364
|
}
|
|
266
|
-
function
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
activeOverlayType = type;
|
|
272
|
-
showOverlay(overlay);
|
|
273
|
-
updateCogAppearance();
|
|
365
|
+
function openLspLogOverlay() {
|
|
366
|
+
lspLogSavedInputValue = input.value;
|
|
367
|
+
input.value = 'lsp.log';
|
|
368
|
+
lspLogOverlay = createLspLogOverlay();
|
|
369
|
+
showLspLogOverlay(lspLogOverlay);
|
|
274
370
|
}
|
|
275
|
-
function
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
activeOverlay._lspLogUnsub();
|
|
371
|
+
function closeLspLogOverlay() {
|
|
372
|
+
if (lspLogOverlay) {
|
|
373
|
+
if (lspLogOverlay._lspLogUnsub) {
|
|
374
|
+
lspLogOverlay._lspLogUnsub();
|
|
280
375
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
input.value = savedInputValue;
|
|
287
|
-
savedInputValue = null;
|
|
376
|
+
lspLogOverlay.remove();
|
|
377
|
+
lspLogOverlay = null;
|
|
378
|
+
if (lspLogSavedInputValue !== null) {
|
|
379
|
+
input.value = lspLogSavedInputValue;
|
|
380
|
+
lspLogSavedInputValue = null;
|
|
288
381
|
}
|
|
289
|
-
updateCogAppearance();
|
|
290
382
|
}
|
|
291
383
|
}
|
|
292
|
-
settingsCog.addEventListener("click", () => {
|
|
293
|
-
if (activeOverlayType) {
|
|
294
|
-
closeOverlay();
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
openOverlay('settings', createSettingsOverlay(view));
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
384
|
lspLogBtn.addEventListener("click", () => {
|
|
301
|
-
if (
|
|
302
|
-
|
|
385
|
+
if (lspLogOverlay) {
|
|
386
|
+
closeLspLogOverlay();
|
|
303
387
|
}
|
|
304
388
|
else {
|
|
305
|
-
|
|
306
|
-
openOverlay('lsp-log', createLspLogOverlay());
|
|
389
|
+
openLspLogOverlay();
|
|
307
390
|
}
|
|
308
391
|
});
|
|
309
392
|
dom.appendChild(lspLogBtn);
|
|
310
|
-
dom.appendChild(settingsCog);
|
|
311
393
|
const resultsList = document.createElement("ul");
|
|
312
394
|
resultsList.className = "cm-search-results";
|
|
313
395
|
dom.appendChild(resultsList);
|
|
314
396
|
let selectedIndex = 0;
|
|
315
397
|
let namingMode = { active: false, type: 'create-file', originalQuery: '' };
|
|
316
398
|
let browseMode = { active: false, currentPath: '/', filter: '' };
|
|
317
|
-
let
|
|
399
|
+
let settingsMode = { active: false, filter: '', editing: null };
|
|
400
|
+
let deleteMode = { active: false, filePath: '' };
|
|
401
|
+
let overwriteMode = { active: false, filePath: '', action: 'create-file' };
|
|
318
402
|
// System theme media query listener
|
|
319
403
|
const systemThemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
320
404
|
function handleSystemThemeChange() {
|
|
@@ -336,14 +420,37 @@ export const toolbarPanel = (view) => {
|
|
|
336
420
|
else {
|
|
337
421
|
view.dom.style.removeProperty('--cm-font-family');
|
|
338
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
|
+
}
|
|
339
434
|
}
|
|
340
435
|
applySettings();
|
|
341
|
-
// Apply initial theme
|
|
436
|
+
// Apply initial theme and auto-hide
|
|
342
437
|
const initialSettings = view.state.field(settingsField);
|
|
343
438
|
const initialDark = resolveThemeDark(initialSettings.theme);
|
|
344
439
|
view.dom.setAttribute('data-theme', initialDark ? 'dark' : 'light');
|
|
345
|
-
//
|
|
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
|
|
346
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`);
|
|
347
454
|
const gutters = view.dom.querySelector('.cm-gutters');
|
|
348
455
|
if (gutters) {
|
|
349
456
|
const gutterWidth = gutters.getBoundingClientRect().width;
|
|
@@ -353,6 +460,15 @@ export const toolbarPanel = (view) => {
|
|
|
353
460
|
const numberGutterWidth = numberGutter.getBoundingClientRect().width;
|
|
354
461
|
view.dom.style.setProperty('--cm-gutter-lineno-width', `${numberGutterWidth}px`);
|
|
355
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`);
|
|
466
|
+
}
|
|
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`);
|
|
356
472
|
}
|
|
357
473
|
}
|
|
358
474
|
// Set up ResizeObserver to watch gutter width changes
|
|
@@ -369,10 +485,434 @@ export const toolbarPanel = (view) => {
|
|
|
369
485
|
// Initial width setup and observer
|
|
370
486
|
updateGutterWidthVariables();
|
|
371
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
|
+
}
|
|
372
910
|
const renderItem = (result, i) => {
|
|
373
911
|
const li = document.createElement("li");
|
|
374
912
|
let resultClass = 'cm-file-result';
|
|
375
|
-
if (
|
|
913
|
+
if (isSettingsEntry(result))
|
|
914
|
+
resultClass = 'cm-command-result';
|
|
915
|
+
else if (isCommandResult(result))
|
|
376
916
|
resultClass = 'cm-command-result';
|
|
377
917
|
else if (isBrowseEntry(result))
|
|
378
918
|
resultClass = result.type === 'browse-file' ? 'cm-file-result' : 'cm-browse-dir-result';
|
|
@@ -381,22 +921,29 @@ export const toolbarPanel = (view) => {
|
|
|
381
921
|
resultIconContainer.className = "cm-search-result-icon-container";
|
|
382
922
|
const resultIcon = document.createElement("div");
|
|
383
923
|
resultIcon.className = "cm-search-result-icon";
|
|
384
|
-
|
|
385
|
-
|
|
924
|
+
if (isSettingsEntry(result)) {
|
|
925
|
+
// Settings entries use emoji or text icons, not nerd fonts
|
|
926
|
+
resultIcon.style.fontFamily = '';
|
|
386
927
|
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
928
|
}
|
|
395
929
|
else {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
+
}
|
|
400
947
|
}
|
|
401
948
|
resultIconContainer.appendChild(resultIcon);
|
|
402
949
|
li.appendChild(resultIconContainer);
|
|
@@ -409,7 +956,10 @@ export const toolbarPanel = (view) => {
|
|
|
409
956
|
li.addEventListener("mousedown", (ev) => {
|
|
410
957
|
ev.preventDefault();
|
|
411
958
|
});
|
|
412
|
-
li.addEventListener("click", () =>
|
|
959
|
+
li.addEventListener("click", (ev) => {
|
|
960
|
+
ev.stopPropagation();
|
|
961
|
+
selectResult(result);
|
|
962
|
+
});
|
|
413
963
|
return li;
|
|
414
964
|
};
|
|
415
965
|
function updateDropdown() {
|
|
@@ -420,9 +970,17 @@ export const toolbarPanel = (view) => {
|
|
|
420
970
|
children.push(renderItem(result, i));
|
|
421
971
|
});
|
|
422
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
|
+
}
|
|
423
978
|
}
|
|
424
979
|
function selectResult(result) {
|
|
425
|
-
if (
|
|
980
|
+
if (isSettingsEntry(result)) {
|
|
981
|
+
handleSettingsEntry(result);
|
|
982
|
+
}
|
|
983
|
+
else if (isBrowseEntry(result)) {
|
|
426
984
|
navigateBrowse(result);
|
|
427
985
|
}
|
|
428
986
|
else if (isCommandResult(result)) {
|
|
@@ -434,7 +992,10 @@ export const toolbarPanel = (view) => {
|
|
|
434
992
|
}
|
|
435
993
|
function updateStateIcon() {
|
|
436
994
|
if (namingMode.active) {
|
|
437
|
-
stateIcon.textContent = namingMode.type === 'create-file' ? DEFAULT_FILE_ICON : '\uf044';
|
|
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;
|
|
438
999
|
}
|
|
439
1000
|
else {
|
|
440
1001
|
stateIcon.textContent = SEARCH_ICON;
|
|
@@ -467,6 +1028,8 @@ export const toolbarPanel = (view) => {
|
|
|
467
1028
|
input.placeholder = '';
|
|
468
1029
|
input.focus();
|
|
469
1030
|
await refreshBrowseEntries();
|
|
1031
|
+
// Ensure click-outside listener is active
|
|
1032
|
+
document.addEventListener("click", handleClickOutside);
|
|
470
1033
|
}
|
|
471
1034
|
async function refreshBrowseEntries() {
|
|
472
1035
|
if (!browseMode.active)
|
|
@@ -570,35 +1133,47 @@ export const toolbarPanel = (view) => {
|
|
|
570
1133
|
fileInput.click();
|
|
571
1134
|
}
|
|
572
1135
|
function handleCommandResult(command) {
|
|
573
|
-
if (command.type === '
|
|
1136
|
+
if (command.type === 'settings') {
|
|
1137
|
+
enterSettingsMode();
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
else if (command.type === 'open-file') {
|
|
574
1141
|
enterBrowseMode();
|
|
575
1142
|
return;
|
|
576
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') {
|
|
1150
|
+
if (command.requiresInput) {
|
|
1151
|
+
const ext = command.query ? languageToFileExtension(command.query) : undefined;
|
|
1152
|
+
enterNamingMode('save-as', command.query, ext);
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
const pathToOpen = command.query.includes('.') ? command.query : `${command.query}.txt`;
|
|
1156
|
+
input.value = pathToOpen;
|
|
1157
|
+
checkOverwriteAndExecute(pathToOpen, 'save-as', () => createAndOpenFile(pathToOpen));
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
577
1160
|
else if (command.type === 'create-file') {
|
|
578
1161
|
if (command.requiresInput) {
|
|
579
|
-
|
|
580
|
-
enterNamingMode('create-file', command.query,
|
|
1162
|
+
const ext = command.query ? languageToFileExtension(command.query) : undefined;
|
|
1163
|
+
enterNamingMode('create-file', command.query, ext);
|
|
581
1164
|
}
|
|
582
1165
|
else {
|
|
583
|
-
// Create file directly and populate toolbar
|
|
584
1166
|
const pathToOpen = command.query.includes('.') ? command.query : `${command.query}.txt`;
|
|
585
1167
|
input.value = pathToOpen;
|
|
586
|
-
|
|
587
|
-
effects: [setSearchResults.of([]), openFileEffect.of({ path: pathToOpen })]
|
|
588
|
-
});
|
|
1168
|
+
checkOverwriteAndExecute(pathToOpen, 'create-file', () => createBlankFile(pathToOpen));
|
|
589
1169
|
}
|
|
590
1170
|
}
|
|
591
1171
|
else if (command.type === 'rename-file') {
|
|
592
|
-
// Rename file directly since the new name is provided by the query
|
|
593
1172
|
const currentFile = view.state.field(currentFileField);
|
|
594
1173
|
if (currentFile.path) {
|
|
595
1174
|
const newPath = command.query.includes('.') ? command.query : `${command.query}.txt`;
|
|
596
1175
|
input.value = newPath;
|
|
597
|
-
|
|
598
|
-
console.log(`Rename ${currentFile.path} to ${newPath}`);
|
|
599
|
-
safeDispatch(view, {
|
|
600
|
-
effects: [setSearchResults.of([]), openFileEffect.of({ path: newPath })]
|
|
601
|
-
});
|
|
1176
|
+
checkOverwriteAndExecute(newPath, 'rename', () => performRename(currentFile.path, newPath), currentFile.path);
|
|
602
1177
|
}
|
|
603
1178
|
}
|
|
604
1179
|
else if (command.type === 'import-local-files') {
|
|
@@ -607,6 +1182,10 @@ export const toolbarPanel = (view) => {
|
|
|
607
1182
|
else if (command.type === 'import-local-folder') {
|
|
608
1183
|
triggerFileImport(true);
|
|
609
1184
|
}
|
|
1185
|
+
else if (command.type === 'file-action' && command.action) {
|
|
1186
|
+
safeDispatch(view, { effects: setSearchResults.of([]) });
|
|
1187
|
+
command.action(view);
|
|
1188
|
+
}
|
|
610
1189
|
}
|
|
611
1190
|
function handleSearchResult(result) {
|
|
612
1191
|
input.value = result.id;
|
|
@@ -614,40 +1193,67 @@ export const toolbarPanel = (view) => {
|
|
|
614
1193
|
effects: [setSearchResults.of([]), openFileEffect.of({ path: result.id })]
|
|
615
1194
|
});
|
|
616
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
|
+
}
|
|
617
1215
|
function executeNamingMode(filename) {
|
|
618
1216
|
if (!namingMode.active || !filename.trim())
|
|
619
1217
|
return;
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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);
|
|
624
1223
|
input.value = pathToOpen;
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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;
|
|
629
1234
|
}
|
|
630
1235
|
else if (namingMode.type === 'rename-file') {
|
|
631
1236
|
const currentFile = view.state.field(currentFileField);
|
|
632
1237
|
if (currentFile.path) {
|
|
633
1238
|
const newPath = filename.includes('.') ? filename : `${filename}.txt`;
|
|
634
1239
|
input.value = newPath;
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
effects: [setSearchResults.of([]), openFileEffect.of({ path: newPath })]
|
|
639
|
-
});
|
|
1240
|
+
exitNamingMode();
|
|
1241
|
+
checkOverwriteAndExecute(newPath, 'rename', () => performRename(currentFile.path, newPath), currentFile.path);
|
|
1242
|
+
return;
|
|
640
1243
|
}
|
|
641
1244
|
}
|
|
642
1245
|
exitNamingMode();
|
|
643
1246
|
}
|
|
644
1247
|
function resetInputToCurrentFile() {
|
|
645
1248
|
const currentFile = view.state.field(currentFileField);
|
|
646
|
-
|
|
1249
|
+
const cfg = view.state.facet(CodeblockFacet);
|
|
1250
|
+
input.value = currentFile.path || cfg.language || '';
|
|
647
1251
|
}
|
|
648
1252
|
// Close dropdown when clicking outside
|
|
649
1253
|
function handleClickOutside(event) {
|
|
650
1254
|
if (!dom.contains(event.target)) {
|
|
1255
|
+
if (settingsMode.active)
|
|
1256
|
+
exitSettingsMode();
|
|
651
1257
|
if (browseMode.active)
|
|
652
1258
|
exitBrowseMode();
|
|
653
1259
|
safeDispatch(view, { effects: setSearchResults.of([]) });
|
|
@@ -655,34 +1261,52 @@ export const toolbarPanel = (view) => {
|
|
|
655
1261
|
}
|
|
656
1262
|
}
|
|
657
1263
|
input.addEventListener("click", () => {
|
|
1264
|
+
// Don't interfere when in a special mode
|
|
1265
|
+
if (namingMode.active || settingsMode.active || browseMode.active) {
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
658
1268
|
// Open dropdown when input is clicked
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
results = searchResults.concat(commands);
|
|
669
|
-
}
|
|
670
|
-
else {
|
|
671
|
-
// Show import commands when dropdown opens with empty query
|
|
672
|
-
results = createCommandResults('', view, []);
|
|
673
|
-
}
|
|
674
|
-
safeDispatch(view, { effects: setSearchResults.of(results) });
|
|
675
|
-
// Add click-outside listener when dropdown opens
|
|
676
|
-
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);
|
|
677
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);
|
|
678
1286
|
});
|
|
679
1287
|
input.addEventListener("input", (event) => {
|
|
680
1288
|
const query = event.target.value;
|
|
681
1289
|
selectedIndex = 0;
|
|
1290
|
+
// Block input during delete/overwrite confirmation
|
|
1291
|
+
if (deleteMode.active || overwriteMode.active) {
|
|
1292
|
+
input.value = '';
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
682
1295
|
// If in naming mode, don't show search results
|
|
683
1296
|
if (namingMode.active) {
|
|
684
1297
|
return;
|
|
685
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
|
+
}
|
|
686
1310
|
// If in browse mode, filter the directory entries
|
|
687
1311
|
if (browseMode.active) {
|
|
688
1312
|
// Extract filter text after the directory path prefix
|
|
@@ -708,6 +1332,35 @@ export const toolbarPanel = (view) => {
|
|
|
708
1332
|
safeDispatch(view, { effects: setSearchResults.of(results) });
|
|
709
1333
|
});
|
|
710
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
|
+
}
|
|
711
1364
|
if (namingMode.active) {
|
|
712
1365
|
// Handle naming mode
|
|
713
1366
|
if (event.key === "Enter") {
|
|
@@ -721,6 +1374,57 @@ export const toolbarPanel = (view) => {
|
|
|
721
1374
|
}
|
|
722
1375
|
return;
|
|
723
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
|
+
}
|
|
724
1428
|
// Browse mode keyboard handling
|
|
725
1429
|
if (browseMode.active) {
|
|
726
1430
|
const results = view.state.field(searchResultsField);
|
|
@@ -753,6 +1457,15 @@ export const toolbarPanel = (view) => {
|
|
|
753
1457
|
refreshBrowseEntries();
|
|
754
1458
|
}
|
|
755
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
|
+
}
|
|
756
1469
|
else if (event.key === "Escape") {
|
|
757
1470
|
event.preventDefault();
|
|
758
1471
|
exitBrowseMode();
|
|
@@ -786,6 +1499,14 @@ export const toolbarPanel = (view) => {
|
|
|
786
1499
|
event.preventDefault();
|
|
787
1500
|
selectResult(results[selectedIndex]);
|
|
788
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
|
+
}
|
|
789
1510
|
else if (event.key === "Escape") {
|
|
790
1511
|
event.preventDefault();
|
|
791
1512
|
safeDispatch(view, { effects: setSearchResults.of([]) });
|
|
@@ -810,43 +1531,78 @@ export const toolbarPanel = (view) => {
|
|
|
810
1531
|
// Apply settings when they change
|
|
811
1532
|
const prevSettings = update.startState.field(settingsField);
|
|
812
1533
|
const nextSettings = update.state.field(settingsField);
|
|
813
|
-
if (prevSettings.fontSize !== nextSettings.fontSize || prevSettings.fontFamily !== nextSettings.fontFamily) {
|
|
1534
|
+
if (prevSettings.fontSize !== nextSettings.fontSize || prevSettings.fontFamily !== nextSettings.fontFamily || prevSettings.maxVisibleLines !== nextSettings.maxVisibleLines) {
|
|
814
1535
|
applySettings();
|
|
815
1536
|
}
|
|
816
1537
|
if (prevSettings.lspLogEnabled !== nextSettings.lspLogEnabled) {
|
|
817
1538
|
updateLspLogVisibility();
|
|
818
1539
|
// Close the log overlay if the user disables it
|
|
819
|
-
if (!nextSettings.lspLogEnabled &&
|
|
820
|
-
|
|
1540
|
+
if (!nextSettings.lspLogEnabled && lspLogOverlay) {
|
|
1541
|
+
closeLspLogOverlay();
|
|
821
1542
|
}
|
|
822
1543
|
}
|
|
823
|
-
//
|
|
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.
|
|
824
1566
|
const prevFile = update.startState.field(currentFileField);
|
|
825
1567
|
const nextFile = update.state.field(currentFileField);
|
|
826
1568
|
if (prevFile.loading !== nextFile.loading) {
|
|
827
1569
|
if (nextFile.loading) {
|
|
828
|
-
|
|
829
|
-
stateIcon.
|
|
830
|
-
stateIcon.
|
|
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
|
+
}
|
|
831
1578
|
}
|
|
832
1579
|
else {
|
|
833
|
-
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
+
}
|
|
842
1598
|
}
|
|
843
1599
|
}
|
|
844
1600
|
// Update LSP log icon when file changes
|
|
845
1601
|
if (prevFile.path !== nextFile.path) {
|
|
846
1602
|
updateLspLogIcon();
|
|
847
1603
|
}
|
|
848
|
-
// Sync input value when file path changes (unless overlay is
|
|
849
|
-
if (prevFile.path !== nextFile.path && !namingMode.active && !
|
|
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) {
|
|
850
1606
|
input.value = nextFile.path || '';
|
|
851
1607
|
}
|
|
852
1608
|
},
|
|
@@ -854,8 +1610,11 @@ export const toolbarPanel = (view) => {
|
|
|
854
1610
|
// Clean up event listeners when panel is destroyed
|
|
855
1611
|
document.removeEventListener("click", handleClickOutside);
|
|
856
1612
|
systemThemeQuery.removeEventListener('change', handleSystemThemeChange);
|
|
857
|
-
// Clean up
|
|
858
|
-
|
|
1613
|
+
// Clean up auto-hide
|
|
1614
|
+
if (autoHideEnabled)
|
|
1615
|
+
disableAutoHide();
|
|
1616
|
+
// Clean up LSP log overlay
|
|
1617
|
+
closeLspLogOverlay();
|
|
859
1618
|
// Clean up ResizeObserver
|
|
860
1619
|
if (gutterObserver) {
|
|
861
1620
|
gutterObserver.disconnect();
|