217 lines
6.5 KiB
Rust
217 lines
6.5 KiB
Rust
|
|
//! Server-side runtime config.
|
|||
|
|
//!
|
|||
|
|
//! Populated from `InitializeParams.initialization_options` at boot and
|
|||
|
|
//! refreshed via `workspace/didChangeConfiguration` (P18). v1.0 single-wiki
|
|||
|
|
//! shapes are accepted and desugared into the v1.1 `wikis = [...]` form so
|
|||
|
|
//! existing setups keep working (P16).
|
|||
|
|
//!
|
|||
|
|
//! Only the fields actually consumed in Phase 11 land here. Phases 13–19
|
|||
|
|
//! extend `WikiConfig` (diary path, html path, template options, …) and
|
|||
|
|
//! the top-level `Config` (diagnostic severity, mappings opt-in, …) as
|
|||
|
|
//! they need them.
|
|||
|
|
|
|||
|
|
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,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[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 {
|
|||
|
|
Self {
|
|||
|
|
name: String::new(),
|
|||
|
|
root: PathBuf::new(),
|
|||
|
|
file_extension: ".wiki".into(),
|
|||
|
|
syntax: "vimwiki".into(),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pub fn from_root(root: PathBuf) -> Self {
|
|||
|
|
Self {
|
|||
|
|
name: root
|
|||
|
|
.file_name()
|
|||
|
|
.map(|s| s.to_string_lossy().into_owned())
|
|||
|
|
.unwrap_or_default(),
|
|||
|
|
root,
|
|||
|
|
file_extension: ".wiki".into(),
|
|||
|
|
syntax: "vimwiki".into(),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
impl Config {
|
|||
|
|
/// Build a `Config` from `InitializeParams`.
|
|||
|
|
///
|
|||
|
|
/// Priority for the wikis list:
|
|||
|
|
/// 1. `initialization_options.wikis = [...]` (v1.1 shape)
|
|||
|
|
/// 2. `initialization_options.wiki_root + file_extension + syntax`
|
|||
|
|
/// (v1.0 single-wiki shape, P16)
|
|||
|
|
/// 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) {
|
|||
|
|
vec![WikiConfig {
|
|||
|
|
name: root
|
|||
|
|
.file_name()
|
|||
|
|
.map(|s| s.to_string_lossy().into_owned())
|
|||
|
|
.unwrap_or_default(),
|
|||
|
|
root,
|
|||
|
|
file_extension: raw.file_extension.unwrap_or_else(|| ".wiki".into()),
|
|||
|
|
syntax: raw.syntax.unwrap_or_else(|| "vimwiki".into()),
|
|||
|
|
}]
|
|||
|
|
} 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.
|
|||
|
|
pub fn apply_change(&mut self, value: &serde_json::Value) {
|
|||
|
|
let raw: InitOptions = match serde_json::from_value(value.clone()) {
|
|||
|
|
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 =====
|
|||
|
|
|
|||
|
|
#[derive(Debug, Default, Deserialize)]
|
|||
|
|
#[serde(default, rename_all = "snake_case")]
|
|||
|
|
struct InitOptions {
|
|||
|
|
wikis: Option<Vec<RawWiki>>,
|
|||
|
|
// v1.0 single-wiki compat fields
|
|||
|
|
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>,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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();
|
|||
|
|
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()),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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) {}
|