@workbench-kit/react 0.0.1-prototype.0 → 0.0.2-prototype.0.1.2

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 (155) hide show
  1. package/package.json +33 -9
  2. package/src/index.ts +545 -43
  3. package/src/jdw/JdwPreview.tsx +38 -0
  4. package/src/jdw/JsonCodeEditorPane.tsx +274 -0
  5. package/src/jdw/builtins/renderBuiltinWidgetLeaf.tsx +29 -0
  6. package/src/jdw/builtins/renderBuiltinWidgetNode.tsx +126 -0
  7. package/src/jdw/createBuiltinJdwRegistry.ts +111 -0
  8. package/src/jdw/cssRenderBackend.tsx +134 -0
  9. package/src/jdw/document.ts +38 -0
  10. package/src/jdw/fixtures/jdw-fixtures.ts +52 -0
  11. package/src/jdw/fixtures/jdw-sample-screens.ts +522 -0
  12. package/src/jdw/index.ts +45 -0
  13. package/src/jdw/parse.ts +1 -0
  14. package/src/jdw/renderJdw.tsx +32 -0
  15. package/src/json-config/JsonConfigWorkbench.tsx +316 -0
  16. package/src/json-config/index.ts +6 -0
  17. package/src/json-config/json-config-editor-state.ts +42 -0
  18. package/src/layout/Panel.tsx +146 -1
  19. package/src/layout/SideBarViewFrame.tsx +367 -328
  20. package/src/layout/WorkbenchCanvas.tsx +849 -0
  21. package/src/layout/WorkbenchFullscreen.tsx +368 -0
  22. package/src/layout/WorkbenchLayout.tsx +5 -0
  23. package/src/layout/WorkbenchLayoutBase.tsx +294 -0
  24. package/src/layout/WorkbenchPropertyPanel.tsx +497 -0
  25. package/src/layout/WorkbenchSidebarActions.tsx +274 -0
  26. package/src/layout/WorkbenchTree.tsx +351 -0
  27. package/src/layout/layoutHelpers.ts +25 -0
  28. package/src/modal/ConfirmDialog.tsx +61 -53
  29. package/src/modal/Modal.tsx +295 -35
  30. package/src/overlay/ContextMenu.tsx +146 -145
  31. package/src/primitives/AbsoluteBox.tsx +46 -0
  32. package/src/primitives/Badge.tsx +12 -12
  33. package/src/primitives/Button.tsx +44 -14
  34. package/src/primitives/Checkbox.tsx +11 -5
  35. package/src/primitives/EmptyState.tsx +27 -26
  36. package/src/primitives/Field.tsx +38 -38
  37. package/src/primitives/IconButton.tsx +5 -2
  38. package/src/primitives/List.tsx +111 -0
  39. package/src/primitives/NumberInput.tsx +38 -0
  40. package/src/primitives/ScrollArea.tsx +42 -0
  41. package/src/primitives/Select.tsx +1 -12
  42. package/src/primitives/StatusBar.tsx +46 -0
  43. package/src/primitives/TextArea.tsx +36 -0
  44. package/src/primitives/TextInput.tsx +11 -2
  45. package/src/primitives/WorkbenchChrome.tsx +424 -0
  46. package/src/primitives/WorkbenchEditor.tsx +394 -0
  47. package/src/primitives/index.ts +105 -0
  48. package/src/primitives/select/Select.tsx +266 -0
  49. package/src/primitives/select/index.ts +2 -0
  50. package/src/primitives/select/options.ts +38 -0
  51. package/src/primitives/select/overlay.ts +63 -0
  52. package/src/primitives/select/select.app.css +9 -0
  53. package/src/primitives/select/select.css +140 -0
  54. package/src/primitives/select/types.ts +18 -0
  55. package/src/primitives/styles.css +955 -0
  56. package/src/scrollbars.css +69 -0
  57. package/src/styles.css +7738 -1762
  58. package/src/utils/codicon.ts +17 -0
  59. package/src/widget-asset/WidgetAssetEditor.tsx +140 -0
  60. package/src/widget-asset/WidgetAssetMetadataForm.tsx +82 -0
  61. package/src/widget-asset/WidgetAssetModeControls.tsx +42 -0
  62. package/src/widget-asset/WidgetAssetSourceEditor.tsx +50 -0
  63. package/src/widget-asset/WidgetAssetWorkbench.tsx +112 -0
  64. package/src/widget-asset/index.ts +17 -0
  65. package/src/widget-asset/widget-asset-document.ts +37 -0
  66. package/src/widget-asset/widget-asset-mode.ts +3 -0
  67. package/src/widget-studio/builtin-widget-asset-catalog.ts +12 -0
  68. package/src/widget-studio/create-widget-studio-workspace-editor.tsx +79 -0
  69. package/src/widget-studio/index.ts +7 -0
  70. package/src/widget-studio/resolve-widget-studio-asset-catalog.ts +26 -0
  71. package/src/widget-tree/WidgetAssetPalette.tsx +77 -0
  72. package/src/widget-tree/WidgetInspectorPanel.tsx +266 -0
  73. package/src/widget-tree/WidgetSourceEditor.tsx +56 -0
  74. package/src/widget-tree/WidgetTreeLab.tsx +272 -0
  75. package/src/widget-tree/WidgetTreeModeControls.tsx +44 -0
  76. package/src/widget-tree/WidgetTreeSidePanel.tsx +49 -0
  77. package/src/widget-tree/WidgetTreeView.tsx +97 -0
  78. package/src/widget-tree/WidgetTreeWorkbench.tsx +111 -0
  79. package/src/widget-tree/WidgetTreeWorkspaceShell.tsx +154 -0
  80. package/src/widget-tree/demo-registry.ts +32 -0
  81. package/src/widget-tree/demo-widget-assets.ts +4 -0
  82. package/src/widget-tree/index.ts +27 -0
  83. package/src/widget-tree/widget-tree-document.ts +20 -0
  84. package/src/widget-tree/widget-tree-layout.ts +31 -0
  85. package/src/widget-tree/widget-tree-mode.ts +7 -0
  86. package/src/widget-tree/widget-tree-play-helpers.ts +74 -0
  87. package/src/workbench/ActivityBar.tsx +52 -52
  88. package/src/workbench/ArtifactShell.tsx +397 -0
  89. package/src/workbench/CommandPalette.tsx +819 -0
  90. package/src/workbench/ConfirmationFlow.tsx +245 -0
  91. package/src/workbench/MarkdownPreview.tsx +151 -0
  92. package/src/workbench/ShortcutCommandBridge.tsx +368 -0
  93. package/src/workbench/SplitView.tsx +136 -136
  94. package/src/workbench/StatusBar.tsx +178 -123
  95. package/src/workbench/StructuredArtifactEditor.tsx +205 -0
  96. package/src/workbench/Timeline.tsx +302 -0
  97. package/src/workbench/WorkbenchCanvasShell.tsx +941 -0
  98. package/src/workbench/WorkbenchShell.tsx +74 -70
  99. package/src/workbench/WorkbenchStandaloneShell.tsx +318 -291
  100. package/src/workbench/auth/WorkbenchAuthGate.tsx +61 -0
  101. package/src/workbench/auth/WorkbenchLoginView.tsx +635 -0
  102. package/src/workbench/auth/index.ts +19 -0
  103. package/src/workbench/chat/ChatComposer.tsx +165 -148
  104. package/src/workbench/chat/ChatMessageItem.tsx +10 -1
  105. package/src/workbench/chat/index.ts +9 -0
  106. package/src/workbench/chat/slashCommand.ts +28 -0
  107. package/src/workbench/chat/useChatRuntimeState.ts +76 -0
  108. package/src/workbench/chat/useSlashCommandSuggest.ts +101 -0
  109. package/src/workbench/commands.ts +520 -490
  110. package/src/workbench/index.ts +443 -98
  111. package/src/workbench/keyboard.ts +53 -0
  112. package/src/workbench/schema/index.ts +48 -0
  113. package/src/workbench/schema/workbenchDocument.ts +31 -0
  114. package/src/workbench/schema/workbenchDocumentActions.ts +6 -0
  115. package/src/workbench/schema/workbenchDocumentAdapter.ts +9 -0
  116. package/src/workbench/schema/workbenchDocumentPatch.ts +13 -0
  117. package/src/workbench/schema/workbenchDocumentRenderer.tsx +702 -0
  118. package/src/workbench/settings/NavigationPanel.tsx +47 -0
  119. package/src/workbench/settings/SchemaForm.tsx +420 -0
  120. package/src/workbench/settings/SectionedPanel.tsx +415 -0
  121. package/src/workbench/settings/StructuredDataForm.tsx +539 -0
  122. package/src/workbench/settings/StructuredDataSchemaPanel.tsx +559 -0
  123. package/src/workbench/settings/StructuredDataSchemaPanelEmbed.tsx +21 -0
  124. package/src/workbench/settings/StructuredDataSchemaPanelFrame.tsx +76 -0
  125. package/src/workbench/settings/StructuredDataTableView.tsx +127 -0
  126. package/src/workbench/settings/WorkbenchPanelRegion.tsx +46 -0
  127. package/src/workbench/settings/WorkbenchSettingsModal.tsx +21 -12
  128. package/src/workbench/settings/WorkbenchSettingsNav.tsx +38 -22
  129. package/src/workbench/settings/index.ts +135 -0
  130. package/src/workbench/settings/panel-region.css +21 -0
  131. package/src/workbench/settings/sectionedPanelScrollSpy.ts +143 -0
  132. package/src/workbench/settings/structured-data-schema-panel.css +330 -0
  133. package/src/workbench/settings/structured-data-table-view.css +85 -0
  134. package/src/workbench/settings/structuredDataSchema.ts +886 -0
  135. package/src/workbench/shell.ts +15 -0
  136. package/src/workbench/shellState.ts +203 -203
  137. package/src/workbench/standalone.ts +101 -94
  138. package/src/workbench/status.ts +129 -0
  139. package/src/workbench/theme.ts +44 -0
  140. package/src/workbench/workspace/MultiProviderExplorer.tsx +625 -0
  141. package/src/workbench/workspace/WorkspaceDraftsContext.tsx +111 -0
  142. package/src/workbench/workspace/WorkspaceEditor.tsx +47 -12
  143. package/src/workbench/workspace/WorkspaceEditorPanel.tsx +334 -353
  144. package/src/workbench/workspace/WorkspaceExplorer.tsx +532 -524
  145. package/src/workbench/workspace/WorkspaceFileIcon.tsx +32 -2
  146. package/src/workbench/workspace/WorkspaceSearchPanel.tsx +113 -113
  147. package/src/workbench/workspace/WorkspaceSearchResults.tsx +47 -47
  148. package/src/workbench/workspace/index.ts +156 -98
  149. package/src/workbench/workspace/mimeType.ts +45 -0
  150. package/src/workbench/workspace/path.ts +10 -10
  151. package/src/workbench/workspace/search.ts +6 -6
  152. package/src/workbench/workspace/tree.ts +1 -1
  153. package/src/workbench/workspace/types.ts +10 -10
  154. package/src/workbench/workspace/useVirtualWorkspace.ts +111 -98
  155. package/src/workbench/workspace/workspaceJsonDiagnostics.ts +83 -0
@@ -1,524 +1,532 @@
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
- }
1
+ import {
2
+ Fragment,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ type DragEvent,
8
+ type KeyboardEvent,
9
+ type MouseEvent,
10
+ type ReactNode,
11
+ } from 'react';
12
+ import {
13
+ getWorkspaceSelectionActionPaths,
14
+ normalizeWorkspaceSelectionPaths,
15
+ parentPathOf,
16
+ updateWorkspaceSelection,
17
+ type WorkspaceSelectionMode,
18
+ type WorkspaceSelectionState,
19
+ } from '@workbench-kit/workspace';
20
+ import { SideBarList, SideBarListItem, sideBarTreeDepthStyle } from '../../layout/SideBarViewFrame';
21
+ import { cxCodicon } from '../../utils/codicon';
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(
258
+ (sourcePath) =>
259
+ parentPathOf(sourcePath) !== targetFolderPath &&
260
+ sourcePath !== targetFolderPath &&
261
+ !targetFolderPath.startsWith(`${sourcePath}/`),
262
+ );
263
+
264
+ const handleItemDragStart = (event: DragEvent<HTMLButtonElement>, node: WorkspaceTreeNode) => {
265
+ if (!onRequestMove) {
266
+ event.preventDefault();
267
+ return;
268
+ }
269
+
270
+ const meta = getItemActionMeta(node);
271
+ const sourcePaths = meta.actionPaths;
272
+ if (sourcePaths.length === 0) {
273
+ event.preventDefault();
274
+ return;
275
+ }
276
+
277
+ if (!meta.selected) {
278
+ onSelectionChange?.(meta.selection, {
279
+ event,
280
+ mode: 'single',
281
+ node,
282
+ reason: 'drag-start',
283
+ });
284
+ }
285
+
286
+ event.dataTransfer.effectAllowed = 'move';
287
+ event.dataTransfer.setData(dragDataType, JSON.stringify(sourcePaths));
288
+ if (dragMetadataFactory) {
289
+ const metadata = dragMetadataFactory({
290
+ event,
291
+ node,
292
+ sourcePaths,
293
+ selection: meta.selection,
294
+ });
295
+
296
+ if (metadata !== undefined) {
297
+ event.dataTransfer.setData(dragMetadataDataType, JSON.stringify(metadata));
298
+ }
299
+ }
300
+ event.dataTransfer.setData('text/plain', sourcePaths.join('\n'));
301
+ draggedPathsRef.current = sourcePaths;
302
+ setDropTargetPath(null);
303
+ };
304
+
305
+ const handleDragEnd = () => {
306
+ draggedPathsRef.current = [];
307
+ setDropTargetPath(null);
308
+ };
309
+
310
+ const handleDropTargetDragOver = (
311
+ event: DragEvent<HTMLButtonElement | HTMLUListElement>,
312
+ targetFolderPath: string,
313
+ ) => {
314
+ if (!canDropToFolder(targetFolderPath)) return;
315
+
316
+ if (targetFolderPath) {
317
+ event.stopPropagation();
318
+ }
319
+ event.preventDefault();
320
+ event.dataTransfer.dropEffect = 'move';
321
+ setDropTargetPath(targetFolderPath);
322
+ };
323
+
324
+ const handleDropTargetDragLeave = (
325
+ event: DragEvent<HTMLButtonElement | HTMLUListElement>,
326
+ targetFolderPath: string,
327
+ ) => {
328
+ if (targetFolderPath) {
329
+ event.stopPropagation();
330
+ }
331
+ if (event.currentTarget.contains(event.relatedTarget as Node | null)) return;
332
+
333
+ setDropTargetPath((currentPath) => (currentPath === targetFolderPath ? null : currentPath));
334
+ };
335
+
336
+ const handleDrop = (
337
+ event: DragEvent<HTMLButtonElement | HTMLUListElement>,
338
+ targetFolderPath: string,
339
+ targetNode?: WorkspaceTreeNode,
340
+ ) => {
341
+ if (!canDropToFolder(targetFolderPath)) return;
342
+
343
+ if (targetFolderPath) {
344
+ event.stopPropagation();
345
+ }
346
+ event.preventDefault();
347
+ onRequestMove?.({
348
+ event,
349
+ sourcePaths: draggedPathsRef.current,
350
+ targetFolderPath,
351
+ targetNode,
352
+ });
353
+ draggedPathsRef.current = [];
354
+ setDropTargetPath(null);
355
+ };
356
+
357
+ const commitInlineEdit = () => {
358
+ if (!inlineEdit) return;
359
+
360
+ onInlineEditCommit?.({
361
+ edit: inlineEdit,
362
+ value: inlineEdit.value.trim(),
363
+ });
364
+ };
365
+
366
+ const handleInlineEditKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
367
+ if (!inlineEdit) return;
368
+
369
+ if (event.key === 'Enter') {
370
+ event.preventDefault();
371
+ commitInlineEdit();
372
+ return;
373
+ }
374
+
375
+ if (event.key === 'Escape') {
376
+ event.preventDefault();
377
+ onInlineEditCancel?.(inlineEdit);
378
+ }
379
+ };
380
+
381
+ const renderInlineEdit = ({
382
+ depth,
383
+ directory,
384
+ key,
385
+ mimeType,
386
+ path,
387
+ }: {
388
+ depth: number;
389
+ directory: boolean;
390
+ key: string;
391
+ mimeType?: string;
392
+ path: string;
393
+ }) => {
394
+ if (!inlineEdit) return null;
395
+
396
+ const depthStyle = sideBarTreeDepthStyle(depth);
397
+
398
+ return (
399
+ <li key={key} className="ui-side-bar-list-entry">
400
+ <div className="ui-side-bar-inline-edit" style={depthStyle}>
401
+ <span className="workbench-tree-prefix">
402
+ <span className="workbench-tree-spacer" />
403
+ <WorkspaceFileIcon directory={directory} mimeType={mimeType} path={path} />
404
+ </span>
405
+ <input
406
+ ref={inlineEditInputRef}
407
+ aria-label="Workspace item name"
408
+ className="ui-side-bar-inline-edit__input"
409
+ value={inlineEdit.value}
410
+ onBlur={commitInlineEdit}
411
+ onChange={(event) => onInlineEditValueChange?.(event.currentTarget.value, inlineEdit)}
412
+ onKeyDown={handleInlineEditKeyDown}
413
+ />
414
+ </div>
415
+ {inlineEdit.error ? (
416
+ <div className="ui-side-bar-inline-edit__error" style={depthStyle}>
417
+ {inlineEdit.error}
418
+ </div>
419
+ ) : null}
420
+ </li>
421
+ );
422
+ };
423
+
424
+ return (
425
+ <SideBarList
426
+ fill
427
+ aria-label="Workspace files"
428
+ dropTarget={dropTargetPath === ''}
429
+ onDragLeave={(event) => handleDropTargetDragLeave(event, '')}
430
+ onDragOver={(event) => handleDropTargetDragOver(event, '')}
431
+ onDrop={(event) => handleDrop(event, '')}
432
+ >
433
+ {inlineEdit?.kind.startsWith('create') && !inlineEdit.parentPath
434
+ ? renderInlineEdit({
435
+ depth: 0,
436
+ directory: inlineEdit.kind === 'create-folder',
437
+ key: `inline-edit:${inlineEditKey}`,
438
+ path: inlineEdit.value,
439
+ })
440
+ : null}
441
+ {visibleNodes.map(({ depth, node }) => {
442
+ const isFolder = node.type === 'folder';
443
+ const expanded = expandedPaths.has(node.path) || Boolean(filterQuery.trim());
444
+ const selected = node.type === 'file' && selectedPathSet.has(node.path);
445
+ const dropTarget = isFolder && dropTargetPath === node.path;
446
+ const editingRename =
447
+ inlineEdit?.kind.startsWith('rename') && inlineEdit.path === node.path;
448
+
449
+ return editingRename ? (
450
+ renderInlineEdit({
451
+ depth,
452
+ directory: isFolder,
453
+ key: `inline-edit:${inlineEditKey}`,
454
+ mimeType: node.file?.mimeType,
455
+ path: node.path,
456
+ })
457
+ ) : (
458
+ <Fragment key={node.path}>
459
+ <SideBarListItem
460
+ active={activePath === node.path}
461
+ data-workspace-path={node.path}
462
+ depth={depth}
463
+ draggable={Boolean(onRequestMove)}
464
+ dropTarget={dropTarget}
465
+ selected={selected}
466
+ onClick={(event) => {
467
+ if (isFolder) {
468
+ onToggleFolder(node.path);
469
+ return;
470
+ }
471
+ selectFile(event, node);
472
+ }}
473
+ onContextMenu={(event) => handleItemContextMenu(event, node)}
474
+ onDragEnd={handleDragEnd}
475
+ onDragLeave={
476
+ isFolder ? (event) => handleDropTargetDragLeave(event, node.path) : undefined
477
+ }
478
+ onDragOver={
479
+ isFolder ? (event) => handleDropTargetDragOver(event, node.path) : undefined
480
+ }
481
+ onDragStart={(event) => handleItemDragStart(event, node)}
482
+ onDrop={isFolder ? (event) => handleDrop(event, node.path, node) : undefined}
483
+ onKeyDown={(event) => handleItemKeyDown(event, node)}
484
+ >
485
+ <span className="workbench-tree-prefix">
486
+ {isFolder ? (
487
+ <i
488
+ aria-hidden="true"
489
+ className={cxCodicon(
490
+ expanded ? 'chevron-down' : 'chevron-right',
491
+ 'workbench-tree-chevron',
492
+ )}
493
+ />
494
+ ) : (
495
+ <span className="workbench-tree-spacer" />
496
+ )}
497
+ <WorkspaceFileIcon
498
+ directory={isFolder}
499
+ expanded={expanded}
500
+ mimeType={node.file?.mimeType}
501
+ path={node.path}
502
+ />
503
+ </span>
504
+ <span className="workbench-tree-label">{node.name}</span>
505
+ </SideBarListItem>
506
+ {isFolder &&
507
+ inlineEdit?.kind.startsWith('create') &&
508
+ inlineEdit.parentPath === node.path
509
+ ? renderInlineEdit({
510
+ depth: depth + 1,
511
+ directory: inlineEdit.kind === 'create-folder',
512
+ key: `inline-edit:${inlineEditKey}`,
513
+ path: `${node.path}/${inlineEdit.value}`,
514
+ })
515
+ : null}
516
+ </Fragment>
517
+ );
518
+ })}
519
+ {visibleNodes.length === 0 ? <SideBarListItem disabled>No files</SideBarListItem> : null}
520
+ </SideBarList>
521
+ );
522
+ }
523
+
524
+ function resolveSelectionMode(event: MouseEvent<HTMLElement>): WorkspaceSelectionMode {
525
+ const range = event.shiftKey;
526
+ const toggle = event.ctrlKey || event.metaKey;
527
+
528
+ if (range && toggle) return 'toggle-range';
529
+ if (range) return 'range';
530
+ if (toggle) return 'toggle';
531
+ return 'single';
532
+ }