M Cargo.lock => Cargo.lock +51 -0
@@ 199,6 199,7 @@ dependencies = [
"toml",
"tree-sitter",
"tree-sitter-protobuf",
+ "walkdir",
]
[[package]]
@@ 293,6 294,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
name = "serde"
version = "1.0.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 469,6 479,47 @@ dependencies = [
]
[[package]]
+name = "walkdir"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
M Cargo.toml => Cargo.toml +1 -0
@@ 13,6 13,7 @@ tree-sitter-protobuf = { git = "https://github.com/yusdacra/tree-sitter-protobuf
tree-sitter = "~0.20.0"
log = "0.4.20"
env_logger = { version = "0.10.1", default-features = false }
+walkdir = "2.4.0"
[dev-dependencies]
pretty_assertions = "1.4.0"
M src/file.rs => src/file.rs +1 -1
@@ 259,7 259,7 @@ impl File {
);
log::trace!(
- "Getting completion context for node text:\n{}",
+ "Getting completion context for node text: {}",
self.get_text(node),
);
M src/lib.rs => src/lib.rs +8 -1
@@ 264,7 264,7 @@ pub fn run(connection: Connection) -> Result<()> {
} else {
log::info!("Using default config");
Config {
- proto_paths: find_import_paths(root)?,
+ proto_paths: find_import_paths(root.clone())?,
}
};
log::info!("Using config {:?}", conf);
@@ 272,6 272,13 @@ pub fn run(connection: Connection) -> Result<()> {
let proto_paths = conf
.proto_paths
.iter()
+ .map(|path| {
+ if path.is_relative() {
+ root.join(path)
+ } else {
+ path.clone()
+ }
+ })
.filter_map(|p| match p.canonicalize() {
Ok(path) => Some(path),
Err(err) => {
M src/workspace.rs => src/workspace.rs +128 -36
@@ 197,31 197,7 @@ impl Workspace {
}
}
- // Return all available imports for a given file.
- // Excludes the file itself and any files already imported.
- pub fn available_imports<'a>(
- &'a self,
- uri: &'a Url,
- ) -> Result<impl Iterator<Item = String> + 'a> {
- let name = std::path::Path::new(uri.path())
- .file_name()
- .ok_or("Invalid path: {uri}")?;
- let file = self.files.get(uri).ok_or("File not loaded: {uri}")?;
- let mut qc = tree_sitter::QueryCursor::new();
- let imports = file.imports(&mut qc).collect::<Vec<_>>();
- Ok(self
- .proto_paths
- .iter()
- .filter_map(|dir| std::fs::read_dir(dir).ok())
- .flatten()
- .filter_map(|entry| entry.ok())
- .filter(|entry| entry.metadata().is_ok_and(|m| m.is_file()))
- .map(|entry| entry.file_name())
- .filter(move |fname| fname != name)
- .filter_map(|fname| fname.into_string().ok())
- .filter(|fname| fname.ends_with(".proto"))
- .filter(move |fname| !imports.contains(&fname.as_str())))
- }
+ // Return the relative paths of proto files under the given dir.
pub fn goto(&self, uri: Url, pos: lsp_types::Position) -> Result<Option<lsp_types::Location>> {
let file = self.get(&uri)?;
@@ 413,17 389,102 @@ impl Workspace {
&self,
url: &lsp_types::Url,
) -> Result<Option<lsp_types::CompletionResponse>> {
+ log::debug!("Completing imports for {url:?}");
+
+ let current = std::path::Path::new(url.path())
+ .file_name()
+ .ok_or("Invalid path: {uri}")?
+ .to_str()
+ .ok_or("Invalid path: {uri}")?;
+
+ let file = self.files.get(url).ok_or("File not loaded: {uri}")?;
+ let mut qc = tree_sitter::QueryCursor::new();
+ let existing = file
+ .imports(&mut qc)
+ .chain(std::iter::once(current))
+ .collect::<Vec<_>>();
+
+ log::trace!("Excluding existing imports: {existing:?}");
+
let items = self
- .available_imports(&url)?
- .map(|s| lsp_types::CompletionItem {
- label: s.clone(),
- label_details: None,
- kind: Some(lsp_types::CompletionItemKind::FILE),
- insert_text: Some(format!("{}\";", s)),
- ..Default::default()
- });
- Ok(Some(lsp_types::CompletionResponse::Array(items.collect())))
+ .proto_paths
+ .iter()
+ .map(|p| find_protos(p.as_path(), &existing))
+ .flat_map(|p| {
+ p.iter()
+ .map(|s| lsp_types::CompletionItem {
+ insert_text: Some(format!("{}\";", s)),
+ label: s.to_owned(),
+ label_details: None,
+ kind: Some(lsp_types::CompletionItemKind::FILE),
+ ..Default::default()
+ })
+ .collect::<Vec<_>>()
+ })
+ .collect();
+ Ok(Some(lsp_types::CompletionResponse::Array(items)))
+ }
+}
+
+fn find_protos(dir: &std::path::Path, excludes: &Vec<&str>) -> Vec<String> {
+ let mut res = vec![];
+ let entries = match std::fs::read_dir(dir) {
+ Ok(ok) => ok,
+ Err(err) => {
+ log::warn!("Failed to read dir {dir:?}: {err:?}");
+ return res;
+ }
+ };
+ log::trace!("Finding imports under {dir:?}");
+ for path in entries {
+ let path = match path {
+ Ok(ok) => ok,
+ Err(err) => {
+ log::warn!("Failed to read dir {dir:?}: {err:?}");
+ continue;
+ }
+ };
+
+ let meta = match path.metadata() {
+ Ok(ok) => ok,
+ Err(err) => {
+ log::warn!("Failed to read dir {dir:?}: {err:?}");
+ continue;
+ }
+ };
+
+ if meta.is_dir() {
+ let dir = dir.join(path.path());
+ let protos = find_protos(dir.as_path(), excludes);
+ let root = &path.file_name();
+ let root = std::path::PathBuf::from(root);
+ res.extend(
+ protos
+ .iter()
+ .filter_map(|p| root.join(p).to_str().map(str::to_string)),
+ );
+ continue;
+ }
+
+ if !meta.is_file() {
+ continue;
+ }
+
+ let name = &path.file_name();
+ let Some(name) = name.to_str() else {
+ continue;
+ };
+
+ if !name.ends_with(".proto") {
+ continue;
+ }
+
+ if !excludes.contains(&name) {
+ log::trace!("Found import {name:?}");
+ res.push(name.to_string())
+ }
}
+ res
}
fn complete_keywords() -> Option<lsp_types::CompletionResponse> {
@@ 495,8 556,8 @@ mod tests {
(Workspace::new(vec![tmp.path().into()]), tmp)
}
- fn proto(tmp: &tempfile::TempDir, path: &str, lines: &[&str]) -> (Url, String) {
- let path = tmp.path().join(path);
+ fn proto(dir: impl AsRef<std::path::Path>, path: &str, lines: &[&str]) -> (Url, String) {
+ let path = dir.as_ref().join(path);
let text = lines.join("\n") + "\n";
std::fs::write(&path, &text).unwrap();
(Url::from_file_path(path).unwrap(), text)
@@ 568,6 629,37 @@ mod tests {
}
#[test]
+ fn test_complete_nested_import() {
+ let (mut ws, tmp) = setup();
+ let (uri, text) = proto(&tmp, "foo.proto", &["syntax = \"proto3\";", "import \""]);
+ proto(&tmp, "bar.proto", &["syntax = \"proto3\";"]);
+
+ let subdir = tmp.path().join("subdir");
+ let subdir = subdir.as_path();
+ std::fs::create_dir(subdir).unwrap();
+ proto(subdir, "baz.proto", &["syntax = \"proto3\";"]);
+
+ ws.open(uri.clone(), text).unwrap();
+ assert_eq!(
+ ws.complete(&uri, 1, "import \"".len()).unwrap().unwrap(),
+ lsp_types::CompletionResponse::Array(vec![
+ lsp_types::CompletionItem {
+ label: "bar.proto".into(),
+ kind: Some(lsp_types::CompletionItemKind::FILE),
+ insert_text: Some("bar.proto\";".into()),
+ ..Default::default()
+ },
+ lsp_types::CompletionItem {
+ label: "subdir/baz.proto".into(),
+ kind: Some(lsp_types::CompletionItemKind::FILE),
+ insert_text: Some("subdir/baz.proto\";".into()),
+ ..Default::default()
+ },
+ ])
+ );
+ }
+
+ #[test]
fn test_complete_options() {
let (mut ws, tmp) = setup();
let (uri, text) = proto(
A testdata/folder/stuff.proto => testdata/folder/stuff.proto +5 -0
@@ 0,0 1,5 @@
+syntax = "proto3";
+
+package folder.stuff;
+
+message Stuff{}
M testdata/simple.proto => testdata/simple.proto +2 -0
@@ 4,6 4,7 @@ package main;
import "dep.proto";
import "other.proto";
+import "folder/stuff.proto";
enum Thing {
THING_FOO = 0;
@@ 25,6 26,7 @@ message Bar {
other.Other other = 2;
Foo.Buz buz = 3;
other.Other.Nested other_nested = 4;
+ folder.stuff.Stuff stuff = 5;
}
message Empty {}
M tests/integration_test.rs => tests/integration_test.rs +23 -7
@@ 32,6 32,10 @@ fn dep_uri() -> Url {
Url::from_file_path(std::fs::canonicalize("./testdata/dep.proto").unwrap()).unwrap()
}
+fn stuff_uri() -> Url {
+ Url::from_file_path(std::fs::canonicalize("./testdata/folder/stuff.proto").unwrap()).unwrap()
+}
+
fn error_uri() -> Url {
Url::from_file_path(std::fs::canonicalize("./testdata/error.proto").unwrap()).unwrap()
}
@@ 210,6 214,7 @@ struct TestClient {
impl TestClient {
fn new() -> Result<TestClient> {
+ let _ = env_logger::builder().is_test(true).try_init();
Self::new_with_root("testdata")
}
@@ 507,6 512,7 @@ fn test_workspace_symbols() -> pbls::Result<()> {
sym(error_uri(), "Nope", "enum Nope"),
sym(error_uri(), "Nah", "message Nah"),
sym(error_uri(), "Noo", "message Noo"),
+ // sym(stuff_uri(), "Stuff", "message Stuff"), BUG: should find nested symbols
];
assert_elements_equal(actual, expected, |s| s.name.clone());
Ok(())
@@ 643,7 649,7 @@ fn test_complete_import() -> pbls::Result<()> {
let mut client = TestClient::new()?;
client.open(base_uri())?;
- let loc = locate_sym(base_uri(), "import \"other.proto\"");
+ let loc = locate_sym(base_uri(), "import \"folder");
let pos = lsp_types::Position {
line: loc.range.start.line + 1,
character: 0,
@@ 679,12 685,21 @@ fn test_complete_import() -> pbls::Result<()> {
// excludes dep.proto (already imported)
assert_elements_equal(
actual,
- vec![CompletionItem {
- label: "error.proto".into(),
- kind: Some(CompletionItemKind::FILE),
- insert_text: Some("error.proto\";".into()),
- ..Default::default()
- }],
+ vec![
+ CompletionItem {
+ label: "error.proto".into(),
+ kind: Some(CompletionItemKind::FILE),
+ insert_text: Some("error.proto\";".into()),
+ ..Default::default()
+ },
+ // BUG: Should be excluded
+ CompletionItem {
+ label: "folder/stuff.proto".into(),
+ kind: Some(CompletionItemKind::FILE),
+ insert_text: Some("folder/stuff.proto\";".into()),
+ ..Default::default()
+ },
+ ],
|s| s.label.clone(),
);
@@ 821,6 836,7 @@ fn test_complete_type() -> pbls::Result<()> {
_enum("Dep2"),
_struct("other.Other"),
_struct("other.Other.Nested"),
+ _struct("folder.Stuff"), // BUG: should be folder.stuff.Stuff
],
|s| s.label.clone(),
);