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.as_str())?;
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 && child.kind() == "string"
173 {
174 let start = child.start_byte();
175 let end = child.end_byte().min(source_bytes.len());
176 if end >= start + 2 {
177 let inner_start = start + 1;
178 let inner_end = end - 1;
179 let s = utils::byte_offset_to_position(source_str, inner_start);
180 let e = utils::byte_offset_to_position(source_str, inner_end);
181 let r = Range { start: s, end: e };
182 if position >= r.start && position <= r.end {
183 return Some(r);
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 && let Some(r) = find_assembly_flags_range(child, source_bytes, source_str, position)
223 {
224 return Some(r);
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 && (gc.kind() == "\"" || gc.kind() == "'")
269 {
270 let q = source_bytes[gc.start_byte()];
271 let inner_start = gc.start_byte() + 1;
272 let inner_end = find_closing_quote(source_bytes, inner_start, q);
273 let path =
274 String::from_utf8_lossy(&source_bytes[inner_start..inner_end])
275 .to_string();
276 let start_pos = 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 return;
292 }
293
294 if node.kind() == "ERROR" {
303 let mut has_import = false;
304 let mut quote_byte: Option<usize> = None;
305 let mut quote_ch: Option<u8> = None;
306 for i in 0..node.child_count() {
307 if let Some(child) = node.child(i as u32) {
308 let kind = child.kind();
309 if kind == "import" {
310 has_import = true;
311 }
312 if has_import && (kind == "\"" || kind == "'") {
313 let q = source_bytes[child.start_byte()];
315 quote_byte = Some(child.start_byte() + 1); quote_ch = Some(q);
317 break;
318 }
319 if has_import && kind == "string" {
321 push_string_node(child, source_bytes, source_str, out);
322 quote_byte = None; break;
324 }
325 }
326 }
327 if let (Some(inner_start), Some(q)) = (quote_byte, quote_ch) {
328 let inner_end = find_closing_quote(source_bytes, inner_start, q);
330 let path = String::from_utf8_lossy(&source_bytes[inner_start..inner_end]).to_string();
331 let start_pos = utils::byte_offset_to_position(source_str, inner_start);
332 let end_pos = utils::byte_offset_to_position(source_str, inner_end);
333 out.push(TsImport {
334 path,
335 inner_range: Range {
336 start: start_pos,
337 end: end_pos,
338 },
339 });
340 }
341 return;
342 }
343
344 for i in 0..node.child_count() {
345 if let Some(child) = node.child(i as u32) {
346 collect_imports(child, source_bytes, out);
347 }
348 }
349}
350
351fn push_string_node(
358 node: tree_sitter::Node,
359 source_bytes: &[u8],
360 source_str: &str,
361 out: &mut Vec<TsImport>,
362) {
363 let start = node.start_byte();
364 let raw_end = node.end_byte().min(source_bytes.len());
365 if raw_end < start + 1 {
366 return;
367 }
368 let inner_start = start + 1;
369 let eol = find_closing_quote(source_bytes, inner_start, b'\n');
372 let closing_quote = source_bytes
373 .get(inner_start..eol)
374 .and_then(|s| s.iter().position(|&b| b == source_bytes[start]))
375 .map(|p| inner_start + p);
376 let inner_end = closing_quote.unwrap_or(eol);
377
378 let path = String::from_utf8_lossy(&source_bytes[inner_start..inner_end]).to_string();
379 let start_pos = utils::byte_offset_to_position(source_str, inner_start);
380 let end_pos = utils::byte_offset_to_position(source_str, inner_end);
381 out.push(TsImport {
382 path,
383 inner_range: Range {
384 start: start_pos,
385 end: end_pos,
386 },
387 });
388}
389
390fn find_closing_quote(source_bytes: &[u8], from: usize, quote_char: u8) -> usize {
393 for (i, &b) in source_bytes.iter().enumerate().skip(from) {
394 if b == quote_char {
395 return i;
396 }
397 if b == b'\n' || b == b'\r' {
399 return i;
400 }
401 }
402 source_bytes.len()
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 fn pos(line: u32, character: u32) -> Position {
410 Position { line, character }
411 }
412
413 #[test]
414 fn ts_cursor_in_import_string_inside() {
415 let src = b"import \"./Foo.sol\";";
417 let range = ts_cursor_in_import_string(src, pos(0, 12));
418 assert!(range.is_some(), "expected Some inside import string");
419 let r = range.unwrap();
420 assert_eq!(r.start.character, 8);
422 assert_eq!(r.end.character, 17);
424 }
425
426 #[test]
427 fn ts_cursor_in_import_string_outside_semicolon() {
428 let src = b"import \"./Foo.sol\";";
430 let range = ts_cursor_in_import_string(src, pos(0, 19));
431 assert!(range.is_none(), "should be None past closing quote");
432 }
433
434 #[test]
435 fn ts_cursor_in_import_string_non_import_literal() {
436 let src = b"string memory s = \"hello\";";
438 let range = ts_cursor_in_import_string(src, pos(0, 20));
439 assert!(range.is_none(), "should be None for non-import string");
440 }
441
442 #[test]
443 fn ts_cursor_in_import_string_from_style() {
444 let src = b"import {Foo} from \"./Foo.sol\";";
446 let range = ts_cursor_in_import_string(src, pos(0, 19));
448 assert!(range.is_some(), "expected Some for from-style import");
449 }
450
451 #[test]
452 fn ts_cursor_in_import_string_empty_string() {
453 let src = b"import \"\"";
455 let range = ts_cursor_in_import_string(src, pos(0, 8));
456 assert!(range.is_some(), "expected Some for empty import string");
457 let r = range.unwrap();
458 assert_eq!(r.start.character, 8);
459 assert_eq!(r.end.character, 8); }
461
462 #[test]
463 fn ts_cursor_in_import_string_unclosed() {
464 let src = b"import {} from \"";
466 let range = ts_cursor_in_import_string(src, pos(0, 16));
467 assert!(range.is_some(), "expected Some for unclosed import string");
468 let r = range.unwrap();
469 assert_eq!(r.start.character, 16); }
471
472 #[test]
473 fn ts_cursor_in_import_string_unclosed_mid() {
474 let src = b"import {} from \"whi";
476 let range = ts_cursor_in_import_string(src, pos(0, 19));
477 assert!(range.is_some(), "expected Some mid unclosed import string");
478 }
479
480 #[test]
481 fn ts_cursor_in_import_string_non_import_unclosed() {
482 let src = b"string memory s = \"hello";
484 let range = ts_cursor_in_import_string(src, pos(0, 20));
485 assert!(
486 range.is_none(),
487 "should be None for unclosed non-import string"
488 );
489 }
490
491 #[test]
492 fn ts_cursor_in_import_string_bare_unclosed() {
493 let src = b"import \"";
495 let range = ts_cursor_in_import_string(src, pos(0, 8));
496 assert!(range.is_some(), "expected Some for bare unclosed import");
497 let r = range.unwrap();
498 assert_eq!(r.start.character, 8);
499 }
500
501 #[test]
502 fn ts_cursor_in_import_string_bare_unclosed_mid() {
503 let src = b"import \"forge-std/";
505 let range = ts_cursor_in_import_string(src, pos(0, 18));
507 assert!(
508 range.is_some(),
509 "expected Some for `import \"forge-std/` (unclosed mid-path)"
510 );
511 let r = range.unwrap();
512 assert_eq!(r.start.character, 8); assert_eq!(r.end.character, 18); }
515
516 #[test]
517 fn ts_cursor_in_import_string_assembly_flags() {
518 let src = b"contract A { function f() internal { assembly (\"memory-safe\") {} } }";
520 let range = ts_cursor_in_import_string(src, pos(0, 50));
521 assert!(
522 range.is_none(),
523 "assembly dialect string must not trigger import completions"
524 );
525 }
526
527 #[test]
528 fn ts_cursor_in_import_string_revert_string() {
529 let src = b"contract A { function f() public { revert(\"err\"); } }";
531 let range = ts_cursor_in_import_string(src, pos(0, 43));
532 assert!(
533 range.is_none(),
534 "revert string must not trigger import completions"
535 );
536 }
537
538 #[test]
541 fn ts_cursor_in_assembly_flags_complete() {
542 let src = b"contract A { function f() internal { assembly (\"memory-safe\") {} } }";
544 let range = ts_cursor_in_assembly_flags(src, pos(0, 50));
545 assert!(
546 range.is_some(),
547 "expected Some inside assembly flags string"
548 );
549 }
550
551 #[test]
552 fn ts_cursor_in_assembly_flags_unclosed() {
553 let src = b"contract A { function f() internal { assembly (\"";
555 let range = ts_cursor_in_assembly_flags(src, pos(0, 48));
556 assert!(
557 range.is_some(),
558 "expected Some for unclosed assembly flags string"
559 );
560 }
561
562 #[test]
563 fn ts_cursor_in_assembly_flags_not_import() {
564 let src = b"import \"./Foo.sol\";";
566 let range = ts_cursor_in_assembly_flags(src, pos(0, 12));
567 assert!(
568 range.is_none(),
569 "import string must not match assembly_flags"
570 );
571 }
572
573 #[test]
574 fn ts_cursor_in_assembly_flags_not_revert() {
575 let src = b"contract A { function f() public { revert(\"err\"); } }";
577 let range = ts_cursor_in_assembly_flags(src, pos(0, 43));
578 assert!(
579 range.is_none(),
580 "revert string must not match assembly_flags"
581 );
582 }
583}