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:
Remco Smits
2025-03-18 12:55:25 -04:00
committed by GitHub
co-authored by Piotr Osiewicz Anthony Eid Anthony Piotr Osiewicz Piotr
parent ed4e654fdf
commit 41a60ffecf
156 changed files with 25840 additions and 451 deletions
+30
View File
@@ -0,0 +1,30 @@
use gpui::{FocusHandle, Focusable};
use ui::{
h_flex, Color, Context, IntoElement, Label, LabelCommon, ParentElement, Render, Styled, Window,
};
pub(crate) struct FailedState {
focus_handle: FocusHandle,
}
impl FailedState {
pub(super) fn new(cx: &mut Context<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
}
}
}
impl Focusable for FailedState {
fn focus_handle(&self, _: &ui::App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for FailedState {
fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
h_flex()
.size_full()
.items_center()
.justify_center()
.child(Label::new("Failed to spawn debugging session").color(Color::Error))
}
}
+219
View File
@@ -0,0 +1,219 @@
use std::path::PathBuf;
use dap::{DebugAdapterConfig, DebugAdapterKind, DebugRequestType};
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{App, AppContext, Entity, EventEmitter, FocusHandle, Focusable, TextStyle, WeakEntity};
use settings::Settings as _;
use task::TCPHost;
use theme::ThemeSettings;
use ui::{
h_flex, relative, v_flex, ActiveTheme as _, Button, ButtonCommon, ButtonStyle, Clickable,
Context, ContextMenu, Disableable, DropdownMenu, InteractiveElement, IntoElement,
ParentElement, Render, SharedString, Styled, Window,
};
use workspace::Workspace;
use crate::attach_modal::AttachModal;
pub(crate) struct InertState {
focus_handle: FocusHandle,
selected_debugger: Option<SharedString>,
program_editor: Entity<Editor>,
cwd_editor: Entity<Editor>,
workspace: WeakEntity<Workspace>,
}
impl InertState {
pub(super) fn new(
workspace: WeakEntity<Workspace>,
default_cwd: &str,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let program_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
editor.set_placeholder_text("Program path", cx);
editor
});
let cwd_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
editor.insert(default_cwd, window, cx);
editor.set_placeholder_text("Working directory", cx);
editor
});
Self {
workspace,
cwd_editor,
program_editor,
selected_debugger: None,
focus_handle: cx.focus_handle(),
}
}
}
impl Focusable for InertState {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
pub(crate) enum InertEvent {
Spawned { config: DebugAdapterConfig },
}
impl EventEmitter<InertEvent> for InertState {}
static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
impl Render for InertState {
fn render(
&mut self,
window: &mut ui::Window,
cx: &mut ui::Context<'_, Self>,
) -> impl ui::IntoElement {
let weak = cx.weak_entity();
let disable_buttons = self.selected_debugger.is_none();
v_flex()
.track_focus(&self.focus_handle)
.size_full()
.gap_1()
.p_2()
.child(
v_flex().gap_1()
.child(
h_flex()
.w_full()
.gap_2()
.child(Self::render_editor(&self.program_editor, cx))
.child(
h_flex().child(DropdownMenu::new(
"dap-adapter-picker",
self.selected_debugger
.as_ref()
.unwrap_or_else(|| &SELECT_DEBUGGER_LABEL)
.clone(),
ContextMenu::build(window, cx, move |this, _, _| {
let setter_for_name = |name: &'static str| {
let weak = weak.clone();
move |_: &mut Window, cx: &mut App| {
let name = name;
(&weak)
.update(cx, move |this, _| {
this.selected_debugger = Some(name.into());
})
.ok();
}
};
this.entry("GDB", None, setter_for_name("GDB"))
.entry("Delve", None, setter_for_name("Delve"))
.entry("LLDB", None, setter_for_name("LLDB"))
.entry("PHP", None, setter_for_name("PHP"))
.entry("JavaScript", None, setter_for_name("JavaScript"))
.entry("Debugpy", None, setter_for_name("Debugpy"))
}),
)),
)
)
.child(
h_flex().gap_2().child(
Self::render_editor(&self.cwd_editor, cx),
).child(h_flex()
.gap_4()
.pl_2()
.child(
Button::new("launch-dap", "Launch")
.style(ButtonStyle::Filled)
.disabled(disable_buttons)
.on_click(cx.listener(|this, _, _, cx| {
let program = this.program_editor.read(cx).text(cx);
let cwd = PathBuf::from(this.cwd_editor.read(cx).text(cx));
let kind = kind_for_label(this.selected_debugger.as_deref().unwrap_or_else(|| unimplemented!("Automatic selection of a debugger based on users project")));
cx.emit(InertEvent::Spawned {
config: DebugAdapterConfig {
label: "hard coded".into(),
kind,
request: DebugRequestType::Launch,
program: Some(program),
cwd: Some(cwd),
initialize_args: None,
supports_attach: false,
},
});
})),
)
.child(Button::new("attach-dap", "Attach")
.style(ButtonStyle::Filled)
.disabled(disable_buttons)
.on_click(cx.listener(|this, _, window, cx| this.attach(window, cx)))
))
)
)
}
}
fn kind_for_label(label: &str) -> DebugAdapterKind {
match label {
"LLDB" => DebugAdapterKind::Lldb,
"Debugpy" => DebugAdapterKind::Python(TCPHost::default()),
"JavaScript" => DebugAdapterKind::Javascript(TCPHost::default()),
"PHP" => DebugAdapterKind::Php(TCPHost::default()),
"Delve" => DebugAdapterKind::Go(TCPHost::default()),
_ => {
unimplemented!()
} // Maybe we should set a toast notification here
}
}
impl InertState {
fn render_editor(editor: &Entity<Editor>, cx: &Context<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: 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(
editor,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
}
fn attach(&self, window: &mut Window, cx: &mut Context<Self>) {
let process_id = self.program_editor.read(cx).text(cx).parse::<u32>().ok();
let cwd = PathBuf::from(self.cwd_editor.read(cx).text(cx));
let kind = kind_for_label(self.selected_debugger.as_deref().unwrap_or_else(|| {
unimplemented!("Automatic selection of a debugger based on users project")
}));
let config = DebugAdapterConfig {
label: "hard coded attach".into(),
kind,
request: DebugRequestType::Attach(task::AttachConfig { process_id }),
program: None,
cwd: Some(cwd),
initialize_args: None,
supports_attach: true,
};
if process_id.is_some() {
cx.emit(InertEvent::Spawned { config });
} else {
let _ = self.workspace.update(cx, |workspace, cx| {
let project = workspace.project().clone();
workspace.toggle_modal(window, cx, |window, cx| {
AttachModal::new(project, config, window, cx)
});
});
}
}
}
+686
View File
@@ -0,0 +1,686 @@
mod console;
mod loaded_source_list;
mod module_list;
pub mod stack_frame_list;
pub mod variable_list;
use super::{DebugPanelItemEvent, ThreadItem};
use console::Console;
use dap::{client::SessionId, debugger_settings::DebuggerSettings, Capabilities, Thread};
use gpui::{AppContext, Entity, EventEmitter, FocusHandle, Focusable, Subscription, WeakEntity};
use loaded_source_list::LoadedSourceList;
use module_list::ModuleList;
use project::debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus};
use rpc::proto::ViewId;
use settings::Settings;
use stack_frame_list::StackFrameList;
use ui::{
div, h_flex, v_flex, ActiveTheme, AnyElement, App, Button, ButtonCommon, Clickable, Context,
ContextMenu, Disableable, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize,
Indicator, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
StatefulInteractiveElement, Styled, Tooltip, Window,
};
use util::ResultExt;
use variable_list::VariableList;
use workspace::Workspace;
pub struct RunningState {
session: Entity<Session>,
thread_id: Option<ThreadId>,
console: Entity<console::Console>,
focus_handle: FocusHandle,
_remote_id: Option<ViewId>,
show_console_indicator: bool,
module_list: Entity<module_list::ModuleList>,
active_thread_item: ThreadItem,
workspace: WeakEntity<Workspace>,
session_id: SessionId,
variable_list: Entity<variable_list::VariableList>,
_subscriptions: Vec<Subscription>,
stack_frame_list: Entity<stack_frame_list::StackFrameList>,
loaded_source_list: Entity<loaded_source_list::LoadedSourceList>,
}
impl Render for RunningState {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let threads = self.session.update(cx, |this, cx| this.threads(cx));
self.select_current_thread(&threads, cx);
let thread_status = self
.thread_id
.map(|thread_id| self.session.read(cx).thread_status(thread_id))
.unwrap_or(ThreadStatus::Exited);
let selected_thread_name = threads
.iter()
.find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
.map(|(thread, _)| thread.name.clone())
.unwrap_or("Threads".to_owned());
self.variable_list.update(cx, |this, cx| {
this.disabled(thread_status != ThreadStatus::Stopped, cx);
});
let is_terminated = self.session.read(cx).is_terminated();
let active_thread_item = &self.active_thread_item;
let has_no_threads = threads.is_empty();
let capabilities = self.capabilities(cx);
let state = cx.entity();
h_flex()
.when(is_terminated, |this| this.bg(gpui::red()))
.key_context("DebugPanelItem")
.track_focus(&self.focus_handle(cx))
.size_full()
.items_start()
.child(
v_flex()
.size_full()
.items_start()
.child(
h_flex()
.w_full()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.justify_between()
.child(
h_flex()
.p_1()
.w_full()
.gap_2()
.map(|this| {
if thread_status == ThreadStatus::Running {
this.child(
IconButton::new(
"debug-pause",
IconName::DebugPause,
)
.icon_size(IconSize::Small)
.on_click(cx.listener(|this, _, _window, cx| {
this.pause_thread(cx);
}))
.tooltip(move |window, cx| {
Tooltip::text("Pause program")(window, cx)
}),
)
} else {
this.child(
IconButton::new(
"debug-continue",
IconName::DebugContinue,
)
.icon_size(IconSize::Small)
.on_click(cx.listener(|this, _, _window, cx| {
this.continue_thread(cx)
}))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip(move |window, cx| {
Tooltip::text("Continue program")(window, cx)
}),
)
}
})
.when(
capabilities.supports_step_back.unwrap_or(false),
|this| {
this.child(
IconButton::new(
"debug-step-back",
IconName::DebugStepBack,
)
.icon_size(IconSize::Small)
.on_click(cx.listener(|this, _, _window, cx| {
this.step_back(cx);
}))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip(move |window, cx| {
Tooltip::text("Step back")(window, cx)
}),
)
},
)
.child(
IconButton::new("debug-step-over", IconName::DebugStepOver)
.icon_size(IconSize::Small)
.on_click(cx.listener(|this, _, _window, cx| {
this.step_over(cx);
}))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip(move |window, cx| {
Tooltip::text("Step over")(window, cx)
}),
)
.child(
IconButton::new("debug-step-in", IconName::DebugStepInto)
.icon_size(IconSize::Small)
.on_click(cx.listener(|this, _, _window, cx| {
this.step_in(cx);
}))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip(move |window, cx| {
Tooltip::text("Step in")(window, cx)
}),
)
.child(
IconButton::new("debug-step-out", IconName::DebugStepOut)
.icon_size(IconSize::Small)
.on_click(cx.listener(|this, _, _window, cx| {
this.step_out(cx);
}))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip(move |window, cx| {
Tooltip::text("Step out")(window, cx)
}),
)
.child(
IconButton::new("debug-restart", IconName::DebugRestart)
.icon_size(IconSize::Small)
.on_click(cx.listener(|this, _, _window, cx| {
this.restart_session(cx);
}))
.disabled(
!capabilities
.supports_restart_request
.unwrap_or_default(),
)
.tooltip(move |window, cx| {
Tooltip::text("Restart")(window, cx)
}),
)
.child(
IconButton::new("debug-stop", IconName::DebugStop)
.icon_size(IconSize::Small)
.on_click(cx.listener(|this, _, _window, cx| {
this.stop_thread(cx);
}))
.disabled(
thread_status != ThreadStatus::Stopped
&& thread_status != ThreadStatus::Running,
)
.tooltip({
let label = if capabilities
.supports_terminate_threads_request
.unwrap_or_default()
{
"Terminate Thread"
} else {
"Terminate all Threads"
};
move |window, cx| Tooltip::text(label)(window, cx)
}),
)
.child(
IconButton::new(
"debug-disconnect",
IconName::DebugDisconnect,
)
.icon_size(IconSize::Small)
.on_click(cx.listener(|this, _, _window, cx| {
this.disconnect_client(cx);
}))
.disabled(
thread_status == ThreadStatus::Exited
|| thread_status == ThreadStatus::Ended,
)
.tooltip(
move |window, cx| {
Tooltip::text("Disconnect")(window, cx)
},
),
)
.child(
IconButton::new(
"debug-ignore-breakpoints",
if self.session.read(cx).breakpoints_enabled() {
IconName::DebugBreakpoint
} else {
IconName::DebugIgnoreBreakpoints
},
)
.icon_size(IconSize::Small)
.on_click(cx.listener(|this, _, _window, cx| {
this.toggle_ignore_breakpoints(cx);
}))
.disabled(
thread_status == ThreadStatus::Exited
|| thread_status == ThreadStatus::Ended,
)
.tooltip(
move |window, cx| {
Tooltip::text("Ignore breakpoints")(window, cx)
},
),
),
)
//.child(h_flex())
.child(
h_flex().p_1().mx_2().w_3_4().justify_end().child(
DropdownMenu::new(
("thread-list", self.session_id.0),
selected_thread_name,
ContextMenu::build(window, cx, move |mut this, _, _| {
for (thread, _) in threads {
let state = state.clone();
let thread_id = thread.id;
this =
this.entry(thread.name, None, move |_, cx| {
state.update(cx, |state, cx| {
state.select_thread(
ThreadId(thread_id),
cx,
);
});
});
}
this
}),
)
.disabled(
has_no_threads || thread_status != ThreadStatus::Stopped,
),
),
),
)
.child(
h_flex()
.size_full()
.items_start()
.p_1()
.gap_4()
.child(self.stack_frame_list.clone()),
),
)
.child(
v_flex()
.border_l_1()
.border_color(cx.theme().colors().border_variant)
.size_full()
.items_start()
.child(
h_flex()
.border_b_1()
.w_full()
.border_color(cx.theme().colors().border_variant)
.child(self.render_entry_button(
&SharedString::from("Variables"),
ThreadItem::Variables,
cx,
))
.when(
capabilities.supports_modules_request.unwrap_or_default(),
|this| {
this.child(self.render_entry_button(
&SharedString::from("Modules"),
ThreadItem::Modules,
cx,
))
},
)
.when(
capabilities
.supports_loaded_sources_request
.unwrap_or_default(),
|this| {
this.child(self.render_entry_button(
&SharedString::from("Loaded Sources"),
ThreadItem::LoadedSource,
cx,
))
},
)
.child(self.render_entry_button(
&SharedString::from("Console"),
ThreadItem::Console,
cx,
)),
)
.when(*active_thread_item == ThreadItem::Variables, |this| {
this.child(self.variable_list.clone())
})
.when(*active_thread_item == ThreadItem::Modules, |this| {
this.size_full().child(self.module_list.clone())
})
.when(*active_thread_item == ThreadItem::LoadedSource, |this| {
this.size_full().child(self.loaded_source_list.clone())
})
.when(*active_thread_item == ThreadItem::Console, |this| {
this.child(self.console.clone())
}),
)
}
}
impl RunningState {
pub fn new(
session: Entity<Session>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
let session_id = session.read(cx).session_id();
let weak_state = cx.weak_entity();
let stack_frame_list = cx.new(|cx| {
StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
});
let variable_list =
cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx));
let console = cx.new(|cx| {
Console::new(
session.clone(),
stack_frame_list.clone(),
variable_list.clone(),
window,
cx,
)
});
let _subscriptions = vec![
cx.observe(&module_list, |_, _, cx| cx.notify()),
cx.subscribe_in(&session, window, |this, _, event, window, cx| {
match event {
SessionEvent::Stopped(thread_id) => {
this.workspace
.update(cx, |workspace, cx| {
workspace.open_panel::<crate::DebugPanel>(window, cx);
})
.log_err();
if let Some(thread_id) = thread_id {
this.select_thread(*thread_id, cx);
}
}
SessionEvent::Threads => {
let threads = this.session.update(cx, |this, cx| this.threads(cx));
this.select_current_thread(&threads, cx);
}
_ => {}
}
cx.notify()
}),
];
Self {
session,
console,
workspace,
module_list,
focus_handle,
variable_list,
_subscriptions,
thread_id: None,
_remote_id: None,
stack_frame_list,
loaded_source_list,
session_id,
show_console_indicator: false,
active_thread_item: ThreadItem::Variables,
}
}
pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
if self.thread_id.is_some() {
self.stack_frame_list
.update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
}
}
pub fn session(&self) -> &Entity<Session> {
&self.session
}
pub fn session_id(&self) -> SessionId {
self.session_id
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_thread_item(&mut self, thread_item: ThreadItem, cx: &mut Context<Self>) {
self.active_thread_item = thread_item;
cx.notify()
}
#[cfg(any(test, feature = "test-support"))]
pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
&self.stack_frame_list
}
#[cfg(any(test, feature = "test-support"))]
pub fn console(&self) -> &Entity<Console> {
&self.console
}
#[cfg(any(test, feature = "test-support"))]
pub fn module_list(&self) -> &Entity<ModuleList> {
&self.module_list
}
#[cfg(any(test, feature = "test-support"))]
pub fn variable_list(&self) -> &Entity<VariableList> {
&self.variable_list
}
#[cfg(any(test, feature = "test-support"))]
pub fn are_breakpoints_ignored(&self, cx: &App) -> bool {
self.session.read(cx).ignore_breakpoints()
}
pub fn capabilities(&self, cx: &App) -> Capabilities {
self.session().read(cx).capabilities().clone()
}
pub fn select_current_thread(
&mut self,
threads: &Vec<(Thread, ThreadStatus)>,
cx: &mut Context<Self>,
) {
let selected_thread = self
.thread_id
.and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0))
.or_else(|| threads.first());
let Some((selected_thread, _)) = selected_thread else {
return;
};
if Some(ThreadId(selected_thread.id)) != self.thread_id {
self.select_thread(ThreadId(selected_thread.id), cx);
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn selected_thread_id(&self) -> Option<ThreadId> {
self.thread_id
}
pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> {
self.thread_id
.map(|id| self.session().read(cx).thread_status(id))
}
fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
if self.thread_id.is_some_and(|id| id == thread_id) {
return;
}
self.thread_id = Some(thread_id);
self.stack_frame_list
.update(cx, |list, cx| list.refresh(cx));
cx.notify();
}
fn render_entry_button(
&self,
label: &SharedString,
thread_item: ThreadItem,
cx: &mut Context<Self>,
) -> AnyElement {
let has_indicator =
matches!(thread_item, ThreadItem::Console) && self.show_console_indicator;
div()
.id(label.clone())
.px_2()
.py_1()
.cursor_pointer()
.border_b_2()
.when(self.active_thread_item == thread_item, |this| {
this.border_color(cx.theme().colors().border)
})
.child(
h_flex()
.child(Button::new(label.clone(), label.clone()))
.when(has_indicator, |this| this.child(Indicator::dot())),
)
.on_click(cx.listener(move |this, _, _window, cx| {
this.active_thread_item = thread_item;
if matches!(this.active_thread_item, ThreadItem::Console) {
this.show_console_indicator = false;
}
cx.notify();
}))
.into_any_element()
}
pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
let Some(thread_id) = self.thread_id else {
return;
};
self.session().update(cx, |state, cx| {
state.continue_thread(thread_id, cx);
});
}
pub fn step_over(&mut self, cx: &mut Context<Self>) {
let Some(thread_id) = self.thread_id else {
return;
};
let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
self.session().update(cx, |state, cx| {
state.step_over(thread_id, granularity, cx);
});
}
pub fn step_in(&mut self, cx: &mut Context<Self>) {
let Some(thread_id) = self.thread_id else {
return;
};
let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
self.session().update(cx, |state, cx| {
state.step_in(thread_id, granularity, cx);
});
}
pub fn step_out(&mut self, cx: &mut Context<Self>) {
let Some(thread_id) = self.thread_id else {
return;
};
let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
self.session().update(cx, |state, cx| {
state.step_out(thread_id, granularity, cx);
});
}
pub fn step_back(&mut self, cx: &mut Context<Self>) {
let Some(thread_id) = self.thread_id else {
return;
};
let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
self.session().update(cx, |state, cx| {
state.step_back(thread_id, granularity, cx);
});
}
pub fn restart_session(&self, cx: &mut Context<Self>) {
self.session().update(cx, |state, cx| {
state.restart(None, cx);
});
}
pub fn pause_thread(&self, cx: &mut Context<Self>) {
let Some(thread_id) = self.thread_id else {
return;
};
self.session().update(cx, |state, cx| {
state.pause_thread(thread_id, cx);
});
}
pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
self.workspace
.update(cx, |workspace, cx| {
workspace
.project()
.read(cx)
.breakpoint_store()
.update(cx, |store, cx| {
store.remove_active_position(Some(self.session_id), cx)
})
})
.log_err();
self.session.update(cx, |session, cx| {
session.shutdown(cx).detach();
})
}
pub fn stop_thread(&self, cx: &mut Context<Self>) {
let Some(thread_id) = self.thread_id else {
return;
};
self.workspace
.update(cx, |workspace, cx| {
workspace
.project()
.read(cx)
.breakpoint_store()
.update(cx, |store, cx| {
store.remove_active_position(Some(self.session_id), cx)
})
})
.log_err();
self.session().update(cx, |state, cx| {
state.terminate_threads(Some(vec![thread_id; 1]), cx);
});
}
pub fn disconnect_client(&self, cx: &mut Context<Self>) {
self.session().update(cx, |state, cx| {
state.disconnect_client(cx);
});
}
pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
self.session.update(cx, |session, cx| {
session.toggle_ignore_breakpoints(cx).detach();
});
}
}
impl EventEmitter<DebugPanelItemEvent> for RunningState {}
impl Focusable for RunningState {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
@@ -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(),
))
})
}
}
@@ -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())
}
}
@@ -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())
}
}
@@ -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 {}
@@ -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,
}
}
@@ -0,0 +1,80 @@
use std::time::Duration;
use anyhow::Result;
use dap::client::SessionId;
use gpui::{
percentage, Animation, AnimationExt, Entity, EventEmitter, FocusHandle, Focusable, Task,
Transformation,
};
use project::debugger::session::Session;
use ui::{v_flex, Color, Context, Icon, IconName, IntoElement, ParentElement, Render, Styled};
pub(crate) struct StartingState {
focus_handle: FocusHandle,
pub(super) session_id: SessionId,
_notify_parent: Task<()>,
}
pub(crate) enum StartingEvent {
Failed,
Finished(Entity<Session>),
}
impl EventEmitter<StartingEvent> for StartingState {}
impl StartingState {
pub(crate) fn new(
session_id: SessionId,
task: Task<Result<Entity<Session>>>,
cx: &mut Context<Self>,
) -> Self {
let _notify_parent = cx.spawn(move |this, mut cx| async move {
let entity = task.await;
this.update(&mut cx, |_, cx| {
if let Ok(entity) = entity {
cx.emit(StartingEvent::Finished(entity))
} else {
cx.emit(StartingEvent::Failed)
}
})
.ok();
});
Self {
session_id,
focus_handle: cx.focus_handle(),
_notify_parent,
}
}
}
impl Focusable for StartingState {
fn focus_handle(&self, _: &ui::App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for StartingState {
fn render(
&mut self,
_window: &mut ui::Window,
_cx: &mut ui::Context<'_, Self>,
) -> impl ui::IntoElement {
v_flex()
.size_full()
.gap_1()
.items_center()
.child("Starting a debug adapter")
.child(
Icon::new(IconName::ArrowCircle)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element(),
)
}
}