diff --git a/README.md b/README.md index 7c4d5c1..7b7523f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ## ✨ Features - ✅ **Code Completion**: Auto-complete messages, enums, and keywords in your `.proto` files. -- ✅ **Diagnostics**: Syntax errors detected with the tree-sitter parser. +- ✅ **Diagnostics**: Syntax errors and import error detected with the tree-sitter parser. - ✅ **Document Symbols**: Navigate and view all symbols, including messages and enums. - ✅ **Code Formatting**: Format `.proto` files using `clang-format` for a consistent style. - ✅ **Go to Definition**: Jump to the definition of symbols like messages or enums. @@ -138,7 +138,7 @@ Jump directly to the definition of any custom symbol, including those in other f ### Hover Information -Hover over any symbol to get detailed documentation and comments associated with it. This works seamlessly across different packages and namespaces. +Hover over any symbol or imports to get detailed documentation and comments associated with it. This works seamlessly across different packages and namespaces. ### Rename Symbols diff --git a/src/context/hoverable.rs b/src/context/hoverable.rs new file mode 100644 index 0000000..7bc5f8e --- /dev/null +++ b/src/context/hoverable.rs @@ -0,0 +1,5 @@ +pub enum Hoverables { + FieldType(String), + ImportPath(String), + Identifier(String), +} diff --git a/src/context/mod.rs b/src/context/mod.rs new file mode 100644 index 0000000..64ed538 --- /dev/null +++ b/src/context/mod.rs @@ -0,0 +1 @@ +pub mod hoverable; diff --git a/src/lsp.rs b/src/lsp.rs index 52c91af..4e1ad6b 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -10,11 +10,10 @@ use async_lsp::lsp_types::{ DocumentSymbolParams, DocumentSymbolResponse, FileOperationFilter, FileOperationPattern, FileOperationPatternKind, FileOperationRegistrationOptions, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, HoverProviderCapability, - InitializeParams, InitializeResult, Location, OneOf, PrepareRenameResponse, - ReferenceParams, RenameFilesParams, RenameOptions, RenameParams, - ServerCapabilities, ServerInfo, TextDocumentPositionParams, TextDocumentSyncCapability, - TextDocumentSyncKind, TextEdit, Url, WorkspaceEdit, - WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities, + InitializeParams, InitializeResult, Location, OneOf, PrepareRenameResponse, ReferenceParams, + RenameFilesParams, RenameOptions, RenameParams, ServerCapabilities, ServerInfo, + TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Url, + WorkspaceEdit, WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities, }; use async_lsp::{LanguageClient, LanguageServer, ResponseError}; @@ -131,10 +130,10 @@ impl LanguageServer for ProtoLanguageServer { }; let content = self.state.get_content(&uri); - let identifier = tree.get_hoverable_node_text_at_position(&pos, content.as_bytes()); + let hv = tree.get_hoverable_at_position(&pos, content.as_bytes()); let current_package_name = tree.get_package_name(content.as_bytes()); - let Some(identifier) = identifier else { + let Some(hv) = hv else { error!(uri=%uri, "failed to get identifier"); return Box::pin(async move { Ok(None) }); }; @@ -144,9 +143,8 @@ impl LanguageServer for ProtoLanguageServer { return Box::pin(async move { Ok(None) }); }; - let result = self - .state - .hover(current_package_name.as_ref(), identifier.as_ref()); + let ipath = self.configs.get_include_paths(&uri).unwrap_or_default(); + let result = self.state.hover(&ipath, current_package_name.as_ref(), hv); Box::pin(async move { Ok(result.map(|r| Hover { @@ -290,7 +288,7 @@ impl LanguageServer for ProtoLanguageServer { }; let content = self.state.get_content(&uri); - let identifier = tree.get_actionable_node_text_at_position(&pos, content.as_bytes()); + let identifier = tree.get_user_defined_text(&pos, content.as_bytes()); let current_package_name = tree.get_package_name(content.as_bytes()); let Some(identifier) = identifier else { diff --git a/src/main.rs b/src/main.rs index 8a45be5..6d5e5b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ mod server; mod state; mod utils; mod workspace; +mod context; #[tokio::main(flavor = "current_thread")] async fn main() { diff --git a/src/parser/tree.rs b/src/parser/tree.rs index 8032065..6ec4f14 100644 --- a/src/parser/tree.rs +++ b/src/parser/tree.rs @@ -2,6 +2,7 @@ use async_lsp::lsp_types::{Position, Range}; use tree_sitter::{Node, TreeCursor}; use crate::{ + context::hoverable::Hoverables, nodekind::NodeKind, utils::{lsp_to_ts_point, ts_to_lsp_position}, }; @@ -57,7 +58,7 @@ impl ParsedTree { } } - pub fn get_actionable_node_text_at_position<'a>( + pub fn get_user_defined_text<'a>( &'a self, pos: &Position, content: &'a [u8], @@ -66,14 +67,30 @@ impl ParsedTree { .map(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error")) } - pub fn get_hoverable_node_text_at_position<'a>( + pub fn get_hoverable_at_position<'a>( &'a self, pos: &Position, content: &'a [u8], - ) -> Option<&'a str> { + ) -> Option { let n = self.get_node_at_position(pos)?; - self.get_actionable_node_text_at_position(pos, content) - .or(Some(n.kind())) + + // If node is import path. return the whole path, removing the quotes + if n.parent().filter(NodeKind::is_import_path).is_some() { + return Some(Hoverables::ImportPath( + n.utf8_text(content) + .expect("utf-8 parse error") + .trim_matches('"') + .to_string(), + )); + } + + // If node is user defined enum/message + if let Some(identifier) = self.get_user_defined_text(pos, content) { + return Some(Hoverables::Identifier(identifier.to_string())); + } + + // Lastly; fallback to either wellknown or builtin types + Some(Hoverables::FieldType(n.kind().to_string())) } pub fn get_ancestor_nodes_at_position<'a>(&'a self, pos: &Position) -> Vec> { diff --git a/src/workspace/hover.rs b/src/workspace/hover.rs index 8fa6c18..2f56c52 100644 --- a/src/workspace/hover.rs +++ b/src/workspace/hover.rs @@ -1,8 +1,10 @@ -use std::{collections::HashMap, sync::LazyLock}; +use std::{collections::HashMap, path::PathBuf, sync::LazyLock}; use async_lsp::lsp_types::{MarkupContent, MarkupKind}; -use crate::{state::ProtoLanguageState, utils::split_identifier_package}; +use crate::{ + context::hoverable::Hoverables, state::ProtoLanguageState, utils::split_identifier_package, +}; static BUITIN_DOCS: LazyLock> = LazyLock::new(|| { HashMap::from([ @@ -586,45 +588,76 @@ message Value { }); impl ProtoLanguageState { - pub fn hover(&self, curr_package: &str, identifier: &str) -> Option { - if let Some(docs) = BUITIN_DOCS.get(identifier) { - return Some(MarkupContent { - kind: MarkupKind::Markdown, - value: docs.to_string(), - }); - } + pub fn hover( + &self, + ipath: &[PathBuf], + curr_package: &str, + hv: Hoverables, + ) -> Option { + let v = match hv { + Hoverables::FieldType(field) => { + // Type is a builtin + if let Some(docs) = BUITIN_DOCS.get(field.as_str()) { + docs.to_string() + } else { + String::new() + } + } + Hoverables::ImportPath(path) => { + if let Some(p) = ipath.iter().map(|p| p.join(&path)).find(|p| p.exists()) { + format!( + r#"Import: `{path}` protobuf file, +--- +Included from {}"#, + p.to_string_lossy(), + ) + } else { + String::new() + } + } + Hoverables::Identifier(identifier) => { + let (mut package, identifier) = split_identifier_package(identifier.as_str()); + if package.is_empty() { + package = curr_package; + } - if let Some(wellknown) = WELLKNOWN_DOCS - .get(identifier) - .or(WELLKNOWN_DOCS.get(format!("google.protobuf.{identifier}").as_str())) - { - return Some(MarkupContent { - kind: MarkupKind::Markdown, - value: wellknown.to_string(), - }); - } + // Node is user defined type or well known type + // If user defined, + let mut result = WELLKNOWN_DOCS + .get(format!("{package}.{identifier}").as_str()) + .map(|&s| s.to_string()) + .unwrap_or_default(); - let (mut package, identifier) = split_identifier_package(identifier); - if package.is_empty() { - package = curr_package; - } + // If no well known was found; try parsing from trees. + if result.is_empty() { + for tree in self.get_trees_for_package(package) { + let res = tree.hover(identifier, self.get_content(&tree.uri)); + + if res.is_empty() { + continue; + } - for tree in self.get_trees_for_package(package) { - let res = tree.hover(identifier, self.get_content(&tree.uri)); - if !res.is_empty() { - return Some(MarkupContent { - kind: MarkupKind::Markdown, - value: format!( - r#"`{identifier}` message or enum type, package: `{package}` + result = format!( + r#"`{identifier}` message or enum type, package: `{package}` --- {}"#, - res[0].clone() - ), - }); + res[0].clone() + ); + break; + } + } + + result } - } + }; - None + match v { + v if v.is_empty() => None, + v => Some(MarkupContent { + kind: MarkupKind::Markdown, + value: v, + }), + } } } @@ -634,6 +667,7 @@ mod test { use insta::assert_yaml_snapshot; + use crate::context::hoverable::Hoverables; use crate::state::ProtoLanguageState; #[test] @@ -652,11 +686,40 @@ mod test { state.upsert_file(&b_uri, b.to_owned(), &ipath); state.upsert_file(&c_uri, c.to_owned(), &ipath); - assert_yaml_snapshot!(state.hover("com.workspace", "google.protobuf.Any")); - assert_yaml_snapshot!(state.hover("com.workspace", "Author")); - assert_yaml_snapshot!(state.hover("com.workspace", "int64")); - 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")); + assert_yaml_snapshot!(state.hover( + &ipath, + "com.workspace", + Hoverables::Identifier("google.protobuf.Any".to_string()) + )); + assert_yaml_snapshot!(state.hover( + &ipath, + "com.workspace", + Hoverables::Identifier("Author".to_string()) + )); + assert_yaml_snapshot!(state.hover( + &ipath, + "com.workspace", + Hoverables::FieldType("int64".to_string()) + )); + assert_yaml_snapshot!(state.hover( + &ipath, + "com.workspace", + Hoverables::Identifier("Author.Address".to_string()) + )); + assert_yaml_snapshot!(state.hover( + &ipath, + "com.workspace", + Hoverables::Identifier("com.utility.Foobar.Baz".to_string()) + )); + assert_yaml_snapshot!(state.hover( + &ipath, + "com.utility", + Hoverables::Identifier("Baz".to_string()) + )); + assert_yaml_snapshot!(state.hover( + &ipath, + "com.workspace", + Hoverables::ImportPath("c.proto".to_string()) + )) } } diff --git a/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-7.snap b/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-7.snap new file mode 100644 index 0000000..cec01f9 --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-7.snap @@ -0,0 +1,7 @@ +--- +source: src/workspace/hover.rs +expression: "state.hover(&ipath, \"com.workspace\",\nHoverables::ImportPath(\"c.proto\".to_string()))" +snapshot_kind: text +--- +kind: markdown +value: "Import: `c.proto` protobuf file,\n---\nIncluded from src/workspace/input/c.proto"