fix(lsp): resolve wikilinks that include the .wiki file extension
`[[page.wiki]]` should resolve identically to `[[page]]`. The index keyed by stem, so links with the explicit extension fell through to the broken-link diagnostic and to-page navigation failed. Strip the configured extension before every page lookup (resolve, definition, backlinks, classify_link, classify_outgoing) and stop appending the extension in synthesise_page_uri when it's already present. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -96,6 +96,10 @@ pub struct WorkspaceIndex {
|
||||
/// 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>,
|
||||
/// File extension for this wiki's pages (e.g. `".wiki"` or `"wiki"`).
|
||||
/// Used to normalise link targets that include the extension so that
|
||||
/// `[[page.wiki]]` resolves the same way as `[[page]]`.
|
||||
pub file_extension: Option<String>,
|
||||
pub pages_by_uri: HashMap<Url, IndexedPage>,
|
||||
pub pages_by_name: HashMap<String, Url>,
|
||||
pub backlinks: HashMap<String, Vec<Backlink>>,
|
||||
@@ -118,6 +122,19 @@ impl WorkspaceIndex {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_file_extension(mut self, ext: Option<String>) -> Self {
|
||||
self.file_extension = ext;
|
||||
self
|
||||
}
|
||||
|
||||
/// Strip the wiki's configured file extension from a link target path.
|
||||
/// Handles both `"page.wiki"` → `"page"` and `"subdir/page.wiki"` →
|
||||
/// `"subdir/page"`. Returns `path` unchanged when the extension doesn't
|
||||
/// match or no extension is configured.
|
||||
pub fn strip_link_extension<'a>(&self, path: &'a str) -> &'a str {
|
||||
strip_wiki_extension(path, self.file_extension.as_deref())
|
||||
}
|
||||
|
||||
/// Insert or update an indexed page. Removes any prior indexing for
|
||||
/// the same URI first (handles renames and re-parses without leaking
|
||||
/// stale backlinks).
|
||||
@@ -142,10 +159,12 @@ impl WorkspaceIndex {
|
||||
};
|
||||
index_blocks(&ast.children, &mut page);
|
||||
|
||||
let ext = self.file_extension.as_deref();
|
||||
for link in &page.outgoing {
|
||||
if let Some(tgt) = &link.target_page {
|
||||
let norm = strip_wiki_extension(tgt, ext);
|
||||
self.backlinks
|
||||
.entry(tgt.clone())
|
||||
.entry(norm.to_string())
|
||||
.or_default()
|
||||
.push(Backlink {
|
||||
source_uri: uri.clone(),
|
||||
@@ -199,10 +218,10 @@ impl WorkspaceIndex {
|
||||
/// needed for `AnchorOnly` resolution.
|
||||
pub fn resolve(&self, target: &LinkTarget, source_page: &str) -> Option<&Url> {
|
||||
match target.kind {
|
||||
LinkKind::Wiki => target
|
||||
.path
|
||||
.as_deref()
|
||||
.and_then(|p| self.pages_by_name.get(p)),
|
||||
LinkKind::Wiki => target.path.as_deref().and_then(|p| {
|
||||
let normalized = self.strip_link_extension(p);
|
||||
self.pages_by_name.get(normalized)
|
||||
}),
|
||||
LinkKind::AnchorOnly => self.pages_by_name.get(source_page),
|
||||
// Interwiki / Diary / File / Local / Raw aren't resolved
|
||||
// against the workspace index here. The handlers fall back to
|
||||
@@ -467,6 +486,30 @@ fn inline_to_text_into(inlines: &[InlineNode], out: &mut String) {
|
||||
|
||||
// ===== Filesystem walking =====
|
||||
|
||||
/// Strip a wiki file extension from a link target path without allocating.
|
||||
///
|
||||
/// `file_extension` may be `".wiki"` (with leading dot) or `"wiki"` (without).
|
||||
/// Returns `path` unchanged when no extension is configured, the extension is
|
||||
/// empty, or the path doesn't end with `.{ext}`.
|
||||
pub fn strip_wiki_extension<'a>(path: &'a str, file_extension: Option<&str>) -> &'a str {
|
||||
let ext = match file_extension {
|
||||
Some(e) => e.trim_start_matches('.'),
|
||||
None => return path,
|
||||
};
|
||||
if ext.is_empty() {
|
||||
return path;
|
||||
}
|
||||
let dot_ext_len = ext.len() + 1; // +1 for the '.'
|
||||
if path.len() > dot_ext_len
|
||||
&& path.ends_with(ext)
|
||||
&& path.as_bytes()[path.len() - dot_ext_len] == b'.'
|
||||
{
|
||||
&path[..path.len() - dot_ext_len]
|
||||
} else {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively collect every `.wiki` file under `root`, skipping dotfiles
|
||||
/// and dot-directories.
|
||||
pub fn walk_wiki_files(root: &Path) -> Vec<PathBuf> {
|
||||
|
||||
Reference in New Issue
Block a user