Skip to content

Commit 7bee1ba

Browse files
authored
feat: add find references feature (#40)
- fixed a bug which caused prefix-ed fields to be renames as well.
1 parent 5e07a16 commit 7bee1ba

16 files changed

+357
-36
lines changed

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "protols"
33
description = "Language server for proto3 files"
4-
version = "0.7.1"
4+
version = "0.8.0"
55
edition = "2021"
66
license = "MIT"
77
homepage = "https://github.com/coder3101/protols"

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- ✅ Go to Definition
1717
- ✅ Hover Information
1818
- ✅ Rename Symbols
19+
- ✅ Find references
1920

2021
## 🚀 Getting Started
2122

@@ -71,6 +72,10 @@ Displays comments and documentation for protobuf symbols on hover. Works seamles
7172

7273
Allows renaming of symbols like messages and enums, along with all their usages across packages. Currently, renaming fields within symbols is not supported directly.
7374

75+
### Find References
76+
77+
Allows user defined types like messages and enums can be checked for references. Nested fields are completely supported.
78+
7479
---
7580

7681
Protols is designed to supercharge your workflow with **proto** files. We welcome contributions and feedback from the community! Feel free to check out the [repository](https://github.com/coder3101/protols) and join in on improving this tool! 🎉

src/lsp.rs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,19 @@ use std::thread;
66
use tracing::{error, info};
77

88
use async_lsp::lsp_types::{
9-
CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse, CreateFilesParams, DeleteFilesParams, DidChangeConfigurationParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentFormattingParams, DocumentRangeFormattingParams, DocumentSymbolParams, DocumentSymbolResponse, FileOperationFilter, FileOperationPattern, FileOperationPatternKind, FileOperationRegistrationOptions, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, HoverProviderCapability, InitializeParams, InitializeResult, OneOf, PrepareRenameResponse, ProgressParams, RenameFilesParams, RenameOptions, RenameParams, ServerCapabilities, ServerInfo, TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Url, WorkspaceEdit, WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities
9+
CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse,
10+
CreateFilesParams, DeleteFilesParams, DidChangeConfigurationParams,
11+
DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
12+
DidSaveTextDocumentParams, DocumentFormattingParams, DocumentRangeFormattingParams,
13+
DocumentSymbolParams, DocumentSymbolResponse, FileOperationFilter, FileOperationPattern,
14+
FileOperationPatternKind, FileOperationRegistrationOptions, GotoDefinitionParams,
15+
GotoDefinitionResponse, Hover, HoverContents, HoverParams, HoverProviderCapability,
16+
InitializeParams, InitializeResult, Location, OneOf, PrepareRenameResponse, ProgressParams,
17+
ReferenceParams, RenameFilesParams, RenameOptions, RenameParams, ServerCapabilities,
18+
ServerInfo, TextDocumentPositionParams, TextDocumentSyncCapability,
19+
TextDocumentSyncKind, TextEdit, Url, WorkspaceEdit,
20+
WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities,
21+
WorkspaceServerCapabilities,
1022
};
1123
use async_lsp::{LanguageClient, LanguageServer, ResponseError};
1224
use futures::future::BoxFuture;
@@ -69,7 +81,8 @@ impl LanguageServer for ProtoLanguageServer {
6981
let mut formatter_provider = None;
7082
let mut formatter_range_provider = None;
7183
if let Some(folders) = params.workspace_folders {
72-
if let Ok(f) = ClangFormatter::new("clang-format", folders.first().map(|f| f.uri.path()))
84+
if let Ok(f) =
85+
ClangFormatter::new("clang-format", folders.first().map(|f| f.uri.path()))
7386
{
7487
self.state.add_formatter(f);
7588
formatter_provider = Some(OneOf::Left(true));
@@ -124,6 +137,7 @@ impl LanguageServer for ProtoLanguageServer {
124137
rename_provider: Some(rename_provider),
125138
document_formatting_provider: formatter_provider,
126139
document_range_formatting_provider: formatter_range_provider,
140+
references_provider: Some(OneOf::Left(true)),
127141

128142
..ServerCapabilities::default()
129143
},
@@ -265,6 +279,43 @@ impl LanguageServer for ProtoLanguageServer {
265279
Box::pin(async move { Ok(response) })
266280
}
267281

282+
fn references(
283+
&mut self,
284+
param: ReferenceParams,
285+
) -> BoxFuture<'static, Result<Option<Vec<Location>>, ResponseError>> {
286+
let uri = param.text_document_position.text_document.uri;
287+
let pos = param.text_document_position.position;
288+
289+
let Some(tree) = self.state.get_tree(&uri) else {
290+
error!(uri=%uri, "failed to get tree");
291+
return Box::pin(async move { Ok(None) });
292+
};
293+
294+
let content = self.state.get_content(&uri);
295+
296+
let Some(current_package) = tree.get_package_name(content.as_bytes()) else {
297+
error!(uri=%uri, "failed to get package name");
298+
return Box::pin(async move { Ok(None) });
299+
};
300+
301+
let Some((mut refs, otext)) = tree.reference_tree(&pos, content.as_bytes()) else {
302+
error!(uri=%uri, "failed to find references in a tree");
303+
return Box::pin(async move { Ok(None) });
304+
};
305+
306+
if let Some(v) = self.state.reference_fields(current_package, &otext) {
307+
refs.extend(v);
308+
}
309+
310+
Box::pin(async move {
311+
if refs.is_empty() {
312+
Ok(None)
313+
} else {
314+
Ok(Some(refs))
315+
}
316+
})
317+
}
318+
268319
fn definition(
269320
&mut self,
270321
param: GotoDefinitionParams,

src/parser/input/test_reference.proto

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
syntax = "proto3";
2+
3+
package com.parser;
4+
5+
// A Book is book
6+
message Book {
7+
8+
// This is represents author
9+
// A author is a someone who writes books
10+
//
11+
// Author has a name and a country where they were born
12+
message Author {
13+
string name = 1;
14+
string country = 2;
15+
};
16+
Author author = 1;
17+
int price_usd = 2;
18+
}
19+
20+
message BookShelf {}
21+
22+
message Library {
23+
repeated Book books = 1;
24+
Book.Author collection = 2;
25+
BookShelf shelf = 3;
26+
}
27+
28+
service Myservice {
29+
rpc GetBook(Empty) returns (Book);
30+
rpc GetAuthor(Empty) returns (Book.Author)
31+
}

src/parser/input/test_rename.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ message Book {
1717
int price_usd = 2;
1818
}
1919

20+
message BookShelf {}
21+
2022
message Library {
2123
repeated Book books = 1;
2224
Book.Author collection = 2;
25+
BookShelf shelf = 3;
2326
}
2427

2528
service Myservice {

src/parser/rename.rs

Lines changed: 97 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use async_lsp::lsp_types::{Position, Range, TextEdit};
1+
use async_lsp::lsp_types::{Location, Position, Range, TextEdit};
22
use tree_sitter::Node;
33

44
use crate::{nodekind::NodeKind, utils::ts_to_lsp_position};
@@ -21,28 +21,52 @@ impl ParsedTree {
2121
})
2222
}
2323

24-
fn rename_within(
24+
fn nodes_within<'a>(
2525
&self,
26-
n: Node<'_>,
26+
n: Node<'a>,
2727
identifier: &str,
28-
new_identifier: &str,
2928
content: impl AsRef<[u8]>,
30-
) -> Option<Vec<TextEdit>> {
29+
) -> Option<Vec<Node<'a>>> {
3130
n.parent().map(|p| {
3231
self.filter_nodes_from(p, NodeKind::is_field_name)
3332
.into_iter()
3433
.filter(|i| i.utf8_text(content.as_ref()).expect("utf-8 parse error") == identifier)
35-
.map(|i| TextEdit {
36-
range: Range {
37-
start: ts_to_lsp_position(&i.start_position()),
38-
end: ts_to_lsp_position(&i.end_position()),
39-
},
40-
new_text: new_identifier.to_string(),
41-
})
4234
.collect()
4335
})
4436
}
4537

38+
pub fn reference_tree(
39+
&self,
40+
pos: &Position,
41+
content: impl AsRef<[u8]>,
42+
) -> Option<(Vec<Location>, String)> {
43+
let rename_range = self.can_rename(pos)?;
44+
45+
let mut res = vec![Location {
46+
uri: self.uri.clone(),
47+
range: rename_range,
48+
}];
49+
50+
let nodes = self.get_ancestor_nodes_at_position(pos);
51+
let mut i = 1;
52+
let mut otext = nodes.first()?.utf8_text(content.as_ref()).ok()?.to_owned();
53+
while nodes.len() > i {
54+
let id = nodes[i].utf8_text(content.as_ref()).ok()?;
55+
if let Some(inodes) = self.nodes_within(nodes[i], &otext, content.as_ref()) {
56+
res.extend(inodes.into_iter().map(|n| Location {
57+
uri: self.uri.clone(),
58+
range: Range {
59+
start: ts_to_lsp_position(&n.start_position()),
60+
end: ts_to_lsp_position(&n.end_position()),
61+
},
62+
}))
63+
}
64+
otext = format!("{id}.{otext}");
65+
i += 1
66+
}
67+
Some((res, otext))
68+
}
69+
4670
pub fn rename_tree(
4771
&self,
4872
pos: &Position,
@@ -65,8 +89,14 @@ impl ParsedTree {
6589
while nodes.len() > i {
6690
let id = nodes[i].utf8_text(content.as_ref()).ok()?;
6791

68-
if let Some(edit) = self.rename_within(nodes[i], &otext, &ntext, content.as_ref()) {
69-
v.extend(edit);
92+
if let Some(inodes) = self.nodes_within(nodes[i], &otext, content.as_ref()) {
93+
v.extend(inodes.into_iter().map(|n| TextEdit {
94+
range: Range {
95+
start: ts_to_lsp_position(&n.start_position()),
96+
end: ts_to_lsp_position(&n.end_position()),
97+
},
98+
new_text: ntext.to_owned(),
99+
}));
70100
}
71101

72102
otext = format!("{id}.{otext}");
@@ -87,9 +117,9 @@ impl ParsedTree {
87117
self.filter_nodes(NodeKind::is_field_name)
88118
.into_iter()
89119
.filter(|n| {
90-
n.utf8_text(content.as_ref())
91-
.expect("utf-8 parse error")
92-
.starts_with(old_identifier)
120+
let ntext = n.utf8_text(content.as_ref()).expect("utf-8 parse error");
121+
let sc = format!("{old_identifier}.");
122+
return ntext == old_identifier || ntext.starts_with(&sc);
93123
})
94124
.map(|n| {
95125
let text = n.utf8_text(content.as_ref()).expect("utf-8 parse error");
@@ -103,6 +133,20 @@ impl ParsedTree {
103133
})
104134
.collect()
105135
}
136+
137+
pub fn reference_field(&self, id: &str, content: impl AsRef<[u8]>) -> Vec<Location> {
138+
self.filter_nodes(NodeKind::is_field_name)
139+
.into_iter()
140+
.filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == id)
141+
.map(|n| Location {
142+
uri: self.uri.clone(),
143+
range: Range {
144+
start: ts_to_lsp_position(&n.start_position()),
145+
end: ts_to_lsp_position(&n.end_position()),
146+
},
147+
})
148+
.collect()
149+
}
106150
}
107151

108152
#[cfg(test)]
@@ -148,6 +192,42 @@ mod test {
148192
assert_yaml_snapshot!(rename_fn("xyx", &pos_non_rename));
149193
}
150194

195+
#[test]
196+
fn test_reference() {
197+
let uri: Url = "file://foo/bar.proto".parse().unwrap();
198+
let pos_book = Position {
199+
line: 5,
200+
character: 9,
201+
};
202+
let pos_author = Position {
203+
line: 11,
204+
character: 14,
205+
};
206+
let pos_non_ref = Position {
207+
line: 21,
208+
character: 5,
209+
};
210+
let contents = include_str!("input/test_reference.proto");
211+
212+
let parsed = ProtoParser::new().parse(uri.clone(), contents);
213+
assert!(parsed.is_some());
214+
let tree = parsed.unwrap();
215+
216+
let reference_fn = |pos: &Position| {
217+
if let Some(k) = tree.reference_tree(pos, contents) {
218+
let mut v = tree.reference_field(&k.1, contents);
219+
v.extend(k.0);
220+
v
221+
} else {
222+
vec![]
223+
}
224+
};
225+
226+
assert_yaml_snapshot!(reference_fn(&pos_book));
227+
assert_yaml_snapshot!(reference_fn(&pos_author));
228+
assert_yaml_snapshot!(reference_fn(&pos_non_ref));
229+
}
230+
151231
#[test]
152232
fn test_can_rename() {
153233
let uri: Url = "file://foo/bar/test.proto".parse().unwrap();
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
source: src/parser/rename.rs
3+
expression: reference_fn(&pos_author)
4+
---
5+
- uri: "file://foo/bar.proto"
6+
range:
7+
start:
8+
line: 23
9+
character: 2
10+
end:
11+
line: 23
12+
character: 13
13+
- uri: "file://foo/bar.proto"
14+
range:
15+
start:
16+
line: 29
17+
character: 32
18+
end:
19+
line: 29
20+
character: 43
21+
- uri: "file://foo/bar.proto"
22+
range:
23+
start:
24+
line: 11
25+
character: 10
26+
end:
27+
line: 11
28+
character: 16
29+
- uri: "file://foo/bar.proto"
30+
range:
31+
start:
32+
line: 15
33+
character: 2
34+
end:
35+
line: 15
36+
character: 8
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
source: src/parser/rename.rs
3+
expression: reference_fn(&pos_non_ref)
4+
---
5+
[]

0 commit comments

Comments
 (0)