use crate::goto::{CachedBuild, bytes_to_pos};
use crate::types::SourceLoc;
use crate::utils;
use tower_lsp::lsp_types::{DocumentLink, Position, Range, Url};
use tree_sitter::Parser;
pub fn document_links(
build: &CachedBuild,
file_uri: &Url,
source_bytes: &[u8],
) -> Vec<DocumentLink> {
let mut links = Vec::new();
let file_path = match file_uri.to_file_path() {
Ok(p) => p,
Err(_) => return links,
};
let file_path_str = match file_path.to_str() {
Some(s) => s,
None => return links,
};
let abs_path = match build.path_to_abs.get(file_path_str) {
Some(a) => a.as_str(),
None => return links,
};
let file_nodes = match build.nodes.get(abs_path) {
Some(n) => n,
None => return links,
};
for (_id, node_info) in file_nodes.iter() {
if node_info.node_type.as_deref() == Some("ImportDirective")
&& let Some(link) = import_link(node_info, source_bytes)
{
links.push(link);
}
}
links.sort_by(|a, b| {
a.range
.start
.line
.cmp(&b.range.start.line)
.then(a.range.start.character.cmp(&b.range.start.character))
});
links
}
pub fn import_path_range(node_info: &crate::goto::NodeInfo, source_bytes: &[u8]) -> Option<Range> {
let src_loc = SourceLoc::parse(node_info.src.as_str())?;
let (start_byte, length) = (src_loc.offset, src_loc.length);
let end_byte = start_byte + length;
if end_byte > source_bytes.len() || end_byte < 3 {
return None;
}
let close_quote = end_byte - 2;
let open_quote = (start_byte..close_quote)
.rev()
.find(|&i| source_bytes[i] == b'"' || source_bytes[i] == b'\'')?;
let start_pos = bytes_to_pos(source_bytes, open_quote + 1)?;
let end_pos = bytes_to_pos(source_bytes, close_quote)?;
Some(Range {
start: start_pos,
end: end_pos,
})
}
fn import_link(node_info: &crate::goto::NodeInfo, source_bytes: &[u8]) -> Option<DocumentLink> {
let absolute_path = node_info.absolute_path.as_deref()?;
let range = import_path_range(node_info, source_bytes)?;
let target_path = std::path::Path::new(absolute_path);
let full_path = if target_path.is_absolute() {
target_path.to_path_buf()
} else {
std::env::current_dir().ok()?.join(target_path)
};
let target_uri = Url::from_file_path(&full_path).ok()?;
Some(DocumentLink {
range,
target: Some(target_uri),
tooltip: Some(absolute_path.to_string()),
data: None,
})
}
pub struct TsImport {
pub path: String,
pub inner_range: Range,
}
pub fn ts_find_imports(source_bytes: &[u8]) -> Vec<TsImport> {
let source = match std::str::from_utf8(source_bytes) {
Ok(s) => s,
Err(_) => return vec![],
};
let mut parser = Parser::new();
if parser
.set_language(&tree_sitter_solidity::LANGUAGE.into())
.is_err()
{
return vec![];
}
let tree = match parser.parse(source, None) {
Some(t) => t,
None => return vec![],
};
let mut imports = Vec::new();
collect_imports(tree.root_node(), source_bytes, &mut imports);
imports
}
pub fn ts_cursor_in_assembly_flags(source_bytes: &[u8], position: Position) -> Option<Range> {
let source_str = std::str::from_utf8(source_bytes).unwrap_or("");
let mut parser = Parser::new();
if parser
.set_language(&tree_sitter_solidity::LANGUAGE.into())
.is_err()
{
return None;
}
let tree = parser.parse(source_str, None)?;
find_assembly_flags_range(tree.root_node(), source_bytes, source_str, position)
}
fn find_assembly_flags_range(
node: tree_sitter::Node,
source_bytes: &[u8],
source_str: &str,
position: Position,
) -> Option<Range> {
if node.kind() == "assembly_flags" {
for i in 0..node.named_child_count() {
if let Some(child) = node.named_child(i as u32)
&& child.kind() == "string"
{
let start = child.start_byte();
let end = child.end_byte().min(source_bytes.len());
if end >= start + 2 {
let inner_start = start + 1;
let inner_end = end - 1;
let s = utils::byte_offset_to_position(source_str, inner_start);
let e = utils::byte_offset_to_position(source_str, inner_end);
let r = Range { start: s, end: e };
if position >= r.start && position <= r.end {
return Some(r);
}
}
}
}
}
if node.kind() == "ERROR" {
let mut saw_assembly = false;
let mut saw_lparen = false;
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
match child.kind() {
"assembly" => {
saw_assembly = true;
}
"(" if saw_assembly => {
saw_lparen = true;
}
"\"" | "'" if saw_assembly && saw_lparen => {
let q = source_bytes[child.start_byte()];
let inner_start = child.start_byte() + 1;
let inner_end = find_closing_quote(source_bytes, inner_start, q);
let s = utils::byte_offset_to_position(source_str, inner_start);
let e = utils::byte_offset_to_position(source_str, inner_end);
let r = Range { start: s, end: e };
if position >= r.start && position <= r.end {
return Some(r);
}
}
_ => {}
}
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32)
&& let Some(r) = find_assembly_flags_range(child, source_bytes, source_str, position)
{
return Some(r);
}
}
None
}
pub fn ts_cursor_in_import_string(source_bytes: &[u8], position: Position) -> Option<Range> {
ts_find_imports(source_bytes)
.into_iter()
.find(|imp| {
let r = &imp.inner_range;
position >= r.start && position <= r.end
})
.map(|imp| imp.inner_range)
}
fn collect_imports(node: tree_sitter::Node, source_bytes: &[u8], out: &mut Vec<TsImport>) {
let source_str = std::str::from_utf8(source_bytes).unwrap_or("");
if node.kind() == "import_directive" {
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
if child.kind() == "string" {
push_string_node(child, source_bytes, source_str, out);
return;
}
if child.kind() == "ERROR" {
for j in 0..child.child_count() {
if let Some(gc) = child.child(j as u32)
&& (gc.kind() == "\"" || gc.kind() == "'")
{
let q = source_bytes[gc.start_byte()];
let inner_start = gc.start_byte() + 1;
let inner_end = find_closing_quote(source_bytes, inner_start, q);
let path =
String::from_utf8_lossy(&source_bytes[inner_start..inner_end])
.to_string();
let start_pos = utils::byte_offset_to_position(source_str, inner_start);
let end_pos = utils::byte_offset_to_position(source_str, inner_end);
out.push(TsImport {
path,
inner_range: Range {
start: start_pos,
end: end_pos,
},
});
return;
}
}
}
}
}
return;
}
if node.kind() == "ERROR" {
let mut has_import = false;
let mut quote_byte: Option<usize> = None;
let mut quote_ch: Option<u8> = None;
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
let kind = child.kind();
if kind == "import" {
has_import = true;
}
if has_import && (kind == "\"" || kind == "'") {
let q = source_bytes[child.start_byte()];
quote_byte = Some(child.start_byte() + 1); quote_ch = Some(q);
break;
}
if has_import && kind == "string" {
push_string_node(child, source_bytes, source_str, out);
quote_byte = None; break;
}
}
}
if let (Some(inner_start), Some(q)) = (quote_byte, quote_ch) {
let inner_end = find_closing_quote(source_bytes, inner_start, q);
let path = String::from_utf8_lossy(&source_bytes[inner_start..inner_end]).to_string();
let start_pos = utils::byte_offset_to_position(source_str, inner_start);
let end_pos = utils::byte_offset_to_position(source_str, inner_end);
out.push(TsImport {
path,
inner_range: Range {
start: start_pos,
end: end_pos,
},
});
}
return;
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
collect_imports(child, source_bytes, out);
}
}
}
fn push_string_node(
node: tree_sitter::Node,
source_bytes: &[u8],
source_str: &str,
out: &mut Vec<TsImport>,
) {
let start = node.start_byte();
let raw_end = node.end_byte().min(source_bytes.len());
if raw_end < start + 1 {
return;
}
let inner_start = start + 1;
let eol = find_closing_quote(source_bytes, inner_start, b'\n');
let closing_quote = source_bytes
.get(inner_start..eol)
.and_then(|s| s.iter().position(|&b| b == source_bytes[start]))
.map(|p| inner_start + p);
let inner_end = closing_quote.unwrap_or(eol);
let path = String::from_utf8_lossy(&source_bytes[inner_start..inner_end]).to_string();
let start_pos = utils::byte_offset_to_position(source_str, inner_start);
let end_pos = utils::byte_offset_to_position(source_str, inner_end);
out.push(TsImport {
path,
inner_range: Range {
start: start_pos,
end: end_pos,
},
});
}
fn find_closing_quote(source_bytes: &[u8], from: usize, quote_char: u8) -> usize {
for (i, &b) in source_bytes.iter().enumerate().skip(from) {
if b == quote_char {
return i;
}
if b == b'\n' || b == b'\r' {
return i;
}
}
source_bytes.len()
}
#[cfg(test)]
mod tests {
use super::*;
fn pos(line: u32, character: u32) -> Position {
Position { line, character }
}
#[test]
fn ts_cursor_in_import_string_inside() {
let src = b"import \"./Foo.sol\";";
let range = ts_cursor_in_import_string(src, pos(0, 12));
assert!(range.is_some(), "expected Some inside import string");
let r = range.unwrap();
assert_eq!(r.start.character, 8);
assert_eq!(r.end.character, 17);
}
#[test]
fn ts_cursor_in_import_string_outside_semicolon() {
let src = b"import \"./Foo.sol\";";
let range = ts_cursor_in_import_string(src, pos(0, 19));
assert!(range.is_none(), "should be None past closing quote");
}
#[test]
fn ts_cursor_in_import_string_non_import_literal() {
let src = b"string memory s = \"hello\";";
let range = ts_cursor_in_import_string(src, pos(0, 20));
assert!(range.is_none(), "should be None for non-import string");
}
#[test]
fn ts_cursor_in_import_string_from_style() {
let src = b"import {Foo} from \"./Foo.sol\";";
let range = ts_cursor_in_import_string(src, pos(0, 19));
assert!(range.is_some(), "expected Some for from-style import");
}
#[test]
fn ts_cursor_in_import_string_empty_string() {
let src = b"import \"\"";
let range = ts_cursor_in_import_string(src, pos(0, 8));
assert!(range.is_some(), "expected Some for empty import string");
let r = range.unwrap();
assert_eq!(r.start.character, 8);
assert_eq!(r.end.character, 8); }
#[test]
fn ts_cursor_in_import_string_unclosed() {
let src = b"import {} from \"";
let range = ts_cursor_in_import_string(src, pos(0, 16));
assert!(range.is_some(), "expected Some for unclosed import string");
let r = range.unwrap();
assert_eq!(r.start.character, 16); }
#[test]
fn ts_cursor_in_import_string_unclosed_mid() {
let src = b"import {} from \"whi";
let range = ts_cursor_in_import_string(src, pos(0, 19));
assert!(range.is_some(), "expected Some mid unclosed import string");
}
#[test]
fn ts_cursor_in_import_string_non_import_unclosed() {
let src = b"string memory s = \"hello";
let range = ts_cursor_in_import_string(src, pos(0, 20));
assert!(
range.is_none(),
"should be None for unclosed non-import string"
);
}
#[test]
fn ts_cursor_in_import_string_bare_unclosed() {
let src = b"import \"";
let range = ts_cursor_in_import_string(src, pos(0, 8));
assert!(range.is_some(), "expected Some for bare unclosed import");
let r = range.unwrap();
assert_eq!(r.start.character, 8);
}
#[test]
fn ts_cursor_in_import_string_bare_unclosed_mid() {
let src = b"import \"forge-std/";
let range = ts_cursor_in_import_string(src, pos(0, 18));
assert!(
range.is_some(),
"expected Some for `import \"forge-std/` (unclosed mid-path)"
);
let r = range.unwrap();
assert_eq!(r.start.character, 8); assert_eq!(r.end.character, 18); }
#[test]
fn ts_cursor_in_import_string_assembly_flags() {
let src = b"contract A { function f() internal { assembly (\"memory-safe\") {} } }";
let range = ts_cursor_in_import_string(src, pos(0, 50));
assert!(
range.is_none(),
"assembly dialect string must not trigger import completions"
);
}
#[test]
fn ts_cursor_in_import_string_revert_string() {
let src = b"contract A { function f() public { revert(\"err\"); } }";
let range = ts_cursor_in_import_string(src, pos(0, 43));
assert!(
range.is_none(),
"revert string must not trigger import completions"
);
}
#[test]
fn ts_cursor_in_assembly_flags_complete() {
let src = b"contract A { function f() internal { assembly (\"memory-safe\") {} } }";
let range = ts_cursor_in_assembly_flags(src, pos(0, 50));
assert!(
range.is_some(),
"expected Some inside assembly flags string"
);
}
#[test]
fn ts_cursor_in_assembly_flags_unclosed() {
let src = b"contract A { function f() internal { assembly (\"";
let range = ts_cursor_in_assembly_flags(src, pos(0, 48));
assert!(
range.is_some(),
"expected Some for unclosed assembly flags string"
);
}
#[test]
fn ts_cursor_in_assembly_flags_not_import() {
let src = b"import \"./Foo.sol\";";
let range = ts_cursor_in_assembly_flags(src, pos(0, 12));
assert!(
range.is_none(),
"import string must not match assembly_flags"
);
}
#[test]
fn ts_cursor_in_assembly_flags_not_revert() {
let src = b"contract A { function f() public { revert(\"err\"); } }";
let range = ts_cursor_in_assembly_flags(src, pos(0, 43));
assert!(
range.is_none(),
"revert string must not match assembly_flags"
);
}
}