
Tolaria ADR-0033 解析子文件夾掃描與側邊欄文件夾樹導航的實現【免費下載鏈接】tolariaDesktop app to manage markdown knowledge bases項目地址: https://gitcode.com/GitHub_Trending/to/tolariaTolaria一個基于 Tauri React 的 Markdown 知識庫桌面應用早期采用扁平庫設計只索引庫根目錄下的.md文件導致使用 PARA、帶文件夾的 Zettelkasten 或項目目錄工作流的用戶無法看到任何子目錄中的筆記。本文基于決策記錄 ADR-0033 完整還原這一架構決策Rust 端如何用walkdir遞歸掃描所有可見子目錄、如何用獨立的list_vault_foldersTauri 命令向側邊欄輸出折疊式 FOLDERS 樹以及前端如何以{ kind: folder }選擇類型渲染這棵可展開的目錄樹。讀完本文你可以理解從扁平庫到任意深度目錄 文件夾導航的演進邏輯、掃描與過濾的具體規則以及每個關鍵決策在源碼中的落點。背景ADR-0006 的扁平庫約束為何被取代Tolaria 最初的庫結構由 ADR-0006Flat vault structure 定義所有用戶筆記以扁平的.md文件存放在庫根目錄類型Type完全由 frontmatter 的type:字段決定從不從文件夾位置推斷。其掃描約束明確寫道scan_vault 只索引根級.md文件加受保護文件夾非受保護子目錄會被忽略。這一設計簡化了 wikilink 解析和類型變更改類型 改 frontmatter而非移動文件但付出了可用性代價按文件夾組織筆記的用戶PARA、帶目錄的 Zettelkasten、項目子目錄在側邊欄中看不到也無法按目錄過濾任何子目錄筆記庫掃描器靜默忽略所有子目錄中的.md文件帶有任意文件夾結構的庫實際上不可用ADR-0006 自己也預設了重評估觸發條件如果用戶需要嵌套文件夾層級做非類型組織例如項目專屬子目錄。ADR-0033 正是在該觸發條件被滿足后做出的決策擴展 Rust 端庫掃描器以索引所有可見子目錄中的.md文件并通過新的list_vault_foldersTauri 命令暴露庫的文件夾樹使側邊欄可以渲染一個可折疊的 FOLDERS 區塊。同時明確排除規則隱藏目錄以.開頭的目錄以及.git和.laputa同時被排除在掃描和文件夾樹之外。決策的三個候選方案與取舍ADR-0033 完整記錄了三條路線方案內容結論Option A采納用walkdir掃描所有子目錄另暴露獨立的list_vault_folders命令簡單VaultEntry無 schema 變更文件夾樹輕量且獨立于條目緩存Option B給VaultEntry加folder字段前端派生文件夾樹把文件夾元數據與條目緩存耦合僅創建/刪除文件夾無文件變化時的緩存失效被復雜化Option C保持扁平掃描增加虛擬文件夾功能按 frontmatter 路徑前綴分組未解決子目錄筆記缺失這一核心問題選擇 Option A 的關鍵理由在于關注點分離文件夾樹與筆記條目緩存完全解耦。這一取舍在源碼中清晰可見——VaultEntry結構src-tauri/src/vault/entry.rs中并不存在folder字段而文件夾樹由獨立的FolderNode結構承載/// 庫文件夾樹中的一個節點。只包含目錄不含文件。 #[derive(Debug, Serialize, Deserialize, Clone)] pub struct FolderNode { /// 文件夾名路徑最后一段。 pub name: String, /// 相對于庫根的路徑使用 / 分隔符例如 projects/laputa。 pub path: String, /// 子文件夾按字母排序。 pub children: VecFolderNode, }見 src-tauri/src/vault/entry.rs。樹形遞歸結構與children的按字母排序注釋直接對應實現細節下文會驗證。Rust 端實現一遞歸掃描所有可見子目錄ADR 承諾的擴展掃描器落地在scan_vault及其核心scan_all_files。見 src-tauri/src/vault/mod.rs/// 掃描 vault 中所有文件包括子目錄。 /// 隱藏目錄以 . 開頭被排除。 fn scan_all_files( vault_path: Path, git_dates: HashMapString, GitDates, entries: mut VecVaultEntry, ) { let walker WalkDir::new(vault_path) .follow_links(true) .into_iter() .filter_entry(|e| { if e.file_type().is_dir() { let name e.file_name().to_string_lossy(); // 跳過 vault 根本身depth 0—— 只過濾子目錄 if e.depth() 0 { return true; } return !is_hidden_dir(name); } true }); for entry in walker.filter_map(|e| e.ok()) { if entry.path().is_file() { // 跳過隱藏文件以 . 開頭—— 例如 .gitignore、.DS_Store let fname entry.file_name().to_string_lossy(); if fname.starts_with(.) { continue; } try_parse_file(entry.path(), vault_path, git_dates, entries); } } }實現細節與 ADR 承諾逐條對應遞歸深度無限制WalkDir::new(vault_path)不加min_depth/max_depth限制任意深度的.md文件都會被解析進VaultEntrytry_parse_file按is_md_file分派到parse_md_file或parse_non_md_file解析失敗僅log::warn!后跳過不會中斷整體掃描。隱藏目錄排除filter_entry回調對depth 0的目錄節點調用is_hidden_dir判斷覆蓋以.開頭的目錄自然包含.git與.laputa。注意根節點depth 0始終保留即使庫根目錄本身以.開頭也能被打開。隱藏文件排除.gitignore、.DS_Store等以.開頭的文件被顯式跳過避免進入筆記列表。符號鏈接跟隨follow_links(true)使軟鏈接目錄也被納入掃描——這是 ADR 未顯式提及、但從源碼結構看屬于有意為之的行為。排序穩定scan_vault最后按modified_at降序排序條目保證側邊欄展示順序與日期git 日期優先于文件系統日期見modified_dates_tests.rs語義一致。ADR 還提到用戶未來若有需要可以加.laputaignore來緩解非筆記.md文件如node_modules產生的多余條目。當前倉庫中實際的緩解機制比這更進一步掃描結果會經過 gitignore 感知過濾——filter_visible_vault_entriessrc-tauri/src/commands/vault/file_cmds.rs在返回條目前調用vault::filter_gitignored_entries其開關來自hide_gitignored_files_enabled()設置。對應測試startup_snapshot_visibility_keeps_snapshot_and_filters_ignored_entries驗證了被.gitignore忽略的ignored.md不會出現在結果中而visible.md會保留。Rust 端實現二list_vault_folders命令與文件夾樹構建文件夾樹獨立于條目緩存這一點體現為一條與list_vault平行且互不依賴的命令鏈路。命令注冊與定義src-tauri/src/commands/vault/file_cmds.rs#[tauri::command] pub async fn list_vault(path: PathBuf) - ResultVecVaultEntry, String { tokio::task::spawn_blocking(move || { with_expanded_vault_root(path.as_path(), scan_visible_vault_entries) }) .await .map_err(|e| format!(Task panicked: {e}))? } #[tauri::command] pub async fn list_vault_folders(path: PathBuf) - ResultVecFolderNode, String { tokio::task::spawn_blocking(move || { with_expanded_vault_root(path.as_path(), scan_visible_vault_folders) }) .await .map_err(|e| format!(Task panicked: {e}))? }兩個命令都在spawn_blocking中執行以避免阻塞 tokio 線程池且都先經with_expanded_vault_root展開用戶輸入的路徑處理~等命令在 src-tauri/src/lib.rs 的 invoke 列表中注冊。可見性過濾ADR 只提了隱藏目錄排除gitignore 過濾是后續演進疊加的fn scan_visible_vault_folders(vault_path: Path) - ResultVecFolderNode, String { let folders vault::scan_vault_folders(vault_path)?; Ok(vault::filter_gitignored_folders( vault_path, folders, crate::settings::hide_gitignored_files_enabled(), )) }即先構建原始樹再按設置裁剪被 git 忽略的分支見 src-tauri/src/commands/vault/file_cmds.rs忽略集合的計算在 src-tauri/src/vault/ignored.rs。樹的構建算法src-tauri/src/vault/mod.rs/// 構建 vault 中用戶創建文件夾的樹。 pub fn scan_vault_folders(vault_path: Path) - ResultVecFolderNode, String { if !vault_path.is_dir() { return Err(format!(Not a directory: {}, vault_path.display())); } fn build_tree(dir: Path, vault_root: Path) - VecFolderNode { let mut nodes: VecFolderNode Vec::new(); let entries match fs::read_dir(dir) { Ok(d) d, Err(_) return nodes, }; for entry in entries.flatten() { let path entry.path(); if !path.is_dir() { continue; // 只收目錄不收文件 } let name entry.file_name().to_string_lossy().to_string(); if is_folder_tree_hidden_dir(name) { continue; // 隱藏目錄不入樹 } let rel_path path_identity::vault_relative_path_string(vault_root, path) .unwrap_or_else(|_| { path_identity::normalize_path_for_identity(path.to_string_lossy()) }); let children build_tree(path, vault_root); nodes.push(FolderNode { name, path: rel_path, children }); } nodes.sort_by_key(|node| node.name.to_lowercase()); nodes } Ok(build_tree(vault_path, vault_path)) }值得注意的三個工程點與條目掃描不同文件夾樹只用fs::read_dir遞歸不解析任何 frontmatter因此成本遠低于scan_vault——這正是 Option A 文件夾樹輕量主張的直接體現path字段是相對于庫根、統一用/分隔的規范化字符串vault_relative_path_string跨平臺一致前端可直接用它做過濾鍵和身份鍵同級節點按名字不區分大小寫排序name.to_lowercase()與FolderNode文檔注釋sorted alphabetically一致。前后端協作選擇模型與側邊欄 FOLDERS 區塊ADR 的后果部分預言了SidebarSelection獲得新的{ kind: folder; path: string }變體——所有對 selection kind 的窮盡 switch 都必須處理它。當前代碼庫驗證了這一變體已全面鋪開選擇類型在 src/types.ts 中定義{ kind: folder; path: string }貫穿側邊欄、集合構建與鍵盤導航src/collections/collectionFromSelection.ts 會為文件夾選擇構造該目錄下筆記的集合測試 src/collections/collectionFromSelection.test.ts 斷言collectionFromSelection({ kind: folder, path: clients, rootPath: /vault })正確生成集合前端文件夾樹組件位于 src/components/folder-tree/ 目錄FolderTree.tsx區塊渲染、FolderTreeRow.tsx單行節點、FolderItemRow.tsx、FolderContextMenu.tsx右鍵操作、FolderNameInput.tsx內聯重命名/新建、folderTreeLayout.ts縮進/展開布局計算以及三個自定義 hookuseFolderTreeDisclosure.ts展開/折疊狀態、useFolderRowInteractions.ts行交互、useFolderContextMenu.ts上下文菜單展開狀態管理展示了手動狀態 必需路徑的合并模式src/components/folder-tree/useFolderTreeDisclosure.ts當用戶選中某個文件夾時requiredExpandedPaths會強制展開該文件夾的所有祖先路徑ancestorTreePaths與用戶的manualExpanded手動狀態合并mergeExpandedPaths頂層節點key 以::結尾或空 key默認展開。這保證了點擊側邊欄中任意層級文件夾其父鏈一定可見的體驗測試 src/components/FolderTree.test.tsx 覆蓋了區塊行為渲染FOLDERS標題與頂層文件夾、點擊標題可折疊整個區塊、行點擊回調{ kind: folder, path: projects }、多工作區下path: 的庫根節點選擇等場景。另外Rust 端同時提供create_vault_folder命令支持在樹中直接創建文件夾src-tauri/src/commands/vault/file_cmds.rs它通過 path boundary 校驗拒絕逃逸庫根的路徑測試commands_reject_paths_outside_requested_vault驗證了../escape會報 Path must stay inside the active vault并拒絕與已有文件夾重名。文件夾創建會觸發新的list_vault_folders拉取而條目緩存src-tauri/src/vault/cache.rs 的scan_vault_cached只在必要時失效——Option A 解耦收益在此兌現。決策的后果與邊界條件ADR-0033 明確記錄了以下后果均可在倉庫中找到對應實現或測試佐證所有深度的.md文件都被索引——非筆記 Markdown如node_modules內文件會產生多余條目。當前緩解手段是隱藏目錄排除 gitignore 過濾見上文ADR 中提到的.laputaignore機制是未來如需要的可選項倉庫中尚未實現。掃描與 git 緩存對齊git 緩存cache.rs本就使用walkdir做變更檢測本次改動使掃描語義與緩存語義一致避免緩存感知到子目錄變化但掃描忽略的錯位。回歸測試 src-tauri/src/vault/mod_tests/real_vault_consistency.rs 用獨立的WalkDir遍歷真實演示庫校驗掃描結果一致性。SidebarSelection的窮盡 switch 擴展——已在多處落地側邊欄、集合構建、筆記列表渲染測試均處理kind: folder。ADR-0006 的扁平庫原則被放松筆記現在可以放在任意子目錄中但類型定義文檔仍然只存在于庫根的type/目錄——這一約束在 ADR-0096 中延續類型文檔只能由根目錄創建確保類型來自 frontmatter、不來自文件夾位置這一 ADR-0006 的核心不變量沒有被破壞。遞歸文件夾過濾被明確推遲當前選中一個文件夾時只展示其直接子級筆記非遞歸ADR 要求如果用戶要求遞歸文件夾過濾再重新評估。這與collectionFromSelection中按path前綴構造集合的實現方向一致。小結ADR-0033 是一次典型的約束放松 關注點分離重構掃描器從根目錄 受保護文件夾放寬到所有非隱藏子目錄同時把文件夾樹從筆記條目模型中徹底拆出以一條輕量、獨立失效的list_vault_folders命令供給前端。三個方案對比中落選者 B 和 C 分別輸在耦合緩存和沒解決核心問題上。理解這條決策鏈對把握 Tolaria 的后續演進文件夾右鍵操作、集合構建、gitignore 可見性邊界、非 git 庫支持等非常有用——它們的共同前提都是文件夾是一等導航對象但類型語義依然只由 frontmatter 承載。【免費下載鏈接】tolariaDesktop app to manage markdown knowledge bases項目地址: https://gitcode.com/GitHub_Trending/to/tolaria創作聲明:本文部分內容由AI輔助生成(AIGC),僅供參考