phase 13: workspace/rename + executeCommand router + nuwiki.file.delete
CI / cargo fmt --check (push) Successful in 29s
CI / cargo clippy (push) Successful in 1m15s
CI / cargo test (push) Successful in 1m15s

Server-side rename with cross-document link rewriting (P11's
read_or_open delivers on its promise) and the first executeCommand
entry. Plus the routing infrastructure that Phases 14-17 will hang
their own commands off of.

commands.rs:
- commands::execute(backend, name, args) dispatches by command name
  and returns the WorkspaceEdit to apply.
- COMMANDS const slice is the source of truth for what's advertised
  in ExecuteCommandOptions.commands.
- First entry: nuwiki.file.delete with args { uri: <Url> } returning
  a WorkspaceEdit with a DeleteFile op.

rename.rs:
- compute_rename(backend, target_uri, new_name, utf8):
  1. Resolve the wiki via wiki_for_uri.
  2. Build new URI from wiki.root + new_name + extension.
  3. Snapshot the index's backlinks for the old page name before
     releasing the read lock so we don't hold it across awaits.
  4. For each backlink, read_or_open the source (live state or
     disk + reparse) and validate the cached span against current
     text.
  5. Rewrite the [[...]] segment with rewrite_wikilink_target,
     preserving anchor and description.
  6. Add the RenameFile op so the builder emits it before the text
     edits in documentChanges ordering.
- build_new_uri handles subdirectory paths and avoids double-
  appending the file extension.
- rewrite_wikilink_target is pub so other tooling can call it.

lib.rs:
- Capabilities: rename_provider + execute_command_provider listing
  commands::COMMANDS.
- New rename handler: cursor-on-wikilink renames the linked page,
  else the buffer's file. Delegates to rename::compute_rename.
- New execute_command handler dispatches via commands::execute, then
  pushes the resulting WorkspaceEdit through client.apply_edit. On
  unknown command or bad args, logs and returns null.
- read_or_open loses its allow(dead_code) attribute now that Phase
  13 calls it.
- rename_target_uri helper shares index-resolution with goto_def.

Tests (11 new in phase13_rename_commands.rs):
- rewrite_wikilink_target: plain, with description, with anchor,
  with both, subdirectory path, non-wikilink-text rejection.
- build_new_uri: under root, with subdirectory, extension dedup,
  empty path segments skipped.
- nuwiki.file.delete builder smoke test (DocumentChanges + DeleteFile).

All 211 v1.0+Phase 11+12 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 16:27:39 +00:00
parent 8b29d4e9b9
commit cb4bfae5e7
5 changed files with 367 additions and 15 deletions
+51
View File
@@ -0,0 +1,51 @@
//! `workspace/executeCommand` dispatcher.
//!
//! Phase 13 lands the router + the first command. Phases 1417 register
//! their own commands here. Each handler returns the `WorkspaceEdit` it
//! wants applied; the `Backend` calls `client.apply_edit` after the call.
//!
//! Phase 13 surface:
//! - `nuwiki.file.delete` — args: `{ "uri": <Url> }`
use serde::Deserialize;
use tower_lsp::lsp_types::{Url, WorkspaceEdit};
use crate::edits::{op_delete, WorkspaceEditBuilder};
use crate::Backend;
/// All commands the server advertises in `ExecuteCommandOptions.commands`.
/// Keep this in lock-step with the match arms in `execute` below — adding
/// here without a handler will fail at runtime; adding a handler without
/// listing here means the client won't see it as available.
pub const COMMANDS: &[&str] = &["nuwiki.file.delete"];
/// Dispatch an `executeCommand` request. Returns `Ok(None)` for commands
/// that don't produce a `WorkspaceEdit` (e.g. read-only queries); returns
/// `Err` with a human-readable message for unknown commands or bad args
/// — the LSP backend logs it and returns `null` to the client.
pub(crate) async fn execute(
_backend: &Backend,
command: &str,
args: Vec<serde_json::Value>,
) -> Result<Option<WorkspaceEdit>, String> {
match command {
"nuwiki.file.delete" => file_delete(args),
other => Err(format!("unknown nuwiki command: {other}")),
}
}
fn file_delete(args: Vec<serde_json::Value>) -> Result<Option<WorkspaceEdit>, String> {
#[derive(Deserialize)]
struct Args {
uri: Url,
}
let raw = args
.into_iter()
.next()
.ok_or_else(|| "nuwiki.file.delete: missing arguments".to_string())?;
let parsed: Args = serde_json::from_value(raw)
.map_err(|e| format!("nuwiki.file.delete: invalid args — {e}"))?;
let mut builder = WorkspaceEditBuilder::new();
builder.file_op(op_delete(parsed.uri));
Ok(Some(builder.build()))
}