diff --git a/src/context/jumpable.rs b/src/context/jumpable.rs new file mode 100644 index 0000000..0514afb --- /dev/null +++ b/src/context/jumpable.rs @@ -0,0 +1,4 @@ +pub enum Jumpable { + Import(String), + Identifier(String) +} diff --git a/src/context/mod.rs b/src/context/mod.rs index 64ed538..7f79361 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -1 +1,2 @@ pub mod hoverable; +pub mod jumpable; diff --git a/src/lsp.rs b/src/lsp.rs index 4e1ad6b..bb4d00f 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -5,15 +5,16 @@ use tracing::{error, info}; use async_lsp::lsp_types::{ CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse, CreateFilesParams, DeleteFilesParams, DidChangeConfigurationParams, - DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, - DidSaveTextDocumentParams, DocumentFormattingParams, DocumentRangeFormattingParams, - 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, + DidChangeTextDocumentParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams, + DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentFormattingParams, + DocumentRangeFormattingParams, 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, WorkspaceServerCapabilities, }; use async_lsp::{LanguageClient, LanguageServer, ResponseError}; @@ -134,7 +135,7 @@ impl LanguageServer for ProtoLanguageServer { let current_package_name = tree.get_package_name(content.as_bytes()); let Some(hv) = hv else { - error!(uri=%uri, "failed to get identifier"); + error!(uri=%uri, "failed to get hoverable identifier"); return Box::pin(async move { Ok(None) }); }; @@ -153,6 +154,7 @@ impl LanguageServer for ProtoLanguageServer { })) }) } + fn completion( &mut self, params: CompletionParams, @@ -288,11 +290,11 @@ impl LanguageServer for ProtoLanguageServer { }; let content = self.state.get_content(&uri); - let identifier = tree.get_user_defined_text(&pos, content.as_bytes()); + let jump = tree.get_jumpable_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"); + let Some(jump) = jump else { + error!(uri=%uri, "failed to get jump identifier"); return Box::pin(async move { Ok(None) }); }; @@ -301,9 +303,10 @@ impl LanguageServer for ProtoLanguageServer { return Box::pin(async move { Ok(None) }); }; + let ipath = self.configs.get_include_paths(&uri).unwrap_or_default(); let locations = self .state - .definition(current_package_name.as_ref(), identifier.as_ref()); + .definition(&ipath, current_package_name.as_ref(), jump); let response = match locations.len() { 0 => None, @@ -464,4 +467,12 @@ impl LanguageServer for ProtoLanguageServer { fn did_change_configuration(&mut self, _: DidChangeConfigurationParams) -> Self::NotifyResult { ControlFlow::Continue(()) } + + // Required because when jumping to outside the workspace; this is triggered + fn did_change_workspace_folders( + &mut self, + _: DidChangeWorkspaceFoldersParams, + ) -> Self::NotifyResult { + ControlFlow::Continue(()) + } } diff --git a/src/parser/tree.rs b/src/parser/tree.rs index 6ec4f14..08839fc 100644 --- a/src/parser/tree.rs +++ b/src/parser/tree.rs @@ -2,7 +2,7 @@ use async_lsp::lsp_types::{Position, Range}; use tree_sitter::{Node, TreeCursor}; use crate::{ - context::hoverable::Hoverables, + context::{hoverable::Hoverables, jumpable::Jumpable}, nodekind::NodeKind, utils::{lsp_to_ts_point, ts_to_lsp_position}, }; @@ -63,10 +63,31 @@ impl ParsedTree { pos: &Position, content: &'a [u8], ) -> Option<&'a str> { - self.get_actionable_node_at_position(pos) + self.get_user_defined_node(pos) .map(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error")) } + pub fn get_jumpable_at_position(&self, pos: &Position, content: &[u8]) -> Option { + let n = self.get_node_at_position(pos)?; + + // If node is import path. return the whole path, removing the quotes + if n.parent().filter(NodeKind::is_import_path).is_some() { + return Some(Jumpable::Import( + 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(Jumpable::Identifier(identifier.to_string())); + } + + None + } + pub fn get_hoverable_at_position<'a>( &'a self, pos: &Position, @@ -94,7 +115,7 @@ impl ParsedTree { } pub fn get_ancestor_nodes_at_position<'a>(&'a self, pos: &Position) -> Vec> { - let Some(mut n) = self.get_actionable_node_at_position(pos) else { + let Some(mut n) = self.get_user_defined_node(pos) else { return vec![]; }; @@ -113,7 +134,7 @@ impl ParsedTree { nodes } - pub fn get_actionable_node_at_position<'a>(&'a self, pos: &Position) -> Option> { + pub fn get_user_defined_node<'a>(&'a self, pos: &Position) -> Option> { self.get_node_at_position(pos) .map(|n| { if NodeKind::is_actionable(&n) { @@ -161,7 +182,7 @@ impl ParsedTree { .collect() } - pub fn get_import_path<'a>(&self, content: &'a [u8]) -> Vec<&'a str> { + pub fn get_import_paths<'a>(&self, content: &'a [u8]) -> Vec<&'a str> { self.get_import_node() .into_iter() .map(|n| { @@ -218,7 +239,7 @@ mod test { let package_name = tree.get_package_name(contents.as_ref()); assert_yaml_snapshot!(package_name); - let imports = tree.get_import_path(contents.as_ref()); + let imports = tree.get_import_paths(contents.as_ref()); assert_yaml_snapshot!(imports); } } diff --git a/src/state.rs b/src/state.rs index 9c076e0..fcc025c 100644 --- a/src/state.rs +++ b/src/state.rs @@ -98,7 +98,7 @@ impl ProtoLanguageState { // After content is upserted, those imports which couldn't be located // are flagged as import error self.get_tree(uri) - .map(|t| t.get_import_path(content.as_ref())) + .map(|t| t.get_import_paths(content.as_ref())) .unwrap_or_default() .into_iter() .map(ToOwned::to_owned) diff --git a/src/workspace/definition.rs b/src/workspace/definition.rs index 020e992..36ec841 100644 --- a/src/workspace/definition.rs +++ b/src/workspace/definition.rs @@ -1,24 +1,53 @@ -use async_lsp::lsp_types::Location; +use std::path::PathBuf; -use crate::{state::ProtoLanguageState, utils::split_identifier_package}; +use async_lsp::lsp_types::{Location, Range, Url}; + +use crate::{ + context::jumpable::Jumpable, 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; + pub fn definition( + &self, + ipath: &[PathBuf], + curr_package: &str, + jump: Jumpable, + ) -> Vec { + match jump { + Jumpable::Import(path) => { + let Some(p) = ipath.iter().map(|p| p.join(&path)).find(|p| p.exists()) else { + return vec![]; + }; + + let Ok(uri) = Url::from_file_path(p) else { + return vec![]; + }; + + vec![Location { + uri, + range: Range::default(), // just start of the file + }] + } + Jumpable::Identifier(identifier) => { + let (mut package, identifier) = split_identifier_package(identifier.as_str()); + 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 + }) + } } - 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 { + use crate::context::jumpable::Jumpable; use std::path::PathBuf; use insta::assert_yaml_snapshot; @@ -41,9 +70,38 @@ mod test { state.upsert_file(&b_uri, b.to_owned(), &ipath); state.upsert_file(&c_uri, c.to_owned(), &ipath); - 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")); + assert_yaml_snapshot!(state.definition( + &ipath, + "com.workspace", + Jumpable::Identifier("Author".to_owned()) + )); + assert_yaml_snapshot!(state.definition( + &ipath, + "com.workspace", + Jumpable::Identifier("Author.Address".to_owned()) + )); + assert_yaml_snapshot!(state.definition( + &ipath, + "com.workspace", + Jumpable::Identifier("com.utility.Foobar.Baz".to_owned()) + )); + assert_yaml_snapshot!(state.definition( + &ipath, + "com.utility", + Jumpable::Identifier("Baz".to_owned()) + )); + + let loc = state.definition( + &vec![std::env::current_dir().unwrap().join(&ipath[0])], + "com.workspace", + Jumpable::Import("c.proto".to_owned()), + ); + + assert_eq!(loc.len(), 1); + assert!(loc[0] + .uri + .to_file_path() + .unwrap() + .ends_with(ipath[0].join("c.proto"))) } }