use crate::goto;
use crate::goto::CachedBuild;
use crate::references;
use crate::types::SourceLoc;
use std::collections::HashMap;
use tower_lsp::lsp_types::{Location, Position, Range, TextEdit, Url, WorkspaceEdit};
fn find_identifier_on_line(source_bytes: &[u8], line: u32, identifier: &str) -> Option<Range> {
let text = String::from_utf8_lossy(source_bytes);
let target_line = text.lines().nth(line as usize)?;
let ident_bytes = identifier.as_bytes();
let mut search_start = 0;
while let Some(offset) = target_line[search_start..].find(identifier) {
let col = search_start + offset;
let before_ok = col == 0 || {
let b = target_line.as_bytes()[col - 1];
!b.is_ascii_alphanumeric() && b != b'_'
};
let after_ok = col + ident_bytes.len() >= target_line.len() || {
let b = target_line.as_bytes()[col + ident_bytes.len()];
!b.is_ascii_alphanumeric() && b != b'_'
};
if before_ok && after_ok {
let line_start_byte: usize = text
.lines()
.take(line as usize)
.map(|l| l.len() + 1) .sum();
let start = crate::utils::byte_offset_to_position(&text, line_start_byte + col);
let end = crate::utils::byte_offset_to_position(
&text,
line_start_byte + col + ident_bytes.len(),
);
return Some(Range { start, end });
}
search_start = col + 1;
}
None
}
fn get_text_at_range(source_bytes: &[u8], range: &Range) -> Option<String> {
let start_byte = goto::pos_to_bytes(source_bytes, range.start);
let end_byte = goto::pos_to_bytes(source_bytes, range.end);
if end_byte > source_bytes.len() {
return None;
}
String::from_utf8(source_bytes[start_byte..end_byte].to_vec()).ok()
}
fn get_name_location_index(
build: &CachedBuild,
file_uri: &Url,
position: Position,
source_bytes: &[u8],
) -> Option<usize> {
let path = file_uri.to_file_path().ok()?;
let path_str = path.to_str()?;
let abs_path = build.path_to_abs.get(path_str)?;
let byte_position = goto::pos_to_bytes(source_bytes, position);
let node_id = references::byte_to_id(&build.nodes, abs_path, byte_position)?;
let file_nodes = build.nodes.get(abs_path)?;
let node_info = file_nodes.get(&node_id)?;
if !node_info.name_locations.is_empty() {
for (i, name_loc) in node_info.name_locations.iter().enumerate() {
if let Some(loc) = SourceLoc::parse(name_loc)
&& loc.offset <= byte_position
&& byte_position < loc.end()
{
return Some(i);
}
}
}
None
}
pub fn get_identifier_at_position(source_bytes: &[u8], position: Position) -> Option<String> {
let text = String::from_utf8_lossy(source_bytes);
let abs_offset = crate::utils::position_to_byte_offset(&text, position);
let lines: Vec<&str> = text.lines().collect();
let line = lines.get(position.line as usize)?;
let line_start = text
.as_bytes()
.iter()
.take(abs_offset)
.enumerate()
.rev()
.find(|&(_, &b)| b == b'\n')
.map(|(i, _)| i + 1)
.unwrap_or(0);
let col_byte = abs_offset - line_start;
if col_byte > line.len() {
return None;
}
let mut start = col_byte;
let mut end = col_byte;
while start > 0
&& (line.as_bytes()[start - 1].is_ascii_alphanumeric()
|| line.as_bytes()[start - 1] == b'_')
{
start -= 1;
}
while end < line.len()
&& (line.as_bytes()[end].is_ascii_alphanumeric() || line.as_bytes()[end] == b'_')
{
end += 1;
}
if start == end {
return None;
}
if line.as_bytes()[start].is_ascii_digit() {
return None;
}
Some(line[start..end].to_string())
}
pub fn get_identifier_range(source_bytes: &[u8], position: Position) -> Option<Range> {
let text = String::from_utf8_lossy(source_bytes);
let abs_offset = crate::utils::position_to_byte_offset(&text, position);
let lines: Vec<&str> = text.lines().collect();
let line = lines.get(position.line as usize)?;
let line_start = text
.as_bytes()
.iter()
.take(abs_offset)
.enumerate()
.rev()
.find(|&(_, &b)| b == b'\n')
.map(|(i, _)| i + 1)
.unwrap_or(0);
let col_byte = abs_offset - line_start;
if col_byte > line.len() {
return None;
}
let mut start = col_byte;
let mut end = col_byte;
while start > 0
&& (line.as_bytes()[start - 1].is_ascii_alphanumeric()
|| line.as_bytes()[start - 1] == b'_')
{
start -= 1;
}
while end < line.len()
&& (line.as_bytes()[end].is_ascii_alphanumeric() || line.as_bytes()[end] == b'_')
{
end += 1;
}
if start == end {
return None;
}
if line.as_bytes()[start].is_ascii_digit() {
return None;
}
let start = crate::utils::byte_offset_to_position(&text, line_start + start);
let end = crate::utils::byte_offset_to_position(&text, line_start + end);
Some(Range { start, end })
}
pub fn ts_alias_local_name_at_cursor(source_bytes: &[u8], cursor_byte: usize) -> Option<String> {
let source_str = std::str::from_utf8(source_bytes).ok()?;
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&tree_sitter_solidity::LANGUAGE.into())
.ok()?;
let tree = parser.parse(source_str, None)?;
fn find_alias(node: tree_sitter::Node, source: &str, cursor_byte: usize) -> Option<String> {
if node.kind() == "import_directive" {
let count = node.child_count();
let mut i = 0;
while i < count {
let child = node.child(i as u32)?;
if child.kind() == "as" {
if let Some(next) = node.child((i + 1) as u32)
&& next.kind() == "identifier"
&& next.start_byte() <= cursor_byte
&& cursor_byte < next.end_byte()
{
return Some(source[next.start_byte()..next.end_byte()].to_string());
}
}
i += 1;
}
return None;
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32)
&& let Some(result) = find_alias(child, source, cursor_byte)
{
return Some(result);
}
}
None
}
find_alias(tree.root_node(), source_str, cursor_byte)
}
pub fn ts_collect_identifier_locations(
source_bytes: &[u8],
file_uri: &Url,
name: &str,
) -> Vec<Location> {
let Some(source_str) = std::str::from_utf8(source_bytes).ok() else {
return vec![];
};
let mut parser = tree_sitter::Parser::new();
if parser
.set_language(&tree_sitter_solidity::LANGUAGE.into())
.is_err()
{
return vec![];
}
let Some(tree) = parser.parse(source_str, None) else {
return vec![];
};
let mut locations = Vec::new();
fn collect(
node: tree_sitter::Node,
source: &str,
source_bytes: &[u8],
name: &str,
file_uri: &Url,
out: &mut Vec<Location>,
) {
if node.kind() == "identifier" {
let text = &source[node.start_byte()..node.end_byte()];
if text == name
&& let (Some(start), Some(end)) = (
goto::bytes_to_pos(source_bytes, node.start_byte()),
goto::bytes_to_pos(source_bytes, node.end_byte()),
)
{
out.push(Location {
uri: file_uri.clone(),
range: Range { start, end },
});
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
collect(child, source, source_bytes, name, file_uri, out);
}
}
}
collect(
tree.root_node(),
source_str,
source_bytes,
name,
file_uri,
&mut locations,
);
locations
}
pub fn ts_find_alias_names(source_bytes: &[u8]) -> std::collections::HashSet<String> {
let mut names = std::collections::HashSet::new();
let Some(source_str) = std::str::from_utf8(source_bytes).ok() else {
return names;
};
let mut parser = tree_sitter::Parser::new();
if parser
.set_language(&tree_sitter_solidity::LANGUAGE.into())
.is_err()
{
return names;
}
let Some(tree) = parser.parse(source_str, None) else {
return names;
};
fn collect_aliases(
node: tree_sitter::Node,
source: &str,
out: &mut std::collections::HashSet<String>,
) {
if node.kind() == "import_directive" {
let count = node.child_count();
let mut i = 0;
while i < count {
if let Some(child) = node.child(i as u32)
&& child.kind() == "as"
{
if let Some(next) = node.child((i + 1) as u32)
&& next.kind() == "identifier"
{
out.insert(source[next.start_byte()..next.end_byte()].to_string());
}
}
i += 1;
}
return;
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
collect_aliases(child, source, out);
}
}
}
collect_aliases(tree.root_node(), source_str, &mut names);
names
}
pub fn ts_find_alias_declaration(source_bytes: &[u8], alias_name: &str) -> Option<Range> {
let source_str = std::str::from_utf8(source_bytes).ok()?;
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&tree_sitter_solidity::LANGUAGE.into())
.ok()?;
let tree = parser.parse(source_str, None)?;
fn find_decl(
node: tree_sitter::Node,
source: &str,
source_bytes: &[u8],
alias_name: &str,
) -> Option<Range> {
if node.kind() == "import_directive" {
let count = node.child_count();
let mut i = 0;
while i < count {
if let Some(child) = node.child(i as u32)
&& child.kind() == "as"
&& let Some(next) = node.child((i + 1) as u32)
&& next.kind() == "identifier"
{
let text = &source[next.start_byte()..next.end_byte()];
if text == alias_name {
let start = goto::bytes_to_pos(source_bytes, next.start_byte())?;
let end = goto::bytes_to_pos(source_bytes, next.end_byte())?;
return Some(Range { start, end });
}
}
i += 1;
}
return None;
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32)
&& let Some(result) = find_decl(child, source, source_bytes, alias_name)
{
return Some(result);
}
}
None
}
find_decl(tree.root_node(), source_str, source_bytes, alias_name)
}
pub fn ts_alias_foreign_byte_offset(source_bytes: &[u8], cursor_byte: usize) -> Option<usize> {
let source_str = std::str::from_utf8(source_bytes).ok()?;
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&tree_sitter_solidity::LANGUAGE.into())
.ok()?;
let tree = parser.parse(source_str, None)?;
fn find_foreign(node: tree_sitter::Node, cursor_byte: usize) -> Option<usize> {
if node.kind() == "import_directive" {
let count = node.child_count();
let mut i = 0;
while i < count {
let child = node.child(i as u32)?;
if child.kind() == "as" {
if let Some(next) = node.child((i + 1) as u32)
&& next.kind() == "identifier"
&& next.start_byte() <= cursor_byte
&& cursor_byte < next.end_byte()
{
if i > 0
&& let Some(prev) = node.child((i - 1) as u32)
&& prev.kind() == "identifier"
{
return Some(prev.start_byte());
}
return None;
}
}
i += 1;
}
return None;
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32)
&& let Some(result) = find_foreign(child, cursor_byte)
{
return Some(result);
}
}
None
}
find_foreign(tree.root_node(), cursor_byte)
}
type RenameEdits = HashMap<Url, HashMap<(u32, u32, u32, u32), TextEdit>>;
pub fn rename_symbol(
build: &CachedBuild,
file_uri: &Url,
position: Position,
source_bytes: &[u8],
new_name: String,
other_builds: &[&CachedBuild],
text_buffers: &HashMap<String, Vec<u8>>,
) -> Option<WorkspaceEdit> {
let original_identifier = get_identifier_at_position(source_bytes, position)?;
let cursor_byte = goto::pos_to_bytes(source_bytes, position);
let is_alias =
if let Some(alias_name) = ts_alias_local_name_at_cursor(source_bytes, cursor_byte) {
if alias_name == original_identifier {
Some(alias_name)
} else {
None
}
} else {
let alias_names = ts_find_alias_names(source_bytes);
if alias_names.contains(original_identifier.as_str()) {
Some(original_identifier.clone())
} else {
None
}
};
if let Some(alias_name) = is_alias {
let file_bytes = text_buffers
.get(file_uri.as_str())
.cloned()
.unwrap_or_else(|| source_bytes.to_vec());
let locations = ts_collect_identifier_locations(&file_bytes, file_uri, &alias_name);
return build_workspace_edit(
locations,
&alias_name,
new_name,
text_buffers,
source_bytes,
file_uri,
);
}
let name_location_index = get_name_location_index(build, file_uri, position, source_bytes);
let mut locations = references::goto_references_cached(
build,
file_uri,
position,
source_bytes,
name_location_index,
true, );
let current_abs = file_uri
.to_file_path()
.ok()
.and_then(|p| p.to_str().map(String::from));
if let Some((def_abs_path, def_byte_offset)) =
references::resolve_target_location(build, file_uri, position, source_bytes)
{
for other_build in other_builds {
let other_locations = references::goto_references_for_target(
other_build,
&def_abs_path,
def_byte_offset,
name_location_index,
true, current_abs.as_deref(),
);
locations.extend(other_locations);
}
}
build_workspace_edit(
locations,
&original_identifier,
new_name,
text_buffers,
source_bytes,
file_uri,
)
}
fn build_workspace_edit(
mut locations: Vec<Location>,
original_identifier: &str,
new_name: String,
text_buffers: &HashMap<String, Vec<u8>>,
_source_bytes: &[u8],
_file_uri: &Url,
) -> Option<WorkspaceEdit> {
let mut seen = std::collections::HashSet::new();
locations.retain(|loc| {
seen.insert((
loc.uri.clone(),
loc.range.start.line,
loc.range.start.character,
loc.range.end.line,
loc.range.end.character,
))
});
if locations.is_empty() {
return None;
}
let mut changes: RenameEdits = HashMap::new();
for location in locations {
let file_source_bytes = if let Some(buf) = text_buffers.get(location.uri.as_str()) {
buf.clone()
} else {
let absolute_path = match location.uri.to_file_path() {
Ok(p) => p,
Err(_) => continue,
};
match std::fs::read(&absolute_path) {
Ok(b) => b,
Err(_) => continue,
}
};
let text_at_range = get_text_at_range(&file_source_bytes, &location.range);
let actual_range = if text_at_range.as_deref() == Some(original_identifier) {
location.range
} else if text_at_range.is_some_and(|t| !t.is_empty()) {
continue;
} else {
match find_identifier_on_line(
&file_source_bytes,
location.range.start.line,
original_identifier,
) {
Some(corrected) => corrected,
None => continue,
}
};
let text_edit = TextEdit {
range: actual_range,
new_text: new_name.clone(),
};
let key = (
actual_range.start.line,
actual_range.start.character,
actual_range.end.line,
actual_range.end.character,
);
changes
.entry(location.uri)
.or_default()
.insert(key, text_edit);
}
let changes_vec: HashMap<Url, Vec<TextEdit>> = changes
.into_iter()
.map(|(uri, edits_map)| (uri, edits_map.into_values().collect()))
.collect();
Some(WorkspaceEdit {
changes: Some(changes_vec),
document_changes: None,
change_annotations: None,
})
}
#[cfg(test)]
mod tests {
use super::*;
const ALIAS_SOL: &[u8] = b"// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import {Test as MyTest} from \"./A.sol\";
import \"./A.sol\" as AFile;
import \"./A.sol\";
contract AliasUser {
MyTest public myTest;
AFile.Test public afileTest;
}
";
#[test]
fn test_ts_find_alias_names_finds_symbol_alias() {
let names = ts_find_alias_names(ALIAS_SOL);
assert!(
names.contains("MyTest"),
"should find symbol alias MyTest: {:?}",
names
);
}
#[test]
fn test_ts_find_alias_names_finds_unit_alias() {
let names = ts_find_alias_names(ALIAS_SOL);
assert!(
names.contains("AFile"),
"should find unit alias AFile: {:?}",
names
);
}
#[test]
fn test_ts_find_alias_names_does_not_include_original_name() {
let names = ts_find_alias_names(ALIAS_SOL);
assert!(
!names.contains("Test"),
"should NOT include original name Test: {:?}",
names
);
}
#[test]
fn test_ts_find_alias_names_count() {
let names = ts_find_alias_names(ALIAS_SOL);
assert_eq!(names.len(), 2, "should find exactly 2 aliases: {:?}", names);
}
#[test]
fn test_ts_find_alias_names_no_aliases() {
let source = b"// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import {Test} from \"./A.sol\";
import \"./A.sol\";
";
let names = ts_find_alias_names(source);
assert!(names.is_empty(), "should find no aliases: {:?}", names);
}
#[test]
fn test_ts_find_alias_names_empty_source() {
let names = ts_find_alias_names(b"");
assert!(names.is_empty());
}
#[test]
fn test_ts_alias_at_cursor_on_mytest_import() {
let result = ts_alias_local_name_at_cursor(ALIAS_SOL, 80);
assert_eq!(result.as_deref(), Some("MyTest"));
}
#[test]
fn test_ts_alias_at_cursor_on_afile_import() {
let result = ts_alias_local_name_at_cursor(ALIAS_SOL, 124);
assert_eq!(result.as_deref(), Some("AFile"));
}
#[test]
fn test_ts_alias_at_cursor_on_test_import_returns_none() {
let result = ts_alias_local_name_at_cursor(ALIAS_SOL, 72);
assert_eq!(result, None);
}
#[test]
fn test_ts_alias_at_cursor_on_usage_returns_none() {
let result = ts_alias_local_name_at_cursor(ALIAS_SOL, 175);
assert_eq!(result, None);
}
#[test]
fn test_ts_collect_mytest_locations() {
let uri = Url::parse("file:///test/Alias.sol").unwrap();
let locations = ts_collect_identifier_locations(ALIAS_SOL, &uri, "MyTest");
assert_eq!(
locations.len(),
2,
"MyTest should appear at import alias + usage: {:?}",
locations
.iter()
.map(|l| format!("{}:{}", l.range.start.line, l.range.start.character))
.collect::<Vec<_>>()
);
assert!(
locations
.iter()
.any(|l| l.range.start.line == 3 && l.range.start.character == 16)
);
assert!(
locations
.iter()
.any(|l| l.range.start.line == 8 && l.range.start.character == 4)
);
}
#[test]
fn test_ts_collect_afile_locations() {
let uri = Url::parse("file:///test/Alias.sol").unwrap();
let locations = ts_collect_identifier_locations(ALIAS_SOL, &uri, "AFile");
assert_eq!(
locations.len(),
2,
"AFile should appear at import alias + usage: {:?}",
locations
.iter()
.map(|l| format!("{}:{}", l.range.start.line, l.range.start.character))
.collect::<Vec<_>>()
);
assert!(
locations
.iter()
.any(|l| l.range.start.line == 4 && l.range.start.character == 20)
);
assert!(
locations
.iter()
.any(|l| l.range.start.line == 9 && l.range.start.character == 4)
);
}
#[test]
fn test_ts_collect_nonexistent_name() {
let uri = Url::parse("file:///test/Alias.sol").unwrap();
let locations = ts_collect_identifier_locations(ALIAS_SOL, &uri, "Nonexistent");
assert!(locations.is_empty());
}
#[test]
fn test_ts_find_alias_declaration_symbol_alias() {
let range = ts_find_alias_declaration(ALIAS_SOL, "MyTest");
assert!(range.is_some(), "should find MyTest declaration");
let range = range.unwrap();
assert_eq!(range.start.line, 3);
assert_eq!(range.start.character, 16);
assert_eq!(range.end.character, 22);
}
#[test]
fn test_ts_find_alias_declaration_unit_alias() {
let range = ts_find_alias_declaration(ALIAS_SOL, "AFile");
assert!(range.is_some(), "should find AFile declaration");
let range = range.unwrap();
assert_eq!(range.start.line, 4);
assert_eq!(range.start.character, 20);
assert_eq!(range.end.character, 25);
}
#[test]
fn test_ts_find_alias_declaration_nonexistent() {
let range = ts_find_alias_declaration(ALIAS_SOL, "Nonexistent");
assert!(range.is_none(), "should not find nonexistent alias");
}
#[test]
fn test_ts_find_alias_declaration_original_name() {
let range = ts_find_alias_declaration(ALIAS_SOL, "Test");
assert!(range.is_none(), "should not find original name as alias");
}
#[test]
fn test_ts_find_alias_declaration_empty_source() {
let range = ts_find_alias_declaration(b"", "MyTest");
assert!(range.is_none());
}
#[test]
fn test_ts_alias_foreign_byte_offset_symbol_alias() {
let result = ts_alias_foreign_byte_offset(ALIAS_SOL, 80);
assert_eq!(
result,
Some(72),
"should return byte offset of foreign 'Test'"
);
}
#[test]
fn test_ts_alias_foreign_byte_offset_unit_alias() {
let result = ts_alias_foreign_byte_offset(ALIAS_SOL, 124);
assert_eq!(result, None, "unit alias has no foreign identifier");
}
#[test]
fn test_ts_alias_foreign_byte_offset_usage_site() {
let result = ts_alias_foreign_byte_offset(ALIAS_SOL, 175);
assert_eq!(result, None, "usage site is not inside import_directive");
}
#[test]
fn test_ts_alias_foreign_byte_offset_empty_source() {
let result = ts_alias_foreign_byte_offset(b"", 0);
assert_eq!(result, None);
}
#[test]
fn test_ts_collect_mytest_does_not_include_test() {
let uri = Url::parse("file:///test/Alias.sol").unwrap();
let locations = ts_collect_identifier_locations(ALIAS_SOL, &uri, "MyTest");
for loc in &locations {
let len = loc.range.end.character - loc.range.start.character;
assert_eq!(
len, 6,
"each match should be 6 chars (MyTest), got {} at line {}",
len, loc.range.start.line
);
}
}
}