Skip to content

Commit 18a494d

Browse files
committed
feat: Allow goto definition for strings
1 parent 88c3220 commit 18a494d

File tree

7 files changed

+98
-63
lines changed

7 files changed

+98
-63
lines changed

crates/roughly/src/cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ pub fn ast(path: &Path) -> Result<(), DebugError> {
473473
};
474474

475475
let tree = tree::parse(&mut tree::new_parser(), &text, None);
476-
eprintln!("{}", tree::format(tree.root_node()));
476+
eprintln!("{}", tree::display_ast(tree.root_node()));
477477
Ok(())
478478
}
479479

crates/roughly/src/definition.rs

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ pub fn goto(
1717
tree: &Tree,
1818
symbols_map: &impl SymbolsMap,
1919
) -> Option<GotoDefinitionResponse> {
20-
let start = tree::try_get_identifier(tree, line, col)?;
21-
let name = rope.byte_slice(start.byte_range()).to_string();
20+
// note: R allows to call strings like "function_name"().
21+
// also useful for S4: e.g. to goto "Person" definition in
22+
// setMethod("age", "Person", ...) or setClass("Employee", contains = "Person")
23+
let (start, byte_range) = tree::try_get_identifier_or_string(tree, line, col)?;
24+
let name = rope.byte_slice(byte_range).to_string();
2225

2326
tracing::debug!(?name, start = start.kind(), "goto definition");
2427

@@ -35,23 +38,26 @@ pub fn goto(
3538
)));
3639
}
3740

38-
let is_local_scope = tree::find_containing_function(start).is_some();
41+
let is_global_scope = tree::find_containing_function(start).is_none();
3942

4043
let globals = symbols_map.filter_map(
4144
|path, symbols| {
4245
let other_uri = Uri::from_file_path(path).unwrap();
43-
// Only include locations from other files
44-
(if is_local_scope || &other_uri != uri {
45-
symbols
46-
} else {
47-
&[]
48-
})
49-
.iter()
50-
.filter(|symbol| symbol.name == name)
51-
.map(move |symbol| Location {
52-
uri: other_uri.clone(),
53-
range: symbol.range,
54-
})
46+
// note that s4 and r6 are not found by find_previous_definition.
47+
// therefore if is same file and global scope we must ignore later defintions
48+
let ignore_later = is_global_scope && *uri == other_uri;
49+
let name = &name;
50+
symbols
51+
.iter()
52+
.filter(move |symbol| {
53+
symbol.name == *name
54+
&& (!ignore_later
55+
|| symbol.range.end < utils::point_to_position(start.start_position()))
56+
})
57+
.map(move |symbol| Location {
58+
uri: other_uri.clone(),
59+
range: symbol.range,
60+
})
5561
},
5662
128,
5763
);
@@ -115,10 +121,9 @@ mod tests {
115121

116122
fn setup_a(src: &str, line: usize, col: usize) -> Option<(usize, usize)> {
117123
let rope = Rope::from_str(src);
118-
let mut parser = tree::new_parser();
119-
let tree = tree::parse(&mut parser, src, None);
120-
let start = tree::try_get_identifier(&tree, line, col).unwrap();
121-
let name = rope.byte_slice(start.byte_range()).to_string();
124+
let tree = tree::parse(&mut tree::new_parser(), src, None);
125+
let (start, byte_range) = tree::try_get_identifier_or_string(&tree, line, col)?;
126+
let name = rope.byte_slice(byte_range).to_string();
122127
find_previous_definition(start, &rope, &name).map(|node| {
123128
let point = node.start_position();
124129
(point.row, point.column)
@@ -127,24 +132,25 @@ mod tests {
127132

128133
fn setup_b(src: &str, line: usize, col: usize) -> Option<String> {
129134
let rope = Rope::from_str(src);
130-
let mut parser = tree::new_parser();
131-
let tree = tree::parse(&mut parser, src, None);
132-
tree::try_get_identifier(&tree, line, col)
133-
.map(|node| rope.byte_slice(node.byte_range()).to_string())
135+
let tree = tree::parse(&mut tree::new_parser(), src, None);
136+
tree::try_get_identifier_or_string(&tree, line, col)
137+
.map(|(_, byte_range)| rope.byte_slice(byte_range).to_string())
134138
}
135139

136140
#[test]
137-
fn try_get_identifier_returns_none_for_non_identifier() {
141+
fn try_get_identifier_base() {
138142
let src = indoc! {r#"
139143
123
140-
"string"
141144
x <- 1
142-
x
145+
x
146+
setMethod("age", "Person", \(x) x@age)
143147
"#};
144148
assert_eq!(setup_b(src, 0, 0), None);
145-
assert_eq!(setup_b(src, 1, 0), None);
146-
assert_eq!(setup_b(src, 2, 0), Some("x".to_string()));
147-
assert_eq!(setup_b(src, 3, 0), None);
149+
assert_eq!(setup_b(src, 1, 0), Some("x".into()));
150+
assert_eq!(setup_b(src, 2, 0), None);
151+
assert_eq!(setup_b(src, 3, 17), Some("Person".into()));
152+
// assert_eq!(setup_b(src, 3, 18), Some("Person".into()));
153+
assert_eq!(setup_b(src, 3, 24), Some("Person".into()));
148154
}
149155

150156
#[test]

crates/roughly/src/index.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ impl Item {
6969
pub trait SymbolsMap {
7070
fn filter_map<'a, T, I>(
7171
&'a self,
72-
key: impl Fn(&'a PathBuf, &'a [Item]) -> I,
72+
key: impl Fn(&'a Path, &'a [Item]) -> I,
7373
limit: usize,
7474
) -> Vec<T>
7575
where
@@ -79,7 +79,7 @@ pub trait SymbolsMap {
7979
impl SymbolsMap for HashMap<PathBuf, Vec<Item>> {
8080
fn filter_map<'a, T, I>(
8181
&'a self,
82-
key: impl Fn(&'a PathBuf, &'a [Item]) -> I,
82+
key: impl Fn(&'a Path, &'a [Item]) -> I,
8383
limit: usize,
8484
) -> Vec<T>
8585
where
@@ -394,7 +394,7 @@ fn index_call(call: Node, rope: &Rope, nested: bool) -> Option<Item> {
394394
})
395395
.unwrap_or_else(|| "Unknown".to_string());
396396

397-
Some(Item::new(
397+
(!method_name.is_empty()).then_some(Item::new(
398398
method_name,
399399
None,
400400
range,

crates/roughly/src/symbols.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ use crate::{
55
};
66

77
pub fn document(items: &[Item]) -> Vec<DocumentSymbol> {
8-
items.iter().map(to_document_symbol).collect()
8+
items
9+
.iter()
10+
.filter(|item| !item.name.is_empty()) // lsp: doens't allow empty names
11+
.map(to_document_symbol)
12+
.collect()
913
}
1014

1115
pub fn workspace(query: &str, workspace_symbols: &impl SymbolsMap) -> Vec<WorkspaceSymbol> {

crates/roughly/src/tree.rs

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -131,24 +131,43 @@ pub fn try_get_identifier<'tree>(
131131
line: usize,
132132
col: usize,
133133
) -> Option<Node<'tree>> {
134-
let start = {
135-
let point = Point::new(line, col);
136-
let node = tree.root_node().descendant_for_point_range(point, point)?;
137-
// If the cursor is at the very start of the program, `descendant_for_point_range` may
138-
// return the program node itself. Since only the program node can start at the same
139-
// position as an identifier, it's safe to check the first child node in this case
140-
match node.kind_id() {
141-
kind::PROGRAM => match node.child(0) {
142-
Some(child) if point_in_range(point, child.range()) => {
143-
child.descendant_for_point_range(point, point)?
144-
}
145-
_ => return None,
146-
},
147-
_ => node,
134+
let start = node_at_position(tree, Point::new(line, col))?;
135+
(start.kind_id() == kind::IDENTIFIER).then_some(start)
136+
}
137+
138+
pub fn try_get_identifier_or_string<'tree>(
139+
tree: &'tree Tree,
140+
line: usize,
141+
col: usize,
142+
) -> Option<(Node<'tree>, std::ops::Range<usize>)> {
143+
let start = node_at_position(tree, Point::new(line, col))?;
144+
Some(match start.kind_id() {
145+
kind::IDENTIFIER => (start, start.byte_range()),
146+
kind::STRING_CONTENT | kind::SINGLE_QUOTE | kind::DOUBLE_QUOTE => {
147+
let parent = start.parent()?;
148+
(
149+
parent,
150+
parent.child_by_field_id(field::CONTENT)?.byte_range(),
151+
)
148152
}
149-
};
153+
_ => return None,
154+
})
155+
}
150156

151-
(start.kind_id() == kind::IDENTIFIER).then_some(start)
157+
pub fn node_at_position<'tree>(tree: &'tree Tree, point: Point) -> Option<Node<'tree>> {
158+
let node = tree.root_node().descendant_for_point_range(point, point)?;
159+
// If the cursor is at the very start of the program, `descendant_for_point_range` may
160+
// return the program node itself. Since only the program node can start at the same
161+
// position as e.g. an identifier, it's safe to check the first child node in this case
162+
match node.kind_id() {
163+
kind::PROGRAM => match node.child(0) {
164+
Some(child) if point_in_range(point, child.range()) => {
165+
child.descendant_for_point_range(point, point)
166+
}
167+
_ => None,
168+
},
169+
_ => Some(node),
170+
}
152171
}
153172

154173
pub fn is_rhs_of_extract_or_namespace(node: Node) -> bool {
@@ -161,10 +180,10 @@ pub fn is_rhs_of_extract_or_namespace(node: Node) -> bool {
161180
}
162181

163182
//
164-
// FORMAT AST
183+
// DISPLAY AST
165184
//
166185

167-
pub fn format(node: Node) -> String {
186+
pub fn display_ast(node: Node) -> String {
168187
fn traverse(cursor: &mut TreeCursor, output: &mut String) {
169188
let indent = " ".repeat(cursor.depth() as usize);
170189
let node = cursor.node();

crates/roughly/src/utils.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use {
2-
crate::lsp_types::{Position, Range as LspRange},
2+
crate::lsp_types::{Position, Range},
33
ropey::Rope,
44
std::{fs::File, io::BufReader, path::Path},
5-
tree_sitter::Node,
5+
tree_sitter::{Node, Point},
66
};
77

88
pub fn starts_with_lowercase(name: &str, query: &str) -> bool {
@@ -13,15 +13,18 @@ pub fn read_to_rope(path: impl AsRef<Path>) -> std::io::Result<Rope> {
1313
Rope::from_reader(BufReader::new(File::open(path)?))
1414
}
1515

16-
pub fn node_range(node: Node) -> LspRange {
17-
let start = node.start_position();
18-
let end = node.end_position();
19-
LspRange::new(
20-
Position::new(start.row as u32, start.column as u32),
21-
Position::new(end.row as u32, end.column as u32),
16+
pub fn node_range(node: Node) -> Range {
17+
Range::new(
18+
point_to_position(node.start_position()),
19+
point_to_position(node.end_position()),
2220
)
2321
}
2422

23+
#[inline(always)]
24+
pub fn point_to_position(point: Point) -> Position {
25+
Position::new(point.row as u32, point.column as u32)
26+
}
27+
2528
// adapted from https://doc.rust-lang.org/stable/nightly-rustc/src/clippy_utils/str_utils.rs.html
2629

2730
/// ```

justfile

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ install-extension:
3838
# BUILD
3939
#
4040

41+
build:
42+
cargo build
43+
4144
build-linux:
4245
cargo build --release --target x86_64-unknown-linux-gnu
4346

@@ -71,7 +74,7 @@ publish $version $kind:
7174
set -euo pipefail
7275

7376
just bump-version $version
74-
just build $version $kind
77+
just release $version $kind
7578
just publish-github $version
7679
just publish-marketplace $version $kind
7780

@@ -83,10 +86,10 @@ publish-commit $version="":
8386
version=$(git rev-parse --short=6 HEAD)
8487
echo "info: using git revision $version as version"
8588
fi
86-
just build $version pre-release
89+
just release $version pre-release
8790
just publish-github $version
8891

89-
build $version $kind:
92+
release $version $kind:
9093
#!/usr/bin/env bash
9194
set -euo pipefail
9295

0 commit comments

Comments
 (0)