Major clean up and improvements to vimwiki compatibility and documentation.
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
//! `workspace/executeCommand` dispatcher.
|
||||
//!
|
||||
//! Phase 13 introduced the router + `nuwiki.file.delete`. Phase 14 added
|
||||
//! the cheap surgical edits — checkbox toggle/cycle/reject, heading
|
||||
//! promote/demote, "next task" navigation. Phase 15 layers on the
|
||||
//! generation + workspace-query commands: `toc.generate`,
|
||||
//! `links.generate`, `workspace.checkLinks`, `workspace.findOrphans`.
|
||||
//! The router handles `nuwiki.file.delete`, the cheap surgical edits —
|
||||
//! checkbox toggle/cycle/reject, heading promote/demote, "next task"
|
||||
//! navigation — and the generation + workspace-query commands:
|
||||
//! `toc.generate`, `links.generate`, `workspace.checkLinks`,
|
||||
//! `workspace.findOrphans`.
|
||||
//!
|
||||
//! Every command resolves to either a `WorkspaceEdit` (the server then
|
||||
//! calls `apply_edit`) or a JSON `Value` (returned to the client) — see
|
||||
@@ -87,7 +87,17 @@ pub(crate) async fn execute(
|
||||
Ok(list_checkbox(backend, args, ops::toggle_state)?.map(CommandOutcome::Edit))
|
||||
}
|
||||
"nuwiki.list.cycleCheckbox" => {
|
||||
Ok(list_checkbox(backend, args, ops::cycle_state)?.map(CommandOutcome::Edit))
|
||||
let reverse = args
|
||||
.first()
|
||||
.and_then(|v| v.get("reverse"))
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
let mutate = if reverse {
|
||||
ops::cycle_state_back
|
||||
} else {
|
||||
ops::cycle_state
|
||||
};
|
||||
Ok(list_checkbox(backend, args, mutate)?.map(CommandOutcome::Edit))
|
||||
}
|
||||
"nuwiki.list.rejectCheckbox" => {
|
||||
Ok(list_checkbox(backend, args, ops::reject_state)?.map(CommandOutcome::Edit))
|
||||
@@ -653,7 +663,7 @@ fn tags_rebuild(backend: &Backend, args: Vec<Value>) -> Result<Option<Value>, St
|
||||
})))
|
||||
}
|
||||
|
||||
// ===== Phase 18: multi-wiki picker commands =====
|
||||
// ===== multi-wiki picker commands =====
|
||||
|
||||
fn wiki_list_all(backend: &Backend) -> Result<Option<Value>, String> {
|
||||
let wikis = backend.wikis_snapshot();
|
||||
@@ -811,7 +821,7 @@ fn wiki_goto_page(backend: &Backend, args: Vec<Value>) -> Result<Option<Value>,
|
||||
})))
|
||||
}
|
||||
|
||||
// ===== §13.1 Cluster A: list rewriters =====
|
||||
// ===== Cluster A: list rewriters =====
|
||||
|
||||
fn list_remove_done(backend: &Backend, args: Vec<Value>) -> Result<Option<WorkspaceEdit>, String> {
|
||||
let p = parse_optional_uri_arg(args.clone())?;
|
||||
@@ -944,7 +954,7 @@ fn list_change_level(backend: &Backend, args: Vec<Value>) -> Result<Option<Works
|
||||
))
|
||||
}
|
||||
|
||||
// ===== §13.1 Cluster B: table rewriters =====
|
||||
// ===== Cluster B: table rewriters =====
|
||||
|
||||
fn table_insert(_backend: &Backend, args: Vec<Value>) -> Result<Option<WorkspaceEdit>, String> {
|
||||
#[derive(Deserialize)]
|
||||
@@ -1020,7 +1030,7 @@ fn table_move_column(backend: &Backend, args: Vec<Value>) -> Result<Option<Works
|
||||
))
|
||||
}
|
||||
|
||||
// ===== Phase 17: HTML export commands =====
|
||||
// ===== HTML export commands =====
|
||||
|
||||
fn export_current(
|
||||
backend: &Backend,
|
||||
@@ -1233,6 +1243,20 @@ pub mod ops {
|
||||
}
|
||||
}
|
||||
|
||||
/// Exact inverse of [`cycle_state`] — used by the `glp` mapping
|
||||
/// ("cycle the checkbox state backward").
|
||||
pub fn cycle_state_back(current: &str) -> Option<&'static str> {
|
||||
match current {
|
||||
"[ ]" => Some("[X]"),
|
||||
"[X]" => Some("[O]"),
|
||||
"[O]" => Some("[o]"),
|
||||
"[o]" => Some("[.]"),
|
||||
"[.]" => Some("[ ]"),
|
||||
"[-]" => Some("[ ]"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle the `[-]` "rejected" state.
|
||||
pub fn reject_state(current: &str) -> Option<&'static str> {
|
||||
match current {
|
||||
@@ -1647,7 +1671,7 @@ pub mod ops {
|
||||
None
|
||||
}
|
||||
|
||||
// ===== Phase 15: TOC + Links generation, workspace queries =====
|
||||
// ===== TOC + Links generation, workspace queries =====
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -1973,7 +1997,7 @@ pub mod ops {
|
||||
/// on the diagnostics module's internals.
|
||||
pub use crate::diagnostics::collect_wiki_links;
|
||||
|
||||
// ===== §13.1 Cluster A: list rewriters =====
|
||||
// ===== Cluster A: list rewriters =====
|
||||
|
||||
use crate::edits::text_edit_delete;
|
||||
use nuwiki_core::ast::ListSymbol;
|
||||
@@ -2095,7 +2119,9 @@ pub mod ops {
|
||||
|
||||
/// Delete every checkbox item whose state is `Done` or `Rejected`,
|
||||
/// cascading into their sublists. When `pos` is `Some`, restrict to
|
||||
/// the list containing `pos`'s line; otherwise sweep the whole doc.
|
||||
/// the contiguous list block containing `pos`'s line (the "current
|
||||
/// list" — including its nested sublists); otherwise sweep the whole
|
||||
/// doc.
|
||||
pub fn remove_done_edit(
|
||||
text: &str,
|
||||
ast: &DocumentNode,
|
||||
@@ -2103,16 +2129,8 @@ pub mod ops {
|
||||
pos: Option<LspPosition>,
|
||||
utf8: bool,
|
||||
) -> Option<WorkspaceEdit> {
|
||||
let restrict_line = pos.map(|p| p.line);
|
||||
let mut victims: Vec<Span> = Vec::new();
|
||||
walk_list_items(&ast.children, &mut |item| {
|
||||
let matches_scope = match restrict_line {
|
||||
None => true,
|
||||
Some(line) => (item.span.start.line..=item.span.end.line).contains(&line),
|
||||
};
|
||||
if !matches_scope {
|
||||
return;
|
||||
}
|
||||
let mut collect = |item: &ListItemNode| {
|
||||
if matches!(
|
||||
item.checkbox,
|
||||
Some(nuwiki_core::ast::CheckboxState::Done)
|
||||
@@ -2120,7 +2138,19 @@ pub mod ops {
|
||||
) {
|
||||
victims.push(extend_to_line_end(text, item.span));
|
||||
}
|
||||
});
|
||||
};
|
||||
match pos {
|
||||
None => walk_list_items(&ast.children, &mut collect),
|
||||
Some(p) => {
|
||||
// "Current list" = the top-level contiguous list block the
|
||||
// cursor sits in. Walk just that block (and its sublists),
|
||||
// leaving done items in sibling lists untouched.
|
||||
let list = find_top_list_at_line(ast, p.line)?;
|
||||
for item in &list.items {
|
||||
walk_list_items_in_item(item, &mut collect);
|
||||
}
|
||||
}
|
||||
}
|
||||
if victims.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -2131,6 +2161,40 @@ pub mod ops {
|
||||
Some(b.build())
|
||||
}
|
||||
|
||||
/// Find the top-level (contiguous) list block whose span contains
|
||||
/// `line`. Unlike [`find_list_at_line`], this does not descend into
|
||||
/// sublists — it returns the outermost list so callers operate on the
|
||||
/// whole list block the cursor belongs to.
|
||||
fn find_top_list_at_line(ast: &DocumentNode, line: u32) -> Option<&ListNode> {
|
||||
for block in &ast.children {
|
||||
if let Some(list) = find_top_list_in_block(block, line) {
|
||||
return Some(list);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn find_top_list_in_block(block: &BlockNode, line: u32) -> Option<&ListNode> {
|
||||
match block {
|
||||
BlockNode::List(list) => {
|
||||
if (list.span.start.line..=list.span.end.line).contains(&line) {
|
||||
Some(list)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
BlockNode::Blockquote(BlockquoteNode { children, .. }) => {
|
||||
for c in children {
|
||||
if let Some(l) = find_top_list_in_block(c, line) {
|
||||
return Some(l);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extend a span to include the trailing newline (so deletion removes
|
||||
/// the empty line that would otherwise be left behind).
|
||||
fn extend_to_line_end(text: &str, span: Span) -> Span {
|
||||
@@ -2480,7 +2544,7 @@ pub mod ops {
|
||||
Some((span, marker_str, i))
|
||||
}
|
||||
|
||||
// ===== §13.1 Cluster B: table rewriters =====
|
||||
// ===== Cluster B: table rewriters =====
|
||||
|
||||
use nuwiki_core::ast::{TableNode, TableRowNode};
|
||||
|
||||
@@ -2738,7 +2802,7 @@ pub mod ops {
|
||||
Some(out)
|
||||
}
|
||||
|
||||
// ===== Phase 16: diary helpers =====
|
||||
// ===== diary helpers =====
|
||||
|
||||
use crate::edits::op_create;
|
||||
|
||||
@@ -2792,10 +2856,10 @@ pub mod ops {
|
||||
b.build()
|
||||
}
|
||||
|
||||
// ===== Tag ops (Phase 12 commands) =====
|
||||
// ===== Tag ops =====
|
||||
//
|
||||
// SPEC §12.3 calls for three tag-driven commands beyond the indexing
|
||||
// that Phase 12 already shipped. `tag_hits` powers `nuwiki.tags.search`;
|
||||
// Three tag-driven commands beyond the indexing itself.
|
||||
// `tag_hits` powers `nuwiki.tags.search`;
|
||||
// `tag_links_edit` powers `nuwiki.tags.generateLinks`; the rebuild
|
||||
// command is a direct re-walk of the wiki root in the dispatcher.
|
||||
|
||||
@@ -2984,7 +3048,7 @@ pub mod ops {
|
||||
}
|
||||
}
|
||||
|
||||
/// Disk-touching helpers for the Phase 17 `nuwiki.export.*` commands.
|
||||
/// Disk-touching helpers for the `nuwiki.export.*` commands.
|
||||
/// Kept out of `ops` because everything here is side-effecting — pure
|
||||
/// rendering / templating lives in [`crate::export`].
|
||||
pub mod export_ops {
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
//! 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).
|
||||
//! refreshed via `workspace/didChangeConfiguration`. Legacy single-wiki
|
||||
//! shapes are accepted and desugared into the `wikis = [...]` form so
|
||||
//! existing setups keep working.
|
||||
//!
|
||||
//! 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.
|
||||
//! `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.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -39,9 +38,8 @@ pub struct WikiConfig {
|
||||
/// The full path is `<root>/<diary_rel_path>/<diary_index>.wiki`.
|
||||
pub diary_index: String,
|
||||
/// `daily`/`weekly`/`monthly`/`yearly`. Picks the date format the
|
||||
/// diary commands use. v1.1 ships `daily`; the others fall through
|
||||
/// to vimwiki-style names but the commands are no-ops until
|
||||
/// Cluster 4 lands.
|
||||
/// diary commands use. `daily` is supported; the others fall through
|
||||
/// to vimwiki-style names but the commands are no-ops for them.
|
||||
pub diary_frequency: String,
|
||||
/// First day of the week (`monday`..`sunday`). Used by weekly
|
||||
/// diary entry naming.
|
||||
@@ -74,7 +72,7 @@ pub struct WikiConfig {
|
||||
pub nested_syntaxes: std::collections::HashMap<String, String>,
|
||||
/// Rebuild the page's TOC on save when set.
|
||||
pub auto_toc: bool,
|
||||
/// Phase 17 HTML export options. See SPEC §12.8.
|
||||
/// HTML export options.
|
||||
pub html: HtmlConfig,
|
||||
}
|
||||
|
||||
@@ -104,7 +102,7 @@ pub struct HtmlConfig {
|
||||
/// to skip during `allToHtml*`.
|
||||
pub exclude_files: Vec<String>,
|
||||
/// `color_dic`: vimwiki colour-tag name → CSS value mapping used
|
||||
/// by the HTML renderer (SPEC §12.11). Empty by default; the
|
||||
/// by the HTML renderer. Empty by default; the
|
||||
/// renderer falls back to `class="color-<name>"` when a name
|
||||
/// isn't in the dict.
|
||||
pub color_dic: std::collections::HashMap<String, String>,
|
||||
@@ -130,7 +128,7 @@ impl Default for HtmlConfig {
|
||||
impl HtmlConfig {
|
||||
/// Synthesise default paths under `root` when the user hasn't
|
||||
/// specified explicit ones. `html_path` defaults to `<root>/_html`
|
||||
/// and `template_path` to `<root>/_templates` per SPEC §12.8.
|
||||
/// and `template_path` to `<root>/_templates`.
|
||||
pub fn from_root(root: &Path) -> Self {
|
||||
Self {
|
||||
html_path: root.join("_html"),
|
||||
@@ -284,7 +282,7 @@ fn default_links_space_char() -> String {
|
||||
" ".to_string()
|
||||
}
|
||||
|
||||
/// Fill in the v1.1 per-wiki keys that none of the constructors care
|
||||
/// Fill in the per-wiki keys that none of the constructors care
|
||||
/// about. Centralising the defaults here keeps `empty()`, `from_root()`,
|
||||
/// the legacy single-wiki path, and `From<RawWiki>` in sync.
|
||||
fn wiki_defaults() -> WikiDefaults {
|
||||
@@ -329,9 +327,9 @@ impl Config {
|
||||
/// Build a `Config` from `InitializeParams`.
|
||||
///
|
||||
/// Priority for the wikis list:
|
||||
/// 1. `initialization_options.wikis = [...]` (v1.1 shape)
|
||||
/// 1. `initialization_options.wikis = [...]` (multi-wiki shape)
|
||||
/// 2. `initialization_options.wiki_root + file_extension + syntax`
|
||||
/// (v1.0 single-wiki shape, P16)
|
||||
/// (legacy single-wiki shape)
|
||||
/// 3. `workspace_folders[0]` if present
|
||||
/// 4. deprecated `root_uri` if present
|
||||
/// 5. empty list (server stays alive but won't index anything)
|
||||
@@ -445,7 +443,7 @@ mod opt_bool_or_int {
|
||||
#[serde(default, rename_all = "snake_case")]
|
||||
struct InitOptions {
|
||||
wikis: Option<Vec<RawWiki>>,
|
||||
// v1.0 single-wiki compat fields
|
||||
// legacy single-wiki compat fields
|
||||
wiki_root: Option<String>,
|
||||
file_extension: Option<String>,
|
||||
syntax: Option<String>,
|
||||
@@ -466,7 +464,7 @@ struct RawWiki {
|
||||
diary_rel_path: Option<String>,
|
||||
#[serde(default)]
|
||||
diary_index: Option<String>,
|
||||
// Phase 17 HTML keys — all optional, fall back to per-root defaults.
|
||||
// HTML keys — all optional, fall back to per-root defaults.
|
||||
#[serde(default)]
|
||||
html_path: Option<String>,
|
||||
#[serde(default)]
|
||||
@@ -487,7 +485,7 @@ struct RawWiki {
|
||||
exclude_files: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
color_dic: Option<std::collections::HashMap<String, String>>,
|
||||
// v1.1 per-wiki keys mirroring vimwiki globals. All optional so
|
||||
// per-wiki keys mirroring vimwiki globals. All optional so
|
||||
// existing single-wiki configs migrate without touching anything.
|
||||
#[serde(default)]
|
||||
index: Option<String>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Diagnostic sources beyond parse-time `ErrorNode`s.
|
||||
//!
|
||||
//! Phase 15 lands the `nuwiki.link` source — broken-link warnings. Walks
|
||||
//! The `nuwiki.link` source emits broken-link warnings. Walks
|
||||
//! every `WikiLinkNode` in the AST and emits one diagnostic per:
|
||||
//! - `Wiki` target that doesn't resolve to a page in the workspace index,
|
||||
//! - `Wiki`/`AnchorOnly` target with an anchor that matches neither a
|
||||
@@ -8,7 +8,7 @@
|
||||
//! - `File`/`Local` target whose resolved path doesn't exist on disk.
|
||||
//!
|
||||
//! Interwiki, Diary, Raw, and external links are not diagnosed —
|
||||
//! interwiki resolution is Phase 18's job, diary is Phase 16's, and we
|
||||
//! interwiki resolution and diary handling live elsewhere, and we
|
||||
//! don't fetch URLs.
|
||||
|
||||
use std::path::PathBuf;
|
||||
@@ -332,8 +332,7 @@ fn resolve_file_path(
|
||||
index.root.as_ref().map(|r| r.join(&candidate))
|
||||
}
|
||||
|
||||
/// `nuwiki.link` diagnostics for a single document. Phase 11 left this as
|
||||
/// a stub; Phase 15 fills it in.
|
||||
/// `nuwiki.link` diagnostics for a single document.
|
||||
pub fn link_health(
|
||||
ast: &DocumentNode,
|
||||
text: &str,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Diary subsystem (Phase 16) — pure helpers used by the
|
||||
//! Diary subsystem — pure helpers used by the
|
||||
//! `nuwiki.diary.*` `executeCommand` handlers.
|
||||
//!
|
||||
//! All path/URI math lives here so the command dispatcher stays a thin
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Helpers for building `WorkspaceEdit`s.
|
||||
//!
|
||||
//! Phase 13+ (rename, list/table edits, link/TOC generation) all return
|
||||
//! Rename, list/table edits, and link/TOC generation all return
|
||||
//! `WorkspaceEdit`. The builder centralises the byte-offset → LSP-position
|
||||
//! conversion (UTF-8 or UTF-16, matching whatever was negotiated in
|
||||
//! `initialize`) so each call site doesn't have to redo it.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! HTML export — pure helpers shared by the Phase 17 `nuwiki.export.*`
|
||||
//! HTML export — pure helpers shared by the `nuwiki.export.*`
|
||||
//! command handlers. Side-effecting work (reading the template from
|
||||
//! disk, writing the rendered HTML, copying CSS) lives in `commands.rs`
|
||||
//! since the dispatcher is the one with a `&Backend` and async context.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! `textDocument/foldingRange` provider — SPEC §12.10 / P14.
|
||||
//! `textDocument/foldingRange` provider.
|
||||
//!
|
||||
//! Strategy: one fold per heading (line → line before the next heading
|
||||
//! of same-or-higher level) plus one fold per top-level list block.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Workspace index — the in-memory model of every `.wiki` page nuwiki
|
||||
//! has seen, used to power Phase 8 nav features.
|
||||
//! has seen, used to power nav features.
|
||||
//!
|
||||
//! Per P8 (SPEC §11), the initial scan runs as a background tokio task so
|
||||
//! The initial scan runs as a background tokio task so
|
||||
//! the server stays responsive on startup. Per-document updates land here
|
||||
//! synchronously via `upsert` whenever the LSP backend re-parses on
|
||||
//! `didOpen` / `didChange`.
|
||||
@@ -29,10 +29,10 @@ pub struct IndexedPage {
|
||||
pub title: Option<String>,
|
||||
pub headings: Vec<HeadingInfo>,
|
||||
pub outgoing: Vec<OutgoingLink>,
|
||||
/// Phase 12: every `TagNode` on the page. `tags_by_name` on the
|
||||
/// Every `TagNode` on the page. `tags_by_name` on the
|
||||
/// containing `WorkspaceIndex` is the reverse map.
|
||||
pub tags: Vec<TagInfo>,
|
||||
/// Phase 16: `Some(date)` when this page is a *daily* diary entry —
|
||||
/// `Some(date)` when this page is a *daily* diary entry —
|
||||
/// stem parses as `YYYY-MM-DD`. Equivalent to
|
||||
/// `diary_period.and_then(|p| if let DiaryPeriod::Day(d) = p { Some(d) } else { None })`.
|
||||
/// Kept as its own field for back-compat with prev/next/list callers
|
||||
@@ -97,7 +97,7 @@ pub struct Backlink {
|
||||
}
|
||||
|
||||
/// A tag's location, used by `WorkspaceIndex::tags_for` for cross-page
|
||||
/// `[[Page#tag]]` resolution and `nuwiki.tags.search` (Phase 13).
|
||||
/// `[[Page#tag]]` resolution and `nuwiki.tags.search`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TagOccurrence {
|
||||
pub uri: Url,
|
||||
@@ -108,7 +108,7 @@ pub struct TagOccurrence {
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WorkspaceIndex {
|
||||
pub root: Option<PathBuf>,
|
||||
/// Phase 16: subdir relative to `root` that holds diary entries.
|
||||
/// Subdir relative to `root` that holds diary entries.
|
||||
/// Mirrors `WikiConfig::diary_rel_path`; stored here so `upsert` can
|
||||
/// classify each indexed URI without a back-reference to the config.
|
||||
pub diary_rel_path: Option<String>,
|
||||
@@ -119,9 +119,9 @@ pub struct WorkspaceIndex {
|
||||
pub pages_by_uri: HashMap<Url, IndexedPage>,
|
||||
pub pages_by_name: HashMap<String, Url>,
|
||||
pub backlinks: HashMap<String, Vec<Backlink>>,
|
||||
/// Phase 12: tag name → every occurrence across the workspace. Used by
|
||||
/// the v1.1 nav handlers (tag-as-anchor `definition`, `workspace/symbol`
|
||||
/// inclusion) and the Phase 13 `nuwiki.tags.search` command.
|
||||
/// Tag name → every occurrence across the workspace. Used by
|
||||
/// the nav handlers (tag-as-anchor `definition`, `workspace/symbol`
|
||||
/// inclusion) and the `nuwiki.tags.search` command.
|
||||
pub tags_by_name: HashMap<String, Vec<TagOccurrence>>,
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ impl WorkspaceIndex {
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 12: maintain the reverse tag map alongside the per-page list.
|
||||
// Maintain the reverse tag map alongside the per-page list.
|
||||
for tag in &page.tags {
|
||||
self.tags_by_name
|
||||
.entry(tag.name.clone())
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
//! LSP protocol bridge for nuwiki.
|
||||
//!
|
||||
//! `tower-lsp` server that maintains a `DocumentStore` (SPEC §6.10) plus a
|
||||
//! workspace-wide index (SPEC §6.10 + P8) and exposes every LSP method
|
||||
//! through Phase 8:
|
||||
//! `tower-lsp` server that maintains a `DocumentStore` plus a
|
||||
//! workspace-wide index and exposes every LSP method:
|
||||
//!
|
||||
//! - `initialize` / `initialized` / `shutdown`
|
||||
//! - `textDocument/didOpen`, `didChange` (full sync), `didClose`
|
||||
@@ -14,12 +13,12 @@
|
||||
//! - `workspace/symbol` — search across indexed headings
|
||||
//!
|
||||
//! Position encoding is negotiated as UTF-8 when the client supports LSP
|
||||
//! 3.17+ encodings (SPEC §6.11). When the client only supports the legacy
|
||||
//! 3.17+ encodings. When the client only supports the legacy
|
||||
//! UTF-16 default, positions get translated through a per-line lookup.
|
||||
//!
|
||||
//! Re-parses are full per change (incremental parsing deferred post-v1).
|
||||
//! Re-parses are full per change (no incremental parsing).
|
||||
//! The workspace scan runs in a background tokio task spawned from
|
||||
//! `initialize` (P8) with `window/workDoneProgress` begin/end events.
|
||||
//! `initialize` with `window/workDoneProgress` begin/end events.
|
||||
|
||||
pub mod commands;
|
||||
pub mod config;
|
||||
@@ -111,8 +110,8 @@ pub async fn run_stdio() -> io::Result<()> {
|
||||
struct DocumentState {
|
||||
text: String,
|
||||
ast: DocumentNode,
|
||||
/// Last version we observed from the client. Held per SPEC §6.10 even
|
||||
/// though the foundation phase doesn't yet need to read it back.
|
||||
/// Last version we observed from the client. Held even
|
||||
/// though nothing reads it back yet.
|
||||
#[allow(dead_code)]
|
||||
version: i32,
|
||||
}
|
||||
@@ -124,8 +123,8 @@ struct Backend {
|
||||
/// True after `initialize` if the client opted into UTF-8 encoding.
|
||||
/// Otherwise we keep the LSP 3.16 default of UTF-16.
|
||||
use_utf8: Arc<AtomicBool>,
|
||||
/// Per-wiki state (P11). Vector holds one entry until Phase 18
|
||||
/// (multi-wiki) lets users add more.
|
||||
/// Per-wiki state. Vector holds one entry until multi-wiki
|
||||
/// support lets users add more.
|
||||
wikis: Arc<RwLock<Vec<Wiki>>>,
|
||||
/// Server config received via `initializationOptions` and refreshed
|
||||
/// via `workspace/didChangeConfiguration` (P18).
|
||||
@@ -151,7 +150,7 @@ impl Backend {
|
||||
wikis.iter().find(|w| w.id == id).cloned()
|
||||
}
|
||||
|
||||
/// Used by Phase 15 workspace-level commands (`checkLinks`, `findOrphans`)
|
||||
/// Used by workspace-level commands (`checkLinks`, `findOrphans`)
|
||||
/// when no URI is supplied — they fall back to the first registered wiki.
|
||||
fn default_wiki(&self) -> Option<Wiki> {
|
||||
let wikis = self.wikis.read().ok()?;
|
||||
@@ -290,7 +289,7 @@ impl Backend {
|
||||
|
||||
/// Return live state if `uri` is open in the editor, otherwise read
|
||||
/// the file from disk and re-parse. The closed-doc path is used by
|
||||
/// cross-document operations (Phase 13 `workspace/rename` and link
|
||||
/// cross-document operations (`workspace/rename` and link
|
||||
/// health) that need accurate spans + text without trusting cached
|
||||
/// `WorkspaceIndex` data.
|
||||
pub async fn read_or_open(&self, uri: &Url) -> io::Result<DocSnapshot> {
|
||||
@@ -318,7 +317,7 @@ impl Backend {
|
||||
}
|
||||
|
||||
async fn update_document(&self, uri: Url, text: String, version: i32) {
|
||||
// For Phase 6 we always treat unknown buffers as vimwiki — if/when
|
||||
// We always treat unknown buffers as vimwiki — if/when
|
||||
// multi-syntax dispatch lands the pick should follow the URI's ext.
|
||||
let plugin = match self.registry.get("vimwiki") {
|
||||
Some(p) => p,
|
||||
@@ -373,7 +372,7 @@ impl Backend {
|
||||
#[tower_lsp::async_trait]
|
||||
impl LanguageServer for Backend {
|
||||
async fn initialize(&self, params: InitializeParams) -> LspResult<InitializeResult> {
|
||||
// SPEC §6.11: prefer UTF-8 if the client advertises support.
|
||||
// Prefer UTF-8 if the client advertises support.
|
||||
let supports_utf8 = params
|
||||
.capabilities
|
||||
.general
|
||||
@@ -404,7 +403,7 @@ impl LanguageServer for Backend {
|
||||
change: Some(TextDocumentSyncKind::FULL),
|
||||
will_save: None,
|
||||
will_save_wait_until: None,
|
||||
// Phase 17 `auto_export` listens on `did_save`. Asking
|
||||
// `auto_export` listens on `did_save`. Asking
|
||||
// for `include_text: false` since the document store
|
||||
// already mirrors the live buffer.
|
||||
save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
|
||||
@@ -502,9 +501,9 @@ impl LanguageServer for Backend {
|
||||
|
||||
async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
|
||||
// Apply the new settings to `Config`. Re-indexing on root changes
|
||||
// is out of scope for Phase 11; for now we just accept the new
|
||||
// is out of scope here; for now we just accept the new
|
||||
// values so later commands see the right diagnostic settings etc.
|
||||
// Phase 18 (multi-wiki) revisits this with proper rebuild semantics.
|
||||
// Multi-wiki support revisits this with proper rebuild semantics.
|
||||
let mut cfg = self.config.write().expect("config lock poisoned");
|
||||
cfg.apply_change(¶ms.settings);
|
||||
}
|
||||
@@ -530,7 +529,7 @@ impl LanguageServer for Backend {
|
||||
}
|
||||
|
||||
async fn did_rename_files(&self, params: RenameFilesParams) {
|
||||
// Phase 13 advertises this capability so the editor pushes
|
||||
// We advertise this capability so the editor pushes
|
||||
// out-of-band renames (file explorer drags, `:VimwikiRenameFile`
|
||||
// when the client opts to surface the new URI). Update the index
|
||||
// so backlinks/headings track the new URI without waiting for
|
||||
@@ -576,7 +575,7 @@ impl LanguageServer for Backend {
|
||||
}
|
||||
|
||||
async fn did_save(&self, params: DidSaveTextDocumentParams) {
|
||||
// Phase 17: when `auto_export` is set on the resolved wiki, run
|
||||
// When `auto_export` is set on the resolved wiki, run
|
||||
// the equivalent of `nuwiki.export.currentToHtml` after the file
|
||||
// hits disk. Best-effort — failures are logged but do not bubble
|
||||
// to the client. Skips pages with the `%nohtml` directive.
|
||||
@@ -934,7 +933,7 @@ impl LanguageServer for Backend {
|
||||
});
|
||||
}
|
||||
}
|
||||
// Phase 12: surface tags too, named as `:tag:` so editors
|
||||
// Surface tags too, named as `:tag:` so editors
|
||||
// can distinguish them from headings at a glance.
|
||||
for t in &page.tags {
|
||||
if q.is_empty() || t.name.to_lowercase().contains(&q) {
|
||||
@@ -1121,9 +1120,9 @@ fn utf16_column(text: &str, line: u32, byte_col: u32) -> u32 {
|
||||
.sum::<usize>() as u32
|
||||
}
|
||||
|
||||
/// Backward-compat wrapper kept for v1.0 test surface. New call sites
|
||||
/// Backward-compat wrapper kept for the legacy test surface. New call sites
|
||||
/// should use `collect_diagnostics` so they can opt into link-health
|
||||
/// (Phase 15) and other sources.
|
||||
/// and other sources.
|
||||
pub fn ast_diagnostics(ast: &DocumentNode, text: &str, utf8: bool) -> Vec<Diagnostic> {
|
||||
collect_diagnostics(
|
||||
ast,
|
||||
@@ -1136,12 +1135,12 @@ pub fn ast_diagnostics(ast: &DocumentNode, text: &str, utf8: bool) -> Vec<Diagno
|
||||
)
|
||||
}
|
||||
|
||||
/// Composable diagnostic collector (P11 §12.2.4).
|
||||
/// Composable diagnostic collector.
|
||||
///
|
||||
/// Sources, each gated by `cfg`:
|
||||
///
|
||||
/// 1. Parse errors (always on).
|
||||
/// 2. Broken-link warnings (Phase 15) — emitted when an index, source
|
||||
/// 2. Broken-link warnings — emitted when an index, source
|
||||
/// page name, and `link_severity != Off` are all available.
|
||||
pub fn collect_diagnostics(
|
||||
ast: &DocumentNode,
|
||||
@@ -1317,8 +1316,8 @@ fn keyword_str(k: nuwiki_core::ast::Keyword) -> &'static str {
|
||||
|
||||
// ===== Workspace + navigation helpers =====
|
||||
//
|
||||
// `workspace_root_from_params` lived here through Phase 10; Phase 11 lifted
|
||||
// that logic into `Config::from_init_params` (see `config.rs`) so workspace
|
||||
// `workspace_root_from_params` logic now lives in
|
||||
// `Config::from_init_params` (see `config.rs`) so workspace
|
||||
// folders and `root_uri` are handled alongside `initializationOptions`.
|
||||
|
||||
/// Background workspace scan (P8). Walks every `.wiki` file under `root`,
|
||||
@@ -1469,7 +1468,7 @@ fn heading_range_in(idx: &WorkspaceIndex, uri: &Url, anchor: &str, _utf8: bool)
|
||||
if let Some(h) = page.find_heading_by_anchor(anchor) {
|
||||
return Some(span_to_lsp_range_no_text(&h.span));
|
||||
}
|
||||
// Phase 12: tags-as-anchors. `[[Page#tag-name]]` lands on the
|
||||
// Tags-as-anchors. `[[Page#tag-name]]` lands on the
|
||||
// matching `TagNode` on the target page.
|
||||
let t = page.tags.iter().find(|t| t.name == anchor)?;
|
||||
Some(span_to_lsp_range_no_text(&t.span))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Helpers shared by the Phase 8 navigation handlers (definition,
|
||||
//! Helpers shared by the navigation handlers (definition,
|
||||
//! references, hover, completion).
|
||||
//!
|
||||
//! Two responsibilities:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
//! `textDocument/rename` — server-side rename with cross-document link
|
||||
//! rewriting.
|
||||
//!
|
||||
//! Phase 13 implementation:
|
||||
//! Implementation:
|
||||
//! 1. Resolve the rename target — cursor on a wikilink renames the linked
|
||||
//! page; otherwise rename the current file.
|
||||
//! 2. Build the new URI from the wiki root + the user-supplied path.
|
||||
//! 3. For every page in the wiki's index with an outgoing link to the
|
||||
//! old page name, read its source via the Phase 11 closed-doc loader
|
||||
//! old page name, read its source via the closed-doc loader
|
||||
//! so cached spans are validated against current text, then emit a
|
||||
//! `TextEdit` that swaps the `[[old]]` for `[[new]]` while preserving
|
||||
//! anchors and descriptions.
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
//! Semantic-token emission for the vimwiki AST.
|
||||
//!
|
||||
//! Token-type scheme settled in P7: custom `vimwiki*` types plus
|
||||
//! `level1`..`level6` + `centered` modifiers (SPEC §11). Default highlight
|
||||
//! groups for these arrive in Phase 9.
|
||||
//! Token-type scheme: custom `vimwiki*` types plus
|
||||
//! `level1`..`level6` + `centered` modifiers.
|
||||
//!
|
||||
//! Pipeline:
|
||||
//!
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! `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.
|
||||
//! The single `Arc<RwLock<WorkspaceIndex>>` on `Backend` is lifted into a
|
||||
//! `Wiki` aggregate so multi-wiki support only changes config shape, not
|
||||
//! data flow. Handlers always operate on a `Wiki`; in practice the `wikis`
|
||||
//! `Vec` has one entry until multi-wiki support lands.
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
Reference in New Issue
Block a user