Skip to main content

solidity_language_server/
links.rs

1use crate::goto::{CachedBuild, bytes_to_pos};
2use crate::references::id_to_location;
3use crate::types::SourceLoc;
4use tower_lsp::lsp_types::{DocumentLink, Range, Url};
5
6/// Extract document links for every node in the current file that
7/// references a declaration elsewhere.
8///
9/// For `ImportDirective` nodes, the link covers the import path string
10/// and targets the imported file. For all other nodes with a
11/// `referencedDeclaration`, the link covers the node's name and targets
12/// the declaration's location.
13pub fn document_links(
14    build: &CachedBuild,
15    file_uri: &Url,
16    source_bytes: &[u8],
17) -> Vec<DocumentLink> {
18    let mut links = Vec::new();
19
20    let file_path = match file_uri.to_file_path() {
21        Ok(p) => p,
22        Err(_) => return links,
23    };
24    let file_path_str = match file_path.to_str() {
25        Some(s) => s,
26        None => return links,
27    };
28
29    let abs_path = match build.path_to_abs.get(file_path_str) {
30        Some(a) => a.as_str(),
31        None => return links,
32    };
33
34    let file_nodes = match build.nodes.get(abs_path) {
35        Some(n) => n,
36        None => return links,
37    };
38
39    let tmp = file_nodes.iter();
40    for (_id, node_info) in tmp {
41        // ImportDirective: link the import path to the imported file
42        if node_info.node_type.as_deref() == Some("ImportDirective") {
43            if let Some(link) = import_link(node_info, source_bytes) {
44                links.push(link);
45            }
46            continue;
47        }
48
49        // Any node with a referencedDeclaration: link to that declaration
50        let ref_id = match node_info.referenced_declaration {
51            Some(id) => id,
52            None => continue,
53        };
54
55        // Use name_location if available, otherwise fall back to src
56        let loc_str = node_info.name_location.as_deref().unwrap_or(&node_info.src);
57        let src_loc = match SourceLoc::parse(loc_str) {
58            Some(loc) => loc,
59            None => continue,
60        };
61        let (start_byte, length) = (src_loc.offset, src_loc.length);
62
63        let start_pos = match bytes_to_pos(source_bytes, start_byte) {
64            Some(p) => p,
65            None => continue,
66        };
67        let end_pos = match bytes_to_pos(source_bytes, start_byte + length) {
68            Some(p) => p,
69            None => continue,
70        };
71
72        // Resolve the target declaration to a file location
73        let target_location = match id_to_location(&build.nodes, &build.id_to_path_map, ref_id) {
74            Some(loc) => loc,
75            None => continue,
76        };
77
78        links.push(DocumentLink {
79            range: Range {
80                start: start_pos,
81                end: end_pos,
82            },
83            target: Some(target_location.uri),
84            tooltip: None,
85            data: None,
86        });
87    }
88
89    links.sort_by(|a, b| {
90        a.range
91            .start
92            .line
93            .cmp(&b.range.start.line)
94            .then(a.range.start.character.cmp(&b.range.start.character))
95    });
96
97    links
98}
99
100/// Build a document link for an ImportDirective node.
101/// The link covers the quoted import path and targets the resolved file.
102fn import_link(node_info: &crate::goto::NodeInfo, source_bytes: &[u8]) -> Option<DocumentLink> {
103    let absolute_path = node_info.absolute_path.as_deref()?;
104    let src_loc = SourceLoc::parse(&node_info.src)?;
105    let (start_byte, length) = (src_loc.offset, src_loc.length);
106    let end_byte = start_byte + length;
107
108    if end_byte > source_bytes.len() || end_byte < 3 {
109        return None;
110    }
111
112    // Walk backwards: `;` then closing quote then file string then opening quote
113    let close_quote = end_byte - 2;
114    let open_quote = (start_byte..close_quote)
115        .rev()
116        .find(|&i| source_bytes[i] == b'"' || source_bytes[i] == b'\'')?;
117
118    let start_pos = bytes_to_pos(source_bytes, open_quote + 1)?;
119    let end_pos = bytes_to_pos(source_bytes, close_quote)?;
120
121    let target_path = std::path::Path::new(absolute_path);
122    let full_path = if target_path.is_absolute() {
123        target_path.to_path_buf()
124    } else {
125        std::env::current_dir().ok()?.join(target_path)
126    };
127    let target_uri = Url::from_file_path(&full_path).ok()?;
128
129    Some(DocumentLink {
130        range: Range {
131            start: start_pos,
132            end: end_pos,
133        },
134        target: Some(target_uri),
135        tooltip: Some(absolute_path.to_string()),
136        data: None,
137    })
138}