Skip to content

Commit e38bf35

Browse files
committed
feat: allow hovering over imports
1 parent 6001477 commit e38bf35

File tree

8 files changed

+150
-58
lines changed

8 files changed

+150
-58
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
## ✨ Features
1313

1414
-**Code Completion**: Auto-complete messages, enums, and keywords in your `.proto` files.
15-
-**Diagnostics**: Syntax errors detected with the tree-sitter parser.
15+
-**Diagnostics**: Syntax errors and import error detected with the tree-sitter parser.
1616
-**Document Symbols**: Navigate and view all symbols, including messages and enums.
1717
-**Code Formatting**: Format `.proto` files using `clang-format` for a consistent style.
1818
-**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
138138

139139
### Hover Information
140140

141-
Hover over any symbol to get detailed documentation and comments associated with it. This works seamlessly across different packages and namespaces.
141+
Hover over any symbol or imports to get detailed documentation and comments associated with it. This works seamlessly across different packages and namespaces.
142142

143143
### Rename Symbols
144144

src/context/hoverable.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pub enum Hoverables {
2+
FieldType(String),
3+
ImportPath(String),
4+
Identifier(String),
5+
}

src/context/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod hoverable;

src/lsp.rs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@ use async_lsp::lsp_types::{
1010
DocumentSymbolParams, DocumentSymbolResponse, FileOperationFilter, FileOperationPattern,
1111
FileOperationPatternKind, FileOperationRegistrationOptions, GotoDefinitionParams,
1212
GotoDefinitionResponse, Hover, HoverContents, HoverParams, HoverProviderCapability,
13-
InitializeParams, InitializeResult, Location, OneOf, PrepareRenameResponse,
14-
ReferenceParams, RenameFilesParams, RenameOptions, RenameParams,
15-
ServerCapabilities, ServerInfo, TextDocumentPositionParams, TextDocumentSyncCapability,
16-
TextDocumentSyncKind, TextEdit, Url, WorkspaceEdit,
17-
WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities,
13+
InitializeParams, InitializeResult, Location, OneOf, PrepareRenameResponse, ReferenceParams,
14+
RenameFilesParams, RenameOptions, RenameParams, ServerCapabilities, ServerInfo,
15+
TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Url,
16+
WorkspaceEdit, WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities,
1817
WorkspaceServerCapabilities,
1918
};
2019
use async_lsp::{LanguageClient, LanguageServer, ResponseError};
@@ -131,10 +130,10 @@ impl LanguageServer for ProtoLanguageServer {
131130
};
132131

133132
let content = self.state.get_content(&uri);
134-
let identifier = tree.get_hoverable_node_text_at_position(&pos, content.as_bytes());
133+
let hv = tree.get_hoverable_at_position(&pos, content.as_bytes());
135134
let current_package_name = tree.get_package_name(content.as_bytes());
136135

137-
let Some(identifier) = identifier else {
136+
let Some(hv) = hv else {
138137
error!(uri=%uri, "failed to get identifier");
139138
return Box::pin(async move { Ok(None) });
140139
};
@@ -144,9 +143,8 @@ impl LanguageServer for ProtoLanguageServer {
144143
return Box::pin(async move { Ok(None) });
145144
};
146145

147-
let result = self
148-
.state
149-
.hover(current_package_name.as_ref(), identifier.as_ref());
146+
let ipath = self.configs.get_include_paths(&uri).unwrap_or_default();
147+
let result = self.state.hover(&ipath, current_package_name.as_ref(), hv);
150148

151149
Box::pin(async move {
152150
Ok(result.map(|r| Hover {
@@ -290,7 +288,7 @@ impl LanguageServer for ProtoLanguageServer {
290288
};
291289

292290
let content = self.state.get_content(&uri);
293-
let identifier = tree.get_actionable_node_text_at_position(&pos, content.as_bytes());
291+
let identifier = tree.get_user_defined_text(&pos, content.as_bytes());
294292
let current_package_name = tree.get_package_name(content.as_bytes());
295293

296294
let Some(identifier) = identifier else {

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ mod server;
1818
mod state;
1919
mod utils;
2020
mod workspace;
21+
mod context;
2122

2223
#[tokio::main(flavor = "current_thread")]
2324
async fn main() {

src/parser/tree.rs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use async_lsp::lsp_types::{Position, Range};
22
use tree_sitter::{Node, TreeCursor};
33

44
use crate::{
5+
context::hoverable::Hoverables,
56
nodekind::NodeKind,
67
utils::{lsp_to_ts_point, ts_to_lsp_position},
78
};
@@ -57,7 +58,7 @@ impl ParsedTree {
5758
}
5859
}
5960

60-
pub fn get_actionable_node_text_at_position<'a>(
61+
pub fn get_user_defined_text<'a>(
6162
&'a self,
6263
pos: &Position,
6364
content: &'a [u8],
@@ -66,14 +67,30 @@ impl ParsedTree {
6667
.map(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error"))
6768
}
6869

69-
pub fn get_hoverable_node_text_at_position<'a>(
70+
pub fn get_hoverable_at_position<'a>(
7071
&'a self,
7172
pos: &Position,
7273
content: &'a [u8],
73-
) -> Option<&'a str> {
74+
) -> Option<Hoverables> {
7475
let n = self.get_node_at_position(pos)?;
75-
self.get_actionable_node_text_at_position(pos, content)
76-
.or(Some(n.kind()))
76+
77+
// If node is import path. return the whole path, removing the quotes
78+
if n.parent().filter(NodeKind::is_import_path).is_some() {
79+
return Some(Hoverables::ImportPath(
80+
n.utf8_text(content)
81+
.expect("utf-8 parse error")
82+
.trim_matches('"')
83+
.to_string(),
84+
));
85+
}
86+
87+
// If node is user defined enum/message
88+
if let Some(identifier) = self.get_user_defined_text(pos, content) {
89+
return Some(Hoverables::Identifier(identifier.to_string()));
90+
}
91+
92+
// Lastly; fallback to either wellknown or builtin types
93+
Some(Hoverables::FieldType(n.kind().to_string()))
7794
}
7895

7996
pub fn get_ancestor_nodes_at_position<'a>(&'a self, pos: &Position) -> Vec<Node<'a>> {

src/workspace/hover.rs

Lines changed: 103 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
use std::{collections::HashMap, sync::LazyLock};
1+
use std::{collections::HashMap, path::PathBuf, sync::LazyLock};
22

33
use async_lsp::lsp_types::{MarkupContent, MarkupKind};
44

5-
use crate::{state::ProtoLanguageState, utils::split_identifier_package};
5+
use crate::{
6+
context::hoverable::Hoverables, state::ProtoLanguageState, utils::split_identifier_package,
7+
};
68

79
static BUITIN_DOCS: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
810
HashMap::from([
@@ -586,45 +588,76 @@ message Value {
586588
});
587589

588590
impl ProtoLanguageState {
589-
pub fn hover(&self, curr_package: &str, identifier: &str) -> Option<MarkupContent> {
590-
if let Some(docs) = BUITIN_DOCS.get(identifier) {
591-
return Some(MarkupContent {
592-
kind: MarkupKind::Markdown,
593-
value: docs.to_string(),
594-
});
595-
}
591+
pub fn hover(
592+
&self,
593+
ipath: &[PathBuf],
594+
curr_package: &str,
595+
hv: Hoverables,
596+
) -> Option<MarkupContent> {
597+
let v = match hv {
598+
Hoverables::FieldType(field) => {
599+
// Type is a builtin
600+
if let Some(docs) = BUITIN_DOCS.get(field.as_str()) {
601+
docs.to_string()
602+
} else {
603+
String::new()
604+
}
605+
}
606+
Hoverables::ImportPath(path) => {
607+
if let Some(p) = ipath.iter().map(|p| p.join(&path)).find(|p| p.exists()) {
608+
format!(
609+
r#"Import: `{path}` protobuf file,
610+
---
611+
Included from {}"#,
612+
p.to_string_lossy(),
613+
)
614+
} else {
615+
String::new()
616+
}
617+
}
618+
Hoverables::Identifier(identifier) => {
619+
let (mut package, identifier) = split_identifier_package(identifier.as_str());
620+
if package.is_empty() {
621+
package = curr_package;
622+
}
596623

597-
if let Some(wellknown) = WELLKNOWN_DOCS
598-
.get(identifier)
599-
.or(WELLKNOWN_DOCS.get(format!("google.protobuf.{identifier}").as_str()))
600-
{
601-
return Some(MarkupContent {
602-
kind: MarkupKind::Markdown,
603-
value: wellknown.to_string(),
604-
});
605-
}
624+
// Node is user defined type or well known type
625+
// If user defined,
626+
let mut result = WELLKNOWN_DOCS
627+
.get(format!("{package}.{identifier}").as_str())
628+
.map(|&s| s.to_string())
629+
.unwrap_or_default();
606630

607-
let (mut package, identifier) = split_identifier_package(identifier);
608-
if package.is_empty() {
609-
package = curr_package;
610-
}
631+
// If no well known was found; try parsing from trees.
632+
if result.is_empty() {
633+
for tree in self.get_trees_for_package(package) {
634+
let res = tree.hover(identifier, self.get_content(&tree.uri));
635+
636+
if res.is_empty() {
637+
continue;
638+
}
611639

612-
for tree in self.get_trees_for_package(package) {
613-
let res = tree.hover(identifier, self.get_content(&tree.uri));
614-
if !res.is_empty() {
615-
return Some(MarkupContent {
616-
kind: MarkupKind::Markdown,
617-
value: format!(
618-
r#"`{identifier}` message or enum type, package: `{package}`
640+
result = format!(
641+
r#"`{identifier}` message or enum type, package: `{package}`
619642
---
620643
{}"#,
621-
res[0].clone()
622-
),
623-
});
644+
res[0].clone()
645+
);
646+
break;
647+
}
648+
}
649+
650+
result
624651
}
625-
}
652+
};
626653

627-
None
654+
match v {
655+
v if v.is_empty() => None,
656+
v => Some(MarkupContent {
657+
kind: MarkupKind::Markdown,
658+
value: v,
659+
}),
660+
}
628661
}
629662
}
630663

@@ -634,6 +667,7 @@ mod test {
634667

635668
use insta::assert_yaml_snapshot;
636669

670+
use crate::context::hoverable::Hoverables;
637671
use crate::state::ProtoLanguageState;
638672

639673
#[test]
@@ -652,11 +686,40 @@ mod test {
652686
state.upsert_file(&b_uri, b.to_owned(), &ipath);
653687
state.upsert_file(&c_uri, c.to_owned(), &ipath);
654688

655-
assert_yaml_snapshot!(state.hover("com.workspace", "google.protobuf.Any"));
656-
assert_yaml_snapshot!(state.hover("com.workspace", "Author"));
657-
assert_yaml_snapshot!(state.hover("com.workspace", "int64"));
658-
assert_yaml_snapshot!(state.hover("com.workspace", "Author.Address"));
659-
assert_yaml_snapshot!(state.hover("com.workspace", "com.utility.Foobar.Baz"));
660-
assert_yaml_snapshot!(state.hover("com.utility", "Baz"));
689+
assert_yaml_snapshot!(state.hover(
690+
&ipath,
691+
"com.workspace",
692+
Hoverables::Identifier("google.protobuf.Any".to_string())
693+
));
694+
assert_yaml_snapshot!(state.hover(
695+
&ipath,
696+
"com.workspace",
697+
Hoverables::Identifier("Author".to_string())
698+
));
699+
assert_yaml_snapshot!(state.hover(
700+
&ipath,
701+
"com.workspace",
702+
Hoverables::FieldType("int64".to_string())
703+
));
704+
assert_yaml_snapshot!(state.hover(
705+
&ipath,
706+
"com.workspace",
707+
Hoverables::Identifier("Author.Address".to_string())
708+
));
709+
assert_yaml_snapshot!(state.hover(
710+
&ipath,
711+
"com.workspace",
712+
Hoverables::Identifier("com.utility.Foobar.Baz".to_string())
713+
));
714+
assert_yaml_snapshot!(state.hover(
715+
&ipath,
716+
"com.utility",
717+
Hoverables::Identifier("Baz".to_string())
718+
));
719+
assert_yaml_snapshot!(state.hover(
720+
&ipath,
721+
"com.workspace",
722+
Hoverables::ImportPath("c.proto".to_string())
723+
))
661724
}
662725
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
source: src/workspace/hover.rs
3+
expression: "state.hover(&ipath, \"com.workspace\",\nHoverables::ImportPath(\"c.proto\".to_string()))"
4+
snapshot_kind: text
5+
---
6+
kind: markdown
7+
value: "Import: `c.proto` protobuf file,\n---\nIncluded from src/workspace/input/c.proto"

0 commit comments

Comments
 (0)