backfill: tag commands + file-operations capability
SPEC audit of Phases 12–13 surfaced two gaps. Closing them here so the
v1.1 command surface and capability advertisement match what §12.3 and
§12.4 promise.
Phase 12 — tag commands (§12.3):
- `nuwiki.tags.search` — case-insensitive substring filter over the
workspace tag index. Returns `[{ name, uri, range, scope, page }]`
where `scope` is one of `file` / `heading` / `standalone`.
- `nuwiki.tags.generateLinks` — generates/refreshes a tagged-pages
section. Two modes:
- `{ uri, tag }` → `= Tag: <name> =` + flat list of `[[Page]]`
entries for every page tagged with `<name>`.
- `{ uri }` (no tag) → `= Tags =` umbrella section with `== <tag>
==` subgroups, mirroring `:VimwikiGenerateTagLinks` (no arg).
Idempotent: re-runs replace the existing section by
case-insensitive heading match.
- `nuwiki.tags.rebuild` — synchronous full re-walk of the wiki root.
Drops index entries for pages whose files no longer exist on disk,
re-parses every `.wiki`, and returns `{ pages, files_seen,
stale_removed }`. Useful for cleaning up after out-of-band edits
the LSP didn't see.
Ops layer:
- `tag_hits(index, query) → Vec<TagHit>` — pure filter, sorted by
(name, page, line).
- `tag_pages_snapshot(index) → BTreeMap<tag, [pages]>` — taken under
the read lock so the dispatcher can release the lock before
building the edit.
- `build_tag_links_text` / `tag_links_edit` — text-level rendering
and the WorkspaceEdit producer.
Phase 13 — file-operations capability (§12.4):
- `ServerCapabilities.workspace.file_operations` now advertises
`did_rename` + `did_delete` with filters scoped to `**/*.wiki`
and `**/*.md`. (will_* deferred — those return a WorkspaceEdit
and we'd want to wire them into the rename pipeline before
promising support.)
- `did_rename_files` handler — moves the live DocumentState forward
to the new URI when the file was open, then refreshes the index
(remove old URI, upsert new via read_or_open).
- `did_delete_files` handler — drops both DocumentState and index
entry so backlink lookups don't surface dangling source URIs.
Tests: 15 new in `phase17_backfill.rs` covering tag hit filtering &
ordering, snapshot dedup, single-tag and full-index render shapes,
edit replace-vs-insert flow, JSON shape of `TagHit`, and the COMMANDS
list. Total 324 tests pass; fmt + clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,18 +42,21 @@ use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tower_lsp::jsonrpc::Result as LspResult;
|
||||
use tower_lsp::lsp_types::{
|
||||
CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse,
|
||||
Diagnostic, DiagnosticSeverity, DidChangeConfigurationParams, DidChangeTextDocumentParams,
|
||||
DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentSymbol, DocumentSymbolParams,
|
||||
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,
|
||||
DeleteFilesParams, Diagnostic, DiagnosticSeverity, DidChangeConfigurationParams,
|
||||
DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
|
||||
DocumentSymbol, DocumentSymbolParams, DocumentSymbolResponse, ExecuteCommandOptions,
|
||||
ExecuteCommandParams, FileOperationFilter, FileOperationPattern, FileOperationPatternKind,
|
||||
FileOperationRegistrationOptions, GotoDefinitionParams, GotoDefinitionResponse, Hover,
|
||||
HoverContents, HoverParams, HoverProviderCapability, InitializeParams, InitializeResult,
|
||||
InitializedParams, Location, MarkupContent, MarkupKind, MessageType, OneOf,
|
||||
PositionEncodingKind, ProgressParams, ProgressParamsValue, ProgressToken, Range,
|
||||
ReferenceParams, RenameFilesParams, RenameParams, SemanticTokens, SemanticTokensFullOptions,
|
||||
SemanticTokensOptions, SemanticTokensParams, SemanticTokensRangeParams,
|
||||
SemanticTokensRangeResult, SemanticTokensResult, SemanticTokensServerCapabilities,
|
||||
ServerCapabilities, ServerInfo, SymbolInformation as LspSymbolInformation, SymbolKind,
|
||||
TextDocumentSyncCapability, TextDocumentSyncKind, Url, WorkDoneProgress, WorkDoneProgressBegin,
|
||||
WorkDoneProgressEnd, WorkDoneProgressOptions, WorkspaceEdit, WorkspaceSymbolParams,
|
||||
WorkDoneProgressEnd, WorkDoneProgressOptions, WorkspaceEdit,
|
||||
WorkspaceFileOperationsServerCapabilities, WorkspaceServerCapabilities, WorkspaceSymbolParams,
|
||||
};
|
||||
use tower_lsp::{Client, LanguageServer, LspService, Server};
|
||||
|
||||
@@ -320,6 +323,10 @@ impl LanguageServer for Backend {
|
||||
},
|
||||
),
|
||||
),
|
||||
workspace: Some(WorkspaceServerCapabilities {
|
||||
workspace_folders: None,
|
||||
file_operations: Some(wiki_file_operations()),
|
||||
}),
|
||||
..ServerCapabilities::default()
|
||||
},
|
||||
server_info: Some(ServerInfo {
|
||||
@@ -387,6 +394,52 @@ impl LanguageServer for Backend {
|
||||
}
|
||||
}
|
||||
|
||||
async fn did_rename_files(&self, params: RenameFilesParams) {
|
||||
// Phase 13 advertises 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
|
||||
// didOpen on the renamed buffer.
|
||||
for fr in ¶ms.files {
|
||||
let Ok(old_uri) = Url::parse(&fr.old_uri) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(new_uri) = Url::parse(&fr.new_uri) else {
|
||||
continue;
|
||||
};
|
||||
// Pull the live document forward if it was open under the old
|
||||
// URI. Otherwise read the new path from disk and parse.
|
||||
if let Some((_, state)) = self.documents.remove(&old_uri) {
|
||||
self.documents.insert(new_uri.clone(), state);
|
||||
}
|
||||
if let Some(wiki) = self.wiki_for_uri(&new_uri) {
|
||||
let mut idx = wiki.index.write().expect("index lock poisoned");
|
||||
idx.remove(&old_uri);
|
||||
}
|
||||
if let Ok(snapshot) = self.read_or_open(&new_uri).await {
|
||||
if let Some(wiki) = self.wiki_for_uri(&new_uri) {
|
||||
let mut idx = wiki.index.write().expect("index lock poisoned");
|
||||
idx.upsert(new_uri.clone(), &snapshot.ast);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn did_delete_files(&self, params: DeleteFilesParams) {
|
||||
// Mirror of did_rename: drop the index entry so backlink lookups
|
||||
// stop returning the now-missing source URI.
|
||||
for f in ¶ms.files {
|
||||
let Ok(uri) = Url::parse(&f.uri) else {
|
||||
continue;
|
||||
};
|
||||
self.documents.remove(&uri);
|
||||
if let Some(wiki) = self.wiki_for_uri(&uri) {
|
||||
let mut idx = wiki.index.write().expect("index lock poisoned");
|
||||
idx.remove(&uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn did_close(&self, params: DidCloseTextDocumentParams) {
|
||||
let uri = ¶ms.text_document.uri;
|
||||
self.documents.remove(uri);
|
||||
@@ -713,6 +766,43 @@ impl LanguageServer for Backend {
|
||||
}
|
||||
}
|
||||
|
||||
/// File-operation capability for `.wiki` files. Tells the client to
|
||||
/// notify us before/after the user renames or deletes files via the file
|
||||
/// explorer (or any other out-of-buffer trigger). Filters scope these
|
||||
/// notifications to wiki sources so we don't see every C++ rename in a
|
||||
/// polyglot workspace.
|
||||
fn wiki_file_operations() -> WorkspaceFileOperationsServerCapabilities {
|
||||
let filters = vec![
|
||||
FileOperationFilter {
|
||||
scheme: Some("file".into()),
|
||||
pattern: FileOperationPattern {
|
||||
glob: "**/*.wiki".into(),
|
||||
matches: Some(FileOperationPatternKind::File),
|
||||
options: None,
|
||||
},
|
||||
},
|
||||
FileOperationFilter {
|
||||
scheme: Some("file".into()),
|
||||
pattern: FileOperationPattern {
|
||||
glob: "**/*.md".into(),
|
||||
matches: Some(FileOperationPatternKind::File),
|
||||
options: None,
|
||||
},
|
||||
},
|
||||
];
|
||||
let opts = || FileOperationRegistrationOptions {
|
||||
filters: filters.clone(),
|
||||
};
|
||||
WorkspaceFileOperationsServerCapabilities {
|
||||
did_create: None,
|
||||
will_create: None,
|
||||
did_rename: Some(opts()),
|
||||
will_rename: None,
|
||||
did_delete: Some(opts()),
|
||||
will_delete: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn pack_data(flat: &[u32]) -> Vec<tower_lsp::lsp_types::SemanticToken> {
|
||||
flat.chunks_exact(5)
|
||||
.map(|c| tower_lsp::lsp_types::SemanticToken {
|
||||
|
||||
Reference in New Issue
Block a user