phase 13: workspace/rename + executeCommand router + nuwiki.file.delete
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:
@@ -21,10 +21,12 @@
|
||||
//! The workspace scan runs in a background tokio task spawned from
|
||||
//! `initialize` (P8) with `window/workDoneProgress` begin/end events.
|
||||
|
||||
pub mod commands;
|
||||
pub mod config;
|
||||
pub mod edits;
|
||||
pub mod index;
|
||||
pub mod nav;
|
||||
pub mod rename;
|
||||
pub mod semantic_tokens;
|
||||
pub mod wiki;
|
||||
|
||||
@@ -40,16 +42,16 @@ use tower_lsp::lsp_types::{
|
||||
CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse,
|
||||
Diagnostic, DiagnosticSeverity, DidChangeConfigurationParams, DidChangeTextDocumentParams,
|
||||
DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentSymbol, DocumentSymbolParams,
|
||||
DocumentSymbolResponse, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents,
|
||||
HoverParams, HoverProviderCapability, InitializeParams, InitializeResult, InitializedParams,
|
||||
Location, MarkupContent, MarkupKind, MessageType, OneOf, PositionEncodingKind, ProgressParams,
|
||||
ProgressParamsValue, ProgressToken, Range, ReferenceParams, SemanticTokens,
|
||||
SemanticTokensFullOptions, SemanticTokensOptions, SemanticTokensParams,
|
||||
SemanticTokensRangeParams, SemanticTokensRangeResult, SemanticTokensResult,
|
||||
SemanticTokensServerCapabilities, ServerCapabilities, ServerInfo,
|
||||
SymbolInformation as LspSymbolInformation, SymbolKind, TextDocumentSyncCapability,
|
||||
TextDocumentSyncKind, Url, WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressEnd,
|
||||
WorkDoneProgressOptions, WorkspaceSymbolParams,
|
||||
DocumentSymbolResponse, ExecuteCommandOptions, ExecuteCommandParams, GotoDefinitionParams,
|
||||
GotoDefinitionResponse, Hover, HoverContents, HoverParams, HoverProviderCapability,
|
||||
InitializeParams, InitializeResult, InitializedParams, Location, MarkupContent, MarkupKind,
|
||||
MessageType, OneOf, PositionEncodingKind, ProgressParams, ProgressParamsValue, ProgressToken,
|
||||
Range, ReferenceParams, RenameParams, SemanticTokens, SemanticTokensFullOptions,
|
||||
SemanticTokensOptions, SemanticTokensParams, SemanticTokensRangeParams,
|
||||
SemanticTokensRangeResult, SemanticTokensResult, SemanticTokensServerCapabilities,
|
||||
ServerCapabilities, ServerInfo, SymbolInformation as LspSymbolInformation, SymbolKind,
|
||||
TextDocumentSyncCapability, TextDocumentSyncKind, Url, WorkDoneProgress, WorkDoneProgressBegin,
|
||||
WorkDoneProgressEnd, WorkDoneProgressOptions, WorkspaceEdit, WorkspaceSymbolParams,
|
||||
};
|
||||
use tower_lsp::{Client, LanguageServer, LspService, Server};
|
||||
|
||||
@@ -147,6 +149,25 @@ impl Backend {
|
||||
wikis.first().cloned()
|
||||
}
|
||||
|
||||
/// If the cursor sits on a wikilink, return the linked page's URI so
|
||||
/// `rename` operates on the *target* (matches vimwiki's behaviour).
|
||||
/// Otherwise return `None` and the caller falls back to the buffer's
|
||||
/// own URI.
|
||||
fn rename_target_uri(&self, uri: &Url, pos: tower_lsp::lsp_types::Position) -> Option<Url> {
|
||||
let doc = self.documents.get(uri)?;
|
||||
let utf8 = self.use_utf8.load(Ordering::Relaxed);
|
||||
let (line, col) = nav::lsp_to_byte_pos(pos, &doc.text, utf8);
|
||||
let node = nav::find_inline_at(&doc.ast, line, col)?;
|
||||
let wikilink = match node {
|
||||
InlineNode::WikiLink(w) => w,
|
||||
_ => return None,
|
||||
};
|
||||
let wiki = self.wiki_for_uri(uri)?;
|
||||
let source_page = index::page_name_from_uri(uri, Some(&wiki.config.root));
|
||||
let idx = wiki.index.read().ok()?;
|
||||
idx.resolve(&wikilink.target, &source_page).cloned()
|
||||
}
|
||||
|
||||
/// Resolve `uri` to the wiki whose root is the longest prefix of it.
|
||||
/// Falls back to the default wiki when no root matches — that's the
|
||||
/// usual case for ad-hoc `.wiki` files outside any registered wiki.
|
||||
@@ -163,10 +184,6 @@ impl Backend {
|
||||
/// cross-document operations (Phase 13 `workspace/rename` and link
|
||||
/// health) that need accurate spans + text without trusting cached
|
||||
/// `WorkspaceIndex` data.
|
||||
///
|
||||
/// Scaffolded ahead of the Phase 13 caller — `#[allow(dead_code)]`
|
||||
/// drops once Phase 13 wires it in.
|
||||
#[allow(dead_code)]
|
||||
pub async fn read_or_open(&self, uri: &Url) -> io::Result<DocSnapshot> {
|
||||
if let Some(doc) = self.documents.get(uri) {
|
||||
return Ok(DocSnapshot {
|
||||
@@ -278,6 +295,14 @@ impl LanguageServer for Backend {
|
||||
workspace_symbol_provider: Some(OneOf::Left(true)),
|
||||
definition_provider: Some(OneOf::Left(true)),
|
||||
references_provider: Some(OneOf::Left(true)),
|
||||
rename_provider: Some(OneOf::Left(true)),
|
||||
execute_command_provider: Some(ExecuteCommandOptions {
|
||||
commands: commands::COMMANDS
|
||||
.iter()
|
||||
.map(|s| (*s).to_string())
|
||||
.collect(),
|
||||
work_done_progress_options: WorkDoneProgressOptions::default(),
|
||||
}),
|
||||
hover_provider: Some(HoverProviderCapability::Simple(true)),
|
||||
completion_provider: Some(CompletionOptions {
|
||||
trigger_characters: Some(vec!["[".into()]),
|
||||
@@ -596,6 +621,45 @@ impl LanguageServer for Backend {
|
||||
Ok(Some(CompletionResponse::Array(items)))
|
||||
}
|
||||
|
||||
async fn rename(&self, params: RenameParams) -> LspResult<Option<WorkspaceEdit>> {
|
||||
let uri = params.text_document_position.text_document.uri;
|
||||
let pos = params.text_document_position.position;
|
||||
let utf8 = self.use_utf8.load(Ordering::Relaxed);
|
||||
|
||||
// If the cursor sits on a wikilink, rename the *linked* page;
|
||||
// otherwise rename the buffer's own file. Closed-doc reads happen
|
||||
// inside `compute_rename` via `read_or_open`.
|
||||
let target_uri = self.rename_target_uri(&uri, pos).unwrap_or(uri);
|
||||
let edit = rename::compute_rename(self, target_uri, params.new_name, utf8).await;
|
||||
Ok(edit)
|
||||
}
|
||||
|
||||
async fn execute_command(
|
||||
&self,
|
||||
params: ExecuteCommandParams,
|
||||
) -> LspResult<Option<serde_json::Value>> {
|
||||
match commands::execute(self, ¶ms.command, params.arguments).await {
|
||||
Ok(Some(edit)) => {
|
||||
if let Err(e) = self.client.apply_edit(edit).await {
|
||||
self.client
|
||||
.log_message(
|
||||
MessageType::ERROR,
|
||||
format!("nuwiki: apply_edit failed — {e}"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
Ok(None) => Ok(None),
|
||||
Err(msg) => {
|
||||
self.client
|
||||
.log_message(MessageType::ERROR, format!("nuwiki: {msg}"))
|
||||
.await;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn symbol(
|
||||
&self,
|
||||
params: WorkspaceSymbolParams,
|
||||
|
||||
Reference in New Issue
Block a user