feat(04-02): add [[Directory]] sentinel and list_vault_files() to vault.rs

- resolve_wiki_link() returns PathBuf::from("__directory__") for case-insensitive 'directory' target
- DirEntry struct with depth, name, is_dir, vault_path fields
- list_vault_files() using WalkDir::new().sort_by_file_name() for alphabetical tree
- Skips hidden entries (leading '.') and non-.md files silently
This commit is contained in:
2026-03-01 10:56:48 +01:00
parent 9b12d4beee
commit fe69cf5fab
+69
View File
@@ -1,5 +1,6 @@
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use walkdir::WalkDir;
// ── VaultDocument ───────────────────────────────────────────────────────────── // ── VaultDocument ─────────────────────────────────────────────────────────────
@@ -83,6 +84,11 @@ pub fn is_within_vault(vault_path: &Path, candidate: &Path) -> bool {
/// → matches "guides/getting-started.md" /// → matches "guides/getting-started.md"
/// ``` /// ```
pub fn resolve_wiki_link(vault_path: &Path, raw_target: &str) -> Option<PathBuf> { pub fn resolve_wiki_link(vault_path: &Path, raw_target: &str) -> Option<PathBuf> {
// Magic sentinel — [[Directory]] navigates to the virtual directory page
if raw_target.eq_ignore_ascii_case("directory") {
return Some(PathBuf::from("__directory__"));
}
// Split off subpath prefix if present: "guides/Getting Started" → ("guides", "Getting Started") // Split off subpath prefix if present: "guides/Getting Started" → ("guides", "Getting Started")
let (subdir, name) = match raw_target.rfind('/') { let (subdir, name) = match raw_target.rfind('/') {
Some(i) => (&raw_target[..i], &raw_target[i + 1..]), Some(i) => (&raw_target[..i], &raw_target[i + 1..]),
@@ -182,3 +188,66 @@ pub fn resolve_standard_link(vault_path: &Path, current_doc: &str, dest: &str) -
None None
} }
// ── Directory listing ─────────────────────────────────────────────────────────
/// Entry in the vault directory listing.
pub struct DirEntry {
/// Indentation depth (1-based, depth 1 = vault root children).
pub depth: usize,
/// Display name (without .md extension for files).
pub name: String,
/// True if this entry is a directory.
pub is_dir: bool,
/// Vault-relative path (only for .md files; None for directories).
pub vault_path: Option<String>,
}
/// List all markdown files and directories in the vault, sorted alphabetically.
///
/// Skips hidden files/directories (starting with '.') and non-.md files.
/// Returns entries with depth for tree indentation.
pub fn list_vault_files(vault_path: &Path) -> Vec<DirEntry> {
let mut entries = Vec::new();
for entry in WalkDir::new(vault_path)
.sort_by_file_name()
.into_iter()
.filter_map(|e| e.ok())
.skip(1) // skip vault root itself
{
let depth = entry.depth();
let name = entry.file_name().to_string_lossy().to_string();
// Skip hidden files/dirs
if name.starts_with('.') {
continue;
}
let is_dir = entry.file_type().is_dir();
if is_dir {
entries.push(DirEntry {
depth,
name,
is_dir: true,
vault_path: None,
});
} else if name.ends_with(".md") {
let display_name = name.strip_suffix(".md").unwrap_or(&name).to_string();
let rel = entry
.path()
.strip_prefix(vault_path)
.unwrap_or(entry.path())
.to_string_lossy()
.to_string();
entries.push(DirEntry {
depth,
name: display_name,
is_dir: false,
vault_path: Some(rel),
});
}
// Skip non-.md files silently
}
entries
}