momoi-explorer 0.8.0 → 0.8.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.
- package/README.md +407 -355
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,355 +1,407 @@
|
|
|
1
|
-
# momoi-explorer
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install momoi-explorer
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
|
16
|
-
|---|---|---|
|
|
17
|
-
| `momoi-explorer` |
|
|
18
|
-
| `momoi-explorer/react` | React
|
|
19
|
-
| `momoi-explorer/ui` |
|
|
20
|
-
|
|
21
|
-
##
|
|
22
|
-
|
|
23
|
-
### 1. FileSystemAdapter
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
```ts
|
|
28
|
-
import type { FileSystemAdapter } from 'momoi-explorer'
|
|
29
|
-
|
|
30
|
-
const adapter: FileSystemAdapter = {
|
|
31
|
-
//
|
|
32
|
-
async readDir(dirPath) {
|
|
33
|
-
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
|
34
|
-
return entries.map(e => ({
|
|
35
|
-
name: e.name,
|
|
36
|
-
path: path.join(dirPath, e.name),
|
|
37
|
-
isDirectory: e.isDirectory(),
|
|
38
|
-
}))
|
|
39
|
-
},
|
|
40
|
-
//
|
|
41
|
-
async rename(oldPath, newPath) {
|
|
42
|
-
await fs.rename(oldPath, newPath)
|
|
43
|
-
},
|
|
44
|
-
//
|
|
45
|
-
async delete(paths) {
|
|
46
|
-
for (const p of paths) await fs.rm(p, { recursive: true })
|
|
47
|
-
},
|
|
48
|
-
//
|
|
49
|
-
async createFile(parentPath, name) {
|
|
50
|
-
await fs.writeFile(path.join(parentPath, name), '')
|
|
51
|
-
},
|
|
52
|
-
//
|
|
53
|
-
async createDir(parentPath, name) {
|
|
54
|
-
await fs.mkdir(path.join(parentPath, name))
|
|
55
|
-
},
|
|
56
|
-
//
|
|
57
|
-
watch(dirPath, callback) {
|
|
58
|
-
const watcher = fs.watch(dirPath, { recursive: true }, (event, filename) => {
|
|
59
|
-
callback([{ type: event === 'rename' ? 'create' : 'modify', path: filename, isDirectory: false }])
|
|
60
|
-
})
|
|
61
|
-
return () => watcher.close()
|
|
62
|
-
},
|
|
63
|
-
}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### 2a.
|
|
67
|
-
|
|
68
|
-
```tsx
|
|
69
|
-
import { FileExplorer } from 'momoi-explorer/ui'
|
|
70
|
-
import 'momoi-explorer/ui/style.css'
|
|
71
|
-
|
|
72
|
-
function App() {
|
|
73
|
-
return (
|
|
74
|
-
<FileExplorer
|
|
75
|
-
adapter={adapter}
|
|
76
|
-
rootPath="/home/user/project"
|
|
77
|
-
onOpen={(path) => openFile(path)}
|
|
78
|
-
onEvent={(e) => console.log('tree event:', e)}
|
|
79
|
-
showFilterBar
|
|
80
|
-
/>
|
|
81
|
-
)
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
### 2b.
|
|
86
|
-
|
|
87
|
-
```tsx
|
|
88
|
-
import { TreeProvider, useFileTree, useTreeNode } from 'momoi-explorer/react'
|
|
89
|
-
|
|
90
|
-
function App() {
|
|
91
|
-
return (
|
|
92
|
-
<TreeProvider adapter={adapter} rootPath="/home/user/project">
|
|
93
|
-
<MyCustomTree />
|
|
94
|
-
</TreeProvider>
|
|
95
|
-
)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function MyCustomTree() {
|
|
99
|
-
const { flatList, controller } = useFileTree()
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<div>
|
|
103
|
-
{flatList.map(({ node, depth }) => (
|
|
104
|
-
<div key={node.path} style={{ paddingLeft: depth * 16 }}>
|
|
105
|
-
<span onClick={() => controller.toggleExpand(node.path)}>
|
|
106
|
-
{node.name}
|
|
107
|
-
</span>
|
|
108
|
-
</div>
|
|
109
|
-
))}
|
|
110
|
-
</div>
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### 2c.
|
|
116
|
-
|
|
117
|
-
```ts
|
|
118
|
-
import { createFileTree } from 'momoi-explorer'
|
|
119
|
-
|
|
120
|
-
const tree = createFileTree({
|
|
121
|
-
adapter,
|
|
122
|
-
rootPath: '/home/user/project',
|
|
123
|
-
onEvent: (e) => console.log(e),
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
tree.subscribe((state) => {
|
|
128
|
-
console.log('nodes:', state.rootNodes)
|
|
129
|
-
console.log('flatList:', state.flatList)
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
//
|
|
133
|
-
await tree.loadRoot()
|
|
134
|
-
|
|
135
|
-
//
|
|
136
|
-
await tree.expand('/home/user/project/src')
|
|
137
|
-
tree.select('/home/user/project/src/index.ts')
|
|
138
|
-
tree.setSearchQuery('config')
|
|
139
|
-
|
|
140
|
-
//
|
|
141
|
-
tree.destroy()
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
## API
|
|
145
|
-
|
|
146
|
-
###
|
|
147
|
-
|
|
148
|
-
#### `createFileTree(options): FileTreeController`
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
**FileTreeOptions:**
|
|
153
|
-
|
|
|
154
|
-
|---|---|---|
|
|
155
|
-
| `adapter` | `FileSystemAdapter` |
|
|
156
|
-
| `rootPath` | `string` |
|
|
157
|
-
| `sort` | `(a, b) => number` |
|
|
158
|
-
| `filter` | `(entry) => boolean` |
|
|
159
|
-
| `watchOptions` | `WatchOptions` |
|
|
160
|
-
| `onEvent` | `(event: TreeEvent) => void` |
|
|
161
|
-
|
|
162
|
-
**FileTreeController
|
|
163
|
-
|
|
164
|
-
|
|
|
165
|
-
|---|---|
|
|
166
|
-
| `getState()` |
|
|
167
|
-
| `subscribe(listener)` |
|
|
168
|
-
| `loadRoot()` |
|
|
169
|
-
| `expand(path)` |
|
|
170
|
-
| `collapse(path)` |
|
|
171
|
-
| `toggleExpand(path)` |
|
|
172
|
-
| `expandTo(path)` |
|
|
173
|
-
| `select(path, mode?)` |
|
|
174
|
-
| `selectAll()` |
|
|
175
|
-
| `clearSelection()` |
|
|
176
|
-
| `startRename(path)` |
|
|
177
|
-
| `commitRename(newName)` |
|
|
178
|
-
| `cancelRename()` |
|
|
179
|
-
| `startCreate(parentPath, isDirectory)` |
|
|
180
|
-
| `commitCreate(name)` |
|
|
181
|
-
| `cancelCreate()` |
|
|
182
|
-
| `createFile(parentPath, name)` |
|
|
183
|
-
| `createDir(parentPath, name)` |
|
|
184
|
-
| `deleteSelected()` |
|
|
185
|
-
| `refresh(path?)` |
|
|
186
|
-
| `setSearchQuery(query)` |
|
|
187
|
-
| `collectAllFiles()` |
|
|
188
|
-
| `setFilter(fn)` |
|
|
189
|
-
| `setSort(fn)` |
|
|
190
|
-
| `destroy()` |
|
|
191
|
-
|
|
192
|
-
####
|
|
193
|
-
|
|
194
|
-
|
|
|
195
|
-
|---|---|
|
|
196
|
-
| `flattenTree(nodes, expandedPaths, matchingPaths?)` |
|
|
197
|
-
| `computeSelection(current, anchor, target, mode, flatList)` |
|
|
198
|
-
| `fuzzyMatch(query, target)` |
|
|
199
|
-
| `fuzzyFind(files, query, maxResults?)` |
|
|
200
|
-
| `findMatchingPaths(nodes, query)` |
|
|
201
|
-
| `coalesceEvents(raw)` |
|
|
202
|
-
| `createEventProcessor(callback, options?)` |
|
|
203
|
-
| `defaultSort(a, b)` |
|
|
204
|
-
| `defaultFilter(entry)` |
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
|
211
|
-
|
|
212
|
-
| `
|
|
213
|
-
| `
|
|
214
|
-
| `
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
|
223
|
-
|
|
224
|
-
| `
|
|
225
|
-
| `
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
interface
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
| { type: '
|
|
326
|
-
| { type: '
|
|
327
|
-
| { type: '
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
1
|
+
# momoi-explorer
|
|
2
|
+
|
|
3
|
+
A headless file explorer library. Framework-agnostic core + React bindings + default UI in a 3-layer architecture.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install momoi-explorer
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Architecture
|
|
12
|
+
|
|
13
|
+
Three entry points with a layered architecture:
|
|
14
|
+
|
|
15
|
+
| Entry Point | Purpose | Requires React |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `momoi-explorer` | Core engine (framework-agnostic) | No |
|
|
18
|
+
| `momoi-explorer/react` | React bindings (hooks + context) | Yes |
|
|
19
|
+
| `momoi-explorer/ui` | Default UI components | Yes |
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### 1. Implement a FileSystemAdapter
|
|
24
|
+
|
|
25
|
+
Everything starts with implementing `FileSystemAdapter`. Only `readDir` is required; other methods are optional (implementing them enables the corresponding features).
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import type { FileSystemAdapter } from 'momoi-explorer'
|
|
29
|
+
|
|
30
|
+
const adapter: FileSystemAdapter = {
|
|
31
|
+
// Required: return directory contents
|
|
32
|
+
async readDir(dirPath) {
|
|
33
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
|
34
|
+
return entries.map(e => ({
|
|
35
|
+
name: e.name,
|
|
36
|
+
path: path.join(dirPath, e.name),
|
|
37
|
+
isDirectory: e.isDirectory(),
|
|
38
|
+
}))
|
|
39
|
+
},
|
|
40
|
+
// Optional: rename
|
|
41
|
+
async rename(oldPath, newPath) {
|
|
42
|
+
await fs.rename(oldPath, newPath)
|
|
43
|
+
},
|
|
44
|
+
// Optional: delete
|
|
45
|
+
async delete(paths) {
|
|
46
|
+
for (const p of paths) await fs.rm(p, { recursive: true })
|
|
47
|
+
},
|
|
48
|
+
// Optional: create file
|
|
49
|
+
async createFile(parentPath, name) {
|
|
50
|
+
await fs.writeFile(path.join(parentPath, name), '')
|
|
51
|
+
},
|
|
52
|
+
// Optional: create directory
|
|
53
|
+
async createDir(parentPath, name) {
|
|
54
|
+
await fs.mkdir(path.join(parentPath, name))
|
|
55
|
+
},
|
|
56
|
+
// Optional: file watching (debounce & coalescing handled by core)
|
|
57
|
+
watch(dirPath, callback) {
|
|
58
|
+
const watcher = fs.watch(dirPath, { recursive: true }, (event, filename) => {
|
|
59
|
+
callback([{ type: event === 'rename' ? 'create' : 'modify', path: filename, isDirectory: false }])
|
|
60
|
+
})
|
|
61
|
+
return () => watcher.close()
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 2a. Use the Default UI (easiest)
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import { FileExplorer } from 'momoi-explorer/ui'
|
|
70
|
+
import 'momoi-explorer/ui/style.css'
|
|
71
|
+
|
|
72
|
+
function App() {
|
|
73
|
+
return (
|
|
74
|
+
<FileExplorer
|
|
75
|
+
adapter={adapter}
|
|
76
|
+
rootPath="/home/user/project"
|
|
77
|
+
onOpen={(path) => openFile(path)}
|
|
78
|
+
onEvent={(e) => console.log('tree event:', e)}
|
|
79
|
+
showFilterBar
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 2b. Build Custom UI with React Hooks
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import { TreeProvider, useFileTree, useTreeNode } from 'momoi-explorer/react'
|
|
89
|
+
|
|
90
|
+
function App() {
|
|
91
|
+
return (
|
|
92
|
+
<TreeProvider adapter={adapter} rootPath="/home/user/project">
|
|
93
|
+
<MyCustomTree />
|
|
94
|
+
</TreeProvider>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function MyCustomTree() {
|
|
99
|
+
const { flatList, controller } = useFileTree()
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div>
|
|
103
|
+
{flatList.map(({ node, depth }) => (
|
|
104
|
+
<div key={node.path} style={{ paddingLeft: depth * 16 }}>
|
|
105
|
+
<span onClick={() => controller.toggleExpand(node.path)}>
|
|
106
|
+
{node.name}
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 2c. Core Only (framework-agnostic)
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { createFileTree } from 'momoi-explorer'
|
|
119
|
+
|
|
120
|
+
const tree = createFileTree({
|
|
121
|
+
adapter,
|
|
122
|
+
rootPath: '/home/user/project',
|
|
123
|
+
onEvent: (e) => console.log(e),
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// Subscribe to state changes
|
|
127
|
+
tree.subscribe((state) => {
|
|
128
|
+
console.log('nodes:', state.rootNodes)
|
|
129
|
+
console.log('flatList:', state.flatList)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// Load the tree
|
|
133
|
+
await tree.loadRoot()
|
|
134
|
+
|
|
135
|
+
// Operations
|
|
136
|
+
await tree.expand('/home/user/project/src')
|
|
137
|
+
tree.select('/home/user/project/src/index.ts')
|
|
138
|
+
tree.setSearchQuery('config')
|
|
139
|
+
|
|
140
|
+
// Cleanup
|
|
141
|
+
tree.destroy()
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## API Reference
|
|
145
|
+
|
|
146
|
+
### Core (`momoi-explorer`)
|
|
147
|
+
|
|
148
|
+
#### `createFileTree(options): FileTreeController`
|
|
149
|
+
|
|
150
|
+
Main entry point for the headless file tree.
|
|
151
|
+
|
|
152
|
+
**FileTreeOptions:**
|
|
153
|
+
| Property | Type | Description |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| `adapter` | `FileSystemAdapter` | File system adapter (required) |
|
|
156
|
+
| `rootPath` | `string` | Absolute path to the root directory |
|
|
157
|
+
| `sort` | `(a, b) => number` | Custom sort function |
|
|
158
|
+
| `filter` | `(entry) => boolean` | Custom filter function |
|
|
159
|
+
| `watchOptions` | `WatchOptions` | File watching options |
|
|
160
|
+
| `onEvent` | `(event: TreeEvent) => void` | Event callback |
|
|
161
|
+
|
|
162
|
+
**FileTreeController Methods:**
|
|
163
|
+
|
|
164
|
+
| Method | Description |
|
|
165
|
+
|---|---|
|
|
166
|
+
| `getState()` | Get current TreeState |
|
|
167
|
+
| `subscribe(listener)` | Subscribe to state changes. Returns unsubscribe function |
|
|
168
|
+
| `loadRoot()` | Load and initialize root (must be called first) |
|
|
169
|
+
| `expand(path)` | Expand a directory |
|
|
170
|
+
| `collapse(path)` | Collapse a directory |
|
|
171
|
+
| `toggleExpand(path)` | Toggle expand/collapse |
|
|
172
|
+
| `expandTo(path)` | Expand all ancestors up to the given path |
|
|
173
|
+
| `select(path, mode?)` | Select a node (mode: 'replace' / 'toggle' / 'range') |
|
|
174
|
+
| `selectAll()` | Select all nodes |
|
|
175
|
+
| `clearSelection()` | Clear selection |
|
|
176
|
+
| `startRename(path)` | Enter rename mode |
|
|
177
|
+
| `commitRename(newName)` | Commit rename |
|
|
178
|
+
| `cancelRename()` | Cancel rename |
|
|
179
|
+
| `startCreate(parentPath, isDirectory, insertAfterPath?)` | Enter inline creation mode (insertAfterPath: position for the input row) |
|
|
180
|
+
| `commitCreate(name)` | Commit creation |
|
|
181
|
+
| `cancelCreate()` | Cancel creation |
|
|
182
|
+
| `createFile(parentPath, name)` | Create a file |
|
|
183
|
+
| `createDir(parentPath, name)` | Create a directory |
|
|
184
|
+
| `deleteSelected()` | Delete selected items |
|
|
185
|
+
| `refresh(path?)` | Refresh the tree (preserves expanded state) |
|
|
186
|
+
| `setSearchQuery(query)` | Set fuzzy search query (null to clear) |
|
|
187
|
+
| `collectAllFiles()` | Recursively collect all files (for QuickOpen) |
|
|
188
|
+
| `setFilter(fn)` | Dynamically change the filter function |
|
|
189
|
+
| `setSort(fn)` | Dynamically change the sort function |
|
|
190
|
+
| `destroy()` | Destroy the controller (stops watching, clears subscriptions) |
|
|
191
|
+
|
|
192
|
+
#### Utility Functions
|
|
193
|
+
|
|
194
|
+
| Function | Description |
|
|
195
|
+
|---|---|
|
|
196
|
+
| `flattenTree(nodes, expandedPaths, matchingPaths?)` | Convert tree to flat list |
|
|
197
|
+
| `computeSelection(current, anchor, target, mode, flatList)` | Compute selection state |
|
|
198
|
+
| `fuzzyMatch(query, target)` | Fuzzy match (match + score) |
|
|
199
|
+
| `fuzzyFind(files, query, maxResults?)` | Fuzzy search sorted by score |
|
|
200
|
+
| `findMatchingPaths(nodes, query)` | Return Set of matching paths |
|
|
201
|
+
| `coalesceEvents(raw)` | Coalesce raw watch events |
|
|
202
|
+
| `createEventProcessor(callback, options?)` | Event processor with debounce |
|
|
203
|
+
| `defaultSort(a, b)` | Default sort (directories first, name ascending) |
|
|
204
|
+
| `defaultFilter(entry)` | Default filter (show all) |
|
|
205
|
+
| `ExplorerCommands` | Explorer command ID constants (DELETE, RENAME, etc.) |
|
|
206
|
+
| `defaultExplorerKeybindings` | Default keybinding definitions (for momoi-keybind) |
|
|
207
|
+
|
|
208
|
+
### React (`momoi-explorer/react`)
|
|
209
|
+
|
|
210
|
+
| Export | Kind | Description |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| `TreeProvider` | Component | File tree context provider. Calls `createFileTree` + `loadRoot` internally |
|
|
213
|
+
| `useFileTree()` | Hook | Returns full tree state and controller |
|
|
214
|
+
| `useTreeNode(path)` | Hook | Returns expand/select/rename state for a node (null if not found) |
|
|
215
|
+
| `useContextMenu()` | Hook | Context menu visibility control (show/hide + position) |
|
|
216
|
+
| `useExplorerKeybindings(inputService)` | Hook | momoi-keybind integration. Registers explorer command handlers |
|
|
217
|
+
| `useExplorerFocus(inputService)` | Hook | Syncs focus state with momoi-keybind context |
|
|
218
|
+
| `useTreeContext()` | Hook | Raw TreeContext value (usually use useFileTree instead) |
|
|
219
|
+
|
|
220
|
+
### UI (`momoi-explorer/ui`)
|
|
221
|
+
|
|
222
|
+
| Export | Description |
|
|
223
|
+
|---|---|
|
|
224
|
+
| `FileExplorer` | All-in-one component (includes TreeProvider, virtual scrolling, context menu) |
|
|
225
|
+
| `TreeNodeRow` | Single tree row (icon, indent, selection, rename) |
|
|
226
|
+
| `ContextMenu` | Right-click menu (closes on outside click/Esc) |
|
|
227
|
+
| `InlineRename` | Inline rename input (Enter to confirm, Esc to cancel) |
|
|
228
|
+
| `TreeFilterBar` | Fuzzy search filter bar |
|
|
229
|
+
| `QuickOpen` | VSCode-style quick open dialog (Ctrl+P equivalent) |
|
|
230
|
+
|
|
231
|
+
**Styles:**
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import 'momoi-explorer/ui/style.css'
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
VSCode-style dark theme. Customizable via CSS variables and class names (`.momoi-explorer-*`).
|
|
238
|
+
|
|
239
|
+
### FileExplorer Props
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
<FileExplorer
|
|
243
|
+
adapter={adapter} // FileSystemAdapter (required)
|
|
244
|
+
rootPath="/path/to/dir" // Root path (required)
|
|
245
|
+
sort={(a, b) => ...} // Custom sort
|
|
246
|
+
filter={(entry) => ...} // Custom filter
|
|
247
|
+
watchOptions={{ ... }} // File watching options
|
|
248
|
+
onEvent={(e) => ...} // Tree event callback
|
|
249
|
+
onOpen={(path) => ...} // File double-click handler
|
|
250
|
+
renderIcon={(node, expanded) => ...} // Custom icon renderer
|
|
251
|
+
renderBadge={(node) => ...} // Custom badge renderer (e.g. git status)
|
|
252
|
+
contextMenuItems={(nodes) => [...]} // Context menu items
|
|
253
|
+
showFilterBar // Show filter bar
|
|
254
|
+
onControllerReady={(ctrl) => ...} // Get controller reference
|
|
255
|
+
inputService={inputService} // momoi-keybind InputService instance (optional)
|
|
256
|
+
onKeyDown={(e) => ...} // Key event handler when not using momoi-keybind
|
|
257
|
+
className="my-explorer" // CSS class
|
|
258
|
+
style={{ height: 400 }} // Inline styles
|
|
259
|
+
/>
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### QuickOpen Usage
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
import { FileExplorer, QuickOpen } from 'momoi-explorer/ui'
|
|
266
|
+
|
|
267
|
+
function App() {
|
|
268
|
+
const [ctrl, setCtrl] = useState<FileTreeController | null>(null)
|
|
269
|
+
const [quickOpen, setQuickOpen] = useState(false)
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<>
|
|
273
|
+
<FileExplorer
|
|
274
|
+
adapter={adapter}
|
|
275
|
+
rootPath={rootPath}
|
|
276
|
+
onControllerReady={setCtrl}
|
|
277
|
+
/>
|
|
278
|
+
{ctrl && (
|
|
279
|
+
<QuickOpen
|
|
280
|
+
controller={ctrl}
|
|
281
|
+
isOpen={quickOpen}
|
|
282
|
+
onClose={() => setQuickOpen(false)}
|
|
283
|
+
onSelect={(entry) => openFile(entry.path)}
|
|
284
|
+
/>
|
|
285
|
+
)}
|
|
286
|
+
</>
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Key Types
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
interface FileEntry {
|
|
295
|
+
name: string // File name
|
|
296
|
+
path: string // Absolute path
|
|
297
|
+
isDirectory: boolean // Is directory
|
|
298
|
+
meta?: Record<string, unknown> // Extension metadata
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
interface TreeNode extends FileEntry {
|
|
302
|
+
depth: number
|
|
303
|
+
children?: TreeNode[]
|
|
304
|
+
childrenLoaded: boolean
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
interface FlatNode {
|
|
308
|
+
node: TreeNode
|
|
309
|
+
depth: number
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
interface TreeState {
|
|
313
|
+
rootPath: string
|
|
314
|
+
rootNodes: TreeNode[]
|
|
315
|
+
expandedPaths: Set<string>
|
|
316
|
+
selectedPaths: Set<string>
|
|
317
|
+
anchorPath: string | null
|
|
318
|
+
renamingPath: string | null
|
|
319
|
+
creatingState: CreatingState | null
|
|
320
|
+
searchQuery: string | null
|
|
321
|
+
flatList: FlatNode[]
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
type TreeEvent =
|
|
325
|
+
| { type: 'expand'; path: string }
|
|
326
|
+
| { type: 'collapse'; path: string }
|
|
327
|
+
| { type: 'select'; paths: string[] }
|
|
328
|
+
| { type: 'open'; path: string }
|
|
329
|
+
| { type: 'rename'; oldPath: string; newPath: string }
|
|
330
|
+
| { type: 'delete'; paths: string[] }
|
|
331
|
+
| { type: 'create'; parentPath: string; name: string; isDirectory: boolean }
|
|
332
|
+
| { type: 'refresh'; path?: string }
|
|
333
|
+
| { type: 'external-change'; changes: WatchEvent[] }
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## File Watching
|
|
337
|
+
|
|
338
|
+
Implementing `adapter.watch` automatically enables file watching. Just emit raw events; the core handles:
|
|
339
|
+
|
|
340
|
+
- **Debounce** (75ms, VSCode-compatible)
|
|
341
|
+
- **Event coalescing**: rename → delete+create, delete+create (same path) → modify, child events removed on parent delete
|
|
342
|
+
- **Throttling**: Chunk splitting for large batches (500 events / 200ms interval)
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
const tree = createFileTree({
|
|
346
|
+
adapter,
|
|
347
|
+
rootPath: '/project',
|
|
348
|
+
watchOptions: {
|
|
349
|
+
debounceMs: 100, // Default: 75
|
|
350
|
+
coalesce: true, // Default: true
|
|
351
|
+
throttle: {
|
|
352
|
+
maxChunkSize: 1000, // Default: 500
|
|
353
|
+
delayMs: 300, // Default: 200
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
})
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## Keybinding Integration (momoi-keybind)
|
|
360
|
+
|
|
361
|
+
When `momoi-keybind` is installed, pass an `inputService` prop to enable keybindings. `momoi-keybind` is an optional peer dependency.
|
|
362
|
+
|
|
363
|
+
```tsx
|
|
364
|
+
import { InputService } from 'momoi-keybind'
|
|
365
|
+
import { defaultExplorerKeybindings } from 'momoi-explorer'
|
|
366
|
+
import { FileExplorer } from 'momoi-explorer/ui'
|
|
367
|
+
|
|
368
|
+
const inputService = new InputService({
|
|
369
|
+
defaultKeybindings: defaultExplorerKeybindings,
|
|
370
|
+
})
|
|
371
|
+
inputService.start()
|
|
372
|
+
|
|
373
|
+
<FileExplorer
|
|
374
|
+
adapter={adapter}
|
|
375
|
+
rootPath={rootPath}
|
|
376
|
+
inputService={inputService}
|
|
377
|
+
/>
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
**Default Keybindings:**
|
|
381
|
+
|
|
382
|
+
| Key | Command |
|
|
383
|
+
|---|---|
|
|
384
|
+
| `Delete` | Delete selected items |
|
|
385
|
+
| `F2` | Rename |
|
|
386
|
+
| `Ctrl+N` | New file |
|
|
387
|
+
| `Ctrl+Shift+N` | New folder |
|
|
388
|
+
| `Ctrl+R` | Refresh |
|
|
389
|
+
| `Ctrl+Shift+E` | Collapse all folders |
|
|
390
|
+
| `Ctrl+A` | Select all |
|
|
391
|
+
| `Ctrl+Shift+C` | Copy path |
|
|
392
|
+
|
|
393
|
+
Override, add, or disable keybindings on the user side:
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
const inputService = new InputService({
|
|
397
|
+
defaultKeybindings: defaultExplorerKeybindings,
|
|
398
|
+
userKeybindings: [
|
|
399
|
+
{ key: 'F2', command: 'myApp.quickPreview', when: 'explorerFocus' }, // Override
|
|
400
|
+
{ key: '', command: '-explorer.delete' }, // Disable
|
|
401
|
+
],
|
|
402
|
+
})
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## License
|
|
406
|
+
|
|
407
|
+
MIT
|