Skip to content

Commit

Permalink
feat: allow hovering over imports
Browse files Browse the repository at this point in the history
  • Loading branch information
coder3101 committed Jan 25, 2025
1 parent 6001477 commit e38bf35
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 58 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions src/context/hoverable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub enum Hoverables {
FieldType(String),
ImportPath(String),
Identifier(String),
}
1 change: 1 addition & 0 deletions src/context/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod hoverable;
20 changes: 9 additions & 11 deletions src/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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) });
};
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod server;
mod state;
mod utils;
mod workspace;
mod context;

#[tokio::main(flavor = "current_thread")]
async fn main() {
Expand Down
27 changes: 22 additions & 5 deletions src/parser/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};
Expand Down Expand Up @@ -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],
Expand All @@ -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<Hoverables> {
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<Node<'a>> {
Expand Down
143 changes: 103 additions & 40 deletions src/workspace/hover.rs
Original file line number Diff line number Diff line change
@@ -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<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
HashMap::from([
Expand Down Expand Up @@ -586,45 +588,76 @@ message Value {
});

impl ProtoLanguageState {
pub fn hover(&self, curr_package: &str, identifier: &str) -> Option<MarkupContent> {
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<MarkupContent> {
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,
}),
}
}
}

Expand All @@ -634,6 +667,7 @@ mod test {

use insta::assert_yaml_snapshot;

use crate::context::hoverable::Hoverables;
use crate::state::ProtoLanguageState;

#[test]
Expand All @@ -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())
))
}
}
Original file line number Diff line number Diff line change
@@ -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"

0 comments on commit e38bf35

Please sign in to comment.