Skip to main content

llmwiki_tooling/cmd/
sections.rs

1use crate::config::WikiConfig;
2use crate::error::WikiError;
3use crate::splice;
4use crate::wiki::Wiki;
5
6/// Run `sections rename`: rename a heading across the wiki, including fragment references.
7pub fn rename(
8    wiki: &mut Wiki,
9    old_name: &str,
10    new_name: &str,
11    dirs: &Option<Vec<String>>,
12    write: bool,
13) -> Result<usize, WikiError> {
14    // Collect all changes first (read phase)
15    let mut changes: super::FileEdits = Vec::new();
16
17    for file_path in wiki.all_scannable_files() {
18        let rel_path = wiki.rel_path(&file_path);
19
20        // If dirs filter is set, skip files outside those directories
21        if let Some(dir_filter) = dirs
22            && !WikiConfig::matches_dirs(rel_path, dir_filter)
23        {
24            continue;
25        }
26
27        let source = wiki.source(&file_path)?;
28        let mut file_edits = Vec::new();
29
30        // Find heading occurrences to rename
31        let headings = wiki.headings(&file_path)?;
32        for h in headings {
33            if h.text.eq_ignore_ascii_case(old_name) {
34                // Replace just the heading text within the heading range.
35                // The heading range includes `## ` prefix. Find the text portion.
36                let heading_src = &source[h.byte_range.clone()];
37                if let Some(text_offset) = heading_src.find(&h.text) {
38                    let abs_start = h.byte_range.start + text_offset;
39                    let abs_end = abs_start + h.text.len();
40                    file_edits.push((abs_start..abs_end, new_name.to_owned()));
41                }
42            }
43        }
44
45        // Find wikilink heading fragment references: [[page#Old Name]] -> [[page#New Name]]
46        let wikilinks = wiki.wikilinks(&file_path)?;
47        for wl in wikilinks {
48            if let Some(crate::page::WikilinkFragment::Heading(ref heading)) = wl.fragment
49                && heading.as_str().eq_ignore_ascii_case(old_name)
50            {
51                let wl_src = &source[wl.byte_range.clone()];
52                let new_wl = wl_src.replace(heading.as_str(), new_name);
53                if new_wl != wl_src {
54                    file_edits.push((wl.byte_range.clone(), new_wl));
55                }
56            }
57        }
58
59        if !file_edits.is_empty() {
60            changes.push((file_path, source.to_owned(), file_edits));
61        }
62    }
63
64    // Apply changes (write phase)
65    let mut total_changes = 0;
66    for (file_path, source, file_edits) in changes {
67        let rel_path = wiki.rel_path(&file_path);
68
69        if write {
70            let result = splice::apply(&source, &file_edits);
71            wiki.write_file(&file_path, &result)?;
72            println!(
73                "{}: renamed {} occurrence(s)",
74                rel_path.display(),
75                file_edits.len()
76            );
77        } else {
78            print!("{}", splice::diff(&source, rel_path, &file_edits));
79        }
80
81        total_changes += file_edits.len();
82    }
83
84    Ok(total_changes)
85}