Skip to main content

solidity_language_server/
links.rs

1use crate::goto::{CachedBuild, bytes_to_pos};
2use crate::types::SourceLoc;
3use crate::utils;
4use tower_lsp::lsp_types::{DocumentLink, Range, Url};
5use tree_sitter::Parser;
6
7/// Extract document links for import directives in the current file.
8///
9/// Each `ImportDirective` node produces a clickable link over the import
10/// path string that targets the imported file. Other identifier links
11/// are handled by `textDocument/definition`.
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    for (_id, node_info) in file_nodes.iter() {
39        if node_info.node_type.as_deref() == Some("ImportDirective")
40            && let Some(link) = import_link(node_info, source_bytes)
41        {
42            links.push(link);
43        }
44    }
45
46    links.sort_by(|a, b| {
47        a.range
48            .start
49            .line
50            .cmp(&b.range.start.line)
51            .then(a.range.start.character.cmp(&b.range.start.character))
52    });
53
54    links
55}
56
57/// Find the LSP Range of the import path string inside an ImportDirective.
58///
59/// Returns the range covering just the text between the quotes in the
60/// import statement. Used by both `document_links` (for clickable links)
61/// and `file_operations::rename_imports` (for path rewriting).
62pub fn import_path_range(node_info: &crate::goto::NodeInfo, source_bytes: &[u8]) -> Option<Range> {
63    let src_loc = SourceLoc::parse(&node_info.src)?;
64    let (start_byte, length) = (src_loc.offset, src_loc.length);
65    let end_byte = start_byte + length;
66
67    if end_byte > source_bytes.len() || end_byte < 3 {
68        return None;
69    }
70
71    // Walk backwards: `;` then closing quote then file string then opening quote
72    let close_quote = end_byte - 2;
73    let open_quote = (start_byte..close_quote)
74        .rev()
75        .find(|&i| source_bytes[i] == b'"' || source_bytes[i] == b'\'')?;
76
77    let start_pos = bytes_to_pos(source_bytes, open_quote + 1)?;
78    let end_pos = bytes_to_pos(source_bytes, close_quote)?;
79
80    Some(Range {
81        start: start_pos,
82        end: end_pos,
83    })
84}
85
86/// Build a document link for an ImportDirective node.
87/// The link covers the quoted import path and targets the resolved file.
88fn import_link(node_info: &crate::goto::NodeInfo, source_bytes: &[u8]) -> Option<DocumentLink> {
89    let absolute_path = node_info.absolute_path.as_deref()?;
90    let range = import_path_range(node_info, source_bytes)?;
91
92    let target_path = std::path::Path::new(absolute_path);
93    let full_path = if target_path.is_absolute() {
94        target_path.to_path_buf()
95    } else {
96        std::env::current_dir().ok()?.join(target_path)
97    };
98    let target_uri = Url::from_file_path(&full_path).ok()?;
99
100    Some(DocumentLink {
101        range,
102        target: Some(target_uri),
103        tooltip: Some(absolute_path.to_string()),
104        data: None,
105    })
106}
107
108/// An import found by tree-sitter: the quoted path string and its LSP range
109/// (covering only the text between the quotes).
110pub struct TsImport {
111    /// The import path string (without quotes), e.g. `./Extsload.sol`.
112    pub path: String,
113    /// LSP range covering the path text between quotes.
114    pub inner_range: Range,
115}
116
117/// Parse `source_bytes` with tree-sitter and return all import paths with
118/// their ranges.  This is independent of the solc AST and always reflects
119/// the **current** source content, making it safe to use when the AST is
120/// stale or unavailable (e.g. after a failed re-index).
121pub fn ts_find_imports(source_bytes: &[u8]) -> Vec<TsImport> {
122    let source = match std::str::from_utf8(source_bytes) {
123        Ok(s) => s,
124        Err(_) => return vec![],
125    };
126    let mut parser = Parser::new();
127    if parser
128        .set_language(&tree_sitter_solidity::LANGUAGE.into())
129        .is_err()
130    {
131        return vec![];
132    }
133    let tree = match parser.parse(source, None) {
134        Some(t) => t,
135        None => return vec![],
136    };
137
138    let mut imports = Vec::new();
139    collect_imports(tree.root_node(), source_bytes, &mut imports);
140    imports
141}
142
143/// Recursively walk the tree-sitter CST to find `import_directive` nodes.
144fn collect_imports(node: tree_sitter::Node, source_bytes: &[u8], out: &mut Vec<TsImport>) {
145    if node.kind() == "import_directive" {
146        // The `string` child contains the quoted import path.
147        // In tree-sitter-solidity the string node includes the quotes.
148        for i in 0..node.named_child_count() {
149            if let Some(child) = node.named_child(i as u32) {
150                if child.kind() == "string" {
151                    let start = child.start_byte();
152                    let end = child.end_byte();
153                    if end > start + 2 && end <= source_bytes.len() {
154                        // Strip quotes: the string node is `"path"` or `'path'`
155                        let inner_start = start + 1;
156                        let inner_end = end - 1;
157                        let path = String::from_utf8_lossy(&source_bytes[inner_start..inner_end])
158                            .to_string();
159
160                        // Convert byte offsets to LSP positions using the
161                        // negotiated encoding (UTF-8 or UTF-16).  Tree-sitter
162                        // columns are byte offsets, which only coincide with
163                        // LSP character units for pure-ASCII lines.
164                        let source_str = std::str::from_utf8(source_bytes).unwrap_or("");
165                        let start_pos = utils::byte_offset_to_position(source_str, inner_start);
166                        let end_pos = utils::byte_offset_to_position(source_str, inner_end);
167
168                        out.push(TsImport {
169                            path,
170                            inner_range: Range {
171                                start: start_pos,
172                                end: end_pos,
173                            },
174                        });
175                    }
176                    break; // only one path per import
177                }
178            }
179        }
180        return; // no need to recurse into import_directive children
181    }
182
183    for i in 0..node.child_count() {
184        if let Some(child) = node.child(i as u32) {
185            collect_imports(child, source_bytes, out);
186        }
187    }
188}