@joinezco/codeblock 0.0.8
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/LICENSE +661 -0
- package/dist/assets/clike-C8IJ2oj_.js +1 -0
- package/dist/assets/cmake-BQqOBYOt.js +1 -0
- package/dist/assets/dockerfile-C_y-rIpk.js +1 -0
- package/dist/assets/fs.worker-BwEqZcql.ts +109 -0
- package/dist/assets/go-CTD25R5P.js +1 -0
- package/dist/assets/haskell-BWDZoCOh.js +1 -0
- package/dist/assets/index-9HdhmM_Y.js +1 -0
- package/dist/assets/index-C-QhPFHP.js +3 -0
- package/dist/assets/index-C3BnE2cG.js +222 -0
- package/dist/assets/index-CGx5MZO7.js +6 -0
- package/dist/assets/index-CIuq3uTk.js +1 -0
- package/dist/assets/index-CXFONXS8.js +1 -0
- package/dist/assets/index-D5Z27j1C.js +1 -0
- package/dist/assets/index-DWOBdRjn.js +1 -0
- package/dist/assets/index-Dvu-FFzd.js +1 -0
- package/dist/assets/index-Dx_VuNNd.js +1 -0
- package/dist/assets/index-I0dlv-r3.js +1 -0
- package/dist/assets/index-MGle_v2x.js +1 -0
- package/dist/assets/index-N-GE7HTU.js +1 -0
- package/dist/assets/index-aEsF5o-7.js +2 -0
- package/dist/assets/index-as7ELo0J.js +1 -0
- package/dist/assets/index-gUUzXNuP.js +1 -0
- package/dist/assets/index-pGm0qkrJ.js +13 -0
- package/dist/assets/javascript.worker-C1zGArKk.js +527 -0
- package/dist/assets/lua-BgMRiT3U.js +1 -0
- package/dist/assets/perl-CdXCOZ3F.js +1 -0
- package/dist/assets/process-Dw9K5EnD.js +1357 -0
- package/dist/assets/properties-C78fOPTZ.js +1 -0
- package/dist/assets/ruby-B2Rjki9n.js +1 -0
- package/dist/assets/shell-CjFT_Tl9.js +1 -0
- package/dist/assets/swift-BzpIVaGY.js +1 -0
- package/dist/assets/toml-BXUEaScT.js +1 -0
- package/dist/assets/vb-CmGdzxic.js +1 -0
- package/dist/e2e/example.spec.d.ts +5 -0
- package/dist/e2e/example.spec.js +44 -0
- package/dist/editor.d.ts +53 -0
- package/dist/editor.js +248 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.html +16 -0
- package/dist/index.js +6 -0
- package/dist/lsps/index.d.ts +96 -0
- package/dist/lsps/index.js +198 -0
- package/dist/lsps/typescript.d.ts +55 -0
- package/dist/lsps/typescript.js +48 -0
- package/dist/panels/toolbar.d.ts +20 -0
- package/dist/panels/toolbar.js +453 -0
- package/dist/panels/toolbar.test.d.ts +1 -0
- package/dist/panels/toolbar.test.js +146 -0
- package/dist/resources/config.json +13 -0
- package/dist/rpc/serde.d.ts +11 -0
- package/dist/rpc/serde.js +49 -0
- package/dist/rpc/transport.d.ts +11 -0
- package/dist/rpc/transport.js +38 -0
- package/dist/snapshot.bin +0 -0
- package/dist/styles.css +7 -0
- package/dist/themes/index.d.ts +1 -0
- package/dist/themes/index.js +169 -0
- package/dist/themes/util.d.ts +24 -0
- package/dist/themes/util.js +63 -0
- package/dist/themes/vscode.d.ts +6 -0
- package/dist/themes/vscode.js +187 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.js +1 -0
- package/dist/utils/fs.d.ts +29 -0
- package/dist/utils/fs.js +310 -0
- package/dist/utils/indent.d.ts +1 -0
- package/dist/utils/indent.js +38 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/lsp.d.ts +26 -0
- package/dist/utils/lsp.js +74 -0
- package/dist/utils/search.d.ts +30 -0
- package/dist/utils/search.js +68 -0
- package/dist/utils/snapshot.d.ts +60 -0
- package/dist/utils/snapshot.js +299 -0
- package/dist/workers/fs.worker.d.ts +11 -0
- package/dist/workers/fs.worker.js +93 -0
- package/dist/workers/javascript.worker.d.ts +1 -0
- package/dist/workers/javascript.worker.js +20 -0
- package/package.json +95 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { StateEffect, StateField } from "@codemirror/state";
|
|
2
|
+
import { CodeblockFacet, openFileEffect, currentFileField } from "../editor";
|
|
3
|
+
import { extOrLanguageToLanguageId } from "../lsps";
|
|
4
|
+
// Type guards
|
|
5
|
+
function isCommandResult(result) {
|
|
6
|
+
return 'type' in result;
|
|
7
|
+
}
|
|
8
|
+
function isSearchResult(result) {
|
|
9
|
+
return 'score' in result;
|
|
10
|
+
}
|
|
11
|
+
// Search results state - now handles both commands and search results
|
|
12
|
+
export const setSearchResults = StateEffect.define();
|
|
13
|
+
export const searchResultsField = StateField.define({
|
|
14
|
+
create() {
|
|
15
|
+
return [];
|
|
16
|
+
},
|
|
17
|
+
update(value, tr) {
|
|
18
|
+
for (let e of tr.effects)
|
|
19
|
+
if (e.is(setSearchResults))
|
|
20
|
+
return e.value;
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const mod = (n, m) => ((n % m) + m) % m;
|
|
25
|
+
// A safe dispatcher to avoid nested-update errors from UI events during CM updates
|
|
26
|
+
function safeDispatch(view, spec) {
|
|
27
|
+
// Always queue to a microtask so we never dispatch within an ongoing update cycle
|
|
28
|
+
queueMicrotask(() => {
|
|
29
|
+
try {
|
|
30
|
+
view.dispatch(spec);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
console.error(e);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
// Check if query matches a programming language
|
|
38
|
+
function isValidProgrammingLanguage(query) {
|
|
39
|
+
const lowerQuery = query.toLowerCase();
|
|
40
|
+
return Object.keys(extOrLanguageToLanguageId).some(key => key.toLowerCase() === lowerQuery ||
|
|
41
|
+
extOrLanguageToLanguageId[key].toLowerCase() === lowerQuery);
|
|
42
|
+
}
|
|
43
|
+
// Get appropriate icon for language/extension
|
|
44
|
+
function getLanguageIcon(query) {
|
|
45
|
+
const lowerQuery = query.toLowerCase();
|
|
46
|
+
// Language/extension icons matching extOrLanguageToLanguageId
|
|
47
|
+
const iconMap = {
|
|
48
|
+
// JavaScript/TypeScript family
|
|
49
|
+
'javascript': '🟨',
|
|
50
|
+
'js': '🟨',
|
|
51
|
+
'typescript': '🔷',
|
|
52
|
+
'ts': '🔷',
|
|
53
|
+
'jsx': '⚛️',
|
|
54
|
+
'tsx': '⚛️',
|
|
55
|
+
// Python
|
|
56
|
+
'python': '🐍',
|
|
57
|
+
'py': '🐍',
|
|
58
|
+
// Ruby
|
|
59
|
+
'ruby': '💎',
|
|
60
|
+
'rb': '💎',
|
|
61
|
+
// PHP
|
|
62
|
+
'php': '🐘',
|
|
63
|
+
// Java
|
|
64
|
+
'java': '☕',
|
|
65
|
+
// C/C++
|
|
66
|
+
'cpp': '⚙️',
|
|
67
|
+
'c': '⚙️',
|
|
68
|
+
// C#
|
|
69
|
+
'csharp': '🔷',
|
|
70
|
+
'cs': '🔷',
|
|
71
|
+
// Go
|
|
72
|
+
'go': '🐹',
|
|
73
|
+
// Swift
|
|
74
|
+
'swift': '🦉',
|
|
75
|
+
// Kotlin
|
|
76
|
+
'kotlin': '🟣',
|
|
77
|
+
'kt': '🟣',
|
|
78
|
+
// Rust
|
|
79
|
+
'rust': '🦀',
|
|
80
|
+
'rs': '🦀',
|
|
81
|
+
// Scala
|
|
82
|
+
'scala': '🔴',
|
|
83
|
+
// Visual Basic
|
|
84
|
+
'vb': '🔵',
|
|
85
|
+
// Haskell
|
|
86
|
+
'haskell': '🎭',
|
|
87
|
+
'hs': '🎭',
|
|
88
|
+
// Lua
|
|
89
|
+
'lua': '🌙',
|
|
90
|
+
// Perl
|
|
91
|
+
'perl': '🐪',
|
|
92
|
+
'pl': '🐪',
|
|
93
|
+
// Shell/Bash
|
|
94
|
+
'bash': '🐚',
|
|
95
|
+
'shell': '🐚',
|
|
96
|
+
'sh': '🐚',
|
|
97
|
+
'zsh': '🐚',
|
|
98
|
+
// SQL
|
|
99
|
+
'mysql': '🗃️',
|
|
100
|
+
'sql': '🗃️',
|
|
101
|
+
// Web technologies
|
|
102
|
+
'html': '🌐',
|
|
103
|
+
'css': '🎨',
|
|
104
|
+
'scss': '🎨',
|
|
105
|
+
'less': '🎨',
|
|
106
|
+
// Data formats
|
|
107
|
+
'json': '📋',
|
|
108
|
+
'yaml': '⚙️',
|
|
109
|
+
'yml': '⚙️',
|
|
110
|
+
'xml': '📄',
|
|
111
|
+
'toml': '⚙️',
|
|
112
|
+
'ini': '⚙️',
|
|
113
|
+
'conf': '⚙️',
|
|
114
|
+
'log': '📄',
|
|
115
|
+
'env': '🔧',
|
|
116
|
+
// Documentation
|
|
117
|
+
'markdown': '📝',
|
|
118
|
+
'md': '📝',
|
|
119
|
+
// Docker/Build
|
|
120
|
+
'dockerfile': '🐳',
|
|
121
|
+
'makefile': '🔨',
|
|
122
|
+
'dockerignore': '🐳',
|
|
123
|
+
'gitignore': '📝'
|
|
124
|
+
};
|
|
125
|
+
return iconMap[lowerQuery] || '📄';
|
|
126
|
+
}
|
|
127
|
+
// Create command results for the first section
|
|
128
|
+
function createCommandResults(query, view, searchResults) {
|
|
129
|
+
const commands = [];
|
|
130
|
+
const currentFile = view.state.field(currentFileField);
|
|
131
|
+
const hasValidFile = currentFile.path && !currentFile.loading;
|
|
132
|
+
const isLanguageQuery = isValidProgrammingLanguage(query);
|
|
133
|
+
// TODO: fix language ext for new file with full language names, "typescript" -> "file.ts"
|
|
134
|
+
// Check if query matches an existing file (first search result with exact match)
|
|
135
|
+
const hasExactFileMatch = searchResults.length > 0 && searchResults[0].id === query;
|
|
136
|
+
if (query.trim()) {
|
|
137
|
+
// Create new file command (only if query doesn't match existing file)
|
|
138
|
+
if (!hasExactFileMatch) {
|
|
139
|
+
const createFileCommand = {
|
|
140
|
+
id: isLanguageQuery ? "Create new file" : `Create new file "${query}"`,
|
|
141
|
+
type: 'create-file',
|
|
142
|
+
icon: isLanguageQuery ? getLanguageIcon(query) : '📄',
|
|
143
|
+
query,
|
|
144
|
+
requiresInput: isLanguageQuery
|
|
145
|
+
};
|
|
146
|
+
commands.push(createFileCommand);
|
|
147
|
+
}
|
|
148
|
+
// Rename file command (only if file is open, query is not a language, and doesn't match current file)
|
|
149
|
+
if (hasValidFile && !isLanguageQuery && !hasExactFileMatch) {
|
|
150
|
+
const renameCommand = {
|
|
151
|
+
id: `Rename to "${query}"`,
|
|
152
|
+
type: 'rename-file',
|
|
153
|
+
icon: '✏️',
|
|
154
|
+
query
|
|
155
|
+
};
|
|
156
|
+
commands.push(renameCommand);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return commands;
|
|
160
|
+
}
|
|
161
|
+
// Toolbar Panel
|
|
162
|
+
export const toolbarPanel = (view) => {
|
|
163
|
+
let { filepath, language, index } = view.state.facet(CodeblockFacet);
|
|
164
|
+
const dom = document.createElement("div");
|
|
165
|
+
dom.className = "cm-toolbar-panel";
|
|
166
|
+
// Create state icon (left side)
|
|
167
|
+
const stateIcon = document.createElement("div");
|
|
168
|
+
stateIcon.className = "cm-toolbar-state-icon";
|
|
169
|
+
stateIcon.textContent = "📄"; // Default file icon
|
|
170
|
+
// Create container for state icon to help with alignment
|
|
171
|
+
const stateIconContainer = document.createElement("div");
|
|
172
|
+
stateIconContainer.className = "cm-toolbar-state-icon-container";
|
|
173
|
+
stateIconContainer.appendChild(stateIcon);
|
|
174
|
+
dom.appendChild(stateIconContainer);
|
|
175
|
+
// Create input container for the right-aligned input
|
|
176
|
+
const inputContainer = document.createElement("div");
|
|
177
|
+
inputContainer.className = "cm-toolbar-input-container";
|
|
178
|
+
dom.appendChild(inputContainer);
|
|
179
|
+
const input = document.createElement("input");
|
|
180
|
+
input.type = "text";
|
|
181
|
+
input.value = filepath || language || "";
|
|
182
|
+
input.className = "cm-toolbar-input";
|
|
183
|
+
inputContainer.appendChild(input);
|
|
184
|
+
const resultsList = document.createElement("ul");
|
|
185
|
+
resultsList.className = "cm-search-results";
|
|
186
|
+
dom.appendChild(resultsList);
|
|
187
|
+
let selectedIndex = 0;
|
|
188
|
+
let namingMode = { active: false, type: 'create-file', originalQuery: '' };
|
|
189
|
+
// Tracks gutter width for toolbar alignment
|
|
190
|
+
function updateGutterWidthVariables() {
|
|
191
|
+
const gutters = view.dom.querySelector('.cm-gutters');
|
|
192
|
+
if (gutters) {
|
|
193
|
+
const gutterWidth = gutters.getBoundingClientRect().width;
|
|
194
|
+
dom.style.setProperty('--cm-gutter-width', `${gutterWidth}px`);
|
|
195
|
+
const numberGutter = gutters.querySelector('.cm-lineNumbers');
|
|
196
|
+
if (numberGutter) {
|
|
197
|
+
const numberGutterWidth = numberGutter.getBoundingClientRect().width;
|
|
198
|
+
dom.style.setProperty('--cm-gutter-lineno-width', `${numberGutterWidth}px`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Set up ResizeObserver to watch gutter width changes
|
|
203
|
+
let gutterObserver = null;
|
|
204
|
+
function setupGutterObserver() {
|
|
205
|
+
const gutters = view.dom.querySelector('.cm-gutters');
|
|
206
|
+
if (gutters && window.ResizeObserver) {
|
|
207
|
+
gutterObserver = new ResizeObserver(() => {
|
|
208
|
+
updateGutterWidthVariables();
|
|
209
|
+
});
|
|
210
|
+
gutterObserver.observe(gutters);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Initial width setup and observer
|
|
214
|
+
updateGutterWidthVariables();
|
|
215
|
+
setupGutterObserver();
|
|
216
|
+
const renderItem = (result, i) => {
|
|
217
|
+
const li = document.createElement("li");
|
|
218
|
+
li.className = `cm-search-result ${isCommandResult(result) ? 'cm-command-result' : 'cm-file-result'}`;
|
|
219
|
+
const resultIconContainer = document.createElement("div");
|
|
220
|
+
resultIconContainer.className = "cm-search-result-icon-container";
|
|
221
|
+
const resultIcon = document.createElement("div");
|
|
222
|
+
resultIcon.className = "cm-search-result-icon";
|
|
223
|
+
resultIcon.textContent = isCommandResult(result) ? result.icon : '📄';
|
|
224
|
+
resultIconContainer.appendChild(resultIcon);
|
|
225
|
+
li.appendChild(resultIconContainer);
|
|
226
|
+
const resultLabel = document.createElement("div");
|
|
227
|
+
resultLabel.className = "cm-search-result-label";
|
|
228
|
+
resultLabel.textContent = result.id;
|
|
229
|
+
li.appendChild(resultLabel);
|
|
230
|
+
if (i === selectedIndex)
|
|
231
|
+
li.classList.add("selected");
|
|
232
|
+
li.addEventListener("mousedown", (ev) => {
|
|
233
|
+
ev.preventDefault();
|
|
234
|
+
});
|
|
235
|
+
li.addEventListener("click", () => selectResult(result));
|
|
236
|
+
return li;
|
|
237
|
+
};
|
|
238
|
+
function updateDropdown() {
|
|
239
|
+
const results = view.state.field(searchResultsField);
|
|
240
|
+
const children = [];
|
|
241
|
+
// Separate commands from search results
|
|
242
|
+
const commands = results.filter(isCommandResult);
|
|
243
|
+
const searchResults = results.filter(isSearchResult);
|
|
244
|
+
let currentIndex = 0;
|
|
245
|
+
// Render commands section
|
|
246
|
+
commands.forEach((command) => {
|
|
247
|
+
children.push(renderItem(command, currentIndex));
|
|
248
|
+
currentIndex++;
|
|
249
|
+
});
|
|
250
|
+
// Render search results section
|
|
251
|
+
searchResults.forEach((result) => {
|
|
252
|
+
children.push(renderItem(result, currentIndex));
|
|
253
|
+
currentIndex++;
|
|
254
|
+
});
|
|
255
|
+
resultsList.replaceChildren(...children);
|
|
256
|
+
}
|
|
257
|
+
function selectResult(result) {
|
|
258
|
+
if (isCommandResult(result)) {
|
|
259
|
+
handleCommandResult(result);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
handleSearchResult(result);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function updateStateIcon() {
|
|
266
|
+
if (namingMode.active) {
|
|
267
|
+
stateIcon.textContent = namingMode.type === 'create-file' ? '📄' : '✏️';
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
stateIcon.textContent = '📄'; // Default file icon
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function enterNamingMode(type, originalQuery, languageExtension) {
|
|
274
|
+
namingMode = { active: true, type, originalQuery, languageExtension };
|
|
275
|
+
// Update state icon
|
|
276
|
+
updateStateIcon();
|
|
277
|
+
// Clear input and focus
|
|
278
|
+
input.value = '';
|
|
279
|
+
input.placeholder = languageExtension ? `filename.${languageExtension}` : 'filename';
|
|
280
|
+
input.focus();
|
|
281
|
+
// Clear search results
|
|
282
|
+
safeDispatch(view, { effects: setSearchResults.of([]) });
|
|
283
|
+
}
|
|
284
|
+
function exitNamingMode() {
|
|
285
|
+
namingMode = { active: false, type: 'create-file', originalQuery: '' };
|
|
286
|
+
updateStateIcon();
|
|
287
|
+
input.placeholder = '';
|
|
288
|
+
}
|
|
289
|
+
function handleCommandResult(command) {
|
|
290
|
+
if (command.type === 'create-file') {
|
|
291
|
+
if (command.requiresInput) {
|
|
292
|
+
// Enter naming mode for language-specific file
|
|
293
|
+
enterNamingMode('create-file', command.query, command.query);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
// Create file directly and populate toolbar
|
|
297
|
+
const pathToOpen = command.query.includes('.') ? command.query : `${command.query}.txt`;
|
|
298
|
+
input.value = pathToOpen;
|
|
299
|
+
safeDispatch(view, {
|
|
300
|
+
effects: [setSearchResults.of([]), openFileEffect.of({ path: pathToOpen })]
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else if (command.type === 'rename-file') {
|
|
305
|
+
// Rename file directly since the new name is provided by the query
|
|
306
|
+
const currentFile = view.state.field(currentFileField);
|
|
307
|
+
if (currentFile.path) {
|
|
308
|
+
const newPath = command.query.includes('.') ? command.query : `${command.query}.txt`;
|
|
309
|
+
input.value = newPath;
|
|
310
|
+
// TODO: Implement actual file rename logic
|
|
311
|
+
console.log(`Rename ${currentFile.path} to ${newPath}`);
|
|
312
|
+
safeDispatch(view, {
|
|
313
|
+
effects: [setSearchResults.of([]), openFileEffect.of({ path: newPath })]
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function handleSearchResult(result) {
|
|
319
|
+
input.value = result.id;
|
|
320
|
+
safeDispatch(view, {
|
|
321
|
+
effects: [setSearchResults.of([]), openFileEffect.of({ path: result.id })]
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
function executeNamingMode(filename) {
|
|
325
|
+
if (!namingMode.active || !filename.trim())
|
|
326
|
+
return;
|
|
327
|
+
if (namingMode.type === 'create-file') {
|
|
328
|
+
const pathToOpen = namingMode.languageExtension && !filename.includes('.')
|
|
329
|
+
? `${filename}.${namingMode.languageExtension}`
|
|
330
|
+
: filename;
|
|
331
|
+
input.value = pathToOpen;
|
|
332
|
+
// TODO: handle edge-cases like trying to create folders, invalid characters, etc.
|
|
333
|
+
safeDispatch(view, {
|
|
334
|
+
effects: [setSearchResults.of([]), openFileEffect.of({ path: pathToOpen })]
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
else if (namingMode.type === 'rename-file') {
|
|
338
|
+
const currentFile = view.state.field(currentFileField);
|
|
339
|
+
if (currentFile.path) {
|
|
340
|
+
const newPath = filename.includes('.') ? filename : `${filename}.txt`;
|
|
341
|
+
input.value = newPath;
|
|
342
|
+
// TODO: Implement actual file rename logic
|
|
343
|
+
console.log(`Rename ${currentFile.path} to ${newPath}`);
|
|
344
|
+
safeDispatch(view, {
|
|
345
|
+
effects: [setSearchResults.of([]), openFileEffect.of({ path: newPath })]
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
exitNamingMode();
|
|
350
|
+
}
|
|
351
|
+
// Close dropdown when clicking outside
|
|
352
|
+
function handleClickOutside(event) {
|
|
353
|
+
if (!dom.contains(event.target)) {
|
|
354
|
+
safeDispatch(view, { effects: setSearchResults.of([]) });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
input.addEventListener("click", () => {
|
|
358
|
+
// Open dropdown when input is clicked
|
|
359
|
+
if (!namingMode.active) {
|
|
360
|
+
const query = input.value;
|
|
361
|
+
let results = [];
|
|
362
|
+
if (query.trim()) {
|
|
363
|
+
// Get regular search results from index first
|
|
364
|
+
const searchResults = (index?.search(query) || []).slice(0, 100);
|
|
365
|
+
// Add command results first (passing search results to check for existing files)
|
|
366
|
+
const commands = createCommandResults(query, view, searchResults);
|
|
367
|
+
results = searchResults.concat(commands);
|
|
368
|
+
}
|
|
369
|
+
safeDispatch(view, { effects: setSearchResults.of(results) });
|
|
370
|
+
// Add click-outside listener when dropdown opens
|
|
371
|
+
document.addEventListener("click", handleClickOutside);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
input.addEventListener("input", (event) => {
|
|
375
|
+
const query = event.target.value;
|
|
376
|
+
selectedIndex = 0;
|
|
377
|
+
// If in naming mode, don't show search results
|
|
378
|
+
if (namingMode.active) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
let results = [];
|
|
382
|
+
if (query.trim()) {
|
|
383
|
+
// Get regular search results from index first
|
|
384
|
+
const searchResults = (index?.search(query) || []).slice(0, 1000);
|
|
385
|
+
// Add command results first (passing search results to check for existing files)
|
|
386
|
+
const commands = createCommandResults(query, view, searchResults);
|
|
387
|
+
results.push(...commands);
|
|
388
|
+
// Add search results
|
|
389
|
+
results.push(...searchResults);
|
|
390
|
+
}
|
|
391
|
+
safeDispatch(view, { effects: setSearchResults.of(results) });
|
|
392
|
+
});
|
|
393
|
+
input.addEventListener("keydown", (event) => {
|
|
394
|
+
if (namingMode.active) {
|
|
395
|
+
// Handle naming mode
|
|
396
|
+
if (event.key === "Enter") {
|
|
397
|
+
event.preventDefault();
|
|
398
|
+
executeNamingMode(input.value);
|
|
399
|
+
}
|
|
400
|
+
else if (event.key === "Escape") {
|
|
401
|
+
event.preventDefault();
|
|
402
|
+
exitNamingMode();
|
|
403
|
+
input.value = namingMode.originalQuery;
|
|
404
|
+
}
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
// Normal search mode
|
|
408
|
+
const results = view.state.field(searchResultsField);
|
|
409
|
+
if (event.key === "ArrowDown") {
|
|
410
|
+
event.preventDefault();
|
|
411
|
+
if (results.length) {
|
|
412
|
+
selectedIndex = mod(selectedIndex + 1, results.length);
|
|
413
|
+
updateDropdown();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else if (event.key === "ArrowUp") {
|
|
417
|
+
event.preventDefault();
|
|
418
|
+
if (results.length) {
|
|
419
|
+
selectedIndex = mod(selectedIndex - 1, results.length);
|
|
420
|
+
updateDropdown();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
else if (event.key === "Enter" && results.length && selectedIndex >= 0) {
|
|
424
|
+
event.preventDefault();
|
|
425
|
+
selectResult(results[selectedIndex]);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
return {
|
|
429
|
+
dom,
|
|
430
|
+
top: true,
|
|
431
|
+
update(update) {
|
|
432
|
+
// Re-render dropdown when search results change
|
|
433
|
+
const a = update.startState.field(searchResultsField);
|
|
434
|
+
const b = update.state.field(searchResultsField);
|
|
435
|
+
if (a !== b) {
|
|
436
|
+
updateDropdown();
|
|
437
|
+
// Remove click-outside listener when dropdown closes
|
|
438
|
+
if (b.length === 0) {
|
|
439
|
+
document.removeEventListener("click", handleClickOutside);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
destroy() {
|
|
444
|
+
// Clean up event listeners when panel is destroyed
|
|
445
|
+
document.removeEventListener("click", handleClickOutside);
|
|
446
|
+
// Clean up ResizeObserver
|
|
447
|
+
if (gutterObserver) {
|
|
448
|
+
gutterObserver.disconnect();
|
|
449
|
+
gutterObserver = null;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { EditorState } from '@codemirror/state';
|
|
3
|
+
import { EditorView } from '@codemirror/view';
|
|
4
|
+
import { searchResultsField } from './toolbar';
|
|
5
|
+
import { CodeblockFacet, currentFileField } from '../editor';
|
|
6
|
+
// Mock dependencies
|
|
7
|
+
vi.mock('../lsps', () => ({
|
|
8
|
+
extOrLanguageToLanguageId: {
|
|
9
|
+
'js': 'javascript',
|
|
10
|
+
'ts': 'typescript',
|
|
11
|
+
'py': 'python',
|
|
12
|
+
'rs': 'rust',
|
|
13
|
+
'go': 'go'
|
|
14
|
+
}
|
|
15
|
+
}));
|
|
16
|
+
vi.mock('../editor', () => ({
|
|
17
|
+
CodeblockFacet: {
|
|
18
|
+
of: vi.fn(),
|
|
19
|
+
},
|
|
20
|
+
currentFileField: {
|
|
21
|
+
init: vi.fn(() => ({ path: null, content: '', language: null, loading: false }))
|
|
22
|
+
},
|
|
23
|
+
openFileEffect: {
|
|
24
|
+
of: vi.fn()
|
|
25
|
+
}
|
|
26
|
+
}));
|
|
27
|
+
describe('Toolbar Panel', () => {
|
|
28
|
+
let view;
|
|
29
|
+
let mockFs;
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
mockFs = {
|
|
32
|
+
readFile: vi.fn(),
|
|
33
|
+
writeFile: vi.fn(),
|
|
34
|
+
exists: vi.fn()
|
|
35
|
+
};
|
|
36
|
+
const state = EditorState.create({
|
|
37
|
+
doc: '',
|
|
38
|
+
extensions: [
|
|
39
|
+
CodeblockFacet.of({
|
|
40
|
+
fs: mockFs,
|
|
41
|
+
cwd: '/',
|
|
42
|
+
filepath: null,
|
|
43
|
+
content: '',
|
|
44
|
+
toolbar: true,
|
|
45
|
+
index: null,
|
|
46
|
+
language: null
|
|
47
|
+
}),
|
|
48
|
+
searchResultsField,
|
|
49
|
+
currentFileField
|
|
50
|
+
]
|
|
51
|
+
});
|
|
52
|
+
view = new EditorView({
|
|
53
|
+
state,
|
|
54
|
+
parent: document.createElement('div')
|
|
55
|
+
});
|
|
56
|
+
console.log(view);
|
|
57
|
+
});
|
|
58
|
+
describe('Command Results Generation', () => {
|
|
59
|
+
it('should generate create file command for any query', () => {
|
|
60
|
+
// Test will be implemented
|
|
61
|
+
expect(true).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
it('should generate rename command when file is open and query is not a language', () => {
|
|
64
|
+
// Test will be implemented
|
|
65
|
+
expect(true).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
it('should require input for language-specific file creation', () => {
|
|
68
|
+
// Test will be implemented
|
|
69
|
+
expect(true).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
it('should not show rename command when no file is open', () => {
|
|
72
|
+
// Test will be implemented
|
|
73
|
+
expect(true).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
it('should not show rename command when query matches a programming language', () => {
|
|
76
|
+
// Test will be implemented
|
|
77
|
+
expect(true).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('Search Results Separation', () => {
|
|
81
|
+
it('should separate command results from file search results', () => {
|
|
82
|
+
// Test will be implemented
|
|
83
|
+
expect(true).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
it('should show divider between sections when both exist', () => {
|
|
86
|
+
// Test will be implemented
|
|
87
|
+
expect(true).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
it('should not show divider when only one section exists', () => {
|
|
90
|
+
// Test will be implemented
|
|
91
|
+
expect(true).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe('Command Execution', () => {
|
|
95
|
+
it('should create file directly for non-language queries', () => {
|
|
96
|
+
// Test will be implemented
|
|
97
|
+
expect(true).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
it('should prompt for filename for language-specific queries', () => {
|
|
100
|
+
// Test will be implemented
|
|
101
|
+
expect(true).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
it('should handle rename command correctly', () => {
|
|
104
|
+
// Test will be implemented
|
|
105
|
+
expect(true).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe('UI Rendering', () => {
|
|
109
|
+
it('should render command results with icons', () => {
|
|
110
|
+
// Test will be implemented
|
|
111
|
+
expect(true).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
it('should render file results without icons', () => {
|
|
114
|
+
// Test will be implemented
|
|
115
|
+
expect(true).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
it('should apply correct CSS classes to different result types', () => {
|
|
118
|
+
// Test will be implemented
|
|
119
|
+
expect(true).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('Keyboard Navigation', () => {
|
|
123
|
+
it('should navigate through all results with arrow keys', () => {
|
|
124
|
+
// Test will be implemented
|
|
125
|
+
expect(true).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
it('should execute selected result on Enter', () => {
|
|
128
|
+
// Test will be implemented
|
|
129
|
+
expect(true).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
it('should handle navigation correctly with dividers', () => {
|
|
132
|
+
// Test will be implemented
|
|
133
|
+
expect(true).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe('Language Detection', () => {
|
|
137
|
+
it('should correctly identify valid programming languages', () => {
|
|
138
|
+
// Test will be implemented
|
|
139
|
+
expect(true).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
it('should handle case-insensitive language matching', () => {
|
|
142
|
+
// Test will be implemented
|
|
143
|
+
expect(true).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TransferHandler } from 'comlink';
|
|
2
|
+
export declare const asyncGeneratorTransferHandler: TransferHandler<AsyncGenerator<unknown>, unknown>;
|
|
3
|
+
export declare const watchOptionsTransferHandler: TransferHandler<{
|
|
4
|
+
signal: AbortSignal;
|
|
5
|
+
encoding?: string;
|
|
6
|
+
recursive?: boolean;
|
|
7
|
+
}, {
|
|
8
|
+
port: MessagePort;
|
|
9
|
+
encoding?: string;
|
|
10
|
+
recursive?: boolean;
|
|
11
|
+
}>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { transferHandlers, proxy } from 'comlink';
|
|
2
|
+
const proxyTransferHandler = transferHandlers.get('proxy');
|
|
3
|
+
// Allows us to use watch as a normal async generator
|
|
4
|
+
export const asyncGeneratorTransferHandler = {
|
|
5
|
+
canHandle(obj) {
|
|
6
|
+
return (obj &&
|
|
7
|
+
typeof obj === 'object' &&
|
|
8
|
+
typeof obj.next === 'function' &&
|
|
9
|
+
(typeof obj[Symbol.iterator] === 'function' ||
|
|
10
|
+
typeof obj[Symbol.asyncIterator] === 'function'));
|
|
11
|
+
},
|
|
12
|
+
serialize(obj) {
|
|
13
|
+
return proxyTransferHandler.serialize(proxy(obj));
|
|
14
|
+
},
|
|
15
|
+
async *deserialize(obj) {
|
|
16
|
+
const iterator = proxyTransferHandler.deserialize(obj);
|
|
17
|
+
while (true) {
|
|
18
|
+
const { value, done } = await iterator.next();
|
|
19
|
+
if (done) {
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
yield value;
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
// Allows aborting watches across workers (like when our current file changes).
|
|
27
|
+
export const watchOptionsTransferHandler = {
|
|
28
|
+
canHandle: (obj) => {
|
|
29
|
+
return obj && typeof obj === "object" && obj.signal instanceof AbortSignal;
|
|
30
|
+
},
|
|
31
|
+
serialize: (options) => {
|
|
32
|
+
const { signal, ...rest } = options;
|
|
33
|
+
const { port1, port2 } = new MessageChannel();
|
|
34
|
+
signal.addEventListener("abort", () => {
|
|
35
|
+
port1.postMessage({});
|
|
36
|
+
port1.close();
|
|
37
|
+
}, { once: true });
|
|
38
|
+
return [{ port: port2, ...rest }, [port2]];
|
|
39
|
+
},
|
|
40
|
+
deserialize: (data) => {
|
|
41
|
+
const { port, ...rest } = data;
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
port.onmessage = () => {
|
|
44
|
+
controller.abort();
|
|
45
|
+
port.close();
|
|
46
|
+
};
|
|
47
|
+
return { signal: controller.signal, ...rest };
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Transport } from "@open-rpc/client-js/build/transports/Transport";
|
|
2
|
+
import type { JSONRPCRequestData } from "@open-rpc/client-js/src/Request";
|
|
3
|
+
export default class MessagePortTransport extends Transport {
|
|
4
|
+
port: MessagePort;
|
|
5
|
+
postMessageID: string;
|
|
6
|
+
constructor(port: MessagePort);
|
|
7
|
+
private messageHandler;
|
|
8
|
+
connect(): Promise<void>;
|
|
9
|
+
sendData(data: JSONRPCRequestData): Promise<any>;
|
|
10
|
+
close(): void;
|
|
11
|
+
}
|