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