From ce107221101c1cb915d6e7f742e0bf143d3575ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Oct 2025 14:13:40 +0000 Subject: [PATCH 1/3] Initial plan From ad7ba221ed7d18e7de1a62d4c4dec68a4e356666 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Oct 2025 14:35:13 +0000 Subject: [PATCH 2/3] Add workspace symbols support to protols LSP server - Add WorkspaceSymbolRequest handler registration in server.rs - Implement workspace_symbol method in lsp.rs to handle workspace symbol requests - Add workspace_symbol_provider capability in initialize response - Create find_workspace_symbols method in state.rs to collect and filter symbols from all parsed trees - Add comprehensive test for workspace symbols functionality with snapshots - Sort symbols by name and URI for consistent ordering Co-authored-by: coder3101 <22212259+coder3101@users.noreply.github.com> --- src/lsp.rs | 20 ++- src/server.rs | 2 + src/state.rs | 72 +++++++++- src/workspace/mod.rs | 1 + ...rkspace_symbol__test__address_symbols.snap | 28 ++++ ...__workspace_symbol__test__all_symbols.snap | 129 ++++++++++++++++++ ...orkspace_symbol__test__author_symbols.snap | 26 ++++ src/workspace/workspace_symbol.rs | 40 ++++++ 8 files changed, 316 insertions(+), 2 deletions(-) create mode 100644 src/workspace/snapshots/protols__workspace__workspace_symbol__test__address_symbols.snap create mode 100644 src/workspace/snapshots/protols__workspace__workspace_symbol__test__all_symbols.snap create mode 100644 src/workspace/snapshots/protols__workspace__workspace_symbol__test__author_symbols.snap create mode 100644 src/workspace/workspace_symbol.rs diff --git a/src/lsp.rs b/src/lsp.rs index 43e3101..37a5f6b 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,23 @@ 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 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..9c9b849 100644 --- a/src/state.rs +++ b/src/state.rs @@ -6,7 +6,10 @@ 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 std::sync::mpsc::Sender; use tree_sitter::Node; use walkdir::WalkDir; @@ -73,6 +76,73 @@ 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.collect_workspace_symbols(&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 collect_workspace_symbols( + &self, + doc_symbol: &async_lsp::lsp_types::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.collect_workspace_symbols( + child, + uri, + query, + Some(doc_symbol.name.clone()), + symbols, + ); + } + } + } + fn upsert_content_impl( &mut self, uri: &Url, 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__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..956d29d --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__workspace_symbol__test__all_symbols.snap @@ -0,0 +1,129 @@ +--- +source: src/workspace/workspace_symbol.rs +expression: all_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 +- 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 +- name: Baz + kind: 23 + containerName: Foobar + location: + uri: "file:///home/runner/work/protols/protols/src/workspace/input/c.proto" + range: + start: + line: 8 + character: 3 + end: + line: 10 + character: 4 +- 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:///home/runner/work/protols/protols/src/workspace/input/c.proto" + range: + start: + line: 5 + character: 0 + end: + line: 13 + character: 1 +- name: Foobar + kind: 23 + location: + uri: "file://input/c.proto" + range: + start: + line: 5 + character: 0 + end: + line: 13 + character: 1 +- name: SomeSecret + kind: 23 + location: + uri: "file:///home/runner/work/protols/protols/src/workspace/input/inner/secret/y.proto" + range: + start: + line: 5 + character: 0 + end: + line: 7 + character: 1 +- name: Why + kind: 23 + location: + uri: "file:///home/runner/work/protols/protols/src/workspace/input/inner/x.proto" + range: + start: + line: 7 + character: 0 + end: + line: 11 + 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/workspace_symbol.rs b/src/workspace/workspace_symbol.rs new file mode 100644 index 0000000..1cc967c --- /dev/null +++ b/src/workspace/workspace_symbol.rs @@ -0,0 +1,40 @@ +#[cfg(test)] +mod test { + use insta::assert_yaml_snapshot; + + use crate::config::Config; + use crate::state::ProtoLanguageState; + + #[test] + fn test_workspace_symbols() { + let ipath = vec![std::env::current_dir().unwrap().join("src/workspace/input")]; + let a_uri = "file://input/a.proto".parse().unwrap(); + let b_uri = "file://input/b.proto".parse().unwrap(); + let c_uri = "file://input/c.proto".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(""); + assert_yaml_snapshot!("all_symbols", all_symbols); + + // Test query for "author" - should match Author and Address + let author_symbols = state.find_workspace_symbols("author"); + assert_yaml_snapshot!("author_symbols", author_symbols); + + // Test query for "address" - should match Address + let address_symbols = state.find_workspace_symbols("address"); + assert_yaml_snapshot!("address_symbols", address_symbols); + + // Test query that should not match anything + let no_match = state.find_workspace_symbols("nonexistent"); + assert!(no_match.is_empty()); + } +} From 9d63252e0dfb1d0c7d2da6df91ba8c8fee457985 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Oct 2025 05:40:51 +0000 Subject: [PATCH 3/3] Add full workspace parsing to workspace_symbol request - Add get_workspaces() method to WorkspaceProtoConfigs to retrieve all workspace folders - Update workspace_symbol handler to parse all files from all workspaces before collecting symbols - Add progress reporting support for workspace symbol requests - Now workspace symbols work across entire workspace, not just opened files Co-authored-by: coder3101 <22212259+coder3101@users.noreply.github.com> --- src/config/workspace.rs | 4 ++++ src/lsp.rs | 12 ++++++++++++ 2 files changed, 16 insertions(+) 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 37a5f6b..62bf5a9 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -385,6 +385,18 @@ impl ProtoLanguageServer { 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);