Skip to content

Commit adda8ae

Browse files
authored
feat: recursively parse import files and goto definition of well known types (#56)
When a file is opened, all imports are parsed recursively up to a depth of 8. When a file is being edited it and its imports are parsed with a depth of 2.
1 parent f392094 commit adda8ae

18 files changed

+150
-61
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
-**Diagnostics**: Syntax errors and import error detected with the tree-sitter parser.
1616
-**Document Symbols**: Navigate and view all symbols, including messages and enums.
1717
-**Code Formatting**: Format `.proto` files using `clang-format` for a consistent style.
18-
-**Go to Definition**: Jump to the definition of symbols like messages or enums.
18+
-**Go to Definition**: Jump to the definition of symbols like messages or enums and imports.
1919
-**Hover Information**: Get detailed information and documentation on hover.
2020
-**Rename Symbols**: Rename protobuf symbols and propagate changes across the codebase.
2121
-**Find References**: Find where messages, enums, and fields are used throughout the codebase.
@@ -134,7 +134,7 @@ Protols provides a list of symbols in the current document, including nested sym
134134

135135
### Go to Definition
136136

137-
Jump directly to the definition of any custom symbol, including those in other files or packages. This feature works across package boundaries.
137+
Jump directly to the definition of any custom symbol or imports, including those in other files or packages. This feature works across package boundaries.
138138

139139
### Hover Information
140140

src/lsp.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ impl LanguageServer for ProtoLanguageServer {
381381
return ControlFlow::Continue(());
382382
};
383383

384-
let Some(diagnostics) = self.state.upsert_file(&uri, content.clone(), &ipath) else {
384+
let Some(diagnostics) = self.state.upsert_file(&uri, content.clone(), &ipath, 8) else {
385385
return ControlFlow::Continue(());
386386
};
387387

@@ -405,7 +405,7 @@ impl LanguageServer for ProtoLanguageServer {
405405
return ControlFlow::Continue(());
406406
};
407407

408-
let Some(diagnostics) = self.state.upsert_file(&uri, content, &ipath) else {
408+
let Some(diagnostics) = self.state.upsert_file(&uri, content, &ipath, 2) else {
409409
return ControlFlow::Continue(());
410410
};
411411

@@ -427,9 +427,10 @@ impl LanguageServer for ProtoLanguageServer {
427427
if let Ok(uri) = Url::from_file_path(&file.uri) {
428428
// Safety: The uri is always a file type
429429
let content = read_to_string(uri.to_file_path().unwrap()).unwrap_or_default();
430-
self.state.upsert_content(&uri, content, &[]);
431-
} else {
432-
error!(uri=%file.uri, "failed parse uri");
430+
431+
if let Some(ipath) = self.configs.get_include_paths(&uri) {
432+
self.state.upsert_content(&uri, content, &ipath, 2);
433+
}
433434
}
434435
}
435436
ControlFlow::Continue(())

src/state.rs

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::{
2-
collections::HashMap,
2+
collections::{HashMap, HashSet},
33
path::PathBuf,
4-
sync::{Arc, Mutex, MutexGuard, RwLock, RwLockWriteGuard},
4+
sync::{Arc, Mutex, RwLock},
55
};
66
use tracing::info;
77

@@ -66,34 +66,74 @@ impl ProtoLanguageState {
6666
}
6767

6868
fn upsert_content_impl(
69-
mut parser: MutexGuard<ProtoParser>,
69+
&mut self,
7070
uri: &Url,
7171
content: String,
72-
mut docs: RwLockWriteGuard<HashMap<Url, String>>,
73-
mut trees: RwLockWriteGuard<HashMap<Url, ParsedTree>>,
74-
) -> bool {
75-
if let Some(parsed) = parser.parse(uri.clone(), content.as_bytes()) {
76-
trees.insert(uri.clone(), parsed);
77-
docs.insert(uri.clone(), content);
78-
true
79-
} else {
80-
false
72+
ipath: &[PathBuf],
73+
depth: usize,
74+
parse_session: &mut HashSet<Url>,
75+
) {
76+
// Safety: to not cause stack overflow
77+
if depth == 0 {
78+
return;
79+
}
80+
81+
// avoid re-parsing same file incase of circular dependencies
82+
if parse_session.contains(uri) {
83+
return;
8184
}
85+
86+
let Some(parsed) = self
87+
.parser
88+
.lock()
89+
.expect("poison")
90+
.parse(uri.clone(), content.as_bytes())
91+
else {
92+
return;
93+
};
94+
95+
self.trees
96+
.write()
97+
.expect("posion")
98+
.insert(uri.clone(), parsed);
99+
100+
self.documents
101+
.write()
102+
.expect("poison")
103+
.insert(uri.clone(), content.clone());
104+
105+
parse_session.insert(uri.clone());
106+
let imports = self.get_owned_imports(uri, content.as_str());
107+
108+
for import in imports.iter() {
109+
if let Some(p) = ipath.iter().map(|p| p.join(import)).find(|p| p.exists()) {
110+
if let Ok(uri) = Url::from_file_path(p.clone()) {
111+
if let Ok(content) = std::fs::read_to_string(p) {
112+
self.upsert_content_impl(&uri, content, ipath, depth - 1, parse_session);
113+
}
114+
}
115+
}
116+
}
117+
}
118+
119+
fn get_owned_imports(&self, uri: &Url, content: &str) -> Vec<String> {
120+
self.get_tree(uri)
121+
.map(|t| t.get_import_paths(content.as_ref()))
122+
.unwrap_or_default()
123+
.into_iter()
124+
.map(ToOwned::to_owned)
125+
.collect()
82126
}
83127

84128
pub fn upsert_content(
85129
&mut self,
86130
uri: &Url,
87131
content: String,
88132
ipath: &[PathBuf],
133+
depth: usize,
89134
) -> Vec<String> {
90-
// Drop locks at end of block
91-
{
92-
let parser = self.parser.lock().expect("poison");
93-
let tree = self.trees.write().expect("poison");
94-
let docs = self.documents.write().expect("poison");
95-
Self::upsert_content_impl(parser, uri, content.clone(), docs, tree);
96-
}
135+
let mut session = HashSet::new();
136+
self.upsert_content_impl(uri, content.clone(), ipath, depth, &mut session);
97137

98138
// After content is upserted, those imports which couldn't be located
99139
// are flagged as import error
@@ -189,9 +229,10 @@ impl ProtoLanguageState {
189229
uri: &Url,
190230
content: String,
191231
ipath: &[PathBuf],
232+
depth: usize,
192233
) -> Option<PublishDiagnosticsParams> {
193-
info!(uri=%uri, "upserting file");
194-
let diag = self.upsert_content(uri, content.clone(), ipath);
234+
info!(%uri, %depth, "upserting file");
235+
let diag = self.upsert_content(uri, content.clone(), ipath, depth);
195236
self.get_tree(uri).map(|tree| {
196237
let diag = tree.collect_import_diagnostics(content.as_ref(), diag);
197238
let mut d = tree.collect_parse_diagnostics();
@@ -205,13 +246,13 @@ impl ProtoLanguageState {
205246
}
206247

207248
pub fn delete_file(&mut self, uri: &Url) {
208-
info!(uri=%uri, "deleting file");
249+
info!(%uri, "deleting file");
209250
self.documents.write().expect("poison").remove(uri);
210251
self.trees.write().expect("poison").remove(uri);
211252
}
212253

213254
pub fn rename_file(&mut self, new_uri: &Url, old_uri: &Url) {
214-
info!(new_uri=%new_uri, old_uri=%new_uri, "renaming file");
255+
info!(%new_uri, %new_uri, "renaming file");
215256

216257
if let Some(v) = self.documents.write().expect("poison").remove(old_uri) {
217258
self.documents

src/workspace/definition.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ mod test {
6666
let c = include_str!("input/c.proto");
6767

6868
let mut state: ProtoLanguageState = ProtoLanguageState::new();
69-
state.upsert_file(&a_uri, a.to_owned(), &ipath);
70-
state.upsert_file(&b_uri, b.to_owned(), &ipath);
71-
state.upsert_file(&c_uri, c.to_owned(), &ipath);
69+
state.upsert_file(&a_uri, a.to_owned(), &ipath, 2);
70+
state.upsert_file(&b_uri, b.to_owned(), &ipath, 2);
71+
state.upsert_file(&c_uri, c.to_owned(), &ipath, 2);
7272

7373
assert_yaml_snapshot!(state.definition(
7474
&ipath,

src/workspace/hover.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -672,7 +672,7 @@ mod test {
672672

673673
#[test]
674674
fn workspace_test_hover() {
675-
let ipath = vec![PathBuf::from("src/workspace/input")];
675+
let ipath = vec![std::env::current_dir().unwrap().join("src/workspace/input")];
676676
let a_uri = "file://input/a.proto".parse().unwrap();
677677
let b_uri = "file://input/b.proto".parse().unwrap();
678678
let c_uri = "file://input/c.proto".parse().unwrap();
@@ -682,9 +682,9 @@ mod test {
682682
let c = include_str!("input/c.proto");
683683

684684
let mut state: ProtoLanguageState = ProtoLanguageState::new();
685-
state.upsert_file(&a_uri, a.to_owned(), &ipath);
686-
state.upsert_file(&b_uri, b.to_owned(), &ipath);
687-
state.upsert_file(&c_uri, c.to_owned(), &ipath);
685+
state.upsert_file(&a_uri, a.to_owned(), &ipath, 3);
686+
state.upsert_file(&b_uri, b.to_owned(), &ipath, 2);
687+
state.upsert_file(&c_uri, c.to_owned(), &ipath, 2);
688688

689689
assert_yaml_snapshot!(state.hover(
690690
&ipath,
@@ -719,7 +719,12 @@ mod test {
719719
assert_yaml_snapshot!(state.hover(
720720
&ipath,
721721
"com.workspace",
722-
Hoverables::ImportPath("c.proto".to_string())
723-
))
722+
Hoverables::Identifier("com.inner.Why".to_string())
723+
));
724+
assert_yaml_snapshot!(state.hover(
725+
&ipath,
726+
"com.workspace",
727+
Hoverables::Identifier("com.super.secret.SomeSecret".to_string())
728+
));
724729
}
725730
}

src/workspace/input/a.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ syntax = "proto3";
33
package com.workspace;
44

55
import "c.proto";
6+
import "inner/x.proto";
67

78
// A Book is a book
89
message Book {
910
Author author = 1;
1011
Author.Address foo = 2;
1112
com.utility.Foobar.Baz z = 3;
13+
com.inner.Why reason = 4;
1214
}
1315

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
syntax = "proto3";
2+
3+
package com.super.secret;
4+
5+
// SomeSecret is a real secret with hidden string
6+
message SomeSecret {
7+
string secret = 1;
8+
}

src/workspace/input/inner/x.proto

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
syntax = "proto3";
2+
3+
package com.inner;
4+
5+
import "inner/secret/y.proto";
6+
7+
// Why is a reason with secret
8+
message Why {
9+
string reason = 1;
10+
com.super.secret.SomeSecret secret = 2;
11+
}
12+

src/workspace/rename.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@ mod test {
7979
let c = include_str!("input/c.proto");
8080

8181
let mut state: ProtoLanguageState = ProtoLanguageState::new();
82-
state.upsert_file(&a_uri, a.to_owned(), &ipath);
83-
state.upsert_file(&b_uri, b.to_owned(), &ipath);
84-
state.upsert_file(&c_uri, c.to_owned(), &ipath);
82+
state.upsert_file(&a_uri, a.to_owned(), &ipath, 2);
83+
state.upsert_file(&b_uri, b.to_owned(), &ipath, 2);
84+
state.upsert_file(&c_uri, c.to_owned(), &ipath, 2);
8585

8686
assert_yaml_snapshot!(state.rename_fields("com.workspace", "Author", "Writer"));
8787
assert_yaml_snapshot!(state.rename_fields(
@@ -104,9 +104,9 @@ mod test {
104104
let c = include_str!("input/c.proto");
105105

106106
let mut state: ProtoLanguageState = ProtoLanguageState::new();
107-
state.upsert_file(&a_uri, a.to_owned(), &ipath);
108-
state.upsert_file(&b_uri, b.to_owned(), &ipath);
109-
state.upsert_file(&c_uri, c.to_owned(), &ipath);
107+
state.upsert_file(&a_uri, a.to_owned(), &ipath, 2);
108+
state.upsert_file(&b_uri, b.to_owned(), &ipath, 2);
109+
state.upsert_file(&c_uri, c.to_owned(), &ipath, 2);
110110

111111
assert_yaml_snapshot!(state.reference_fields("com.workspace", "Author"));
112112
assert_yaml_snapshot!(state.reference_fields("com.workspace", "Author.Address"));
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
source: src/workspace/hover.rs
3-
expression: "state.hover(&ipath, \"com.workspace\",\nHoverables::ImportPath(\"c.proto\".to_string()))"
3+
expression: "state.hover(&ipath, \"com.workspace\",\nHoverables::Identifier(\"com.inner.Why\".to_string()))"
44
snapshot_kind: text
55
---
66
kind: markdown
7-
value: "Import: `c.proto` protobuf file,\n---\nIncluded from src/workspace/input/c.proto"
7+
value: "`Why` message or enum type, package: `com.inner`\n---\nWhy is a reason with secret"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
source: src/workspace/hover.rs
3+
expression: "state.hover(&ipath, \"com.workspace\",\nHoverables::Identifier(\"com.super.secret.SomeSecret\".to_string()))"
4+
snapshot_kind: text
5+
---
6+
kind: markdown
7+
value: "`SomeSecret` message or enum type, package: `com.super.secret`\n---\nSomeSecret is a real secret with hidden string"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
source: src/workspace/hover.rs
3+
expression: "state.hover(&ipath, \"com.workspace\",\nHoverables::Identifier(\"com.super.secret.SomeSecret\".to_string()))"
4+
snapshot_kind: text
5+
---
6+
kind: markdown
7+
value: "`SomeSecret` message or enum type, package: `com.super.secret`\n---\nSomeSecret is a real secret with hidden string"
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
---
22
source: src/workspace/rename.rs
33
expression: "state.reference_fields(\"com.workspace\", \"Author.Address\")"
4+
snapshot_kind: text
45
---
56
- uri: "file://input/a.proto"
67
range:
78
start:
8-
line: 9
9+
line: 10
910
character: 3
1011
end:
11-
line: 9
12+
line: 10
1213
character: 17
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
---
22
source: src/workspace/rename.rs
33
expression: "state.reference_fields(\"com.utility\", \"Foobar.Baz\")"
4+
snapshot_kind: text
45
---
56
- uri: "file://input/a.proto"
67
range:
78
start:
8-
line: 10
9+
line: 11
910
character: 3
1011
end:
11-
line: 10
12+
line: 11
1213
character: 25
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
---
22
source: src/workspace/rename.rs
33
expression: "state.reference_fields(\"com.workspace\", \"Author\")"
4+
snapshot_kind: text
45
---
56
- uri: "file://input/a.proto"
67
range:
78
start:
8-
line: 8
9+
line: 9
910
character: 3
1011
end:
11-
line: 8
12+
line: 9
1213
character: 9
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
---
22
source: src/workspace/rename.rs
33
expression: "state.rename_fields(\"com.workspace\", \"Author.Address\", \"Author.Location\")"
4+
snapshot_kind: text
45
---
56
"file://input/a.proto":
67
- range:
78
start:
8-
line: 9
9+
line: 10
910
character: 3
1011
end:
11-
line: 9
12+
line: 10
1213
character: 17
1314
newText: Author.Location

0 commit comments

Comments
 (0)