Skip to content

Commit f65a6f0

Browse files
authored
feat(lsp): add type definition and call hierarchy handlers (#279)
2 parents dd9a3aa + 14ec233 commit f65a6f0

6 files changed

Lines changed: 508 additions & 6 deletions

File tree

src/global_state/dispatch/request.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ impl GlobalState {
6767
.on_no_retry::<HoverRequest>(handle_hover)
6868
.on_no_retry::<GotoDefinition>(handle_goto_definition)
6969
.on_no_retry::<GotoDeclaration>(handle_goto_declaration)
70+
.on_no_retry::<GotoTypeDefinition>(handle_goto_type_definition)
71+
.on_no_retry::<CallHierarchyPrepare>(handle_prepare_call_hierarchy)
72+
.on_no_retry::<CallHierarchyIncomingCalls>(handle_call_hierarchy_incoming)
73+
.on_no_retry::<CallHierarchyOutgoingCalls>(handle_call_hierarchy_outgoing)
7074
.on_no_retry::<DocumentHighlightRequest>(handle_document_highlight)
7175
.on_no_retry::<References>(handle_references)
7276
.on_no_retry::<PrepareRenameRequest>(handle_prepare_rename)

src/global_state/handlers/request.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ pub(crate) use hints_lens::{
2020
handle_code_lens, handle_code_lens_resolve, handle_inlay_hint, handle_signature_help,
2121
};
2222
pub(crate) use navigation::{
23-
handle_document_highlight, handle_goto_declaration, handle_goto_definition, handle_hover,
24-
handle_references,
23+
handle_call_hierarchy_incoming, handle_call_hierarchy_outgoing, handle_document_highlight,
24+
handle_goto_declaration, handle_goto_definition, handle_goto_type_definition, handle_hover,
25+
handle_prepare_call_hierarchy, handle_references,
2526
};
2627
pub(crate) use rename::{handle_prepare_rename, handle_rename};
2728
pub(crate) use semantic_tokens::{

src/global_state/handlers/request/navigation.rs

Lines changed: 284 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
use ide::{FileRange, references::References};
1+
use ide::{
2+
FilePosition, FileRange, SymbolKind, document_symbols::DocumentSymbol,
3+
navigation_target::NavTarget, references::References,
4+
};
25
use itertools::Itertools;
6+
use utils::text_edit::{TextRange, TextSize};
37

48
use crate::{
59
global_state::snapshot::GlobalStateSnapshot,
@@ -33,6 +37,98 @@ pub(crate) fn handle_goto_declaration(
3337
Ok(Some(res))
3438
}
3539

40+
pub(crate) fn handle_goto_type_definition(
41+
snap: GlobalStateSnapshot,
42+
params: lsp_types::request::GotoTypeDefinitionParams,
43+
) -> anyhow::Result<Option<lsp_types::request::GotoTypeDefinitionResponse>> {
44+
handle_goto_definition(snap, params)
45+
}
46+
47+
pub(crate) fn handle_prepare_call_hierarchy(
48+
snap: GlobalStateSnapshot,
49+
params: lsp_types::CallHierarchyPrepareParams,
50+
) -> anyhow::Result<Option<Vec<lsp_types::CallHierarchyItem>>> {
51+
let position = from_proto::file_position(&snap, params.text_document_position_params)?;
52+
let Some(nav_info) = snap.analysis.goto_definition(position)? else {
53+
return Ok(None);
54+
};
55+
56+
let items = nav_info
57+
.info
58+
.into_iter()
59+
.filter_map(|nav| call_hierarchy_item_for_nav(&snap, nav).transpose())
60+
.collect::<anyhow::Result<Vec<_>>>()?
61+
.into_iter()
62+
.unique_by(call_hierarchy_item_key)
63+
.collect_vec();
64+
65+
Ok((!items.is_empty()).then_some(items))
66+
}
67+
68+
pub(crate) fn handle_call_hierarchy_incoming(
69+
snap: GlobalStateSnapshot,
70+
params: lsp_types::CallHierarchyIncomingCallsParams,
71+
) -> anyhow::Result<Option<Vec<lsp_types::CallHierarchyIncomingCall>>> {
72+
let target = params.item;
73+
let target_file_id = from_proto::file_id(&snap, &target.uri)?;
74+
let target_line_info = snap.line_info(target_file_id)?;
75+
let target_selection_range = from_proto::text_range(&target_line_info, target.selection_range)?;
76+
77+
let mut groups = Vec::<(lsp_types::CallHierarchyItem, Vec<lsp_types::Range>)>::new();
78+
for reference in reference_ranges_for_call_item(&snap, &target)? {
79+
if reference.file_id == target_file_id && reference.range == target_selection_range {
80+
continue;
81+
}
82+
83+
let Some(caller) = enclosing_module_item(&snap, reference)? else {
84+
continue;
85+
};
86+
let line_info = snap.line_info(reference.file_id)?;
87+
push_call_range(&mut groups, caller, to_proto::range(&line_info, reference.range));
88+
}
89+
90+
let calls = groups
91+
.into_iter()
92+
.map(|(from, from_ranges)| lsp_types::CallHierarchyIncomingCall { from, from_ranges })
93+
.collect_vec();
94+
Ok((!calls.is_empty()).then_some(calls))
95+
}
96+
97+
pub(crate) fn handle_call_hierarchy_outgoing(
98+
snap: GlobalStateSnapshot,
99+
params: lsp_types::CallHierarchyOutgoingCallsParams,
100+
) -> anyhow::Result<Option<Vec<lsp_types::CallHierarchyOutgoingCall>>> {
101+
let caller = params.item;
102+
let caller_file_id = from_proto::file_id(&snap, &caller.uri)?;
103+
let caller_line_info = snap.line_info(caller_file_id)?;
104+
let caller_range = from_proto::text_range(&caller_line_info, caller.range)?;
105+
106+
let mut groups = Vec::<(lsp_types::CallHierarchyItem, Vec<lsp_types::Range>)>::new();
107+
for callee in workspace_module_items(&snap)? {
108+
if same_call_hierarchy_item(&caller, &callee) {
109+
continue;
110+
}
111+
112+
for reference in reference_ranges_for_call_item(&snap, &callee)? {
113+
if reference.file_id == caller_file_id
114+
&& range_contains_range(caller_range, reference.range)
115+
{
116+
push_call_range(
117+
&mut groups,
118+
callee.clone(),
119+
to_proto::range(&caller_line_info, reference.range),
120+
);
121+
}
122+
}
123+
}
124+
125+
let calls = groups
126+
.into_iter()
127+
.map(|(to, from_ranges)| lsp_types::CallHierarchyOutgoingCall { to, from_ranges })
128+
.collect_vec();
129+
Ok((!calls.is_empty()).then_some(calls))
130+
}
131+
36132
pub(crate) fn handle_document_highlight(
37133
snap: GlobalStateSnapshot,
38134
params: lsp_types::DocumentHighlightParams,
@@ -88,6 +184,193 @@ pub(crate) fn handle_references(
88184
Ok(Some(locations))
89185
}
90186

187+
fn reference_ranges_for_call_item(
188+
snap: &GlobalStateSnapshot,
189+
item: &lsp_types::CallHierarchyItem,
190+
) -> anyhow::Result<Vec<FileRange>> {
191+
let file_id = from_proto::file_id(snap, &item.uri)?;
192+
let line_info = snap.line_info(file_id)?;
193+
let offset = from_proto::offset(&line_info, item.selection_range.start)?;
194+
let position = FilePosition { file_id, offset };
195+
let config = snap.config.references();
196+
let Some(references) = snap.analysis.references(position, config)? else {
197+
return Ok(Vec::new());
198+
};
199+
200+
Ok(references
201+
.into_iter()
202+
.flat_map(|References { refs, .. }| {
203+
refs.into_iter().flat_map(|(file_id, refs)| {
204+
refs.into_iter().map(move |(range, _)| FileRange { file_id, range })
205+
})
206+
})
207+
.unique()
208+
.collect_vec())
209+
}
210+
211+
fn call_hierarchy_item_for_nav(
212+
snap: &GlobalStateSnapshot,
213+
nav: NavTarget,
214+
) -> anyhow::Result<Option<lsp_types::CallHierarchyItem>> {
215+
let Some(kind) = nav.kind else {
216+
return Ok(None);
217+
};
218+
if !is_call_hierarchy_kind(kind) {
219+
return Ok(None);
220+
}
221+
222+
let line_info = snap.line_info(nav.file_id)?;
223+
let uri = to_proto::url(snap, nav.file_id)?;
224+
let range = to_proto::range(&line_info, nav.full_range);
225+
let selection_range = to_proto::range(&line_info, nav.focus_or_full_range());
226+
let name = nav
227+
.name
228+
.map(|name| name.to_string())
229+
.unwrap_or_else(|| nav.description.clone().unwrap_or_else(|| "<anonymous>".to_owned()));
230+
let detail = nav.container_name.map(|name| name.to_string()).or(nav.description);
231+
232+
Ok(Some(lsp_types::CallHierarchyItem {
233+
name,
234+
kind: to_proto::symbol_kind(kind),
235+
tags: None,
236+
detail,
237+
uri,
238+
range,
239+
selection_range,
240+
data: None,
241+
}))
242+
}
243+
244+
fn workspace_module_items(
245+
snap: &GlobalStateSnapshot,
246+
) -> anyhow::Result<Vec<lsp_types::CallHierarchyItem>> {
247+
let mut file_ids = snap.file_ids();
248+
file_ids.sort_unstable_by_key(|file_id| file_id.0);
249+
file_ids.dedup();
250+
251+
let mut items = Vec::new();
252+
for file_id in file_ids {
253+
let uri = to_proto::url(snap, file_id)?;
254+
let line_info = snap.line_info(file_id)?;
255+
for symbol in snap.analysis.document_symbol(file_id)? {
256+
collect_module_items(&uri, &line_info, symbol, &mut items);
257+
}
258+
}
259+
260+
Ok(items.into_iter().unique_by(call_hierarchy_item_key).collect())
261+
}
262+
263+
fn collect_module_items(
264+
uri: &lsp_types::Url,
265+
line_info: &utils::lines::LineInfo,
266+
symbol: DocumentSymbol,
267+
items: &mut Vec<lsp_types::CallHierarchyItem>,
268+
) {
269+
if symbol.kind == SymbolKind::Module {
270+
items.push(call_hierarchy_item_for_symbol(uri, line_info, &symbol));
271+
}
272+
273+
for child in symbol.children {
274+
collect_module_items(uri, line_info, child, items);
275+
}
276+
}
277+
278+
fn enclosing_module_item(
279+
snap: &GlobalStateSnapshot,
280+
range: FileRange,
281+
) -> anyhow::Result<Option<lsp_types::CallHierarchyItem>> {
282+
let uri = to_proto::url(snap, range.file_id)?;
283+
let line_info = snap.line_info(range.file_id)?;
284+
let mut best = None;
285+
for symbol in snap.analysis.document_symbol(range.file_id)? {
286+
find_enclosing_module_symbol(symbol, range.range.start(), &mut best);
287+
}
288+
289+
Ok(best.map(|symbol| call_hierarchy_item_for_symbol(&uri, &line_info, &symbol)))
290+
}
291+
292+
fn find_enclosing_module_symbol(
293+
symbol: DocumentSymbol,
294+
offset: TextSize,
295+
best: &mut Option<DocumentSymbol>,
296+
) {
297+
if !range_contains_offset(symbol.full_range, offset) {
298+
return;
299+
}
300+
301+
if symbol.kind == SymbolKind::Module
302+
&& best.as_ref().is_none_or(|current| symbol.full_range.len() < current.full_range.len())
303+
{
304+
*best = Some(symbol.clone());
305+
}
306+
307+
for child in symbol.children {
308+
find_enclosing_module_symbol(child, offset, best);
309+
}
310+
}
311+
312+
fn call_hierarchy_item_for_symbol(
313+
uri: &lsp_types::Url,
314+
line_info: &utils::lines::LineInfo,
315+
symbol: &DocumentSymbol,
316+
) -> lsp_types::CallHierarchyItem {
317+
lsp_types::CallHierarchyItem {
318+
name: symbol.name.clone(),
319+
kind: to_proto::symbol_kind(symbol.kind),
320+
tags: None,
321+
detail: symbol.container_name.clone(),
322+
uri: uri.clone(),
323+
range: to_proto::range(line_info, symbol.full_range),
324+
selection_range: to_proto::range(line_info, symbol.focus_range),
325+
data: None,
326+
}
327+
}
328+
329+
fn is_call_hierarchy_kind(kind: SymbolKind) -> bool {
330+
matches!(kind, SymbolKind::Module)
331+
}
332+
333+
fn push_call_range(
334+
groups: &mut Vec<(lsp_types::CallHierarchyItem, Vec<lsp_types::Range>)>,
335+
item: lsp_types::CallHierarchyItem,
336+
range: lsp_types::Range,
337+
) {
338+
if let Some((_, ranges)) =
339+
groups.iter_mut().find(|(existing, _)| same_call_hierarchy_item(existing, &item))
340+
{
341+
if !ranges.contains(&range) {
342+
ranges.push(range);
343+
}
344+
return;
345+
}
346+
347+
groups.push((item, vec![range]));
348+
}
349+
350+
fn same_call_hierarchy_item(
351+
lhs: &lsp_types::CallHierarchyItem,
352+
rhs: &lsp_types::CallHierarchyItem,
353+
) -> bool {
354+
lhs.name == rhs.name
355+
&& lhs.uri == rhs.uri
356+
&& lhs.range == rhs.range
357+
&& lhs.selection_range == rhs.selection_range
358+
}
359+
360+
fn call_hierarchy_item_key(
361+
item: &lsp_types::CallHierarchyItem,
362+
) -> (String, lsp_types::Url, lsp_types::Range, lsp_types::Range) {
363+
(item.name.clone(), item.uri.clone(), item.range, item.selection_range)
364+
}
365+
366+
fn range_contains_offset(range: TextRange, offset: TextSize) -> bool {
367+
range.start() <= offset && offset < range.end()
368+
}
369+
370+
fn range_contains_range(container: TextRange, range: TextRange) -> bool {
371+
container.start() <= range.start() && range.end() <= container.end()
372+
}
373+
91374
pub(crate) fn handle_hover(
92375
snap: GlobalStateSnapshot,
93376
params: lsp_types::HoverParams,

src/lsp_ext/to_proto.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ fn diagnostic_is_unnecessary(diag: &ide_diagnostics::Diagnostic) -> bool {
252252
diag.tags.contains(&ide_diagnostics::DiagnosticTag::Unnecessary)
253253
}
254254

255-
fn symbol_kind(symbol_kind: SymbolKind) -> lsp_types::SymbolKind {
255+
pub(crate) fn symbol_kind(symbol_kind: SymbolKind) -> lsp_types::SymbolKind {
256256
use lsp_types::SymbolKind as LspSymbolKind;
257257
match symbol_kind {
258258
SymbolKind::Module => LspSymbolKind::MODULE,

0 commit comments

Comments
 (0)