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

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:
Mohamad Khani
2026-07-14 02:22:17 +03:30
commit b72a46db68
3984 changed files with 1583326 additions and 0 deletions

View 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

View File

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

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

File diff suppressed because it is too large Load Diff

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

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

File diff suppressed because it is too large Load Diff

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

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

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

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

File diff suppressed because it is too large Load Diff

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

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff