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