c63ec679ae
Consume several per-wiki config keys that the server deserialized but
ignored, and remove keys whose effect is purely client-side:
- diagnostics: re-publish open-doc diagnostics on
didChangeConfiguration so a link_severity change takes effect
immediately; fix the single-key unwrap in apply_change so a minimal
`{ diagnostic: {...} }` payload isn't mistaken for a namespace wrapper.
- auto_toc: rebuild an existing TOC section on save (new
ops::toc_rebuild_edit, no-op when the page has no TOC).
- diary index: honour diary_header, diary_sort and diary_caption_level
when rendering the diary index body.
- links_space_char: apply on rename so spaces in the link target and
the on-disk path become the configured glyph (default " " = verbatim).
- list_margin: thread the per-wiki value into render_page_html.
- remove nested_syntaxes, maxhi and diary_start_week_day from the server
config: nested-syntax and heading highlighting are client-side, and
the weekly diary is ISO-week based so a custom week start has no clean
server-side meaning.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
136 lines
4.7 KiB
Rust
136 lines
4.7 KiB
Rust
//! `textDocument/rename` — server-side rename with cross-document link
|
|
//! rewriting.
|
|
//!
|
|
//! 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 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.
|
|
//! 4. Add the `RenameFile` op last so the builder emits it before content
|
|
//! edits in `documentChanges` ordering.
|
|
|
|
use std::path::Path;
|
|
|
|
use tower_lsp::lsp_types::{Url, WorkspaceEdit};
|
|
|
|
use crate::edits::{op_rename, text_edit_replace, WorkspaceEditBuilder};
|
|
use crate::index::page_name_from_uri;
|
|
use crate::Backend;
|
|
|
|
/// Compute the `WorkspaceEdit` for renaming `target_uri` to `new_name`.
|
|
///
|
|
/// Returns `None` when:
|
|
/// - the URI doesn't belong to any registered wiki,
|
|
/// - the new URI couldn't be built from the wiki root + new_name,
|
|
/// - the new URI is the same as the old one.
|
|
pub(crate) async fn compute_rename(
|
|
backend: &Backend,
|
|
target_uri: Url,
|
|
new_name: String,
|
|
utf8: bool,
|
|
) -> Option<WorkspaceEdit> {
|
|
let wiki = backend.wiki_for_uri(&target_uri)?;
|
|
let old_name = page_name_from_uri(&target_uri, Some(&wiki.config.root));
|
|
// Spaces in the link *target* (and the file on disk) are replaced with
|
|
// the wiki's `links_space_char`; descriptions keep their original text.
|
|
let new_name = apply_links_space_char(&new_name, &wiki.config.links_space_char);
|
|
let new_uri = build_new_uri(&wiki.config.root, &new_name, &wiki.config.file_extension)?;
|
|
if new_uri == target_uri {
|
|
return None;
|
|
}
|
|
|
|
let mut builder = WorkspaceEditBuilder::new();
|
|
builder.file_op(op_rename(target_uri.clone(), new_uri));
|
|
|
|
// Snapshot the backlinks so the index read lock is released before we
|
|
// await any disk I/O.
|
|
let backlinks = {
|
|
let idx = wiki.index.read().ok()?;
|
|
idx.backlinks_for(&old_name).to_vec()
|
|
};
|
|
|
|
for bl in backlinks {
|
|
let Ok(snapshot) = backend.read_or_open(&bl.source_uri).await else {
|
|
continue;
|
|
};
|
|
let start = bl.source_span.start.offset.min(snapshot.text.len());
|
|
let end = bl.source_span.end.offset.min(snapshot.text.len());
|
|
if start >= end {
|
|
continue;
|
|
}
|
|
let original = &snapshot.text[start..end];
|
|
if let Some(new_link) = rewrite_wikilink_target(original, &new_name) {
|
|
let edit = text_edit_replace(bl.source_span, new_link, &snapshot.text, utf8);
|
|
builder.edit(bl.source_uri.clone(), edit);
|
|
}
|
|
}
|
|
|
|
if builder.is_empty() {
|
|
None
|
|
} else {
|
|
Some(builder.build())
|
|
}
|
|
}
|
|
|
|
/// Replace spaces in a link path with the configured `links_space_char`.
|
|
/// The default `" "` is an identity transform (spaces kept verbatim).
|
|
pub fn apply_links_space_char(name: &str, space_char: &str) -> String {
|
|
if space_char == " " {
|
|
name.to_string()
|
|
} else {
|
|
name.replace(' ', space_char)
|
|
}
|
|
}
|
|
|
|
/// Build `<root>/<new_name>(<ext>)` as a `file://` URL.
|
|
pub fn build_new_uri(root: &Path, new_name: &str, ext: &str) -> Option<Url> {
|
|
let mut path = root.to_path_buf();
|
|
for part in new_name.split(['/', '\\']) {
|
|
if !part.is_empty() {
|
|
path.push(part);
|
|
}
|
|
}
|
|
let path_str = path.to_string_lossy().to_string();
|
|
let with_ext = if path_str.ends_with(ext) {
|
|
path_str
|
|
} else {
|
|
format!("{path_str}{ext}")
|
|
};
|
|
Url::from_file_path(with_ext).ok()
|
|
}
|
|
|
|
/// Rewrite a `[[...]]` source segment so the path portion becomes
|
|
/// `new_path`, keeping any `#anchor` and `|description` parts intact.
|
|
///
|
|
/// Returns `None` if `segment` doesn't look like a wikilink (starts with
|
|
/// `[[`, ends with `]]`).
|
|
pub fn rewrite_wikilink_target(segment: &str, new_path: &str) -> Option<String> {
|
|
if !segment.starts_with("[[") || !segment.ends_with("]]") || segment.len() < 4 {
|
|
return None;
|
|
}
|
|
let inner = &segment[2..segment.len() - 2];
|
|
let (path_part, description) = match inner.find('|') {
|
|
Some(i) => (&inner[..i], Some(&inner[i + 1..])),
|
|
None => (inner, None),
|
|
};
|
|
let anchor = path_part.find('#').map(|i| &path_part[i + 1..]);
|
|
|
|
let mut out = String::with_capacity(segment.len());
|
|
out.push_str("[[");
|
|
out.push_str(new_path);
|
|
if let Some(a) = anchor {
|
|
out.push('#');
|
|
out.push_str(a);
|
|
}
|
|
if let Some(d) = description {
|
|
out.push('|');
|
|
out.push_str(d);
|
|
}
|
|
out.push_str("]]");
|
|
Some(out)
|
|
}
|