diff --git a/Cargo.lock b/Cargo.lock index d55d6c1..5b95e2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,6 +138,15 @@ version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bytes" version = "1.10.1" @@ -218,6 +227,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -233,6 +251,16 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "deranged" version = "0.5.3" @@ -242,6 +270,16 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -385,6 +423,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.3.3" @@ -565,6 +613,8 @@ checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0" dependencies = [ "console", "once_cell", + "pest", + "pest_derive", "serde", "similar", ] @@ -742,6 +792,49 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "pest_meta" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -957,6 +1050,17 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1329,6 +1433,18 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.19" @@ -1365,6 +1481,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "waitpid-any" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 17e29c4..a02e0fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,4 +32,4 @@ pkg-config = "0.3" clap = { version = "4.5", features = ["derive"] } [dev-dependencies] -insta = { version = "1.43", features = ["yaml"] } +insta = { version = "1.43", features = ["yaml", "redactions"] } diff --git a/src/config/workspace.rs b/src/config/workspace.rs index 87ed113..d094d1e 100644 --- a/src/config/workspace.rs +++ b/src/config/workspace.rs @@ -131,6 +131,10 @@ impl WorkspaceProtoConfigs { Some(ipath) } + pub fn get_workspaces(&self) -> Vec<&Url> { + self.workspaces.iter().collect() + } + pub fn no_workspace_mode(&mut self) { let wr = ProtolsConfig::default(); let rp = if cfg!(target_os = "windows") { diff --git a/src/lsp.rs b/src/lsp.rs index 43e3101..62bf5a9 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -14,7 +14,7 @@ use async_lsp::lsp_types::{ RenameParams, ServerCapabilities, ServerInfo, TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Url, WorkspaceEdit, WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities, - WorkspaceServerCapabilities, + WorkspaceServerCapabilities, WorkspaceSymbolParams, WorkspaceSymbolResponse, }; use async_lsp::{LanguageClient, ResponseError}; use futures::future::BoxFuture; @@ -113,6 +113,7 @@ impl ProtoLanguageServer { definition_provider: Some(OneOf::Left(true)), hover_provider: Some(HoverProviderCapability::Simple(true)), document_symbol_provider: Some(OneOf::Left(true)), + workspace_symbol_provider: Some(OneOf::Left(true)), completion_provider: Some(CompletionOptions::default()), rename_provider: Some(rename_provider), document_formatting_provider: Some(OneOf::Left(true)), @@ -379,6 +380,35 @@ impl ProtoLanguageServer { Box::pin(async move { Ok(Some(response)) }) } + pub(super) fn workspace_symbol( + &mut self, + params: WorkspaceSymbolParams, + ) -> BoxFuture<'static, Result, ResponseError>> { + let query = params.query.to_lowercase(); + let work_done_token = params.work_done_progress_params.work_done_token; + + // Parse all files from all workspaces + let workspaces = self.configs.get_workspaces(); + let progress_sender = work_done_token.map(|token| self.with_report_progress(token)); + + for workspace in workspaces { + if let Ok(workspace_path) = workspace.to_file_path() { + self.state + .parse_all_from_workspace(workspace_path, progress_sender.clone()); + } + } + + let symbols = self.state.find_workspace_symbols(&query); + + Box::pin(async move { + if symbols.is_empty() { + Ok(None) + } else { + Ok(Some(WorkspaceSymbolResponse::Nested(symbols))) + } + }) + } + pub(super) fn formatting( &mut self, params: DocumentFormattingParams, diff --git a/src/server.rs b/src/server.rs index fcb45af..11e299d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -9,6 +9,7 @@ use async_lsp::{ request::{ Completion, DocumentSymbolRequest, Formatting, GotoDefinition, HoverRequest, Initialize, PrepareRenameRequest, RangeFormatting, References, Rename, + WorkspaceSymbolRequest, }, }, router::Router, @@ -59,6 +60,7 @@ impl ProtoLanguageServer { router.request::(|st, params| st.references(params)); router.request::(|st, params| st.definition(params)); router.request::(|st, params| st.document_symbol(params)); + router.request::(|st, params| st.workspace_symbol(params)); router.request::(|st, params| st.formatting(params)); router.request::(|st, params| st.range_formatting(params)); diff --git a/src/state.rs b/src/state.rs index a687cac..9d89a31 100644 --- a/src/state.rs +++ b/src/state.rs @@ -5,8 +5,11 @@ use std::{ }; use tracing::info; -use async_lsp::lsp_types::ProgressParamsValue; -use async_lsp::lsp_types::{CompletionItem, CompletionItemKind, PublishDiagnosticsParams, Url}; +use async_lsp::lsp_types::{ + CompletionItem, CompletionItemKind, Location, OneOf, PublishDiagnosticsParams, Url, + WorkspaceSymbol, +}; +use async_lsp::lsp_types::{DocumentSymbol, ProgressParamsValue}; use std::sync::mpsc::Sender; use tree_sitter::Node; use walkdir::WalkDir; @@ -73,6 +76,78 @@ impl ProtoLanguageState { .collect() } + pub fn find_workspace_symbols(&self, query: &str) -> Vec { + let mut symbols = Vec::new(); + + for tree in self.get_trees() { + let content = self.get_content(&tree.uri); + let doc_symbols = tree.find_document_locations(content.as_bytes()); + + for doc_symbol in doc_symbols { + Self::find_workspace_symbols_impl( + &doc_symbol, + &tree.uri, + query, + None, + &mut symbols, + ); + } + } + + // Sort symbols by name and then by URI for consistent ordering + symbols.sort_by(|a, b| { + let name_cmp = a.name.cmp(&b.name); + if name_cmp != std::cmp::Ordering::Equal { + return name_cmp; + } + // Extract URI from location + match (&a.location, &b.location) { + (OneOf::Left(loc_a), OneOf::Left(loc_b)) => { + loc_a.uri.as_str().cmp(loc_b.uri.as_str()) + } + _ => std::cmp::Ordering::Equal, + } + }); + + symbols + } + + fn find_workspace_symbols_impl( + doc_symbol: &DocumentSymbol, + uri: &Url, + query: &str, + container_name: Option, + symbols: &mut Vec, + ) { + let symbol_name_lower = doc_symbol.name.to_lowercase(); + + if query.is_empty() || symbol_name_lower.contains(query) { + symbols.push(WorkspaceSymbol { + name: doc_symbol.name.clone(), + kind: doc_symbol.kind, + tags: doc_symbol.tags.clone(), + container_name: container_name.clone(), + location: OneOf::Left(Location { + uri: uri.clone(), + range: doc_symbol.range, + }), + data: None, + }); + } + + if let Some(children) = &doc_symbol.children { + for child in children { + Self::find_workspace_symbols_impl( + child, + uri, + query, + Some(doc_symbol.name.clone()), + symbols, + ); + } + } + } + fn upsert_content_impl( &mut self, uri: &Url, diff --git a/src/workspace/definition.rs b/src/workspace/definition.rs index f23131e..c541d75 100644 --- a/src/workspace/definition.rs +++ b/src/workspace/definition.rs @@ -107,13 +107,9 @@ mod test { Jumpable::Import("c.proto".to_owned()), ); - assert_eq!(loc.len(), 1); - assert!( - loc[0] - .uri - .to_file_path() - .unwrap() - .ends_with(ipath[0].join("c.proto")) - ) + assert_yaml_snapshot!(loc, {"[0].uri" => insta::dynamic_redaction(|c, _| { + assert!(c.as_str().unwrap().ends_with("c.proto")); + "file:///c.proto".to_string() + })}); } } diff --git a/src/workspace/mod.rs b/src/workspace/mod.rs index cca1e38..827348a 100644 --- a/src/workspace/mod.rs +++ b/src/workspace/mod.rs @@ -1,3 +1,4 @@ mod definition; mod hover; mod rename; +mod workspace_symbol; diff --git a/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-5.snap b/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-5.snap new file mode 100644 index 0000000..401acac --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-5.snap @@ -0,0 +1,12 @@ +--- +source: src/workspace/definition.rs +expression: loc +--- +- uri: "file:///c.proto" + range: + start: + line: 0 + character: 0 + end: + line: 0 + character: 0 diff --git a/src/workspace/snapshots/protols__workspace__workspace_symbol__test__address_symbols.snap b/src/workspace/snapshots/protols__workspace__workspace_symbol__test__address_symbols.snap new file mode 100644 index 0000000..06f3207 --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__workspace_symbol__test__address_symbols.snap @@ -0,0 +1,28 @@ +--- +source: src/workspace/workspace_symbol.rs +expression: address_symbols +--- +- name: Address + kind: 23 + containerName: Author + location: + uri: "file:///home/runner/work/protols/protols/src/workspace/input/b.proto" + range: + start: + line: 9 + character: 3 + end: + line: 11 + character: 4 +- name: Address + kind: 23 + containerName: Author + location: + uri: "file://input/b.proto" + range: + start: + line: 9 + character: 3 + end: + line: 11 + character: 4 diff --git a/src/workspace/snapshots/protols__workspace__workspace_symbol__test__all_symbols.snap b/src/workspace/snapshots/protols__workspace__workspace_symbol__test__all_symbols.snap new file mode 100644 index 0000000..f40ba5b --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__workspace_symbol__test__all_symbols.snap @@ -0,0 +1,61 @@ +--- +source: src/workspace/workspace_symbol.rs +expression: all_symbols +--- +- name: Address + kind: 23 + containerName: Author + location: + uri: "file://input/b.proto" + range: + start: + line: 9 + character: 3 + end: + line: 11 + character: 4 +- name: Author + kind: 23 + location: + uri: "file://input/b.proto" + range: + start: + line: 5 + character: 0 + end: + line: 14 + character: 1 +- name: Baz + kind: 23 + containerName: Foobar + location: + uri: "file://input/c.proto" + range: + start: + line: 8 + character: 3 + end: + line: 10 + character: 4 +- name: Book + kind: 23 + location: + uri: "file://input/a.proto" + range: + start: + line: 9 + character: 0 + end: + line: 14 + character: 1 +- name: Foobar + kind: 23 + location: + uri: "file://input/c.proto" + range: + start: + line: 5 + character: 0 + end: + line: 13 + character: 1 diff --git a/src/workspace/snapshots/protols__workspace__workspace_symbol__test__author_symbols.snap b/src/workspace/snapshots/protols__workspace__workspace_symbol__test__author_symbols.snap new file mode 100644 index 0000000..229fdd0 --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__workspace_symbol__test__author_symbols.snap @@ -0,0 +1,26 @@ +--- +source: src/workspace/workspace_symbol.rs +expression: author_symbols +--- +- name: Author + kind: 23 + location: + uri: "file:///home/runner/work/protols/protols/src/workspace/input/b.proto" + range: + start: + line: 5 + character: 0 + end: + line: 14 + character: 1 +- name: Author + kind: 23 + location: + uri: "file://input/b.proto" + range: + start: + line: 5 + character: 0 + end: + line: 14 + character: 1 diff --git a/src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols-2.snap b/src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols-2.snap new file mode 100644 index 0000000..807e053 --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols-2.snap @@ -0,0 +1,15 @@ +--- +source: src/workspace/workspace_symbol.rs +expression: author_symbols +--- +- name: Author + kind: 23 + location: + uri: "file:///src/workspace/input/b.proto" + range: + start: + line: 5 + character: 0 + end: + line: 14 + character: 1 diff --git a/src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols-3.snap b/src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols-3.snap new file mode 100644 index 0000000..629e820 --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols-3.snap @@ -0,0 +1,16 @@ +--- +source: src/workspace/workspace_symbol.rs +expression: address_symbols +--- +- name: Address + kind: 23 + containerName: Author + location: + uri: "file:///src/workspace/input/b.proto" + range: + start: + line: 9 + character: 3 + end: + line: 11 + character: 4 diff --git a/src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols.snap b/src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols.snap new file mode 100644 index 0000000..0fb8224 --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols.snap @@ -0,0 +1,83 @@ +--- +source: src/workspace/workspace_symbol.rs +expression: all_symbols +--- +- name: Address + kind: 23 + containerName: Author + location: + uri: "file:///src/workspace/input/b.proto" + range: + start: + line: 9 + character: 3 + end: + line: 11 + character: 4 +- name: Author + kind: 23 + location: + uri: "file:///src/workspace/input/b.proto" + range: + start: + line: 5 + character: 0 + end: + line: 14 + character: 1 +- name: Baz + kind: 23 + containerName: Foobar + location: + uri: "file:///src/workspace/input/c.proto" + range: + start: + line: 8 + character: 3 + end: + line: 10 + character: 4 +- name: Book + kind: 23 + location: + uri: "file:///src/workspace/input/a.proto" + range: + start: + line: 9 + character: 0 + end: + line: 14 + character: 1 +- name: Foobar + kind: 23 + location: + uri: "file:///src/workspace/input/c.proto" + range: + start: + line: 5 + character: 0 + end: + line: 13 + character: 1 +- name: SomeSecret + kind: 23 + location: + uri: "file:///src/workspace/input/y.proto" + range: + start: + line: 5 + character: 0 + end: + line: 7 + character: 1 +- name: Why + kind: 23 + location: + uri: "file:///src/workspace/input/x.proto" + range: + start: + line: 7 + character: 0 + end: + line: 11 + character: 1 diff --git a/src/workspace/workspace_symbol.rs b/src/workspace/workspace_symbol.rs new file mode 100644 index 0000000..5a57b15 --- /dev/null +++ b/src/workspace/workspace_symbol.rs @@ -0,0 +1,90 @@ +#[cfg(test)] +mod test { + use insta::assert_yaml_snapshot; + use insta::internals::{Content, ContentPath}; + + use crate::config::Config; + use crate::state::ProtoLanguageState; + + #[test] + fn test_workspace_symbols() { + let current_dir = std::env::current_dir().unwrap(); + let ipath = vec![current_dir.join("src/workspace/input")]; + let a_uri = (&format!( + "file://{}/src/workspace/input/a.proto", + current_dir.to_str().unwrap() + )) + .parse() + .unwrap(); + let b_uri = (&format!( + "file://{}/src/workspace/input/b.proto", + current_dir.to_str().unwrap() + )) + .parse() + .unwrap(); + let c_uri = (&format!( + "file://{}/src/workspace/input/c.proto", + current_dir.to_str().unwrap() + )) + .parse() + .unwrap(); + + let a = include_str!("input/a.proto"); + let b = include_str!("input/b.proto"); + let c = include_str!("input/c.proto"); + + let mut state: ProtoLanguageState = ProtoLanguageState::new(); + state.upsert_file(&a_uri, a.to_owned(), &ipath, 3, &Config::default(), false); + state.upsert_file(&b_uri, b.to_owned(), &ipath, 2, &Config::default(), false); + state.upsert_file(&c_uri, c.to_owned(), &ipath, 2, &Config::default(), false); + + // Test empty query - should return all symbols + let all_symbols = state.find_workspace_symbols(""); + let cdir = current_dir.to_str().unwrap().to_string(); + assert_yaml_snapshot!(all_symbols, { "[].location.uri" => insta::dynamic_redaction(move |c, _| { + assert!( + c.as_str() + .unwrap() + .contains(&cdir) + ); + format!( + "file:///src/workspace/input/{}", + c.as_str().unwrap().split('/').last().unwrap() + ) + + })}); + + // Test query for "author" - should match Author and Address + let author_symbols = state.find_workspace_symbols("author"); + let cdir = current_dir.to_str().unwrap().to_string(); + assert_yaml_snapshot!(author_symbols, {"[].location.uri" => insta::dynamic_redaction(move |c ,_|{ + assert!( + c.as_str() + .unwrap() + .contains(&cdir) + ); + format!( + "file:///src/workspace/input/{}", + c.as_str().unwrap().split('/').last().unwrap() + ) + })}); + + // Test query for "address" - should match Address + let address_symbols = state.find_workspace_symbols("address"); + assert_yaml_snapshot!(address_symbols, {"[].location.uri" => insta::dynamic_redaction(move |c ,_|{ + assert!( + c.as_str() + .unwrap() + .contains(¤t_dir.to_str().unwrap()) + ); + format!( + "file:///src/workspace/input/{}", + c.as_str().unwrap().split('/').last().unwrap() + ) + })}); + + // Test query that should not match anything + let no_match = state.find_workspace_symbols("nonexistent"); + assert!(no_match.is_empty()); + } +}