diff --git a/src-tauri/src/frontmatter.rs b/src-tauri/src/frontmatter.rs index 005310b4..ae88284b 100644 --- a/src-tauri/src/frontmatter.rs +++ b/src-tauri/src/frontmatter.rs @@ -128,7 +128,12 @@ fn is_list_continuation(line: &str) -> bool { /// Split content into frontmatter body and the rest after the closing `---`. /// Returns `(fm_content, rest)` where `fm_content` is between the opening and closing `---`. fn split_frontmatter(content: &str) -> Result<(&str, &str), String> { - let fm_end = content[4..] + let after_open = &content[4..]; + // Handle empty frontmatter: closing --- immediately after opening ---\n + if let Some(stripped) = after_open.strip_prefix("---") { + return Ok(("", stripped)); + } + let fm_end = after_open .find("\n---") .map(|i| i + 4) .ok_or_else(|| "Malformed frontmatter: no closing ---".to_string())?; @@ -639,4 +644,73 @@ mod tests { fn test_format_yaml_key_with_period() { assert_eq!(format_yaml_key("key.name"), "\"key.name\""); } + + // --- split_frontmatter / empty frontmatter edge cases --- + + #[test] + fn test_split_frontmatter_empty_block() { + // ---\n---\n (no fields between opening and closing ---) + let result = split_frontmatter("---\n---\n"); + assert!( + result.is_ok(), + "split_frontmatter should handle empty frontmatter block" + ); + let (fm, rest) = result.unwrap(); + assert_eq!(fm, ""); + assert_eq!(rest, "\n"); + } + + #[test] + fn test_split_frontmatter_empty_block_no_trailing_newline() { + // ---\n--- (no trailing newline) + let result = split_frontmatter("---\n---"); + assert!( + result.is_ok(), + "split_frontmatter should handle empty frontmatter without trailing newline" + ); + } + + #[test] + fn test_split_frontmatter_empty_block_with_body() { + // ---\n---\n\n# Title\n + let result = split_frontmatter("---\n---\n\n# Title\n"); + assert!( + result.is_ok(), + "split_frontmatter should handle empty frontmatter with body" + ); + let (fm, rest) = result.unwrap(); + assert_eq!(fm, ""); + assert!(rest.contains("# Title")); + } + + #[test] + fn test_update_frontmatter_empty_block() { + let content = "---\n---\n\n# Test\n"; + let result = update_frontmatter_content( + content, + "title", + Some(FrontmatterValue::String("New Title".to_string())), + ); + assert!( + result.is_ok(), + "update_frontmatter_content should handle empty frontmatter block" + ); + let updated = result.unwrap(); + assert!(updated.contains("title: New Title")); + } + + #[test] + fn test_update_frontmatter_no_body_after_closing() { + // Frontmatter with title, no body after closing --- + let content = "---\ntitle: Old\n---\n"; + let result = update_frontmatter_content( + content, + "title", + Some(FrontmatterValue::String("New".to_string())), + ); + assert!(result.is_ok()); + let updated = result.unwrap(); + assert!(updated.contains("title: New")); + assert!(!updated.contains("title: Old")); + } } diff --git a/src-tauri/src/vault/rename.rs b/src-tauri/src/vault/rename.rs index 8db6908a..6299f179 100644 --- a/src-tauri/src/vault/rename.rs +++ b/src-tauri/src/vault/rename.rs @@ -402,4 +402,73 @@ mod tests { assert!(content.contains("title: New Name")); assert!(content.contains("# New Name")); } + + // --- Regression: rename empty / minimal notes (nota vuota) --- + + /// Helper: create a note, rename it, assert the rename succeeded and old file is gone. + /// Returns the content of the renamed file for further assertions. + fn rename_test_note(filename: &str, content: &str, new_title: &str) -> String { + let dir = TempDir::new().unwrap(); + let vault = dir.path(); + create_test_file(vault, filename, content); + + let old_path = vault.join(filename); + let result = rename_note( + vault.to_str().unwrap(), + old_path.to_str().unwrap(), + new_title, + ) + .expect("rename_note should succeed"); + + let expected_slug = title_to_slug(new_title); + assert!( + result.new_path.ends_with(&format!("{}.md", expected_slug)), + "new path should end with slug: {}", + expected_slug + ); + assert!(!old_path.exists(), "old file should be removed"); + assert!( + Path::new(&result.new_path).exists(), + "new file should exist" + ); + + fs::read_to_string(&result.new_path).unwrap() + } + + #[test] + fn test_rename_note_empty_file() { + rename_test_note("note/empty.md", "", "Renamed Empty"); + } + + #[test] + fn test_rename_note_empty_frontmatter_no_body() { + rename_test_note("note/empty-fm.md", "---\n---\n", "Renamed Note"); + } + + #[test] + fn test_rename_note_frontmatter_title_no_body() { + let content = rename_test_note( + "note/titled.md", + "---\ntitle: Old Title\ntype: Note\n---\n", + "New Title", + ); + assert!(content.contains("title: New Title")); + } + + #[test] + fn test_rename_note_h1_only_no_body() { + let content = rename_test_note("note/heading-only.md", "# Old Heading\n", "New Heading"); + assert!(content.contains("# New Heading")); + } + + #[test] + fn test_rename_note_frontmatter_and_h1_no_body() { + let content = rename_test_note( + "note/full-empty.md", + "---\ntitle: My Note\ntype: Note\nstatus: Active\n---\n\n# My Note\n\n", + "Renamed Note", + ); + assert!(content.contains("title: Renamed Note")); + assert!(content.contains("# Renamed Note")); + } }