momoi-explorer 0.8.2 → 0.8.4

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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # momoi-explorer
2
2
 
3
+ > Built with [Claude Code](https://claude.com/claude-code) by Anthropic.
4
+
3
5
  A headless file explorer library. Framework-agnostic core + React bindings + default UI in a 3-layer architecture.
4
6
 
5
7
  ## Install
@@ -53,6 +55,11 @@ const adapter: FileSystemAdapter = {
53
55
  async createDir(parentPath, name) {
54
56
  await fs.mkdir(path.join(parentPath, name))
55
57
  },
58
+ // Optional: move (enables drag & drop)
59
+ async move(srcPath, destDir) {
60
+ const name = path.basename(srcPath)
61
+ await fs.rename(srcPath, path.join(destDir, name))
62
+ },
56
63
  // Optional: file watching (debounce & coalescing handled by core)
57
64
  watch(dirPath, callback) {
58
65
  const watcher = fs.watch(dirPath, { recursive: true }, (event, filename) => {
@@ -182,6 +189,8 @@ Main entry point for the headless file tree.
182
189
  | `createFile(parentPath, name)` | Create a file |
183
190
  | `createDir(parentPath, name)` | Create a directory |
184
191
  | `deleteSelected()` | Delete selected items |
192
+ | `moveItems(srcPaths, destDir)` | Move files/folders to another directory (requires adapter.move) |
193
+ | `canDrop(srcPaths, targetPath)` | Check if drop target is valid (self/descendant/same-parent check) |
185
194
  | `refresh(path?)` | Refresh the tree (preserves expanded state) |
186
195
  | `setSearchQuery(query)` | Set fuzzy search query (null to clear) |
187
196
  | `collectAllFiles()` | Recursively collect all files (for QuickOpen) |
@@ -215,6 +224,7 @@ Main entry point for the headless file tree.
215
224
  | `useContextMenu()` | Hook | Context menu visibility control (show/hide + position) |
216
225
  | `useExplorerKeybindings(inputService)` | Hook | momoi-keybind integration. Registers explorer command handlers |
217
226
  | `useExplorerFocus(inputService)` | Hook | Syncs focus state with momoi-keybind context |
227
+ | `useDragDrop(enabled)` | Hook | Drag & drop container event handlers (used internally by FileExplorer) |
218
228
  | `useTreeContext()` | Hook | Raw TreeContext value (usually use useFileTree instead) |
219
229
 
220
230
  ### UI (`momoi-explorer/ui`)
@@ -329,6 +339,7 @@ type TreeEvent =
329
339
  | { type: 'rename'; oldPath: string; newPath: string }
330
340
  | { type: 'delete'; paths: string[] }
331
341
  | { type: 'create'; parentPath: string; name: string; isDirectory: boolean }
342
+ | { type: 'move'; srcPaths: string[]; destDir: string }
332
343
  | { type: 'refresh'; path?: string }
333
344
  | { type: 'external-change'; changes: WatchEvent[] }
334
345
  ```
package/dist/index.cjs CHANGED
@@ -319,6 +319,11 @@ function dirname(path) {
319
319
  const idx = path.lastIndexOf(sep);
320
320
  return idx === -1 ? "" : path.slice(0, idx);
321
321
  }
322
+ function basename(path) {
323
+ const sep = path.includes("\\") ? "\\" : "/";
324
+ const idx = path.lastIndexOf(sep);
325
+ return idx === -1 ? path : path.slice(idx + 1);
326
+ }
322
327
  function createFileTree(options) {
323
328
  const { adapter, rootPath, onEvent } = options;
324
329
  let sortFn = options.sort ?? defaultSort;
@@ -602,6 +607,47 @@ function createFileTree(options) {
602
607
  notify();
603
608
  emit({ type: "delete", paths });
604
609
  },
610
+ canDrop(srcPaths, targetPath) {
611
+ if (!adapter.move) return false;
612
+ if (srcPaths.length === 0) return false;
613
+ const node = findNode(state.rootNodes, targetPath);
614
+ let destDir;
615
+ if (!node) {
616
+ destDir = rootPath;
617
+ } else if (node.isDirectory) {
618
+ destDir = targetPath;
619
+ } else {
620
+ destDir = dirname(targetPath);
621
+ }
622
+ for (const src of srcPaths) {
623
+ if (src === destDir) return false;
624
+ const sep = src.includes("\\") ? "\\" : "/";
625
+ if (destDir.startsWith(src + sep)) return false;
626
+ if (dirname(src) === destDir) return false;
627
+ }
628
+ return true;
629
+ },
630
+ async moveItems(srcPaths, destDir) {
631
+ if (!adapter.move || srcPaths.length === 0) return;
632
+ for (const src of srcPaths) {
633
+ await adapter.move(src, destDir);
634
+ }
635
+ const parentDirs = new Set(srcPaths.map(dirname));
636
+ parentDirs.add(destDir);
637
+ for (const dir of parentDirs) {
638
+ await refreshParent(dir);
639
+ }
640
+ if (destDir !== rootPath && !state.expandedPaths.has(destDir)) {
641
+ state.expandedPaths = new Set(state.expandedPaths);
642
+ state.expandedPaths.add(destDir);
643
+ }
644
+ const sep = destDir.includes("\\") ? "\\" : "/";
645
+ const newPaths = srcPaths.map((src) => destDir + sep + basename(src));
646
+ state.selectedPaths = new Set(newPaths);
647
+ state.anchorPath = newPaths[0] ?? null;
648
+ notify();
649
+ emit({ type: "move", srcPaths, destDir });
650
+ },
605
651
  async refresh(path) {
606
652
  if (!path || path === rootPath) {
607
653
  await refreshParent(rootPath);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/core/event-processor.ts","../src/core/flatten.ts","../src/core/selection.ts","../src/core/sort.ts","../src/core/filter.ts","../src/core/search.ts","../src/core/tree.ts","../src/core/keybindings.ts"],"sourcesContent":["export * from './core'\n","// イベント処理ユーティリティ\r\n//\r\n// VSCode準拠のデバウンス・イベント合体・スロットリング\r\n\r\nimport type { RawWatchEvent, WatchEvent, WatchOptions } from './types'\r\n\r\nconst DEFAULT_DEBOUNCE_MS = 75\r\nconst DEFAULT_THROTTLE_CHUNK_SIZE = 500\r\nconst DEFAULT_THROTTLE_DELAY_MS = 200\r\n\r\n/**\r\n * 生イベントを合体する(VSCode EventCoalescer 準拠)。\r\n * - rename → delete(old) + create(new) に分解\r\n * - delete + create(同一パス) → modify に合体\r\n * - create + modify(同一パス) → create を維持\r\n * - 親フォルダ delete → 子の delete を除去\r\n *\r\n * @param raw - アダプタから受け取った生イベントの配列\r\n * @returns 合体処理後のWatchEvent配列\r\n */\r\nexport function coalesceEvents(raw: RawWatchEvent[]): WatchEvent[] {\r\n // rename を分解\r\n const expanded: WatchEvent[] = []\r\n for (const event of raw) {\r\n if (event.type === 'rename' && event.newPath) {\r\n expanded.push({ type: 'delete', path: event.path, isDirectory: event.isDirectory })\r\n expanded.push({ type: 'create', path: event.newPath, isDirectory: event.isDirectory })\r\n } else {\r\n expanded.push({ type: event.type as WatchEvent['type'], path: event.path, isDirectory: event.isDirectory })\r\n }\r\n }\r\n\r\n // 同一パスのイベントを合体\r\n const byPath = new Map<string, WatchEvent>()\r\n for (const event of expanded) {\r\n const existing = byPath.get(event.path)\r\n if (!existing) {\r\n byPath.set(event.path, event)\r\n continue\r\n }\r\n\r\n // delete + create → modify\r\n if (existing.type === 'delete' && event.type === 'create') {\r\n byPath.set(event.path, { type: 'modify', path: event.path, isDirectory: event.isDirectory })\r\n continue\r\n }\r\n\r\n // create + modify → create を維持\r\n if (existing.type === 'create' && event.type === 'modify') {\r\n continue\r\n }\r\n\r\n // create + delete → 相殺して除去\r\n if (existing.type === 'create' && event.type === 'delete') {\r\n byPath.delete(event.path)\r\n continue\r\n }\r\n\r\n // それ以外は後勝ち\r\n byPath.set(event.path, event)\r\n }\r\n\r\n const result = Array.from(byPath.values())\r\n\r\n // 親フォルダ delete がある場合、子の delete を除去\r\n const deletedDirs = new Set<string>()\r\n for (const event of result) {\r\n if (event.type === 'delete' && event.isDirectory) {\r\n deletedDirs.add(event.path)\r\n }\r\n }\r\n\r\n if (deletedDirs.size === 0) return result\r\n\r\n return result.filter((event) => {\r\n if (event.type !== 'delete') return true\r\n for (const dir of deletedDirs) {\r\n if (event.path !== dir && event.path.startsWith(dir + '/')) {\r\n return false\r\n }\r\n }\r\n return true\r\n })\r\n}\r\n\r\n/**\r\n * イベントプロセッサを生成する。\r\n * デバウンス → 合体 → スロットリング → コールバック のパイプラインで処理する。\r\n *\r\n * @param callback - 処理済みイベントを受け取るコールバック\r\n * @param options - デバウンス・合体・スロットリングの設定\r\n * @returns push/flush/destroyメソッドを持つプロセッサオブジェクト\r\n */\r\nexport function createEventProcessor(\r\n callback: (events: WatchEvent[]) => void,\r\n options: WatchOptions = {},\r\n): {\r\n push(events: RawWatchEvent[]): void\r\n flush(): void\r\n destroy(): void\r\n} {\r\n const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS\r\n const shouldCoalesce = options.coalesce ?? true\r\n const throttleChunkSize = options.throttle?.maxChunkSize ?? DEFAULT_THROTTLE_CHUNK_SIZE\r\n const throttleDelayMs = options.throttle?.delayMs ?? DEFAULT_THROTTLE_DELAY_MS\r\n\r\n let buffer: RawWatchEvent[] = []\r\n let debounceTimer: ReturnType<typeof setTimeout> | null = null\r\n let throttleTimer: ReturnType<typeof setTimeout> | null = null\r\n let destroyed = false\r\n\r\n function processBuffer(): void {\r\n if (destroyed || buffer.length === 0) return\r\n\r\n const raw = buffer\r\n buffer = []\r\n\r\n const events = shouldCoalesce ? coalesceEvents(raw) : raw.map((e) => ({\r\n type: e.type === 'rename' ? 'modify' as const : e.type as WatchEvent['type'],\r\n path: e.type === 'rename' && e.newPath ? e.newPath : e.path,\r\n isDirectory: e.isDirectory,\r\n }))\r\n\r\n if (events.length === 0) return\r\n\r\n // スロットリング: 大量イベントをチャンクに分割\r\n if (events.length <= throttleChunkSize) {\r\n callback(events)\r\n return\r\n }\r\n\r\n let offset = 0\r\n function emitChunk(): void {\r\n if (destroyed || offset >= events.length) return\r\n const chunk = events.slice(offset, offset + throttleChunkSize)\r\n offset += throttleChunkSize\r\n callback(chunk)\r\n if (offset < events.length) {\r\n throttleTimer = setTimeout(emitChunk, throttleDelayMs)\r\n }\r\n }\r\n emitChunk()\r\n }\r\n\r\n return {\r\n push(events: RawWatchEvent[]): void {\r\n if (destroyed) return\r\n buffer.push(...events)\r\n if (debounceTimer !== null) {\r\n clearTimeout(debounceTimer)\r\n }\r\n debounceTimer = setTimeout(processBuffer, debounceMs)\r\n },\r\n\r\n flush(): void {\r\n if (debounceTimer !== null) {\r\n clearTimeout(debounceTimer)\r\n debounceTimer = null\r\n }\r\n processBuffer()\r\n },\r\n\r\n destroy(): void {\r\n destroyed = true\r\n if (debounceTimer !== null) clearTimeout(debounceTimer)\r\n if (throttleTimer !== null) clearTimeout(throttleTimer)\r\n buffer = []\r\n },\r\n }\r\n}\r\n","// ツリー → フラットリスト変換(仮想スクロール用)\n\nimport type { FlatNode, TreeNode } from './types'\n\n/**\n * 展開されたツリーをフラットリストに変換する。\n * react-virtuoso等の仮想スクロールに渡すためのもの。\n * matchingPaths が指定された場合、そこに含まれるノードのみ表示する。\n */\nexport function flattenTree(\n nodes: TreeNode[],\n expandedPaths: Set<string>,\n matchingPaths?: Set<string> | null,\n): FlatNode[] {\n const result: FlatNode[] = []\n\n function walk(children: TreeNode[], depth: number): void {\n for (const node of children) {\n if (matchingPaths && !matchingPaths.has(node.path)) continue\n\n result.push({ node, depth })\n if (node.isDirectory && node.children) {\n // 検索中はマッチしたディレクトリを自動展開\n if (matchingPaths || expandedPaths.has(node.path)) {\n walk(node.children, depth + 1)\n }\n }\n }\n }\n\n walk(nodes, 0)\n return result\n}\n","// 選択管理(単一/複数/範囲)\n\nimport type { FlatNode } from './types'\n\n/**\n * replace: 既存選択をクリアして新しいパスのみ選択\n * toggle: 指定パスの選択を反転(Ctrl+Click)\n * range: anchorから指定パスまでの範囲を選択(Shift+Click)\n */\nexport function computeSelection(\n currentSelected: Set<string>,\n anchorPath: string | null,\n targetPath: string,\n mode: 'replace' | 'toggle' | 'range',\n flatList: FlatNode[],\n): { selectedPaths: Set<string>; anchorPath: string } {\n switch (mode) {\n case 'replace':\n return {\n selectedPaths: new Set([targetPath]),\n anchorPath: targetPath,\n }\n\n case 'toggle': {\n const next = new Set(currentSelected)\n if (next.has(targetPath)) {\n next.delete(targetPath)\n } else {\n next.add(targetPath)\n }\n return {\n selectedPaths: next,\n anchorPath: targetPath,\n }\n }\n\n case 'range': {\n if (!anchorPath) {\n return {\n selectedPaths: new Set([targetPath]),\n anchorPath: targetPath,\n }\n }\n\n const paths = flatList.map((f) => f.node.path)\n const anchorIdx = paths.indexOf(anchorPath)\n const targetIdx = paths.indexOf(targetPath)\n\n if (anchorIdx === -1 || targetIdx === -1) {\n return {\n selectedPaths: new Set([targetPath]),\n anchorPath: targetPath,\n }\n }\n\n const start = Math.min(anchorIdx, targetIdx)\n const end = Math.max(anchorIdx, targetIdx)\n const rangePaths = new Set(paths.slice(start, end + 1))\n\n return {\n selectedPaths: rangePaths,\n anchorPath, // range選択ではanchorは変えない\n }\n }\n }\n}\n","// ソート\n\nimport type { FileEntry } from './types'\n\n/**\n * デフォルトソート: フォルダ優先 → 名前の大文字小文字無視で昇順\n */\nexport function defaultSort(a: FileEntry, b: FileEntry): number {\n if (a.isDirectory !== b.isDirectory) {\n return a.isDirectory ? -1 : 1\n }\n return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })\n}\n","// フィルタ\n\nimport type { FileEntry } from './types'\n\n/**\n * デフォルトフィルタ: 全表示(フィルタなし)\n */\nexport function defaultFilter(_entry: FileEntry): boolean {\n return true\n}\n","// ファイル名検索ユーティリティ\n\nimport type { FileEntry, TreeNode } from './types'\n\n/**\n * ファジーマッチ: クエリの各文字が順番に含まれているかチェック\n * スコアも返す(連続マッチ・先頭マッチ・セパレータ後マッチが高スコア)\n */\nexport function fuzzyMatch(query: string, target: string): { match: boolean; score: number } {\n const q = query.toLowerCase()\n const t = target.toLowerCase()\n\n if (q.length === 0) return { match: true, score: 0 }\n if (q.length > t.length) return { match: false, score: 0 }\n\n // 完全一致は最高スコア\n if (t === q) return { match: true, score: 100 }\n\n // 前方一致は高スコア\n if (t.startsWith(q)) return { match: true, score: 90 }\n\n // 部分一致\n if (t.includes(q)) return { match: true, score: 80 }\n\n // ファジーマッチ\n let qi = 0\n let score = 0\n let lastMatchIndex = -2\n\n for (let ti = 0; ti < t.length && qi < q.length; ti++) {\n if (t[ti] === q[qi]) {\n qi++\n // 連続マッチボーナス\n if (ti === lastMatchIndex + 1) {\n score += 5\n }\n // セパレータ直後のマッチボーナス(パス区切り、ハイフン、アンダースコア等)\n if (ti === 0 || '/\\\\-_.'.includes(t[ti - 1])) {\n score += 10\n }\n score += 1\n lastMatchIndex = ti\n }\n }\n\n if (qi < q.length) return { match: false, score: 0 }\n return { match: true, score }\n}\n\n/**\n * ツリーフィルタ用: クエリにマッチするノードとその祖先を残す\n * マッチしたパスのSetを返す\n */\nexport function findMatchingPaths(\n nodes: TreeNode[],\n query: string,\n): Set<string> {\n const matching = new Set<string>()\n\n function walk(node: TreeNode, ancestors: string[]): boolean {\n const nameMatch = fuzzyMatch(query, node.name).match\n let childMatch = false\n\n if (node.children) {\n for (const child of node.children) {\n if (walk(child, [...ancestors, node.path])) {\n childMatch = true\n }\n }\n }\n\n if (nameMatch || childMatch) {\n matching.add(node.path)\n for (const a of ancestors) {\n matching.add(a)\n }\n return true\n }\n return false\n }\n\n for (const node of nodes) {\n walk(node, [])\n }\n\n return matching\n}\n\n/**\n * ファジーファインド用: 全ファイルからスコア順に候補を返す\n */\nexport function fuzzyFind(\n files: FileEntry[],\n query: string,\n maxResults: number = 50,\n): FileEntry[] {\n if (!query) return []\n\n const scored: Array<{ entry: FileEntry; score: number }> = []\n\n for (const entry of files) {\n // ファイル名でマッチ\n const nameResult = fuzzyMatch(query, entry.name)\n // パスでもマッチ(スコアは低め)\n const pathResult = fuzzyMatch(query, entry.path)\n\n const bestScore = Math.max(nameResult.score, pathResult.score * 0.5)\n\n if (nameResult.match || pathResult.match) {\n scored.push({ entry, score: bestScore })\n }\n }\n\n scored.sort((a, b) => b.score - a.score)\n return scored.slice(0, maxResults).map((s) => s.entry)\n}\n","// createFileTree - ヘッドレスファイルツリーコントローラ\r\n\r\nimport type {\r\n FileEntry,\r\n FileSystemAdapter,\r\n FileTreeController,\r\n FileTreeOptions,\r\n FlatNode,\r\n TreeEvent,\r\n TreeNode,\r\n TreeState,\r\n WatchEvent,\r\n} from './types'\r\nimport { createEventProcessor } from './event-processor'\r\nimport { flattenTree } from './flatten'\r\nimport { computeSelection } from './selection'\r\nimport { defaultSort } from './sort'\r\nimport { defaultFilter } from './filter'\r\nimport { findMatchingPaths } from './search'\r\n\r\nfunction toTreeNode(entry: FileEntry, depth: number): TreeNode {\r\n return {\r\n ...entry,\r\n depth,\r\n children: entry.isDirectory ? undefined : undefined,\r\n childrenLoaded: false,\r\n }\r\n}\r\n\r\nfunction findNode(nodes: TreeNode[], path: string): TreeNode | undefined {\r\n for (const node of nodes) {\r\n if (node.path === path) return node\r\n if (node.children) {\r\n const found = findNode(node.children, path)\r\n if (found) return found\r\n }\r\n }\r\n return undefined\r\n}\r\n\r\nfunction findParentNodes(nodes: TreeNode[], targetPath: string): TreeNode | undefined {\r\n for (const node of nodes) {\r\n if (node.children) {\r\n for (const child of node.children) {\r\n if (child.path === targetPath) return node\r\n }\r\n const found = findParentNodes(node.children, targetPath)\r\n if (found) return found\r\n }\r\n }\r\n return undefined\r\n}\r\n\r\n/** パスの親ディレクトリを取得 */\r\nfunction dirname(path: string): string {\r\n const sep = path.includes('\\\\') ? '\\\\' : '/'\r\n const idx = path.lastIndexOf(sep)\r\n return idx === -1 ? '' : path.slice(0, idx)\r\n}\r\n\r\n/** パスの末尾(ファイル名)を取得 */\r\nfunction basename(path: string): string {\r\n const sep = path.includes('\\\\') ? '\\\\' : '/'\r\n const idx = path.lastIndexOf(sep)\r\n return idx === -1 ? path : path.slice(idx + 1)\r\n}\r\n\r\n/**\r\n * ヘッドレスファイルツリーコントローラを生成する。\r\n * フレームワーク非依存。React等で使う場合は `momoi-explorer/react` のTreeProviderを推奨。\r\n *\r\n * @param options - ツリーの初期化オプション\r\n * @returns FileTreeController インスタンス\r\n *\r\n * @example\r\n * ```ts\r\n * import { createFileTree } from 'momoi-explorer'\r\n *\r\n * const tree = createFileTree({\r\n * adapter: myAdapter,\r\n * rootPath: '/home/user/project',\r\n * onEvent: (e) => console.log(e),\r\n * })\r\n * await tree.loadRoot()\r\n * ```\r\n */\r\nexport function createFileTree(options: FileTreeOptions): FileTreeController {\r\n const { adapter, rootPath, onEvent } = options\r\n let sortFn = options.sort ?? defaultSort\r\n let filterFn = options.filter ?? defaultFilter\r\n\r\n let state: TreeState = {\r\n rootPath,\r\n rootNodes: [],\r\n expandedPaths: new Set(),\r\n selectedPaths: new Set(),\r\n anchorPath: null,\r\n renamingPath: null,\r\n creatingState: null,\r\n searchQuery: null,\r\n flatList: [],\r\n }\r\n\r\n const listeners = new Set<(state: TreeState) => void>()\r\n let expandingPaths = new Set<string>()\r\n\r\n function emit(event: TreeEvent): void {\r\n onEvent?.(event)\r\n }\r\n\r\n function notify(): void {\r\n const matchingPaths = state.searchQuery\r\n ? findMatchingPaths(state.rootNodes, state.searchQuery)\r\n : null\r\n state = { ...state, flatList: flattenTree(state.rootNodes, state.expandedPaths, matchingPaths) }\r\n for (const listener of listeners) {\r\n listener(state)\r\n }\r\n }\r\n\r\n async function loadChildren(node: TreeNode): Promise<void> {\r\n const entries = await adapter.readDir(node.path)\r\n const filtered = entries.filter(filterFn)\r\n filtered.sort(sortFn)\r\n // 既存の展開済み子ノードのchildrenを引き継ぐ\r\n const oldChildMap = node.children\r\n ? new Map(node.children.map((c) => [c.path, c]))\r\n : new Map<string, TreeNode>()\r\n node.children = filtered.map((e) => {\r\n const existing = oldChildMap.get(e.path)\r\n if (existing && existing.childrenLoaded) {\r\n return { ...toTreeNode(e, node.depth + 1), children: existing.children, childrenLoaded: true }\r\n }\r\n return toTreeNode(e, node.depth + 1)\r\n })\r\n node.childrenLoaded = true\r\n }\r\n\r\n /** 親ディレクトリをリフレッシュし、展開中のフォルダの子も再読み込みする */\r\n async function refreshParent(parentPath: string): Promise<void> {\r\n const parentNode = findNode(state.rootNodes, parentPath)\r\n if (parentNode) {\r\n await loadChildren(parentNode)\r\n state.expandedPaths = new Set(state.expandedPaths)\r\n state.expandedPaths.add(parentPath)\r\n } else if (parentPath === rootPath) {\r\n const entries = await adapter.readDir(rootPath)\r\n const filtered = entries.filter(filterFn)\r\n filtered.sort(sortFn)\r\n // 既存の展開済みノードのchildrenを引き継ぐ\r\n const oldNodeMap = new Map(state.rootNodes.map((n) => [n.path, n]))\r\n state.rootNodes = filtered.map((e) => {\r\n const existing = oldNodeMap.get(e.path)\r\n if (existing && existing.childrenLoaded) {\r\n return { ...toTreeNode(e, 0), children: existing.children, childrenLoaded: true }\r\n }\r\n return toTreeNode(e, 0)\r\n })\r\n }\r\n }\r\n\r\n function sortNodes(nodes: TreeNode[]): void {\r\n nodes.sort(sortFn)\r\n for (const node of nodes) {\r\n if (node.children) {\r\n sortNodes(node.children)\r\n }\r\n }\r\n }\r\n\r\n function filterNodes(nodes: TreeNode[]): TreeNode[] {\r\n return nodes.filter((node) => {\r\n if (!filterFn(node)) return false\r\n if (node.children) {\r\n node.children = filterNodes(node.children)\r\n }\r\n return true\r\n })\r\n }\r\n\r\n // ウォッチ関連\r\n let unwatchFn: (() => void) | null = null\r\n let eventProcessor: ReturnType<typeof createEventProcessor> | null = null\r\n\r\n function handleWatchEvents(events: WatchEvent[]): void {\r\n emit({ type: 'external-change', changes: events })\r\n\r\n // 変更されたパスの親ディレクトリを収集してリフレッシュ\r\n const dirsToRefresh = new Set<string>()\r\n for (const event of events) {\r\n const parent = dirname(event.path)\r\n // 展開中のディレクトリのみリフレッシュ\r\n if (parent === rootPath || state.expandedPaths.has(parent)) {\r\n dirsToRefresh.add(parent)\r\n }\r\n // ディレクトリ自体が変更された場合、展開中ならそれもリフレッシュ\r\n if (event.isDirectory && state.expandedPaths.has(event.path)) {\r\n dirsToRefresh.add(event.path)\r\n }\r\n }\r\n\r\n // 非同期でリフレッシュ(展開状態を保持)\r\n for (const dir of dirsToRefresh) {\r\n refreshParent(dir).then(() => notify()).catch(() => {})\r\n }\r\n }\r\n\r\n function startWatching(): void {\r\n if (!adapter.watch) return\r\n\r\n eventProcessor = createEventProcessor(handleWatchEvents, options.watchOptions)\r\n unwatchFn = adapter.watch(rootPath, (events) => {\r\n eventProcessor!.push(events)\r\n })\r\n }\r\n\r\n function stopWatching(): void {\r\n if (unwatchFn) {\r\n unwatchFn()\r\n unwatchFn = null\r\n }\r\n if (eventProcessor) {\r\n eventProcessor.destroy()\r\n eventProcessor = null\r\n }\r\n }\r\n\r\n const controller: FileTreeController = {\r\n getState(): TreeState {\r\n return state\r\n },\r\n\r\n subscribe(listener: (s: TreeState) => void): () => void {\r\n listeners.add(listener)\r\n return () => listeners.delete(listener)\r\n },\r\n\r\n async loadRoot(): Promise<void> {\r\n const entries = await adapter.readDir(rootPath)\r\n const filtered = entries.filter(filterFn)\r\n filtered.sort(sortFn)\r\n state.rootNodes = filtered.map((e) => toTreeNode(e, 0))\r\n notify()\r\n startWatching()\r\n },\r\n\r\n async expand(path: string): Promise<void> {\r\n // 再入ガード: 同じパスの展開処理が進行中なら無視\r\n if (expandingPaths.has(path)) return\r\n expandingPaths.add(path)\r\n\r\n try {\r\n const node = findNode(state.rootNodes, path)\r\n if (!node || !node.isDirectory) return\r\n\r\n if (!node.childrenLoaded) {\r\n await loadChildren(node)\r\n }\r\n\r\n // await後にstateが変わっている可能性があるので再確認\r\n state.expandedPaths = new Set(state.expandedPaths)\r\n state.expandedPaths.add(path)\r\n notify()\r\n emit({ type: 'expand', path })\r\n } finally {\r\n expandingPaths.delete(path)\r\n }\r\n },\r\n\r\n collapse(path: string): void {\r\n const sep = path.includes('\\\\') ? '\\\\' : '/'\r\n const prefix = path + sep\r\n state.expandedPaths = new Set(state.expandedPaths)\r\n state.expandedPaths.delete(path)\r\n // 子孫の展開状態もクリア\r\n for (const p of state.expandedPaths) {\r\n if (p.startsWith(prefix)) {\r\n state.expandedPaths.delete(p)\r\n }\r\n }\r\n notify()\r\n emit({ type: 'collapse', path })\r\n },\r\n\r\n async toggleExpand(path: string): Promise<void> {\r\n // expand進行中は無視(ダブルクリックで展開→即collapseを防止)\r\n if (expandingPaths.has(path)) return\r\n\r\n if (state.expandedPaths.has(path)) {\r\n controller.collapse(path)\r\n } else {\r\n await controller.expand(path)\r\n }\r\n },\r\n\r\n async expandTo(path: string): Promise<void> {\r\n // パスの各祖先を展開していく\r\n const parts: string[] = []\r\n let current = path\r\n while (current !== rootPath && current !== '') {\r\n const parent = dirname(current)\r\n if (parent === current) break\r\n parts.unshift(parent)\r\n current = parent\r\n }\r\n\r\n for (const ancestorPath of parts) {\r\n if (ancestorPath === rootPath) continue\r\n if (!state.expandedPaths.has(ancestorPath)) {\r\n await controller.expand(ancestorPath)\r\n }\r\n }\r\n },\r\n\r\n select(path: string, mode: 'replace' | 'toggle' | 'range' = 'replace'): void {\r\n const result = computeSelection(\r\n state.selectedPaths,\r\n state.anchorPath,\r\n path,\r\n mode,\r\n state.flatList,\r\n )\r\n state.selectedPaths = result.selectedPaths\r\n state.anchorPath = result.anchorPath\r\n notify()\r\n emit({ type: 'select', paths: Array.from(result.selectedPaths) })\r\n },\r\n\r\n selectAll(): void {\r\n state.selectedPaths = new Set(state.flatList.map((f) => f.node.path))\r\n notify()\r\n emit({ type: 'select', paths: Array.from(state.selectedPaths) })\r\n },\r\n\r\n clearSelection(): void {\r\n state.selectedPaths = new Set()\r\n state.anchorPath = null\r\n notify()\r\n emit({ type: 'select', paths: [] })\r\n },\r\n\r\n startRename(path: string): void {\r\n state.renamingPath = path\r\n notify()\r\n },\r\n\r\n async commitRename(newName: string): Promise<void> {\r\n if (!state.renamingPath || !adapter.rename) return\r\n\r\n const oldPath = state.renamingPath\r\n const parent = dirname(oldPath)\r\n const sep = oldPath.includes('\\\\') ? '\\\\' : '/'\r\n const newPath = parent + sep + newName\r\n\r\n await adapter.rename(oldPath, newPath)\r\n state.renamingPath = null\r\n\r\n // リネームされたノードの親をリフレッシュ\r\n if (parent === rootPath) {\r\n await controller.loadRoot()\r\n } else {\r\n const parentNode = findNode(state.rootNodes, parent)\r\n if (parentNode) {\r\n await loadChildren(parentNode)\r\n }\r\n }\r\n\r\n notify()\r\n emit({ type: 'rename', oldPath, newPath })\r\n },\r\n\r\n cancelRename(): void {\r\n state.renamingPath = null\r\n notify()\r\n },\r\n\r\n async startCreate(parentPath: string, isDirectory: boolean, insertAfterPath?: string): Promise<void> {\r\n // 親フォルダを展開してから作成モードに入る\r\n if (parentPath !== rootPath && !state.expandedPaths.has(parentPath)) {\r\n await controller.expand(parentPath)\r\n }\r\n state.creatingState = { parentPath, isDirectory, insertAfterPath }\r\n notify()\r\n },\r\n\r\n async commitCreate(name: string): Promise<void> {\r\n if (!state.creatingState) return\r\n\r\n const { parentPath, isDirectory } = state.creatingState\r\n state.creatingState = null\r\n\r\n if (isDirectory) {\r\n await controller.createDir(parentPath, name)\r\n } else {\r\n await controller.createFile(parentPath, name)\r\n }\r\n },\r\n\r\n cancelCreate(): void {\r\n state.creatingState = null\r\n notify()\r\n },\r\n\r\n async createFile(parentPath: string, name: string): Promise<void> {\r\n if (!adapter.createFile) return\r\n await adapter.createFile(parentPath, name)\r\n await refreshParent(parentPath)\r\n\r\n notify()\r\n emit({ type: 'create', parentPath, name, isDirectory: false })\r\n },\r\n\r\n async createDir(parentPath: string, name: string): Promise<void> {\r\n if (!adapter.createDir) return\r\n await adapter.createDir(parentPath, name)\r\n await refreshParent(parentPath)\r\n\r\n notify()\r\n emit({ type: 'create', parentPath, name, isDirectory: true })\r\n },\r\n\r\n async deleteSelected(): Promise<void> {\r\n if (!adapter.delete || state.selectedPaths.size === 0) return\r\n\r\n const paths = Array.from(state.selectedPaths)\r\n await adapter.delete(paths)\r\n\r\n state.selectedPaths = new Set()\r\n state.anchorPath = null\r\n\r\n // 影響を受ける親ディレクトリをリフレッシュ\r\n const parentDirs = new Set(paths.map(dirname))\r\n for (const dir of parentDirs) {\r\n await refreshParent(dir)\r\n }\r\n\r\n notify()\r\n emit({ type: 'delete', paths })\r\n },\r\n\r\n async refresh(path?: string): Promise<void> {\r\n if (!path || path === rootPath) {\r\n await refreshParent(rootPath)\r\n } else {\r\n const node = findNode(state.rootNodes, path)\r\n if (node && node.isDirectory) {\r\n await loadChildren(node)\r\n }\r\n }\r\n\r\n notify()\r\n emit({ type: 'refresh', path })\r\n },\r\n\r\n setSearchQuery(query: string | null): void {\r\n state.searchQuery = query && query.trim() ? query.trim() : null\r\n notify()\r\n },\r\n\r\n async collectAllFiles(): Promise<FileEntry[]> {\r\n const result: FileEntry[] = []\r\n\r\n async function walk(dirPath: string): Promise<void> {\r\n const entries = await adapter.readDir(dirPath)\r\n for (const entry of entries) {\r\n if (!filterFn(entry)) continue\r\n result.push(entry)\r\n if (entry.isDirectory) {\r\n await walk(entry.path)\r\n }\r\n }\r\n }\r\n\r\n await walk(rootPath)\r\n return result\r\n },\r\n\r\n setFilter(fn: ((entry: FileEntry) => boolean) | null): void {\r\n filterFn = fn ?? defaultFilter\r\n // 既に読み込み済みのノードを再フィルタ\r\n state.rootNodes = filterNodes(state.rootNodes)\r\n notify()\r\n },\r\n\r\n setSort(fn: ((a: FileEntry, b: FileEntry) => number) | null): void {\r\n sortFn = fn ?? defaultSort\r\n sortNodes(state.rootNodes)\r\n notify()\r\n },\r\n\r\n destroy(): void {\r\n stopWatching()\r\n listeners.clear()\r\n },\r\n }\r\n\r\n return controller\r\n}\r\n","// エクスプローラー用のコマンドIDとデフォルトキーバインド定義\n//\n// momoi-keybind の KeybindingEntry 互換の形式でエクスポートする。\n// momoi-keybind が無くても型として参照できるように、独自の型を定義。\n\n/** momoi-keybind の KeybindingEntry と互換の型 */\nexport interface ExplorerKeybindingEntry {\n key: string\n command: string\n when?: string\n args?: unknown\n}\n\n/** エクスプローラーのコマンドID */\nexport const ExplorerCommands = {\n DELETE: 'explorer.delete',\n RENAME: 'explorer.rename',\n NEW_FILE: 'explorer.newFile',\n NEW_FOLDER: 'explorer.newFolder',\n REFRESH: 'explorer.refresh',\n COLLAPSE_ALL: 'explorer.collapseAll',\n SELECT_ALL: 'explorer.selectAll',\n COPY_PATH: 'explorer.copyPath',\n} as const\n\nexport type ExplorerCommandId = (typeof ExplorerCommands)[keyof typeof ExplorerCommands]\n\n/** デフォルトのキーバインド定義。momoi-keybindのInputServiceに渡せる形式 */\nexport const defaultExplorerKeybindings: ExplorerKeybindingEntry[] = [\n { key: 'Delete', command: ExplorerCommands.DELETE, when: 'explorerFocus' },\n { key: 'F2', command: ExplorerCommands.RENAME, when: 'explorerFocus' },\n { key: 'Ctrl+N', command: ExplorerCommands.NEW_FILE, when: 'explorerFocus' },\n { key: 'Ctrl+Shift+N', command: ExplorerCommands.NEW_FOLDER, when: 'explorerFocus' },\n { key: 'Ctrl+R', command: ExplorerCommands.REFRESH, when: 'explorerFocus' },\n { key: 'Ctrl+Shift+E', command: ExplorerCommands.COLLAPSE_ALL, when: 'explorerFocus' },\n { key: 'Ctrl+A', command: ExplorerCommands.SELECT_ALL, when: 'explorerFocus' },\n { key: 'Ctrl+Shift+C', command: ExplorerCommands.COPY_PATH, when: 'explorerFocus' },\n]\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/core/event-processor.ts","../src/core/flatten.ts","../src/core/selection.ts","../src/core/sort.ts","../src/core/filter.ts","../src/core/search.ts","../src/core/tree.ts","../src/core/keybindings.ts"],"sourcesContent":["export * from './core'\n","// イベント処理ユーティリティ\r\n//\r\n// VSCode準拠のデバウンス・イベント合体・スロットリング\r\n\r\nimport type { RawWatchEvent, WatchEvent, WatchOptions } from './types'\r\n\r\nconst DEFAULT_DEBOUNCE_MS = 75\r\nconst DEFAULT_THROTTLE_CHUNK_SIZE = 500\r\nconst DEFAULT_THROTTLE_DELAY_MS = 200\r\n\r\n/**\r\n * 生イベントを合体する(VSCode EventCoalescer 準拠)。\r\n * - rename → delete(old) + create(new) に分解\r\n * - delete + create(同一パス) → modify に合体\r\n * - create + modify(同一パス) → create を維持\r\n * - 親フォルダ delete → 子の delete を除去\r\n *\r\n * @param raw - アダプタから受け取った生イベントの配列\r\n * @returns 合体処理後のWatchEvent配列\r\n */\r\nexport function coalesceEvents(raw: RawWatchEvent[]): WatchEvent[] {\r\n // rename を分解\r\n const expanded: WatchEvent[] = []\r\n for (const event of raw) {\r\n if (event.type === 'rename' && event.newPath) {\r\n expanded.push({ type: 'delete', path: event.path, isDirectory: event.isDirectory })\r\n expanded.push({ type: 'create', path: event.newPath, isDirectory: event.isDirectory })\r\n } else {\r\n expanded.push({ type: event.type as WatchEvent['type'], path: event.path, isDirectory: event.isDirectory })\r\n }\r\n }\r\n\r\n // 同一パスのイベントを合体\r\n const byPath = new Map<string, WatchEvent>()\r\n for (const event of expanded) {\r\n const existing = byPath.get(event.path)\r\n if (!existing) {\r\n byPath.set(event.path, event)\r\n continue\r\n }\r\n\r\n // delete + create → modify\r\n if (existing.type === 'delete' && event.type === 'create') {\r\n byPath.set(event.path, { type: 'modify', path: event.path, isDirectory: event.isDirectory })\r\n continue\r\n }\r\n\r\n // create + modify → create を維持\r\n if (existing.type === 'create' && event.type === 'modify') {\r\n continue\r\n }\r\n\r\n // create + delete → 相殺して除去\r\n if (existing.type === 'create' && event.type === 'delete') {\r\n byPath.delete(event.path)\r\n continue\r\n }\r\n\r\n // それ以外は後勝ち\r\n byPath.set(event.path, event)\r\n }\r\n\r\n const result = Array.from(byPath.values())\r\n\r\n // 親フォルダ delete がある場合、子の delete を除去\r\n const deletedDirs = new Set<string>()\r\n for (const event of result) {\r\n if (event.type === 'delete' && event.isDirectory) {\r\n deletedDirs.add(event.path)\r\n }\r\n }\r\n\r\n if (deletedDirs.size === 0) return result\r\n\r\n return result.filter((event) => {\r\n if (event.type !== 'delete') return true\r\n for (const dir of deletedDirs) {\r\n if (event.path !== dir && event.path.startsWith(dir + '/')) {\r\n return false\r\n }\r\n }\r\n return true\r\n })\r\n}\r\n\r\n/**\r\n * イベントプロセッサを生成する。\r\n * デバウンス → 合体 → スロットリング → コールバック のパイプラインで処理する。\r\n *\r\n * @param callback - 処理済みイベントを受け取るコールバック\r\n * @param options - デバウンス・合体・スロットリングの設定\r\n * @returns push/flush/destroyメソッドを持つプロセッサオブジェクト\r\n */\r\nexport function createEventProcessor(\r\n callback: (events: WatchEvent[]) => void,\r\n options: WatchOptions = {},\r\n): {\r\n push(events: RawWatchEvent[]): void\r\n flush(): void\r\n destroy(): void\r\n} {\r\n const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS\r\n const shouldCoalesce = options.coalesce ?? true\r\n const throttleChunkSize = options.throttle?.maxChunkSize ?? DEFAULT_THROTTLE_CHUNK_SIZE\r\n const throttleDelayMs = options.throttle?.delayMs ?? DEFAULT_THROTTLE_DELAY_MS\r\n\r\n let buffer: RawWatchEvent[] = []\r\n let debounceTimer: ReturnType<typeof setTimeout> | null = null\r\n let throttleTimer: ReturnType<typeof setTimeout> | null = null\r\n let destroyed = false\r\n\r\n function processBuffer(): void {\r\n if (destroyed || buffer.length === 0) return\r\n\r\n const raw = buffer\r\n buffer = []\r\n\r\n const events = shouldCoalesce ? coalesceEvents(raw) : raw.map((e) => ({\r\n type: e.type === 'rename' ? 'modify' as const : e.type as WatchEvent['type'],\r\n path: e.type === 'rename' && e.newPath ? e.newPath : e.path,\r\n isDirectory: e.isDirectory,\r\n }))\r\n\r\n if (events.length === 0) return\r\n\r\n // スロットリング: 大量イベントをチャンクに分割\r\n if (events.length <= throttleChunkSize) {\r\n callback(events)\r\n return\r\n }\r\n\r\n let offset = 0\r\n function emitChunk(): void {\r\n if (destroyed || offset >= events.length) return\r\n const chunk = events.slice(offset, offset + throttleChunkSize)\r\n offset += throttleChunkSize\r\n callback(chunk)\r\n if (offset < events.length) {\r\n throttleTimer = setTimeout(emitChunk, throttleDelayMs)\r\n }\r\n }\r\n emitChunk()\r\n }\r\n\r\n return {\r\n push(events: RawWatchEvent[]): void {\r\n if (destroyed) return\r\n buffer.push(...events)\r\n if (debounceTimer !== null) {\r\n clearTimeout(debounceTimer)\r\n }\r\n debounceTimer = setTimeout(processBuffer, debounceMs)\r\n },\r\n\r\n flush(): void {\r\n if (debounceTimer !== null) {\r\n clearTimeout(debounceTimer)\r\n debounceTimer = null\r\n }\r\n processBuffer()\r\n },\r\n\r\n destroy(): void {\r\n destroyed = true\r\n if (debounceTimer !== null) clearTimeout(debounceTimer)\r\n if (throttleTimer !== null) clearTimeout(throttleTimer)\r\n buffer = []\r\n },\r\n }\r\n}\r\n","// ツリー → フラットリスト変換(仮想スクロール用)\n\nimport type { FlatNode, TreeNode } from './types'\n\n/**\n * 展開されたツリーをフラットリストに変換する。\n * react-virtuoso等の仮想スクロールに渡すためのもの。\n * matchingPaths が指定された場合、そこに含まれるノードのみ表示する。\n */\nexport function flattenTree(\n nodes: TreeNode[],\n expandedPaths: Set<string>,\n matchingPaths?: Set<string> | null,\n): FlatNode[] {\n const result: FlatNode[] = []\n\n function walk(children: TreeNode[], depth: number): void {\n for (const node of children) {\n if (matchingPaths && !matchingPaths.has(node.path)) continue\n\n result.push({ node, depth })\n if (node.isDirectory && node.children) {\n // 検索中はマッチしたディレクトリを自動展開\n if (matchingPaths || expandedPaths.has(node.path)) {\n walk(node.children, depth + 1)\n }\n }\n }\n }\n\n walk(nodes, 0)\n return result\n}\n","// 選択管理(単一/複数/範囲)\n\nimport type { FlatNode } from './types'\n\n/**\n * replace: 既存選択をクリアして新しいパスのみ選択\n * toggle: 指定パスの選択を反転(Ctrl+Click)\n * range: anchorから指定パスまでの範囲を選択(Shift+Click)\n */\nexport function computeSelection(\n currentSelected: Set<string>,\n anchorPath: string | null,\n targetPath: string,\n mode: 'replace' | 'toggle' | 'range',\n flatList: FlatNode[],\n): { selectedPaths: Set<string>; anchorPath: string } {\n switch (mode) {\n case 'replace':\n return {\n selectedPaths: new Set([targetPath]),\n anchorPath: targetPath,\n }\n\n case 'toggle': {\n const next = new Set(currentSelected)\n if (next.has(targetPath)) {\n next.delete(targetPath)\n } else {\n next.add(targetPath)\n }\n return {\n selectedPaths: next,\n anchorPath: targetPath,\n }\n }\n\n case 'range': {\n if (!anchorPath) {\n return {\n selectedPaths: new Set([targetPath]),\n anchorPath: targetPath,\n }\n }\n\n const paths = flatList.map((f) => f.node.path)\n const anchorIdx = paths.indexOf(anchorPath)\n const targetIdx = paths.indexOf(targetPath)\n\n if (anchorIdx === -1 || targetIdx === -1) {\n return {\n selectedPaths: new Set([targetPath]),\n anchorPath: targetPath,\n }\n }\n\n const start = Math.min(anchorIdx, targetIdx)\n const end = Math.max(anchorIdx, targetIdx)\n const rangePaths = new Set(paths.slice(start, end + 1))\n\n return {\n selectedPaths: rangePaths,\n anchorPath, // range選択ではanchorは変えない\n }\n }\n }\n}\n","// ソート\n\nimport type { FileEntry } from './types'\n\n/**\n * デフォルトソート: フォルダ優先 → 名前の大文字小文字無視で昇順\n */\nexport function defaultSort(a: FileEntry, b: FileEntry): number {\n if (a.isDirectory !== b.isDirectory) {\n return a.isDirectory ? -1 : 1\n }\n return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })\n}\n","// フィルタ\n\nimport type { FileEntry } from './types'\n\n/**\n * デフォルトフィルタ: 全表示(フィルタなし)\n */\nexport function defaultFilter(_entry: FileEntry): boolean {\n return true\n}\n","// ファイル名検索ユーティリティ\n\nimport type { FileEntry, TreeNode } from './types'\n\n/**\n * ファジーマッチ: クエリの各文字が順番に含まれているかチェック\n * スコアも返す(連続マッチ・先頭マッチ・セパレータ後マッチが高スコア)\n */\nexport function fuzzyMatch(query: string, target: string): { match: boolean; score: number } {\n const q = query.toLowerCase()\n const t = target.toLowerCase()\n\n if (q.length === 0) return { match: true, score: 0 }\n if (q.length > t.length) return { match: false, score: 0 }\n\n // 完全一致は最高スコア\n if (t === q) return { match: true, score: 100 }\n\n // 前方一致は高スコア\n if (t.startsWith(q)) return { match: true, score: 90 }\n\n // 部分一致\n if (t.includes(q)) return { match: true, score: 80 }\n\n // ファジーマッチ\n let qi = 0\n let score = 0\n let lastMatchIndex = -2\n\n for (let ti = 0; ti < t.length && qi < q.length; ti++) {\n if (t[ti] === q[qi]) {\n qi++\n // 連続マッチボーナス\n if (ti === lastMatchIndex + 1) {\n score += 5\n }\n // セパレータ直後のマッチボーナス(パス区切り、ハイフン、アンダースコア等)\n if (ti === 0 || '/\\\\-_.'.includes(t[ti - 1])) {\n score += 10\n }\n score += 1\n lastMatchIndex = ti\n }\n }\n\n if (qi < q.length) return { match: false, score: 0 }\n return { match: true, score }\n}\n\n/**\n * ツリーフィルタ用: クエリにマッチするノードとその祖先を残す\n * マッチしたパスのSetを返す\n */\nexport function findMatchingPaths(\n nodes: TreeNode[],\n query: string,\n): Set<string> {\n const matching = new Set<string>()\n\n function walk(node: TreeNode, ancestors: string[]): boolean {\n const nameMatch = fuzzyMatch(query, node.name).match\n let childMatch = false\n\n if (node.children) {\n for (const child of node.children) {\n if (walk(child, [...ancestors, node.path])) {\n childMatch = true\n }\n }\n }\n\n if (nameMatch || childMatch) {\n matching.add(node.path)\n for (const a of ancestors) {\n matching.add(a)\n }\n return true\n }\n return false\n }\n\n for (const node of nodes) {\n walk(node, [])\n }\n\n return matching\n}\n\n/**\n * ファジーファインド用: 全ファイルからスコア順に候補を返す\n */\nexport function fuzzyFind(\n files: FileEntry[],\n query: string,\n maxResults: number = 50,\n): FileEntry[] {\n if (!query) return []\n\n const scored: Array<{ entry: FileEntry; score: number }> = []\n\n for (const entry of files) {\n // ファイル名でマッチ\n const nameResult = fuzzyMatch(query, entry.name)\n // パスでもマッチ(スコアは低め)\n const pathResult = fuzzyMatch(query, entry.path)\n\n const bestScore = Math.max(nameResult.score, pathResult.score * 0.5)\n\n if (nameResult.match || pathResult.match) {\n scored.push({ entry, score: bestScore })\n }\n }\n\n scored.sort((a, b) => b.score - a.score)\n return scored.slice(0, maxResults).map((s) => s.entry)\n}\n","// createFileTree - ヘッドレスファイルツリーコントローラ\r\n\r\nimport type {\r\n FileEntry,\r\n FileSystemAdapter,\r\n FileTreeController,\r\n FileTreeOptions,\r\n FlatNode,\r\n TreeEvent,\r\n TreeNode,\r\n TreeState,\r\n WatchEvent,\r\n} from './types'\r\nimport { createEventProcessor } from './event-processor'\r\nimport { flattenTree } from './flatten'\r\nimport { computeSelection } from './selection'\r\nimport { defaultSort } from './sort'\r\nimport { defaultFilter } from './filter'\r\nimport { findMatchingPaths } from './search'\r\n\r\nfunction toTreeNode(entry: FileEntry, depth: number): TreeNode {\r\n return {\r\n ...entry,\r\n depth,\r\n children: entry.isDirectory ? undefined : undefined,\r\n childrenLoaded: false,\r\n }\r\n}\r\n\r\nfunction findNode(nodes: TreeNode[], path: string): TreeNode | undefined {\r\n for (const node of nodes) {\r\n if (node.path === path) return node\r\n if (node.children) {\r\n const found = findNode(node.children, path)\r\n if (found) return found\r\n }\r\n }\r\n return undefined\r\n}\r\n\r\nfunction findParentNodes(nodes: TreeNode[], targetPath: string): TreeNode | undefined {\r\n for (const node of nodes) {\r\n if (node.children) {\r\n for (const child of node.children) {\r\n if (child.path === targetPath) return node\r\n }\r\n const found = findParentNodes(node.children, targetPath)\r\n if (found) return found\r\n }\r\n }\r\n return undefined\r\n}\r\n\r\n/** パスの親ディレクトリを取得 */\r\nfunction dirname(path: string): string {\r\n const sep = path.includes('\\\\') ? '\\\\' : '/'\r\n const idx = path.lastIndexOf(sep)\r\n return idx === -1 ? '' : path.slice(0, idx)\r\n}\r\n\r\n/** パスの末尾(ファイル名)を取得 */\r\nfunction basename(path: string): string {\r\n const sep = path.includes('\\\\') ? '\\\\' : '/'\r\n const idx = path.lastIndexOf(sep)\r\n return idx === -1 ? path : path.slice(idx + 1)\r\n}\r\n\r\n/**\r\n * ヘッドレスファイルツリーコントローラを生成する。\r\n * フレームワーク非依存。React等で使う場合は `momoi-explorer/react` のTreeProviderを推奨。\r\n *\r\n * @param options - ツリーの初期化オプション\r\n * @returns FileTreeController インスタンス\r\n *\r\n * @example\r\n * ```ts\r\n * import { createFileTree } from 'momoi-explorer'\r\n *\r\n * const tree = createFileTree({\r\n * adapter: myAdapter,\r\n * rootPath: '/home/user/project',\r\n * onEvent: (e) => console.log(e),\r\n * })\r\n * await tree.loadRoot()\r\n * ```\r\n */\r\nexport function createFileTree(options: FileTreeOptions): FileTreeController {\r\n const { adapter, rootPath, onEvent } = options\r\n let sortFn = options.sort ?? defaultSort\r\n let filterFn = options.filter ?? defaultFilter\r\n\r\n let state: TreeState = {\r\n rootPath,\r\n rootNodes: [],\r\n expandedPaths: new Set(),\r\n selectedPaths: new Set(),\r\n anchorPath: null,\r\n renamingPath: null,\r\n creatingState: null,\r\n searchQuery: null,\r\n flatList: [],\r\n }\r\n\r\n const listeners = new Set<(state: TreeState) => void>()\r\n let expandingPaths = new Set<string>()\r\n\r\n function emit(event: TreeEvent): void {\r\n onEvent?.(event)\r\n }\r\n\r\n function notify(): void {\r\n const matchingPaths = state.searchQuery\r\n ? findMatchingPaths(state.rootNodes, state.searchQuery)\r\n : null\r\n state = { ...state, flatList: flattenTree(state.rootNodes, state.expandedPaths, matchingPaths) }\r\n for (const listener of listeners) {\r\n listener(state)\r\n }\r\n }\r\n\r\n async function loadChildren(node: TreeNode): Promise<void> {\r\n const entries = await adapter.readDir(node.path)\r\n const filtered = entries.filter(filterFn)\r\n filtered.sort(sortFn)\r\n // 既存の展開済み子ノードのchildrenを引き継ぐ\r\n const oldChildMap = node.children\r\n ? new Map(node.children.map((c) => [c.path, c]))\r\n : new Map<string, TreeNode>()\r\n node.children = filtered.map((e) => {\r\n const existing = oldChildMap.get(e.path)\r\n if (existing && existing.childrenLoaded) {\r\n return { ...toTreeNode(e, node.depth + 1), children: existing.children, childrenLoaded: true }\r\n }\r\n return toTreeNode(e, node.depth + 1)\r\n })\r\n node.childrenLoaded = true\r\n }\r\n\r\n /** 親ディレクトリをリフレッシュし、展開中のフォルダの子も再読み込みする */\r\n async function refreshParent(parentPath: string): Promise<void> {\r\n const parentNode = findNode(state.rootNodes, parentPath)\r\n if (parentNode) {\r\n await loadChildren(parentNode)\r\n state.expandedPaths = new Set(state.expandedPaths)\r\n state.expandedPaths.add(parentPath)\r\n } else if (parentPath === rootPath) {\r\n const entries = await adapter.readDir(rootPath)\r\n const filtered = entries.filter(filterFn)\r\n filtered.sort(sortFn)\r\n // 既存の展開済みノードのchildrenを引き継ぐ\r\n const oldNodeMap = new Map(state.rootNodes.map((n) => [n.path, n]))\r\n state.rootNodes = filtered.map((e) => {\r\n const existing = oldNodeMap.get(e.path)\r\n if (existing && existing.childrenLoaded) {\r\n return { ...toTreeNode(e, 0), children: existing.children, childrenLoaded: true }\r\n }\r\n return toTreeNode(e, 0)\r\n })\r\n }\r\n }\r\n\r\n function sortNodes(nodes: TreeNode[]): void {\r\n nodes.sort(sortFn)\r\n for (const node of nodes) {\r\n if (node.children) {\r\n sortNodes(node.children)\r\n }\r\n }\r\n }\r\n\r\n function filterNodes(nodes: TreeNode[]): TreeNode[] {\r\n return nodes.filter((node) => {\r\n if (!filterFn(node)) return false\r\n if (node.children) {\r\n node.children = filterNodes(node.children)\r\n }\r\n return true\r\n })\r\n }\r\n\r\n // ウォッチ関連\r\n let unwatchFn: (() => void) | null = null\r\n let eventProcessor: ReturnType<typeof createEventProcessor> | null = null\r\n\r\n function handleWatchEvents(events: WatchEvent[]): void {\r\n emit({ type: 'external-change', changes: events })\r\n\r\n // 変更されたパスの親ディレクトリを収集してリフレッシュ\r\n const dirsToRefresh = new Set<string>()\r\n for (const event of events) {\r\n const parent = dirname(event.path)\r\n // 展開中のディレクトリのみリフレッシュ\r\n if (parent === rootPath || state.expandedPaths.has(parent)) {\r\n dirsToRefresh.add(parent)\r\n }\r\n // ディレクトリ自体が変更された場合、展開中ならそれもリフレッシュ\r\n if (event.isDirectory && state.expandedPaths.has(event.path)) {\r\n dirsToRefresh.add(event.path)\r\n }\r\n }\r\n\r\n // 非同期でリフレッシュ(展開状態を保持)\r\n for (const dir of dirsToRefresh) {\r\n refreshParent(dir).then(() => notify()).catch(() => {})\r\n }\r\n }\r\n\r\n function startWatching(): void {\r\n if (!adapter.watch) return\r\n\r\n eventProcessor = createEventProcessor(handleWatchEvents, options.watchOptions)\r\n unwatchFn = adapter.watch(rootPath, (events) => {\r\n eventProcessor!.push(events)\r\n })\r\n }\r\n\r\n function stopWatching(): void {\r\n if (unwatchFn) {\r\n unwatchFn()\r\n unwatchFn = null\r\n }\r\n if (eventProcessor) {\r\n eventProcessor.destroy()\r\n eventProcessor = null\r\n }\r\n }\r\n\r\n const controller: FileTreeController = {\r\n getState(): TreeState {\r\n return state\r\n },\r\n\r\n subscribe(listener: (s: TreeState) => void): () => void {\r\n listeners.add(listener)\r\n return () => listeners.delete(listener)\r\n },\r\n\r\n async loadRoot(): Promise<void> {\r\n const entries = await adapter.readDir(rootPath)\r\n const filtered = entries.filter(filterFn)\r\n filtered.sort(sortFn)\r\n state.rootNodes = filtered.map((e) => toTreeNode(e, 0))\r\n notify()\r\n startWatching()\r\n },\r\n\r\n async expand(path: string): Promise<void> {\r\n // 再入ガード: 同じパスの展開処理が進行中なら無視\r\n if (expandingPaths.has(path)) return\r\n expandingPaths.add(path)\r\n\r\n try {\r\n const node = findNode(state.rootNodes, path)\r\n if (!node || !node.isDirectory) return\r\n\r\n if (!node.childrenLoaded) {\r\n await loadChildren(node)\r\n }\r\n\r\n // await後にstateが変わっている可能性があるので再確認\r\n state.expandedPaths = new Set(state.expandedPaths)\r\n state.expandedPaths.add(path)\r\n notify()\r\n emit({ type: 'expand', path })\r\n } finally {\r\n expandingPaths.delete(path)\r\n }\r\n },\r\n\r\n collapse(path: string): void {\r\n const sep = path.includes('\\\\') ? '\\\\' : '/'\r\n const prefix = path + sep\r\n state.expandedPaths = new Set(state.expandedPaths)\r\n state.expandedPaths.delete(path)\r\n // 子孫の展開状態もクリア\r\n for (const p of state.expandedPaths) {\r\n if (p.startsWith(prefix)) {\r\n state.expandedPaths.delete(p)\r\n }\r\n }\r\n notify()\r\n emit({ type: 'collapse', path })\r\n },\r\n\r\n async toggleExpand(path: string): Promise<void> {\r\n // expand進行中は無視(ダブルクリックで展開→即collapseを防止)\r\n if (expandingPaths.has(path)) return\r\n\r\n if (state.expandedPaths.has(path)) {\r\n controller.collapse(path)\r\n } else {\r\n await controller.expand(path)\r\n }\r\n },\r\n\r\n async expandTo(path: string): Promise<void> {\r\n // パスの各祖先を展開していく\r\n const parts: string[] = []\r\n let current = path\r\n while (current !== rootPath && current !== '') {\r\n const parent = dirname(current)\r\n if (parent === current) break\r\n parts.unshift(parent)\r\n current = parent\r\n }\r\n\r\n for (const ancestorPath of parts) {\r\n if (ancestorPath === rootPath) continue\r\n if (!state.expandedPaths.has(ancestorPath)) {\r\n await controller.expand(ancestorPath)\r\n }\r\n }\r\n },\r\n\r\n select(path: string, mode: 'replace' | 'toggle' | 'range' = 'replace'): void {\r\n const result = computeSelection(\r\n state.selectedPaths,\r\n state.anchorPath,\r\n path,\r\n mode,\r\n state.flatList,\r\n )\r\n state.selectedPaths = result.selectedPaths\r\n state.anchorPath = result.anchorPath\r\n notify()\r\n emit({ type: 'select', paths: Array.from(result.selectedPaths) })\r\n },\r\n\r\n selectAll(): void {\r\n state.selectedPaths = new Set(state.flatList.map((f) => f.node.path))\r\n notify()\r\n emit({ type: 'select', paths: Array.from(state.selectedPaths) })\r\n },\r\n\r\n clearSelection(): void {\r\n state.selectedPaths = new Set()\r\n state.anchorPath = null\r\n notify()\r\n emit({ type: 'select', paths: [] })\r\n },\r\n\r\n startRename(path: string): void {\r\n state.renamingPath = path\r\n notify()\r\n },\r\n\r\n async commitRename(newName: string): Promise<void> {\r\n if (!state.renamingPath || !adapter.rename) return\r\n\r\n const oldPath = state.renamingPath\r\n const parent = dirname(oldPath)\r\n const sep = oldPath.includes('\\\\') ? '\\\\' : '/'\r\n const newPath = parent + sep + newName\r\n\r\n await adapter.rename(oldPath, newPath)\r\n state.renamingPath = null\r\n\r\n // リネームされたノードの親をリフレッシュ\r\n if (parent === rootPath) {\r\n await controller.loadRoot()\r\n } else {\r\n const parentNode = findNode(state.rootNodes, parent)\r\n if (parentNode) {\r\n await loadChildren(parentNode)\r\n }\r\n }\r\n\r\n notify()\r\n emit({ type: 'rename', oldPath, newPath })\r\n },\r\n\r\n cancelRename(): void {\r\n state.renamingPath = null\r\n notify()\r\n },\r\n\r\n async startCreate(parentPath: string, isDirectory: boolean, insertAfterPath?: string): Promise<void> {\r\n // 親フォルダを展開してから作成モードに入る\r\n if (parentPath !== rootPath && !state.expandedPaths.has(parentPath)) {\r\n await controller.expand(parentPath)\r\n }\r\n state.creatingState = { parentPath, isDirectory, insertAfterPath }\r\n notify()\r\n },\r\n\r\n async commitCreate(name: string): Promise<void> {\r\n if (!state.creatingState) return\r\n\r\n const { parentPath, isDirectory } = state.creatingState\r\n state.creatingState = null\r\n\r\n if (isDirectory) {\r\n await controller.createDir(parentPath, name)\r\n } else {\r\n await controller.createFile(parentPath, name)\r\n }\r\n },\r\n\r\n cancelCreate(): void {\r\n state.creatingState = null\r\n notify()\r\n },\r\n\r\n async createFile(parentPath: string, name: string): Promise<void> {\r\n if (!adapter.createFile) return\r\n await adapter.createFile(parentPath, name)\r\n await refreshParent(parentPath)\r\n\r\n notify()\r\n emit({ type: 'create', parentPath, name, isDirectory: false })\r\n },\r\n\r\n async createDir(parentPath: string, name: string): Promise<void> {\r\n if (!adapter.createDir) return\r\n await adapter.createDir(parentPath, name)\r\n await refreshParent(parentPath)\r\n\r\n notify()\r\n emit({ type: 'create', parentPath, name, isDirectory: true })\r\n },\r\n\r\n async deleteSelected(): Promise<void> {\r\n if (!adapter.delete || state.selectedPaths.size === 0) return\r\n\r\n const paths = Array.from(state.selectedPaths)\r\n await adapter.delete(paths)\r\n\r\n state.selectedPaths = new Set()\r\n state.anchorPath = null\r\n\r\n // 影響を受ける親ディレクトリをリフレッシュ\r\n const parentDirs = new Set(paths.map(dirname))\r\n for (const dir of parentDirs) {\r\n await refreshParent(dir)\r\n }\r\n\r\n notify()\r\n emit({ type: 'delete', paths })\r\n },\r\n\r\n canDrop(srcPaths: string[], targetPath: string): boolean {\r\n if (!adapter.move) return false\r\n if (srcPaths.length === 0) return false\r\n\r\n // targetPathをディレクトリに解決\r\n const node = findNode(state.rootNodes, targetPath)\r\n let destDir: string\r\n if (!node) {\r\n // ノードが見つからない場合(ルート or 不明) → ルート\r\n destDir = rootPath\r\n } else if (node.isDirectory) {\r\n destDir = targetPath\r\n } else {\r\n // ファイル → 親ディレクトリ\r\n destDir = dirname(targetPath)\r\n }\r\n\r\n for (const src of srcPaths) {\r\n // 自分自身へのドロップ禁止\r\n if (src === destDir) return false\r\n // 子孫へのドロップ禁止(循環防止)\r\n const sep = src.includes('\\\\') ? '\\\\' : '/'\r\n if (destDir.startsWith(src + sep)) return false\r\n // 同一親フォルダへのドロップはno-op\r\n if (dirname(src) === destDir) return false\r\n }\r\n\r\n return true\r\n },\r\n\r\n async moveItems(srcPaths: string[], destDir: string): Promise<void> {\r\n if (!adapter.move || srcPaths.length === 0) return\r\n\r\n // 各アイテムを移動\r\n for (const src of srcPaths) {\r\n await adapter.move(src, destDir)\r\n }\r\n\r\n // 影響を受けるディレクトリをリフレッシュ\r\n const parentDirs = new Set(srcPaths.map(dirname))\r\n parentDirs.add(destDir)\r\n for (const dir of parentDirs) {\r\n await refreshParent(dir)\r\n }\r\n\r\n // 移動先フォルダを展開(移動したアイテムが見えるように)\r\n if (destDir !== rootPath && !state.expandedPaths.has(destDir)) {\r\n state.expandedPaths = new Set(state.expandedPaths)\r\n state.expandedPaths.add(destDir)\r\n }\r\n\r\n // 選択を移動後のパスに更新\r\n const sep = destDir.includes('\\\\') ? '\\\\' : '/'\r\n const newPaths = srcPaths.map((src) => destDir + sep + basename(src))\r\n state.selectedPaths = new Set(newPaths)\r\n state.anchorPath = newPaths[0] ?? null\r\n\r\n notify()\r\n emit({ type: 'move', srcPaths, destDir })\r\n },\r\n\r\n async refresh(path?: string): Promise<void> {\r\n if (!path || path === rootPath) {\r\n await refreshParent(rootPath)\r\n } else {\r\n const node = findNode(state.rootNodes, path)\r\n if (node && node.isDirectory) {\r\n await loadChildren(node)\r\n }\r\n }\r\n\r\n notify()\r\n emit({ type: 'refresh', path })\r\n },\r\n\r\n setSearchQuery(query: string | null): void {\r\n state.searchQuery = query && query.trim() ? query.trim() : null\r\n notify()\r\n },\r\n\r\n async collectAllFiles(): Promise<FileEntry[]> {\r\n const result: FileEntry[] = []\r\n\r\n async function walk(dirPath: string): Promise<void> {\r\n const entries = await adapter.readDir(dirPath)\r\n for (const entry of entries) {\r\n if (!filterFn(entry)) continue\r\n result.push(entry)\r\n if (entry.isDirectory) {\r\n await walk(entry.path)\r\n }\r\n }\r\n }\r\n\r\n await walk(rootPath)\r\n return result\r\n },\r\n\r\n setFilter(fn: ((entry: FileEntry) => boolean) | null): void {\r\n filterFn = fn ?? defaultFilter\r\n // 既に読み込み済みのノードを再フィルタ\r\n state.rootNodes = filterNodes(state.rootNodes)\r\n notify()\r\n },\r\n\r\n setSort(fn: ((a: FileEntry, b: FileEntry) => number) | null): void {\r\n sortFn = fn ?? defaultSort\r\n sortNodes(state.rootNodes)\r\n notify()\r\n },\r\n\r\n destroy(): void {\r\n stopWatching()\r\n listeners.clear()\r\n },\r\n }\r\n\r\n return controller\r\n}\r\n","// エクスプローラー用のコマンドIDとデフォルトキーバインド定義\n//\n// momoi-keybind の KeybindingEntry 互換の形式でエクスポートする。\n// momoi-keybind が無くても型として参照できるように、独自の型を定義。\n\n/** momoi-keybind の KeybindingEntry と互換の型 */\nexport interface ExplorerKeybindingEntry {\n key: string\n command: string\n when?: string\n args?: unknown\n}\n\n/** エクスプローラーのコマンドID */\nexport const ExplorerCommands = {\n DELETE: 'explorer.delete',\n RENAME: 'explorer.rename',\n NEW_FILE: 'explorer.newFile',\n NEW_FOLDER: 'explorer.newFolder',\n REFRESH: 'explorer.refresh',\n COLLAPSE_ALL: 'explorer.collapseAll',\n SELECT_ALL: 'explorer.selectAll',\n COPY_PATH: 'explorer.copyPath',\n} as const\n\nexport type ExplorerCommandId = (typeof ExplorerCommands)[keyof typeof ExplorerCommands]\n\n/** デフォルトのキーバインド定義。momoi-keybindのInputServiceに渡せる形式 */\nexport const defaultExplorerKeybindings: ExplorerKeybindingEntry[] = [\n { key: 'Delete', command: ExplorerCommands.DELETE, when: 'explorerFocus' },\n { key: 'F2', command: ExplorerCommands.RENAME, when: 'explorerFocus' },\n { key: 'Ctrl+N', command: ExplorerCommands.NEW_FILE, when: 'explorerFocus' },\n { key: 'Ctrl+Shift+N', command: ExplorerCommands.NEW_FOLDER, when: 'explorerFocus' },\n { key: 'Ctrl+R', command: ExplorerCommands.REFRESH, when: 'explorerFocus' },\n { key: 'Ctrl+Shift+E', command: ExplorerCommands.COLLAPSE_ALL, when: 'explorerFocus' },\n { key: 'Ctrl+A', command: ExplorerCommands.SELECT_ALL, when: 'explorerFocus' },\n { key: 'Ctrl+Shift+C', command: ExplorerCommands.COPY_PATH, when: 'explorerFocus' },\n]\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -57,6 +57,13 @@ interface FileSystemAdapter {
57
57
  * @param name - 新規フォルダ名
58
58
  */
59
59
  createDir?(parentPath: string, name: string): Promise<void>;
60
+ /**
61
+ * ファイルまたはフォルダを別のディレクトリへ移動する。
62
+ * 実装するとドラッグ&ドロップによる移動操作が可能になる。
63
+ * @param srcPath - 移動元の絶対パス
64
+ * @param destDir - 移動先ディレクトリの絶対パス
65
+ */
66
+ move?(srcPath: string, destDir: string): Promise<void>;
60
67
  /**
61
68
  * ファイル変更監視を開始する。
62
69
  * 生イベントを投げるだけでOK。デバウンス・合体・スロットリングはコアが行う。
@@ -201,6 +208,10 @@ type TreeEvent = {
201
208
  parentPath: string;
202
209
  name: string;
203
210
  isDirectory: boolean;
211
+ } | {
212
+ type: 'move';
213
+ srcPaths: string[];
214
+ destDir: string;
204
215
  } | {
205
216
  type: 'refresh';
206
217
  path?: string;
@@ -355,6 +366,20 @@ interface FileTreeController {
355
366
  createDir(parentPath: string, name: string): Promise<void>;
356
367
  /** 選択中のアイテムをすべて削除する。adapter.deleteが呼ばれる */
357
368
  deleteSelected(): Promise<void>;
369
+ /**
370
+ * ファイル/フォルダを別のディレクトリへ移動する。adapter.moveが必要。
371
+ * @param srcPaths - 移動元の絶対パス配列
372
+ * @param destDir - 移動先ディレクトリの絶対パス
373
+ */
374
+ moveItems(srcPaths: string[], destDir: string): Promise<void>;
375
+ /**
376
+ * 指定のドロップ先が有効かどうかを判定する。
377
+ * 自分自身・子孫・同一親フォルダへのドロップを禁止する。
378
+ * @param srcPaths - ドラッグ中のパス配列
379
+ * @param targetPath - ドロップ先のパス(ファイルの場合は親ディレクトリに解決される)
380
+ * @returns ドロップ可能ならtrue
381
+ */
382
+ canDrop(srcPaths: string[], targetPath: string): boolean;
358
383
  /**
359
384
  * ツリーを再読み込みする。展開状態は保持される。
360
385
  * @param path - リフレッシュ対象のディレクトリ。省略時はルート全体
package/dist/index.d.ts CHANGED
@@ -57,6 +57,13 @@ interface FileSystemAdapter {
57
57
  * @param name - 新規フォルダ名
58
58
  */
59
59
  createDir?(parentPath: string, name: string): Promise<void>;
60
+ /**
61
+ * ファイルまたはフォルダを別のディレクトリへ移動する。
62
+ * 実装するとドラッグ&ドロップによる移動操作が可能になる。
63
+ * @param srcPath - 移動元の絶対パス
64
+ * @param destDir - 移動先ディレクトリの絶対パス
65
+ */
66
+ move?(srcPath: string, destDir: string): Promise<void>;
60
67
  /**
61
68
  * ファイル変更監視を開始する。
62
69
  * 生イベントを投げるだけでOK。デバウンス・合体・スロットリングはコアが行う。
@@ -201,6 +208,10 @@ type TreeEvent = {
201
208
  parentPath: string;
202
209
  name: string;
203
210
  isDirectory: boolean;
211
+ } | {
212
+ type: 'move';
213
+ srcPaths: string[];
214
+ destDir: string;
204
215
  } | {
205
216
  type: 'refresh';
206
217
  path?: string;
@@ -355,6 +366,20 @@ interface FileTreeController {
355
366
  createDir(parentPath: string, name: string): Promise<void>;
356
367
  /** 選択中のアイテムをすべて削除する。adapter.deleteが呼ばれる */
357
368
  deleteSelected(): Promise<void>;
369
+ /**
370
+ * ファイル/フォルダを別のディレクトリへ移動する。adapter.moveが必要。
371
+ * @param srcPaths - 移動元の絶対パス配列
372
+ * @param destDir - 移動先ディレクトリの絶対パス
373
+ */
374
+ moveItems(srcPaths: string[], destDir: string): Promise<void>;
375
+ /**
376
+ * 指定のドロップ先が有効かどうかを判定する。
377
+ * 自分自身・子孫・同一親フォルダへのドロップを禁止する。
378
+ * @param srcPaths - ドラッグ中のパス配列
379
+ * @param targetPath - ドロップ先のパス(ファイルの場合は親ディレクトリに解決される)
380
+ * @returns ドロップ可能ならtrue
381
+ */
382
+ canDrop(srcPaths: string[], targetPath: string): boolean;
358
383
  /**
359
384
  * ツリーを再読み込みする。展開状態は保持される。
360
385
  * @param path - リフレッシュ対象のディレクトリ。省略時はルート全体
package/dist/index.js CHANGED
@@ -282,6 +282,11 @@ function dirname(path) {
282
282
  const idx = path.lastIndexOf(sep);
283
283
  return idx === -1 ? "" : path.slice(0, idx);
284
284
  }
285
+ function basename(path) {
286
+ const sep = path.includes("\\") ? "\\" : "/";
287
+ const idx = path.lastIndexOf(sep);
288
+ return idx === -1 ? path : path.slice(idx + 1);
289
+ }
285
290
  function createFileTree(options) {
286
291
  const { adapter, rootPath, onEvent } = options;
287
292
  let sortFn = options.sort ?? defaultSort;
@@ -565,6 +570,47 @@ function createFileTree(options) {
565
570
  notify();
566
571
  emit({ type: "delete", paths });
567
572
  },
573
+ canDrop(srcPaths, targetPath) {
574
+ if (!adapter.move) return false;
575
+ if (srcPaths.length === 0) return false;
576
+ const node = findNode(state.rootNodes, targetPath);
577
+ let destDir;
578
+ if (!node) {
579
+ destDir = rootPath;
580
+ } else if (node.isDirectory) {
581
+ destDir = targetPath;
582
+ } else {
583
+ destDir = dirname(targetPath);
584
+ }
585
+ for (const src of srcPaths) {
586
+ if (src === destDir) return false;
587
+ const sep = src.includes("\\") ? "\\" : "/";
588
+ if (destDir.startsWith(src + sep)) return false;
589
+ if (dirname(src) === destDir) return false;
590
+ }
591
+ return true;
592
+ },
593
+ async moveItems(srcPaths, destDir) {
594
+ if (!adapter.move || srcPaths.length === 0) return;
595
+ for (const src of srcPaths) {
596
+ await adapter.move(src, destDir);
597
+ }
598
+ const parentDirs = new Set(srcPaths.map(dirname));
599
+ parentDirs.add(destDir);
600
+ for (const dir of parentDirs) {
601
+ await refreshParent(dir);
602
+ }
603
+ if (destDir !== rootPath && !state.expandedPaths.has(destDir)) {
604
+ state.expandedPaths = new Set(state.expandedPaths);
605
+ state.expandedPaths.add(destDir);
606
+ }
607
+ const sep = destDir.includes("\\") ? "\\" : "/";
608
+ const newPaths = srcPaths.map((src) => destDir + sep + basename(src));
609
+ state.selectedPaths = new Set(newPaths);
610
+ state.anchorPath = newPaths[0] ?? null;
611
+ notify();
612
+ emit({ type: "move", srcPaths, destDir });
613
+ },
568
614
  async refresh(path) {
569
615
  if (!path || path === rootPath) {
570
616
  await refreshParent(rootPath);