From 9b6413c3de6c914289f9b6250d0d07b77a9324ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Fr=C3=B3es=20Franco?= Date: Sat, 30 May 2026 21:53:46 -0300 Subject: [PATCH] feat(lsp): wire diagnostic.link_severity through from client config The broken-link diagnostic severity was already consulted by collect_diagnostics, but Config.diagnostic was hardcoded to the default and never populated from initializationOptions/didChangeConfiguration. Parse a client-supplied `diagnostic.link_severity` ('off'|'hint'|'warn'|'error', case-insensitive) into LinkSeverity and apply it in both from_init_params and apply_change. Partial config updates that omit `diagnostic` preserve the existing severity rather than resetting it. Co-Authored-By: Claude Opus 4.7 --- crates/nuwiki-lsp/src/config.rs | 41 +++++++++++++++- crates/nuwiki-lsp/tests/index_and_config.rs | 54 +++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/crates/nuwiki-lsp/src/config.rs b/crates/nuwiki-lsp/src/config.rs index d216cff..186dd60 100644 --- a/crates/nuwiki-lsp/src/config.rs +++ b/crates/nuwiki-lsp/src/config.rs @@ -153,6 +153,21 @@ pub enum LinkSeverity { Error, } +impl LinkSeverity { + /// Parse a client-supplied severity string. Case-insensitive; + /// accepts both `warn` and `warning`. Returns `None` for unknown + /// values so callers can fall back to the default. + pub fn parse(s: &str) -> Option { + match s.trim().to_ascii_lowercase().as_str() { + "off" => Some(Self::Off), + "hint" => Some(Self::Hint), + "warn" | "warning" => Some(Self::Warning), + "error" => Some(Self::Error), + _ => None, + } + } +} + impl WikiConfig { /// Synthetic default used when the client supplies neither /// `initializationOptions` nor a workspace root. `name` is empty; @@ -360,7 +375,7 @@ impl Config { Config { wikis, - diagnostic: DiagnosticConfig::default(), + diagnostic: diagnostic_from_raw(&raw.diagnostic), } } @@ -402,6 +417,11 @@ impl Config { } self.wikis = vec![wc]; } + // Only touch diagnostic settings when the payload carries them, so a + // partial `didChangeConfiguration` doesn't reset severity to default. + if raw.diagnostic.is_some() { + self.diagnostic = diagnostic_from_raw(&raw.diagnostic); + } } } @@ -449,6 +469,25 @@ struct InitOptions { file_extension: Option, syntax: Option, log_level: Option, + diagnostic: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +struct RawDiagnostic { + #[serde(default)] + link_severity: Option, +} + +/// Build a [`DiagnosticConfig`] from the raw client payload, falling back +/// to defaults for absent or unrecognised values. +fn diagnostic_from_raw(raw: &Option) -> DiagnosticConfig { + let link_severity = raw + .as_ref() + .and_then(|d| d.link_severity.as_deref()) + .and_then(LinkSeverity::parse) + .unwrap_or_default(); + DiagnosticConfig { link_severity } } #[derive(Debug, Deserialize)] diff --git a/crates/nuwiki-lsp/tests/index_and_config.rs b/crates/nuwiki-lsp/tests/index_and_config.rs index 7267c15..36fe0df 100644 --- a/crates/nuwiki-lsp/tests/index_and_config.rs +++ b/crates/nuwiki-lsp/tests/index_and_config.rs @@ -319,6 +319,54 @@ fn raw_wiki_parses_every_new_key() { assert!(w.auto_toc); } +#[test] +fn link_severity_parses_from_client_config() { + for (input, expected) in [ + ("off", LinkSeverity::Off), + ("hint", LinkSeverity::Hint), + ("warn", LinkSeverity::Warning), + ("warning", LinkSeverity::Warning), + ("error", LinkSeverity::Error), + ("ERROR", LinkSeverity::Error), + ] { + let cfg = config_from_json(json!({ + "wiki_root": "/tmp/w", + "diagnostic": { "link_severity": input }, + })); + assert_eq!( + cfg.diagnostic.link_severity, expected, + "input {input:?} should map to {expected:?}" + ); + } +} + +#[test] +fn link_severity_invalid_or_absent_falls_back_to_default() { + // Unknown string → default (Warning). + let bad = config_from_json(json!({ + "wiki_root": "/tmp/w", + "diagnostic": { "link_severity": "loud" }, + })); + assert_eq!(bad.diagnostic.link_severity, LinkSeverity::Warning); + + // No diagnostic key at all → default. + let none = config_from_json(json!({ "wiki_root": "/tmp/w" })); + assert_eq!(none.diagnostic.link_severity, LinkSeverity::Warning); +} + +#[test] +fn apply_change_preserves_severity_on_partial_update() { + let mut cfg = config_from_json(json!({ + "wiki_root": "/tmp/w", + "diagnostic": { "link_severity": "error" }, + })); + assert_eq!(cfg.diagnostic.link_severity, LinkSeverity::Error); + + // A later update that omits `diagnostic` must not reset severity. + cfg.apply_change(&json!({ "wiki_root": "/tmp/w2" })); + assert_eq!(cfg.diagnostic.link_severity, LinkSeverity::Error); +} + #[test] fn vim_flat_and_neovim_wrapped_payloads_desugar_identically() { // Parity guard for the two client shapes: @@ -332,6 +380,7 @@ fn vim_flat_and_neovim_wrapped_payloads_desugar_identically() { "file_extension": ".md", "syntax": "vimwiki", "log_level": "debug", + "diagnostic": { "link_severity": "error" }, "wikis": [ { "name": "personal", @@ -347,6 +396,11 @@ fn vim_flat_and_neovim_wrapped_payloads_desugar_identically() { let flat = config_from_json(inner.clone()); let wrapped = config_from_json(json!({ "nuwiki": inner })); + assert_eq!( + flat.diagnostic.link_severity, + wrapped.diagnostic.link_severity + ); + assert_eq!(flat.diagnostic.link_severity, LinkSeverity::Error); assert_eq!(flat.wikis.len(), wrapped.wikis.len()); for (a, b) in flat.wikis.iter().zip(wrapped.wikis.iter()) { assert_eq!(a.name, b.name);