phase 11: plumbing prereqs — Wiki, edits, config, snapshot, diags
Cross-cutting infrastructure that every v1.1 phase depends on. Lands as a numbered phase before any user-visible feature so Phases 12–19 stay mechanical. 5 pieces: - `config.rs` — `Config` + `WikiConfig` + `DiagnosticConfig` + `LinkSeverity`. Serde-deserialised from `initialization_options`. v1.0 single-wiki shape (`wiki_root` + `file_extension` + `syntax`) desugars to one-entry `wikis = [...]` per P16. `~/...` expansion without pulling in `dirs`. Fallbacks: workspace_folders[0] → deprecated root_uri → empty list. - `wiki.rs` — `Wiki` aggregate (id + WikiConfig + Arc<RwLock<Index>>). `build_wikis(&[WikiConfig])` materialises the list; `resolve_uri_to_wiki` picks the longest-prefix root match so nested wikis route correctly. Single-entry today, multi-entry once Phase 18 lands. - `edits.rs` — `WorkspaceEditBuilder` + `text_edit_replace/insert/delete` + `op_rename/delete/create`. Handles the UTF-8/UTF-16 conversion at the span boundary so each Phase 13+ command stays a 3-liner. Builder picks `documentChanges` when file ops are present (LSP requires it), `changes` map otherwise (works for LSP 3.15- clients). File ops precede content edits in the output so created/renamed files exist when edits apply. `DeleteFile` is built without `annotation_id` — lsp-types 0.94.x ships it that way; later versions added the field. - `Backend` refactor — `index: Arc<RwLock<WorkspaceIndex>>` replaced by `wikis: Arc<RwLock<Vec<Wiki>>>` + `config: Arc<RwLock<Config>>`. New `wiki(id)` / `default_wiki()` / `wiki_for_uri(uri)` helpers. Every v1.0 handler (definition, references, hover, completion, workspace/symbol, update_document) goes through `wiki_for_uri`. `initialize` builds wikis from `Config::from_init_params`; `initialized` spawns one indexer task per wiki with the existing `window/workDoneProgress` wrapper. New `did_change_configuration` handler re-applies settings (rebuild semantics revisited in Phase 18). The old `workspace_root_from_params` helper is gone. - `read_or_open` + `DocSnapshot` (scaffolded, `#[allow(dead_code)]` until Phase 13's `workspace/rename` consumes it). Returns the live document when held in the documents map, otherwise async-reads from disk and re-parses so cross-document operations get accurate text and spans instead of trusting stale `WorkspaceIndex` data. - `collect_diagnostics` composable collector. Same `ErrorNode` walk today; takes `Option<&WorkspaceIndex>` + `page_name` + `&DiagnosticConfig` so Phase 15 can drop a link-health source in without further refactor. `ast_diagnostics` kept as a thin wrapper for the v1.0 test surface. New deps: `serde` + `serde_json` (already transitive through tower-lsp; elevated to direct deps for clarity and stability). `tokio` gains the `fs` feature for `read_or_open`'s async disk read. Tests (19 new in `phase11_plumbing.rs`): - Edits: UTF-8 passthrough + UTF-16 conversion (`héllo`), insert/delete shape, builder mode selection, file-op accumulation, `op_create` round-trip - Config: defaults, v1.0 desugar, explicit list overrides legacy, empty JSON, invalid JSON preserves prior state - Wiki: sequential id assignment, longest-prefix resolution, no-match, `Wiki::contains` - Diagnostics: error-node composition, link-health stub returns empty in Phase 11 All 172 v1.0 tests still pass — backward compatible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
//! 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) {}
|
||||
Reference in New Issue
Block a user