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,70 @@
|
||||
//! `Wiki` aggregate — per-wiki state, even when there's only one.
|
||||
//!
|
||||
//! v1.0 shipped with a single `Arc<RwLock<WorkspaceIndex>>` on `Backend`.
|
||||
//! Phase 11 lifts that into a `Wiki` aggregate so Phase 18 (multi-wiki)
|
||||
//! only changes config shape, not data flow. Phases 12–17 always operate
|
||||
//! on a `Wiki`; in practice the `wikis` `Vec` has one entry until 18.
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use tower_lsp::lsp_types::Url;
|
||||
|
||||
use crate::config::WikiConfig;
|
||||
use crate::index::WorkspaceIndex;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
|
||||
pub struct WikiId(pub u32);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Wiki {
|
||||
pub id: WikiId,
|
||||
pub config: WikiConfig,
|
||||
pub index: Arc<RwLock<WorkspaceIndex>>,
|
||||
}
|
||||
|
||||
impl Wiki {
|
||||
pub fn new(id: WikiId, config: WikiConfig) -> Self {
|
||||
let index = Arc::new(RwLock::new(WorkspaceIndex::new(Some(config.root.clone()))));
|
||||
Self { id, config, index }
|
||||
}
|
||||
|
||||
/// True when `uri` resolves to a file under this wiki's root.
|
||||
pub fn contains(&self, uri: &Url) -> bool {
|
||||
let Ok(path) = uri.to_file_path() else {
|
||||
return false;
|
||||
};
|
||||
path.starts_with(&self.config.root)
|
||||
}
|
||||
|
||||
/// Length of the wiki's root path (component count) — used to break
|
||||
/// ties in nested-root layouts.
|
||||
pub fn root_depth(&self) -> usize {
|
||||
self.config.root.components().count()
|
||||
}
|
||||
|
||||
pub fn root(&self) -> &Path {
|
||||
&self.config.root
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick the wiki whose root is the longest prefix of `uri`. Returns the
|
||||
/// wiki id, not a guard, so callers can release the read lock before
|
||||
/// awaiting.
|
||||
pub fn resolve_uri_to_wiki(wikis: &[Wiki], uri: &Url) -> Option<WikiId> {
|
||||
wikis
|
||||
.iter()
|
||||
.filter(|w| w.contains(uri))
|
||||
.max_by_key(|w| w.root_depth())
|
||||
.map(|w| w.id)
|
||||
}
|
||||
|
||||
/// Build the initial `Vec<Wiki>` from a list of `WikiConfig`s. Always
|
||||
/// assigns ids by position so `WikiId(0)` is the default wiki.
|
||||
pub fn build_wikis(configs: &[WikiConfig]) -> Vec<Wiki> {
|
||||
configs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, cfg)| Wiki::new(WikiId(i as u32), cfg.clone()))
|
||||
.collect()
|
||||
}
|
||||
Reference in New Issue
Block a user