diff options
author | Jeremy Kolb <[email protected]> | 2019-12-30 14:12:06 +0000 |
---|---|---|
committer | kjeremy <[email protected]> | 2020-01-08 15:15:49 +0000 |
commit | 1b19a8aa5ecfc9d7115f291b97d413bd845c89b5 (patch) | |
tree | cf209285ee7020bb0ab75b9a71eab4c006b86532 /crates/ra_ide/src | |
parent | 928ecd069a508845ef4dbfd1bc1b9bf975d76e5b (diff) |
Implement proposed CallHierarchy feature
See: https://github.com/microsoft/vscode-languageserver-node/blob/master/protocol/src/protocol.callHierarchy.proposed.ts
Diffstat (limited to 'crates/ra_ide/src')
-rw-r--r-- | crates/ra_ide/src/call_hierarchy.rs | 337 | ||||
-rw-r--r-- | crates/ra_ide/src/call_info.rs | 15 | ||||
-rw-r--r-- | crates/ra_ide/src/display/navigation_target.rs | 2 | ||||
-rw-r--r-- | crates/ra_ide/src/lib.rs | 20 |
4 files changed, 371 insertions, 3 deletions
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 @@ | |||
1 | //! Entry point for call-hierarchy | ||
2 | |||
3 | use indexmap::IndexMap; | ||
4 | |||
5 | use hir::db::AstDatabase; | ||
6 | use ra_syntax::{ | ||
7 | ast::{self, DocCommentsOwner}, | ||
8 | match_ast, AstNode, TextRange, | ||
9 | }; | ||
10 | |||
11 | use crate::{ | ||
12 | call_info::FnCallNode, | ||
13 | db::RootDatabase, | ||
14 | display::{ShortLabel, ToNav}, | ||
15 | expand::descend_into_macros, | ||
16 | goto_definition, references, FilePosition, NavigationTarget, RangeInfo, | ||
17 | }; | ||
18 | |||
19 | #[derive(Default)] | ||
20 | struct CallLocations { | ||
21 | funcs: IndexMap<NavigationTarget, Vec<TextRange>>, | ||
22 | } | ||
23 | |||
24 | impl CallLocations { | ||
25 | pub fn add(&mut self, target: &NavigationTarget, range: TextRange) { | ||
26 | self.funcs.entry(target.clone()).or_default().push(range); | ||
27 | } | ||
28 | |||
29 | pub fn into_items(self) -> Vec<CallItem> { | ||
30 | self.funcs.into_iter().map(|(target, ranges)| CallItem { target, ranges }).collect() | ||
31 | } | ||
32 | } | ||
33 | |||
34 | #[derive(Debug, Clone)] | ||
35 | pub struct CallItem { | ||
36 | pub target: NavigationTarget, | ||
37 | pub ranges: Vec<TextRange>, | ||
38 | } | ||
39 | |||
40 | impl CallItem { | ||
41 | #[cfg(test)] | ||
42 | pub(crate) fn assert_match(&self, expected: &str) { | ||
43 | let actual = self.debug_render(); | ||
44 | test_utils::assert_eq_text!(expected.trim(), actual.trim(),); | ||
45 | } | ||
46 | |||
47 | #[cfg(test)] | ||
48 | pub(crate) fn debug_render(&self) -> String { | ||
49 | format!("{} : {:?}", self.target.debug_render(), self.ranges) | ||
50 | } | ||
51 | } | ||
52 | |||
53 | pub(crate) fn call_hierarchy( | ||
54 | db: &RootDatabase, | ||
55 | position: FilePosition, | ||
56 | ) -> Option<RangeInfo<Vec<NavigationTarget>>> { | ||
57 | goto_definition::goto_definition(db, position) | ||
58 | } | ||
59 | |||
60 | pub(crate) fn incoming_calls(db: &RootDatabase, position: FilePosition) -> Option<Vec<CallItem>> { | ||
61 | // 1. Find all refs | ||
62 | // 2. Loop through refs and determine unique fndef. This will become our `from: CallHierarchyItem,` in the reply. | ||
63 | // 3. Add ranges relative to the start of the fndef. | ||
64 | let refs = references::find_all_refs(db, position, None)?; | ||
65 | |||
66 | let mut calls = CallLocations::default(); | ||
67 | |||
68 | for reference in refs.info.references() { | ||
69 | let file_id = reference.file_range.file_id; | ||
70 | let file = db.parse_or_expand(file_id.into())?; | ||
71 | let token = file.token_at_offset(reference.file_range.range.start()).next()?; | ||
72 | let token = descend_into_macros(db, file_id, token); | ||
73 | let syntax = token.value.parent(); | ||
74 | |||
75 | // This target is the containing function | ||
76 | if let Some(nav) = syntax.ancestors().find_map(|node| { | ||
77 | match_ast! { | ||
78 | match node { | ||
79 | ast::FnDef(it) => { | ||
80 | Some(NavigationTarget::from_named( | ||
81 | db, | ||
82 | token.with_value(&it), | ||
83 | it.doc_comment_text(), | ||
84 | it.short_label(), | ||
85 | )) | ||
86 | }, | ||
87 | _ => { None }, | ||
88 | } | ||
89 | } | ||
90 | }) { | ||
91 | let relative_range = reference.file_range.range; | ||
92 | calls.add(&nav, relative_range); | ||
93 | } | ||
94 | } | ||
95 | |||
96 | Some(calls.into_items()) | ||
97 | } | ||
98 | |||
99 | pub(crate) fn outgoing_calls(db: &RootDatabase, position: FilePosition) -> Option<Vec<CallItem>> { | ||
100 | let file_id = position.file_id; | ||
101 | let file = db.parse_or_expand(file_id.into())?; | ||
102 | let token = file.token_at_offset(position.offset).next()?; | ||
103 | let token = descend_into_macros(db, file_id, token); | ||
104 | let syntax = token.value.parent(); | ||
105 | |||
106 | let mut calls = CallLocations::default(); | ||
107 | |||
108 | syntax | ||
109 | .descendants() | ||
110 | .filter_map(|node| FnCallNode::with_node_exact(&node)) | ||
111 | .filter_map(|call_node| { | ||
112 | let name_ref = call_node.name_ref()?; | ||
113 | let name_ref = token.with_value(name_ref.syntax()); | ||
114 | |||
115 | let analyzer = hir::SourceAnalyzer::new(db, name_ref, None); | ||
116 | |||
117 | if let Some(func_target) = match &call_node { | ||
118 | FnCallNode::CallExpr(expr) => { | ||
119 | //FIXME: Type::as_callable is broken | ||
120 | let callable_def = analyzer.type_of(db, &expr.expr()?)?.as_callable()?; | ||
121 | match callable_def { | ||
122 | hir::CallableDef::FunctionId(it) => { | ||
123 | let fn_def: hir::Function = it.into(); | ||
124 | let nav = fn_def.to_nav(db); | ||
125 | Some(nav) | ||
126 | } | ||
127 | _ => None, | ||
128 | } | ||
129 | } | ||
130 | FnCallNode::MethodCallExpr(expr) => { | ||
131 | let function = analyzer.resolve_method_call(&expr)?; | ||
132 | Some(function.to_nav(db)) | ||
133 | } | ||
134 | FnCallNode::MacroCallExpr(expr) => { | ||
135 | let macro_def = analyzer.resolve_macro_call(db, name_ref.with_value(&expr))?; | ||
136 | Some(macro_def.to_nav(db)) | ||
137 | } | ||
138 | } { | ||
139 | Some((func_target.clone(), name_ref.value.text_range())) | ||
140 | } else { | ||
141 | None | ||
142 | } | ||
143 | }) | ||
144 | .for_each(|(nav, range)| calls.add(&nav, range)); | ||
145 | |||
146 | Some(calls.into_items()) | ||
147 | } | ||
148 | |||
149 | #[cfg(test)] | ||
150 | mod tests { | ||
151 | use ra_db::FilePosition; | ||
152 | |||
153 | use crate::mock_analysis::analysis_and_position; | ||
154 | |||
155 | fn check_hierarchy( | ||
156 | fixture: &str, | ||
157 | expected: &str, | ||
158 | expected_incoming: &[&str], | ||
159 | expected_outgoing: &[&str], | ||
160 | ) { | ||
161 | let (analysis, pos) = analysis_and_position(fixture); | ||
162 | |||
163 | let mut navs = analysis.call_hierarchy(pos).unwrap().unwrap().info; | ||
164 | assert_eq!(navs.len(), 1); | ||
165 | let nav = navs.pop().unwrap(); | ||
166 | nav.assert_match(expected); | ||
167 | |||
168 | let item_pos = FilePosition { file_id: nav.file_id(), offset: nav.range().start() }; | ||
169 | let incoming_calls = analysis.incoming_calls(item_pos).unwrap().unwrap(); | ||
170 | assert_eq!(incoming_calls.len(), expected_incoming.len()); | ||
171 | |||
172 | for call in 0..incoming_calls.len() { | ||
173 | incoming_calls[call].assert_match(expected_incoming[call]); | ||
174 | } | ||
175 | |||
176 | let outgoing_calls = analysis.outgoing_calls(item_pos).unwrap().unwrap(); | ||
177 | assert_eq!(outgoing_calls.len(), expected_outgoing.len()); | ||
178 | |||
179 | for call in 0..outgoing_calls.len() { | ||
180 | outgoing_calls[call].assert_match(expected_outgoing[call]); | ||
181 | } | ||
182 | } | ||
183 | |||
184 | #[test] | ||
185 | fn test_call_hierarchy_on_ref() { | ||
186 | check_hierarchy( | ||
187 | r#" | ||
188 | //- /lib.rs | ||
189 | fn callee() {} | ||
190 | fn caller() { | ||
191 | call<|>ee(); | ||
192 | } | ||
193 | "#, | ||
194 | "callee FN_DEF FileId(1) [0; 14) [3; 9)", | ||
195 | &["caller FN_DEF FileId(1) [15; 44) [18; 24) : [[33; 39)]"], | ||
196 | &[], | ||
197 | ); | ||
198 | } | ||
199 | |||
200 | #[test] | ||
201 | fn test_call_hierarchy_on_def() { | ||
202 | check_hierarchy( | ||
203 | r#" | ||
204 | //- /lib.rs | ||
205 | fn call<|>ee() {} | ||
206 | fn caller() { | ||
207 | callee(); | ||
208 | } | ||
209 | "#, | ||
210 | "callee FN_DEF FileId(1) [0; 14) [3; 9)", | ||
211 | &["caller FN_DEF FileId(1) [15; 44) [18; 24) : [[33; 39)]"], | ||
212 | &[], | ||
213 | ); | ||
214 | } | ||
215 | |||
216 | #[test] | ||
217 | fn test_call_hierarchy_in_same_fn() { | ||
218 | check_hierarchy( | ||
219 | r#" | ||
220 | //- /lib.rs | ||
221 | fn callee() {} | ||
222 | fn caller() { | ||
223 | call<|>ee(); | ||
224 | callee(); | ||
225 | } | ||
226 | "#, | ||
227 | "callee FN_DEF FileId(1) [0; 14) [3; 9)", | ||
228 | &["caller FN_DEF FileId(1) [15; 58) [18; 24) : [[33; 39), [47; 53)]"], | ||
229 | &[], | ||
230 | ); | ||
231 | } | ||
232 | |||
233 | #[test] | ||
234 | fn test_call_hierarchy_in_different_fn() { | ||
235 | check_hierarchy( | ||
236 | r#" | ||
237 | //- /lib.rs | ||
238 | fn callee() {} | ||
239 | fn caller1() { | ||
240 | call<|>ee(); | ||
241 | } | ||
242 | |||
243 | fn caller2() { | ||
244 | callee(); | ||
245 | } | ||
246 | "#, | ||
247 | "callee FN_DEF FileId(1) [0; 14) [3; 9)", | ||
248 | &[ | ||
249 | "caller1 FN_DEF FileId(1) [15; 45) [18; 25) : [[34; 40)]", | ||
250 | "caller2 FN_DEF FileId(1) [46; 76) [49; 56) : [[65; 71)]", | ||
251 | ], | ||
252 | &[], | ||
253 | ); | ||
254 | } | ||
255 | |||
256 | #[test] | ||
257 | fn test_call_hierarchy_in_different_files() { | ||
258 | check_hierarchy( | ||
259 | r#" | ||
260 | //- /lib.rs | ||
261 | mod foo; | ||
262 | use foo::callee; | ||
263 | |||
264 | fn caller() { | ||
265 | call<|>ee(); | ||
266 | } | ||
267 | |||
268 | //- /foo/mod.rs | ||
269 | pub fn callee() {} | ||
270 | "#, | ||
271 | "callee FN_DEF FileId(2) [0; 18) [7; 13)", | ||
272 | &["caller FN_DEF FileId(1) [26; 55) [29; 35) : [[44; 50)]"], | ||
273 | &[], | ||
274 | ); | ||
275 | } | ||
276 | |||
277 | #[test] | ||
278 | fn test_call_hierarchy_outgoing() { | ||
279 | check_hierarchy( | ||
280 | r#" | ||
281 | //- /lib.rs | ||
282 | fn callee() {} | ||
283 | fn call<|>er() { | ||
284 | callee(); | ||
285 | callee(); | ||
286 | } | ||
287 | "#, | ||
288 | "caller FN_DEF FileId(1) [15; 58) [18; 24)", | ||
289 | &[], | ||
290 | &["callee FN_DEF FileId(1) [0; 14) [3; 9) : [[33; 39), [47; 53)]"], | ||
291 | ); | ||
292 | } | ||
293 | |||
294 | #[test] | ||
295 | fn test_call_hierarchy_outgoing_in_different_files() { | ||
296 | check_hierarchy( | ||
297 | r#" | ||
298 | //- /lib.rs | ||
299 | mod foo; | ||
300 | use foo::callee; | ||
301 | |||
302 | fn call<|>er() { | ||
303 | callee(); | ||
304 | } | ||
305 | |||
306 | //- /foo/mod.rs | ||
307 | pub fn callee() {} | ||
308 | "#, | ||
309 | "caller FN_DEF FileId(1) [26; 55) [29; 35)", | ||
310 | &[], | ||
311 | &["callee FN_DEF FileId(2) [0; 18) [7; 13) : [[44; 50)]"], | ||
312 | ); | ||
313 | } | ||
314 | |||
315 | #[test] | ||
316 | fn test_call_hierarchy_incoming_outgoing() { | ||
317 | check_hierarchy( | ||
318 | r#" | ||
319 | //- /lib.rs | ||
320 | fn caller1() { | ||
321 | call<|>er2(); | ||
322 | } | ||
323 | |||
324 | fn caller2() { | ||
325 | caller3(); | ||
326 | } | ||
327 | |||
328 | fn caller3() { | ||
329 | |||
330 | } | ||
331 | "#, | ||
332 | "caller2 FN_DEF FileId(1) [32; 63) [35; 42)", | ||
333 | &["caller1 FN_DEF FileId(1) [0; 31) [3; 10) : [[19; 26)]"], | ||
334 | &["caller3 FN_DEF FileId(1) [64; 80) [67; 74) : [[51; 58)]"], | ||
335 | ); | ||
336 | } | ||
337 | } | ||
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<Cal | |||
88 | } | 88 | } |
89 | 89 | ||
90 | #[derive(Debug)] | 90 | #[derive(Debug)] |
91 | enum FnCallNode { | 91 | pub(crate) enum FnCallNode { |
92 | CallExpr(ast::CallExpr), | 92 | CallExpr(ast::CallExpr), |
93 | MethodCallExpr(ast::MethodCallExpr), | 93 | MethodCallExpr(ast::MethodCallExpr), |
94 | MacroCallExpr(ast::MacroCall), | 94 | MacroCallExpr(ast::MacroCall), |
@@ -108,7 +108,18 @@ impl FnCallNode { | |||
108 | }) | 108 | }) |
109 | } | 109 | } |
110 | 110 | ||
111 | fn name_ref(&self) -> Option<ast::NameRef> { | 111 | pub(crate) fn with_node_exact(node: &SyntaxNode) -> Option<FnCallNode> { |
112 | match_ast! { | ||
113 | match node { | ||
114 | ast::CallExpr(it) => { Some(FnCallNode::CallExpr(it)) }, | ||
115 | ast::MethodCallExpr(it) => { Some(FnCallNode::MethodCallExpr(it)) }, | ||
116 | ast::MacroCall(it) => { Some(FnCallNode::MacroCallExpr(it)) }, | ||
117 | _ => { None }, | ||
118 | } | ||
119 | } | ||
120 | } | ||
121 | |||
122 | pub(crate) fn name_ref(&self) -> Option<ast::NameRef> { | ||
112 | match self { | 123 | match self { |
113 | FnCallNode::CallExpr(call_expr) => Some(match call_expr.expr()? { | 124 | FnCallNode::CallExpr(call_expr) => Some(match call_expr.expr()? { |
114 | ast::Expr::PathExpr(path_expr) => path_expr.path()?.segment()?.name_ref()?, | 125 | 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; | |||
19 | /// | 19 | /// |
20 | /// Typically, a `NavigationTarget` corresponds to some element in the source | 20 | /// Typically, a `NavigationTarget` corresponds to some element in the source |
21 | /// code, like a function or a struct, but this is not strictly required. | 21 | /// code, like a function or a struct, but this is not strictly required. |
22 | #[derive(Debug, Clone)] | 22 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] |
23 | pub struct NavigationTarget { | 23 | pub struct NavigationTarget { |
24 | file_id: FileId, | 24 | file_id: FileId, |
25 | name: SmolStr, | 25 | 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; | |||
24 | mod goto_type_definition; | 24 | mod goto_type_definition; |
25 | mod extend_selection; | 25 | mod extend_selection; |
26 | mod hover; | 26 | mod hover; |
27 | mod call_hierarchy; | ||
27 | mod call_info; | 28 | mod call_info; |
28 | mod syntax_highlighting; | 29 | mod syntax_highlighting; |
29 | mod parent_module; | 30 | mod parent_module; |
@@ -62,6 +63,7 @@ use crate::{db::LineIndexDatabase, display::ToNav, symbol_index::FileSymbol}; | |||
62 | 63 | ||
63 | pub use crate::{ | 64 | pub use crate::{ |
64 | assists::{Assist, AssistId}, | 65 | assists::{Assist, AssistId}, |
66 | call_hierarchy::CallItem, | ||
65 | change::{AnalysisChange, LibraryData}, | 67 | change::{AnalysisChange, LibraryData}, |
66 | completion::{CompletionItem, CompletionItemKind, InsertTextFormat}, | 68 | completion::{CompletionItem, CompletionItemKind, InsertTextFormat}, |
67 | diagnostics::Severity, | 69 | diagnostics::Severity, |
@@ -412,6 +414,24 @@ impl Analysis { | |||
412 | self.with_db(|db| call_info::call_info(db, position)) | 414 | self.with_db(|db| call_info::call_info(db, position)) |
413 | } | 415 | } |
414 | 416 | ||
417 | /// Computes call hierarchy candidates for the given file position. | ||
418 | pub fn call_hierarchy( | ||
419 | &self, | ||
420 | position: FilePosition, | ||
421 | ) -> Cancelable<Option<RangeInfo<Vec<NavigationTarget>>>> { | ||
422 | self.with_db(|db| call_hierarchy::call_hierarchy(db, position)) | ||
423 | } | ||
424 | |||
425 | /// Computes incoming calls for the given file position. | ||
426 | pub fn incoming_calls(&self, position: FilePosition) -> Cancelable<Option<Vec<CallItem>>> { | ||
427 | self.with_db(|db| call_hierarchy::incoming_calls(db, position)) | ||
428 | } | ||
429 | |||
430 | /// Computes incoming calls for the given file position. | ||
431 | pub fn outgoing_calls(&self, position: FilePosition) -> Cancelable<Option<Vec<CallItem>>> { | ||
432 | self.with_db(|db| call_hierarchy::outgoing_calls(db, position)) | ||
433 | } | ||
434 | |||
415 | /// Returns a `mod name;` declaration which created the current module. | 435 | /// Returns a `mod name;` declaration which created the current module. |
416 | pub fn parent_module(&self, position: FilePosition) -> Cancelable<Vec<NavigationTarget>> { | 436 | pub fn parent_module(&self, position: FilePosition) -> Cancelable<Vec<NavigationTarget>> { |
417 | self.with_db(|db| parent_module::parent_module(db, position)) | 437 | self.with_db(|db| parent_module::parent_module(db, position)) |