diff --git a/Cargo.lock b/Cargo.lock index 603f1e0..077c67e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,6 +97,18 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "crossbeam-channel" version = "0.5.13" @@ -121,6 +133,12 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "errno" version = "0.3.9" @@ -251,6 +269,19 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "insta" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "810ae6042d48e2c9e9215043563a58a80b877bc863228a74cf10c49d4620a6f5" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "serde", + "similar", +] + [[package]] name = "itoa" version = "1.0.11" @@ -269,6 +300,12 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -435,10 +472,11 @@ dependencies = [ [[package]] name = "protols" -version = "0.3.0" +version = "0.4.0" dependencies = [ "async-lsp", "futures", + "insta", "protols-tree-sitter-proto", "tokio", "tokio-util", @@ -607,6 +645,12 @@ dependencies = [ "libc", ] +[[package]] +name = "similar" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" + [[package]] name = "slab" version = "0.4.9" diff --git a/Cargo.toml b/Cargo.toml index adc14a4..4c8f272 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "protols" description = "Language server for proto3 files" -version = "0.3.0" +version = "0.4.0" edition = "2021" license = "MIT" homepage = "https://github.com/coder3101/protols" @@ -23,3 +23,6 @@ tree-sitter = "0.22.6" tracing-appender = "0.2.3" protols-tree-sitter-proto = "0.2.0" walkdir = "2.5.0" + +[dev-dependencies] +insta = { version = "1.39.0", features = ["yaml"] } diff --git a/README.md b/README.md index acf9f18..818d39f 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,22 @@ # protols -A Language Server for **proto3** files. It uses tree-sitter parser for all operations and always runs in **single file mode**. +[![Crates](https://img.shields.io/crates/v/protols.svg)](https://crates.io/crates/protols) +[![Build and Test](https://github.com/coder3101/protols/actions/workflows/ci.yml/badge.svg)](https://github.com/coder3101/protols/actions/workflows/ci.yml) + +A Language Server for **proto3** files. It uses tree-sitter parser for all operations. ![](./assets/protols.mov) ## Features -- [x] Hover -- [x] Go to definition -- [x] Diagnostics +- [x] Completion (keywords, enums and messages of the package) +- [x] Diagnostics - based on sytax errors - [x] Document Symbols for message and enums -- [x] Rename message, enum and rpc -- [x] Completion for proto3 keywords +- [x] Go to definition - across packages +- [x] Hover - across packages +- [x] Rename - in current buffer only ## Installation -Run `cargo install protols` to install and add below to setup using [`nvim-lspconfig`](https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md#protols) until we start shipping this via Mason. +Run `cargo install protols` to install and add below to setup using [`nvim-lspconfig`](https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md#protols) ```lua require'lspconfig'.protols.setup{} diff --git a/sample/simple.proto b/sample/simple.proto index a896698..ee51325 100644 --- a/sample/simple.proto +++ b/sample/simple.proto @@ -7,7 +7,20 @@ message Book { // Of a message called Book int64 isbn = 1; string title = 2; - string author = 3; + Author author = 3; + google.protobuf.Any data = 4; + BookState state = 5; + + // Author is a author of a book + message Author { + string name = 1; + int64 age = 2; + } + + enum BookState { + HARD_COVER = 1; + SOFT_COVER = 2; + } } // This is a comment on message @@ -22,7 +35,7 @@ message GotoBookRequest { } message GetBookViaAuthor { - string author = 1; + Book.Author author = 1; } @@ -31,6 +44,7 @@ service BookService { // This is GetBook RPC that takes a book request // and returns a Book, simple and sweet rpc GetBook (GetBookRequest) returns (Book) {} + rpc GetBookAuthor (GetBookRequest) returns (Book.Author) {} rpc GetBooksViaAuthor (GetBookViaAuthor) returns (stream Book) {} rpc GetGreatestBook (stream GetBookRequest) returns (Book) {} rpc GetBooks (stream GetBookRequest) returns (stream Book) {} diff --git a/src/lsp.rs b/src/lsp.rs index bf9405a..2a261c9 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -1,22 +1,25 @@ +use std::fs::read_to_string; use std::ops::ControlFlow; use tracing::{error, info}; use async_lsp::lsp_types::{ CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse, - DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, - DidSaveTextDocumentParams, DocumentSymbolParams, DocumentSymbolResponse, GotoDefinitionParams, - GotoDefinitionResponse, Hover, HoverContents, HoverParams, HoverProviderCapability, - InitializeParams, InitializeResult, OneOf, PrepareRenameResponse, RenameParams, - ServerCapabilities, ServerInfo, TextDocumentPositionParams, TextDocumentSyncCapability, - TextDocumentSyncKind, WorkspaceEdit, WorkspaceFoldersServerCapabilities, + CreateFilesParams, DeleteFilesParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams, + DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentSymbolParams, + DocumentSymbolResponse, FileOperationFilter, FileOperationPattern, FileOperationPatternKind, + FileOperationRegistrationOptions, GotoDefinitionParams, GotoDefinitionResponse, Hover, + HoverContents, HoverParams, HoverProviderCapability, InitializeParams, InitializeResult, OneOf, + PrepareRenameResponse, RenameFilesParams, RenameOptions, RenameParams, ServerCapabilities, + ServerInfo, TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, Url, + WorkspaceEdit, WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities, }; use async_lsp::{LanguageClient, LanguageServer, ResponseError}; use futures::future::BoxFuture; -use crate::server::ServerState; +use crate::server::ProtoLanguageServer; -impl LanguageServer for ServerState { +impl LanguageServer for ProtoLanguageServer { type Error = ResponseError; type NotifyResult = ControlFlow>; @@ -34,18 +37,52 @@ impl LanguageServer for ServerState { info!("Connected with client {cname} {cversion}"); + let file_operation_filers = vec![FileOperationFilter { + scheme: Some(String::from("file")), + pattern: FileOperationPattern { + glob: String::from("**/*.{proto}"), + matches: Some(FileOperationPatternKind::File), + ..Default::default() + }, + }]; + + let file_registration_option = FileOperationRegistrationOptions { + filters: file_operation_filers.clone(), + }; + let mut workspace_capabilities = None; if let Some(folders) = params.workspace_folders { for workspace in folders { info!("Workspace folder: {workspace:?}"); - self.add_workspace_folder(workspace) + self.state.add_workspace_folder(workspace) } workspace_capabilities = Some(WorkspaceServerCapabilities { workspace_folders: Some(WorkspaceFoldersServerCapabilities { supported: Some(true), ..Default::default() }), - ..Default::default() + + file_operations: Some(WorkspaceFileOperationsServerCapabilities { + did_create: Some(file_registration_option.clone()), + did_rename: Some(file_registration_option.clone()), + did_delete: Some(file_registration_option.clone()), + ..Default::default() + }), + }) + } + + let mut rename_provider: OneOf = OneOf::Left(true); + + if params + .capabilities + .text_document + .and_then(|cap| cap.rename) + .and_then(|r| r.prepare_support) + .unwrap_or_default() + { + rename_provider = OneOf::Right(RenameOptions { + prepare_provider: Some(true), + work_done_progress_options: Default::default(), }) } @@ -60,7 +97,7 @@ impl LanguageServer for ServerState { hover_provider: Some(HoverProviderCapability::Simple(true)), document_symbol_provider: Some(OneOf::Left(true)), completion_provider: Some(CompletionOptions::default()), - rename_provider: Some(OneOf::Left(true)), + rename_provider: Some(rename_provider), ..ServerCapabilities::default() }, @@ -80,37 +117,55 @@ impl LanguageServer for ServerState { let uri = param.text_document_position_params.text_document.uri; let pos = param.text_document_position_params.position; - match self.get_parsed_tree_and_content(&uri) { - Err(e) => Box::pin(async move { Err(e) }), - Ok((tree, content)) => { - let comments = tree.hover(&pos, content.as_bytes()); - - let response = match comments.len() { - 0 => None, - 1 => Some(Hover { - contents: HoverContents::Scalar(comments[0].clone()), - range: None, - }), - 2.. => Some(Hover { - contents: HoverContents::Array(comments), - range: None, - }), - }; - - Box::pin(async move { Ok(response) }) - } - } + 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 identifier = tree.get_actionable_node_text_at_position(&pos, content.as_bytes()); + let current_package_name = tree.get_package_name(content.as_bytes()); + + let Some(identifier) = identifier else { + error!(uri=%uri, "failed to get identifier"); + return Box::pin(async move { Ok(None) }); + }; + + let Some(current_package_name) = current_package_name else { + error!(uri=%uri, "failed to get package name"); + return Box::pin(async move { Ok(None) }); + }; + + let comments = self + .state + .hover(current_package_name.as_ref(), identifier.as_ref()); + + let response = match comments.len() { + 0 => None, + 1 => Some(Hover { + contents: HoverContents::Scalar(comments[0].clone()), + range: None, + }), + 2.. => Some(Hover { + contents: HoverContents::Array(comments), + range: None, + }), + }; + + Box::pin(async move { Ok(response) }) } fn completion( &mut self, - _params: CompletionParams, + params: CompletionParams, ) -> BoxFuture<'static, Result, Self::Error>> { + let uri = params.text_document_position.text_document.uri; + let keywords = vec![ "syntax", "package", "option", "import", "service", "rpc", "returns", "message", "enum", "oneof", "repeated", "reserved", "to", ]; - let keywords = keywords + let mut keywords: Vec = keywords .into_iter() .map(|w| CompletionItem { label: w.to_string(), @@ -119,6 +174,12 @@ impl LanguageServer for ServerState { }) .collect(); + 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()) { + keywords.extend(self.state.completion_items(package_name)); + } + } Box::pin(async move { Ok(Some(CompletionResponse::Array(keywords))) }) } @@ -129,14 +190,14 @@ impl LanguageServer for ServerState { let uri = params.text_document.uri; let pos = params.position; - match self.get_parsed_tree_and_content(&uri) { - Err(e) => Box::pin(async move { Err(e) }), - Ok((tree, _)) => { - let response = tree.can_rename(&pos).map(PrepareRenameResponse::Range); + let Some(tree) = self.state.get_tree(&uri) else { + error!(uri=%uri, "failed to get tree"); + return Box::pin(async move { Ok(None) }); + }; - Box::pin(async move { Ok(response) }) - } - } + let response = tree.can_rename(&pos).map(PrepareRenameResponse::Range); + + Box::pin(async move { Ok(response) }) } fn rename( @@ -148,18 +209,20 @@ impl LanguageServer for ServerState { let new_name = params.new_name; - match self.get_parsed_tree_and_content(&uri) { - Err(e) => Box::pin(async move { Err(e) }), - Ok((tree, content)) => { - let response = if tree.can_rename(&pos).is_some() { - tree.rename(&uri, &pos, &new_name, content) - } else { - None - }; + let Some(tree) = self.state.get_tree(&uri) else { + error!(uri=%uri, "failed to get tree"); + return Box::pin(async move { Ok(None) }); + }; - Box::pin(async move { Ok(response) }) - } - } + let content = self.state.get_content(&uri); + + let response = if tree.can_rename(&pos).is_some() { + tree.rename(&pos, &new_name, content) + } else { + None + }; + + Box::pin(async move { Ok(response) }) } fn definition( @@ -169,20 +232,36 @@ impl LanguageServer for ServerState { let uri = param.text_document_position_params.text_document.uri; let pos = param.text_document_position_params.position; - match self.get_parsed_tree_and_content(&uri) { - Err(e) => Box::pin(async move { Err(e) }), - Ok((tree, content)) => { - let locations = tree.definition(&pos, &uri, content.as_bytes()); + 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 identifier = tree.get_actionable_node_text_at_position(&pos, content.as_bytes()); + let current_package_name = tree.get_package_name(content.as_bytes()); - let response = match locations.len() { - 0 => None, - 1 => Some(GotoDefinitionResponse::Scalar(locations[0].clone())), - 2.. => Some(GotoDefinitionResponse::Array(locations)), - }; + let Some(identifier) = identifier else { + error!(uri=%uri, "failed to get identifier"); + return Box::pin(async move { Ok(None) }); + }; - Box::pin(async move { Ok(response) }) - } - } + let Some(current_package_name) = current_package_name else { + error!(uri=%uri, "failed to get package name"); + return Box::pin(async move { Ok(None) }); + }; + + let locations = self + .state + .definition(current_package_name.as_ref(), identifier.as_ref()); + + let response = match locations.len() { + 0 => None, + 1 => Some(GotoDefinitionResponse::Scalar(locations[0].clone())), + 2.. => Some(GotoDefinitionResponse::Array(locations)), + }; + + Box::pin(async move { Ok(response) }) } fn document_symbol( @@ -191,15 +270,16 @@ impl LanguageServer for ServerState { ) -> BoxFuture<'static, Result, Self::Error>> { let uri = params.text_document.uri; - match self.get_parsed_tree_and_content(&uri) { - Err(e) => Box::pin(async move { Err(e) }), - Ok((tree, content)) => { - let locations = tree.find_document_locations(content.as_bytes()); - let response = DocumentSymbolResponse::Nested(locations); + let Some(tree) = self.state.get_tree(&uri) else { + error!(uri=%uri, "failed to get tree"); + return Box::pin(async move { Ok(None) }); + }; - Box::pin(async move { Ok(Some(response)) }) - } - } + let content = self.state.get_content(&uri); + let locations = tree.find_document_locations(content.as_bytes()); + let response = DocumentSymbolResponse::Nested(locations); + + Box::pin(async move { Ok(Some(response)) }) } fn did_save(&mut self, _: DidSaveTextDocumentParams) -> Self::NotifyResult { @@ -212,42 +292,66 @@ impl LanguageServer for ServerState { fn did_open(&mut self, params: DidOpenTextDocumentParams) -> Self::NotifyResult { let uri = params.text_document.uri; - let contents = params.text_document.text; - - info!("opened file at: {uri}"); - self.documents.insert(uri.clone(), contents.clone()); + let content = params.text_document.text; - let Some(tree) = self.parser.parse(contents.as_bytes()) else { - error!("failed to parse content at {uri}"); - return ControlFlow::Continue(()); - }; - - let diagnostics = tree.collect_parse_errors(&uri); - if let Err(e) = self.client.publish_diagnostics(diagnostics) { - error!(error=%e, "failed to publish diagnostics") + if let Some(diagnostics) = self.state.upsert_file(&uri, content) { + if let Err(e) = self.client.publish_diagnostics(diagnostics) { + error!(error=%e, "failed to publish diagnostics") + } } - - self.trees.insert(uri.clone(), tree); ControlFlow::Continue(()) } fn did_change(&mut self, params: DidChangeTextDocumentParams) -> Self::NotifyResult { let uri = params.text_document.uri; - let contents = params.content_changes[0].text.clone(); + let content = params.content_changes[0].text.clone(); - self.documents.insert(uri.clone(), contents.clone()); + if let Some(diagnostics) = self.state.upsert_file(&uri, content) { + if let Err(e) = self.client.publish_diagnostics(diagnostics) { + error!(error=%e, "failed to publish diagnostics") + } + } + ControlFlow::Continue(()) + } - let Some(tree) = self.parser.parse(contents.as_bytes()) else { - error!("failed to parse content at {uri}"); - return ControlFlow::Continue(()); - }; + fn did_create_files(&mut self, params: CreateFilesParams) -> Self::NotifyResult { + for file in params.files { + if let Ok(uri) = Url::from_file_path(&file.uri) { + // Safety: The uri is always a file type + let content = read_to_string(uri.to_file_path().unwrap()).unwrap_or_default(); + self.state.upsert_content(&uri, content); + } else { + error!(uri=%file.uri, "failed parse uri"); + } + } + ControlFlow::Continue(()) + } + + fn did_rename_files(&mut self, params: RenameFilesParams) -> Self::NotifyResult { + for file in params.files { + let Ok(new_uri) = Url::from_file_path(&file.new_uri) else { + error!(uri = file.new_uri, "failed to parse uri"); + continue; + }; + + let Ok(old_uri) = Url::from_file_path(&file.old_uri) else { + error!(uri = file.old_uri, "failed to parse uri"); + continue; + }; - let diagnostics = tree.collect_parse_errors(&uri); - if let Err(e) = self.client.publish_diagnostics(diagnostics) { - error!(error=%e, "failed to publish diagnostics") + self.state.rename_file(&new_uri, &old_uri); } + ControlFlow::Continue(()) + } - self.trees.insert(uri.clone(), tree); + fn did_delete_files(&mut self, params: DeleteFilesParams) -> Self::NotifyResult { + for file in params.files { + if let Ok(uri) = Url::from_file_path(&file.uri) { + self.state.delete_file(&uri); + } else { + error!(uri = file.uri, "failed to parse uri"); + } + } ControlFlow::Continue(()) } } diff --git a/src/main.rs b/src/main.rs index 2cf9f68..ed10ffb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,14 +5,17 @@ use async_lsp::concurrency::ConcurrencyLayer; use async_lsp::panic::CatchUnwindLayer; use async_lsp::server::LifecycleLayer; use async_lsp::tracing::TracingLayer; -use server::{ServerState, TickEvent}; +use server::{ProtoLanguageServer, TickEvent}; use tower::ServiceBuilder; use tracing::Level; mod lsp; +mod nodekind; mod parser; mod server; +mod state; mod utils; +mod workspace; #[tokio::main(flavor = "current_thread")] async fn main() { @@ -36,16 +39,13 @@ async fn main() { .layer(CatchUnwindLayer::default()) .layer(ConcurrencyLayer::default()) .layer(ClientProcessMonitorLayer::new(client.clone())) - .service(ServerState::new_router(client)) + .service(ProtoLanguageServer::new_router(client)) }); - let mut dir = std::env::temp_dir(); - dir.push("protols.log"); + let dir = std::env::temp_dir(); + eprintln!("Logs are being written to directory {:?}", dir); - eprintln!("Logs are being written to {:?}", dir); - - let file_appender = - tracing_appender::rolling::daily(std::env::temp_dir().as_path(), "protols.log"); + let file_appender = tracing_appender::rolling::daily(dir, "protols.log"); let (non_blocking, _gaurd) = tracing_appender::non_blocking(file_appender); tracing_subscriber::fmt() diff --git a/src/nodekind.rs b/src/nodekind.rs new file mode 100644 index 0000000..83a7d94 --- /dev/null +++ b/src/nodekind.rs @@ -0,0 +1,72 @@ +use async_lsp::lsp_types::SymbolKind; +use tree_sitter::Node; + +pub enum NodeKind { + Identifier, + Error, + MessageName, + EnumName, + FieldName, + ServiceName, + RpcName, + PackageName, +} + +#[allow(unused)] +impl NodeKind { + pub fn as_str(&self) -> &'static str { + match self { + NodeKind::Identifier => "identifier", + NodeKind::Error => "ERROR", + NodeKind::MessageName => "message_name", + NodeKind::EnumName => "enum_name", + NodeKind::FieldName => "message_or_enum_type", + NodeKind::ServiceName => "service_name", + NodeKind::RpcName => "rpc_name", + NodeKind::PackageName => "full_ident", + } + } + + 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_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_userdefined(n: &Node) -> bool { + n.kind() == Self::EnumName.as_str() || n.kind() == Self::MessageName.as_str() + } + + 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 + } + } +} diff --git a/src/parser/definition.rs b/src/parser/definition.rs index 84fc540..56da01d 100644 --- a/src/parser/definition.rs +++ b/src/parser/definition.rs @@ -1,91 +1,75 @@ -use async_lsp::lsp_types::{Location, Position, Range, Url}; -use tracing::info; +use async_lsp::lsp_types::{Location, Range}; +use tree_sitter::Node; -use crate::{parser::nodekind::NodeKind, utils::ts_to_lsp_position}; +use crate::{nodekind::NodeKind, utils::ts_to_lsp_position}; use super::ParsedTree; impl ParsedTree { - pub fn definition( + 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, - pos: &Position, - uri: &Url, + identifier: &str, + n: Node, + v: &mut Vec, content: impl AsRef<[u8]>, - ) -> Vec { - let text = self.get_node_text_at_position(pos, content.as_ref()); - info!("Looking for definition of: {:?}", text); + ) { + if identifier.is_empty() { + return; + } - match text { - Some(text) => self - .filter_node(NodeKind::is_userdefined) + if !identifier.contains(".") { + let locations: Vec = self + .filter_nodes_from(n, NodeKind::is_userdefined) .into_iter() - .filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == text) + .filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == identifier) .map(|n| Location { - uri: uri.clone(), + uri: self.uri.clone(), range: Range { start: ts_to_lsp_position(&n.start_position()), end: ts_to_lsp_position(&n.end_position()), }, }) - .collect(), - None => vec![], + .collect(); + + v.extend(locations); + return; + } + + // Safety: identifier contains a . + let (parent_identifier, remaining) = identifier.split_once(".").unwrap(); + let child_node = self + .filter_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); } } } #[cfg(test)] mod test { - use async_lsp::lsp_types::{Position, Range, Url}; + use async_lsp::lsp_types::Url; + use insta::assert_yaml_snapshot; use crate::parser::ProtoParser; #[test] fn test_goto_definition() { - let url = "file://foo/bar.proto"; - let posinvalid = Position { - line: 0, - character: 1, - }; - let posauthor = Position { - line: 10, - character: 5, - }; - let contents = r#"syntax = "proto3"; + 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); -package com.book; - -message Book { - message Author { - string name = 1; - string country = 2; - }; - - Author author = 1; - string isbn = 2; -} -"#; - let parsed = ProtoParser::new().parse(contents); assert!(parsed.is_some()); let tree = parsed.unwrap(); - let res = tree.definition(&posauthor, &url.parse().unwrap(), contents); - - assert_eq!(res.len(), 1); - assert_eq!(res[0].uri, Url::parse(url).unwrap()); - assert_eq!( - res[0].range, - Range { - start: Position { - line: 5, - character: 12 - }, - end: Position { - line: 5, - character: 18 - }, - } - ); - - let res = tree.definition(&posinvalid, &url.parse().unwrap(), contents); - assert_eq!(res.len(), 0); + assert_yaml_snapshot!(tree.definition("Author", contents)); + assert_yaml_snapshot!(tree.definition("", contents)); } } diff --git a/src/parser/diagnostics.rs b/src/parser/diagnostics.rs index ae05c65..e84f5f2 100644 --- a/src/parser/diagnostics.rs +++ b/src/parser/diagnostics.rs @@ -1,13 +1,13 @@ -use async_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, PublishDiagnosticsParams, Range, Url}; +use async_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, PublishDiagnosticsParams, Range}; -use crate::utils::ts_to_lsp_position; +use crate::{nodekind::NodeKind, utils::ts_to_lsp_position}; -use super::{nodekind::NodeKind, ParsedTree}; +use super::ParsedTree; impl ParsedTree { - pub fn collect_parse_errors(&self, uri: &Url) -> PublishDiagnosticsParams { + pub fn collect_parse_errors(&self) -> PublishDiagnosticsParams { let diagnostics = self - .filter_node(NodeKind::is_error) + .filter_nodes(NodeKind::is_error) .into_iter() .map(|n| Diagnostic { range: Range { @@ -21,7 +21,7 @@ impl ParsedTree { }) .collect(); PublishDiagnosticsParams { - uri: uri.clone(), + uri: self.uri.clone(), diagnostics, version: None, } @@ -30,65 +30,24 @@ impl ParsedTree { #[cfg(test)] mod test { - use async_lsp::lsp_types::{DiagnosticSeverity, Position, Range, Url}; + use async_lsp::lsp_types::Url; + use insta::assert_yaml_snapshot; use crate::parser::ProtoParser; #[test] fn test_collect_parse_error() { - let url = "file://foo/bar.proto"; - let contents = r#"syntax = "proto3"; + let url: Url = "file://foo/bar.proto".parse().unwrap(); + let contents = include_str!("input/test_collect_parse_error1.proto"); -package test; - -message Foo { - reserved 1; - reserved "baz"; - int bar = 2; -}"#; - - let parsed = ProtoParser::new().parse(contents); + let parsed = ProtoParser::new().parse(url.clone(), contents); assert!(parsed.is_some()); - let tree = parsed.unwrap(); - let diagnostics = tree.collect_parse_errors(&url.parse().unwrap()); - assert_eq!(diagnostics.uri, Url::parse(url).unwrap()); - assert_eq!(diagnostics.diagnostics.len(), 0); - - let url = "file://foo/bar.proto"; - let contents = r#"syntax = "proto3"; + assert_yaml_snapshot!(parsed.unwrap().collect_parse_errors()); -package com.book; + let contents = include_str!("input/test_collect_parse_error2.proto"); -message Book { - message Author { - string name; - string country = 2; - }; -}"#; - let parsed = ProtoParser::new().parse(contents); + let parsed = ProtoParser::new().parse(url.clone(), contents); assert!(parsed.is_some()); - let tree = parsed.unwrap(); - let diagnostics = tree.collect_parse_errors(&url.parse().unwrap()); - - assert_eq!(diagnostics.uri, Url::parse(url).unwrap()); - assert_eq!(diagnostics.diagnostics.len(), 1); - - let error = &diagnostics.diagnostics[0]; - assert_eq!(error.severity, Some(DiagnosticSeverity::ERROR)); - assert_eq!(error.source, Some("protols".to_owned())); - assert_eq!(error.message, "Syntax error"); - assert_eq!( - error.range, - Range { - start: Position { - line: 6, - character: 8 - }, - end: Position { - line: 6, - character: 19 - } - } - ); + assert_yaml_snapshot!(parsed.unwrap().collect_parse_errors()); } } diff --git a/src/parser/docsymbol.rs b/src/parser/docsymbol.rs index 1982f66..0d6bf04 100644 --- a/src/parser/docsymbol.rs +++ b/src/parser/docsymbol.rs @@ -1,9 +1,9 @@ use async_lsp::lsp_types::{DocumentSymbol, Range}; use tree_sitter::TreeCursor; -use crate::utils::ts_to_lsp_position; +use crate::{nodekind::NodeKind, utils::ts_to_lsp_position}; -use super::{ nodekind::NodeKind, ParsedTree}; +use super::ParsedTree; #[derive(Default)] pub(super) struct DocumentSymbolTreeBuilder { @@ -98,149 +98,21 @@ impl ParsedTree { #[cfg(test)] mod test { - use async_lsp::lsp_types::{DocumentSymbol, Position, Range, SymbolKind}; + use async_lsp::lsp_types::Url; + use insta::assert_yaml_snapshot; use crate::parser::ProtoParser; #[test] #[allow(deprecated)] fn test_document_symbols() { - let contents = r#"syntax = "proto3"; + let uri: Url = "file://foo/bar/pro.proto".parse().unwrap(); + let contents = include_str!("input/test_document_symbols.proto"); -package com.symbols; - -// outer 1 comment -message Outer1 { - message Inner1 { - string name = 1; - }; - - Inner1 i = 1; -} - -message Outer2 { - message Inner2 { - string name = 1; - }; - // Inner 3 comment here - message Inner3 { - string name = 1; - - enum X { - a = 1; - b = 2; - } - } - Inner1 i = 1; - Inner2 y = 2; -} - -"#; - let parsed = ProtoParser::new().parse(contents); + let parsed = ProtoParser::new().parse(uri.clone(), contents); assert!(parsed.is_some()); + let tree = parsed.unwrap(); - let res = tree.find_document_locations(contents); - - assert_eq!(res.len(), 2); - assert_eq!( - res, - vec!( - DocumentSymbol { - name: "Outer1".to_string(), - detail: Some("outer 1 comment".to_string()), - kind: SymbolKind::STRUCT, - tags: None, - range: Range { - start: Position::new(5, 0), - end: Position::new(11, 1), - }, - selection_range: Range { - start: Position::new(5, 8), - end: Position::new(5, 14), - }, - children: Some(vec!(DocumentSymbol { - name: "Inner1".to_string(), - detail: None, - kind: SymbolKind::STRUCT, - tags: None, - deprecated: None, - range: Range { - start: Position::new(6, 4), - end: Position::new(8, 5), - }, - selection_range: Range { - start: Position::new(6, 12), - end: Position::new(6, 18), - }, - children: Some(vec!()), - },)), - deprecated: None, - }, - DocumentSymbol { - name: "Outer2".to_string(), - detail: None, - kind: SymbolKind::STRUCT, - tags: None, - range: Range { - start: Position::new(13, 0), - end: Position::new(28, 1), - }, - selection_range: Range { - start: Position::new(13, 8), - end: Position::new(13, 14), - }, - children: Some(vec!( - DocumentSymbol { - name: "Inner2".to_string(), - detail: None, - kind: SymbolKind::STRUCT, - tags: None, - deprecated: None, - range: Range { - start: Position::new(14, 4), - end: Position::new(16, 5), - }, - selection_range: Range { - start: Position::new(14, 12), - end: Position::new(14, 18), - }, - children: Some(vec!()), - }, - DocumentSymbol { - name: "Inner3".to_string(), - detail: Some("Inner 3 comment here".to_string()), - kind: SymbolKind::STRUCT, - tags: None, - deprecated: None, - range: Range { - start: Position::new(18, 4), - end: Position::new(25, 5), - }, - selection_range: Range { - start: Position::new(18, 12), - end: Position::new(18, 18), - }, - children: Some(vec!(DocumentSymbol { - name: "X".to_string(), - detail: None, - kind: SymbolKind::ENUM, - tags: None, - deprecated: None, - range: Range { - start: Position::new(21, 8), - end: Position::new(24, 9), - }, - selection_range: Range { - start: Position::new(21, 13), - end: Position::new(21, 14), - }, - children: Some(vec!()), - })), - } - )), - deprecated: None, - }, - ) - ); + assert_yaml_snapshot!(tree.find_document_locations(contents)); } } diff --git a/src/parser/hover.rs b/src/parser/hover.rs index a1e4b26..c9a659e 100644 --- a/src/parser/hover.rs +++ b/src/parser/hover.rs @@ -1,8 +1,7 @@ -use async_lsp::lsp_types::{MarkedString, Position}; -use tracing::info; +use async_lsp::lsp_types::MarkedString; +use tree_sitter::Node; - -use crate::parser::nodekind::NodeKind; +use crate::nodekind::NodeKind; use super::ParsedTree; @@ -48,82 +47,79 @@ impl ParsedTree { } } - pub fn hover(&self, pos: &Position, content: impl AsRef<[u8]>) -> Vec { - let text = self.get_actionable_node_text_at_position(pos, content.as_ref()); - info!("Looking for hover response on: {:?}", text); + pub fn hover(&self, identifier: &str, content: impl AsRef<[u8]>) -> Vec { + let mut results = vec![]; + self.hover_impl(identifier, self.tree.root_node(), &mut results, content); + results + } - match text { - Some(text) => self - .filter_node(NodeKind::is_actionable) + fn hover_impl( + &self, + identifier: &str, + n: Node, + v: &mut Vec, + content: impl AsRef<[u8]>, + ) { + if identifier.is_empty() { + return; + } + + if !identifier.contains(".") { + let comments: Vec = self + .filter_nodes_from(n, NodeKind::is_userdefined) .into_iter() - .filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == text) + .filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == identifier) .filter_map(|n| self.find_preceding_comments(n.id(), content.as_ref())) .map(MarkedString::String) - .collect(), - None => vec![], + .collect(); + + v.extend(comments); + return; + } + + // Safety: identifier contains a . + let (parent_identifier, remaining) = identifier.split_once(".").unwrap(); + let child_node = self + .filter_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.hover_impl(remaining, inner, v, content); } } } #[cfg(test)] mod test { - use async_lsp::lsp_types::{MarkedString, Position}; + use async_lsp::lsp_types::Url; + use insta::assert_yaml_snapshot; use crate::parser::ProtoParser; #[test] fn test_hover() { - let posbook = Position { - line: 5, - character: 9, - }; - let posinvalid = Position { - line: 0, - character: 1, - }; - let posauthor = Position { - line: 11, - character: 14, - }; - let contents = r#"syntax = "proto3"; - -package com.book; - -// A Book is book -message Book { - - // This is represents author - // A author is a someone who writes books - // - // Author has a name and a country where they were born - message Author { - string name = 1; - string country = 2; - }; -} -"#; - let parsed = ProtoParser::new().parse(contents); + let uri: Url = "file://foo.bar/p.proto".parse().unwrap(); + let contents = include_str!("input/test_hover.proto"); + let parsed = ProtoParser::new().parse(uri.clone(), contents); + assert!(parsed.is_some()); let tree = parsed.unwrap(); - let res = tree.hover(&posbook, contents); - - assert_eq!(res.len(), 1); - assert_eq!(res[0], MarkedString::String("A Book is book".to_owned())); - - let res = tree.hover(&posinvalid, contents); - assert_eq!(res.len(), 0); - - let res = tree.hover(&posauthor, contents); - assert_eq!(res.len(), 1); - assert_eq!( - res[0], - MarkedString::String( - r#"This is represents author -A author is a someone who writes books - -Author has a name and a country where they were born"# - .to_owned() - ) - ); + + let res = tree.hover("Book", contents); + assert_yaml_snapshot!(res); + + let res = tree.hover("", contents); + assert_yaml_snapshot!(res); + + let res = tree.hover("Book.Author", contents); + assert_yaml_snapshot!(res); + + let res = tree.hover("Comic.Author", contents); + assert_yaml_snapshot!(res); + + let res = tree.hover("Author", contents); + assert_yaml_snapshot!(res); } } diff --git a/src/parser/input/test_can_rename.proto b/src/parser/input/test_can_rename.proto new file mode 100644 index 0000000..ebc6df6 --- /dev/null +++ b/src/parser/input/test_can_rename.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package com.parser; + +// A Book is book +message Book { + + // This is represents author + // A author is a someone who writes books + // + // Author has a name and a country where they were born + message Author { + string name = 1; + string country = 2; + }; + +} + +message Outer { + Book.Author a = 1; +} diff --git a/src/parser/input/test_collect_parse_error1.proto b/src/parser/input/test_collect_parse_error1.proto new file mode 100644 index 0000000..4f931bf --- /dev/null +++ b/src/parser/input/test_collect_parse_error1.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package com.parser; + +message Foo { + reserved 1; + reserved "baz"; + int bar = 2; +} diff --git a/src/parser/input/test_collect_parse_error2.proto b/src/parser/input/test_collect_parse_error2.proto new file mode 100644 index 0000000..15e7bdd --- /dev/null +++ b/src/parser/input/test_collect_parse_error2.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package com.parser; + +message Book { + message Author { + string name; + string country = 2; + }; +} diff --git a/src/parser/input/test_document_symbols.proto b/src/parser/input/test_document_symbols.proto new file mode 100644 index 0000000..05a1577 --- /dev/null +++ b/src/parser/input/test_document_symbols.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +package com.parser; + +// outer 1 comment +message Outer1 { + // Inner 1 + message Inner1 { + string name = 1; + }; + + Inner1 i = 1; +} + +message Outer2 { + message Inner2 { + string name = 1; + }; + // Inner 3 comment here + message Inner3 { + string name = 1; + + enum X { + a = 1; + b = 2; + } + } + Inner1 i = 1; + Inner2 y = 2; +} diff --git a/src/parser/input/test_filter.proto b/src/parser/input/test_filter.proto new file mode 100644 index 0000000..2d36db0 --- /dev/null +++ b/src/parser/input/test_filter.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package com.parser; + +message Book { + + message Author { + string name = 1; + string country = 2; + }; + // This is a multi line comment on the field name + // Of a message called Book + int64 isbn = 1; + string title = 2; + Author author = 3; +} diff --git a/src/parser/input/test_goto_definition.proto b/src/parser/input/test_goto_definition.proto new file mode 100644 index 0000000..7cad4be --- /dev/null +++ b/src/parser/input/test_goto_definition.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package com.parser; + +message Book { + message Author { + string name = 1; + string country = 2; + }; + + Author author = 1; + string isbn = 2; +} diff --git a/src/parser/input/test_hover.proto b/src/parser/input/test_hover.proto new file mode 100644 index 0000000..34e6761 --- /dev/null +++ b/src/parser/input/test_hover.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +package com.parser; + +// A Book is book +message Book { + + // This is represents author + // A author is a someone who writes books + // + // Author has a name and a country where they were born + message Author { + string name = 1; + string country = 2; + }; +} + +// Comic is a type of book but who cares +message Comic { + // Author of a comic is different from others + message Author { + string name = 1; + string country = 2; + }; +} diff --git a/src/parser/input/test_rename.proto b/src/parser/input/test_rename.proto new file mode 100644 index 0000000..a10bef9 --- /dev/null +++ b/src/parser/input/test_rename.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package com.parser; + +// A Book is book +message Book { + + // This is represents author + // A author is a someone who writes books + // + // Author has a name and a country where they were born + message Author { + string name = 1; + string country = 2; + }; + Author author = 1; + int price_usd = 2; +} + +message Library { + repeated Book books = 1; + Book.Author collection = 2; +} + +service Myservice { + rpc GetBook(Empty) returns (Book); +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b2563ca..180b99d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1,10 +1,10 @@ +use async_lsp::lsp_types::Url; use tree_sitter::Tree; mod definition; mod diagnostics; mod docsymbol; mod hover; -mod nodekind; mod rename; mod tree; @@ -13,6 +13,7 @@ pub struct ProtoParser { } pub struct ParsedTree { + pub uri: Url, tree: Tree, } @@ -25,9 +26,9 @@ impl ProtoParser { Self { parser } } - pub fn parse(&mut self, contents: impl AsRef<[u8]>) -> Option { + pub fn parse(&mut self, uri: Url, contents: impl AsRef<[u8]>) -> Option { self.parser .parse(contents, None) - .map(|t| ParsedTree { tree: t }) + .map(|t| ParsedTree { tree: t, uri }) } } diff --git a/src/parser/nodekind.rs b/src/parser/nodekind.rs deleted file mode 100644 index cd9130a..0000000 --- a/src/parser/nodekind.rs +++ /dev/null @@ -1,59 +0,0 @@ -use async_lsp::lsp_types::SymbolKind; -use tree_sitter::Node; - -#[allow(unused)] -pub enum NodeKind { - Identifier, - Error, - MessageName, - EnumName, - FieldName, - ServiceName, - RpcName, - PackageName, -} - -#[allow(unused)] -impl NodeKind { - pub fn as_str(&self) -> &'static str { - match self { - NodeKind::Identifier => "identifier", - NodeKind::Error => "ERROR", - NodeKind::MessageName => "message_name", - NodeKind::EnumName => "enum_name", - NodeKind::FieldName => "message_or_enum_type", - NodeKind::ServiceName => "service_name", - NodeKind::RpcName => "rpc_name", - NodeKind::PackageName => "package_name", - } - } - - pub fn is_identifier(n: &Node) -> bool { - n.kind() == "identifier" - } - - pub fn is_error(n: &Node) -> bool { - n.kind() == "ERROR" - } - - pub fn is_userdefined(n: &Node) -> bool { - matches!(n.kind(), "message_name" | "enum_name") - } - - pub fn is_actionable(n: &Node) -> bool { - matches!( - n.kind(), - "message_name" | "enum_name" | "message_or_enum_type" | "rpc_name" | "service_name" - ) - } - - pub fn to_symbolkind(n: &Node) -> SymbolKind { - match n.kind() { - "message_name" => SymbolKind::STRUCT, - "enum_name" => SymbolKind::ENUM, - _ => SymbolKind::NULL, - } - } -} - - diff --git a/src/parser/rename.rs b/src/parser/rename.rs index 67b16c5..e61f231 100644 --- a/src/parser/rename.rs +++ b/src/parser/rename.rs @@ -1,26 +1,29 @@ use std::collections::HashMap; -use async_lsp::lsp_types::{Position, Range, TextEdit, Url, WorkspaceEdit}; +use async_lsp::lsp_types::{Position, Range, TextEdit, WorkspaceEdit}; -use crate::utils::ts_to_lsp_position; +use crate::{nodekind::NodeKind, utils::ts_to_lsp_position}; -use super::{nodekind::NodeKind, ParsedTree }; +use super::ParsedTree; impl ParsedTree { pub fn can_rename(&self, pos: &Position) -> Option { self.get_node_at_position(pos) .filter(NodeKind::is_identifier) - .map(|n| n.parent().unwrap()) // Safety: Identifier must have a parent node - .filter(NodeKind::is_actionable) - .map(|n| Range { - start: ts_to_lsp_position(&n.start_position()), - end: ts_to_lsp_position(&n.end_position()), + .and_then(|n| { + if n.parent().is_some() && NodeKind::is_userdefined(&n.parent().unwrap()) { + Some(Range { + start: ts_to_lsp_position(&n.start_position()), + end: ts_to_lsp_position(&n.end_position()), + }) + } else { + None + } }) } pub fn rename( &self, - uri: &Url, pos: &Position, new_text: &str, content: impl AsRef<[u8]>, @@ -32,7 +35,7 @@ impl ParsedTree { let mut changes = HashMap::new(); let diff: Vec<_> = self - .filter_node(NodeKind::is_identifier) + .filter_nodes(NodeKind::is_identifier) .into_iter() .filter(|n| n.utf8_text(content.as_ref()).unwrap() == old_text) .map(|n| TextEdit { @@ -48,7 +51,7 @@ impl ParsedTree { return None; } - changes.insert(uri.clone(), diff); + changes.insert(self.uri.clone(), diff); Some(WorkspaceEdit { changes: Some(changes), @@ -59,13 +62,14 @@ impl ParsedTree { #[cfg(test)] mod test { - use async_lsp::lsp_types::{Position, Range, TextEdit}; + use async_lsp::lsp_types::{Position, Url}; + use insta::assert_yaml_snapshot; use crate::parser::ProtoParser; #[test] fn test_rename() { - let uri = "file://foo/bar.proto".parse().unwrap(); + let uri: Url = "file://foo/bar.proto".parse().unwrap(); let pos_book_rename = Position { line: 5, character: 9, @@ -78,164 +82,20 @@ mod test { line: 24, character: 4, }; - let contents = r#"syntax = "proto3"; + let contents = include_str!("input/test_rename.proto"); -package com.book; - -// A Book is book -message Book { - - // This is represents author - // A author is a someone who writes books - // - // Author has a name and a country where they were born - message Author { - string name = 1; - string country = 2; - }; - Author author = 1; - int price_usd = 2; -} - -message Library { - repeated Book books = 1; - Book.Author collection = 2; -} - -service Myservice { - rpc GetBook(Empty) returns (Book); -} -"#; - - let parsed = ProtoParser::new().parse(contents); + let parsed = ProtoParser::new().parse(uri.clone(), contents); assert!(parsed.is_some()); let tree = parsed.unwrap(); - let res = tree.rename(&uri, &pos_book_rename, "Kitab", contents); - assert!(res.is_some()); - let changes = res.unwrap().changes; - assert!(changes.is_some()); - let changes = changes.unwrap(); - assert!(changes.contains_key(&uri)); - let edits = changes.get(&uri).unwrap(); - - assert_eq!( - *edits, - vec![ - TextEdit { - range: Range { - start: Position { - line: 5, - character: 8, - }, - end: Position { - line: 5, - character: 12, - }, - }, - new_text: "Kitab".to_string(), - }, - TextEdit { - range: Range { - start: Position { - line: 20, - character: 13, - }, - end: Position { - line: 20, - character: 17, - }, - }, - new_text: "Kitab".to_string(), - }, - TextEdit { - range: Range { - start: Position { - line: 21, - character: 4, - }, - end: Position { - line: 21, - character: 8, - }, - }, - new_text: "Kitab".to_string(), - }, - TextEdit { - range: Range { - start: Position { - line: 25, - character: 32, - }, - end: Position { - line: 25, - character: 36, - }, - }, - new_text: "Kitab".to_string(), - }, - ], - ); - - let res = tree.rename(&uri, &pos_author_rename, "Writer", contents); - assert!(res.is_some()); - let changes = res.unwrap().changes; - assert!(changes.is_some()); - let changes = changes.unwrap(); - assert!(changes.contains_key(&uri)); - let edits = changes.get(&uri).unwrap(); - - assert_eq!( - *edits, - vec![ - TextEdit { - range: Range { - start: Position { - line: 11, - character: 12, - }, - end: Position { - line: 11, - character: 18, - }, - }, - new_text: "Writer".to_string(), - }, - TextEdit { - range: Range { - start: Position { - line: 15, - character: 4, - }, - end: Position { - line: 15, - character: 10, - }, - }, - new_text: "Writer".to_string(), - }, - TextEdit { - range: Range { - start: Position { - line: 21, - character: 9, - }, - end: Position { - line: 21, - character: 15, - }, - }, - new_text: "Writer".to_string(), - }, - ], - ); - - let res = tree.rename(&uri, &pos_non_renamble, "Doesn't matter", contents); - assert!(res.is_none()); + assert_yaml_snapshot!(tree.rename(&pos_book_rename, "Kitab", contents)); + assert_yaml_snapshot!(tree.rename(&pos_author_rename, "Writer", contents)); + assert_yaml_snapshot!(tree.rename(&pos_non_renamble, "Doesn't matter", contents)); } #[test] fn test_can_rename() { + let uri: Url = "file://foo/bar/test.proto".parse().unwrap(); let pos_rename = Position { line: 5, character: 9, @@ -244,44 +104,23 @@ service Myservice { line: 2, character: 2, }; - let contents = r#"syntax = "proto3"; - -package com.book; - -// A Book is book -message Book { + let pos_inner_type = Position { + line: 19, + character: 11, + }; + let pos_outer_type = Position { + line: 19, + character: 5, + }; - // This is represents author - // A author is a someone who writes books - // - // Author has a name and a country where they were born - message Author { - string name = 1; - string country = 2; - }; -} -"#; - let parsed = ProtoParser::new().parse(contents); + let contents = include_str!("input/test_can_rename.proto"); + let parsed = ProtoParser::new().parse(uri.clone(), contents); assert!(parsed.is_some()); - let tree = parsed.unwrap(); - let res = tree.can_rename(&pos_rename); - assert!(res.is_some()); - assert_eq!( - res.unwrap(), - Range { - start: Position { - line: 5, - character: 8 - }, - end: Position { - line: 5, - character: 12 - }, - }, - ); - - let res = tree.can_rename(&pos_non_rename); - assert!(res.is_none()); + 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)); } } 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 new file mode 100644 index 0000000..5b0d62f --- /dev/null +++ b/src/parser/snapshots/protols__parser__definition__test__goto_definition-2.snap @@ -0,0 +1,5 @@ +--- +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 new file mode 100644 index 0000000..3aba38a --- /dev/null +++ b/src/parser/snapshots/protols__parser__definition__test__goto_definition.snap @@ -0,0 +1,12 @@ +--- +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__diagnostics__test__collect_parse_error-2.snap b/src/parser/snapshots/protols__parser__diagnostics__test__collect_parse_error-2.snap new file mode 100644 index 0000000..1dd7eb8 --- /dev/null +++ b/src/parser/snapshots/protols__parser__diagnostics__test__collect_parse_error-2.snap @@ -0,0 +1,16 @@ +--- +source: src/parser/diagnostics.rs +expression: parsed.unwrap().collect_parse_errors() +--- +uri: "file://foo/bar.proto" +diagnostics: + - range: + start: + line: 6 + character: 8 + end: + line: 6 + character: 19 + severity: 1 + source: protols + message: Syntax error diff --git a/src/parser/snapshots/protols__parser__diagnostics__test__collect_parse_error.snap b/src/parser/snapshots/protols__parser__diagnostics__test__collect_parse_error.snap new file mode 100644 index 0000000..3f41aea --- /dev/null +++ b/src/parser/snapshots/protols__parser__diagnostics__test__collect_parse_error.snap @@ -0,0 +1,6 @@ +--- +source: src/parser/diagnostics.rs +expression: parsed.unwrap().collect_parse_errors() +--- +uri: "file://foo/bar.proto" +diagnostics: [] diff --git a/src/parser/snapshots/protols__parser__docsymbol__test__document_symbols.snap b/src/parser/snapshots/protols__parser__docsymbol__test__document_symbols.snap new file mode 100644 index 0000000..a85f83b --- /dev/null +++ b/src/parser/snapshots/protols__parser__docsymbol__test__document_symbols.snap @@ -0,0 +1,109 @@ +--- +source: src/parser/docsymbol.rs +expression: tree.find_document_locations(contents) +--- +- name: Outer1 + detail: outer 1 comment + kind: 23 + range: + start: + line: 5 + character: 0 + end: + line: 12 + character: 1 + selectionRange: + start: + line: 5 + character: 8 + end: + line: 5 + character: 14 + children: + - name: Inner1 + detail: Inner 1 + kind: 23 + range: + start: + line: 7 + character: 4 + end: + line: 9 + character: 5 + selectionRange: + start: + line: 7 + character: 12 + end: + line: 7 + character: 18 + children: [] +- name: Outer2 + kind: 23 + range: + start: + line: 14 + character: 0 + end: + line: 29 + character: 1 + selectionRange: + start: + line: 14 + character: 8 + end: + line: 14 + character: 14 + children: + - name: Inner2 + kind: 23 + range: + start: + line: 15 + character: 4 + end: + line: 17 + character: 5 + selectionRange: + start: + line: 15 + character: 12 + end: + line: 15 + character: 18 + children: [] + - name: Inner3 + detail: Inner 3 comment here + kind: 23 + range: + start: + line: 19 + character: 4 + end: + line: 26 + character: 5 + selectionRange: + start: + line: 19 + character: 12 + end: + line: 19 + character: 18 + children: + - name: X + kind: 10 + range: + start: + line: 22 + character: 8 + end: + line: 25 + character: 9 + selectionRange: + start: + line: 22 + character: 13 + end: + line: 22 + character: 14 + children: [] diff --git a/src/parser/snapshots/protols__parser__hover__test__hover-2.snap b/src/parser/snapshots/protols__parser__hover__test__hover-2.snap new file mode 100644 index 0000000..c7defde --- /dev/null +++ b/src/parser/snapshots/protols__parser__hover__test__hover-2.snap @@ -0,0 +1,5 @@ +--- +source: src/parser/hover.rs +expression: res +--- +[] diff --git a/src/parser/snapshots/protols__parser__hover__test__hover-3.snap b/src/parser/snapshots/protols__parser__hover__test__hover-3.snap new file mode 100644 index 0000000..4cdfa74 --- /dev/null +++ b/src/parser/snapshots/protols__parser__hover__test__hover-3.snap @@ -0,0 +1,5 @@ +--- +source: src/parser/hover.rs +expression: res +--- +- "This is represents author\nA author is a someone who writes books\n\nAuthor has a name and a country where they were born" diff --git a/src/parser/snapshots/protols__parser__hover__test__hover-4.snap b/src/parser/snapshots/protols__parser__hover__test__hover-4.snap new file mode 100644 index 0000000..c850beb --- /dev/null +++ b/src/parser/snapshots/protols__parser__hover__test__hover-4.snap @@ -0,0 +1,5 @@ +--- +source: src/parser/hover.rs +expression: res +--- +- Author of a comic is different from others diff --git a/src/parser/snapshots/protols__parser__hover__test__hover-5.snap b/src/parser/snapshots/protols__parser__hover__test__hover-5.snap new file mode 100644 index 0000000..f2900d1 --- /dev/null +++ b/src/parser/snapshots/protols__parser__hover__test__hover-5.snap @@ -0,0 +1,6 @@ +--- +source: src/parser/hover.rs +expression: res +--- +- "This is represents author\nA author is a someone who writes books\n\nAuthor has a name and a country where they were born" +- Author of a comic is different from others diff --git a/src/parser/snapshots/protols__parser__hover__test__hover.snap b/src/parser/snapshots/protols__parser__hover__test__hover.snap new file mode 100644 index 0000000..72d7361 --- /dev/null +++ b/src/parser/snapshots/protols__parser__hover__test__hover.snap @@ -0,0 +1,5 @@ +--- +source: src/parser/hover.rs +expression: res +--- +- A Book is book 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 new file mode 100644 index 0000000..4683ffb --- /dev/null +++ b/src/parser/snapshots/protols__parser__rename__test__can_rename-2.snap @@ -0,0 +1,5 @@ +--- +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 new file mode 100644 index 0000000..9292d37 --- /dev/null +++ b/src/parser/snapshots/protols__parser__rename__test__can_rename-3.snap @@ -0,0 +1,5 @@ +--- +source: src/parser/rename.rs +expression: tree.can_rename(&pos_inner_type) +--- +~ 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 new file mode 100644 index 0000000..9545453 --- /dev/null +++ b/src/parser/snapshots/protols__parser__rename__test__can_rename-4.snap @@ -0,0 +1,5 @@ +--- +source: src/parser/rename.rs +expression: tree.can_rename(&pos_outer_type) +--- +~ diff --git a/src/parser/snapshots/protols__parser__rename__test__can_rename.snap b/src/parser/snapshots/protols__parser__rename__test__can_rename.snap new file mode 100644 index 0000000..6a024e6 --- /dev/null +++ b/src/parser/snapshots/protols__parser__rename__test__can_rename.snap @@ -0,0 +1,10 @@ +--- +source: src/parser/rename.rs +expression: tree.can_rename(&pos_rename) +--- +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 new file mode 100644 index 0000000..a0eebe6 --- /dev/null +++ b/src/parser/snapshots/protols__parser__rename__test__rename-2.snap @@ -0,0 +1,30 @@ +--- +source: src/parser/rename.rs +expression: "tree.rename(&pos_author_rename, \"Writer\", contents)" +--- +changes: + "file://foo/bar.proto": + - 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 + - range: + start: + line: 21 + character: 9 + end: + line: 21 + character: 15 + 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 new file mode 100644 index 0000000..9fe1587 --- /dev/null +++ b/src/parser/snapshots/protols__parser__rename__test__rename-3.snap @@ -0,0 +1,5 @@ +--- +source: src/parser/rename.rs +expression: "tree.rename(&pos_non_renamble, \"Doesn't matter\", contents)" +--- +~ diff --git a/src/parser/snapshots/protols__parser__rename__test__rename.snap b/src/parser/snapshots/protols__parser__rename__test__rename.snap new file mode 100644 index 0000000..91465d7 --- /dev/null +++ b/src/parser/snapshots/protols__parser__rename__test__rename.snap @@ -0,0 +1,38 @@ +--- +source: src/parser/rename.rs +expression: "tree.rename(&pos_book_rename, \"Kitab\", contents)" +--- +changes: + "file://foo/bar.proto": + - range: + start: + line: 5 + character: 8 + end: + line: 5 + character: 12 + newText: Kitab + - range: + start: + line: 20 + character: 13 + end: + line: 20 + character: 17 + newText: Kitab + - range: + start: + line: 21 + character: 4 + end: + line: 21 + character: 8 + newText: Kitab + - range: + start: + line: 25 + character: 32 + end: + line: 25 + character: 36 + newText: Kitab diff --git a/src/parser/snapshots/protols__parser__tree__test__filter-2.snap b/src/parser/snapshots/protols__parser__tree__test__filter-2.snap new file mode 100644 index 0000000..022e111 --- /dev/null +++ b/src/parser/snapshots/protols__parser__tree__test__filter-2.snap @@ -0,0 +1,5 @@ +--- +source: src/parser/tree.rs +expression: package_name +--- +com.parser diff --git a/src/parser/snapshots/protols__parser__tree__test__filter.snap b/src/parser/snapshots/protols__parser__tree__test__filter.snap new file mode 100644 index 0000000..6627aea --- /dev/null +++ b/src/parser/snapshots/protols__parser__tree__test__filter.snap @@ -0,0 +1,6 @@ +--- +source: src/parser/tree.rs +expression: names +--- +- Book +- Author diff --git a/src/parser/tree.rs b/src/parser/tree.rs index 9eb2835..fc82d40 100644 --- a/src/parser/tree.rs +++ b/src/parser/tree.rs @@ -1,14 +1,15 @@ use async_lsp::lsp_types::Position; use tree_sitter::{Node, TreeCursor}; -use crate::utils::lsp_to_ts_point; +use crate::{nodekind::NodeKind, utils::lsp_to_ts_point}; -use super::{nodekind::NodeKind, ParsedTree}; +use super::ParsedTree; impl ParsedTree { pub(super) fn walk_and_collect_filter<'a>( cursor: &mut TreeCursor<'a>, f: fn(&Node) -> bool, + early: bool, ) -> Vec> { let mut v = vec![]; @@ -16,11 +17,14 @@ impl ParsedTree { let node = cursor.node(); if f(&node) { - v.push(node) + v.push(node); + if early { + break; + } } if cursor.goto_first_child() { - v.extend(Self::walk_and_collect_filter(cursor, f)); + v.extend(Self::walk_and_collect_filter(cursor, f, early)); cursor.goto_parent(); } @@ -50,7 +54,7 @@ impl ParsedTree { } } - pub(super) fn get_node_text_at_position<'a>( + pub fn get_node_text_at_position<'a>( &'a self, pos: &Position, content: &'a [u8], @@ -59,7 +63,7 @@ impl ParsedTree { .map(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error")) } - pub(super) fn get_actionable_node_text_at_position<'a>( + pub fn get_actionable_node_text_at_position<'a>( &'a self, pos: &Position, content: &'a [u8], @@ -68,10 +72,7 @@ impl ParsedTree { .map(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error")) } - pub(super) fn get_actionable_node_at_position<'a>( - &'a self, - pos: &Position, - ) -> Option> { + pub fn get_actionable_node_at_position<'a>(&'a self, pos: &Position) -> Option> { self.get_node_at_position(pos) .map(|n| { if NodeKind::is_actionable(&n) { @@ -83,19 +84,40 @@ impl ParsedTree { .filter(NodeKind::is_actionable) } - pub(super) fn get_node_at_position<'a>(&'a self, pos: &Position) -> Option> { + pub fn get_node_at_position<'a>(&'a self, pos: &Position) -> Option> { let pos = lsp_to_ts_point(pos); self.tree.root_node().descendant_for_point_range(pos, pos) } - pub(super) fn filter_node(&self, f: fn(&Node) -> bool) -> Vec { - let mut cursor = self.tree.root_node().walk(); - Self::walk_and_collect_filter(&mut cursor, f) + pub fn filter_nodes(&self, f: fn(&Node) -> bool) -> Vec { + self.filter_nodes_from(self.tree.root_node(), f) + } + + pub fn filter_nodes_from<'a>(&self, n: Node<'a>, f: fn(&Node) -> bool) -> Vec> { + let mut cursor = n.walk(); + Self::walk_and_collect_filter(&mut cursor, f, false) + } + + pub fn find_node(&self, f: fn(&Node) -> bool) -> Vec { + self.find_node_from(self.tree.root_node(), f) + } + + pub fn find_node_from<'a>(&self, n: Node<'a>, f: fn(&Node) -> bool) -> Vec> { + let mut cursor = n.walk(); + Self::walk_and_collect_filter(&mut cursor, f, true) + } + + pub fn get_package_name<'a>(&self, content: &'a [u8]) -> Option<&'a str> { + self.find_node(NodeKind::is_package_name) + .first() + .map(|n| n.utf8_text(content).expect("utf-8 parse error")) } } #[cfg(test)] mod test { + use async_lsp::lsp_types::Url; + use insta::assert_yaml_snapshot; use tree_sitter::Node; use crate::parser::ProtoParser; @@ -105,28 +127,14 @@ mod test { } #[test] - fn test_find_children_by_kind() { - let contents = r#"syntax = "proto3"; - -package com.book; - -message Book { - - message Author { - string name = 1; - string country = 2; - }; - // This is a multi line comment on the field name - // Of a message called Book - int64 isbn = 1; - string title = 2; - Author author = 3; -} -"#; - let parsed = ProtoParser::new().parse(contents); + 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); + assert!(parsed.is_some()); let tree = parsed.unwrap(); - let nodes = tree.filter_node(is_message); + let nodes = tree.filter_nodes(is_message); assert_eq!(nodes.len(), 2); @@ -134,7 +142,10 @@ message Book { .into_iter() .map(|n| n.utf8_text(contents.as_ref()).unwrap()) .collect(); - assert_eq!(names[0], "Book"); - assert_eq!(names[1], "Author"); + + assert_yaml_snapshot!(names); + + let package_name = tree.get_package_name(contents.as_ref()); + assert_yaml_snapshot!(package_name); } } diff --git a/src/server.rs b/src/server.rs index 8082746..86a3f13 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,31 +1,21 @@ -use async_lsp::{ - lsp_types::{Url, WorkspaceFolder}, - router::Router, - ClientSocket, ErrorCode, ResponseError, -}; -use std::{collections::HashMap, fs::read_to_string, ops::ControlFlow}; -use tracing::{error, info}; -use walkdir::WalkDir; +use async_lsp::{router::Router, ClientSocket}; +use std::ops::ControlFlow; -use crate::parser::{ParsedTree, ProtoParser}; +use crate::state::ProtoLanguageState; pub struct TickEvent; -pub struct ServerState { +pub struct ProtoLanguageServer { pub client: ClientSocket, pub counter: i32, - pub documents: HashMap, - pub trees: HashMap, - pub parser: ProtoParser, + pub state: ProtoLanguageState, } -impl ServerState { +impl ProtoLanguageServer { pub fn new_router(client: ClientSocket) -> Router { let mut router = Router::from_language_server(Self { client, counter: 0, - documents: Default::default(), - trees: Default::default(), - parser: ProtoParser::new(), + state: ProtoLanguageState::new(), }); router.event(Self::on_tick); router @@ -35,64 +25,4 @@ impl ServerState { self.counter += 1; ControlFlow::Continue(()) } - - pub fn get_parsed_tree_and_content( - &mut self, - uri: &Url, - ) -> Result<(&ParsedTree, &str), ResponseError> { - let Some(content) = self.documents.get(uri) else { - error!("failed to get document at {uri}"); - return Err(ResponseError::new( - ErrorCode::INVALID_REQUEST, - "uri was never opened", - )); - }; - - if !self.trees.contains_key(uri) { - let Some(parsed) = self.parser.parse(content.as_bytes()) else { - error!("failed to parse content at {uri}"); - return Err(ResponseError::new( - ErrorCode::REQUEST_FAILED, - "ts failed to parse contents", - )); - }; - self.trees.insert(uri.clone(), parsed); - } - - let parsed = self.trees.get(uri).unwrap(); // Safety: already inserted above - Ok((parsed, content)) - } - - pub fn add_workspace_folder(&mut self, workspace: WorkspaceFolder) { - for entry in WalkDir::new(workspace.uri.path()) - .into_iter() - .filter_map(|e| e.ok()) - { - let path = entry.path(); - if path.is_absolute() && path.is_file() { - let Some(ext) = path.extension() else { - continue; - }; - - let Ok(content) = read_to_string(path) else { - continue; - }; - - let Ok(uri) = Url::from_file_path(path) else { - continue; - }; - - if ext == "proto" { - self.documents.insert(uri.clone(), content); - let r = self.get_parsed_tree_and_content(&uri); - - info!( - "workspace parse file: {}, result: {}", - path.display(), - r.is_ok() - ); - } - } - } - } } diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..899eed3 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,142 @@ +use std::{collections::HashMap, fs::read_to_string}; +use tracing::{error, info}; + +use async_lsp::lsp_types::{ + CompletionItem, CompletionItemKind, PublishDiagnosticsParams, Url, WorkspaceFolder, +}; +use tree_sitter::Node; +use walkdir::WalkDir; + +use crate::{ + nodekind::NodeKind, + parser::{ParsedTree, ProtoParser}, +}; + +pub struct ProtoLanguageState { + documents: HashMap, + pub trees: HashMap, + parser: ProtoParser, +} + +impl ProtoLanguageState { + pub fn new() -> Self { + ProtoLanguageState { + documents: Default::default(), + trees: Default::default(), + parser: ProtoParser::new(), + } + } + + pub fn get_content(&self, uri: &Url) -> &str { + self.documents + .get(uri) + .map(|s| s.as_str()) + .unwrap_or_default() + } + + pub fn get_tree(&self, uri: &Url) -> Option<&ParsedTree> { + self.trees.get(uri) + } + + pub fn get_trees_for_package(&self, package: &str) -> Vec<&ParsedTree> { + self.trees + .values() + .filter(|tree| { + let content = self.get_content(&tree.uri); + tree.get_package_name(content.as_bytes()) + .unwrap_or_default() + == package + }) + .collect() + } + + pub fn upsert_content(&mut self, uri: &Url, content: String) -> bool { + if let Some(parsed) = self.parser.parse(uri.clone(), content.as_bytes()) { + self.trees.insert(uri.clone(), parsed); + self.documents.insert(uri.clone(), content); + true + } else { + error!(uri=%uri, "failed to parse content"); + false + } + } + + pub fn add_workspace_folder(&mut self, workspace: WorkspaceFolder) { + for entry in WalkDir::new(workspace.uri.path()) + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry.path(); + if path.is_absolute() && path.is_file() { + let Some(ext) = path.extension() else { + continue; + }; + + let Ok(content) = read_to_string(path) else { + continue; + }; + + let Ok(uri) = Url::from_file_path(path) else { + continue; + }; + + if ext == "proto" { + let r = self.upsert_content(&uri, content); + info!("workspace parse file: {}, result: {}", path.display(), r); + } + } + } + } + + pub fn upsert_file(&mut self, uri: &Url, content: String) -> Option { + info!(uri=%uri, "upserting file"); + self.upsert_content(uri, content); + self.get_tree(uri).map(|tree| tree.collect_parse_errors()) + } + + pub fn delete_file(&mut self, uri: &Url) { + info!(uri=%uri, "deleting file"); + self.documents.remove(uri); + self.trees.remove(uri); + } + + pub fn rename_file(&mut self, new_uri: &Url, old_uri: &Url) { + info!(new_uri=%new_uri, old_uri=%new_uri, "renaming file"); + + if let Some(v) = self.documents.remove(old_uri) { + self.documents.insert(new_uri.clone(), v); + } + + if let Some(mut v) = self.trees.remove(old_uri) { + v.uri = new_uri.clone(); + self.trees.insert(new_uri.clone(), v); + } + } + + pub fn completion_items(&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.filter_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, + )); + // Better ways to dedup, but who cares?... + result.sort_by_key(|k| k.label.clone()); + result.dedup_by_key(|k| k.label.clone()); + result + } +} diff --git a/src/utils.rs b/src/utils.rs index 4b26ccb..542d7f1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -14,3 +14,76 @@ pub fn lsp_to_ts_point(p: &Position) -> Point { column: p.character as usize, } } + +fn is_title_case(s: &str) -> bool { + s.chars() + .next() + .map(|x| x.is_uppercase()) + .unwrap_or_default() +} + +fn is_first_lower_case(s: &&str) -> bool { + s.chars() + .next() + .map(|x| x.is_lowercase()) + .unwrap_or_default() +} + +pub fn is_inner_identifier(s: &str) -> bool { + if !s.contains(".") { + return false; + } + s.split(".").all(is_title_case) +} + +pub fn split_identifier_package(s: &str) -> (&str, &str) { + if is_inner_identifier(s) || !s.contains(".") { + return ("", s); + } + + let i = s + .split(".") + .take_while(is_first_lower_case) + .fold(0, |mut c, s| { + if c != 0 { + c += 1; + } + c += s.len(); + c + }); + + let (package, identifier) = s.split_at(i); + return (package, identifier.trim_matches('.')); +} + +#[cfg(test)] +mod test { + use crate::utils::{is_inner_identifier, split_identifier_package}; + + #[test] + fn test_is_inner_identifier() { + assert!(is_inner_identifier("Book.Author")); + assert!(is_inner_identifier("Book.Author.Address")); + + assert!(!is_inner_identifier("com.book.Foo")); + assert!(!is_inner_identifier("Book")); + assert!(!is_inner_identifier("foo.Bar")); + } + + #[test] + fn test_split_identifier_package() { + assert_eq!( + split_identifier_package("com.book.Book"), + ("com.book", "Book") + ); + assert_eq!( + split_identifier_package("com.book.Book.Author"), + ("com.book", "Book.Author") + ); + + assert_eq!(split_identifier_package("com.Book"), ("com", "Book")); + assert_eq!(split_identifier_package("Book"), ("", "Book")); + assert_eq!(split_identifier_package("Book.Author"), ("", "Book.Author")); + assert_eq!(split_identifier_package("com.book"), ("com.book", "")); + } +} diff --git a/src/workspace/definition.rs b/src/workspace/definition.rs new file mode 100644 index 0000000..f0be3f3 --- /dev/null +++ b/src/workspace/definition.rs @@ -0,0 +1,46 @@ +use async_lsp::lsp_types::Location; + +use crate::{state::ProtoLanguageState, utils::split_identifier_package}; + +impl ProtoLanguageState { + pub fn definition(&self, curr_package: &str, identifier: &str) -> Vec { + let (mut package, identifier) = split_identifier_package(identifier); + if package.is_empty() { + package = curr_package; + } + self.get_trees_for_package(package) + .into_iter() + .fold(vec![], |mut v, tree| { + v.extend(tree.definition(identifier, self.get_content(&tree.uri))); + v + }) + } +} + +#[cfg(test)] +mod test { + use insta::assert_yaml_snapshot; + + use crate::state::ProtoLanguageState; + + #[test] + fn workspace_test_definition() { + 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::new(); + state.upsert_file(&a_uri, a.to_owned()); + state.upsert_file(&b_uri, b.to_owned()); + state.upsert_file(&c_uri, c.to_owned()); + + assert_yaml_snapshot!(state.definition("com.workspace", "Author")); + assert_yaml_snapshot!(state.definition("com.workspace", "Author.Address")); + assert_yaml_snapshot!(state.definition("com.workspace", "com.utility.Foobar.Baz")); + assert_yaml_snapshot!(state.definition("com.utility", "Baz")); + } +} diff --git a/src/workspace/hover.rs b/src/workspace/hover.rs new file mode 100644 index 0000000..a71f68b --- /dev/null +++ b/src/workspace/hover.rs @@ -0,0 +1,47 @@ +use async_lsp::lsp_types::MarkedString; + +use crate::{state::ProtoLanguageState, utils::split_identifier_package}; + +impl ProtoLanguageState { + pub fn hover(&self, curr_package: &str, identifier: &str) -> Vec { + let (mut package, identifier) = split_identifier_package(identifier); + if package.is_empty() { + package = curr_package; + } + + self.get_trees_for_package(package) + .into_iter() + .fold(vec![], |mut v, tree| { + v.extend(tree.hover(identifier, self.get_content(&tree.uri))); + v + }) + } +} + +#[cfg(test)] +mod test { + use insta::assert_yaml_snapshot; + + use crate::state::ProtoLanguageState; + + #[test] + fn workspace_test_hover() { + 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::new(); + state.upsert_file(&a_uri, a.to_owned()); + state.upsert_file(&b_uri, b.to_owned()); + state.upsert_file(&c_uri, c.to_owned()); + + assert_yaml_snapshot!(state.hover("com.workspace", "Author")); + assert_yaml_snapshot!(state.hover("com.workspace", "Author.Address")); + assert_yaml_snapshot!(state.hover("com.workspace", "com.utility.Foobar.Baz")); + assert_yaml_snapshot!(state.hover("com.utility", "Baz")); + } +} diff --git a/src/workspace/input/a.proto b/src/workspace/input/a.proto new file mode 100644 index 0000000..a982e4a --- /dev/null +++ b/src/workspace/input/a.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package com.workspace; + +import "c.proto"; + +// A Book is a book +message Book { + Author author = 1; + Author.Address foo = 2; + com.utility.FooBar.Baz z = 3; +} + diff --git a/src/workspace/input/b.proto b/src/workspace/input/b.proto new file mode 100644 index 0000000..668c23b --- /dev/null +++ b/src/workspace/input/b.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package com.workspace; + +// A author is a author +message Author { + string name = 1; + + // Address is a Address + message Address { + int64 zip = 1; + } + + Address foo = 2; +} diff --git a/src/workspace/input/c.proto b/src/workspace/input/c.proto new file mode 100644 index 0000000..f3fc7ab --- /dev/null +++ b/src/workspace/input/c.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package com.utility; + +// A foobar is a dummy message +message Foobar { + + // What is baz? + message Baz { + int64 b = 1; + } + + Baz a = 2; +} + + diff --git a/src/workspace/mod.rs b/src/workspace/mod.rs new file mode 100644 index 0000000..9a26930 --- /dev/null +++ b/src/workspace/mod.rs @@ -0,0 +1,2 @@ +mod definition; +mod hover; diff --git a/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-2.snap b/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-2.snap new file mode 100644 index 0000000..671dc99 --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-2.snap @@ -0,0 +1,12 @@ +--- +source: src/workspace/definition.rs +expression: "state.definition(\"com.library\", \"Author.Address\")" +--- +- uri: "file://input/b.proto" + range: + start: + line: 9 + character: 11 + end: + line: 9 + character: 18 diff --git a/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-3.snap b/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-3.snap new file mode 100644 index 0000000..3d795a3 --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-3.snap @@ -0,0 +1,12 @@ +--- +source: src/workspace/definition.rs +expression: "state.definition(\"com.library\", \"com.utility.Foobar.Baz\")" +--- +- uri: "file://input/c.proto" + range: + start: + line: 8 + character: 11 + end: + line: 8 + character: 14 diff --git a/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-4.snap b/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-4.snap new file mode 100644 index 0000000..1bb9bd2 --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-4.snap @@ -0,0 +1,12 @@ +--- +source: src/workspace/definition.rs +expression: "state.definition(\"com.utility\", \"Baz\")" +--- +- uri: "file://input/c.proto" + range: + start: + line: 8 + character: 11 + end: + line: 8 + character: 14 diff --git a/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition.snap b/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition.snap new file mode 100644 index 0000000..8bd8ef2 --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition.snap @@ -0,0 +1,12 @@ +--- +source: src/workspace/definition.rs +expression: "state.definition(\"com.library\", \"Author\")" +--- +- uri: "file://input/b.proto" + range: + start: + line: 5 + character: 8 + end: + line: 5 + character: 14 diff --git a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-2.snap b/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-2.snap new file mode 100644 index 0000000..55ec65d --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-2.snap @@ -0,0 +1,5 @@ +--- +source: src/workspace/hover.rs +expression: "state.hover(\"com.library\", \"Author.Address\")" +--- +- Address is a Address diff --git a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-3.snap b/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-3.snap new file mode 100644 index 0000000..7e3ab87 --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-3.snap @@ -0,0 +1,5 @@ +--- +source: src/workspace/hover.rs +expression: "state.hover(\"com.library\", \"com.utility.Foobar.Baz\")" +--- +- What is baz? diff --git a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-4.snap b/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-4.snap new file mode 100644 index 0000000..130490c --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-4.snap @@ -0,0 +1,5 @@ +--- +source: src/workspace/hover.rs +expression: "state.hover(\"com.utility\", \"Baz\")" +--- +- What is baz? diff --git a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover.snap b/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover.snap new file mode 100644 index 0000000..43180c0 --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover.snap @@ -0,0 +1,5 @@ +--- +source: src/workspace/hover.rs +expression: "state.hover(\"com.library\", \"Author\")" +--- +- A author is a author