Debugger implementation (#13433)
### DISCLAIMER > As of 6th March 2025, debugger is still in development. We plan to merge it behind a staff-only feature flag for staff use only, followed by non-public release and then finally a public one (akin to how Git panel release was handled). This is done to ensure the best experience when it gets released. ### END OF DISCLAIMER **The current state of the debugger implementation:** https://github.com/user-attachments/assets/c4deff07-80dd-4dc6-ad2e-0c252a478fe9 https://github.com/user-attachments/assets/e1ed2345-b750-4bb6-9c97-50961b76904f ---- All the todo's are in the following channel, so it's easier to work on this together: https://zed.dev/channel/zed-debugger-11370 If you are on Linux, you can use the following command to join the channel: ```cli zed https://zed.dev/channel/zed-debugger-11370 ``` ## Current Features - Collab - Breakpoints - Sync when you (re)join a project - Sync when you add/remove a breakpoint - Sync active debug line - Stack frames - Click on stack frame - View variables that belong to the stack frame - Visit the source file - Restart stack frame (if adapter supports this) - Variables - Loaded sources - Modules - Controls - Continue - Step back - Stepping granularity (configurable) - Step into - Stepping granularity (configurable) - Step over - Stepping granularity (configurable) - Step out - Stepping granularity (configurable) - Debug console - Breakpoints - Log breakpoints - line breakpoints - Persistent between zed sessions (configurable) - Multi buffer support - Toggle disable/enable all breakpoints - Stack frames - Click on stack frame - View variables that belong to the stack frame - Visit the source file - Show collapsed stack frames - Restart stack frame (if adapter supports this) - Loaded sources - View all used loaded sources if supported by adapter. - Modules - View all used modules (if adapter supports this) - Variables - Copy value - Copy name - Copy memory reference - Set value (if adapter supports this) - keyboard navigation - Debug Console - See logs - View output that was sent from debug adapter - Output grouping - Evaluate code - Updates the variable list - Auto completion - If not supported by adapter, we will show auto-completion for existing variables - Debug Terminal - Run custom commands and change env values right inside your Zed terminal - Attach to process (if adapter supports this) - Process picker - Controls - Continue - Step back - Stepping granularity (configurable) - Step into - Stepping granularity (configurable) - Step over - Stepping granularity (configurable) - Step out - Stepping granularity (configurable) - Disconnect - Restart - Stop - Warning when a debug session exited without hitting any breakpoint - Debug view to see Adapter/RPC log messages - Testing - Fake debug adapter - Fake requests & events --- Release Notes: - N/A --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Co-authored-by: Anthony Eid <hello@anthonyeid.me> Co-authored-by: Anthony <anthony@zed.dev> Co-authored-by: Piotr Osiewicz <peterosiewicz@gmail.com> Co-authored-by: Piotr <piotr@zed.dev>
This commit is contained in:
parent
ed4e654fdf
commit
41a60ffecf
156 changed files with 25840 additions and 451 deletions
419
crates/debugger_ui/src/session/running/console.rs
Normal file
419
crates/debugger_ui/src/session/running/console.rs
Normal file
|
@ -0,0 +1,419 @@
|
|||
use super::{
|
||||
stack_frame_list::{StackFrameList, StackFrameListEvent},
|
||||
variable_list::VariableList,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use dap::OutputEvent;
|
||||
use editor::{CompletionProvider, Editor, EditorElement, EditorStyle};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{Context, Entity, Render, Subscription, Task, TextStyle, WeakEntity};
|
||||
use language::{Buffer, CodeLabel};
|
||||
use menu::Confirm;
|
||||
use project::{
|
||||
debugger::session::{CompletionsQuery, OutputToken, Session},
|
||||
Completion,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::{cell::RefCell, rc::Rc, usize};
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
|
||||
pub struct Console {
|
||||
console: Entity<Editor>,
|
||||
query_bar: Entity<Editor>,
|
||||
session: Entity<Session>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
variable_list: Entity<VariableList>,
|
||||
stack_frame_list: Entity<StackFrameList>,
|
||||
last_token: OutputToken,
|
||||
update_output_task: Task<()>,
|
||||
}
|
||||
|
||||
impl Console {
|
||||
pub fn new(
|
||||
session: Entity<Session>,
|
||||
stack_frame_list: Entity<StackFrameList>,
|
||||
variable_list: Entity<VariableList>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let console = cx.new(|cx| {
|
||||
let mut editor = Editor::multi_line(window, cx);
|
||||
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_gutter(true, cx);
|
||||
editor.set_show_runnables(false, cx);
|
||||
editor.set_show_breakpoints(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_line_numbers(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_autoindent(false);
|
||||
editor.set_input_enabled(false);
|
||||
editor.set_use_autoclose(false);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_show_edit_predictions(Some(false), window, cx);
|
||||
editor
|
||||
});
|
||||
|
||||
let this = cx.weak_entity();
|
||||
let query_bar = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
editor.set_placeholder_text("Evaluate an expression", cx);
|
||||
editor.set_use_autoclose(false);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_completion_provider(Some(Box::new(ConsoleQueryBarCompletionProvider(this))));
|
||||
|
||||
editor
|
||||
});
|
||||
|
||||
let _subscriptions =
|
||||
vec![cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events)];
|
||||
|
||||
Self {
|
||||
session,
|
||||
console,
|
||||
query_bar,
|
||||
variable_list,
|
||||
_subscriptions,
|
||||
stack_frame_list,
|
||||
update_output_task: Task::ready(()),
|
||||
last_token: OutputToken(0),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn editor(&self) -> &Entity<Editor> {
|
||||
&self.console
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn query_bar(&self) -> &Entity<Editor> {
|
||||
&self.query_bar
|
||||
}
|
||||
|
||||
fn is_local(&self, cx: &Context<Self>) -> bool {
|
||||
self.session.read(cx).is_local()
|
||||
}
|
||||
|
||||
fn handle_stack_frame_list_events(
|
||||
&mut self,
|
||||
_: Entity<StackFrameList>,
|
||||
event: &StackFrameListEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
StackFrameListEvent::SelectedStackFrameChanged(_) => cx.notify(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_messages<'a>(
|
||||
&mut self,
|
||||
events: impl Iterator<Item = &'a OutputEvent>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.console.update(cx, |console, cx| {
|
||||
let mut to_insert = String::default();
|
||||
for event in events {
|
||||
use std::fmt::Write;
|
||||
|
||||
_ = write!(to_insert, "{}\n", event.output.trim_end());
|
||||
}
|
||||
|
||||
console.set_read_only(false);
|
||||
console.move_to_end(&editor::actions::MoveToEnd, window, cx);
|
||||
console.insert(&to_insert, window, cx);
|
||||
console.set_read_only(true);
|
||||
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let expression = self.query_bar.update(cx, |editor, cx| {
|
||||
let expression = editor.text(cx);
|
||||
|
||||
editor.clear(window, cx);
|
||||
|
||||
expression
|
||||
});
|
||||
|
||||
self.session.update(cx, |state, cx| {
|
||||
state.evaluate(
|
||||
expression,
|
||||
Some(dap::EvaluateArgumentsContext::Variables),
|
||||
self.stack_frame_list.read(cx).current_stack_frame_id(),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_console(&self, cx: &Context<Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: if self.console.read(cx).read_only(cx) {
|
||||
cx.theme().colors().text_disabled
|
||||
} else {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
&self.console,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: if self.console.read(cx).read_only(cx) {
|
||||
cx.theme().colors().text_disabled
|
||||
} else {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: TextSize::Editor.rems(cx).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: relative(1.3),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
&self.query_bar,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Console {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let session = self.session.clone();
|
||||
let token = self.last_token;
|
||||
self.update_output_task = cx.spawn_in(window, move |this, mut cx| async move {
|
||||
_ = session.update_in(&mut cx, move |session, window, cx| {
|
||||
let (output, last_processed_token) = session.output(token);
|
||||
|
||||
_ = this.update(cx, |this, cx| {
|
||||
if last_processed_token == this.last_token {
|
||||
return;
|
||||
}
|
||||
this.add_messages(output, window, cx);
|
||||
|
||||
this.last_token = last_processed_token;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
v_flex()
|
||||
.key_context("DebugConsole")
|
||||
.on_action(cx.listener(Self::evaluate))
|
||||
.size_full()
|
||||
.child(self.render_console(cx))
|
||||
.when(self.is_local(cx), |this| {
|
||||
this.child(self.render_query_bar(cx))
|
||||
.pt(DynamicSpacing::Base04.rems(cx))
|
||||
})
|
||||
.border_2()
|
||||
}
|
||||
}
|
||||
|
||||
struct ConsoleQueryBarCompletionProvider(WeakEntity<Console>);
|
||||
|
||||
impl CompletionProvider for ConsoleQueryBarCompletionProvider {
|
||||
fn completions(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
buffer_position: language::Anchor,
|
||||
_trigger: editor::CompletionContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Option<Vec<Completion>>>> {
|
||||
let Some(console) = self.0.upgrade() else {
|
||||
return Task::ready(Ok(None));
|
||||
};
|
||||
|
||||
let support_completions = console
|
||||
.read(cx)
|
||||
.session
|
||||
.read(cx)
|
||||
.capabilities()
|
||||
.supports_completions_request
|
||||
.unwrap_or_default();
|
||||
|
||||
if support_completions {
|
||||
self.client_completions(&console, buffer, buffer_position, cx)
|
||||
} else {
|
||||
self.variable_list_completions(&console, buffer, buffer_position, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_completions(
|
||||
&self,
|
||||
_buffer: Entity<Buffer>,
|
||||
_completion_indices: Vec<usize>,
|
||||
_completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
_cx: &mut Context<Editor>,
|
||||
) -> gpui::Task<gpui::Result<bool>> {
|
||||
Task::ready(Ok(false))
|
||||
}
|
||||
|
||||
fn apply_additional_edits_for_completion(
|
||||
&self,
|
||||
_buffer: Entity<Buffer>,
|
||||
_completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
_completion_index: usize,
|
||||
_push_to_history: bool,
|
||||
_cx: &mut Context<Editor>,
|
||||
) -> gpui::Task<gpui::Result<Option<language::Transaction>>> {
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
_buffer: &Entity<Buffer>,
|
||||
_position: language::Anchor,
|
||||
_text: &str,
|
||||
_trigger_in_words: bool,
|
||||
_cx: &mut Context<Editor>,
|
||||
) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl ConsoleQueryBarCompletionProvider {
|
||||
fn variable_list_completions(
|
||||
&self,
|
||||
console: &Entity<Console>,
|
||||
buffer: &Entity<Buffer>,
|
||||
buffer_position: language::Anchor,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Option<Vec<Completion>>>> {
|
||||
let (variables, string_matches) = console.update(cx, |console, cx| {
|
||||
let mut variables = HashMap::default();
|
||||
let mut string_matches = Vec::default();
|
||||
|
||||
for variable in console.variable_list.update(cx, |variable_list, cx| {
|
||||
variable_list.completion_variables(cx)
|
||||
}) {
|
||||
if let Some(evaluate_name) = &variable.evaluate_name {
|
||||
variables.insert(evaluate_name.clone(), variable.value.clone());
|
||||
string_matches.push(StringMatchCandidate {
|
||||
id: 0,
|
||||
string: evaluate_name.clone(),
|
||||
char_bag: evaluate_name.chars().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
variables.insert(variable.name.clone(), variable.value.clone());
|
||||
|
||||
string_matches.push(StringMatchCandidate {
|
||||
id: 0,
|
||||
string: variable.name.clone(),
|
||||
char_bag: variable.name.chars().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
(variables, string_matches)
|
||||
});
|
||||
|
||||
let query = buffer.read(cx).text();
|
||||
|
||||
cx.spawn(|_, cx| async move {
|
||||
let matches = fuzzy::match_strings(
|
||||
&string_matches,
|
||||
&query,
|
||||
true,
|
||||
10,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(Some(
|
||||
matches
|
||||
.iter()
|
||||
.filter_map(|string_match| {
|
||||
let variable_value = variables.get(&string_match.string)?;
|
||||
|
||||
Some(project::Completion {
|
||||
old_range: buffer_position..buffer_position,
|
||||
new_text: string_match.string.clone(),
|
||||
label: CodeLabel {
|
||||
filter_range: 0..string_match.string.len(),
|
||||
text: format!("{} {}", string_match.string.clone(), variable_value),
|
||||
runs: Vec::new(),
|
||||
},
|
||||
documentation: None,
|
||||
confirm: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn client_completions(
|
||||
&self,
|
||||
console: &Entity<Console>,
|
||||
buffer: &Entity<Buffer>,
|
||||
buffer_position: language::Anchor,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Option<Vec<Completion>>>> {
|
||||
let completion_task = console.update(cx, |console, cx| {
|
||||
console.session.update(cx, |state, cx| {
|
||||
let frame_id = console.stack_frame_list.read(cx).current_stack_frame_id();
|
||||
|
||||
state.completions(
|
||||
CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
cx.background_executor().spawn(async move {
|
||||
Ok(Some(
|
||||
completion_task
|
||||
.await?
|
||||
.iter()
|
||||
.map(|completion| project::Completion {
|
||||
old_range: buffer_position..buffer_position, // TODO(debugger): change this
|
||||
new_text: completion.text.clone().unwrap_or(completion.label.clone()),
|
||||
label: CodeLabel {
|
||||
filter_range: 0..completion.label.len(),
|
||||
text: completion.label.clone(),
|
||||
runs: Vec::new(),
|
||||
},
|
||||
documentation: None,
|
||||
confirm: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
103
crates/debugger_ui/src/session/running/loaded_source_list.rs
Normal file
103
crates/debugger_ui/src/session/running/loaded_source_list.rs
Normal file
|
@ -0,0 +1,103 @@
|
|||
use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription};
|
||||
use project::debugger::session::{Session, SessionEvent};
|
||||
use ui::prelude::*;
|
||||
use util::maybe;
|
||||
|
||||
pub struct LoadedSourceList {
|
||||
list: ListState,
|
||||
invalidate: bool,
|
||||
focus_handle: FocusHandle,
|
||||
_subscription: Subscription,
|
||||
session: Entity<Session>,
|
||||
}
|
||||
|
||||
impl LoadedSourceList {
|
||||
pub fn new(session: Entity<Session>, cx: &mut Context<Self>) -> Self {
|
||||
let weak_entity = cx.weak_entity();
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let list = ListState::new(
|
||||
0,
|
||||
gpui::ListAlignment::Top,
|
||||
px(1000.),
|
||||
move |ix, _window, cx| {
|
||||
weak_entity
|
||||
.upgrade()
|
||||
.map(|loaded_sources| {
|
||||
loaded_sources.update(cx, |this, cx| this.render_entry(ix, cx))
|
||||
})
|
||||
.unwrap_or(div().into_any())
|
||||
},
|
||||
);
|
||||
|
||||
let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
|
||||
SessionEvent::Stopped(_) | SessionEvent::LoadedSources => {
|
||||
this.invalidate = true;
|
||||
cx.notify();
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
Self {
|
||||
list,
|
||||
session,
|
||||
focus_handle,
|
||||
_subscription,
|
||||
invalidate: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
|
||||
let Some(source) = maybe!({
|
||||
self.session
|
||||
.update(cx, |state, cx| state.loaded_sources(cx).get(ix).cloned())
|
||||
}) else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.rounded_md()
|
||||
.w_full()
|
||||
.group("")
|
||||
.p_1()
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.text_ui_sm(cx)
|
||||
.when_some(source.name.clone(), |this, name| this.child(name)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.text_ui_xs(cx)
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.when_some(source.path.clone(), |this, path| this.child(path)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for LoadedSourceList {
|
||||
fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for LoadedSourceList {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if self.invalidate {
|
||||
let len = self
|
||||
.session
|
||||
.update(cx, |session, cx| session.loaded_sources(cx).len());
|
||||
self.list.reset(len);
|
||||
self.invalidate = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.p_1()
|
||||
.child(list(self.list.clone()).size_full())
|
||||
}
|
||||
}
|
183
crates/debugger_ui/src/session/running/module_list.rs
Normal file
183
crates/debugger_ui/src/session/running/module_list.rs
Normal file
|
@ -0,0 +1,183 @@
|
|||
use anyhow::anyhow;
|
||||
use gpui::{
|
||||
list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription, WeakEntity,
|
||||
};
|
||||
use project::{
|
||||
debugger::session::{Session, SessionEvent},
|
||||
ProjectItem as _, ProjectPath,
|
||||
};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use ui::prelude::*;
|
||||
use util::maybe;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct ModuleList {
|
||||
list: ListState,
|
||||
invalidate: bool,
|
||||
session: Entity<Session>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl ModuleList {
|
||||
pub fn new(
|
||||
session: Entity<Session>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let weak_entity = cx.weak_entity();
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let list = ListState::new(
|
||||
0,
|
||||
gpui::ListAlignment::Top,
|
||||
px(1000.),
|
||||
move |ix, _window, cx| {
|
||||
weak_entity
|
||||
.upgrade()
|
||||
.map(|module_list| module_list.update(cx, |this, cx| this.render_entry(ix, cx)))
|
||||
.unwrap_or(div().into_any())
|
||||
},
|
||||
);
|
||||
|
||||
let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
|
||||
SessionEvent::Stopped(_) | SessionEvent::Modules => {
|
||||
this.invalidate = true;
|
||||
cx.notify();
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
Self {
|
||||
list,
|
||||
session,
|
||||
workspace,
|
||||
focus_handle,
|
||||
_subscription,
|
||||
invalidate: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.spawn_in(window, move |this, mut cx| async move {
|
||||
let (worktree, relative_path) = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.workspace.update(cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |this, cx| {
|
||||
this.find_or_create_worktree(&path, false, cx)
|
||||
})
|
||||
})
|
||||
})??
|
||||
.await?;
|
||||
|
||||
let buffer = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.workspace.update(cx, |this, cx| {
|
||||
this.project().update(cx, |this, cx| {
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
this.open_buffer(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: relative_path.into(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
})??
|
||||
.await?;
|
||||
|
||||
this.update_in(&mut cx, |this, window, cx| {
|
||||
this.workspace.update(cx, |workspace, cx| {
|
||||
let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| {
|
||||
anyhow!("Could not select a stack frame for unnamed buffer")
|
||||
})?;
|
||||
anyhow::Ok(workspace.open_path_preview(
|
||||
project_path,
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
})???
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
|
||||
let Some(module) = maybe!({
|
||||
self.session
|
||||
.update(cx, |state, cx| state.modules(cx).get(ix).cloned())
|
||||
}) else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.rounded_md()
|
||||
.w_full()
|
||||
.group("")
|
||||
.id(("module-list", ix))
|
||||
.when(module.path.is_some(), |this| {
|
||||
this.on_click({
|
||||
let path = module.path.as_deref().map(|path| Arc::<Path>::from(Path::new(path)));
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
if let Some(path) = path.as_ref() {
|
||||
this.open_module(path.clone(), window, cx);
|
||||
} else {
|
||||
log::error!("Wasn't able to find module path, but was still able to click on module list entry");
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
.p_1()
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover))
|
||||
.child(h_flex().gap_0p5().text_ui_sm(cx).child(module.name.clone()))
|
||||
.child(
|
||||
h_flex()
|
||||
.text_ui_xs(cx)
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.when_some(module.path.clone(), |this, path| this.child(path)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl ModuleList {
|
||||
pub fn modules(&self, cx: &mut Context<Self>) -> Vec<dap::Module> {
|
||||
self.session
|
||||
.update(cx, |session, cx| session.modules(cx).to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for ModuleList {
|
||||
fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ModuleList {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if self.invalidate {
|
||||
let len = self
|
||||
.session
|
||||
.update(cx, |session, cx| session.modules(cx).len());
|
||||
self.list.reset(len);
|
||||
self.invalidate = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.p_1()
|
||||
.child(list(self.list.clone()).size_full())
|
||||
}
|
||||
}
|
519
crates/debugger_ui/src/session/running/stack_frame_list.rs
Normal file
519
crates/debugger_ui/src/session/running/stack_frame_list.rs
Normal file
|
@ -0,0 +1,519 @@
|
|||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use dap::StackFrameId;
|
||||
use gpui::{
|
||||
list, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, Subscription, Task,
|
||||
WeakEntity,
|
||||
};
|
||||
|
||||
use language::PointUtf16;
|
||||
use project::debugger::session::{Session, SessionEvent, StackFrame};
|
||||
use project::{ProjectItem, ProjectPath};
|
||||
use ui::{prelude::*, Tooltip};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
use super::RunningState;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StackFrameListEvent {
|
||||
SelectedStackFrameChanged(StackFrameId),
|
||||
}
|
||||
|
||||
pub struct StackFrameList {
|
||||
list: ListState,
|
||||
focus_handle: FocusHandle,
|
||||
_subscription: Subscription,
|
||||
session: Entity<Session>,
|
||||
state: WeakEntity<RunningState>,
|
||||
invalidate: bool,
|
||||
entries: Vec<StackFrameEntry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
current_stack_frame_id: Option<StackFrameId>,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum StackFrameEntry {
|
||||
Normal(dap::StackFrame),
|
||||
Collapsed(Vec<dap::StackFrame>),
|
||||
}
|
||||
|
||||
impl StackFrameList {
|
||||
pub fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
session: Entity<Session>,
|
||||
state: WeakEntity<RunningState>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let weak_entity = cx.weak_entity();
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let list = ListState::new(
|
||||
0,
|
||||
gpui::ListAlignment::Top,
|
||||
px(1000.),
|
||||
move |ix, _window, cx| {
|
||||
weak_entity
|
||||
.upgrade()
|
||||
.map(|stack_frame_list| {
|
||||
stack_frame_list.update(cx, |this, cx| this.render_entry(ix, cx))
|
||||
})
|
||||
.unwrap_or(div().into_any())
|
||||
},
|
||||
);
|
||||
|
||||
let _subscription =
|
||||
cx.subscribe_in(&session, window, |this, _, event, _, cx| match event {
|
||||
SessionEvent::Stopped(_) | SessionEvent::StackTrace | SessionEvent::Threads => {
|
||||
this.refresh(cx);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
Self {
|
||||
list,
|
||||
session,
|
||||
workspace,
|
||||
focus_handle,
|
||||
state,
|
||||
_subscription,
|
||||
invalidate: true,
|
||||
entries: Default::default(),
|
||||
current_stack_frame_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn entries(&self) -> &Vec<StackFrameEntry> {
|
||||
&self.entries
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn flatten_entries(&self) -> Vec<dap::StackFrame> {
|
||||
self.entries
|
||||
.iter()
|
||||
.flat_map(|frame| match frame {
|
||||
StackFrameEntry::Normal(frame) => vec![frame.clone()],
|
||||
StackFrameEntry::Collapsed(frames) => frames.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn stack_frames(&self, cx: &mut App) -> Vec<StackFrame> {
|
||||
self.state
|
||||
.read_with(cx, |state, _| state.thread_id)
|
||||
.log_err()
|
||||
.flatten()
|
||||
.map(|thread_id| {
|
||||
self.session
|
||||
.update(cx, |this, cx| this.stack_frames(thread_id, cx))
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
|
||||
self.stack_frames(cx)
|
||||
.into_iter()
|
||||
.map(|stack_frame| stack_frame.dap.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn _get_main_stack_frame_id(&self, cx: &mut Context<Self>) -> u64 {
|
||||
self.stack_frames(cx)
|
||||
.first()
|
||||
.map(|stack_frame| stack_frame.dap.id)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn current_stack_frame_id(&self) -> Option<StackFrameId> {
|
||||
self.current_stack_frame_id
|
||||
}
|
||||
|
||||
pub(super) fn refresh(&mut self, cx: &mut Context<Self>) {
|
||||
self.invalidate = true;
|
||||
self.entries.clear();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn build_entries(
|
||||
&mut self,
|
||||
select_first_stack_frame: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut entries = Vec::new();
|
||||
let mut collapsed_entries = Vec::new();
|
||||
let mut current_stack_frame = None;
|
||||
|
||||
let stack_frames = self.stack_frames(cx);
|
||||
for stack_frame in &stack_frames {
|
||||
match stack_frame.dap.presentation_hint {
|
||||
Some(dap::StackFramePresentationHint::Deemphasize) => {
|
||||
collapsed_entries.push(stack_frame.dap.clone());
|
||||
}
|
||||
_ => {
|
||||
let collapsed_entries = std::mem::take(&mut collapsed_entries);
|
||||
if !collapsed_entries.is_empty() {
|
||||
entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
|
||||
}
|
||||
|
||||
current_stack_frame.get_or_insert(&stack_frame.dap);
|
||||
entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let collapsed_entries = std::mem::take(&mut collapsed_entries);
|
||||
if !collapsed_entries.is_empty() {
|
||||
entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
|
||||
}
|
||||
|
||||
std::mem::swap(&mut self.entries, &mut entries);
|
||||
self.list.reset(self.entries.len());
|
||||
|
||||
if let Some(current_stack_frame) = current_stack_frame.filter(|_| select_first_stack_frame)
|
||||
{
|
||||
self.select_stack_frame(current_stack_frame, true, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn go_to_selected_stack_frame(&mut self, window: &Window, cx: &mut Context<Self>) {
|
||||
if let Some(current_stack_frame_id) = self.current_stack_frame_id {
|
||||
let frame = self
|
||||
.entries
|
||||
.iter()
|
||||
.find_map(|entry| match entry {
|
||||
StackFrameEntry::Normal(dap) => {
|
||||
if dap.id == current_stack_frame_id {
|
||||
Some(dap)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
StackFrameEntry::Collapsed(daps) => {
|
||||
daps.iter().find(|dap| dap.id == current_stack_frame_id)
|
||||
}
|
||||
})
|
||||
.cloned();
|
||||
|
||||
if let Some(frame) = frame.as_ref() {
|
||||
self.select_stack_frame(frame, true, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_stack_frame(
|
||||
&mut self,
|
||||
stack_frame: &dap::StackFrame,
|
||||
go_to_stack_frame: bool,
|
||||
window: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.current_stack_frame_id = Some(stack_frame.id);
|
||||
|
||||
cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
|
||||
stack_frame.id,
|
||||
));
|
||||
cx.notify();
|
||||
|
||||
if !go_to_stack_frame {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
|
||||
let row = (stack_frame.line.saturating_sub(1)) as u32;
|
||||
|
||||
let Some(abs_path) = self.abs_path_from_stack_frame(&stack_frame) else {
|
||||
return Task::ready(Err(anyhow!("Project path not found")));
|
||||
};
|
||||
|
||||
cx.spawn_in(window, move |this, mut cx| async move {
|
||||
let (worktree, relative_path) = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.workspace.update(cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |this, cx| {
|
||||
this.find_or_create_worktree(&abs_path, false, cx)
|
||||
})
|
||||
})
|
||||
})??
|
||||
.await?;
|
||||
let buffer = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.workspace.update(cx, |this, cx| {
|
||||
this.project().update(cx, |this, cx| {
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
this.open_buffer(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: relative_path.into(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
})??
|
||||
.await?;
|
||||
let position = buffer.update(&mut cx, |this, _| {
|
||||
this.snapshot().anchor_after(PointUtf16::new(row, 0))
|
||||
})?;
|
||||
this.update_in(&mut cx, |this, window, cx| {
|
||||
this.workspace.update(cx, |workspace, cx| {
|
||||
let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| {
|
||||
anyhow!("Could not select a stack frame for unnamed buffer")
|
||||
})?;
|
||||
anyhow::Ok(workspace.open_path_preview(
|
||||
project_path,
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
})???
|
||||
.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.workspace.update(cx, |workspace, cx| {
|
||||
let breakpoint_store = workspace.project().read(cx).breakpoint_store();
|
||||
|
||||
breakpoint_store.update(cx, |store, cx| {
|
||||
store.set_active_position(
|
||||
(this.session.read(cx).session_id(), abs_path, position),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
})?
|
||||
})
|
||||
}
|
||||
|
||||
fn abs_path_from_stack_frame(&self, stack_frame: &dap::StackFrame) -> Option<Arc<Path>> {
|
||||
stack_frame.source.as_ref().and_then(|s| {
|
||||
s.path
|
||||
.as_deref()
|
||||
.map(|path| Arc::<Path>::from(Path::new(path)))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut Context<Self>) {
|
||||
self.session.update(cx, |state, cx| {
|
||||
state.restart_stack_frame(stack_frame_id, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn render_normal_entry(
|
||||
&self,
|
||||
stack_frame: &dap::StackFrame,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let source = stack_frame.source.clone();
|
||||
let is_selected_frame = Some(stack_frame.id) == self.current_stack_frame_id;
|
||||
|
||||
let formatted_path = format!(
|
||||
"{}:{}",
|
||||
source.clone().and_then(|s| s.name).unwrap_or_default(),
|
||||
stack_frame.line,
|
||||
);
|
||||
|
||||
let supports_frame_restart = self
|
||||
.session
|
||||
.read(cx)
|
||||
.capabilities()
|
||||
.supports_restart_frame
|
||||
.unwrap_or_default();
|
||||
|
||||
let origin = stack_frame
|
||||
.source
|
||||
.to_owned()
|
||||
.and_then(|source| source.origin);
|
||||
|
||||
h_flex()
|
||||
.rounded_md()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.group("")
|
||||
.id(("stack-frame", stack_frame.id))
|
||||
.tooltip({
|
||||
let formatted_path = formatted_path.clone();
|
||||
move |_window, app| {
|
||||
app.new(|_| {
|
||||
let mut tooltip = Tooltip::new(formatted_path.clone());
|
||||
|
||||
if let Some(origin) = &origin {
|
||||
tooltip = tooltip.meta(origin);
|
||||
}
|
||||
|
||||
tooltip
|
||||
})
|
||||
.into()
|
||||
}
|
||||
})
|
||||
.p_1()
|
||||
.when(is_selected_frame, |this| {
|
||||
this.bg(cx.theme().colors().element_hover)
|
||||
})
|
||||
.on_click(cx.listener({
|
||||
let stack_frame = stack_frame.clone();
|
||||
move |this, _, window, cx| {
|
||||
this.select_stack_frame(&stack_frame, true, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}))
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.text_ui_sm(cx)
|
||||
.truncate()
|
||||
.child(stack_frame.name.clone())
|
||||
.child(formatted_path),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.text_ui_xs(cx)
|
||||
.truncate()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.when_some(source.and_then(|s| s.path), |this, path| this.child(path)),
|
||||
),
|
||||
)
|
||||
.when(
|
||||
supports_frame_restart && stack_frame.can_restart.unwrap_or(true),
|
||||
|this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.id(("restart-stack-frame", stack_frame.id))
|
||||
.visible_on_hover("")
|
||||
.absolute()
|
||||
.right_2()
|
||||
.overflow_hidden()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().element_selected)
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.hover(|style| {
|
||||
style
|
||||
.bg(cx.theme().colors().ghost_element_hover)
|
||||
.cursor_pointer()
|
||||
})
|
||||
.child(
|
||||
IconButton::new(
|
||||
("restart-stack-frame", stack_frame.id),
|
||||
IconName::DebugRestart,
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener({
|
||||
let stack_frame_id = stack_frame.id;
|
||||
move |this, _, _window, cx| {
|
||||
this.restart_stack_frame(stack_frame_id, cx);
|
||||
}
|
||||
}))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Restart Stack Frame")(window, cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
pub fn expand_collapsed_entry(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
stack_frames: &Vec<dap::StackFrame>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.entries.splice(
|
||||
ix..ix + 1,
|
||||
stack_frames
|
||||
.iter()
|
||||
.map(|frame| StackFrameEntry::Normal(frame.clone())),
|
||||
);
|
||||
self.list.reset(self.entries.len());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_collapsed_entry(
|
||||
&self,
|
||||
ix: usize,
|
||||
stack_frames: &Vec<dap::StackFrame>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let first_stack_frame = &stack_frames[0];
|
||||
|
||||
h_flex()
|
||||
.rounded_md()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.group("")
|
||||
.id(("stack-frame", first_stack_frame.id))
|
||||
.p_1()
|
||||
.on_click(cx.listener({
|
||||
let stack_frames = stack_frames.clone();
|
||||
move |this, _, _window, cx| {
|
||||
this.expand_collapsed_entry(ix, &stack_frames, cx);
|
||||
}
|
||||
}))
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
|
||||
.child(
|
||||
v_flex()
|
||||
.text_ui_sm(cx)
|
||||
.truncate()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(format!(
|
||||
"Show {} more{}",
|
||||
stack_frames.len(),
|
||||
first_stack_frame
|
||||
.source
|
||||
.as_ref()
|
||||
.and_then(|source| source.origin.as_ref())
|
||||
.map_or(String::new(), |origin| format!(": {}", origin))
|
||||
)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
|
||||
match &self.entries[ix] {
|
||||
StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(stack_frame, cx),
|
||||
StackFrameEntry::Collapsed(stack_frames) => {
|
||||
self.render_collapsed_entry(ix, stack_frames, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for StackFrameList {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if self.invalidate {
|
||||
self.build_entries(self.entries.is_empty(), window, cx);
|
||||
self.invalidate = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.p_1()
|
||||
.child(list(self.list.clone()).size_full())
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for StackFrameList {
|
||||
fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<StackFrameListEvent> for StackFrameList {}
|
946
crates/debugger_ui/src/session/running/variable_list.rs
Normal file
946
crates/debugger_ui/src/session/running/variable_list.rs
Normal file
|
@ -0,0 +1,946 @@
|
|||
use super::stack_frame_list::{StackFrameList, StackFrameListEvent};
|
||||
use dap::{ScopePresentationHint, StackFrameId, VariablePresentationHintKind, VariableReference};
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
actions, anchored, deferred, uniform_list, AnyElement, ClickEvent, ClipboardItem, Context,
|
||||
DismissEvent, Entity, FocusHandle, Focusable, Hsla, MouseButton, MouseDownEvent, Point,
|
||||
Stateful, Subscription, TextStyleRefinement, UniformListScrollHandle,
|
||||
};
|
||||
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
|
||||
use project::debugger::session::{Session, SessionEvent};
|
||||
use std::{collections::HashMap, ops::Range, sync::Arc};
|
||||
use ui::{prelude::*, ContextMenu, ListItem, Scrollbar, ScrollbarState};
|
||||
use util::{debug_panic, maybe};
|
||||
|
||||
actions!(variable_list, [ExpandSelectedEntry, CollapseSelectedEntry]);
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct EntryState {
|
||||
depth: usize,
|
||||
is_expanded: bool,
|
||||
parent_reference: VariableReference,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||
pub(crate) struct EntryPath {
|
||||
pub leaf_name: Option<SharedString>,
|
||||
pub indices: Arc<[SharedString]>,
|
||||
}
|
||||
|
||||
impl EntryPath {
|
||||
fn for_scope(scope_name: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
leaf_name: Some(scope_name.into()),
|
||||
indices: Arc::new([]),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_name(&self, name: SharedString) -> Self {
|
||||
Self {
|
||||
leaf_name: Some(name),
|
||||
indices: self.indices.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new child of this variable path
|
||||
fn with_child(&self, name: SharedString) -> Self {
|
||||
Self {
|
||||
leaf_name: None,
|
||||
indices: self
|
||||
.indices
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(std::iter::once(name))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum EntryKind {
|
||||
Variable(dap::Variable),
|
||||
Scope(dap::Scope),
|
||||
}
|
||||
|
||||
impl EntryKind {
|
||||
fn as_variable(&self) -> Option<&dap::Variable> {
|
||||
match self {
|
||||
EntryKind::Variable(dap) => Some(dap),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_scope(&self) -> Option<&dap::Scope> {
|
||||
match self {
|
||||
EntryKind::Scope(dap) => Some(dap),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn name(&self) -> &str {
|
||||
match self {
|
||||
EntryKind::Variable(dap) => &dap.name,
|
||||
EntryKind::Scope(dap) => &dap.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct ListEntry {
|
||||
dap_kind: EntryKind,
|
||||
path: EntryPath,
|
||||
}
|
||||
|
||||
impl ListEntry {
|
||||
fn as_variable(&self) -> Option<&dap::Variable> {
|
||||
self.dap_kind.as_variable()
|
||||
}
|
||||
|
||||
fn as_scope(&self) -> Option<&dap::Scope> {
|
||||
self.dap_kind.as_scope()
|
||||
}
|
||||
|
||||
fn item_id(&self) -> ElementId {
|
||||
use std::fmt::Write;
|
||||
let mut id = match &self.dap_kind {
|
||||
EntryKind::Variable(dap) => format!("variable-{}", dap.name),
|
||||
EntryKind::Scope(dap) => format!("scope-{}", dap.name),
|
||||
};
|
||||
for name in self.path.indices.iter() {
|
||||
_ = write!(id, "-{}", name);
|
||||
}
|
||||
SharedString::from(id).into()
|
||||
}
|
||||
|
||||
fn item_value_id(&self) -> ElementId {
|
||||
use std::fmt::Write;
|
||||
let mut id = match &self.dap_kind {
|
||||
EntryKind::Variable(dap) => format!("variable-{}", dap.name),
|
||||
EntryKind::Scope(dap) => format!("scope-{}", dap.name),
|
||||
};
|
||||
for name in self.path.indices.iter() {
|
||||
_ = write!(id, "-{}", name);
|
||||
}
|
||||
_ = write!(id, "-value");
|
||||
SharedString::from(id).into()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VariableList {
|
||||
entries: Vec<ListEntry>,
|
||||
entry_states: HashMap<EntryPath, EntryState>,
|
||||
selected_stack_frame_id: Option<StackFrameId>,
|
||||
list_handle: UniformListScrollHandle,
|
||||
scrollbar_state: ScrollbarState,
|
||||
session: Entity<Session>,
|
||||
selection: Option<EntryPath>,
|
||||
open_context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
focus_handle: FocusHandle,
|
||||
edited_path: Option<(EntryPath, Entity<Editor>)>,
|
||||
disabled: bool,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl VariableList {
|
||||
pub fn new(
|
||||
session: Entity<Session>,
|
||||
stack_frame_list: Entity<StackFrameList>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let _subscriptions = vec![
|
||||
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
|
||||
cx.subscribe(&session, |this, _, event, _| match event {
|
||||
SessionEvent::Stopped(_) => {
|
||||
this.selection.take();
|
||||
this.edited_path.take();
|
||||
this.selected_stack_frame_id.take();
|
||||
}
|
||||
_ => {}
|
||||
}),
|
||||
cx.on_focus_out(&focus_handle, window, |this, _, _, cx| {
|
||||
this.edited_path.take();
|
||||
cx.notify();
|
||||
}),
|
||||
];
|
||||
|
||||
let list_state = UniformListScrollHandle::default();
|
||||
|
||||
Self {
|
||||
scrollbar_state: ScrollbarState::new(list_state.clone()),
|
||||
list_handle: list_state,
|
||||
session,
|
||||
focus_handle,
|
||||
_subscriptions,
|
||||
selected_stack_frame_id: None,
|
||||
selection: None,
|
||||
open_context_menu: None,
|
||||
disabled: false,
|
||||
edited_path: None,
|
||||
entries: Default::default(),
|
||||
entry_states: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
|
||||
let old_disabled = std::mem::take(&mut self.disabled);
|
||||
self.disabled = disabled;
|
||||
if old_disabled != disabled {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn build_entries(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(stack_frame_id) = self.selected_stack_frame_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut entries = vec![];
|
||||
let scopes: Vec<_> = self.session.update(cx, |session, cx| {
|
||||
session.scopes(stack_frame_id, cx).iter().cloned().collect()
|
||||
});
|
||||
|
||||
let mut contains_local_scope = false;
|
||||
|
||||
let mut stack = scopes
|
||||
.into_iter()
|
||||
.rev()
|
||||
.filter(|scope| {
|
||||
if scope
|
||||
.presentation_hint
|
||||
.as_ref()
|
||||
.map(|hint| *hint == ScopePresentationHint::Locals)
|
||||
.unwrap_or(scope.name.to_lowercase().starts_with("local"))
|
||||
{
|
||||
contains_local_scope = true;
|
||||
}
|
||||
|
||||
self.session.update(cx, |session, cx| {
|
||||
session.variables(scope.variables_reference, cx).len() > 0
|
||||
})
|
||||
})
|
||||
.map(|scope| {
|
||||
(
|
||||
scope.variables_reference,
|
||||
scope.variables_reference,
|
||||
EntryPath::for_scope(&scope.name),
|
||||
EntryKind::Scope(scope),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let scopes_count = stack.len();
|
||||
|
||||
while let Some((container_reference, variables_reference, mut path, dap_kind)) = stack.pop()
|
||||
{
|
||||
match &dap_kind {
|
||||
EntryKind::Variable(dap) => path = path.with_name(dap.name.clone().into()),
|
||||
EntryKind::Scope(dap) => path = path.with_child(dap.name.clone().into()),
|
||||
}
|
||||
|
||||
let var_state = self
|
||||
.entry_states
|
||||
.entry(path.clone())
|
||||
.and_modify(|state| {
|
||||
state.parent_reference = container_reference;
|
||||
})
|
||||
.or_insert(EntryState {
|
||||
depth: path.indices.len(),
|
||||
is_expanded: dap_kind.as_scope().is_some_and(|scope| {
|
||||
(scopes_count == 1 && !contains_local_scope)
|
||||
|| scope
|
||||
.presentation_hint
|
||||
.as_ref()
|
||||
.map(|hint| *hint == ScopePresentationHint::Locals)
|
||||
.unwrap_or(scope.name.to_lowercase().starts_with("local"))
|
||||
}),
|
||||
parent_reference: container_reference,
|
||||
});
|
||||
|
||||
entries.push(ListEntry {
|
||||
dap_kind,
|
||||
path: path.clone(),
|
||||
});
|
||||
|
||||
if var_state.is_expanded {
|
||||
let children = self
|
||||
.session
|
||||
.update(cx, |session, cx| session.variables(variables_reference, cx));
|
||||
stack.extend(children.into_iter().rev().map(|child| {
|
||||
(
|
||||
variables_reference,
|
||||
child.variables_reference,
|
||||
path.with_child(child.name.clone().into()),
|
||||
EntryKind::Variable(child),
|
||||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
self.entries = entries;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle_stack_frame_list_events(
|
||||
&mut self,
|
||||
_: Entity<StackFrameList>,
|
||||
event: &StackFrameListEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
StackFrameListEvent::SelectedStackFrameChanged(stack_frame_id) => {
|
||||
self.selected_stack_frame_id = Some(*stack_frame_id);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn completion_variables(&self, _cx: &mut Context<Self>) -> Vec<dap::Variable> {
|
||||
self.entries
|
||||
.iter()
|
||||
.filter_map(|entry| match &entry.dap_kind {
|
||||
EntryKind::Variable(dap) => Some(dap.clone()),
|
||||
EntryKind::Scope(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_entries(
|
||||
&mut self,
|
||||
ix: Range<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<AnyElement> {
|
||||
ix.into_iter()
|
||||
.filter_map(|ix| {
|
||||
let (entry, state) = self
|
||||
.entries
|
||||
.get(ix)
|
||||
.and_then(|entry| Some(entry).zip(self.entry_states.get(&entry.path)))?;
|
||||
|
||||
match &entry.dap_kind {
|
||||
EntryKind::Variable(_) => Some(self.render_variable(entry, *state, window, cx)),
|
||||
EntryKind::Scope(_) => Some(self.render_scope(entry, *state, cx)),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_entry(&mut self, var_path: &EntryPath, cx: &mut Context<Self>) {
|
||||
let Some(entry) = self.entry_states.get_mut(var_path) else {
|
||||
log::error!("Could not find variable list entry state to toggle");
|
||||
return;
|
||||
};
|
||||
|
||||
entry.is_expanded = !entry.is_expanded;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.cancel_variable_edit(&Default::default(), window, cx);
|
||||
if let Some(variable) = self.entries.first() {
|
||||
self.selection = Some(variable.path.clone());
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.cancel_variable_edit(&Default::default(), window, cx);
|
||||
if let Some(variable) = self.entries.last() {
|
||||
self.selection = Some(variable.path.clone());
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.cancel_variable_edit(&Default::default(), window, cx);
|
||||
if let Some(selection) = &self.selection {
|
||||
if let Some(var_ix) = self.entries.iter().enumerate().find_map(|(ix, var)| {
|
||||
if &var.path == selection {
|
||||
Some(ix.saturating_sub(1))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
if let Some(new_selection) = self.entries.get(var_ix).map(|var| var.path.clone()) {
|
||||
self.selection = Some(new_selection);
|
||||
cx.notify();
|
||||
} else {
|
||||
self.select_first(&SelectFirst, window, cx);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.select_first(&SelectFirst, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.cancel_variable_edit(&Default::default(), window, cx);
|
||||
if let Some(selection) = &self.selection {
|
||||
if let Some(var_ix) = self.entries.iter().enumerate().find_map(|(ix, var)| {
|
||||
if &var.path == selection {
|
||||
Some(ix.saturating_add(1))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
if let Some(new_selection) = self.entries.get(var_ix).map(|var| var.path.clone()) {
|
||||
self.selection = Some(new_selection);
|
||||
cx.notify();
|
||||
} else {
|
||||
self.select_first(&SelectFirst, window, cx);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.select_first(&SelectFirst, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_variable_edit(
|
||||
&mut self,
|
||||
_: &menu::Cancel,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.edited_path.take();
|
||||
self.focus_handle.focus(window);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm_variable_edit(
|
||||
&mut self,
|
||||
_: &menu::Confirm,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let res = maybe!({
|
||||
let (var_path, editor) = self.edited_path.take()?;
|
||||
let state = self.entry_states.get(&var_path)?;
|
||||
let variables_reference = state.parent_reference;
|
||||
let name = var_path.leaf_name?;
|
||||
let value = editor.read(cx).text(cx);
|
||||
|
||||
self.session.update(cx, |session, cx| {
|
||||
session.set_variable_value(variables_reference, name.into(), value, cx)
|
||||
});
|
||||
Some(())
|
||||
});
|
||||
|
||||
if res.is_none() {
|
||||
log::error!("Couldn't confirm variable edit because variable doesn't have a leaf name or a parent reference id");
|
||||
}
|
||||
}
|
||||
|
||||
fn collapse_selected_entry(
|
||||
&mut self,
|
||||
_: &CollapseSelectedEntry,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(ref selected_entry) = self.selection {
|
||||
let Some(entry_state) = self.entry_states.get_mut(selected_entry) else {
|
||||
debug_panic!("Trying to toggle variable in variable list that has an no state");
|
||||
return;
|
||||
};
|
||||
|
||||
entry_state.is_expanded = false;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_selected_entry(
|
||||
&mut self,
|
||||
_: &ExpandSelectedEntry,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(ref selected_entry) = self.selection {
|
||||
let Some(entry_state) = self.entry_states.get_mut(selected_entry) else {
|
||||
debug_panic!("Trying to toggle variable in variable list that has an no state");
|
||||
return;
|
||||
};
|
||||
|
||||
entry_state.is_expanded = true;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn deploy_variable_context_menu(
|
||||
&mut self,
|
||||
variable: ListEntry,
|
||||
position: Point<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(dap_var) = variable.as_variable() else {
|
||||
debug_panic!("Trying to open variable context menu on a scope");
|
||||
return;
|
||||
};
|
||||
|
||||
let variable_value = dap_var.value.clone();
|
||||
let variable_name = dap_var.name.clone();
|
||||
let this = cx.entity().clone();
|
||||
|
||||
let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
|
||||
menu.entry("Copy name", None, move |_, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(variable_name.clone()))
|
||||
})
|
||||
.entry("Copy value", None, {
|
||||
let variable_value = variable_value.clone();
|
||||
move |_, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(variable_value.clone()))
|
||||
}
|
||||
})
|
||||
.entry("Set value", None, move |window, cx| {
|
||||
this.update(cx, |variable_list, cx| {
|
||||
let editor = Self::create_variable_editor(&variable_value, window, cx);
|
||||
variable_list.edited_path = Some((variable.path.clone(), editor));
|
||||
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
cx.focus_view(&context_menu, window);
|
||||
let subscription = cx.subscribe_in(
|
||||
&context_menu,
|
||||
window,
|
||||
|this, _, _: &DismissEvent, window, cx| {
|
||||
if this.open_context_menu.as_ref().is_some_and(|context_menu| {
|
||||
context_menu.0.focus_handle(cx).contains_focused(window, cx)
|
||||
}) {
|
||||
cx.focus_self(window);
|
||||
}
|
||||
this.open_context_menu.take();
|
||||
cx.notify();
|
||||
},
|
||||
);
|
||||
|
||||
self.open_context_menu = Some((context_menu, position, subscription));
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn assert_visual_entries(&self, expected: Vec<&str>) {
|
||||
const INDENT: &'static str = " ";
|
||||
|
||||
let entries = &self.entries;
|
||||
let mut visual_entries = Vec::with_capacity(entries.len());
|
||||
for entry in entries {
|
||||
let state = self
|
||||
.entry_states
|
||||
.get(&entry.path)
|
||||
.expect("If there's a variable entry there has to be a state that goes with it");
|
||||
|
||||
visual_entries.push(format!(
|
||||
"{}{} {}{}",
|
||||
INDENT.repeat(state.depth - 1),
|
||||
if state.is_expanded { "v" } else { ">" },
|
||||
entry.dap_kind.name(),
|
||||
if self.selection.as_ref() == Some(&entry.path) {
|
||||
" <=== selected"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
pretty_assertions::assert_eq!(expected, visual_entries);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn scopes(&self) -> Vec<dap::Scope> {
|
||||
self.entries
|
||||
.iter()
|
||||
.filter_map(|entry| match &entry.dap_kind {
|
||||
EntryKind::Scope(scope) => Some(scope),
|
||||
_ => None,
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn variables_per_scope(&self) -> Vec<(dap::Scope, Vec<dap::Variable>)> {
|
||||
let mut scopes: Vec<(dap::Scope, Vec<_>)> = Vec::new();
|
||||
let mut idx = 0;
|
||||
|
||||
for entry in self.entries.iter() {
|
||||
match &entry.dap_kind {
|
||||
EntryKind::Variable(dap) => scopes[idx].1.push(dap.clone()),
|
||||
EntryKind::Scope(scope) => {
|
||||
if scopes.len() > 0 {
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
scopes.push((scope.clone(), Vec::new()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scopes
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn variables(&self) -> Vec<dap::Variable> {
|
||||
self.entries
|
||||
.iter()
|
||||
.filter_map(|entry| match &entry.dap_kind {
|
||||
EntryKind::Variable(variable) => Some(variable),
|
||||
_ => None,
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn create_variable_editor(default: &str, window: &mut Window, cx: &mut App) -> Entity<Editor> {
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
|
||||
let refinement = TextStyleRefinement {
|
||||
font_size: Some(
|
||||
TextSize::XSmall
|
||||
.rems(cx)
|
||||
.to_pixels(window.rem_size())
|
||||
.into(),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
editor.set_text_style_refinement(refinement);
|
||||
editor.set_text(default, window, cx);
|
||||
editor.select_all(&editor::actions::SelectAll, window, cx);
|
||||
editor
|
||||
});
|
||||
editor.focus_handle(cx).focus(window);
|
||||
editor
|
||||
}
|
||||
|
||||
fn render_scope(
|
||||
&self,
|
||||
entry: &ListEntry,
|
||||
state: EntryState,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let Some(scope) = entry.as_scope() else {
|
||||
debug_panic!("Called render scope on non scope variable list entry variant");
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let var_ref = scope.variables_reference;
|
||||
let is_selected = self
|
||||
.selection
|
||||
.as_ref()
|
||||
.is_some_and(|selection| selection == &entry.path);
|
||||
|
||||
let colors = get_entry_color(cx);
|
||||
let bg_hover_color = if !is_selected {
|
||||
colors.hover
|
||||
} else {
|
||||
colors.default
|
||||
};
|
||||
let border_color = if is_selected {
|
||||
colors.marked_active
|
||||
} else {
|
||||
colors.default
|
||||
};
|
||||
|
||||
div()
|
||||
.id(var_ref as usize)
|
||||
.group("variable_list_entry")
|
||||
.border_1()
|
||||
.border_r_2()
|
||||
.border_color(border_color)
|
||||
.flex()
|
||||
.w_full()
|
||||
.h_full()
|
||||
.hover(|style| style.bg(bg_hover_color))
|
||||
.on_click(cx.listener({
|
||||
move |_this, _, _window, cx| {
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
ListItem::new(SharedString::from(format!("scope-{}", var_ref)))
|
||||
.selectable(false)
|
||||
.indent_level(state.depth + 1)
|
||||
.indent_step_size(px(20.))
|
||||
.always_show_disclosure_icon(true)
|
||||
.toggle(state.is_expanded)
|
||||
.on_toggle({
|
||||
let var_path = entry.path.clone();
|
||||
cx.listener(move |this, _, _, cx| this.toggle_entry(&var_path, cx))
|
||||
})
|
||||
.child(div().text_ui(cx).w_full().child(scope.name.clone())),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_variable(
|
||||
&self,
|
||||
variable: &ListEntry,
|
||||
state: EntryState,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let dap = match &variable.dap_kind {
|
||||
EntryKind::Variable(dap) => dap,
|
||||
EntryKind::Scope(_) => {
|
||||
debug_panic!("Called render variable on variable list entry kind scope");
|
||||
return div().into_any_element();
|
||||
}
|
||||
};
|
||||
|
||||
let syntax_color_for = |name| cx.theme().syntax().get(name).color;
|
||||
let variable_name_color = match &dap
|
||||
.presentation_hint
|
||||
.as_ref()
|
||||
.and_then(|hint| hint.kind.as_ref())
|
||||
.unwrap_or(&VariablePresentationHintKind::Unknown)
|
||||
{
|
||||
VariablePresentationHintKind::Class
|
||||
| VariablePresentationHintKind::BaseClass
|
||||
| VariablePresentationHintKind::InnerClass
|
||||
| VariablePresentationHintKind::MostDerivedClass => syntax_color_for("type"),
|
||||
VariablePresentationHintKind::Data => syntax_color_for("variable"),
|
||||
VariablePresentationHintKind::Unknown | _ => syntax_color_for("variable"),
|
||||
};
|
||||
let variable_color = syntax_color_for("variable.special");
|
||||
|
||||
let var_ref = dap.variables_reference;
|
||||
let colors = get_entry_color(cx);
|
||||
let is_selected = self
|
||||
.selection
|
||||
.as_ref()
|
||||
.is_some_and(|selected_path| *selected_path == variable.path);
|
||||
|
||||
let bg_hover_color = if !is_selected {
|
||||
colors.hover
|
||||
} else {
|
||||
colors.default
|
||||
};
|
||||
let border_color = if is_selected && self.focus_handle.contains_focused(window, cx) {
|
||||
colors.marked_active
|
||||
} else {
|
||||
colors.default
|
||||
};
|
||||
let path = variable.path.clone();
|
||||
div()
|
||||
.id(variable.item_id())
|
||||
.group("variable_list_entry")
|
||||
.border_1()
|
||||
.border_r_2()
|
||||
.border_color(border_color)
|
||||
.h_4()
|
||||
.size_full()
|
||||
.hover(|style| style.bg(bg_hover_color))
|
||||
.on_click(cx.listener({
|
||||
move |this, _, _window, cx| {
|
||||
this.selection = Some(path.clone());
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
ListItem::new(SharedString::from(format!(
|
||||
"variable-item-{}-{}",
|
||||
dap.name, state.depth
|
||||
)))
|
||||
.disabled(self.disabled)
|
||||
.selectable(false)
|
||||
.indent_level(state.depth + 1_usize)
|
||||
.indent_step_size(px(20.))
|
||||
.always_show_disclosure_icon(true)
|
||||
.when(var_ref > 0, |list_item| {
|
||||
list_item.toggle(state.is_expanded).on_toggle(cx.listener({
|
||||
let var_path = variable.path.clone();
|
||||
move |this, _, _, cx| {
|
||||
this.session.update(cx, |session, cx| {
|
||||
session.variables(var_ref, cx);
|
||||
});
|
||||
|
||||
this.toggle_entry(&var_path, cx);
|
||||
}
|
||||
}))
|
||||
})
|
||||
.on_secondary_mouse_down(cx.listener({
|
||||
let variable = variable.clone();
|
||||
move |this, event: &MouseDownEvent, window, cx| {
|
||||
this.deploy_variable_context_menu(
|
||||
variable.clone(),
|
||||
event.position,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.text_ui_sm(cx)
|
||||
.w_full()
|
||||
.child(
|
||||
Label::new(&dap.name).when_some(variable_name_color, |this, color| {
|
||||
this.color(Color::from(color))
|
||||
}),
|
||||
)
|
||||
.when(!dap.value.is_empty(), |this| {
|
||||
this.child(div().w_full().id(variable.item_value_id()).map(|this| {
|
||||
if let Some((_, editor)) = self
|
||||
.edited_path
|
||||
.as_ref()
|
||||
.filter(|(path, _)| path == &variable.path)
|
||||
{
|
||||
this.child(div().size_full().px_2().child(editor.clone()))
|
||||
} else {
|
||||
this.text_color(cx.theme().colors().text_muted)
|
||||
.when(
|
||||
!self.disabled
|
||||
&& self
|
||||
.session
|
||||
.read(cx)
|
||||
.capabilities()
|
||||
.supports_set_variable
|
||||
.unwrap_or_default(),
|
||||
|this| {
|
||||
let path = variable.path.clone();
|
||||
let variable_value = dap.value.clone();
|
||||
this.on_click(cx.listener(
|
||||
move |this, click: &ClickEvent, window, cx| {
|
||||
if click.down.click_count < 2 {
|
||||
return;
|
||||
}
|
||||
let editor = Self::create_variable_editor(
|
||||
&variable_value,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
this.edited_path =
|
||||
Some((path.clone(), editor));
|
||||
|
||||
cx.notify();
|
||||
},
|
||||
))
|
||||
},
|
||||
)
|
||||
.child(
|
||||
Label::new(format!("= {}", &dap.value))
|
||||
.single_line()
|
||||
.truncate()
|
||||
.size(LabelSize::Small)
|
||||
.when_some(variable_color, |this, color| {
|
||||
this.color(Color::from(color))
|
||||
}),
|
||||
)
|
||||
}
|
||||
}))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
|
||||
div()
|
||||
.occlude()
|
||||
.id("variable-list-vertical-scrollbar")
|
||||
.on_mouse_move(cx.listener(|_, _, _, cx| {
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
}))
|
||||
.on_hover(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
cx.listener(|_, _, _, cx| {
|
||||
cx.stop_propagation();
|
||||
}),
|
||||
)
|
||||
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
.h_full()
|
||||
.absolute()
|
||||
.right_1()
|
||||
.top_1()
|
||||
.bottom_0()
|
||||
.w(px(12.))
|
||||
.cursor_default()
|
||||
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for VariableList {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for VariableList {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.build_entries(cx);
|
||||
|
||||
v_flex()
|
||||
.key_context("VariableList")
|
||||
.id("variable-list")
|
||||
.group("variable-list")
|
||||
.overflow_y_scroll()
|
||||
.size_full()
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
.on_action(cx.listener(Self::select_prev))
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::expand_selected_entry))
|
||||
.on_action(cx.listener(Self::collapse_selected_entry))
|
||||
.on_action(cx.listener(Self::cancel_variable_edit))
|
||||
.on_action(cx.listener(Self::confirm_variable_edit))
|
||||
//
|
||||
.child(
|
||||
uniform_list(
|
||||
cx.entity().clone(),
|
||||
"variable-list",
|
||||
self.entries.len(),
|
||||
move |this, range, window, cx| this.render_entries(range, window, cx),
|
||||
)
|
||||
.track_scroll(self.list_handle.clone())
|
||||
.gap_1_5()
|
||||
.size_full()
|
||||
.flex_grow(),
|
||||
)
|
||||
.children(self.open_context_menu.as_ref().map(|(menu, position, _)| {
|
||||
deferred(
|
||||
anchored()
|
||||
.position(*position)
|
||||
.anchor(gpui::Corner::TopLeft)
|
||||
.child(menu.clone()),
|
||||
)
|
||||
.with_priority(1)
|
||||
}))
|
||||
.child(self.render_vertical_scrollbar(cx))
|
||||
}
|
||||
}
|
||||
|
||||
struct EntryColors {
|
||||
default: Hsla,
|
||||
hover: Hsla,
|
||||
marked_active: Hsla,
|
||||
}
|
||||
|
||||
fn get_entry_color(cx: &Context<VariableList>) -> EntryColors {
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
EntryColors {
|
||||
default: colors.panel_background,
|
||||
hover: colors.ghost_element_hover,
|
||||
marked_active: colors.ghost_element_selected,
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue