Skip to main content

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, &current_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, &current_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}