diff --git a/src/global_state/dispatch/request.rs b/src/global_state/dispatch/request.rs index c22c960bd..341c51919 100644 --- a/src/global_state/dispatch/request.rs +++ b/src/global_state/dispatch/request.rs @@ -67,6 +67,10 @@ impl GlobalState { .on_no_retry::(handle_hover) .on_no_retry::(handle_goto_definition) .on_no_retry::(handle_goto_declaration) + .on_no_retry::(handle_goto_type_definition) + .on_no_retry::(handle_prepare_call_hierarchy) + .on_no_retry::(handle_call_hierarchy_incoming) + .on_no_retry::(handle_call_hierarchy_outgoing) .on_no_retry::(handle_document_highlight) .on_no_retry::(handle_references) .on_no_retry::(handle_prepare_rename) diff --git a/src/global_state/handlers/request.rs b/src/global_state/handlers/request.rs index 1f586a75e..676676802 100644 --- a/src/global_state/handlers/request.rs +++ b/src/global_state/handlers/request.rs @@ -20,8 +20,9 @@ pub(crate) use hints_lens::{ handle_code_lens, handle_code_lens_resolve, handle_inlay_hint, handle_signature_help, }; pub(crate) use navigation::{ - handle_document_highlight, handle_goto_declaration, handle_goto_definition, handle_hover, - handle_references, + handle_call_hierarchy_incoming, handle_call_hierarchy_outgoing, handle_document_highlight, + handle_goto_declaration, handle_goto_definition, handle_goto_type_definition, handle_hover, + handle_prepare_call_hierarchy, handle_references, }; pub(crate) use rename::{handle_prepare_rename, handle_rename}; pub(crate) use semantic_tokens::{ diff --git a/src/global_state/handlers/request/navigation.rs b/src/global_state/handlers/request/navigation.rs index cd62076a2..11f3e1df5 100644 --- a/src/global_state/handlers/request/navigation.rs +++ b/src/global_state/handlers/request/navigation.rs @@ -1,5 +1,9 @@ -use ide::{FileRange, references::References}; +use ide::{ + FilePosition, FileRange, SymbolKind, document_symbols::DocumentSymbol, + navigation_target::NavTarget, references::References, +}; use itertools::Itertools; +use utils::text_edit::{TextRange, TextSize}; use crate::{ global_state::snapshot::GlobalStateSnapshot, @@ -33,6 +37,98 @@ pub(crate) fn handle_goto_declaration( Ok(Some(res)) } +pub(crate) fn handle_goto_type_definition( + snap: GlobalStateSnapshot, + params: lsp_types::request::GotoTypeDefinitionParams, +) -> anyhow::Result> { + handle_goto_definition(snap, params) +} + +pub(crate) fn handle_prepare_call_hierarchy( + snap: GlobalStateSnapshot, + params: lsp_types::CallHierarchyPrepareParams, +) -> anyhow::Result>> { + let position = from_proto::file_position(&snap, params.text_document_position_params)?; + let Some(nav_info) = snap.analysis.goto_definition(position)? else { + return Ok(None); + }; + + let items = nav_info + .info + .into_iter() + .filter_map(|nav| call_hierarchy_item_for_nav(&snap, nav).transpose()) + .collect::>>()? + .into_iter() + .unique_by(call_hierarchy_item_key) + .collect_vec(); + + Ok((!items.is_empty()).then_some(items)) +} + +pub(crate) fn handle_call_hierarchy_incoming( + snap: GlobalStateSnapshot, + params: lsp_types::CallHierarchyIncomingCallsParams, +) -> anyhow::Result>> { + let target = params.item; + let target_file_id = from_proto::file_id(&snap, &target.uri)?; + let target_line_info = snap.line_info(target_file_id)?; + let target_selection_range = from_proto::text_range(&target_line_info, target.selection_range)?; + + let mut groups = Vec::<(lsp_types::CallHierarchyItem, Vec)>::new(); + for reference in reference_ranges_for_call_item(&snap, &target)? { + if reference.file_id == target_file_id && reference.range == target_selection_range { + continue; + } + + let Some(caller) = enclosing_module_item(&snap, reference)? else { + continue; + }; + let line_info = snap.line_info(reference.file_id)?; + push_call_range(&mut groups, caller, to_proto::range(&line_info, reference.range)); + } + + let calls = groups + .into_iter() + .map(|(from, from_ranges)| lsp_types::CallHierarchyIncomingCall { from, from_ranges }) + .collect_vec(); + Ok((!calls.is_empty()).then_some(calls)) +} + +pub(crate) fn handle_call_hierarchy_outgoing( + snap: GlobalStateSnapshot, + params: lsp_types::CallHierarchyOutgoingCallsParams, +) -> anyhow::Result>> { + let caller = params.item; + let caller_file_id = from_proto::file_id(&snap, &caller.uri)?; + let caller_line_info = snap.line_info(caller_file_id)?; + let caller_range = from_proto::text_range(&caller_line_info, caller.range)?; + + let mut groups = Vec::<(lsp_types::CallHierarchyItem, Vec)>::new(); + for callee in workspace_module_items(&snap)? { + if same_call_hierarchy_item(&caller, &callee) { + continue; + } + + for reference in reference_ranges_for_call_item(&snap, &callee)? { + if reference.file_id == caller_file_id + && range_contains_range(caller_range, reference.range) + { + push_call_range( + &mut groups, + callee.clone(), + to_proto::range(&caller_line_info, reference.range), + ); + } + } + } + + let calls = groups + .into_iter() + .map(|(to, from_ranges)| lsp_types::CallHierarchyOutgoingCall { to, from_ranges }) + .collect_vec(); + Ok((!calls.is_empty()).then_some(calls)) +} + pub(crate) fn handle_document_highlight( snap: GlobalStateSnapshot, params: lsp_types::DocumentHighlightParams, @@ -88,6 +184,193 @@ pub(crate) fn handle_references( Ok(Some(locations)) } +fn reference_ranges_for_call_item( + snap: &GlobalStateSnapshot, + item: &lsp_types::CallHierarchyItem, +) -> anyhow::Result> { + let file_id = from_proto::file_id(snap, &item.uri)?; + let line_info = snap.line_info(file_id)?; + let offset = from_proto::offset(&line_info, item.selection_range.start)?; + let position = FilePosition { file_id, offset }; + let config = snap.config.references(); + let Some(references) = snap.analysis.references(position, config)? else { + return Ok(Vec::new()); + }; + + Ok(references + .into_iter() + .flat_map(|References { refs, .. }| { + refs.into_iter().flat_map(|(file_id, refs)| { + refs.into_iter().map(move |(range, _)| FileRange { file_id, range }) + }) + }) + .unique() + .collect_vec()) +} + +fn call_hierarchy_item_for_nav( + snap: &GlobalStateSnapshot, + nav: NavTarget, +) -> anyhow::Result> { + let Some(kind) = nav.kind else { + return Ok(None); + }; + if !is_call_hierarchy_kind(kind) { + return Ok(None); + } + + let line_info = snap.line_info(nav.file_id)?; + let uri = to_proto::url(snap, nav.file_id)?; + let range = to_proto::range(&line_info, nav.full_range); + let selection_range = to_proto::range(&line_info, nav.focus_or_full_range()); + let name = nav + .name + .map(|name| name.to_string()) + .unwrap_or_else(|| nav.description.clone().unwrap_or_else(|| "".to_owned())); + let detail = nav.container_name.map(|name| name.to_string()).or(nav.description); + + Ok(Some(lsp_types::CallHierarchyItem { + name, + kind: to_proto::symbol_kind(kind), + tags: None, + detail, + uri, + range, + selection_range, + data: None, + })) +} + +fn workspace_module_items( + snap: &GlobalStateSnapshot, +) -> anyhow::Result> { + let mut file_ids = snap.file_ids(); + file_ids.sort_unstable_by_key(|file_id| file_id.0); + file_ids.dedup(); + + let mut items = Vec::new(); + for file_id in file_ids { + let uri = to_proto::url(snap, file_id)?; + let line_info = snap.line_info(file_id)?; + for symbol in snap.analysis.document_symbol(file_id)? { + collect_module_items(&uri, &line_info, symbol, &mut items); + } + } + + Ok(items.into_iter().unique_by(call_hierarchy_item_key).collect()) +} + +fn collect_module_items( + uri: &lsp_types::Url, + line_info: &utils::lines::LineInfo, + symbol: DocumentSymbol, + items: &mut Vec, +) { + if symbol.kind == SymbolKind::Module { + items.push(call_hierarchy_item_for_symbol(uri, line_info, &symbol)); + } + + for child in symbol.children { + collect_module_items(uri, line_info, child, items); + } +} + +fn enclosing_module_item( + snap: &GlobalStateSnapshot, + range: FileRange, +) -> anyhow::Result> { + let uri = to_proto::url(snap, range.file_id)?; + let line_info = snap.line_info(range.file_id)?; + let mut best = None; + for symbol in snap.analysis.document_symbol(range.file_id)? { + find_enclosing_module_symbol(symbol, range.range.start(), &mut best); + } + + Ok(best.map(|symbol| call_hierarchy_item_for_symbol(&uri, &line_info, &symbol))) +} + +fn find_enclosing_module_symbol( + symbol: DocumentSymbol, + offset: TextSize, + best: &mut Option, +) { + if !range_contains_offset(symbol.full_range, offset) { + return; + } + + if symbol.kind == SymbolKind::Module + && best.as_ref().is_none_or(|current| symbol.full_range.len() < current.full_range.len()) + { + *best = Some(symbol.clone()); + } + + for child in symbol.children { + find_enclosing_module_symbol(child, offset, best); + } +} + +fn call_hierarchy_item_for_symbol( + uri: &lsp_types::Url, + line_info: &utils::lines::LineInfo, + symbol: &DocumentSymbol, +) -> lsp_types::CallHierarchyItem { + lsp_types::CallHierarchyItem { + name: symbol.name.clone(), + kind: to_proto::symbol_kind(symbol.kind), + tags: None, + detail: symbol.container_name.clone(), + uri: uri.clone(), + range: to_proto::range(line_info, symbol.full_range), + selection_range: to_proto::range(line_info, symbol.focus_range), + data: None, + } +} + +fn is_call_hierarchy_kind(kind: SymbolKind) -> bool { + matches!(kind, SymbolKind::Module) +} + +fn push_call_range( + groups: &mut Vec<(lsp_types::CallHierarchyItem, Vec)>, + item: lsp_types::CallHierarchyItem, + range: lsp_types::Range, +) { + if let Some((_, ranges)) = + groups.iter_mut().find(|(existing, _)| same_call_hierarchy_item(existing, &item)) + { + if !ranges.contains(&range) { + ranges.push(range); + } + return; + } + + groups.push((item, vec![range])); +} + +fn same_call_hierarchy_item( + lhs: &lsp_types::CallHierarchyItem, + rhs: &lsp_types::CallHierarchyItem, +) -> bool { + lhs.name == rhs.name + && lhs.uri == rhs.uri + && lhs.range == rhs.range + && lhs.selection_range == rhs.selection_range +} + +fn call_hierarchy_item_key( + item: &lsp_types::CallHierarchyItem, +) -> (String, lsp_types::Url, lsp_types::Range, lsp_types::Range) { + (item.name.clone(), item.uri.clone(), item.range, item.selection_range) +} + +fn range_contains_offset(range: TextRange, offset: TextSize) -> bool { + range.start() <= offset && offset < range.end() +} + +fn range_contains_range(container: TextRange, range: TextRange) -> bool { + container.start() <= range.start() && range.end() <= container.end() +} + pub(crate) fn handle_hover( snap: GlobalStateSnapshot, params: lsp_types::HoverParams, diff --git a/src/lsp_ext/to_proto.rs b/src/lsp_ext/to_proto.rs index 1b81e22c3..d13b6fb60 100644 --- a/src/lsp_ext/to_proto.rs +++ b/src/lsp_ext/to_proto.rs @@ -252,7 +252,7 @@ fn diagnostic_is_unnecessary(diag: &ide_diagnostics::Diagnostic) -> bool { diag.tags.contains(&ide_diagnostics::DiagnosticTag::Unnecessary) } -fn symbol_kind(symbol_kind: SymbolKind) -> lsp_types::SymbolKind { +pub(crate) fn symbol_kind(symbol_kind: SymbolKind) -> lsp_types::SymbolKind { use lsp_types::SymbolKind as LspSymbolKind; match symbol_kind { SymbolKind::Module => LspSymbolKind::MODULE, diff --git a/src/tests.rs b/src/tests.rs index 4f1b7a538..d9224fcd5 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -5,6 +5,8 @@ use std::{ use lsp_server::{Connection, Message, Notification, Request}; use lsp_types::{ + CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem, + CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams, ClientCapabilities, CodeActionCapabilityResolveSupport, CodeActionClientCapabilities, CodeActionContext, CodeActionKind, CodeActionKindLiteralSupport, CodeActionLiteralSupport, CodeActionOrCommand, CodeActionParams, CompletionParams, CompletionResponse, @@ -23,10 +25,12 @@ use lsp_types::{ DidSaveTextDocument, Exit, Notification as _, }, request::{ + CallHierarchyIncomingCalls, CallHierarchyOutgoingCalls, CallHierarchyPrepare, CodeActionRequest, CodeLensRequest, CodeLensResolve, Completion as CompletionRequest, DocumentDiagnosticRequest, DocumentSymbolRequest, ExecuteCommand, FoldingRangeRequest, - GotoDefinition, HoverRequest, References, Request as _, SemanticTokensFullRequest, - Shutdown, WorkspaceConfiguration, WorkspaceDiagnosticRequest, WorkspaceSymbolRequest, + GotoDefinition, GotoTypeDefinition, HoverRequest, References, Request as _, + SemanticTokensFullRequest, Shutdown, WorkspaceConfiguration, WorkspaceDiagnosticRequest, + WorkspaceSymbolRequest, }, }; use serde::de::DeserializeOwned; @@ -422,6 +426,111 @@ fn request_goto_definition_uris( definition.map(goto_definition_response_uris).unwrap_or_default() } +fn request_type_definition_uris( + client: &Connection, + uri: Url, + text: &str, + needle: &str, + request_id: i32, +) -> Vec { + let request_id = lsp_server::RequestId::from(request_id); + client + .sender + .send(Message::Request(Request::new( + request_id.clone(), + GotoTypeDefinition::METHOD.to_string(), + GotoDefinitionParams { + text_document_position_params: TextDocumentPositionParams { + text_document: TextDocumentIdentifier { uri }, + position: position_of(text, needle), + }, + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: Default::default(), + }, + ))) + .unwrap(); + + let definition: Option = + recv_response(client, request_id, "typeDefinition"); + definition.map(goto_definition_response_uris).unwrap_or_default() +} + +fn prepare_call_hierarchy( + client: &Connection, + uri: Url, + text: &str, + needle: &str, + request_id: i32, +) -> Vec { + let request_id = lsp_server::RequestId::from(request_id); + client + .sender + .send(Message::Request(Request::new( + request_id.clone(), + CallHierarchyPrepare::METHOD.to_string(), + CallHierarchyPrepareParams { + text_document_position_params: TextDocumentPositionParams { + text_document: TextDocumentIdentifier { uri }, + position: position_of(text, needle), + }, + work_done_progress_params: WorkDoneProgressParams::default(), + }, + ))) + .unwrap(); + + let items: Option> = + recv_response(client, request_id, "prepareCallHierarchy"); + items.unwrap_or_default() +} + +fn request_call_hierarchy_incoming( + client: &Connection, + item: CallHierarchyItem, + request_id: i32, +) -> Vec { + let request_id = lsp_server::RequestId::from(request_id); + client + .sender + .send(Message::Request(Request::new( + request_id.clone(), + CallHierarchyIncomingCalls::METHOD.to_string(), + CallHierarchyIncomingCallsParams { + item, + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: Default::default(), + }, + ))) + .unwrap(); + + let calls: Option> = + recv_response(client, request_id, "callHierarchy/incomingCalls"); + calls.unwrap_or_default() +} + +fn request_call_hierarchy_outgoing( + client: &Connection, + item: CallHierarchyItem, + request_id: i32, +) -> Vec { + let request_id = lsp_server::RequestId::from(request_id); + client + .sender + .send(Message::Request(Request::new( + request_id.clone(), + CallHierarchyOutgoingCalls::METHOD.to_string(), + CallHierarchyOutgoingCallsParams { + item, + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: Default::default(), + }, + ))) + .unwrap(); + + let calls: Option> = + recv_response(client, request_id, "callHierarchy/outgoingCalls"); + calls.unwrap_or_default() +} + fn request_reference_uris( client: &Connection, uri: Url, diff --git a/src/tests/navigation.rs b/src/tests/navigation.rs index f65ebefed..35b823f7a 100644 --- a/src/tests/navigation.rs +++ b/src/tests/navigation.rs @@ -32,6 +32,111 @@ fn unconfigured_workspace_goto_definition_uses_indexed_unopened_files() { shutdown_test_server(&client, server_thread); } +#[test] +fn type_definition_request_uses_module_definition_navigation() { + let temp_dir = TempDir::new("type-definition-module-nav"); + let rtl_dir = temp_dir.path().join("rtl"); + fs::create_dir_all(&rtl_dir).unwrap(); + + let top_text = "module top;\n child u_child();\nendmodule\n"; + let child_text = "module child;\nendmodule\n"; + + fs::write( + temp_dir.path().join("vide.toml"), + "top_modules = [\"top\"]\nsources = [\"rtl/*.v\"]\ninclude_dirs = [\"rtl\"]\n", + ) + .unwrap(); + let top_path = rtl_dir.join("top.v"); + let child_path = rtl_dir.join("child.v"); + fs::write(&top_path, top_text).unwrap(); + fs::write(&child_path, child_text).unwrap(); + + let (client, server_thread) = spawn_test_workspace( + temp_dir.path().to_path_buf(), + ClientCapabilities::default(), + UserConfig::default(), + ); + let top_uri = to_proto::url_from_abs_path(top_path.as_path()).unwrap(); + let child_uri = to_proto::url_from_abs_path(child_path.as_path()).unwrap(); + open_test_document(&client, top_uri.clone(), top_text); + open_test_document(&client, child_uri.clone(), child_text); + let _ = request_document_diagnostics(&client, top_uri.clone(), 1); + + let definition_uris = + request_type_definition_uris(&client, top_uri, top_text, "child u_child", 2); + assert!( + definition_uris.contains(&child_uri), + "typeDefinition should reach child.v through the advertised capability: {definition_uris:?}" + ); + + shutdown_test_server(&client, server_thread); +} + +#[test] +fn call_hierarchy_reports_module_instance_edges() { + let temp_dir = TempDir::new("call-hierarchy-module-edges"); + let rtl_dir = temp_dir.path().join("rtl"); + fs::create_dir_all(&rtl_dir).unwrap(); + + let top_text = "module top;\n child u_child();\nendmodule\n"; + let child_text = "module child;\n leaf u_leaf();\nendmodule\n"; + let leaf_text = "module leaf;\nendmodule\n"; + + fs::write( + temp_dir.path().join("vide.toml"), + "top_modules = [\"top\"]\nsources = [\"rtl/*.v\"]\ninclude_dirs = [\"rtl\"]\n", + ) + .unwrap(); + let top_path = rtl_dir.join("top.v"); + let child_path = rtl_dir.join("child.v"); + let leaf_path = rtl_dir.join("leaf.v"); + fs::write(&top_path, top_text).unwrap(); + fs::write(&child_path, child_text).unwrap(); + fs::write(&leaf_path, leaf_text).unwrap(); + + let (client, server_thread) = spawn_test_workspace( + temp_dir.path().to_path_buf(), + ClientCapabilities::default(), + UserConfig::default(), + ); + let top_uri = to_proto::url_from_abs_path(top_path.as_path()).unwrap(); + let child_uri = to_proto::url_from_abs_path(child_path.as_path()).unwrap(); + let leaf_uri = to_proto::url_from_abs_path(leaf_path.as_path()).unwrap(); + open_test_document(&client, top_uri.clone(), top_text); + open_test_document(&client, child_uri.clone(), child_text); + open_test_document(&client, leaf_uri.clone(), leaf_text); + let _ = request_document_diagnostics(&client, top_uri.clone(), 1); + + let prepared = prepare_call_hierarchy(&client, child_uri.clone(), child_text, "child;", 2); + let child_item = prepared + .into_iter() + .find(|item| item.name == "child") + .unwrap_or_else(|| panic!("child module should prepare call hierarchy item")); + assert_eq!(child_item.kind, lsp_types::SymbolKind::MODULE); + + let incoming = request_call_hierarchy_incoming(&client, child_item.clone(), 3); + assert!( + incoming.iter().any(|call| { + call.from.name == "top" + && call.from.uri == top_uri + && call.from_ranges.contains(&range_of(top_text, "child")) + }), + "incoming calls should include top instantiating child: {incoming:?}" + ); + + let outgoing = request_call_hierarchy_outgoing(&client, child_item, 4); + assert!( + outgoing.iter().any(|call| { + call.to.name == "leaf" + && call.to.uri == leaf_uri + && call.from_ranges.contains(&range_of(child_text, "leaf")) + }), + "outgoing calls should include child instantiating leaf: {outgoing:?}" + ); + + shutdown_test_server(&client, server_thread); +} + #[test] fn include_expanded_parameter_decls_keep_module_navigation_available() { let temp_dir = TempDir::new("include-param-module-nav");