logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree with a 3-file patch applied (no upstream history). Patch (crates/gpui_linux/src/linux/wayland/): - serial.rs: add SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; add latest_serial_of() - window.rs: activate() uses keyboard-enter serial (Mutter focus gate) Mutter honors window activation only when the token carries the keyboard- focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial. See docs/tray-window-focus-wayland.md in logiguard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
92
crates/debugger_ui/Cargo.toml
Normal file
92
crates/debugger_ui/Cargo.toml
Normal file
@@ -0,0 +1,92 @@
|
||||
[package]
|
||||
name = "debugger_ui"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/debugger_ui.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"dap/test-support",
|
||||
"dap_adapters/test-support",
|
||||
"debugger_tools/test-support",
|
||||
"editor/test-support",
|
||||
"gpui/test-support",
|
||||
"project/test-support",
|
||||
"util/test-support",
|
||||
"workspace/test-support",
|
||||
"unindent",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
alacritty_terminal.workspace = true
|
||||
anyhow.workspace = true
|
||||
bitflags.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
dap.workspace = true
|
||||
dap_adapters = { workspace = true, optional = true }
|
||||
db.workspace = true
|
||||
debugger_tools.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
file_icons.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
hex.workspace = true
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
notifications.workspace = true
|
||||
parking_lot.workspace = true
|
||||
parse_int.workspace = true
|
||||
paths.workspace = true
|
||||
picker.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
project.workspace = true
|
||||
rpc.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
sysinfo.workspace = true
|
||||
task.workspace = true
|
||||
tasks_ui.workspace = true
|
||||
terminal_view.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
theme_settings.workspace = true
|
||||
tree-sitter-json.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
unindent = { workspace = true, optional = true }
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
dap = { workspace = true, features = ["test-support"] }
|
||||
dap_adapters = { workspace = true, features = ["test-support"] }
|
||||
debugger_tools = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
tree-sitter-go.workspace = true
|
||||
unindent.workspace = true
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
zlog.workspace = true
|
||||
1
crates/debugger_ui/LICENSE-GPL
Symbolic link
1
crates/debugger_ui/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
439
crates/debugger_ui/src/attach_modal.rs
Normal file
439
crates/debugger_ui/src/attach_modal.rs
Normal file
@@ -0,0 +1,439 @@
|
||||
use dap::{DapRegistry, DebugRequest};
|
||||
use futures::channel::oneshot;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render, Task, TaskExt};
|
||||
use gpui::{Subscription, WeakEntity};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::Project;
|
||||
use rpc::proto;
|
||||
use task::ZedDebugConfig;
|
||||
use util::debug_panic;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind};
|
||||
use ui::{Context, Tooltip, prelude::*};
|
||||
use ui::{ListItem, ListItemSpacing};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::debugger_panel::DebugPanel;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct Candidate {
|
||||
pub(super) pid: u32,
|
||||
pub(super) name: SharedString,
|
||||
pub(super) command: Vec<String>,
|
||||
}
|
||||
|
||||
pub(crate) enum ModalIntent {
|
||||
ResolveProcessId(Option<oneshot::Sender<Option<i32>>>),
|
||||
AttachToProcess(ZedDebugConfig),
|
||||
}
|
||||
|
||||
pub(crate) struct AttachModalDelegate {
|
||||
selected_index: usize,
|
||||
matches: Vec<StringMatch>,
|
||||
placeholder_text: Arc<str>,
|
||||
pub(crate) intent: ModalIntent,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
candidates: Arc<[Candidate]>,
|
||||
}
|
||||
|
||||
impl AttachModalDelegate {
|
||||
fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
intent: ModalIntent,
|
||||
candidates: Arc<[Candidate]>,
|
||||
) -> Self {
|
||||
Self {
|
||||
workspace,
|
||||
candidates,
|
||||
intent,
|
||||
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(crate) fn new(
|
||||
intent: ModalIntent,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
modal: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let processes_task = get_processes_for_project(&project, cx);
|
||||
|
||||
let modal = Self::with_processes(workspace, Arc::new([]), modal, intent, window, cx);
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let processes = processes_task.await;
|
||||
this.update_in(cx, |modal, window, cx| {
|
||||
modal.picker.update(cx, |picker, cx| {
|
||||
picker.delegate.candidates = processes;
|
||||
picker.refresh(window, cx);
|
||||
});
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
modal
|
||||
}
|
||||
|
||||
pub(super) fn with_processes(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
processes: Arc<[Candidate]>,
|
||||
modal: bool,
|
||||
intent: ModalIntent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let picker = cx.new(|cx| {
|
||||
Picker::uniform_list(
|
||||
AttachModalDelegate::new(workspace, intent, processes),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.modal(modal)
|
||||
});
|
||||
Self {
|
||||
_subscription: cx.subscribe(&picker, |_, _, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
}),
|
||||
picker,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AttachModal {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
||||
v_flex()
|
||||
.key_context("AttachModal")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.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(async move |this, cx| {
|
||||
let Some(processes) = this
|
||||
.read_with(cx, |this, _| this.delegate.candidates.clone())
|
||||
.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,
|
||||
true,
|
||||
100,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
this.update(cx, |this, _| {
|
||||
let delegate = &mut this.delegate;
|
||||
|
||||
delegate.matches = matches;
|
||||
|
||||
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, _secondary: 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.get(ix)
|
||||
});
|
||||
|
||||
match &mut self.intent {
|
||||
ModalIntent::ResolveProcessId(sender) => {
|
||||
cx.emit(DismissEvent);
|
||||
|
||||
if let Some(sender) = sender.take() {
|
||||
sender
|
||||
.send(candidate.map(|candidate| candidate.pid as i32))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
ModalIntent::AttachToProcess(definition) => {
|
||||
let Some(candidate) = candidate else {
|
||||
return cx.emit(DismissEvent);
|
||||
};
|
||||
|
||||
match &mut definition.request {
|
||||
DebugRequest::Attach(config) => {
|
||||
config.process_id = Some(candidate.pid);
|
||||
}
|
||||
DebugRequest::Launch(_) => {
|
||||
debug_panic!("Debugger attach modal used on launch debug config");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(panel) = workspace
|
||||
.update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
|
||||
.ok()
|
||||
.flatten()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(adapter) = cx.read_global::<DapRegistry, _>(|registry, _| {
|
||||
registry.adapter(&definition.adapter)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let definition = definition.clone();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let Ok(scenario) = adapter.config_from_zed_format(definition).await else {
|
||||
return;
|
||||
};
|
||||
|
||||
panel
|
||||
.update_in(cx, |panel, window, cx| {
|
||||
panel.start_session(
|
||||
scenario,
|
||||
Default::default(),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
this.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
self.selected_index = 0;
|
||||
|
||||
match &mut self.intent {
|
||||
ModalIntent::ResolveProcessId(sender) => {
|
||||
if let Some(sender) = sender.take() {
|
||||
sender.send(None).ok();
|
||||
}
|
||||
}
|
||||
ModalIntent::AttachToProcess(_) => {}
|
||||
}
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
_: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let hit = &self.matches.get(ix)?;
|
||||
let candidate = self.candidates.get(hit.candidate_id)?;
|
||||
|
||||
Some(
|
||||
ListItem::new(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(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),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_processes_for_project(project: &Entity<Project>, cx: &mut App) -> Task<Arc<[Candidate]>> {
|
||||
let project = project.read(cx);
|
||||
|
||||
if let Some(remote_client) = project.remote_client() {
|
||||
let proto_client = remote_client.read(cx).proto_client();
|
||||
cx.background_spawn(async move {
|
||||
let response = proto_client
|
||||
.request(proto::GetProcesses {
|
||||
project_id: proto::REMOTE_SERVER_PROJECT_ID,
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|_| proto::GetProcessesResponse {
|
||||
processes: Vec::new(),
|
||||
});
|
||||
|
||||
let mut processes: Vec<Candidate> = response
|
||||
.processes
|
||||
.into_iter()
|
||||
.map(|p| Candidate {
|
||||
pid: p.pid,
|
||||
name: p.name.into(),
|
||||
command: p.command,
|
||||
})
|
||||
.collect();
|
||||
|
||||
processes.sort_by_key(|k| k.name.clone());
|
||||
Arc::from(processes.into_boxed_slice())
|
||||
})
|
||||
} else {
|
||||
let refresh_kind = RefreshKind::nothing().with_processes(
|
||||
ProcessRefreshKind::nothing()
|
||||
.without_tasks()
|
||||
.with_cmd(UpdateKind::Always),
|
||||
);
|
||||
let mut processes: Box<[_]> = System::new_with_specifics(refresh_kind)
|
||||
.processes()
|
||||
.values()
|
||||
.map(|process| {
|
||||
let name = process.name().to_string_lossy().into_owned();
|
||||
Candidate {
|
||||
name: name.into(),
|
||||
pid: process.pid().as_u32(),
|
||||
command: process
|
||||
.cmd()
|
||||
.iter()
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
processes.sort_by_key(|k| k.name.clone());
|
||||
let processes = processes.into_iter().collect();
|
||||
Task::ready(processes)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn set_candidates(
|
||||
modal: &AttachModal,
|
||||
candidates: Arc<[Candidate]>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<AttachModal>,
|
||||
) {
|
||||
modal.picker.update(cx, |picker, cx| {
|
||||
picker.delegate.candidates = candidates;
|
||||
picker.refresh(window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
|
||||
modal.picker.read_with(cx, |picker, _| {
|
||||
picker
|
||||
.delegate
|
||||
.matches
|
||||
.iter()
|
||||
.map(|hit| hit.string.clone())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
2009
crates/debugger_ui/src/debugger_panel.rs
Normal file
2009
crates/debugger_ui/src/debugger_panel.rs
Normal file
File diff suppressed because it is too large
Load Diff
446
crates/debugger_ui/src/debugger_ui.rs
Normal file
446
crates/debugger_ui/src/debugger_ui.rs
Normal file
@@ -0,0 +1,446 @@
|
||||
use std::any::TypeId;
|
||||
|
||||
use debugger_panel::DebugPanel;
|
||||
use editor::{Editor, MultiBufferOffsetUtf16};
|
||||
use gpui::{Action, App, DispatchPhase, EntityInputHandler, TaskExt, actions};
|
||||
use new_process_modal::{NewProcessModal, NewProcessMode};
|
||||
use project::debugger::{self, breakpoint_store::SourceBreakpoint, session::ThreadStatus};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use session::DebugSession;
|
||||
|
||||
use tasks_ui::{Spawn, TaskOverrides};
|
||||
use ui::{FluentBuilder, InteractiveElement};
|
||||
use util::maybe;
|
||||
use workspace::{ShutdownDebugAdapters, Workspace};
|
||||
use zed_actions::debug_panel::{Toggle, ToggleFocus};
|
||||
|
||||
pub mod attach_modal;
|
||||
pub mod debugger_panel;
|
||||
mod dropdown_menus;
|
||||
mod new_process_modal;
|
||||
mod persistence;
|
||||
pub(crate) mod session;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod tests;
|
||||
|
||||
// Let's see the diff-test in action.
|
||||
actions!(
|
||||
debugger,
|
||||
[
|
||||
/// Starts a new debugging session.
|
||||
Start,
|
||||
/// Continues execution until the next breakpoint.
|
||||
Continue,
|
||||
/// Detaches the debugger from the running process.
|
||||
Detach,
|
||||
/// Pauses the currently running program.
|
||||
Pause,
|
||||
/// Restarts the current debugging session.
|
||||
Restart,
|
||||
/// Reruns the current debugging session with the same configuration.
|
||||
RerunSession,
|
||||
/// Steps into the next function call.
|
||||
StepInto,
|
||||
/// Steps over the current line.
|
||||
StepOver,
|
||||
/// Steps out of the current function.
|
||||
StepOut,
|
||||
/// Steps back to the previous statement.
|
||||
StepBack,
|
||||
/// Stops the debugging session.
|
||||
Stop,
|
||||
/// Toggles whether to ignore all breakpoints.
|
||||
ToggleIgnoreBreakpoints,
|
||||
/// Clears all breakpoints in the project.
|
||||
ClearAllBreakpoints,
|
||||
/// Focuses on the debugger console panel.
|
||||
FocusConsole,
|
||||
/// Focuses on the variables panel.
|
||||
FocusVariables,
|
||||
/// Focuses on the breakpoint list panel.
|
||||
FocusBreakpointList,
|
||||
/// Focuses on the call stack frames panel.
|
||||
FocusFrames,
|
||||
/// Focuses on the loaded modules panel.
|
||||
FocusModules,
|
||||
/// Focuses on the loaded sources panel.
|
||||
FocusLoadedSources,
|
||||
/// Focuses on the terminal panel.
|
||||
FocusTerminal,
|
||||
/// Toggles the thread picker dropdown.
|
||||
ToggleThreadPicker,
|
||||
/// Toggles the session picker dropdown.
|
||||
ToggleSessionPicker,
|
||||
/// Reruns the last debugging session.
|
||||
#[action(deprecated_aliases = ["debugger::RerunLastSession"])]
|
||||
Rerun,
|
||||
/// Toggles expansion of the selected item in the debugger UI.
|
||||
ToggleExpandItem,
|
||||
/// Toggle the user frame filter in the stack frame list
|
||||
/// When toggled on, only frames from the user's code are shown
|
||||
/// When toggled off, all frames are shown
|
||||
ToggleUserFrames,
|
||||
]
|
||||
);
|
||||
|
||||
/// Set a data breakpoint on the selected variable or memory region.
|
||||
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
||||
#[action(namespace = debugger)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ToggleDataBreakpoint {
|
||||
/// The type of data breakpoint
|
||||
/// Read & Write
|
||||
/// Read
|
||||
/// Write
|
||||
#[serde(default)]
|
||||
pub access_type: Option<dap::DataBreakpointAccessType>,
|
||||
}
|
||||
|
||||
actions!(
|
||||
dev,
|
||||
[
|
||||
/// Copies debug adapter launch arguments to clipboard.
|
||||
CopyDebugAdapterArguments
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
workspace::FollowableViewRegistry::register::<DebugSession>(cx);
|
||||
|
||||
cx.observe_new(|workspace: &mut Workspace, _, _| {
|
||||
workspace
|
||||
.register_action(spawn_task_or_modal)
|
||||
.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
||||
workspace.toggle_panel_focus::<DebugPanel>(window, cx);
|
||||
})
|
||||
.register_action(|workspace, _: &Toggle, window, cx| {
|
||||
if !workspace.toggle_panel_focus::<DebugPanel>(window, cx) {
|
||||
workspace.close_panel::<DebugPanel>(window, cx);
|
||||
}
|
||||
})
|
||||
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
|
||||
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
|
||||
})
|
||||
.register_action(|workspace: &mut Workspace, _: &Rerun, window, cx| {
|
||||
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
debug_panel.update(cx, |debug_panel, cx| {
|
||||
debug_panel.rerun_last_session(workspace, window, 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();
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
.register_action_renderer(|div, workspace, _, cx| {
|
||||
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
|
||||
return div;
|
||||
};
|
||||
let Some(active_item) = debug_panel
|
||||
.read(cx)
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
else {
|
||||
return div;
|
||||
};
|
||||
let running_state = active_item.read(cx);
|
||||
if running_state.session().read(cx).is_terminated() {
|
||||
return div;
|
||||
}
|
||||
|
||||
let caps = running_state.capabilities(cx);
|
||||
let supports_step_back = caps.supports_step_back.unwrap_or_default();
|
||||
let supports_detach = running_state.session().read(cx).is_attached();
|
||||
let status = running_state.thread_status(cx);
|
||||
|
||||
let active_item = active_item.downgrade();
|
||||
div.when(status == Some(ThreadStatus::Running), |div| {
|
||||
let active_item = active_item.clone();
|
||||
div.on_action(move |_: &Pause, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.pause_thread(cx))
|
||||
.ok();
|
||||
})
|
||||
})
|
||||
.when(status == Some(ThreadStatus::Stopped), |div| {
|
||||
div.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &StepInto, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_in(cx)).ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &StepOver, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_over(cx)).ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &StepOut, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_out(cx)).ok();
|
||||
}
|
||||
})
|
||||
.when(supports_step_back, |div| {
|
||||
let active_item = active_item.clone();
|
||||
div.on_action(move |_: &StepBack, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.step_back(cx)).ok();
|
||||
})
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &Continue, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.continue_thread(cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
})
|
||||
.when(supports_detach, |div| {
|
||||
let active_item = active_item.clone();
|
||||
div.on_action(move |_: &Detach, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.detach_client(cx))
|
||||
.ok();
|
||||
})
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &Restart, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.restart_session(cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &RerunSession, window, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.rerun_session(window, cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &Stop, _, cx| {
|
||||
active_item.update(cx, |item, cx| item.stop_thread(cx)).ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &ToggleIgnoreBreakpoints, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_action(move |_: &ToggleUserFrames, _, cx| {
|
||||
if let Some((thread_status, stack_frame_list)) = active_item
|
||||
.read_with(cx, |item, cx| {
|
||||
(item.thread_status(cx), item.stack_frame_list().clone())
|
||||
})
|
||||
.ok()
|
||||
{
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
stack_frame_list.toggle_frame_filter(thread_status, cx);
|
||||
})
|
||||
}
|
||||
})
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe_new({
|
||||
move |editor: &mut Editor, _, _| {
|
||||
editor
|
||||
.register_action_renderer(move |editor, window, cx| {
|
||||
let Some(workspace) = editor.workspace() else {
|
||||
return;
|
||||
};
|
||||
let Some(debug_panel) = workspace.read(cx).panel::<DebugPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
let Some(active_session) =
|
||||
debug_panel.update(cx, |panel, _| panel.active_session())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let session = active_session
|
||||
.read(cx)
|
||||
.running_state
|
||||
.read(cx)
|
||||
.session()
|
||||
.read(cx);
|
||||
|
||||
if session.is_terminated() {
|
||||
return;
|
||||
}
|
||||
|
||||
let editor = cx.entity().downgrade();
|
||||
|
||||
window.on_action_when(
|
||||
session.any_stopped_thread(),
|
||||
TypeId::of::<editor::actions::RunToCursor>(),
|
||||
{
|
||||
let editor = editor.clone();
|
||||
let active_session = active_session.clone();
|
||||
move |_, phase, _, cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
maybe!({
|
||||
let (buffer, position) = editor
|
||||
.update(cx, |editor, cx| {
|
||||
let cursor_point: language::Point = editor
|
||||
.selections
|
||||
.newest(&editor.display_snapshot(cx))
|
||||
.head();
|
||||
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.point_to_buffer_point(cursor_point, cx)
|
||||
})
|
||||
.ok()??;
|
||||
|
||||
let path =
|
||||
debugger::breakpoint_store::BreakpointStore::abs_path_from_buffer(
|
||||
&buffer, cx,
|
||||
)?;
|
||||
|
||||
let source_breakpoint = SourceBreakpoint {
|
||||
row: position.row,
|
||||
path,
|
||||
message: None,
|
||||
condition: None,
|
||||
hit_condition: None,
|
||||
state: debugger::breakpoint_store::BreakpointState::Enabled,
|
||||
};
|
||||
|
||||
active_session.update(cx, |session, cx| {
|
||||
session.running_state().update(cx, |state, cx| {
|
||||
if let Some(thread_id) = state.selected_thread_id() {
|
||||
state.session().update(cx, |session, cx| {
|
||||
session.run_to_position(
|
||||
source_breakpoint,
|
||||
thread_id,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
window.on_action(
|
||||
TypeId::of::<editor::actions::EvaluateSelectedText>(),
|
||||
move |_, _, window, cx| {
|
||||
let status = maybe!({
|
||||
let text = editor
|
||||
.update(cx, |editor, cx| {
|
||||
let range = editor
|
||||
.selections
|
||||
.newest::<MultiBufferOffsetUtf16>(
|
||||
&editor.display_snapshot(cx),
|
||||
)
|
||||
.range();
|
||||
editor.text_for_range(
|
||||
range.start.0.0..range.end.0.0,
|
||||
&mut None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok()??;
|
||||
|
||||
active_session.update(cx, |session, cx| {
|
||||
session.running_state().update(cx, |state, cx| {
|
||||
let stack_id = state.selected_stack_frame_id(cx);
|
||||
|
||||
state.session().update(cx, |session, cx| {
|
||||
session
|
||||
.evaluate(
|
||||
text,
|
||||
Some(dap::EvaluateArgumentsContext::Repl),
|
||||
stack_id,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Some(())
|
||||
});
|
||||
if status.is_some() {
|
||||
cx.stop_propagation();
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn spawn_task_or_modal(
|
||||
workspace: &mut Workspace,
|
||||
action: &Spawn,
|
||||
window: &mut ui::Window,
|
||||
cx: &mut ui::Context<Workspace>,
|
||||
) {
|
||||
match action {
|
||||
Spawn::ByName {
|
||||
task_name,
|
||||
reveal_target,
|
||||
} => {
|
||||
let overrides = reveal_target.map(|reveal_target| TaskOverrides {
|
||||
reveal_target: Some(reveal_target),
|
||||
});
|
||||
let name = task_name.clone();
|
||||
tasks_ui::spawn_tasks_filtered(
|
||||
move |(_, task)| task.label.eq(&name),
|
||||
overrides,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
Spawn::ByTag {
|
||||
task_tag,
|
||||
reveal_target,
|
||||
} => {
|
||||
let overrides = reveal_target.map(|reveal_target| TaskOverrides {
|
||||
reveal_target: Some(reveal_target),
|
||||
});
|
||||
let tag = task_tag.clone();
|
||||
tasks_ui::spawn_tasks_filtered(
|
||||
move |(_, task)| task.tags.contains(&tag),
|
||||
overrides,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
Spawn::ViaModal { reveal_target } => {
|
||||
NewProcessModal::show(workspace, window, NewProcessMode::Task, *reveal_target, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
335
crates/debugger_ui/src/dropdown_menus.rs
Normal file
335
crates/debugger_ui/src/dropdown_menus.rs
Normal file
@@ -0,0 +1,335 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::{Anchor, Entity, WeakEntity};
|
||||
use project::debugger::session::{ThreadId, ThreadStatus};
|
||||
use ui::{CommonAnimationExt, ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
|
||||
use util::{maybe, truncate_and_trailoff};
|
||||
|
||||
use crate::{
|
||||
debugger_panel::DebugPanel,
|
||||
session::{DebugSession, running::RunningState},
|
||||
};
|
||||
|
||||
struct SessionListEntry {
|
||||
ancestors: Vec<Entity<DebugSession>>,
|
||||
leaf: Entity<DebugSession>,
|
||||
}
|
||||
|
||||
impl SessionListEntry {
|
||||
pub(crate) fn label_element(&self, depth: usize, cx: &mut App) -> AnyElement {
|
||||
const MAX_LABEL_CHARS: usize = 150;
|
||||
|
||||
let mut label = String::new();
|
||||
for ancestor in &self.ancestors {
|
||||
label.push_str(&ancestor.update(cx, |ancestor, cx| {
|
||||
ancestor.label(cx).unwrap_or("(child)".into())
|
||||
}));
|
||||
label.push_str(" » ");
|
||||
}
|
||||
label.push_str(
|
||||
&self
|
||||
.leaf
|
||||
.update(cx, |leaf, cx| leaf.label(cx).unwrap_or("(child)".into())),
|
||||
);
|
||||
let label = truncate_and_trailoff(&label, MAX_LABEL_CHARS);
|
||||
|
||||
let is_terminated = self
|
||||
.leaf
|
||||
.read(cx)
|
||||
.running_state
|
||||
.read(cx)
|
||||
.session()
|
||||
.read(cx)
|
||||
.is_terminated();
|
||||
let icon = {
|
||||
if is_terminated {
|
||||
Some(Indicator::dot().color(Color::Error))
|
||||
} else {
|
||||
match self
|
||||
.leaf
|
||||
.read(cx)
|
||||
.running_state
|
||||
.read(cx)
|
||||
.thread_status(cx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
project::debugger::session::ThreadStatus::Stopped => {
|
||||
Some(Indicator::dot().color(Color::Conflict))
|
||||
}
|
||||
_ => Some(Indicator::dot().color(Color::Success)),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id("session-label")
|
||||
.ml(depth * px(16.0))
|
||||
.gap_2()
|
||||
.when_some(icon, |this, indicator| this.child(indicator))
|
||||
.justify_between()
|
||||
.child(
|
||||
Label::new(label)
|
||||
.size(LabelSize::Small)
|
||||
.when(is_terminated, |this| this.strikethrough()),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl DebugPanel {
|
||||
fn dropdown_label(label: impl Into<SharedString>) -> Label {
|
||||
const MAX_LABEL_CHARS: usize = 50;
|
||||
let label = truncate_and_trailoff(&label.into(), MAX_LABEL_CHARS);
|
||||
Label::new(label).size(LabelSize::Small)
|
||||
}
|
||||
|
||||
pub fn render_session_menu(
|
||||
&mut self,
|
||||
active_session: Option<Entity<DebugSession>>,
|
||||
running_state: Option<Entity<RunningState>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<impl IntoElement> {
|
||||
let running_state = running_state?;
|
||||
|
||||
let mut session_entries = Vec::with_capacity(self.sessions_with_children.len() * 3);
|
||||
let mut sessions_with_children = self.sessions_with_children.iter().peekable();
|
||||
|
||||
while let Some((root, children)) = sessions_with_children.next() {
|
||||
let root_entry = if let Ok([single_child]) = <&[_; 1]>::try_from(children.as_slice())
|
||||
&& let Some(single_child) = single_child.upgrade()
|
||||
&& single_child.read(cx).quirks.compact
|
||||
{
|
||||
sessions_with_children.next();
|
||||
SessionListEntry {
|
||||
leaf: single_child.clone(),
|
||||
ancestors: vec![root.clone()],
|
||||
}
|
||||
} else {
|
||||
SessionListEntry {
|
||||
leaf: root.clone(),
|
||||
ancestors: Vec::new(),
|
||||
}
|
||||
};
|
||||
session_entries.push(root_entry);
|
||||
}
|
||||
|
||||
let weak = cx.weak_entity();
|
||||
let trigger_label = if let Some(active_session) = active_session.clone() {
|
||||
active_session.update(cx, |active_session, cx| {
|
||||
active_session.label(cx).unwrap_or("(child)".into())
|
||||
})
|
||||
} else {
|
||||
SharedString::new_static("Unknown Session")
|
||||
};
|
||||
let running_state = running_state.read(cx);
|
||||
|
||||
let is_terminated = running_state.session().read(cx).is_terminated();
|
||||
let is_started = active_session
|
||||
.is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
|
||||
|
||||
let session_state_indicator = if is_terminated {
|
||||
Indicator::dot().color(Color::Error).into_any_element()
|
||||
} else if !is_started {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_rotate_animation(2)
|
||||
.into_any_element()
|
||||
} else {
|
||||
match running_state.thread_status(cx).unwrap_or_default() {
|
||||
ThreadStatus::Stopped => Indicator::dot().color(Color::Conflict).into_any_element(),
|
||||
_ => Indicator::dot().color(Color::Success).into_any_element(),
|
||||
}
|
||||
};
|
||||
|
||||
let trigger = h_flex()
|
||||
.gap_2()
|
||||
.child(session_state_indicator)
|
||||
.justify_between()
|
||||
.child(
|
||||
DebugPanel::dropdown_label(trigger_label)
|
||||
.when(is_terminated, |this| this.strikethrough()),
|
||||
)
|
||||
.into_any_element();
|
||||
|
||||
let menu = DropdownMenu::new_with_element(
|
||||
"debugger-session-list",
|
||||
trigger,
|
||||
ContextMenu::build(window, cx, move |mut this, _, cx| {
|
||||
let context_menu = cx.weak_entity();
|
||||
let mut session_depths = HashMap::default();
|
||||
for session_entry in session_entries {
|
||||
let session_id = session_entry.leaf.read(cx).session_id(cx);
|
||||
let parent_depth = session_entry
|
||||
.ancestors
|
||||
.first()
|
||||
.unwrap_or(&session_entry.leaf)
|
||||
.read(cx)
|
||||
.session(cx)
|
||||
.read(cx)
|
||||
.parent_id(cx)
|
||||
.and_then(|parent_id| session_depths.get(&parent_id).cloned());
|
||||
let self_depth = *session_depths
|
||||
.entry(session_id)
|
||||
.or_insert_with(|| parent_depth.map(|depth| depth + 1).unwrap_or(0usize));
|
||||
this = this.custom_entry(
|
||||
{
|
||||
let weak = weak.clone();
|
||||
let context_menu = context_menu.clone();
|
||||
let ancestors: Rc<[_]> = session_entry
|
||||
.ancestors
|
||||
.iter()
|
||||
.map(|session| session.downgrade())
|
||||
.collect();
|
||||
let leaf = session_entry.leaf.downgrade();
|
||||
move |window, cx| {
|
||||
Self::render_session_menu_entry(
|
||||
weak.clone(),
|
||||
context_menu.clone(),
|
||||
ancestors.clone(),
|
||||
leaf.clone(),
|
||||
self_depth,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
let weak = weak.clone();
|
||||
let leaf = session_entry.leaf.clone();
|
||||
move |window, cx| {
|
||||
weak.update(cx, |panel, cx| {
|
||||
panel.activate_session(leaf.clone(), window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
this
|
||||
}),
|
||||
)
|
||||
.attach(Anchor::BottomLeft)
|
||||
.style(DropdownStyle::Ghost)
|
||||
.handle(self.session_picker_menu_handle.clone());
|
||||
|
||||
Some(menu)
|
||||
}
|
||||
|
||||
fn render_session_menu_entry(
|
||||
weak: WeakEntity<DebugPanel>,
|
||||
context_menu: WeakEntity<ContextMenu>,
|
||||
ancestors: Rc<[WeakEntity<DebugSession>]>,
|
||||
leaf: WeakEntity<DebugSession>,
|
||||
self_depth: usize,
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let Some(session_entry) = maybe!({
|
||||
let ancestors = ancestors
|
||||
.iter()
|
||||
.map(|ancestor| ancestor.upgrade())
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
let leaf = leaf.upgrade()?;
|
||||
Some(SessionListEntry { ancestors, leaf })
|
||||
}) else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let id: SharedString = format!(
|
||||
"debug-session-{}",
|
||||
session_entry.leaf.read(cx).session_id(cx).0
|
||||
)
|
||||
.into();
|
||||
let session_entity_id = session_entry.leaf.entity_id();
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.group(id.clone())
|
||||
.justify_between()
|
||||
.child(session_entry.label_element(self_depth, cx))
|
||||
.child(
|
||||
IconButton::new("close-debug-session", IconName::Close)
|
||||
.visible_on_hover(id)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click({
|
||||
move |_, window, cx| {
|
||||
weak.update(cx, |panel, cx| {
|
||||
panel.close_session(session_entity_id, window, cx);
|
||||
})
|
||||
.ok();
|
||||
context_menu
|
||||
.update(cx, |this, cx| {
|
||||
this.cancel(&Default::default(), window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
pub(crate) fn render_thread_dropdown(
|
||||
&self,
|
||||
running_state: &Entity<RunningState>,
|
||||
threads: Vec<(dap::Thread, ThreadStatus)>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<DropdownMenu> {
|
||||
const MAX_LABEL_CHARS: usize = 150;
|
||||
|
||||
let running_state = running_state.clone();
|
||||
let running_state_read = running_state.read(cx);
|
||||
let thread_id = running_state_read.thread_id();
|
||||
let session = running_state_read.session();
|
||||
let session_id = session.read(cx).session_id();
|
||||
let session_terminated = session.read(cx).is_terminated();
|
||||
let selected_thread_name = threads
|
||||
.iter()
|
||||
.find(|(thread, _)| thread_id.map(|id| id.0) == Some(thread.id))
|
||||
.map(|(thread, _)| {
|
||||
thread
|
||||
.name
|
||||
.is_empty()
|
||||
.then(|| format!("Tid: {}", thread.id))
|
||||
.unwrap_or_else(|| thread.name.clone())
|
||||
});
|
||||
|
||||
if let Some(selected_thread_name) = selected_thread_name {
|
||||
let trigger = DebugPanel::dropdown_label(selected_thread_name).into_any_element();
|
||||
Some(
|
||||
DropdownMenu::new_with_element(
|
||||
("thread-list", session_id.0),
|
||||
trigger,
|
||||
ContextMenu::build(window, cx, move |mut this, _, _| {
|
||||
for (thread, _) in threads {
|
||||
let running_state = running_state.clone();
|
||||
let thread_id = thread.id;
|
||||
let entry_name = thread
|
||||
.name
|
||||
.is_empty()
|
||||
.then(|| format!("Tid: {}", thread.id))
|
||||
.unwrap_or_else(|| thread.name);
|
||||
let entry_name = truncate_and_trailoff(&entry_name, MAX_LABEL_CHARS);
|
||||
|
||||
this = this.entry(entry_name, None, move |window, cx| {
|
||||
running_state.update(cx, |running_state, cx| {
|
||||
running_state.select_thread(ThreadId(thread_id), window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
this
|
||||
}),
|
||||
)
|
||||
.attach(Anchor::BottomLeft)
|
||||
.disabled(session_terminated)
|
||||
.style(DropdownStyle::Ghost)
|
||||
.handle(self.thread_picker_menu_handle.clone()),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
1664
crates/debugger_ui/src/new_process_modal.rs
Normal file
1664
crates/debugger_ui/src/new_process_modal.rs
Normal file
File diff suppressed because it is too large
Load Diff
382
crates/debugger_ui/src/persistence.rs
Normal file
382
crates/debugger_ui/src/persistence.rs
Normal file
@@ -0,0 +1,382 @@
|
||||
use anyhow::Context as _;
|
||||
use collections::HashMap;
|
||||
use dap::{Capabilities, adapters::DebugAdapterName};
|
||||
use db::kvp::KeyValueStore;
|
||||
use gpui::{Axis, Context, Entity, EntityId, Focusable, Subscription, WeakEntity, Window};
|
||||
use project::Project;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::{App, SharedString};
|
||||
use util::ResultExt;
|
||||
use workspace::{Member, Pane, PaneAxis, Workspace};
|
||||
|
||||
use crate::session::running::{
|
||||
self, DebugTerminal, RunningState, SubView, breakpoint_list::BreakpointList, console::Console,
|
||||
loaded_source_list::LoadedSourceList, memory_view::MemoryView, module_list::ModuleList,
|
||||
stack_frame_list::StackFrameList, variable_list::VariableList,
|
||||
};
|
||||
|
||||
#[derive(Clone, Hash, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub(crate) enum DebuggerPaneItem {
|
||||
Console,
|
||||
Variables,
|
||||
BreakpointList,
|
||||
Frames,
|
||||
Modules,
|
||||
LoadedSources,
|
||||
Terminal,
|
||||
MemoryView,
|
||||
}
|
||||
|
||||
impl DebuggerPaneItem {
|
||||
pub(crate) fn all() -> &'static [DebuggerPaneItem] {
|
||||
static VARIANTS: &[DebuggerPaneItem] = &[
|
||||
DebuggerPaneItem::Console,
|
||||
DebuggerPaneItem::Variables,
|
||||
DebuggerPaneItem::BreakpointList,
|
||||
DebuggerPaneItem::Frames,
|
||||
DebuggerPaneItem::Modules,
|
||||
DebuggerPaneItem::LoadedSources,
|
||||
DebuggerPaneItem::Terminal,
|
||||
DebuggerPaneItem::MemoryView,
|
||||
];
|
||||
VARIANTS
|
||||
}
|
||||
|
||||
pub(crate) fn is_supported(&self, capabilities: &Capabilities) -> bool {
|
||||
match self {
|
||||
DebuggerPaneItem::Modules => capabilities.supports_modules_request.unwrap_or_default(),
|
||||
DebuggerPaneItem::MemoryView => capabilities
|
||||
.supports_read_memory_request
|
||||
.unwrap_or_default(),
|
||||
DebuggerPaneItem::LoadedSources => capabilities
|
||||
.supports_loaded_sources_request
|
||||
.unwrap_or_default(),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_shared_string(self) -> SharedString {
|
||||
match self {
|
||||
DebuggerPaneItem::Console => SharedString::new_static("Console"),
|
||||
DebuggerPaneItem::Variables => SharedString::new_static("Variables"),
|
||||
DebuggerPaneItem::BreakpointList => SharedString::new_static("Breakpoints"),
|
||||
DebuggerPaneItem::Frames => SharedString::new_static("Frames"),
|
||||
DebuggerPaneItem::Modules => SharedString::new_static("Modules"),
|
||||
DebuggerPaneItem::LoadedSources => SharedString::new_static("Sources"),
|
||||
DebuggerPaneItem::Terminal => SharedString::new_static("Terminal"),
|
||||
DebuggerPaneItem::MemoryView => SharedString::new_static("Memory View"),
|
||||
}
|
||||
}
|
||||
pub(crate) fn tab_tooltip(self) -> SharedString {
|
||||
let tooltip = match self {
|
||||
DebuggerPaneItem::Console => {
|
||||
"Displays program output and allows manual input of debugger commands."
|
||||
}
|
||||
DebuggerPaneItem::Variables => {
|
||||
"Shows current values of local and global variables in the current stack frame."
|
||||
}
|
||||
DebuggerPaneItem::BreakpointList => "Lists all active breakpoints set in the code.",
|
||||
DebuggerPaneItem::Frames => {
|
||||
"Displays the call stack, letting you navigate between function calls."
|
||||
}
|
||||
DebuggerPaneItem::Modules => "Shows all modules or libraries loaded by the program.",
|
||||
DebuggerPaneItem::LoadedSources => {
|
||||
"Lists all source files currently loaded and used by the debugger."
|
||||
}
|
||||
DebuggerPaneItem::Terminal => {
|
||||
"Provides an interactive terminal session within the debugging environment."
|
||||
}
|
||||
DebuggerPaneItem::MemoryView => "Allows inspection of memory contents.",
|
||||
};
|
||||
SharedString::new_static(tooltip)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DebuggerPaneItem> for SharedString {
|
||||
fn from(item: DebuggerPaneItem) -> Self {
|
||||
item.to_shared_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct SerializedLayout {
|
||||
pub(crate) panes: SerializedPaneLayout,
|
||||
pub(crate) dock_axis: Axis,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub(crate) enum SerializedPaneLayout {
|
||||
Pane(SerializedPane),
|
||||
Group {
|
||||
axis: Axis,
|
||||
flexes: Option<Vec<f32>>,
|
||||
children: Vec<SerializedPaneLayout>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub(crate) struct SerializedPane {
|
||||
pub children: Vec<DebuggerPaneItem>,
|
||||
pub active_item: Option<DebuggerPaneItem>,
|
||||
}
|
||||
|
||||
const DEBUGGER_PANEL_PREFIX: &str = "debugger_panel_";
|
||||
|
||||
pub(crate) async fn serialize_pane_layout(
|
||||
adapter_name: DebugAdapterName,
|
||||
pane_group: SerializedLayout,
|
||||
kvp: KeyValueStore,
|
||||
) -> anyhow::Result<()> {
|
||||
let serialized_pane_group = serde_json::to_string(&pane_group)
|
||||
.context("Serializing pane group with serde_json as a string")?;
|
||||
kvp.write_kvp(
|
||||
format!("{DEBUGGER_PANEL_PREFIX}-{adapter_name}"),
|
||||
serialized_pane_group,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) fn build_serialized_layout(
|
||||
pane_group: &Member,
|
||||
dock_axis: Axis,
|
||||
cx: &App,
|
||||
) -> SerializedLayout {
|
||||
SerializedLayout {
|
||||
dock_axis,
|
||||
panes: build_serialized_pane_layout(pane_group, cx),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_serialized_pane_layout(pane_group: &Member, cx: &App) -> SerializedPaneLayout {
|
||||
match pane_group {
|
||||
Member::Axis(PaneAxis {
|
||||
axis,
|
||||
members,
|
||||
flexes,
|
||||
bounding_boxes: _,
|
||||
}) => SerializedPaneLayout::Group {
|
||||
axis: *axis,
|
||||
children: members
|
||||
.iter()
|
||||
.map(|member| build_serialized_pane_layout(member, cx))
|
||||
.collect::<Vec<_>>(),
|
||||
flexes: Some(flexes.lock().clone()),
|
||||
},
|
||||
Member::Pane(pane_handle) => SerializedPaneLayout::Pane(serialize_pane(pane_handle, cx)),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_pane(pane: &Entity<Pane>, cx: &App) -> SerializedPane {
|
||||
let pane = pane.read(cx);
|
||||
let children = pane
|
||||
.items()
|
||||
.filter_map(|item| {
|
||||
item.act_as::<SubView>(cx)
|
||||
.map(|view| view.read(cx).view_kind())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let active_item = pane
|
||||
.active_item()
|
||||
.and_then(|item| item.act_as::<SubView>(cx))
|
||||
.map(|view| view.read(cx).view_kind());
|
||||
|
||||
SerializedPane {
|
||||
children,
|
||||
active_item,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_serialized_layout(
|
||||
adapter_name: impl AsRef<str>,
|
||||
kvp: &KeyValueStore,
|
||||
) -> Option<SerializedLayout> {
|
||||
let key = format!("{DEBUGGER_PANEL_PREFIX}-{}", adapter_name.as_ref());
|
||||
|
||||
kvp.read_kvp(&key)
|
||||
.log_err()
|
||||
.flatten()
|
||||
.and_then(|value| serde_json::from_str::<SerializedLayout>(&value).ok())
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize_pane_layout(
|
||||
serialized: SerializedPaneLayout,
|
||||
should_invert: bool,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
project: &Entity<Project>,
|
||||
stack_frame_list: &Entity<StackFrameList>,
|
||||
variable_list: &Entity<VariableList>,
|
||||
module_list: &Entity<ModuleList>,
|
||||
console: &Entity<Console>,
|
||||
breakpoint_list: &Entity<BreakpointList>,
|
||||
loaded_sources: &Entity<LoadedSourceList>,
|
||||
terminal: &Entity<DebugTerminal>,
|
||||
memory_view: &Entity<MemoryView>,
|
||||
subscriptions: &mut HashMap<EntityId, Subscription>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<RunningState>,
|
||||
) -> Option<Member> {
|
||||
match serialized {
|
||||
SerializedPaneLayout::Group {
|
||||
axis,
|
||||
flexes,
|
||||
children,
|
||||
} => {
|
||||
let mut members = Vec::new();
|
||||
for child in children {
|
||||
if let Some(new_member) = deserialize_pane_layout(
|
||||
child,
|
||||
should_invert,
|
||||
workspace,
|
||||
project,
|
||||
stack_frame_list,
|
||||
variable_list,
|
||||
module_list,
|
||||
console,
|
||||
breakpoint_list,
|
||||
loaded_sources,
|
||||
terminal,
|
||||
memory_view,
|
||||
subscriptions,
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
members.push(new_member);
|
||||
}
|
||||
}
|
||||
|
||||
if members.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if members.len() == 1 {
|
||||
return Some(members.remove(0));
|
||||
}
|
||||
|
||||
Some(Member::Axis(PaneAxis::load(
|
||||
if should_invert { axis.invert() } else { axis },
|
||||
members,
|
||||
flexes,
|
||||
)))
|
||||
}
|
||||
SerializedPaneLayout::Pane(serialized_pane) => {
|
||||
let pane = running::new_debugger_pane(workspace.clone(), project.clone(), window, cx);
|
||||
subscriptions.insert(
|
||||
pane.entity_id(),
|
||||
cx.subscribe_in(&pane, window, RunningState::handle_pane_event),
|
||||
);
|
||||
let running_state = cx.weak_entity();
|
||||
let pane_handle = pane.downgrade();
|
||||
|
||||
let sub_views: Vec<_> = serialized_pane
|
||||
.children
|
||||
.iter()
|
||||
.map(|child| match child {
|
||||
DebuggerPaneItem::Frames => Box::new(SubView::stack_frame_list(
|
||||
stack_frame_list.clone(),
|
||||
running_state.clone(),
|
||||
pane_handle.clone(),
|
||||
cx,
|
||||
)),
|
||||
DebuggerPaneItem::Variables => Box::new(SubView::new(
|
||||
variable_list.focus_handle(cx),
|
||||
variable_list.clone().into(),
|
||||
DebuggerPaneItem::Variables,
|
||||
running_state.clone(),
|
||||
pane_handle.clone(),
|
||||
cx,
|
||||
)),
|
||||
DebuggerPaneItem::BreakpointList => Box::new(SubView::breakpoint_list(
|
||||
breakpoint_list.clone(),
|
||||
running_state.clone(),
|
||||
pane_handle.clone(),
|
||||
cx,
|
||||
)),
|
||||
DebuggerPaneItem::Modules => Box::new(SubView::new(
|
||||
module_list.focus_handle(cx),
|
||||
module_list.clone().into(),
|
||||
DebuggerPaneItem::Modules,
|
||||
running_state.clone(),
|
||||
pane_handle.clone(),
|
||||
cx,
|
||||
)),
|
||||
DebuggerPaneItem::LoadedSources => Box::new(SubView::new(
|
||||
loaded_sources.focus_handle(cx),
|
||||
loaded_sources.clone().into(),
|
||||
DebuggerPaneItem::LoadedSources,
|
||||
running_state.clone(),
|
||||
pane_handle.clone(),
|
||||
cx,
|
||||
)),
|
||||
DebuggerPaneItem::Console => {
|
||||
let view = SubView::console(
|
||||
console.clone(),
|
||||
running_state.clone(),
|
||||
pane_handle.clone(),
|
||||
cx,
|
||||
);
|
||||
Box::new(view)
|
||||
}
|
||||
DebuggerPaneItem::Terminal => Box::new(SubView::new(
|
||||
terminal.focus_handle(cx),
|
||||
terminal.clone().into(),
|
||||
DebuggerPaneItem::Terminal,
|
||||
running_state.clone(),
|
||||
pane_handle.clone(),
|
||||
cx,
|
||||
)),
|
||||
DebuggerPaneItem::MemoryView => Box::new(SubView::new(
|
||||
memory_view.focus_handle(cx),
|
||||
memory_view.clone().into(),
|
||||
DebuggerPaneItem::MemoryView,
|
||||
running_state.clone(),
|
||||
pane_handle.clone(),
|
||||
cx,
|
||||
)),
|
||||
})
|
||||
.collect();
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
let mut active_idx = 0;
|
||||
for (idx, sub_view) in sub_views.into_iter().enumerate() {
|
||||
if serialized_pane
|
||||
.active_item
|
||||
.is_some_and(|active| active == sub_view.read(cx).view_kind())
|
||||
{
|
||||
active_idx = idx;
|
||||
}
|
||||
pane.add_item(sub_view, false, false, None, window, cx);
|
||||
}
|
||||
|
||||
pane.activate_item(active_idx, false, false, window, cx);
|
||||
});
|
||||
|
||||
Some(Member::Pane(pane.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl SerializedPaneLayout {
|
||||
pub(crate) fn in_order(&self) -> Vec<SerializedPaneLayout> {
|
||||
let mut panes = vec![];
|
||||
|
||||
Self::inner_in_order(self, &mut panes);
|
||||
panes
|
||||
}
|
||||
|
||||
fn inner_in_order(&self, panes: &mut Vec<SerializedPaneLayout>) {
|
||||
match self {
|
||||
SerializedPaneLayout::Pane(_) => panes.push((*self).clone()),
|
||||
SerializedPaneLayout::Group {
|
||||
axis: _,
|
||||
flexes: _,
|
||||
children,
|
||||
} => {
|
||||
for child in children {
|
||||
child.inner_in_order(panes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
179
crates/debugger_ui/src/session.rs
Normal file
179
crates/debugger_ui/src/session.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
pub mod running;
|
||||
|
||||
use crate::{persistence::SerializedLayout, session::running::DebugTerminal};
|
||||
use dap::client::SessionId;
|
||||
use gpui::{App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity};
|
||||
use project::debugger::session::Session;
|
||||
|
||||
use project::{Project, debugger::session::SessionQuirks};
|
||||
use rpc::proto;
|
||||
use running::RunningState;
|
||||
use ui::prelude::*;
|
||||
use workspace::{
|
||||
CollaboratorId, FollowableItem, ViewId, Workspace,
|
||||
item::{self, Item},
|
||||
};
|
||||
|
||||
pub struct DebugSession {
|
||||
remote_id: Option<workspace::ViewId>,
|
||||
pub(crate) running_state: Entity<RunningState>,
|
||||
pub(crate) quirks: SessionQuirks,
|
||||
}
|
||||
|
||||
impl DebugSession {
|
||||
pub(crate) fn running(
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
parent_terminal: Option<Entity<DebugTerminal>>,
|
||||
session: Entity<Session>,
|
||||
serialized_layout: Option<SerializedLayout>,
|
||||
dock_axis: Axis,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
let running_state = cx.new(|cx| {
|
||||
RunningState::new(
|
||||
session.clone(),
|
||||
project.clone(),
|
||||
workspace.clone(),
|
||||
parent_terminal,
|
||||
serialized_layout,
|
||||
dock_axis,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let quirks = session.read(cx).quirks();
|
||||
|
||||
cx.new(|_| Self {
|
||||
remote_id: None,
|
||||
running_state,
|
||||
quirks,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn session_id(&self, cx: &App) -> SessionId {
|
||||
self.running_state.read(cx).session_id()
|
||||
}
|
||||
|
||||
pub fn session(&self, cx: &App) -> Entity<Session> {
|
||||
self.running_state.read(cx).session().clone()
|
||||
}
|
||||
|
||||
pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
|
||||
self.running_state
|
||||
.update(cx, |state, cx| state.shutdown(cx));
|
||||
}
|
||||
|
||||
pub(crate) fn label(&self, cx: &mut App) -> Option<SharedString> {
|
||||
let session = self.running_state.read(cx).session().clone();
|
||||
session.update(cx, |session, cx| {
|
||||
let session_label = session.label();
|
||||
let quirks = session.quirks();
|
||||
let mut single_thread_name = || {
|
||||
let threads = session.threads(cx);
|
||||
match threads.as_slice() {
|
||||
[(thread, _)] => Some(SharedString::from(&thread.name)),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
if quirks.prefer_thread_name {
|
||||
single_thread_name().or(session_label)
|
||||
} else {
|
||||
session_label.or_else(single_thread_name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn running_state(&self) -> &Entity<RunningState> {
|
||||
&self.running_state
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for DebugSession {}
|
||||
|
||||
impl Focusable for DebugSession {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.running_state.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for DebugSession {
|
||||
type Event = ();
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
"Debugger".into()
|
||||
}
|
||||
}
|
||||
|
||||
impl FollowableItem for DebugSession {
|
||||
fn remote_id(&self) -> Option<workspace::ViewId> {
|
||||
self.remote_id
|
||||
}
|
||||
|
||||
fn to_state_proto(&self, _window: &mut Window, _cx: &mut 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<anyhow::Result<Entity<Self>>>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn add_event_to_update_proto(
|
||||
&self,
|
||||
_event: &Self::Event,
|
||||
_update: &mut Option<proto::update_view::Variant>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut 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<anyhow::Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn set_leader_id(
|
||||
&mut self,
|
||||
_leader_id: Option<CollaboratorId>,
|
||||
_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 {
|
||||
self.running_state
|
||||
.update(cx, |this, cx| this.render(window, cx).into_any_element())
|
||||
}
|
||||
}
|
||||
2072
crates/debugger_ui/src/session/running.rs
Normal file
2072
crates/debugger_ui/src/session/running.rs
Normal file
File diff suppressed because it is too large
Load Diff
1517
crates/debugger_ui/src/session/running/breakpoint_list.rs
Normal file
1517
crates/debugger_ui/src/session/running/breakpoint_list.rs
Normal file
File diff suppressed because it is too large
Load Diff
1013
crates/debugger_ui/src/session/running/console.rs
Normal file
1013
crates/debugger_ui/src/session/running/console.rs
Normal file
File diff suppressed because it is too large
Load Diff
97
crates/debugger_ui/src/session/running/loaded_source_list.rs
Normal file
97
crates/debugger_ui/src/session/running/loaded_source_list.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use gpui::{AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription, list};
|
||||
use project::debugger::session::{Session, SessionEvent};
|
||||
use ui::prelude::*;
|
||||
use util::maybe;
|
||||
|
||||
pub(crate) 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 focus_handle = cx.focus_handle();
|
||||
let list = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
|
||||
|
||||
let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
|
||||
SessionEvent::Stopped(_)
|
||||
| SessionEvent::HistoricSnapshotSelected
|
||||
| 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, |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(),
|
||||
cx.processor(|this, ix, _window, cx| this.render_entry(ix, cx)),
|
||||
)
|
||||
.size_full(),
|
||||
)
|
||||
}
|
||||
}
|
||||
935
crates/debugger_ui/src/session/running/memory_view.rs
Normal file
935
crates/debugger_ui/src/session/running/memory_view.rs
Normal file
@@ -0,0 +1,935 @@
|
||||
use std::{
|
||||
cell::{LazyCell, RefCell, RefMut},
|
||||
fmt::Write,
|
||||
ops::RangeInclusive,
|
||||
rc::Rc,
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{
|
||||
Action, Along, AppContext, Axis, DismissEvent, DragMoveEvent, Empty, Entity, FocusHandle,
|
||||
Focusable, ListHorizontalSizingBehavior, MouseButton, Point, ScrollStrategy, ScrollWheelEvent,
|
||||
Subscription, Task, TextStyle, UniformList, UniformListScrollHandle, WeakEntity, actions,
|
||||
anchored, deferred, uniform_list,
|
||||
};
|
||||
use notifications::status_toast::StatusToast;
|
||||
use project::debugger::{MemoryCell, dap_command::DataBreakpointContext, session::Session};
|
||||
use settings::Settings;
|
||||
use theme_settings::ThemeSettings;
|
||||
use ui::{
|
||||
ContextMenu, Divider, DropdownMenu, FluentBuilder, IntoElement, PopoverMenuHandle, Render,
|
||||
ScrollableHandle, StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{ToggleDataBreakpoint, session::running::stack_frame_list::StackFrameList};
|
||||
|
||||
actions!(debugger, [GoToSelectedAddress]);
|
||||
|
||||
pub(crate) struct MemoryView {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
stack_frame_list: WeakEntity<StackFrameList>,
|
||||
focus_handle: FocusHandle,
|
||||
view_state_handle: ViewStateHandle,
|
||||
query_editor: Entity<Editor>,
|
||||
session: Entity<Session>,
|
||||
width_picker_handle: PopoverMenuHandle<ContextMenu>,
|
||||
is_writing_memory: bool,
|
||||
open_context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
}
|
||||
|
||||
impl Focusable for MemoryView {
|
||||
fn focus_handle(&self, _: &ui::App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
struct Drag {
|
||||
start_address: u64,
|
||||
end_address: u64,
|
||||
}
|
||||
|
||||
impl Drag {
|
||||
fn contains(&self, address: u64) -> bool {
|
||||
let range = self.memory_range();
|
||||
range.contains(&address)
|
||||
}
|
||||
|
||||
fn memory_range(&self) -> RangeInclusive<u64> {
|
||||
if self.start_address < self.end_address {
|
||||
self.start_address..=self.end_address
|
||||
} else {
|
||||
self.end_address..=self.start_address
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
enum SelectedMemoryRange {
|
||||
DragUnderway(Drag),
|
||||
DragComplete(Drag),
|
||||
}
|
||||
|
||||
impl SelectedMemoryRange {
|
||||
fn contains(&self, address: u64) -> bool {
|
||||
match self {
|
||||
SelectedMemoryRange::DragUnderway(drag) => drag.contains(address),
|
||||
SelectedMemoryRange::DragComplete(drag) => drag.contains(address),
|
||||
}
|
||||
}
|
||||
fn is_dragging(&self) -> bool {
|
||||
matches!(self, SelectedMemoryRange::DragUnderway(_))
|
||||
}
|
||||
fn drag(&self) -> &Drag {
|
||||
match self {
|
||||
SelectedMemoryRange::DragUnderway(drag) => drag,
|
||||
SelectedMemoryRange::DragComplete(drag) => drag,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ViewStateHandle(Rc<RefCell<ViewState>>);
|
||||
|
||||
impl ViewStateHandle {
|
||||
fn new(base_row: u64, line_width: ViewWidth) -> Self {
|
||||
Self(Rc::new(RefCell::new(ViewState::new(base_row, line_width))))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ViewState {
|
||||
/// Uppermost row index
|
||||
base_row: u64,
|
||||
/// How many cells per row do we have?
|
||||
line_width: ViewWidth,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
selection: Option<SelectedMemoryRange>,
|
||||
}
|
||||
|
||||
impl ViewState {
|
||||
fn new(base_row: u64, line_width: ViewWidth) -> Self {
|
||||
Self {
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
base_row,
|
||||
line_width,
|
||||
selection: None,
|
||||
}
|
||||
}
|
||||
fn row_count(&self) -> u64 {
|
||||
// This was picked fully arbitrarily. There's no incentive for us to care about page sizes other than the fact that it seems to be a good
|
||||
// middle ground for data size.
|
||||
const PAGE_SIZE: u64 = 4096;
|
||||
PAGE_SIZE / self.line_width.width as u64
|
||||
}
|
||||
fn schedule_scroll_down(&mut self) {
|
||||
self.base_row = self.base_row.saturating_add(1)
|
||||
}
|
||||
fn schedule_scroll_up(&mut self) {
|
||||
self.base_row = self.base_row.saturating_sub(1);
|
||||
}
|
||||
|
||||
fn set_offset(&mut self, point: Point<Pixels>) {
|
||||
if point.y >= -Pixels::ZERO {
|
||||
self.schedule_scroll_up();
|
||||
} else if point.y <= -self.scroll_handle.max_offset().y {
|
||||
self.schedule_scroll_down();
|
||||
}
|
||||
self.scroll_handle.set_offset(point);
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollableHandle for ViewStateHandle {
|
||||
fn max_offset(&self) -> gpui::Point<Pixels> {
|
||||
self.0.borrow().scroll_handle.max_offset()
|
||||
}
|
||||
|
||||
fn set_offset(&self, point: Point<Pixels>) {
|
||||
self.0.borrow_mut().set_offset(point);
|
||||
}
|
||||
|
||||
fn offset(&self) -> Point<Pixels> {
|
||||
self.0.borrow().scroll_handle.offset()
|
||||
}
|
||||
|
||||
fn viewport(&self) -> gpui::Bounds<Pixels> {
|
||||
self.0.borrow().scroll_handle.viewport()
|
||||
}
|
||||
}
|
||||
|
||||
static HEX_BYTES_MEMOIZED: LazyLock<[SharedString; 256]> =
|
||||
LazyLock::new(|| std::array::from_fn(|byte| SharedString::from(format!("{byte:02X}"))));
|
||||
static UNKNOWN_BYTE: SharedString = SharedString::new_static("??");
|
||||
|
||||
impl MemoryView {
|
||||
pub(crate) fn new(
|
||||
session: Entity<Session>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
stack_frame_list: WeakEntity<StackFrameList>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let view_state_handle = ViewStateHandle::new(0, WIDTHS[4].clone());
|
||||
|
||||
let query_editor = cx.new(|cx| Editor::single_line(window, cx));
|
||||
|
||||
let mut this = Self {
|
||||
workspace,
|
||||
stack_frame_list,
|
||||
focus_handle: cx.focus_handle(),
|
||||
view_state_handle,
|
||||
query_editor,
|
||||
session,
|
||||
width_picker_handle: Default::default(),
|
||||
is_writing_memory: true,
|
||||
open_context_menu: None,
|
||||
};
|
||||
this.change_query_bar_mode(false, window, cx);
|
||||
cx.on_focus_out(&this.focus_handle, window, |this, _, window, cx| {
|
||||
this.change_query_bar_mode(false, window, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
this
|
||||
}
|
||||
|
||||
fn view_state(&self) -> RefMut<'_, ViewState> {
|
||||
self.view_state_handle.0.borrow_mut()
|
||||
}
|
||||
|
||||
fn render_memory(&self, cx: &mut Context<Self>) -> UniformList {
|
||||
let weak = cx.weak_entity();
|
||||
let session = self.session.clone();
|
||||
let view_state = self.view_state_handle.0.borrow().clone();
|
||||
uniform_list(
|
||||
"debugger-memory-view",
|
||||
view_state.row_count() as usize,
|
||||
move |range, _, cx| {
|
||||
let mut line_buffer = Vec::with_capacity(view_state.line_width.width as usize);
|
||||
let memory_start =
|
||||
(view_state.base_row + range.start as u64) * view_state.line_width.width as u64;
|
||||
let memory_end = (view_state.base_row + range.end as u64)
|
||||
* view_state.line_width.width as u64
|
||||
- 1;
|
||||
let mut memory = session.update(cx, |this, cx| {
|
||||
this.read_memory(memory_start..=memory_end, cx)
|
||||
});
|
||||
let mut rows = Vec::with_capacity(range.end - range.start);
|
||||
for ix in range {
|
||||
line_buffer.extend((&mut memory).take(view_state.line_width.width as usize));
|
||||
rows.push(render_single_memory_view_line(
|
||||
&line_buffer,
|
||||
ix as u64,
|
||||
weak.clone(),
|
||||
cx,
|
||||
));
|
||||
line_buffer.clear();
|
||||
}
|
||||
rows
|
||||
},
|
||||
)
|
||||
.track_scroll(&view_state.scroll_handle)
|
||||
.with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
|
||||
.on_scroll_wheel(cx.listener(|this, evt: &ScrollWheelEvent, window, _| {
|
||||
let mut view_state = this.view_state();
|
||||
let delta = evt.delta.pixel_delta(window.line_height());
|
||||
let current_offset = view_state.scroll_handle.offset();
|
||||
view_state
|
||||
.set_offset(current_offset.apply_along(Axis::Vertical, |offset| offset + delta.y));
|
||||
}))
|
||||
}
|
||||
fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
|
||||
EditorElement::new(
|
||||
&self.query_editor,
|
||||
Self::editor_style(&self.query_editor, cx),
|
||||
)
|
||||
}
|
||||
pub(super) fn go_to_memory_reference(
|
||||
&mut self,
|
||||
memory_reference: &str,
|
||||
evaluate_name: Option<&str>,
|
||||
stack_frame_id: Option<u64>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
use parse_int::parse;
|
||||
let Ok(as_address) = parse::<u64>(memory_reference) else {
|
||||
return;
|
||||
};
|
||||
let access_size = evaluate_name
|
||||
.map(|typ| {
|
||||
self.session.update(cx, |this, cx| {
|
||||
this.data_access_size(stack_frame_id, typ, cx)
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| Task::ready(None));
|
||||
cx.spawn(async move |this, cx| {
|
||||
let access_size = access_size.await.unwrap_or(1);
|
||||
this.update(cx, |this, cx| {
|
||||
this.view_state().selection = Some(SelectedMemoryRange::DragComplete(Drag {
|
||||
start_address: as_address,
|
||||
end_address: as_address + access_size - 1,
|
||||
}));
|
||||
this.jump_to_address(as_address, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn handle_memory_drag(&mut self, evt: &DragMoveEvent<Drag>) {
|
||||
let mut view_state = self.view_state();
|
||||
if !view_state
|
||||
.selection
|
||||
.as_ref()
|
||||
.is_some_and(|selection| selection.is_dragging())
|
||||
{
|
||||
return;
|
||||
}
|
||||
let row_count = view_state.row_count();
|
||||
debug_assert!(row_count > 1);
|
||||
let scroll_handle = &view_state.scroll_handle;
|
||||
let viewport = scroll_handle.viewport();
|
||||
|
||||
if viewport.bottom() < evt.event.position.y {
|
||||
view_state.schedule_scroll_down();
|
||||
} else if viewport.top() > evt.event.position.y {
|
||||
view_state.schedule_scroll_up();
|
||||
}
|
||||
}
|
||||
|
||||
fn editor_style(editor: &Entity<Editor>, cx: &Context<Self>) -> EditorStyle {
|
||||
let is_read_only = editor.read(cx).read_only(cx);
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let theme = cx.theme();
|
||||
let text_style = TextStyle {
|
||||
color: if is_read_only {
|
||||
theme.colors().text_muted
|
||||
} else {
|
||||
theme.colors().text
|
||||
},
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: TextSize::Small.rems(cx).into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
|
||||
..Default::default()
|
||||
};
|
||||
EditorStyle {
|
||||
background: theme.colors().editor_background,
|
||||
local_player: theme.players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_width_picker(&self, window: &mut Window, cx: &mut Context<Self>) -> DropdownMenu {
|
||||
let weak = cx.weak_entity();
|
||||
let selected_width = self.view_state().line_width.clone();
|
||||
DropdownMenu::new(
|
||||
"memory-view-width-picker",
|
||||
selected_width.label.clone(),
|
||||
ContextMenu::build(window, cx, |mut this, window, cx| {
|
||||
for width in &WIDTHS {
|
||||
let weak = weak.clone();
|
||||
let width = width.clone();
|
||||
this = this.entry(width.label.clone(), None, move |_, cx| {
|
||||
_ = weak.update(cx, |this, _| {
|
||||
let mut view_state = this.view_state();
|
||||
// Convert base ix between 2 line widths to keep the shown memory address roughly the same.
|
||||
// All widths are powers of 2, so the conversion should be lossless.
|
||||
match view_state.line_width.width.cmp(&width.width) {
|
||||
std::cmp::Ordering::Less => {
|
||||
// We're converting up.
|
||||
let shift = width.width.trailing_zeros()
|
||||
- view_state.line_width.width.trailing_zeros();
|
||||
view_state.base_row >>= shift;
|
||||
}
|
||||
std::cmp::Ordering::Greater => {
|
||||
// We're converting down.
|
||||
let shift = view_state.line_width.width.trailing_zeros()
|
||||
- width.width.trailing_zeros();
|
||||
view_state.base_row <<= shift;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
view_state.line_width = width.clone();
|
||||
});
|
||||
});
|
||||
}
|
||||
if let Some(ix) = WIDTHS
|
||||
.iter()
|
||||
.position(|width| width.width == selected_width.width)
|
||||
{
|
||||
for _ in 0..=ix {
|
||||
this.select_next(&Default::default(), window, cx);
|
||||
}
|
||||
}
|
||||
this
|
||||
}),
|
||||
)
|
||||
.handle(self.width_picker_handle.clone())
|
||||
}
|
||||
|
||||
fn page_down(&mut self, _: &menu::SelectLast, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let mut view_state = self.view_state();
|
||||
view_state.base_row = view_state
|
||||
.base_row
|
||||
.overflowing_add(view_state.row_count())
|
||||
.0;
|
||||
cx.notify();
|
||||
}
|
||||
fn page_up(&mut self, _: &menu::SelectFirst, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let mut view_state = self.view_state();
|
||||
view_state.base_row = view_state
|
||||
.base_row
|
||||
.overflowing_sub(view_state.row_count())
|
||||
.0;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn change_query_bar_mode(
|
||||
&mut self,
|
||||
is_writing_memory: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if is_writing_memory == self.is_writing_memory {
|
||||
return;
|
||||
}
|
||||
if !self.is_writing_memory {
|
||||
self.query_editor.update(cx, |this, cx| {
|
||||
this.clear(window, cx);
|
||||
this.set_placeholder_text("Write to Selected Memory Range", window, cx);
|
||||
});
|
||||
self.is_writing_memory = true;
|
||||
self.query_editor.focus_handle(cx).focus(window, cx);
|
||||
} else {
|
||||
self.query_editor.update(cx, |this, cx| {
|
||||
this.clear(window, cx);
|
||||
this.set_placeholder_text("Go to Memory Address / Expression", window, cx);
|
||||
});
|
||||
self.is_writing_memory = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_data_breakpoint(
|
||||
&mut self,
|
||||
_: &crate::ToggleDataBreakpoint,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(SelectedMemoryRange::DragComplete(selection)) =
|
||||
self.view_state().selection.clone()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let range = selection.memory_range();
|
||||
let context = Arc::new(DataBreakpointContext::Address {
|
||||
address: range.start().to_string(),
|
||||
bytes: Some(*range.end() - *range.start()),
|
||||
});
|
||||
|
||||
self.session.update(cx, |this, cx| {
|
||||
let data_breakpoint_info = this.data_breakpoint_info(context.clone(), None, cx);
|
||||
cx.spawn(async move |this, cx| {
|
||||
if let Some(info) = data_breakpoint_info.await {
|
||||
let Some(data_id) = info.data_id else {
|
||||
return;
|
||||
};
|
||||
_ = this.update(cx, |this, cx| {
|
||||
this.create_data_breakpoint(
|
||||
context,
|
||||
data_id.clone(),
|
||||
dap::DataBreakpoint {
|
||||
data_id,
|
||||
access_type: None,
|
||||
condition: None,
|
||||
hit_condition: None,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let selection = self.view_state().selection.clone();
|
||||
if let Some(SelectedMemoryRange::DragComplete(drag)) = selection {
|
||||
// Go into memory writing mode.
|
||||
if !self.is_writing_memory {
|
||||
let should_return = self.session.update(cx, |session, cx| {
|
||||
if !session
|
||||
.capabilities()
|
||||
.supports_write_memory_request
|
||||
.unwrap_or_default()
|
||||
{
|
||||
let adapter_name = session.adapter();
|
||||
// We cannot write memory with this adapter.
|
||||
_ = self.workspace.update(cx, |this, cx| {
|
||||
this.toggle_status_toast(
|
||||
StatusToast::new(format!(
|
||||
"Debug Adapter `{adapter_name}` does not support writing to memory"
|
||||
), cx, |this, cx| {
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||
_ = this.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent)
|
||||
});
|
||||
}).detach();
|
||||
this.icon(Icon::new(IconName::XCircle).size(IconSize::Small).color(Color::Error))
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
if should_return {
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_query_bar_mode(true, window, cx);
|
||||
} else if self.query_editor.focus_handle(cx).is_focused(window) {
|
||||
let mut text = self.query_editor.read(cx).text(cx);
|
||||
if text.chars().any(|c| !c.is_ascii_hexdigit()) {
|
||||
// Interpret this text as a string and oh-so-conveniently convert it.
|
||||
text = text.bytes().map(|byte| format!("{:02x}", byte)).collect();
|
||||
}
|
||||
self.session.update(cx, |this, cx| {
|
||||
let range = drag.memory_range();
|
||||
|
||||
if let Ok(as_hex) = hex::decode(text) {
|
||||
this.write_memory(*range.start(), &as_hex, cx);
|
||||
}
|
||||
});
|
||||
self.change_query_bar_mode(false, window, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
// Just change the currently viewed address.
|
||||
if !self.query_editor.focus_handle(cx).is_focused(window) {
|
||||
return;
|
||||
}
|
||||
self.jump_to_query_bar_address(cx);
|
||||
}
|
||||
|
||||
fn jump_to_query_bar_address(&mut self, cx: &mut Context<Self>) {
|
||||
use parse_int::parse;
|
||||
let text = self.query_editor.read(cx).text(cx);
|
||||
|
||||
let Ok(as_address) = parse::<u64>(&text) else {
|
||||
return self.jump_to_expression(text, cx);
|
||||
};
|
||||
self.jump_to_address(as_address, cx);
|
||||
}
|
||||
|
||||
fn jump_to_address(&mut self, address: u64, cx: &mut Context<Self>) {
|
||||
let mut view_state = self.view_state();
|
||||
view_state.base_row = (address & !0xfff) / view_state.line_width.width as u64;
|
||||
let line_ix = (address & 0xfff) / view_state.line_width.width as u64;
|
||||
view_state
|
||||
.scroll_handle
|
||||
.scroll_to_item(line_ix as usize, ScrollStrategy::Center);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn jump_to_expression(&mut self, expr: String, cx: &mut Context<Self>) {
|
||||
let Ok(selected_frame) = self
|
||||
.stack_frame_list
|
||||
.update(cx, |this, _| this.opened_stack_frame_id())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let expr = format!("?${{{expr}}}");
|
||||
let reference = self.session.update(cx, |this, cx| {
|
||||
this.memory_reference_of_expr(selected_frame, expr, cx)
|
||||
});
|
||||
cx.spawn(async move |this, cx| {
|
||||
if let Some((reference, typ)) = reference.await {
|
||||
_ = this.update(cx, |this, cx| {
|
||||
let sizeof_expr = if typ.as_ref().is_some_and(|t| {
|
||||
t.chars()
|
||||
.all(|c| c.is_whitespace() || c.is_alphabetic() || c == '*')
|
||||
}) {
|
||||
typ.as_deref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
this.go_to_memory_reference(&reference, sizeof_expr, selected_frame, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.view_state().selection = None;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Jump to memory pointed to by selected memory range.
|
||||
fn go_to_address(
|
||||
&mut self,
|
||||
_: &GoToSelectedAddress,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(SelectedMemoryRange::DragComplete(drag)) = self.view_state().selection.clone()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let range = drag.memory_range();
|
||||
let Some(memory): Option<Vec<u8>> = self.session.update(cx, |this, cx| {
|
||||
this.read_memory(range, cx).map(|cell| cell.0).collect()
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
if memory.len() > 8 {
|
||||
return;
|
||||
}
|
||||
let zeros_to_write = 8 - memory.len();
|
||||
let mut acc = String::from("0x");
|
||||
acc.extend(std::iter::repeat("00").take(zeros_to_write));
|
||||
let as_query = memory.into_iter().rev().fold(acc, |mut acc, byte| {
|
||||
_ = write!(&mut acc, "{:02x}", byte);
|
||||
acc
|
||||
});
|
||||
self.query_editor.update(cx, |this, cx| {
|
||||
this.set_text(as_query, window, cx);
|
||||
});
|
||||
self.jump_to_query_bar_address(cx);
|
||||
}
|
||||
|
||||
fn deploy_memory_context_menu(
|
||||
&mut self,
|
||||
range: RangeInclusive<u64>,
|
||||
position: Point<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let session = self.session.clone();
|
||||
let context_menu = ContextMenu::build(window, cx, |menu, _, cx| {
|
||||
let range_too_large = range.end() - range.start() > std::mem::size_of::<u64>() as u64;
|
||||
let caps = session.read(cx).capabilities();
|
||||
let supports_data_breakpoints = caps.supports_data_breakpoints.unwrap_or_default()
|
||||
&& caps.supports_data_breakpoint_bytes.unwrap_or_default();
|
||||
let memory_unreadable = LazyCell::new(|| {
|
||||
session.update(cx, |this, cx| {
|
||||
this.read_memory(range.clone(), cx)
|
||||
.any(|cell| cell.0.is_none())
|
||||
})
|
||||
});
|
||||
|
||||
let mut menu = menu.action_disabled_when(
|
||||
range_too_large || *memory_unreadable,
|
||||
"Go To Selected Address",
|
||||
GoToSelectedAddress.boxed_clone(),
|
||||
);
|
||||
|
||||
if supports_data_breakpoints {
|
||||
menu = menu.action_disabled_when(
|
||||
*memory_unreadable,
|
||||
"Set Data Breakpoint",
|
||||
ToggleDataBreakpoint { access_type: None }.boxed_clone(),
|
||||
);
|
||||
}
|
||||
menu.context(self.focus_handle.clone())
|
||||
});
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ViewWidth {
|
||||
width: u8,
|
||||
label: SharedString,
|
||||
}
|
||||
|
||||
impl ViewWidth {
|
||||
const fn new(width: u8, label: &'static str) -> Self {
|
||||
Self {
|
||||
width,
|
||||
label: SharedString::new_static(label),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static WIDTHS: [ViewWidth; 7] = [
|
||||
ViewWidth::new(1, "1 byte"),
|
||||
ViewWidth::new(2, "2 bytes"),
|
||||
ViewWidth::new(4, "4 bytes"),
|
||||
ViewWidth::new(8, "8 bytes"),
|
||||
ViewWidth::new(16, "16 bytes"),
|
||||
ViewWidth::new(32, "32 bytes"),
|
||||
ViewWidth::new(64, "64 bytes"),
|
||||
];
|
||||
|
||||
fn render_single_memory_view_line(
|
||||
memory: &[MemoryCell],
|
||||
ix: u64,
|
||||
weak: gpui::WeakEntity<MemoryView>,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let Ok(view_state) = weak.update(cx, |this, _| this.view_state().clone()) else {
|
||||
return div().into_any();
|
||||
};
|
||||
let base_address = (view_state.base_row + ix) * view_state.line_width.width as u64;
|
||||
|
||||
h_flex()
|
||||
.id((
|
||||
"memory-view-row-full",
|
||||
ix * view_state.line_width.width as u64,
|
||||
))
|
||||
.size_full()
|
||||
.gap_x_2()
|
||||
.child(
|
||||
div()
|
||||
.child(
|
||||
Label::new(format!("{:016X}", base_address))
|
||||
.buffer_font(cx)
|
||||
.size(ui::LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.px_1()
|
||||
.border_r_1()
|
||||
.border_color(Color::Muted.color(cx)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.id((
|
||||
"memory-view-row-raw-memory",
|
||||
ix * view_state.line_width.width as u64,
|
||||
))
|
||||
.px_1()
|
||||
.children(memory.iter().enumerate().map(|(cell_ix, cell)| {
|
||||
let weak = weak.clone();
|
||||
div()
|
||||
.id(("memory-view-row-raw-memory-cell", cell_ix as u64))
|
||||
.px_0p5()
|
||||
.when_some(view_state.selection.as_ref(), |this, selection| {
|
||||
this.when(selection.contains(base_address + cell_ix as u64), |this| {
|
||||
let weak = weak.clone();
|
||||
|
||||
this.bg(Color::Selected.color(cx).opacity(0.2)).when(
|
||||
!selection.is_dragging(),
|
||||
|this| {
|
||||
let selection = selection.drag().memory_range();
|
||||
this.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
move |click, window, cx| {
|
||||
_ = weak.update(cx, |this, cx| {
|
||||
this.deploy_memory_context_menu(
|
||||
selection.clone(),
|
||||
click.position,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.stop_propagation();
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
.child(
|
||||
Label::new(
|
||||
cell.0
|
||||
.map(|val| HEX_BYTES_MEMOIZED[val as usize].clone())
|
||||
.unwrap_or_else(|| UNKNOWN_BYTE.clone()),
|
||||
)
|
||||
.buffer_font(cx)
|
||||
.when(cell.0.is_none(), |this| this.color(Color::Muted))
|
||||
.size(ui::LabelSize::Small),
|
||||
)
|
||||
.on_drag(
|
||||
Drag {
|
||||
start_address: base_address + cell_ix as u64,
|
||||
end_address: base_address + cell_ix as u64,
|
||||
},
|
||||
{
|
||||
let weak = weak.clone();
|
||||
move |drag, _, _, cx| {
|
||||
_ = weak.update(cx, |this, _| {
|
||||
this.view_state().selection =
|
||||
Some(SelectedMemoryRange::DragUnderway(drag.clone()));
|
||||
});
|
||||
|
||||
cx.new(|_| Empty)
|
||||
}
|
||||
},
|
||||
)
|
||||
.on_drop({
|
||||
let weak = weak.clone();
|
||||
move |drag: &Drag, _, cx| {
|
||||
_ = weak.update(cx, |this, _| {
|
||||
this.view_state().selection =
|
||||
Some(SelectedMemoryRange::DragComplete(Drag {
|
||||
start_address: drag.start_address,
|
||||
end_address: base_address + cell_ix as u64,
|
||||
}));
|
||||
});
|
||||
}
|
||||
})
|
||||
.drag_over(move |style, drag: &Drag, _, cx| {
|
||||
_ = weak.update(cx, |this, _| {
|
||||
this.view_state().selection =
|
||||
Some(SelectedMemoryRange::DragUnderway(Drag {
|
||||
start_address: drag.start_address,
|
||||
end_address: base_address + cell_ix as u64,
|
||||
}));
|
||||
});
|
||||
|
||||
style
|
||||
})
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.id((
|
||||
"memory-view-row-ascii-memory",
|
||||
ix * view_state.line_width.width as u64,
|
||||
))
|
||||
.h_full()
|
||||
.px_1()
|
||||
.mr_4()
|
||||
// .gap_x_1p5()
|
||||
.border_x_1()
|
||||
.border_color(Color::Muted.color(cx))
|
||||
.children(memory.iter().enumerate().map(|(ix, cell)| {
|
||||
let as_character = char::from(cell.0.unwrap_or(0));
|
||||
let as_visible = if as_character.is_ascii_graphic() {
|
||||
as_character
|
||||
} else {
|
||||
'·'
|
||||
};
|
||||
div()
|
||||
.px_0p5()
|
||||
.when_some(view_state.selection.as_ref(), |this, selection| {
|
||||
this.when(selection.contains(base_address + ix as u64), |this| {
|
||||
this.bg(Color::Selected.color(cx).opacity(0.2))
|
||||
})
|
||||
})
|
||||
.child(
|
||||
Label::new(format!("{as_visible}"))
|
||||
.buffer_font(cx)
|
||||
.when(cell.0.is_none(), |this| this.color(Color::Muted))
|
||||
.size(ui::LabelSize::Small),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
impl Render for MemoryView {
|
||||
fn render(
|
||||
&mut self,
|
||||
window: &mut ui::Window,
|
||||
cx: &mut ui::Context<Self>,
|
||||
) -> impl ui::IntoElement {
|
||||
let (icon, tooltip_text) = if self.is_writing_memory {
|
||||
(IconName::Pencil, "Edit memory at a selected address")
|
||||
} else {
|
||||
(
|
||||
IconName::LocationEdit,
|
||||
"Change address of currently viewed memory",
|
||||
)
|
||||
};
|
||||
v_flex()
|
||||
.id("Memory-view")
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::go_to_address))
|
||||
.p_1()
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::toggle_data_breakpoint))
|
||||
.on_action(cx.listener(Self::page_down))
|
||||
.on_action(cx.listener(Self::page_up))
|
||||
.size_full()
|
||||
.track_focus(&self.focus_handle)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.mb_0p5()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.gap_x_2()
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.mb_0p5()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.when_else(
|
||||
self.query_editor
|
||||
.focus_handle(cx)
|
||||
.contains_focused(window, cx),
|
||||
|this| this.border_color(cx.theme().colors().border_focused),
|
||||
|this| this.border_color(cx.theme().colors().border_transparent),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("memory-view-editor-icon")
|
||||
.child(Icon::new(icon).size(ui::IconSize::XSmall))
|
||||
.tooltip(Tooltip::text(tooltip_text)),
|
||||
)
|
||||
.child(self.render_query_bar(cx)),
|
||||
)
|
||||
.child(self.render_width_picker(window, cx)),
|
||||
)
|
||||
.child(Divider::horizontal())
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.on_drag_move(cx.listener(|this, evt, _, _| {
|
||||
this.handle_memory_drag(evt);
|
||||
}))
|
||||
.child(self.render_memory(cx).size_full())
|
||||
.children(self.open_context_menu.as_ref().map(|(menu, position, _)| {
|
||||
deferred(
|
||||
anchored()
|
||||
.position(*position)
|
||||
.anchor(gpui::Anchor::TopLeft)
|
||||
.child(menu.clone()),
|
||||
)
|
||||
.with_priority(1)
|
||||
}))
|
||||
.custom_scrollbars(
|
||||
ui::Scrollbars::new(ui::ScrollAxes::Both)
|
||||
.tracked_scroll_handle(&self.view_state_handle)
|
||||
.with_track_along(
|
||||
ui::ScrollAxes::Both,
|
||||
cx.theme().colors().panel_background,
|
||||
)
|
||||
.tracked_entity(cx.entity_id()),
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
286
crates/debugger_ui/src/session/running/module_list.rs
Normal file
286
crates/debugger_ui/src/session/running/module_list.rs
Normal file
@@ -0,0 +1,286 @@
|
||||
use anyhow::anyhow;
|
||||
use dap::Module;
|
||||
use gpui::{
|
||||
AnyElement, Entity, FocusHandle, Focusable, ScrollStrategy, Subscription, Task,
|
||||
UniformListScrollHandle, WeakEntity, uniform_list,
|
||||
};
|
||||
use project::{
|
||||
ProjectItem as _, ProjectPath,
|
||||
debugger::session::{Session, SessionEvent},
|
||||
};
|
||||
use std::{ops::Range, path::Path, sync::Arc};
|
||||
use ui::{WithScrollbar, prelude::*};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct ModuleList {
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
selected_ix: Option<usize>,
|
||||
session: Entity<Session>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
entries: Vec<Module>,
|
||||
_rebuild_task: Option<Task<()>>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl ModuleList {
|
||||
pub fn new(
|
||||
session: Entity<Session>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
|
||||
SessionEvent::Stopped(_)
|
||||
| SessionEvent::HistoricSnapshotSelected
|
||||
| SessionEvent::Modules => {
|
||||
if this._rebuild_task.is_some() {
|
||||
this.schedule_rebuild(cx);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
let scroll_handle = UniformListScrollHandle::new();
|
||||
|
||||
Self {
|
||||
scroll_handle,
|
||||
session,
|
||||
workspace,
|
||||
focus_handle,
|
||||
entries: Vec::new(),
|
||||
selected_ix: None,
|
||||
_subscription,
|
||||
_rebuild_task: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn schedule_rebuild(&mut self, cx: &mut Context<Self>) {
|
||||
self._rebuild_task = Some(cx.spawn(async move |this, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
let modules = this
|
||||
.session
|
||||
.update(cx, |session, cx| session.modules(cx).to_owned());
|
||||
this.entries = modules;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
|
||||
fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let (worktree, relative_path) = this
|
||||
.update(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(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,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
})??
|
||||
.await?;
|
||||
|
||||
this.update_in(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();
|
||||
}
|
||||
|
||||
fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
|
||||
let module = self.entries[ix].clone();
|
||||
|
||||
v_flex()
|
||||
.rounded_md()
|
||||
.w_full()
|
||||
.group("")
|
||||
.id(("module-list", ix))
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.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| {
|
||||
this.selected_ix = Some(ix);
|
||||
if let Some(path) = path.as_ref() {
|
||||
this.open_module(path.clone(), window, cx);
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
})
|
||||
.p_1()
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover))
|
||||
.when(Some(ix) == self.selected_ix, |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, |this, path| this.child(path)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn modules(&self, cx: &mut Context<Self>) -> Vec<dap::Module> {
|
||||
self.session
|
||||
.update(cx, |session, cx| session.modules(cx).to_vec())
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(ix) = self.selected_ix else { return };
|
||||
let Some(entry) = self.entries.get(ix) else {
|
||||
return;
|
||||
};
|
||||
let Some(path) = entry.path.as_deref() else {
|
||||
return;
|
||||
};
|
||||
let path = Arc::from(Path::new(path));
|
||||
self.open_module(path, window, cx);
|
||||
}
|
||||
|
||||
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
|
||||
self.selected_ix = ix;
|
||||
if let Some(ix) = ix {
|
||||
self.scroll_handle
|
||||
.scroll_to_item(ix, ScrollStrategy::Center);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let ix = match self.selected_ix {
|
||||
_ if self.entries.is_empty() => None,
|
||||
None => Some(0),
|
||||
Some(ix) => {
|
||||
if ix == self.entries.len() - 1 {
|
||||
Some(0)
|
||||
} else {
|
||||
Some(ix + 1)
|
||||
}
|
||||
}
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
}
|
||||
|
||||
fn select_previous(
|
||||
&mut self,
|
||||
_: &menu::SelectPrevious,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let ix = match self.selected_ix {
|
||||
_ if self.entries.is_empty() => None,
|
||||
None => Some(self.entries.len() - 1),
|
||||
Some(ix) => {
|
||||
if ix == 0 {
|
||||
Some(self.entries.len() - 1)
|
||||
} else {
|
||||
Some(ix - 1)
|
||||
}
|
||||
}
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
}
|
||||
|
||||
fn select_first(
|
||||
&mut self,
|
||||
_: &menu::SelectFirst,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let ix = if !self.entries.is_empty() {
|
||||
Some(0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let ix = if !self.entries.is_empty() {
|
||||
Some(self.entries.len() - 1)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
}
|
||||
|
||||
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
uniform_list(
|
||||
"module-list",
|
||||
self.entries.len(),
|
||||
cx.processor(|this, range: Range<usize>, _window, cx| {
|
||||
range.map(|ix| this.render_entry(ix, cx)).collect()
|
||||
}),
|
||||
)
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.size_full()
|
||||
}
|
||||
}
|
||||
|
||||
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._rebuild_task.is_none() {
|
||||
self.schedule_rebuild(cx);
|
||||
}
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_previous))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.size_full()
|
||||
.p_1()
|
||||
.child(self.render_list(window, cx))
|
||||
.vertical_scrollbar_for(&self.scroll_handle, window, cx)
|
||||
}
|
||||
}
|
||||
966
crates/debugger_ui/src/session/running/stack_frame_list.rs
Normal file
966
crates/debugger_ui/src/session/running/stack_frame_list.rs
Normal file
@@ -0,0 +1,966 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use dap::StackFrameId;
|
||||
use dap::adapters::DebugAdapterName;
|
||||
use db::kvp::KeyValueStore;
|
||||
use gpui::{
|
||||
Action, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState,
|
||||
Subscription, Task, TaskExt, WeakEntity, list,
|
||||
};
|
||||
use util::{
|
||||
debug_panic,
|
||||
paths::{PathStyle, is_absolute},
|
||||
};
|
||||
|
||||
use crate::ToggleUserFrames;
|
||||
use language::PointUtf16;
|
||||
use project::debugger::breakpoint_store::ActiveStackFrame;
|
||||
use project::debugger::session::{Session, SessionEvent, StackFrame, ThreadStatus};
|
||||
use project::{ProjectItem, ProjectPath};
|
||||
use ui::{Tooltip, WithScrollbar, prelude::*};
|
||||
use workspace::{Workspace, WorkspaceId};
|
||||
|
||||
use super::RunningState;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StackFrameListEvent {
|
||||
SelectedStackFrameChanged(StackFrameId),
|
||||
BuiltEntries,
|
||||
}
|
||||
|
||||
/// Represents the filter applied to the stack frame list
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
pub(crate) enum StackFrameFilter {
|
||||
/// Show all frames
|
||||
All,
|
||||
/// Show only frames from the user's code
|
||||
OnlyUserFrames,
|
||||
}
|
||||
|
||||
impl StackFrameFilter {
|
||||
fn from_str_or_default(s: impl AsRef<str>) -> Self {
|
||||
match s.as_ref() {
|
||||
"user" => StackFrameFilter::OnlyUserFrames,
|
||||
"all" => StackFrameFilter::All,
|
||||
_ => StackFrameFilter::All,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StackFrameFilter> for String {
|
||||
fn from(filter: StackFrameFilter) -> Self {
|
||||
match filter {
|
||||
StackFrameFilter::All => "all".to_string(),
|
||||
StackFrameFilter::OnlyUserFrames => "user".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn stack_frame_filter_key(
|
||||
adapter_name: &DebugAdapterName,
|
||||
workspace_id: WorkspaceId,
|
||||
) -> String {
|
||||
let database_id: i64 = workspace_id.into();
|
||||
format!("stack-frame-list-filter-{}-{}", adapter_name.0, database_id)
|
||||
}
|
||||
|
||||
pub struct StackFrameList {
|
||||
focus_handle: FocusHandle,
|
||||
_subscription: Subscription,
|
||||
session: Entity<Session>,
|
||||
state: WeakEntity<RunningState>,
|
||||
entries: Vec<StackFrameEntry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
selected_ix: Option<usize>,
|
||||
opened_stack_frame_id: Option<StackFrameId>,
|
||||
list_state: ListState,
|
||||
list_filter: StackFrameFilter,
|
||||
filter_entries_indices: Vec<usize>,
|
||||
error: Option<SharedString>,
|
||||
_refresh_task: Task<()>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum StackFrameEntry {
|
||||
Normal(dap::StackFrame),
|
||||
/// Used to indicate that the frame is artificial and is a visual label or separator
|
||||
Label(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 focus_handle = cx.focus_handle();
|
||||
|
||||
let _subscription =
|
||||
cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
|
||||
SessionEvent::Threads => {
|
||||
this.schedule_refresh(false, window, cx);
|
||||
}
|
||||
SessionEvent::Stopped(..)
|
||||
| SessionEvent::StackTrace
|
||||
| SessionEvent::HistoricSnapshotSelected => {
|
||||
this.schedule_refresh(true, window, cx);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
|
||||
|
||||
let list_filter = workspace
|
||||
.read_with(cx, |workspace, _| workspace.database_id())
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|database_id| {
|
||||
let key = stack_frame_filter_key(&session.read(cx).adapter(), database_id);
|
||||
KeyValueStore::global(cx)
|
||||
.read_kvp(&key)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(StackFrameFilter::from_str_or_default)
|
||||
})
|
||||
.unwrap_or(StackFrameFilter::All);
|
||||
|
||||
let mut this = Self {
|
||||
session,
|
||||
workspace,
|
||||
focus_handle,
|
||||
state,
|
||||
_subscription,
|
||||
entries: Default::default(),
|
||||
filter_entries_indices: Vec::default(),
|
||||
error: None,
|
||||
selected_ix: None,
|
||||
opened_stack_frame_id: None,
|
||||
list_filter,
|
||||
list_state,
|
||||
_refresh_task: Task::ready(()),
|
||||
};
|
||||
this.schedule_refresh(true, window, cx);
|
||||
this
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn entries(&self) -> &Vec<StackFrameEntry> {
|
||||
&self.entries
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn flatten_entries(
|
||||
&self,
|
||||
show_collapsed: bool,
|
||||
show_labels: bool,
|
||||
) -> Vec<dap::StackFrame> {
|
||||
self.entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(ix, _)| {
|
||||
self.list_filter == StackFrameFilter::All
|
||||
|| self
|
||||
.filter_entries_indices
|
||||
.binary_search_by_key(&ix, |ix| ix)
|
||||
.is_ok()
|
||||
})
|
||||
.flat_map(|(_, frame)| match frame {
|
||||
StackFrameEntry::Normal(frame) => vec![frame.clone()],
|
||||
StackFrameEntry::Label(frame) if show_labels => vec![frame.clone()],
|
||||
StackFrameEntry::Collapsed(frames) if show_collapsed => frames.clone(),
|
||||
_ => vec![],
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn stack_frames(&self, cx: &mut App) -> Result<Vec<StackFrame>> {
|
||||
if let Ok(Some(thread_id)) = self.state.read_with(cx, |state, _| state.thread_id) {
|
||||
self.session
|
||||
.update(cx, |this, cx| this.stack_frames(thread_id, cx))
|
||||
} else {
|
||||
Ok(Vec::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
|
||||
match self.list_filter {
|
||||
StackFrameFilter::All => self
|
||||
.stack_frames(cx)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|stack_frame| stack_frame.dap)
|
||||
.collect(),
|
||||
StackFrameFilter::OnlyUserFrames => self
|
||||
.filter_entries_indices
|
||||
.iter()
|
||||
.map(|ix| match &self.entries[*ix] {
|
||||
StackFrameEntry::Label(label) => label,
|
||||
StackFrameEntry::Collapsed(_) => panic!("Collapsed tabs should not be visible"),
|
||||
StackFrameEntry::Normal(frame) => frame,
|
||||
})
|
||||
.cloned()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn list_filter(&self) -> StackFrameFilter {
|
||||
self.list_filter
|
||||
}
|
||||
|
||||
pub fn opened_stack_frame_id(&self) -> Option<StackFrameId> {
|
||||
self.opened_stack_frame_id
|
||||
}
|
||||
|
||||
pub(super) fn schedule_refresh(
|
||||
&mut self,
|
||||
select_first: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
const REFRESH_DEBOUNCE: Duration = Duration::from_millis(20);
|
||||
|
||||
self._refresh_task = cx.spawn_in(window, async move |this, cx| {
|
||||
let debounce = this
|
||||
.update(cx, |this, cx| {
|
||||
let new_stack_frames = this.stack_frames(cx);
|
||||
new_stack_frames.unwrap_or_default().is_empty() && !this.entries.is_empty()
|
||||
})
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
if debounce {
|
||||
cx.background_executor().timer(REFRESH_DEBOUNCE).await;
|
||||
}
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.build_entries(select_first, window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_entries(
|
||||
&mut self,
|
||||
open_first_stack_frame: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let old_selected_frame_id = self
|
||||
.selected_ix
|
||||
.and_then(|ix| self.entries.get(ix))
|
||||
.and_then(|entry| match entry {
|
||||
StackFrameEntry::Normal(stack_frame) => Some(stack_frame.id),
|
||||
StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => None,
|
||||
});
|
||||
let mut entries = Vec::new();
|
||||
let mut collapsed_entries = Vec::new();
|
||||
let mut first_stack_frame = None;
|
||||
let mut first_stack_frame_with_path = None;
|
||||
|
||||
let stack_frames = match self.stack_frames(cx) {
|
||||
Ok(stack_frames) => stack_frames,
|
||||
Err(e) => {
|
||||
self.error = Some(format!("{}", e).into());
|
||||
self.entries.clear();
|
||||
self.selected_ix = None;
|
||||
self.list_state.reset(0);
|
||||
self.filter_entries_indices.clear();
|
||||
cx.emit(StackFrameListEvent::BuiltEntries);
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let worktree_prefixes: Vec<_> = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, cx| {
|
||||
workspace
|
||||
.visible_worktrees(cx)
|
||||
.map(|tree| tree.read(cx).abs_path())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut filter_entries_indices = Vec::default();
|
||||
for stack_frame in stack_frames.iter() {
|
||||
let frame_in_visible_worktree = stack_frame.dap.source.as_ref().is_some_and(|source| {
|
||||
source.path.as_ref().is_some_and(|path| {
|
||||
worktree_prefixes
|
||||
.iter()
|
||||
.filter_map(|tree| tree.to_str())
|
||||
.any(|tree| path.starts_with(tree))
|
||||
})
|
||||
});
|
||||
|
||||
match stack_frame.dap.presentation_hint {
|
||||
Some(dap::StackFramePresentationHint::Deemphasize)
|
||||
| Some(dap::StackFramePresentationHint::Subtle) => {
|
||||
collapsed_entries.push(stack_frame.dap.clone());
|
||||
}
|
||||
Some(dap::StackFramePresentationHint::Label) => {
|
||||
entries.push(StackFrameEntry::Label(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()));
|
||||
}
|
||||
|
||||
first_stack_frame.get_or_insert(entries.len());
|
||||
|
||||
if stack_frame
|
||||
.dap
|
||||
.source
|
||||
.as_ref()
|
||||
.is_some_and(|source| source.path.is_some())
|
||||
{
|
||||
first_stack_frame_with_path.get_or_insert(entries.len());
|
||||
}
|
||||
entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
|
||||
if frame_in_visible_worktree {
|
||||
filter_entries_indices.push(entries.len() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let collapsed_entries = std::mem::take(&mut collapsed_entries);
|
||||
if !collapsed_entries.is_empty() {
|
||||
entries.push(StackFrameEntry::Collapsed(collapsed_entries));
|
||||
}
|
||||
self.entries = entries;
|
||||
self.filter_entries_indices = filter_entries_indices;
|
||||
|
||||
if let Some(ix) = first_stack_frame_with_path
|
||||
.or(first_stack_frame)
|
||||
.filter(|_| open_first_stack_frame)
|
||||
{
|
||||
self.select_ix(Some(ix), cx);
|
||||
self.activate_selected_entry(window, cx);
|
||||
} else if let Some(old_selected_frame_id) = old_selected_frame_id {
|
||||
let ix = self.entries.iter().position(|entry| match entry {
|
||||
StackFrameEntry::Normal(frame) => frame.id == old_selected_frame_id,
|
||||
StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => false,
|
||||
});
|
||||
self.selected_ix = ix;
|
||||
}
|
||||
|
||||
match self.list_filter {
|
||||
StackFrameFilter::All => {
|
||||
self.list_state.reset(self.entries.len());
|
||||
}
|
||||
StackFrameFilter::OnlyUserFrames => {
|
||||
self.list_state.reset(self.filter_entries_indices.len());
|
||||
}
|
||||
}
|
||||
cx.emit(StackFrameListEvent::BuiltEntries);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn go_to_stack_frame(
|
||||
&mut self,
|
||||
stack_frame_id: StackFrameId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(stack_frame) = self
|
||||
.entries
|
||||
.iter()
|
||||
.flat_map(|entry| match entry {
|
||||
StackFrameEntry::Label(stack_frame) => std::slice::from_ref(stack_frame),
|
||||
StackFrameEntry::Normal(stack_frame) => std::slice::from_ref(stack_frame),
|
||||
StackFrameEntry::Collapsed(stack_frames) => stack_frames.as_slice(),
|
||||
})
|
||||
.find(|stack_frame| stack_frame.id == stack_frame_id)
|
||||
.cloned()
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("No stack frame for ID")));
|
||||
};
|
||||
self.go_to_stack_frame_inner(stack_frame, window, cx)
|
||||
}
|
||||
|
||||
fn go_to_stack_frame_inner(
|
||||
&mut self,
|
||||
stack_frame: dap::StackFrame,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let stack_frame_id = stack_frame.id;
|
||||
self.opened_stack_frame_id = Some(stack_frame_id);
|
||||
let Some(abs_path) = Self::abs_path_from_stack_frame(&stack_frame) else {
|
||||
return Task::ready(Err(anyhow!("Project path not found")));
|
||||
};
|
||||
let row = stack_frame.line.saturating_sub(1) as u32;
|
||||
cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
|
||||
stack_frame_id,
|
||||
));
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let (worktree, relative_path) = this
|
||||
.update(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(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,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
})??
|
||||
.await?;
|
||||
let position = buffer.read_with(cx, |this, _| {
|
||||
this.snapshot().anchor_after(PointUtf16::new(row, 0))
|
||||
});
|
||||
let opened_item = this
|
||||
.update_in(cx, |this, window, cx| {
|
||||
this.workspace.update(cx, |workspace, cx| {
|
||||
let project_path = buffer
|
||||
.read(cx)
|
||||
.project_path(cx)
|
||||
.context("Could not select a stack frame for unnamed buffer")?;
|
||||
|
||||
let open_preview = true;
|
||||
|
||||
let active_debug_line_pane = workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.breakpoint_store()
|
||||
.read(cx)
|
||||
.active_debug_line_pane_id()
|
||||
.and_then(|id| workspace.pane_for_entity_id(id));
|
||||
|
||||
let debug_pane = if let Some(pane) = active_debug_line_pane {
|
||||
Some(pane.downgrade())
|
||||
} else {
|
||||
// No debug pane set yet. Find a pane where the target file
|
||||
// is already the active tab so we don't disrupt other panes.
|
||||
let pane_with_active_file = workspace.panes().iter().find(|pane| {
|
||||
pane.read(cx)
|
||||
.active_item()
|
||||
.and_then(|item| item.project_path(cx))
|
||||
.is_some_and(|path| path == project_path)
|
||||
});
|
||||
|
||||
pane_with_active_file.map(|pane| pane.downgrade())
|
||||
};
|
||||
|
||||
anyhow::Ok(workspace.open_path_preview(
|
||||
project_path,
|
||||
debug_pane,
|
||||
true,
|
||||
true,
|
||||
open_preview,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
})???
|
||||
.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let thread_id = this.state.read_with(cx, |state, _| {
|
||||
state.thread_id.context("No selected thread ID found")
|
||||
})??;
|
||||
|
||||
this.workspace.update(cx, |workspace, cx| {
|
||||
if let Some(pane_id) = workspace
|
||||
.pane_for(&*opened_item)
|
||||
.map(|pane| pane.entity_id())
|
||||
{
|
||||
workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.breakpoint_store()
|
||||
.update(cx, |store, _cx| {
|
||||
store.set_active_debug_pane_id(pane_id);
|
||||
});
|
||||
}
|
||||
|
||||
let breakpoint_store = workspace.project().read(cx).breakpoint_store();
|
||||
|
||||
breakpoint_store.update(cx, |store, cx| {
|
||||
store.set_active_position(
|
||||
ActiveStackFrame {
|
||||
session_id: this.session.read(cx).session_id(),
|
||||
thread_id,
|
||||
stack_frame_id,
|
||||
path: abs_path,
|
||||
position,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
})?
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn abs_path_from_stack_frame(stack_frame: &dap::StackFrame) -> Option<Arc<Path>> {
|
||||
stack_frame.source.as_ref().and_then(|s| {
|
||||
s.path
|
||||
.as_deref()
|
||||
.filter(|path| {
|
||||
// Since we do not know if we are debugging on the host or (a remote/WSL) target,
|
||||
// we need to check if either the path is absolute as Posix or Windows.
|
||||
is_absolute(path, PathStyle::Posix) || is_absolute(path, PathStyle::Windows)
|
||||
})
|
||||
.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_label_entry(
|
||||
&self,
|
||||
stack_frame: &dap::StackFrame,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
h_flex()
|
||||
.rounded_md()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.group("")
|
||||
.id(("label-stack-frame", stack_frame.id))
|
||||
.p_1()
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.child(
|
||||
v_flex().justify_center().gap_0p5().child(
|
||||
Label::new(stack_frame.name.clone())
|
||||
.size(LabelSize::Small)
|
||||
.weight(FontWeight::BOLD)
|
||||
.truncate()
|
||||
.color(Color::Info),
|
||||
),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_normal_entry(
|
||||
&self,
|
||||
ix: usize,
|
||||
stack_frame: &dap::StackFrame,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let source = stack_frame.source.clone();
|
||||
let is_selected_frame = Some(ix) == self.selected_ix;
|
||||
|
||||
let path = source.and_then(|s| s.path.or(s.name));
|
||||
let formatted_path = path.map(|path| format!("{}:{}", path, stack_frame.line,));
|
||||
let formatted_path = formatted_path.map(|path| {
|
||||
Label::new(path)
|
||||
.size(LabelSize::XSmall)
|
||||
.line_height_style(LineHeightStyle::UiLabel)
|
||||
.truncate()
|
||||
.color(Color::Muted)
|
||||
});
|
||||
|
||||
let supports_frame_restart = self
|
||||
.session
|
||||
.read(cx)
|
||||
.capabilities()
|
||||
.supports_restart_frame
|
||||
.unwrap_or_default();
|
||||
|
||||
let should_deemphasize = matches!(
|
||||
stack_frame.presentation_hint,
|
||||
Some(
|
||||
dap::StackFramePresentationHint::Subtle
|
||||
| dap::StackFramePresentationHint::Deemphasize
|
||||
)
|
||||
);
|
||||
h_flex()
|
||||
.rounded_md()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.group("")
|
||||
.id(("stack-frame", stack_frame.id))
|
||||
.p_1()
|
||||
.when(is_selected_frame, |this| {
|
||||
this.bg(cx.theme().colors().element_hover)
|
||||
})
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.selected_ix = Some(ix);
|
||||
this.activate_selected_entry(window, cx);
|
||||
}))
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
|
||||
.overflow_x_scroll()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(stack_frame.name.clone())
|
||||
.size(LabelSize::Small)
|
||||
.truncate()
|
||||
.when(should_deemphasize, |this| this.color(Color::Muted)),
|
||||
)
|
||||
.children(formatted_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::RotateCcw,
|
||||
)
|
||||
.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(crate) fn expand_collapsed_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
|
||||
let Some(StackFrameEntry::Collapsed(stack_frames)) = self.entries.get_mut(ix) else {
|
||||
return;
|
||||
};
|
||||
let entries = std::mem::take(stack_frames)
|
||||
.into_iter()
|
||||
.map(StackFrameEntry::Normal);
|
||||
// HERE
|
||||
let entries_len = entries.len();
|
||||
self.entries.splice(ix..ix + 1, entries);
|
||||
let (Ok(filtered_indices_start) | Err(filtered_indices_start)) =
|
||||
self.filter_entries_indices.binary_search(&ix);
|
||||
|
||||
for idx in &mut self.filter_entries_indices[filtered_indices_start..] {
|
||||
*idx += entries_len - 1;
|
||||
}
|
||||
|
||||
self.selected_ix = Some(ix);
|
||||
self.list_state.reset(self.entries.len());
|
||||
cx.emit(StackFrameListEvent::BuiltEntries);
|
||||
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];
|
||||
let is_selected = Some(ix) == self.selected_ix;
|
||||
|
||||
h_flex()
|
||||
.rounded_md()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.group("")
|
||||
.id(("stack-frame", first_stack_frame.id))
|
||||
.p_1()
|
||||
.when(is_selected, |this| {
|
||||
this.bg(cx.theme().colors().element_hover)
|
||||
})
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.selected_ix = Some(ix);
|
||||
this.activate_selected_entry(window, 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 {
|
||||
let ix = match self.list_filter {
|
||||
StackFrameFilter::All => ix,
|
||||
StackFrameFilter::OnlyUserFrames => self.filter_entries_indices[ix],
|
||||
};
|
||||
|
||||
match &self.entries[ix] {
|
||||
StackFrameEntry::Label(stack_frame) => self.render_label_entry(stack_frame, cx),
|
||||
StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx),
|
||||
StackFrameEntry::Collapsed(stack_frames) => {
|
||||
self.render_collapsed_entry(ix, stack_frames, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
|
||||
self.selected_ix = ix;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let ix = match self.selected_ix {
|
||||
_ if self.entries.is_empty() => None,
|
||||
None => Some(0),
|
||||
Some(ix) => {
|
||||
if ix == self.entries.len() - 1 {
|
||||
Some(0)
|
||||
} else {
|
||||
Some(ix + 1)
|
||||
}
|
||||
}
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
}
|
||||
|
||||
fn select_previous(
|
||||
&mut self,
|
||||
_: &menu::SelectPrevious,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let ix = match self.selected_ix {
|
||||
_ if self.entries.is_empty() => None,
|
||||
None => Some(self.entries.len() - 1),
|
||||
Some(ix) => {
|
||||
if ix == 0 {
|
||||
Some(self.entries.len() - 1)
|
||||
} else {
|
||||
Some(ix - 1)
|
||||
}
|
||||
}
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
}
|
||||
|
||||
fn select_first(
|
||||
&mut self,
|
||||
_: &menu::SelectFirst,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let ix = if !self.entries.is_empty() {
|
||||
Some(0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let ix = if !self.entries.is_empty() {
|
||||
Some(self.entries.len() - 1)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
}
|
||||
|
||||
fn activate_selected_entry(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(ix) = self.selected_ix else {
|
||||
return;
|
||||
};
|
||||
let Some(entry) = self.entries.get_mut(ix) else {
|
||||
return;
|
||||
};
|
||||
match entry {
|
||||
StackFrameEntry::Normal(stack_frame) => {
|
||||
let stack_frame = stack_frame.clone();
|
||||
self.go_to_stack_frame_inner(stack_frame, window, cx)
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
StackFrameEntry::Label(_) => {
|
||||
debug_panic!("You should not be able to select a label stack frame")
|
||||
}
|
||||
StackFrameEntry::Collapsed(_) => self.expand_collapsed_entry(ix, cx),
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.activate_selected_entry(window, cx);
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_frame_filter(
|
||||
&mut self,
|
||||
thread_status: Option<ThreadStatus>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.list_filter = match self.list_filter {
|
||||
StackFrameFilter::All => StackFrameFilter::OnlyUserFrames,
|
||||
StackFrameFilter::OnlyUserFrames => StackFrameFilter::All,
|
||||
};
|
||||
|
||||
if let Some(database_id) = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| workspace.database_id())
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
let key = stack_frame_filter_key(&self.session.read(cx).adapter(), database_id);
|
||||
let kvp = KeyValueStore::global(cx);
|
||||
let filter: String = self.list_filter.into();
|
||||
cx.background_spawn(async move { kvp.write_kvp(key, filter).await })
|
||||
.detach();
|
||||
}
|
||||
|
||||
if let Some(ThreadStatus::Stopped) = thread_status {
|
||||
match self.list_filter {
|
||||
StackFrameFilter::All => {
|
||||
self.list_state.reset(self.entries.len());
|
||||
}
|
||||
StackFrameFilter::OnlyUserFrames => {
|
||||
self.list_state.reset(self.filter_entries_indices.len());
|
||||
if !self
|
||||
.selected_ix
|
||||
.map(|ix| self.filter_entries_indices.contains(&ix))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
self.selected_ix = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ix) = self.selected_ix {
|
||||
let scroll_to = match self.list_filter {
|
||||
StackFrameFilter::All => ix,
|
||||
StackFrameFilter::OnlyUserFrames => self
|
||||
.filter_entries_indices
|
||||
.binary_search_by_key(&ix, |ix| *ix)
|
||||
.expect("This index will always exist"),
|
||||
};
|
||||
self.list_state.scroll_to_reveal_item(scroll_to);
|
||||
}
|
||||
|
||||
cx.emit(StackFrameListEvent::BuiltEntries);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().p_1().size_full().child(
|
||||
list(
|
||||
self.list_state.clone(),
|
||||
cx.processor(|this, ix, _window, cx| this.render_entry(ix, cx)),
|
||||
)
|
||||
.size_full(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn render_control_strip(&self) -> AnyElement {
|
||||
let tooltip_title = match self.list_filter {
|
||||
StackFrameFilter::All => "Show stack frames from your project",
|
||||
StackFrameFilter::OnlyUserFrames => "Show all stack frames",
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.child(
|
||||
IconButton::new(
|
||||
"filter-by-visible-worktree-stack-frame-list",
|
||||
IconName::ListFilter,
|
||||
)
|
||||
.tooltip(move |_window, cx| {
|
||||
Tooltip::for_action(tooltip_title, &ToggleUserFrames, cx)
|
||||
})
|
||||
.toggle_state(self.list_filter == StackFrameFilter::OnlyUserFrames)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(ToggleUserFrames.boxed_clone(), cx)
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for StackFrameList {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_previous))
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.when_some(self.error.clone(), |el, error| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.bg(cx.theme().status().warning_background)
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().status().warning_border)
|
||||
.pl_1()
|
||||
.child(Icon::new(IconName::Warning).color(Color::Warning))
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new(error)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Warning),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(self.render_list(window, cx))
|
||||
.vertical_scrollbar_for(&self.list_state, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for StackFrameList {
|
||||
fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<StackFrameListEvent> for StackFrameList {}
|
||||
1664
crates/debugger_ui/src/session/running/variable_list.rs
Normal file
1664
crates/debugger_ui/src/session/running/variable_list.rs
Normal file
File diff suppressed because it is too large
Load Diff
170
crates/debugger_ui/src/tests.rs
Normal file
170
crates/debugger_ui/src/tests.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use dap::adapters::DebugTaskDefinition;
|
||||
use dap::client::DebugAdapterClient;
|
||||
use gpui::{Entity, TestAppContext, WindowHandle};
|
||||
use project::{Project, debugger::session::Session};
|
||||
use settings::SettingsStore;
|
||||
use task::SharedTaskContext;
|
||||
use terminal_view::terminal_panel::TerminalPanel;
|
||||
use workspace::MultiWorkspace;
|
||||
|
||||
use crate::{debugger_panel::DebugPanel, session::DebugSession};
|
||||
|
||||
#[cfg(test)]
|
||||
mod attach_modal;
|
||||
#[cfg(test)]
|
||||
mod console;
|
||||
#[cfg(test)]
|
||||
mod dap_logger;
|
||||
#[cfg(test)]
|
||||
mod debugger_panel;
|
||||
#[cfg(test)]
|
||||
mod inline_values;
|
||||
#[cfg(test)]
|
||||
mod module_list;
|
||||
#[cfg(test)]
|
||||
mod new_process_modal;
|
||||
#[cfg(test)]
|
||||
mod persistence;
|
||||
#[cfg(test)]
|
||||
mod stack_frame_list;
|
||||
#[cfg(test)]
|
||||
mod variable_list;
|
||||
|
||||
pub fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
#[cfg(test)]
|
||||
zlog::init_test();
|
||||
|
||||
cx.update(|cx| {
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
terminal_view::init(cx);
|
||||
theme_settings::init(theme::LoadThemes::JustBase, cx);
|
||||
command_palette_hooks::init(cx);
|
||||
editor::init(cx);
|
||||
crate::init(cx);
|
||||
dap_adapters::init(cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn init_test_workspace(
|
||||
project: &Entity<Project>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> WindowHandle<MultiWorkspace> {
|
||||
let workspace_handle =
|
||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
|
||||
let debugger_panel = workspace_handle
|
||||
.update(cx, |multi, window, cx| {
|
||||
multi.workspace().update(cx, |_workspace, cx| {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
DebugPanel::load(this, cx).await
|
||||
})
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.expect("Failed to load debug panel");
|
||||
|
||||
let terminal_panel = workspace_handle
|
||||
.update(cx, |multi, window, cx| {
|
||||
let weak_workspace = multi.workspace().downgrade();
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
TerminalPanel::load(weak_workspace, cx.clone()).await
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.expect("Failed to load terminal panel");
|
||||
|
||||
workspace_handle
|
||||
.update(cx, |multi, window, cx| {
|
||||
multi.workspace().update(cx, |workspace, cx| {
|
||||
workspace.add_panel(debugger_panel, window, cx);
|
||||
workspace.add_panel(terminal_panel, window, cx);
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
workspace_handle
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn active_debug_session_panel(
|
||||
workspace: WindowHandle<MultiWorkspace>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Entity<DebugSession> {
|
||||
workspace
|
||||
.update(cx, |multi, _window, cx| {
|
||||
multi.workspace().update(cx, |workspace, cx| {
|
||||
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
|
||||
debug_panel
|
||||
.update(cx, |this, _| this.active_session())
|
||||
.unwrap()
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn start_debug_session_with<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
|
||||
workspace: &WindowHandle<MultiWorkspace>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
config: DebugTaskDefinition,
|
||||
configure: T,
|
||||
) -> Result<Entity<Session>> {
|
||||
let _subscription = project::debugger::test::intercept_debug_sessions(cx, configure);
|
||||
workspace.update(cx, |multi, window, cx| {
|
||||
multi.workspace().update(cx, |workspace, cx| {
|
||||
workspace.start_debug_session(
|
||||
config.to_scenario(),
|
||||
SharedTaskContext::default(),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?;
|
||||
cx.run_until_parked();
|
||||
let session = workspace.read_with(cx, |workspace, cx| {
|
||||
workspace
|
||||
.workspace()
|
||||
.read(cx)
|
||||
.panel::<DebugPanel>(cx)
|
||||
.and_then(|panel| {
|
||||
panel
|
||||
.read(cx)
|
||||
.sessions_with_children
|
||||
.keys()
|
||||
.max_by_key(|session| session.read(cx).session_id(cx))
|
||||
})
|
||||
.map(|session| session.read(cx).running_state().read(cx).session())
|
||||
.cloned()
|
||||
.context("Failed to get active session")
|
||||
})??;
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
pub fn start_debug_session<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
|
||||
workspace: &WindowHandle<MultiWorkspace>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
configure: T,
|
||||
) -> Result<Entity<Session>> {
|
||||
use serde_json::json;
|
||||
|
||||
start_debug_session_with(
|
||||
workspace,
|
||||
cx,
|
||||
DebugTaskDefinition {
|
||||
adapter: "fake-adapter".into(),
|
||||
label: "test".into(),
|
||||
config: json!({
|
||||
"request": "launch"
|
||||
}),
|
||||
tcp_connection: None,
|
||||
},
|
||||
configure,
|
||||
)
|
||||
}
|
||||
320
crates/debugger_ui/src/tests/attach_modal.rs
Normal file
320
crates/debugger_ui/src/tests/attach_modal.rs
Normal file
@@ -0,0 +1,320 @@
|
||||
#![expect(clippy::result_large_err)]
|
||||
use crate::{
|
||||
attach_modal::{Candidate, ModalIntent},
|
||||
tests::start_debug_session_with,
|
||||
*,
|
||||
};
|
||||
use attach_modal::AttachModal;
|
||||
use dap::{FakeAdapter, adapters::DebugTaskDefinition};
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use menu::Confirm;
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use task::{AttachRequest, SharedTaskContext};
|
||||
use tests::{init_test, init_test_workspace};
|
||||
use util::path;
|
||||
|
||||
#[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(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.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 _session = start_debug_session_with(
|
||||
&workspace,
|
||||
cx,
|
||||
DebugTaskDefinition {
|
||||
adapter: "fake-adapter".into(),
|
||||
label: "label".into(),
|
||||
config: json!({
|
||||
"request": "attach",
|
||||
"process_id": 10,
|
||||
}),
|
||||
tcp_connection: None,
|
||||
},
|
||||
|client| {
|
||||
client.on_request::<dap::requests::Attach, _>(move |_, args| {
|
||||
let raw = &args.raw;
|
||||
assert_eq!(raw["request"], "attach");
|
||||
assert_eq!(raw["process_id"], 10);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// assert we didn't show the attach modal
|
||||
workspace
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
assert!(
|
||||
workspace
|
||||
.workspace()
|
||||
.read(cx)
|
||||
.active_modal::<AttachModal>(cx)
|
||||
.is_none()
|
||||
);
|
||||
})
|
||||
.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(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.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);
|
||||
// Set up handlers for sessions spawned via modal.
|
||||
let _initialize_subscription =
|
||||
project::debugger::test::intercept_debug_sessions(cx, |client| {
|
||||
client.on_request::<dap::requests::Attach, _>(move |_, args| {
|
||||
let raw = &args.raw;
|
||||
assert_eq!(raw["request"], "attach");
|
||||
assert_eq!(raw["process_id"], 1);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
});
|
||||
let attach_modal = workspace
|
||||
.update(cx, |multi, window, cx| {
|
||||
let workspace_handle = multi.workspace().downgrade();
|
||||
multi.toggle_modal(window, cx, |window, cx| {
|
||||
AttachModal::with_processes(
|
||||
workspace_handle,
|
||||
vec![
|
||||
Candidate {
|
||||
pid: 0,
|
||||
name: "fake-binary-1".into(),
|
||||
command: vec![],
|
||||
},
|
||||
Candidate {
|
||||
pid: 3,
|
||||
name: "real-binary-1".into(),
|
||||
command: vec![],
|
||||
},
|
||||
Candidate {
|
||||
pid: 1,
|
||||
name: "fake-binary-2".into(),
|
||||
command: vec![],
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
true,
|
||||
ModalIntent::AttachToProcess(task::ZedDebugConfig {
|
||||
adapter: FakeAdapter::ADAPTER_NAME.into(),
|
||||
request: dap::DebugRequest::Attach(AttachRequest::default()),
|
||||
label: "attach example".into(),
|
||||
stop_on_entry: None,
|
||||
}),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
multi.active_modal::<AttachModal>(cx).unwrap()
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// assert we got the expected processes
|
||||
workspace
|
||||
.update(cx, |_, window, cx| {
|
||||
let names = attach_modal.update(cx, |modal, cx| attach_modal::process_names(modal, cx));
|
||||
// Initially all processes are visible.
|
||||
assert_eq!(3, names.len());
|
||||
attach_modal.update(cx, |this, cx| {
|
||||
this.picker.update(cx, |this, cx| {
|
||||
this.set_query("fakb", window, cx);
|
||||
})
|
||||
})
|
||||
})
|
||||
.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));
|
||||
// Initially all processes are visible.
|
||||
assert_eq!(2, 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();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_attach_with_pick_pid_variable(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.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 _initialize_subscription =
|
||||
project::debugger::test::intercept_debug_sessions(cx, |client| {
|
||||
client.on_request::<dap::requests::Attach, _>(move |_, args| {
|
||||
let raw = &args.raw;
|
||||
assert_eq!(raw["request"], "attach");
|
||||
assert_eq!(
|
||||
raw["process_id"], "42",
|
||||
"verify process id has been replaced"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
});
|
||||
|
||||
let pick_pid_placeholder = task::VariableName::PickProcessId.template_value();
|
||||
workspace
|
||||
.update(cx, |multi, window, cx| {
|
||||
multi.workspace().update(cx, |workspace, cx| {
|
||||
workspace.start_debug_session(
|
||||
DebugTaskDefinition {
|
||||
adapter: FakeAdapter::ADAPTER_NAME.into(),
|
||||
label: "attach with picker".into(),
|
||||
config: json!({
|
||||
"request": "attach",
|
||||
"process_id": pick_pid_placeholder,
|
||||
}),
|
||||
tcp_connection: None,
|
||||
}
|
||||
.to_scenario(),
|
||||
SharedTaskContext::default(),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let attach_modal = workspace
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
workspace.active_modal::<AttachModal>(cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
attach_modal.is_some(),
|
||||
"Attach modal should open when config contains ZED_PICK_PID"
|
||||
);
|
||||
|
||||
let attach_modal = attach_modal.unwrap();
|
||||
|
||||
workspace
|
||||
.update(cx, |_, window, cx| {
|
||||
attach_modal.update(cx, |modal, cx| {
|
||||
attach_modal::set_candidates(
|
||||
modal,
|
||||
vec![
|
||||
Candidate {
|
||||
pid: 10,
|
||||
name: "process-1".into(),
|
||||
command: vec![],
|
||||
},
|
||||
Candidate {
|
||||
pid: 42,
|
||||
name: "target-process".into(),
|
||||
command: vec![],
|
||||
},
|
||||
Candidate {
|
||||
pid: 99,
|
||||
name: "process-3".into(),
|
||||
command: vec![],
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
workspace
|
||||
.update(cx, |_, window, cx| {
|
||||
attach_modal.update(cx, |modal, cx| {
|
||||
modal.picker.update(cx, |picker, cx| {
|
||||
picker.set_query("target", window, cx);
|
||||
})
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
workspace
|
||||
.update(cx, |_, _, cx| {
|
||||
let names = attach_modal.update(cx, |modal, cx| attach_modal::process_names(modal, cx));
|
||||
assert_eq!(names.len(), 1);
|
||||
assert_eq!(names[0], " 42 target-process");
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
cx.dispatch_action(Confirm);
|
||||
cx.run_until_parked();
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
assert!(
|
||||
workspace.active_modal::<AttachModal>(cx).is_none(),
|
||||
"Attach modal should be dismissed after selection"
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
1095
crates/debugger_ui/src/tests/console.rs
Normal file
1095
crates/debugger_ui/src/tests/console.rs
Normal file
File diff suppressed because it is too large
Load Diff
118
crates/debugger_ui/src/tests/dap_logger.rs
Normal file
118
crates/debugger_ui/src/tests/dap_logger.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
#![expect(clippy::result_large_err)]
|
||||
use crate::tests::{init_test, init_test_workspace, start_debug_session};
|
||||
use dap::requests::{StackTrace, Threads};
|
||||
use debugger_tools::LogStore;
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use project::Project;
|
||||
use serde_json::json;
|
||||
use std::cell::OnceCell;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dap_logger_captures_all_session_rpc_messages(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let log_store_cell = std::rc::Rc::new(OnceCell::new());
|
||||
|
||||
cx.update(|cx| {
|
||||
let log_store_cell = log_store_cell.clone();
|
||||
cx.observe_new::<LogStore>(move |_, _, cx| {
|
||||
log_store_cell.set(cx.entity()).unwrap();
|
||||
})
|
||||
.detach();
|
||||
debugger_tools::init(cx);
|
||||
});
|
||||
init_test(cx);
|
||||
|
||||
let log_store = log_store_cell.get().unwrap().clone();
|
||||
|
||||
// Create a filesystem with a simple project
|
||||
let fs = project::FakeFs::new(executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"main.rs": "fn main() {\n println!(\"Hello, world!\");\n}"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
log_store.read_with(cx, |log_store, _| !log_store.has_projects()),
|
||||
"log_store shouldn't contain any projects before any projects were created"
|
||||
);
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
assert!(
|
||||
log_store.read_with(cx, |log_store, _| log_store.has_projects()),
|
||||
"log_store shouldn't contain any projects before any projects were created"
|
||||
);
|
||||
assert!(
|
||||
log_store.read_with(cx, |log_store, _| log_store
|
||||
.contained_session_ids(&project.downgrade())
|
||||
.is_empty()),
|
||||
"log_store shouldn't contain any projects before any projects were created"
|
||||
);
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
// Start a debug session
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let session_id = session.read_with(cx, |session, _| session.session_id());
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
assert_eq!(
|
||||
log_store.read_with(cx, |log_store, _| log_store
|
||||
.contained_session_ids(&project.downgrade())
|
||||
.len()),
|
||||
1,
|
||||
);
|
||||
|
||||
assert!(
|
||||
log_store.read_with(cx, |log_store, _| log_store
|
||||
.contained_session_ids(&project.downgrade())
|
||||
.contains(&session_id)),
|
||||
"log_store should contain the session IDs of the started session"
|
||||
);
|
||||
|
||||
assert!(
|
||||
!log_store.read_with(cx, |log_store, _| log_store
|
||||
.rpc_messages_for_session_id(&project.downgrade(), session_id)
|
||||
.is_empty()),
|
||||
"We should have the initialization sequence in the log store"
|
||||
);
|
||||
|
||||
// Set up basic responses for common requests
|
||||
client.on_request::<Threads, _>(move |_, _| {
|
||||
Ok(dap::ThreadsResponse {
|
||||
threads: vec![dap::Thread {
|
||||
id: 1,
|
||||
name: "Thread 1".into(),
|
||||
}],
|
||||
})
|
||||
});
|
||||
|
||||
client.on_request::<StackTrace, _>(move |_, _| {
|
||||
Ok(dap::StackTraceResponse {
|
||||
stack_frames: Vec::default(),
|
||||
total_frames: None,
|
||||
})
|
||||
});
|
||||
|
||||
// Run until all pending tasks are executed
|
||||
cx.run_until_parked();
|
||||
|
||||
// Simulate a stopped event to generate more DAP messages
|
||||
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;
|
||||
}
|
||||
2557
crates/debugger_ui/src/tests/debugger_panel.rs
Normal file
2557
crates/debugger_ui/src/tests/debugger_panel.rs
Normal file
File diff suppressed because it is too large
Load Diff
2523
crates/debugger_ui/src/tests/inline_values.rs
Normal file
2523
crates/debugger_ui/src/tests/inline_values.rs
Normal file
File diff suppressed because it is too large
Load Diff
217
crates/debugger_ui/src/tests/module_list.rs
Normal file
217
crates/debugger_ui/src/tests/module_list.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
#![expect(clippy::result_large_err)]
|
||||
use crate::{
|
||||
debugger_panel::DebugPanel,
|
||||
persistence::DebuggerPaneItem,
|
||||
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
|
||||
};
|
||||
use dap::{
|
||||
StoppedEvent,
|
||||
requests::{Initialize, Modules},
|
||||
};
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use project::{FakeFs, Project};
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, AtomicI32, Ordering},
|
||||
};
|
||||
use util::path;
|
||||
|
||||
#[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, [path!("/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 session = start_debug_session(&workspace, cx, |client| {
|
||||
client.on_request::<Initialize, _>(move |_, _| {
|
||||
Ok(dap::Capabilities {
|
||||
supports_modules_request: Some(true),
|
||||
..Default::default()
|
||||
})
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
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::<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),
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
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.running_state().clone()
|
||||
});
|
||||
|
||||
running_state.update_in(cx, |this, window, cx| {
|
||||
this.activate_item(DebuggerPaneItem::Modules, window, 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| {
|
||||
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));
|
||||
});
|
||||
}
|
||||
448
crates/debugger_ui/src/tests/new_process_modal.rs
Normal file
448
crates/debugger_ui/src/tests/new_process_modal.rs
Normal file
@@ -0,0 +1,448 @@
|
||||
#![expect(clippy::result_large_err)]
|
||||
use dap::DapRegistry;
|
||||
use editor::Editor;
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use project::{FakeFs, Fs as _, Project};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use task::{
|
||||
DebugRequest, DebugScenario, LaunchRequest, SharedTaskContext, TaskContext, VariableName,
|
||||
ZedDebugConfig,
|
||||
};
|
||||
use text::Point;
|
||||
use util::path;
|
||||
|
||||
use crate::NewProcessMode;
|
||||
use crate::new_process_modal::NewProcessModal;
|
||||
use crate::tests::{init_test, init_test_workspace};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_debug_session_substitutes_variables_and_relativizes_paths(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"main.rs": "fn main() {}"
|
||||
}),
|
||||
)
|
||||
.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 test_variables = vec![(
|
||||
VariableName::WorktreeRoot,
|
||||
path!("/test/worktree/path").to_string(),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let task_context: SharedTaskContext = TaskContext {
|
||||
cwd: None,
|
||||
task_variables: test_variables,
|
||||
project_env: Default::default(),
|
||||
}
|
||||
.into();
|
||||
|
||||
let home_dir = paths::home_dir();
|
||||
|
||||
let test_cases: Vec<(&'static str, &'static str)> = vec![
|
||||
// Absolute path - should not be relativized
|
||||
(
|
||||
path!("/absolute/path/to/program"),
|
||||
path!("/absolute/path/to/program"),
|
||||
),
|
||||
// Relative path - should be prefixed with worktree root
|
||||
(
|
||||
format!(".{0}src{0}program", std::path::MAIN_SEPARATOR).leak(),
|
||||
path!("/test/worktree/path/src/program"),
|
||||
),
|
||||
// Home directory path - should be expanded to full home directory path
|
||||
(
|
||||
format!("~{0}src{0}program", std::path::MAIN_SEPARATOR).leak(),
|
||||
home_dir
|
||||
.join("src")
|
||||
.join("program")
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.leak(),
|
||||
),
|
||||
// Path with $ZED_WORKTREE_ROOT - should be substituted without double appending
|
||||
(
|
||||
format!(
|
||||
"$ZED_WORKTREE_ROOT{0}src{0}program",
|
||||
std::path::MAIN_SEPARATOR
|
||||
)
|
||||
.leak(),
|
||||
path!("/test/worktree/path/src/program"),
|
||||
),
|
||||
];
|
||||
|
||||
let called_launch = Arc::new(AtomicBool::new(false));
|
||||
|
||||
for (input_path, expected_path) in test_cases {
|
||||
let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
|
||||
let called_launch = called_launch.clone();
|
||||
move |client| {
|
||||
client.on_request::<dap::requests::Launch, _>({
|
||||
let called_launch = called_launch.clone();
|
||||
|
||||
move |_, args| {
|
||||
let config = args.raw.as_object().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config["program"].as_str().unwrap(),
|
||||
expected_path,
|
||||
"Program path was not correctly substituted for input: {}",
|
||||
input_path
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config["cwd"].as_str().unwrap(),
|
||||
expected_path,
|
||||
"CWD path was not correctly substituted for input: {}",
|
||||
input_path
|
||||
);
|
||||
|
||||
let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") {
|
||||
input_path.replace("$ZED_WORKTREE_ROOT", path!("/test/worktree/path"))
|
||||
} else {
|
||||
input_path.to_string()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
config["otherField"].as_str().unwrap(),
|
||||
&expected_other_field,
|
||||
"Other field was incorrectly modified for input: {}",
|
||||
input_path
|
||||
);
|
||||
|
||||
called_launch.store(true, Ordering::SeqCst);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let scenario = DebugScenario {
|
||||
adapter: "fake-adapter".into(),
|
||||
label: "test-debug-session".into(),
|
||||
build: None,
|
||||
config: json!({
|
||||
"request": "launch",
|
||||
"program": input_path,
|
||||
"cwd": input_path,
|
||||
"otherField": input_path
|
||||
}),
|
||||
tcp_connection: None,
|
||||
};
|
||||
|
||||
workspace
|
||||
.update(cx, |multi, window, cx| {
|
||||
multi.workspace().update(cx, |workspace, cx| {
|
||||
workspace.start_debug_session(
|
||||
scenario,
|
||||
task_context.clone(),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
assert!(called_launch.load(Ordering::SeqCst));
|
||||
called_launch.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"main.rs": "fn main() {}"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
workspace
|
||||
.update(cx, |multi, window, cx| {
|
||||
multi.workspace().update(cx, |workspace, cx| {
|
||||
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let modal = workspace
|
||||
.update(cx, |workspace, _, cx| {
|
||||
workspace.active_modal::<NewProcessModal>(cx)
|
||||
})
|
||||
.unwrap()
|
||||
.expect("Modal should be active");
|
||||
|
||||
modal.update_in(cx, |modal, window, cx| {
|
||||
modal.set_configure("/project/main", "/project", false, window, cx);
|
||||
modal.save_debug_scenario(window, cx);
|
||||
});
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let editor = workspace
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
workspace.active_item_as::<Editor>(cx).unwrap()
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let debug_json_content = fs
|
||||
.load(path!("/project/.zed/debug.json").as_ref())
|
||||
.await
|
||||
.expect("debug.json should exist")
|
||||
.lines()
|
||||
.filter(|line| !line.starts_with("//"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
let expected_content = indoc::indoc! {r#"
|
||||
[
|
||||
{
|
||||
"adapter": "fake-adapter",
|
||||
"label": "main (fake-adapter)",
|
||||
"request": "launch",
|
||||
"program": "/project/main",
|
||||
"cwd": "/project",
|
||||
"args": [],
|
||||
"env": {}
|
||||
}
|
||||
]"#};
|
||||
|
||||
pretty_assertions::assert_eq!(expected_content, debug_json_content);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx))
|
||||
.head(),
|
||||
Point::new(5, 2)
|
||||
)
|
||||
});
|
||||
|
||||
modal.update_in(cx, |modal, window, cx| {
|
||||
modal.set_configure("/project/other", "/project", true, window, cx);
|
||||
modal.save_debug_scenario(window, cx);
|
||||
});
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let expected_content = indoc::indoc! {r#"
|
||||
[
|
||||
{
|
||||
"adapter": "fake-adapter",
|
||||
"label": "main (fake-adapter)",
|
||||
"request": "launch",
|
||||
"program": "/project/main",
|
||||
"cwd": "/project",
|
||||
"args": [],
|
||||
"env": {}
|
||||
},
|
||||
{
|
||||
"adapter": "fake-adapter",
|
||||
"label": "other (fake-adapter)",
|
||||
"request": "launch",
|
||||
"program": "/project/other",
|
||||
"cwd": "/project",
|
||||
"args": [],
|
||||
"env": {}
|
||||
}
|
||||
]"#};
|
||||
|
||||
let debug_json_content = fs
|
||||
.load(path!("/project/.zed/debug.json").as_ref())
|
||||
.await
|
||||
.expect("debug.json should exist")
|
||||
.lines()
|
||||
.filter(|line| !line.starts_with("//"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
pretty_assertions::assert_eq!(expected_content, debug_json_content);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_debug_modal_subtitles_with_multiple_worktrees(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
path!("/workspace1"),
|
||||
json!({
|
||||
".zed": {
|
||||
"debug.json": r#"[
|
||||
{
|
||||
"adapter": "fake-adapter",
|
||||
"label": "Debug App 1",
|
||||
"request": "launch",
|
||||
"program": "./app1",
|
||||
"cwd": "."
|
||||
},
|
||||
{
|
||||
"adapter": "fake-adapter",
|
||||
"label": "Debug Tests 1",
|
||||
"request": "launch",
|
||||
"program": "./test1",
|
||||
"cwd": "."
|
||||
}
|
||||
]"#
|
||||
},
|
||||
"main.rs": "fn main() {}"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/workspace1").as_ref()], cx).await;
|
||||
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
workspace
|
||||
.update(cx, |multi, window, cx| {
|
||||
multi.workspace().update(cx, |workspace, cx| {
|
||||
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let modal = workspace
|
||||
.update(cx, |workspace, _, cx| {
|
||||
workspace.active_modal::<NewProcessModal>(cx)
|
||||
})
|
||||
.unwrap()
|
||||
.expect("Modal should be active");
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let subtitles = modal.update_in(cx, |modal, _, cx| {
|
||||
modal.debug_picker_candidate_subtitles(cx)
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
subtitles.as_slice(),
|
||||
[path!(".zed/debug.json"), path!(".zed/debug.json")]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let mut expected_adapters = vec![
|
||||
"CodeLLDB",
|
||||
"Debugpy",
|
||||
"JavaScript",
|
||||
"Delve",
|
||||
"GDB",
|
||||
"fake-adapter",
|
||||
];
|
||||
|
||||
let adapter_names = cx.update(|cx| {
|
||||
let registry = DapRegistry::global(cx);
|
||||
registry.enumerate_adapters::<Vec<_>>()
|
||||
});
|
||||
|
||||
let zed_config = ZedDebugConfig {
|
||||
label: "test_debug_session".into(),
|
||||
adapter: "test_adapter".into(),
|
||||
request: DebugRequest::Launch(LaunchRequest {
|
||||
program: "test_program".into(),
|
||||
cwd: None,
|
||||
args: vec![],
|
||||
env: Default::default(),
|
||||
}),
|
||||
stop_on_entry: Some(true),
|
||||
};
|
||||
|
||||
for adapter_name in adapter_names {
|
||||
let adapter_str = adapter_name.to_string();
|
||||
if let Some(pos) = expected_adapters.iter().position(|&x| x == adapter_str) {
|
||||
expected_adapters.remove(pos);
|
||||
}
|
||||
|
||||
let adapter = cx
|
||||
.update(|cx| {
|
||||
let registry = DapRegistry::global(cx);
|
||||
registry.adapter(adapter_name.as_ref())
|
||||
})
|
||||
.unwrap_or_else(|| panic!("Adapter {} should exist", adapter_name));
|
||||
|
||||
let mut adapter_specific_config = zed_config.clone();
|
||||
adapter_specific_config.adapter = adapter_name.to_string().into();
|
||||
|
||||
let debug_scenario = adapter
|
||||
.config_from_zed_format(adapter_specific_config)
|
||||
.await
|
||||
.unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"Adapter {} should successfully convert from Zed format",
|
||||
adapter_name
|
||||
)
|
||||
});
|
||||
|
||||
assert!(
|
||||
debug_scenario.config.is_object(),
|
||||
"Adapter {} should produce a JSON object for config",
|
||||
adapter_name
|
||||
);
|
||||
|
||||
let request_type = adapter
|
||||
.request_kind(&debug_scenario.config)
|
||||
.await
|
||||
.unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"Adapter {} should validate the config successfully",
|
||||
adapter_name
|
||||
)
|
||||
});
|
||||
|
||||
match request_type {
|
||||
dap::StartDebuggingRequestArgumentsRequest::Launch => {}
|
||||
dap::StartDebuggingRequestArgumentsRequest::Attach => {
|
||||
panic!(
|
||||
"Expected Launch request but got Attach for adapter {}",
|
||||
adapter_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
expected_adapters.is_empty(),
|
||||
"The following expected adapters were not found in the registry: {:?}",
|
||||
expected_adapters
|
||||
);
|
||||
}
|
||||
132
crates/debugger_ui/src/tests/persistence.rs
Normal file
132
crates/debugger_ui/src/tests/persistence.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
#![expect(clippy::result_large_err)]
|
||||
use std::iter::zip;
|
||||
|
||||
use crate::{
|
||||
debugger_panel::DebugPanel,
|
||||
persistence::SerializedPaneLayout,
|
||||
tests::{init_test, init_test_workspace, start_debug_session},
|
||||
};
|
||||
use dap::{StoppedEvent, StoppedEventReason, messages::Events};
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use util::path;
|
||||
use workspace::{Panel, dock::DockPosition};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_invert_axis_on_panel_position_change(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"main.rs": "fn main() {\n println!(\"Hello, world!\");\n}",
|
||||
}),
|
||||
)
|
||||
.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);
|
||||
|
||||
// Start a debug session
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
// Setup thread response
|
||||
client.on_request::<dap::requests::Threads, _>(move |_, _| {
|
||||
Ok(dap::ThreadsResponse { threads: vec![] })
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
client
|
||||
.fake_event(Events::Stopped(StoppedEvent {
|
||||
reason: 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 (debug_panel, dock_position) = workspace
|
||||
.update(cx, |workspace, window, cx| {
|
||||
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
|
||||
let dock_position = debug_panel.read(cx).position(window, cx);
|
||||
(debug_panel, dock_position)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
dock_position,
|
||||
DockPosition::Bottom,
|
||||
"Default dock position should be bottom for debug panel"
|
||||
);
|
||||
|
||||
let pre_serialized_layout = debug_panel
|
||||
.read_with(cx, |panel, cx| {
|
||||
panel
|
||||
.active_session()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.running_state()
|
||||
.read(cx)
|
||||
.serialized_layout(cx)
|
||||
})
|
||||
.panes;
|
||||
|
||||
let post_serialized_layout = debug_panel
|
||||
.update_in(cx, |panel, window, cx| {
|
||||
panel.set_position(DockPosition::Right, window, cx);
|
||||
|
||||
panel
|
||||
.active_session()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.running_state()
|
||||
.read(cx)
|
||||
.serialized_layout(cx)
|
||||
})
|
||||
.panes;
|
||||
|
||||
let pre_panes = pre_serialized_layout.in_order();
|
||||
let post_panes = post_serialized_layout.in_order();
|
||||
|
||||
assert_eq!(pre_panes.len(), post_panes.len());
|
||||
|
||||
for (pre, post) in zip(pre_panes, post_panes) {
|
||||
match (pre, post) {
|
||||
(
|
||||
SerializedPaneLayout::Group {
|
||||
axis: pre_axis,
|
||||
flexes: pre_flexes,
|
||||
children: _,
|
||||
},
|
||||
SerializedPaneLayout::Group {
|
||||
axis: post_axis,
|
||||
flexes: post_flexes,
|
||||
children: _,
|
||||
},
|
||||
) => {
|
||||
assert_ne!(pre_axis, post_axis);
|
||||
assert_eq!(pre_flexes, post_flexes);
|
||||
}
|
||||
(SerializedPaneLayout::Pane(pre_pane), SerializedPaneLayout::Pane(post_pane)) => {
|
||||
assert_eq!(pre_pane.children, post_pane.children);
|
||||
assert_eq!(pre_pane.active_item, post_pane.active_item);
|
||||
}
|
||||
_ => {
|
||||
panic!("Variants don't match")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1272
crates/debugger_ui/src/tests/stack_frame_list.rs
Normal file
1272
crates/debugger_ui/src/tests/stack_frame_list.rs
Normal file
File diff suppressed because it is too large
Load Diff
2343
crates/debugger_ui/src/tests/variable_list.rs
Normal file
2343
crates/debugger_ui/src/tests/variable_list.rs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user