feat(config): configurable generated-section captions (P2)
TOC / links / tags section headings (and their levels) were hardcoded. Added per-wiki toc_header/toc_header_level, links_header/links_header_level, tags_header/tags_header_level — defaults Contents / Generated Links / Generated Tags at level 1, matching upstream (also fixes the tags index heading: Tags -> Generated Tags). A caption_line(name, level) helper emits the `=`-markers; the configured text + level thread through toc_edit / links_edit / tag_links_edit and the auto_toc save hook. find_section_range matches the configured text, so regeneration stays idempotent at any level. Tests: captions_honour_custom_header_and_level (link_health) + config round-trip + default assertions (index_and_config); existing tag/toc/links tests updated for the new arity and the Generated Tags default. fmt/clippy clean (8-arg ops get allow(too_many_arguments), matching the codebase precedent); 0 Rust test failures; config-parity green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -324,7 +324,14 @@ fn toc_generate(backend: &Backend, args: Vec<Value>) -> Result<Option<WorkspaceE
|
||||
None => return Ok(None),
|
||||
};
|
||||
let utf8 = backend.use_utf8.load(Ordering::Relaxed);
|
||||
Ok(ops::toc_edit(&doc.text, &doc.ast, &p.uri, utf8))
|
||||
let wiki = backend.wiki_for_uri(&p.uri);
|
||||
let (header, level) = wiki
|
||||
.as_ref()
|
||||
.map(|w| (w.config.toc_header.clone(), w.config.toc_header_level))
|
||||
.unwrap_or_else(|| (ops::TOC_HEADING.to_string(), 1));
|
||||
Ok(ops::toc_edit(
|
||||
&doc.text, &doc.ast, &p.uri, utf8, &header, level,
|
||||
))
|
||||
}
|
||||
|
||||
fn links_generate(backend: &Backend, args: Vec<Value>) -> Result<Option<WorkspaceEdit>, String> {
|
||||
@@ -353,6 +360,8 @@ fn links_generate(backend: &Backend, args: Vec<Value>) -> Result<Option<Workspac
|
||||
¤t_page,
|
||||
&pages,
|
||||
utf8,
|
||||
&wiki.config.links_header,
|
||||
wiki.config.links_header_level,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -615,6 +624,8 @@ fn tags_generate_links(
|
||||
parsed.tag.as_deref(),
|
||||
&snapshot,
|
||||
utf8,
|
||||
&wiki.config.tags_header,
|
||||
wiki.config.tags_header_level,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1780,14 +1791,18 @@ pub mod ops {
|
||||
anchor: String,
|
||||
}
|
||||
|
||||
/// A `= Heading =` line at the given level (clamped 1..=6), with trailing
|
||||
/// newline. Drives the configurable `*_header` / `*_header_level` captions.
|
||||
pub fn caption_line(name: &str, level: u8) -> String {
|
||||
let eq = "=".repeat((level as usize).clamp(1, 6));
|
||||
format!("{eq} {name} {eq}\n")
|
||||
}
|
||||
|
||||
/// Build a heading + nested list TOC for the current document. The TOC
|
||||
/// itself (any existing `= Contents =` heading on the page) is skipped
|
||||
/// so re-generation is idempotent.
|
||||
pub fn build_toc_text(items: &[(u8, String, String)], heading_name: &str) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str("= ");
|
||||
out.push_str(heading_name);
|
||||
out.push_str(" =\n");
|
||||
pub fn build_toc_text(items: &[(u8, String, String)], heading_name: &str, level: u8) -> String {
|
||||
let mut out = caption_line(heading_name, level);
|
||||
if items.is_empty() {
|
||||
return out;
|
||||
}
|
||||
@@ -1808,11 +1823,13 @@ pub mod ops {
|
||||
|
||||
/// Build a flat list of wikilinks to every page (sorted), optionally
|
||||
/// excluding `current_page`.
|
||||
pub fn build_links_text(pages: &[String], heading_name: &str, exclude: Option<&str>) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str("= ");
|
||||
out.push_str(heading_name);
|
||||
out.push_str(" =\n");
|
||||
pub fn build_links_text(
|
||||
pages: &[String],
|
||||
heading_name: &str,
|
||||
level: u8,
|
||||
exclude: Option<&str>,
|
||||
) -> String {
|
||||
let mut out = caption_line(heading_name, level);
|
||||
for name in pages {
|
||||
if Some(name.as_str()) == exclude {
|
||||
continue;
|
||||
@@ -1938,8 +1955,10 @@ pub mod ops {
|
||||
ast: &DocumentNode,
|
||||
uri: &Url,
|
||||
utf8: bool,
|
||||
heading: &str,
|
||||
level: u8,
|
||||
) -> Option<WorkspaceEdit> {
|
||||
let items = collect_toc_items(ast, TOC_HEADING);
|
||||
let items = collect_toc_items(ast, heading);
|
||||
if items.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -1947,9 +1966,9 @@ pub mod ops {
|
||||
.into_iter()
|
||||
.map(|it| (it.level, it.title, it.anchor))
|
||||
.collect();
|
||||
let new_text = build_toc_text(&triples, TOC_HEADING);
|
||||
let new_text = build_toc_text(&triples, heading, level);
|
||||
let mut b = WorkspaceEditBuilder::new();
|
||||
match find_section_range(ast, TOC_HEADING) {
|
||||
match find_section_range(ast, heading) {
|
||||
Some((start, end)) => {
|
||||
let span = Span::new(start, end);
|
||||
b.edit(uri.clone(), text_edit_replace(span, new_text, text, utf8));
|
||||
@@ -1980,13 +1999,16 @@ pub mod ops {
|
||||
ast: &DocumentNode,
|
||||
uri: &Url,
|
||||
utf8: bool,
|
||||
heading: &str,
|
||||
level: u8,
|
||||
) -> Option<WorkspaceEdit> {
|
||||
find_section_range(ast, TOC_HEADING)?;
|
||||
toc_edit(text, ast, uri, utf8)
|
||||
find_section_range(ast, heading)?;
|
||||
toc_edit(text, ast, uri, utf8, heading, level)
|
||||
}
|
||||
|
||||
/// Produce a `WorkspaceEdit` that replaces or inserts the
|
||||
/// auto-generated links section for the given document.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn links_edit(
|
||||
text: &str,
|
||||
ast: &DocumentNode,
|
||||
@@ -1994,13 +2016,15 @@ pub mod ops {
|
||||
current_page: &str,
|
||||
all_pages: &[String],
|
||||
utf8: bool,
|
||||
heading: &str,
|
||||
level: u8,
|
||||
) -> Option<WorkspaceEdit> {
|
||||
if all_pages.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let new_text = build_links_text(all_pages, LINKS_HEADING, Some(current_page));
|
||||
let new_text = build_links_text(all_pages, heading, level, Some(current_page));
|
||||
let mut b = WorkspaceEditBuilder::new();
|
||||
match find_section_range(ast, LINKS_HEADING) {
|
||||
match find_section_range(ast, heading) {
|
||||
Some((start, end)) => {
|
||||
let span = Span::new(start, end);
|
||||
b.edit(uri.clone(), text_edit_replace(span, new_text, text, utf8));
|
||||
@@ -3118,6 +3142,8 @@ pub mod ops {
|
||||
pub fn build_tag_links_text(
|
||||
pages_by_tag: &std::collections::BTreeMap<String, Vec<String>>,
|
||||
tag: Option<&str>,
|
||||
header: &str,
|
||||
level: u8,
|
||||
) -> Option<String> {
|
||||
match tag {
|
||||
Some(t) => {
|
||||
@@ -3125,7 +3151,7 @@ pub mod ops {
|
||||
if pages.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut out = format!("= Tag: {t} =\n");
|
||||
let mut out = caption_line(&format!("Tag: {t}"), level);
|
||||
for p in pages {
|
||||
out.push_str(&format!("- [[{p}]]\n"));
|
||||
}
|
||||
@@ -3135,9 +3161,11 @@ pub mod ops {
|
||||
if pages_by_tag.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut out = String::from("= Tags =\n");
|
||||
let mut out = caption_line(header, level);
|
||||
let sub = level.saturating_add(1);
|
||||
for (name, pages) in pages_by_tag {
|
||||
out.push_str(&format!("\n== {name} ==\n"));
|
||||
out.push('\n');
|
||||
out.push_str(&caption_line(name, sub));
|
||||
for p in pages {
|
||||
out.push_str(&format!("- [[{p}]]\n"));
|
||||
}
|
||||
@@ -3148,11 +3176,12 @@ pub mod ops {
|
||||
}
|
||||
|
||||
/// Heading name used to locate an existing tag-links section so
|
||||
/// re-generation is idempotent.
|
||||
fn tag_section_heading(tag: Option<&str>) -> String {
|
||||
/// re-generation is idempotent. The no-arg index uses the configured
|
||||
/// `tags_header`; a single-tag section uses `Tag: <name>`.
|
||||
fn tag_section_heading(tag: Option<&str>, header: &str) -> String {
|
||||
match tag {
|
||||
Some(t) => format!("Tag: {t}"),
|
||||
None => "Tags".to_string(),
|
||||
None => header.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3160,6 +3189,7 @@ pub mod ops {
|
||||
/// section (case-insensitive match on the h1 title) or inserts at the
|
||||
/// top of the document when no such section exists. Returns `None`
|
||||
/// when there are no matching tagged pages.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn tag_links_edit(
|
||||
text: &str,
|
||||
ast: &DocumentNode,
|
||||
@@ -3167,9 +3197,11 @@ pub mod ops {
|
||||
tag: Option<&str>,
|
||||
pages_by_tag: &std::collections::BTreeMap<String, Vec<String>>,
|
||||
utf8: bool,
|
||||
header: &str,
|
||||
level: u8,
|
||||
) -> Option<WorkspaceEdit> {
|
||||
let body = build_tag_links_text(pages_by_tag, tag)?;
|
||||
let heading = tag_section_heading(tag);
|
||||
let body = build_tag_links_text(pages_by_tag, tag, header, level)?;
|
||||
let heading = tag_section_heading(tag, header);
|
||||
let mut b = WorkspaceEditBuilder::new();
|
||||
match find_section_range(ast, &heading) {
|
||||
Some((start, end)) => {
|
||||
|
||||
@@ -75,6 +75,18 @@ pub struct WikiConfig {
|
||||
pub links_space_char: String,
|
||||
/// Rebuild the page's TOC on save when set.
|
||||
pub auto_toc: bool,
|
||||
/// Heading text + level for the generated `:VimwikiTOC` section
|
||||
/// (vimwiki's `toc_header` / `toc_header_level`).
|
||||
pub toc_header: String,
|
||||
pub toc_header_level: u8,
|
||||
/// Heading text + level for `:VimwikiGenerateLinks` (`links_header` /
|
||||
/// `links_header_level`).
|
||||
pub links_header: String,
|
||||
pub links_header_level: u8,
|
||||
/// Heading text + level for the no-arg `:VimwikiGenerateTagLinks` index
|
||||
/// (`tags_header` / `tags_header_level`).
|
||||
pub tags_header: String,
|
||||
pub tags_header_level: u8,
|
||||
/// HTML export options.
|
||||
pub html: HtmlConfig,
|
||||
}
|
||||
@@ -196,6 +208,12 @@ impl WikiConfig {
|
||||
list_margin: d.list_margin,
|
||||
links_space_char: d.links_space_char,
|
||||
auto_toc: d.auto_toc,
|
||||
toc_header: d.toc_header,
|
||||
toc_header_level: d.toc_header_level,
|
||||
links_header: d.links_header,
|
||||
links_header_level: d.links_header_level,
|
||||
tags_header: d.tags_header,
|
||||
tags_header_level: d.tags_header_level,
|
||||
html: HtmlConfig::default(),
|
||||
}
|
||||
}
|
||||
@@ -226,6 +244,12 @@ impl WikiConfig {
|
||||
list_margin: d.list_margin,
|
||||
links_space_char: d.links_space_char,
|
||||
auto_toc: d.auto_toc,
|
||||
toc_header: d.toc_header,
|
||||
toc_header_level: d.toc_header_level,
|
||||
links_header: d.links_header,
|
||||
links_header_level: d.links_header_level,
|
||||
tags_header: d.tags_header,
|
||||
tags_header_level: d.tags_header_level,
|
||||
html,
|
||||
}
|
||||
}
|
||||
@@ -322,6 +346,18 @@ fn default_listsym_rejected() -> String {
|
||||
"-".to_string()
|
||||
}
|
||||
|
||||
fn default_toc_header() -> String {
|
||||
"Contents".to_string()
|
||||
}
|
||||
|
||||
fn default_links_header() -> String {
|
||||
"Generated Links".to_string()
|
||||
}
|
||||
|
||||
fn default_tags_header() -> String {
|
||||
"Generated Tags".to_string()
|
||||
}
|
||||
|
||||
fn default_links_space_char() -> String {
|
||||
" ".to_string()
|
||||
}
|
||||
@@ -348,6 +384,12 @@ fn wiki_defaults() -> WikiDefaults {
|
||||
list_margin: -1,
|
||||
links_space_char: default_links_space_char(),
|
||||
auto_toc: false,
|
||||
toc_header: default_toc_header(),
|
||||
toc_header_level: 1,
|
||||
links_header: default_links_header(),
|
||||
links_header_level: 1,
|
||||
tags_header: default_tags_header(),
|
||||
tags_header_level: 1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,6 +409,12 @@ struct WikiDefaults {
|
||||
list_margin: i32,
|
||||
links_space_char: String,
|
||||
auto_toc: bool,
|
||||
toc_header: String,
|
||||
toc_header_level: u8,
|
||||
links_header: String,
|
||||
links_header_level: u8,
|
||||
tags_header: String,
|
||||
tags_header_level: u8,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -593,6 +641,18 @@ struct RawWiki {
|
||||
links_space_char: Option<String>,
|
||||
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
||||
auto_toc: Option<bool>,
|
||||
#[serde(default)]
|
||||
toc_header: Option<String>,
|
||||
#[serde(default)]
|
||||
toc_header_level: Option<u8>,
|
||||
#[serde(default)]
|
||||
links_header: Option<String>,
|
||||
#[serde(default)]
|
||||
links_header_level: Option<u8>,
|
||||
#[serde(default)]
|
||||
tags_header: Option<String>,
|
||||
#[serde(default)]
|
||||
tags_header_level: Option<u8>,
|
||||
}
|
||||
|
||||
impl From<RawWiki> for WikiConfig {
|
||||
@@ -654,6 +714,12 @@ impl From<RawWiki> for WikiConfig {
|
||||
list_margin: r.list_margin.unwrap_or(d.list_margin),
|
||||
links_space_char: r.links_space_char.unwrap_or(d.links_space_char),
|
||||
auto_toc: r.auto_toc.unwrap_or(d.auto_toc),
|
||||
toc_header: r.toc_header.unwrap_or(d.toc_header),
|
||||
toc_header_level: r.toc_header_level.unwrap_or(d.toc_header_level),
|
||||
links_header: r.links_header.unwrap_or(d.links_header),
|
||||
links_header_level: r.links_header_level.unwrap_or(d.links_header_level),
|
||||
tags_header: r.tags_header.unwrap_or(d.tags_header),
|
||||
tags_header_level: r.tags_header_level.unwrap_or(d.tags_header_level),
|
||||
html,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,6 +625,8 @@ impl LanguageServer for Backend {
|
||||
&doc.ast,
|
||||
&uri,
|
||||
self.use_utf8.load(Ordering::Relaxed),
|
||||
&wiki.config.toc_header,
|
||||
wiki.config.toc_header_level,
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user