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 17:55:25 +01:00 committed by GitHub
parent ed4e654fdf
commit 41a60ffecf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
156 changed files with 25840 additions and 451 deletions

View file

@ -0,0 +1,58 @@
[package]
name = "debugger_ui"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[features]
test-support = [
"dap/test-support",
"editor/test-support",
"gpui/test-support",
"project/test-support",
"util/test-support",
"workspace/test-support",
]
[dependencies]
anyhow.workspace = true
client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
dap.workspace = true
editor.workspace = true
feature_flags.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
menu.workspace = true
picker.workspace = true
pretty_assertions.workspace = true
project.workspace = true
rpc.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
sysinfo.workspace = true
task.workspace = true
terminal_view.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
[dev-dependencies]
dap = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
unindent.workspace = true
util = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }

View file

@ -0,0 +1 @@
../../LICENSE-GPL

View file

@ -0,0 +1,293 @@
use dap::DebugRequestType;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::Subscription;
use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render};
use picker::{Picker, PickerDelegate};
use project::debugger::attach_processes;
use std::sync::Arc;
use sysinfo::System;
use ui::{prelude::*, Context, Tooltip};
use ui::{ListItem, ListItemSpacing};
use util::debug_panic;
use workspace::ModalView;
#[derive(Debug, Clone)]
struct Candidate {
pid: u32,
name: String,
command: Vec<String>,
}
pub(crate) struct AttachModalDelegate {
selected_index: usize,
matches: Vec<StringMatch>,
placeholder_text: Arc<str>,
project: Entity<project::Project>,
debug_config: task::DebugAdapterConfig,
candidates: Option<Vec<Candidate>>,
}
impl AttachModalDelegate {
pub fn new(project: Entity<project::Project>, debug_config: task::DebugAdapterConfig) -> Self {
Self {
project,
debug_config,
candidates: None,
selected_index: 0,
matches: Vec::default(),
placeholder_text: Arc::from("Select the process you want to attach the debugger to"),
}
}
}
pub struct AttachModal {
_subscription: Subscription,
pub(crate) picker: Entity<Picker<AttachModalDelegate>>,
}
impl AttachModal {
pub fn new(
project: Entity<project::Project>,
debug_config: task::DebugAdapterConfig,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let picker = cx.new(|cx| {
Picker::uniform_list(AttachModalDelegate::new(project, debug_config), window, cx)
});
Self {
_subscription: cx.subscribe(&picker, |_, _, _, cx| {
cx.emit(DismissEvent);
}),
picker,
}
}
}
impl Render for AttachModal {
fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl ui::IntoElement {
v_flex()
.key_context("AttachModal")
.w(rems(34.))
.child(self.picker.clone())
}
}
impl EventEmitter<DismissEvent> for AttachModal {}
impl Focusable for AttachModal {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.picker.read(cx).focus_handle(cx)
}
}
impl ModalView for AttachModal {}
impl PickerDelegate for AttachModalDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
self.placeholder_text.clone()
}
fn update_matches(
&mut self,
query: String,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> gpui::Task<()> {
cx.spawn(|this, mut cx| async move {
let Some(processes) = this
.update(&mut cx, |this, _| {
if let Some(processes) = this.delegate.candidates.clone() {
processes
} else {
let system = System::new_all();
let processes =
attach_processes(&this.delegate.debug_config.kind, &system.processes());
let candidates = processes
.into_iter()
.map(|(pid, process)| Candidate {
pid: pid.as_u32(),
name: process.name().to_string_lossy().into_owned(),
command: process
.cmd()
.iter()
.map(|s| s.to_string_lossy().to_string())
.collect::<Vec<_>>(),
})
.collect::<Vec<Candidate>>();
let _ = this.delegate.candidates.insert(candidates.clone());
candidates
}
})
.ok()
else {
return;
};
let matches = fuzzy::match_strings(
&processes
.iter()
.enumerate()
.map(|(id, candidate)| {
StringMatchCandidate::new(
id,
format!(
"{} {} {}",
candidate.command.join(" "),
candidate.pid,
candidate.name
)
.as_str(),
)
})
.collect::<Vec<_>>(),
&query,
true,
100,
&Default::default(),
cx.background_executor().clone(),
)
.await;
this.update(&mut cx, |this, _| {
let delegate = &mut this.delegate;
delegate.matches = matches;
delegate.candidates = Some(processes);
if delegate.matches.is_empty() {
delegate.selected_index = 0;
} else {
delegate.selected_index =
delegate.selected_index.min(delegate.matches.len() - 1);
}
})
.ok();
})
}
fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
let candidate = self
.matches
.get(self.selected_index())
.and_then(|current_match| {
let ix = current_match.candidate_id;
self.candidates.as_ref().map(|candidates| &candidates[ix])
});
let Some(candidate) = candidate else {
return cx.emit(DismissEvent);
};
match &mut self.debug_config.request {
DebugRequestType::Attach(config) => {
config.process_id = Some(candidate.pid);
}
DebugRequestType::Launch => {
debug_panic!("Debugger attach modal used on launch debug config");
return;
}
}
let config = self.debug_config.clone();
self.project
.update(cx, |project, cx| project.start_debug_session(config, cx))
.detach_and_log_err(cx);
cx.emit(DismissEvent);
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.selected_index = 0;
self.candidates.take();
cx.emit(DismissEvent);
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let candidates = self.candidates.as_ref()?;
let hit = &self.matches[ix];
let candidate = &candidates.get(hit.candidate_id)?;
Some(
ListItem::new(SharedString::from(format!("process-entry-{ix}")))
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(
v_flex()
.items_start()
.child(Label::new(format!("{} {}", candidate.name, candidate.pid)))
.child(
div()
.id(SharedString::from(format!("process-entry-{ix}-command")))
.tooltip(Tooltip::text(
candidate
.command
.clone()
.into_iter()
.collect::<Vec<_>>()
.join(" "),
))
.child(
Label::new(format!(
"{} {}",
candidate.name,
candidate
.command
.clone()
.into_iter()
.skip(1)
.collect::<Vec<_>>()
.join(" ")
))
.size(LabelSize::Small)
.color(Color::Muted),
),
),
),
)
}
}
#[allow(dead_code)]
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
modal.picker.update(cx, |picker, _| {
picker
.delegate
.matches
.iter()
.map(|hit| hit.string.clone())
.collect::<Vec<_>>()
})
}

View file

@ -0,0 +1,536 @@
use crate::session::DebugSession;
use anyhow::{anyhow, Result};
use collections::HashMap;
use command_palette_hooks::CommandPaletteFilter;
use dap::{
client::SessionId, debugger_settings::DebuggerSettings, ContinuedEvent, LoadedSourceEvent,
ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
};
use futures::{channel::mpsc, SinkExt as _};
use gpui::{
actions, Action, App, AsyncWindowContext, Context, Entity, EventEmitter, FocusHandle,
Focusable, Subscription, Task, WeakEntity,
};
use project::{
debugger::dap_store::{self, DapStore},
terminals::TerminalKind,
Project,
};
use rpc::proto::{self};
use settings::Settings;
use std::{any::TypeId, path::PathBuf};
use terminal_view::terminal_panel::TerminalPanel;
use ui::prelude::*;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
pane, Continue, Disconnect, Pane, Pause, Restart, StepBack, StepInto, StepOut, StepOver, Stop,
ToggleIgnoreBreakpoints, Workspace,
};
pub enum DebugPanelEvent {
Exited(SessionId),
Terminated(SessionId),
Stopped {
client_id: SessionId,
event: StoppedEvent,
go_to_stack_frame: bool,
},
Thread((SessionId, ThreadEvent)),
Continued((SessionId, ContinuedEvent)),
Output((SessionId, OutputEvent)),
Module((SessionId, ModuleEvent)),
LoadedSource((SessionId, LoadedSourceEvent)),
ClientShutdown(SessionId),
CapabilitiesChanged(SessionId),
}
actions!(debug_panel, [ToggleFocus]);
pub struct DebugPanel {
size: Pixels,
pane: Entity<Pane>,
project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>,
_subscriptions: Vec<Subscription>,
}
impl DebugPanel {
pub fn new(
workspace: &Workspace,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Entity<Self> {
cx.new(|cx| {
let project = workspace.project().clone();
let dap_store = project.read(cx).dap_store();
let weak_workspace = workspace.weak_handle();
let pane = cx.new(|cx| {
let mut pane = Pane::new(
workspace.weak_handle(),
project.clone(),
Default::default(),
None,
gpui::NoAction.boxed_clone(),
window,
cx,
);
pane.set_can_split(None);
pane.set_can_navigate(true, cx);
pane.display_nav_history_buttons(None);
pane.set_should_display_tab_bar(|_window, _cx| true);
pane.set_close_pane_if_empty(true, cx);
pane.set_render_tab_bar_buttons(cx, {
let project = project.clone();
let weak_workspace = weak_workspace.clone();
move |_, _, cx| {
let project = project.clone();
let weak_workspace = weak_workspace.clone();
(
None,
Some(
h_flex()
.child(
IconButton::new("new-debug-session", IconName::Plus)
.icon_size(IconSize::Small)
.on_click(cx.listener(move |pane, _, window, cx| {
pane.add_item(
Box::new(DebugSession::inert(
project.clone(),
weak_workspace.clone(),
window,
cx,
)),
false,
false,
None,
window,
cx,
);
})),
)
.into_any_element(),
),
)
}
});
pane.add_item(
Box::new(DebugSession::inert(
project.clone(),
weak_workspace.clone(),
window,
cx,
)),
false,
false,
None,
window,
cx,
);
pane
});
let _subscriptions = vec![
cx.observe(&pane, |_, _, cx| cx.notify()),
cx.subscribe_in(&pane, window, Self::handle_pane_event),
cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event),
];
let debug_panel = Self {
pane,
size: px(300.),
_subscriptions,
project: project.downgrade(),
workspace: workspace.weak_handle(),
};
debug_panel
})
}
pub fn load(
workspace: WeakEntity<Workspace>,
cx: AsyncWindowContext,
) -> Task<Result<Entity<Self>>> {
cx.spawn(|mut cx| async move {
workspace.update_in(&mut cx, |workspace, window, cx| {
let debug_panel = DebugPanel::new(workspace, window, cx);
cx.observe(&debug_panel, |_, debug_panel, cx| {
let (has_active_session, supports_restart, support_step_back) = debug_panel
.update(cx, |this, cx| {
this.active_session(cx)
.map(|item| {
let running = item.read(cx).mode().as_running().cloned();
match running {
Some(running) => {
let caps = running.read(cx).capabilities(cx);
(
true,
caps.supports_restart_request.unwrap_or_default(),
caps.supports_step_back.unwrap_or_default(),
)
}
None => (false, false, false),
}
})
.unwrap_or((false, false, false))
});
let filter = CommandPaletteFilter::global_mut(cx);
let debugger_action_types = [
TypeId::of::<Continue>(),
TypeId::of::<StepOver>(),
TypeId::of::<StepInto>(),
TypeId::of::<StepOut>(),
TypeId::of::<Stop>(),
TypeId::of::<Disconnect>(),
TypeId::of::<Pause>(),
TypeId::of::<ToggleIgnoreBreakpoints>(),
];
let step_back_action_type = [TypeId::of::<StepBack>()];
let restart_action_type = [TypeId::of::<Restart>()];
if has_active_session {
filter.show_action_types(debugger_action_types.iter());
if supports_restart {
filter.show_action_types(restart_action_type.iter());
} else {
filter.hide_action_types(&restart_action_type);
}
if support_step_back {
filter.show_action_types(step_back_action_type.iter());
} else {
filter.hide_action_types(&step_back_action_type);
}
} else {
// show only the `debug: start`
filter.hide_action_types(&debugger_action_types);
filter.hide_action_types(&step_back_action_type);
filter.hide_action_types(&restart_action_type);
}
})
.detach();
debug_panel
})
})
}
pub fn active_session(&self, cx: &App) -> Option<Entity<DebugSession>> {
self.pane
.read(cx)
.active_item()
.and_then(|panel| panel.downcast::<DebugSession>())
}
pub fn debug_panel_items_by_client(
&self,
client_id: &SessionId,
cx: &Context<Self>,
) -> Vec<Entity<DebugSession>> {
self.pane
.read(cx)
.items()
.filter_map(|item| item.downcast::<DebugSession>())
.filter(|item| item.read(cx).session_id(cx) == Some(*client_id))
.map(|item| item.clone())
.collect()
}
pub fn debug_panel_item_by_client(
&self,
client_id: SessionId,
cx: &mut Context<Self>,
) -> Option<Entity<DebugSession>> {
self.pane
.read(cx)
.items()
.filter_map(|item| item.downcast::<DebugSession>())
.find(|item| {
let item = item.read(cx);
item.session_id(cx) == Some(client_id)
})
}
fn handle_dap_store_event(
&mut self,
dap_store: &Entity<DapStore>,
event: &dap_store::DapStoreEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
let Some(session) = dap_store.read(cx).session_by_id(session_id) else {
return log::error!("Couldn't get session with id: {session_id:?} from DebugClientStarted event");
};
let Some(project) = self.project.upgrade() else {
return log::error!("Debug Panel out lived it's weak reference to Project");
};
if self.pane.read_with(cx, |pane, cx| {
pane.items_of_type::<DebugSession>()
.any(|item| item.read(cx).session_id(cx) == Some(*session_id))
}) {
// We already have an item for this session.
return;
}
let session_item =
DebugSession::running(project, self.workspace.clone(), session, window, cx);
self.pane.update(cx, |pane, cx| {
pane.add_item(Box::new(session_item), true, true, None, window, cx);
window.focus(&pane.focus_handle(cx));
cx.notify();
});
}
dap_store::DapStoreEvent::RunInTerminal {
title,
cwd,
command,
args,
envs,
sender,
..
} => {
self.handle_run_in_terminal_request(
title.clone(),
cwd.clone(),
command.clone(),
args.clone(),
envs.clone(),
sender.clone(),
window,
cx,
)
.detach_and_log_err(cx);
}
_ => {}
}
}
fn handle_run_in_terminal_request(
&self,
title: Option<String>,
cwd: PathBuf,
command: Option<String>,
args: Vec<String>,
envs: HashMap<String, String>,
mut sender: mpsc::Sender<Result<u32>>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<()>> {
let terminal_task = self.workspace.update(cx, |workspace, cx| {
let terminal_panel = workspace.panel::<TerminalPanel>(cx).ok_or_else(|| {
anyhow!("RunInTerminal DAP request failed because TerminalPanel wasn't found")
});
let terminal_panel = match terminal_panel {
Ok(panel) => panel,
Err(err) => return Task::ready(Err(err)),
};
terminal_panel.update(cx, |terminal_panel, cx| {
let terminal_task = terminal_panel.add_terminal(
TerminalKind::Debug {
command,
args,
envs,
cwd,
title,
},
task::RevealStrategy::Always,
window,
cx,
);
cx.spawn(|_, mut cx| async move {
let pid_task = async move {
let terminal = terminal_task.await?;
terminal.read_with(&mut cx, |terminal, _| terminal.pty_info.pid())
};
pid_task.await
})
})
});
cx.background_spawn(async move {
match terminal_task {
Ok(pid_task) => match pid_task.await {
Ok(Some(pid)) => sender.send(Ok(pid.as_u32())).await?,
Ok(None) => {
sender
.send(Err(anyhow!(
"Terminal was spawned but PID was not available"
)))
.await?
}
Err(error) => sender.send(Err(anyhow!(error))).await?,
},
Err(error) => sender.send(Err(anyhow!(error))).await?,
};
Ok(())
})
}
fn handle_pane_event(
&mut self,
_: &Entity<Pane>,
event: &pane::Event,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
pane::Event::Remove { .. } => cx.emit(PanelEvent::Close),
pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
pane::Event::AddItem { item } => {
self.workspace
.update(cx, |workspace, cx| {
item.added_to_pane(workspace, self.pane.clone(), window, cx)
})
.ok();
}
pane::Event::RemovedItem { item } => {
if let Some(debug_session) = item.downcast::<DebugSession>() {
debug_session.update(cx, |session, cx| {
session.shutdown(cx);
})
}
}
pane::Event::ActivateItem {
local: _,
focus_changed,
} => {
if *focus_changed {
if let Some(debug_session) = self
.pane
.read(cx)
.active_item()
.and_then(|item| item.downcast::<DebugSession>())
{
if let Some(running) = debug_session
.read_with(cx, |session, _| session.mode().as_running().cloned())
{
running.update(cx, |running, cx| {
running.go_to_selected_stack_frame(window, cx);
});
}
}
}
}
_ => {}
}
}
}
impl EventEmitter<PanelEvent> for DebugPanel {}
impl EventEmitter<DebugPanelEvent> for DebugPanel {}
impl EventEmitter<project::Event> for DebugPanel {}
impl Focusable for DebugPanel {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.pane.focus_handle(cx)
}
}
impl Panel for DebugPanel {
fn pane(&self) -> Option<Entity<Pane>> {
Some(self.pane.clone())
}
fn persistent_name() -> &'static str {
"DebugPanel"
}
fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
DockPosition::Bottom
}
fn position_is_valid(&self, position: DockPosition) -> bool {
position == DockPosition::Bottom
}
fn set_position(
&mut self,
_position: DockPosition,
_window: &mut Window,
_cx: &mut Context<Self>,
) {
}
fn size(&self, _window: &Window, _cx: &App) -> Pixels {
self.size
}
fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
self.size = size.unwrap();
}
fn remote_id() -> Option<proto::PanelId> {
Some(proto::PanelId::DebugPanel)
}
fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
Some(IconName::Debug)
}
fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
if DebuggerSettings::get_global(cx).button {
Some("Debug Panel")
} else {
None
}
}
fn toggle_action(&self) -> Box<dyn Action> {
Box::new(ToggleFocus)
}
fn activation_priority(&self) -> u32 {
9
}
fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
if active && self.pane.read(cx).items_len() == 0 {
let Some(project) = self.project.clone().upgrade() else {
return;
};
// todo: We need to revisit it when we start adding stopped items to pane (as that'll cause us to add two items).
self.pane.update(cx, |this, cx| {
this.add_item(
Box::new(DebugSession::inert(
project,
self.workspace.clone(),
window,
cx,
)),
false,
false,
None,
window,
cx,
);
});
}
}
}
impl Render for DebugPanel {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.key_context("DebugPanel")
.track_focus(&self.focus_handle(cx))
.size_full()
.child(self.pane.clone())
.into_any()
}
}

View file

@ -0,0 +1,122 @@
use dap::debugger_settings::DebuggerSettings;
use debugger_panel::{DebugPanel, ToggleFocus};
use feature_flags::{Debugger, FeatureFlagViewExt};
use gpui::App;
use session::DebugSession;
use settings::Settings;
use workspace::{
Pause, Restart, ShutdownDebugAdapters, StepBack, StepInto, StepOver, Stop,
ToggleIgnoreBreakpoints, Workspace,
};
pub mod attach_modal;
pub mod debugger_panel;
pub mod session;
#[cfg(test)]
mod tests;
pub fn init(cx: &mut App) {
DebuggerSettings::register(cx);
workspace::FollowableViewRegistry::register::<DebugSession>(cx);
cx.observe_new(|_: &mut Workspace, window, cx| {
let Some(window) = window else {
return;
};
cx.when_flag_enabled::<Debugger>(window, |workspace, _, _| {
workspace
.register_action(|workspace, _: &ToggleFocus, window, cx| {
workspace.toggle_panel_focus::<DebugPanel>(window, cx);
})
.register_action(|workspace, _: &Pause, _, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
panel
.active_session(cx)
.and_then(|session| session.read(cx).mode().as_running().cloned())
}) {
active_item.update(cx, |item, cx| item.pause_thread(cx))
}
})
.register_action(|workspace, _: &Restart, _, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
panel
.active_session(cx)
.and_then(|session| session.read(cx).mode().as_running().cloned())
}) {
active_item.update(cx, |item, cx| item.restart_session(cx))
}
})
.register_action(|workspace, _: &StepInto, _, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
panel
.active_session(cx)
.and_then(|session| session.read(cx).mode().as_running().cloned())
}) {
active_item.update(cx, |item, cx| item.step_in(cx))
}
})
.register_action(|workspace, _: &StepOver, _, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
panel
.active_session(cx)
.and_then(|session| session.read(cx).mode().as_running().cloned())
}) {
active_item.update(cx, |item, cx| item.step_over(cx))
}
})
.register_action(|workspace, _: &StepBack, _, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
panel
.active_session(cx)
.and_then(|session| session.read(cx).mode().as_running().cloned())
}) {
active_item.update(cx, |item, cx| item.step_back(cx))
}
})
.register_action(|workspace, _: &Stop, _, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
panel
.active_session(cx)
.and_then(|session| session.read(cx).mode().as_running().cloned())
}) {
active_item.update(cx, |item, cx| item.stop_thread(cx))
}
})
.register_action(|workspace, _: &ToggleIgnoreBreakpoints, _, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
panel
.active_session(cx)
.and_then(|session| session.read(cx).mode().as_running().cloned())
}) {
active_item.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
}
})
.register_action(
|workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
workspace.project().update(cx, |project, cx| {
project.dap_store().update(cx, |store, cx| {
store.shutdown_sessions(cx).detach();
})
})
},
);
})
})
.detach();
}

View file

@ -0,0 +1,313 @@
mod failed;
mod inert;
pub mod running;
mod starting;
use std::time::Duration;
use dap::client::SessionId;
use failed::FailedState;
use gpui::{
percentage, Animation, AnimationExt, AnyElement, App, Entity, EventEmitter, FocusHandle,
Focusable, Subscription, Task, Transformation, WeakEntity,
};
use inert::{InertEvent, InertState};
use project::debugger::{dap_store::DapStore, session::Session};
use project::worktree_store::WorktreeStore;
use project::Project;
use rpc::proto::{self, PeerId};
use running::RunningState;
use starting::{StartingEvent, StartingState};
use ui::prelude::*;
use workspace::{
item::{self, Item},
FollowableItem, ViewId, Workspace,
};
pub(crate) enum DebugSessionState {
Inert(Entity<InertState>),
Starting(Entity<StartingState>),
Failed(Entity<FailedState>),
Running(Entity<running::RunningState>),
}
impl DebugSessionState {
pub(crate) fn as_running(&self) -> Option<&Entity<running::RunningState>> {
match &self {
DebugSessionState::Running(entity) => Some(entity),
_ => None,
}
}
}
pub struct DebugSession {
remote_id: Option<workspace::ViewId>,
mode: DebugSessionState,
dap_store: WeakEntity<DapStore>,
worktree_store: WeakEntity<WorktreeStore>,
workspace: WeakEntity<Workspace>,
_subscriptions: [Subscription; 1],
}
#[derive(Debug)]
pub enum DebugPanelItemEvent {
Close,
Stopped { go_to_stack_frame: bool },
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ThreadItem {
Console,
LoadedSource,
Modules,
Variables,
}
impl DebugSession {
pub(super) fn inert(
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
let default_cwd = project
.read(cx)
.worktrees(cx)
.next()
.and_then(|tree| tree.read(cx).abs_path().to_str().map(|str| str.to_string()))
.unwrap_or_default();
let inert = cx.new(|cx| InertState::new(workspace.clone(), &default_cwd, window, cx));
let project = project.read(cx);
let dap_store = project.dap_store().downgrade();
let worktree_store = project.worktree_store().downgrade();
cx.new(|cx| {
let _subscriptions = [cx.subscribe_in(&inert, window, Self::on_inert_event)];
Self {
remote_id: None,
mode: DebugSessionState::Inert(inert),
dap_store,
worktree_store,
workspace,
_subscriptions,
}
})
}
pub(crate) fn running(
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
session: Entity<Session>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
let mode = cx.new(|cx| RunningState::new(session.clone(), workspace.clone(), window, cx));
cx.new(|cx| Self {
_subscriptions: [cx.subscribe(&mode, |_, _, _, cx| {
cx.notify();
})],
remote_id: None,
mode: DebugSessionState::Running(mode),
dap_store: project.read(cx).dap_store().downgrade(),
worktree_store: project.read(cx).worktree_store().downgrade(),
workspace,
})
}
pub(crate) fn session_id(&self, cx: &App) -> Option<SessionId> {
match &self.mode {
DebugSessionState::Inert(_) => None,
DebugSessionState::Starting(entity) => Some(entity.read(cx).session_id),
DebugSessionState::Failed(_) => None,
DebugSessionState::Running(entity) => Some(entity.read(cx).session_id()),
}
}
pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
match &self.mode {
DebugSessionState::Inert(_) => {}
DebugSessionState::Starting(_entity) => {} // todo(debugger): we need to shutdown the starting process in this case (or recreate it on a breakpoint being hit)
DebugSessionState::Failed(_) => {}
DebugSessionState::Running(state) => state.update(cx, |state, cx| state.shutdown(cx)),
}
}
pub(crate) fn mode(&self) -> &DebugSessionState {
&self.mode
}
fn on_inert_event(
&mut self,
_: &Entity<InertState>,
event: &InertEvent,
window: &mut Window,
cx: &mut Context<'_, Self>,
) {
let dap_store = self.dap_store.clone();
let InertEvent::Spawned { config } = event;
let config = config.clone();
let worktree = self
.worktree_store
.update(cx, |this, _| this.worktrees().next())
.ok()
.flatten()
.expect("worktree-less project");
let Ok((new_session_id, task)) = dap_store.update(cx, |store, cx| {
store.new_session(config, &worktree, None, cx)
}) else {
return;
};
let starting = cx.new(|cx| StartingState::new(new_session_id, task, cx));
self._subscriptions = [cx.subscribe_in(&starting, window, Self::on_starting_event)];
self.mode = DebugSessionState::Starting(starting);
}
fn on_starting_event(
&mut self,
_: &Entity<StartingState>,
event: &StartingEvent,
window: &mut Window,
cx: &mut Context<'_, Self>,
) {
if let StartingEvent::Finished(session) = event {
let mode =
cx.new(|cx| RunningState::new(session.clone(), self.workspace.clone(), window, cx));
self.mode = DebugSessionState::Running(mode);
} else if let StartingEvent::Failed = event {
self.mode = DebugSessionState::Failed(cx.new(FailedState::new));
};
cx.notify();
}
}
impl EventEmitter<DebugPanelItemEvent> for DebugSession {}
impl Focusable for DebugSession {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.mode {
DebugSessionState::Inert(inert_state) => inert_state.focus_handle(cx),
DebugSessionState::Starting(starting_state) => starting_state.focus_handle(cx),
DebugSessionState::Failed(failed_state) => failed_state.focus_handle(cx),
DebugSessionState::Running(running_state) => running_state.focus_handle(cx),
}
}
}
impl Item for DebugSession {
type Event = DebugPanelItemEvent;
fn tab_content(&self, _: item::TabContentParams, _: &Window, cx: &App) -> AnyElement {
let (label, color) = match &self.mode {
DebugSessionState::Inert(_) => ("New Session", Color::Default),
DebugSessionState::Starting(_) => ("Starting", Color::Default),
DebugSessionState::Failed(_) => ("Failed", Color::Error),
DebugSessionState::Running(state) => (
state
.read_with(cx, |state, cx| state.thread_status(cx))
.map(|status| status.label())
.unwrap_or("Running"),
Color::Default,
),
};
let is_starting = matches!(self.mode, DebugSessionState::Starting(_));
h_flex()
.gap_1()
.children(is_starting.then(|| {
Icon::new(IconName::ArrowCircle).with_animation(
"starting-debug-session",
Animation::new(Duration::from_secs(2)).repeat(),
|this, delta| this.transform(Transformation::rotate(percentage(delta))),
)
}))
.child(Label::new(label).color(color))
.into_any_element()
}
}
impl FollowableItem for DebugSession {
fn remote_id(&self) -> Option<workspace::ViewId> {
self.remote_id
}
fn to_state_proto(&self, _window: &Window, _cx: &App) -> Option<proto::view::Variant> {
None
}
fn from_state_proto(
_workspace: Entity<Workspace>,
_remote_id: ViewId,
_state: &mut Option<proto::view::Variant>,
_window: &mut Window,
_cx: &mut App,
) -> Option<gpui::Task<gpui::Result<Entity<Self>>>> {
None
}
fn add_event_to_update_proto(
&self,
_event: &Self::Event,
_update: &mut Option<proto::update_view::Variant>,
_window: &Window,
_cx: &App,
) -> bool {
// update.get_or_insert_with(|| proto::update_view::Variant::DebugPanel(Default::default()));
true
}
fn apply_update_proto(
&mut self,
_project: &Entity<project::Project>,
_message: proto::update_view::Variant,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> gpui::Task<gpui::Result<()>> {
Task::ready(Ok(()))
}
fn set_leader_peer_id(
&mut self,
_leader_peer_id: Option<PeerId>,
_window: &mut Window,
_cx: &mut Context<Self>,
) {
}
fn to_follow_event(_event: &Self::Event) -> Option<workspace::item::FollowEvent> {
None
}
fn dedup(&self, existing: &Self, _window: &Window, cx: &App) -> Option<workspace::item::Dedup> {
if existing.session_id(cx) == self.session_id(cx) {
Some(item::Dedup::KeepExisting)
} else {
None
}
}
fn is_project_item(&self, _window: &Window, _cx: &App) -> bool {
true
}
}
impl Render for DebugSession {
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
match &self.mode {
DebugSessionState::Inert(inert_state) => {
inert_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
}
DebugSessionState::Starting(starting_state) => {
starting_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
}
DebugSessionState::Failed(failed_state) => {
failed_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
}
DebugSessionState::Running(running_state) => {
running_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
}
}
}
}

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))
}
}

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)
});
});
}
}
}

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()
}
}

View 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(),
))
})
}
}

View 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())
}
}

View 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())
}
}

View 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 {}

View 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,
}
}

View file

@ -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(),
)
}
}

View file

@ -0,0 +1,75 @@
use gpui::{Entity, TestAppContext, WindowHandle};
use project::Project;
use settings::SettingsStore;
use terminal_view::terminal_panel::TerminalPanel;
use workspace::Workspace;
use crate::{debugger_panel::DebugPanel, session::DebugSession};
mod attach_modal;
mod console;
mod debugger_panel;
mod module_list;
mod stack_frame_list;
mod variable_list;
pub fn init_test(cx: &mut gpui::TestAppContext) {
if std::env::var("RUST_LOG").is_ok() {
env_logger::try_init().ok();
}
cx.update(|cx| {
let settings = SettingsStore::test(cx);
cx.set_global(settings);
terminal_view::init(cx);
theme::init(theme::LoadThemes::JustBase, cx);
command_palette_hooks::init(cx);
language::init(cx);
workspace::init_settings(cx);
Project::init_settings(cx);
editor::init(cx);
crate::init(cx);
});
}
pub async fn init_test_workspace(
project: &Entity<Project>,
cx: &mut TestAppContext,
) -> WindowHandle<Workspace> {
let workspace_handle =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let debugger_panel = workspace_handle
.update(cx, |_, window, cx| cx.spawn_in(window, DebugPanel::load))
.unwrap()
.await
.expect("Failed to load debug panel");
let terminal_panel = workspace_handle
.update(cx, |_, window, cx| cx.spawn_in(window, TerminalPanel::load))
.unwrap()
.await
.expect("Failed to load terminal panel");
workspace_handle
.update(cx, |workspace, window, cx| {
workspace.add_panel(debugger_panel, window, cx);
workspace.add_panel(terminal_panel, window, cx);
})
.unwrap();
workspace_handle
}
pub fn active_debug_session_panel(
workspace: WindowHandle<Workspace>,
cx: &mut TestAppContext,
) -> Entity<DebugSession> {
workspace
.update(cx, |workspace, _window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
debug_panel
.update(cx, |this, cx| this.active_session(cx))
.unwrap()
})
.unwrap()
}

View file

@ -0,0 +1,136 @@
use crate::*;
use attach_modal::AttachModal;
use dap::client::SessionId;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use menu::Confirm;
use project::{FakeFs, Project};
use serde_json::json;
use task::AttachConfig;
use tests::{init_test, init_test_workspace};
#[gpui::test]
async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
"/project",
json!({
"main.rs": "First line\nSecond line\nThird line\nFourth line",
}),
)
.await;
let project = Project::test(fs, ["/project".as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(
dap::DebugRequestType::Attach(AttachConfig {
process_id: Some(10),
}),
None,
None,
),
cx,
)
});
let session = task.await.unwrap();
cx.run_until_parked();
// assert we didn't show the attach modal
workspace
.update(cx, |workspace, _window, cx| {
assert!(workspace.active_modal::<AttachModal>(cx).is_none());
})
.unwrap();
let shutdown_session = project.update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
dap_store.shutdown_session(session.read(cx).session_id(), cx)
})
});
shutdown_session.await.unwrap();
}
#[gpui::test]
async fn test_show_attach_modal_and_select_process(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
"/project",
json!({
"main.rs": "First line\nSecond line\nThird line\nFourth line",
}),
)
.await;
let project = Project::test(fs, ["/project".as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let attach_modal = workspace
.update(cx, |workspace, window, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
AttachModal::new(
project.clone(),
dap::test_config(
dap::DebugRequestType::Attach(AttachConfig { process_id: None }),
None,
None,
),
window,
cx,
)
});
workspace.active_modal::<AttachModal>(cx).unwrap()
})
.unwrap();
cx.run_until_parked();
// assert we got the expected processes
workspace
.update(cx, |_, _, cx| {
let names =
attach_modal.update(cx, |modal, cx| attach_modal::process_names(&modal, cx));
// we filtered out all processes that are not the current process(zed itself)
assert_eq!(1, names.len());
})
.unwrap();
// select the only existing process
cx.dispatch_action(Confirm);
cx.run_until_parked();
// assert attach modal was dismissed
workspace
.update(cx, |workspace, _window, cx| {
assert!(workspace.active_modal::<AttachModal>(cx).is_none());
})
.unwrap();
let shutdown_session = project.update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
let session = dap_store.session_by_id(SessionId(0)).unwrap();
dap_store.shutdown_session(session.read(cx).session_id(), cx)
})
});
shutdown_session.await.unwrap();
}

View file

@ -0,0 +1,916 @@
use crate::{tests::active_debug_session_panel, *};
use dap::requests::StackTrace;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
use serde_json::json;
use tests::{init_test, init_test_workspace};
#[gpui::test]
async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
"/project",
json!({
"main.rs": "First line\nSecond line\nThird line\nFourth line",
}),
)
.await;
let project = Project::test(fs, ["/project".as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
workspace
.update(cx, |workspace, window, cx| {
workspace.focus_panel::<DebugPanel>(window, cx);
})
.unwrap();
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(dap::DebugRequestType::Launch, None, None),
cx,
)
});
let session = task.await.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client
.on_request::<StackTrace, _>(move |_, _| {
Ok(dap::StackTraceResponse {
stack_frames: Vec::default(),
total_frames: None,
})
})
.await;
client
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
category: None,
output: "First console output line before thread stopped!".to_string(),
data: None,
variables_reference: None,
source: None,
line: None,
column: None,
group: None,
location_reference: None,
}))
.await;
client
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
category: Some(dap::OutputEventCategory::Stdout),
output: "First output line before thread stopped!".to_string(),
data: None,
variables_reference: None,
source: None,
line: None,
column: None,
group: None,
location_reference: None,
}))
.await;
client
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
reason: dap::StoppedEventReason::Pause,
description: None,
thread_id: Some(1),
preserve_focus_hint: None,
text: None,
all_threads_stopped: None,
hit_breakpoint_ids: None,
}))
.await;
cx.run_until_parked();
let running_state =
active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
cx.focus_self(window);
item.mode()
.as_running()
.expect("Session should be running by this point")
.clone()
});
running_state.update(cx, |state, cx| {
state.set_thread_item(session::ThreadItem::Console, cx);
cx.refresh_windows();
});
cx.run_until_parked();
// assert we have output from before the thread stopped
workspace
.update(cx, |workspace, _window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
let active_debug_session_panel = debug_panel
.update(cx, |this, cx| this.active_session(cx))
.unwrap();
assert_eq!(
"First console output line before thread stopped!\nFirst output line before thread stopped!\n",
active_debug_session_panel.read(cx).mode().as_running().unwrap().read(cx).console().read(cx).editor().read(cx).text(cx).as_str()
);
})
.unwrap();
client
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
category: Some(dap::OutputEventCategory::Stdout),
output: "Second output line after thread stopped!".to_string(),
data: None,
variables_reference: None,
source: None,
line: None,
column: None,
group: None,
location_reference: None,
}))
.await;
client
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
category: Some(dap::OutputEventCategory::Console),
output: "Second console output line after thread stopped!".to_string(),
data: None,
variables_reference: None,
source: None,
line: None,
column: None,
group: None,
location_reference: None,
}))
.await;
cx.run_until_parked();
running_state.update(cx, |state, cx| {
state.set_thread_item(session::ThreadItem::Console, cx);
cx.refresh_windows();
});
cx.run_until_parked();
// assert we have output from before and after the thread stopped
workspace
.update(cx, |workspace, _window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
let active_session_panel = debug_panel
.update(cx, |this, cx| this.active_session(cx))
.unwrap();
assert_eq!(
"First console output line before thread stopped!\nFirst output line before thread stopped!\nSecond output line after thread stopped!\nSecond console output line after thread stopped!\n",
active_session_panel.read(cx).mode().as_running().unwrap().read(cx).console().read(cx).editor().read(cx).text(cx).as_str()
);
})
.unwrap();
let shutdown_session = project.update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
dap_store.shutdown_session(session.read(cx).session_id(), cx)
})
});
shutdown_session.await.unwrap();
}
// #[gpui::test]
// async fn test_grouped_output(executor: BackgroundExecutor, cx: &mut TestAppContext) {
// init_test(cx);
// let fs = FakeFs::new(executor.clone());
// fs.insert_tree(
// "/project",
// json!({
// "main.rs": "First line\nSecond line\nThird line\nFourth line",
// }),
// )
// .await;
// let project = Project::test(fs, ["/project".as_ref()], cx).await;
// let workspace = init_test_workspace(&project, cx).await;
// let cx = &mut VisualTestContext::from_window(*workspace, cx);
// let task = project.update(cx, |project, cx| {
// project.start_debug_session(
// dap::test_config(dap::DebugRequestType::Launch, None, None),
// cx,
// )
// });
// let session = task.await.unwrap();
// let client = session.update(cx, |session, _| session.adapter_client().unwrap());
// client
// .on_request::<StackTrace, _>(move |_, _| {
// Ok(dap::StackTraceResponse {
// stack_frames: Vec::default(),
// total_frames: None,
// })
// })
// .await;
// client
// .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
// reason: dap::StoppedEventReason::Pause,
// description: None,
// thread_id: Some(1),
// preserve_focus_hint: None,
// text: None,
// all_threads_stopped: None,
// hit_breakpoint_ids: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: None,
// output: "First line".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: None,
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "First group".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: Some(dap::OutputEventGroup::Start),
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "First item in group 1".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: None,
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "Second item in group 1".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: None,
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "Second group".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: Some(dap::OutputEventGroup::Start),
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "First item in group 2".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: None,
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "Second item in group 2".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: None,
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "End group 2".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: Some(dap::OutputEventGroup::End),
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "Third group".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: Some(dap::OutputEventGroup::StartCollapsed),
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "First item in group 3".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: None,
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "Second item in group 3".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: None,
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "End group 3".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: Some(dap::OutputEventGroup::End),
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "Third item in group 1".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: None,
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "Second item".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: Some(dap::OutputEventGroup::End),
// location_reference: None,
// }))
// .await;
// cx.run_until_parked();
// active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
// debug_panel_item
// .mode()
// .as_running()
// .unwrap()
// .update(cx, |running_state, cx| {
// running_state.console().update(cx, |console, cx| {
// console.editor().update(cx, |editor, cx| {
// pretty_assertions::assert_eq!(
// "
// First line
// First group
// First item in group 1
// Second item in group 1
// Second group
// First item in group 2
// Second item in group 2
// End group 2
// ⋯ End group 3
// Third item in group 1
// Second item
// "
// .unindent(),
// editor.display_text(cx)
// );
// })
// });
// });
// });
// let shutdown_session = project.update(cx, |project, cx| {
// project.dap_store().update(cx, |dap_store, cx| {
// dap_store.shutdown_session(session.read(cx).session_id(), cx)
// })
// });
// shutdown_session.await.unwrap();
// }
// todo(debugger): enable this again
// #[gpui::test]
// async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestAppContext) {
// init_test(cx);
// const NEW_VALUE: &str = "{nested1: \"Nested 1 updated\", nested2: \"Nested 2 updated\"}";
// let called_evaluate = Arc::new(AtomicBool::new(false));
// let fs = FakeFs::new(executor.clone());
// let test_file_content = r#"
// const variable1 = {
// nested1: "Nested 1",
// nested2: "Nested 2",
// };
// const variable2 = "Value 2";
// const variable3 = "Value 3";
// "#
// .unindent();
// fs.insert_tree(
// "/project",
// json!({
// "src": {
// "test.js": test_file_content,
// }
// }),
// )
// .await;
// let project = Project::test(fs, ["/project".as_ref()], cx).await;
// let workspace = init_test_workspace(&project, cx).await;
// let cx = &mut VisualTestContext::from_window(*workspace, cx);
// let task = project.update(cx, |project, cx| {
// project.start_debug_session(dap::test_config(None), cx)
// });
// let session = task.await.unwrap();
// let client = session.update(cx, |session, _| session.adapter_client().unwrap());
// client
// .on_request::<Threads, _>(move |_, _| {
// Ok(dap::ThreadsResponse {
// threads: vec![dap::Thread {
// id: 1,
// name: "Thread 1".into(),
// }],
// })
// })
// .await;
// let stack_frames = vec![StackFrame {
// id: 1,
// name: "Stack Frame 1".into(),
// source: Some(dap::Source {
// name: Some("test.js".into()),
// path: Some("/project/src/test.js".into()),
// source_reference: None,
// presentation_hint: None,
// origin: None,
// sources: None,
// adapter_data: None,
// checksums: None,
// }),
// line: 3,
// column: 1,
// end_line: None,
// end_column: None,
// can_restart: None,
// instruction_pointer_reference: None,
// module_id: None,
// presentation_hint: None,
// }];
// client
// .on_request::<StackTrace, _>({
// let stack_frames = Arc::new(stack_frames.clone());
// move |_, args| {
// assert_eq!(1, args.thread_id);
// Ok(dap::StackTraceResponse {
// stack_frames: (*stack_frames).clone(),
// total_frames: None,
// })
// }
// })
// .await;
// let scopes = vec![
// Scope {
// name: "Scope 1".into(),
// presentation_hint: None,
// variables_reference: 2,
// named_variables: None,
// indexed_variables: None,
// expensive: false,
// source: None,
// line: None,
// column: None,
// end_line: None,
// end_column: None,
// },
// Scope {
// name: "Scope 2".into(),
// presentation_hint: None,
// variables_reference: 4,
// named_variables: None,
// indexed_variables: None,
// expensive: false,
// source: None,
// line: None,
// column: None,
// end_line: None,
// end_column: None,
// },
// ];
// client
// .on_request::<Scopes, _>({
// let scopes = Arc::new(scopes.clone());
// move |_, args| {
// assert_eq!(1, args.frame_id);
// Ok(dap::ScopesResponse {
// scopes: (*scopes).clone(),
// })
// }
// })
// .await;
// let scope1_variables = Arc::new(Mutex::new(vec![
// Variable {
// name: "variable1".into(),
// value: "{nested1: \"Nested 1\", nested2: \"Nested 2\"}".into(),
// type_: None,
// presentation_hint: None,
// evaluate_name: None,
// variables_reference: 3,
// named_variables: None,
// indexed_variables: None,
// memory_reference: None,
// declaration_location_reference: None,
// value_location_reference: None,
// },
// Variable {
// name: "variable2".into(),
// value: "Value 2".into(),
// type_: None,
// presentation_hint: None,
// evaluate_name: None,
// variables_reference: 0,
// named_variables: None,
// indexed_variables: None,
// memory_reference: None,
// declaration_location_reference: None,
// value_location_reference: None,
// },
// ]));
// let nested_variables = vec![
// Variable {
// name: "nested1".into(),
// value: "Nested 1".into(),
// type_: None,
// presentation_hint: None,
// evaluate_name: None,
// variables_reference: 0,
// named_variables: None,
// indexed_variables: None,
// memory_reference: None,
// declaration_location_reference: None,
// value_location_reference: None,
// },
// Variable {
// name: "nested2".into(),
// value: "Nested 2".into(),
// type_: None,
// presentation_hint: None,
// evaluate_name: None,
// variables_reference: 0,
// named_variables: None,
// indexed_variables: None,
// memory_reference: None,
// declaration_location_reference: None,
// value_location_reference: None,
// },
// ];
// let scope2_variables = vec![Variable {
// name: "variable3".into(),
// value: "Value 3".into(),
// type_: None,
// presentation_hint: None,
// evaluate_name: None,
// variables_reference: 0,
// named_variables: None,
// indexed_variables: None,
// memory_reference: None,
// declaration_location_reference: None,
// value_location_reference: None,
// }];
// client
// .on_request::<Variables, _>({
// let scope1_variables = scope1_variables.clone();
// let nested_variables = Arc::new(nested_variables.clone());
// let scope2_variables = Arc::new(scope2_variables.clone());
// move |_, args| match args.variables_reference {
// 4 => Ok(dap::VariablesResponse {
// variables: (*scope2_variables).clone(),
// }),
// 3 => Ok(dap::VariablesResponse {
// variables: (*nested_variables).clone(),
// }),
// 2 => Ok(dap::VariablesResponse {
// variables: scope1_variables.lock().unwrap().clone(),
// }),
// id => unreachable!("unexpected variables reference {id}"),
// }
// })
// .await;
// client
// .on_request::<Evaluate, _>({
// let called_evaluate = called_evaluate.clone();
// let scope1_variables = scope1_variables.clone();
// move |_, args| {
// called_evaluate.store(true, Ordering::SeqCst);
// assert_eq!(format!("$variable1 = {}", NEW_VALUE), args.expression);
// assert_eq!(Some(1), args.frame_id);
// assert_eq!(Some(dap::EvaluateArgumentsContext::Variables), args.context);
// scope1_variables.lock().unwrap()[0].value = NEW_VALUE.to_string();
// Ok(dap::EvaluateResponse {
// result: NEW_VALUE.into(),
// type_: None,
// presentation_hint: None,
// variables_reference: 0,
// named_variables: None,
// indexed_variables: None,
// memory_reference: None,
// value_location_reference: None,
// })
// }
// })
// .await;
// client
// .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
// reason: dap::StoppedEventReason::Pause,
// description: None,
// thread_id: Some(1),
// preserve_focus_hint: None,
// text: None,
// all_threads_stopped: None,
// hit_breakpoint_ids: None,
// }))
// .await;
// cx.run_until_parked();
// // toggle nested variables for scope 1
// active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
// debug_panel_item
// .mode()
// .as_running()
// .unwrap()
// .update(cx, |running_state, cx| {
// running_state
// .variable_list()
// .update(cx, |variable_list, cx| {
// variable_list.toggle_variable(
// &VariablePath {
// indices: Arc::from([scopes[0].variables_reference]),
// },
// cx,
// );
// });
// });
// });
// cx.run_until_parked();
// active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
// debug_panel_item
// .mode()
// .as_running()
// .unwrap()
// .update(cx, |running_state, cx| {
// running_state.console().update(cx, |console, item_cx| {
// console
// .query_bar()
// .update(item_cx, |query_bar, console_cx| {
// query_bar.set_text(
// format!("$variable1 = {}", NEW_VALUE),
// window,
// console_cx,
// );
// });
// console.evaluate(&menu::Confirm, window, item_cx);
// });
// });
// });
// cx.run_until_parked();
// active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
// assert_eq!(
// "",
// debug_panel_item
// .mode()
// .as_running()
// .unwrap()
// .read(cx)
// .console()
// .read(cx)
// .query_bar()
// .read(cx)
// .text(cx)
// .as_str()
// );
// assert_eq!(
// format!("{}\n", NEW_VALUE),
// debug_panel_item
// .mode()
// .as_running()
// .unwrap()
// .read(cx)
// .console()
// .read(cx)
// .editor()
// .read(cx)
// .text(cx)
// .as_str()
// );
// debug_panel_item
// .mode()
// .as_running()
// .unwrap()
// .update(cx, |running_state, cx| {
// running_state
// .variable_list()
// .update(cx, |variable_list, _| {
// let scope1_variables = scope1_variables.lock().unwrap().clone();
// // scope 1
// // assert_eq!(
// // vec![
// // VariableContainer {
// // container_reference: scopes[0].variables_reference,
// // variable: scope1_variables[0].clone(),
// // depth: 1,
// // },
// // VariableContainer {
// // container_reference: scope1_variables[0].variables_reference,
// // variable: nested_variables[0].clone(),
// // depth: 2,
// // },
// // VariableContainer {
// // container_reference: scope1_variables[0].variables_reference,
// // variable: nested_variables[1].clone(),
// // depth: 2,
// // },
// // VariableContainer {
// // container_reference: scopes[0].variables_reference,
// // variable: scope1_variables[1].clone(),
// // depth: 1,
// // },
// // ],
// // variable_list.variables_by_scope(1, 2).unwrap().variables()
// // );
// // scope 2
// // assert_eq!(
// // vec![VariableContainer {
// // container_reference: scopes[1].variables_reference,
// // variable: scope2_variables[0].clone(),
// // depth: 1,
// // }],
// // variable_list.variables_by_scope(1, 4).unwrap().variables()
// // );
// variable_list.assert_visual_entries(vec![
// "v Scope 1",
// " v variable1",
// " > nested1",
// " > nested2",
// " > variable2",
// ]);
// // assert visual entries
// // assert_eq!(
// // vec![
// // VariableListEntry::Scope(scopes[0].clone()),
// // VariableListEntry::Variable {
// // depth: 1,
// // scope: Arc::new(scopes[0].clone()),
// // has_children: true,
// // variable: Arc::new(scope1_variables[0].clone()),
// // container_reference: scopes[0].variables_reference,
// // },
// // VariableListEntry::Variable {
// // depth: 2,
// // scope: Arc::new(scopes[0].clone()),
// // has_children: false,
// // variable: Arc::new(nested_variables[0].clone()),
// // container_reference: scope1_variables[0].variables_reference,
// // },
// // VariableListEntry::Variable {
// // depth: 2,
// // scope: Arc::new(scopes[0].clone()),
// // has_children: false,
// // variable: Arc::new(nested_variables[1].clone()),
// // container_reference: scope1_variables[0].variables_reference,
// // },
// // VariableListEntry::Variable {
// // depth: 1,
// // scope: Arc::new(scopes[0].clone()),
// // has_children: false,
// // variable: Arc::new(scope1_variables[1].clone()),
// // container_reference: scopes[0].variables_reference,
// // },
// // VariableListEntry::Scope(scopes[1].clone()),
// // ],
// // variable_list.entries().get(&1).unwrap().clone()
// // );
// });
// });
// });
// assert!(
// called_evaluate.load(std::sync::atomic::Ordering::SeqCst),
// "Expected evaluate request to be called"
// );
// let shutdown_session = project.update(cx, |project, cx| {
// project.dap_store().update(cx, |dap_store, cx| {
// dap_store.shutdown_session(&session.read(cx).session_id(), cx)
// })
// });
// shutdown_session.await.unwrap();
// }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,262 @@
use crate::{
debugger_panel::DebugPanel,
session::ThreadItem,
tests::{active_debug_session_panel, init_test, init_test_workspace},
};
use dap::{
requests::{Modules, StackTrace, Threads},
DebugRequestType, StoppedEvent,
};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
use std::sync::{
atomic::{AtomicBool, AtomicI32, Ordering},
Arc,
};
#[gpui::test]
async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
let project = Project::test(fs, ["/project".as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
workspace
.update(cx, |workspace, window, cx| {
workspace.focus_panel::<DebugPanel>(window, cx);
})
.unwrap();
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(
DebugRequestType::Launch,
None,
Some(dap::Capabilities {
supports_modules_request: Some(true),
..Default::default()
}),
),
cx,
)
});
let session = task.await.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client
.on_request::<StackTrace, _>(move |_, args| {
assert!(args.thread_id == 1);
Ok(dap::StackTraceResponse {
stack_frames: Vec::default(),
total_frames: None,
})
})
.await;
let called_modules = Arc::new(AtomicBool::new(false));
let modules = vec![
dap::Module {
id: dap::ModuleId::Number(1),
name: "First Module".into(),
address_range: None,
date_time_stamp: None,
path: None,
symbol_file_path: None,
symbol_status: None,
version: None,
is_optimized: None,
is_user_code: None,
},
dap::Module {
id: dap::ModuleId::Number(2),
name: "Second Module".into(),
address_range: None,
date_time_stamp: None,
path: None,
symbol_file_path: None,
symbol_status: None,
version: None,
is_optimized: None,
is_user_code: None,
},
];
client
.on_request::<Threads, _>(move |_, _| {
Ok(dap::ThreadsResponse {
threads: vec![dap::Thread {
id: 1,
name: "Thread 1".into(),
}],
})
})
.await;
client
.on_request::<Modules, _>({
let called_modules = called_modules.clone();
let modules_request_count = AtomicI32::new(0);
let modules = modules.clone();
move |_, _| {
modules_request_count.fetch_add(1, Ordering::SeqCst);
assert_eq!(
1,
modules_request_count.load(Ordering::SeqCst),
"This request should only be called once from the host"
);
called_modules.store(true, Ordering::SeqCst);
Ok(dap::ModulesResponse {
modules: modules.clone(),
total_modules: Some(2u64),
})
}
})
.await;
client
.fake_event(dap::messages::Events::Stopped(StoppedEvent {
reason: dap::StoppedEventReason::Pause,
description: None,
thread_id: Some(1),
preserve_focus_hint: None,
text: None,
all_threads_stopped: None,
hit_breakpoint_ids: None,
}))
.await;
cx.run_until_parked();
let running_state =
active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
cx.focus_self(window);
item.mode()
.as_running()
.expect("Session should be running by this point")
.clone()
});
assert!(
!called_modules.load(std::sync::atomic::Ordering::SeqCst),
"Request Modules shouldn't be called before it's needed"
);
running_state.update(cx, |state, cx| {
state.set_thread_item(ThreadItem::Modules, cx);
cx.refresh_windows();
});
cx.run_until_parked();
assert!(
called_modules.load(std::sync::atomic::Ordering::SeqCst),
"Request Modules should be called because a user clicked on the module list"
);
active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
running_state.update(cx, |state, cx| {
state.set_thread_item(ThreadItem::Modules, cx)
});
let actual_modules = running_state.update(cx, |state, cx| {
state.module_list().update(cx, |list, cx| list.modules(cx))
});
assert_eq!(modules, actual_modules);
});
// Test all module events now
// New Module
// Changed
// Removed
let new_module = dap::Module {
id: dap::ModuleId::Number(3),
name: "Third Module".into(),
address_range: None,
date_time_stamp: None,
path: None,
symbol_file_path: None,
symbol_status: None,
version: None,
is_optimized: None,
is_user_code: None,
};
client
.fake_event(dap::messages::Events::Module(dap::ModuleEvent {
reason: dap::ModuleEventReason::New,
module: new_module.clone(),
}))
.await;
cx.run_until_parked();
active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
let actual_modules = running_state.update(cx, |state, cx| {
state.module_list().update(cx, |list, cx| list.modules(cx))
});
assert_eq!(actual_modules.len(), 3);
assert!(actual_modules.contains(&new_module));
});
let changed_module = dap::Module {
id: dap::ModuleId::Number(2),
name: "Modified Second Module".into(),
address_range: None,
date_time_stamp: None,
path: None,
symbol_file_path: None,
symbol_status: None,
version: None,
is_optimized: None,
is_user_code: None,
};
client
.fake_event(dap::messages::Events::Module(dap::ModuleEvent {
reason: dap::ModuleEventReason::Changed,
module: changed_module.clone(),
}))
.await;
cx.run_until_parked();
active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
let actual_modules = running_state.update(cx, |state, cx| {
state.module_list().update(cx, |list, cx| list.modules(cx))
});
assert_eq!(actual_modules.len(), 3);
assert!(actual_modules.contains(&changed_module));
});
client
.fake_event(dap::messages::Events::Module(dap::ModuleEvent {
reason: dap::ModuleEventReason::Removed,
module: changed_module.clone(),
}))
.await;
cx.run_until_parked();
active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
let actual_modules = running_state.update(cx, |state, cx| {
state.module_list().update(cx, |list, cx| list.modules(cx))
});
assert_eq!(actual_modules.len(), 2);
assert!(!actual_modules.contains(&changed_module));
});
let shutdown_session = project.update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
dap_store.shutdown_session(session.read(cx).session_id(), cx)
})
});
shutdown_session.await.unwrap();
}

View file

@ -0,0 +1,845 @@
use crate::{
debugger_panel::DebugPanel,
session::running::stack_frame_list::StackFrameEntry,
tests::{active_debug_session_panel, init_test, init_test_workspace},
};
use dap::{
requests::{StackTrace, Threads},
StackFrame,
};
use editor::{Editor, ToPoint as _};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
use serde_json::json;
use std::sync::Arc;
use unindent::Unindent as _;
use util::path;
#[gpui::test]
async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
let test_file_content = r#"
import { SOME_VALUE } './module.js';
console.log(SOME_VALUE);
"#
.unindent();
let module_file_content = r#"
export SOME_VALUE = 'some value';
"#
.unindent();
fs.insert_tree(
path!("/project"),
json!({
"src": {
"test.js": test_file_content,
"module.js": module_file_content,
}
}),
)
.await;
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(dap::DebugRequestType::Launch, None, None),
cx,
)
});
let session = task.await.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client
.on_request::<Threads, _>(move |_, _| {
Ok(dap::ThreadsResponse {
threads: vec![dap::Thread {
id: 1,
name: "Thread 1".into(),
}],
})
})
.await;
let stack_frames = vec![
StackFrame {
id: 1,
name: "Stack Frame 1".into(),
source: Some(dap::Source {
name: Some("test.js".into()),
path: Some(path!("/project/src/test.js").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: 3,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: None,
},
StackFrame {
id: 2,
name: "Stack Frame 2".into(),
source: Some(dap::Source {
name: Some("module.js".into()),
path: Some(path!("/project/src/module.js").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: 1,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: None,
},
];
client
.on_request::<StackTrace, _>({
let stack_frames = Arc::new(stack_frames.clone());
move |_, args| {
assert_eq!(1, args.thread_id);
Ok(dap::StackTraceResponse {
stack_frames: (*stack_frames).clone(),
total_frames: None,
})
}
})
.await;
client
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
reason: dap::StoppedEventReason::Pause,
description: None,
thread_id: Some(1),
preserve_focus_hint: None,
text: None,
all_threads_stopped: None,
hit_breakpoint_ids: None,
}))
.await;
cx.run_until_parked();
// trigger to load threads
active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
session
.mode()
.as_running()
.unwrap()
.update(cx, |running_state, cx| {
running_state
.session()
.update(cx, |session, cx| session.threads(cx));
});
});
cx.run_until_parked();
// select first thread
active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
session
.mode()
.as_running()
.unwrap()
.update(cx, |running_state, cx| {
running_state.select_current_thread(
&running_state
.session()
.update(cx, |session, cx| session.threads(cx)),
cx,
);
});
});
cx.run_until_parked();
active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
let stack_frame_list = session
.mode()
.as_running()
.unwrap()
.update(cx, |state, _| state.stack_frame_list().clone());
stack_frame_list.update(cx, |stack_frame_list, cx| {
assert_eq!(Some(1), stack_frame_list.current_stack_frame_id());
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
});
});
let shutdown_session = project.update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
dap_store.shutdown_session(session.read(cx).session_id(), cx)
})
});
shutdown_session.await.unwrap();
}
#[gpui::test]
async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
let test_file_content = r#"
import { SOME_VALUE } './module.js';
console.log(SOME_VALUE);
"#
.unindent();
let module_file_content = r#"
export SOME_VALUE = 'some value';
"#
.unindent();
fs.insert_tree(
path!("/project"),
json!({
"src": {
"test.js": test_file_content,
"module.js": module_file_content,
}
}),
)
.await;
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let _ = workspace.update(cx, |workspace, window, cx| {
workspace.toggle_dock(workspace::dock::DockPosition::Bottom, window, cx);
});
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(dap::DebugRequestType::Launch, None, None),
cx,
)
});
let session = task.await.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client
.on_request::<Threads, _>(move |_, _| {
Ok(dap::ThreadsResponse {
threads: vec![dap::Thread {
id: 1,
name: "Thread 1".into(),
}],
})
})
.await;
let stack_frames = vec![
StackFrame {
id: 1,
name: "Stack Frame 1".into(),
source: Some(dap::Source {
name: Some("test.js".into()),
path: Some(path!("/project/src/test.js").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: 3,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: None,
},
StackFrame {
id: 2,
name: "Stack Frame 2".into(),
source: Some(dap::Source {
name: Some("module.js".into()),
path: Some(path!("/project/src/module.js").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: 1,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: None,
},
];
client
.on_request::<StackTrace, _>({
let stack_frames = Arc::new(stack_frames.clone());
move |_, args| {
assert_eq!(1, args.thread_id);
Ok(dap::StackTraceResponse {
stack_frames: (*stack_frames).clone(),
total_frames: None,
})
}
})
.await;
client
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
reason: dap::StoppedEventReason::Pause,
description: None,
thread_id: Some(1),
preserve_focus_hint: None,
text: None,
all_threads_stopped: None,
hit_breakpoint_ids: None,
}))
.await;
cx.run_until_parked();
// trigger threads to load
active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
session
.mode()
.as_running()
.unwrap()
.update(cx, |running_state, cx| {
running_state
.session()
.update(cx, |session, cx| session.threads(cx));
});
});
cx.run_until_parked();
// select first thread
active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
session
.mode()
.as_running()
.unwrap()
.update(cx, |running_state, cx| {
running_state.select_current_thread(
&running_state
.session()
.update(cx, |session, cx| session.threads(cx)),
cx,
);
});
});
cx.run_until_parked();
workspace
.update(cx, |workspace, window, cx| {
let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
assert_eq!(1, editors.len());
let project_path = editors[0]
.update(cx, |editor, cx| editor.project_path(cx))
.unwrap();
let expected = if cfg!(target_os = "windows") {
"src\\test.js"
} else {
"src/test.js"
};
assert_eq!(expected, project_path.path.to_string_lossy());
assert_eq!(test_file_content, editors[0].read(cx).text(cx));
assert_eq!(
vec![2..3],
editors[0].update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
editor
.highlighted_rows::<editor::DebugCurrentRowHighlight>()
.map(|(range, _)| {
let start = range.start.to_point(&snapshot.buffer_snapshot);
let end = range.end.to_point(&snapshot.buffer_snapshot);
start.row..end.row
})
.collect::<Vec<_>>()
})
);
})
.unwrap();
let stack_frame_list = workspace
.update(cx, |workspace, _window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
let active_debug_panel_item = debug_panel
.update(cx, |this, cx| this.active_session(cx))
.unwrap();
active_debug_panel_item
.read(cx)
.mode()
.as_running()
.unwrap()
.read(cx)
.stack_frame_list()
.clone()
})
.unwrap();
stack_frame_list.update(cx, |stack_frame_list, cx| {
assert_eq!(Some(1), stack_frame_list.current_stack_frame_id());
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
});
// select second stack frame
stack_frame_list
.update_in(cx, |stack_frame_list, window, cx| {
stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx)
})
.await
.unwrap();
cx.run_until_parked();
stack_frame_list.update(cx, |stack_frame_list, cx| {
assert_eq!(Some(2), stack_frame_list.current_stack_frame_id());
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
});
let _ = workspace.update(cx, |workspace, window, cx| {
let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
assert_eq!(1, editors.len());
let project_path = editors[0]
.update(cx, |editor, cx| editor.project_path(cx))
.unwrap();
let expected = if cfg!(target_os = "windows") {
"src\\module.js"
} else {
"src/module.js"
};
assert_eq!(expected, project_path.path.to_string_lossy());
assert_eq!(module_file_content, editors[0].read(cx).text(cx));
assert_eq!(
vec![0..1],
editors[0].update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
editor
.highlighted_rows::<editor::DebugCurrentRowHighlight>()
.map(|(range, _)| {
let start = range.start.to_point(&snapshot.buffer_snapshot);
let end = range.end.to_point(&snapshot.buffer_snapshot);
start.row..end.row
})
.collect::<Vec<_>>()
})
);
});
let shutdown_session = project.update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
dap_store.shutdown_session(session.read(cx).session_id(), cx)
})
});
shutdown_session.await.unwrap();
}
#[gpui::test]
async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
let test_file_content = r#"
import { SOME_VALUE } './module.js';
console.log(SOME_VALUE);
"#
.unindent();
let module_file_content = r#"
export SOME_VALUE = 'some value';
"#
.unindent();
fs.insert_tree(
path!("/project"),
json!({
"src": {
"test.js": test_file_content,
"module.js": module_file_content,
}
}),
)
.await;
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(dap::DebugRequestType::Launch, None, None),
cx,
)
});
let session = task.await.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client
.on_request::<Threads, _>(move |_, _| {
Ok(dap::ThreadsResponse {
threads: vec![dap::Thread {
id: 1,
name: "Thread 1".into(),
}],
})
})
.await;
let stack_frames = vec![
StackFrame {
id: 1,
name: "Stack Frame 1".into(),
source: Some(dap::Source {
name: Some("test.js".into()),
path: Some(path!("/project/src/test.js").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: 3,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: None,
},
StackFrame {
id: 2,
name: "Stack Frame 2".into(),
source: Some(dap::Source {
name: Some("module.js".into()),
path: Some(path!("/project/src/module.js").into()),
source_reference: None,
presentation_hint: None,
origin: Some("ignored".into()),
sources: None,
adapter_data: None,
checksums: None,
}),
line: 1,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
},
StackFrame {
id: 3,
name: "Stack Frame 3".into(),
source: Some(dap::Source {
name: Some("module.js".into()),
path: Some(path!("/project/src/module.js").into()),
source_reference: None,
presentation_hint: None,
origin: Some("ignored".into()),
sources: None,
adapter_data: None,
checksums: None,
}),
line: 1,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
},
StackFrame {
id: 4,
name: "Stack Frame 4".into(),
source: Some(dap::Source {
name: Some("module.js".into()),
path: Some(path!("/project/src/module.js").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: 1,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: None,
},
StackFrame {
id: 5,
name: "Stack Frame 5".into(),
source: Some(dap::Source {
name: Some("module.js".into()),
path: Some(path!("/project/src/module.js").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: 1,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
},
StackFrame {
id: 6,
name: "Stack Frame 6".into(),
source: Some(dap::Source {
name: Some("module.js".into()),
path: Some(path!("/project/src/module.js").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: 1,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
},
StackFrame {
id: 7,
name: "Stack Frame 7".into(),
source: Some(dap::Source {
name: Some("module.js".into()),
path: Some(path!("/project/src/module.js").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: 1,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: None,
},
];
client
.on_request::<StackTrace, _>({
let stack_frames = Arc::new(stack_frames.clone());
move |_, args| {
assert_eq!(1, args.thread_id);
Ok(dap::StackTraceResponse {
stack_frames: (*stack_frames).clone(),
total_frames: None,
})
}
})
.await;
client
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
reason: dap::StoppedEventReason::Pause,
description: None,
thread_id: Some(1),
preserve_focus_hint: None,
text: None,
all_threads_stopped: None,
hit_breakpoint_ids: None,
}))
.await;
cx.run_until_parked();
// trigger threads to load
active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
session
.mode()
.as_running()
.unwrap()
.update(cx, |running_state, cx| {
running_state
.session()
.update(cx, |session, cx| session.threads(cx));
});
});
cx.run_until_parked();
// select first thread
active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
session
.mode()
.as_running()
.unwrap()
.update(cx, |running_state, cx| {
running_state.select_current_thread(
&running_state
.session()
.update(cx, |session, cx| session.threads(cx)),
cx,
);
});
});
cx.run_until_parked();
// trigger stack frames to loaded
active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
let stack_frame_list = debug_panel_item
.mode()
.as_running()
.unwrap()
.update(cx, |state, _| state.stack_frame_list().clone());
stack_frame_list.update(cx, |stack_frame_list, cx| {
stack_frame_list.dap_stack_frames(cx);
});
});
cx.run_until_parked();
active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
let stack_frame_list = debug_panel_item
.mode()
.as_running()
.unwrap()
.update(cx, |state, _| state.stack_frame_list().clone());
stack_frame_list.update(cx, |stack_frame_list, cx| {
stack_frame_list.build_entries(true, window, cx);
assert_eq!(
&vec![
StackFrameEntry::Normal(stack_frames[0].clone()),
StackFrameEntry::Collapsed(vec![
stack_frames[1].clone(),
stack_frames[2].clone()
]),
StackFrameEntry::Normal(stack_frames[3].clone()),
StackFrameEntry::Collapsed(vec![
stack_frames[4].clone(),
stack_frames[5].clone()
]),
StackFrameEntry::Normal(stack_frames[6].clone()),
],
stack_frame_list.entries()
);
stack_frame_list.expand_collapsed_entry(
1,
&vec![stack_frames[1].clone(), stack_frames[2].clone()],
cx,
);
assert_eq!(
&vec![
StackFrameEntry::Normal(stack_frames[0].clone()),
StackFrameEntry::Normal(stack_frames[1].clone()),
StackFrameEntry::Normal(stack_frames[2].clone()),
StackFrameEntry::Normal(stack_frames[3].clone()),
StackFrameEntry::Collapsed(vec![
stack_frames[4].clone(),
stack_frames[5].clone()
]),
StackFrameEntry::Normal(stack_frames[6].clone()),
],
stack_frame_list.entries()
);
stack_frame_list.expand_collapsed_entry(
4,
&vec![stack_frames[4].clone(), stack_frames[5].clone()],
cx,
);
assert_eq!(
&vec![
StackFrameEntry::Normal(stack_frames[0].clone()),
StackFrameEntry::Normal(stack_frames[1].clone()),
StackFrameEntry::Normal(stack_frames[2].clone()),
StackFrameEntry::Normal(stack_frames[3].clone()),
StackFrameEntry::Normal(stack_frames[4].clone()),
StackFrameEntry::Normal(stack_frames[5].clone()),
StackFrameEntry::Normal(stack_frames[6].clone()),
],
stack_frame_list.entries()
);
});
});
let shutdown_session = project.update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
dap_store.shutdown_session(session.read(cx).session_id(), cx)
})
});
shutdown_session.await.unwrap();
}

File diff suppressed because it is too large Load diff