Skip to main content

solidity_language_server/
links.rs

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