2026-05-11 14:32:22 +00:00
|
|
|
//! Server-side runtime config.
|
|
|
|
|
//!
|
|
|
|
|
//! Populated from `InitializeParams.initialization_options` at boot and
|
2026-05-30 18:35:40 +00:00
|
|
|
//! refreshed via `workspace/didChangeConfiguration`. Legacy single-wiki
|
|
|
|
|
//! shapes are accepted and desugared into the `wikis = [...]` form so
|
|
|
|
|
//! existing setups keep working.
|
2026-05-11 14:32:22 +00:00
|
|
|
//!
|
2026-05-30 18:35:40 +00:00
|
|
|
//! `WikiConfig` (diary path, html path, template options, …) and the
|
|
|
|
|
//! top-level `Config` (diagnostic severity, mappings opt-in, …) carry the
|
|
|
|
|
//! fields actually consumed by the server.
|
2026-05-11 14:32:22 +00:00
|
|
|
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
|
|
|
|
|
use serde::Deserialize;
|
|
|
|
|
use tower_lsp::lsp_types::{InitializeParams, Url};
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
|
|
|
pub struct Config {
|
|
|
|
|
pub wikis: Vec<WikiConfig>,
|
|
|
|
|
pub diagnostic: DiagnosticConfig,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct WikiConfig {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub root: PathBuf,
|
|
|
|
|
pub file_extension: String,
|
|
|
|
|
pub syntax: String,
|
2026-05-12 15:10:27 +00:00
|
|
|
/// Stem of the wiki's index page (vimwiki's `index`, default
|
|
|
|
|
/// `"index"`). `<root>/<index><file_extension>` is what
|
|
|
|
|
/// `:VimwikiIndex` opens.
|
|
|
|
|
pub index: String,
|
2026-05-11 21:04:55 +00:00
|
|
|
/// Subdirectory under `root` where diary entries live. Matches
|
|
|
|
|
/// vimwiki's `g:vimwiki_diary_rel_path` (default `"diary"`).
|
|
|
|
|
pub diary_rel_path: String,
|
|
|
|
|
/// Stem of the diary index page within the diary subdirectory.
|
|
|
|
|
/// Matches vimwiki's `g:vimwiki_diary_index` (default `"diary"`).
|
|
|
|
|
/// The full path is `<root>/<diary_rel_path>/<diary_index>.wiki`.
|
|
|
|
|
pub diary_index: String,
|
2026-05-12 15:10:27 +00:00
|
|
|
/// `daily`/`weekly`/`monthly`/`yearly`. Picks the date format the
|
2026-05-30 18:35:40 +00:00
|
|
|
/// diary commands use. `daily` is supported; the others fall through
|
|
|
|
|
/// to vimwiki-style names but the commands are no-ops for them.
|
2026-05-12 15:10:27 +00:00
|
|
|
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).
|
|
|
|
|
pub diary_caption_level: u8,
|
|
|
|
|
/// Newest-first (`desc`) or oldest-first (`asc`) in the diary
|
|
|
|
|
/// index.
|
|
|
|
|
pub diary_sort: String,
|
|
|
|
|
/// H1 text for the diary index page.
|
|
|
|
|
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
|
|
|
|
|
/// fractional progress states for checkboxes; the i-th char (0..=4)
|
|
|
|
|
/// is the rendered marker for state `i / 4`. Default: `" .oOX"`.
|
|
|
|
|
pub listsyms: String,
|
|
|
|
|
/// When toggling a parent list item, also cascade to descendants.
|
|
|
|
|
pub listsyms_propagate: bool,
|
|
|
|
|
/// `-1` keeps tight lists; positive integers indent list items by
|
|
|
|
|
/// that many extra columns. Layout hint for the renderer.
|
|
|
|
|
pub list_margin: i32,
|
|
|
|
|
/// Character used in place of literal spaces inside wikilink
|
2026-05-30 19:24:08 -03:00
|
|
|
/// targets when writing to disk (a single space `" "` by default,
|
|
|
|
|
/// i.e. spaces are kept verbatim).
|
2026-05-12 15:10:27 +00:00
|
|
|
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.
|
|
|
|
|
pub auto_toc: bool,
|
2026-05-30 18:35:40 +00:00
|
|
|
/// HTML export options.
|
2026-05-11 21:37:36 +00:00
|
|
|
pub html: HtmlConfig,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Per-wiki HTML export settings. All paths are absolute (relative
|
|
|
|
|
/// paths from `initializationOptions` are resolved against the wiki
|
|
|
|
|
/// root at config-load time).
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct HtmlConfig {
|
|
|
|
|
/// Output directory for `nuwiki.export.*` commands.
|
|
|
|
|
/// Default: `<root>/_html`.
|
|
|
|
|
pub html_path: PathBuf,
|
|
|
|
|
/// Template search root. Default: `<root>/_templates`.
|
|
|
|
|
pub template_path: PathBuf,
|
|
|
|
|
/// Template name used when a page has no `%template` directive.
|
|
|
|
|
pub template_default: String,
|
|
|
|
|
/// Template file extension appended when looking up a template.
|
|
|
|
|
pub template_ext: String,
|
|
|
|
|
/// `%Y`/`%m`/`%d` format string passed to `{{date}}`.
|
|
|
|
|
pub template_date_format: String,
|
|
|
|
|
/// CSS file name relative to `html_path` (copied/created if missing).
|
|
|
|
|
pub css_name: String,
|
|
|
|
|
/// Run `currentToHtml` automatically on `textDocument/didSave`.
|
|
|
|
|
pub auto_export: bool,
|
|
|
|
|
/// When true, the output filename for a page is URL-safe-slugified.
|
|
|
|
|
pub html_filename_parameterization: bool,
|
|
|
|
|
/// Glob patterns (matched against the page name relative to root)
|
|
|
|
|
/// to skip during `allToHtml*`.
|
|
|
|
|
pub exclude_files: Vec<String>,
|
2026-05-12 14:45:39 +00:00
|
|
|
/// `color_dic`: vimwiki colour-tag name → CSS value mapping used
|
2026-05-30 18:35:40 +00:00
|
|
|
/// by the HTML renderer. Empty by default; the
|
2026-05-12 14:45:39 +00:00
|
|
|
/// renderer falls back to `class="color-<name>"` when a name
|
|
|
|
|
/// isn't in the dict.
|
|
|
|
|
pub color_dic: std::collections::HashMap<String, String>,
|
2026-05-11 21:37:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for HtmlConfig {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
html_path: PathBuf::new(),
|
|
|
|
|
template_path: PathBuf::new(),
|
|
|
|
|
template_default: "default".into(),
|
|
|
|
|
template_ext: ".tpl".into(),
|
|
|
|
|
template_date_format: "%Y-%m-%d".into(),
|
|
|
|
|
css_name: "style.css".into(),
|
|
|
|
|
auto_export: false,
|
|
|
|
|
html_filename_parameterization: false,
|
|
|
|
|
exclude_files: Vec::new(),
|
2026-05-12 14:45:39 +00:00
|
|
|
color_dic: std::collections::HashMap::new(),
|
2026-05-11 21:37:36 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl HtmlConfig {
|
|
|
|
|
/// Synthesise default paths under `root` when the user hasn't
|
|
|
|
|
/// specified explicit ones. `html_path` defaults to `<root>/_html`
|
2026-05-30 18:35:40 +00:00
|
|
|
/// and `template_path` to `<root>/_templates`.
|
2026-05-11 21:37:36 +00:00
|
|
|
pub fn from_root(root: &Path) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
html_path: root.join("_html"),
|
|
|
|
|
template_path: root.join("_templates"),
|
|
|
|
|
..Self::default()
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-11 14:32:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
|
|
|
pub struct DiagnosticConfig {
|
|
|
|
|
pub link_severity: LinkSeverity,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
|
|
|
|
pub enum LinkSeverity {
|
|
|
|
|
Off,
|
|
|
|
|
Hint,
|
|
|
|
|
#[default]
|
|
|
|
|
Warning,
|
|
|
|
|
Error,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl WikiConfig {
|
|
|
|
|
/// Synthetic default used when the client supplies neither
|
|
|
|
|
/// `initializationOptions` nor a workspace root. `name` is empty;
|
|
|
|
|
/// callers shouldn't depend on it for routing.
|
|
|
|
|
pub fn empty() -> Self {
|
2026-05-12 15:10:27 +00:00
|
|
|
let d = wiki_defaults();
|
2026-05-11 14:32:22 +00:00
|
|
|
Self {
|
|
|
|
|
name: String::new(),
|
|
|
|
|
root: PathBuf::new(),
|
|
|
|
|
file_extension: ".wiki".into(),
|
|
|
|
|
syntax: "vimwiki".into(),
|
2026-05-12 15:10:27 +00:00
|
|
|
index: d.index,
|
|
|
|
|
diary_rel_path: d.diary_rel_path,
|
|
|
|
|
diary_index: d.diary_index,
|
|
|
|
|
diary_frequency: d.diary_frequency,
|
|
|
|
|
diary_start_week_day: d.diary_start_week_day,
|
|
|
|
|
diary_caption_level: d.diary_caption_level,
|
|
|
|
|
diary_sort: d.diary_sort,
|
|
|
|
|
diary_header: d.diary_header,
|
|
|
|
|
maxhi: d.maxhi,
|
|
|
|
|
listsyms: d.listsyms,
|
|
|
|
|
listsyms_propagate: d.listsyms_propagate,
|
|
|
|
|
list_margin: d.list_margin,
|
|
|
|
|
links_space_char: d.links_space_char,
|
|
|
|
|
nested_syntaxes: d.nested_syntaxes,
|
|
|
|
|
auto_toc: d.auto_toc,
|
2026-05-11 21:37:36 +00:00
|
|
|
html: HtmlConfig::default(),
|
2026-05-11 14:32:22 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn from_root(root: PathBuf) -> Self {
|
2026-05-11 21:37:36 +00:00
|
|
|
let html = HtmlConfig::from_root(&root);
|
2026-05-12 15:10:27 +00:00
|
|
|
let d = wiki_defaults();
|
2026-05-11 14:32:22 +00:00
|
|
|
Self {
|
|
|
|
|
name: root
|
|
|
|
|
.file_name()
|
|
|
|
|
.map(|s| s.to_string_lossy().into_owned())
|
|
|
|
|
.unwrap_or_default(),
|
|
|
|
|
root,
|
|
|
|
|
file_extension: ".wiki".into(),
|
|
|
|
|
syntax: "vimwiki".into(),
|
2026-05-12 15:10:27 +00:00
|
|
|
index: d.index,
|
|
|
|
|
diary_rel_path: d.diary_rel_path,
|
|
|
|
|
diary_index: d.diary_index,
|
|
|
|
|
diary_frequency: d.diary_frequency,
|
|
|
|
|
diary_start_week_day: d.diary_start_week_day,
|
|
|
|
|
diary_caption_level: d.diary_caption_level,
|
|
|
|
|
diary_sort: d.diary_sort,
|
|
|
|
|
diary_header: d.diary_header,
|
|
|
|
|
maxhi: d.maxhi,
|
|
|
|
|
listsyms: d.listsyms,
|
|
|
|
|
listsyms_propagate: d.listsyms_propagate,
|
|
|
|
|
list_margin: d.list_margin,
|
|
|
|
|
links_space_char: d.links_space_char,
|
|
|
|
|
nested_syntaxes: d.nested_syntaxes,
|
|
|
|
|
auto_toc: d.auto_toc,
|
2026-05-11 21:37:36 +00:00
|
|
|
html,
|
2026-05-11 14:32:22 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-11 21:04:55 +00:00
|
|
|
|
|
|
|
|
/// Absolute path to the diary subdirectory under this wiki's root.
|
|
|
|
|
pub fn diary_dir(&self) -> PathBuf {
|
|
|
|
|
self.root.join(&self.diary_rel_path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Absolute path to this wiki's diary index page.
|
|
|
|
|
pub fn diary_index_path(&self) -> PathBuf {
|
|
|
|
|
let mut p = self.diary_dir();
|
|
|
|
|
p.push(format!("{}{}", self.diary_index, self.file_extension));
|
|
|
|
|
p
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Absolute path for a diary entry on `date`.
|
|
|
|
|
pub fn diary_path_for(&self, date: &nuwiki_core::date::DiaryDate) -> PathBuf {
|
|
|
|
|
let mut p = self.diary_dir();
|
|
|
|
|
p.push(format!("{}{}", date.format(), self.file_extension));
|
|
|
|
|
p
|
|
|
|
|
}
|
2026-05-12 22:09:14 +00:00
|
|
|
|
|
|
|
|
/// Absolute path for a diary entry covering `period`. Honours the
|
|
|
|
|
/// wiki's `diary_frequency` indirectly — callers compute the period
|
|
|
|
|
/// at the right cadence and we just pick the stem from its format.
|
|
|
|
|
pub fn diary_path_for_period(&self, period: &nuwiki_core::date::DiaryPeriod) -> PathBuf {
|
|
|
|
|
let mut p = self.diary_dir();
|
|
|
|
|
p.push(format!("{}{}", period.format(), self.file_extension));
|
|
|
|
|
p
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// `DiaryFrequency` parsed from this wiki's `diary_frequency` field.
|
|
|
|
|
pub fn frequency(&self) -> nuwiki_core::date::DiaryFrequency {
|
|
|
|
|
nuwiki_core::date::DiaryFrequency::parse(&self.diary_frequency)
|
|
|
|
|
}
|
2026-05-11 21:04:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_diary_rel_path() -> String {
|
|
|
|
|
"diary".to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_diary_index() -> String {
|
|
|
|
|
"diary".to_string()
|
2026-05-11 14:32:22 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-12 15:10:27 +00:00
|
|
|
fn default_index() -> String {
|
|
|
|
|
"index".to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_diary_frequency() -> String {
|
|
|
|
|
"daily".to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_diary_start_week_day() -> String {
|
|
|
|
|
"monday".to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_diary_sort() -> String {
|
|
|
|
|
"desc".to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_diary_header() -> String {
|
|
|
|
|
"Diary".to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_listsyms() -> String {
|
|
|
|
|
" .oOX".to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_links_space_char() -> String {
|
|
|
|
|
" ".to_string()
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-30 18:35:40 +00:00
|
|
|
/// Fill in the per-wiki keys that none of the constructors care
|
2026-05-12 15:10:27 +00:00
|
|
|
/// about. Centralising the defaults here keeps `empty()`, `from_root()`,
|
|
|
|
|
/// the legacy single-wiki path, and `From<RawWiki>` in sync.
|
|
|
|
|
fn wiki_defaults() -> WikiDefaults {
|
|
|
|
|
WikiDefaults {
|
|
|
|
|
index: default_index(),
|
|
|
|
|
diary_rel_path: default_diary_rel_path(),
|
|
|
|
|
diary_index: default_diary_index(),
|
|
|
|
|
diary_frequency: default_diary_frequency(),
|
|
|
|
|
diary_start_week_day: default_diary_start_week_day(),
|
|
|
|
|
diary_caption_level: 1,
|
|
|
|
|
diary_sort: default_diary_sort(),
|
|
|
|
|
diary_header: default_diary_header(),
|
|
|
|
|
maxhi: 1,
|
|
|
|
|
listsyms: default_listsyms(),
|
|
|
|
|
listsyms_propagate: true,
|
|
|
|
|
list_margin: -1,
|
|
|
|
|
links_space_char: default_links_space_char(),
|
|
|
|
|
nested_syntaxes: std::collections::HashMap::new(),
|
|
|
|
|
auto_toc: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct WikiDefaults {
|
|
|
|
|
index: String,
|
|
|
|
|
diary_rel_path: String,
|
|
|
|
|
diary_index: String,
|
|
|
|
|
diary_frequency: String,
|
|
|
|
|
diary_start_week_day: String,
|
|
|
|
|
diary_caption_level: u8,
|
|
|
|
|
diary_sort: String,
|
|
|
|
|
diary_header: String,
|
|
|
|
|
maxhi: u8,
|
|
|
|
|
listsyms: String,
|
|
|
|
|
listsyms_propagate: bool,
|
|
|
|
|
list_margin: i32,
|
|
|
|
|
links_space_char: String,
|
|
|
|
|
nested_syntaxes: std::collections::HashMap<String, String>,
|
|
|
|
|
auto_toc: bool,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 14:32:22 +00:00
|
|
|
impl Config {
|
|
|
|
|
/// Build a `Config` from `InitializeParams`.
|
|
|
|
|
///
|
|
|
|
|
/// Priority for the wikis list:
|
2026-05-30 18:35:40 +00:00
|
|
|
/// 1. `initialization_options.wikis = [...]` (multi-wiki shape)
|
2026-05-11 14:32:22 +00:00
|
|
|
/// 2. `initialization_options.wiki_root + file_extension + syntax`
|
2026-05-30 18:35:40 +00:00
|
|
|
/// (legacy single-wiki shape)
|
2026-05-11 14:32:22 +00:00
|
|
|
/// 3. `workspace_folders[0]` if present
|
|
|
|
|
/// 4. deprecated `root_uri` if present
|
|
|
|
|
/// 5. empty list (server stays alive but won't index anything)
|
|
|
|
|
pub fn from_init_params(params: &InitializeParams) -> Self {
|
|
|
|
|
let raw = params
|
|
|
|
|
.initialization_options
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|v| serde_json::from_value::<InitOptions>(v.clone()).ok())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
let wikis = if let Some(list) = raw.wikis {
|
|
|
|
|
list.into_iter().map(WikiConfig::from).collect()
|
|
|
|
|
} else if let Some(root) = raw.wiki_root.as_deref().and_then(expand_path) {
|
2026-05-12 15:10:27 +00:00
|
|
|
let mut wc = WikiConfig::from_root(root);
|
|
|
|
|
if let Some(ext) = raw.file_extension {
|
|
|
|
|
wc.file_extension = ext;
|
|
|
|
|
}
|
|
|
|
|
if let Some(s) = raw.syntax {
|
|
|
|
|
wc.syntax = s;
|
|
|
|
|
}
|
|
|
|
|
vec![wc]
|
2026-05-11 14:32:22 +00:00
|
|
|
} else if let Some(folder) = first_workspace_folder(params) {
|
|
|
|
|
vec![WikiConfig::from_root(folder)]
|
|
|
|
|
} else {
|
|
|
|
|
Vec::new()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Config {
|
|
|
|
|
wikis,
|
|
|
|
|
diagnostic: DiagnosticConfig::default(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Re-apply settings received via `workspace/didChangeConfiguration`.
|
|
|
|
|
/// On parse failure the existing config is returned unchanged.
|
2026-05-21 23:24:51 -03:00
|
|
|
///
|
|
|
|
|
/// Accepts both the flat shape (`{ "wikis": [...] }`) produced by the
|
|
|
|
|
/// VimL client, **and** the Neovim-style wrapper (`{ "nuwiki": { ... } }`)
|
|
|
|
|
/// that `server_settings()` in `lua/nuwiki/lsp.lua` produces. Any other
|
|
|
|
|
/// single-key object whose value is an object is also unwrapped so
|
|
|
|
|
/// editors that namespace settings by server name continue to work.
|
2026-05-11 14:32:22 +00:00
|
|
|
pub fn apply_change(&mut self, value: &serde_json::Value) {
|
2026-05-21 23:24:51 -03:00
|
|
|
// Unwrap { "nuwiki": { ... } } → { ... } so both clients work.
|
|
|
|
|
let inner = value
|
|
|
|
|
.as_object()
|
2026-05-26 23:34:31 -03:00
|
|
|
.and_then(|m| {
|
|
|
|
|
if m.len() == 1 {
|
|
|
|
|
m.values().next()
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-05-21 23:24:51 -03:00
|
|
|
.filter(|v| v.is_object())
|
|
|
|
|
.unwrap_or(value);
|
|
|
|
|
|
|
|
|
|
let raw: InitOptions = match serde_json::from_value(inner.clone()) {
|
2026-05-11 14:32:22 +00:00
|
|
|
Ok(r) => r,
|
|
|
|
|
Err(_) => return,
|
|
|
|
|
};
|
|
|
|
|
if let Some(list) = raw.wikis {
|
|
|
|
|
self.wikis = list.into_iter().map(WikiConfig::from).collect();
|
|
|
|
|
} else if let Some(root) = raw.wiki_root.as_deref().and_then(expand_path) {
|
|
|
|
|
let mut wc = WikiConfig::from_root(root);
|
|
|
|
|
if let Some(ext) = raw.file_extension {
|
|
|
|
|
wc.file_extension = ext;
|
|
|
|
|
}
|
|
|
|
|
if let Some(s) = raw.syntax {
|
|
|
|
|
wc.syntax = s;
|
|
|
|
|
}
|
|
|
|
|
self.wikis = vec![wc];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== Wire schema =====
|
|
|
|
|
|
2026-05-21 23:03:36 -03:00
|
|
|
/// Custom serde helper: accept JSON `true`/`false` **or** integers `0`/`1`
|
|
|
|
|
/// for `Option<bool>` fields. VimL dicts serialise `auto_export = 1` as
|
|
|
|
|
/// the JSON number `1`, not `true`, which would otherwise silently break
|
|
|
|
|
/// deserialisation of the entire `RawWiki` and cause the server to fall
|
|
|
|
|
/// back to `wiki_root` (ignoring the wikis list and marking every link
|
|
|
|
|
/// broken).
|
|
|
|
|
mod opt_bool_or_int {
|
|
|
|
|
use serde::{Deserialize, Deserializer};
|
|
|
|
|
|
|
|
|
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
|
|
|
|
|
where
|
|
|
|
|
D: Deserializer<'de>,
|
|
|
|
|
{
|
|
|
|
|
let v = Option::<serde_json::Value>::deserialize(deserializer)?;
|
|
|
|
|
match v {
|
|
|
|
|
None | Some(serde_json::Value::Null) => Ok(None),
|
|
|
|
|
Some(serde_json::Value::Bool(b)) => Ok(Some(b)),
|
|
|
|
|
Some(serde_json::Value::Number(ref n)) => {
|
|
|
|
|
if let Some(i) = n.as_i64() {
|
|
|
|
|
Ok(Some(i != 0))
|
|
|
|
|
} else {
|
|
|
|
|
Err(serde::de::Error::custom(
|
|
|
|
|
"expected bool or 0/1 integer for bool field",
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Some(other) => Err(serde::de::Error::custom(format!(
|
|
|
|
|
"expected bool or 0/1 integer, got {other}"
|
|
|
|
|
))),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 14:32:22 +00:00
|
|
|
#[derive(Debug, Default, Deserialize)]
|
|
|
|
|
#[serde(default, rename_all = "snake_case")]
|
|
|
|
|
struct InitOptions {
|
|
|
|
|
wikis: Option<Vec<RawWiki>>,
|
2026-05-30 18:35:40 +00:00
|
|
|
// legacy single-wiki compat fields
|
2026-05-11 14:32:22 +00:00
|
|
|
wiki_root: Option<String>,
|
|
|
|
|
file_extension: Option<String>,
|
|
|
|
|
syntax: Option<String>,
|
|
|
|
|
log_level: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
#[serde(rename_all = "snake_case")]
|
|
|
|
|
struct RawWiki {
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
name: Option<String>,
|
|
|
|
|
root: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
file_extension: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
syntax: Option<String>,
|
2026-05-11 21:04:55 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
diary_rel_path: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
diary_index: Option<String>,
|
2026-05-30 18:35:40 +00:00
|
|
|
// HTML keys — all optional, fall back to per-root defaults.
|
2026-05-11 21:37:36 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
html_path: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
template_path: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
template_default: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
template_ext: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
template_date_format: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
css_name: Option<String>,
|
2026-05-21 23:03:36 -03:00
|
|
|
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
2026-05-11 21:37:36 +00:00
|
|
|
auto_export: Option<bool>,
|
2026-05-21 23:03:36 -03:00
|
|
|
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
2026-05-11 21:37:36 +00:00
|
|
|
html_filename_parameterization: Option<bool>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
exclude_files: Option<Vec<String>>,
|
2026-05-12 14:45:39 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
color_dic: Option<std::collections::HashMap<String, String>>,
|
2026-05-30 18:35:40 +00:00
|
|
|
// per-wiki keys mirroring vimwiki globals. All optional so
|
2026-05-12 15:10:27 +00:00
|
|
|
// existing single-wiki configs migrate without touching anything.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
index: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
diary_frequency: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
diary_start_week_day: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
diary_caption_level: Option<u8>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
diary_sort: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
diary_header: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
maxhi: Option<u8>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
listsyms: Option<String>,
|
2026-05-21 23:03:36 -03:00
|
|
|
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
2026-05-12 15:10:27 +00:00
|
|
|
listsyms_propagate: Option<bool>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
list_margin: Option<i32>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
links_space_char: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
nested_syntaxes: Option<std::collections::HashMap<String, String>>,
|
2026-05-21 23:03:36 -03:00
|
|
|
#[serde(default, deserialize_with = "opt_bool_or_int::deserialize")]
|
2026-05-12 15:10:27 +00:00
|
|
|
auto_toc: Option<bool>,
|
2026-05-11 14:32:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<RawWiki> for WikiConfig {
|
|
|
|
|
fn from(r: RawWiki) -> Self {
|
|
|
|
|
let root = expand_path(&r.root).unwrap_or_default();
|
|
|
|
|
let derived_name = root
|
|
|
|
|
.file_name()
|
|
|
|
|
.map(|s| s.to_string_lossy().into_owned())
|
|
|
|
|
.unwrap_or_default();
|
2026-05-11 21:37:36 +00:00
|
|
|
let mut html = HtmlConfig::from_root(&root);
|
|
|
|
|
if let Some(p) = r.html_path.as_deref().and_then(expand_path) {
|
|
|
|
|
html.html_path = p;
|
|
|
|
|
}
|
|
|
|
|
if let Some(p) = r.template_path.as_deref().and_then(expand_path) {
|
|
|
|
|
html.template_path = p;
|
|
|
|
|
}
|
|
|
|
|
if let Some(s) = r.template_default {
|
|
|
|
|
html.template_default = s;
|
|
|
|
|
}
|
|
|
|
|
if let Some(s) = r.template_ext {
|
|
|
|
|
html.template_ext = s;
|
|
|
|
|
}
|
|
|
|
|
if let Some(s) = r.template_date_format {
|
|
|
|
|
html.template_date_format = s;
|
|
|
|
|
}
|
|
|
|
|
if let Some(s) = r.css_name {
|
|
|
|
|
html.css_name = s;
|
|
|
|
|
}
|
|
|
|
|
if let Some(b) = r.auto_export {
|
|
|
|
|
html.auto_export = b;
|
|
|
|
|
}
|
|
|
|
|
if let Some(b) = r.html_filename_parameterization {
|
|
|
|
|
html.html_filename_parameterization = b;
|
|
|
|
|
}
|
|
|
|
|
if let Some(v) = r.exclude_files {
|
|
|
|
|
html.exclude_files = v;
|
|
|
|
|
}
|
2026-05-12 14:45:39 +00:00
|
|
|
if let Some(v) = r.color_dic {
|
|
|
|
|
html.color_dic = v;
|
|
|
|
|
}
|
2026-05-12 15:10:27 +00:00
|
|
|
let d = wiki_defaults();
|
2026-05-11 14:32:22 +00:00
|
|
|
WikiConfig {
|
|
|
|
|
name: r.name.unwrap_or(derived_name),
|
|
|
|
|
root,
|
|
|
|
|
file_extension: r.file_extension.unwrap_or_else(|| ".wiki".into()),
|
|
|
|
|
syntax: r.syntax.unwrap_or_else(|| "vimwiki".into()),
|
2026-05-12 15:10:27 +00:00
|
|
|
index: r.index.unwrap_or(d.index),
|
|
|
|
|
diary_rel_path: r.diary_rel_path.unwrap_or(d.diary_rel_path),
|
|
|
|
|
diary_index: r.diary_index.unwrap_or(d.diary_index),
|
|
|
|
|
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_sort: r.diary_sort.unwrap_or(d.diary_sort),
|
|
|
|
|
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_propagate: r.listsyms_propagate.unwrap_or(d.listsyms_propagate),
|
|
|
|
|
list_margin: r.list_margin.unwrap_or(d.list_margin),
|
|
|
|
|
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),
|
2026-05-11 21:37:36 +00:00
|
|
|
html,
|
2026-05-11 14:32:22 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn expand_path(s: &str) -> Option<PathBuf> {
|
|
|
|
|
let trimmed = s.trim();
|
|
|
|
|
if trimmed.is_empty() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
// Best-effort `~` expansion. Avoids pulling in `dirs` or `home`.
|
|
|
|
|
if let Some(rest) = trimmed.strip_prefix("~/") {
|
|
|
|
|
if let Some(home) = std::env::var_os("HOME") {
|
|
|
|
|
return Some(Path::new(&home).join(rest));
|
|
|
|
|
}
|
|
|
|
|
} else if trimmed == "~" {
|
|
|
|
|
if let Some(home) = std::env::var_os("HOME") {
|
|
|
|
|
return Some(PathBuf::from(home));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Some(PathBuf::from(trimmed))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn first_workspace_folder(params: &InitializeParams) -> Option<PathBuf> {
|
|
|
|
|
if let Some(folders) = ¶ms.workspace_folders {
|
|
|
|
|
if let Some(first) = folders.first() {
|
|
|
|
|
return first.uri.to_file_path().ok();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#[allow(deprecated)]
|
|
|
|
|
if let Some(uri) = ¶ms.root_uri {
|
|
|
|
|
return uri.to_file_path().ok();
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Test helper: build a `Config` from a JSON value (mimics what arrives
|
|
|
|
|
/// in `initializationOptions`).
|
|
|
|
|
pub fn config_from_json(value: serde_json::Value) -> Config {
|
|
|
|
|
let mut cfg = Config::default();
|
|
|
|
|
cfg.apply_change(&value);
|
|
|
|
|
cfg
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
fn _ensure_url_is_used(_u: &Url) {}
|