diff --git a/docs/debugging.md b/docs/debugging.md index d67c083..79de818 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -33,7 +33,7 @@ The Neovim instance in the Dev Container is pre-configured to detect the ### 3. Verify Connection To ensure the debugger is working and the server is responding: -1. Set a breakpoint in the Rust code (e.g., in `src/parser/docsymbol.rs`). +1. Set a breakpoint in the Rust code (e.g., in `src/document/docsymbol.rs`). 2. In Neovim, trigger an LSP request, such as fetching document symbols: ```vim :lua vim.lsp.buf.document_symbol() diff --git a/src/config/mod.rs b/src/config/mod.rs index bf3a319..7190a33 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,14 +1,8 @@ -use serde::{Deserialize, Serialize}; - -pub mod workspace; +mod workspace; -fn default_clang_format_path() -> String { - "clang-format".to_string() -} +pub use workspace::WorkspaceProtoConfigs; -fn default_protoc_path() -> String { - "protoc".to_string() -} +use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Clone, Default)] #[serde(default)] @@ -50,3 +44,11 @@ pub struct RenameConfig { /// convention. pub chain_rpc_request_response: bool, } + +fn default_clang_format_path() -> String { + "clang-format".to_string() +} + +fn default_protoc_path() -> String { + "protoc".to_string() +} diff --git a/src/config/workspace.rs b/src/config/workspace.rs index 5eb8f28..ed0d12e 100644 --- a/src/config/workspace.rs +++ b/src/config/workspace.rs @@ -7,9 +7,9 @@ use std::{ use async_lsp::lsp_types::{Url, WorkspaceFolder}; use pkg_config::Config; -use crate::formatter::clang::ClangFormatter; +use crate::formatter::ClangFormatter; -use super::ProtolsConfig; +use crate::config::ProtolsConfig; const CONFIG_FILE_NAMES: [&str; 2] = [".protols.toml", "protols.toml"]; diff --git a/src/context/jumpable.rs b/src/context/jumpable.rs deleted file mode 100644 index f38b5b4..0000000 --- a/src/context/jumpable.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub enum Jumpable { - Import(String), - Identifier(String), -} diff --git a/src/context/mod.rs b/src/context/mod.rs deleted file mode 100644 index eed0484..0000000 --- a/src/context/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod jumpable; diff --git a/src/docs.rs b/src/docs/mod.rs similarity index 94% rename from src/docs.rs rename to src/docs/mod.rs index 2bffeac..e7944cb 100644 --- a/src/docs.rs +++ b/src/docs/mod.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::LazyLock}; macro_rules! docmap_builtin { ($name:literal) => { - ($name, include_str!(concat!("docs/builtin/", $name, ".md"))) + ($name, include_str!(concat!("builtin/", $name, ".md"))) }; } @@ -10,7 +10,7 @@ macro_rules! docmap_wellknown { ($name:literal) => { ( concat!("google.protobuf.", $name), - include_str!(concat!("docs/wellknown/", $name, ".md")), + include_str!(concat!("wellknown/", $name, ".md")), ) }; } diff --git a/src/document/diagnostics.rs b/src/document/diagnostics.rs new file mode 100644 index 0000000..818356a --- /dev/null +++ b/src/document/diagnostics.rs @@ -0,0 +1,18 @@ +use async_lsp::lsp_types::{Diagnostic, DiagnosticSeverity}; + +use super::parser::ProtoDocument; + +impl ProtoDocument { + pub fn collect_import_diagnostics(&self, import: &[&str]) -> Vec { + self.import_path_ranges(import) + .into_iter() + .map(|r| Diagnostic { + range: r, + severity: Some(DiagnosticSeverity::ERROR), + source: Some(String::from("protols")), + message: "failed to find proto file".to_string(), + ..Default::default() + }) + .collect() + } +} diff --git a/src/parser/docsymbol.rs b/src/document/docsymbol.rs similarity index 93% rename from src/parser/docsymbol.rs rename to src/document/docsymbol.rs index 373e202..71a56a6 100644 --- a/src/parser/docsymbol.rs +++ b/src/document/docsymbol.rs @@ -1,18 +1,18 @@ //! Document Symbol hierarchy compilation layer for protobuf abstract syntax -//! trees. +//! documents. //! //! This module translates a flat, vector-backed metamodel registry into a fully -//! nested, tree-structured representation matching the LSP [`DocumentSymbol`] +//! nested, document-structured representation matching the LSP [`DocumentSymbol`] //! specification. use async_lsp::lsp_types::{DocumentSymbol, Range, SymbolKind, SymbolTag}; use crate::model::{ElementKind, ElementMeta, ModelElement}; -use super::ParsedTree; +use super::parser::ProtoDocument; -impl ParsedTree { - /// Compiles a fully resolved hierarchical tree of document symbols from the +impl ProtoDocument { + /// Compiles a fully resolved hierarchical document of document symbols from the /// internal flat elements registry. /// /// # Returns @@ -172,8 +172,8 @@ mod test { state.upsert_file(&uri, contents, &ipath, 3, &Config::default(), false); state - .get_tree(&uri) - .map(|tree| tree.document_symbols()) + .get_document(&uri) + .map(|document| document.document_symbols()) .unwrap_or_default() } @@ -214,8 +214,8 @@ mod test { state.upsert_file(&uri, "", &ipath, 3, &Config::default(), false); let symbols = state - .get_tree(&uri) - .map(|tree| tree.document_symbols()) + .get_document(&uri) + .map(|document| document.document_symbols()) .unwrap_or_default(); assert!(symbols.is_empty()); @@ -231,8 +231,8 @@ mod test { ); let symbols_minimal = state_minimal - .get_tree(&uri) - .map(|tree| tree.document_symbols()) + .get_document(&uri) + .map(|document| document.document_symbols()) .unwrap_or_default(); assert!(symbols_minimal.is_empty()); diff --git a/src/parser/hover.rs b/src/document/hover.rs similarity index 96% rename from src/parser/hover.rs rename to src/document/hover.rs index ed36337..b98a379 100644 --- a/src/parser/hover.rs +++ b/src/document/hover.rs @@ -1,5 +1,5 @@ //! Spatial coordinate resolution and hover query layer for protobuf parsed -//! trees. +//! documents. //! //! This module implements high-performance geometric intersection algorithms //! that map a precise cursor point (line and character) to memory-cached @@ -9,9 +9,9 @@ use async_lsp::lsp_types::Position; use crate::model::SpatialEntry; -use super::ParsedTree; +use super::parser::ProtoDocument; -impl ParsedTree { +impl ProtoDocument { /// Performs a search to locate the innermost spatial index block /// intersecting with the specified LSP [`Position`]. /// @@ -62,10 +62,10 @@ mod test { let mut hover_results = Vec::new(); - if let Some(parsed_tree) = state.get_tree(&uri) { + if let Some(parsed_document) = state.get_document(&uri) { let mut tested_positions = std::collections::HashSet::new(); - for element in &parsed_tree.elements { + for element in &parsed_document.elements { let mut targets = vec![(element.meta.selection_range.start, "definition")]; match &element.kind { diff --git a/src/parser/input/syntax_variants/test_editions.proto b/src/document/input/syntax_variants/test_editions.proto similarity index 100% rename from src/parser/input/syntax_variants/test_editions.proto rename to src/document/input/syntax_variants/test_editions.proto diff --git a/src/parser/input/syntax_variants/test_package_duplicate.proto b/src/document/input/syntax_variants/test_package_duplicate.proto similarity index 100% rename from src/parser/input/syntax_variants/test_package_duplicate.proto rename to src/document/input/syntax_variants/test_package_duplicate.proto diff --git a/src/parser/input/syntax_variants/test_proto2.proto b/src/document/input/syntax_variants/test_proto2.proto similarity index 100% rename from src/parser/input/syntax_variants/test_proto2.proto rename to src/document/input/syntax_variants/test_proto2.proto diff --git a/src/parser/input/syntax_variants/test_proto3.proto b/src/document/input/syntax_variants/test_proto3.proto similarity index 100% rename from src/parser/input/syntax_variants/test_proto3.proto rename to src/document/input/syntax_variants/test_proto3.proto diff --git a/src/parser/input/test_can_rename.proto b/src/document/input/test_can_rename.proto similarity index 100% rename from src/parser/input/test_can_rename.proto rename to src/document/input/test_can_rename.proto diff --git a/src/parser/input/test_collect_parse_error1.proto b/src/document/input/test_collect_parse_error1.proto similarity index 100% rename from src/parser/input/test_collect_parse_error1.proto rename to src/document/input/test_collect_parse_error1.proto diff --git a/src/parser/input/test_collect_parse_error2.proto b/src/document/input/test_collect_parse_error2.proto similarity index 100% rename from src/parser/input/test_collect_parse_error2.proto rename to src/document/input/test_collect_parse_error2.proto diff --git a/src/parser/input/test_filter.proto b/src/document/input/test_filter.proto similarity index 100% rename from src/parser/input/test_filter.proto rename to src/document/input/test_filter.proto diff --git a/src/parser/input/test_goto_definition.proto b/src/document/input/test_goto_definition.proto similarity index 100% rename from src/parser/input/test_goto_definition.proto rename to src/document/input/test_goto_definition.proto diff --git a/src/parser/input/test_reference.proto b/src/document/input/test_reference.proto similarity index 100% rename from src/parser/input/test_reference.proto rename to src/document/input/test_reference.proto diff --git a/src/parser/input/test_rename.proto b/src/document/input/test_rename.proto similarity index 100% rename from src/parser/input/test_rename.proto rename to src/document/input/test_rename.proto diff --git a/src/parser/input/test_rename_field.proto b/src/document/input/test_rename_field.proto similarity index 100% rename from src/parser/input/test_rename_field.proto rename to src/document/input/test_rename_field.proto diff --git a/src/parser/input/test_rename_service.proto b/src/document/input/test_rename_service.proto similarity index 100% rename from src/parser/input/test_rename_service.proto rename to src/document/input/test_rename_service.proto diff --git a/src/document/mod.rs b/src/document/mod.rs new file mode 100644 index 0000000..50e5713 --- /dev/null +++ b/src/document/mod.rs @@ -0,0 +1,9 @@ +mod parser; + +mod diagnostics; +mod docsymbol; +mod hover; +mod rename; +mod syntax; + +pub use parser::{ProtoDocument, ProtoParser}; diff --git a/src/parser.rs b/src/document/parser.rs similarity index 63% rename from src/parser.rs rename to src/document/parser.rs index f16a277..4ebd625 100644 --- a/src/parser.rs +++ b/src/document/parser.rs @@ -1,31 +1,23 @@ use std::sync::Arc; -use async_lsp::lsp_types::Url; +use async_lsp::lsp_types::{Range, Url}; use tree_sitter::{Parser, Query, Tree}; -use crate::model::{ModelElement, SpatialEntry, build_meta_model}; - -mod definition; -mod diagnostics; -mod docsymbol; -mod hover; -mod rename; -mod tree; - +use crate::model::{ElementKind, ModelElement, SpatialEntry, build_meta_model}; pub struct ProtoParser { parser: tree_sitter::Parser, } #[derive(Clone)] -pub struct ParsedTree { +pub struct ProtoDocument { pub uri: Url, pub package: String, pub elements: Vec, pub spatial_index: Vec, - tree: Arc, + pub tree: Arc, } -impl ParsedTree { +impl ProtoDocument { /// Attempts to parse a raw protobuf document and compile its optimized /// pure-memory metamodel and sorted spatial index. /// @@ -71,6 +63,39 @@ impl ParsedTree { tree: Arc::new(tree), }) } + + /// Returns the package namespace, defaulting to `"."` when undeclared. + pub fn package_name(&self) -> &str { + if self.package.is_empty() { + "." + } else { + &self.package + } + } + + /// Returns the paths of all `import` statements declared in source. + pub fn import_paths(&self) -> Vec { + self.elements + .iter() + .filter_map(|element| match &element.kind { + ElementKind::Import { path } => Some(path.clone()), + _ => None, + }) + .collect() + } + + /// Returns the ranges of import statements whose path is in `import`. + pub fn import_path_ranges(&self, import: &[&str]) -> Vec { + self.elements + .iter() + .filter_map(|element| match &element.kind { + ElementKind::Import { path } if import.contains(&path.as_str()) => { + Some(element.meta.selection_range) + } + _ => None, + }) + .collect() + } } impl ProtoParser { @@ -95,7 +120,7 @@ impl ProtoParser { uri: Url, contents: impl AsRef<[u8]>, metamodel_query: &Query, - ) -> Option { - ParsedTree::try_from_input(uri, contents.as_ref(), metamodel_query, &mut self.parser) + ) -> Option { + ProtoDocument::try_from_input(uri, contents.as_ref(), metamodel_query, &mut self.parser) } } diff --git a/src/document/rename.rs b/src/document/rename.rs new file mode 100644 index 0000000..9c843ad --- /dev/null +++ b/src/document/rename.rs @@ -0,0 +1,280 @@ +use async_lsp::lsp_types::{Position, Range}; + +use crate::{ + model::{ElementKind, SpatialEntry, TypeReference}, + utils::is_position_inside_range, +}; + +use super::parser::ProtoDocument; + +impl ProtoDocument { + pub fn can_rename(&self, pos: Position) -> Option { + let SpatialEntry { element_id, .. } = self.find_entry_at_position(pos)?; + let element = self.elements.get(*element_id)?; + if matches!(element.kind, ElementKind::Import { .. }) { + return None; + } + if is_position_inside_range(pos, element.meta.selection_range) { + return Some(element.meta.selection_range); + } + // Cursor rests on a type reference; return the precise segment range. + let type_ref = element.type_reference_at(pos)?; + type_ref_segment_range(type_ref, pos) + } + + /// If the given position is on the rpc name of an rpc declaration, returns + /// the rpc's name along with its declared request and response type texts. + /// Used to drive the rpc/request/response chained rename. + pub fn rpc_at_position( + &self, + pos: Position, + _content: impl AsRef<[u8]>, + ) -> Option<(String, String, String)> { + let SpatialEntry { element_id, .. } = self.find_entry_at_position(pos)?; + let element = self.elements.get(*element_id)?; + let ElementKind::Rpc { + request_type_ref, + response_type_ref, + .. + } = &element.kind + else { + return None; + }; + if !is_position_inside_range(pos, element.meta.selection_range) { + return None; + } + Some(( + element.meta.name.clone(), + request_type_ref.name.clone(), + response_type_ref.name.clone(), + )) + } + + /// If the given position is on a message name, returns that name. + pub fn message_name_at_position( + &self, + pos: Position, + _content: impl AsRef<[u8]>, + ) -> Option { + let SpatialEntry { element_id, .. } = self.find_entry_at_position(pos)?; + let element = self.elements.get(*element_id)?; + if !matches!(element.kind, ElementKind::Message { .. }) { + return None; + } + if !is_position_inside_range(pos, element.meta.selection_range) { + return None; + } + Some(element.meta.name.clone()) + } + + /// Returns the (request, response) type texts for every `rpc` element in + /// this document. Used to verify that a request/response type is uniquely used + /// by a single rpc before chain-renaming it. + pub fn all_rpc_signatures(&self, _content: impl AsRef<[u8]>) -> Vec<(String, String)> { + self.elements + .iter() + .filter_map(|element| match &element.kind { + ElementKind::Rpc { + request_type_ref, + response_type_ref, + .. + } => Some(( + request_type_ref.name.clone(), + response_type_ref.name.clone(), + )), + _ => None, + }) + .collect() + } +} + +/// Determines which dot-separated segment of a type reference the cursor rests +/// on and returns that segment's precise range, based on the cursor's character +/// offset within the reference's range. +fn type_ref_segment_range(type_ref: &TypeReference, position: Position) -> Option { + if position.line != type_ref.range.start.line { + return None; + } + let line = type_ref.range.start.line; + let mut cursor = type_ref.range.start.character as usize; + let position_char = position.character as usize; + for segment in type_ref.name.split('.') { + let seg_start = cursor; + let seg_end = cursor + segment.len(); + if position_char >= seg_start && position_char < seg_end { + return Some(Range { + start: Position { + line, + character: u32::try_from(seg_start).ok()?, + }, + end: Position { + line, + character: u32::try_from(seg_end).ok()?, + }, + }); + } + cursor = seg_end + 1; // skip the '.' separator + } + None +} + +#[cfg(test)] +mod test { + use async_lsp::lsp_types::{Position, Url}; + use insta::assert_yaml_snapshot; + + use crate::document::parser::ProtoParser; + use crate::utils::compile_test_query; + + #[test] + fn test_can_rename() { + let uri: Url = "file://foo/bar/test.proto".parse().unwrap(); + let pos_rename = Position { + line: 5, + character: 9, + }; + let pos_non_rename = Position { + line: 2, + character: 2, + }; + let pos_inner_type = Position { + line: 19, + character: 11, + }; + let pos_outer_type = Position { + line: 19, + character: 5, + }; + + let contents = include_str!("input/test_can_rename.proto"); + + let parsed = ProtoParser::new().parse(uri, contents, &compile_test_query()); + assert!(parsed.is_some()); + + let document = parsed.unwrap(); + assert_yaml_snapshot!(document.can_rename(pos_rename)); + assert_yaml_snapshot!(document.can_rename(pos_non_rename)); + assert_yaml_snapshot!(document.can_rename(pos_inner_type)); + assert_yaml_snapshot!(document.can_rename(pos_outer_type)); + } + + #[test] + fn test_can_rename_service_and_rpc() { + let uri: Url = "file://foo/bar/test.proto".parse().unwrap(); + let contents = include_str!("input/test_rename_service.proto"); + let parsed = ProtoParser::new().parse(uri, contents, &compile_test_query()); + assert!(parsed.is_some()); + let document = parsed.unwrap(); + + let pos_service = Position { + line: 10, + character: 10, + }; + let pos_rpc = Position { + line: 11, + character: 9, + }; + let pos_rpc_request_type = Position { + line: 11, + character: 17, + }; + + assert_yaml_snapshot!(document.can_rename(pos_service)); + assert_yaml_snapshot!(document.can_rename(pos_rpc)); + // Type references inside an RPC declaration are renameable from the + // reference site; the LSP layer pivots to the declaration. + assert_yaml_snapshot!(document.can_rename(pos_rpc_request_type)); + } + + #[test] + fn test_rpc_at_position_and_signatures() { + let uri: Url = "file://foo/bar.proto".parse().unwrap(); + let contents = include_str!("input/test_rename_service.proto"); + let parsed = ProtoParser::new() + .parse(uri, contents, &compile_test_query()) + .unwrap(); + + // `GetBook` rpc at line 11 chars 8..15 + let pos_rpc = Position { + line: 11, + character: 10, + }; + assert_eq!( + parsed.rpc_at_position(pos_rpc, contents), + Some(("GetBook".to_owned(), "Empty".to_owned(), "Book".to_owned(),)), + ); + + // Cursor on a non-rpc identifier should return None. + let pos_service = Position { + line: 10, + character: 10, + }; + assert_eq!(parsed.rpc_at_position(pos_service, contents), None); + + // all_rpc_signatures should pick up both rpcs in the file. + let sigs = parsed.all_rpc_signatures(contents); + assert_eq!( + sigs, + vec![ + ("Empty".to_owned(), "Book".to_owned()), + ("Empty".to_owned(), "Book".to_owned()), + ] + ); + } + + #[test] + fn test_message_name_at_position() { + let uri: Url = "file://foo/bar.proto".parse().unwrap(); + let contents = include_str!("input/test_rename_service.proto"); + let parsed = ProtoParser::new() + .parse(uri, contents, &compile_test_query()) + .unwrap(); + + // `Book` declaration at line 6 chars 8..12 + let pos = Position { + line: 6, + character: 9, + }; + assert_eq!( + parsed.message_name_at_position(pos, contents), + Some("Book".to_owned()) + ); + + // RPC name shouldn't match. + let pos_rpc = Position { + line: 11, + character: 10, + }; + assert_eq!(parsed.message_name_at_position(pos_rpc, contents), None); + } + + #[test] + fn test_can_rename_field_and_enum_value() { + let uri: Url = "file://foo/bar.proto".parse().unwrap(); + let contents = include_str!("input/test_rename_field.proto"); + let parsed = ProtoParser::new() + .parse(uri, contents, &compile_test_query()) + .unwrap(); + + // Cursor on a type identifier inside a field (`Author` at line 15 + // chars 4..10) is a reference site — already supported. + let pos_type_ref = Position { + line: 15, + character: 6, + }; + // Cursor on the field name should now be renameable. + let pos_plain_field = Position { + line: 14, + character: 12, + }; + // Cursor on the int_lit `1` is not a renameable identifier. + let pos_field_number = Position { + line: 14, + character: 19, + }; + + assert_yaml_snapshot!(parsed.can_rename(pos_type_ref)); + assert_yaml_snapshot!(parsed.can_rename(pos_plain_field)); + assert_yaml_snapshot!(parsed.can_rename(pos_field_number)); + } +} diff --git a/src/parser/snapshots/protols__parser__docsymbol__test__test_editions_document_symbols.snap b/src/document/snapshots/protols__document__docsymbol__test__test_editions_document_symbols.snap similarity index 98% rename from src/parser/snapshots/protols__parser__docsymbol__test__test_editions_document_symbols.snap rename to src/document/snapshots/protols__document__docsymbol__test__test_editions_document_symbols.snap index 3179c4f..11efdcc 100644 --- a/src/parser/snapshots/protols__parser__docsymbol__test__test_editions_document_symbols.snap +++ b/src/document/snapshots/protols__document__docsymbol__test__test_editions_document_symbols.snap @@ -1,5 +1,5 @@ --- -source: src/parser/docsymbol.rs +source: src/document/docsymbol.rs expression: symbols --- - name: NextGenBook diff --git a/src/parser/snapshots/protols__parser__docsymbol__test__test_package_duplicate_document_symbols.snap b/src/document/snapshots/protols__document__docsymbol__test__test_package_duplicate_document_symbols.snap similarity index 94% rename from src/parser/snapshots/protols__parser__docsymbol__test__test_package_duplicate_document_symbols.snap rename to src/document/snapshots/protols__document__docsymbol__test__test_package_duplicate_document_symbols.snap index 257d79d..4711f50 100644 --- a/src/parser/snapshots/protols__parser__docsymbol__test__test_package_duplicate_document_symbols.snap +++ b/src/document/snapshots/protols__document__docsymbol__test__test_package_duplicate_document_symbols.snap @@ -1,5 +1,5 @@ --- -source: src/parser/docsymbol.rs +source: src/document/docsymbol.rs expression: symbols --- - name: Test diff --git a/src/parser/snapshots/protols__parser__docsymbol__test__test_proto2_document_symbols.snap b/src/document/snapshots/protols__document__docsymbol__test__test_proto2_document_symbols.snap similarity index 99% rename from src/parser/snapshots/protols__parser__docsymbol__test__test_proto2_document_symbols.snap rename to src/document/snapshots/protols__document__docsymbol__test__test_proto2_document_symbols.snap index 0d2abb6..3b013c2 100644 --- a/src/parser/snapshots/protols__parser__docsymbol__test__test_proto2_document_symbols.snap +++ b/src/document/snapshots/protols__document__docsymbol__test__test_proto2_document_symbols.snap @@ -1,5 +1,5 @@ --- -source: src/parser/docsymbol.rs +source: src/document/docsymbol.rs expression: symbols --- - name: LegacyBook diff --git a/src/parser/snapshots/protols__parser__docsymbol__test__test_proto3_document_symbols.snap b/src/document/snapshots/protols__document__docsymbol__test__test_proto3_document_symbols.snap similarity index 99% rename from src/parser/snapshots/protols__parser__docsymbol__test__test_proto3_document_symbols.snap rename to src/document/snapshots/protols__document__docsymbol__test__test_proto3_document_symbols.snap index b545fda..7bfdf45 100644 --- a/src/parser/snapshots/protols__parser__docsymbol__test__test_proto3_document_symbols.snap +++ b/src/document/snapshots/protols__document__docsymbol__test__test_proto3_document_symbols.snap @@ -1,5 +1,5 @@ --- -source: src/parser/docsymbol.rs +source: src/document/docsymbol.rs expression: symbols --- - name: InventoryService diff --git a/src/parser/snapshots/protols__parser__hover__test__test_editions_hover.snap b/src/document/snapshots/protols__document__hover__test__test_editions_hover.snap similarity index 99% rename from src/parser/snapshots/protols__parser__hover__test__test_editions_hover.snap rename to src/document/snapshots/protols__document__hover__test__test_editions_hover.snap index b6b9a7c..3929651 100644 --- a/src/parser/snapshots/protols__parser__hover__test__test_editions_hover.snap +++ b/src/document/snapshots/protols__document__hover__test__test_editions_hover.snap @@ -1,5 +1,5 @@ --- -source: src/parser/hover.rs +source: src/document/hover.rs expression: results --- - target: "NextGenBook [definition]" diff --git a/src/parser/snapshots/protols__parser__hover__test__test_package_duplicate_hover.snap b/src/document/snapshots/protols__document__hover__test__test_package_duplicate_hover.snap similarity index 97% rename from src/parser/snapshots/protols__parser__hover__test__test_package_duplicate_hover.snap rename to src/document/snapshots/protols__document__hover__test__test_package_duplicate_hover.snap index 6e8070b..d25a585 100644 --- a/src/parser/snapshots/protols__parser__hover__test__test_package_duplicate_hover.snap +++ b/src/document/snapshots/protols__document__hover__test__test_package_duplicate_hover.snap @@ -1,5 +1,5 @@ --- -source: src/parser/hover.rs +source: src/document/hover.rs expression: results --- - target: "Test [definition]" diff --git a/src/parser/snapshots/protols__parser__hover__test__test_proto2_hover.snap b/src/document/snapshots/protols__document__hover__test__test_proto2_hover.snap similarity index 99% rename from src/parser/snapshots/protols__parser__hover__test__test_proto2_hover.snap rename to src/document/snapshots/protols__document__hover__test__test_proto2_hover.snap index b306484..00945e3 100644 --- a/src/parser/snapshots/protols__parser__hover__test__test_proto2_hover.snap +++ b/src/document/snapshots/protols__document__hover__test__test_proto2_hover.snap @@ -1,5 +1,5 @@ --- -source: src/parser/hover.rs +source: src/document/hover.rs expression: results --- - target: "google/api/annotations.proto [definition]" diff --git a/src/parser/snapshots/protols__parser__hover__test__test_proto3_hover.snap b/src/document/snapshots/protols__document__hover__test__test_proto3_hover.snap similarity index 99% rename from src/parser/snapshots/protols__parser__hover__test__test_proto3_hover.snap rename to src/document/snapshots/protols__document__hover__test__test_proto3_hover.snap index bf9a7b0..ff6e623 100644 --- a/src/parser/snapshots/protols__parser__hover__test__test_proto3_hover.snap +++ b/src/document/snapshots/protols__document__hover__test__test_proto3_hover.snap @@ -1,5 +1,5 @@ --- -source: src/parser/hover.rs +source: src/document/hover.rs expression: results --- - target: "google/protobuf/any.proto [definition]" diff --git a/src/document/snapshots/protols__document__rename__test__can_rename-2.snap b/src/document/snapshots/protols__document__rename__test__can_rename-2.snap new file mode 100644 index 0000000..62d3c9d --- /dev/null +++ b/src/document/snapshots/protols__document__rename__test__can_rename-2.snap @@ -0,0 +1,5 @@ +--- +source: src/document/rename.rs +expression: tree.can_rename(pos_non_rename) +--- +~ diff --git a/src/document/snapshots/protols__document__rename__test__can_rename-3.snap b/src/document/snapshots/protols__document__rename__test__can_rename-3.snap new file mode 100644 index 0000000..adda07e --- /dev/null +++ b/src/document/snapshots/protols__document__rename__test__can_rename-3.snap @@ -0,0 +1,10 @@ +--- +source: src/document/rename.rs +expression: tree.can_rename(pos_inner_type) +--- +start: + line: 19 + character: 9 +end: + line: 19 + character: 15 diff --git a/src/document/snapshots/protols__document__rename__test__can_rename-4.snap b/src/document/snapshots/protols__document__rename__test__can_rename-4.snap new file mode 100644 index 0000000..3c2463b --- /dev/null +++ b/src/document/snapshots/protols__document__rename__test__can_rename-4.snap @@ -0,0 +1,10 @@ +--- +source: src/document/rename.rs +expression: tree.can_rename(pos_outer_type) +--- +start: + line: 19 + character: 4 +end: + line: 19 + character: 8 diff --git a/src/parser/snapshots/protols__parser__rename__test__can_rename.snap b/src/document/snapshots/protols__document__rename__test__can_rename.snap similarity index 50% rename from src/parser/snapshots/protols__parser__rename__test__can_rename.snap rename to src/document/snapshots/protols__document__rename__test__can_rename.snap index 6a024e6..71684e7 100644 --- a/src/parser/snapshots/protols__parser__rename__test__can_rename.snap +++ b/src/document/snapshots/protols__document__rename__test__can_rename.snap @@ -1,6 +1,6 @@ --- -source: src/parser/rename.rs -expression: tree.can_rename(&pos_rename) +source: src/document/rename.rs +expression: tree.can_rename(pos_rename) --- start: line: 5 diff --git a/src/document/snapshots/protols__document__rename__test__can_rename_field_and_enum_value-2.snap b/src/document/snapshots/protols__document__rename__test__can_rename_field_and_enum_value-2.snap new file mode 100644 index 0000000..2021a71 --- /dev/null +++ b/src/document/snapshots/protols__document__rename__test__can_rename_field_and_enum_value-2.snap @@ -0,0 +1,10 @@ +--- +source: src/document/rename.rs +expression: parsed.can_rename(pos_plain_field) +--- +start: + line: 14 + character: 11 +end: + line: 14 + character: 16 diff --git a/src/document/snapshots/protols__document__rename__test__can_rename_field_and_enum_value-3.snap b/src/document/snapshots/protols__document__rename__test__can_rename_field_and_enum_value-3.snap new file mode 100644 index 0000000..57ffc29 --- /dev/null +++ b/src/document/snapshots/protols__document__rename__test__can_rename_field_and_enum_value-3.snap @@ -0,0 +1,5 @@ +--- +source: src/document/rename.rs +expression: parsed.can_rename(pos_field_number) +--- +~ diff --git a/src/document/snapshots/protols__document__rename__test__can_rename_field_and_enum_value.snap b/src/document/snapshots/protols__document__rename__test__can_rename_field_and_enum_value.snap new file mode 100644 index 0000000..a4bfdc6 --- /dev/null +++ b/src/document/snapshots/protols__document__rename__test__can_rename_field_and_enum_value.snap @@ -0,0 +1,10 @@ +--- +source: src/document/rename.rs +expression: parsed.can_rename(pos_type_ref) +--- +start: + line: 15 + character: 4 +end: + line: 15 + character: 10 diff --git a/src/parser/snapshots/protols__parser__rename__test__can_rename_service_and_rpc-2.snap b/src/document/snapshots/protols__document__rename__test__can_rename_service_and_rpc-2.snap similarity index 51% rename from src/parser/snapshots/protols__parser__rename__test__can_rename_service_and_rpc-2.snap rename to src/document/snapshots/protols__document__rename__test__can_rename_service_and_rpc-2.snap index e4e36a2..69fea2b 100644 --- a/src/parser/snapshots/protols__parser__rename__test__can_rename_service_and_rpc-2.snap +++ b/src/document/snapshots/protols__document__rename__test__can_rename_service_and_rpc-2.snap @@ -1,6 +1,6 @@ --- -source: src/parser/rename.rs -expression: tree.can_rename(&pos_rpc) +source: src/document/rename.rs +expression: tree.can_rename(pos_rpc) --- start: line: 11 diff --git a/src/document/snapshots/protols__document__rename__test__can_rename_service_and_rpc-3.snap b/src/document/snapshots/protols__document__rename__test__can_rename_service_and_rpc-3.snap new file mode 100644 index 0000000..1cb9bbc --- /dev/null +++ b/src/document/snapshots/protols__document__rename__test__can_rename_service_and_rpc-3.snap @@ -0,0 +1,10 @@ +--- +source: src/document/rename.rs +expression: tree.can_rename(pos_rpc_request_type) +--- +start: + line: 11 + character: 16 +end: + line: 11 + character: 21 diff --git a/src/parser/snapshots/protols__parser__rename__test__can_rename_service_and_rpc.snap b/src/document/snapshots/protols__document__rename__test__can_rename_service_and_rpc.snap similarity index 50% rename from src/parser/snapshots/protols__parser__rename__test__can_rename_service_and_rpc.snap rename to src/document/snapshots/protols__document__rename__test__can_rename_service_and_rpc.snap index 74b2212..18a1191 100644 --- a/src/parser/snapshots/protols__parser__rename__test__can_rename_service_and_rpc.snap +++ b/src/document/snapshots/protols__document__rename__test__can_rename_service_and_rpc.snap @@ -1,6 +1,6 @@ --- -source: src/parser/rename.rs -expression: tree.can_rename(&pos_service) +source: src/document/rename.rs +expression: tree.can_rename(pos_service) --- start: line: 10 diff --git a/src/parser/snapshots/protols__parser__diagnostics__test__collect_parse_error-2.snap b/src/document/snapshots/protols__document__syntax__test__collect_parse_error-2.snap similarity index 80% rename from src/parser/snapshots/protols__parser__diagnostics__test__collect_parse_error-2.snap rename to src/document/snapshots/protols__document__syntax__test__collect_parse_error-2.snap index 55f2810..c4b0f07 100644 --- a/src/parser/snapshots/protols__parser__diagnostics__test__collect_parse_error-2.snap +++ b/src/document/snapshots/protols__document__syntax__test__collect_parse_error-2.snap @@ -1,7 +1,6 @@ --- -source: src/parser/diagnostics.rs +source: src/document/diagnostics.rs expression: parsed.unwrap().collect_parse_diagnostics() -snapshot_kind: text --- - range: start: diff --git a/src/parser/snapshots/protols__parser__diagnostics__test__collect_parse_error.snap b/src/document/snapshots/protols__document__syntax__test__collect_parse_error.snap similarity index 55% rename from src/parser/snapshots/protols__parser__diagnostics__test__collect_parse_error.snap rename to src/document/snapshots/protols__document__syntax__test__collect_parse_error.snap index d90c57d..68a58d3 100644 --- a/src/parser/snapshots/protols__parser__diagnostics__test__collect_parse_error.snap +++ b/src/document/snapshots/protols__document__syntax__test__collect_parse_error.snap @@ -1,6 +1,5 @@ --- -source: src/parser/diagnostics.rs +source: src/document/diagnostics.rs expression: parsed.unwrap().collect_parse_diagnostics() -snapshot_kind: text --- [] diff --git a/src/parser/snapshots/protols__parser__tree__test__filter-2.snap b/src/document/snapshots/protols__document__tree__test__filter-2.snap similarity index 60% rename from src/parser/snapshots/protols__parser__tree__test__filter-2.snap rename to src/document/snapshots/protols__document__tree__test__filter-2.snap index 022e111..7cb4af2 100644 --- a/src/parser/snapshots/protols__parser__tree__test__filter-2.snap +++ b/src/document/snapshots/protols__document__tree__test__filter-2.snap @@ -1,5 +1,5 @@ --- -source: src/parser/tree.rs +source: src/document/tree.rs expression: package_name --- com.parser diff --git a/src/parser/snapshots/protols__parser__tree__test__filter-3.snap b/src/document/snapshots/protols__document__tree__test__filter-3.snap similarity index 67% rename from src/parser/snapshots/protols__parser__tree__test__filter-3.snap rename to src/document/snapshots/protols__document__tree__test__filter-3.snap index 6dc639c..6e5cb31 100644 --- a/src/parser/snapshots/protols__parser__tree__test__filter-3.snap +++ b/src/document/snapshots/protols__document__tree__test__filter-3.snap @@ -1,5 +1,5 @@ --- -source: src/parser/tree.rs +source: src/document/tree.rs expression: imports --- - foo/bar.proto diff --git a/src/parser/snapshots/protols__parser__tree__test__filter.snap b/src/document/snapshots/protols__document__tree__test__filter.snap similarity index 59% rename from src/parser/snapshots/protols__parser__tree__test__filter.snap rename to src/document/snapshots/protols__document__tree__test__filter.snap index 6627aea..3c5ddbc 100644 --- a/src/parser/snapshots/protols__parser__tree__test__filter.snap +++ b/src/document/snapshots/protols__document__tree__test__filter.snap @@ -1,5 +1,5 @@ --- -source: src/parser/tree.rs +source: src/document/tree.rs expression: names --- - Book diff --git a/src/parser/diagnostics.rs b/src/document/syntax.rs similarity index 54% rename from src/parser/diagnostics.rs rename to src/document/syntax.rs index 88d2bd8..432fca8 100644 --- a/src/parser/diagnostics.rs +++ b/src/document/syntax.rs @@ -1,12 +1,25 @@ +//! Syntax-level diagnostics that still require direct access to the raw +//! Tree-sitter tree. +//! +//! Parse errors (`ERROR` nodes) are not part of the semantic metamodel — the +//! extractor only records well-formed entities — so collecting them is the one +//! place we traverse the raw syntax tree directly. + use async_lsp::lsp_types::{Diagnostic, DiagnosticSeverity}; +use tree_sitter::Node; -use crate::{nodekind::NodeKind, utils::to_lsp_range}; +use crate::utils::to_lsp_range; -use super::ParsedTree; +use super::parser::ProtoDocument; -impl ParsedTree { +impl ProtoDocument { + /// Collects parse diagnostics by walking the raw syntax tree for `ERROR` + /// nodes. pub fn collect_parse_diagnostics(&self) -> Vec { - self.find_all_nodes(NodeKind::is_error) + let mut errors = Vec::new(); + collect_error_nodes(self.tree.root_node(), &mut errors); + + errors .into_iter() .map(|n| Diagnostic { range: to_lsp_range(n), @@ -17,18 +30,17 @@ impl ParsedTree { }) .collect() } +} - pub fn collect_import_diagnostics(&self, content: &[u8], import: &[&str]) -> Vec { - self.get_import_path_range(content, import) - .into_iter() - .map(|r| Diagnostic { - range: r, - severity: Some(DiagnosticSeverity::ERROR), - source: Some(String::from("protols")), - message: "failed to find proto file".to_string(), - ..Default::default() - }) - .collect() +fn collect_error_nodes<'a>(n: Node<'a>, out: &mut Vec>) { + // Tree-sitter marks malformed regions with an `ERROR` node; these are the + // only raw-tree nodes we still inspect (they are absent from the metamodel). + if n.kind() == "ERROR" { + out.push(n); + } + let mut cursor = n.walk(); + for child in n.children(&mut cursor) { + collect_error_nodes(child, out); } } @@ -37,7 +49,7 @@ mod test { use async_lsp::lsp_types::Url; use insta::assert_yaml_snapshot; - use crate::parser::ProtoParser; + use crate::document::parser::ProtoParser; use crate::utils::compile_test_query; #[test] diff --git a/src/formatter/mod.rs b/src/formatter/mod.rs index 5cb3af5..cae7738 100644 --- a/src/formatter/mod.rs +++ b/src/formatter/mod.rs @@ -1,6 +1,8 @@ +mod clang; + use async_lsp::lsp_types::{Range, TextEdit}; -pub mod clang; +pub use clang::ClangFormatter; pub trait ProtoFormatter: Sized { fn format_document(&self, filename: &str, content: &str) -> Option>; diff --git a/src/lsp.rs b/src/lsp.rs index e878040..1bcfc24 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -20,7 +20,6 @@ use async_lsp::{Error, LanguageClient, ResponseError}; use futures::future::BoxFuture; use serde_json::Value; -use crate::context::jumpable::Jumpable; use crate::formatter::ProtoFormatter; use crate::server::ProtoLanguageServer; use crate::{docs, log}; @@ -130,10 +129,10 @@ impl ProtoLanguageServer { }), }; - // Phase 2: Index all configured workspaces once at startup. This - // populates the in-memory metamodel pool that `workspace/symbol` - // queries against, keeping per-request symbol lookups free of - // on-the-fly workspace re-scans and re-parses. + // Index all configured workspaces once at startup. This populates the + // in-memory metamodel pool that `workspace/symbol` queries against, + // keeping per-request symbol lookups free of on-the-fly workspace + // re-scans and re-parses. let workspace_paths: Vec = self .configs .get_workspaces() @@ -207,19 +206,19 @@ impl ProtoLanguageServer { ..CompletionItem::default() })); - // Build completion item from the current tree - if let Some(tree) = self.state.get_tree(&uri) { - let content = self.state.get_content(&uri); - if let Some(package_name) = tree.get_package_name(content.as_bytes()) { + // Build completion item from the current document + if let Some(document) = self.state.get_document(&uri) { + let package_name = document.package_name(); + if package_name != "." { completions.extend(self.state.completion_items_for_package(package_name)); } if let Some(ipath) = self.configs.get_include_paths(&uri) { - for import in &tree.get_import_paths(content.as_bytes()) { - if let Some(p) = ipath.iter().map(|p| p.join(import)).find(|p| p.exists()) + for import in document.import_paths() { + if let Some(p) = ipath.iter().map(|p| p.join(&import)).find(|p| p.exists()) && let Ok(uri) = Url::from_file_path(p.clone()) { - completions.extend(self.state.completion_items_for_tree(&uri)); + completions.extend(self.state.completion_items_for_document(&uri)); } } } @@ -234,12 +233,12 @@ impl ProtoLanguageServer { let uri = params.text_document.uri; let pos = params.position; - let Some(tree) = self.state.get_tree(&uri) else { - error!(uri=%uri, "failed to get tree"); + let Some(document) = self.state.get_document(&uri) else { + error!(uri=%uri, "failed to get document"); return Box::pin(async move { Ok(None) }); }; - let response = tree.can_rename(pos).map(PrepareRenameResponse::Range); + let response = document.can_rename(pos).map(PrepareRenameResponse::Range); Box::pin(async move { Ok(response) }) } @@ -252,47 +251,21 @@ impl ProtoLanguageServer { let pos = params.text_document_position.position; let new_name = params.new_name; - let Some(tree) = self.state.get_tree(&uri) else { - error!(uri=%uri, "failed to get tree"); - return Box::pin(async move { Ok(None) }); - }; - - let content = self.state.get_content(&uri); - let current_package = tree.get_package_name(content.as_bytes()).unwrap_or("."); let ipath = self.configs.get_include_paths(&uri).unwrap_or_default(); - // If the cursor is on a type reference (inside a message_or_enum_type - // node), pivot to the declaration and rename from there. The workspace - // pass then handles all references — including the one the user is - // standing on. - let (decl_uri, decl_pos) = match tree.rename_pivot_identifier(pos, content.as_bytes()) { - Some(decl_path) => { - let locations = - self.state - .definition(&ipath, current_package, Jumpable::Identifier(decl_path)); - let Some(decl) = locations.into_iter().next() else { - error!(uri=%uri, "failed to resolve declaration for reference-site rename"); - return Box::pin(async move { Ok(None) }); - }; - (decl.uri, decl.range.start) - } - None => (uri.clone(), pos), - }; - - let Some(workspace) = self.configs.get_workspace_for_uri(&decl_uri) else { - error!(uri=%decl_uri, "failed to get workspace"); + // Resolve the symbol under the cursor directly from the metamodel, + // using its position (like hover / go-to-definition). This handles both + // declaration sites and reference sites (pivoting to the referenced + // declaration) without any string-based identifier reconstruction. + let Some(target_fqn) = self.state.resolve_target_fqn(&uri, pos) else { + error!(uri=%uri, "failed to resolve target fqn for rename"); return Box::pin(async move { Ok(None) }); }; - let Ok(workspace_path) = workspace.to_file_path() else { - error!(uri=%workspace, "workspace url is not a file path"); + let Some((decl_uri, decl_pos)) = self.state.declaration_for_fqn(&target_fqn) else { + error!(fqn=%target_fqn, "failed to locate declaration for rename"); return Box::pin(async move { Ok(None) }); }; - let progress_sender = params - .work_done_progress_params - .work_done_token - .map(|token| self.with_report_progress(token)); - // The rpc/request/response chain rename is opt-in via the workspace's // `[config.rename]` settings; without a config it stays off. let chain_rpc_request_response = self @@ -307,10 +280,7 @@ impl ProtoLanguageServer { &ipath, chain_rpc_request_response, ); - let Some(all_edits) = self - .state - .apply_rename_ops(&ops, &workspace_path, progress_sender) - else { + let Some(all_edits) = self.state.apply_rename_ops(&ops) else { error!(uri=%decl_uri, "failed to apply primary rename"); return Box::pin(async move { Ok(None) }); }; @@ -333,37 +303,16 @@ impl ProtoLanguageServer { ) -> BoxFuture<'static, Result>, ResponseError>> { let uri = param.text_document_position.text_document.uri; let pos = param.text_document_position.position; - let work_done_token = param.work_done_progress_params.work_done_token; - let Some(tree) = self.state.get_tree(&uri) else { - error!(uri=%uri, "failed to get tree"); + // The workspace is already fully indexed once at startup (see the + // `initialize` handler), so cross-file reference resolution operates on + // the cached metamodel pool without any per-request re-scan. + let Some(target_fqn) = self.state.resolve_target_fqn(&uri, pos) else { + error!(uri=%uri, "failed to resolve target fqn"); return Box::pin(async move { Ok(None) }); }; - let content = self.state.get_content(&uri); - - let current_package = tree.get_package_name(content.as_bytes()).unwrap_or("."); - - let Some((mut refs, otext)) = tree.reference_tree(pos, content.as_bytes()) else { - error!(uri=%uri, "failed to find references in a tree"); - return Box::pin(async move { Ok(None) }); - }; - - let Some(workspace) = self.configs.get_workspace_for_uri(&uri) else { - error!(uri=%uri, "failed to get workspace"); - return Box::pin(async move { Ok(None) }); - }; - - let progress_sender = work_done_token.map(|token| self.with_report_progress(token)); - - if let Some(v) = self.state.reference_fields( - current_package, - &otext, - &workspace.to_file_path().unwrap(), - progress_sender.as_ref(), - ) { - refs.extend(v); - } + let refs = self.state.references_for_fqn(&target_fqn); Box::pin(async move { if refs.is_empty() { @@ -381,24 +330,8 @@ impl ProtoLanguageServer { let uri = param.text_document_position_params.text_document.uri; let pos = param.text_document_position_params.position; - let Some(tree) = self.state.get_tree(&uri) else { - error!(uri=%uri, "failed to get tree"); - return Box::pin(async move { Ok(None) }); - }; - - let content = self.state.get_content(&uri); - let jump = tree.get_jumpable_at_position(pos, content.as_bytes()); - let current_package_name = tree.get_package_name(content.as_bytes()).unwrap_or("."); - - let Some(jump) = jump else { - error!(uri=%uri, "failed to get jump identifier"); - return Box::pin(async move { Ok(None) }); - }; - let ipath = self.configs.get_include_paths(&uri).unwrap_or_default(); - let locations = self - .state - .definition(&ipath, current_package_name.as_ref(), jump); + let locations = self.state.definition(&uri, pos, &ipath); let response = match locations.len() { 0 => None, @@ -415,12 +348,12 @@ impl ProtoLanguageServer { ) -> BoxFuture<'static, Result, ResponseError>> { let uri = params.text_document.uri; - let Some(tree) = self.state.get_tree(&uri) else { - error!(uri=%uri, "failed to get tree"); + let Some(document) = self.state.get_document(&uri) else { + error!(uri=%uri, "failed to get document"); return Box::pin(async move { Ok(None) }); }; - let symbols = tree.document_symbols(); + let symbols = document.document_symbols(); let response = DocumentSymbolResponse::Nested(symbols); Box::pin(async move { Ok(Some(response)) }) diff --git a/src/main.rs b/src/main.rs index bc0585e..c95ab23 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,20 +15,17 @@ use crate::transport::create_transport; mod cli; mod config; -mod context; mod docs; +mod document; mod formatter; mod log; mod lsp; mod model; -mod nodekind; -mod parser; mod protoc; mod server; mod state; mod transport; mod utils; -mod workspace; const FALLBACK_INCLUDE_PATH: Option<&str> = option_env!("FALLBACK_INCLUDE_PATH"); diff --git a/src/model/extractor.rs b/src/model/extractor.rs index 75b70c1..793b6a7 100644 --- a/src/model/extractor.rs +++ b/src/model/extractor.rs @@ -1,7 +1,7 @@ //! High-performance metadata extractor pipeline for protobuf schemas. //! //! This module coordinates the execution of compiled Tree-sitter queries -//! against the source abstract syntax tree (AST). It handles sequential stream +//! against the source abstract syntax document (AST). It handles sequential stream //! matching, manages the container hierarchy stack, accumulates floating //! docstrings, and compiles the final elements registry. @@ -17,18 +17,18 @@ use super::types::{CommentBlock, ElementKind, ElementMeta, ModelElement}; mod handlers; mod query; -/// Executes the compiled Tree-sitter query against the syntax tree root to +/// Executes the compiled Tree-sitter query against the syntax document root to /// build the pure-memory metamodel registry. /// /// This function acts as the orchestrator of the extraction pipeline. It runs a -/// `QueryCursor` top-down through the syntax tree nodes, captures matched +/// `QueryCursor` top-down through the syntax document nodes, captures matched /// patterns, dispatches them to specialized structural handlers, and /// incrementally feeds the results into the chronological context building /// loop. /// /// # Arguments /// -/// * `root_node` - The top-level abstract syntax tree [`Node`] representing the +/// * `root_node` - The top-level abstract syntax document [`Node`] representing the /// fully parsed document. /// * `source` - The raw UTF-8 byte array sequence containing the complete /// original source file content on disk. @@ -153,7 +153,7 @@ impl ParsedMatch { /// /// This geometric layout reference isolates the coordinate ranges across /// distinct variants, providing a uniform anchor point used primarily to - /// sort non-linear tree-sitter streaming buffers back into topological + /// sort non-linear document-sitter streaming buffers back into topological /// top-down code order. /// /// # Returns diff --git a/src/model/extractor/query.rs b/src/model/extractor/query.rs index 2608649..8e3f078 100644 --- a/src/model/extractor/query.rs +++ b/src/model/extractor/query.rs @@ -13,7 +13,7 @@ use super::super::captures::{ /// Generates the master Tree-sitter SCM (Source Code Matcher) query string used /// for metadata and layout extraction across all protobuf structural elements. /// -/// This function constructs a unified, high-performance tree query that +/// This function constructs a unified, high-performance document query that /// patterns and captures messages, enums, services, fields, and independent /// modifiers (like deprecation markers or docstrings). /// diff --git a/src/model.rs b/src/model/mod.rs similarity index 100% rename from src/model.rs rename to src/model/mod.rs diff --git a/src/model/presentation.rs b/src/model/presentation.rs index ca7f72d..565e7a1 100644 --- a/src/model/presentation.rs +++ b/src/model/presentation.rs @@ -48,8 +48,7 @@ impl ModelElement { /// accumulated leading comments. pub fn to_hover_markdown(&self, position: Position) -> Option { if let Some(referenced_type) = self.inspect_nested_type_reference(position) { - // Returns `None` for user-defined types. This should be fixed at - // Phase 3 of https://github.com/coder3101/protols/issues/130 + // Returns `None` for user-defined types. return docs::BUILTIN .get(referenced_type) .or_else(|| crate::docs::WELLKNOWN.get(referenced_type)) diff --git a/src/model/spatial.rs b/src/model/spatial.rs index 848df93..d1c8437 100644 --- a/src/model/spatial.rs +++ b/src/model/spatial.rs @@ -11,6 +11,40 @@ use crate::{model::TypeReference, utils::is_position_inside_range}; use super::types::{ElementKind, ModelElement, SpatialEntry}; +impl ElementKind { + /// Returns every type reference embedded in this element kind (field + /// types, map key/value types, and RPC request/response types). + pub fn type_references(&self) -> Vec<&TypeReference> { + match self { + ElementKind::Field { type_ref, .. } | ElementKind::OneofField { type_ref, .. } => { + vec![type_ref] + } + ElementKind::MapField { + key_type_ref, + value_type_ref, + .. + } => vec![key_type_ref, value_type_ref], + ElementKind::Rpc { + request_type_ref, + response_type_ref, + .. + } => vec![request_type_ref, response_type_ref], + _ => Vec::new(), + } + } +} + +impl ModelElement { + /// Returns the type reference whose geometric bounds contain `position`, + /// if any. + pub fn type_reference_at(&self, position: Position) -> Option<&TypeReference> { + self.kind + .type_references() + .into_iter() + .find(|r| is_position_inside_range(position, r.range)) + } +} + impl ModelElement { /// Flattens the element's internal name boundaries and type reference /// bounds into autonomous geometric intersection entries. diff --git a/src/model/types.rs b/src/model/types.rs index a05626c..c1b2eb5 100644 --- a/src/model/types.rs +++ b/src/model/types.rs @@ -425,7 +425,7 @@ pub enum ElementKind { } /// A normalized, index-backed semantic graph node representing a single -/// declared entity within a protobuf abstract syntax tree. +/// declared entity within a protobuf abstract syntax document. /// /// Instead of relying on heavy runtime pointer networks or heap-allocated /// reference counting pointers (`Rc`/`Arc`), this model utilizes flat, safe @@ -436,7 +436,7 @@ pub enum ElementKind { #[derive(Debug, Clone)] pub struct ModelElement { /// The unique sequential identifier and position index of this specific - /// element within the master flat vector registry of the parsed tree. + /// element within the master flat vector registry of the parsed document. pub id: usize, /// The unique numerical identifier of the parent container enclosing this diff --git a/src/nodekind.rs b/src/nodekind.rs deleted file mode 100644 index 141ac63..0000000 --- a/src/nodekind.rs +++ /dev/null @@ -1,464 +0,0 @@ -use async_lsp::lsp_types::SymbolKind; -use tree_sitter::Node; - -pub enum NodeKind { - Identifier, - Error, - MessageName, - Message, - EnumName, - FieldName, - ServiceName, - RpcName, - PackageName, - PackageImport, -} - -#[allow(unused)] -impl NodeKind { - pub fn as_str(&self) -> &'static str { - match self { - NodeKind::Identifier => "identifier", - NodeKind::Error => "ERROR", - NodeKind::MessageName => "message_name", - NodeKind::Message => "message", - NodeKind::EnumName => "enum_name", - NodeKind::FieldName => "message_or_enum_type", - NodeKind::ServiceName => "service_name", - NodeKind::RpcName => "rpc_name", - NodeKind::PackageName => "full_ident", - NodeKind::PackageImport => "import", - } - } - - pub fn is_identifier(n: &Node) -> bool { - n.kind() == Self::Identifier.as_str() - } - - pub fn is_error(n: &Node) -> bool { - n.kind() == Self::Error.as_str() - } - - pub fn is_import_path(n: &Node) -> bool { - n.kind() == Self::PackageImport.as_str() - } - - pub fn is_package_name(n: &Node) -> bool { - n.kind() == Self::PackageName.as_str() - } - - pub fn is_enum_name(n: &Node) -> bool { - n.kind() == Self::EnumName.as_str() - } - - pub fn is_message_name(n: &Node) -> bool { - n.kind() == Self::MessageName.as_str() - } - - pub fn is_message(n: &Node) -> bool { - n.kind() == Self::Message.as_str() - } - - pub fn is_field_name(n: &Node) -> bool { - n.kind() == Self::FieldName.as_str() - } - - pub fn is_rpc_name(n: &Node) -> bool { - n.kind() == Self::RpcName.as_str() - } - - pub fn is_userdefined(n: &Node) -> bool { - n.kind() == Self::EnumName.as_str() || n.kind() == Self::MessageName.as_str() - } - - pub fn is_renameable(n: &Node) -> bool { - Self::is_userdefined(n) - || n.kind() == Self::ServiceName.as_str() - || n.kind() == Self::RpcName.as_str() - || n.kind() == Self::FieldName.as_str() - || Self::is_field_decl_parent(n) - } - - /// Kinds whose direct identifier child is the *name* of a field-like - /// declaration: regular fields, map fields, oneof fields, the oneof itself, - /// and enum values. For `string title = 1;`, the identifier `title` has - /// parent `field` — that's what we match here. The type identifier (e.g. - /// `Author` in `Author author = 2;`) is nested deeper under - /// `message_or_enum_type`, so it isn't caught by this predicate. - pub fn is_field_decl_parent(n: &Node) -> bool { - matches!( - n.kind(), - "field" | "map_field" | "oneof_field" | "oneof" | "enum_field" - ) - } - - pub fn is_actionable(n: &Node) -> bool { - n.kind() == Self::MessageName.as_str() - || n.kind() == Self::EnumName.as_str() - || n.kind() == Self::FieldName.as_str() - || n.kind() == Self::PackageName.as_str() - || n.kind() == Self::ServiceName.as_str() - || n.kind() == Self::RpcName.as_str() - } - - pub fn to_symbolkind(n: &Node) -> SymbolKind { - if n.kind() == Self::MessageName.as_str() { - SymbolKind::STRUCT - } else if n.kind() == Self::EnumName.as_str() { - SymbolKind::ENUM - } else { - SymbolKind::NULL - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use tree_sitter::Parser; - - fn parse_proto(source: &str) -> tree_sitter::Tree { - let mut parser = Parser::new(); - parser - .set_language(&tree_sitter_proto::LANGUAGE.into()) - .unwrap(); - parser.parse(source, None).unwrap() - } - - #[test] - fn test_is_identifier() { - let tree = parse_proto("message Foo { string name = 1; }"); - let mut cursor = tree.root_node().walk(); - // Find the "name" identifier node (child of field) - let mut found = false; - loop { - let n = cursor.node(); - if n.kind() == "identifier" { - assert!(NodeKind::is_identifier(&n)); - found = true; - } else { - assert!(!NodeKind::is_identifier(&n)); - } - if cursor.goto_first_child() { - continue; - } - if !cursor.goto_next_sibling() { - break; - } - } - assert!(found, "expected at least one identifier node"); - } - - #[test] - fn test_is_message_name() { - let tree = parse_proto("message Foo {}"); - let mut cursor = tree.root_node().walk(); - loop { - let n = cursor.node(); - if n.kind() == "message_name" { - assert!(NodeKind::is_message_name(&n)); - } - if cursor.goto_first_child() { - continue; - } - if !cursor.goto_next_sibling() { - break; - } - } - } - - #[test] - fn test_is_enum_name() { - let tree = parse_proto("enum Color { RED = 0; }"); - let mut cursor = tree.root_node().walk(); - loop { - let n = cursor.node(); - if n.kind() == "enum_name" { - assert!(NodeKind::is_enum_name(&n)); - } - if cursor.goto_first_child() { - continue; - } - if !cursor.goto_next_sibling() { - break; - } - } - } - - #[test] - fn test_is_field_name() { - let tree = parse_proto("message Foo { string bar = 1; }"); - let mut cursor = tree.root_node().walk(); - loop { - let n = cursor.node(); - if n.kind() == "message_or_enum_type" { - assert!(NodeKind::is_field_name(&n)); - } - if cursor.goto_first_child() { - continue; - } - if !cursor.goto_next_sibling() { - break; - } - } - } - - #[test] - fn test_is_rpc_name() { - let tree = parse_proto("service S { rpc Foo(Empty) returns (Empty); }"); - let mut cursor = tree.root_node().walk(); - loop { - let n = cursor.node(); - if n.kind() == "rpc_name" { - assert!(NodeKind::is_rpc_name(&n)); - } - if cursor.goto_first_child() { - continue; - } - if !cursor.goto_next_sibling() { - break; - } - } - } - - #[test] - fn test_service_name_node_kind() { - let tree = parse_proto("service MyService {}"); - let mut cursor = tree.root_node().walk(); - loop { - let n = cursor.node(); - if n.kind() == "service_name" { - assert_eq!(n.kind(), "service_name"); - } - if cursor.goto_first_child() { - continue; - } - if !cursor.goto_next_sibling() { - break; - } - } - } - - #[test] - fn test_is_package_name() { - let tree = parse_proto("package foo.bar;"); - let mut cursor = tree.root_node().walk(); - loop { - let n = cursor.node(); - if n.kind() == "full_ident" { - assert!(NodeKind::is_package_name(&n)); - } - if cursor.goto_first_child() { - continue; - } - if !cursor.goto_next_sibling() { - break; - } - } - } - - #[test] - fn test_is_import_path() { - let tree = parse_proto("import \"foo/bar.proto\";"); - let mut cursor = tree.root_node().walk(); - loop { - let n = cursor.node(); - if n.kind() == "import" { - assert!(NodeKind::is_import_path(&n)); - } - if cursor.goto_first_child() { - continue; - } - if !cursor.goto_next_sibling() { - break; - } - } - } - - #[test] - fn test_is_error() { - let tree = parse_proto("message Foo { invalid_syntax }"); - let mut cursor = tree.root_node().walk(); - loop { - let n = cursor.node(); - if n.kind() == "ERROR" { - assert!(NodeKind::is_error(&n)); - } - if cursor.goto_first_child() { - continue; - } - if !cursor.goto_next_sibling() { - break; - } - } - } - - #[test] - fn test_is_message() { - let tree = parse_proto("message Foo {}"); - let mut cursor = tree.root_node().walk(); - loop { - let n = cursor.node(); - if n.kind() == "message" { - assert!(NodeKind::is_message(&n)); - } - if cursor.goto_first_child() { - continue; - } - if !cursor.goto_next_sibling() { - break; - } - } - } - - #[test] - fn test_is_userdefined() { - let tree = parse_proto("message Foo { enum Bar { X = 0; } }"); - let mut cursor = tree.root_node().walk(); - loop { - let n = cursor.node(); - if n.kind() == "message_name" || n.kind() == "enum_name" { - assert!(NodeKind::is_userdefined(&n)); - } else { - assert!(!NodeKind::is_userdefined(&n)); - } - if cursor.goto_first_child() { - continue; - } - if !cursor.goto_next_sibling() { - break; - } - } - } - - #[test] - fn test_is_renameable() { - let tree = parse_proto( - "message Foo { string bar = 1; } enum E { X = 0; } service S { rpc F(Empty) returns (Empty); }", - ); - let mut cursor = tree.root_node().walk(); - loop { - let n = cursor.node(); - match n.kind() { - "message_name" - | "enum_name" - | "service_name" - | "rpc_name" - | "message_or_enum_type" - | "field" - | "map_field" - | "oneof_field" - | "oneof" - | "enum_field" => { - assert!( - NodeKind::is_renameable(&n), - "expected {} to be renameable", - n.kind() - ); - } - _ => { - assert!( - !NodeKind::is_renameable(&n), - "expected {} to NOT be renameable", - n.kind() - ); - } - } - if cursor.goto_first_child() { - continue; - } - if !cursor.goto_next_sibling() { - break; - } - } - } - - #[test] - fn test_is_field_decl_parent() { - let tree = parse_proto( - "message Foo { string a = 1; map m = 2; oneof o { string b = 3; } } enum E { X = 0; }", - ); - let mut cursor = tree.root_node().walk(); - loop { - let n = cursor.node(); - match n.kind() { - "field" | "map_field" | "oneof_field" | "oneof" | "enum_field" => { - assert!(NodeKind::is_field_decl_parent(&n)); - } - _ => { - assert!(!NodeKind::is_field_decl_parent(&n)); - } - } - if cursor.goto_first_child() { - continue; - } - if !cursor.goto_next_sibling() { - break; - } - } - } - - #[test] - fn test_is_actionable() { - let tree = parse_proto( - "message Foo { string bar = 1; } service S { rpc F(Empty) returns (Empty); }", - ); - let mut cursor = tree.root_node().walk(); - loop { - let n = cursor.node(); - match n.kind() { - "message_name" - | "enum_name" - | "message_or_enum_type" - | "full_ident" - | "service_name" - | "rpc_name" => { - assert!(NodeKind::is_actionable(&n)); - } - _ => { - assert!(!NodeKind::is_actionable(&n)); - } - } - if cursor.goto_first_child() { - continue; - } - if !cursor.goto_next_sibling() { - break; - } - } - } - - #[test] - fn test_to_symbolkind() { - let tree = parse_proto("message Foo {} enum Bar { X = 0; } syntax = \"proto3\";"); - let mut cursor = tree.root_node().walk(); - loop { - let n = cursor.node(); - match n.kind() { - "message_name" => assert_eq!(NodeKind::to_symbolkind(&n), SymbolKind::STRUCT), - "enum_name" => assert_eq!(NodeKind::to_symbolkind(&n), SymbolKind::ENUM), - _ => assert_eq!(NodeKind::to_symbolkind(&n), SymbolKind::NULL), - } - if cursor.goto_first_child() { - continue; - } - if !cursor.goto_next_sibling() { - break; - } - } - } - - #[test] - fn test_as_str() { - assert_eq!(NodeKind::Identifier.as_str(), "identifier"); - assert_eq!(NodeKind::Error.as_str(), "ERROR"); - assert_eq!(NodeKind::MessageName.as_str(), "message_name"); - assert_eq!(NodeKind::Message.as_str(), "message"); - assert_eq!(NodeKind::EnumName.as_str(), "enum_name"); - assert_eq!(NodeKind::FieldName.as_str(), "message_or_enum_type"); - assert_eq!(NodeKind::ServiceName.as_str(), "service_name"); - assert_eq!(NodeKind::RpcName.as_str(), "rpc_name"); - assert_eq!(NodeKind::PackageName.as_str(), "full_ident"); - assert_eq!(NodeKind::PackageImport.as_str(), "import"); - } -} diff --git a/src/parser/definition.rs b/src/parser/definition.rs deleted file mode 100644 index 8f992a3..0000000 --- a/src/parser/definition.rs +++ /dev/null @@ -1,71 +0,0 @@ -use async_lsp::lsp_types::Location; -use tree_sitter::Node; - -use crate::{nodekind::NodeKind, utils::to_lsp_range}; - -use super::ParsedTree; - -impl ParsedTree { - pub fn definition(&self, identifier: &str, content: impl AsRef<[u8]>) -> Vec { - let mut results = vec![]; - self.definition_impl(identifier, self.tree.root_node(), &mut results, content); - results - } - - fn definition_impl( - &self, - identifier: &str, - n: Node, - v: &mut Vec, - content: impl AsRef<[u8]>, - ) { - if identifier.is_empty() { - return; - } - - if let Some((parent_identifier, remaining)) = identifier.split_once('.') { - let child_node = Self::find_all_nodes_from(n, NodeKind::is_userdefined) - .into_iter() - .find(|n| { - n.utf8_text(content.as_ref()).expect("utf8-parse error") == parent_identifier - }) - .and_then(|n| n.parent()); - - if let Some(inner) = child_node { - self.definition_impl(remaining, inner, v, content); - } - } else { - let locations: Vec = Self::find_all_nodes_from(n, NodeKind::is_userdefined) - .into_iter() - .filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == identifier) - .map(|n| Location { - uri: self.uri.clone(), - range: to_lsp_range(n), - }) - .collect(); - - v.extend(locations); - } - } -} - -#[cfg(test)] -mod test { - use async_lsp::lsp_types::Url; - use insta::assert_yaml_snapshot; - - use crate::parser::ProtoParser; - use crate::utils::compile_test_query; - - #[test] - fn test_goto_definition() { - let url: Url = "file://foo/bar.proto".parse().unwrap(); - let contents = include_str!("input/test_goto_definition.proto"); - let parsed = ProtoParser::new().parse(url, contents, &compile_test_query()); - - assert!(parsed.is_some()); - let tree = parsed.unwrap(); - assert_yaml_snapshot!(tree.definition("Author", contents)); - assert_yaml_snapshot!(tree.definition("", contents)); - } -} diff --git a/src/parser/rename.rs b/src/parser/rename.rs deleted file mode 100644 index 56d8148..0000000 --- a/src/parser/rename.rs +++ /dev/null @@ -1,656 +0,0 @@ -use async_lsp::lsp_types::{Location, Position, Range, TextEdit}; -use tree_sitter::Node; - -use crate::{nodekind::NodeKind, utils::to_lsp_range}; - -use super::ParsedTree; - -impl ParsedTree { - pub fn can_rename(&self, pos: Position) -> Option { - self.get_node_at_position(pos) - .filter(NodeKind::is_identifier) - .and_then(|n| { - if let Some(parent) = n.parent() - && NodeKind::is_renameable(&parent) - { - Some(to_lsp_range(n)) - } else { - None - } - }) - } - - /// When the cursor is on a type-reference identifier (inside a - /// `message_or_enum_type` node), return the partial qualified path up to - /// and including the segment under cursor. This is the identifier whose - /// declaration the rename should pivot to. - /// - /// For `Outer.Inner` with cursor on `Inner`, returns `Some("Outer.Inner")`. - /// For `Outer.Inner` with cursor on `Outer`, returns `Some("Outer")`. - /// For a non-reference position, returns `None`. - /// - /// The reconstruction walks every child of `message_or_enum_type` (named - /// *and* anonymous) and concatenates their `utf8_text`. This depends on - /// tree-sitter-proto emitting the `.` separators as anonymous children - /// whose `utf8_text` is literally `"."`, and on the grammar leaving no - /// whitespace between identifier tokens. Both invariants are exercised by - /// `test_rename_pivot_identifier_qualified`, which asserts the exact - /// `"Book.Author"` reconstruction — a grammar change that violated either - /// invariant would fail that test rather than silently produce e.g. - /// `"BookAuthor"`. - pub fn rename_pivot_identifier( - &self, - pos: Position, - content: impl AsRef<[u8]>, - ) -> Option { - let n = self.get_node_at_position(pos)?; - if !NodeKind::is_identifier(&n) { - return None; - } - let parent = n.parent()?; - if !NodeKind::is_field_name(&parent) { - return None; - } - - let cursor_end = n.end_byte(); - let bytes = content.as_ref(); - let mut path = String::new(); - let mut cursor = parent.walk(); - for child in parent.children(&mut cursor) { - let text = child.utf8_text(bytes).ok()?; - path.push_str(text); - if child.end_byte() >= cursor_end { - break; - } - } - Some(path) - } - - /// If the given position is on the rpc name of an rpc declaration, returns - /// the rpc's name along with its declared request and response type texts. - /// Used to drive the rpc/request/response chained rename. - pub fn rpc_at_position( - &self, - pos: Position, - content: impl AsRef<[u8]>, - ) -> Option<(String, String, String)> { - let n = self.get_node_at_position(pos)?; - if !NodeKind::is_identifier(&n) { - return None; - } - let parent = n.parent()?; - if parent.kind() != NodeKind::RpcName.as_str() { - return None; - } - let rpc = parent.parent()?; - let bytes = content.as_ref(); - let mut cursor = rpc.walk(); - let mut types = rpc - .children(&mut cursor) - .filter(|c| c.kind() == NodeKind::FieldName.as_str()); - let request = types.next()?.utf8_text(bytes).ok()?.to_owned(); - let response = types.next()?.utf8_text(bytes).ok()?.to_owned(); - let rpc_name = n.utf8_text(bytes).ok()?.to_owned(); - Some((rpc_name, request, response)) - } - - /// If the given position is on a message name, returns that name. - pub fn message_name_at_position( - &self, - pos: Position, - content: impl AsRef<[u8]>, - ) -> Option { - let n = self.get_node_at_position(pos)?; - if !NodeKind::is_identifier(&n) { - return None; - } - let parent = n.parent()?; - if parent.kind() != NodeKind::MessageName.as_str() { - return None; - } - Some(n.utf8_text(content.as_ref()).ok()?.to_owned()) - } - - /// Returns the (request, response) type texts for every `rpc` node in this - /// tree. Used to verify that a request/response type is uniquely used by a - /// single rpc before chain-renaming it. - pub fn all_rpc_signatures(&self, content: impl AsRef<[u8]>) -> Vec<(String, String)> { - let bytes = content.as_ref(); - let mut out = vec![]; - for rpc in self.find_all_nodes(|n: &Node| n.kind() == "rpc") { - let mut cursor = rpc.walk(); - let mut types = rpc - .children(&mut cursor) - .filter(|c| c.kind() == NodeKind::FieldName.as_str()); - let Some(req) = types.next().and_then(|c| c.utf8_text(bytes).ok()) else { - continue; - }; - let Some(resp) = types.next().and_then(|c| c.utf8_text(bytes).ok()) else { - continue; - }; - out.push((req.to_owned(), resp.to_owned())); - } - out - } - - fn nodes_within<'a>( - n: Node<'a>, - identifier: &str, - content: impl AsRef<[u8]>, - ) -> Option>> { - n.parent().map(|p| { - Self::find_all_nodes_from(p, NodeKind::is_field_name) - .into_iter() - .filter(|i| i.utf8_text(content.as_ref()).expect("utf-8 parse error") == identifier) - .collect() - }) - } - - pub fn reference_tree( - &self, - pos: Position, - content: impl AsRef<[u8]>, - ) -> Option<(Vec, String)> { - let rename_range = self.can_rename(pos)?; - - let mut res = vec![Location { - uri: self.uri.clone(), - range: rename_range, - }]; - - let nodes = self.get_ancestor_nodes_at_position(pos); - let mut i = 1; - let mut otext = nodes.first()?.utf8_text(content.as_ref()).ok()?.to_owned(); - while nodes.len() > i { - let id = nodes[i].utf8_text(content.as_ref()).ok()?; - if let Some(inodes) = Self::nodes_within(nodes[i], &otext, content.as_ref()) { - res.extend(inodes.into_iter().map(|n| Location { - uri: self.uri.clone(), - range: to_lsp_range(n), - })); - } - otext = format!("{id}.{otext}"); - i += 1; - } - Some((res, otext)) - } - - pub fn rename_tree( - &self, - pos: Position, - new_name: &str, - content: impl AsRef<[u8]>, - ) -> Option<(Vec, String, String)> { - let rename_range = self.can_rename(pos)?; - - let mut v = vec![TextEdit { - range: rename_range, - new_text: new_name.to_owned(), - }]; - - let nodes = self.get_ancestor_nodes_at_position(pos); - - // Renameable symbols with no message ancestor: top-level enums, services, RPCs, - // and the various field-like declarations (regular fields, map fields, oneof, - // oneof fields, enum values). Only top-level enums are referenced as types - // from other files; the rest are single-site, so we hand the workspace pass - // a name it won't find — making it a harmless no-op without risking that a - // field named the same as some lowercase type triggers an unwanted rename. - if nodes.is_empty() { - let n = self.get_node_at_position(pos)?; - let identifier = n.utf8_text(content.as_ref()).ok()?.to_owned(); - let is_type_symbol = n - .parent() - .is_some_and(|p| p.kind() == NodeKind::EnumName.as_str()); - let (otext, ntext) = if is_type_symbol { - (identifier, new_name.to_owned()) - } else { - (new_name.to_owned(), new_name.to_owned()) - }; - return Some((v, otext, ntext)); - } - - let mut i = 1; - let mut otext = nodes.first()?.utf8_text(content.as_ref()).ok()?.to_owned(); - let mut ntext = new_name.to_owned(); - - while nodes.len() > i { - let id = nodes[i].utf8_text(content.as_ref()).ok()?; - - if let Some(inodes) = Self::nodes_within(nodes[i], &otext, content.as_ref()) { - v.extend(inodes.into_iter().map(|n| TextEdit { - range: to_lsp_range(n), - new_text: ntext.clone(), - })); - } - - otext = format!("{id}.{otext}"); - ntext = format!("{id}.{ntext}"); - - i += 1; - } - - Some((v, otext, ntext)) - } - - pub fn rename_field( - &self, - old_identifier: &str, - new_identifier: &str, - content: impl AsRef<[u8]>, - ) -> Vec { - self.find_all_nodes(NodeKind::is_field_name) - .into_iter() - .filter(|n| { - let ntext = n.utf8_text(content.as_ref()).expect("utf-8 parse error"); - let sc = format!("{old_identifier}."); - ntext == old_identifier || ntext.starts_with(&sc) - }) - .map(|n| { - let text = n.utf8_text(content.as_ref()).expect("utf-8 parse error"); - TextEdit { - new_text: text.replace(old_identifier, new_identifier), - range: to_lsp_range(n), - } - }) - .collect() - } - - pub fn reference_field(&self, id: &str, content: impl AsRef<[u8]>) -> Vec { - self.find_all_nodes(NodeKind::is_field_name) - .into_iter() - .filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == id) - .map(|n| Location { - uri: self.uri.clone(), - range: to_lsp_range(n), - }) - .collect() - } -} - -#[cfg(test)] -mod test { - use async_lsp::lsp_types::{Position, Url}; - use insta::assert_yaml_snapshot; - - use crate::parser::ProtoParser; - use crate::utils::compile_test_query; - - #[test] - fn test_rename() { - let uri: Url = "file://foo/bar.proto".parse().unwrap(); - let pos_book = Position { - line: 5, - character: 9, - }; - let pos_author = Position { - line: 11, - character: 14, - }; - let pos_non_rename = Position { - line: 21, - character: 5, - }; - let contents = include_str!("input/test_rename.proto"); - - let parsed = ProtoParser::new().parse(uri, contents, &compile_test_query()); - assert!(parsed.is_some()); - let tree = parsed.unwrap(); - - let rename_fn = |nt: &str, pos: Position| match tree.rename_tree(pos, nt, contents) { - Some(k) => { - let mut v = tree.rename_field(&k.1, &k.2, contents); - v.extend(k.0); - v - } - _ => { - vec![] - } - }; - - assert_yaml_snapshot!(rename_fn("Kitab", pos_book)); - assert_yaml_snapshot!(rename_fn("Writer", pos_author)); - assert_yaml_snapshot!(rename_fn("xyx", pos_non_rename)); - } - - #[test] - fn test_reference() { - let uri: Url = "file://foo/bar.proto".parse().unwrap(); - let pos_book = Position { - line: 5, - character: 9, - }; - let pos_author = Position { - line: 11, - character: 14, - }; - let pos_non_ref = Position { - line: 21, - character: 5, - }; - let contents = include_str!("input/test_reference.proto"); - - let parsed = ProtoParser::new().parse(uri, contents, &compile_test_query()); - assert!(parsed.is_some()); - let tree = parsed.unwrap(); - - let reference_fn = |pos: Position| match tree.reference_tree(pos, contents) { - Some(k) => { - let mut v = tree.reference_field(&k.1, contents); - v.extend(k.0); - v - } - _ => { - vec![] - } - }; - - assert_yaml_snapshot!(reference_fn(pos_book)); - assert_yaml_snapshot!(reference_fn(pos_author)); - assert_yaml_snapshot!(reference_fn(pos_non_ref)); - } - - #[test] - fn test_can_rename() { - let uri: Url = "file://foo/bar/test.proto".parse().unwrap(); - let pos_rename = Position { - line: 5, - character: 9, - }; - let pos_non_rename = Position { - line: 2, - character: 2, - }; - let pos_inner_type = Position { - line: 19, - character: 11, - }; - let pos_outer_type = Position { - line: 19, - character: 5, - }; - - let contents = include_str!("input/test_can_rename.proto"); - - let parsed = ProtoParser::new().parse(uri, contents, &compile_test_query()); - assert!(parsed.is_some()); - - let tree = parsed.unwrap(); - assert_yaml_snapshot!(tree.can_rename(pos_rename)); - assert_yaml_snapshot!(tree.can_rename(pos_non_rename)); - assert_yaml_snapshot!(tree.can_rename(pos_inner_type)); - assert_yaml_snapshot!(tree.can_rename(pos_outer_type)); - } - - #[test] - fn test_can_rename_service_and_rpc() { - let uri: Url = "file://foo/bar/test.proto".parse().unwrap(); - let contents = include_str!("input/test_rename_service.proto"); - let parsed = ProtoParser::new().parse(uri, contents, &compile_test_query()); - assert!(parsed.is_some()); - let tree = parsed.unwrap(); - - let pos_service = Position { - line: 10, - character: 10, - }; - let pos_rpc = Position { - line: 11, - character: 9, - }; - let pos_rpc_request_type = Position { - line: 11, - character: 17, - }; - - assert_yaml_snapshot!(tree.can_rename(pos_service)); - assert_yaml_snapshot!(tree.can_rename(pos_rpc)); - // Type references inside an RPC declaration are renameable from the - // reference site; the LSP layer pivots to the declaration. - assert_yaml_snapshot!(tree.can_rename(pos_rpc_request_type)); - } - - #[test] - fn test_rename_service_and_rpc() { - let uri: Url = "file://foo/bar.proto".parse().unwrap(); - let contents = include_str!("input/test_rename_service.proto"); - let parsed = ProtoParser::new().parse(uri, contents, &compile_test_query()); - assert!(parsed.is_some()); - let tree = parsed.unwrap(); - - let pos_service = Position { - line: 10, - character: 10, - }; - let pos_rpc = Position { - line: 11, - character: 9, - }; - - let rename_fn = |nt: &str, pos: Position| match tree.rename_tree(pos, nt, contents) { - Some(k) => { - let mut v = tree.rename_field(&k.1, &k.2, contents); - v.extend(k.0); - v - } - _ => vec![], - }; - - assert_yaml_snapshot!(rename_fn("Catalog", pos_service)); - assert_yaml_snapshot!(rename_fn("FetchBook", pos_rpc)); - } - - #[test] - fn test_rename_pivot_identifier() { - let uri: Url = "file://foo/bar/test.proto".parse().unwrap(); - let contents = include_str!("input/test_rename_service.proto"); - let parsed = ProtoParser::new() - .parse(uri, contents, &compile_test_query()) - .unwrap(); - - // `Empty` reference at line 11 (the rpc Get(Empty) ...): char 16 = 'E' - let pos_unqualified_ref = Position { - line: 11, - character: 17, - }; - // `Book` return type at line 11 char 32..36 - let pos_other_ref = Position { - line: 11, - character: 33, - }; - // Service declaration site — not a reference, so no pivot needed - let pos_decl = Position { - line: 10, - character: 10, - }; - - assert_eq!( - parsed.rename_pivot_identifier(pos_unqualified_ref, contents), - Some("Empty".to_owned()) - ); - assert_eq!( - parsed.rename_pivot_identifier(pos_other_ref, contents), - Some("Book".to_owned()) - ); - assert_eq!(parsed.rename_pivot_identifier(pos_decl, contents), None); - } - - #[test] - fn test_rpc_at_position_and_signatures() { - let uri: Url = "file://foo/bar.proto".parse().unwrap(); - let contents = include_str!("input/test_rename_service.proto"); - let parsed = ProtoParser::new() - .parse(uri, contents, &compile_test_query()) - .unwrap(); - - // `GetBook` rpc at line 11 chars 8..15 - let pos_rpc = Position { - line: 11, - character: 10, - }; - assert_eq!( - parsed.rpc_at_position(pos_rpc, contents), - Some(("GetBook".to_owned(), "Empty".to_owned(), "Book".to_owned(),)), - ); - - // Cursor on a non-rpc identifier should return None. - let pos_service = Position { - line: 10, - character: 10, - }; - assert_eq!(parsed.rpc_at_position(pos_service, contents), None); - - // all_rpc_signatures should pick up both rpcs in the file. - let sigs = parsed.all_rpc_signatures(contents); - assert_eq!( - sigs, - vec![ - ("Empty".to_owned(), "Book".to_owned()), - ("Empty".to_owned(), "Book".to_owned()), - ] - ); - } - - #[test] - fn test_message_name_at_position() { - let uri: Url = "file://foo/bar.proto".parse().unwrap(); - let contents = include_str!("input/test_rename_service.proto"); - let parsed = ProtoParser::new() - .parse(uri, contents, &compile_test_query()) - .unwrap(); - - // `Book` declaration at line 6 chars 8..12 - let pos = Position { - line: 6, - character: 9, - }; - assert_eq!( - parsed.message_name_at_position(pos, contents), - Some("Book".to_owned()) - ); - - // RPC name shouldn't match. - let pos_rpc = Position { - line: 11, - character: 10, - }; - assert_eq!(parsed.message_name_at_position(pos_rpc, contents), None); - } - - #[test] - fn test_rename_field_and_enum_value() { - let uri: Url = "file://foo/bar.proto".parse().unwrap(); - let contents = include_str!("input/test_rename_field.proto"); - let parsed = ProtoParser::new() - .parse(uri, contents, &compile_test_query()) - .unwrap(); - let tree = parsed; - - let rename_fn = |nt: &str, pos: Position| match tree.rename_tree(pos, nt, contents) { - Some(k) => { - let mut v = tree.rename_field(&k.1, &k.2, contents); - v.extend(k.0); - v - } - _ => vec![], - }; - - // Enum value: RED at line 5 chars 4..7 - let pos_enum_value = Position { - line: 5, - character: 5, - }; - // Plain field: title at line 14 chars 11..16 - let pos_plain_field = Position { - line: 14, - character: 12, - }; - // User-type field: author at line 15 chars 11..17 - let pos_user_type_field = Position { - line: 15, - character: 12, - }; - // Map field: counts at line 16 chars 23..29 - let pos_map_field = Position { - line: 16, - character: 24, - }; - // Oneof name: body at line 17 chars 10..14 - let pos_oneof_name = Position { - line: 17, - character: 11, - }; - // Oneof field: text at line 18 chars 15..19 - let pos_oneof_field = Position { - line: 18, - character: 16, - }; - - assert_yaml_snapshot!(rename_fn("CRIMSON", pos_enum_value)); - assert_yaml_snapshot!(rename_fn("name", pos_plain_field)); - assert_yaml_snapshot!(rename_fn("writer", pos_user_type_field)); - assert_yaml_snapshot!(rename_fn("tallies", pos_map_field)); - assert_yaml_snapshot!(rename_fn("content", pos_oneof_name)); - assert_yaml_snapshot!(rename_fn("words", pos_oneof_field)); - } - - #[test] - fn test_can_rename_field_and_enum_value() { - let uri: Url = "file://foo/bar.proto".parse().unwrap(); - let contents = include_str!("input/test_rename_field.proto"); - let parsed = ProtoParser::new() - .parse(uri, contents, &compile_test_query()) - .unwrap(); - - // Cursor on a type identifier inside a field (`Author` at line 15 - // chars 4..10) is a reference site — already supported. - let pos_type_ref = Position { - line: 15, - character: 6, - }; - // Cursor on the field name should now be renameable. - let pos_plain_field = Position { - line: 14, - character: 12, - }; - // Cursor on the int_lit `1` is not a renameable identifier. - let pos_field_number = Position { - line: 14, - character: 19, - }; - - assert_yaml_snapshot!(parsed.can_rename(pos_type_ref)); - assert_yaml_snapshot!(parsed.can_rename(pos_plain_field)); - assert_yaml_snapshot!(parsed.can_rename(pos_field_number)); - } - - #[test] - fn test_rename_pivot_identifier_qualified() { - // `Book.Author a = 1;` at line 19 of test_can_rename.proto - let uri: Url = "file://foo/bar/test.proto".parse().unwrap(); - let contents = include_str!("input/test_can_rename.proto"); - let parsed = ProtoParser::new() - .parse(uri, contents, &compile_test_query()) - .unwrap(); - - // Cursor on `Book` (the outer segment): chars 4..8 - let pos_outer = Position { - line: 19, - character: 5, - }; - // Cursor on `Author` (the inner segment): chars 9..15 - let pos_inner = Position { - line: 19, - character: 11, - }; - - assert_eq!( - parsed.rename_pivot_identifier(pos_outer, contents), - Some("Book".to_owned()) - ); - assert_eq!( - parsed.rename_pivot_identifier(pos_inner, contents), - Some("Book.Author".to_owned()) - ); - } -} diff --git a/src/parser/snapshots/protols__parser__definition__test__goto_definition-2.snap b/src/parser/snapshots/protols__parser__definition__test__goto_definition-2.snap deleted file mode 100644 index 5b0d62f..0000000 --- a/src/parser/snapshots/protols__parser__definition__test__goto_definition-2.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/parser/definition.rs -expression: "tree.definition(&posinvalid, contents)" ---- -[] diff --git a/src/parser/snapshots/protols__parser__definition__test__goto_definition.snap b/src/parser/snapshots/protols__parser__definition__test__goto_definition.snap deleted file mode 100644 index 3aba38a..0000000 --- a/src/parser/snapshots/protols__parser__definition__test__goto_definition.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/parser/definition.rs -expression: "tree.definition(&posauthor, contents)" ---- -- uri: "file://foo/bar.proto" - range: - start: - line: 5 - character: 12 - end: - line: 5 - character: 18 diff --git a/src/parser/snapshots/protols__parser__rename__test__can_rename-2.snap b/src/parser/snapshots/protols__parser__rename__test__can_rename-2.snap deleted file mode 100644 index 4683ffb..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__can_rename-2.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/parser/rename.rs -expression: tree.can_rename(&pos_non_rename) ---- -~ diff --git a/src/parser/snapshots/protols__parser__rename__test__can_rename-3.snap b/src/parser/snapshots/protols__parser__rename__test__can_rename-3.snap deleted file mode 100644 index 25a8302..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__can_rename-3.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/parser/rename.rs -expression: tree.can_rename(&pos_inner_type) ---- -start: - line: 19 - character: 9 -end: - line: 19 - character: 15 diff --git a/src/parser/snapshots/protols__parser__rename__test__can_rename-4.snap b/src/parser/snapshots/protols__parser__rename__test__can_rename-4.snap deleted file mode 100644 index 491df1e..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__can_rename-4.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/parser/rename.rs -expression: tree.can_rename(&pos_outer_type) ---- -start: - line: 19 - character: 4 -end: - line: 19 - character: 8 diff --git a/src/parser/snapshots/protols__parser__rename__test__can_rename_field_and_enum_value-2.snap b/src/parser/snapshots/protols__parser__rename__test__can_rename_field_and_enum_value-2.snap deleted file mode 100644 index d52c7fc..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__can_rename_field_and_enum_value-2.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/parser/rename.rs -expression: parsed.can_rename(&pos_plain_field) ---- -start: - line: 14 - character: 11 -end: - line: 14 - character: 16 diff --git a/src/parser/snapshots/protols__parser__rename__test__can_rename_field_and_enum_value-3.snap b/src/parser/snapshots/protols__parser__rename__test__can_rename_field_and_enum_value-3.snap deleted file mode 100644 index 4943cbd..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__can_rename_field_and_enum_value-3.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/parser/rename.rs -expression: parsed.can_rename(&pos_field_number) ---- -~ diff --git a/src/parser/snapshots/protols__parser__rename__test__can_rename_field_and_enum_value.snap b/src/parser/snapshots/protols__parser__rename__test__can_rename_field_and_enum_value.snap deleted file mode 100644 index f5227ad..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__can_rename_field_and_enum_value.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/parser/rename.rs -expression: parsed.can_rename(&pos_type_ref) ---- -start: - line: 15 - character: 4 -end: - line: 15 - character: 10 diff --git a/src/parser/snapshots/protols__parser__rename__test__can_rename_service_and_rpc-3.snap b/src/parser/snapshots/protols__parser__rename__test__can_rename_service_and_rpc-3.snap deleted file mode 100644 index 27f212b..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__can_rename_service_and_rpc-3.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/parser/rename.rs -expression: tree.can_rename(&pos_rpc_request_type) ---- -start: - line: 11 - character: 16 -end: - line: 11 - character: 21 diff --git a/src/parser/snapshots/protols__parser__rename__test__reference-2.snap b/src/parser/snapshots/protols__parser__rename__test__reference-2.snap deleted file mode 100644 index e7d5b9e..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__reference-2.snap +++ /dev/null @@ -1,36 +0,0 @@ ---- -source: src/parser/rename.rs -expression: reference_fn(&pos_author) ---- -- uri: "file://foo/bar.proto" - range: - start: - line: 23 - character: 2 - end: - line: 23 - character: 13 -- uri: "file://foo/bar.proto" - range: - start: - line: 29 - character: 32 - end: - line: 29 - character: 43 -- uri: "file://foo/bar.proto" - range: - start: - line: 11 - character: 10 - end: - line: 11 - character: 16 -- uri: "file://foo/bar.proto" - range: - start: - line: 15 - character: 2 - end: - line: 15 - character: 8 diff --git a/src/parser/snapshots/protols__parser__rename__test__reference-3.snap b/src/parser/snapshots/protols__parser__rename__test__reference-3.snap deleted file mode 100644 index cb88ea3..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__reference-3.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/parser/rename.rs -expression: reference_fn(&pos_non_ref) ---- -[] diff --git a/src/parser/snapshots/protols__parser__rename__test__reference.snap b/src/parser/snapshots/protols__parser__rename__test__reference.snap deleted file mode 100644 index 90b752d..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__reference.snap +++ /dev/null @@ -1,28 +0,0 @@ ---- -source: src/parser/rename.rs -expression: reference_fn(&pos_book) ---- -- uri: "file://foo/bar.proto" - range: - start: - line: 22 - character: 11 - end: - line: 22 - character: 15 -- uri: "file://foo/bar.proto" - range: - start: - line: 28 - character: 30 - end: - line: 28 - character: 34 -- uri: "file://foo/bar.proto" - range: - start: - line: 5 - character: 8 - end: - line: 5 - character: 12 diff --git a/src/parser/snapshots/protols__parser__rename__test__rename-2.snap b/src/parser/snapshots/protols__parser__rename__test__rename-2.snap deleted file mode 100644 index ba767a2..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__rename-2.snap +++ /dev/null @@ -1,36 +0,0 @@ ---- -source: src/parser/rename.rs -expression: "rename_fn(\"Writer\", &pos_author)" ---- -- range: - start: - line: 23 - character: 4 - end: - line: 23 - character: 15 - newText: Book.Writer -- range: - start: - line: 29 - character: 34 - end: - line: 29 - character: 45 - newText: Book.Writer -- range: - start: - line: 11 - character: 12 - end: - line: 11 - character: 18 - newText: Writer -- range: - start: - line: 15 - character: 4 - end: - line: 15 - character: 10 - newText: Writer diff --git a/src/parser/snapshots/protols__parser__rename__test__rename-3.snap b/src/parser/snapshots/protols__parser__rename__test__rename-3.snap deleted file mode 100644 index 50e431e..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__rename-3.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/parser/rename.rs -expression: "rename_fn(\"xyx\", &pos_non_rename)" ---- -[] diff --git a/src/parser/snapshots/protols__parser__rename__test__rename.snap b/src/parser/snapshots/protols__parser__rename__test__rename.snap deleted file mode 100644 index 8f05c52..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__rename.snap +++ /dev/null @@ -1,44 +0,0 @@ ---- -source: src/parser/rename.rs -expression: "rename_fn(\"Kitab\", &pos_book)" ---- -- range: - start: - line: 22 - character: 13 - end: - line: 22 - character: 17 - newText: Kitab -- range: - start: - line: 23 - character: 4 - end: - line: 23 - character: 15 - newText: Kitab.Author -- range: - start: - line: 28 - character: 32 - end: - line: 28 - character: 36 - newText: Kitab -- range: - start: - line: 29 - character: 34 - end: - line: 29 - character: 45 - newText: Kitab.Author -- range: - start: - line: 5 - character: 8 - end: - line: 5 - character: 12 - newText: Kitab diff --git a/src/parser/snapshots/protols__parser__rename__test__rename_field_and_enum_value-2.snap b/src/parser/snapshots/protols__parser__rename__test__rename_field_and_enum_value-2.snap deleted file mode 100644 index 19d2434..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__rename_field_and_enum_value-2.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/parser/rename.rs -expression: "rename_fn(\"name\", &pos_plain_field)" ---- -- range: - start: - line: 14 - character: 11 - end: - line: 14 - character: 16 - newText: name diff --git a/src/parser/snapshots/protols__parser__rename__test__rename_field_and_enum_value-3.snap b/src/parser/snapshots/protols__parser__rename__test__rename_field_and_enum_value-3.snap deleted file mode 100644 index 0154bca..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__rename_field_and_enum_value-3.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/parser/rename.rs -expression: "rename_fn(\"writer\", &pos_user_type_field)" ---- -- range: - start: - line: 15 - character: 11 - end: - line: 15 - character: 17 - newText: writer diff --git a/src/parser/snapshots/protols__parser__rename__test__rename_field_and_enum_value-4.snap b/src/parser/snapshots/protols__parser__rename__test__rename_field_and_enum_value-4.snap deleted file mode 100644 index 2b01e20..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__rename_field_and_enum_value-4.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/parser/rename.rs -expression: "rename_fn(\"tallies\", &pos_map_field)" ---- -- range: - start: - line: 16 - character: 23 - end: - line: 16 - character: 29 - newText: tallies diff --git a/src/parser/snapshots/protols__parser__rename__test__rename_field_and_enum_value-5.snap b/src/parser/snapshots/protols__parser__rename__test__rename_field_and_enum_value-5.snap deleted file mode 100644 index 47afe03..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__rename_field_and_enum_value-5.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/parser/rename.rs -expression: "rename_fn(\"content\", &pos_oneof_name)" ---- -- range: - start: - line: 17 - character: 10 - end: - line: 17 - character: 14 - newText: content diff --git a/src/parser/snapshots/protols__parser__rename__test__rename_field_and_enum_value-6.snap b/src/parser/snapshots/protols__parser__rename__test__rename_field_and_enum_value-6.snap deleted file mode 100644 index e36760e..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__rename_field_and_enum_value-6.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/parser/rename.rs -expression: "rename_fn(\"words\", &pos_oneof_field)" ---- -- range: - start: - line: 18 - character: 15 - end: - line: 18 - character: 19 - newText: words diff --git a/src/parser/snapshots/protols__parser__rename__test__rename_field_and_enum_value.snap b/src/parser/snapshots/protols__parser__rename__test__rename_field_and_enum_value.snap deleted file mode 100644 index faaf177..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__rename_field_and_enum_value.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/parser/rename.rs -expression: "rename_fn(\"CRIMSON\", &pos_enum_value)" ---- -- range: - start: - line: 5 - character: 4 - end: - line: 5 - character: 7 - newText: CRIMSON diff --git a/src/parser/snapshots/protols__parser__rename__test__rename_service_and_rpc-2.snap b/src/parser/snapshots/protols__parser__rename__test__rename_service_and_rpc-2.snap deleted file mode 100644 index 6359cbc..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__rename_service_and_rpc-2.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/parser/rename.rs -expression: "rename_fn(\"FetchBook\", &pos_rpc)" ---- -- range: - start: - line: 11 - character: 8 - end: - line: 11 - character: 15 - newText: FetchBook diff --git a/src/parser/snapshots/protols__parser__rename__test__rename_service_and_rpc.snap b/src/parser/snapshots/protols__parser__rename__test__rename_service_and_rpc.snap deleted file mode 100644 index b69ea55..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__rename_service_and_rpc.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/parser/rename.rs -expression: "rename_fn(\"Catalog\", &pos_service)" ---- -- range: - start: - line: 10 - character: 8 - end: - line: 10 - character: 15 - newText: Catalog diff --git a/src/parser/tree.rs b/src/parser/tree.rs deleted file mode 100644 index 5036547..0000000 --- a/src/parser/tree.rs +++ /dev/null @@ -1,198 +0,0 @@ -use async_lsp::lsp_types::{Position, Range}; -use tree_sitter::{Node, TreeCursor}; - -use crate::{ - context::jumpable::Jumpable, - nodekind::NodeKind, - utils::{to_lsp_range, to_ts_point}, -}; - -use super::ParsedTree; - -impl ParsedTree { - pub(super) fn walk_and_filter<'a>( - cursor: &mut TreeCursor<'a>, - f: fn(&Node) -> bool, - early: bool, - ) -> Vec> { - let mut v = vec![]; - - loop { - let node = cursor.node(); - - if f(&node) { - v.push(node); - if early { - break; - } - } - - if cursor.goto_first_child() { - v.extend(Self::walk_and_filter(cursor, f, early)); - cursor.goto_parent(); - } - - if !cursor.goto_next_sibling() { - break; - } - } - - v - } - - pub fn get_user_defined_text<'a>( - &'a self, - pos: Position, - content: &'a [u8], - ) -> Option<&'a str> { - self.get_user_defined_node(pos) - .map(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error")) - } - - pub fn get_jumpable_at_position(&self, pos: Position, content: &[u8]) -> Option { - let n = self.get_node_at_position(pos)?; - - // If node is import path. return the whole path, removing the quotes - if n.parent().as_ref().is_some_and(NodeKind::is_import_path) { - return Some(Jumpable::Import( - n.utf8_text(content) - .expect("utf-8 parse error") - .trim_matches('"') - .to_string(), - )); - } - - // If node is user defined enum/message - if let Some(identifier) = self.get_user_defined_text(pos, content) { - return Some(Jumpable::Identifier(identifier.to_string())); - } - - None - } - - pub fn get_ancestor_nodes_at_position(&self, pos: Position) -> Vec> { - let Some(mut n) = self.get_user_defined_node(pos) else { - return vec![]; - }; - - let mut nodes = vec![]; - while let Some(p) = n.parent() { - if NodeKind::is_message(&p) { - for i in 0..p.child_count() { - let t = p.child(u32::try_from(i).unwrap()).unwrap(); - if NodeKind::is_message_name(&t) { - nodes.push(t); - } - } - } - n = p; - } - nodes - } - - pub fn get_user_defined_node(&self, pos: Position) -> Option> { - self.get_node_at_position(pos) - .and_then(|n| { - if NodeKind::is_actionable(&n) { - Some(n) - } else { - n.parent() - } - }) - .filter(NodeKind::is_actionable) - } - - pub fn get_node_at_position(&self, pos: Position) -> Option> { - let pos = to_ts_point(pos); - self.tree.root_node().descendant_for_point_range(pos, pos) - } - - pub fn find_all_nodes(&self, f: fn(&Node) -> bool) -> Vec> { - Self::find_all_nodes_from(self.tree.root_node(), f) - } - - pub fn find_all_nodes_from(n: Node<'_>, f: fn(&Node) -> bool) -> Vec> { - let mut cursor = n.walk(); - Self::walk_and_filter(&mut cursor, f, false) - } - - pub fn find_first_node(&self, f: fn(&Node) -> bool) -> Vec> { - Self::find_node_from(self.tree.root_node(), f) - } - - pub fn find_node_from(n: Node<'_>, f: fn(&Node) -> bool) -> Vec> { - let mut cursor = n.walk(); - Self::walk_and_filter(&mut cursor, f, true) - } - - pub fn get_package_name<'a>(&self, content: &'a [u8]) -> Option<&'a str> { - self.find_first_node(NodeKind::is_package_name) - .first() - .map(|n| n.utf8_text(content).expect("utf-8 parse error")) - } - - pub fn get_import_node(&self) -> Vec> { - self.find_all_nodes(NodeKind::is_import_path) - .into_iter() - .filter_map(|n| n.child_by_field_name("path")) - .collect() - } - - pub fn get_import_paths<'a>(&self, content: &'a [u8]) -> Vec<&'a str> { - self.get_import_node() - .into_iter() - .map(|n| { - n.utf8_text(content) - .expect("utf-8 parse error") - .trim_matches('"') - }) - .collect() - } - - pub fn get_import_path_range(&self, content: &[u8], import: &[&str]) -> Vec { - self.get_import_node() - .into_iter() - .filter(|n| { - let t = n - .utf8_text(content) - .expect("utf8-parse error") - .trim_matches('"'); - import.contains(&t) - }) - .map(to_lsp_range) - .collect() - } -} - -#[cfg(test)] -mod test { - use async_lsp::lsp_types::Url; - use insta::assert_yaml_snapshot; - - use crate::{nodekind::NodeKind, parser::ProtoParser, utils::compile_test_query}; - - #[test] - fn test_filter() { - let uri: Url = "file://foo/bar/test.proto".parse().unwrap(); - let contents = include_str!("input/test_filter.proto"); - let parsed = ProtoParser::new().parse(uri, contents, &compile_test_query()); - - assert!(parsed.is_some()); - let tree = parsed.unwrap(); - let nodes = tree.find_all_nodes(NodeKind::is_message_name); - - assert_eq!(nodes.len(), 2); - - let names: Vec<_> = nodes - .into_iter() - .map(|n| n.utf8_text(contents.as_ref()).unwrap()) - .collect(); - - assert_yaml_snapshot!(names); - - let package_name = tree.get_package_name(contents.as_ref()); - assert_yaml_snapshot!(package_name); - let imports = tree.get_import_paths(contents.as_ref()); - assert_yaml_snapshot!(imports); - } -} diff --git a/src/server.rs b/src/server.rs index 3115e24..880bbe2 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,7 +1,6 @@ use async_lsp::{ - ClientSocket, LanguageClient, + ClientSocket, lsp_types::{ - NumberOrString, ProgressParams, ProgressParamsValue, notification::{ DidChangeTextDocument, DidCreateFiles, DidDeleteFiles, DidOpenTextDocument, DidRenameFiles, DidSaveTextDocument, Exit, SetTrace, @@ -14,14 +13,9 @@ use async_lsp::{ }, router::Router, }; -use std::{ - ops::ControlFlow, - path::PathBuf, - sync::{mpsc, mpsc::Sender}, - thread, -}; +use std::{ops::ControlFlow, path::PathBuf}; -use crate::{config::workspace::WorkspaceProtoConfigs, log, state::ProtoLanguageState}; +use crate::{config::WorkspaceProtoConfigs, log, state::ProtoLanguageState}; pub struct TickEvent; pub struct ProtoLanguageServer { @@ -86,22 +80,4 @@ impl ProtoLanguageServer { router } - - pub fn with_report_progress(&self, token: NumberOrString) -> Sender { - let (tx, rx) = mpsc::channel(); - let mut socket = self.client.clone(); - - thread::spawn(move || { - while let Ok(value) = rx.recv() { - if let Err(e) = socket.progress(ProgressParams { - token: token.clone(), - value, - }) { - tracing::error!(error=%e, "failed to report parse progress"); - } - } - }); - - tx - } } diff --git a/src/state/definition.rs b/src/state/definition.rs new file mode 100644 index 0000000..0451b26 --- /dev/null +++ b/src/state/definition.rs @@ -0,0 +1,241 @@ +use std::path::PathBuf; + +use async_lsp::lsp_types::{Location, Position, Range, Url}; + +use crate::{ + model::{ElementKind, SpatialEntry}, + state::ProtoLanguageState, +}; + +impl ProtoLanguageState { + /// Resolves the target definition location(s) for the symbol under + /// `position`. + /// + /// The jump kind is inferred directly from the metamodel element at the + /// cursor: an `import` statement jumps to the imported file, while any + /// other symbol (or a type reference) is resolved to its declaration via + /// the shared cross-file name resolution engine. + pub fn definition(&self, uri: &Url, pos: Position, ipath: &[PathBuf]) -> Vec { + let Some(document) = self.get_document(uri) else { + return vec![]; + }; + let Some(SpatialEntry { element_id, .. }) = document.find_entry_at_position(pos) else { + return vec![]; + }; + let Some(element) = document.elements.get(*element_id) else { + return vec![]; + }; + + if let ElementKind::Import { path } = &element.kind { + let Some(p) = ipath.iter().map(|p| p.join(path)).find(|p| p.exists()) else { + return vec![]; + }; + let Ok(uri) = Url::from_file_path(p) else { + return vec![]; + }; + return vec![Location { + uri, + range: Range::default(), // just start of the file + }]; + } + + let Some(fqn) = self.resolve_target_fqn(uri, pos) else { + return vec![]; + }; + self.declarations_for_fqn(&fqn) + } +} + +#[cfg(test)] +mod test { + use async_lsp::lsp_types::{Position, Url}; + use std::path::PathBuf; + + use insta::assert_yaml_snapshot; + + use crate::config::Config; + use crate::state::ProtoLanguageState; + + fn setup_workspace() -> (Vec, Url, Url, Url, ProtoLanguageState) { + let ipath = vec![PathBuf::from("src/state/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 mut state: ProtoLanguageState = ProtoLanguageState::new(); + state.upsert_file( + &a_uri, + include_str!("input/a.proto"), + &ipath, + 2, + &Config::default(), + false, + ); + state.upsert_file( + &b_uri, + include_str!("input/b.proto"), + &ipath, + 2, + &Config::default(), + false, + ); + state.upsert_file( + &c_uri, + include_str!("input/c.proto"), + &ipath, + 2, + &Config::default(), + false, + ); + + (ipath, a_uri, b_uri, c_uri, state) + } + + #[test] + fn workspace_test_definition_identifiers() { + let (_ipath, _a, _b, _c, state) = setup_workspace(); + + assert_yaml_snapshot!(state.resolve_identifier_locations("com.workspace", "Author")); + assert_yaml_snapshot!( + state.resolve_identifier_locations("com.workspace", "Author.Address") + ); + assert_yaml_snapshot!( + state.resolve_identifier_locations("com.workspace", "com.utility.Foobar.Baz") + ); + assert_yaml_snapshot!(state.resolve_identifier_locations("com.utility", "Baz")); + } + + #[test] + fn test_definition_position_based() { + let (ipath, a_uri, b_uri, _c_uri, state) = setup_workspace(); + + // Cursor on the `Author` message declaration name in b.proto. + assert_yaml_snapshot!(state.definition( + &b_uri, + Position { + line: 5, + character: 10 + }, + &ipath + )); + // Cursor on the `Author` type reference inside a field in a.proto. + assert_yaml_snapshot!(state.definition( + &a_uri, + Position { + line: 11, + character: 5 + }, + &ipath + )); + // Cursor on empty whitespace -> no definition. + assert!( + state + .definition( + &a_uri, + Position { + line: 0, + character: 0 + }, + &ipath + ) + .is_empty() + ); + } + + #[test] + fn test_definition_import_position_based() { + let ipath = vec![std::env::current_dir().unwrap().join("src/state/input")]; + let a_uri = "file://input/a.proto".parse().unwrap(); + let mut state: ProtoLanguageState = ProtoLanguageState::new(); + state.upsert_file( + &a_uri, + include_str!("input/a.proto"), + &ipath, + 2, + &Config::default(), + false, + ); + + // Cursor on the `import "c.proto"` statement -> jump to the file. + let loc = state.definition( + &a_uri, + Position { + line: 4, + character: 10, + }, + &ipath, + ); + assert_yaml_snapshot!(loc, {"[0].uri" => insta::dynamic_redaction(|c, _| { + assert!(c.as_str().unwrap().ends_with("c.proto")); + "file:///c.proto".to_string() + })}); + } + + #[test] + fn workspace_test_definition_service_rpc_field() { + let ipath = vec![PathBuf::from("src/state/input")]; + let svc_uri: Url = "file://input/service.proto".parse().unwrap(); + let msg_uri: Url = "file://input/messages.proto".parse().unwrap(); + + let mut state: ProtoLanguageState = ProtoLanguageState::new(); + state.upsert_file( + &svc_uri, + include_str!("input/service.proto"), + &ipath, + 2, + &Config::default(), + false, + ); + state.upsert_file( + &msg_uri, + include_str!("input/messages.proto"), + &ipath, + 2, + &Config::default(), + false, + ); + + // Jump to a service declaration. + assert_yaml_snapshot!(state.resolve_identifier_locations("com.workspace", "Library")); + // Jump to an rpc method. + assert_yaml_snapshot!(state.resolve_identifier_locations("com.workspace", "GetBook")); + // Jump to a field inside a message. + assert_yaml_snapshot!( + state.resolve_identifier_locations("com.workspace", "GetBookResponse.title") + ); + // Fully-qualified with a leading dot. + assert_yaml_snapshot!( + state.resolve_identifier_locations("com.workspace", ".com.workspace.GetBookRequest") + ); + } + + #[test] + fn workspace_test_definition_enum_and_nonexistent() { + let ipath = vec![PathBuf::from("src/state/input")]; + let uri: Url = "file://input/enums.proto".parse().unwrap(); + let mut state: ProtoLanguageState = ProtoLanguageState::new(); + state.upsert_file( + &uri, + concat!( + "syntax = \"proto3\";\n", + "package com.enums;\n", + "enum Color { RED = 0; GREEN = 1; }\n", + ), + &ipath, + 2, + &Config::default(), + false, + ); + + // Jump to the enum type. + assert_yaml_snapshot!(state.resolve_identifier_locations("com.enums", "Color")); + // Jump to an enum value. + assert_yaml_snapshot!(state.resolve_identifier_locations("com.enums", "Color.RED")); + // Unknown symbol resolves to nothing. + assert!( + state + .resolve_identifier_locations("com.enums", "Nope") + .is_empty() + ); + } +} diff --git a/src/workspace/hover.rs b/src/state/hover.rs similarity index 69% rename from src/workspace/hover.rs rename to src/state/hover.rs index 43c84d2..ab73376 100644 --- a/src/workspace/hover.rs +++ b/src/state/hover.rs @@ -1,9 +1,8 @@ use async_lsp::lsp_types::{Hover, HoverContents, MarkupContent, MarkupKind, Position, Url}; use crate::model::{ElementKind, ModelElement, SpatialEntry}; -use crate::parser::ParsedTree; use crate::state::ProtoLanguageState; -use crate::utils::{is_position_inside_range, split_identifier_package}; +use crate::utils::is_position_inside_range; impl ProtoLanguageState { /// Dispatches a hover query, returning a formatted markdown tooltip and the @@ -14,16 +13,26 @@ impl ProtoLanguageState { /// Returns `Some(Hover)` containing the unified markdown payload, or `None` /// if the token carries no hoverable metadata. pub fn hover(&self, uri: &Url, position: Position) -> Option { - let current_tree = self.get_tree(uri)?; + let current_document = self.get_document(uri)?; let SpatialEntry { element_id, range } = - current_tree.find_entry_at_position(position).copied()?; - let element = current_tree.elements.get(element_id)?; + current_document.find_entry_at_position(position).copied()?; + let element = current_document.elements.get(element_id)?; let value = element.to_hover_markdown(position).or_else(|| { + let scope = element.kind.fqn().unwrap_or(¤t_document.package); element .inspect_nested_type_reference(position) - .and_then(|type_name| self.resolve_package_bound_type(¤t_tree, type_name)) + .and_then(|type_name| { + self.resolve_reference(scope, type_name) + .into_iter() + .next() + .and_then(|target| { + target + .element + .to_hover_markdown(target.element.meta.selection_range.start) + }) + }) })?; Some(Hover { @@ -34,52 +43,6 @@ impl ProtoLanguageState { range: Some(range), }) } - - /// Semi-dynamic, package-bound fallback mechanism designed to resolve type signatures - /// within adjacent schemas matching the target namespace bounds. - /// - /// # Note - /// - /// - /// - /// This implementation relies on suffix-based matching and loose scope stitching - /// as an interim Phase 1 strategy. It is scheduled to be completely replaced by a strict, - /// index-backed Cross-File Name Resolution engine during Phase 3 of development. - fn resolve_package_bound_type( - &self, - current_tree: &ParsedTree, - type_name: &str, - ) -> Option { - let (mut package, id_name) = split_identifier_package(type_name); - let curr_package = ¤t_tree.package; - - if package.is_empty() { - package = curr_package.as_str(); - } - - let mut candidate_trees = vec![]; - - // Evaluate and resolve relative namespace cascading rules - if curr_package != package { - let root_segment = curr_package.split('.').next().unwrap_or_default(); - - // Avoid generating redundant combinations if the signature is already fully-qualified - if root_segment.is_empty() || !package.starts_with(root_segment) { - let full_package = format!("{curr_package}.{package}"); - candidate_trees.append(&mut self.get_trees_for_package(&full_package)); - } - } - - // Collect direct package trees mapped to the target namespace - candidate_trees.append(&mut self.get_trees_for_package(package)); - - // Evaluate FQN trailing intersections across compiled tree scopes - candidate_trees - .iter() - .flat_map(|t| &t.elements) - .find(|e| e.kind.fqn().is_some_and(|fqn| fqn.ends_with(id_name))) - .and_then(|target| target.to_hover_markdown(target.meta.selection_range.start)) - } } impl ModelElement { @@ -128,7 +91,7 @@ mod test { use crate::state::ProtoLanguageState; #[test] fn workspace_test_hover() { - let ipath = vec![std::env::current_dir().unwrap().join("src/workspace/input")]; + let ipath = vec![std::env::current_dir().unwrap().join("src/state/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(); @@ -210,4 +173,51 @@ mod test { } )); } + + #[test] + fn test_hover_builtin_and_wellknown() { + let ipath = vec![]; + let uri = "file:///hover.proto".parse().unwrap(); + let mut state: ProtoLanguageState = ProtoLanguageState::new(); + state.upsert_file( + &uri, + concat!( + "syntax = \"proto3\";\n", + "package com.hover;\n", + "message Book {\n", + " string title = 1;\n", + " google.protobuf.Any ctx = 2;\n", + "}\n", + ), + &ipath, + 3, + &Config::default(), + false, + ); + + // Hover over the builtin `string` type. + assert_yaml_snapshot!(state.hover( + &uri, + Position { + line: 3, + character: 3 + } + )); + // Hover over the field name `title`. + assert_yaml_snapshot!(state.hover( + &uri, + Position { + line: 3, + character: 11 + } + )); + // Hover over the well-known `google.protobuf.Any` type. + assert_yaml_snapshot!(state.hover( + &uri, + Position { + line: 4, + character: 3 + } + )); + } } diff --git a/src/workspace/input/a.proto b/src/state/input/a.proto similarity index 100% rename from src/workspace/input/a.proto rename to src/state/input/a.proto diff --git a/src/workspace/input/b.proto b/src/state/input/b.proto similarity index 100% rename from src/workspace/input/b.proto rename to src/state/input/b.proto diff --git a/src/workspace/input/c.proto b/src/state/input/c.proto similarity index 100% rename from src/workspace/input/c.proto rename to src/state/input/c.proto diff --git a/src/workspace/input/collision_bar.proto b/src/state/input/collision_bar.proto similarity index 100% rename from src/workspace/input/collision_bar.proto rename to src/state/input/collision_bar.proto diff --git a/src/workspace/input/collision_foo.proto b/src/state/input/collision_foo.proto similarity index 100% rename from src/workspace/input/collision_foo.proto rename to src/state/input/collision_foo.proto diff --git a/src/workspace/input/inner/secret/y.proto b/src/state/input/inner/secret/y.proto similarity index 100% rename from src/workspace/input/inner/secret/y.proto rename to src/state/input/inner/secret/y.proto diff --git a/src/workspace/input/inner/x.proto b/src/state/input/inner/x.proto similarity index 100% rename from src/workspace/input/inner/x.proto rename to src/state/input/inner/x.proto diff --git a/src/workspace/input/messages.proto b/src/state/input/messages.proto similarity index 100% rename from src/workspace/input/messages.proto rename to src/state/input/messages.proto diff --git a/src/workspace/input/service.proto b/src/state/input/service.proto similarity index 100% rename from src/workspace/input/service.proto rename to src/state/input/service.proto diff --git a/src/state.rs b/src/state/mod.rs similarity index 77% rename from src/state.rs rename to src/state/mod.rs index 48c7e94..1fc9c40 100644 --- a/src/state.rs +++ b/src/state/mod.rs @@ -1,3 +1,9 @@ +mod definition; +mod hover; +mod rename; +mod resolve; +mod workspace_symbol; + use std::{ collections::{HashMap, HashSet}, path::{Path, PathBuf}, @@ -9,20 +15,19 @@ use async_lsp::lsp_types::{ CompletionItem, CompletionItemKind, Location, OneOf, ProgressParamsValue, PublishDiagnosticsParams, Range, SymbolKind, SymbolTag, Url, WorkspaceSymbol, }; -use tree_sitter::{Node, Query, QueryError}; +use tree_sitter::{Query, QueryError}; use walkdir::WalkDir; use crate::{ config::Config, + document::{ProtoDocument, ProtoParser}, model::{ElementKind, generate_metamodel_query}, - nodekind::NodeKind, - parser::{ParsedTree, ProtoParser}, protoc::collect_diagnostics, }; pub struct ProtoLanguageState { - documents: Arc>>, - trees: Arc>>, + sources: Arc>>, + documents: Arc>>, parser: Arc>, parsed_workspaces: Arc>>, metamodel_query: Query, @@ -43,8 +48,8 @@ impl ProtoLanguageState { .expect("Tree-sitter query compilation failed"); Self { + sources: Arc::default(), documents: Arc::default(), - trees: Arc::default(), parser: Arc::new(Mutex::new(ProtoParser::new())), parsed_workspaces: Arc::new(RwLock::new(HashSet::new())), metamodel_query, @@ -52,7 +57,7 @@ impl ProtoLanguageState { } pub fn get_content(&self, uri: &Url) -> String { - self.documents + self.sources .read() .expect("poison") .get(uri) @@ -60,12 +65,12 @@ impl ProtoLanguageState { .unwrap_or_default() } - pub fn get_tree(&self, uri: &Url) -> Option { - self.trees.read().expect("poison").get(uri).cloned() + pub fn get_document(&self, uri: &Url) -> Option { + self.documents.read().expect("poison").get(uri).cloned() } - pub fn get_trees(&self) -> Vec { - self.trees + pub fn get_documents(&self) -> Vec { + self.documents .read() .expect("poison") .values() @@ -73,12 +78,12 @@ impl ProtoLanguageState { .collect() } - pub fn get_trees_for_package(&self, package: &str) -> Vec { - self.trees + pub fn get_documents_for_package(&self, package: &str) -> Vec { + self.documents .read() .expect("poison") .values() - .filter(|tree| tree.package == package) + .filter(|document| document.package == package) .map(ToOwned::to_owned) .collect() } @@ -87,15 +92,15 @@ impl ProtoLanguageState { /// populated during startup indexing. /// /// This deliberately avoids re-parsing the workspace or rebuilding the - /// hierarchical [`DocumentSymbol`] tree on every request. Instead it scans + /// hierarchical [`DocumentSymbol`] document on every request. Instead it scans /// the flat, already-indexed [`ModelElement`] registry and resolves each /// candidate's container name by walking the in-memory parent links. pub fn find_workspace_symbols(&self, query: &str) -> Vec { let query = query.to_lowercase(); let mut symbols = Vec::new(); - for tree in self.get_trees() { - for element in &tree.elements { + for document in self.get_documents() { + for element in &document.elements { if matches!(element.kind, ElementKind::Import { .. }) { continue; } @@ -107,7 +112,7 @@ impl ProtoLanguageState { let container_name = element .parent_id - .and_then(|parent_id| tree.elements.get(parent_id)) + .and_then(|parent_id| document.elements.get(parent_id)) .map(|parent| parent.meta.name.clone()); let range = @@ -129,7 +134,7 @@ impl ProtoLanguageState { .then(|| vec![SymbolTag::DEPRECATED]), container_name, location: OneOf::Left(Location { - uri: tree.uri.clone(), + uri: document.uri.clone(), range, }), data: None, @@ -181,12 +186,12 @@ impl ProtoLanguageState { return; }; - self.trees + self.documents .write() .expect("posion") .insert(uri.clone(), parsed); - self.documents + self.sources .write() .expect("poison") .insert(uri.clone(), content.to_string()); @@ -204,13 +209,10 @@ impl ProtoLanguageState { } } - fn get_owned_imports(&self, uri: &Url, content: &str) -> Vec { - self.get_tree(uri) - .map(|t| t.get_import_paths(content.as_ref())) + fn get_owned_imports(&self, uri: &Url, _content: &str) -> Vec { + self.get_document(uri) + .map(|t| t.import_paths()) .unwrap_or_default() - .into_iter() - .map(ToOwned::to_owned) - .collect() } pub fn upsert_content( @@ -225,11 +227,10 @@ impl ProtoLanguageState { // After content is upserted, those imports which couldn't be located // are flagged as import error - self.get_tree(uri) - .map(|t| t.get_import_paths(content.as_ref())) + self.get_document(uri) + .map(|t| t.import_paths()) .unwrap_or_default() .into_iter() - .map(ToOwned::to_owned) .filter(|import| !ipath.iter().any(|p| p.join(import.as_str()).exists())) .collect() } @@ -311,10 +312,10 @@ impl ProtoLanguageState { info!(%uri, %depth, "upserting file"); let diag = self.upsert_content(uri, content, ipath, depth); let diag_slice: Vec<&str> = diag.iter().map(String::as_str).collect(); - self.get_tree(uri).map(|tree| { + self.get_document(uri).map(|document| { let mut d = vec![]; - d.extend(tree.collect_parse_diagnostics()); - d.extend(tree.collect_import_diagnostics(content.as_ref(), diag_slice.as_slice())); + d.extend(document.collect_parse_diagnostics()); + d.extend(document.collect_import_diagnostics(diag_slice.as_slice())); // Add protoc diagnostics if enabled if protoc_diagnostics && let Ok(file_path) = uri.to_file_path() { @@ -330,7 +331,7 @@ impl ProtoLanguageState { } PublishDiagnosticsParams { - uri: tree.uri.clone(), + uri: document.uri.clone(), diagnostics: d, version: None, } @@ -339,60 +340,53 @@ impl ProtoLanguageState { pub fn delete_file(&mut self, uri: &Url) { info!(%uri, "deleting file"); + self.sources.write().expect("poison").remove(uri); self.documents.write().expect("poison").remove(uri); - self.trees.write().expect("poison").remove(uri); } pub fn rename_file(&mut self, new_uri: &Url, old_uri: &Url) { info!(%new_uri, %old_uri, "renaming file"); - let content = self.documents.write().expect("poison").remove(old_uri); + let content = self.sources.write().expect("poison").remove(old_uri); if let Some(v) = content { - self.documents + self.sources .write() .expect("poison") .insert(new_uri.clone(), v); } - let mut tree = self.trees.write().expect("poison").remove(old_uri); - if let Some(ref mut v) = tree { + let mut document = self.documents.write().expect("poison").remove(old_uri); + if let Some(ref mut v) = document { v.uri = new_uri.clone(); } - if let Some(v) = tree { - self.trees + if let Some(v) = document { + self.documents .write() .expect("poison") .insert(new_uri.clone(), v); } } - pub fn completion_items_for_tree(&self, url: &Url) -> Vec { - let collector = |f: fn(&Node) -> bool, k: CompletionItemKind| { - self.get_tree(url) - .map(|tree| { - let content = self.get_content(&tree.uri); - - tree.find_all_nodes(f) - .into_iter() - .map(|n| { - let name = n.utf8_text(content.as_bytes()).unwrap().to_string(); - - CompletionItem { - label: format!(".{}.{name}", tree.package), - kind: Some(k), - ..Default::default() - } + pub fn completion_items_for_document(&self, url: &Url) -> Vec { + let collector = |f: fn(&ElementKind) -> bool, k: CompletionItemKind| { + self.get_document(url) + .map(|document| { + document + .elements + .iter() + .filter(|e| f(&e.kind)) + .map(|e| CompletionItem { + label: format!(".{}.{}", document.package, e.meta.name), + kind: Some(k), + ..Default::default() }) .collect::>() }) .unwrap_or_default() }; - let mut result = collector(NodeKind::is_enum_name, CompletionItemKind::ENUM); - result.extend(collector( - NodeKind::is_message_name, - CompletionItemKind::STRUCT, - )); + let mut result = collector(is_enum_kind, CompletionItemKind::ENUM); + result.extend(collector(is_message_kind, CompletionItemKind::STRUCT)); // Better ways to dedup, but who cares?... result.sort_by_key(|k| k.label.clone()); result.dedup_by_key(|k| k.label.clone()); @@ -400,26 +394,26 @@ impl ProtoLanguageState { } pub fn completion_items_for_package(&self, package: &str) -> Vec { - let collector = |f: fn(&Node) -> bool, k: CompletionItemKind| { - self.get_trees_for_package(package) - .into_iter() - .fold(vec![], |mut v, tree| { - let content = self.get_content(&tree.uri); - let t = tree.find_all_nodes(f).into_iter().map(|n| CompletionItem { - label: n.utf8_text(content.as_bytes()).unwrap().to_string(), - kind: Some(k), - ..Default::default() - }); - v.extend(t); - v - }) - }; - - let mut result = collector(NodeKind::is_enum_name, CompletionItemKind::ENUM); - result.extend(collector( - NodeKind::is_message_name, - CompletionItemKind::STRUCT, - )); + let collector = + |f: fn(&ElementKind) -> bool, k: CompletionItemKind| { + self.get_documents_for_package(package).into_iter().fold( + vec![], + |mut v, document| { + let t = document.elements.iter().filter(|e| f(&e.kind)).map(|e| { + CompletionItem { + label: e.meta.name.clone(), + kind: Some(k), + ..Default::default() + } + }); + v.extend(t); + v + }, + ) + }; + + let mut result = collector(is_enum_kind, CompletionItemKind::ENUM); + result.extend(collector(is_message_kind, CompletionItemKind::STRUCT)); // Better ways to dedup, but who cares?... result.sort_by_key(|k| k.label.clone()); result.dedup_by_key(|k| k.label.clone()); @@ -427,6 +421,14 @@ impl ProtoLanguageState { } } +fn is_enum_kind(kind: &ElementKind) -> bool { + matches!(kind, ElementKind::Enum { .. }) +} + +fn is_message_kind(kind: &ElementKind) -> bool { + matches!(kind, ElementKind::Message { .. }) +} + #[cfg(test)] mod test { use super::*; @@ -473,42 +475,68 @@ mod test { } #[test] - fn test_get_tree() { + fn test_get_document() { let state = setup_state(); - assert!(state.get_tree(&uri("file:///test.proto")).is_some()); - assert!(state.get_tree(&uri("file:///nonexistent.proto")).is_none()); + assert!(state.get_document(&uri("file:///test.proto")).is_some()); + assert!( + state + .get_document(&uri("file:///nonexistent.proto")) + .is_none() + ); } #[test] - fn test_get_trees() { + fn test_get_documents() { let state = setup_state(); - let trees = state.get_trees(); - assert_eq!(trees.len(), 3); + let documents = state.get_documents(); + assert_eq!(documents.len(), 3); } #[test] - fn test_get_trees_for_package() { + fn test_get_documents_for_package() { let state = setup_state(); - let test_trees = state.get_trees_for_package("com.test"); - assert_eq!(test_trees.len(), 2); + let test_documents = state.get_documents_for_package("com.test"); + assert_eq!(test_documents.len(), 2); - let other_trees = state.get_trees_for_package("com.other"); - assert_eq!(other_trees.len(), 1); + let other_documents = state.get_documents_for_package("com.other"); + assert_eq!(other_documents.len(), 1); - let empty_trees = state.get_trees_for_package("com.nonexistent"); - assert!(empty_trees.is_empty()); + let empty_documents = state.get_documents_for_package("com.nonexistent"); + assert!(empty_documents.is_empty()); } #[test] - fn test_tree_completion_items() { + fn test_document_completion_items() { let state = setup_state(); - let items = state.completion_items_for_tree(&uri("file:///test.proto")); + let items = state.completion_items_for_document(&uri("file:///test.proto")); let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect(); assert!(labels.contains(&".com.test.Book")); assert!(labels.contains(&".com.test.Color")); assert!(!labels.contains(&".com.test.Author")); } + #[test] + fn test_completion_excludes_fields_enum_values_and_imports() { + // Completion offers only type-level symbols (messages/enums); fields, + // enum values, and imports must never leak in. + let state = setup_state(); + let items = state.completion_items_for_document(&uri("file:///test.proto")); + let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect(); + assert!(!labels.contains(&".com.test.Book.title")); + assert!(!labels.contains(&".com.test.Color.RED")); + assert!(!labels.contains(&".com.test.import")); + } + + #[test] + fn test_document_completion_empty_for_missing_document() { + let state = setup_state(); + assert!( + state + .completion_items_for_document(&uri("file:///missing.proto")) + .is_empty() + ); + } + #[test] fn test_package_completion_items() { let state = setup_state(); @@ -572,9 +600,9 @@ mod test { fn test_delete_file() { let mut state = setup_state(); let test_uri = uri("file:///test.proto"); - assert!(state.get_tree(&test_uri).is_some()); + assert!(state.get_document(&test_uri).is_some()); state.delete_file(&test_uri); - assert!(state.get_tree(&test_uri).is_none()); + assert!(state.get_document(&test_uri).is_none()); assert_eq!(state.get_content(&test_uri), ""); } @@ -584,13 +612,13 @@ mod test { let old_uri = uri("file:///test.proto"); let new_uri = uri("file:///renamed.proto"); - assert!(state.get_tree(&old_uri).is_some()); - assert!(state.get_tree(&new_uri).is_none()); + assert!(state.get_document(&old_uri).is_some()); + assert!(state.get_document(&new_uri).is_none()); state.rename_file(&new_uri, &old_uri); - assert!(state.get_tree(&old_uri).is_none()); - assert!(state.get_tree(&new_uri).is_some()); + assert!(state.get_document(&old_uri).is_none()); + assert!(state.get_document(&new_uri).is_some()); assert_eq!( state.get_content(&new_uri), "syntax = \"proto3\";\npackage com.test;\nmessage Book { string title = 1; }\nenum Color { RED = 0; }\n" @@ -625,7 +653,7 @@ mod test { 1, ); assert!(unresolved.is_empty()); - assert!(state.get_tree(&uri("file:///main.proto")).is_some()); + assert!(state.get_document(&uri("file:///main.proto")).is_some()); } #[test] @@ -653,7 +681,7 @@ mod test { &ipath, 0, ); - assert!(state0.get_tree(&uri("file:///a.proto")).is_none()); + assert!(state0.get_document(&uri("file:///a.proto")).is_none()); // depth=1 should parse a.proto but not follow imports let mut state1 = ProtoLanguageState::new(); @@ -663,8 +691,8 @@ mod test { &ipath, 1, ); - assert!(state1.get_tree(&uri("file:///a.proto")).is_some()); - assert!(state1.get_tree(&uri("file:///b.proto")).is_none()); + assert!(state1.get_document(&uri("file:///a.proto")).is_some()); + assert!(state1.get_document(&uri("file:///b.proto")).is_none()); } #[test] @@ -685,11 +713,11 @@ mod test { std::fs::write(dir.path().join("notes.txt"), "hello").unwrap(); state.parse_all_from_workspace(dir.path(), None); - assert_eq!(state.get_trees().len(), 2); + assert_eq!(state.get_documents().len(), 2); // Second call should be idempotent state.parse_all_from_workspace(dir.path(), None); - assert_eq!(state.get_trees().len(), 2); + assert_eq!(state.get_documents().len(), 2); } #[test] diff --git a/src/workspace/rename.rs b/src/state/rename.rs similarity index 64% rename from src/workspace/rename.rs rename to src/state/rename.rs index 7aad962..549dbc6 100644 --- a/src/workspace/rename.rs +++ b/src/state/rename.rs @@ -1,15 +1,11 @@ -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::sync::mpsc::Sender; +use std::collections::{BTreeMap, HashMap}; +use std::path::PathBuf; -use async_lsp::lsp_types::{Location, Position, ProgressParamsValue, TextEdit, Url}; +use async_lsp::lsp_types::{Location, Position, TextEdit, Url}; -use crate::context::jumpable::Jumpable; -use crate::nodekind::NodeKind; +use crate::model::ElementKind; use crate::state::ProtoLanguageState; -use crate::utils::{ - is_position_inside_range, split_identifier_package, to_lsp_range, trailing_segment, -}; +use crate::utils::{is_position_inside_range, trailing_segment}; /// A single rename operation to apply against the workspace: rename whatever /// symbol is declared at `(uri, pos)` to `new_name`. Multiple ops are merged @@ -23,126 +19,6 @@ pub struct RenameOp { } impl ProtoLanguageState { - pub fn rename_fields( - &mut self, - current_package: &str, - identifier: &str, - new_text: &str, - workspace: &Path, - progress_sender: Option<&Sender>, - ) -> HashMap> { - self.parse_all_from_workspace(workspace, progress_sender); - let (_, identifier) = split_identifier_package(identifier); - self.get_trees() - .into_iter() - .fold(HashMap::new(), |mut h, tree| { - let content = self.get_content(&tree.uri); - let package = tree.get_package_name(content.as_ref()).unwrap_or("."); - let mut old = identifier.to_string(); - let mut new = new_text.to_string(); - let mut v = vec![]; - - // Global scope: Reference by only . or within global directly - if current_package == "." { - if package == "." { - v.extend(tree.rename_field(&old, &new, content.as_str())); - } - - old = format!(".{old}"); - new = format!(".{new}"); - - v.extend(tree.rename_field(&old, &new, content.as_str())); - - if !v.is_empty() { - h.insert(tree.uri.clone(), v); - } - return h; - } - - let full_old = format!("{current_package}.{old}"); - let full_new = format!("{current_package}.{new}"); - let global_full_old = format!(".{current_package}.{old}"); - let global_full_new = format!(".{current_package}.{new}"); - - // Current package: Reference by full or relative name or directly - if current_package == package { - v.extend(tree.rename_field(&old, &new, content.as_str())); - } else if current_package.starts_with(package) { - // Safety: prefix check already done - // get the relative part of the package - let packagepart = current_package - .strip_prefix(package) - .unwrap() - .trim_start_matches('.'); - let relative_old = format!("{packagepart}.{old}"); - let relative_new = format!("{packagepart}.{new}"); - v.extend(tree.rename_field(&relative_old, &relative_new, content.as_str())); - } - - // Otherwise, full reference - v.extend(tree.rename_field(&full_old, &full_new, content.as_str())); - v.extend(tree.rename_field(&global_full_old, &global_full_new, content.as_str())); - - if !v.is_empty() { - h.insert(tree.uri.clone(), v); - } - h - }) - } - - pub fn reference_fields( - &mut self, - current_package: &str, - identifier: &str, - workspace: &Path, - progress_sender: Option<&Sender>, - ) -> Option> { - self.parse_all_from_workspace(workspace, progress_sender); - let (_, identifier) = split_identifier_package(identifier); - let r = self - .get_trees() - .into_iter() - .fold(Vec::::new(), |mut v, tree| { - let content = self.get_content(&tree.uri); - let package = tree.get_package_name(content.as_ref()).unwrap_or("."); - let mut ident = identifier.to_owned(); - // Global scope: Reference by only . or within global directly - if current_package == "." { - if package == "." { - v.extend(tree.reference_field(&ident, content.as_str())); - } - - ident = format!(".{ident}"); - v.extend(tree.reference_field(&ident, content.as_str())); - - return v; - } - - let full_ident = format!("{current_package}.{ident}"); - let global_full_ident = format!(".{current_package}.{ident}"); - - // Current package: Reference by full or relative name or directly - if current_package == package { - v.extend(tree.reference_field(&ident, content.as_str())); - } else if current_package.starts_with(package) { - // Safety: prefix check already done - // get the relative part of the package - let packagepart = current_package - .strip_prefix(package) - .unwrap() - .trim_start_matches('.'); - let relative = format!("{packagepart}.{ident}"); - v.extend(tree.reference_field(&relative, content.as_str())); - } - - // Otherwise, full reference - v.extend(tree.reference_field(&full_ident, content.as_str())); - v.extend(tree.reference_field(&global_full_ident, content.as_str())); - v - }); - if r.is_empty() { None } else { Some(r) } - } - /// Find every rpc declaration in the workspace whose simple name matches /// `rpc_name`. Used by the rpc/request/response chain rename to enumerate /// candidate rpcs when the user invokes rename on a convention-named @@ -150,16 +26,13 @@ impl ProtoLanguageState { /// actually references the user's primary message. pub fn find_rpc_decls(&self, rpc_name: &str) -> Vec { let mut out = vec![]; - for tree in self.get_trees() { - let content = self.get_content(&tree.uri); - for node in tree.find_all_nodes(NodeKind::is_rpc_name) { - let Ok(text) = node.utf8_text(content.as_bytes()) else { - continue; - }; - if text == rpc_name { + for document in self.get_documents() { + for element in &document.elements { + if matches!(element.kind, ElementKind::Rpc { .. }) && element.meta.name == rpc_name + { out.push(Location { - uri: tree.uri.clone(), - range: to_lsp_range(node), + uri: document.uri.clone(), + range: element.meta.selection_range, }); } } @@ -172,9 +45,9 @@ impl ProtoLanguageState { /// the uniqueness check before chain-renaming a request/response message. pub fn count_rpc_uses_of_type(&self, type_simple_name: &str) -> usize { let mut count = 0; - for tree in self.get_trees() { - let content = self.get_content(&tree.uri); - for (req, resp) in tree.all_rpc_signatures(content.as_bytes()) { + for document in self.get_documents() { + let content = self.get_content(&document.uri); + for (req, resp) in document.all_rpc_signatures(content.as_bytes()) { if trailing_segment(&req) == type_simple_name || trailing_segment(&resp) == type_simple_name { @@ -219,19 +92,10 @@ impl ProtoLanguageState { /// single map. Returns `None` if the *primary* (first) op fails — in that /// case the user's invocation should produce no edit at all. Sibling /// failures are silently skipped so the primary always lands. - pub fn apply_rename_ops( - &mut self, - ops: &[RenameOp], - workspace: &Path, - progress_sender: Option>, - ) -> Option>> { + pub fn apply_rename_ops(&mut self, ops: &[RenameOp]) -> Option>> { let mut all: HashMap> = HashMap::new(); - let mut progress = progress_sender; for (i, op) in ops.iter().enumerate() { - // Only the first op gets the progress sender; subsequent ops would - // double-report. - let sender = progress.take(); - match self.run_single_rename(op, workspace, sender.as_ref()) { + match self.run_single_rename(op) { Some(edits) => { for (u, e) in edits { all.entry(u).or_default().extend(e); @@ -244,22 +108,12 @@ impl ProtoLanguageState { Some(all) } - fn run_single_rename( - &mut self, - op: &RenameOp, - workspace: &Path, - progress_sender: Option<&Sender>, - ) -> Option>> { - let tree = self.get_tree(&op.uri)?; - let content = self.get_content(&op.uri); - let package = tree.get_package_name(content.as_bytes()).unwrap_or("."); - - let (edit, otext, ntext) = tree.rename_tree(op.pos, &op.new_name, content.as_bytes())?; - - let mut h: HashMap> = HashMap::new(); - h.extend(self.rename_fields(package, &otext, &ntext, workspace, progress_sender)); - h.entry(tree.uri.clone()).or_default().extend(edit); - Some(h) + fn run_single_rename(&mut self, op: &RenameOp) -> Option>> { + // The workspace is already fully indexed once at startup (see the LSP + // `initialize` handler), so cross-file rename resolves against the + // cached metamodel pool without any per-request re-scan. + let target_fqn = self.resolve_target_fqn(&op.uri, op.pos)?; + Some(self.rename_for_fqn(&target_fqn, &op.new_name)) } fn compute_chain_siblings( @@ -272,16 +126,16 @@ impl ProtoLanguageState { if new_name.is_empty() { return vec![]; } - let Some(tree) = self.get_tree(decl_uri) else { + let Some(document) = self.get_document(decl_uri) else { return vec![]; }; let content = self.get_content(decl_uri); let bytes = content.as_bytes(); - if tree.rpc_at_position(decl_pos, bytes).is_some() { + if document.rpc_at_position(decl_pos, bytes).is_some() { return self.chain_from_rpc_cursor(decl_uri, decl_pos, new_name, ipath); } - if tree.message_name_at_position(decl_pos, bytes).is_some() { + if document.message_name_at_position(decl_pos, bytes).is_some() { return self.chain_from_message_cursor(decl_uri, decl_pos, new_name, ipath); } vec![] @@ -296,9 +150,9 @@ impl ProtoLanguageState { new_name: &str, ipath: &[PathBuf], ) -> Vec { - let tree = self.get_tree(decl_uri).expect("checked by caller"); + let document = self.get_document(decl_uri).expect("checked by caller"); let content = self.get_content(decl_uri); - let (old_rpc_name, request_text, response_text) = tree + let (old_rpc_name, request_text, response_text) = document .rpc_at_position(decl_pos, content.as_bytes()) .expect("checked by caller"); self.sibling_message_ops( @@ -329,9 +183,9 @@ impl ProtoLanguageState { new_name: &str, ipath: &[PathBuf], ) -> Vec { - let tree = self.get_tree(decl_uri).expect("checked by caller"); + let document = self.get_document(decl_uri).expect("checked by caller"); let content = self.get_content(decl_uri); - let msg_name = tree + let msg_name = document .message_name_at_position(decl_pos, content.as_bytes()) .expect("checked by caller"); @@ -348,12 +202,12 @@ impl ProtoLanguageState { // slot resolves to the user's actual declaration. let mut matching: Vec<(Location, String, String)> = vec![]; for rpc_loc in self.find_rpc_decls(&rpc_base) { - let Some(rpc_tree) = self.get_tree(&rpc_loc.uri) else { + let Some(rpc_document) = self.get_document(&rpc_loc.uri) else { continue; }; let rpc_content = self.get_content(&rpc_loc.uri); let Some((_, rpc_req, rpc_resp)) = - rpc_tree.rpc_at_position(rpc_loc.range.start, rpc_content.as_bytes()) + rpc_document.rpc_at_position(rpc_loc.range.start, rpc_content.as_bytes()) else { continue; }; @@ -362,11 +216,9 @@ impl ProtoLanguageState { } else { &rpc_resp }; - let rpc_pkg = rpc_tree - .get_package_name(rpc_content.as_bytes()) - .unwrap_or("."); + let rpc_pkg = rpc_document.package_name(); let resolves_to_primary = self - .definition(ipath, rpc_pkg, Jumpable::Identifier(slot_text.clone())) + .resolve_identifier_locations(rpc_pkg, slot_text) .iter() .any(|l| l.uri == *decl_uri && is_position_inside_range(decl_pos, l.range)); if resolves_to_primary { @@ -413,17 +265,13 @@ impl ProtoLanguageState { anchor_uri: &Url, old_rpc_name: &str, new_rpc_name: &str, - ipath: &[PathBuf], + _ipath: &[PathBuf], slots: &[(&str, &str)], ) -> Vec { - let Some(anchor_tree) = self.get_tree(anchor_uri) else { + let Some(anchor_document) = self.get_document(anchor_uri) else { return vec![]; }; - let anchor_content = self.get_content(anchor_uri); - let anchor_package = anchor_tree - .get_package_name(anchor_content.as_bytes()) - .unwrap_or(".") - .to_owned(); + let anchor_package = anchor_document.package_name().to_owned(); let mut ops = vec![]; for (suffix, type_text) in slots { @@ -434,11 +282,7 @@ impl ProtoLanguageState { if self.count_rpc_uses_of_type(&expected_name) != 1 { continue; } - let locations = self.definition( - ipath, - &anchor_package, - Jumpable::Identifier((*type_text).to_owned()), - ); + let locations = self.resolve_identifier_locations(&anchor_package, type_text); let Some(decl) = locations.into_iter().next() else { continue; }; @@ -473,12 +317,12 @@ fn strip_convention_suffix( mod test { use std::path::PathBuf; - use async_lsp::lsp_types::Position; + use async_lsp::lsp_types::{Position, Url}; use insta::assert_yaml_snapshot; use crate::config::Config; use crate::state::ProtoLanguageState; - use crate::workspace::rename::RenameOp; + use crate::state::rename::RenameOp; fn make_state(files: &[(&str, &str)], ipath: &[PathBuf]) -> ProtoLanguageState { let mut state = ProtoLanguageState::new(); @@ -498,77 +342,99 @@ mod test { } #[test] - fn test_rename() { - let ipath = vec![PathBuf::from("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"); + fn test_rename_for_fqn() { + let ipath = vec![PathBuf::from("src/state/input")]; + let state = make_state( + &[ + ("file://input/a.proto", include_str!("input/a.proto")), + ("file://input/b.proto", include_str!("input/b.proto")), + ("file://input/c.proto", include_str!("input/c.proto")), + ], + &ipath, + ); - let mut state: ProtoLanguageState = ProtoLanguageState::new(); - state.upsert_file(&a_uri, a, &ipath, 2, &Config::default(), false); - state.upsert_file(&b_uri, b, &ipath, 2, &Config::default(), false); - state.upsert_file(&c_uri, c, &ipath, 2, &Config::default(), false); - - assert_yaml_snapshot!(state.rename_fields( - "com.workspace", - "Author", - "Writer", - &PathBuf::from("src/workspace/input"), - None - )); - assert_yaml_snapshot!(state.rename_fields( - "com.workspace", - "Author.Address", - "Author.Location", - &PathBuf::from("src/workspace/input"), - None - )); - assert_yaml_snapshot!(state.rename_fields( - "com.utility", - "Foobar.Baz", - "Foobar.Baaz", - &PathBuf::from("src/workspace/input"), - None - )); + // Rename a top-level message: declaration + every reference (including + // nested-qualified usages like `Author.Address`) are updated. + assert_yaml_snapshot!(state.rename_for_fqn("com.workspace.Author", "Writer")); + // Rename a nested message referenced via qualified paths. + assert_yaml_snapshot!(state.rename_for_fqn("com.workspace.Author.Address", "Location")); + // Rename a message referenced from another package (fully qualified). + assert_yaml_snapshot!(state.rename_for_fqn("com.utility.Foobar.Baz", "Baaz")); } #[test] - fn test_reference() { - let ipath = vec![PathBuf::from("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(); + fn test_references_for_fqn() { + let ipath = vec![PathBuf::from("src/state/input")]; + let state = make_state( + &[ + ("file://input/a.proto", include_str!("input/a.proto")), + ("file://input/b.proto", include_str!("input/b.proto")), + ("file://input/c.proto", include_str!("input/c.proto")), + ], + &ipath, + ); - let a = include_str!("input/a.proto"); - let b = include_str!("input/b.proto"); - let c = include_str!("input/c.proto"); + assert_yaml_snapshot!(state.references_for_fqn("com.workspace.Author")); + assert_yaml_snapshot!(state.references_for_fqn("com.workspace.Author.Address")); + assert_yaml_snapshot!(state.references_for_fqn("com.utility.Foobar.Baz")); + assert!( + state + .references_for_fqn("com.nonexistent.Missing") + .is_empty() + ); + } - let mut state: ProtoLanguageState = ProtoLanguageState::new(); - state.upsert_file(&a_uri, a, &ipath, 2, &Config::default(), false); - state.upsert_file(&b_uri, b, &ipath, 2, &Config::default(), false); - state.upsert_file(&c_uri, c, &ipath, 2, &Config::default(), false); - - assert_yaml_snapshot!(state.reference_fields( - "com.workspace", - "Author", - &PathBuf::from("src/workspace/input"), - None - )); - assert_yaml_snapshot!(state.reference_fields( - "com.workspace", - "Author.Address", - &PathBuf::from("src/workspace/input"), + #[test] + fn test_resolve_target_fqn() { + let ipath = vec![PathBuf::from("src/state/input")]; + let state = make_state( + &[ + ("file://input/a.proto", include_str!("input/a.proto")), + ("file://input/b.proto", include_str!("input/b.proto")), + ], + &ipath, + ); + let a_uri: Url = "file://input/a.proto".parse().unwrap(); + let b_uri: Url = "file://input/b.proto".parse().unwrap(); + + // Cursor on the `Author` message declaration name in b.proto. + assert_eq!( + state.resolve_target_fqn( + &b_uri, + Position { + line: 5, + character: 10 + } + ), + Some("com.workspace.Author".to_owned()) + ); + // Cursor on the `Author` type reference inside a field in a.proto. + assert_eq!( + state.resolve_target_fqn( + &a_uri, + Position { + line: 11, + character: 5 + } + ), + Some("com.workspace.Author".to_owned()) + ); + // Cursor on whitespace -> None. + assert_eq!( + state.resolve_target_fqn( + &a_uri, + Position { + line: 0, + character: 0 + } + ), None - )); + ); } #[test] fn test_find_rpc_decls_and_count_uses() { - let ipath = vec![PathBuf::from("src/workspace/input")]; + let ipath = vec![PathBuf::from("src/state/input")]; let svc_uri = "file://input/service.proto".parse().unwrap(); let msg_uri = "file://input/messages.proto".parse().unwrap(); let svc = include_str!("input/service.proto"); @@ -607,7 +473,7 @@ mod test { // find_rpc_decls' iteration order could let bar's GetBook poison the // chain. With the fix, foo's GetBook is uniquely identified by // resolving its request slot back to the user's primary message. - let ipath = vec![PathBuf::from("src/workspace/input")]; + let ipath = vec![PathBuf::from("src/state/input")]; let foo_uri = "file://input/collision_foo.proto".parse().unwrap(); let state = make_state( &[ @@ -642,7 +508,7 @@ mod test { #[test] fn test_compute_rename_ops_chain_from_rpc_cursor() { - let ipath = vec![PathBuf::from("src/workspace/input")]; + let ipath = vec![PathBuf::from("src/state/input")]; let svc_uri = "file://input/service.proto".parse().unwrap(); let state = make_state( &[ @@ -687,7 +553,7 @@ mod test { // Same setup as `chain_from_rpc_cursor`, but with the chain flag off: // the rpc/request/response chain is gated behind the `[config.rename]` // `chain_rpc_request_response` setting, so only the primary op fires. - let ipath = vec![PathBuf::from("src/workspace/input")]; + let ipath = vec![PathBuf::from("src/state/input")]; let svc_uri = "file://input/service.proto".parse().unwrap(); let state = make_state( &[ @@ -720,7 +586,7 @@ mod test { #[test] fn test_compute_rename_ops_chain_from_request_cursor() { - let ipath = vec![PathBuf::from("src/workspace/input")]; + let ipath = vec![PathBuf::from("src/state/input")]; let msg_uri = "file://input/messages.proto".parse().unwrap(); let state = make_state( &[ @@ -762,7 +628,7 @@ mod test { #[test] fn test_compute_rename_ops_shared_request_blocks_chain() { - let ipath = vec![PathBuf::from("src/workspace/input")]; + let ipath = vec![PathBuf::from("src/state/input")]; let msg_uri = "file://input/messages.proto".parse().unwrap(); let state = make_state( &[ @@ -798,7 +664,7 @@ mod test { #[test] fn test_compute_rename_ops_new_name_breaks_convention() { - let ipath = vec![PathBuf::from("src/workspace/input")]; + let ipath = vec![PathBuf::from("src/state/input")]; let msg_uri = "file://input/messages.proto".parse().unwrap(); let state = make_state( &[ @@ -836,7 +702,7 @@ mod test { // reference site (not a declaration) yields a primary-only op that // would no-op the workspace pass. The LSP layer is responsible for // resolving the reference to its declaration *before* calling this. - let ipath = vec![PathBuf::from("src/workspace/input")]; + let ipath = vec![PathBuf::from("src/state/input")]; let svc_uri = "file://input/service.proto".parse().unwrap(); let state = make_state( &[ @@ -877,7 +743,7 @@ mod test { // The snapshot pins both the URIs and the exact edit ranges so any // regression in chain detection, edit merging, or workspace-pass // resolution will show up here. - let ipath = vec![PathBuf::from("src/workspace/input")]; + let ipath = vec![PathBuf::from("src/state/input")]; let svc_uri = "file://input/service.proto".parse().unwrap(); let mut state = make_state( &[ @@ -899,7 +765,7 @@ mod test { }; let ops = state.compute_rename_ops(&svc_uri, pos, "FetchBook", &ipath, true); let edits = state - .apply_rename_ops(&ops, &PathBuf::from("src/workspace/input"), None) + .apply_rename_ops(&ops) .expect("primary rename should not fail"); // Sort within each file so the snapshot is order-independent across @@ -912,4 +778,181 @@ mod test { } assert_yaml_snapshot!(normalized); } + + #[test] + fn test_rename_service_and_rpc() { + // Renaming a service and an rpc should update their declarations plus + // any cross-file type references (rpc request/response types stay put). + let ipath = vec![PathBuf::from("src/state/input")]; + let state = make_state( + &[ + ( + "file://input/service.proto", + include_str!("input/service.proto"), + ), + ( + "file://input/messages.proto", + include_str!("input/messages.proto"), + ), + ], + &ipath, + ); + + assert_yaml_snapshot!(state.rename_for_fqn("com.workspace.Library", "Catalog")); + assert_yaml_snapshot!(state.rename_for_fqn("com.workspace.GetBook", "FetchBook")); + } + + #[test] + fn test_rename_cross_file_message() { + // `GetBookRequest` is declared in messages.proto and referenced by the + // rpc in service.proto; renaming it must update the reference site too. + let ipath = vec![PathBuf::from("src/state/input")]; + let state = make_state( + &[ + ( + "file://input/service.proto", + include_str!("input/service.proto"), + ), + ( + "file://input/messages.proto", + include_str!("input/messages.proto"), + ), + ], + &ipath, + ); + + assert_yaml_snapshot!( + state.rename_for_fqn("com.workspace.GetBookRequest", "FetchBookRequest") + ); + } + + #[test] + fn test_rename_field_and_enum_value_single_site() { + // Fields and enum values are referenced only by their own declaration, + // so a rename is a single-site edit that must not touch anything else. + let ipath = vec![PathBuf::from("src/state/input")]; + let state = make_state( + &[( + "file://input/single.proto", + concat!( + "syntax = \"proto3\";\n", + "package com.single;\n", + "enum Color { RED = 0; GREEN = 1; }\n", + "message Book { string title = 1; Color color = 2; }\n", + ), + )], + &ipath, + ); + + assert_yaml_snapshot!(state.rename_for_fqn("com.single.Book.title", "name")); + assert_yaml_snapshot!(state.rename_for_fqn("com.single.Color.RED", "CRIMSON")); + } + + #[test] + fn test_rename_partial_name_safety() { + // Renaming `Book` must not touch `BookShelf` — only exact FQN matches + // (and nested-qualified usages) are rewritten. + let ipath = vec![PathBuf::from("src/state/input")]; + let state = make_state( + &[( + "file://input/partial.proto", + concat!( + "syntax = \"proto3\";\n", + "package com.p;\n", + "message Book {}\nmessage BookShelf { Book b = 1; }\n", + ), + )], + &ipath, + ); + + let edits = state.rename_for_fqn("com.p.Book", "Novel"); + assert_yaml_snapshot!(edits); + + // BookShelf itself must be untouched. + let shelf = state + .get_documents() + .into_iter() + .flat_map(|d| d.elements) + .find(|e| e.kind.fqn() == Some("com.p.BookShelf")); + assert!(shelf.is_some()); + } + + #[test] + fn test_rename_cross_package_collision_safety() { + // `com.foo.GetBookRequest` and the (unrelated) rpc in com.bar share the + // simple name; renaming the com.foo message must not leak into com.bar. + let ipath = vec![PathBuf::from("src/state/input")]; + let state = make_state( + &[ + ( + "file://input/foo.proto", + include_str!("input/collision_foo.proto"), + ), + ( + "file://input/bar.proto", + include_str!("input/collision_bar.proto"), + ), + ], + &ipath, + ); + + let edits = state.rename_for_fqn("com.foo.GetBookRequest", "FetchRequest"); + assert_yaml_snapshot!(edits); + + let bar_uri: Url = "file://input/bar.proto".parse().unwrap(); + let bar_touched = edits.contains_key(&bar_uri); + assert!( + !bar_touched, + "com.bar must not be touched by a com.foo rename" + ); + } + + #[test] + fn test_apply_rename_ops_from_reference_site() { + // End-to-end: invoking rename on a *reference site* (the `GetBookRequest` + // type inside the rpc signature) pivots to the declaration and renames + // both the declaration and the reference. + let ipath = vec![PathBuf::from("src/state/input")]; + let svc_uri = "file://input/service.proto".parse().unwrap(); + let mut state = make_state( + &[ + ( + "file://input/service.proto", + include_str!("input/service.proto"), + ), + ( + "file://input/messages.proto", + include_str!("input/messages.proto"), + ), + ], + &ipath, + ); + + // Cursor on `GetBookRequest` inside `rpc GetBook(GetBookRequest) ...` + // at line 7, character 19. + let pos = Position { + line: 7, + character: 19, + }; + let ops = state.compute_rename_ops(&svc_uri, pos, "FetchBookRequest", &ipath, false); + let edits = state.apply_rename_ops(&ops).expect("rename should succeed"); + let mut normalized: std::collections::BTreeMap> = + std::collections::BTreeMap::new(); + for (url, mut v) in edits { + v.sort_by_key(|e| (e.range.start.line, e.range.start.character)); + normalized.insert(url.to_string(), v); + } + assert_yaml_snapshot!(normalized); + } + + #[test] + fn test_rename_unknown_symbol_is_noop() { + let ipath = vec![PathBuf::from("src/state/input")]; + let state = make_state( + &[("file://input/a.proto", include_str!("input/a.proto"))], + &ipath, + ); + let edits = state.rename_for_fqn("com.workspace.DoesNotExist", "X"); + assert!(edits.is_empty()); + } } diff --git a/src/state/resolve.rs b/src/state/resolve.rs new file mode 100644 index 0000000..21c8b8e --- /dev/null +++ b/src/state/resolve.rs @@ -0,0 +1,468 @@ +//! Cross-file name resolution engine. +//! +//! This module implements the name resolution layer that links type references +//! to their definitions across files using fully-qualified names (FQNs), fully +//! decoupling features like go-to-definition, hover, and rename from raw +//! syntax-document node types. + +use std::collections::BTreeMap; + +use async_lsp::lsp_types::{Location, Position, TextEdit, Url}; + +use crate::model::{ModelElement, SpatialEntry, TypeReference}; +use crate::state::ProtoLanguageState; +use crate::utils::{is_position_inside_range, split_identifier_package, trailing_segment}; + +/// A resolved definition target located anywhere in the workspace. +#[derive(Debug, Clone)] +pub struct ResolvedTarget { + /// The document containing the definition. + pub uri: Url, + /// The metamodel element defining the symbol. + pub element: ModelElement, +} + +impl ProtoLanguageState { + /// Performs tiered protobuf name resolution for a reference identifier. + /// + /// Resolution order: + /// 1. **Fully-qualified** names (prefixed with `.`) — matched exactly. + /// 2. **Lexical scope chain** — walk the enclosing scope prefixes from + /// innermost to outermost, trying `{scope}.{reference}` for each. + /// 3. **Loose suffix fallback** — for short names defined inside a nested + /// container that aren't reachable through the direct scope chain. + pub fn resolve_reference(&self, scope: &str, reference: &str) -> Vec { + if let Some(fqn) = reference.strip_prefix('.') { + return self.lookup_fqn(fqn); + } + + for prefix in scope_prefixes(scope) { + let fqn = if prefix.is_empty() { + reference.to_string() + } else { + format!("{prefix}.{reference}") + }; + let matches = self.lookup_fqn(&fqn); + if !matches.is_empty() { + return matches; + } + } + + self.lookup_fqn_suffix(reference) + } + + /// Finds every element whose Fully Qualified Name equals `fqn`. + fn lookup_fqn(&self, fqn: &str) -> Vec { + let mut out = Vec::new(); + for document in self.get_documents() { + for element in &document.elements { + if element.kind.fqn() == Some(fqn) { + out.push(ResolvedTarget { + uri: document.uri.clone(), + element: element.clone(), + }); + } + } + } + out + } + + /// Finds every element whose FQN is exactly `name` or ends with a `.name` + /// component boundary (avoids partial identifier matches). + fn lookup_fqn_suffix(&self, name: &str) -> Vec { + let boundary = format!(".{name}"); + let mut out = Vec::new(); + for document in self.get_documents() { + for element in &document.elements { + if let Some(fqn) = element.kind.fqn() + && (fqn == name || fqn.ends_with(&boundary)) + { + out.push(ResolvedTarget { + uri: document.uri.clone(), + element: element.clone(), + }); + } + } + } + out + } + + /// Resolves the fully-qualified name of the symbol under `position`. + /// + /// If the cursor rests on a declaration name, the symbol's own FQN is + /// returned. If it rests on a type reference, the segment under the cursor + /// is resolved to its referenced definition's FQN (so renaming the outer + /// segment of `Book.Author` targets `Book`, not the nested `Author`). + pub fn resolve_target_fqn(&self, uri: &Url, position: Position) -> Option { + let document = self.get_document(uri)?; + let SpatialEntry { element_id, .. } = document.find_entry_at_position(position)?; + let element = document.elements.get(*element_id)?; + + if is_position_inside_range(position, element.meta.selection_range) { + return element.kind.fqn().map(ToOwned::to_owned); + } + + let type_ref = element.type_reference_at(position)?; + let scope = element.kind.fqn().unwrap_or(&document.package); + let ref_path = type_ref_segment_prefix(type_ref, position)?; + self.resolve_reference(scope, &ref_path) + .into_iter() + .next() + .and_then(|target| target.element.kind.fqn().map(ToOwned::to_owned)) + } + + /// Returns the declaration location (URI + name position) of the first + /// element matching `target_fqn` in the indexed workspace. + pub fn declaration_for_fqn(&self, target_fqn: &str) -> Option<(Url, Position)> { + for document in self.get_documents() { + for element in &document.elements { + if element.kind.fqn() == Some(target_fqn) { + return Some((document.uri.clone(), element.meta.selection_range.start)); + } + } + } + None + } + + /// Returns the declaration location(s) of every element matching + /// `target_fqn` in the indexed workspace. + pub fn declarations_for_fqn(&self, target_fqn: &str) -> Vec { + let mut out = Vec::new(); + for document in self.get_documents() { + for element in &document.elements { + if element.kind.fqn() == Some(target_fqn) { + out.push(Location { + uri: document.uri.clone(), + range: element.meta.selection_range, + }); + } + } + } + out + } + + /// Resolves an identifier (possibly package-qualified) against `scope` and + /// returns the declaration locations of its target. + pub fn resolve_identifier_locations(&self, scope: &str, identifier: &str) -> Vec { + let (package_part, id_name) = split_identifier_package(identifier); + let scope = if package_part.is_empty() { + scope + } else { + package_part + }; + self.resolve_reference(scope, id_name) + .into_iter() + .map(|target| Location { + uri: target.uri, + range: target.element.meta.selection_range, + }) + .collect() + } + + /// Collects every reference site for a symbol identified by its FQN across + /// the indexed workspace: all matching declarations plus every type + /// reference that resolves back to the same FQN. + pub fn references_for_fqn(&self, target_fqn: &str) -> Vec { + let mut refs = Vec::new(); + for document in self.get_documents() { + for element in &document.elements { + if element.kind.fqn() == Some(target_fqn) { + refs.push(Location { + uri: document.uri.clone(), + range: element.meta.selection_range, + }); + } + let scope = element.kind.fqn().unwrap_or(&document.package); + for type_ref in element.kind.type_references() { + if self + .resolve_reference(scope, &type_ref.name) + .iter() + .any(|r| r.element.kind.fqn() == Some(target_fqn)) + { + refs.push(Location { + uri: document.uri.clone(), + range: type_ref.range, + }); + } + } + } + } + // Deterministic output ordering for stable snapshots / tests. + refs.sort_by_key(|l| { + ( + l.uri.as_str().to_string(), + l.range.start.line, + l.range.start.character, + ) + }); + refs + } + + /// Produces rename edits that update the declaration(s) and every reference + /// site for a symbol identified by its FQN to `new_name`. + /// + /// Beyond sites that resolve *exactly* to `target_fqn`, this also rewrites + /// references to types nested underneath it (e.g. renaming `Author` to + /// `Writer` also updates `Author.Address` → `Writer.Address`), since the + /// nested qualification shifts with the enclosing message. + pub fn rename_for_fqn(&self, target_fqn: &str, new_name: &str) -> BTreeMap> { + let old_simple = trailing_segment(target_fqn); + let nested_prefix = format!("{target_fqn}."); + let mut edits: BTreeMap> = BTreeMap::new(); + + for document in self.get_documents() { + for element in &document.elements { + if element.kind.fqn() == Some(target_fqn) { + edits + .entry(document.uri.clone()) + .or_default() + .push(TextEdit { + range: element.meta.selection_range, + new_text: new_name.to_owned(), + }); + } + + let scope = element.kind.fqn().unwrap_or(&document.package); + for type_ref in element.kind.type_references() { + let resolves = self.resolve_reference(scope, &type_ref.name); + let is_target = resolves + .iter() + .any(|r| r.element.kind.fqn() == Some(target_fqn)); + let is_nested = resolves.iter().any(|r| { + r.element + .kind + .fqn() + .is_some_and(|fqn| fqn.starts_with(&nested_prefix)) + }); + if is_target || is_nested { + edits + .entry(document.uri.clone()) + .or_default() + .push(TextEdit { + range: type_ref.range, + new_text: rename_reference_text( + &type_ref.name, + old_simple, + new_name, + ), + }); + } + } + } + } + + // Deterministic output ordering for stable snapshots / tests. + for edits_in_file in edits.values_mut() { + edits_in_file.sort_by_key(|e| { + ( + e.range.start.line, + e.range.start.character, + e.range.end.line, + e.range.end.character, + ) + }); + } + edits + } +} + +/// Produces the chain of enclosing scope prefixes for a scope FQN, from the +/// innermost scope down to the empty (root) scope. +/// +/// `com.example.Book` yields `["com.example.Book", "com.example", "com", ""]`. +fn scope_prefixes(scope: &str) -> Vec<&str> { + let mut prefixes = Vec::new(); + let mut current = scope; + loop { + prefixes.push(current); + match current.rfind('.') { + Some(idx) => current = ¤t[..idx], + None => break, + } + } + prefixes +} + +/// Rewrites the segment of a type reference that names the renamed type to +/// `new_name`, preserving any surrounding qualification prefix and nested +/// suffix. +/// +/// `rename_reference_text("Author.Address", "Author", "Writer")` yields +/// `"Writer.Address"`; `"com.workspace.Author"` becomes +/// `"com.workspace.Writer"`; an unqualified `"Author"` becomes `"Writer"`. +fn rename_reference_text(name: &str, old_simple: &str, new_name: &str) -> String { + let mut parts: Vec<&str> = name.split('.').collect(); + if let Some(idx) = parts.iter().position(|p| *p == old_simple) { + parts[idx] = new_name; + } + parts.join(".") +} + +/// Returns the dot-joined path of a type reference up to and including the +/// segment the cursor rests on. +/// +/// For `Outer.Inner` with the cursor on `Inner`, returns `"Outer.Inner"`. +fn type_ref_segment_prefix(type_ref: &TypeReference, position: Position) -> Option { + if position.line != type_ref.range.start.line { + return None; + } + let mut cursor = type_ref.range.start.character as usize; + let position_char = position.character as usize; + let mut prefix: Vec<&str> = Vec::new(); + for segment in type_ref.name.split('.') { + prefix.push(segment); + let seg_end = cursor + segment.len(); + if position_char < seg_end { + return Some(prefix.join(".")); + } + cursor = seg_end + 1; // skip the '.' separator + } + None +} + +#[cfg(test)] +mod test { + use async_lsp::lsp_types::Url; + use std::path::PathBuf; + + use crate::config::Config; + use crate::state::ProtoLanguageState; + + fn setup() -> ProtoLanguageState { + let ipath = vec![PathBuf::from("src/state/input")]; + let a_uri: Url = "file://input/a.proto".parse().unwrap(); + let b_uri: Url = "file://input/b.proto".parse().unwrap(); + let c_uri: Url = "file://input/c.proto".parse().unwrap(); + + let mut state = ProtoLanguageState::new(); + state.upsert_file( + &a_uri, + include_str!("input/a.proto"), + &ipath, + 2, + &Config::default(), + false, + ); + state.upsert_file( + &b_uri, + include_str!("input/b.proto"), + &ipath, + 2, + &Config::default(), + false, + ); + state.upsert_file( + &c_uri, + include_str!("input/c.proto"), + &ipath, + 2, + &Config::default(), + false, + ); + state + } + + fn fqns(state: &ProtoLanguageState, scope: &str, reference: &str) -> Vec { + state + .resolve_reference(scope, reference) + .into_iter() + .map(|t| t.element.kind.fqn().unwrap_or_default().to_string()) + .collect() + } + + #[test] + fn test_resolve_scope_chain() { + let state = setup(); + assert_eq!( + fqns(&state, "com.workspace", "Author"), + vec!["com.workspace.Author"] + ); + assert_eq!( + fqns(&state, "com.workspace", "Author.Address"), + vec!["com.workspace.Author.Address"] + ); + assert_eq!( + fqns(&state, "com.workspace", "com.utility.Foobar.Baz"), + vec!["com.utility.Foobar.Baz"] + ); + } + + #[test] + fn test_resolve_suffix_fallback() { + let state = setup(); + // Baz is nested inside Foobar and not on the direct scope chain. + assert_eq!( + fqns(&state, "com.utility", "Baz"), + vec!["com.utility.Foobar.Baz"] + ); + } + + #[test] + fn test_resolve_fully_qualified() { + let state = setup(); + assert_eq!( + fqns(&state, "com.workspace", ".com.utility.Foobar.Baz"), + vec!["com.utility.Foobar.Baz"] + ); + } + + #[test] + fn test_resolve_boundary_avoids_partial_identifier() { + let mut state = ProtoLanguageState::new(); + let ipath: &[PathBuf] = &[]; + let uri: Url = "file:///t.proto".parse().unwrap(); + let content = "syntax = \"proto3\";\npackage com.test;\nmessage FooBaz { int32 x = 1; }\n"; + state.upsert_file(&uri, content, ipath, 1, &Config::default(), false); + + // "Baz" is only a partial suffix of FooBaz -> must not resolve. + assert!(state.resolve_reference("com.test", "Baz").is_empty()); + assert_eq!(fqns(&state, "com.test", "FooBaz"), vec!["com.test.FooBaz"]); + } + + #[test] + fn test_scope_prefixes() { + assert_eq!( + super::scope_prefixes("com.example.Book"), + vec!["com.example.Book", "com.example", "com"] + ); + assert_eq!( + super::scope_prefixes("com.workspace"), + vec!["com.workspace", "com"] + ); + assert_eq!(super::scope_prefixes(""), vec![""]); + } + + #[test] + fn test_rename_reference_text() { + // Unqualified. + assert_eq!( + super::rename_reference_text("Author", "Author", "Writer"), + "Writer" + ); + // Nested-qualified (leading segment renamed). + assert_eq!( + super::rename_reference_text("Author.Address", "Author", "Writer"), + "Writer.Address" + ); + // Fully package-qualified (middle segment renamed). + assert_eq!( + super::rename_reference_text("com.utility.Foobar.Baz", "Baz", "Baaz"), + "com.utility.Foobar.Baaz" + ); + } + + #[test] + fn test_resolve_relative_and_leading_dot() { + let state = setup(); + // Relative reference resolved against the current package. + assert_eq!( + fqns(&state, "com.workspace.Book", "Author"), + vec!["com.workspace.Author"] + ); + // Explicit leading-dot fully-qualified name. + assert_eq!( + fqns(&state, "com.workspace", ".com.utility.Foobar.Baz"), + vec!["com.utility.Foobar.Baz"] + ); + } +} diff --git a/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-5.snap b/src/state/snapshots/protols__state__definition__test__definition_import_position_based.snap similarity index 81% rename from src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-5.snap rename to src/state/snapshots/protols__state__definition__test__definition_import_position_based.snap index 401acac..52ea948 100644 --- a/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-5.snap +++ b/src/state/snapshots/protols__state__definition__test__definition_import_position_based.snap @@ -1,5 +1,5 @@ --- -source: src/workspace/definition.rs +source: src/state/definition.rs expression: loc --- - uri: "file:///c.proto" diff --git a/src/state/snapshots/protols__state__definition__test__definition_position_based-2.snap b/src/state/snapshots/protols__state__definition__test__definition_position_based-2.snap new file mode 100644 index 0000000..8f150c5 --- /dev/null +++ b/src/state/snapshots/protols__state__definition__test__definition_position_based-2.snap @@ -0,0 +1,12 @@ +--- +source: src/state/definition.rs +expression: "state.definition(&a_uri, Position { line: 11, character: 5 }, &ipath)" +--- +- uri: "file://input/b.proto" + range: + start: + line: 5 + character: 8 + end: + line: 5 + character: 14 diff --git a/src/state/snapshots/protols__state__definition__test__definition_position_based.snap b/src/state/snapshots/protols__state__definition__test__definition_position_based.snap new file mode 100644 index 0000000..ae14bdc --- /dev/null +++ b/src/state/snapshots/protols__state__definition__test__definition_position_based.snap @@ -0,0 +1,12 @@ +--- +source: src/state/definition.rs +expression: "state.definition(&b_uri, Position { line: 5, character: 10 }, &ipath)" +--- +- uri: "file://input/b.proto" + range: + start: + line: 5 + character: 8 + end: + line: 5 + character: 14 diff --git a/src/state/snapshots/protols__state__definition__test__workspace_test_definition-2.snap b/src/state/snapshots/protols__state__definition__test__workspace_test_definition-2.snap new file mode 100644 index 0000000..37b01d0 --- /dev/null +++ b/src/state/snapshots/protols__state__definition__test__workspace_test_definition-2.snap @@ -0,0 +1,12 @@ +--- +source: src/state/definition.rs +expression: "state.definition(&ipath, \"com.workspace\",\nJumpable::Identifier(\"Author.Address\".to_owned()))" +--- +- uri: "file://input/b.proto" + range: + start: + line: 9 + character: 11 + end: + line: 9 + character: 18 diff --git a/src/state/snapshots/protols__state__definition__test__workspace_test_definition-3.snap b/src/state/snapshots/protols__state__definition__test__workspace_test_definition-3.snap new file mode 100644 index 0000000..66cddd1 --- /dev/null +++ b/src/state/snapshots/protols__state__definition__test__workspace_test_definition-3.snap @@ -0,0 +1,12 @@ +--- +source: src/state/definition.rs +expression: "state.definition(&ipath, \"com.workspace\",\nJumpable::Identifier(\"com.utility.Foobar.Baz\".to_owned()))" +--- +- uri: "file://input/c.proto" + range: + start: + line: 8 + character: 11 + end: + line: 8 + character: 14 diff --git a/src/state/snapshots/protols__state__definition__test__workspace_test_definition-4.snap b/src/state/snapshots/protols__state__definition__test__workspace_test_definition-4.snap new file mode 100644 index 0000000..ede8b31 --- /dev/null +++ b/src/state/snapshots/protols__state__definition__test__workspace_test_definition-4.snap @@ -0,0 +1,12 @@ +--- +source: src/state/definition.rs +expression: "state.definition(&ipath, \"com.utility\",\nJumpable::Identifier(\"Baz\".to_owned()))" +--- +- uri: "file://input/c.proto" + range: + start: + line: 8 + character: 11 + end: + line: 8 + character: 14 diff --git a/src/state/snapshots/protols__state__definition__test__workspace_test_definition-5.snap b/src/state/snapshots/protols__state__definition__test__workspace_test_definition-5.snap new file mode 100644 index 0000000..52ea948 --- /dev/null +++ b/src/state/snapshots/protols__state__definition__test__workspace_test_definition-5.snap @@ -0,0 +1,12 @@ +--- +source: src/state/definition.rs +expression: loc +--- +- uri: "file:///c.proto" + range: + start: + line: 0 + character: 0 + end: + line: 0 + character: 0 diff --git a/src/state/snapshots/protols__state__definition__test__workspace_test_definition_enum_and_nonexistent-2.snap b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_enum_and_nonexistent-2.snap new file mode 100644 index 0000000..fad9a90 --- /dev/null +++ b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_enum_and_nonexistent-2.snap @@ -0,0 +1,12 @@ +--- +source: src/state/definition.rs +expression: "state.definition(&ipath, \"com.enums\",\nJumpable::Identifier(\"Color.RED\".to_owned()))" +--- +- uri: "file://input/enums.proto" + range: + start: + line: 2 + character: 13 + end: + line: 2 + character: 16 diff --git a/src/state/snapshots/protols__state__definition__test__workspace_test_definition_enum_and_nonexistent.snap b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_enum_and_nonexistent.snap new file mode 100644 index 0000000..b2c721f --- /dev/null +++ b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_enum_and_nonexistent.snap @@ -0,0 +1,12 @@ +--- +source: src/state/definition.rs +expression: "state.definition(&ipath, \"com.enums\",\nJumpable::Identifier(\"Color\".to_owned()))" +--- +- uri: "file://input/enums.proto" + range: + start: + line: 2 + character: 5 + end: + line: 2 + character: 10 diff --git a/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-2.snap b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_identifiers-2.snap similarity index 52% rename from src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-2.snap rename to src/state/snapshots/protols__state__definition__test__workspace_test_definition_identifiers-2.snap index 671dc99..b1f44ec 100644 --- a/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-2.snap +++ b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_identifiers-2.snap @@ -1,6 +1,6 @@ --- -source: src/workspace/definition.rs -expression: "state.definition(\"com.library\", \"Author.Address\")" +source: src/state/definition.rs +expression: "state.resolve_identifier_locations(\"com.workspace\", \"Author.Address\")" --- - uri: "file://input/b.proto" range: diff --git a/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-3.snap b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_identifiers-3.snap similarity index 51% rename from src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-3.snap rename to src/state/snapshots/protols__state__definition__test__workspace_test_definition_identifiers-3.snap index 3d795a3..529c699 100644 --- a/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-3.snap +++ b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_identifiers-3.snap @@ -1,6 +1,6 @@ --- -source: src/workspace/definition.rs -expression: "state.definition(\"com.library\", \"com.utility.Foobar.Baz\")" +source: src/state/definition.rs +expression: "state.resolve_identifier_locations(\"com.workspace\", \"com.utility.Foobar.Baz\")" --- - uri: "file://input/c.proto" range: diff --git a/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-4.snap b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_identifiers-4.snap similarity index 55% rename from src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-4.snap rename to src/state/snapshots/protols__state__definition__test__workspace_test_definition_identifiers-4.snap index 1bb9bd2..cc24900 100644 --- a/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-4.snap +++ b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_identifiers-4.snap @@ -1,6 +1,6 @@ --- -source: src/workspace/definition.rs -expression: "state.definition(\"com.utility\", \"Baz\")" +source: src/state/definition.rs +expression: "state.resolve_identifier_locations(\"com.utility\", \"Baz\")" --- - uri: "file://input/c.proto" range: diff --git a/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition.snap b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_identifiers.snap similarity index 54% rename from src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition.snap rename to src/state/snapshots/protols__state__definition__test__workspace_test_definition_identifiers.snap index 8bd8ef2..48fac8f 100644 --- a/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition.snap +++ b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_identifiers.snap @@ -1,6 +1,6 @@ --- -source: src/workspace/definition.rs -expression: "state.definition(\"com.library\", \"Author\")" +source: src/state/definition.rs +expression: "state.resolve_identifier_locations(\"com.workspace\", \"Author\")" --- - uri: "file://input/b.proto" range: diff --git a/src/state/snapshots/protols__state__definition__test__workspace_test_definition_service_rpc_field-2.snap b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_service_rpc_field-2.snap new file mode 100644 index 0000000..bf90a3b --- /dev/null +++ b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_service_rpc_field-2.snap @@ -0,0 +1,12 @@ +--- +source: src/state/definition.rs +expression: "state.definition(&ipath, \"com.workspace\",\nJumpable::Identifier(\"GetBook\".to_owned()))" +--- +- uri: "file://input/service.proto" + range: + start: + line: 7 + character: 8 + end: + line: 7 + character: 15 diff --git a/src/state/snapshots/protols__state__definition__test__workspace_test_definition_service_rpc_field-3.snap b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_service_rpc_field-3.snap new file mode 100644 index 0000000..8d1c662 --- /dev/null +++ b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_service_rpc_field-3.snap @@ -0,0 +1,12 @@ +--- +source: src/state/definition.rs +expression: "state.definition(&ipath, \"com.workspace\",\nJumpable::Identifier(\"GetBookResponse.title\".to_owned()))" +--- +- uri: "file://input/messages.proto" + range: + start: + line: 6 + character: 11 + end: + line: 6 + character: 16 diff --git a/src/state/snapshots/protols__state__definition__test__workspace_test_definition_service_rpc_field-4.snap b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_service_rpc_field-4.snap new file mode 100644 index 0000000..9ed8676 --- /dev/null +++ b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_service_rpc_field-4.snap @@ -0,0 +1,12 @@ +--- +source: src/state/definition.rs +expression: "state.definition(&ipath, \"com.workspace\",\nJumpable::Identifier(\".com.workspace.GetBookRequest\".to_owned()))" +--- +- uri: "file://input/messages.proto" + range: + start: + line: 4 + character: 8 + end: + line: 4 + character: 22 diff --git a/src/state/snapshots/protols__state__definition__test__workspace_test_definition_service_rpc_field.snap b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_service_rpc_field.snap new file mode 100644 index 0000000..8d8cb43 --- /dev/null +++ b/src/state/snapshots/protols__state__definition__test__workspace_test_definition_service_rpc_field.snap @@ -0,0 +1,12 @@ +--- +source: src/state/definition.rs +expression: "state.definition(&ipath, \"com.workspace\",\nJumpable::Identifier(\"Library\".to_owned()))" +--- +- uri: "file://input/service.proto" + range: + start: + line: 6 + character: 8 + end: + line: 6 + character: 15 diff --git a/src/state/snapshots/protols__state__hover__test__hover_builtin_and_wellknown-2.snap b/src/state/snapshots/protols__state__hover__test__hover_builtin_and_wellknown-2.snap new file mode 100644 index 0000000..c162242 --- /dev/null +++ b/src/state/snapshots/protols__state__hover__test__hover_builtin_and_wellknown-2.snap @@ -0,0 +1,14 @@ +--- +source: src/state/hover.rs +expression: "state.hover(&uri, Position { line: 3, character: 11 })" +--- +contents: + kind: markdown + value: "```protobuf\ncom.hover.Book.title\nstring title = 1;\n```" +range: + start: + line: 3 + character: 9 + end: + line: 3 + character: 14 diff --git a/src/state/snapshots/protols__state__hover__test__hover_builtin_and_wellknown-3.snap b/src/state/snapshots/protols__state__hover__test__hover_builtin_and_wellknown-3.snap new file mode 100644 index 0000000..8bd51d7 --- /dev/null +++ b/src/state/snapshots/protols__state__hover__test__hover_builtin_and_wellknown-3.snap @@ -0,0 +1,14 @@ +--- +source: src/state/hover.rs +expression: "state.hover(&uri, Position { line: 4, character: 3 })" +--- +contents: + kind: markdown + value: "`google.protobuf.Any`\n\n---\n\n**Well-Known Type**\n\n`Any` contains an arbitrary serialized message along with a URL that describes the type of the serialized message.\n\nThe JSON representation of an `Any` value uses the regular representation of the deserialized, embedded message, with an additional field `@type` which contains the type URL.\n\n**Fields:**\n\n- `type_url`: A URL or resource name that uniquely identifies the type of the serialized protocol buffer message.\n- `value`: Must be a valid serialized protocol buffer.\n\n```protobuf\nmessage Any {\n string type_url = 1;\n bytes value = 2;\n}\n```\n" +range: + start: + line: 4 + character: 2 + end: + line: 4 + character: 21 diff --git a/src/state/snapshots/protols__state__hover__test__hover_builtin_and_wellknown.snap b/src/state/snapshots/protols__state__hover__test__hover_builtin_and_wellknown.snap new file mode 100644 index 0000000..5590a06 --- /dev/null +++ b/src/state/snapshots/protols__state__hover__test__hover_builtin_and_wellknown.snap @@ -0,0 +1,14 @@ +--- +source: src/state/hover.rs +expression: "state.hover(&uri, Position { line: 3, character: 3 })" +--- +contents: + kind: markdown + value: "`string`\n\n---\n\n**Built-in Type**\n\nA string of text.\n\n**Details:**\n\n- **Wire format**: Length-delimited.\n- **Encoding**: Must always contain UTF-8 encoded or 7-bit ASCII text. Use `bytes` if you need to store arbitrary binary data or other encodings.\n- **Capacity**: Stores at most 4 GiB of text.\n- **Go type**: `string`\n- **C++ type**: `std::string`\n- **Rust type**: `::std::string::String`\n" +range: + start: + line: 3 + character: 2 + end: + line: 3 + character: 8 diff --git a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-2.snap b/src/state/snapshots/protols__state__hover__test__workspace_test_hover-2.snap similarity index 90% rename from src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-2.snap rename to src/state/snapshots/protols__state__hover__test__workspace_test_hover-2.snap index 6858522..5f9193a 100644 --- a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-2.snap +++ b/src/state/snapshots/protols__state__hover__test__workspace_test_hover-2.snap @@ -1,5 +1,5 @@ --- -source: src/workspace/hover.rs +source: src/state/hover.rs expression: "state.hover(&a_uri, Position { line: 11, character: 6 })" --- contents: diff --git a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-3.snap b/src/state/snapshots/protols__state__hover__test__workspace_test_hover-3.snap similarity index 95% rename from src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-3.snap rename to src/state/snapshots/protols__state__hover__test__workspace_test_hover-3.snap index 42a2818..eb4b0f0 100644 --- a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-3.snap +++ b/src/state/snapshots/protols__state__hover__test__workspace_test_hover-3.snap @@ -1,5 +1,5 @@ --- -source: src/workspace/hover.rs +source: src/state/hover.rs expression: "state.hover(&b_uri, Position { line: 10, character: 7 })" --- contents: diff --git a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-4.snap b/src/state/snapshots/protols__state__hover__test__workspace_test_hover-4.snap similarity index 90% rename from src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-4.snap rename to src/state/snapshots/protols__state__hover__test__workspace_test_hover-4.snap index 1d5f679..a41b9b4 100644 --- a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-4.snap +++ b/src/state/snapshots/protols__state__hover__test__workspace_test_hover-4.snap @@ -1,5 +1,5 @@ --- -source: src/workspace/hover.rs +source: src/state/hover.rs expression: "state.hover(&a_uri, Position { line: 12, character: 14 })" --- contents: diff --git a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-5.snap b/src/state/snapshots/protols__state__hover__test__workspace_test_hover-5.snap similarity index 89% rename from src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-5.snap rename to src/state/snapshots/protols__state__hover__test__workspace_test_hover-5.snap index 7973284..512f2df 100644 --- a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-5.snap +++ b/src/state/snapshots/protols__state__hover__test__workspace_test_hover-5.snap @@ -1,5 +1,5 @@ --- -source: src/workspace/hover.rs +source: src/state/hover.rs expression: "state.hover(&a_uri, Position { line: 13, character: 16 })" --- contents: diff --git a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-6.snap b/src/state/snapshots/protols__state__hover__test__workspace_test_hover-6.snap similarity index 89% rename from src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-6.snap rename to src/state/snapshots/protols__state__hover__test__workspace_test_hover-6.snap index be6dcce..0daf86f 100644 --- a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-6.snap +++ b/src/state/snapshots/protols__state__hover__test__workspace_test_hover-6.snap @@ -1,5 +1,5 @@ --- -source: src/workspace/hover.rs +source: src/state/hover.rs expression: "state.hover(&c_uri, Position { line: 12, character: 5 })" --- contents: diff --git a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-7.snap b/src/state/snapshots/protols__state__hover__test__workspace_test_hover-7.snap similarity index 90% rename from src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-7.snap rename to src/state/snapshots/protols__state__hover__test__workspace_test_hover-7.snap index 133c8c0..10c1928 100644 --- a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-7.snap +++ b/src/state/snapshots/protols__state__hover__test__workspace_test_hover-7.snap @@ -1,5 +1,5 @@ --- -source: src/workspace/hover.rs +source: src/state/hover.rs expression: "state.hover(&a_uri, Position { line: 14, character: 10 })" --- contents: diff --git a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-8.snap b/src/state/snapshots/protols__state__hover__test__workspace_test_hover-8.snap similarity index 91% rename from src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-8.snap rename to src/state/snapshots/protols__state__hover__test__workspace_test_hover-8.snap index 09bde43..23720d5 100644 --- a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-8.snap +++ b/src/state/snapshots/protols__state__hover__test__workspace_test_hover-8.snap @@ -1,5 +1,5 @@ --- -source: src/workspace/hover.rs +source: src/state/hover.rs expression: "state.hover(&x_uri, Position { line: 9, character: 18 })" --- contents: diff --git a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-9.snap b/src/state/snapshots/protols__state__hover__test__workspace_test_hover-9.snap similarity index 91% rename from src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-9.snap rename to src/state/snapshots/protols__state__hover__test__workspace_test_hover-9.snap index cf021f8..3eb3ed6 100644 --- a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-9.snap +++ b/src/state/snapshots/protols__state__hover__test__workspace_test_hover-9.snap @@ -1,5 +1,5 @@ --- -source: src/workspace/hover.rs +source: src/state/hover.rs expression: "state.hover(&x_uri, Position { line: 10, character: 4 })" --- contents: diff --git a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover.snap b/src/state/snapshots/protols__state__hover__test__workspace_test_hover.snap similarity index 96% rename from src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover.snap rename to src/state/snapshots/protols__state__hover__test__workspace_test_hover.snap index b6137aa..30fc531 100644 --- a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover.snap +++ b/src/state/snapshots/protols__state__hover__test__workspace_test_hover.snap @@ -1,5 +1,5 @@ --- -source: src/workspace/hover.rs +source: src/state/hover.rs expression: "state.hover(&a_uri, Position { line: 15, character: 10 })" --- contents: diff --git a/src/workspace/snapshots/protols__workspace__rename__test__apply_rename_ops_chain_from_rpc_cursor.snap b/src/state/snapshots/protols__state__rename__test__apply_rename_ops_chain_from_rpc_cursor.snap similarity index 96% rename from src/workspace/snapshots/protols__workspace__rename__test__apply_rename_ops_chain_from_rpc_cursor.snap rename to src/state/snapshots/protols__state__rename__test__apply_rename_ops_chain_from_rpc_cursor.snap index 9e38963..1f74ead 100644 --- a/src/workspace/snapshots/protols__workspace__rename__test__apply_rename_ops_chain_from_rpc_cursor.snap +++ b/src/state/snapshots/protols__state__rename__test__apply_rename_ops_chain_from_rpc_cursor.snap @@ -1,5 +1,5 @@ --- -source: src/workspace/rename.rs +source: src/state/rename.rs expression: normalized --- "file://input/messages.proto": diff --git a/src/state/snapshots/protols__state__rename__test__apply_rename_ops_from_reference_site.snap b/src/state/snapshots/protols__state__rename__test__apply_rename_ops_from_reference_site.snap new file mode 100644 index 0000000..ea6ceb7 --- /dev/null +++ b/src/state/snapshots/protols__state__rename__test__apply_rename_ops_from_reference_site.snap @@ -0,0 +1,22 @@ +--- +source: src/state/rename.rs +expression: normalized +--- +"file://input/messages.proto": + - range: + start: + line: 4 + character: 8 + end: + line: 4 + character: 22 + newText: FetchBookRequest +"file://input/service.proto": + - range: + start: + line: 7 + character: 16 + end: + line: 7 + character: 30 + newText: FetchBookRequest diff --git a/src/state/snapshots/protols__state__rename__test__references_for_fqn-2.snap b/src/state/snapshots/protols__state__rename__test__references_for_fqn-2.snap new file mode 100644 index 0000000..754bf32 --- /dev/null +++ b/src/state/snapshots/protols__state__rename__test__references_for_fqn-2.snap @@ -0,0 +1,28 @@ +--- +source: src/state/rename.rs +expression: "state.references_for_fqn(\"com.workspace.Author.Address\")" +--- +- uri: "file://input/a.proto" + range: + start: + line: 12 + character: 3 + end: + line: 12 + character: 17 +- uri: "file://input/b.proto" + range: + start: + line: 9 + character: 11 + end: + line: 9 + character: 18 +- uri: "file://input/b.proto" + range: + start: + line: 13 + character: 3 + end: + line: 13 + character: 10 diff --git a/src/state/snapshots/protols__state__rename__test__references_for_fqn-3.snap b/src/state/snapshots/protols__state__rename__test__references_for_fqn-3.snap new file mode 100644 index 0000000..c9f74cf --- /dev/null +++ b/src/state/snapshots/protols__state__rename__test__references_for_fqn-3.snap @@ -0,0 +1,28 @@ +--- +source: src/state/rename.rs +expression: "state.references_for_fqn(\"com.utility.Foobar.Baz\")" +--- +- uri: "file://input/a.proto" + range: + start: + line: 13 + character: 3 + end: + line: 13 + character: 25 +- uri: "file://input/c.proto" + range: + start: + line: 8 + character: 11 + end: + line: 8 + character: 14 +- uri: "file://input/c.proto" + range: + start: + line: 12 + character: 3 + end: + line: 12 + character: 6 diff --git a/src/state/snapshots/protols__state__rename__test__references_for_fqn.snap b/src/state/snapshots/protols__state__rename__test__references_for_fqn.snap new file mode 100644 index 0000000..c03a273 --- /dev/null +++ b/src/state/snapshots/protols__state__rename__test__references_for_fqn.snap @@ -0,0 +1,20 @@ +--- +source: src/state/rename.rs +expression: "state.references_for_fqn(\"com.workspace.Author\")" +--- +- uri: "file://input/a.proto" + range: + start: + line: 11 + character: 3 + end: + line: 11 + character: 9 +- uri: "file://input/b.proto" + range: + start: + line: 5 + character: 8 + end: + line: 5 + character: 14 diff --git a/src/state/snapshots/protols__state__rename__test__rename_cross_file_message.snap b/src/state/snapshots/protols__state__rename__test__rename_cross_file_message.snap new file mode 100644 index 0000000..63897ba --- /dev/null +++ b/src/state/snapshots/protols__state__rename__test__rename_cross_file_message.snap @@ -0,0 +1,22 @@ +--- +source: src/state/rename.rs +expression: "state.rename_for_fqn(\"com.workspace.GetBookRequest\", \"FetchBookRequest\")" +--- +"file://input/messages.proto": + - range: + start: + line: 4 + character: 8 + end: + line: 4 + character: 22 + newText: FetchBookRequest +"file://input/service.proto": + - range: + start: + line: 7 + character: 16 + end: + line: 7 + character: 30 + newText: FetchBookRequest diff --git a/src/state/snapshots/protols__state__rename__test__rename_cross_package_collision_safety.snap b/src/state/snapshots/protols__state__rename__test__rename_cross_package_collision_safety.snap new file mode 100644 index 0000000..e76cccf --- /dev/null +++ b/src/state/snapshots/protols__state__rename__test__rename_cross_package_collision_safety.snap @@ -0,0 +1,21 @@ +--- +source: src/state/rename.rs +expression: edits +--- +"file://input/foo.proto": + - range: + start: + line: 4 + character: 8 + end: + line: 4 + character: 22 + newText: FetchRequest + - range: + start: + line: 8 + character: 16 + end: + line: 8 + character: 30 + newText: FetchRequest diff --git a/src/state/snapshots/protols__state__rename__test__rename_field_and_enum_value_single_site-2.snap b/src/state/snapshots/protols__state__rename__test__rename_field_and_enum_value_single_site-2.snap new file mode 100644 index 0000000..296eaf3 --- /dev/null +++ b/src/state/snapshots/protols__state__rename__test__rename_field_and_enum_value_single_site-2.snap @@ -0,0 +1,13 @@ +--- +source: src/state/rename.rs +expression: "state.rename_for_fqn(\"com.single.Color.RED\", \"CRIMSON\")" +--- +"file://input/single.proto": + - range: + start: + line: 2 + character: 13 + end: + line: 2 + character: 16 + newText: CRIMSON diff --git a/src/state/snapshots/protols__state__rename__test__rename_field_and_enum_value_single_site.snap b/src/state/snapshots/protols__state__rename__test__rename_field_and_enum_value_single_site.snap new file mode 100644 index 0000000..b519eec --- /dev/null +++ b/src/state/snapshots/protols__state__rename__test__rename_field_and_enum_value_single_site.snap @@ -0,0 +1,13 @@ +--- +source: src/state/rename.rs +expression: "state.rename_for_fqn(\"com.single.Book.title\", \"name\")" +--- +"file://input/single.proto": + - range: + start: + line: 3 + character: 22 + end: + line: 3 + character: 27 + newText: name diff --git a/src/state/snapshots/protols__state__rename__test__rename_for_fqn-2.snap b/src/state/snapshots/protols__state__rename__test__rename_for_fqn-2.snap new file mode 100644 index 0000000..7febb29 --- /dev/null +++ b/src/state/snapshots/protols__state__rename__test__rename_for_fqn-2.snap @@ -0,0 +1,30 @@ +--- +source: src/state/rename.rs +expression: "state.rename_for_fqn(\"com.workspace.Author.Address\", \"Location\")" +--- +"file://input/a.proto": + - range: + start: + line: 12 + character: 3 + end: + line: 12 + character: 17 + newText: Author.Location +"file://input/b.proto": + - range: + start: + line: 9 + character: 11 + end: + line: 9 + character: 18 + newText: Location + - range: + start: + line: 13 + character: 3 + end: + line: 13 + character: 10 + newText: Location diff --git a/src/state/snapshots/protols__state__rename__test__rename_for_fqn-3.snap b/src/state/snapshots/protols__state__rename__test__rename_for_fqn-3.snap new file mode 100644 index 0000000..3e06b7b --- /dev/null +++ b/src/state/snapshots/protols__state__rename__test__rename_for_fqn-3.snap @@ -0,0 +1,30 @@ +--- +source: src/state/rename.rs +expression: "state.rename_for_fqn(\"com.utility.Foobar.Baz\", \"Baaz\")" +--- +"file://input/a.proto": + - range: + start: + line: 13 + character: 3 + end: + line: 13 + character: 25 + newText: com.utility.Foobar.Baaz +"file://input/c.proto": + - range: + start: + line: 8 + character: 11 + end: + line: 8 + character: 14 + newText: Baaz + - range: + start: + line: 12 + character: 3 + end: + line: 12 + character: 6 + newText: Baaz diff --git a/src/state/snapshots/protols__state__rename__test__rename_for_fqn.snap b/src/state/snapshots/protols__state__rename__test__rename_for_fqn.snap new file mode 100644 index 0000000..af4bd38 --- /dev/null +++ b/src/state/snapshots/protols__state__rename__test__rename_for_fqn.snap @@ -0,0 +1,38 @@ +--- +source: src/state/rename.rs +expression: "state.rename_for_fqn(\"com.workspace.Author\", \"Writer\")" +--- +"file://input/a.proto": + - range: + start: + line: 11 + character: 3 + end: + line: 11 + character: 9 + newText: Writer + - range: + start: + line: 12 + character: 3 + end: + line: 12 + character: 17 + newText: Writer.Address +"file://input/b.proto": + - range: + start: + line: 5 + character: 8 + end: + line: 5 + character: 14 + newText: Writer + - range: + start: + line: 13 + character: 3 + end: + line: 13 + character: 10 + newText: Address diff --git a/src/state/snapshots/protols__state__rename__test__rename_partial_name_safety.snap b/src/state/snapshots/protols__state__rename__test__rename_partial_name_safety.snap new file mode 100644 index 0000000..5e9bfb9 --- /dev/null +++ b/src/state/snapshots/protols__state__rename__test__rename_partial_name_safety.snap @@ -0,0 +1,21 @@ +--- +source: src/state/rename.rs +expression: edits +--- +"file://input/partial.proto": + - range: + start: + line: 2 + character: 8 + end: + line: 2 + character: 12 + newText: Novel + - range: + start: + line: 3 + character: 20 + end: + line: 3 + character: 24 + newText: Novel diff --git a/src/state/snapshots/protols__state__rename__test__rename_service_and_rpc-2.snap b/src/state/snapshots/protols__state__rename__test__rename_service_and_rpc-2.snap new file mode 100644 index 0000000..3c6e01a --- /dev/null +++ b/src/state/snapshots/protols__state__rename__test__rename_service_and_rpc-2.snap @@ -0,0 +1,5 @@ +--- +source: src/state/rename.rs +expression: "state.rename_for_fqn(\"com.workspace.GetBook\", \"FetchBook\")" +--- +{} diff --git a/src/state/snapshots/protols__state__rename__test__rename_service_and_rpc.snap b/src/state/snapshots/protols__state__rename__test__rename_service_and_rpc.snap new file mode 100644 index 0000000..08b975f --- /dev/null +++ b/src/state/snapshots/protols__state__rename__test__rename_service_and_rpc.snap @@ -0,0 +1,13 @@ +--- +source: src/state/rename.rs +expression: "state.rename_for_fqn(\"com.workspace.Library\", \"Catalog\")" +--- +"file://input/service.proto": + - range: + start: + line: 6 + character: 8 + end: + line: 6 + character: 15 + newText: Catalog diff --git a/src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols-2.snap b/src/state/snapshots/protols__state__workspace_symbol__test__workspace_symbols-2.snap similarity index 69% rename from src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols-2.snap rename to src/state/snapshots/protols__state__workspace_symbol__test__workspace_symbols-2.snap index 9ac099b..2514f7c 100644 --- a/src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols-2.snap +++ b/src/state/snapshots/protols__state__workspace_symbol__test__workspace_symbols-2.snap @@ -1,11 +1,11 @@ --- -source: src/workspace/workspace_symbol.rs +source: src/state/workspace_symbol.rs expression: author_symbols --- - name: Author kind: 23 location: - uri: "file:///src/workspace/input/b.proto" + uri: "file:///src/state/input/b.proto" range: start: line: 4 @@ -17,7 +17,7 @@ expression: author_symbols kind: 8 containerName: Book location: - uri: "file:///src/workspace/input/a.proto" + uri: "file:///src/state/input/a.proto" range: start: line: 11 diff --git a/src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols-3.snap b/src/state/snapshots/protols__state__workspace_symbol__test__workspace_symbols-3.snap similarity index 67% rename from src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols-3.snap rename to src/state/snapshots/protols__state__workspace_symbol__test__workspace_symbols-3.snap index 260ffb8..b429bf9 100644 --- a/src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols-3.snap +++ b/src/state/snapshots/protols__state__workspace_symbol__test__workspace_symbols-3.snap @@ -1,12 +1,12 @@ --- -source: src/workspace/workspace_symbol.rs +source: src/state/workspace_symbol.rs expression: address_symbols --- - name: Address kind: 23 containerName: Author location: - uri: "file:///src/workspace/input/b.proto" + uri: "file:///src/state/input/b.proto" range: start: line: 8 diff --git a/src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols.snap b/src/state/snapshots/protols__state__workspace_symbol__test__workspace_symbols.snap similarity index 73% rename from src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols.snap rename to src/state/snapshots/protols__state__workspace_symbol__test__workspace_symbols.snap index b5fa82f..93b665d 100644 --- a/src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols.snap +++ b/src/state/snapshots/protols__state__workspace_symbol__test__workspace_symbols.snap @@ -1,12 +1,12 @@ --- -source: src/workspace/workspace_symbol.rs +source: src/state/workspace_symbol.rs expression: all_symbols --- - name: Address kind: 23 containerName: Author location: - uri: "file:///src/workspace/input/b.proto" + uri: "file:///src/state/input/b.proto" range: start: line: 8 @@ -17,7 +17,7 @@ expression: all_symbols - name: Author kind: 23 location: - uri: "file:///src/workspace/input/b.proto" + uri: "file:///src/state/input/b.proto" range: start: line: 4 @@ -29,7 +29,7 @@ expression: all_symbols kind: 23 containerName: Foobar location: - uri: "file:///src/workspace/input/c.proto" + uri: "file:///src/state/input/c.proto" range: start: line: 7 @@ -40,7 +40,7 @@ expression: all_symbols - name: Book kind: 23 location: - uri: "file:///src/workspace/input/a.proto" + uri: "file:///src/state/input/a.proto" range: start: line: 9 @@ -51,7 +51,7 @@ expression: all_symbols - name: Foobar kind: 23 location: - uri: "file:///src/workspace/input/c.proto" + uri: "file:///src/state/input/c.proto" range: start: line: 4 @@ -62,7 +62,7 @@ expression: all_symbols - name: SomeSecret kind: 23 location: - uri: "file:///src/workspace/input/y.proto" + uri: "file:///src/state/input/y.proto" range: start: line: 4 @@ -73,7 +73,7 @@ expression: all_symbols - name: Why kind: 23 location: - uri: "file:///src/workspace/input/x.proto" + uri: "file:///src/state/input/x.proto" range: start: line: 6 @@ -85,7 +85,7 @@ expression: all_symbols kind: 8 containerName: Foobar location: - uri: "file:///src/workspace/input/c.proto" + uri: "file:///src/state/input/c.proto" range: start: line: 12 @@ -97,7 +97,7 @@ expression: all_symbols kind: 8 containerName: Book location: - uri: "file:///src/workspace/input/a.proto" + uri: "file:///src/state/input/a.proto" range: start: line: 11 @@ -109,7 +109,7 @@ expression: all_symbols kind: 8 containerName: Baz location: - uri: "file:///src/workspace/input/c.proto" + uri: "file:///src/state/input/c.proto" range: start: line: 9 @@ -121,7 +121,7 @@ expression: all_symbols kind: 8 containerName: Book location: - uri: "file:///src/workspace/input/a.proto" + uri: "file:///src/state/input/a.proto" range: start: line: 15 @@ -133,7 +133,7 @@ expression: all_symbols kind: 8 containerName: Book location: - uri: "file:///src/workspace/input/a.proto" + uri: "file:///src/state/input/a.proto" range: start: line: 12 @@ -145,7 +145,7 @@ expression: all_symbols kind: 8 containerName: Author location: - uri: "file:///src/workspace/input/b.proto" + uri: "file:///src/state/input/b.proto" range: start: line: 13 @@ -157,7 +157,7 @@ expression: all_symbols kind: 8 containerName: Author location: - uri: "file:///src/workspace/input/b.proto" + uri: "file:///src/state/input/b.proto" range: start: line: 6 @@ -169,7 +169,7 @@ expression: all_symbols kind: 8 containerName: Book location: - uri: "file:///src/workspace/input/a.proto" + uri: "file:///src/state/input/a.proto" range: start: line: 14 @@ -181,7 +181,7 @@ expression: all_symbols kind: 8 containerName: Why location: - uri: "file:///src/workspace/input/x.proto" + uri: "file:///src/state/input/x.proto" range: start: line: 8 @@ -193,7 +193,7 @@ expression: all_symbols kind: 8 containerName: SomeSecret location: - uri: "file:///src/workspace/input/y.proto" + uri: "file:///src/state/input/y.proto" range: start: line: 6 @@ -205,7 +205,7 @@ expression: all_symbols kind: 8 containerName: Why location: - uri: "file:///src/workspace/input/x.proto" + uri: "file:///src/state/input/x.proto" range: start: line: 9 @@ -217,7 +217,7 @@ expression: all_symbols kind: 8 containerName: Why location: - uri: "file:///src/workspace/input/x.proto" + uri: "file:///src/state/input/x.proto" range: start: line: 10 @@ -229,7 +229,7 @@ expression: all_symbols kind: 8 containerName: Book location: - uri: "file:///src/workspace/input/a.proto" + uri: "file:///src/state/input/a.proto" range: start: line: 13 @@ -241,7 +241,7 @@ expression: all_symbols kind: 8 containerName: Address location: - uri: "file:///src/workspace/input/b.proto" + uri: "file:///src/state/input/b.proto" range: start: line: 10 diff --git a/src/workspace/workspace_symbol.rs b/src/state/workspace_symbol.rs similarity index 82% rename from src/workspace/workspace_symbol.rs rename to src/state/workspace_symbol.rs index 9d7bb13..e50830c 100644 --- a/src/workspace/workspace_symbol.rs +++ b/src/state/workspace_symbol.rs @@ -9,15 +9,15 @@ mod test { #[test] fn test_workspace_symbols() { let current_dir = std::env::current_dir().unwrap(); - let ipath = vec![current_dir.join("src/workspace/input")]; + let ipath = vec![current_dir.join("src/state/input")]; let base_uri_str = Url::from_directory_path(¤t_dir) .unwrap() .to_string() .trim_end_matches('/') .to_string(); - let a_uri = Url::from_file_path(current_dir.join("src/workspace/input/a.proto")).unwrap(); - let b_uri = Url::from_file_path(current_dir.join("src/workspace/input/b.proto")).unwrap(); - let c_uri = Url::from_file_path(current_dir.join("src/workspace/input/c.proto")).unwrap(); + let a_uri = Url::from_file_path(current_dir.join("src/state/input/a.proto")).unwrap(); + let b_uri = Url::from_file_path(current_dir.join("src/state/input/b.proto")).unwrap(); + let c_uri = Url::from_file_path(current_dir.join("src/state/input/c.proto")).unwrap(); let a = include_str!("input/a.proto"); let b = include_str!("input/b.proto"); @@ -40,7 +40,7 @@ mod test { ); let file_name = uri_str.split('/').next_back().unwrap(); - format!("file:///src/workspace/input/{file_name}") + format!("file:///src/state/input/{file_name}") })}); @@ -56,7 +56,7 @@ mod test { ); let file_name = uri_str.split('/').next_back().unwrap(); - format!("file:///src/workspace/input/{file_name}") + format!("file:///src/state/input/{file_name}") })}); // Test query for "address" - should match Address @@ -72,7 +72,7 @@ mod test { let file_name = uri_str.split('/').next_back().unwrap(); - format!("file:///src/workspace/input/{file_name}") + format!("file:///src/state/input/{file_name}") })}); // Test query that should not match anything diff --git a/src/utils.rs b/src/utils.rs index cf69e3e..4c9c70c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -3,7 +3,7 @@ use tree_sitter::{Node, Point}; /// Converts a Tree-sitter [`Point`] into an LSP [`Position`]. /// -/// This helper maps the row and column coordinates from the syntax tree to the +/// This helper maps the row and column coordinates from the syntax document to the /// line-and-character coordinate system expected by LSP clients. /// /// # Saturation Behavior @@ -22,7 +22,7 @@ pub fn to_lsp_position(Point { row, column }: Point) -> Position { /// Converts a Tree-sitter [`Node`] boundary into an LSP [`Range`]. /// -/// This helper extracts the line-and-column boundaries from the syntax tree +/// This helper extracts the line-and-column boundaries from the syntax document /// and maps them directly to the coordinate system expected by LSP clients. #[inline] pub fn to_lsp_range(node: Node) -> Range { @@ -38,14 +38,6 @@ pub fn to_lsp_range(node: Node) -> Range { } } -#[inline] -pub const fn to_ts_point(Position { line, character }: Position) -> Point { - Point { - row: line as usize, - column: character as usize, - } -} - /// Evaluates whether a given LSP [`Position`] falls inclusively within the /// boundaries of an LSP [`Range`]. /// @@ -140,9 +132,8 @@ pub fn compile_test_query() -> tree_sitter::Query { mod test { use crate::utils::{ clean_proto_comment, is_inner_identifier, split_identifier_package, to_lsp_position, - to_ts_point, trailing_segment, + trailing_segment, }; - use async_lsp::lsp_types::Position; use tree_sitter::Point; #[test] @@ -153,36 +144,12 @@ mod test { assert_eq!(pos.character, 10); } - #[test] - fn test_to_ts_point() { - let pos = Position { - line: 3, - character: 7, - }; - let p = to_ts_point(pos); - assert_eq!(p.row, 3); - assert_eq!(p.column, 7); - } - - #[test] - fn test_position_roundtrip() { - let original = Point { - row: 42, - column: 15, - }; - let pos = to_lsp_position(original); - let back = to_ts_point(pos); - assert_eq!(original, back); - } - #[test] fn test_position_zero() { let p = Point { row: 0, column: 0 }; let pos = to_lsp_position(p); assert_eq!(pos.line, 0); assert_eq!(pos.character, 0); - let back = to_ts_point(pos); - assert_eq!(p, back); } #[test] diff --git a/src/workspace/definition.rs b/src/workspace/definition.rs deleted file mode 100644 index 1e5a908..0000000 --- a/src/workspace/definition.rs +++ /dev/null @@ -1,115 +0,0 @@ -use std::path::PathBuf; - -use async_lsp::lsp_types::{Location, Range, Url}; - -use crate::{ - context::jumpable::Jumpable, state::ProtoLanguageState, utils::split_identifier_package, -}; - -impl ProtoLanguageState { - pub fn definition( - &self, - ipath: &[PathBuf], - curr_package: &str, - jump: Jumpable, - ) -> Vec { - match jump { - Jumpable::Import(path) => { - let Some(p) = ipath.iter().map(|p| p.join(&path)).find(|p| p.exists()) else { - return vec![]; - }; - - let Ok(uri) = Url::from_file_path(p) else { - return vec![]; - }; - - vec![Location { - uri, - range: Range::default(), // just start of the file - }] - } - Jumpable::Identifier(identifier) => { - let (mut package, identifier) = split_identifier_package(identifier.as_str()); - if package.is_empty() { - package = curr_package; - } - - let mut trees = vec![]; - - // If package != curr_package, either identifier is from a completely new package - // or relative package from within. As per name resolution first resolve relative - // packages, add all relative trees in search list - if curr_package != package { - let fullpackage = format!("{curr_package}.{package}"); - trees.append(&mut self.get_trees_for_package(&fullpackage)); - } - - // Add all direct package trees - trees.append(&mut self.get_trees_for_package(package)); - trees.into_iter().fold(vec![], |mut v, tree| { - v.extend(tree.definition(identifier, self.get_content(&tree.uri))); - v - }) - } - } - } -} - -#[cfg(test)] -mod test { - use crate::context::jumpable::Jumpable; - use std::path::PathBuf; - - use insta::assert_yaml_snapshot; - - use crate::config::Config; - use crate::state::ProtoLanguageState; - #[test] - fn workspace_test_definition() { - let ipath = vec![PathBuf::from("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, &ipath, 2, &Config::default(), false); - state.upsert_file(&b_uri, b, &ipath, 2, &Config::default(), false); - state.upsert_file(&c_uri, c, &ipath, 2, &Config::default(), false); - - assert_yaml_snapshot!(state.definition( - &ipath, - "com.workspace", - Jumpable::Identifier("Author".to_owned()) - )); - assert_yaml_snapshot!(state.definition( - &ipath, - "com.workspace", - Jumpable::Identifier("Author.Address".to_owned()) - )); - assert_yaml_snapshot!(state.definition( - &ipath, - "com.workspace", - Jumpable::Identifier("com.utility.Foobar.Baz".to_owned()) - )); - assert_yaml_snapshot!(state.definition( - &ipath, - "com.utility", - Jumpable::Identifier("Baz".to_owned()) - )); - - let loc = state.definition( - &[std::env::current_dir().unwrap().join(&ipath[0])], - "com.workspace", - Jumpable::Import("c.proto".to_owned()), - ); - - 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 deleted file mode 100644 index 827348a..0000000 --- a/src/workspace/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod definition; -mod hover; -mod rename; -mod workspace_symbol; diff --git a/src/workspace/snapshots/protols__workspace__rename__test__reference-2.snap b/src/workspace/snapshots/protols__workspace__rename__test__reference-2.snap deleted file mode 100644 index a14ed4a..0000000 --- a/src/workspace/snapshots/protols__workspace__rename__test__reference-2.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/workspace/rename.rs -expression: "state.reference_fields(\"com.workspace\", \"Author.Address\",\nPathBuf::from(\"src/workspace/input\"), None)" ---- -- uri: "file://input/a.proto" - range: - start: - line: 12 - character: 3 - end: - line: 12 - character: 17 diff --git a/src/workspace/snapshots/protols__workspace__rename__test__reference.snap b/src/workspace/snapshots/protols__workspace__rename__test__reference.snap deleted file mode 100644 index 7139c9e..0000000 --- a/src/workspace/snapshots/protols__workspace__rename__test__reference.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/workspace/rename.rs -expression: "state.reference_fields(\"com.workspace\", \"Author\",\nPathBuf::from(\"src/workspace/input\"), None)" ---- -- uri: "file://input/a.proto" - range: - start: - line: 11 - character: 3 - end: - line: 11 - character: 9 diff --git a/src/workspace/snapshots/protols__workspace__rename__test__rename-2.snap b/src/workspace/snapshots/protols__workspace__rename__test__rename-2.snap deleted file mode 100644 index e0e5e0a..0000000 --- a/src/workspace/snapshots/protols__workspace__rename__test__rename-2.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: src/workspace/rename.rs -expression: "state.rename_fields(\"com.workspace\", \"Author.Address\", \"Author.Location\",\nPathBuf::from(\"src/workspace/input\"), None)" ---- -"file://input/a.proto": - - range: - start: - line: 12 - character: 3 - end: - line: 12 - character: 17 - newText: Author.Location diff --git a/src/workspace/snapshots/protols__workspace__rename__test__rename-3.snap b/src/workspace/snapshots/protols__workspace__rename__test__rename-3.snap deleted file mode 100644 index fa75d07..0000000 --- a/src/workspace/snapshots/protols__workspace__rename__test__rename-3.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: src/workspace/rename.rs -expression: "state.rename_fields(\"com.utility\", \"Foobar.Baz\", \"Foobar.Baaz\",\nPathBuf::from(\"src/workspace/input\"), None)" ---- -"file://input/a.proto": - - range: - start: - line: 13 - character: 3 - end: - line: 13 - character: 25 - newText: com.utility.Foobar.Baaz diff --git a/src/workspace/snapshots/protols__workspace__rename__test__rename.snap b/src/workspace/snapshots/protols__workspace__rename__test__rename.snap deleted file mode 100644 index 76dfb67..0000000 --- a/src/workspace/snapshots/protols__workspace__rename__test__rename.snap +++ /dev/null @@ -1,21 +0,0 @@ ---- -source: src/workspace/rename.rs -expression: "state.rename_fields(\"com.workspace\", \"Author\", \"Writer\",\nPathBuf::from(\"src/workspace/input\"), None)" ---- -"file://input/a.proto": - - range: - start: - line: 11 - character: 3 - end: - line: 11 - character: 9 - newText: Writer - - range: - start: - line: 12 - character: 3 - end: - line: 12 - character: 17 - newText: Writer.Address