2026-05-13 01:30:55 +00:00
|
|
|
//! Link-related LSP commands.
|
|
|
|
|
//!
|
|
|
|
|
//! Covers:
|
|
|
|
|
//! - `nuwiki.link.pasteWikilink` / `nuwiki.link.pasteUrl` — page-name
|
|
|
|
|
//! synthesis from a URI and the textual shape the handler inserts
|
|
|
|
|
//! at the cursor.
|
|
|
|
|
//! - Wikilink resolution, including the regression where `<CR>` on a
|
|
|
|
|
//! wikilink to a not-yet-created page must still resolve to a URI
|
|
|
|
|
//! so the editor can open (and on save create) the future page.
|
|
|
|
|
//! The resolver itself is private; we exercise the synthesised-path
|
|
|
|
|
//! semantics via the public config helpers + index lookup.
|
|
|
|
|
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
|
|
use nuwiki_core::ast::LinkKind;
|
|
|
|
|
use nuwiki_core::syntax::vimwiki::VimwikiSyntax;
|
|
|
|
|
use nuwiki_core::syntax::SyntaxPlugin;
|
|
|
|
|
use nuwiki_lsp::config::WikiConfig;
|
|
|
|
|
use nuwiki_lsp::index::{page_name_from_uri, WorkspaceIndex};
|
|
|
|
|
use tower_lsp::lsp_types::Url;
|
|
|
|
|
|
|
|
|
|
fn parse(src: &str) -> nuwiki_core::ast::DocumentNode {
|
|
|
|
|
VimwikiSyntax::new().parse(src)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== paste helpers =====
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn commands_list_advertises_link_helpers() {
|
|
|
|
|
let names: Vec<&str> = nuwiki_lsp::commands::COMMANDS.to_vec();
|
2026-05-30 17:32:41 -03:00
|
|
|
for name in [
|
|
|
|
|
"nuwiki.link.pasteWikilink",
|
|
|
|
|
"nuwiki.link.pasteUrl",
|
|
|
|
|
"nuwiki.link.catUrl",
|
2026-06-05 01:23:42 +00:00
|
|
|
// Both GenerateLinks forms must be advertised so coc/vim-lsp accept
|
|
|
|
|
// them (the scoped form was previously sent but unregistered).
|
|
|
|
|
"nuwiki.links.generate",
|
|
|
|
|
"nuwiki.links.generateForPath",
|
2026-05-30 17:32:41 -03:00
|
|
|
] {
|
2026-05-13 01:30:55 +00:00
|
|
|
assert!(names.contains(&name), "missing: {name}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn page_name_for_root_level_page() {
|
|
|
|
|
let cfg = WikiConfig::from_root(PathBuf::from("/tmp/wiki"));
|
|
|
|
|
let uri = Url::from_file_path("/tmp/wiki/Home.wiki").unwrap();
|
|
|
|
|
assert_eq!(page_name_from_uri(&uri, Some(&cfg.root)), "Home");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn page_name_for_subdir_page() {
|
|
|
|
|
let cfg = WikiConfig::from_root(PathBuf::from("/tmp/wiki"));
|
|
|
|
|
let uri = Url::from_file_path("/tmp/wiki/diary/2026-05-11.wiki").unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
page_name_from_uri(&uri, Some(&cfg.root)),
|
|
|
|
|
"diary/2026-05-11"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn paste_url_format_matches_export_output_path_for() {
|
|
|
|
|
// The URL the `pasteUrl` handler emits is `<name>.html`, where
|
|
|
|
|
// `<name>` walks path segments. Mirror that derivation and assert
|
|
|
|
|
// the shape we'd insert at cursor.
|
|
|
|
|
let name = "diary/2026-05-11";
|
|
|
|
|
let mut url = String::new();
|
|
|
|
|
for (i, seg) in name.split('/').enumerate() {
|
|
|
|
|
if i > 0 {
|
|
|
|
|
url.push('/');
|
|
|
|
|
}
|
|
|
|
|
url.push_str(seg);
|
|
|
|
|
}
|
|
|
|
|
url.push_str(".html");
|
|
|
|
|
assert_eq!(url, "diary/2026-05-11.html");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn paste_wikilink_shape_for_root_page() {
|
|
|
|
|
// `[[<name>]]` is what the handler inserts at the cursor.
|
|
|
|
|
let name = "Notes";
|
|
|
|
|
assert_eq!(format!("[[{name}]]"), "[[Notes]]");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== resolve follow-link to non-existent page =====
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn link_to_indexed_page_resolves_via_index() {
|
|
|
|
|
let root = "/tmp/follow-link-1";
|
|
|
|
|
let mut idx = WorkspaceIndex::new(Some(PathBuf::from(root)));
|
|
|
|
|
let target = Url::from_file_path(format!("{root}/Notes.wiki")).unwrap();
|
|
|
|
|
idx.upsert(target.clone(), &parse("= notes =\n"));
|
|
|
|
|
|
|
|
|
|
let resolved = idx.pages_by_name.get("Notes").cloned();
|
|
|
|
|
assert_eq!(resolved, Some(target));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn missing_page_is_not_in_index() {
|
|
|
|
|
let root = "/tmp/follow-link-2";
|
|
|
|
|
let idx = WorkspaceIndex::new(Some(PathBuf::from(root)));
|
|
|
|
|
// Empty index — "Missing" isn't there, so the fallback synthesise
|
|
|
|
|
// path is the only way to follow `[[Missing]]`.
|
|
|
|
|
assert!(!idx.pages_by_name.contains_key("Missing"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn config_root_plus_path_plus_ext_is_a_valid_uri() {
|
|
|
|
|
// Sanity: the synthesise logic constructs <root>/<path>.<ext>.
|
|
|
|
|
let cfg = WikiConfig::from_root(PathBuf::from("/tmp/follow-link-3"));
|
|
|
|
|
assert_eq!(cfg.file_extension, ".wiki");
|
|
|
|
|
let target_path = cfg.root.join("Missing.wiki");
|
|
|
|
|
assert!(Url::from_file_path(&target_path).is_ok());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn wikilink_to_subdir_page_parses_with_slash_in_path() {
|
|
|
|
|
// Synthesis must walk path segments, not just concatenate.
|
|
|
|
|
let doc = parse("[[notes/Daily]]\n");
|
|
|
|
|
let inline = match &doc.children[0] {
|
|
|
|
|
nuwiki_core::ast::BlockNode::Paragraph(p) => &p.children[0],
|
|
|
|
|
_ => panic!("expected paragraph"),
|
|
|
|
|
};
|
|
|
|
|
let target = match inline {
|
|
|
|
|
nuwiki_core::ast::InlineNode::WikiLink(w) => &w.target,
|
|
|
|
|
_ => panic!("expected wikilink"),
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(target.kind, LinkKind::Wiki);
|
|
|
|
|
assert_eq!(target.path.as_deref(), Some("notes/Daily"));
|
|
|
|
|
}
|