solidity_language_server/
links.rs1use crate::goto::{CachedBuild, bytes_to_pos};
2use crate::types::SourceLoc;
3use crate::utils;
4use tower_lsp::lsp_types::{DocumentLink, Position, Range, Url};
5use tree_sitter::Parser;
6
7pub 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
57pub 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 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
86fn 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
108pub struct TsImport {
111 pub path: String,
113 pub inner_range: Range,
115}
116
117pub 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
143pub fn ts_cursor_in_assembly_flags(source_bytes: &[u8], position: Position) -> Option<Range> {
150 let source_str = std::str::from_utf8(source_bytes).unwrap_or("");
151 let mut parser = Parser::new();
152 if parser
153 .set_language(&tree_sitter_solidity::LANGUAGE.into())
154 .is_err()
155 {
156 return None;
157 }
158 let tree = parser.parse(source_str, None)?;
159 find_assembly_flags_range(tree.root_node(), source_bytes, source_str, position)
160}
161
162fn find_assembly_flags_range(
163 node: tree_sitter::Node,
164 source_bytes: &[u8],
165 source_str: &str,
166 position: Position,
167) -> Option<Range> {
168 if node.kind() == "assembly_flags" {
170 for i in 0..node.named_child_count() {
171 if let Some(child) = node.named_child(i as u32) {
172 if child.kind() == "string" {
173 let start = child.start_byte();
174 let end = child.end_byte().min(source_bytes.len());
175 if end >= start + 2 {
176 let inner_start = start + 1;
177 let inner_end = end - 1;
178 let s = utils::byte_offset_to_position(source_str, inner_start);
179 let e = utils::byte_offset_to_position(source_str, inner_end);
180 let r = Range { start: s, end: e };
181 if position >= r.start && position <= r.end {
182 return Some(r);
183 }
184 }
185 }
186 }
187 }
188 }
189
190 if node.kind() == "ERROR" {
192 let mut saw_assembly = false;
193 let mut saw_lparen = false;
194 for i in 0..node.child_count() {
195 if let Some(child) = node.child(i as u32) {
196 match child.kind() {
197 "assembly" => {
198 saw_assembly = true;
199 }
200 "(" if saw_assembly => {
201 saw_lparen = true;
202 }
203 "\"" | "'" if saw_assembly && saw_lparen => {
204 let q = source_bytes[child.start_byte()];
205 let inner_start = child.start_byte() + 1;
206 let inner_end = find_closing_quote(source_bytes, inner_start, q);
207 let s = utils::byte_offset_to_position(source_str, inner_start);
208 let e = utils::byte_offset_to_position(source_str, inner_end);
209 let r = Range { start: s, end: e };
210 if position >= r.start && position <= r.end {
211 return Some(r);
212 }
213 }
214 _ => {}
215 }
216 }
217 }
218 }
219
220 for i in 0..node.child_count() {
221 if let Some(child) = node.child(i as u32) {
222 if let Some(r) = find_assembly_flags_range(child, source_bytes, source_str, position) {
223 return Some(r);
224 }
225 }
226 }
227 None
228}
229
230pub fn ts_cursor_in_import_string(source_bytes: &[u8], position: Position) -> Option<Range> {
236 ts_find_imports(source_bytes)
237 .into_iter()
238 .find(|imp| {
239 let r = &imp.inner_range;
240 position >= r.start && position <= r.end
241 })
242 .map(|imp| imp.inner_range)
243}
244
245fn collect_imports(node: tree_sitter::Node, source_bytes: &[u8], out: &mut Vec<TsImport>) {
249 let source_str = std::str::from_utf8(source_bytes).unwrap_or("");
250
251 if node.kind() == "import_directive" {
252 for i in 0..node.child_count() {
259 if let Some(child) = node.child(i as u32) {
260 if child.kind() == "string" {
261 push_string_node(child, source_bytes, source_str, out);
262 return;
263 }
264 if child.kind() == "ERROR" {
265 for j in 0..child.child_count() {
267 if let Some(gc) = child.child(j as u32) {
268 if gc.kind() == "\"" || gc.kind() == "'" {
269 let q = source_bytes[gc.start_byte()];
270 let inner_start = gc.start_byte() + 1;
271 let inner_end = find_closing_quote(source_bytes, inner_start, q);
272 let path =
273 String::from_utf8_lossy(&source_bytes[inner_start..inner_end])
274 .to_string();
275 let start_pos =
276 utils::byte_offset_to_position(source_str, inner_start);
277 let end_pos = utils::byte_offset_to_position(source_str, inner_end);
278 out.push(TsImport {
279 path,
280 inner_range: Range {
281 start: start_pos,
282 end: end_pos,
283 },
284 });
285 return;
286 }
287 }
288 }
289 }
290 }
291 }
292 return;
293 }
294
295 if node.kind() == "ERROR" {
304 let mut has_import = false;
305 let mut quote_byte: Option<usize> = None;
306 let mut quote_ch: Option<u8> = None;
307 for i in 0..node.child_count() {
308 if let Some(child) = node.child(i as u32) {
309 let kind = child.kind();
310 if kind == "import" {
311 has_import = true;
312 }
313 if has_import && (kind == "\"" || kind == "'") {
314 let q = source_bytes[child.start_byte()];
316 quote_byte = Some(child.start_byte() + 1); quote_ch = Some(q);
318 break;
319 }
320 if has_import && kind == "string" {
322 push_string_node(child, source_bytes, source_str, out);
323 quote_byte = None; break;
325 }
326 }
327 }
328 if let (Some(inner_start), Some(q)) = (quote_byte, quote_ch) {
329 let inner_end = find_closing_quote(source_bytes, inner_start, q);
331 let path = String::from_utf8_lossy(&source_bytes[inner_start..inner_end]).to_string();
332 let start_pos = utils::byte_offset_to_position(source_str, inner_start);
333 let end_pos = utils::byte_offset_to_position(source_str, inner_end);
334 out.push(TsImport {
335 path,
336 inner_range: Range {
337 start: start_pos,
338 end: end_pos,
339 },
340 });
341 }
342 return;
343 }
344
345 for i in 0..node.child_count() {
346 if let Some(child) = node.child(i as u32) {
347 collect_imports(child, source_bytes, out);
348 }
349 }
350}
351
352fn push_string_node(
359 node: tree_sitter::Node,
360 source_bytes: &[u8],
361 source_str: &str,
362 out: &mut Vec<TsImport>,
363) {
364 let start = node.start_byte();
365 let raw_end = node.end_byte().min(source_bytes.len());
366 if raw_end < start + 1 {
367 return;
368 }
369 let inner_start = start + 1;
370 let eol = find_closing_quote(source_bytes, inner_start, b'\n');
373 let closing_quote = source_bytes
374 .get(inner_start..eol)
375 .and_then(|s| s.iter().position(|&b| b == source_bytes[start]))
376 .map(|p| inner_start + p);
377 let inner_end = closing_quote.unwrap_or(eol);
378
379 let path = String::from_utf8_lossy(&source_bytes[inner_start..inner_end]).to_string();
380 let start_pos = utils::byte_offset_to_position(source_str, inner_start);
381 let end_pos = utils::byte_offset_to_position(source_str, inner_end);
382 out.push(TsImport {
383 path,
384 inner_range: Range {
385 start: start_pos,
386 end: end_pos,
387 },
388 });
389}
390
391fn find_closing_quote(source_bytes: &[u8], from: usize, quote_char: u8) -> usize {
394 for i in from..source_bytes.len() {
395 let b = source_bytes[i];
396 if b == quote_char {
397 return i;
398 }
399 if b == b'\n' || b == b'\r' {
401 return i;
402 }
403 }
404 source_bytes.len()
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410
411 fn pos(line: u32, character: u32) -> Position {
412 Position { line, character }
413 }
414
415 #[test]
416 fn ts_cursor_in_import_string_inside() {
417 let src = b"import \"./Foo.sol\";";
419 let range = ts_cursor_in_import_string(src, pos(0, 12));
420 assert!(range.is_some(), "expected Some inside import string");
421 let r = range.unwrap();
422 assert_eq!(r.start.character, 8);
424 assert_eq!(r.end.character, 17);
426 }
427
428 #[test]
429 fn ts_cursor_in_import_string_outside_semicolon() {
430 let src = b"import \"./Foo.sol\";";
432 let range = ts_cursor_in_import_string(src, pos(0, 19));
433 assert!(range.is_none(), "should be None past closing quote");
434 }
435
436 #[test]
437 fn ts_cursor_in_import_string_non_import_literal() {
438 let src = b"string memory s = \"hello\";";
440 let range = ts_cursor_in_import_string(src, pos(0, 20));
441 assert!(range.is_none(), "should be None for non-import string");
442 }
443
444 #[test]
445 fn ts_cursor_in_import_string_from_style() {
446 let src = b"import {Foo} from \"./Foo.sol\";";
448 let range = ts_cursor_in_import_string(src, pos(0, 19));
450 assert!(range.is_some(), "expected Some for from-style import");
451 }
452
453 #[test]
454 fn ts_cursor_in_import_string_empty_string() {
455 let src = b"import \"\"";
457 let range = ts_cursor_in_import_string(src, pos(0, 8));
458 assert!(range.is_some(), "expected Some for empty import string");
459 let r = range.unwrap();
460 assert_eq!(r.start.character, 8);
461 assert_eq!(r.end.character, 8); }
463
464 #[test]
465 fn ts_cursor_in_import_string_unclosed() {
466 let src = b"import {} from \"";
468 let range = ts_cursor_in_import_string(src, pos(0, 16));
469 assert!(range.is_some(), "expected Some for unclosed import string");
470 let r = range.unwrap();
471 assert_eq!(r.start.character, 16); }
473
474 #[test]
475 fn ts_cursor_in_import_string_unclosed_mid() {
476 let src = b"import {} from \"whi";
478 let range = ts_cursor_in_import_string(src, pos(0, 19));
479 assert!(range.is_some(), "expected Some mid unclosed import string");
480 }
481
482 #[test]
483 fn ts_cursor_in_import_string_non_import_unclosed() {
484 let src = b"string memory s = \"hello";
486 let range = ts_cursor_in_import_string(src, pos(0, 20));
487 assert!(
488 range.is_none(),
489 "should be None for unclosed non-import string"
490 );
491 }
492
493 #[test]
494 fn ts_cursor_in_import_string_bare_unclosed() {
495 let src = b"import \"";
497 let range = ts_cursor_in_import_string(src, pos(0, 8));
498 assert!(range.is_some(), "expected Some for bare unclosed import");
499 let r = range.unwrap();
500 assert_eq!(r.start.character, 8);
501 }
502
503 #[test]
504 fn ts_cursor_in_import_string_bare_unclosed_mid() {
505 let src = b"import \"forge-std/";
507 let range = ts_cursor_in_import_string(src, pos(0, 18));
509 assert!(
510 range.is_some(),
511 "expected Some for `import \"forge-std/` (unclosed mid-path)"
512 );
513 let r = range.unwrap();
514 assert_eq!(r.start.character, 8); assert_eq!(r.end.character, 18); }
517
518 #[test]
519 fn ts_cursor_in_import_string_assembly_flags() {
520 let src = b"contract A { function f() internal { assembly (\"memory-safe\") {} } }";
522 let range = ts_cursor_in_import_string(src, pos(0, 50));
523 assert!(
524 range.is_none(),
525 "assembly dialect string must not trigger import completions"
526 );
527 }
528
529 #[test]
530 fn ts_cursor_in_import_string_revert_string() {
531 let src = b"contract A { function f() public { revert(\"err\"); } }";
533 let range = ts_cursor_in_import_string(src, pos(0, 43));
534 assert!(
535 range.is_none(),
536 "revert string must not trigger import completions"
537 );
538 }
539
540 #[test]
543 fn ts_cursor_in_assembly_flags_complete() {
544 let src = b"contract A { function f() internal { assembly (\"memory-safe\") {} } }";
546 let range = ts_cursor_in_assembly_flags(src, pos(0, 50));
547 assert!(
548 range.is_some(),
549 "expected Some inside assembly flags string"
550 );
551 }
552
553 #[test]
554 fn ts_cursor_in_assembly_flags_unclosed() {
555 let src = b"contract A { function f() internal { assembly (\"";
557 let range = ts_cursor_in_assembly_flags(src, pos(0, 48));
558 assert!(
559 range.is_some(),
560 "expected Some for unclosed assembly flags string"
561 );
562 }
563
564 #[test]
565 fn ts_cursor_in_assembly_flags_not_import() {
566 let src = b"import \"./Foo.sol\";";
568 let range = ts_cursor_in_assembly_flags(src, pos(0, 12));
569 assert!(
570 range.is_none(),
571 "import string must not match assembly_flags"
572 );
573 }
574
575 #[test]
576 fn ts_cursor_in_assembly_flags_not_revert() {
577 let src = b"contract A { function f() public { revert(\"err\"); } }";
579 let range = ts_cursor_in_assembly_flags(src, pos(0, 43));
580 assert!(
581 range.is_none(),
582 "revert string must not match assembly_flags"
583 );
584 }
585}