From 6bf70eb6350f8ae6faafea67e6c00066b16a6c44 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Fri, 29 May 2026 03:19:59 +0200 Subject: [PATCH] refactor: consolidate note-list body search --- docs/ABSTRACTIONS.md | 2 +- docs/ARCHITECTURE.md | 2 +- src-tauri/src/commands/vault/scan_cmds.rs | 22 ++++-- src-tauri/src/search.rs | 68 ++++++++++++++++++- src/components/NoteList.rendering.test.tsx | 51 +++++--------- .../note-list/noteListFullTextSearch.ts | 49 ++----------- src/mock-tauri/mock-handlers.ts | 21 +++++- src/mock-tauri/vault-api.ts | 4 +- tests/helpers/fixtureVault.ts | 5 +- vite.config.ts | 29 ++++++-- 10 files changed, 156 insertions(+), 97 deletions(-) diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md index e6b4ab6a..7c847f59 100644 --- a/docs/ABSTRACTIONS.md +++ b/docs/ABSTRACTIONS.md @@ -771,7 +771,7 @@ interface SearchResult { - Click result to open note in editor - Shows relevance score and snippet -The NoteList header search keeps its local title/snippet/property filtering for immediate scoped results, then augments the match set with `search_vault` full-content hits from the visible workspace roots. It verifies backend hits against Markdown body text with frontmatter excluded, and stores only matching paths so body-only matches appear in the current list scope without rendering private matched text in note rows. +The NoteList header search keeps its local title/snippet/property filtering for immediate scoped results, then augments the match set with `search_vault` hits from the visible workspace roots using the command's frontmatter-excluding search option. React stores only matching paths so body-only matches appear in the current list scope without a second content-read pass or rendering private matched text in note rows. No indexing step required — search runs directly against the filesystem. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1dae9d68..6ac60875 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -454,7 +454,7 @@ Search is keyword-based, using `walkdir` to scan all `.md` files in the vault di The `search_vault` Tauri command runs the scan in a blocking Tokio task and returns results sorted by relevance score. -The note-list search field combines client-side scoped filtering with that same command: title, snippet, and visible-property matches resolve immediately, while backend full-content hits are verified against Markdown body text, excluding frontmatter, before adding matching paths for the currently visible workspace roots without displaying matched body text in the note row. +The note-list search field combines client-side scoped filtering with that same command: title, snippet, and visible-property matches resolve immediately, while backend body-content hits use `search_vault` with frontmatter excluded before adding matching paths for the currently visible workspace roots without displaying matched body text in the note row. ## Vault Cache System diff --git a/src-tauri/src/commands/vault/scan_cmds.rs b/src-tauri/src/commands/vault/scan_cmds.rs index b0f4d334..d92b5c1d 100644 --- a/src-tauri/src/commands/vault/scan_cmds.rs +++ b/src-tauri/src/commands/vault/scan_cmds.rs @@ -100,14 +100,24 @@ pub async fn reload_vault( pub async fn search_vault( vault_path: String, query: String, - mode: String, limit: Option, + exclude_frontmatter: Option, ) -> Result { let vault_path = expand_tilde(&vault_path).into_owned(); let limit = limit.unwrap_or(20); - tokio::task::spawn_blocking(move || search::search_vault(&vault_path, &query, &mode, limit)) - .await - .map_err(|e| format!("Search task failed: {}", e))? + let exclude_frontmatter = exclude_frontmatter.unwrap_or(false); + tokio::task::spawn_blocking(move || { + search::search_vault_with_options(search::SearchOptions { + vault_path: &vault_path, + query: &query, + mode: "keyword", + limit, + hide_gitignored_files: crate::settings::hide_gitignored_files_enabled(), + exclude_frontmatter, + }) + }) + .await + .map_err(|e| format!("Search task failed: {}", e))? } #[cfg(test)] @@ -264,7 +274,7 @@ mod tests { let response = search_vault( dir.path().to_string_lossy().into_owned(), "needle".to_string(), - "keyword".to_string(), + None, None, ) .await @@ -287,8 +297,8 @@ mod tests { let response = search_vault( dir.path().to_string_lossy().into_owned(), "needle".to_string(), - "keyword".to_string(), Some(1), + None, ) .await .unwrap(); diff --git a/src-tauri/src/search.rs b/src-tauri/src/search.rs index dc7e9641..d3c29f14 100644 --- a/src-tauri/src/search.rs +++ b/src-tauri/src/search.rs @@ -26,6 +26,7 @@ pub struct SearchOptions<'a> { pub mode: &'a str, pub limit: usize, pub hide_gitignored_files: bool, + pub exclude_frontmatter: bool, } struct Utf8Boundary<'a> { @@ -126,9 +127,29 @@ pub fn search_vault( mode: _mode, limit, hide_gitignored_files: crate::settings::hide_gitignored_files_enabled(), + exclude_frontmatter: false, }) } +fn strip_frontmatter(content: &str) -> &str { + let Some(rest) = content.strip_prefix("---") else { + return content; + }; + + match rest.find("\n---") { + Some(end) => rest[end + 4..].trim_start(), + None => content, + } +} + +fn searchable_content(content: &str, exclude_frontmatter: bool) -> &str { + if exclude_frontmatter { + strip_frontmatter(content) + } else { + content + } +} + fn is_markdown_search_candidate(vault_dir: &Path, path: &Path) -> bool { if !path.extension().is_some_and(|ext| ext == "md") { return false; @@ -164,7 +185,8 @@ pub fn search_vault_with_options(options: SearchOptions<'_>) -> Result continue, }; - let content_lower = content.to_lowercase(); + let searchable_content = searchable_content(&content, options.exclude_frontmatter); + let content_lower = searchable_content.to_lowercase(); let filename = path .file_name() .and_then(|value| value.to_str()) @@ -183,7 +205,7 @@ pub fn search_vault_with_options(options: SearchOptions<'_>) -> Result