@workbench-kit/react 0.0.1-prototype.0

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.
Files changed (51) hide show
  1. package/package.json +62 -0
  2. package/src/index.ts +43 -0
  3. package/src/layout/Panel.tsx +27 -0
  4. package/src/layout/SideBarViewFrame.tsx +328 -0
  5. package/src/modal/ConfirmDialog.tsx +53 -0
  6. package/src/modal/Modal.tsx +110 -0
  7. package/src/overlay/ContextMenu.tsx +145 -0
  8. package/src/primitives/Badge.tsx +12 -0
  9. package/src/primitives/Button.tsx +14 -0
  10. package/src/primitives/Checkbox.tsx +15 -0
  11. package/src/primitives/EmptyState.tsx +26 -0
  12. package/src/primitives/Field.tsx +38 -0
  13. package/src/primitives/IconButton.tsx +32 -0
  14. package/src/primitives/Select.tsx +12 -0
  15. package/src/primitives/TextInput.tsx +24 -0
  16. package/src/primitives/Toolbar.tsx +8 -0
  17. package/src/styles.css +1762 -0
  18. package/src/utils/cx.ts +3 -0
  19. package/src/workbench/ActivityBar.tsx +52 -0
  20. package/src/workbench/SplitView.tsx +136 -0
  21. package/src/workbench/StatusBar.tsx +123 -0
  22. package/src/workbench/WorkbenchShell.tsx +70 -0
  23. package/src/workbench/WorkbenchStandaloneShell.tsx +291 -0
  24. package/src/workbench/chat/ChatComposer.tsx +148 -0
  25. package/src/workbench/chat/ChatMessageItem.tsx +35 -0
  26. package/src/workbench/chat/ChatMessageList.tsx +49 -0
  27. package/src/workbench/chat/ChatPanel.tsx +55 -0
  28. package/src/workbench/chat/index.ts +9 -0
  29. package/src/workbench/chat/types.ts +10 -0
  30. package/src/workbench/commands.ts +490 -0
  31. package/src/workbench/index.ts +98 -0
  32. package/src/workbench/settings/WorkbenchSettingsModal.tsx +188 -0
  33. package/src/workbench/settings/WorkbenchSettingsNav.tsx +41 -0
  34. package/src/workbench/settings/WorkbenchSettingsSection.tsx +30 -0
  35. package/src/workbench/settings/index.ts +7 -0
  36. package/src/workbench/settings/types.ts +16 -0
  37. package/src/workbench/shellState.ts +203 -0
  38. package/src/workbench/standalone.ts +94 -0
  39. package/src/workbench/workspace/WorkspaceEditor.tsx +226 -0
  40. package/src/workbench/workspace/WorkspaceEditorPanel.tsx +353 -0
  41. package/src/workbench/workspace/WorkspaceExplorer.tsx +524 -0
  42. package/src/workbench/workspace/WorkspaceFileIcon.tsx +149 -0
  43. package/src/workbench/workspace/WorkspaceHighlightedText.tsx +22 -0
  44. package/src/workbench/workspace/WorkspaceSearchPanel.tsx +113 -0
  45. package/src/workbench/workspace/WorkspaceSearchResults.tsx +47 -0
  46. package/src/workbench/workspace/index.ts +98 -0
  47. package/src/workbench/workspace/path.ts +10 -0
  48. package/src/workbench/workspace/search.ts +6 -0
  49. package/src/workbench/workspace/tree.ts +1 -0
  50. package/src/workbench/workspace/types.ts +10 -0
  51. package/src/workbench/workspace/useVirtualWorkspace.ts +98 -0
@@ -0,0 +1,524 @@
1
+ import {
2
+ Fragment,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ type CSSProperties,
8
+ type DragEvent,
9
+ type KeyboardEvent,
10
+ type MouseEvent,
11
+ type ReactNode,
12
+ } from 'react';
13
+ import {
14
+ getWorkspaceSelectionActionPaths,
15
+ normalizeWorkspaceSelectionPaths,
16
+ parentPathOf,
17
+ updateWorkspaceSelection,
18
+ type WorkspaceSelectionMode,
19
+ type WorkspaceSelectionState,
20
+ } from '@workbench-kit/workspace';
21
+ import { SideBarList, SideBarListItem } from '../../layout/SideBarViewFrame';
22
+ import { flattenWorkspaceTree } from './tree';
23
+ import { WorkspaceFileIcon } from './WorkspaceFileIcon';
24
+ import type { WorkspaceTreeNode } from './types';
25
+
26
+ export const WORKSPACE_EXPLORER_DRAG_DATA_TYPE = 'application/x-newchobo-ui-workspace-paths';
27
+ export const WORKSPACE_EXPLORER_DRAG_METADATA_DATA_TYPE = `${WORKSPACE_EXPLORER_DRAG_DATA_TYPE}.metadata`;
28
+
29
+ export interface WorkspaceExplorerSelectionChangeMeta {
30
+ event: DragEvent<HTMLButtonElement> | MouseEvent<HTMLButtonElement>;
31
+ mode: WorkspaceSelectionMode;
32
+ node: WorkspaceTreeNode;
33
+ reason: 'click' | 'context-menu' | 'drag-start';
34
+ }
35
+
36
+ export interface WorkspaceExplorerItemActionMeta {
37
+ actionPaths: string[];
38
+ selected: boolean;
39
+ selection: WorkspaceSelectionState;
40
+ }
41
+
42
+ export type WorkspaceExplorerItemContextMenuMeta = WorkspaceExplorerItemActionMeta;
43
+
44
+ export interface WorkspaceExplorerDragMetadataContext {
45
+ event: DragEvent<HTMLButtonElement>;
46
+ node: WorkspaceTreeNode;
47
+ sourcePaths: string[];
48
+ selection: WorkspaceSelectionState;
49
+ }
50
+
51
+ export type WorkspaceExplorerDragMetadata = unknown;
52
+
53
+ export type WorkspaceExplorerDragMetadataFactory<TMetadata = WorkspaceExplorerDragMetadata> = (
54
+ meta: WorkspaceExplorerDragMetadataContext,
55
+ ) => TMetadata | undefined;
56
+
57
+ export interface WorkspaceExplorerItemKeyboardActionMeta extends WorkspaceExplorerItemActionMeta {
58
+ event: KeyboardEvent<HTMLButtonElement>;
59
+ node: WorkspaceTreeNode;
60
+ }
61
+
62
+ export interface WorkspaceExplorerMoveRequestMeta {
63
+ event: DragEvent<HTMLButtonElement | HTMLUListElement>;
64
+ sourcePaths: string[];
65
+ targetFolderPath: string;
66
+ targetNode?: WorkspaceTreeNode;
67
+ }
68
+
69
+ export type WorkspaceExplorerInlineEditKind =
70
+ | 'create-file'
71
+ | 'create-folder'
72
+ | 'rename-file'
73
+ | 'rename-folder';
74
+
75
+ export interface WorkspaceExplorerInlineEditState {
76
+ error?: ReactNode;
77
+ id?: string;
78
+ kind: WorkspaceExplorerInlineEditKind;
79
+ parentPath?: string;
80
+ path?: string;
81
+ value: string;
82
+ }
83
+
84
+ export interface WorkspaceExplorerInlineEditCommitMeta {
85
+ edit: WorkspaceExplorerInlineEditState;
86
+ value: string;
87
+ }
88
+
89
+ export interface WorkspaceExplorerProps {
90
+ activePath?: string;
91
+ dragDataType?: string;
92
+ dragMetadataDataType?: string;
93
+ dragMetadataFactory?: WorkspaceExplorerDragMetadataFactory;
94
+ expandedPaths: Set<string>;
95
+ filterQuery?: string;
96
+ inlineEdit?: WorkspaceExplorerInlineEditState;
97
+ nodes: WorkspaceTreeNode[];
98
+ onActivateFile: (path: string) => void;
99
+ onInlineEditCancel?: (edit: WorkspaceExplorerInlineEditState) => void;
100
+ onInlineEditCommit?: (meta: WorkspaceExplorerInlineEditCommitMeta) => void;
101
+ onInlineEditValueChange?: (value: string, edit: WorkspaceExplorerInlineEditState) => void;
102
+ onItemContextMenu?: (
103
+ event: MouseEvent<HTMLButtonElement>,
104
+ node: WorkspaceTreeNode,
105
+ meta: WorkspaceExplorerItemContextMenuMeta,
106
+ ) => void;
107
+ onRequestDelete?: (meta: WorkspaceExplorerItemKeyboardActionMeta) => void;
108
+ onRequestMove?: (meta: WorkspaceExplorerMoveRequestMeta) => void;
109
+ onRequestRename?: (meta: WorkspaceExplorerItemKeyboardActionMeta) => void;
110
+ onSelectionChange?: (
111
+ selection: WorkspaceSelectionState,
112
+ meta: WorkspaceExplorerSelectionChangeMeta,
113
+ ) => void;
114
+ onToggleFolder: (path: string) => void;
115
+ selectedPaths?: Iterable<string>;
116
+ selectionAnchorPath?: string;
117
+ }
118
+
119
+ export function WorkspaceExplorer({
120
+ activePath,
121
+ dragDataType = WORKSPACE_EXPLORER_DRAG_DATA_TYPE,
122
+ dragMetadataDataType = WORKSPACE_EXPLORER_DRAG_METADATA_DATA_TYPE,
123
+ dragMetadataFactory,
124
+ expandedPaths,
125
+ filterQuery = '',
126
+ inlineEdit,
127
+ nodes,
128
+ onActivateFile,
129
+ onInlineEditCancel,
130
+ onInlineEditCommit,
131
+ onInlineEditValueChange,
132
+ onItemContextMenu,
133
+ onRequestDelete,
134
+ onRequestMove,
135
+ onRequestRename,
136
+ onSelectionChange,
137
+ onToggleFolder,
138
+ selectedPaths = [],
139
+ selectionAnchorPath,
140
+ }: WorkspaceExplorerProps) {
141
+ const draggedPathsRef = useRef<string[]>([]);
142
+ const inlineEditInputRef = useRef<HTMLInputElement>(null);
143
+ const [dropTargetPath, setDropTargetPath] = useState<string | null>(null);
144
+ const visibleNodes = useMemo(
145
+ () => flattenWorkspaceTree({ expandedPaths, filterQuery, nodes }),
146
+ [expandedPaths, filterQuery, nodes],
147
+ );
148
+ const visibleFilePaths = useMemo(
149
+ () => visibleNodes.filter(({ node }) => node.type === 'file').map(({ node }) => node.path),
150
+ [visibleNodes],
151
+ );
152
+ const normalizedSelectedPaths = useMemo(
153
+ () => normalizeWorkspaceSelectionPaths(selectedPaths),
154
+ [selectedPaths],
155
+ );
156
+ const selectedPathSet = useMemo(
157
+ () => new Set(normalizedSelectedPaths),
158
+ [normalizedSelectedPaths],
159
+ );
160
+ const currentSelection = useMemo<WorkspaceSelectionState>(
161
+ () => ({
162
+ anchorPath: selectionAnchorPath,
163
+ paths: normalizedSelectedPaths,
164
+ }),
165
+ [normalizedSelectedPaths, selectionAnchorPath],
166
+ );
167
+ const inlineEditKey = inlineEdit
168
+ ? (inlineEdit.id ??
169
+ `${inlineEdit.kind}:${inlineEdit.path ?? ''}:${inlineEdit.parentPath ?? ''}`)
170
+ : undefined;
171
+
172
+ useEffect(() => {
173
+ const input = inlineEditInputRef.current;
174
+ if (!input) return;
175
+
176
+ input.focus();
177
+ input.select();
178
+ }, [inlineEditKey]);
179
+
180
+ const selectFile = (event: MouseEvent<HTMLButtonElement>, node: WorkspaceTreeNode) => {
181
+ const mode = resolveSelectionMode(event);
182
+
183
+ if (onSelectionChange) {
184
+ onSelectionChange(
185
+ updateWorkspaceSelection({
186
+ mode,
187
+ orderedPaths: visibleFilePaths,
188
+ selection: currentSelection,
189
+ targetPath: node.path,
190
+ }),
191
+ { event, mode, node, reason: 'click' },
192
+ );
193
+ }
194
+
195
+ if (mode === 'single' || !onSelectionChange) {
196
+ onActivateFile(node.path);
197
+ }
198
+ };
199
+
200
+ const getItemActionMeta = (node: WorkspaceTreeNode): WorkspaceExplorerItemActionMeta => {
201
+ const selected = node.type === 'file' && selectedPathSet.has(node.path);
202
+ const selection =
203
+ node.type === 'file' && !selected
204
+ ? {
205
+ anchorPath: node.path,
206
+ paths: [node.path],
207
+ }
208
+ : currentSelection;
209
+
210
+ return {
211
+ actionPaths: getWorkspaceSelectionActionPaths({
212
+ selectedPaths: selection.paths,
213
+ targetPath: node.path,
214
+ }),
215
+ selected,
216
+ selection,
217
+ };
218
+ };
219
+
220
+ const handleItemContextMenu = (event: MouseEvent<HTMLButtonElement>, node: WorkspaceTreeNode) => {
221
+ const meta = getItemActionMeta(node);
222
+
223
+ if (node.type === 'file' && !meta.selected) {
224
+ onSelectionChange?.(meta.selection, {
225
+ event,
226
+ mode: 'single',
227
+ node,
228
+ reason: 'context-menu',
229
+ });
230
+ }
231
+
232
+ onItemContextMenu?.(event, node, meta);
233
+ };
234
+
235
+ const handleItemKeyDown = (event: KeyboardEvent<HTMLButtonElement>, node: WorkspaceTreeNode) => {
236
+ if (event.key !== 'Delete' && event.key !== 'F2') return;
237
+
238
+ const meta = getItemActionMeta(node);
239
+
240
+ if (event.key === 'Delete') {
241
+ if (node.type === 'file' && meta.actionPaths.length === 0) return;
242
+
243
+ event.preventDefault();
244
+ onRequestDelete?.({ ...meta, event, node });
245
+ return;
246
+ }
247
+
248
+ if (meta.actionPaths.length !== 1) return;
249
+
250
+ event.preventDefault();
251
+ onRequestRename?.({ ...meta, event, node });
252
+ };
253
+
254
+ const canDropToFolder = (targetFolderPath: string) =>
255
+ Boolean(onRequestMove) &&
256
+ draggedPathsRef.current.length > 0 &&
257
+ draggedPathsRef.current.some((sourcePath) => parentPathOf(sourcePath) !== targetFolderPath);
258
+
259
+ const handleItemDragStart = (event: DragEvent<HTMLButtonElement>, node: WorkspaceTreeNode) => {
260
+ if (!onRequestMove || node.type !== 'file') {
261
+ event.preventDefault();
262
+ return;
263
+ }
264
+
265
+ const meta = getItemActionMeta(node);
266
+ const sourcePaths = meta.actionPaths;
267
+ if (sourcePaths.length === 0) {
268
+ event.preventDefault();
269
+ return;
270
+ }
271
+
272
+ if (!meta.selected) {
273
+ onSelectionChange?.(meta.selection, {
274
+ event,
275
+ mode: 'single',
276
+ node,
277
+ reason: 'drag-start',
278
+ });
279
+ }
280
+
281
+ event.dataTransfer.effectAllowed = 'move';
282
+ event.dataTransfer.setData(dragDataType, JSON.stringify(sourcePaths));
283
+ if (dragMetadataFactory) {
284
+ const metadata = dragMetadataFactory({
285
+ event,
286
+ node,
287
+ sourcePaths,
288
+ selection: meta.selection,
289
+ });
290
+
291
+ if (metadata !== undefined) {
292
+ event.dataTransfer.setData(dragMetadataDataType, JSON.stringify(metadata));
293
+ }
294
+ }
295
+ event.dataTransfer.setData('text/plain', sourcePaths.join('\n'));
296
+ draggedPathsRef.current = sourcePaths;
297
+ setDropTargetPath(null);
298
+ };
299
+
300
+ const handleDragEnd = () => {
301
+ draggedPathsRef.current = [];
302
+ setDropTargetPath(null);
303
+ };
304
+
305
+ const handleDropTargetDragOver = (
306
+ event: DragEvent<HTMLButtonElement | HTMLUListElement>,
307
+ targetFolderPath: string,
308
+ ) => {
309
+ if (!canDropToFolder(targetFolderPath)) return;
310
+
311
+ if (targetFolderPath) {
312
+ event.stopPropagation();
313
+ }
314
+ event.preventDefault();
315
+ event.dataTransfer.dropEffect = 'move';
316
+ setDropTargetPath(targetFolderPath);
317
+ };
318
+
319
+ const handleDropTargetDragLeave = (
320
+ event: DragEvent<HTMLButtonElement | HTMLUListElement>,
321
+ targetFolderPath: string,
322
+ ) => {
323
+ if (targetFolderPath) {
324
+ event.stopPropagation();
325
+ }
326
+ if (event.currentTarget.contains(event.relatedTarget as Node | null)) return;
327
+
328
+ setDropTargetPath((currentPath) => (currentPath === targetFolderPath ? null : currentPath));
329
+ };
330
+
331
+ const handleDrop = (
332
+ event: DragEvent<HTMLButtonElement | HTMLUListElement>,
333
+ targetFolderPath: string,
334
+ targetNode?: WorkspaceTreeNode,
335
+ ) => {
336
+ if (!canDropToFolder(targetFolderPath)) return;
337
+
338
+ if (targetFolderPath) {
339
+ event.stopPropagation();
340
+ }
341
+ event.preventDefault();
342
+ onRequestMove?.({
343
+ event,
344
+ sourcePaths: draggedPathsRef.current,
345
+ targetFolderPath,
346
+ targetNode,
347
+ });
348
+ draggedPathsRef.current = [];
349
+ setDropTargetPath(null);
350
+ };
351
+
352
+ const commitInlineEdit = () => {
353
+ if (!inlineEdit) return;
354
+
355
+ onInlineEditCommit?.({
356
+ edit: inlineEdit,
357
+ value: inlineEdit.value.trim(),
358
+ });
359
+ };
360
+
361
+ const handleInlineEditKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
362
+ if (!inlineEdit) return;
363
+
364
+ if (event.key === 'Enter') {
365
+ event.preventDefault();
366
+ commitInlineEdit();
367
+ return;
368
+ }
369
+
370
+ if (event.key === 'Escape') {
371
+ event.preventDefault();
372
+ onInlineEditCancel?.(inlineEdit);
373
+ }
374
+ };
375
+
376
+ const renderInlineEdit = ({
377
+ depth,
378
+ directory,
379
+ key,
380
+ mimeType,
381
+ path,
382
+ }: {
383
+ depth: number;
384
+ directory: boolean;
385
+ key: string;
386
+ mimeType?: string;
387
+ path: string;
388
+ }) => {
389
+ if (!inlineEdit) return null;
390
+
391
+ const depthStyle = { '--depth': depth } as CSSProperties;
392
+
393
+ return (
394
+ <li key={key} className="ui-side-bar-list-entry">
395
+ <div className="ui-side-bar-inline-edit" style={depthStyle}>
396
+ <span className="workbench-tree-prefix">
397
+ <span className="workbench-tree-spacer" />
398
+ <WorkspaceFileIcon directory={directory} mimeType={mimeType} path={path} />
399
+ </span>
400
+ <input
401
+ ref={inlineEditInputRef}
402
+ aria-label="Workspace item name"
403
+ className="ui-side-bar-inline-edit__input"
404
+ value={inlineEdit.value}
405
+ onBlur={commitInlineEdit}
406
+ onChange={(event) => onInlineEditValueChange?.(event.currentTarget.value, inlineEdit)}
407
+ onKeyDown={handleInlineEditKeyDown}
408
+ />
409
+ </div>
410
+ {inlineEdit.error ? (
411
+ <div className="ui-side-bar-inline-edit__error" style={depthStyle}>
412
+ {inlineEdit.error}
413
+ </div>
414
+ ) : null}
415
+ </li>
416
+ );
417
+ };
418
+
419
+ return (
420
+ <SideBarList
421
+ fill
422
+ aria-label="Workspace files"
423
+ dropTarget={dropTargetPath === ''}
424
+ onDragLeave={(event) => handleDropTargetDragLeave(event, '')}
425
+ onDragOver={(event) => handleDropTargetDragOver(event, '')}
426
+ onDrop={(event) => handleDrop(event, '')}
427
+ >
428
+ {inlineEdit?.kind.startsWith('create') && !inlineEdit.parentPath
429
+ ? renderInlineEdit({
430
+ depth: 0,
431
+ directory: inlineEdit.kind === 'create-folder',
432
+ key: `inline-edit:${inlineEditKey}`,
433
+ path: inlineEdit.value,
434
+ })
435
+ : null}
436
+ {visibleNodes.map(({ depth, node }) => {
437
+ const isFolder = node.type === 'folder';
438
+ const expanded = expandedPaths.has(node.path) || Boolean(filterQuery.trim());
439
+ const selected = node.type === 'file' && selectedPathSet.has(node.path);
440
+ const dropTarget = isFolder && dropTargetPath === node.path;
441
+ const editingRename =
442
+ inlineEdit?.kind.startsWith('rename') && inlineEdit.path === node.path;
443
+
444
+ return editingRename ? (
445
+ renderInlineEdit({
446
+ depth,
447
+ directory: isFolder,
448
+ key: `inline-edit:${inlineEditKey}`,
449
+ mimeType: node.file?.mimeType,
450
+ path: node.path,
451
+ })
452
+ ) : (
453
+ <Fragment key={node.path}>
454
+ <SideBarListItem
455
+ active={activePath === node.path}
456
+ data-workspace-path={node.path}
457
+ depth={depth}
458
+ draggable={node.type === 'file' && Boolean(onRequestMove)}
459
+ dropTarget={dropTarget}
460
+ selected={selected}
461
+ onClick={(event) => {
462
+ if (isFolder) {
463
+ onToggleFolder(node.path);
464
+ return;
465
+ }
466
+ selectFile(event, node);
467
+ }}
468
+ onContextMenu={(event) => handleItemContextMenu(event, node)}
469
+ onDragEnd={handleDragEnd}
470
+ onDragLeave={
471
+ isFolder ? (event) => handleDropTargetDragLeave(event, node.path) : undefined
472
+ }
473
+ onDragOver={
474
+ isFolder ? (event) => handleDropTargetDragOver(event, node.path) : undefined
475
+ }
476
+ onDragStart={(event) => handleItemDragStart(event, node)}
477
+ onDrop={isFolder ? (event) => handleDrop(event, node.path, node) : undefined}
478
+ onKeyDown={(event) => handleItemKeyDown(event, node)}
479
+ >
480
+ <span className="workbench-tree-prefix">
481
+ {isFolder ? (
482
+ <i
483
+ aria-hidden="true"
484
+ className={`codicon codicon-chevron-${expanded ? 'down' : 'right'} workbench-tree-chevron`}
485
+ />
486
+ ) : (
487
+ <span className="workbench-tree-spacer" />
488
+ )}
489
+ <WorkspaceFileIcon
490
+ directory={isFolder}
491
+ expanded={expanded}
492
+ mimeType={node.file?.mimeType}
493
+ path={node.path}
494
+ />
495
+ </span>
496
+ <span className="workbench-tree-label">{node.name}</span>
497
+ </SideBarListItem>
498
+ {isFolder &&
499
+ inlineEdit?.kind.startsWith('create') &&
500
+ inlineEdit.parentPath === node.path
501
+ ? renderInlineEdit({
502
+ depth: depth + 1,
503
+ directory: inlineEdit.kind === 'create-folder',
504
+ key: `inline-edit:${inlineEditKey}`,
505
+ path: `${node.path}/${inlineEdit.value}`,
506
+ })
507
+ : null}
508
+ </Fragment>
509
+ );
510
+ })}
511
+ {visibleNodes.length === 0 ? <SideBarListItem disabled>No files</SideBarListItem> : null}
512
+ </SideBarList>
513
+ );
514
+ }
515
+
516
+ function resolveSelectionMode(event: MouseEvent<HTMLElement>): WorkspaceSelectionMode {
517
+ const range = event.shiftKey;
518
+ const toggle = event.ctrlKey || event.metaKey;
519
+
520
+ if (range && toggle) return 'toggle-range';
521
+ if (range) return 'range';
522
+ if (toggle) return 'toggle';
523
+ return 'single';
524
+ }
@@ -0,0 +1,149 @@
1
+ import { extensionOfPath, fileNameOfPath } from './path';
2
+
3
+ export type WorkspaceFileIconKind =
4
+ | 'archive'
5
+ | 'config'
6
+ | 'css'
7
+ | 'default'
8
+ | 'env'
9
+ | 'folder'
10
+ | 'git'
11
+ | 'html'
12
+ | 'image'
13
+ | 'javascript'
14
+ | 'json'
15
+ | 'lock'
16
+ | 'markdown'
17
+ | 'package'
18
+ | 'pdf'
19
+ | 'sql'
20
+ | 'text'
21
+ | 'typescript'
22
+ | 'vue'
23
+ | 'xml'
24
+ | 'yaml';
25
+
26
+ export function fileIconKindForPath(path: string, mimeType?: string): WorkspaceFileIconKind {
27
+ const fileName = fileNameOfPath(path).toLowerCase();
28
+ const extension = extensionOfPath(path);
29
+
30
+ if (fileName === 'package.json') return 'package';
31
+ if (fileName.endsWith('.lock') || fileName.includes('lock')) return 'lock';
32
+ if (fileName.startsWith('.env')) return 'env';
33
+ if (fileName.startsWith('.git')) return 'git';
34
+ if (mimeType?.startsWith('image/')) return 'image';
35
+ if (mimeType === 'application/pdf' || extension === 'pdf') return 'pdf';
36
+
37
+ switch (extension) {
38
+ case '7z':
39
+ case 'gz':
40
+ case 'rar':
41
+ case 'tar':
42
+ case 'zip':
43
+ return 'archive';
44
+ case 'css':
45
+ case 'less':
46
+ case 'scss':
47
+ return 'css';
48
+ case 'html':
49
+ return 'html';
50
+ case 'jpeg':
51
+ case 'jpg':
52
+ case 'png':
53
+ case 'svg':
54
+ case 'webp':
55
+ return 'image';
56
+ case 'cjs':
57
+ case 'js':
58
+ case 'jsx':
59
+ case 'mjs':
60
+ return 'javascript';
61
+ case 'json':
62
+ return 'json';
63
+ case 'md':
64
+ case 'mdx':
65
+ return 'markdown';
66
+ case 'sql':
67
+ return 'sql';
68
+ case 'ts':
69
+ case 'tsx':
70
+ return 'typescript';
71
+ case 'vue':
72
+ return 'vue';
73
+ case 'xml':
74
+ return 'xml';
75
+ case 'yaml':
76
+ case 'yml':
77
+ return 'yaml';
78
+ case 'ini':
79
+ case 'toml':
80
+ return 'config';
81
+ case 'txt':
82
+ return 'text';
83
+ default:
84
+ return 'default';
85
+ }
86
+ }
87
+
88
+ export function codiconForFileKind(kind: WorkspaceFileIconKind) {
89
+ switch (kind) {
90
+ case 'archive':
91
+ return 'codicon-file-zip';
92
+ case 'config':
93
+ return 'codicon-settings';
94
+ case 'css':
95
+ return 'codicon-symbol-color';
96
+ case 'env':
97
+ return 'codicon-key';
98
+ case 'folder':
99
+ return 'codicon-folder';
100
+ case 'git':
101
+ return 'codicon-source-control';
102
+ case 'html':
103
+ return 'codicon-code';
104
+ case 'image':
105
+ return 'codicon-file-media';
106
+ case 'javascript':
107
+ return 'codicon-symbol-method';
108
+ case 'json':
109
+ return 'codicon-json';
110
+ case 'lock':
111
+ return 'codicon-lock';
112
+ case 'markdown':
113
+ return 'codicon-markdown';
114
+ case 'package':
115
+ return 'codicon-package';
116
+ case 'pdf':
117
+ return 'codicon-file-pdf';
118
+ case 'sql':
119
+ return 'codicon-database';
120
+ case 'text':
121
+ return 'codicon-file-text';
122
+ case 'typescript':
123
+ return 'codicon-symbol-class';
124
+ case 'vue':
125
+ return 'codicon-symbol-object';
126
+ case 'xml':
127
+ return 'codicon-code';
128
+ case 'yaml':
129
+ return 'codicon-symbol-property';
130
+ default:
131
+ return 'codicon-file';
132
+ }
133
+ }
134
+
135
+ export interface WorkspaceFileIconProps {
136
+ directory?: boolean;
137
+ expanded?: boolean;
138
+ mimeType?: string;
139
+ path: string;
140
+ }
141
+
142
+ export function WorkspaceFileIcon({ directory, expanded, mimeType, path }: WorkspaceFileIconProps) {
143
+ const kind = directory ? 'folder' : fileIconKindForPath(path, mimeType);
144
+ const icon = directory && expanded ? 'codicon-folder-opened' : codiconForFileKind(kind);
145
+
146
+ return (
147
+ <i aria-hidden="true" className={`codicon ${icon} workspace-file-icon`} data-file-kind={kind} />
148
+ );
149
+ }
@@ -0,0 +1,22 @@
1
+ import { highlightText } from './search';
2
+
3
+ export interface WorkspaceHighlightedTextProps {
4
+ query: string;
5
+ text: string;
6
+ }
7
+
8
+ export function WorkspaceHighlightedText({ query, text }: WorkspaceHighlightedTextProps) {
9
+ return (
10
+ <>
11
+ {highlightText(text, query).map((part, index) =>
12
+ part.match ? (
13
+ <mark key={`${part.text}-${index}`} className="workbench-search-mark">
14
+ {part.text}
15
+ </mark>
16
+ ) : (
17
+ <span key={`${part.text}-${index}`}>{part.text}</span>
18
+ ),
19
+ )}
20
+ </>
21
+ );
22
+ }