bynk_lsp/symbols.rs
1//! Re-exports `bynk-ide`'s symbol logic (#808) — moved there so it's
2//! reachable from `bynk-wasm` too, which can't depend on this crate's
3//! `tower-lsp`/`tokio` stack. See `bynk_ide::symbols` for the real module.
4//!
5//! The one exception is the cross-file lookup trio below: `bynk-ide`'s
6//! versions are `PathBuf`-keyed (wasm has no `Url`), so this crate re-wraps
7//! them in the `Url` shape the LSP handlers actually use. Rust's shadowing
8//! rule lets these locally-defined names win over the glob import.
9pub use bynk_ide::symbols::*;
10
11use std::collections::HashMap;
12use std::path::{Path, PathBuf};
13
14use bynk_syntax::span::Span;
15use tower_lsp::lsp_types::Url;
16
17/// A cross-file declaration lookup result: the URI of the file containing
18/// the declaration, the declaration's source span, and the full source
19/// text of that file (returned because callers need it to convert the
20/// span to an LSP range and to build hover content).
21pub struct CrossFileSymbol {
22 pub uri: Url,
23 pub span: Span,
24 pub source: String,
25}
26
27/// Find `name`'s declaration in any project file other than `current_uri`.
28/// `files` is a pre-read `(path, content)` map (content-ownership track,
29/// #1086, slice 1) — see `bynk_ide::symbols::find_declaration_cross_file`
30/// for the pure logic.
31pub fn find_declaration_cross_file(
32 files: &HashMap<PathBuf, String>,
33 current_uri: &Url,
34 name: &str,
35) -> Option<CrossFileSymbol> {
36 let current_path = current_uri.to_file_path().ok()?;
37 let found = bynk_ide::symbols::find_declaration_cross_file(files, ¤t_path, name)?;
38 Some(CrossFileSymbol {
39 uri: Url::from_file_path(&found.path).ok()?,
40 span: found.span,
41 source: found.source,
42 })
43}
44
45/// Markdown hover content for `name` from any project file other than
46/// `current_uri`, plus the URI of the file that contributed it. See
47/// `bynk_ide::symbols::describe_symbol_cross_file` for the pure logic.
48pub fn describe_symbol_cross_file(
49 files: &HashMap<PathBuf, String>,
50 current_uri: &Url,
51 name: &str,
52) -> Option<(Url, String)> {
53 let current_path = current_uri.to_file_path().ok()?;
54 let (path, desc) = bynk_ide::symbols::describe_symbol_cross_file(files, ¤t_path, name)?;
55 Some((Url::from_file_path(&path).ok()?, desc))
56}
57
58/// #848: the `file://` target a resolved intra-doc link opens — the defining
59/// file, with a `#L<line>,<column>` fragment (1-based, the form VS Code's
60/// opener reads) placing the cursor on the declaration. Without the fragment
61/// a click only opens the file, which for a same-file link (the common case)
62/// does nothing visible. `snapshots` is the round's project-relative path →
63/// analysed text map, against which `def.span` is measured; a target missing
64/// from it still links, to the file alone.
65pub fn doc_link_target(
66 project_root: &Path,
67 def: &bynk_check::index::SiteRef,
68 snapshots: &HashMap<PathBuf, String>,
69) -> Option<Url> {
70 let mut url = Url::from_file_path(project_root.join(&def.path)).ok()?;
71 if let Some(text) = snapshots.get(&def.path) {
72 let pos = crate::position::offset_to_position(text, def.span.start);
73 url.set_fragment(Some(&format!("L{},{}", pos.line + 1, pos.character + 1)));
74 }
75 Some(url)
76}
77
78/// #848: rewrite every resolvable intra-doc-link candidate in `content`
79/// (hover Markdown — a ```bynk fenced signature, optionally followed by the
80/// declaration's doc-comment prose) into a Markdown link, resolved against
81/// `owner_unit`'s doc-link scope order (`crate::index_queries::resolve_doc_link`).
82/// The fenced signature block (and any author example fence) is never
83/// touched — `scan_doc_link_candidates` skips fenced regions itself. An
84/// unresolved candidate is left exactly as authored — no diagnostic,
85/// matching the render-only decision.
86pub fn linkify_doc_links(
87 content: &str,
88 index: &bynk_check::index::ProjectIndex,
89 doc_scope: &HashMap<String, Vec<String>>,
90 project_root: &Path,
91 snapshots: &HashMap<PathBuf, String>,
92 owner_unit: &str,
93) -> String {
94 let candidates = bynk_ide::symbols::scan_doc_link_candidates(content);
95 if candidates.is_empty() {
96 return content.to_string();
97 }
98 let mut out = String::with_capacity(content.len());
99 let mut last = 0;
100 for cand in candidates {
101 let Some(def) =
102 crate::index_queries::resolve_doc_link(index, doc_scope, owner_unit, &cand.name)
103 else {
104 continue;
105 };
106 let Some(url) = doc_link_target(project_root, def, snapshots) else {
107 continue;
108 };
109 out.push_str(&content[last..cand.span.start]);
110 out.push_str(&format!("[{}]({url})", cand.display));
111 last = cand.span.end;
112 }
113 out.push_str(&content[last..]);
114 out
115}