fix(lsp): resolve wikilinks source-relative first, with .. collapsing
`[[llm-wiki-pattern]]` in `tips/index.wiki` should resolve to
`tips/llm-wiki-pattern.wiki` — vimwiki's default is source-relative
for non-absolute targets. The index keyed every page by its
workspace-relative path, so the link looked up `llm-wiki-pattern`
(root-relative) and missed the sibling. Goto-definition then
synthesised a non-existent root URL, and `nuwiki.link` diagnostics
flagged every such link as missing-page.
- `WorkspaceIndex::resolve_wiki_path` and `page_for_wiki_target`
centralise the lookup: try `{source_dir}/{path}` first, fall back
to root-relative. Used by goto-definition, diagnostics, and the
workspace `checkLinks` walker.
- `collapse_dots` resolves `.` / `..` segments before lookup so
`[[../Other]]` from `posts/foo` correctly hits `Other` at the
wiki root (and is reported broken if no such page exists, rather
than silently going to nowhere).
- `canonical_link_name` applies the same rule when keying the
backlinks index, so backlinks queried by the resolved page name
pick up references that were written source-relative.
- `OutgoingLink` carries `is_absolute` now so `classify_outgoing`
(driven from cached index data, not the live AST) gets the same
treatment without re-parsing the source.
- `classify_outgoing`'s File/Local branch now reuses
`resolve_file_path` instead of duplicating the `//absolute` /
relative-to-source logic inline.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -466,6 +466,112 @@ fn collect_wiki_links_finds_nested() {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Source-relative wiki links (vimwiki default) =====
|
||||
|
||||
#[test]
|
||||
fn wiki_link_resolves_source_relative_first() {
|
||||
let root = "/tmp/srcrel1";
|
||||
let idx = build_index(
|
||||
root,
|
||||
&[
|
||||
("tips/index", "[[llm-wiki-pattern|LLM]]\n"),
|
||||
("tips/llm-wiki-pattern", "= LLM =\n"),
|
||||
],
|
||||
);
|
||||
let src = "[[llm-wiki-pattern|LLM]]\n";
|
||||
let ast = parse(src);
|
||||
let diags = diagnostics::link_health(
|
||||
&ast,
|
||||
src,
|
||||
Some(&Url::from_file_path(format!("{root}/tips/index.wiki")).unwrap()),
|
||||
&idx,
|
||||
"tips/index",
|
||||
LinkSeverity::Warning,
|
||||
true,
|
||||
);
|
||||
assert!(diags.is_empty(), "expected no diagnostics, got: {:?}", diags);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wiki_link_falls_back_to_root_relative() {
|
||||
// A source-relative miss should fall through to root-relative — this
|
||||
// keeps `[[posts/foo]]` from `index.wiki` working even though there's
|
||||
// no `posts/posts/foo`.
|
||||
let root = "/tmp/srcrel2";
|
||||
let idx = build_index(
|
||||
root,
|
||||
&[
|
||||
("index", "[[posts/foo|Foo]]\n"),
|
||||
("posts/foo", "= Foo =\n"),
|
||||
],
|
||||
);
|
||||
let src = "[[posts/foo|Foo]]\n";
|
||||
let ast = parse(src);
|
||||
let diags = diagnostics::link_health(
|
||||
&ast,
|
||||
src,
|
||||
Some(&home_uri(root)),
|
||||
&idx,
|
||||
"index",
|
||||
LinkSeverity::Warning,
|
||||
true,
|
||||
);
|
||||
assert!(diags.is_empty(), "got: {:?}", diags);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wiki_link_absolute_skips_source_relative() {
|
||||
// `[[/Page]]` is explicitly root-relative even from a subdir.
|
||||
let root = "/tmp/srcrel3";
|
||||
let idx = build_index(
|
||||
root,
|
||||
&[
|
||||
("tips/index", "[[/RootPage]]\n"),
|
||||
("RootPage", "= Root =\n"),
|
||||
// Intentionally also create tips/RootPage so a source-relative
|
||||
// resolution would (wrongly) prefer it — the absolute form must
|
||||
// ignore source-relative.
|
||||
("tips/RootPage", "= Tips Root =\n"),
|
||||
],
|
||||
);
|
||||
let target = idx
|
||||
.resolve_wiki_path("RootPage", "tips/index", true)
|
||||
.expect("absolute link should resolve");
|
||||
assert!(
|
||||
target.as_str().ends_with("RootPage.wiki"),
|
||||
"expected root RootPage, got {target}",
|
||||
);
|
||||
assert!(
|
||||
!target.as_str().contains("/tips/RootPage"),
|
||||
"absolute link should skip tips/RootPage, got {target}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wiki_link_parent_dir_collapses() {
|
||||
// `[[../Other]]` from `posts/foo` resolves to `Other` at the wiki root.
|
||||
let root = "/tmp/srcrel4";
|
||||
let idx = build_index(
|
||||
root,
|
||||
&[
|
||||
("posts/foo", "[[../Other|Other]]\n"),
|
||||
("Other", "= Other =\n"),
|
||||
],
|
||||
);
|
||||
let src = "[[../Other|Other]]\n";
|
||||
let ast = parse(src);
|
||||
let diags = diagnostics::link_health(
|
||||
&ast,
|
||||
src,
|
||||
Some(&Url::from_file_path(format!("{root}/posts/foo.wiki")).unwrap()),
|
||||
&idx,
|
||||
"posts/foo",
|
||||
LinkSeverity::Warning,
|
||||
true,
|
||||
);
|
||||
assert!(diags.is_empty(), "got: {:?}", diags);
|
||||
}
|
||||
|
||||
// ===== Anchor matching against raw heading text =====
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user