use tower_lsp::lsp_types::{Position, Range, SelectionRange};
use tree_sitter::{Node, Parser, Point};
pub fn selection_ranges(source: &str, positions: &[Position]) -> Vec<SelectionRange> {
let tree = match parse(source) {
Some(t) => t,
None => return positions.iter().map(|_| empty_selection_range()).collect(),
};
let root = tree.root_node();
positions
.iter()
.map(|pos| build_selection_range(root, source, *pos))
.collect()
}
fn build_selection_range(root: Node, source: &str, position: Position) -> SelectionRange {
let point = Point {
row: position.line as usize,
column: position.character as usize,
};
let leaf = match root.descendant_for_point_range(point, point) {
Some(n) => n,
None => return empty_selection_range(),
};
let mut ancestors = Vec::new();
let mut current = leaf;
let mut last_range: Option<Range> = None;
loop {
let range = node_range(current, source);
if last_range != Some(range) {
ancestors.push(range);
last_range = Some(range);
}
match current.parent() {
Some(p) => current = p,
None => break,
}
}
let mut result: Option<SelectionRange> = None;
for range in ancestors.into_iter().rev() {
result = Some(SelectionRange {
range,
parent: result.map(Box::new),
});
}
result.unwrap_or_else(empty_selection_range)
}
fn node_range(node: Node, source: &str) -> Range {
let start = node.start_position();
let end = node.end_position();
Range {
start: Position {
line: start.row as u32,
character: utf16_col(source, start.row, start.column),
},
end: Position {
line: end.row as u32,
character: utf16_col(source, end.row, end.column),
},
}
}
fn utf16_col(source: &str, row: usize, byte_col: usize) -> u32 {
let line_start = source
.split('\n')
.take(row)
.map(|l| l.len() + 1)
.sum::<usize>();
let slice = &source[line_start..line_start + byte_col.min(source.len() - line_start)];
slice.encode_utf16().count() as u32
}
fn empty_selection_range() -> SelectionRange {
SelectionRange {
range: Range::default(),
parent: None,
}
}
fn parse(source: &str) -> Option<tree_sitter::Tree> {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_solidity::LANGUAGE.into())
.ok()?;
parser.parse(source, None)
}
#[cfg(test)]
mod tests {
use super::*;
fn chain(sr: &SelectionRange) -> Vec<Range> {
let mut out = vec![sr.range];
let mut cur = &sr.parent;
while let Some(p) = cur {
out.push(p.range);
cur = &p.parent;
}
out
}
fn depth(sr: &SelectionRange) -> usize {
chain(sr).len()
}
#[test]
fn test_empty_source() {
let results = selection_ranges("", &[Position::new(0, 0)]);
assert_eq!(results.len(), 1);
}
#[test]
fn test_single_identifier() {
let src =
"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nuint256 constant X = 1;\n";
let results = selection_ranges(src, &[Position::new(3, 18)]);
assert_eq!(results.len(), 1);
let ranges = chain(&results[0]);
assert_eq!(ranges[0].start.line, ranges[0].end.line);
let last = ranges.last().unwrap();
assert_eq!(last.start.line, 0);
}
#[test]
fn test_multiple_positions() {
let src = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Foo {\n uint256 x;\n uint256 y;\n}\n";
let results = selection_ranges(
src,
&[
Position::new(4, 12), Position::new(5, 12), ],
);
assert_eq!(results.len(), 2);
assert!(depth(&results[0]) >= 3);
assert!(depth(&results[1]) >= 3);
}
#[test]
fn test_ranges_are_nested() {
let src = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Foo {\n function bar() public pure returns (uint256) {\n return 42;\n }\n}\n";
let results = selection_ranges(src, &[Position::new(5, 15)]);
let ranges = chain(&results[0]);
for i in 1..ranges.len() {
let inner = &ranges[i - 1];
let outer = &ranges[i];
assert!(
(outer.start.line < inner.start.line
|| (outer.start.line == inner.start.line
&& outer.start.character <= inner.start.character))
&& (outer.end.line > inner.end.line
|| (outer.end.line == inner.end.line
&& outer.end.character >= inner.end.character)),
"range[{}] {:?} must contain range[{}] {:?}",
i,
outer,
i - 1,
inner
);
}
}
#[test]
fn test_no_duplicate_ranges() {
let src = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Foo {\n uint256 x;\n}\n";
let results = selection_ranges(src, &[Position::new(4, 12)]);
let ranges = chain(&results[0]);
for i in 1..ranges.len() {
assert_ne!(
ranges[i - 1],
ranges[i],
"consecutive ranges should not be identical"
);
}
}
#[test]
fn test_function_parameter() {
let src = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Foo {\n function add(uint256 a, uint256 b) public pure returns (uint256) {\n return a + b;\n }\n}\n";
let results = selection_ranges(src, &[Position::new(4, 25)]);
assert!(depth(&results[0]) >= 5, "depth = {}", depth(&results[0]));
}
#[test]
fn test_struct_field() {
let src = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nstruct Point {\n uint256 x;\n uint256 y;\n}\n";
let results = selection_ranges(src, &[Position::new(4, 12)]);
let ranges = chain(&results[0]);
assert!(depth(&results[0]) >= 3);
let last = ranges.last().unwrap();
assert_eq!(last.start.line, 0);
}
#[test]
fn test_nested_expression() {
let src = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Foo {\n function calc(uint256 a, uint16 b, uint16 c) internal pure returns (uint256) {\n return a + (a * b / c);\n }\n}\n";
let results = selection_ranges(src, &[Position::new(5, 24)]);
assert!(
depth(&results[0]) >= 8,
"nested expression depth = {}",
depth(&results[0])
);
}
#[test]
fn test_event_parameter() {
let src = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Foo {\n event Transfer(address indexed from, address indexed to, uint256 value);\n}\n";
let results = selection_ranges(src, &[Position::new(4, 35)]);
assert!(depth(&results[0]) >= 3);
}
#[test]
fn test_mapping_type() {
let src = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Foo {\n mapping(address => uint256) public balances;\n}\n";
let results = selection_ranges(src, &[Position::new(4, 12)]);
assert!(depth(&results[0]) >= 3);
}
#[test]
fn test_if_condition() {
let src = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Foo {\n function check(uint256 x) public pure returns (bool) {\n if (x > 0) {\n return true;\n }\n return false;\n }\n}\n";
let results = selection_ranges(src, &[Position::new(5, 12)]);
assert!(depth(&results[0]) >= 6);
}
#[test]
fn test_comment_position() {
let src = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\n// This is a comment\ncontract Foo {}\n";
let results = selection_ranges(src, &[Position::new(3, 5)]);
assert!(depth(&results[0]) >= 1);
}
#[test]
fn test_pragma_position() {
let src = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Foo {}\n";
let results = selection_ranges(src, &[Position::new(1, 10)]);
assert!(depth(&results[0]) >= 2);
}
#[test]
fn test_innermost_is_leaf() {
let src = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Foo {\n uint256 public value;\n}\n";
let results = selection_ranges(src, &[Position::new(4, 19)]);
let ranges = chain(&results[0]);
let inner = &ranges[0];
assert_eq!(inner.start.line, inner.end.line);
assert_eq!(inner.end.character - inner.start.character, 5); }
#[test]
fn test_outermost_is_source_file() {
let src = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\ncontract Foo {\n uint256 x;\n}\n";
let results = selection_ranges(src, &[Position::new(4, 12)]);
let ranges = chain(&results[0]);
let last = ranges.last().unwrap();
assert_eq!(last.start.line, 0);
assert_eq!(last.start.character, 0);
}
#[test]
fn test_shop_sol() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("example/Shop.sol");
let source = std::fs::read_to_string(&path).expect("read Shop.sol");
let positions = vec![
Position::new(42, 15), Position::new(29, 8), Position::new(68, 22), Position::new(0, 5), ];
let results = selection_ranges(&source, &positions);
assert_eq!(results.len(), 4);
for (i, sr) in results.iter().enumerate() {
assert!(
depth(sr) >= 2,
"position {} should have depth >= 2, got {}",
i,
depth(sr)
);
}
for (i, sr) in results.iter().enumerate() {
let ranges = chain(sr);
for j in 1..ranges.len() {
let inner = &ranges[j - 1];
let outer = &ranges[j];
assert!(
(outer.start.line < inner.start.line
|| (outer.start.line == inner.start.line
&& outer.start.character <= inner.start.character))
&& (outer.end.line > inner.end.line
|| (outer.end.line == inner.end.line
&& outer.end.character >= inner.end.character)),
"position {}: range[{}] {:?} must contain range[{}] {:?}",
i,
j,
outer,
j - 1,
inner
);
}
}
}
}