@@ -64,6 +64,14 @@ pub struct EditResult {
6464 pub mode : String ,
6565}
6666
67+ #[ derive( Debug , Clone ) ]
68+ pub struct RewriteInput {
69+ pub file : String ,
70+ pub content : String ,
71+ pub preserve_frontmatter : bool ,
72+ pub modified_by : String ,
73+ }
74+
6775#[ derive( Debug , Clone , serde:: Serialize ) ]
6876pub struct WriteResult {
6977 pub path : String ,
@@ -802,6 +810,50 @@ pub fn edit_note(
802810 } )
803811}
804812
813+ /// Rewrite the body of an existing note, optionally preserving existing frontmatter.
814+ ///
815+ /// If `preserve_frontmatter` is true and the note has frontmatter, the existing
816+ /// YAML block is kept intact and only the body is replaced with `input.content`.
817+ /// If false, the file is replaced entirely with `input.content`.
818+ ///
819+ /// Does NOT re-index — the MCP layer handles that.
820+ pub fn rewrite_note ( store : & Store , vault_path : & Path , input : & RewriteInput ) -> Result < EditResult > {
821+ // Step 1: Resolve file via store
822+ let file_record = store
823+ . resolve_file ( & input. file ) ?
824+ . ok_or_else ( || anyhow:: anyhow!( "file not found: {}" , input. file) ) ?;
825+
826+ let full_path = vault_path. join ( & file_record. path ) ;
827+
828+ // Step 2: Read current content from disk
829+ let existing_content = std:: fs:: read_to_string ( & full_path) ?;
830+
831+ // Step 3: Split frontmatter using crate::markdown::split_frontmatter
832+ let ( maybe_frontmatter, _old_body) = crate :: markdown:: split_frontmatter ( & existing_content) ;
833+
834+ // Step 4: Reconstruct content
835+ let new_content = if input. preserve_frontmatter {
836+ if let Some ( frontmatter) = maybe_frontmatter {
837+ format ! ( "---\n {}\n ---\n \n {}" , frontmatter, input. content)
838+ } else {
839+ // No existing frontmatter — just use new content as-is
840+ input. content . clone ( )
841+ }
842+ } else {
843+ input. content . clone ( )
844+ } ;
845+
846+ // Step 5: Write atomically (overwrite = true)
847+ atomic_write ( & full_path, & new_content, true ) ?;
848+
849+ // Step 6: Return EditResult (reusing existing result type)
850+ Ok ( EditResult {
851+ path : file_record. path ,
852+ heading : String :: new ( ) ,
853+ mode : "Rewrite" . to_string ( ) ,
854+ } )
855+ }
856+
805857/// Move a note to a new folder.
806858pub fn move_note (
807859 file : & str ,
@@ -1419,4 +1471,26 @@ mod tests {
14191471 assert ! ( result. is_err( ) ) ;
14201472 assert ! ( result. unwrap_err( ) . to_string( ) . contains( "file not found" ) ) ;
14211473 }
1474+
1475+ #[ test]
1476+ fn test_rewrite_preserves_frontmatter ( ) {
1477+ let ( tmp, store, root) = setup_vault ( ) ;
1478+ let content = "---\n tags:\n - project\n status: active\n ---\n \n # Old Content\n \n Old body\n " ;
1479+ std:: fs:: write ( root. join ( "note.md" ) , content) . unwrap ( ) ;
1480+ store. insert_file ( "note.md" , "hash" , 100 , & [ "project" . to_string ( ) ] , "rew123" , None ) . unwrap ( ) ;
1481+
1482+ let input = RewriteInput {
1483+ file : "note.md" . into ( ) ,
1484+ content : "# New Content\n \n New body\n " . into ( ) ,
1485+ preserve_frontmatter : true ,
1486+ modified_by : "test" . into ( ) ,
1487+ } ;
1488+ rewrite_note ( & store, & root, & input) . unwrap ( ) ;
1489+
1490+ let updated = std:: fs:: read_to_string ( root. join ( "note.md" ) ) . unwrap ( ) ;
1491+ assert ! ( updated. contains( "status: active" ) ) ;
1492+ assert ! ( updated. contains( "# New Content" ) ) ;
1493+ assert ! ( !updated. contains( "Old body" ) ) ;
1494+ drop ( tmp) ;
1495+ }
14221496}
0 commit comments