dsh-explorer 0.0.0-stage → 0.1.1

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 (37) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +106 -2
  3. package/cordis.patch.yml +5 -0
  4. package/icon.svg +7 -0
  5. package/lib/client.js +859 -0
  6. package/lib/index.js +854 -0
  7. package/package.json +91 -4
  8. package/src/client/ExplorerView.tsx +981 -0
  9. package/src/client/components/ExplorerContextMenu.tsx +157 -0
  10. package/src/client/components/ExplorerDeleteModal.tsx +124 -0
  11. package/src/client/components/ExplorerEmptyContextMenu.tsx +125 -0
  12. package/src/client/components/ExplorerHeader.tsx +81 -0
  13. package/src/client/components/ExplorerOptionsModal.tsx +230 -0
  14. package/src/client/components/ExplorerRow.tsx +258 -0
  15. package/src/client/components/ExplorerSearch.tsx +55 -0
  16. package/src/client/components/TreeNode.tsx +384 -0
  17. package/src/client/css-modules.d.ts +5 -0
  18. package/src/client/explorer.module.css +833 -0
  19. package/src/client/hooks/useKeyboardNav.ts +89 -0
  20. package/src/client/hooks/useLiveSync.ts +81 -0
  21. package/src/client/index.tsx +238 -0
  22. package/src/client/utils/api.ts +251 -0
  23. package/src/client/utils/conversation-draft.ts +644 -0
  24. package/src/client/utils/drag-drop.ts +209 -0
  25. package/src/client/utils/open-tab.ts +69 -0
  26. package/src/client/utils/options.ts +59 -0
  27. package/src/client/utils/vscode-theme.tsx +250 -0
  28. package/src/config.ts +11 -0
  29. package/src/fs-operations.ts +116 -0
  30. package/src/fs-search.ts +116 -0
  31. package/src/fs-tree.ts +155 -0
  32. package/src/fs-watch.ts +179 -0
  33. package/src/git-status.ts +167 -0
  34. package/src/index.ts +177 -0
  35. package/src/path-security.ts +22 -0
  36. package/src/session-path.ts +35 -0
  37. package/src/wire.ts +63 -0
@@ -0,0 +1,384 @@
1
+ import React, { useEffect, useMemo } from 'react'
2
+ import css from '../explorer.module.css'
3
+ import type { FsEntry, FsListing } from '../utils/api.ts'
4
+ import type { ExplorerOptions } from '../utils/options.ts'
5
+ import { ExplorerRow } from './ExplorerRow.tsx'
6
+
7
+ export interface TreeNodeProps {
8
+ entry: FsEntry
9
+ depth: number
10
+ parentDir: string
11
+ rootPath: string
12
+ levels: Record<string, FsListing>
13
+ expanded: Set<string>
14
+ selected: Set<string>
15
+ focusedPath: string | null
16
+ renamingPath: string | null
17
+ gitStatusMap: Map<string, string>
18
+ filterText: string
19
+ matchingPaths?: Set<string> | null
20
+ ancestorPaths?: Set<string> | null
21
+ creating: { parentDir: string; isDir: boolean } | null
22
+ creatingName: string
23
+ createInputRef: React.RefObject<HTMLInputElement>
24
+ options: ExplorerOptions
25
+ indentStep: number
26
+ iconSize: number
27
+ reloadDirectory: (dir: string) => Promise<void>
28
+ onToggle: (path: string) => void
29
+ onSelect: (path: string, e: React.MouseEvent) => void
30
+ onOpenFile: (filePath: string, relPath: string) => void
31
+ onContextMenu: (entry: FsEntry, relPath: string, e: React.MouseEvent) => void
32
+ onInsertReference: (relPath: string, isDir: boolean) => void
33
+ onCopyRelativePath: (relPath: string) => void
34
+ onStartRename: (path: string) => void
35
+ onCancelRename: () => void
36
+ onRenameSubmit: (path: string, newName: string) => void
37
+ onDelete: (entry: FsEntry, relPath: string) => void
38
+ onCreatingNameChange: (val: string) => void
39
+ onCreatingKeyDown: (e: React.KeyboardEvent) => void
40
+ onCreatingBlur: () => void
41
+ getRelativePath: (path: string) => string
42
+ }
43
+
44
+ export const TreeNode: React.FC<TreeNodeProps> = React.memo(({
45
+ entry,
46
+ depth,
47
+ rootPath,
48
+ levels,
49
+ expanded,
50
+ selected,
51
+ focusedPath,
52
+ renamingPath,
53
+ gitStatusMap,
54
+ filterText,
55
+ matchingPaths,
56
+ ancestorPaths,
57
+ creating,
58
+ creatingName,
59
+ createInputRef,
60
+ options,
61
+ indentStep,
62
+ iconSize,
63
+ reloadDirectory,
64
+ onToggle,
65
+ onSelect,
66
+ onOpenFile,
67
+ onContextMenu,
68
+ onInsertReference,
69
+ onCopyRelativePath,
70
+ onStartRename,
71
+ onCancelRename,
72
+ onRenameSubmit,
73
+ onDelete,
74
+ onCreatingNameChange,
75
+ onCreatingKeyDown,
76
+ onCreatingBlur,
77
+ getRelativePath,
78
+ }) => {
79
+ const relPath = getRelativePath(entry?.path ?? '')
80
+ const normPathLower = (entry?.path ?? '').replace(/\\/g, '/').toLowerCase()
81
+ const listing =
82
+ levels[entry?.path ?? ''] ??
83
+ levels[(entry?.path ?? '').replace(/\\/g, '/')] ??
84
+ levels[(entry?.path ?? '').replace(/\//g, '\\')] ??
85
+ (normPathLower ? Object.entries(levels).find(([k]) => k.replace(/\\/g, '/').toLowerCase() === normPathLower)?.[1] : undefined)
86
+ const hasLoaded = listing !== undefined
87
+
88
+ const compactChain = useMemo(() => {
89
+ if (!options.compactFolders || !entry?.isDir) return undefined
90
+ const chain: FsEntry[] = [entry]
91
+ let curr = entry
92
+ let currListing = levels[curr.path] ?? levels[curr.path.replace(/\\/g, '/')] ?? levels[curr.path.replace(/\//g, '\\')]
93
+
94
+ while (currListing && Array.isArray(currListing.entries)) {
95
+ const valid = options.showHiddenFiles
96
+ ? currListing.entries
97
+ : currListing.entries.filter((c) => c && !c.hidden && !c.name.startsWith('.'))
98
+ if (valid.length === 1 && valid[0]!.isDir) {
99
+ const next = valid[0]!
100
+ chain.push(next)
101
+ curr = next
102
+ currListing = levels[curr.path] ?? levels[curr.path.replace(/\\/g, '/')] ?? levels[curr.path.replace(/\//g, '\\')]
103
+ } else {
104
+ break
105
+ }
106
+ }
107
+
108
+ return chain.length > 1 ? chain : undefined
109
+ }, [options.compactFolders, options.showHiddenFiles, entry, levels])
110
+
111
+ const isCompacted = compactChain !== undefined && compactChain.length > 1
112
+ const effectiveEntry = isCompacted ? compactChain[compactChain.length - 1]! : entry
113
+ const targetPath = effectiveEntry?.path ?? ''
114
+ const targetRelPath = getRelativePath(targetPath) || ''
115
+ const normRel = targetRelPath.replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+/, '').toLowerCase()
116
+ const normRelOrig = (relPath || '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+/, '').toLowerCase()
117
+ const normTarget = (targetPath || '').replace(/\\/g, '/').toLowerCase()
118
+ const normSelf = (entry?.path || '').replace(/\\/g, '/').toLowerCase()
119
+
120
+ const isSearchActive = Boolean(filterText && matchingPaths)
121
+
122
+ const isDirectMatch = Boolean(
123
+ isSearchActive &&
124
+ matchingPaths &&
125
+ (matchingPaths.has(normRel) ||
126
+ matchingPaths.has(normRelOrig) ||
127
+ matchingPaths.has(normTarget) ||
128
+ matchingPaths.has(normSelf) ||
129
+ (entry?.path && matchingPaths.has(entry.path)) ||
130
+ (entry?.path && matchingPaths.has(entry.path.toLowerCase())) ||
131
+ (targetPath && matchingPaths.has(targetPath)) ||
132
+ (targetPath && matchingPaths.has(targetPath.toLowerCase())))
133
+ )
134
+
135
+ const isAncestor = Boolean(
136
+ isSearchActive &&
137
+ ancestorPaths &&
138
+ (ancestorPaths.has(normRel) ||
139
+ ancestorPaths.has(normRelOrig) ||
140
+ ancestorPaths.has(normTarget) ||
141
+ ancestorPaths.has(normSelf) ||
142
+ (entry?.path && ancestorPaths.has(entry.path)) ||
143
+ (entry?.path && ancestorPaths.has(entry.path.toLowerCase())) ||
144
+ (targetPath && ancestorPaths.has(targetPath)) ||
145
+ (targetPath && ancestorPaths.has(targetPath.toLowerCase())))
146
+ )
147
+
148
+ const normTargetLower = (targetPath || '').replace(/\\/g, '/').toLowerCase()
149
+ const targetListing =
150
+ levels[targetPath] ??
151
+ levels[targetPath.replace(/\\/g, '/')] ??
152
+ levels[targetPath.replace(/\//g, '\\')] ??
153
+ (normTargetLower ? Object.entries(levels).find(([k]) => k.replace(/\\/g, '/').toLowerCase() === normTargetLower)?.[1] : undefined)
154
+ const hasTargetLoaded = targetListing !== undefined
155
+
156
+ const isExpanded = Boolean(
157
+ entry?.isDir && (
158
+ isAncestor ||
159
+ expanded.has(entry.path) ||
160
+ expanded.has(targetPath) ||
161
+ (normSelf && expanded.has(normSelf)) ||
162
+ (normTarget && expanded.has(normTarget)) ||
163
+ (entry?.path && expanded.has(entry.path.toLowerCase())) ||
164
+ (targetPath && expanded.has(targetPath.toLowerCase()))
165
+ )
166
+ )
167
+
168
+ let shouldRender = true
169
+ if (!entry || (!options.showHiddenFiles && (entry.hidden || entry.name.startsWith('.')))) {
170
+ shouldRender = false
171
+ } else if (isSearchActive) {
172
+ if (entry.isDir) {
173
+ if (!isDirectMatch && !isAncestor) {
174
+ shouldRender = false
175
+ }
176
+ } else {
177
+ if (!isDirectMatch) {
178
+ shouldRender = false
179
+ }
180
+ }
181
+ } else if (filterText) {
182
+ const selfMatches =
183
+ entry.name.toLowerCase().includes(filterText.toLowerCase()) ||
184
+ relPath.toLowerCase().includes(filterText.toLowerCase())
185
+ if (!selfMatches && !entry.isDir) {
186
+ shouldRender = false
187
+ }
188
+ }
189
+
190
+ const inFlightDirRef = React.useRef<string | null>(null)
191
+
192
+ useEffect(() => {
193
+ if (!shouldRender || !entry || !entry.isDir) return
194
+ if (isExpanded && !hasTargetLoaded && inFlightDirRef.current !== targetPath) {
195
+ inFlightDirRef.current = targetPath
196
+ reloadDirectory(targetPath).finally(() => {
197
+ if (inFlightDirRef.current === targetPath) {
198
+ inFlightDirRef.current = null
199
+ }
200
+ })
201
+ }
202
+ }, [shouldRender, isExpanded, targetPath, entry, hasTargetLoaded, reloadDirectory])
203
+
204
+ useEffect(() => {
205
+ if (!shouldRender || !entry) return
206
+ if (options.compactFolders && entry.isDir && entry.compact && !hasLoaded) {
207
+ reloadDirectory(entry.path)
208
+ }
209
+ }, [shouldRender, options.compactFolders, entry, hasLoaded, reloadDirectory])
210
+
211
+ if (!entry || !shouldRender) {
212
+ return null
213
+ }
214
+
215
+ const isSel = selected.has(entry.path) || selected.has(targetPath)
216
+ const isFoc = focusedPath === entry.path || focusedPath === targetPath
217
+ const status =
218
+ gitStatusMap?.get(normRel) ??
219
+ gitStatusMap?.get(normRelOrig) ??
220
+ gitStatusMap?.get(normTarget) ??
221
+ gitStatusMap?.get(normSelf)
222
+
223
+ const handleToggle = () => {
224
+ if (isCompacted && targetPath !== entry.path) {
225
+ const isCurrentlyExpanded = expanded.has(entry.path) || expanded.has(targetPath)
226
+ if (isCurrentlyExpanded) {
227
+ if (expanded.has(entry.path)) onToggle(entry.path)
228
+ if (expanded.has(targetPath)) onToggle(targetPath)
229
+ } else {
230
+ onToggle(entry.path)
231
+ onToggle(targetPath)
232
+ }
233
+ } else {
234
+ onToggle(entry.path)
235
+ }
236
+ }
237
+
238
+ if (!entry.isDir) {
239
+ return (
240
+ <ExplorerRow
241
+ entry={entry}
242
+ depth={depth}
243
+ isExpanded={false}
244
+ isSelected={isSel}
245
+ isFocused={isFoc}
246
+ isRenaming={renamingPath === entry.path}
247
+ gitStatus={status}
248
+ relativePath={relPath}
249
+ indentStep={indentStep}
250
+ iconSize={iconSize}
251
+ enableMotion={options.enableMotion}
252
+ showGitBadges={options.showGitBadges}
253
+ compactFolders={options.compactFolders}
254
+ singleClickAction={options.singleClickAction}
255
+ onToggle={() => {}}
256
+ onSelect={(e) => onSelect(entry.path, e)}
257
+ onDoubleClick={() => onOpenFile(entry.path, relPath)}
258
+ onContextMenu={(e) => onContextMenu(entry, relPath, e)}
259
+ onInsertReference={() => onInsertReference(relPath, false)}
260
+ onCopyRelativePath={() => onCopyRelativePath(relPath)}
261
+ onStartRename={() => onStartRename(entry.path)}
262
+ onCancelRename={onCancelRename}
263
+ onRenameSubmit={(newName) => onRenameSubmit(entry.path, newName)}
264
+ onDelete={() => onDelete(entry, relPath)}
265
+ />
266
+ )
267
+ }
268
+
269
+ const visibleChildren = hasTargetLoaded && Array.isArray(targetListing?.entries)
270
+ ? options.showHiddenFiles
271
+ ? targetListing.entries
272
+ : targetListing.entries.filter((c) => c && !c.hidden && !c.name.startsWith('.'))
273
+ : []
274
+
275
+ const isRenamingFolder = renamingPath === entry.path || (isCompacted && renamingPath === targetPath)
276
+
277
+ return (
278
+ <div className={css.folderGroup}>
279
+ <ExplorerRow
280
+ entry={entry}
281
+ depth={depth}
282
+ isExpanded={isExpanded}
283
+ isSelected={isSel}
284
+ isFocused={isFoc}
285
+ isRenaming={isRenamingFolder}
286
+ gitStatus={status}
287
+ relativePath={targetRelPath}
288
+ compactChain={compactChain}
289
+ indentStep={indentStep}
290
+ iconSize={iconSize}
291
+ enableMotion={options.enableMotion}
292
+ showGitBadges={options.showGitBadges}
293
+ compactFolders={options.compactFolders}
294
+ singleClickAction={options.singleClickAction}
295
+ onToggle={handleToggle}
296
+ onSelect={(e) => onSelect(targetPath, e)}
297
+ onContextMenu={(e) => onContextMenu(entry, targetRelPath, e)}
298
+ onInsertReference={() => onInsertReference(targetRelPath, true)}
299
+ onCopyRelativePath={() => onCopyRelativePath(targetRelPath)}
300
+ onStartRename={() => onStartRename(isCompacted ? targetPath : entry.path)}
301
+ onCancelRename={onCancelRename}
302
+ onRenameSubmit={(newName) => onRenameSubmit(isCompacted ? targetPath : entry.path, newName)}
303
+ onDelete={() => onDelete(entry, targetRelPath)}
304
+ />
305
+
306
+ <div
307
+ className={`${css.folderChildren} ${isExpanded ? css.folderChildrenOpen : css.folderChildrenClosed}`}
308
+ style={!options.enableMotion ? { transition: 'none' } : undefined}
309
+ >
310
+ <div className={css.folderChildrenInner}>
311
+ {creating && (creating.parentDir === entry.path || creating.parentDir === targetPath) && (
312
+ <div className={css.row} style={{ paddingLeft: `${(depth + 1) * indentStep + 8}px` }}>
313
+ <span className={css.iconWrap}>{creating.isDir ? '📁' : '📄'}</span>
314
+ <input
315
+ ref={createInputRef}
316
+ autoFocus
317
+ type="text"
318
+ className={css.inlineInput}
319
+ placeholder={creating.isDir ? 'Folder name...' : 'File name...'}
320
+ value={creatingName}
321
+ onChange={(e) => onCreatingNameChange(e.target.value)}
322
+ onKeyDown={onCreatingKeyDown}
323
+ onBlur={onCreatingBlur}
324
+ />
325
+ </div>
326
+ )}
327
+ {visibleChildren.length > 0 ? (
328
+ visibleChildren.map((child) => (
329
+ <TreeNode
330
+ key={child.path}
331
+ entry={child}
332
+ depth={depth + 1}
333
+ parentDir={targetPath}
334
+ rootPath={rootPath}
335
+ levels={levels}
336
+ expanded={expanded}
337
+ selected={selected}
338
+ focusedPath={focusedPath}
339
+ renamingPath={renamingPath}
340
+ gitStatusMap={gitStatusMap}
341
+ filterText={filterText}
342
+ matchingPaths={matchingPaths}
343
+ ancestorPaths={ancestorPaths}
344
+ creating={creating}
345
+ creatingName={creatingName}
346
+ createInputRef={createInputRef}
347
+ options={options}
348
+ indentStep={indentStep}
349
+ iconSize={iconSize}
350
+ reloadDirectory={reloadDirectory}
351
+ onToggle={onToggle}
352
+ onSelect={onSelect}
353
+ onOpenFile={onOpenFile}
354
+ onContextMenu={onContextMenu}
355
+ onInsertReference={onInsertReference}
356
+ onCopyRelativePath={onCopyRelativePath}
357
+ onStartRename={onStartRename}
358
+ onCancelRename={onCancelRename}
359
+ onRenameSubmit={onRenameSubmit}
360
+ onDelete={onDelete}
361
+ onCreatingNameChange={onCreatingNameChange}
362
+ onCreatingKeyDown={onCreatingKeyDown}
363
+ onCreatingBlur={onCreatingBlur}
364
+ getRelativePath={getRelativePath}
365
+ />
366
+ ))
367
+ ) : !isSearchActive && hasTargetLoaded && targetListing.entries && targetListing.entries.length === 0 ? (
368
+ <div
369
+ className={css.row}
370
+ style={{
371
+ paddingLeft: `${(depth + 1) * indentStep + 8}px`,
372
+ opacity: 0.45,
373
+ fontStyle: 'italic',
374
+ fontSize: '12px',
375
+ }}
376
+ >
377
+ Empty folder
378
+ </div>
379
+ ) : null}
380
+ </div>
381
+ </div>
382
+ </div>
383
+ )
384
+ })
@@ -0,0 +1,5 @@
1
+ declare module '*.module.css' {
2
+ const classes: Record<string, string>
3
+ export default classes
4
+ }
5
+