From 0d3d6abd8ee89810923f6eea7d8d414e8815887c Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sat, 17 Aug 2024 23:06:46 +0530 Subject: [PATCH] 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", "")); + } +}