diff --git a/lapce-app/src/command.rs b/lapce-app/src/command.rs index ef6977871c..1dcc564159 100644 --- a/lapce-app/src/command.rs +++ b/lapce-app/src/command.rs @@ -746,6 +746,9 @@ pub enum InternalCommand { tab_index: usize, terminal_index: usize, }, + CallHierarchyIncoming { + item_id: ViewId, + }, } #[derive(Clone)] diff --git a/lapce-app/src/editor.rs b/lapce-app/src/editor.rs index 5b89aeeafe..8b2bebdc11 100644 --- a/lapce-app/src/editor.rs +++ b/lapce-app/src/editor.rs @@ -25,6 +25,7 @@ use floem::{ visual_line::{ConfigId, Lines, TextLayoutProvider, VLine, VLineInfo}, Editor, }, + ViewId, }; use itertools::Itertools; use lapce_core::{ @@ -48,7 +49,7 @@ use lapce_xi_rope::{Rope, RopeDelta, Transformer}; use lsp_types::{ CompletionItem, CompletionTextEdit, GotoDefinitionResponse, HoverContents, InlayHint, InlayHintLabel, InlineCompletionTriggerKind, Location, MarkedString, - MarkupKind, TextEdit, + MarkupKind, Range, TextEdit, }; use serde::{Deserialize, Serialize}; @@ -71,6 +72,7 @@ use crate::{ markdown::{ from_marked_string, from_plaintext, parse_markdown, MarkdownContent, }, + panel::{call_hierarchy_view::CallHierarchyItemData, kind::PanelKind}, snippet::Snippet, tracing::*, window_tab::{CommonData, Focus, WindowTabData}, @@ -1267,7 +1269,7 @@ impl EditorData { ); } - pub fn show_call_hierarchy(&self) { + pub fn call_hierarchy(&self, window_tab_data: WindowTabData) { let doc = self.doc(); let path = match if doc.loaded() { doc.content.with_untracked(|c| c.path().cloned()) @@ -1285,28 +1287,41 @@ impl EditorData { let position = buffer.offset_to_position(offset); (start_position, position) }); - - let send = create_ext_action(self.scope, move |_rs| { - tracing::debug!("{:?}", _rs); - }); - let proxy = self.common.proxy.clone(); + let scope = window_tab_data.scope; + let range = Range { + start: _start_position, + end: position, + }; self.common.proxy.show_call_hierarchy( - path.clone(), + path, position, - move |result| { + create_ext_action(self.scope, move |result| { if let Ok(ProxyResponse::ShowCallHierarchyResponse { items, .. }) = result { if let Some(item) = items.and_then(|x| x.into_iter().next()) { - proxy.call_hierarchy_incoming( - path.clone(), - item.clone(), - send, + let root = scope.create_rw_signal(CallHierarchyItemData { + view_id: ViewId::new(), + item: Rc::new(item), + from_range: range, + init: false, + open: scope.create_rw_signal(true), + children: scope.create_rw_signal(Vec::with_capacity(0)), + }); + let item = root; + window_tab_data.call_hierarchy_data.root.update(|x| { + *x = Some(root); + }); + window_tab_data.show_panel(PanelKind::CallHierarchy); + window_tab_data.common.internal_command.send( + InternalCommand::CallHierarchyIncoming { + item_id: item.get_untracked().view_id, + }, ); } } - }, + }), ); } @@ -2625,9 +2640,9 @@ impl EditorData { vec![ Some(CommandKind::Focus(FocusCommand::GotoDefinition)), Some(CommandKind::Focus(FocusCommand::GotoTypeDefinition)), - // Some(CommandKind::Workbench( - // LapceWorkbenchCommand::ShowCallHierarchy, - // )), + Some(CommandKind::Workbench( + LapceWorkbenchCommand::ShowCallHierarchy, + )), None, Some(CommandKind::Focus(FocusCommand::Rename)), None, diff --git a/lapce-app/src/main_split.rs b/lapce-app/src/main_split.rs index d9ce0ff376..9819a0c834 100644 --- a/lapce-app/src/main_split.rs +++ b/lapce-app/src/main_split.rs @@ -2164,7 +2164,7 @@ impl MainSplitData { } } - pub fn run_code_lens(&self, args: &Vec, command: &str) { + pub fn run_code_lens(&self, args: &[Value], command: &str) { match command { "rust-analyzer.runSingle" | "rust-analyzer.debugSingle" => { if let Some(config) = get_rust_command_config(args) { diff --git a/lapce-app/src/panel/call_hierarchy_view.rs b/lapce-app/src/panel/call_hierarchy_view.rs new file mode 100644 index 0000000000..6e76bbf4c4 --- /dev/null +++ b/lapce-app/src/panel/call_hierarchy_view.rs @@ -0,0 +1,242 @@ +use std::{ops::AddAssign, rc::Rc}; + +use floem::{ + event::EventPropagation, + reactive::RwSignal, + style::{AlignItems, CursorStyle}, + views::{ + container, label, scroll, stack, svg, virtual_stack, Decorators, + VirtualDirection, VirtualItemSize, VirtualVector, + }, + View, ViewId, +}; +use lsp_types::{CallHierarchyItem, Range}; + +use super::position::PanelPosition; +use crate::{ + command::InternalCommand, + config::{color::LapceColor, icon::LapceIcons}, + editor::location::EditorLocation, + window_tab::CommonData, + window_tab::WindowTabData, +}; + +#[derive(Clone, Debug)] +pub struct CallHierarchyData { + pub root: RwSignal>>, + pub common: Rc, + pub scroll_to_line: RwSignal>, +} + +#[derive(Debug, Clone)] +pub struct CallHierarchyItemData { + pub view_id: ViewId, + pub item: Rc, + pub from_range: Range, + pub init: bool, + pub open: RwSignal, + pub children: RwSignal>>, +} + +impl CallHierarchyItemData { + pub fn child_count(&self) -> usize { + let mut count = 1; + if self.open.get() { + for child in self.children.get_untracked() { + count += child.with(|x| x.child_count()) + } + } + count + } + + pub fn find_by_id( + root: RwSignal, + view_id: ViewId, + ) -> Option> { + if root.get_untracked().view_id == view_id { + Some(root) + } else { + root.get_untracked() + .children + .get_untracked() + .into_iter() + .find_map(|x| Self::find_by_id(x, view_id)) + } + } +} + +fn get_children( + data: RwSignal, + next: &mut usize, + min: usize, + max: usize, + level: usize, +) -> Vec<(usize, usize, RwSignal)> { + let mut children = Vec::new(); + if *next >= min && *next < max { + children.push((*next, level, data)); + } else if *next >= max { + return children; + } + next.add_assign(1); + if data.get_untracked().open.get() { + for child in data.get().children.get_untracked() { + let child_children = get_children(child, next, min, max, level + 1); + children.extend(child_children); + if *next > max { + break; + } + } + } + children +} + +pub struct VirtualList { + root: Option>, +} + +impl VirtualList { + pub fn new(root: Option>) -> Self { + Self { root } + } +} + +impl VirtualVector<(usize, usize, RwSignal)> for VirtualList { + fn total_len(&self) -> usize { + if let Some(root) = &self.root { + root.with(|x| x.child_count()) + } else { + 0 + } + } + + fn slice( + &mut self, + range: std::ops::Range, + ) -> impl Iterator)> { + if let Some(root) = &self.root { + let min = range.start; + let max = range.end; + let children = get_children(*root, &mut 0, min, max, 0); + children.into_iter() + } else { + Vec::new().into_iter() + } + } +} +pub fn show_hierarchy_panel( + window_tab_data: Rc, + _position: PanelPosition, +) -> impl View { + let call_hierarchy_data = window_tab_data.call_hierarchy_data.clone(); + let config = call_hierarchy_data.common.config; + let ui_line_height = call_hierarchy_data.common.ui_line_height; + let scroll_to_line = call_hierarchy_data.scroll_to_line; + scroll( + virtual_stack( + VirtualDirection::Vertical, + VirtualItemSize::Fixed(Box::new(move || ui_line_height.get())), + move || VirtualList::new(call_hierarchy_data.root.get()), + move |(_, _, item)| item.get_untracked().view_id, + move |(_, level, rw_data)| { + let data = rw_data.get_untracked(); + let open = data.open; + stack(( + svg(move || { + let config = config.get(); + let svg_str = match open.get() { + true => LapceIcons::ITEM_OPENED, + false => LapceIcons::ITEM_CLOSED, + }; + config.ui_svg(svg_str) + }) + .style(move |s| { + let config = config.get(); + let size = config.ui.icon_size() as f32; + s.size(size, size) + .flex_shrink(0.0) + .margin_left(10.0) + .margin_right(6.0) + .color(config.color(LapceColor::LAPCE_ICON_ACTIVE)) + }).on_click_stop({ + let window_tab_data = window_tab_data.clone(); + move |_x| { + open.update(|x| { + *x = !*x; + }); + if !rw_data.get_untracked().init { + window_tab_data.common.internal_command.send( + InternalCommand::CallHierarchyIncoming { + item_id: rw_data.get_untracked().view_id, + }, + ); + } + } + }), + container( + label(move || { + format!( + "{} {} {}", + data.item.name, + data.item.detail.as_deref().unwrap_or(""), data.from_range.start.line + ) + }) + .style(move |s| { + s.flex_grow(1.0) + .height(ui_line_height.get()) + .selectable(false) + .align_items(AlignItems::Center) + }), + ) + .style(|s| s.flex_grow(1.0).padding(0.0).margin(0.0)), + )) + .style(move |s| { + s.padding_right(5.0) + .padding_left((level * 10) as f32) + .align_items(AlignItems::Center) + .hover(|s| { + s.background( + config + .get() + .color(LapceColor::PANEL_HOVERED_BACKGROUND), + ) + .cursor(CursorStyle::Pointer) + }) + }) + .on_double_click({ + let window_tab_data = window_tab_data.clone(); + let data = rw_data; + move |_| { + window_tab_data.common.internal_command.send( + InternalCommand::CallHierarchyIncoming { item_id: rw_data.get_untracked().view_id }, + ); + let data = data.get_untracked(); + if let Ok(path) = data.item.uri.to_file_path() { + window_tab_data + .common + .internal_command + .send(InternalCommand::GoToLocation { location: EditorLocation { + path, + position: Some(crate::editor::location::EditorPosition::Position(data.from_range.start)), + scroll_offset: None, + ignore_unconfirmed: false, + same_editor_tab: false, + } }); + } + EventPropagation::Stop + } + }) + }, + ) + .style(|s| s.flex_col().align_items(AlignItems::Stretch).width_full()), + ) + .style(|s| s.size_full()) + .scroll_to(move || { + if let Some(line) = scroll_to_line.get() { + let line_height = ui_line_height.get(); + Some((0.0, line * line_height).into()) + } else { + None + } + }) +} diff --git a/lapce-app/src/panel/data.rs b/lapce-app/src/panel/data.rs index 7507d22a60..edc270678e 100644 --- a/lapce-app/src/panel/data.rs +++ b/lapce-app/src/panel/data.rs @@ -31,7 +31,12 @@ pub fn default_panel_order() -> PanelOrder { ); order.insert( PanelPosition::BottomLeft, - im::vector![PanelKind::Terminal, PanelKind::Search, PanelKind::Problem,], + im::vector![ + PanelKind::Terminal, + PanelKind::Search, + PanelKind::Problem, + PanelKind::CallHierarchy + ], ); order diff --git a/lapce-app/src/panel/kind.rs b/lapce-app/src/panel/kind.rs index 1930b0eeaa..671571ebbf 100644 --- a/lapce-app/src/panel/kind.rs +++ b/lapce-app/src/panel/kind.rs @@ -15,6 +15,7 @@ pub enum PanelKind { Search, Problem, Debug, + CallHierarchy, } impl PanelKind { @@ -27,6 +28,7 @@ impl PanelKind { PanelKind::Search => LapceIcons::SEARCH, PanelKind::Problem => LapceIcons::PROBLEM, PanelKind::Debug => LapceIcons::DEBUG, + PanelKind::CallHierarchy => LapceIcons::LINK, } } diff --git a/lapce-app/src/panel/mod.rs b/lapce-app/src/panel/mod.rs index c6dca06194..a539e9f9f3 100644 --- a/lapce-app/src/panel/mod.rs +++ b/lapce-app/src/panel/mod.rs @@ -1,3 +1,4 @@ +pub mod call_hierarchy_view; pub mod data; pub mod debug_view; pub mod global_search_view; diff --git a/lapce-app/src/panel/view.rs b/lapce-app/src/panel/view.rs index dcb9b58748..1fdc4c40b5 100644 --- a/lapce-app/src/panel/view.rs +++ b/lapce-app/src/panel/view.rs @@ -28,6 +28,7 @@ use crate::{ app::{clickable_icon, clickable_icon_base}, config::{color::LapceColor, icon::LapceIcons, LapceConfig}, file_explorer::view::file_explorer_panel, + panel::call_hierarchy_view::show_hierarchy_panel, window_tab::{DragContent, WindowTabData}, }; @@ -484,6 +485,10 @@ fn panel_view( PanelKind::Debug => { debug_panel(window_tab_data.clone(), position).into_any() } + PanelKind::CallHierarchy => { + show_hierarchy_panel(window_tab_data.clone(), position) + .into_any() + } }; view.style(|s| s.size_pct(100.0, 100.0)) }, @@ -538,6 +543,7 @@ fn panel_picker( PanelKind::Search => (LapceIcons::SEARCH, "Search"), PanelKind::Problem => (LapceIcons::PROBLEM, "Problems"), PanelKind::Debug => (LapceIcons::DEBUG_ALT, "Debug"), + PanelKind::CallHierarchy => (LapceIcons::LINK, "Call Hierarchy"), }; let is_active = { let window_tab_data = window_tab_data.clone(); diff --git a/lapce-app/src/window_tab.rs b/lapce-app/src/window_tab.rs index 320a2bb012..338f08ca3f 100644 --- a/lapce-app/src/window_tab.rs +++ b/lapce-app/src/window_tab.rs @@ -66,6 +66,7 @@ use crate::{ main_split::{MainSplitData, SplitData, SplitDirection, SplitMoveDirection}, palette::{kind::PaletteKind, PaletteData, PaletteStatus}, panel::{ + call_hierarchy_view::{CallHierarchyData, CallHierarchyItemData}, data::{default_panel_order, PanelData, PanelSection}, kind::PanelKind, position::PanelContainerPosition, @@ -168,6 +169,7 @@ pub struct WindowTabData { pub source_control: SourceControlData, pub rename: RenameData, pub global_search: GlobalSearchData, + pub call_hierarchy_data: CallHierarchyData, pub about_data: AboutData, pub alert_data: AlertBoxData, pub layout_rect: RwSignal, @@ -541,6 +543,11 @@ impl WindowTabData { plugin, rename, global_search, + call_hierarchy_data: CallHierarchyData { + root: cx.create_rw_signal(None), + common: common.clone(), + scroll_to_line: cx.create_rw_signal(None), + }, about_data, alert_data, layout_rect: cx.create_rw_signal(Rect::ZERO), @@ -1380,7 +1387,7 @@ impl WindowTabData { if let Some(editor_data) = self.main_split.active_editor.get_untracked() { - editor_data.show_call_hierarchy(); + editor_data.call_hierarchy(self.clone()); } } } @@ -1847,6 +1854,9 @@ impl WindowTabData { raw.write().term.reset_state(); view_id.request_paint(); } + InternalCommand::CallHierarchyIncoming { item_id } => { + self.call_hierarchy_incoming(item_id); + } } } @@ -2396,7 +2406,8 @@ impl WindowTabData { PanelKind::FileExplorer | PanelKind::Plugin | PanelKind::Problem - | PanelKind::Debug => { + | PanelKind::Debug + | PanelKind::CallHierarchy => { // Some panels don't accept focus (yet). Fall back to visibility check // in those cases. self.panel.is_panel_visible(&kind) @@ -2662,6 +2673,59 @@ impl WindowTabData { remove_overlay(old_id); } } + + pub fn call_hierarchy_incoming(&self, item_id: ViewId) { + let Some(root) = self.call_hierarchy_data.root.get_untracked() else { + return; + }; + let Some(item) = CallHierarchyItemData::find_by_id(root, item_id) else { + return; + }; + let root_item = item; + let path: PathBuf = item.get_untracked().item.uri.to_file_path().unwrap(); + let scope = self.scope; + let send = + create_ext_action(scope, move |_rs: Result| { + match _rs { + Ok(ProxyResponse::CallHierarchyIncomingResponse { items }) => { + if let Some(items) = items { + let mut item_children = Vec::new(); + for x in items { + let item = Rc::new(x.from); + for range in x.from_ranges { + item_children.push(scope.create_rw_signal( + CallHierarchyItemData { + view_id: floem::ViewId::new(), + item: item.clone(), + from_range: range, + init: false, + open: scope.create_rw_signal(false), + children: + scope.create_rw_signal(Vec::new()), + }, + )) + } + } + root_item.update(|x| { + x.init = true; + x.children.update(|children| { + *children = item_children; + }) + }); + } + } + Err(err) => { + tracing::error!("{:?}", err); + } + Ok(_) => {} + } + }); + self.common.proxy.call_hierarchy_incoming( + path, + item.get_untracked().item.as_ref().clone(), + send, + ); + } } /// Open path with the default application without blocking.