feat(lsp): wire diary/auto_toc/links_space_char/list_margin; drop client-side keys
Consume several per-wiki config keys that the server deserialized but
ignored, and remove keys whose effect is purely client-side:
- diagnostics: re-publish open-doc diagnostics on
didChangeConfiguration so a link_severity change takes effect
immediately; fix the single-key unwrap in apply_change so a minimal
`{ diagnostic: {...} }` payload isn't mistaken for a namespace wrapper.
- auto_toc: rebuild an existing TOC section on save (new
ops::toc_rebuild_edit, no-op when the page has no TOC).
- diary index: honour diary_header, diary_sort and diary_caption_level
when rendering the diary index body.
- links_space_char: apply on rename so spaces in the link target and
the on-disk path become the configured glyph (default " " = verbatim).
- list_margin: thread the per-wiki value into render_page_html.
- remove nested_syntaxes, maxhi and diary_start_week_day from the server
config: nested-syntax and heading highlighting are client-side, and
the weekly diary is ISO-week based so a custom week start has no clean
server-side meaning.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1936,6 +1936,20 @@ pub mod ops {
|
|||||||
Some(b.build())
|
Some(b.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Like [`toc_edit`], but only when the page *already* has a
|
||||||
|
/// `= Contents =` section — used by the `auto_toc`-on-save hook so we
|
||||||
|
/// rebuild an existing TOC without inserting one into pages that never
|
||||||
|
/// had one.
|
||||||
|
pub fn toc_rebuild_edit(
|
||||||
|
text: &str,
|
||||||
|
ast: &DocumentNode,
|
||||||
|
uri: &Url,
|
||||||
|
utf8: bool,
|
||||||
|
) -> Option<WorkspaceEdit> {
|
||||||
|
find_section_range(ast, TOC_HEADING)?;
|
||||||
|
toc_edit(text, ast, uri, utf8)
|
||||||
|
}
|
||||||
|
|
||||||
/// Produce a `WorkspaceEdit` that replaces or inserts the
|
/// Produce a `WorkspaceEdit` that replaces or inserts the
|
||||||
/// auto-generated links section for the given document.
|
/// auto-generated links section for the given document.
|
||||||
pub fn links_edit(
|
pub fn links_edit(
|
||||||
@@ -3232,6 +3246,7 @@ pub mod export_ops {
|
|||||||
DiaryDate::today_utc(),
|
DiaryDate::today_utc(),
|
||||||
&cfg.html,
|
&cfg.html,
|
||||||
Some(cfg.file_extension.as_str()),
|
Some(cfg.file_extension.as_str()),
|
||||||
|
cfg.list_margin,
|
||||||
std::collections::HashMap::new(),
|
std::collections::HashMap::new(),
|
||||||
)?;
|
)?;
|
||||||
let out_path = output_path_for(&cfg.html, name);
|
let out_path = output_path_for(&cfg.html, name);
|
||||||
|
|||||||
@@ -41,9 +41,6 @@ pub struct WikiConfig {
|
|||||||
/// diary commands use. `daily` is supported; the others fall through
|
/// diary commands use. `daily` is supported; the others fall through
|
||||||
/// to vimwiki-style names but the commands are no-ops for them.
|
/// to vimwiki-style names but the commands are no-ops for them.
|
||||||
pub diary_frequency: String,
|
pub diary_frequency: String,
|
||||||
/// First day of the week (`monday`..`sunday`). Used by weekly
|
|
||||||
/// diary entry naming.
|
|
||||||
pub diary_start_week_day: String,
|
|
||||||
/// Caption level for the diary index page headings (1 = h1).
|
/// Caption level for the diary index page headings (1 = h1).
|
||||||
pub diary_caption_level: u8,
|
pub diary_caption_level: u8,
|
||||||
/// Newest-first (`desc`) or oldest-first (`asc`) in the diary
|
/// Newest-first (`desc`) or oldest-first (`asc`) in the diary
|
||||||
@@ -51,10 +48,6 @@ pub struct WikiConfig {
|
|||||||
pub diary_sort: String,
|
pub diary_sort: String,
|
||||||
/// H1 text for the diary index page.
|
/// H1 text for the diary index page.
|
||||||
pub diary_header: String,
|
pub diary_header: String,
|
||||||
/// Maximum heading level treated as a *navigation* anchor — beyond
|
|
||||||
/// it the LSP doesn't synthesise an anchor symbol. Matches
|
|
||||||
/// vimwiki's `g:vimwiki_maxhi` (default `1`).
|
|
||||||
pub maxhi: u8,
|
|
||||||
/// Vimwiki's `g:vimwiki_listsyms`. The 5-character string maps
|
/// Vimwiki's `g:vimwiki_listsyms`. The 5-character string maps
|
||||||
/// fractional progress states for checkboxes; the i-th char (0..=4)
|
/// fractional progress states for checkboxes; the i-th char (0..=4)
|
||||||
/// is the rendered marker for state `i / 4`. Default: `" .oOX"`.
|
/// is the rendered marker for state `i / 4`. Default: `" .oOX"`.
|
||||||
@@ -68,9 +61,6 @@ pub struct WikiConfig {
|
|||||||
/// targets when writing to disk (a single space `" "` by default,
|
/// targets when writing to disk (a single space `" "` by default,
|
||||||
/// i.e. spaces are kept verbatim).
|
/// i.e. spaces are kept verbatim).
|
||||||
pub links_space_char: String,
|
pub links_space_char: String,
|
||||||
/// `nested_syntaxes` — code-block language → editor filetype map
|
|
||||||
/// used for syntax-highlighting fenced blocks. Empty by default.
|
|
||||||
pub nested_syntaxes: std::collections::HashMap<String, String>,
|
|
||||||
/// Rebuild the page's TOC on save when set.
|
/// Rebuild the page's TOC on save when set.
|
||||||
pub auto_toc: bool,
|
pub auto_toc: bool,
|
||||||
/// HTML export options.
|
/// HTML export options.
|
||||||
@@ -183,16 +173,13 @@ impl WikiConfig {
|
|||||||
diary_rel_path: d.diary_rel_path,
|
diary_rel_path: d.diary_rel_path,
|
||||||
diary_index: d.diary_index,
|
diary_index: d.diary_index,
|
||||||
diary_frequency: d.diary_frequency,
|
diary_frequency: d.diary_frequency,
|
||||||
diary_start_week_day: d.diary_start_week_day,
|
|
||||||
diary_caption_level: d.diary_caption_level,
|
diary_caption_level: d.diary_caption_level,
|
||||||
diary_sort: d.diary_sort,
|
diary_sort: d.diary_sort,
|
||||||
diary_header: d.diary_header,
|
diary_header: d.diary_header,
|
||||||
maxhi: d.maxhi,
|
|
||||||
listsyms: d.listsyms,
|
listsyms: d.listsyms,
|
||||||
listsyms_propagate: d.listsyms_propagate,
|
listsyms_propagate: d.listsyms_propagate,
|
||||||
list_margin: d.list_margin,
|
list_margin: d.list_margin,
|
||||||
links_space_char: d.links_space_char,
|
links_space_char: d.links_space_char,
|
||||||
nested_syntaxes: d.nested_syntaxes,
|
|
||||||
auto_toc: d.auto_toc,
|
auto_toc: d.auto_toc,
|
||||||
html: HtmlConfig::default(),
|
html: HtmlConfig::default(),
|
||||||
}
|
}
|
||||||
@@ -213,16 +200,13 @@ impl WikiConfig {
|
|||||||
diary_rel_path: d.diary_rel_path,
|
diary_rel_path: d.diary_rel_path,
|
||||||
diary_index: d.diary_index,
|
diary_index: d.diary_index,
|
||||||
diary_frequency: d.diary_frequency,
|
diary_frequency: d.diary_frequency,
|
||||||
diary_start_week_day: d.diary_start_week_day,
|
|
||||||
diary_caption_level: d.diary_caption_level,
|
diary_caption_level: d.diary_caption_level,
|
||||||
diary_sort: d.diary_sort,
|
diary_sort: d.diary_sort,
|
||||||
diary_header: d.diary_header,
|
diary_header: d.diary_header,
|
||||||
maxhi: d.maxhi,
|
|
||||||
listsyms: d.listsyms,
|
listsyms: d.listsyms,
|
||||||
listsyms_propagate: d.listsyms_propagate,
|
listsyms_propagate: d.listsyms_propagate,
|
||||||
list_margin: d.list_margin,
|
list_margin: d.list_margin,
|
||||||
links_space_char: d.links_space_char,
|
links_space_char: d.links_space_char,
|
||||||
nested_syntaxes: d.nested_syntaxes,
|
|
||||||
auto_toc: d.auto_toc,
|
auto_toc: d.auto_toc,
|
||||||
html,
|
html,
|
||||||
}
|
}
|
||||||
@@ -278,10 +262,6 @@ fn default_diary_frequency() -> String {
|
|||||||
"daily".to_string()
|
"daily".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_diary_start_week_day() -> String {
|
|
||||||
"monday".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_diary_sort() -> String {
|
fn default_diary_sort() -> String {
|
||||||
"desc".to_string()
|
"desc".to_string()
|
||||||
}
|
}
|
||||||
@@ -307,16 +287,13 @@ fn wiki_defaults() -> WikiDefaults {
|
|||||||
diary_rel_path: default_diary_rel_path(),
|
diary_rel_path: default_diary_rel_path(),
|
||||||
diary_index: default_diary_index(),
|
diary_index: default_diary_index(),
|
||||||
diary_frequency: default_diary_frequency(),
|
diary_frequency: default_diary_frequency(),
|
||||||
diary_start_week_day: default_diary_start_week_day(),
|
|
||||||
diary_caption_level: 1,
|
diary_caption_level: 1,
|
||||||
diary_sort: default_diary_sort(),
|
diary_sort: default_diary_sort(),
|
||||||
diary_header: default_diary_header(),
|
diary_header: default_diary_header(),
|
||||||
maxhi: 1,
|
|
||||||
listsyms: default_listsyms(),
|
listsyms: default_listsyms(),
|
||||||
listsyms_propagate: true,
|
listsyms_propagate: true,
|
||||||
list_margin: -1,
|
list_margin: -1,
|
||||||
links_space_char: default_links_space_char(),
|
links_space_char: default_links_space_char(),
|
||||||
nested_syntaxes: std::collections::HashMap::new(),
|
|
||||||
auto_toc: false,
|
auto_toc: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,16 +303,13 @@ struct WikiDefaults {
|
|||||||
diary_rel_path: String,
|
diary_rel_path: String,
|
||||||
diary_index: String,
|
diary_index: String,
|
||||||
diary_frequency: String,
|
diary_frequency: String,
|
||||||
diary_start_week_day: String,
|
|
||||||
diary_caption_level: u8,
|
diary_caption_level: u8,
|
||||||
diary_sort: String,
|
diary_sort: String,
|
||||||
diary_header: String,
|
diary_header: String,
|
||||||
maxhi: u8,
|
|
||||||
listsyms: String,
|
listsyms: String,
|
||||||
listsyms_propagate: bool,
|
listsyms_propagate: bool,
|
||||||
list_margin: i32,
|
list_margin: i32,
|
||||||
links_space_char: String,
|
links_space_char: String,
|
||||||
nested_syntaxes: std::collections::HashMap<String, String>,
|
|
||||||
auto_toc: bool,
|
auto_toc: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,16 +363,15 @@ impl Config {
|
|||||||
/// editors that namespace settings by server name continue to work.
|
/// editors that namespace settings by server name continue to work.
|
||||||
pub fn apply_change(&mut self, value: &serde_json::Value) {
|
pub fn apply_change(&mut self, value: &serde_json::Value) {
|
||||||
// Unwrap { "nuwiki": { ... } } → { ... } so both clients work.
|
// Unwrap { "nuwiki": { ... } } → { ... } so both clients work.
|
||||||
|
// Only unwrap when the lone key isn't itself a recognised top-level
|
||||||
|
// setting — otherwise a minimal payload like
|
||||||
|
// `{ "diagnostic": { ... } }` would be mistaken for a namespace
|
||||||
|
// wrapper and its contents silently dropped.
|
||||||
let inner = value
|
let inner = value
|
||||||
.as_object()
|
.as_object()
|
||||||
.and_then(|m| {
|
.and_then(|m| if m.len() == 1 { m.iter().next() } else { None })
|
||||||
if m.len() == 1 {
|
.filter(|(k, v)| v.is_object() && !is_init_option_key(k))
|
||||||
m.values().next()
|
.map(|(_, v)| v)
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(|v| v.is_object())
|
|
||||||
.unwrap_or(value);
|
.unwrap_or(value);
|
||||||
|
|
||||||
let raw: InitOptions = match serde_json::from_value(inner.clone()) {
|
let raw: InitOptions = match serde_json::from_value(inner.clone()) {
|
||||||
@@ -460,6 +433,17 @@ mod opt_bool_or_int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Recognised top-level keys in the flat `initializationOptions` shape.
|
||||||
|
/// Used to tell a real single-key payload (e.g. `{ "diagnostic": {…} }`)
|
||||||
|
/// apart from an editor namespace wrapper (e.g. `{ "nuwiki": {…} }`) in
|
||||||
|
/// [`Config::apply_change`].
|
||||||
|
fn is_init_option_key(key: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
key,
|
||||||
|
"wikis" | "wiki_root" | "file_extension" | "syntax" | "log_level" | "diagnostic"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
#[serde(default, rename_all = "snake_case")]
|
#[serde(default, rename_all = "snake_case")]
|
||||||
struct InitOptions {
|
struct InitOptions {
|
||||||
@@ -532,16 +516,12 @@ struct RawWiki {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
diary_frequency: Option<String>,
|
diary_frequency: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
diary_start_week_day: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
diary_caption_level: Option<u8>,
|
diary_caption_level: Option<u8>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
diary_sort: Option<String>,
|
diary_sort: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
diary_header: Option<String>,
|
diary_header: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
maxhi: Option<u8>,
|
|
||||||
#[serde(default)]
|
|
||||||
listsyms: Option<String>,
|
listsyms: Option<String>,
|
||||||
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
||||||
listsyms_propagate: Option<bool>,
|
listsyms_propagate: Option<bool>,
|
||||||
@@ -549,8 +529,6 @@ struct RawWiki {
|
|||||||
list_margin: Option<i32>,
|
list_margin: Option<i32>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
links_space_char: Option<String>,
|
links_space_char: Option<String>,
|
||||||
#[serde(default)]
|
|
||||||
nested_syntaxes: Option<std::collections::HashMap<String, String>>,
|
|
||||||
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
||||||
auto_toc: Option<bool>,
|
auto_toc: Option<bool>,
|
||||||
}
|
}
|
||||||
@@ -603,16 +581,13 @@ impl From<RawWiki> for WikiConfig {
|
|||||||
diary_rel_path: r.diary_rel_path.unwrap_or(d.diary_rel_path),
|
diary_rel_path: r.diary_rel_path.unwrap_or(d.diary_rel_path),
|
||||||
diary_index: r.diary_index.unwrap_or(d.diary_index),
|
diary_index: r.diary_index.unwrap_or(d.diary_index),
|
||||||
diary_frequency: r.diary_frequency.unwrap_or(d.diary_frequency),
|
diary_frequency: r.diary_frequency.unwrap_or(d.diary_frequency),
|
||||||
diary_start_week_day: r.diary_start_week_day.unwrap_or(d.diary_start_week_day),
|
|
||||||
diary_caption_level: r.diary_caption_level.unwrap_or(d.diary_caption_level),
|
diary_caption_level: r.diary_caption_level.unwrap_or(d.diary_caption_level),
|
||||||
diary_sort: r.diary_sort.unwrap_or(d.diary_sort),
|
diary_sort: r.diary_sort.unwrap_or(d.diary_sort),
|
||||||
diary_header: r.diary_header.unwrap_or(d.diary_header),
|
diary_header: r.diary_header.unwrap_or(d.diary_header),
|
||||||
maxhi: r.maxhi.unwrap_or(d.maxhi),
|
|
||||||
listsyms: r.listsyms.unwrap_or(d.listsyms),
|
listsyms: r.listsyms.unwrap_or(d.listsyms),
|
||||||
listsyms_propagate: r.listsyms_propagate.unwrap_or(d.listsyms_propagate),
|
listsyms_propagate: r.listsyms_propagate.unwrap_or(d.listsyms_propagate),
|
||||||
list_margin: r.list_margin.unwrap_or(d.list_margin),
|
list_margin: r.list_margin.unwrap_or(d.list_margin),
|
||||||
links_space_char: r.links_space_char.unwrap_or(d.links_space_char),
|
links_space_char: r.links_space_char.unwrap_or(d.links_space_char),
|
||||||
nested_syntaxes: r.nested_syntaxes.unwrap_or(d.nested_syntaxes),
|
|
||||||
auto_toc: r.auto_toc.unwrap_or(d.auto_toc),
|
auto_toc: r.auto_toc.unwrap_or(d.auto_toc),
|
||||||
html,
|
html,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,37 +168,78 @@ fn period_first_day(p: &DiaryPeriod) -> DiaryDate {
|
|||||||
p.first_day()
|
p.first_day()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate the body of the diary index page — a flat newest-first list
|
/// Ordering of entries in the diary index page.
|
||||||
/// of `[[diary/YYYY-MM-DD]]` wikilinks, grouped under year and month
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum DiarySort {
|
||||||
|
/// Newest first (vimwiki default).
|
||||||
|
Desc,
|
||||||
|
/// Oldest first.
|
||||||
|
Asc,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiarySort {
|
||||||
|
/// Parse a wiki's `diary_sort` field. Anything other than `asc`
|
||||||
|
/// (case-insensitive) is treated as the default `desc`.
|
||||||
|
pub fn parse(s: &str) -> Self {
|
||||||
|
if s.trim().eq_ignore_ascii_case("asc") {
|
||||||
|
Self::Asc
|
||||||
|
} else {
|
||||||
|
Self::Desc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a heading line at `level` (1 = `= … =`). Clamped to vimwiki's
|
||||||
|
/// 1..=6 range so a stray config value can't emit a 200-equals heading.
|
||||||
|
fn heading(level: u8, text: &str) -> String {
|
||||||
|
let n = level.clamp(1, 6) as usize;
|
||||||
|
let eq = "=".repeat(n);
|
||||||
|
format!("{eq} {text} {eq}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate the body of the diary index page — a flat list of
|
||||||
|
/// `[[diary/YYYY-MM-DD]]` wikilinks, grouped under year and month
|
||||||
/// subheadings so the rendered page mirrors `:VimwikiDiaryGenerateLinks`.
|
/// subheadings so the rendered page mirrors `:VimwikiDiaryGenerateLinks`.
|
||||||
|
///
|
||||||
|
/// `caption_level` sets the level of the top caption; the year and month
|
||||||
|
/// subheadings nest one and two levels below it. `sort` controls whether
|
||||||
|
/// the flat list runs newest- or oldest-first.
|
||||||
pub fn build_index_body(
|
pub fn build_index_body(
|
||||||
entries: &[DiaryEntry],
|
entries: &[DiaryEntry],
|
||||||
diary_rel_path: &str,
|
diary_rel_path: &str,
|
||||||
index_heading: &str,
|
index_heading: &str,
|
||||||
|
sort: DiarySort,
|
||||||
|
caption_level: u8,
|
||||||
) -> String {
|
) -> String {
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
out.push_str("= ");
|
out.push_str(&heading(caption_level, index_heading));
|
||||||
out.push_str(index_heading);
|
out.push('\n');
|
||||||
out.push_str(" =\n");
|
|
||||||
|
|
||||||
if entries.is_empty() {
|
if entries.is_empty() {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort descending by date — newest first matches vimwiki.
|
|
||||||
let mut sorted: Vec<&DiaryEntry> = entries.iter().collect();
|
let mut sorted: Vec<&DiaryEntry> = entries.iter().collect();
|
||||||
sorted.sort_by(|a, b| b.date.cmp(&a.date));
|
sorted.sort_by(|a, b| match sort {
|
||||||
|
DiarySort::Desc => b.date.cmp(&a.date),
|
||||||
|
DiarySort::Asc => a.date.cmp(&b.date),
|
||||||
|
});
|
||||||
|
|
||||||
|
let year_level = caption_level.saturating_add(1);
|
||||||
|
let month_level = caption_level.saturating_add(2);
|
||||||
let mut current_year: Option<i32> = None;
|
let mut current_year: Option<i32> = None;
|
||||||
let mut current_month: Option<u8> = None;
|
let mut current_month: Option<u8> = None;
|
||||||
for e in sorted {
|
for e in sorted {
|
||||||
if current_year != Some(e.date.year) {
|
if current_year != Some(e.date.year) {
|
||||||
out.push_str(&format!("\n== {} ==\n", e.date.year));
|
out.push('\n');
|
||||||
|
out.push_str(&heading(year_level, &e.date.year.to_string()));
|
||||||
|
out.push('\n');
|
||||||
current_year = Some(e.date.year);
|
current_year = Some(e.date.year);
|
||||||
current_month = None;
|
current_month = None;
|
||||||
}
|
}
|
||||||
if current_month != Some(e.date.month) {
|
if current_month != Some(e.date.month) {
|
||||||
out.push_str(&format!("=== {} ===\n", month_name(e.date.month)));
|
out.push_str(&heading(month_level, month_name(e.date.month)));
|
||||||
|
out.push('\n');
|
||||||
current_month = Some(e.date.month);
|
current_month = Some(e.date.month);
|
||||||
}
|
}
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
@@ -211,9 +252,16 @@ pub fn build_index_body(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Render the diary-index body for the given wiki's currently-indexed
|
/// Render the diary-index body for the given wiki's currently-indexed
|
||||||
/// entries. Convenience wrapper around [`build_index_body`].
|
/// entries. Convenience wrapper around [`build_index_body`] that honours
|
||||||
|
/// the wiki's `diary_header`, `diary_sort`, and `diary_caption_level`.
|
||||||
pub fn render_index_body(cfg: &WikiConfig, index: &WorkspaceIndex) -> String {
|
pub fn render_index_body(cfg: &WikiConfig, index: &WorkspaceIndex) -> String {
|
||||||
build_index_body(&list_entries(index), &cfg.diary_rel_path, "Diary")
|
build_index_body(
|
||||||
|
&list_entries(index),
|
||||||
|
&cfg.diary_rel_path,
|
||||||
|
&cfg.diary_header,
|
||||||
|
DiarySort::parse(&cfg.diary_sort),
|
||||||
|
cfg.diary_caption_level,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cheap "is this path under the diary subdir of this wiki" check —
|
/// Cheap "is this path under the diary subdir of this wiki" check —
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ pub fn render_page_html(
|
|||||||
today: DiaryDate,
|
today: DiaryDate,
|
||||||
cfg: &HtmlConfig,
|
cfg: &HtmlConfig,
|
||||||
wiki_extension: Option<&str>,
|
wiki_extension: Option<&str>,
|
||||||
|
list_margin: i32,
|
||||||
extra_vars: HashMap<String, String>,
|
extra_vars: HashMap<String, String>,
|
||||||
) -> std::io::Result<String> {
|
) -> std::io::Result<String> {
|
||||||
let date_str = match doc.metadata.date.as_deref() {
|
let date_str = match doc.metadata.date.as_deref() {
|
||||||
@@ -224,7 +225,7 @@ pub fn render_page_html(
|
|||||||
vars.insert(k, v);
|
vars.insert(k, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut r = HtmlRenderer::new();
|
let mut r = HtmlRenderer::new().with_list_margin(list_margin);
|
||||||
if let Some(ext) = wiki_extension.filter(|e| !e.trim_start_matches('.').is_empty()) {
|
if let Some(ext) = wiki_extension.filter(|e| !e.trim_start_matches('.').is_empty()) {
|
||||||
// Strip the wiki extension from page links before the default resolver
|
// Strip the wiki extension from page links before the default resolver
|
||||||
// turns them into `.html` URLs, so `[[todo.wiki]]` → `todo.html` rather
|
// turns them into `.html` URLs, so `[[todo.wiki]]` → `todo.html` rather
|
||||||
|
|||||||
@@ -504,10 +504,35 @@ impl LanguageServer for Backend {
|
|||||||
// is out of scope here; for now we just accept the new
|
// is out of scope here; for now we just accept the new
|
||||||
// values so later commands see the right diagnostic settings etc.
|
// values so later commands see the right diagnostic settings etc.
|
||||||
// Multi-wiki support revisits this with proper rebuild semantics.
|
// Multi-wiki support revisits this with proper rebuild semantics.
|
||||||
|
{
|
||||||
let mut cfg = self.config.write().expect("config lock poisoned");
|
let mut cfg = self.config.write().expect("config lock poisoned");
|
||||||
cfg.apply_change(¶ms.settings);
|
cfg.apply_change(¶ms.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A severity change (e.g. diagnostic.link_severity) only affects
|
||||||
|
// already-published diagnostics once we re-run link-health checks for
|
||||||
|
// open docs — otherwise the editor keeps showing the old severity until
|
||||||
|
// the next edit. Refresh every wiki's open docs against the new config.
|
||||||
|
let wiki_ids: Vec<crate::wiki::WikiId> = self
|
||||||
|
.wikis
|
||||||
|
.read()
|
||||||
|
.expect("wikis lock poisoned")
|
||||||
|
.iter()
|
||||||
|
.map(|w| w.id)
|
||||||
|
.collect();
|
||||||
|
for wiki_id in wiki_ids {
|
||||||
|
refresh_open_diagnostics_for_wiki(
|
||||||
|
wiki_id,
|
||||||
|
Arc::clone(&self.documents),
|
||||||
|
Arc::clone(&self.wikis),
|
||||||
|
Arc::clone(&self.config),
|
||||||
|
self.client.clone(),
|
||||||
|
Arc::clone(&self.use_utf8),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn shutdown(&self) -> LspResult<()> {
|
async fn shutdown(&self) -> LspResult<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -575,15 +600,36 @@ impl LanguageServer for Backend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn did_save(&self, params: DidSaveTextDocumentParams) {
|
async fn did_save(&self, params: DidSaveTextDocumentParams) {
|
||||||
// When `auto_export` is set on the resolved wiki, run
|
|
||||||
// the equivalent of `nuwiki.export.currentToHtml` after the file
|
|
||||||
// hits disk. Best-effort — failures are logged but do not bubble
|
|
||||||
// to the client. Skips pages with the `%nohtml` directive.
|
|
||||||
let uri = params.text_document.uri.clone();
|
let uri = params.text_document.uri.clone();
|
||||||
let wiki = match self.wiki_for_uri(&uri) {
|
let wiki = match self.wiki_for_uri(&uri) {
|
||||||
Some(w) => w,
|
Some(w) => w,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// When `auto_toc` is set, rebuild an existing `= Contents =` section
|
||||||
|
// on save and push the change back to the buffer via
|
||||||
|
// `workspace/applyEdit`. Only rebuilds a TOC that's already there —
|
||||||
|
// it never inserts one into a page that didn't have it.
|
||||||
|
if wiki.config.auto_toc {
|
||||||
|
let edit = {
|
||||||
|
self.documents.get(&uri).and_then(|doc| {
|
||||||
|
commands::ops::toc_rebuild_edit(
|
||||||
|
&doc.text,
|
||||||
|
&doc.ast,
|
||||||
|
&uri,
|
||||||
|
self.use_utf8.load(Ordering::Relaxed),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if let Some(edit) = edit {
|
||||||
|
let _ = self.client.apply_edit(edit).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When `auto_export` is set on the resolved wiki, run
|
||||||
|
// the equivalent of `nuwiki.export.currentToHtml` after the file
|
||||||
|
// hits disk. Best-effort — failures are logged but do not bubble
|
||||||
|
// to the client. Skips pages with the `%nohtml` directive.
|
||||||
if !wiki.config.html.auto_export {
|
if !wiki.config.html.auto_export {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ pub(crate) async fn compute_rename(
|
|||||||
) -> Option<WorkspaceEdit> {
|
) -> Option<WorkspaceEdit> {
|
||||||
let wiki = backend.wiki_for_uri(&target_uri)?;
|
let wiki = backend.wiki_for_uri(&target_uri)?;
|
||||||
let old_name = page_name_from_uri(&target_uri, Some(&wiki.config.root));
|
let old_name = page_name_from_uri(&target_uri, Some(&wiki.config.root));
|
||||||
|
// Spaces in the link *target* (and the file on disk) are replaced with
|
||||||
|
// the wiki's `links_space_char`; descriptions keep their original text.
|
||||||
|
let new_name = apply_links_space_char(&new_name, &wiki.config.links_space_char);
|
||||||
let new_uri = build_new_uri(&wiki.config.root, &new_name, &wiki.config.file_extension)?;
|
let new_uri = build_new_uri(&wiki.config.root, &new_name, &wiki.config.file_extension)?;
|
||||||
if new_uri == target_uri {
|
if new_uri == target_uri {
|
||||||
return None;
|
return None;
|
||||||
@@ -73,6 +76,16 @@ pub(crate) async fn compute_rename(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace spaces in a link path with the configured `links_space_char`.
|
||||||
|
/// The default `" "` is an identity transform (spaces kept verbatim).
|
||||||
|
pub fn apply_links_space_char(name: &str, space_char: &str) -> String {
|
||||||
|
if space_char == " " {
|
||||||
|
name.to_string()
|
||||||
|
} else {
|
||||||
|
name.replace(' ', space_char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Build `<root>/<new_name>(<ext>)` as a `file://` URL.
|
/// Build `<root>/<new_name>(<ext>)` as a `file://` URL.
|
||||||
pub fn build_new_uri(root: &Path, new_name: &str, ext: &str) -> Option<Url> {
|
pub fn build_new_uri(root: &Path, new_name: &str, ext: &str) -> Option<Url> {
|
||||||
let mut path = root.to_path_buf();
|
let mut path = root.to_path_buf();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use nuwiki_lsp::commands::{export_ops, ops, COMMANDS};
|
|||||||
use nuwiki_lsp::config::WikiConfig;
|
use nuwiki_lsp::config::WikiConfig;
|
||||||
use nuwiki_lsp::index::WorkspaceIndex;
|
use nuwiki_lsp::index::WorkspaceIndex;
|
||||||
use nuwiki_lsp::nav::find_inline_at;
|
use nuwiki_lsp::nav::find_inline_at;
|
||||||
use nuwiki_lsp::rename::{build_new_uri, rewrite_wikilink_target};
|
use nuwiki_lsp::rename::{apply_links_space_char, build_new_uri, rewrite_wikilink_target};
|
||||||
use nuwiki_lsp::wiki::{build_wikis, resolve_uri_to_wiki};
|
use nuwiki_lsp::wiki::{build_wikis, resolve_uri_to_wiki};
|
||||||
use nuwiki_lsp::{diary, export};
|
use nuwiki_lsp::{diary, export};
|
||||||
use tower_lsp::lsp_types::Url;
|
use tower_lsp::lsp_types::Url;
|
||||||
@@ -177,6 +177,30 @@ fn nuwiki_rename_file_builds_uri_and_rewrites_links() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// links_space_char rewrites spaces in the link target / on-disk path while
|
||||||
|
// the default of a single space keeps the name verbatim.
|
||||||
|
#[test]
|
||||||
|
fn links_space_char_replaces_spaces_in_target_path() {
|
||||||
|
// Default " " — identity, spaces preserved.
|
||||||
|
assert_eq!(apply_links_space_char("My Page", " "), "My Page");
|
||||||
|
// Custom char — spaces become the configured glyph.
|
||||||
|
assert_eq!(apply_links_space_char("My Page", "_"), "My_Page");
|
||||||
|
// Subdir separators are untouched; only spaces within segments change.
|
||||||
|
assert_eq!(
|
||||||
|
apply_links_space_char("sub dir/My Page", "_"),
|
||||||
|
"sub_dir/My_Page"
|
||||||
|
);
|
||||||
|
|
||||||
|
// The transformed name flows into both the new URI and the rewritten link.
|
||||||
|
let renamed = apply_links_space_char("New Page", "-");
|
||||||
|
let new_uri = build_new_uri(&PathBuf::from("/wiki"), &renamed, ".wiki").unwrap();
|
||||||
|
assert!(new_uri.as_str().ends_with("/wiki/New-Page.wiki"));
|
||||||
|
assert_eq!(
|
||||||
|
rewrite_wikilink_target("[[Old Name|caption]]", &renamed),
|
||||||
|
Some("[[New-Page|caption]]".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// :NuwikiDeleteFile — delete the current page and its on-disk file.
|
// :NuwikiDeleteFile — delete the current page and its on-disk file.
|
||||||
#[test]
|
#[test]
|
||||||
fn nuwiki_delete_file_emits_delete_workspace_edit() {
|
fn nuwiki_delete_file_emits_delete_workspace_edit() {
|
||||||
@@ -238,7 +262,7 @@ fn nuwiki_diary_index_resolves_index_uri() {
|
|||||||
fn nuwiki_diary_generate_links_builds_grouped_body() {
|
fn nuwiki_diary_generate_links_builds_grouped_body() {
|
||||||
let idx = diary_index("/wiki", &["2026-05-10", "2026-05-12", "2026-04-30"]);
|
let idx = diary_index("/wiki", &["2026-05-10", "2026-05-12", "2026-04-30"]);
|
||||||
let entries = diary::list_entries(&idx);
|
let entries = diary::list_entries(&idx);
|
||||||
let body = diary::build_index_body(&entries, "diary", "Diary");
|
let body = diary::build_index_body(&entries, "diary", "Diary", diary::DiarySort::Desc, 1);
|
||||||
assert!(body.starts_with("= Diary ="));
|
assert!(body.starts_with("= Diary ="));
|
||||||
assert!(body.contains("2026-05-12"));
|
assert!(body.contains("2026-05-12"));
|
||||||
assert!(body.contains("2026-05-10"));
|
assert!(body.contains("2026-05-10"));
|
||||||
@@ -407,6 +431,7 @@ fn nuwiki_2html_renders_page() {
|
|||||||
DiaryDate::from_ymd(2026, 5, 12).unwrap(),
|
DiaryDate::from_ymd(2026, 5, 12).unwrap(),
|
||||||
&cfg.html,
|
&cfg.html,
|
||||||
Some(".wiki"),
|
Some(".wiki"),
|
||||||
|
-1,
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -428,6 +453,7 @@ fn nuwiki_2html_browse_shares_render_and_is_advertised() {
|
|||||||
DiaryDate::today_utc(),
|
DiaryDate::today_utc(),
|
||||||
&cfg.html,
|
&cfg.html,
|
||||||
Some(".wiki"),
|
Some(".wiki"),
|
||||||
|
-1,
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ fn build_index_body_is_newest_first_grouped_by_year_month() {
|
|||||||
&["2025-12-31", "2026-05-11", "2026-05-12", "2026-04-30"],
|
&["2025-12-31", "2026-05-11", "2026-05-12", "2026-04-30"],
|
||||||
);
|
);
|
||||||
let entries = diary::list_entries(&idx);
|
let entries = diary::list_entries(&idx);
|
||||||
let body = diary::build_index_body(&entries, "diary", "Diary");
|
let body = diary::build_index_body(&entries, "diary", "Diary", diary::DiarySort::Desc, 1);
|
||||||
let lines: Vec<&str> = body.lines().collect();
|
let lines: Vec<&str> = body.lines().collect();
|
||||||
assert_eq!(lines[0], "= Diary =");
|
assert_eq!(lines[0], "= Diary =");
|
||||||
// First date in output should be the latest: 2026-05-12
|
// First date in output should be the latest: 2026-05-12
|
||||||
@@ -292,10 +292,46 @@ fn build_index_body_is_newest_first_grouped_by_year_month() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_index_body_empty_when_no_entries() {
|
fn build_index_body_empty_when_no_entries() {
|
||||||
let body = diary::build_index_body(&[], "diary", "Diary");
|
let body = diary::build_index_body(&[], "diary", "Diary", diary::DiarySort::Desc, 1);
|
||||||
assert_eq!(body, "= Diary =\n");
|
assert_eq!(body, "= Diary =\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_index_body_ascending_sort_lists_oldest_first() {
|
||||||
|
let idx = build_index_with_entries("/tmp/diarySort", &["2026-05-11", "2026-05-12"]);
|
||||||
|
let entries = diary::list_entries(&idx);
|
||||||
|
let body = diary::build_index_body(&entries, "diary", "Diary", diary::DiarySort::Asc, 1);
|
||||||
|
let older = body.find("2026-05-11").unwrap();
|
||||||
|
let newer = body.find("2026-05-12").unwrap();
|
||||||
|
assert!(older < newer, "ascending sort lists oldest first");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_index_body_caption_level_shifts_all_headings() {
|
||||||
|
let idx = build_index_with_entries("/tmp/diaryCap", &["2026-05-11"]);
|
||||||
|
let entries = diary::list_entries(&idx);
|
||||||
|
let body = diary::build_index_body(&entries, "diary", "Diary", diary::DiarySort::Desc, 2);
|
||||||
|
assert!(body.contains("== Diary =="), "caption at level 2");
|
||||||
|
assert!(
|
||||||
|
body.contains("=== 2026 ==="),
|
||||||
|
"year nests one below caption"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
body.contains("==== May ===="),
|
||||||
|
"month nests two below caption"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_index_body_clamps_caption_level_to_six() {
|
||||||
|
let idx = build_index_with_entries("/tmp/diaryClamp", &["2026-05-11"]);
|
||||||
|
let entries = diary::list_entries(&idx);
|
||||||
|
// caption_level 6 → year/month would be 7/8 but clamp to 6.
|
||||||
|
let body = diary::build_index_body(&entries, "diary", "Diary", diary::DiarySort::Desc, 6);
|
||||||
|
assert!(body.contains("====== Diary ======"));
|
||||||
|
assert!(!body.contains("======="), "no heading exceeds level 6");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_index_body_round_trip() {
|
fn render_index_body_round_trip() {
|
||||||
let cfg = wiki_cfg("/tmp/diaryA");
|
let cfg = wiki_cfg("/tmp/diaryA");
|
||||||
@@ -305,6 +341,23 @@ fn render_index_body_round_trip() {
|
|||||||
assert!(body.contains("[[diary/2026-05-11]]"));
|
assert!(body.contains("[[diary/2026-05-11]]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_index_body_honours_custom_header_sort_and_caption() {
|
||||||
|
let mut cfg = wiki_cfg("/tmp/diaryCustom");
|
||||||
|
cfg.diary_header = "Journal".into();
|
||||||
|
cfg.diary_sort = "asc".into();
|
||||||
|
cfg.diary_caption_level = 2;
|
||||||
|
let idx = build_index_with_entries("/tmp/diaryCustom", &["2026-05-11", "2026-05-12"]);
|
||||||
|
let body = diary::render_index_body(&cfg, &idx);
|
||||||
|
assert!(
|
||||||
|
body.contains("== Journal =="),
|
||||||
|
"custom header at caption level 2"
|
||||||
|
);
|
||||||
|
let older = body.find("2026-05-11").unwrap();
|
||||||
|
let newer = body.find("2026-05-12").unwrap();
|
||||||
|
assert!(older < newer, "asc sort honoured");
|
||||||
|
}
|
||||||
|
|
||||||
// ===== is_in_diary =====
|
// ===== is_in_diary =====
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ fn render_page_html_substitutes_title_content_and_extras() {
|
|||||||
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||||
&c.html,
|
&c.html,
|
||||||
None,
|
None,
|
||||||
|
-1,
|
||||||
extras,
|
extras,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -262,6 +263,7 @@ fn render_page_html_uses_metadata_date_when_set() {
|
|||||||
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||||
&c.html,
|
&c.html,
|
||||||
None,
|
None,
|
||||||
|
-1,
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -279,6 +281,7 @@ fn render_page_html_root_path_for_nested_pages() {
|
|||||||
DiaryDate::today_utc(),
|
DiaryDate::today_utc(),
|
||||||
&c.html,
|
&c.html,
|
||||||
None,
|
None,
|
||||||
|
-1,
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -298,6 +301,7 @@ fn render_page_html_extra_var_overrides_default() {
|
|||||||
DiaryDate::today_utc(),
|
DiaryDate::today_utc(),
|
||||||
&c.html,
|
&c.html,
|
||||||
None,
|
None,
|
||||||
|
-1,
|
||||||
extras,
|
extras,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -320,6 +324,7 @@ fn render_page_html_substitutes_vimwiki_percent_template() {
|
|||||||
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||||||
&c.html,
|
&c.html,
|
||||||
None,
|
None,
|
||||||
|
-1,
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -346,6 +351,7 @@ fn render_page_html_strips_wiki_extension_from_links() {
|
|||||||
DiaryDate::today_utc(),
|
DiaryDate::today_utc(),
|
||||||
&c.html,
|
&c.html,
|
||||||
Some(".wiki"),
|
Some(".wiki"),
|
||||||
|
-1,
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -355,6 +361,49 @@ fn render_page_html_strips_wiki_extension_from_links() {
|
|||||||
assert!(html.contains("href=\"posts/index.html\""), "plain: {html}");
|
assert!(html.contains("href=\"posts/index.html\""), "plain: {html}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_page_html_list_margin_only_styles_top_level_list() {
|
||||||
|
// `list_margin >= 0` forces a `margin-left:<n>em` on the outermost
|
||||||
|
// list; nested lists and the default of `-1` stay unstyled.
|
||||||
|
let ast = parse("- a\n - b\n");
|
||||||
|
let c = cfg("/tmp/x");
|
||||||
|
|
||||||
|
let styled = export::render_page_html(
|
||||||
|
&ast,
|
||||||
|
Some("{{content}}".into()),
|
||||||
|
"index",
|
||||||
|
DiaryDate::today_utc(),
|
||||||
|
&c.html,
|
||||||
|
None,
|
||||||
|
2,
|
||||||
|
HashMap::new(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
styled.contains("<ul style=\"margin-left:2em\">"),
|
||||||
|
"top-level margin: {styled}"
|
||||||
|
);
|
||||||
|
// Exactly one styled list — the nested `<ul>` is plain.
|
||||||
|
assert_eq!(
|
||||||
|
styled.matches("margin-left").count(),
|
||||||
|
1,
|
||||||
|
"only top: {styled}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let plain = export::render_page_html(
|
||||||
|
&ast,
|
||||||
|
Some("{{content}}".into()),
|
||||||
|
"index",
|
||||||
|
DiaryDate::today_utc(),
|
||||||
|
&c.html,
|
||||||
|
None,
|
||||||
|
-1,
|
||||||
|
HashMap::new(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(!plain.contains("margin-left"), "default unstyled: {plain}");
|
||||||
|
}
|
||||||
|
|
||||||
// ===== fallback_template + DEFAULT_CSS =====
|
// ===== fallback_template + DEFAULT_CSS =====
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -267,16 +267,13 @@ fn defaults_carry_vimwiki_per_wiki_keys() {
|
|||||||
let cfg = WikiConfig::empty();
|
let cfg = WikiConfig::empty();
|
||||||
assert_eq!(cfg.index, "index");
|
assert_eq!(cfg.index, "index");
|
||||||
assert_eq!(cfg.diary_frequency, "daily");
|
assert_eq!(cfg.diary_frequency, "daily");
|
||||||
assert_eq!(cfg.diary_start_week_day, "monday");
|
|
||||||
assert_eq!(cfg.diary_caption_level, 1);
|
assert_eq!(cfg.diary_caption_level, 1);
|
||||||
assert_eq!(cfg.diary_sort, "desc");
|
assert_eq!(cfg.diary_sort, "desc");
|
||||||
assert_eq!(cfg.diary_header, "Diary");
|
assert_eq!(cfg.diary_header, "Diary");
|
||||||
assert_eq!(cfg.maxhi, 1);
|
|
||||||
assert_eq!(cfg.listsyms, " .oOX");
|
assert_eq!(cfg.listsyms, " .oOX");
|
||||||
assert!(cfg.listsyms_propagate);
|
assert!(cfg.listsyms_propagate);
|
||||||
assert_eq!(cfg.list_margin, -1);
|
assert_eq!(cfg.list_margin, -1);
|
||||||
assert_eq!(cfg.links_space_char, " ");
|
assert_eq!(cfg.links_space_char, " ");
|
||||||
assert!(cfg.nested_syntaxes.is_empty());
|
|
||||||
assert!(!cfg.auto_toc);
|
assert!(!cfg.auto_toc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,35 +284,26 @@ fn raw_wiki_parses_every_new_key() {
|
|||||||
"root": "/tmp/w",
|
"root": "/tmp/w",
|
||||||
"index": "home",
|
"index": "home",
|
||||||
"diary_frequency": "weekly",
|
"diary_frequency": "weekly",
|
||||||
"diary_start_week_day": "sunday",
|
|
||||||
"diary_caption_level": 2,
|
"diary_caption_level": 2,
|
||||||
"diary_sort": "asc",
|
"diary_sort": "asc",
|
||||||
"diary_header": "Journal",
|
"diary_header": "Journal",
|
||||||
"maxhi": 6,
|
|
||||||
"listsyms": "abcde",
|
"listsyms": "abcde",
|
||||||
"listsyms_propagate": false,
|
"listsyms_propagate": false,
|
||||||
"list_margin": 2,
|
"list_margin": 2,
|
||||||
"links_space_char": "_",
|
"links_space_char": "_",
|
||||||
"nested_syntaxes": { "python": "python", "ruby": "ruby" },
|
|
||||||
"auto_toc": true,
|
"auto_toc": true,
|
||||||
}],
|
}],
|
||||||
}));
|
}));
|
||||||
let w = &cfg.wikis[0];
|
let w = &cfg.wikis[0];
|
||||||
assert_eq!(w.index, "home");
|
assert_eq!(w.index, "home");
|
||||||
assert_eq!(w.diary_frequency, "weekly");
|
assert_eq!(w.diary_frequency, "weekly");
|
||||||
assert_eq!(w.diary_start_week_day, "sunday");
|
|
||||||
assert_eq!(w.diary_caption_level, 2);
|
assert_eq!(w.diary_caption_level, 2);
|
||||||
assert_eq!(w.diary_sort, "asc");
|
assert_eq!(w.diary_sort, "asc");
|
||||||
assert_eq!(w.diary_header, "Journal");
|
assert_eq!(w.diary_header, "Journal");
|
||||||
assert_eq!(w.maxhi, 6);
|
|
||||||
assert_eq!(w.listsyms, "abcde");
|
assert_eq!(w.listsyms, "abcde");
|
||||||
assert!(!w.listsyms_propagate);
|
assert!(!w.listsyms_propagate);
|
||||||
assert_eq!(w.list_margin, 2);
|
assert_eq!(w.list_margin, 2);
|
||||||
assert_eq!(w.links_space_char, "_");
|
assert_eq!(w.links_space_char, "_");
|
||||||
assert_eq!(
|
|
||||||
w.nested_syntaxes.get("python").map(String::as_str),
|
|
||||||
Some("python")
|
|
||||||
);
|
|
||||||
assert!(w.auto_toc);
|
assert!(w.auto_toc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,6 +355,77 @@ fn apply_change_preserves_severity_on_partial_update() {
|
|||||||
assert_eq!(cfg.diagnostic.link_severity, LinkSeverity::Error);
|
assert_eq!(cfg.diagnostic.link_severity, LinkSeverity::Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_change_minimal_diagnostic_payload_is_not_unwrapped() {
|
||||||
|
// A lone `{ "diagnostic": { … } }` is a real flat payload, not a
|
||||||
|
// `{ "nuwiki": { … } }` namespace wrapper. The single-key unwrap must
|
||||||
|
// skip it (it's a recognised top-level key) so the severity applies.
|
||||||
|
let mut cfg = config_from_json(json!({ "wiki_root": "/tmp/w" }));
|
||||||
|
assert_eq!(cfg.diagnostic.link_severity, LinkSeverity::Warning);
|
||||||
|
|
||||||
|
cfg.apply_change(&json!({ "diagnostic": { "link_severity": "error" } }));
|
||||||
|
assert_eq!(cfg.diagnostic.link_severity, LinkSeverity::Error);
|
||||||
|
|
||||||
|
// The Neovim namespace wrapper still unwraps correctly.
|
||||||
|
cfg.apply_change(&json!({ "nuwiki": { "diagnostic": { "link_severity": "hint" } } }));
|
||||||
|
assert_eq!(cfg.diagnostic.link_severity, LinkSeverity::Hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn severity_change_via_apply_change_is_observable_in_collected_diagnostics() {
|
||||||
|
// Contract behind `did_change_configuration`'s diagnostic re-publish:
|
||||||
|
// once `apply_change` updates the severity, re-running `collect_diagnostics`
|
||||||
|
// (what the handler does for every open doc) must emit broken-link
|
||||||
|
// diagnostics at the *new* severity. Guards the data path the handler
|
||||||
|
// depends on — apply_change writing the severity AND collect_diagnostics
|
||||||
|
// reading it.
|
||||||
|
use nuwiki_core::syntax::vimwiki::VimwikiSyntax;
|
||||||
|
use nuwiki_core::syntax::SyntaxPlugin;
|
||||||
|
use nuwiki_lsp::index::WorkspaceIndex;
|
||||||
|
use tower_lsp::lsp_types::DiagnosticSeverity;
|
||||||
|
|
||||||
|
let root = "/tmp/cfgchange";
|
||||||
|
let src = "[[GhostPage]]\n";
|
||||||
|
let ast = VimwikiSyntax::new().parse(src);
|
||||||
|
let mut idx = WorkspaceIndex::new(Some(PathBuf::from(root)));
|
||||||
|
let uri = Url::from_file_path(format!("{root}/Home.wiki")).unwrap();
|
||||||
|
idx.upsert(uri.clone(), &ast);
|
||||||
|
|
||||||
|
let mut cfg = config_from_json(json!({
|
||||||
|
"wiki_root": root,
|
||||||
|
"diagnostic": { "link_severity": "warn" },
|
||||||
|
}));
|
||||||
|
let warn = collect_diagnostics(
|
||||||
|
&ast,
|
||||||
|
src,
|
||||||
|
Some(&uri),
|
||||||
|
Some(&idx),
|
||||||
|
Some("Home"),
|
||||||
|
&cfg.diagnostic,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert_eq!(warn.len(), 1, "broken link should warn");
|
||||||
|
assert_eq!(warn[0].severity, Some(DiagnosticSeverity::WARNING));
|
||||||
|
|
||||||
|
// Simulate the client raising severity via didChangeConfiguration.
|
||||||
|
cfg.apply_change(&json!({ "diagnostic": { "link_severity": "error" } }));
|
||||||
|
let err = collect_diagnostics(
|
||||||
|
&ast,
|
||||||
|
src,
|
||||||
|
Some(&uri),
|
||||||
|
Some(&idx),
|
||||||
|
Some("Home"),
|
||||||
|
&cfg.diagnostic,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert_eq!(err.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
err[0].severity,
|
||||||
|
Some(DiagnosticSeverity::ERROR),
|
||||||
|
"re-collected diagnostics must reflect the new severity"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn vim_flat_and_neovim_wrapped_payloads_desugar_identically() {
|
fn vim_flat_and_neovim_wrapped_payloads_desugar_identically() {
|
||||||
// Parity guard for the two client shapes:
|
// Parity guard for the two client shapes:
|
||||||
|
|||||||
@@ -393,6 +393,27 @@ fn toc_edit_returns_none_for_empty_doc() {
|
|||||||
assert!(ops::toc_edit(src, &ast, &uri, true).is_none());
|
assert!(ops::toc_edit(src, &ast, &uri, true).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toc_rebuild_edit_only_acts_when_toc_already_present() {
|
||||||
|
let uri = Url::parse("file:///tmp/page.wiki").unwrap();
|
||||||
|
|
||||||
|
// No existing TOC → rebuild-on-save must not insert one.
|
||||||
|
let without = "= One =\n== Two ==\n";
|
||||||
|
let ast = parse(without);
|
||||||
|
assert!(
|
||||||
|
ops::toc_rebuild_edit(without, &ast, &uri, true).is_none(),
|
||||||
|
"auto_toc should not insert a TOC where none existed"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Existing TOC → rebuild refreshes it.
|
||||||
|
let with = "= Contents =\n- [[#stale|Stale]]\n\n= Real =\n";
|
||||||
|
let ast = parse(with);
|
||||||
|
let edit = ops::toc_rebuild_edit(with, &ast, &uri, true).expect("rebuild edit");
|
||||||
|
let te = &edit.changes.unwrap()[&uri][0];
|
||||||
|
assert!(te.new_text.contains("[[#real|Real]]"));
|
||||||
|
assert!(!te.new_text.contains("Stale"));
|
||||||
|
}
|
||||||
|
|
||||||
// ===== links_edit =====
|
// ===== links_edit =====
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user