From 1b19a8aa5ecfc9d7115f291b97d413bd845c89b5 Mon Sep 17 00:00:00 2001 From: Jeremy Kolb Date: Mon, 30 Dec 2019 09:12:06 -0500 Subject: Implement proposed CallHierarchy feature See: https://github.com/microsoft/vscode-languageserver-node/blob/master/protocol/src/protocol.callHierarchy.proposed.ts --- crates/ra_ide/Cargo.toml | 1 + crates/ra_ide/src/call_hierarchy.rs | 337 +++++++++++++++++++++++++ crates/ra_ide/src/call_info.rs | 15 +- crates/ra_ide/src/display/navigation_target.rs | 2 +- crates/ra_ide/src/lib.rs | 20 ++ crates/ra_lsp_server/src/caps.rs | 6 +- crates/ra_lsp_server/src/conv.rs | 18 ++ crates/ra_lsp_server/src/main_loop.rs | 3 + crates/ra_lsp_server/src/main_loop/handlers.rs | 98 ++++++- crates/ra_lsp_server/src/req.rs | 16 +- 10 files changed, 501 insertions(+), 15 deletions(-) create mode 100644 crates/ra_ide/src/call_hierarchy.rs (limited to 'crates') diff --git a/crates/ra_ide/Cargo.toml b/crates/ra_ide/Cargo.toml index e3439ae31..2c9f9dce0 100644 --- a/crates/ra_ide/Cargo.toml +++ b/crates/ra_ide/Cargo.toml @@ -13,6 +13,7 @@ wasm = [] [dependencies] either = "1.5" format-buf = "1.0.0" +indexmap = "1.3.0" itertools = "0.8.0" join_to_string = "0.1.3" log = "0.4.5" diff --git a/crates/ra_ide/src/call_hierarchy.rs b/crates/ra_ide/src/call_hierarchy.rs new file mode 100644 index 000000000..75658c20b --- /dev/null +++ b/crates/ra_ide/src/call_hierarchy.rs @@ -0,0 +1,337 @@ +//! Entry point for call-hierarchy + +use indexmap::IndexMap; + +use hir::db::AstDatabase; +use ra_syntax::{ + ast::{self, DocCommentsOwner}, + match_ast, AstNode, TextRange, +}; + +use crate::{ + call_info::FnCallNode, + db::RootDatabase, + display::{ShortLabel, ToNav}, + expand::descend_into_macros, + goto_definition, references, FilePosition, NavigationTarget, RangeInfo, +}; + +#[derive(Default)] +struct CallLocations { + funcs: IndexMap>, +} + +impl CallLocations { + pub fn add(&mut self, target: &NavigationTarget, range: TextRange) { + self.funcs.entry(target.clone()).or_default().push(range); + } + + pub fn into_items(self) -> Vec { + self.funcs.into_iter().map(|(target, ranges)| CallItem { target, ranges }).collect() + } +} + +#[derive(Debug, Clone)] +pub struct CallItem { + pub target: NavigationTarget, + pub ranges: Vec, +} + +impl CallItem { + #[cfg(test)] + pub(crate) fn assert_match(&self, expected: &str) { + let actual = self.debug_render(); + test_utils::assert_eq_text!(expected.trim(), actual.trim(),); + } + + #[cfg(test)] + pub(crate) fn debug_render(&self) -> String { + format!("{} : {:?}", self.target.debug_render(), self.ranges) + } +} + +pub(crate) fn call_hierarchy( + db: &RootDatabase, + position: FilePosition, +) -> Option>> { + goto_definition::goto_definition(db, position) +} + +pub(crate) fn incoming_calls(db: &RootDatabase, position: FilePosition) -> Option> { + // 1. Find all refs + // 2. Loop through refs and determine unique fndef. This will become our `from: CallHierarchyItem,` in the reply. + // 3. Add ranges relative to the start of the fndef. + let refs = references::find_all_refs(db, position, None)?; + + let mut calls = CallLocations::default(); + + for reference in refs.info.references() { + let file_id = reference.file_range.file_id; + let file = db.parse_or_expand(file_id.into())?; + let token = file.token_at_offset(reference.file_range.range.start()).next()?; + let token = descend_into_macros(db, file_id, token); + let syntax = token.value.parent(); + + // This target is the containing function + if let Some(nav) = syntax.ancestors().find_map(|node| { + match_ast! { + match node { + ast::FnDef(it) => { + Some(NavigationTarget::from_named( + db, + token.with_value(&it), + it.doc_comment_text(), + it.short_label(), + )) + }, + _ => { None }, + } + } + }) { + let relative_range = reference.file_range.range; + calls.add(&nav, relative_range); + } + } + + Some(calls.into_items()) +} + +pub(crate) fn outgoing_calls(db: &RootDatabase, position: FilePosition) -> Option> { + let file_id = position.file_id; + let file = db.parse_or_expand(file_id.into())?; + let token = file.token_at_offset(position.offset).next()?; + let token = descend_into_macros(db, file_id, token); + let syntax = token.value.parent(); + + let mut calls = CallLocations::default(); + + syntax + .descendants() + .filter_map(|node| FnCallNode::with_node_exact(&node)) + .filter_map(|call_node| { + let name_ref = call_node.name_ref()?; + let name_ref = token.with_value(name_ref.syntax()); + + let analyzer = hir::SourceAnalyzer::new(db, name_ref, None); + + if let Some(func_target) = match &call_node { + FnCallNode::CallExpr(expr) => { + //FIXME: Type::as_callable is broken + let callable_def = analyzer.type_of(db, &expr.expr()?)?.as_callable()?; + match callable_def { + hir::CallableDef::FunctionId(it) => { + let fn_def: hir::Function = it.into(); + let nav = fn_def.to_nav(db); + Some(nav) + } + _ => None, + } + } + FnCallNode::MethodCallExpr(expr) => { + let function = analyzer.resolve_method_call(&expr)?; + Some(function.to_nav(db)) + } + FnCallNode::MacroCallExpr(expr) => { + let macro_def = analyzer.resolve_macro_call(db, name_ref.with_value(&expr))?; + Some(macro_def.to_nav(db)) + } + } { + Some((func_target.clone(), name_ref.value.text_range())) + } else { + None + } + }) + .for_each(|(nav, range)| calls.add(&nav, range)); + + Some(calls.into_items()) +} + +#[cfg(test)] +mod tests { + use ra_db::FilePosition; + + use crate::mock_analysis::analysis_and_position; + + fn check_hierarchy( + fixture: &str, + expected: &str, + expected_incoming: &[&str], + expected_outgoing: &[&str], + ) { + let (analysis, pos) = analysis_and_position(fixture); + + let mut navs = analysis.call_hierarchy(pos).unwrap().unwrap().info; + assert_eq!(navs.len(), 1); + let nav = navs.pop().unwrap(); + nav.assert_match(expected); + + let item_pos = FilePosition { file_id: nav.file_id(), offset: nav.range().start() }; + let incoming_calls = analysis.incoming_calls(item_pos).unwrap().unwrap(); + assert_eq!(incoming_calls.len(), expected_incoming.len()); + + for call in 0..incoming_calls.len() { + incoming_calls[call].assert_match(expected_incoming[call]); + } + + let outgoing_calls = analysis.outgoing_calls(item_pos).unwrap().unwrap(); + assert_eq!(outgoing_calls.len(), expected_outgoing.len()); + + for call in 0..outgoing_calls.len() { + outgoing_calls[call].assert_match(expected_outgoing[call]); + } + } + + #[test] + fn test_call_hierarchy_on_ref() { + check_hierarchy( + r#" + //- /lib.rs + fn callee() {} + fn caller() { + call<|>ee(); + } + "#, + "callee FN_DEF FileId(1) [0; 14) [3; 9)", + &["caller FN_DEF FileId(1) [15; 44) [18; 24) : [[33; 39)]"], + &[], + ); + } + + #[test] + fn test_call_hierarchy_on_def() { + check_hierarchy( + r#" + //- /lib.rs + fn call<|>ee() {} + fn caller() { + callee(); + } + "#, + "callee FN_DEF FileId(1) [0; 14) [3; 9)", + &["caller FN_DEF FileId(1) [15; 44) [18; 24) : [[33; 39)]"], + &[], + ); + } + + #[test] + fn test_call_hierarchy_in_same_fn() { + check_hierarchy( + r#" + //- /lib.rs + fn callee() {} + fn caller() { + call<|>ee(); + callee(); + } + "#, + "callee FN_DEF FileId(1) [0; 14) [3; 9)", + &["caller FN_DEF FileId(1) [15; 58) [18; 24) : [[33; 39), [47; 53)]"], + &[], + ); + } + + #[test] + fn test_call_hierarchy_in_different_fn() { + check_hierarchy( + r#" + //- /lib.rs + fn callee() {} + fn caller1() { + call<|>ee(); + } + + fn caller2() { + callee(); + } + "#, + "callee FN_DEF FileId(1) [0; 14) [3; 9)", + &[ + "caller1 FN_DEF FileId(1) [15; 45) [18; 25) : [[34; 40)]", + "caller2 FN_DEF FileId(1) [46; 76) [49; 56) : [[65; 71)]", + ], + &[], + ); + } + + #[test] + fn test_call_hierarchy_in_different_files() { + check_hierarchy( + r#" + //- /lib.rs + mod foo; + use foo::callee; + + fn caller() { + call<|>ee(); + } + + //- /foo/mod.rs + pub fn callee() {} + "#, + "callee FN_DEF FileId(2) [0; 18) [7; 13)", + &["caller FN_DEF FileId(1) [26; 55) [29; 35) : [[44; 50)]"], + &[], + ); + } + + #[test] + fn test_call_hierarchy_outgoing() { + check_hierarchy( + r#" + //- /lib.rs + fn callee() {} + fn call<|>er() { + callee(); + callee(); + } + "#, + "caller FN_DEF FileId(1) [15; 58) [18; 24)", + &[], + &["callee FN_DEF FileId(1) [0; 14) [3; 9) : [[33; 39), [47; 53)]"], + ); + } + + #[test] + fn test_call_hierarchy_outgoing_in_different_files() { + check_hierarchy( + r#" + //- /lib.rs + mod foo; + use foo::callee; + + fn call<|>er() { + callee(); + } + + //- /foo/mod.rs + pub fn callee() {} + "#, + "caller FN_DEF FileId(1) [26; 55) [29; 35)", + &[], + &["callee FN_DEF FileId(2) [0; 18) [7; 13) : [[44; 50)]"], + ); + } + + #[test] + fn test_call_hierarchy_incoming_outgoing() { + check_hierarchy( + r#" + //- /lib.rs + fn caller1() { + call<|>er2(); + } + + fn caller2() { + caller3(); + } + + fn caller3() { + + } + "#, + "caller2 FN_DEF FileId(1) [32; 63) [35; 42)", + &["caller1 FN_DEF FileId(1) [0; 31) [3; 10) : [[19; 26)]"], + &["caller3 FN_DEF FileId(1) [64; 80) [67; 74) : [[51; 58)]"], + ); + } +} diff --git a/crates/ra_ide/src/call_info.rs b/crates/ra_ide/src/call_info.rs index 2c2b6fa48..a7023529b 100644 --- a/crates/ra_ide/src/call_info.rs +++ b/crates/ra_ide/src/call_info.rs @@ -88,7 +88,7 @@ pub(crate) fn call_info(db: &RootDatabase, position: FilePosition) -> Option Option { + pub(crate) fn with_node_exact(node: &SyntaxNode) -> Option { + match_ast! { + match node { + ast::CallExpr(it) => { Some(FnCallNode::CallExpr(it)) }, + ast::MethodCallExpr(it) => { Some(FnCallNode::MethodCallExpr(it)) }, + ast::MacroCall(it) => { Some(FnCallNode::MacroCallExpr(it)) }, + _ => { None }, + } + } + } + + pub(crate) fn name_ref(&self) -> Option { match self { FnCallNode::CallExpr(call_expr) => Some(match call_expr.expr()? { ast::Expr::PathExpr(path_expr) => path_expr.path()?.segment()?.name_ref()?, diff --git a/crates/ra_ide/src/display/navigation_target.rs b/crates/ra_ide/src/display/navigation_target.rs index b9ae67828..f2e45fa31 100644 --- a/crates/ra_ide/src/display/navigation_target.rs +++ b/crates/ra_ide/src/display/navigation_target.rs @@ -19,7 +19,7 @@ use super::short_label::ShortLabel; /// /// Typically, a `NavigationTarget` corresponds to some element in the source /// code, like a function or a struct, but this is not strictly required. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct NavigationTarget { file_id: FileId, name: SmolStr, diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs index 779a81b2c..06497617b 100644 --- a/crates/ra_ide/src/lib.rs +++ b/crates/ra_ide/src/lib.rs @@ -24,6 +24,7 @@ mod goto_definition; mod goto_type_definition; mod extend_selection; mod hover; +mod call_hierarchy; mod call_info; mod syntax_highlighting; mod parent_module; @@ -62,6 +63,7 @@ use crate::{db::LineIndexDatabase, display::ToNav, symbol_index::FileSymbol}; pub use crate::{ assists::{Assist, AssistId}, + call_hierarchy::CallItem, change::{AnalysisChange, LibraryData}, completion::{CompletionItem, CompletionItemKind, InsertTextFormat}, diagnostics::Severity, @@ -412,6 +414,24 @@ impl Analysis { self.with_db(|db| call_info::call_info(db, position)) } + /// Computes call hierarchy candidates for the given file position. + pub fn call_hierarchy( + &self, + position: FilePosition, + ) -> Cancelable>>> { + self.with_db(|db| call_hierarchy::call_hierarchy(db, position)) + } + + /// Computes incoming calls for the given file position. + pub fn incoming_calls(&self, position: FilePosition) -> Cancelable>> { + self.with_db(|db| call_hierarchy::incoming_calls(db, position)) + } + + /// Computes incoming calls for the given file position. + pub fn outgoing_calls(&self, position: FilePosition) -> Cancelable>> { + self.with_db(|db| call_hierarchy::outgoing_calls(db, position)) + } + /// Returns a `mod name;` declaration which created the current module. pub fn parent_module(&self, position: FilePosition) -> Cancelable> { self.with_db(|db| parent_module::parent_module(db, position)) diff --git a/crates/ra_lsp_server/src/caps.rs b/crates/ra_lsp_server/src/caps.rs index db502c200..c4711076c 100644 --- a/crates/ra_lsp_server/src/caps.rs +++ b/crates/ra_lsp_server/src/caps.rs @@ -1,8 +1,8 @@ //! Advertizes the capabilities of the LSP Server. use lsp_types::{ - CodeActionProviderCapability, CodeLensOptions, CompletionOptions, - DocumentOnTypeFormattingOptions, FoldingRangeProviderCapability, + CallHierarchyServerCapability, CodeActionProviderCapability, CodeLensOptions, + CompletionOptions, DocumentOnTypeFormattingOptions, FoldingRangeProviderCapability, ImplementationProviderCapability, RenameOptions, RenameProviderCapability, SaveOptions, SelectionRangeProviderCapability, ServerCapabilities, SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, @@ -56,7 +56,7 @@ pub fn server_capabilities() -> ServerCapabilities { color_provider: None, execute_command_provider: None, workspace: None, - call_hierarchy_provider: None, + call_hierarchy_provider: Some(CallHierarchyServerCapability::Simple(true)), experimental: Default::default(), } } diff --git a/crates/ra_lsp_server/src/conv.rs b/crates/ra_lsp_server/src/conv.rs index e93d4ea33..c260b51c4 100644 --- a/crates/ra_lsp_server/src/conv.rs +++ b/crates/ra_lsp_server/src/conv.rs @@ -490,6 +490,24 @@ impl TryConvWith<&WorldSnapshot> for (FileId, RangeInfo>) } } +pub fn to_call_hierarchy_item( + file_id: FileId, + range: TextRange, + world: &WorldSnapshot, + line_index: &LineIndex, + nav: NavigationTarget, +) -> Result { + Ok(lsp_types::CallHierarchyItem { + name: nav.name().to_string(), + kind: nav.kind().conv(), + tags: None, + detail: nav.description().map(|it| it.to_string()), + uri: file_id.try_conv_with(&world)?, + range: nav.range().conv_with(&line_index), + selection_range: range.conv_with(&line_index), + }) +} + pub fn to_location( file_id: FileId, range: TextRange, diff --git a/crates/ra_lsp_server/src/main_loop.rs b/crates/ra_lsp_server/src/main_loop.rs index 4336583fe..047c86a3b 100644 --- a/crates/ra_lsp_server/src/main_loop.rs +++ b/crates/ra_lsp_server/src/main_loop.rs @@ -499,6 +499,9 @@ fn on_request( .on::(handlers::handle_formatting)? .on::(handlers::handle_document_highlight)? .on::(handlers::handle_inlay_hints)? + .on::(handlers::handle_call_hierarchy_prepare)? + .on::(handlers::handle_call_hierarchy_incoming)? + .on::(handlers::handle_call_hierarchy_outgoing)? .finish(); Ok(()) } diff --git a/crates/ra_lsp_server/src/main_loop/handlers.rs b/crates/ra_lsp_server/src/main_loop/handlers.rs index c8f52eb0e..a5b6f48af 100644 --- a/crates/ra_lsp_server/src/main_loop/handlers.rs +++ b/crates/ra_lsp_server/src/main_loop/handlers.rs @@ -5,13 +5,16 @@ use std::{fmt::Write as _, io::Write as _}; use lsp_server::ErrorCode; use lsp_types::{ + CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem, + CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams, CodeAction, CodeActionResponse, CodeLens, Command, CompletionItem, Diagnostic, DocumentFormattingParams, DocumentHighlight, DocumentSymbol, FoldingRange, FoldingRangeParams, Hover, HoverContents, Location, MarkupContent, MarkupKind, Position, PrepareRenameResponse, Range, RenameParams, SymbolInformation, TextDocumentIdentifier, TextEdit, WorkspaceEdit, }; use ra_ide::{ - AssistId, FileId, FilePosition, FileRange, Query, Runnable, RunnableKind, SearchScope, + AssistId, FileId, FilePosition, FileRange, Query, RangeInfo, Runnable, RunnableKind, + SearchScope, }; use ra_prof::profile; use ra_syntax::{AstNode, SyntaxKind, TextRange, TextUnit}; @@ -21,7 +24,10 @@ use serde_json::to_value; use crate::{ cargo_target_spec::{runnable_args, CargoTargetSpec}, - conv::{to_location, Conv, ConvWith, FoldConvCtx, MapConvWith, TryConvWith, TryConvWithToVec}, + conv::{ + to_call_hierarchy_item, to_location, Conv, ConvWith, FoldConvCtx, MapConvWith, TryConvWith, + TryConvWithToVec, + }, req::{self, Decoration, InlayHint, InlayHintsParams, InlayKind}, world::WorldSnapshot, LspError, Result, @@ -936,3 +942,91 @@ pub fn handle_inlay_hints( }) .collect()) } + +pub fn handle_call_hierarchy_prepare( + world: WorldSnapshot, + params: CallHierarchyPrepareParams, +) -> Result>> { + let _p = profile("handle_call_hierarchy_prepare"); + let position = params.text_document_position_params.try_conv_with(&world)?; + let file_id = position.file_id; + + let nav_info = match world.analysis().call_hierarchy(position)? { + None => return Ok(None), + Some(it) => it, + }; + + let line_index = world.analysis().file_line_index(file_id)?; + let RangeInfo { range, info: navs } = nav_info; + let res = navs + .into_iter() + .filter(|it| it.kind() == SyntaxKind::FN_DEF) + .filter_map(|it| to_call_hierarchy_item(file_id, range, &world, &line_index, it).ok()) + .collect(); + + Ok(Some(res)) +} + +pub fn handle_call_hierarchy_incoming( + world: WorldSnapshot, + params: CallHierarchyIncomingCallsParams, +) -> Result>> { + let _p = profile("handle_call_hierarchy_incoming"); + let item = params.item; + + let doc = TextDocumentIdentifier::new(item.uri); + let frange: FileRange = (&doc, item.range).try_conv_with(&world)?; + let fpos = FilePosition { file_id: frange.file_id, offset: frange.range.start() }; + + let call_items = match world.analysis().incoming_calls(fpos)? { + None => return Ok(None), + Some(it) => it, + }; + + let mut res = vec![]; + + for call_item in call_items.into_iter() { + let file_id = call_item.target.file_id(); + let line_index = world.analysis().file_line_index(file_id)?; + let range = call_item.target.range(); + let item = to_call_hierarchy_item(file_id, range, &world, &line_index, call_item.target)?; + res.push(CallHierarchyIncomingCall { + from: item, + from_ranges: call_item.ranges.iter().map(|it| it.conv_with(&line_index)).collect(), + }); + } + + Ok(Some(res)) +} + +pub fn handle_call_hierarchy_outgoing( + world: WorldSnapshot, + params: CallHierarchyOutgoingCallsParams, +) -> Result>> { + let _p = profile("handle_call_hierarchy_outgoing"); + let item = params.item; + + let doc = TextDocumentIdentifier::new(item.uri); + let frange: FileRange = (&doc, item.range).try_conv_with(&world)?; + let fpos = FilePosition { file_id: frange.file_id, offset: frange.range.start() }; + + let call_items = match world.analysis().outgoing_calls(fpos)? { + None => return Ok(None), + Some(it) => it, + }; + + let mut res = vec![]; + + for call_item in call_items.into_iter() { + let file_id = call_item.target.file_id(); + let line_index = world.analysis().file_line_index(file_id)?; + let range = call_item.target.range(); + let item = to_call_hierarchy_item(file_id, range, &world, &line_index, call_item.target)?; + res.push(CallHierarchyOutgoingCall { + to: item, + from_ranges: call_item.ranges.iter().map(|it| it.conv_with(&line_index)).collect(), + }); + } + + Ok(Some(res)) +} diff --git a/crates/ra_lsp_server/src/req.rs b/crates/ra_lsp_server/src/req.rs index 40edaf677..8098ff31d 100644 --- a/crates/ra_lsp_server/src/req.rs +++ b/crates/ra_lsp_server/src/req.rs @@ -6,13 +6,15 @@ use serde::{Deserialize, Serialize}; pub use lsp_types::{ notification::*, request::*, ApplyWorkspaceEditParams, CodeActionParams, CodeLens, - CodeLensParams, CompletionParams, CompletionResponse, DidChangeConfigurationParams, - DidChangeWatchedFilesParams, DidChangeWatchedFilesRegistrationOptions, - DocumentOnTypeFormattingParams, DocumentSymbolParams, DocumentSymbolResponse, - FileSystemWatcher, Hover, InitializeResult, MessageType, ProgressParams, ProgressParamsValue, - ProgressToken, PublishDiagnosticsParams, ReferenceParams, Registration, RegistrationParams, - SelectionRange, SelectionRangeParams, ShowMessageParams, SignatureHelp, TextDocumentEdit, - TextDocumentPositionParams, TextEdit, WorkspaceEdit, WorkspaceSymbolParams, + CodeLensParams, CompletionParams, CompletionResponse, DiagnosticTag, + DidChangeConfigurationParams, DidChangeWatchedFilesParams, + DidChangeWatchedFilesRegistrationOptions, DocumentOnTypeFormattingParams, DocumentSymbolParams, + DocumentSymbolResponse, FileSystemWatcher, Hover, InitializeResult, MessageType, + PartialResultParams, ProgressParams, ProgressParamsValue, ProgressToken, + PublishDiagnosticsParams, ReferenceParams, Registration, RegistrationParams, SelectionRange, + SelectionRangeParams, ServerCapabilities, ShowMessageParams, SignatureHelp, SymbolKind, + TextDocumentEdit, TextDocumentPositionParams, TextEdit, WorkDoneProgressParams, WorkspaceEdit, + WorkspaceSymbolParams, }; pub enum AnalyzerStatus {} -- cgit v1.2.3