momoi-explorer 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +355 -0
- package/dist/index.cjs +690 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +501 -0
- package/dist/index.d.ts +501 -0
- package/dist/index.js +652 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +850 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +499 -0
- package/dist/react.d.ts +499 -0
- package/dist/react.js +816 -0
- package/dist/react.js.map +1 -0
- package/dist/ui/style.css +332 -0
- package/dist/ui.cjs +1379 -0
- package/dist/ui.cjs.map +1 -0
- package/dist/ui.d.cts +554 -0
- package/dist/ui.d.ts +554 -0
- package/dist/ui.js +1347 -0
- package/dist/ui.js.map +1 -0
- package/package.json +95 -0
|
@@ -0,0 +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":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ファイルシステムへのアクセスを抽象化するアダプタインターフェース。
|
|
3
|
+
* ユーザーはこのインターフェースを実装してcreateFileTreeに渡す。
|
|
4
|
+
* `readDir` のみ必須。その他のメソッドはオプションで、対応する機能が有効になる。
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { FileSystemAdapter } from 'momoi-explorer'
|
|
9
|
+
* import fs from 'node:fs/promises'
|
|
10
|
+
* import path from 'node:path'
|
|
11
|
+
*
|
|
12
|
+
* const adapter: FileSystemAdapter = {
|
|
13
|
+
* async readDir(dirPath) {
|
|
14
|
+
* const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
|
15
|
+
* return entries.map(e => ({
|
|
16
|
+
* name: e.name,
|
|
17
|
+
* path: path.join(dirPath, e.name),
|
|
18
|
+
* isDirectory: e.isDirectory(),
|
|
19
|
+
* }))
|
|
20
|
+
* },
|
|
21
|
+
* async rename(oldPath, newPath) { await fs.rename(oldPath, newPath) },
|
|
22
|
+
* async delete(paths) { for (const p of paths) await fs.rm(p, { recursive: true }) },
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
interface FileSystemAdapter {
|
|
27
|
+
/**
|
|
28
|
+
* ディレクトリの中身を読み取る。唯一の必須メソッド。
|
|
29
|
+
* @param path - 読み取るディレクトリの絶対パス
|
|
30
|
+
* @returns ディレクトリ内のFileEntryの配列
|
|
31
|
+
*/
|
|
32
|
+
readDir(path: string): Promise<FileEntry[]>;
|
|
33
|
+
/**
|
|
34
|
+
* ファイルまたはフォルダをリネームする。
|
|
35
|
+
* 実装するとUI上でリネーム操作が可能になる。
|
|
36
|
+
* @param oldPath - リネーム前の絶対パス
|
|
37
|
+
* @param newPath - リネーム後の絶対パス
|
|
38
|
+
*/
|
|
39
|
+
rename?(oldPath: string, newPath: string): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* ファイルまたはフォルダを削除する。
|
|
42
|
+
* 実装すると選択アイテムの削除操作が可能になる。
|
|
43
|
+
* @param paths - 削除対象の絶対パスの配列
|
|
44
|
+
*/
|
|
45
|
+
delete?(paths: string[]): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* ファイルを新規作成する。
|
|
48
|
+
* 実装するとUI上でファイル作成操作が可能になる。
|
|
49
|
+
* @param parentPath - 作成先の親ディレクトリの絶対パス
|
|
50
|
+
* @param name - 新規ファイル名
|
|
51
|
+
*/
|
|
52
|
+
createFile?(parentPath: string, name: string): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* フォルダを新規作成する。
|
|
55
|
+
* 実装するとUI上でフォルダ作成操作が可能になる。
|
|
56
|
+
* @param parentPath - 作成先の親ディレクトリの絶対パス
|
|
57
|
+
* @param name - 新規フォルダ名
|
|
58
|
+
*/
|
|
59
|
+
createDir?(parentPath: string, name: string): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* ファイル変更監視を開始する。
|
|
62
|
+
* 生イベントを投げるだけでOK。デバウンス・合体・スロットリングはコアが行う。
|
|
63
|
+
* @param path - 監視するディレクトリの絶対パス
|
|
64
|
+
* @param callback - 生イベントを受け取るコールバック
|
|
65
|
+
* @returns 監視を停止するunwatch関数
|
|
66
|
+
*/
|
|
67
|
+
watch?(path: string, callback: (events: RawWatchEvent[]) => void): () => void;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* ファイルまたはフォルダを表すエントリ。
|
|
71
|
+
* `readDir` の戻り値として使用される基本データ型。
|
|
72
|
+
*/
|
|
73
|
+
interface FileEntry {
|
|
74
|
+
/** ファイル名(拡張子含む) */
|
|
75
|
+
name: string;
|
|
76
|
+
/** 絶対パス */
|
|
77
|
+
path: string;
|
|
78
|
+
/** ディレクトリならtrue */
|
|
79
|
+
isDirectory: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* ユーザー拡張用のメタデータ。
|
|
82
|
+
* gitStatus, iconHint 等、任意のデータを格納できる。
|
|
83
|
+
*/
|
|
84
|
+
meta?: Record<string, unknown>;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* コア内部で使用されるツリーノード。FileEntryにツリー構造の情報を追加したもの。
|
|
88
|
+
* flatList内のFlatNode.nodeとしても参照される。
|
|
89
|
+
*/
|
|
90
|
+
interface TreeNode extends FileEntry {
|
|
91
|
+
/** ルートからの深さ(ルート直下 = 0) */
|
|
92
|
+
depth: number;
|
|
93
|
+
/** 子ノード。ディレクトリで未読み込みの場合はundefined */
|
|
94
|
+
children?: TreeNode[];
|
|
95
|
+
/** 子ノードが読み込み済みかどうか */
|
|
96
|
+
childrenLoaded: boolean;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 仮想スクロール用のフラットリスト要素。
|
|
100
|
+
* ツリーを展開状態に基づいて一次元配列に変換したもの。
|
|
101
|
+
*/
|
|
102
|
+
interface FlatNode {
|
|
103
|
+
/** 対応するツリーノード */
|
|
104
|
+
node: TreeNode;
|
|
105
|
+
/** 表示上の深さ(インデント計算に使用) */
|
|
106
|
+
depth: number;
|
|
107
|
+
}
|
|
108
|
+
/** インライン新規作成の状態 */
|
|
109
|
+
interface CreatingState {
|
|
110
|
+
/** 新規アイテムを作成する親ディレクトリのパス */
|
|
111
|
+
parentPath: string;
|
|
112
|
+
/** ファイルかフォルダか */
|
|
113
|
+
isDirectory: boolean;
|
|
114
|
+
/** この位置の直後に入力行を表示する。未指定なら親の子の先頭(フォルダ選択時)or末尾(選択なし時) */
|
|
115
|
+
insertAfterPath?: string;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* ツリー全体の現在状態。subscribeで購読可能。
|
|
119
|
+
* 不変(immutable)として扱い、変更時は新しいオブジェクトが生成される。
|
|
120
|
+
*/
|
|
121
|
+
interface TreeState {
|
|
122
|
+
/** ルートディレクトリの絶対パス */
|
|
123
|
+
rootPath: string;
|
|
124
|
+
/** ルート直下のツリーノード配列 */
|
|
125
|
+
rootNodes: TreeNode[];
|
|
126
|
+
/** 現在展開中のディレクトリパスのセット */
|
|
127
|
+
expandedPaths: Set<string>;
|
|
128
|
+
/** 現在選択中のパスのセット */
|
|
129
|
+
selectedPaths: Set<string>;
|
|
130
|
+
/** 範囲選択の起点パス(Shift+Click用) */
|
|
131
|
+
anchorPath: string | null;
|
|
132
|
+
/** 現在リネーム中のパス。nullならリネームモードではない */
|
|
133
|
+
renamingPath: string | null;
|
|
134
|
+
/** インライン新規作成の状態。nullなら作成モードではない */
|
|
135
|
+
creatingState: CreatingState | null;
|
|
136
|
+
/** 現在の検索クエリ。nullなら検索なし */
|
|
137
|
+
searchQuery: string | null;
|
|
138
|
+
/** 仮想スクロール用のフラットリスト(展開・検索状態を反映) */
|
|
139
|
+
flatList: FlatNode[];
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* アダプタのwatchから来る生のファイル監視イベント。
|
|
143
|
+
* renameイベントはnewPathを持つ。コアが合体処理を行う。
|
|
144
|
+
*/
|
|
145
|
+
interface RawWatchEvent {
|
|
146
|
+
/** イベント種別 */
|
|
147
|
+
type: 'create' | 'modify' | 'delete' | 'rename';
|
|
148
|
+
/** 対象パス(renameの場合はリネーム前のパス) */
|
|
149
|
+
path: string;
|
|
150
|
+
/** renameの場合のリネーム後パス */
|
|
151
|
+
newPath?: string;
|
|
152
|
+
/** 対象がディレクトリかどうか */
|
|
153
|
+
isDirectory: boolean;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* コア内部で合体処理後のイベント。
|
|
157
|
+
* renameはdelete+createに分解され、同一パスのイベントは合体される。
|
|
158
|
+
*/
|
|
159
|
+
interface WatchEvent {
|
|
160
|
+
/** イベント種別(renameは含まない) */
|
|
161
|
+
type: 'create' | 'modify' | 'delete';
|
|
162
|
+
/** 対象パス */
|
|
163
|
+
path: string;
|
|
164
|
+
/** 対象がディレクトリかどうか */
|
|
165
|
+
isDirectory: boolean;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* ツリー操作や外部変更をUI層に通知するためのイベント型。
|
|
169
|
+
* `FileTreeOptions.onEvent` で購読できる。
|
|
170
|
+
*
|
|
171
|
+
* - `expand` / `collapse` - ディレクトリの展開/折りたたみ
|
|
172
|
+
* - `select` - 選択変更
|
|
173
|
+
* - `open` - ファイルを開く
|
|
174
|
+
* - `rename` - リネーム完了
|
|
175
|
+
* - `delete` - 削除完了
|
|
176
|
+
* - `create` - ファイル/フォルダ作成完了
|
|
177
|
+
* - `refresh` - ツリーのリフレッシュ
|
|
178
|
+
* - `external-change` - ファイル監視による外部変更検出
|
|
179
|
+
*/
|
|
180
|
+
type TreeEvent = {
|
|
181
|
+
type: 'expand';
|
|
182
|
+
path: string;
|
|
183
|
+
} | {
|
|
184
|
+
type: 'collapse';
|
|
185
|
+
path: string;
|
|
186
|
+
} | {
|
|
187
|
+
type: 'select';
|
|
188
|
+
paths: string[];
|
|
189
|
+
} | {
|
|
190
|
+
type: 'open';
|
|
191
|
+
path: string;
|
|
192
|
+
} | {
|
|
193
|
+
type: 'rename';
|
|
194
|
+
oldPath: string;
|
|
195
|
+
newPath: string;
|
|
196
|
+
} | {
|
|
197
|
+
type: 'delete';
|
|
198
|
+
paths: string[];
|
|
199
|
+
} | {
|
|
200
|
+
type: 'create';
|
|
201
|
+
parentPath: string;
|
|
202
|
+
name: string;
|
|
203
|
+
isDirectory: boolean;
|
|
204
|
+
} | {
|
|
205
|
+
type: 'refresh';
|
|
206
|
+
path?: string;
|
|
207
|
+
} | {
|
|
208
|
+
type: 'external-change';
|
|
209
|
+
changes: WatchEvent[];
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* コンテキストメニューの項目定義。
|
|
213
|
+
* UI層でメニュー表示に使用する。
|
|
214
|
+
*/
|
|
215
|
+
interface MenuItemDef {
|
|
216
|
+
/** メニュー項目の一意識別子 */
|
|
217
|
+
id: string;
|
|
218
|
+
/** 表示ラベル */
|
|
219
|
+
label: string;
|
|
220
|
+
/** キーボードショートカットの表示文字列(例: "Ctrl+C") */
|
|
221
|
+
shortcut?: string;
|
|
222
|
+
/** trueなら項目を無効化(グレーアウト) */
|
|
223
|
+
disabled?: boolean;
|
|
224
|
+
/** trueならこの項目の前にセパレータを表示 */
|
|
225
|
+
separator?: boolean;
|
|
226
|
+
/**
|
|
227
|
+
* メニュー選択時に実行されるアクション。
|
|
228
|
+
* @param targets - 右クリック対象のノード配列
|
|
229
|
+
*/
|
|
230
|
+
action: (targets: TreeNode[]) => void;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* ファイル監視の動作設定。
|
|
234
|
+
* createFileTreeのoptionsで指定する。
|
|
235
|
+
*/
|
|
236
|
+
interface WatchOptions {
|
|
237
|
+
/** デバウンス時間(ms)。デフォルト: 75 (VSCode準拠) */
|
|
238
|
+
debounceMs?: number;
|
|
239
|
+
/** イベント合体を行うか。デフォルト: true */
|
|
240
|
+
coalesce?: boolean;
|
|
241
|
+
/** スロットリング設定。大量イベント発生時にチャンク分割する */
|
|
242
|
+
throttle?: {
|
|
243
|
+
/** 一度に処理するイベント上限。デフォルト: 500 */
|
|
244
|
+
maxChunkSize: number;
|
|
245
|
+
/** チャンク間の休止時間(ms)。デフォルト: 200 */
|
|
246
|
+
delayMs: number;
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* {@link createFileTree} の初期化オプション。
|
|
251
|
+
*/
|
|
252
|
+
interface FileTreeOptions {
|
|
253
|
+
/** ファイルシステムアダプタ(必須) */
|
|
254
|
+
adapter: FileSystemAdapter;
|
|
255
|
+
/** ツリーのルートディレクトリの絶対パス */
|
|
256
|
+
rootPath: string;
|
|
257
|
+
/** カスタムソート関数。未指定時はフォルダ優先・名前昇順 */
|
|
258
|
+
sort?: (a: FileEntry, b: FileEntry) => number;
|
|
259
|
+
/** カスタムフィルタ関数。falseを返すエントリは非表示になる */
|
|
260
|
+
filter?: (entry: FileEntry) => boolean;
|
|
261
|
+
/** ファイル監視のオプション */
|
|
262
|
+
watchOptions?: WatchOptions;
|
|
263
|
+
/** ツリー操作イベントのコールバック */
|
|
264
|
+
onEvent?: (event: TreeEvent) => void;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* ヘッドレスファイルツリーのメインコントローラ。
|
|
268
|
+
* {@link createFileTree} から返される。状態購読・ツリー操作・編集・検索などの全APIを提供する。
|
|
269
|
+
*/
|
|
270
|
+
interface FileTreeController {
|
|
271
|
+
/**
|
|
272
|
+
* 現在のツリー状態を取得する。
|
|
273
|
+
* @returns 現在のTreeState
|
|
274
|
+
*/
|
|
275
|
+
getState(): TreeState;
|
|
276
|
+
/**
|
|
277
|
+
* 状態変更を購読する。状態が変わるたびにlistenerが呼ばれる。
|
|
278
|
+
* @param listener - 状態変更時に呼ばれるコールバック
|
|
279
|
+
* @returns 購読解除関数
|
|
280
|
+
*/
|
|
281
|
+
subscribe(listener: (state: TreeState) => void): () => void;
|
|
282
|
+
/**
|
|
283
|
+
* ルートディレクトリを読み込み、ツリーを初期化する。
|
|
284
|
+
* ファイル監視もここで開始される。最初に必ず呼ぶこと。
|
|
285
|
+
*/
|
|
286
|
+
loadRoot(): Promise<void>;
|
|
287
|
+
/**
|
|
288
|
+
* 指定パスのディレクトリを展開する。未読み込みなら子を読み込む。
|
|
289
|
+
* @param path - 展開するディレクトリの絶対パス
|
|
290
|
+
*/
|
|
291
|
+
expand(path: string): Promise<void>;
|
|
292
|
+
/**
|
|
293
|
+
* 指定パスのディレクトリを折りたたむ。
|
|
294
|
+
* @param path - 折りたたむディレクトリの絶対パス
|
|
295
|
+
*/
|
|
296
|
+
collapse(path: string): void;
|
|
297
|
+
/**
|
|
298
|
+
* 展開/折りたたみをトグルする。
|
|
299
|
+
* @param path - 対象ディレクトリの絶対パス
|
|
300
|
+
*/
|
|
301
|
+
toggleExpand(path: string): Promise<void>;
|
|
302
|
+
/**
|
|
303
|
+
* 指定パスまでの祖先ディレクトリをすべて展開する。
|
|
304
|
+
* ファイルを可視状態にしたい場合に使用する。
|
|
305
|
+
* @param path - 表示したいノードの絶対パス
|
|
306
|
+
*/
|
|
307
|
+
expandTo(path: string): Promise<void>;
|
|
308
|
+
/**
|
|
309
|
+
* ノードを選択する。
|
|
310
|
+
* @param path - 選択するノードの絶対パス
|
|
311
|
+
* @param mode - 選択モード。'replace'=単一選択、'toggle'=Ctrl+Click、'range'=Shift+Click
|
|
312
|
+
*/
|
|
313
|
+
select(path: string, mode?: 'replace' | 'toggle' | 'range'): void;
|
|
314
|
+
/** flatList内の全ノードを選択する */
|
|
315
|
+
selectAll(): void;
|
|
316
|
+
/** 選択をすべて解除する */
|
|
317
|
+
clearSelection(): void;
|
|
318
|
+
/**
|
|
319
|
+
* リネームモードを開始する。UIはこの状態を検知してインライン編集UIを表示する。
|
|
320
|
+
* @param path - リネーム対象の絶対パス
|
|
321
|
+
*/
|
|
322
|
+
startRename(path: string): void;
|
|
323
|
+
/**
|
|
324
|
+
* リネームを確定する。adapter.renameが呼ばれ、親ディレクトリがリフレッシュされる。
|
|
325
|
+
* @param newName - 新しいファイル名(パスではなく名前のみ)
|
|
326
|
+
*/
|
|
327
|
+
commitRename(newName: string): Promise<void>;
|
|
328
|
+
/** リネームをキャンセルする */
|
|
329
|
+
cancelRename(): void;
|
|
330
|
+
/**
|
|
331
|
+
* インライン新規作成モードを開始する。親ディレクトリを展開し、入力行を表示する。
|
|
332
|
+
* @param parentPath - 作成先の親ディレクトリの絶対パス
|
|
333
|
+
* @param isDirectory - フォルダを作成する場合はtrue
|
|
334
|
+
* @param insertAfterPath - この位置の直後に入力行を表示する
|
|
335
|
+
*/
|
|
336
|
+
startCreate(parentPath: string, isDirectory: boolean, insertAfterPath?: string): Promise<void>;
|
|
337
|
+
/**
|
|
338
|
+
* インライン新規作成を確定する。adapter.createFile/createDirが呼ばれる。
|
|
339
|
+
* @param name - 新規ファイル/フォルダ名
|
|
340
|
+
*/
|
|
341
|
+
commitCreate(name: string): Promise<void>;
|
|
342
|
+
/** インライン新規作成をキャンセルする */
|
|
343
|
+
cancelCreate(): void;
|
|
344
|
+
/**
|
|
345
|
+
* ファイルを新規作成する。adapter.createFileが呼ばれる。
|
|
346
|
+
* @param parentPath - 作成先の親ディレクトリの絶対パス
|
|
347
|
+
* @param name - 新規ファイル名
|
|
348
|
+
*/
|
|
349
|
+
createFile(parentPath: string, name: string): Promise<void>;
|
|
350
|
+
/**
|
|
351
|
+
* フォルダを新規作成する。adapter.createDirが呼ばれる。
|
|
352
|
+
* @param parentPath - 作成先の親ディレクトリの絶対パス
|
|
353
|
+
* @param name - 新規フォルダ名
|
|
354
|
+
*/
|
|
355
|
+
createDir(parentPath: string, name: string): Promise<void>;
|
|
356
|
+
/** 選択中のアイテムをすべて削除する。adapter.deleteが呼ばれる */
|
|
357
|
+
deleteSelected(): Promise<void>;
|
|
358
|
+
/**
|
|
359
|
+
* ツリーを再読み込みする。展開状態は保持される。
|
|
360
|
+
* @param path - リフレッシュ対象のディレクトリ。省略時はルート全体
|
|
361
|
+
*/
|
|
362
|
+
refresh(path?: string): Promise<void>;
|
|
363
|
+
/**
|
|
364
|
+
* ファジー検索クエリを設定する。flatListがフィルタされマッチしたノードのみ表示される。
|
|
365
|
+
* @param query - 検索文字列。nullで検索解除
|
|
366
|
+
*/
|
|
367
|
+
setSearchQuery(query: string | null): void;
|
|
368
|
+
/**
|
|
369
|
+
* ツリー配下の全ファイルを再帰的に収集する。
|
|
370
|
+
* fuzzyFind等で全ファイル検索を行う際の入力データとして使用する。
|
|
371
|
+
* @returns 全FileEntryの配列
|
|
372
|
+
*/
|
|
373
|
+
collectAllFiles(): Promise<FileEntry[]>;
|
|
374
|
+
/**
|
|
375
|
+
* フィルタ関数を動的に変更する。既存ノードにも即座に適用される。
|
|
376
|
+
* @param fn - フィルタ関数。nullでフィルタ解除
|
|
377
|
+
*/
|
|
378
|
+
setFilter(fn: ((entry: FileEntry) => boolean) | null): void;
|
|
379
|
+
/**
|
|
380
|
+
* ソート関数を動的に変更する。既存ノードにも即座に適用される。
|
|
381
|
+
* @param fn - ソート関数。nullでデフォルトソートに戻す
|
|
382
|
+
*/
|
|
383
|
+
setSort(fn: ((a: FileEntry, b: FileEntry) => number) | null): void;
|
|
384
|
+
/** コントローラを破棄する。ファイル監視を停止し、購読をすべて解除する */
|
|
385
|
+
destroy(): void;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* ヘッドレスファイルツリーコントローラを生成する。
|
|
390
|
+
* フレームワーク非依存。React等で使う場合は `momoi-explorer/react` のTreeProviderを推奨。
|
|
391
|
+
*
|
|
392
|
+
* @param options - ツリーの初期化オプション
|
|
393
|
+
* @returns FileTreeController インスタンス
|
|
394
|
+
*
|
|
395
|
+
* @example
|
|
396
|
+
* ```ts
|
|
397
|
+
* import { createFileTree } from 'momoi-explorer'
|
|
398
|
+
*
|
|
399
|
+
* const tree = createFileTree({
|
|
400
|
+
* adapter: myAdapter,
|
|
401
|
+
* rootPath: '/home/user/project',
|
|
402
|
+
* onEvent: (e) => console.log(e),
|
|
403
|
+
* })
|
|
404
|
+
* await tree.loadRoot()
|
|
405
|
+
* ```
|
|
406
|
+
*/
|
|
407
|
+
declare function createFileTree(options: FileTreeOptions): FileTreeController;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* 生イベントを合体する(VSCode EventCoalescer 準拠)。
|
|
411
|
+
* - rename → delete(old) + create(new) に分解
|
|
412
|
+
* - delete + create(同一パス) → modify に合体
|
|
413
|
+
* - create + modify(同一パス) → create を維持
|
|
414
|
+
* - 親フォルダ delete → 子の delete を除去
|
|
415
|
+
*
|
|
416
|
+
* @param raw - アダプタから受け取った生イベントの配列
|
|
417
|
+
* @returns 合体処理後のWatchEvent配列
|
|
418
|
+
*/
|
|
419
|
+
declare function coalesceEvents(raw: RawWatchEvent[]): WatchEvent[];
|
|
420
|
+
/**
|
|
421
|
+
* イベントプロセッサを生成する。
|
|
422
|
+
* デバウンス → 合体 → スロットリング → コールバック のパイプラインで処理する。
|
|
423
|
+
*
|
|
424
|
+
* @param callback - 処理済みイベントを受け取るコールバック
|
|
425
|
+
* @param options - デバウンス・合体・スロットリングの設定
|
|
426
|
+
* @returns push/flush/destroyメソッドを持つプロセッサオブジェクト
|
|
427
|
+
*/
|
|
428
|
+
declare function createEventProcessor(callback: (events: WatchEvent[]) => void, options?: WatchOptions): {
|
|
429
|
+
push(events: RawWatchEvent[]): void;
|
|
430
|
+
flush(): void;
|
|
431
|
+
destroy(): void;
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* 展開されたツリーをフラットリストに変換する。
|
|
436
|
+
* react-virtuoso等の仮想スクロールに渡すためのもの。
|
|
437
|
+
* matchingPaths が指定された場合、そこに含まれるノードのみ表示する。
|
|
438
|
+
*/
|
|
439
|
+
declare function flattenTree(nodes: TreeNode[], expandedPaths: Set<string>, matchingPaths?: Set<string> | null): FlatNode[];
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* replace: 既存選択をクリアして新しいパスのみ選択
|
|
443
|
+
* toggle: 指定パスの選択を反転(Ctrl+Click)
|
|
444
|
+
* range: anchorから指定パスまでの範囲を選択(Shift+Click)
|
|
445
|
+
*/
|
|
446
|
+
declare function computeSelection(currentSelected: Set<string>, anchorPath: string | null, targetPath: string, mode: 'replace' | 'toggle' | 'range', flatList: FlatNode[]): {
|
|
447
|
+
selectedPaths: Set<string>;
|
|
448
|
+
anchorPath: string;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* デフォルトソート: フォルダ優先 → 名前の大文字小文字無視で昇順
|
|
453
|
+
*/
|
|
454
|
+
declare function defaultSort(a: FileEntry, b: FileEntry): number;
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* デフォルトフィルタ: 全表示(フィルタなし)
|
|
458
|
+
*/
|
|
459
|
+
declare function defaultFilter(_entry: FileEntry): boolean;
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* ファジーマッチ: クエリの各文字が順番に含まれているかチェック
|
|
463
|
+
* スコアも返す(連続マッチ・先頭マッチ・セパレータ後マッチが高スコア)
|
|
464
|
+
*/
|
|
465
|
+
declare function fuzzyMatch(query: string, target: string): {
|
|
466
|
+
match: boolean;
|
|
467
|
+
score: number;
|
|
468
|
+
};
|
|
469
|
+
/**
|
|
470
|
+
* ツリーフィルタ用: クエリにマッチするノードとその祖先を残す
|
|
471
|
+
* マッチしたパスのSetを返す
|
|
472
|
+
*/
|
|
473
|
+
declare function findMatchingPaths(nodes: TreeNode[], query: string): Set<string>;
|
|
474
|
+
/**
|
|
475
|
+
* ファジーファインド用: 全ファイルからスコア順に候補を返す
|
|
476
|
+
*/
|
|
477
|
+
declare function fuzzyFind(files: FileEntry[], query: string, maxResults?: number): FileEntry[];
|
|
478
|
+
|
|
479
|
+
/** momoi-keybind の KeybindingEntry と互換の型 */
|
|
480
|
+
interface ExplorerKeybindingEntry {
|
|
481
|
+
key: string;
|
|
482
|
+
command: string;
|
|
483
|
+
when?: string;
|
|
484
|
+
args?: unknown;
|
|
485
|
+
}
|
|
486
|
+
/** エクスプローラーのコマンドID */
|
|
487
|
+
declare const ExplorerCommands: {
|
|
488
|
+
readonly DELETE: "explorer.delete";
|
|
489
|
+
readonly RENAME: "explorer.rename";
|
|
490
|
+
readonly NEW_FILE: "explorer.newFile";
|
|
491
|
+
readonly NEW_FOLDER: "explorer.newFolder";
|
|
492
|
+
readonly REFRESH: "explorer.refresh";
|
|
493
|
+
readonly COLLAPSE_ALL: "explorer.collapseAll";
|
|
494
|
+
readonly SELECT_ALL: "explorer.selectAll";
|
|
495
|
+
readonly COPY_PATH: "explorer.copyPath";
|
|
496
|
+
};
|
|
497
|
+
type ExplorerCommandId = (typeof ExplorerCommands)[keyof typeof ExplorerCommands];
|
|
498
|
+
/** デフォルトのキーバインド定義。momoi-keybindのInputServiceに渡せる形式 */
|
|
499
|
+
declare const defaultExplorerKeybindings: ExplorerKeybindingEntry[];
|
|
500
|
+
|
|
501
|
+
export { type CreatingState, type ExplorerCommandId, ExplorerCommands, type ExplorerKeybindingEntry, type FileEntry, type FileSystemAdapter, type FileTreeController, type FileTreeOptions, type FlatNode, type MenuItemDef, type RawWatchEvent, type TreeEvent, type TreeNode, type TreeState, type WatchEvent, type WatchOptions, coalesceEvents, computeSelection, createEventProcessor, createFileTree, defaultExplorerKeybindings, defaultFilter, defaultSort, findMatchingPaths, flattenTree, fuzzyFind, fuzzyMatch };
|