From 11a0324f2319e1aa7d886392264f03eba97a61fd Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sat, 17 Aug 2024 13:10:11 +0530 Subject: [PATCH 01/18] feat: add workspace support --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index adc14a4..f9066ab 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" From a553587e7a7c10958887bea248e512eda7c4eccd Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sat, 17 Aug 2024 15:59:06 +0530 Subject: [PATCH 02/18] workspace: add file update handlers --- Cargo.lock | 2 +- README.md | 6 +- src/lsp.rs | 112 +++++++++++++++++++++++++++----------- src/parser/definition.rs | 17 ++---- src/parser/diagnostics.rs | 21 ++++--- src/parser/docsymbol.rs | 7 ++- src/parser/hover.rs | 6 +- src/parser/mod.rs | 6 +- src/parser/nodekind.rs | 2 - src/parser/rename.rs | 22 ++++---- src/parser/tree.rs | 4 +- src/server.rs | 39 ++++++++++++- 12 files changed, 161 insertions(+), 83 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 603f1e0..0baecf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -435,7 +435,7 @@ dependencies = [ [[package]] name = "protols" -version = "0.3.0" +version = "0.4.0" dependencies = [ "async-lsp", "futures", diff --git a/README.md b/README.md index acf9f18..1e1b7a9 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # protols -A Language Server for **proto3** files. It uses tree-sitter parser for all operations and always runs in **single file mode**. +A Language Server for **proto3** files. It uses tree-sitter parser for all operations. ![](./assets/protols.mov) ## Features -- [x] Hover +- [x] Hover - [x] Go to definition - [x] Diagnostics -- [x] Document Symbols for message and enums +- [x] Document Symbols for message and enums in current document - [x] Rename message, enum and rpc - [x] Completion for proto3 keywords diff --git a/src/lsp.rs b/src/lsp.rs index bf9405a..894e6f3 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -1,14 +1,17 @@ +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, RenameParams, ServerCapabilities, ServerInfo, + TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, Url, + WorkspaceEdit, WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities, }; use async_lsp::{LanguageClient, LanguageServer, ResponseError}; @@ -34,6 +37,19 @@ 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 { @@ -45,7 +61,13 @@ impl LanguageServer for ServerState { 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() + }), }) } @@ -152,7 +174,7 @@ impl LanguageServer for ServerState { 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) + tree.rename(&pos, &new_name, content) } else { None }; @@ -172,7 +194,7 @@ impl LanguageServer for ServerState { 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 locations = tree.definition(&pos, content.as_bytes()); let response = match locations.len() { 0 => None, @@ -212,42 +234,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 Some(tree) = self.parser.parse(contents.as_bytes()) else { - error!("failed to parse content at {uri}"); - return ControlFlow::Continue(()); - }; + let content = params.text_document.text; - 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.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.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.upsert_file(&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 diagnostics = tree.collect_parse_errors(&uri); - if let Err(e) = self.client.publish_diagnostics(diagnostics) { - error!(error=%e, "failed to publish diagnostics") + let Ok(old_uri) = Url::from_file_path(&file.old_uri) else { + error!(uri = file.old_uri, "failed to parse uri"); + continue; + }; + + self.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.delete_file(&uri); + } else { + error!(uri = file.uri, "failed to parse uri"); + } + } ControlFlow::Continue(()) } } diff --git a/src/parser/definition.rs b/src/parser/definition.rs index 84fc540..5c6de35 100644 --- a/src/parser/definition.rs +++ b/src/parser/definition.rs @@ -1,4 +1,4 @@ -use async_lsp::lsp_types::{Location, Position, Range, Url}; +use async_lsp::lsp_types::{Location, Position, Range}; use tracing::info; use crate::{parser::nodekind::NodeKind, utils::ts_to_lsp_position}; @@ -6,12 +6,7 @@ use crate::{parser::nodekind::NodeKind, utils::ts_to_lsp_position}; use super::ParsedTree; impl ParsedTree { - pub fn definition( - &self, - pos: &Position, - uri: &Url, - content: impl AsRef<[u8]>, - ) -> Vec { + pub fn definition(&self, pos: &Position, content: impl AsRef<[u8]>) -> Vec { let text = self.get_node_text_at_position(pos, content.as_ref()); info!("Looking for definition of: {:?}", text); @@ -21,7 +16,7 @@ impl ParsedTree { .into_iter() .filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == text) .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()), @@ -64,10 +59,10 @@ message Book { string isbn = 2; } "#; - let parsed = ProtoParser::new().parse(contents); + let parsed = ProtoParser::new().parse(url.parse().unwrap(), contents); assert!(parsed.is_some()); let tree = parsed.unwrap(); - let res = tree.definition(&posauthor, &url.parse().unwrap(), contents); + let res = tree.definition(&posauthor, contents); assert_eq!(res.len(), 1); assert_eq!(res[0].uri, Url::parse(url).unwrap()); @@ -85,7 +80,7 @@ message Book { } ); - let res = tree.definition(&posinvalid, &url.parse().unwrap(), contents); + let res = tree.definition(&posinvalid, contents); assert_eq!(res.len(), 0); } } diff --git a/src/parser/diagnostics.rs b/src/parser/diagnostics.rs index ae05c65..0110e3a 100644 --- a/src/parser/diagnostics.rs +++ b/src/parser/diagnostics.rs @@ -1,11 +1,11 @@ -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 super::{nodekind::NodeKind, 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) .into_iter() @@ -21,7 +21,7 @@ impl ParsedTree { }) .collect(); PublishDiagnosticsParams { - uri: uri.clone(), + uri: self.uri.clone(), diagnostics, version: None, } @@ -36,7 +36,7 @@ mod test { #[test] fn test_collect_parse_error() { - let url = "file://foo/bar.proto"; + let url: Url = "file://foo/bar.proto".parse().unwrap(); let contents = r#"syntax = "proto3"; package test; @@ -47,14 +47,13 @@ message Foo { 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()); + let diagnostics = tree.collect_parse_errors(); + assert_eq!(diagnostics.uri, url); assert_eq!(diagnostics.diagnostics.len(), 0); - let url = "file://foo/bar.proto"; let contents = r#"syntax = "proto3"; package com.book; @@ -65,12 +64,12 @@ message Book { 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()); + let diagnostics = tree.collect_parse_errors(); - assert_eq!(diagnostics.uri, Url::parse(url).unwrap()); + assert_eq!(diagnostics.uri, url); assert_eq!(diagnostics.diagnostics.len(), 1); let error = &diagnostics.diagnostics[0]; diff --git a/src/parser/docsymbol.rs b/src/parser/docsymbol.rs index 1982f66..90b4d27 100644 --- a/src/parser/docsymbol.rs +++ b/src/parser/docsymbol.rs @@ -3,7 +3,7 @@ use tree_sitter::TreeCursor; use crate::utils::ts_to_lsp_position; -use super::{ nodekind::NodeKind, ParsedTree}; +use super::{nodekind::NodeKind, ParsedTree}; #[derive(Default)] pub(super) struct DocumentSymbolTreeBuilder { @@ -98,13 +98,14 @@ impl ParsedTree { #[cfg(test)] mod test { - use async_lsp::lsp_types::{DocumentSymbol, Position, Range, SymbolKind}; + use async_lsp::lsp_types::{DocumentSymbol, Position, Range, SymbolKind, Url}; use crate::parser::ProtoParser; #[test] #[allow(deprecated)] fn test_document_symbols() { + let uri: Url = "file://foo/bar/pro.proto".parse().unwrap(); let contents = r#"syntax = "proto3"; package com.symbols; @@ -136,7 +137,7 @@ message Outer2 { } "#; - 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); diff --git a/src/parser/hover.rs b/src/parser/hover.rs index a1e4b26..69ca3c0 100644 --- a/src/parser/hover.rs +++ b/src/parser/hover.rs @@ -1,7 +1,6 @@ use async_lsp::lsp_types::{MarkedString, Position}; use tracing::info; - use crate::parser::nodekind::NodeKind; use super::ParsedTree; @@ -67,12 +66,13 @@ impl ParsedTree { #[cfg(test)] mod test { - use async_lsp::lsp_types::{MarkedString, Position}; + use async_lsp::lsp_types::{MarkedString, Position, Url}; use crate::parser::ProtoParser; #[test] fn test_hover() { + let uri: Url = "file://foo.bar/p.proto".parse().unwrap(); let posbook = Position { line: 5, character: 9, @@ -102,7 +102,7 @@ message 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.hover(&posbook, contents); diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b2563ca..51e0336 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1,3 +1,4 @@ +use async_lsp::lsp_types::Url; use tree_sitter::Tree; mod definition; @@ -13,6 +14,7 @@ pub struct ProtoParser { } pub struct ParsedTree { + pub uri: Url, tree: Tree, } @@ -25,9 +27,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 index cd9130a..4c1c2e8 100644 --- a/src/parser/nodekind.rs +++ b/src/parser/nodekind.rs @@ -55,5 +55,3 @@ impl NodeKind { } } } - - diff --git a/src/parser/rename.rs b/src/parser/rename.rs index 67b16c5..3d46e2e 100644 --- a/src/parser/rename.rs +++ b/src/parser/rename.rs @@ -1,10 +1,10 @@ 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 super::{nodekind::NodeKind, ParsedTree }; +use super::{nodekind::NodeKind, ParsedTree}; impl ParsedTree { pub fn can_rename(&self, pos: &Position) -> Option { @@ -20,7 +20,6 @@ impl ParsedTree { pub fn rename( &self, - uri: &Url, pos: &Position, new_text: &str, content: impl AsRef<[u8]>, @@ -48,7 +47,7 @@ impl ParsedTree { return None; } - changes.insert(uri.clone(), diff); + changes.insert(self.uri.clone(), diff); Some(WorkspaceEdit { changes: Some(changes), @@ -59,13 +58,13 @@ impl ParsedTree { #[cfg(test)] mod test { - use async_lsp::lsp_types::{Position, Range, TextEdit}; + use async_lsp::lsp_types::{Position, Range, TextEdit, Url}; 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, @@ -107,11 +106,11 @@ service Myservice { } "#; - 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); + let res = tree.rename(&pos_book_rename, "Kitab", contents); assert!(res.is_some()); let changes = res.unwrap().changes; assert!(changes.is_some()); @@ -177,7 +176,7 @@ service Myservice { ], ); - let res = tree.rename(&uri, &pos_author_rename, "Writer", contents); + let res = tree.rename(&pos_author_rename, "Writer", contents); assert!(res.is_some()); let changes = res.unwrap().changes; assert!(changes.is_some()); @@ -230,12 +229,13 @@ service Myservice { ], ); - let res = tree.rename(&uri, &pos_non_renamble, "Doesn't matter", contents); + let res = tree.rename(&pos_non_renamble, "Doesn't matter", contents); assert!(res.is_none()); } #[test] fn test_can_rename() { + let uri: Url = "file://foo/bar/test.proto".parse().unwrap(); let pos_rename = Position { line: 5, character: 9, @@ -261,7 +261,7 @@ message 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.can_rename(&pos_rename); diff --git a/src/parser/tree.rs b/src/parser/tree.rs index 9eb2835..911799e 100644 --- a/src/parser/tree.rs +++ b/src/parser/tree.rs @@ -96,6 +96,7 @@ impl ParsedTree { #[cfg(test)] mod test { + use async_lsp::lsp_types::Url; use tree_sitter::Node; use crate::parser::ProtoParser; @@ -106,6 +107,7 @@ mod test { #[test] fn test_find_children_by_kind() { + let uri: Url = "file://foo/bar/test.proto".parse().unwrap(); let contents = r#"syntax = "proto3"; package com.book; @@ -123,7 +125,7 @@ message Book { Author author = 3; } "#; - let parsed = ProtoParser::new().parse(contents); + let parsed = ProtoParser::new().parse(uri, contents); assert!(parsed.is_some()); let tree = parsed.unwrap(); let nodes = tree.filter_node(is_message); diff --git a/src/server.rs b/src/server.rs index 8082746..9d7e879 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,5 +1,5 @@ use async_lsp::{ - lsp_types::{Url, WorkspaceFolder}, + lsp_types::{PublishDiagnosticsParams, Url, WorkspaceFolder}, router::Router, ClientSocket, ErrorCode, ResponseError, }; @@ -49,7 +49,7 @@ impl ServerState { }; if !self.trees.contains_key(uri) { - let Some(parsed) = self.parser.parse(content.as_bytes()) else { + let Some(parsed) = self.parser.parse(uri.clone(), content.as_bytes()) else { error!("failed to parse content at {uri}"); return Err(ResponseError::new( ErrorCode::REQUEST_FAILED, @@ -95,4 +95,39 @@ impl ServerState { } } } + + pub fn upsert_file(&mut self, uri: &Url, content: String) -> Option { + info!(uri=%uri, "upserting file"); + + let Some(tree) = self.parser.parse(uri.clone(), &content) else { + error!(uri=%uri, "failed to parse content"); + return None; + }; + + self.documents.insert(uri.clone(), content); + let diagnostics = tree.collect_parse_errors(); + + self.trees.insert(uri.clone(), tree); + Some(diagnostics) + } + + 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); + } + } } From 0d3d6abd8ee89810923f6eea7d8d414e8815887c Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sat, 17 Aug 2024 23:06:46 +0530 Subject: [PATCH 03/18] feat: implement hover across workspace --- sample/simple.proto | 12 +++++- src/lsp.rs | 54 +++++++++++++++++------- src/main.rs | 10 ++--- src/parser/definition.rs | 2 +- src/parser/diagnostics.rs | 2 +- src/parser/hover.rs | 88 +++++++++++++++++++++++++++------------ src/parser/nodekind.rs | 33 +++++++++------ src/parser/rename.rs | 2 +- src/parser/tree.rs | 52 ++++++++++++++++------- src/registry.rs | 25 +++++++++++ src/server.rs | 7 ++++ src/utils.rs | 73 ++++++++++++++++++++++++++++++++ 12 files changed, 279 insertions(+), 81 deletions(-) create mode 100644 src/registry.rs diff --git a/sample/simple.proto b/sample/simple.proto index a896698..6ccdb13 100644 --- a/sample/simple.proto +++ b/sample/simple.proto @@ -7,7 +7,14 @@ message Book { // Of a message called Book int64 isbn = 1; string title = 2; - string author = 3; + Author author = 3; + google.protobuf.Any data = 4; + + // Author is a author of a book + message Author { + string name = 1; + int64 age = 2; + } } // This is a comment on message @@ -22,7 +29,7 @@ message GotoBookRequest { } message GetBookViaAuthor { - string author = 1; + Book.Author author = 1; } @@ -31,6 +38,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 894e6f3..11be239 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -102,26 +102,48 @@ impl LanguageServer for ServerState { let uri = param.text_document_position_params.text_document.uri; let pos = param.text_document_position_params.position; + let identifier; + let current_package_name; + match self.get_parsed_tree_and_content(&uri) { - Err(e) => Box::pin(async move { Err(e) }), + Err(e) => { + return 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, - }), - }; + identifier = tree + .get_actionable_node_text_at_position(&pos, content.as_ref()) + .map(ToOwned::to_owned); - Box::pin(async move { Ok(response) }) + current_package_name = tree + .get_package_name(content.as_ref()) + .map(ToOwned::to_owned); } - } + }; + + 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.registry_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, diff --git a/src/main.rs b/src/main.rs index 2cf9f68..a1ecf9c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ use tracing::Level; mod lsp; mod parser; +mod registry; mod server; mod utils; @@ -39,13 +40,10 @@ async fn main() { .service(ServerState::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/parser/definition.rs b/src/parser/definition.rs index 5c6de35..8d8c52a 100644 --- a/src/parser/definition.rs +++ b/src/parser/definition.rs @@ -12,7 +12,7 @@ impl ParsedTree { match text { Some(text) => self - .filter_node(NodeKind::is_userdefined) + .filter_nodes(NodeKind::is_userdefined) .into_iter() .filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == text) .map(|n| Location { diff --git a/src/parser/diagnostics.rs b/src/parser/diagnostics.rs index 0110e3a..4259888 100644 --- a/src/parser/diagnostics.rs +++ b/src/parser/diagnostics.rs @@ -7,7 +7,7 @@ use super::{nodekind::NodeKind, ParsedTree}; impl ParsedTree { 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 { diff --git a/src/parser/hover.rs b/src/parser/hover.rs index 69ca3c0..3b496c1 100644 --- a/src/parser/hover.rs +++ b/src/parser/hover.rs @@ -1,5 +1,5 @@ -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; @@ -47,44 +47,59 @@ 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 v = vec![]; + self.hover_impl(identifier, self.tree.root_node(), &mut v, content); + v + } + + fn hover_impl( + &self, + identifier: &str, + n: Node, + v: &mut Vec, + content: impl AsRef<[u8]>, + ) { + if identifier.is_empty() { + return; + } - match text { - Some(text) => self - .filter_node(NodeKind::is_actionable) + 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) + .map(|n| n.parent().unwrap()); // Safety: All userdefined types would have a 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, Url}; + use async_lsp::lsp_types::{MarkedString, Url}; use crate::parser::ProtoParser; #[test] fn test_hover() { let uri: Url = "file://foo.bar/p.proto".parse().unwrap(); - 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; @@ -101,19 +116,28 @@ message Book { 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; + }; +} "#; let parsed = ProtoParser::new().parse(uri.clone(), contents); assert!(parsed.is_some()); let tree = parsed.unwrap(); - let res = tree.hover(&posbook, contents); + let res = tree.hover("Book", contents); assert_eq!(res.len(), 1); assert_eq!(res[0], MarkedString::String("A Book is book".to_owned())); - let res = tree.hover(&posinvalid, contents); + let res = tree.hover("", contents); assert_eq!(res.len(), 0); - let res = tree.hover(&posauthor, contents); + let res = tree.hover("Book.Author", contents); assert_eq!(res.len(), 1); assert_eq!( res[0], @@ -125,5 +149,15 @@ Author has a name and a country where they were born"# .to_owned() ) ); + + let res = tree.hover("Comic.Author", contents); + assert_eq!(res.len(), 1); + assert_eq!( + res[0], + MarkedString::String("Author of a comic is different from others".to_owned()) + ); + + let res = tree.hover("Author", contents); + assert_eq!(res.len(), 2); } } diff --git a/src/parser/nodekind.rs b/src/parser/nodekind.rs index 4c1c2e8..adbc67c 100644 --- a/src/parser/nodekind.rs +++ b/src/parser/nodekind.rs @@ -1,7 +1,6 @@ use async_lsp::lsp_types::SymbolKind; use tree_sitter::Node; -#[allow(unused)] pub enum NodeKind { Identifier, Error, @@ -24,34 +23,42 @@ impl NodeKind { NodeKind::FieldName => "message_or_enum_type", NodeKind::ServiceName => "service_name", NodeKind::RpcName => "rpc_name", - NodeKind::PackageName => "package_name", + NodeKind::PackageName => "full_ident", } } pub fn is_identifier(n: &Node) -> bool { - n.kind() == "identifier" + n.kind() == Self::Identifier.as_str() } pub fn is_error(n: &Node) -> bool { - n.kind() == "ERROR" + n.kind() == Self::Error.as_str() + } + + pub fn is_package_name(n: &Node) -> bool { + n.kind() == Self::PackageName.as_str() } pub fn is_userdefined(n: &Node) -> bool { - matches!(n.kind(), "message_name" | "enum_name") + n.kind() == Self::EnumName.as_str() || n.kind() == Self::MessageName.as_str() } pub fn is_actionable(n: &Node) -> bool { - matches!( - n.kind(), - "message_name" | "enum_name" | "message_or_enum_type" | "rpc_name" | "service_name" - ) + 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 { - match n.kind() { - "message_name" => SymbolKind::STRUCT, - "enum_name" => SymbolKind::ENUM, - _ => SymbolKind::NULL, + 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/rename.rs b/src/parser/rename.rs index 3d46e2e..e003bd6 100644 --- a/src/parser/rename.rs +++ b/src/parser/rename.rs @@ -31,7 +31,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 { diff --git a/src/parser/tree.rs b/src/parser/tree.rs index 911799e..dfec1ac 100644 --- a/src/parser/tree.rs +++ b/src/parser/tree.rs @@ -9,6 +9,7 @@ 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,14 +84,33 @@ 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 filter_node(&self, f: fn(&Node) -> bool) -> Vec { + self.filter_node_from(self.tree.root_node(), f) + } + + pub fn filter_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.filter_node(NodeKind::is_package_name) + .first() + .map(|n| n.utf8_text(content).expect("utf-8 parse error")) } } @@ -106,7 +126,7 @@ mod test { } #[test] - fn test_find_children_by_kind() { + fn test_filter() { let uri: Url = "file://foo/bar/test.proto".parse().unwrap(); let contents = r#"syntax = "proto3"; @@ -128,7 +148,7 @@ message Book { 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); @@ -138,5 +158,9 @@ message Book { .collect(); assert_eq!(names[0], "Book"); assert_eq!(names[1], "Author"); + + let package_name = tree.get_package_name(contents.as_ref()); + assert!(package_name.is_some()); + assert_eq!(package_name.unwrap(), "com.book"); } } diff --git a/src/registry.rs b/src/registry.rs new file mode 100644 index 0000000..6418c9e --- /dev/null +++ b/src/registry.rs @@ -0,0 +1,25 @@ +use async_lsp::lsp_types::MarkedString; + +use crate::{server::ServerState, utils::split_identifier_package}; + +impl ServerState { + pub fn registry_hover(&self, curr_package: &str, identifier: &str) -> Vec { + let (mut package, identifier) = split_identifier_package(identifier); + if package.is_empty() { + package = curr_package; + } + + self.trees + .values() + .filter(|tree| { + let content = self.get_content(&tree.uri); + tree.get_package_name(content.as_bytes()) + .unwrap_or_default() + == package + }) + .fold(vec![], |mut v, tree| { + v.extend(tree.hover(identifier, self.get_content(&tree.uri))); + v + }) + } +} diff --git a/src/server.rs b/src/server.rs index 9d7e879..7400300 100644 --- a/src/server.rs +++ b/src/server.rs @@ -36,6 +36,13 @@ impl ServerState { ControlFlow::Continue(()) } + pub fn get_content(&self, uri: &Url) -> &str { + self.documents + .get(uri) + .map(|s| s.as_str()) + .unwrap_or_default() + } + pub fn get_parsed_tree_and_content( &mut self, uri: &Url, 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", "")); + } +} From 3fe8ace490afa96df862908279e6e7695b625546 Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sun, 18 Aug 2024 00:15:17 +0530 Subject: [PATCH 04/18] test: use insta and include file for testing --- Cargo.lock | 44 ++++ Cargo.toml | 3 + src/parser/definition.rs | 43 +--- src/parser/diagnostics.rs | 52 +---- src/parser/docsymbol.rs | 139 +------------ src/parser/hover.rs | 66 ++---- src/parser/input/test_can_rename.proto | 16 ++ .../input/test_collect_parse_error1.proto | 9 + .../input/test_collect_parse_error2.proto | 10 + src/parser/input/test_document_symbols.proto | 30 +++ src/parser/input/test_filter.proto | 16 ++ src/parser/input/test_goto_definition.proto | 13 ++ src/parser/input/test_hover.proto | 25 +++ src/parser/input/test_rename.proto | 27 +++ src/parser/rename.rs | 196 +----------------- ...__definition__test__goto_definition-2.snap | 5 + ...er__definition__test__goto_definition.snap | 12 ++ ...gnostics__test__collect_parse_error-2.snap | 16 ++ ...iagnostics__test__collect_parse_error.snap | 6 + ...er__docsymbol__test__document_symbols.snap | 109 ++++++++++ ...protols__parser__hover__test__hover-2.snap | 5 + ...protols__parser__hover__test__hover-3.snap | 5 + ...protols__parser__hover__test__hover-4.snap | 5 + ...protols__parser__hover__test__hover-5.snap | 6 + .../protols__parser__hover__test__hover.snap | 5 + ...s__parser__rename__test__can_rename-2.snap | 5 + ...ols__parser__rename__test__can_rename.snap | 10 + ...otols__parser__rename__test__rename-2.snap | 30 +++ ...otols__parser__rename__test__rename-3.snap | 5 + ...protols__parser__rename__test__rename.snap | 38 ++++ ...protols__parser__tree__test__filter-2.snap | 5 + .../protols__parser__tree__test__filter.snap | 6 + src/parser/tree.rs | 35 +--- 33 files changed, 518 insertions(+), 479 deletions(-) create mode 100644 src/parser/input/test_can_rename.proto create mode 100644 src/parser/input/test_collect_parse_error1.proto create mode 100644 src/parser/input/test_collect_parse_error2.proto create mode 100644 src/parser/input/test_document_symbols.proto create mode 100644 src/parser/input/test_filter.proto create mode 100644 src/parser/input/test_goto_definition.proto create mode 100644 src/parser/input/test_hover.proto create mode 100644 src/parser/input/test_rename.proto create mode 100644 src/parser/snapshots/protols__parser__definition__test__goto_definition-2.snap create mode 100644 src/parser/snapshots/protols__parser__definition__test__goto_definition.snap create mode 100644 src/parser/snapshots/protols__parser__diagnostics__test__collect_parse_error-2.snap create mode 100644 src/parser/snapshots/protols__parser__diagnostics__test__collect_parse_error.snap create mode 100644 src/parser/snapshots/protols__parser__docsymbol__test__document_symbols.snap create mode 100644 src/parser/snapshots/protols__parser__hover__test__hover-2.snap create mode 100644 src/parser/snapshots/protols__parser__hover__test__hover-3.snap create mode 100644 src/parser/snapshots/protols__parser__hover__test__hover-4.snap create mode 100644 src/parser/snapshots/protols__parser__hover__test__hover-5.snap create mode 100644 src/parser/snapshots/protols__parser__hover__test__hover.snap create mode 100644 src/parser/snapshots/protols__parser__rename__test__can_rename-2.snap create mode 100644 src/parser/snapshots/protols__parser__rename__test__can_rename.snap create mode 100644 src/parser/snapshots/protols__parser__rename__test__rename-2.snap create mode 100644 src/parser/snapshots/protols__parser__rename__test__rename-3.snap create mode 100644 src/parser/snapshots/protols__parser__rename__test__rename.snap create mode 100644 src/parser/snapshots/protols__parser__tree__test__filter-2.snap create mode 100644 src/parser/snapshots/protols__parser__tree__test__filter.snap diff --git a/Cargo.lock b/Cargo.lock index 0baecf7..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" @@ -439,6 +476,7 @@ 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 f9066ab..4c8f272 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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/src/parser/definition.rs b/src/parser/definition.rs index 8d8c52a..e2e87ca 100644 --- a/src/parser/definition.rs +++ b/src/parser/definition.rs @@ -30,13 +30,14 @@ impl ParsedTree { #[cfg(test)] mod test { - use async_lsp::lsp_types::{Position, Range, Url}; + use async_lsp::lsp_types::{Position, Url}; + use insta::assert_yaml_snapshot; use crate::parser::ProtoParser; #[test] fn test_goto_definition() { - let url = "file://foo/bar.proto"; + let url: Url = "file://foo/bar.proto".parse().unwrap(); let posinvalid = Position { line: 0, character: 1, @@ -45,42 +46,12 @@ mod test { line: 10, character: 5, }; - let contents = r#"syntax = "proto3"; + 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(url.parse().unwrap(), contents); assert!(parsed.is_some()); let tree = parsed.unwrap(); - let res = tree.definition(&posauthor, 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, contents); - assert_eq!(res.len(), 0); + assert_yaml_snapshot!(tree.definition(&posauthor, contents)); + assert_yaml_snapshot!(tree.definition(&posinvalid, contents)); } } diff --git a/src/parser/diagnostics.rs b/src/parser/diagnostics.rs index 4259888..218f73f 100644 --- a/src/parser/diagnostics.rs +++ b/src/parser/diagnostics.rs @@ -30,64 +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: Url = "file://foo/bar.proto".parse().unwrap(); - let contents = r#"syntax = "proto3"; - -package test; - -message Foo { - reserved 1; - reserved "baz"; - int bar = 2; -}"#; + let contents = include_str!("input/test_collect_parse_error1.proto"); let parsed = ProtoParser::new().parse(url.clone(), contents); assert!(parsed.is_some()); - let tree = parsed.unwrap(); - let diagnostics = tree.collect_parse_errors(); - assert_eq!(diagnostics.uri, url); - assert_eq!(diagnostics.diagnostics.len(), 0); - - 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(url.clone(), contents); assert!(parsed.is_some()); - let tree = parsed.unwrap(); - let diagnostics = tree.collect_parse_errors(); - - assert_eq!(diagnostics.uri, url); - 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 90b4d27..7bfbeae 100644 --- a/src/parser/docsymbol.rs +++ b/src/parser/docsymbol.rs @@ -98,7 +98,8 @@ impl ParsedTree { #[cfg(test)] mod test { - use async_lsp::lsp_types::{DocumentSymbol, Position, Range, SymbolKind, Url}; + use async_lsp::lsp_types::Url; + use insta::assert_yaml_snapshot; use crate::parser::ProtoParser; @@ -106,142 +107,12 @@ mod test { #[allow(deprecated)] fn test_document_symbols() { let uri: Url = "file://foo/bar/pro.proto".parse().unwrap(); - let contents = r#"syntax = "proto3"; + 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(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 3b496c1..f1c9dba 100644 --- a/src/parser/hover.rs +++ b/src/parser/hover.rs @@ -48,9 +48,9 @@ impl ParsedTree { } pub fn hover(&self, identifier: &str, content: impl AsRef<[u8]>) -> Vec { - let mut v = vec![]; - self.hover_impl(identifier, self.tree.root_node(), &mut v, content); - v + let mut results = vec![]; + self.hover_impl(identifier, self.tree.root_node(), &mut results, content); + results } fn hover_impl( @@ -83,7 +83,7 @@ impl ParsedTree { .filter_nodes_from(n, NodeKind::is_userdefined) .into_iter() .find(|n| n.utf8_text(content.as_ref()).expect("utf8-parse error") == parent_identifier) - .map(|n| n.parent().unwrap()); // Safety: All userdefined types would have a parent + .and_then(|n| n.parent()); if let Some(inner) = child_node { self.hover_impl(remaining, inner, v, content); @@ -93,71 +93,33 @@ impl ParsedTree { #[cfg(test)] mod test { - use async_lsp::lsp_types::{MarkedString, Url}; + use async_lsp::lsp_types::Url; + use insta::assert_yaml_snapshot; use crate::parser::ProtoParser; #[test] fn test_hover() { let uri: Url = "file://foo.bar/p.proto".parse().unwrap(); - 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; - }; -} - -// 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; - }; -} -"#; + 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("Book", contents); - assert_eq!(res.len(), 1); - assert_eq!(res[0], MarkedString::String("A Book is book".to_owned())); + let res = tree.hover("Book", contents); + assert_yaml_snapshot!(res); let res = tree.hover("", contents); - assert_eq!(res.len(), 0); + assert_yaml_snapshot!(res); let res = tree.hover("Book.Author", 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() - ) - ); + assert_yaml_snapshot!(res); let res = tree.hover("Comic.Author", contents); - assert_eq!(res.len(), 1); - assert_eq!( - res[0], - MarkedString::String("Author of a comic is different from others".to_owned()) - ); + assert_yaml_snapshot!(res); let res = tree.hover("Author", contents); - assert_eq!(res.len(), 2); + 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..09f2cda --- /dev/null +++ b/src/parser/input/test_can_rename.proto @@ -0,0 +1,16 @@ +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; + }; +} 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..65c4675 --- /dev/null +++ b/src/parser/input/test_collect_parse_error1.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package test; + +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..6e5341b --- /dev/null +++ b/src/parser/input/test_collect_parse_error2.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package com.book; + +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..87b9c03 --- /dev/null +++ b/src/parser/input/test_document_symbols.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +package com.symbols; + +// 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..f5a1c7d --- /dev/null +++ b/src/parser/input/test_filter.proto @@ -0,0 +1,16 @@ +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; +} diff --git a/src/parser/input/test_goto_definition.proto b/src/parser/input/test_goto_definition.proto new file mode 100644 index 0000000..df14735 --- /dev/null +++ b/src/parser/input/test_goto_definition.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package com.book; + +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..8dbd247 --- /dev/null +++ b/src/parser/input/test_hover.proto @@ -0,0 +1,25 @@ +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; + }; +} + +// 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..97bf732 --- /dev/null +++ b/src/parser/input/test_rename.proto @@ -0,0 +1,27 @@ +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; + }; + 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/rename.rs b/src/parser/rename.rs index e003bd6..7102263 100644 --- a/src/parser/rename.rs +++ b/src/parser/rename.rs @@ -58,7 +58,8 @@ impl ParsedTree { #[cfg(test)] mod test { - use async_lsp::lsp_types::{Position, Range, TextEdit, Url}; + use async_lsp::lsp_types::{Position, Url}; + use insta::assert_yaml_snapshot; use crate::parser::ProtoParser; @@ -77,160 +78,15 @@ mod test { line: 24, character: 4, }; - 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; - }; - 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 contents = include_str!("input/test_rename.proto"); let parsed = ProtoParser::new().parse(uri.clone(), contents); assert!(parsed.is_some()); let tree = parsed.unwrap(); - let res = tree.rename(&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(&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(&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] @@ -244,44 +100,12 @@ service Myservice { line: 2, character: 2, }; - 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 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)); } } 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.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..403f7c4 --- /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.book 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 dfec1ac..cfef121 100644 --- a/src/parser/tree.rs +++ b/src/parser/tree.rs @@ -98,17 +98,17 @@ impl ParsedTree { Self::walk_and_collect_filter(&mut cursor, f, false) } - pub fn filter_node(&self, f: fn(&Node) -> bool) -> Vec { - self.filter_node_from(self.tree.root_node(), f) + pub fn find_node(&self, f: fn(&Node) -> bool) -> Vec { + self.find_node_from(self.tree.root_node(), f) } - pub fn filter_node_from<'a>(&self, n: Node<'a>, f: fn(&Node) -> bool) -> Vec> { + 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.filter_node(NodeKind::is_package_name) + self.find_node(NodeKind::is_package_name) .first() .map(|n| n.utf8_text(content).expect("utf-8 parse error")) } @@ -117,6 +117,7 @@ impl ParsedTree { #[cfg(test)] mod test { use async_lsp::lsp_types::Url; + use insta::assert_yaml_snapshot; use tree_sitter::Node; use crate::parser::ProtoParser; @@ -128,24 +129,9 @@ mod test { #[test] fn test_filter() { let uri: Url = "file://foo/bar/test.proto".parse().unwrap(); - 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 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_nodes(is_message); @@ -156,11 +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!(package_name.is_some()); - assert_eq!(package_name.unwrap(), "com.book"); + assert_yaml_snapshot!(package_name); } } From c07d2520584b3746311ddff80f9100a7b0941c6f Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sun, 18 Aug 2024 00:56:26 +0530 Subject: [PATCH 05/18] refactor: use state out from server --- src/lsp.rs | 31 +++++----- src/main.rs | 7 ++- src/registry.rs | 25 -------- src/server.rs | 126 +++----------------------------------- src/state.rs | 135 +++++++++++++++++++++++++++++++++++++++++ src/workspace/hover.rs | 28 +++++++++ src/workspace/mod.rs | 1 + 7 files changed, 192 insertions(+), 161 deletions(-) delete mode 100644 src/registry.rs create mode 100644 src/state.rs create mode 100644 src/workspace/hover.rs create mode 100644 src/workspace/mod.rs diff --git a/src/lsp.rs b/src/lsp.rs index 11be239..99b5be2 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -17,9 +17,9 @@ use async_lsp::lsp_types::{ 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>; @@ -54,7 +54,7 @@ impl LanguageServer for ServerState { 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 { @@ -105,7 +105,7 @@ impl LanguageServer for ServerState { let identifier; let current_package_name; - match self.get_parsed_tree_and_content(&uri) { + match self.state.get_parsed_tree_and_content(&uri) { Err(e) => { return Box::pin(async move { Err(e) }); } @@ -130,7 +130,10 @@ impl LanguageServer for ServerState { return Box::pin(async move { Ok(None) }); }; - let comments = self.registry_hover(current_package_name.as_ref(), identifier.as_ref()); + let comments = self + .state + .hover(current_package_name.as_ref(), identifier.as_ref()); + let response = match comments.len() { 0 => None, 1 => Some(Hover { @@ -173,7 +176,7 @@ impl LanguageServer for ServerState { let uri = params.text_document.uri; let pos = params.position; - match self.get_parsed_tree_and_content(&uri) { + match self.state.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); @@ -192,7 +195,7 @@ impl LanguageServer for ServerState { let new_name = params.new_name; - match self.get_parsed_tree_and_content(&uri) { + match self.state.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() { @@ -213,7 +216,7 @@ 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) { + match self.state.get_parsed_tree_and_content(&uri) { Err(e) => Box::pin(async move { Err(e) }), Ok((tree, content)) => { let locations = tree.definition(&pos, content.as_bytes()); @@ -235,7 +238,7 @@ impl LanguageServer for ServerState { ) -> BoxFuture<'static, Result, Self::Error>> { let uri = params.text_document.uri; - match self.get_parsed_tree_and_content(&uri) { + match self.state.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()); @@ -258,7 +261,7 @@ impl LanguageServer for ServerState { let uri = params.text_document.uri; let content = params.text_document.text; - if let Some(diagnostics) = self.upsert_file(&uri, content) { + 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") } @@ -270,7 +273,7 @@ impl LanguageServer for ServerState { let uri = params.text_document.uri; let content = params.content_changes[0].text.clone(); - if let Some(diagnostics) = self.upsert_file(&uri, content) { + 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") } @@ -283,7 +286,7 @@ impl LanguageServer for ServerState { 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.upsert_file(&uri, content); + self.state.upsert_file(&uri, content); } else { error!(uri=%file.uri, "failed parse uri"); } @@ -303,7 +306,7 @@ impl LanguageServer for ServerState { continue; }; - self.rename_file(&new_uri, &old_uri); + self.state.rename_file(&new_uri, &old_uri); } ControlFlow::Continue(()) } @@ -311,7 +314,7 @@ impl LanguageServer for ServerState { 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.delete_file(&uri); + self.state.delete_file(&uri); } else { error!(uri = file.uri, "failed to parse uri"); } diff --git a/src/main.rs b/src/main.rs index a1ecf9c..4ea0a2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,14 +5,15 @@ 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 parser; -mod registry; +mod workspace; mod server; +mod state; mod utils; #[tokio::main(flavor = "current_thread")] @@ -37,7 +38,7 @@ 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 dir = std::env::temp_dir(); diff --git a/src/registry.rs b/src/registry.rs deleted file mode 100644 index 6418c9e..0000000 --- a/src/registry.rs +++ /dev/null @@ -1,25 +0,0 @@ -use async_lsp::lsp_types::MarkedString; - -use crate::{server::ServerState, utils::split_identifier_package}; - -impl ServerState { - pub fn registry_hover(&self, curr_package: &str, identifier: &str) -> Vec { - let (mut package, identifier) = split_identifier_package(identifier); - if package.is_empty() { - package = curr_package; - } - - self.trees - .values() - .filter(|tree| { - let content = self.get_content(&tree.uri); - tree.get_package_name(content.as_bytes()) - .unwrap_or_default() - == package - }) - .fold(vec![], |mut v, tree| { - v.extend(tree.hover(identifier, self.get_content(&tree.uri))); - v - }) - } -} diff --git a/src/server.rs b/src/server.rs index 7400300..86a3f13 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,31 +1,21 @@ -use async_lsp::{ - lsp_types::{PublishDiagnosticsParams, 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,106 +25,4 @@ impl ServerState { self.counter += 1; ControlFlow::Continue(()) } - - pub fn get_content(&self, uri: &Url) -> &str { - self.documents - .get(uri) - .map(|s| s.as_str()) - .unwrap_or_default() - } - - 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(uri.clone(), 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() - ); - } - } - } - } - - pub fn upsert_file(&mut self, uri: &Url, content: String) -> Option { - info!(uri=%uri, "upserting file"); - - let Some(tree) = self.parser.parse(uri.clone(), &content) else { - error!(uri=%uri, "failed to parse content"); - return None; - }; - - self.documents.insert(uri.clone(), content); - let diagnostics = tree.collect_parse_errors(); - - self.trees.insert(uri.clone(), tree); - Some(diagnostics) - } - - 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); - } - } } diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..0b74020 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,135 @@ +use std::{collections::HashMap, fs::read_to_string}; +use tracing::{error, info}; + +use async_lsp::{ + lsp_types::{PublishDiagnosticsParams, Url, WorkspaceFolder}, + ErrorCode, ResponseError, +}; +use walkdir::WalkDir; + +use crate::parser::{ParsedTree, ProtoParser}; + +pub struct ProtoLanguageState { + pub documents: HashMap, + pub trees: HashMap, + pub 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_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 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(uri.clone(), 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() + ); + } + } + } + } + + pub fn upsert_file(&mut self, uri: &Url, content: String) -> Option { + info!(uri=%uri, "upserting file"); + + let Some(tree) = self.parser.parse(uri.clone(), &content) else { + error!(uri=%uri, "failed to parse content"); + return None; + }; + + self.documents.insert(uri.clone(), content); + let diagnostics = tree.collect_parse_errors(); + + self.trees.insert(uri.clone(), tree); + Some(diagnostics) + } + + 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); + } + } +} diff --git a/src/workspace/hover.rs b/src/workspace/hover.rs new file mode 100644 index 0000000..a701bae --- /dev/null +++ b/src/workspace/hover.rs @@ -0,0 +1,28 @@ +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 crate::state::ProtoLanguageState; + + #[test] + fn workspace_test_hover() { + } +} diff --git a/src/workspace/mod.rs b/src/workspace/mod.rs new file mode 100644 index 0000000..35beabb --- /dev/null +++ b/src/workspace/mod.rs @@ -0,0 +1 @@ +mod hover; From ca25da73fd51ab76c9c1e8409de406a10992ffce Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sun, 18 Aug 2024 01:16:52 +0530 Subject: [PATCH 06/18] refactor: split get_tree and content --- src/lsp.rs | 104 +++++++++++++++++++---------------------- src/state.rs | 80 +++++++++++-------------------- src/workspace/hover.rs | 2 - 3 files changed, 76 insertions(+), 110 deletions(-) diff --git a/src/lsp.rs b/src/lsp.rs index 99b5be2..645090b 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -102,24 +102,15 @@ impl LanguageServer for ProtoLanguageServer { let uri = param.text_document_position_params.text_document.uri; let pos = param.text_document_position_params.position; - let identifier; - let current_package_name; - - match self.state.get_parsed_tree_and_content(&uri) { - Err(e) => { - return Box::pin(async move { Err(e) }); - } - Ok((tree, content)) => { - identifier = tree - .get_actionable_node_text_at_position(&pos, content.as_ref()) - .map(ToOwned::to_owned); - - current_package_name = tree - .get_package_name(content.as_ref()) - .map(ToOwned::to_owned); - } + 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) }); @@ -176,14 +167,13 @@ impl LanguageServer for ProtoLanguageServer { let uri = params.text_document.uri; let pos = params.position; - match self.state.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( @@ -195,18 +185,20 @@ impl LanguageServer for ProtoLanguageServer { let new_name = params.new_name; - match self.state.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(&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) }); + }; + + let content = self.state.get_content(&uri); - Box::pin(async move { Ok(response) }) - } - } + 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( @@ -216,20 +208,21 @@ impl LanguageServer for ProtoLanguageServer { let uri = param.text_document_position_params.text_document.uri; let pos = param.text_document_position_params.position; - match self.state.get_parsed_tree_and_content(&uri) { - Err(e) => Box::pin(async move { Err(e) }), - Ok((tree, content)) => { - let locations = tree.definition(&pos, 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 response = match locations.len() { - 0 => None, - 1 => Some(GotoDefinitionResponse::Scalar(locations[0].clone())), - 2.. => Some(GotoDefinitionResponse::Array(locations)), - }; + let content = self.state.get_content(&uri); + let locations = tree.definition(&pos, content.as_bytes()); - Box::pin(async move { Ok(response) }) - } - } + 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( @@ -238,15 +231,16 @@ impl LanguageServer for ProtoLanguageServer { ) -> BoxFuture<'static, Result, Self::Error>> { let uri = params.text_document.uri; - match self.state.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 { @@ -286,7 +280,7 @@ impl LanguageServer for ProtoLanguageServer { 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_file(&uri, content); + self.state.upsert_content(&uri, content); } else { error!(uri=%file.uri, "failed parse uri"); } diff --git a/src/state.rs b/src/state.rs index 0b74020..42e4c47 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,18 +1,15 @@ use std::{collections::HashMap, fs::read_to_string}; use tracing::{error, info}; -use async_lsp::{ - lsp_types::{PublishDiagnosticsParams, Url, WorkspaceFolder}, - ErrorCode, ResponseError, -}; +use async_lsp::lsp_types::{PublishDiagnosticsParams, Url, WorkspaceFolder}; use walkdir::WalkDir; use crate::parser::{ParsedTree, ProtoParser}; pub struct ProtoLanguageState { - pub documents: HashMap, + documents: HashMap, pub trees: HashMap, - pub parser: ProtoParser, + parser: ProtoParser, } impl ProtoLanguageState { @@ -31,38 +28,31 @@ impl ProtoLanguageState { .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() + 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 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(uri.clone(), content.as_bytes()) else { - error!("failed to parse content at {uri}"); - return Err(ResponseError::new( - ErrorCode::REQUEST_FAILED, - "ts failed to parse contents", - )); - }; + 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 } - - let parsed = self.trees.get(uri).unwrap(); // Safety: already inserted above - Ok((parsed, content)) } pub fn add_workspace_folder(&mut self, workspace: WorkspaceFolder) { @@ -85,14 +75,8 @@ impl ProtoLanguageState { }; 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() - ); + let r = self.upsert_content(&uri, content); + info!("workspace parse file: {}, result: {}", path.display(), r); } } } @@ -100,22 +84,12 @@ impl ProtoLanguageState { pub fn upsert_file(&mut self, uri: &Url, content: String) -> Option { info!(uri=%uri, "upserting file"); - - let Some(tree) = self.parser.parse(uri.clone(), &content) else { - error!(uri=%uri, "failed to parse content"); - return None; - }; - - self.documents.insert(uri.clone(), content); - let diagnostics = tree.collect_parse_errors(); - - self.trees.insert(uri.clone(), tree); - Some(diagnostics) + 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); } diff --git a/src/workspace/hover.rs b/src/workspace/hover.rs index a701bae..1d05566 100644 --- a/src/workspace/hover.rs +++ b/src/workspace/hover.rs @@ -20,8 +20,6 @@ impl ProtoLanguageState { #[cfg(test)] mod test { - use crate::state::ProtoLanguageState; - #[test] fn workspace_test_hover() { } From b72de4c3caaacd96eb152bf7381913e18b5c01ed Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sun, 18 Aug 2024 01:38:29 +0530 Subject: [PATCH 07/18] feat: add goto definition across workspace --- src/lsp.rs | 17 +++++++++- src/main.rs | 2 +- src/parser/definition.rs | 62 ++++++++++++++++++++++++------------- src/workspace/definition.rs | 24 ++++++++++++++ src/workspace/hover.rs | 3 +- src/workspace/mod.rs | 1 + 6 files changed, 83 insertions(+), 26 deletions(-) create mode 100644 src/workspace/definition.rs diff --git a/src/lsp.rs b/src/lsp.rs index 645090b..690dd1e 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -214,7 +214,22 @@ impl LanguageServer for ProtoLanguageServer { }; let content = self.state.get_content(&uri); - let locations = tree.definition(&pos, content.as_bytes()); + 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 locations = self + .state + .definition(current_package_name.as_ref(), identifier.as_ref()); let response = match locations.len() { 0 => None, diff --git a/src/main.rs b/src/main.rs index 4ea0a2c..f0d88a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,10 +11,10 @@ use tracing::Level; mod lsp; mod parser; -mod workspace; mod server; mod state; mod utils; +mod workspace; #[tokio::main(flavor = "current_thread")] async fn main() { diff --git a/src/parser/definition.rs b/src/parser/definition.rs index e2e87ca..d641b35 100644 --- a/src/parser/definition.rs +++ b/src/parser/definition.rs @@ -1,20 +1,32 @@ -use async_lsp::lsp_types::{Location, Position, Range}; -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 super::ParsedTree; impl ParsedTree { - pub fn definition(&self, pos: &Position, content: impl AsRef<[u8]>) -> Vec { - let text = self.get_node_text_at_position(pos, content.as_ref()); - info!("Looking for definition of: {:?}", text); + 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; + } - match text { - Some(text) => self - .filter_nodes(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: self.uri.clone(), range: Range { @@ -22,15 +34,29 @@ impl ParsedTree { 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, Url}; + use async_lsp::lsp_types::Url; use insta::assert_yaml_snapshot; use crate::parser::ProtoParser; @@ -38,20 +64,12 @@ mod test { #[test] fn test_goto_definition() { let url: Url = "file://foo/bar.proto".parse().unwrap(); - let posinvalid = Position { - line: 0, - character: 1, - }; - let posauthor = Position { - line: 10, - character: 5, - }; let contents = include_str!("input/test_goto_definition.proto"); let parsed = ProtoParser::new().parse(url, contents); assert!(parsed.is_some()); let tree = parsed.unwrap(); - assert_yaml_snapshot!(tree.definition(&posauthor, contents)); - assert_yaml_snapshot!(tree.definition(&posinvalid, contents)); + assert_yaml_snapshot!(tree.definition("Author", contents)); + assert_yaml_snapshot!(tree.definition("", contents)); } } diff --git a/src/workspace/definition.rs b/src/workspace/definition.rs new file mode 100644 index 0000000..bfb65a3 --- /dev/null +++ b/src/workspace/definition.rs @@ -0,0 +1,24 @@ +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 { + #[test] + fn workspace_test_definition() {} +} diff --git a/src/workspace/hover.rs b/src/workspace/hover.rs index 1d05566..47e21d3 100644 --- a/src/workspace/hover.rs +++ b/src/workspace/hover.rs @@ -21,6 +21,5 @@ impl ProtoLanguageState { #[cfg(test)] mod test { #[test] - fn workspace_test_hover() { - } + fn workspace_test_hover() {} } diff --git a/src/workspace/mod.rs b/src/workspace/mod.rs index 35beabb..9a26930 100644 --- a/src/workspace/mod.rs +++ b/src/workspace/mod.rs @@ -1 +1,2 @@ +mod definition; mod hover; From c39a56d921fd9f12572b26d9735ce27874c4f523 Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sun, 18 Aug 2024 11:46:56 +0530 Subject: [PATCH 08/18] test: add testing for workspace hover --- src/workspace/hover.rs | 24 ++++++++++++++++++- .../input/workspace_test_hover/a.proto | 12 ++++++++++ .../input/workspace_test_hover/b.proto | 15 ++++++++++++ .../input/workspace_test_hover/c.proto | 15 ++++++++++++ ...__hover__test__workspace_test_hover-2.snap | 5 ++++ ...__hover__test__workspace_test_hover-3.snap | 5 ++++ ...__hover__test__workspace_test_hover-4.snap | 5 ++++ ...ce__hover__test__workspace_test_hover.snap | 5 ++++ 8 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/workspace/input/workspace_test_hover/a.proto create mode 100644 src/workspace/input/workspace_test_hover/b.proto create mode 100644 src/workspace/input/workspace_test_hover/c.proto create mode 100644 src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-2.snap create mode 100644 src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-3.snap create mode 100644 src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-4.snap create mode 100644 src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover.snap diff --git a/src/workspace/hover.rs b/src/workspace/hover.rs index 47e21d3..e6b2e0a 100644 --- a/src/workspace/hover.rs +++ b/src/workspace/hover.rs @@ -20,6 +20,28 @@ impl ProtoLanguageState { #[cfg(test)] mod test { + use insta::assert_yaml_snapshot; + + use crate::state::ProtoLanguageState; + #[test] - fn workspace_test_hover() {} + fn workspace_test_hover() { + let a_uri = "file://input/workspace_test_hover/a.proto".parse().unwrap(); + let b_uri = "file://input/workspace_test_hover/b.proto".parse().unwrap(); + let c_uri = "file://input/workspace_test_hover/c.proto".parse().unwrap(); + + let a = include_str!("input/workspace_test_hover/a.proto"); + let b = include_str!("input/workspace_test_hover/b.proto"); + let c = include_str!("input/workspace_test_hover/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.library", "Author")); + assert_yaml_snapshot!(state.hover("com.library", "Author.Address")); + assert_yaml_snapshot!(state.hover("com.library", "com.utility.Foobar.Baz")); + assert_yaml_snapshot!(state.hover("com.utility", "com.library.Baz")); + } } diff --git a/src/workspace/input/workspace_test_hover/a.proto b/src/workspace/input/workspace_test_hover/a.proto new file mode 100644 index 0000000..0ca79f4 --- /dev/null +++ b/src/workspace/input/workspace_test_hover/a.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package com.library; + +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/workspace_test_hover/b.proto b/src/workspace/input/workspace_test_hover/b.proto new file mode 100644 index 0000000..dafdbc1 --- /dev/null +++ b/src/workspace/input/workspace_test_hover/b.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package com.library; + +// 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/workspace_test_hover/c.proto b/src/workspace/input/workspace_test_hover/c.proto new file mode 100644 index 0000000..4d1bb55 --- /dev/null +++ b/src/workspace/input/workspace_test_hover/c.proto @@ -0,0 +1,15 @@ +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/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..9d4185f --- /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\", \"com.library.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 From 29cb0a4422007ae20e37418c8519cf3f38747ab4 Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sun, 18 Aug 2024 12:55:13 +0530 Subject: [PATCH 09/18] test: add test for definition --- src/workspace/definition.rs | 25 ++++++++++++++++++- src/workspace/hover.rs | 14 +++++------ .../input/{workspace_test_hover => }/a.proto | 1 + .../input/{workspace_test_hover => }/b.proto | 1 + .../input/{workspace_test_hover => }/c.proto | 1 + ...on__test__workspace_test_definition-2.snap | 12 +++++++++ ...on__test__workspace_test_definition-3.snap | 12 +++++++++ ...on__test__workspace_test_definition-4.snap | 12 +++++++++ ...tion__test__workspace_test_definition.snap | 12 +++++++++ ...__hover__test__workspace_test_hover-4.snap | 4 +-- 10 files changed, 84 insertions(+), 10 deletions(-) rename src/workspace/input/{workspace_test_hover => }/a.proto (99%) rename src/workspace/input/{workspace_test_hover => }/b.proto (99%) rename src/workspace/input/{workspace_test_hover => }/c.proto (99%) create mode 100644 src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-2.snap create mode 100644 src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-3.snap create mode 100644 src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-4.snap create mode 100644 src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition.snap diff --git a/src/workspace/definition.rs b/src/workspace/definition.rs index bfb65a3..4ca3277 100644 --- a/src/workspace/definition.rs +++ b/src/workspace/definition.rs @@ -19,6 +19,29 @@ impl ProtoLanguageState { #[cfg(test)] mod test { + use insta::assert_yaml_snapshot; + + use crate::state::ProtoLanguageState; + #[test] - fn workspace_test_definition() {} + 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.library", "Author")); + assert_yaml_snapshot!(state.definition("com.library", "Author.Address")); + assert_yaml_snapshot!(state.definition("com.library", "com.utility.Foobar.Baz")); + assert_yaml_snapshot!(state.definition("com.utility", "Baz")); + + } } diff --git a/src/workspace/hover.rs b/src/workspace/hover.rs index e6b2e0a..45b8f06 100644 --- a/src/workspace/hover.rs +++ b/src/workspace/hover.rs @@ -26,13 +26,13 @@ mod test { #[test] fn workspace_test_hover() { - let a_uri = "file://input/workspace_test_hover/a.proto".parse().unwrap(); - let b_uri = "file://input/workspace_test_hover/b.proto".parse().unwrap(); - let c_uri = "file://input/workspace_test_hover/c.proto".parse().unwrap(); + 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/workspace_test_hover/a.proto"); - let b = include_str!("input/workspace_test_hover/b.proto"); - let c = include_str!("input/workspace_test_hover/c.proto"); + 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()); @@ -42,6 +42,6 @@ mod test { assert_yaml_snapshot!(state.hover("com.library", "Author")); assert_yaml_snapshot!(state.hover("com.library", "Author.Address")); assert_yaml_snapshot!(state.hover("com.library", "com.utility.Foobar.Baz")); - assert_yaml_snapshot!(state.hover("com.utility", "com.library.Baz")); + assert_yaml_snapshot!(state.hover("com.utility", "Baz")); } } diff --git a/src/workspace/input/workspace_test_hover/a.proto b/src/workspace/input/a.proto similarity index 99% rename from src/workspace/input/workspace_test_hover/a.proto rename to src/workspace/input/a.proto index 0ca79f4..b270b3b 100644 --- a/src/workspace/input/workspace_test_hover/a.proto +++ b/src/workspace/input/a.proto @@ -10,3 +10,4 @@ message Book { Author.Address foo = 2; com.utility.FooBar.Baz z = 3; } + diff --git a/src/workspace/input/workspace_test_hover/b.proto b/src/workspace/input/b.proto similarity index 99% rename from src/workspace/input/workspace_test_hover/b.proto rename to src/workspace/input/b.proto index dafdbc1..db47d7f 100644 --- a/src/workspace/input/workspace_test_hover/b.proto +++ b/src/workspace/input/b.proto @@ -13,3 +13,4 @@ message Author { Address foo = 2; } + diff --git a/src/workspace/input/workspace_test_hover/c.proto b/src/workspace/input/c.proto similarity index 99% rename from src/workspace/input/workspace_test_hover/c.proto rename to src/workspace/input/c.proto index 4d1bb55..f3fc7ab 100644 --- a/src/workspace/input/workspace_test_hover/c.proto +++ b/src/workspace/input/c.proto @@ -13,3 +13,4 @@ message Foobar { Baz a = 2; } + 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-4.snap b/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-4.snap index 9d4185f..130490c 100644 --- 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 @@ -1,5 +1,5 @@ --- source: src/workspace/hover.rs -expression: "state.hover(\"com.utility\", \"com.library.Baz\")" +expression: "state.hover(\"com.utility\", \"Baz\")" --- -[] +- What is baz? From 43f97528fe57808f8af5eadd78d0ff4dc73ed386 Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sun, 18 Aug 2024 13:15:58 +0530 Subject: [PATCH 10/18] feat: can rename identifier --- src/parser/input/test_can_rename.proto | 5 ++++ src/parser/rename.rs | 25 +++++++++++++++---- ...s__parser__rename__test__can_rename-3.snap | 10 ++++++++ ...s__parser__rename__test__can_rename-4.snap | 10 ++++++++ 4 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 src/parser/snapshots/protols__parser__rename__test__can_rename-3.snap create mode 100644 src/parser/snapshots/protols__parser__rename__test__can_rename-4.snap diff --git a/src/parser/input/test_can_rename.proto b/src/parser/input/test_can_rename.proto index 09f2cda..687f950 100644 --- a/src/parser/input/test_can_rename.proto +++ b/src/parser/input/test_can_rename.proto @@ -13,4 +13,9 @@ message Book { string name = 1; string country = 2; }; + +} + +message Outer { + Book.Author a = 1; } diff --git a/src/parser/rename.rs b/src/parser/rename.rs index 7102263..950cf74 100644 --- a/src/parser/rename.rs +++ b/src/parser/rename.rs @@ -10,11 +10,15 @@ 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_actionable(&n.parent().unwrap()) { + Some(Range { + start: ts_to_lsp_position(&n.start_position()), + end: ts_to_lsp_position(&n.end_position()), + }) + } else { + None + } }) } @@ -100,6 +104,15 @@ mod test { 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.clone(), contents); assert!(parsed.is_some()); @@ -107,5 +120,7 @@ mod test { 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__rename__test__can_rename-3.snap b/src/parser/snapshots/protols__parser__rename__test__can_rename-3.snap new file mode 100644 index 0000000..25a8302 --- /dev/null +++ b/src/parser/snapshots/protols__parser__rename__test__can_rename-3.snap @@ -0,0 +1,10 @@ +--- +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 new file mode 100644 index 0000000..491df1e --- /dev/null +++ b/src/parser/snapshots/protols__parser__rename__test__can_rename-4.snap @@ -0,0 +1,10 @@ +--- +source: src/parser/rename.rs +expression: tree.can_rename(&pos_outer_type) +--- +start: + line: 19 + character: 4 +end: + line: 19 + character: 8 From 1ff178c3cb5d1b9e63fcd6869a6396fd285ed10b Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sun, 18 Aug 2024 15:37:12 +0530 Subject: [PATCH 11/18] fix: properly support can_rename option --- src/lsp.rs | 22 +++++++++++++++++++--- src/workspace/definition.rs | 1 - 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/lsp.rs b/src/lsp.rs index 690dd1e..114b241 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -9,8 +9,8 @@ use async_lsp::lsp_types::{ DocumentSymbolResponse, FileOperationFilter, FileOperationPattern, FileOperationPatternKind, FileOperationRegistrationOptions, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, HoverProviderCapability, InitializeParams, InitializeResult, OneOf, - PrepareRenameResponse, RenameFilesParams, RenameParams, ServerCapabilities, ServerInfo, - TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, Url, + PrepareRenameResponse, RenameFilesParams, RenameOptions, RenameParams, ServerCapabilities, + ServerInfo, TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, Url, WorkspaceEdit, WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities, }; @@ -71,6 +71,21 @@ impl LanguageServer for ProtoLanguageServer { }) } + 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(), + }) + } + let response = InitializeResult { capabilities: ServerCapabilities { // todo(): We might prefer incremental sync at some later stage @@ -82,7 +97,7 @@ impl LanguageServer for ProtoLanguageServer { 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() }, @@ -173,6 +188,7 @@ impl LanguageServer for ProtoLanguageServer { }; let response = tree.can_rename(&pos).map(PrepareRenameResponse::Range); + Box::pin(async move { Ok(response) }) } diff --git a/src/workspace/definition.rs b/src/workspace/definition.rs index 4ca3277..79afe20 100644 --- a/src/workspace/definition.rs +++ b/src/workspace/definition.rs @@ -42,6 +42,5 @@ mod test { assert_yaml_snapshot!(state.definition("com.library", "Author.Address")); assert_yaml_snapshot!(state.definition("com.library", "com.utility.Foobar.Baz")); assert_yaml_snapshot!(state.definition("com.utility", "Baz")); - } } From 4757f017136ac5ae0965780e09a9700c6c6da2fe Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sun, 18 Aug 2024 23:22:51 +0530 Subject: [PATCH 12/18] fix: only allow userdefined types to be renamed --- src/parser/rename.rs | 2 +- .../protols__parser__rename__test__can_rename-3.snap | 7 +------ .../protols__parser__rename__test__can_rename-4.snap | 7 +------ 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/parser/rename.rs b/src/parser/rename.rs index 950cf74..5da38fc 100644 --- a/src/parser/rename.rs +++ b/src/parser/rename.rs @@ -11,7 +11,7 @@ impl ParsedTree { self.get_node_at_position(pos) .filter(NodeKind::is_identifier) .and_then(|n| { - if n.parent().is_some() && NodeKind::is_actionable(&n.parent().unwrap()) { + 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()), 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 index 25a8302..9292d37 100644 --- a/src/parser/snapshots/protols__parser__rename__test__can_rename-3.snap +++ b/src/parser/snapshots/protols__parser__rename__test__can_rename-3.snap @@ -2,9 +2,4 @@ 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 index 491df1e..9545453 100644 --- a/src/parser/snapshots/protols__parser__rename__test__can_rename-4.snap +++ b/src/parser/snapshots/protols__parser__rename__test__can_rename-4.snap @@ -2,9 +2,4 @@ source: src/parser/rename.rs expression: tree.can_rename(&pos_outer_type) --- -start: - line: 19 - character: 4 -end: - line: 19 - character: 8 +~ From 129c18971e35c5851e22f4ce090867e17aa9df06 Mon Sep 17 00:00:00 2001 From: coder3101 Date: Mon, 19 Aug 2024 00:44:26 +0530 Subject: [PATCH 13/18] feat: add completion for enum and message in a package --- README.md | 12 +++--- sample/simple.proto | 6 +++ src/lsp.rs | 12 +++++- src/main.rs | 1 + src/{parser => }/nodekind.rs | 8 ++++ src/parser/definition.rs | 2 +- src/parser/diagnostics.rs | 4 +- src/parser/docsymbol.rs | 4 +- src/parser/hover.rs | 2 +- src/parser/input/test_can_rename.proto | 2 +- .../input/test_collect_parse_error1.proto | 2 +- .../input/test_collect_parse_error2.proto | 2 +- src/parser/input/test_document_symbols.proto | 2 +- src/parser/input/test_filter.proto | 2 +- src/parser/input/test_goto_definition.proto | 2 +- src/parser/input/test_hover.proto | 2 +- src/parser/input/test_rename.proto | 2 +- src/parser/mod.rs | 1 - src/parser/rename.rs | 4 +- ...protols__parser__tree__test__filter-2.snap | 2 +- src/parser/tree.rs | 4 +- src/state.rs | 37 ++++++++++++++++++- src/workspace/definition.rs | 6 +-- src/workspace/hover.rs | 6 +-- src/workspace/input/a.proto | 2 +- src/workspace/input/b.proto | 3 +- 26 files changed, 93 insertions(+), 39 deletions(-) rename src/{parser => }/nodekind.rs (90%) diff --git a/README.md b/README.md index 1e1b7a9..d19c8e9 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,12 @@ A Language Server for **proto3** files. It uses tree-sitter parser for all opera ![](./assets/protols.mov) ## Features -- [x] Hover -- [x] Go to definition -- [x] Diagnostics -- [x] Document Symbols for message and enums in current document -- [x] Rename message, enum and rpc -- [x] Completion for proto3 keywords +- [x] Completion (keywords, enums and messages of the package) +- [x] Diagnostics - based on sytax errors +- [x] Document Symbols for message and enums +- [x] Go to definition - across packages +- [x] Hover - across packages +- [x] Rename - in current buffer only ## Installation diff --git a/sample/simple.proto b/sample/simple.proto index 6ccdb13..ee51325 100644 --- a/sample/simple.proto +++ b/sample/simple.proto @@ -9,12 +9,18 @@ message Book { string title = 2; 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 diff --git a/src/lsp.rs b/src/lsp.rs index 114b241..2a261c9 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -156,14 +156,16 @@ impl LanguageServer for ProtoLanguageServer { } 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(), @@ -172,6 +174,12 @@ impl LanguageServer for ProtoLanguageServer { }) .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))) }) } diff --git a/src/main.rs b/src/main.rs index f0d88a1..ed10ffb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ use tower::ServiceBuilder; use tracing::Level; mod lsp; +mod nodekind; mod parser; mod server; mod state; diff --git a/src/parser/nodekind.rs b/src/nodekind.rs similarity index 90% rename from src/parser/nodekind.rs rename to src/nodekind.rs index adbc67c..83a7d94 100644 --- a/src/parser/nodekind.rs +++ b/src/nodekind.rs @@ -39,6 +39,14 @@ impl NodeKind { 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() } diff --git a/src/parser/definition.rs b/src/parser/definition.rs index d641b35..56da01d 100644 --- a/src/parser/definition.rs +++ b/src/parser/definition.rs @@ -1,7 +1,7 @@ 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; diff --git a/src/parser/diagnostics.rs b/src/parser/diagnostics.rs index 218f73f..e84f5f2 100644 --- a/src/parser/diagnostics.rs +++ b/src/parser/diagnostics.rs @@ -1,8 +1,8 @@ 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) -> PublishDiagnosticsParams { diff --git a/src/parser/docsymbol.rs b/src/parser/docsymbol.rs index 7bfbeae..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 { diff --git a/src/parser/hover.rs b/src/parser/hover.rs index f1c9dba..c9a659e 100644 --- a/src/parser/hover.rs +++ b/src/parser/hover.rs @@ -1,7 +1,7 @@ use async_lsp::lsp_types::MarkedString; use tree_sitter::Node; -use crate::parser::nodekind::NodeKind; +use crate::nodekind::NodeKind; use super::ParsedTree; diff --git a/src/parser/input/test_can_rename.proto b/src/parser/input/test_can_rename.proto index 687f950..ebc6df6 100644 --- a/src/parser/input/test_can_rename.proto +++ b/src/parser/input/test_can_rename.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package com.book; +package com.parser; // A Book is book message Book { diff --git a/src/parser/input/test_collect_parse_error1.proto b/src/parser/input/test_collect_parse_error1.proto index 65c4675..4f931bf 100644 --- a/src/parser/input/test_collect_parse_error1.proto +++ b/src/parser/input/test_collect_parse_error1.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package test; +package com.parser; message Foo { reserved 1; diff --git a/src/parser/input/test_collect_parse_error2.proto b/src/parser/input/test_collect_parse_error2.proto index 6e5341b..15e7bdd 100644 --- a/src/parser/input/test_collect_parse_error2.proto +++ b/src/parser/input/test_collect_parse_error2.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package com.book; +package com.parser; message Book { message Author { diff --git a/src/parser/input/test_document_symbols.proto b/src/parser/input/test_document_symbols.proto index 87b9c03..05a1577 100644 --- a/src/parser/input/test_document_symbols.proto +++ b/src/parser/input/test_document_symbols.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package com.symbols; +package com.parser; // outer 1 comment message Outer1 { diff --git a/src/parser/input/test_filter.proto b/src/parser/input/test_filter.proto index f5a1c7d..2d36db0 100644 --- a/src/parser/input/test_filter.proto +++ b/src/parser/input/test_filter.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package com.book; +package com.parser; message Book { diff --git a/src/parser/input/test_goto_definition.proto b/src/parser/input/test_goto_definition.proto index df14735..7cad4be 100644 --- a/src/parser/input/test_goto_definition.proto +++ b/src/parser/input/test_goto_definition.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package com.book; +package com.parser; message Book { message Author { diff --git a/src/parser/input/test_hover.proto b/src/parser/input/test_hover.proto index 8dbd247..34e6761 100644 --- a/src/parser/input/test_hover.proto +++ b/src/parser/input/test_hover.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package com.book; +package com.parser; // A Book is book message Book { diff --git a/src/parser/input/test_rename.proto b/src/parser/input/test_rename.proto index 97bf732..a10bef9 100644 --- a/src/parser/input/test_rename.proto +++ b/src/parser/input/test_rename.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package com.book; +package com.parser; // A Book is book message Book { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 51e0336..180b99d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5,7 +5,6 @@ mod definition; mod diagnostics; mod docsymbol; mod hover; -mod nodekind; mod rename; mod tree; diff --git a/src/parser/rename.rs b/src/parser/rename.rs index 5da38fc..e61f231 100644 --- a/src/parser/rename.rs +++ b/src/parser/rename.rs @@ -2,9 +2,9 @@ use std::collections::HashMap; 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 { diff --git a/src/parser/snapshots/protols__parser__tree__test__filter-2.snap b/src/parser/snapshots/protols__parser__tree__test__filter-2.snap index 403f7c4..022e111 100644 --- a/src/parser/snapshots/protols__parser__tree__test__filter-2.snap +++ b/src/parser/snapshots/protols__parser__tree__test__filter-2.snap @@ -2,4 +2,4 @@ source: src/parser/tree.rs expression: package_name --- -com.book +com.parser diff --git a/src/parser/tree.rs b/src/parser/tree.rs index cfef121..fc82d40 100644 --- a/src/parser/tree.rs +++ b/src/parser/tree.rs @@ -1,9 +1,9 @@ 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>( diff --git a/src/state.rs b/src/state.rs index 42e4c47..d356406 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,10 +1,16 @@ use std::{collections::HashMap, fs::read_to_string}; use tracing::{error, info}; -use async_lsp::lsp_types::{PublishDiagnosticsParams, Url, WorkspaceFolder}; +use async_lsp::lsp_types::{ + CompletionItem, CompletionItemKind, PublishDiagnosticsParams, Url, WorkspaceFolder, +}; +use tree_sitter::Node; use walkdir::WalkDir; -use crate::parser::{ParsedTree, ProtoParser}; +use crate::{ + nodekind::NodeKind, + parser::{ParsedTree, ProtoParser}, +}; pub struct ProtoLanguageState { documents: HashMap, @@ -106,4 +112,31 @@ impl ProtoLanguageState { 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); + return 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/workspace/definition.rs b/src/workspace/definition.rs index 79afe20..f0be3f3 100644 --- a/src/workspace/definition.rs +++ b/src/workspace/definition.rs @@ -38,9 +38,9 @@ mod test { state.upsert_file(&b_uri, b.to_owned()); state.upsert_file(&c_uri, c.to_owned()); - assert_yaml_snapshot!(state.definition("com.library", "Author")); - assert_yaml_snapshot!(state.definition("com.library", "Author.Address")); - assert_yaml_snapshot!(state.definition("com.library", "com.utility.Foobar.Baz")); + 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 index 45b8f06..a71f68b 100644 --- a/src/workspace/hover.rs +++ b/src/workspace/hover.rs @@ -39,9 +39,9 @@ mod test { state.upsert_file(&b_uri, b.to_owned()); state.upsert_file(&c_uri, c.to_owned()); - assert_yaml_snapshot!(state.hover("com.library", "Author")); - assert_yaml_snapshot!(state.hover("com.library", "Author.Address")); - assert_yaml_snapshot!(state.hover("com.library", "com.utility.Foobar.Baz")); + 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 index b270b3b..a982e4a 100644 --- a/src/workspace/input/a.proto +++ b/src/workspace/input/a.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package com.library; +package com.workspace; import "c.proto"; diff --git a/src/workspace/input/b.proto b/src/workspace/input/b.proto index db47d7f..668c23b 100644 --- a/src/workspace/input/b.proto +++ b/src/workspace/input/b.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package com.library; +package com.workspace; // A author is a author message Author { @@ -13,4 +13,3 @@ message Author { Address foo = 2; } - From df828f3fded49c57b7cf124dcdf1ee57eaea882e Mon Sep 17 00:00:00 2001 From: coder3101 Date: Mon, 19 Aug 2024 00:46:51 +0530 Subject: [PATCH 14/18] fix: clippy lint --- src/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state.rs b/src/state.rs index d356406..899eed3 100644 --- a/src/state.rs +++ b/src/state.rs @@ -125,7 +125,7 @@ impl ProtoLanguageState { ..Default::default() }); v.extend(t); - return v; + v }) }; From 0850a947b28e93822a7dcb829a9ecef140fc4475 Mon Sep 17 00:00:00 2001 From: coder3101 Date: Mon, 19 Aug 2024 00:51:08 +0530 Subject: [PATCH 15/18] feat: add build and test tag --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d19c8e9..a564183 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # 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) From 3d0a67561dc3c06f8dff208ffee60199ffad4011 Mon Sep 17 00:00:00 2001 From: Ashar Date: Mon, 19 Aug 2024 00:51:59 +0530 Subject: [PATCH 16/18] fix tag orientation --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a564183..1330a82 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # 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) From 05c4973699125565cf41858a3a81d2102e9ca338 Mon Sep 17 00:00:00 2001 From: Ashar Date: Mon, 19 Aug 2024 00:57:30 +0530 Subject: [PATCH 17/18] feat: add crates io tag --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1330a82..049f24d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # protols +[![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. From 0a8316a179b388c1956731507521e6eeb89619ec Mon Sep 17 00:00:00 2001 From: Ashar Date: Mon, 19 Aug 2024 00:59:04 +0530 Subject: [PATCH 18/18] remove Mason message --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 049f24d..818d39f 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ A Language Server for **proto3** files. It uses tree-sitter parser for all opera ## 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{}