Skip to main content

solidity_language_server/
links.rs

1use crate::goto::{CachedBuild, bytes_to_pos};
2use crate::types::SourceLoc;
3use tower_lsp::lsp_types::{DocumentLink, Range, Url};
4
5/// Extract document links for import directives in the current file.
6///
7/// Each `ImportDirective` node produces a clickable link over the import
8/// path string that targets the imported file. Other identifier links
9/// are handled by `textDocument/definition`.
10pub fn document_links(
11    build: &CachedBuild,
12    file_uri: &Url,
13    source_bytes: &[u8],
14) -> Vec<DocumentLink> {
15    let mut links = Vec::new();
16
17    let file_path = match file_uri.to_file_path() {
18        Ok(p) => p,
19        Err(_) => return links,
20    };
21    let file_path_str = match file_path.to_str() {
22        Some(s) => s,
23        None => return links,
24    };
25
26    let abs_path = match build.path_to_abs.get(file_path_str) {
27        Some(a) => a.as_str(),
28        None => return links,
29    };
30
31    let file_nodes = match build.nodes.get(abs_path) {
32        Some(n) => n,
33        None => return links,
34    };
35
36    for (_id, node_info) in file_nodes.iter() {
37        if node_info.node_type.as_deref() == Some("ImportDirective")
38            && let Some(link) = import_link(node_info, source_bytes)
39        {
40            links.push(link);
41        }
42    }
43
44    links.sort_by(|a, b| {
45        a.range
46            .start
47            .line
48            .cmp(&b.range.start.line)
49            .then(a.range.start.character.cmp(&b.range.start.character))
50    });
51
52    links
53}
54
55/// Build a document link for an ImportDirective node.
56/// The link covers the quoted import path and targets the resolved file.
57fn import_link(node_info: &crate::goto::NodeInfo, source_bytes: &[u8]) -> Option<DocumentLink> {
58    let absolute_path = node_info.absolute_path.as_deref()?;
59    let src_loc = SourceLoc::parse(&node_info.src)?;
60    let (start_byte, length) = (src_loc.offset, src_loc.length);
61    let end_byte = start_byte + length;
62
63    if end_byte > source_bytes.len() || end_byte < 3 {
64        return None;
65    }
66
67    // Walk backwards: `;` then closing quote then file string then opening quote
68    let close_quote = end_byte - 2;
69    let open_quote = (start_byte..close_quote)
70        .rev()
71        .find(|&i| source_bytes[i] == b'"' || source_bytes[i] == b'\'')?;
72
73    let start_pos = bytes_to_pos(source_bytes, open_quote + 1)?;
74    let end_pos = bytes_to_pos(source_bytes, close_quote)?;
75
76    let target_path = std::path::Path::new(absolute_path);
77    let full_path = if target_path.is_absolute() {
78        target_path.to_path_buf()
79    } else {
80        std::env::current_dir().ok()?.join(target_path)
81    };
82    let target_uri = Url::from_file_path(&full_path).ok()?;
83
84    Some(DocumentLink {
85        range: Range {
86            start: start_pos,
87            end: end_pos,
88        },
89        target: Some(target_uri),
90        tooltip: Some(absolute_path.to_string()),
91        data: None,
92    })
93}