@workbench-kit/react 0.0.2-prototype.0.2.13 → 0.0.2-prototype.0.2.15
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/README.md +29 -0
- package/package.json +17 -12
- package/src/index.ts +16 -0
- package/src/jdw/builtins/renderBuiltinWidgetLeaf.tsx +1 -3
- package/src/jdw/createBuiltinJdwRegistry.ts +1 -4
- package/src/jdw/cssRenderBackend.tsx +1 -4
- package/src/layout/index.ts +2 -0
- package/src/modal/modalPosition.ts +3 -6
- package/src/overlay/measureAnchoredOverlayPanel.ts +4 -6
- package/src/primitives/select/Select.tsx +1 -13
- package/src/test-utils/workbenchMonacoMock.tsx +20 -0
- package/src/utils/clamp.ts +4 -0
- package/src/utils/normalizeKeyToken.ts +13 -0
- package/src/utils/readNumber.ts +4 -0
- package/src/widget-tree/WidgetTreeCanvasPreview.tsx +1 -4
- package/src/workbench/commands/ShortcutCommandBridge.tsx +1 -13
- package/src/workbench/commands/WorkbenchQuickOpen.tsx +410 -0
- package/src/workbench/commands/command-ui.ts +3 -0
- package/src/workbench/commands/createWorkspaceFilesQuickOpenProvider.ts +86 -0
- package/src/workbench/commands/quick-open-model.ts +57 -0
- package/src/workbench/index.ts +16 -0
- package/src/workbench/management/ExtensionManagementPanel.tsx +1 -34
- package/src/workbench/management/ExtensionManagementSidebar.tsx +1 -34
- package/src/workbench/management/IntegrationSettingsSurface.tsx +2 -5
- package/src/workbench/management/KeybindingCaptureField.tsx +1 -9
- package/src/workbench/management/WorkbenchNotice.tsx +2 -5
- package/src/workbench/management/extension-management-filters.ts +38 -0
- package/src/workbench/settings/StructuredDataForm.tsx +1 -4
- package/src/workbench/settings/structuredDataFormModel.ts +2 -1
- package/src/workbench/shell/WorkbenchShell.tsx +40 -1
- package/src/workbench/shell/WorkbenchShellTitleBarLayoutControls.tsx +4 -7
- package/src/workbench/shell/activity-bar.css +47 -0
- package/src/workbench/shell/standalone.ts +12 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useId,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type ComponentPropsWithRef,
|
|
9
|
+
type KeyboardEvent,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from 'react';
|
|
12
|
+
import { useModalFocusTrap } from '../../modal/useModalFocusTrap';
|
|
13
|
+
import { Button } from '../../primitives/button';
|
|
14
|
+
import { EmptyState } from '../../primitives/empty-state';
|
|
15
|
+
import { IconButton } from '../../primitives/icon-button';
|
|
16
|
+
import { TextInput } from '../../primitives/text-input';
|
|
17
|
+
import { cxCodicon } from '../../utils/codicon';
|
|
18
|
+
import { cx } from '../../utils/cx';
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_QUICK_OPEN_SEARCH_DEBOUNCE_MS,
|
|
21
|
+
getNextQuickOpenItemIndex,
|
|
22
|
+
isQuickOpenItemSelectable,
|
|
23
|
+
type QuickOpenItem,
|
|
24
|
+
type QuickOpenProvider,
|
|
25
|
+
type QuickOpenSelectContext,
|
|
26
|
+
} from './quick-open-model';
|
|
27
|
+
|
|
28
|
+
interface WorkbenchQuickOpenKeyEvent {
|
|
29
|
+
key: string;
|
|
30
|
+
preventDefault: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function useControllableQuery({
|
|
34
|
+
defaultQuery = '',
|
|
35
|
+
query,
|
|
36
|
+
onQueryChange,
|
|
37
|
+
}: {
|
|
38
|
+
defaultQuery?: string | undefined;
|
|
39
|
+
query?: string | undefined;
|
|
40
|
+
onQueryChange?: ((query: string) => void) | undefined;
|
|
41
|
+
}) {
|
|
42
|
+
const [uncontrolledQuery, setUncontrolledQuery] = useState(defaultQuery);
|
|
43
|
+
const resolvedQuery = query ?? uncontrolledQuery;
|
|
44
|
+
|
|
45
|
+
const setQuery = (nextQuery: string) => {
|
|
46
|
+
if (query === undefined) {
|
|
47
|
+
setUncontrolledQuery(nextQuery);
|
|
48
|
+
}
|
|
49
|
+
onQueryChange?.(nextQuery);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return [resolvedQuery, setQuery] as const;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface WorkbenchQuickOpenProps extends Omit<
|
|
56
|
+
ComponentPropsWithRef<'div'>,
|
|
57
|
+
'children' | 'onSelect' | 'title'
|
|
58
|
+
> {
|
|
59
|
+
activeItemId?: string | undefined;
|
|
60
|
+
closeLabel?: string | undefined;
|
|
61
|
+
debounceMs?: number | undefined;
|
|
62
|
+
defaultQuery?: string | undefined;
|
|
63
|
+
emptyLabel?: ReactNode | undefined;
|
|
64
|
+
onActiveItemChange?: ((itemId: string) => void) | undefined;
|
|
65
|
+
onClose: () => void;
|
|
66
|
+
onQueryChange?: ((query: string) => void) | undefined;
|
|
67
|
+
onSelectItem?: ((item: QuickOpenItem, context: QuickOpenSelectContext) => void) | undefined;
|
|
68
|
+
open?: boolean | undefined;
|
|
69
|
+
placeholder?: string | undefined;
|
|
70
|
+
/** When omitted, the first provider is used. */
|
|
71
|
+
providerId?: string | undefined;
|
|
72
|
+
providers: readonly QuickOpenProvider[];
|
|
73
|
+
query?: string | undefined;
|
|
74
|
+
restoreFocusOnClose?: boolean | undefined;
|
|
75
|
+
title?: ReactNode | undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function WorkbenchQuickOpen({
|
|
79
|
+
activeItemId,
|
|
80
|
+
className,
|
|
81
|
+
closeLabel = 'Close Quick Open',
|
|
82
|
+
debounceMs = DEFAULT_QUICK_OPEN_SEARCH_DEBOUNCE_MS,
|
|
83
|
+
defaultQuery,
|
|
84
|
+
emptyLabel = 'No matching files',
|
|
85
|
+
onActiveItemChange,
|
|
86
|
+
onClose,
|
|
87
|
+
onQueryChange,
|
|
88
|
+
onSelectItem,
|
|
89
|
+
open = true,
|
|
90
|
+
placeholder = 'Search files by name',
|
|
91
|
+
providerId,
|
|
92
|
+
providers,
|
|
93
|
+
query,
|
|
94
|
+
restoreFocusOnClose = true,
|
|
95
|
+
title = 'Quick Open',
|
|
96
|
+
...props
|
|
97
|
+
}: WorkbenchQuickOpenProps) {
|
|
98
|
+
const titleId = useId();
|
|
99
|
+
const listId = useId();
|
|
100
|
+
const dialogRef = useRef<HTMLDivElement>(null);
|
|
101
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
102
|
+
const [uncontrolledActiveItemId, setUncontrolledActiveItemId] = useState<string>();
|
|
103
|
+
const [items, setItems] = useState<QuickOpenItem[]>([]);
|
|
104
|
+
const [searching, setSearching] = useState(false);
|
|
105
|
+
const [resolvedQuery, setResolvedQuery] = useControllableQuery({
|
|
106
|
+
defaultQuery,
|
|
107
|
+
onQueryChange,
|
|
108
|
+
query,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const activeProvider = useMemo(() => {
|
|
112
|
+
if (providerId) {
|
|
113
|
+
return providers.find((provider) => provider.id === providerId) ?? providers[0];
|
|
114
|
+
}
|
|
115
|
+
return providers[0];
|
|
116
|
+
}, [providerId, providers]);
|
|
117
|
+
|
|
118
|
+
const resolvedActiveItemId = activeItemId ?? uncontrolledActiveItemId ?? items[0]?.id;
|
|
119
|
+
const activeIndex = items.findIndex((item) => item.id === resolvedActiveItemId);
|
|
120
|
+
const activeItem = activeIndex >= 0 ? items[activeIndex] : items.find(isQuickOpenItemSelectable);
|
|
121
|
+
|
|
122
|
+
const updateActiveItem = useCallback(
|
|
123
|
+
(itemId: string | undefined) => {
|
|
124
|
+
if (!itemId) return;
|
|
125
|
+
if (activeItemId === undefined) {
|
|
126
|
+
setUncontrolledActiveItemId(itemId);
|
|
127
|
+
}
|
|
128
|
+
onActiveItemChange?.(itemId);
|
|
129
|
+
},
|
|
130
|
+
[activeItemId, onActiveItemChange],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
useModalFocusTrap({
|
|
134
|
+
enabled: open,
|
|
135
|
+
containerRef: dialogRef,
|
|
136
|
+
initialFocusRef: inputRef,
|
|
137
|
+
onClose,
|
|
138
|
+
restoreFocusOnClose,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (!open || !activeProvider) {
|
|
143
|
+
setItems([]);
|
|
144
|
+
setSearching(false);
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let cancelled = false;
|
|
149
|
+
const runSearch = async () => {
|
|
150
|
+
setSearching(true);
|
|
151
|
+
try {
|
|
152
|
+
const nextItems = await Promise.resolve(activeProvider.search(resolvedQuery));
|
|
153
|
+
if (!cancelled) {
|
|
154
|
+
setItems(nextItems);
|
|
155
|
+
}
|
|
156
|
+
} finally {
|
|
157
|
+
if (!cancelled) {
|
|
158
|
+
setSearching(false);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Empty query (recent / top-level) should paint immediately; debounce typed queries.
|
|
164
|
+
const delay = resolvedQuery.trim() ? Math.max(0, debounceMs) : 0;
|
|
165
|
+
if (delay === 0) {
|
|
166
|
+
void runSearch();
|
|
167
|
+
return () => {
|
|
168
|
+
cancelled = true;
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const timer = window.setTimeout(() => {
|
|
173
|
+
void runSearch();
|
|
174
|
+
}, delay);
|
|
175
|
+
|
|
176
|
+
return () => {
|
|
177
|
+
cancelled = true;
|
|
178
|
+
window.clearTimeout(timer);
|
|
179
|
+
};
|
|
180
|
+
}, [activeProvider, debounceMs, open, resolvedQuery]);
|
|
181
|
+
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (!open) return;
|
|
184
|
+
if (activeItemId !== undefined) return;
|
|
185
|
+
|
|
186
|
+
setUncontrolledActiveItemId((currentItemId) => {
|
|
187
|
+
const nextItemId = items.find(isQuickOpenItemSelectable)?.id;
|
|
188
|
+
return currentItemId === nextItemId ? currentItemId : nextItemId;
|
|
189
|
+
});
|
|
190
|
+
}, [activeItemId, items, open]);
|
|
191
|
+
|
|
192
|
+
const selectItem = useCallback(
|
|
193
|
+
(item: QuickOpenItem, index: number) => {
|
|
194
|
+
if (!isQuickOpenItemSelectable(item) || !activeProvider) return;
|
|
195
|
+
onSelectItem?.(item, {
|
|
196
|
+
index,
|
|
197
|
+
providerId: activeProvider.id,
|
|
198
|
+
query: resolvedQuery,
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
[activeProvider, onSelectItem, resolvedQuery],
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const handleQuickOpenKeyDown = useCallback(
|
|
205
|
+
(event: WorkbenchQuickOpenKeyEvent) => {
|
|
206
|
+
if (
|
|
207
|
+
event.key !== 'ArrowDown' &&
|
|
208
|
+
event.key !== 'ArrowUp' &&
|
|
209
|
+
event.key !== 'Home' &&
|
|
210
|
+
event.key !== 'End' &&
|
|
211
|
+
event.key !== 'PageDown' &&
|
|
212
|
+
event.key !== 'PageUp' &&
|
|
213
|
+
event.key !== 'Enter'
|
|
214
|
+
) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (event.key === 'Enter') {
|
|
219
|
+
if (!activeItem) return;
|
|
220
|
+
event.preventDefault();
|
|
221
|
+
selectItem(activeItem, Math.max(activeIndex, 0));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
event.preventDefault();
|
|
226
|
+
|
|
227
|
+
const direction =
|
|
228
|
+
event.key === 'ArrowUp' || event.key === 'End' || event.key === 'PageUp'
|
|
229
|
+
? 'previous'
|
|
230
|
+
: 'next';
|
|
231
|
+
const stepCount = event.key === 'PageDown' || event.key === 'PageUp' ? 5 : 1;
|
|
232
|
+
let nextIndex =
|
|
233
|
+
event.key === 'Home'
|
|
234
|
+
? getNextQuickOpenItemIndex({
|
|
235
|
+
currentIndex: -1,
|
|
236
|
+
direction: 'next',
|
|
237
|
+
items,
|
|
238
|
+
})
|
|
239
|
+
: event.key === 'End'
|
|
240
|
+
? getNextQuickOpenItemIndex({
|
|
241
|
+
currentIndex: 0,
|
|
242
|
+
direction: 'previous',
|
|
243
|
+
items,
|
|
244
|
+
})
|
|
245
|
+
: activeIndex;
|
|
246
|
+
|
|
247
|
+
if (event.key !== 'Home' && event.key !== 'End') {
|
|
248
|
+
for (let step = 0; step < stepCount; step += 1) {
|
|
249
|
+
const steppedIndex = getNextQuickOpenItemIndex({
|
|
250
|
+
currentIndex: nextIndex,
|
|
251
|
+
direction,
|
|
252
|
+
items,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (steppedIndex < 0 || steppedIndex === nextIndex) break;
|
|
256
|
+
nextIndex = steppedIndex;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (nextIndex >= 0) {
|
|
261
|
+
updateActiveItem(items[nextIndex]?.id);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
[activeIndex, activeItem, items, selectItem, updateActiveItem],
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
if (!open) return undefined;
|
|
269
|
+
|
|
270
|
+
const onKeyDown = (event: globalThis.KeyboardEvent) => {
|
|
271
|
+
if (event.defaultPrevented) return;
|
|
272
|
+
handleQuickOpenKeyDown(event);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
window.addEventListener('keydown', onKeyDown);
|
|
276
|
+
return () => {
|
|
277
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
278
|
+
};
|
|
279
|
+
}, [handleQuickOpenKeyDown, open]);
|
|
280
|
+
|
|
281
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
282
|
+
handleQuickOpenKeyDown(event);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
if (!resolvedActiveItemId) return;
|
|
287
|
+
|
|
288
|
+
const activeElement = Array.from(
|
|
289
|
+
dialogRef.current?.querySelectorAll<HTMLElement>('[data-quick-open-id]') ?? [],
|
|
290
|
+
).find((element) => element.dataset.quickOpenId === resolvedActiveItemId);
|
|
291
|
+
|
|
292
|
+
activeElement?.scrollIntoView?.({ block: 'nearest' });
|
|
293
|
+
}, [items, resolvedActiveItemId]);
|
|
294
|
+
|
|
295
|
+
if (!open) return null;
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<div className="ui-workbench-command-palette-overlay" onClick={onClose}>
|
|
299
|
+
<div
|
|
300
|
+
aria-labelledby={titleId}
|
|
301
|
+
aria-modal="true"
|
|
302
|
+
className={cx('ui-workbench-command-palette', className)}
|
|
303
|
+
data-testid="workbench-quick-open"
|
|
304
|
+
role="dialog"
|
|
305
|
+
{...props}
|
|
306
|
+
ref={dialogRef}
|
|
307
|
+
onClick={(event) => event.stopPropagation()}
|
|
308
|
+
>
|
|
309
|
+
<div className="ui-workbench-command-palette__header">
|
|
310
|
+
<span id={titleId} className="ui-workbench-command-palette__title">
|
|
311
|
+
{title}
|
|
312
|
+
</span>
|
|
313
|
+
<IconButton
|
|
314
|
+
className="ui-workbench-command-palette__close"
|
|
315
|
+
icon="codicon-close"
|
|
316
|
+
label={closeLabel}
|
|
317
|
+
onClick={onClose}
|
|
318
|
+
/>
|
|
319
|
+
</div>
|
|
320
|
+
<div className="ui-workbench-command-palette__search">
|
|
321
|
+
<i aria-hidden="true" className="codicon codicon-search" />
|
|
322
|
+
<TextInput
|
|
323
|
+
ref={inputRef}
|
|
324
|
+
aria-activedescendant={
|
|
325
|
+
activeItem ? `${listId}-${activeItem.id.replace(/[^\w-]/g, '_')}` : undefined
|
|
326
|
+
}
|
|
327
|
+
aria-controls={listId}
|
|
328
|
+
aria-label={placeholder}
|
|
329
|
+
className="ui-workbench-command-palette__input"
|
|
330
|
+
controlWidth="full"
|
|
331
|
+
placeholder={placeholder}
|
|
332
|
+
type="search"
|
|
333
|
+
value={resolvedQuery}
|
|
334
|
+
onValueChange={setResolvedQuery}
|
|
335
|
+
onKeyDown={handleKeyDown}
|
|
336
|
+
/>
|
|
337
|
+
</div>
|
|
338
|
+
<div
|
|
339
|
+
id={listId}
|
|
340
|
+
aria-busy={searching || undefined}
|
|
341
|
+
aria-label="Quick Open results"
|
|
342
|
+
className={cx('ui-workbench-command-list', 'ui-workbench-scrollbar')}
|
|
343
|
+
role="listbox"
|
|
344
|
+
>
|
|
345
|
+
{items.length === 0 ? (
|
|
346
|
+
<EmptyState compact icon="codicon-search">
|
|
347
|
+
{emptyLabel}
|
|
348
|
+
</EmptyState>
|
|
349
|
+
) : (
|
|
350
|
+
items.map((item, index) => {
|
|
351
|
+
const active = item.id === resolvedActiveItemId;
|
|
352
|
+
const itemDomId = `${listId}-${item.id.replace(/[^\w-]/g, '_')}`;
|
|
353
|
+
const descriptionId = item.description ? `${itemDomId}-description` : undefined;
|
|
354
|
+
|
|
355
|
+
return (
|
|
356
|
+
<Button
|
|
357
|
+
key={item.id}
|
|
358
|
+
id={itemDomId}
|
|
359
|
+
aria-describedby={descriptionId}
|
|
360
|
+
aria-selected={active}
|
|
361
|
+
className="ui-workbench-command-item"
|
|
362
|
+
data-active={active ? 'true' : undefined}
|
|
363
|
+
data-quick-open-id={item.id}
|
|
364
|
+
disabled={item.disabled}
|
|
365
|
+
role="option"
|
|
366
|
+
onClick={() => selectItem(item, index)}
|
|
367
|
+
onMouseEnter={() => updateActiveItem(item.id)}
|
|
368
|
+
>
|
|
369
|
+
<span className="ui-workbench-command-item__icon">
|
|
370
|
+
{item.icon ? <i aria-hidden="true" className={cxCodicon(item.icon)} /> : null}
|
|
371
|
+
</span>
|
|
372
|
+
<span className="ui-workbench-command-item__content">
|
|
373
|
+
<span className="ui-workbench-command-item__label">{item.label}</span>
|
|
374
|
+
{item.description ? (
|
|
375
|
+
<span id={descriptionId} className="ui-workbench-command-item__description">
|
|
376
|
+
{item.description}
|
|
377
|
+
</span>
|
|
378
|
+
) : null}
|
|
379
|
+
</span>
|
|
380
|
+
<span className="ui-workbench-command-item__meta">
|
|
381
|
+
{item.detail ? (
|
|
382
|
+
<span className="ui-workbench-command-item__category">{item.detail}</span>
|
|
383
|
+
) : activeProvider ? (
|
|
384
|
+
<span className="ui-workbench-command-item__category">
|
|
385
|
+
{activeProvider.label}
|
|
386
|
+
</span>
|
|
387
|
+
) : null}
|
|
388
|
+
</span>
|
|
389
|
+
</Button>
|
|
390
|
+
);
|
|
391
|
+
})
|
|
392
|
+
)}
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export type { QuickOpenItem, QuickOpenProvider, QuickOpenSelectContext } from './quick-open-model';
|
|
400
|
+
export {
|
|
401
|
+
DEFAULT_QUICK_OPEN_SEARCH_DEBOUNCE_MS,
|
|
402
|
+
getNextQuickOpenItemIndex,
|
|
403
|
+
isQuickOpenItemSelectable,
|
|
404
|
+
} from './quick-open-model';
|
|
405
|
+
export {
|
|
406
|
+
WORKSPACE_FILES_QUICK_OPEN_PROVIDER_ID,
|
|
407
|
+
createWorkspaceFilesQuickOpenProvider,
|
|
408
|
+
resolveQuickOpenItemPath,
|
|
409
|
+
} from './createWorkspaceFilesQuickOpenProvider';
|
|
410
|
+
export type { CreateWorkspaceFilesQuickOpenProviderOptions } from './createWorkspaceFilesQuickOpenProvider';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { searchWorkspaceFiles, type WorkspaceFile } from '@workbench-kit/workspace';
|
|
2
|
+
|
|
3
|
+
import type { QuickOpenItem, QuickOpenProvider } from './quick-open-model';
|
|
4
|
+
|
|
5
|
+
export const WORKSPACE_FILES_QUICK_OPEN_PROVIDER_ID = 'workspace.files' as const;
|
|
6
|
+
|
|
7
|
+
export interface CreateWorkspaceFilesQuickOpenProviderOptions {
|
|
8
|
+
/** Current workspace files, or a getter read on each search. */
|
|
9
|
+
files: readonly WorkspaceFile[] | (() => readonly WorkspaceFile[]);
|
|
10
|
+
id?: string | undefined;
|
|
11
|
+
label?: string | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* When the query is empty, these paths appear first (still present in `files`).
|
|
14
|
+
* Remaining files follow in `searchWorkspaceFiles` order.
|
|
15
|
+
*/
|
|
16
|
+
recentPaths?: readonly string[] | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveFiles(
|
|
20
|
+
files: CreateWorkspaceFilesQuickOpenProviderOptions['files'],
|
|
21
|
+
): readonly WorkspaceFile[] {
|
|
22
|
+
return typeof files === 'function' ? files() : files;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toQuickOpenItem(path: string, matchedBy?: string, preview?: string): QuickOpenItem {
|
|
26
|
+
const segments = path.split('/');
|
|
27
|
+
const label = segments[segments.length - 1] || path;
|
|
28
|
+
const parent = segments.length > 1 ? segments.slice(0, -1).join('/') : undefined;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
data: { path },
|
|
32
|
+
description: parent,
|
|
33
|
+
detail: matchedBy ?? preview,
|
|
34
|
+
icon: 'codicon-file',
|
|
35
|
+
id: path,
|
|
36
|
+
label,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createWorkspaceFilesQuickOpenProvider(
|
|
41
|
+
options: CreateWorkspaceFilesQuickOpenProviderOptions,
|
|
42
|
+
): QuickOpenProvider {
|
|
43
|
+
const providerId = options.id ?? WORKSPACE_FILES_QUICK_OPEN_PROVIDER_ID;
|
|
44
|
+
const providerLabel = options.label ?? 'Files';
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
id: providerId,
|
|
48
|
+
label: providerLabel,
|
|
49
|
+
search(query: string) {
|
|
50
|
+
const files = [...resolveFiles(options.files)];
|
|
51
|
+
const results = searchWorkspaceFiles(files, query);
|
|
52
|
+
const items = results.map((result) =>
|
|
53
|
+
toQuickOpenItem(result.path, result.matchedBy, result.preview),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (query.trim() || !options.recentPaths?.length) {
|
|
57
|
+
return items;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const recentSet = new Set(options.recentPaths);
|
|
61
|
+
const byPath = new Map(items.map((item) => [item.id, item]));
|
|
62
|
+
const recentItems: QuickOpenItem[] = [];
|
|
63
|
+
|
|
64
|
+
for (const path of options.recentPaths) {
|
|
65
|
+
const item = byPath.get(path);
|
|
66
|
+
if (item) {
|
|
67
|
+
recentItems.push({ ...item, detail: 'Recent' });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const rest = items.filter((item) => !recentSet.has(item.id));
|
|
72
|
+
return [...recentItems, ...rest];
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function resolveQuickOpenItemPath(item: QuickOpenItem): string | undefined {
|
|
78
|
+
if (typeof item.data === 'object' && item.data !== null && 'path' in item.data) {
|
|
79
|
+
const path = (item.data as { path?: unknown }).path;
|
|
80
|
+
if (typeof path === 'string' && path.length > 0) {
|
|
81
|
+
return path;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return item.id || undefined;
|
|
86
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface QuickOpenItem {
|
|
2
|
+
/** Stable item id (often a workspace path). */
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
description?: string | undefined;
|
|
6
|
+
detail?: string | undefined;
|
|
7
|
+
icon?: string | undefined;
|
|
8
|
+
disabled?: boolean | undefined;
|
|
9
|
+
/** Provider-specific payload (e.g. `{ path }`). */
|
|
10
|
+
data?: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface QuickOpenProvider {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
search: (query: string) => Promise<QuickOpenItem[]> | QuickOpenItem[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface QuickOpenSelectContext {
|
|
20
|
+
index: number;
|
|
21
|
+
providerId: string;
|
|
22
|
+
query: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isQuickOpenItemSelectable(item: QuickOpenItem) {
|
|
26
|
+
return !item.disabled;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getNextQuickOpenItemIndex({
|
|
30
|
+
currentIndex,
|
|
31
|
+
direction,
|
|
32
|
+
items,
|
|
33
|
+
}: {
|
|
34
|
+
currentIndex: number;
|
|
35
|
+
direction: 'next' | 'previous';
|
|
36
|
+
items: readonly QuickOpenItem[];
|
|
37
|
+
}) {
|
|
38
|
+
if (items.length === 0) {
|
|
39
|
+
return -1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const step = direction === 'next' ? 1 : -1;
|
|
43
|
+
let nextIndex = currentIndex;
|
|
44
|
+
|
|
45
|
+
for (let attempt = 0; attempt < items.length; attempt += 1) {
|
|
46
|
+
nextIndex = (nextIndex + step + items.length) % items.length;
|
|
47
|
+
const item = items[nextIndex];
|
|
48
|
+
if (item && isQuickOpenItemSelectable(item)) {
|
|
49
|
+
return nextIndex;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return -1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Default debounce for provider search while typing. */
|
|
57
|
+
export const DEFAULT_QUICK_OPEN_SEARCH_DEBOUNCE_MS = 200;
|
package/src/workbench/index.ts
CHANGED
|
@@ -59,6 +59,15 @@ export {
|
|
|
59
59
|
isWorkbenchCommandRunnable,
|
|
60
60
|
resolveWorkbenchCommandExecutionPolicy,
|
|
61
61
|
} from './commands/CommandPalette';
|
|
62
|
+
export {
|
|
63
|
+
DEFAULT_QUICK_OPEN_SEARCH_DEBOUNCE_MS,
|
|
64
|
+
WORKSPACE_FILES_QUICK_OPEN_PROVIDER_ID,
|
|
65
|
+
WorkbenchQuickOpen,
|
|
66
|
+
createWorkspaceFilesQuickOpenProvider,
|
|
67
|
+
getNextQuickOpenItemIndex,
|
|
68
|
+
isQuickOpenItemSelectable,
|
|
69
|
+
resolveQuickOpenItemPath,
|
|
70
|
+
} from './commands/WorkbenchQuickOpen';
|
|
62
71
|
export { WorkbenchMarkdownPreview } from './markdown/MarkdownPreview';
|
|
63
72
|
export type { WorkbenchMarkdownPreviewProps } from './markdown/MarkdownPreview';
|
|
64
73
|
export { sanitizeMarkdownHref } from './markdown/sanitizeMarkdownHref';
|
|
@@ -215,6 +224,13 @@ export type {
|
|
|
215
224
|
WorkbenchCommandSuggestProps,
|
|
216
225
|
} from './commands/CommandPalette';
|
|
217
226
|
export type { ResolveWorkbenchCommandExecutionPolicyInput } from './commands/CommandPalette';
|
|
227
|
+
export type {
|
|
228
|
+
CreateWorkspaceFilesQuickOpenProviderOptions,
|
|
229
|
+
QuickOpenItem,
|
|
230
|
+
QuickOpenProvider,
|
|
231
|
+
QuickOpenSelectContext,
|
|
232
|
+
WorkbenchQuickOpenProps,
|
|
233
|
+
} from './commands/WorkbenchQuickOpen';
|
|
218
234
|
export type {
|
|
219
235
|
UseWorkbenchShortcutCommandsOptions,
|
|
220
236
|
WorkbenchShortcutCommandBinding,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
formatExtensionCategoryLabel,
|
|
9
9
|
} from './extension-category-display.js';
|
|
10
10
|
import { resolveExtensionInstallOptions } from './extension-install-approval.js';
|
|
11
|
+
import { filterBrowseEntries, filterInstalledEntries } from './extension-management-filters.js';
|
|
11
12
|
import { ManagementFilterChips } from './ManagementFilterChips.js';
|
|
12
13
|
import { ManagementCard, ManagementCardList } from './ManagementCard.js';
|
|
13
14
|
import { ManagementGroup, ManagementGroups } from './ManagementGroup.js';
|
|
@@ -491,37 +492,3 @@ function groupBrowseEntries(entries: readonly ExtensionCatalogBrowseEntry[]) {
|
|
|
491
492
|
})
|
|
492
493
|
.map(([category, categoryEntries]) => ({ category, entries: categoryEntries }));
|
|
493
494
|
}
|
|
494
|
-
|
|
495
|
-
function filterInstalledEntries(entries: readonly ExtensionManagementEntry[], query: string) {
|
|
496
|
-
const normalized = query.trim().toLowerCase();
|
|
497
|
-
if (!normalized) {
|
|
498
|
-
return entries;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
return entries.filter((entry) =>
|
|
502
|
-
[entry.displayName, entry.id, entry.category, entry.description ?? '']
|
|
503
|
-
.join(' ')
|
|
504
|
-
.toLowerCase()
|
|
505
|
-
.includes(normalized),
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function filterBrowseEntries(
|
|
510
|
-
entries: readonly ExtensionCatalogBrowseEntry[],
|
|
511
|
-
query: string,
|
|
512
|
-
category?: string,
|
|
513
|
-
) {
|
|
514
|
-
const normalized = query.trim().toLowerCase();
|
|
515
|
-
return entries.filter((entry) => {
|
|
516
|
-
if (category && entry.category !== category) {
|
|
517
|
-
return false;
|
|
518
|
-
}
|
|
519
|
-
if (!normalized) {
|
|
520
|
-
return true;
|
|
521
|
-
}
|
|
522
|
-
return [entry.displayName, entry.id, entry.category, entry.description]
|
|
523
|
-
.join(' ')
|
|
524
|
-
.toLowerCase()
|
|
525
|
-
.includes(normalized);
|
|
526
|
-
});
|
|
527
|
-
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
formatExtensionCategoryLabel,
|
|
19
19
|
} from './extension-category-display.js';
|
|
20
20
|
import { resolveExtensionInstallOptions } from './extension-install-approval.js';
|
|
21
|
+
import { filterBrowseEntries, filterInstalledEntries } from './extension-management-filters.js';
|
|
21
22
|
import { ManagementFilterChips } from './ManagementFilterChips.js';
|
|
22
23
|
import type {
|
|
23
24
|
ExtensionCatalogBrowseEntry,
|
|
@@ -473,37 +474,3 @@ function formatFeatureBadge(label: string, count: number) {
|
|
|
473
474
|
|
|
474
475
|
return `${label} ${count}`;
|
|
475
476
|
}
|
|
476
|
-
|
|
477
|
-
function filterInstalledEntries(entries: readonly ExtensionManagementEntry[], query: string) {
|
|
478
|
-
const normalized = query.trim().toLowerCase();
|
|
479
|
-
if (!normalized) {
|
|
480
|
-
return entries;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
return entries.filter((entry) =>
|
|
484
|
-
[entry.displayName, entry.id, entry.category, entry.description ?? '']
|
|
485
|
-
.join(' ')
|
|
486
|
-
.toLowerCase()
|
|
487
|
-
.includes(normalized),
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function filterBrowseEntries(
|
|
492
|
-
entries: readonly ExtensionCatalogBrowseEntry[],
|
|
493
|
-
query: string,
|
|
494
|
-
category?: string,
|
|
495
|
-
) {
|
|
496
|
-
const normalized = query.trim().toLowerCase();
|
|
497
|
-
return entries.filter((entry) => {
|
|
498
|
-
if (category && entry.category !== category) {
|
|
499
|
-
return false;
|
|
500
|
-
}
|
|
501
|
-
if (!normalized) {
|
|
502
|
-
return true;
|
|
503
|
-
}
|
|
504
|
-
return [entry.displayName, entry.id, entry.category, entry.description]
|
|
505
|
-
.join(' ')
|
|
506
|
-
.toLowerCase()
|
|
507
|
-
.includes(normalized);
|
|
508
|
-
});
|
|
509
|
-
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
WorkbenchPropertySection,
|
|
8
8
|
WorkbenchPropertyStack,
|
|
9
9
|
} from '../../primitives/index';
|
|
10
|
+
import { cx } from '../../utils/cx';
|
|
10
11
|
|
|
11
12
|
type IntegrationSettingsDensity = 'compact' | 'default';
|
|
12
13
|
type IntegrationSettingsInset = 'default' | 'flush';
|
|
@@ -206,7 +207,7 @@ export function IntegrationAccountRowEditor({
|
|
|
206
207
|
export function IntegrationBodyText({ children, size = 'default' }: IntegrationBodyTextProps) {
|
|
207
208
|
return (
|
|
208
209
|
<WorkbenchPropertyHint
|
|
209
|
-
className={
|
|
210
|
+
className={cx('block', size === 'compact' ? 'text-xs' : undefined)}
|
|
210
211
|
data-size={size}
|
|
211
212
|
>
|
|
212
213
|
{children}
|
|
@@ -284,7 +285,3 @@ function IntegrationEditorDescription({ children }: { children?: ReactNode }) {
|
|
|
284
285
|
function hasRenderableNode(node: ReactNode | undefined): boolean {
|
|
285
286
|
return node !== undefined && node !== null && node !== false;
|
|
286
287
|
}
|
|
287
|
-
|
|
288
|
-
function joinClasses(...classes: Array<string | undefined>): string {
|
|
289
|
-
return classes.filter(Boolean).join(' ');
|
|
290
|
-
}
|