debugger: Improve appearance of session list for JavaScript debugging (#34322)
This PR updates the debugger panel's session list to be more useful in some cases that are commonly hit when using the JavaScript adapter. We make two adjustments, which only apply to JavaScript sessions: - For a child session that's the only child of a root session, we collapse it with its parent. This imitates what VS Code does in the "call stack" view for JavaScript sessions. - When a session has exactly one thread, we label the session with that thread's name, instead of the session label provided by the DAP. VS Code also makes this adjustment, which surfaces more useful information when working with browser sessions. Closes #33072 Release Notes: - debugger: Improved the appearance of JavaScript sessions in the debug panel's session list. --------- Co-authored-by: Julia <julia@zed.dev> Co-authored-by: Remco Smits <djsmits12@gmail.com>
This commit is contained in:
parent
13ddd5e4cb
commit
a8cc927303
11 changed files with 392 additions and 236 deletions
|
@ -448,7 +448,7 @@ impl ActivityIndicator {
|
|||
.into_any_element(),
|
||||
),
|
||||
message: format!("Debug: {}", session.read(cx).adapter()),
|
||||
tooltip_message: Some(session.read(cx).label().to_string()),
|
||||
tooltip_message: session.read(cx).label().map(|label| label.to_string()),
|
||||
on_click: None,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -378,6 +378,14 @@ pub trait DebugAdapter: 'static + Send + Sync {
|
|||
fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn compact_child_session(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn prefer_thread_name(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
|
|
@ -534,6 +534,14 @@ impl DebugAdapter for JsDebugAdapter {
|
|||
.filter(|name| !name.is_empty())?;
|
||||
Some(label.to_owned())
|
||||
}
|
||||
|
||||
fn compact_child_session(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn prefer_thread_name(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_task_type(task_type: &mut Value) {
|
||||
|
|
|
@ -399,7 +399,8 @@ impl LogStore {
|
|||
state.insert(DebugAdapterState::new(
|
||||
id.session_id,
|
||||
adapter_name,
|
||||
session_label,
|
||||
session_label
|
||||
.unwrap_or_else(|| format!("Session {} (child)", id.session_id.0).into()),
|
||||
has_adapter_logs,
|
||||
));
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ use crate::{
|
|||
ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::IndexMap;
|
||||
use dap::adapters::DebugAdapterName;
|
||||
use dap::debugger_settings::DebugPanelDockPosition;
|
||||
use dap::{
|
||||
|
@ -26,7 +27,7 @@ use text::ToPoint as _;
|
|||
|
||||
use itertools::Itertools as _;
|
||||
use language::Buffer;
|
||||
use project::debugger::session::{Session, SessionStateEvent};
|
||||
use project::debugger::session::{Session, SessionQuirks, SessionStateEvent};
|
||||
use project::{DebugScenarioContext, Fs, ProjectPath, TaskSourceKind, WorktreeId};
|
||||
use project::{Project, debugger::session::ThreadStatus};
|
||||
use rpc::proto::{self};
|
||||
|
@ -63,13 +64,14 @@ pub enum DebugPanelEvent {
|
|||
|
||||
pub struct DebugPanel {
|
||||
size: Pixels,
|
||||
sessions: Vec<Entity<DebugSession>>,
|
||||
active_session: Option<Entity<DebugSession>>,
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
debug_scenario_scheduled_last: bool,
|
||||
pub(crate) sessions_with_children:
|
||||
IndexMap<Entity<DebugSession>, Vec<WeakEntity<DebugSession>>>,
|
||||
pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
fs: Arc<dyn Fs>,
|
||||
|
@ -100,7 +102,7 @@ impl DebugPanel {
|
|||
|
||||
Self {
|
||||
size: px(300.),
|
||||
sessions: vec![],
|
||||
sessions_with_children: Default::default(),
|
||||
active_session: None,
|
||||
focus_handle,
|
||||
breakpoint_list: BreakpointList::new(
|
||||
|
@ -138,8 +140,9 @@ impl DebugPanel {
|
|||
});
|
||||
}
|
||||
|
||||
pub(crate) fn sessions(&self) -> Vec<Entity<DebugSession>> {
|
||||
self.sessions.clone()
|
||||
#[cfg(test)]
|
||||
pub(crate) fn sessions(&self) -> impl Iterator<Item = Entity<DebugSession>> {
|
||||
self.sessions_with_children.keys().cloned()
|
||||
}
|
||||
|
||||
pub fn active_session(&self) -> Option<Entity<DebugSession>> {
|
||||
|
@ -185,12 +188,20 @@ impl DebugPanel {
|
|||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let dap_store = self.project.read(cx).dap_store();
|
||||
let Some(adapter) = DapRegistry::global(cx).adapter(&scenario.adapter) else {
|
||||
return;
|
||||
};
|
||||
let quirks = SessionQuirks {
|
||||
compact: adapter.compact_child_session(),
|
||||
prefer_thread_name: adapter.prefer_thread_name(),
|
||||
};
|
||||
let session = dap_store.update(cx, |dap_store, cx| {
|
||||
dap_store.new_session(
|
||||
scenario.label.clone(),
|
||||
Some(scenario.label.clone()),
|
||||
DebugAdapterName(scenario.adapter.clone()),
|
||||
task_context.clone(),
|
||||
None,
|
||||
quirks,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
@ -363,14 +374,15 @@ impl DebugPanel {
|
|||
};
|
||||
|
||||
let dap_store_handle = self.project.read(cx).dap_store().clone();
|
||||
let label = curr_session.read(cx).label().clone();
|
||||
let label = curr_session.read(cx).label();
|
||||
let quirks = curr_session.read(cx).quirks();
|
||||
let adapter = curr_session.read(cx).adapter().clone();
|
||||
let binary = curr_session.read(cx).binary().cloned().unwrap();
|
||||
let task_context = curr_session.read(cx).task_context().clone();
|
||||
|
||||
let curr_session_id = curr_session.read(cx).session_id();
|
||||
self.sessions
|
||||
.retain(|session| session.read(cx).session_id(cx) != curr_session_id);
|
||||
self.sessions_with_children
|
||||
.retain(|session, _| session.read(cx).session_id(cx) != curr_session_id);
|
||||
let task = dap_store_handle.update(cx, |dap_store, cx| {
|
||||
dap_store.shutdown_session(curr_session_id, cx)
|
||||
});
|
||||
|
@ -379,7 +391,7 @@ impl DebugPanel {
|
|||
task.await.log_err();
|
||||
|
||||
let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
|
||||
let session = dap_store.new_session(label, adapter, task_context, None, cx);
|
||||
let session = dap_store.new_session(label, adapter, task_context, None, quirks, cx);
|
||||
|
||||
let task = session.update(cx, |session, cx| {
|
||||
session.boot(binary, worktree, dap_store_handle.downgrade(), cx)
|
||||
|
@ -425,6 +437,7 @@ impl DebugPanel {
|
|||
let dap_store_handle = self.project.read(cx).dap_store().clone();
|
||||
let label = self.label_for_child_session(&parent_session, request, cx);
|
||||
let adapter = parent_session.read(cx).adapter().clone();
|
||||
let quirks = parent_session.read(cx).quirks();
|
||||
let Some(mut binary) = parent_session.read(cx).binary().cloned() else {
|
||||
log::error!("Attempted to start a child-session without a binary");
|
||||
return;
|
||||
|
@ -438,6 +451,7 @@ impl DebugPanel {
|
|||
adapter,
|
||||
task_context,
|
||||
Some(parent_session.clone()),
|
||||
quirks,
|
||||
cx,
|
||||
);
|
||||
|
||||
|
@ -463,8 +477,8 @@ impl DebugPanel {
|
|||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(session) = self
|
||||
.sessions
|
||||
.iter()
|
||||
.sessions_with_children
|
||||
.keys()
|
||||
.find(|other| entity_id == other.entity_id())
|
||||
.cloned()
|
||||
else {
|
||||
|
@ -498,15 +512,14 @@ impl DebugPanel {
|
|||
}
|
||||
session.update(cx, |session, cx| session.shutdown(cx)).ok();
|
||||
this.update(cx, |this, cx| {
|
||||
this.sessions.retain(|other| entity_id != other.entity_id());
|
||||
|
||||
this.retain_sessions(|other| entity_id != other.entity_id());
|
||||
if let Some(active_session_id) = this
|
||||
.active_session
|
||||
.as_ref()
|
||||
.map(|session| session.entity_id())
|
||||
{
|
||||
if active_session_id == entity_id {
|
||||
this.active_session = this.sessions.first().cloned();
|
||||
this.active_session = this.sessions_with_children.keys().next().cloned();
|
||||
}
|
||||
}
|
||||
cx.notify()
|
||||
|
@ -976,8 +989,8 @@ impl DebugPanel {
|
|||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(session) = self
|
||||
.sessions
|
||||
.iter()
|
||||
.sessions_with_children
|
||||
.keys()
|
||||
.find(|session| session.read(cx).session_id(cx) == session_id)
|
||||
{
|
||||
self.activate_session(session.clone(), window, cx);
|
||||
|
@ -990,7 +1003,7 @@ impl DebugPanel {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
debug_assert!(self.sessions.contains(&session_item));
|
||||
debug_assert!(self.sessions_with_children.contains_key(&session_item));
|
||||
session_item.focus_handle(cx).focus(window);
|
||||
session_item.update(cx, |this, cx| {
|
||||
this.running_state().update(cx, |this, cx| {
|
||||
|
@ -1261,18 +1274,27 @@ impl DebugPanel {
|
|||
parent_session: &Entity<Session>,
|
||||
request: &StartDebuggingRequestArguments,
|
||||
cx: &mut Context<'_, Self>,
|
||||
) -> SharedString {
|
||||
) -> Option<SharedString> {
|
||||
let adapter = parent_session.read(cx).adapter();
|
||||
if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) {
|
||||
if let Some(label) = adapter.label_for_child_session(request) {
|
||||
return label.into();
|
||||
return Some(label.into());
|
||||
}
|
||||
}
|
||||
let mut label = parent_session.read(cx).label().clone();
|
||||
if !label.ends_with("(child)") {
|
||||
label = format!("{label} (child)").into();
|
||||
None
|
||||
}
|
||||
|
||||
fn retain_sessions(&mut self, keep: impl Fn(&Entity<DebugSession>) -> bool) {
|
||||
self.sessions_with_children
|
||||
.retain(|session, _| keep(session));
|
||||
for children in self.sessions_with_children.values_mut() {
|
||||
children.retain(|child| {
|
||||
let Some(child) = child.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
keep(&child)
|
||||
});
|
||||
}
|
||||
label
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1302,11 +1324,11 @@ async fn register_session_inner(
|
|||
let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
|
||||
let debug_session = this.update_in(cx, |this, window, cx| {
|
||||
let parent_session = this
|
||||
.sessions
|
||||
.iter()
|
||||
.sessions_with_children
|
||||
.keys()
|
||||
.find(|p| Some(p.read(cx).session_id(cx)) == session.read(cx).parent_id(cx))
|
||||
.cloned();
|
||||
this.sessions.retain(|session| {
|
||||
this.retain_sessions(|session| {
|
||||
!session
|
||||
.read(cx)
|
||||
.running_state()
|
||||
|
@ -1337,13 +1359,23 @@ async fn register_session_inner(
|
|||
)
|
||||
.detach();
|
||||
let insert_position = this
|
||||
.sessions
|
||||
.iter()
|
||||
.sessions_with_children
|
||||
.keys()
|
||||
.position(|session| Some(session) == parent_session.as_ref())
|
||||
.map(|position| position + 1)
|
||||
.unwrap_or(this.sessions.len());
|
||||
.unwrap_or(this.sessions_with_children.len());
|
||||
// Maintain topological sort order of sessions
|
||||
this.sessions.insert(insert_position, debug_session.clone());
|
||||
let (_, old) = this.sessions_with_children.insert_before(
|
||||
insert_position,
|
||||
debug_session.clone(),
|
||||
Default::default(),
|
||||
);
|
||||
debug_assert!(old.is_none());
|
||||
if let Some(parent_session) = parent_session {
|
||||
this.sessions_with_children
|
||||
.entry(parent_session)
|
||||
.and_modify(|children| children.push(debug_session.downgrade()));
|
||||
}
|
||||
|
||||
debug_session
|
||||
})?;
|
||||
|
@ -1383,7 +1415,7 @@ impl Panel for DebugPanel {
|
|||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if position.axis() != self.position(window, cx).axis() {
|
||||
self.sessions.iter().for_each(|session_item| {
|
||||
self.sessions_with_children.keys().for_each(|session_item| {
|
||||
session_item.update(cx, |item, cx| {
|
||||
item.running_state()
|
||||
.update(cx, |state, _| state.invert_axies())
|
||||
|
|
|
@ -1,16 +1,82 @@
|
|||
use std::time::Duration;
|
||||
use std::{rc::Rc, time::Duration};
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::{Animation, AnimationExt as _, Entity, Transformation, percentage};
|
||||
use gpui::{Animation, AnimationExt as _, Entity, Transformation, WeakEntity, percentage};
|
||||
use project::debugger::session::{ThreadId, ThreadStatus};
|
||||
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
|
||||
use util::truncate_and_trailoff;
|
||||
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;
|
||||
|
@ -25,145 +91,205 @@ impl DebugPanel {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<impl IntoElement> {
|
||||
if let Some(running_state) = running_state {
|
||||
let sessions = self.sessions().clone();
|
||||
let weak = cx.weak_entity();
|
||||
let running_state = running_state.read(cx);
|
||||
let label = if let Some(active_session) = active_session.clone() {
|
||||
active_session.read(cx).session(cx).read(cx).label()
|
||||
} else {
|
||||
SharedString::new_static("Unknown Session")
|
||||
};
|
||||
let running_state = running_state?;
|
||||
|
||||
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 mut session_entries = Vec::with_capacity(self.sessions_with_children.len() * 3);
|
||||
let mut sessions_with_children = self.sessions_with_children.iter().peekable();
|
||||
|
||||
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_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element()
|
||||
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 {
|
||||
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(),
|
||||
SessionListEntry {
|
||||
leaf: root.clone(),
|
||||
ancestors: Vec::new(),
|
||||
}
|
||||
};
|
||||
session_entries.push(root_entry);
|
||||
|
||||
let trigger = h_flex()
|
||||
.gap_2()
|
||||
.child(session_state_indicator)
|
||||
.justify_between()
|
||||
.child(
|
||||
DebugPanel::dropdown_label(label)
|
||||
.when(is_terminated, |this| this.strikethrough()),
|
||||
)
|
||||
.into_any_element();
|
||||
|
||||
Some(
|
||||
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 in sessions.into_iter() {
|
||||
let weak_session = session.downgrade();
|
||||
let weak_session_id = weak_session.entity_id();
|
||||
let session_id = session.read(cx).session_id(cx);
|
||||
let parent_depth = session
|
||||
.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();
|
||||
move |_, cx| {
|
||||
weak_session
|
||||
.read_with(cx, |session, cx| {
|
||||
let context_menu = context_menu.clone();
|
||||
|
||||
let id: SharedString =
|
||||
format!("debug-session-{}", session_id.0)
|
||||
.into();
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.group(id.clone())
|
||||
.justify_between()
|
||||
.child(session.label_element(self_depth, cx))
|
||||
.child(
|
||||
IconButton::new(
|
||||
"close-debug-session",
|
||||
IconName::Close,
|
||||
)
|
||||
.visible_on_hover(id.clone())
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click({
|
||||
let weak = weak.clone();
|
||||
move |_, window, cx| {
|
||||
weak.update(cx, |panel, cx| {
|
||||
panel.close_session(
|
||||
weak_session_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
context_menu
|
||||
.update(cx, |this, cx| {
|
||||
this.cancel(
|
||||
&Default::default(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.unwrap_or_else(|_| div().into_any_element())
|
||||
}
|
||||
},
|
||||
{
|
||||
let weak = weak.clone();
|
||||
move |window, cx| {
|
||||
weak.update(cx, |panel, cx| {
|
||||
panel.activate_session(session.clone(), window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
this
|
||||
session_entries.extend(
|
||||
sessions_with_children
|
||||
.by_ref()
|
||||
.take_while(|(session, _)| {
|
||||
session
|
||||
.read(cx)
|
||||
.session(cx)
|
||||
.read(cx)
|
||||
.parent_id(cx)
|
||||
.is_some()
|
||||
})
|
||||
.map(|(session, _)| SessionListEntry {
|
||||
leaf: session.clone(),
|
||||
ancestors: vec![],
|
||||
}),
|
||||
)
|
||||
.style(DropdownStyle::Ghost)
|
||||
.handle(self.session_picker_menu_handle.clone()),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
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_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.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
|
||||
}),
|
||||
)
|
||||
.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.clone())
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click({
|
||||
let weak = weak.clone();
|
||||
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(
|
||||
|
|
|
@ -5,14 +5,13 @@ use dap::client::SessionId;
|
|||
use gpui::{
|
||||
App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use project::Project;
|
||||
use project::debugger::session::Session;
|
||||
use project::worktree_store::WorktreeStore;
|
||||
use project::{Project, debugger::session::SessionQuirks};
|
||||
use rpc::proto;
|
||||
use running::RunningState;
|
||||
use std::{cell::OnceCell, sync::OnceLock};
|
||||
use ui::{Indicator, prelude::*};
|
||||
use util::truncate_and_trailoff;
|
||||
use std::cell::OnceCell;
|
||||
use ui::prelude::*;
|
||||
use workspace::{
|
||||
CollaboratorId, FollowableItem, ViewId, Workspace,
|
||||
item::{self, Item},
|
||||
|
@ -20,8 +19,8 @@ use workspace::{
|
|||
|
||||
pub struct DebugSession {
|
||||
remote_id: Option<workspace::ViewId>,
|
||||
running_state: Entity<RunningState>,
|
||||
label: OnceLock<SharedString>,
|
||||
pub(crate) running_state: Entity<RunningState>,
|
||||
pub(crate) quirks: SessionQuirks,
|
||||
stack_trace_view: OnceCell<Entity<StackTraceView>>,
|
||||
_worktree_store: WeakEntity<WorktreeStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
|
@ -57,6 +56,7 @@ impl DebugSession {
|
|||
cx,
|
||||
)
|
||||
});
|
||||
let quirks = session.read(cx).quirks();
|
||||
|
||||
cx.new(|cx| Self {
|
||||
_subscriptions: [cx.subscribe(&running_state, |_, _, _, cx| {
|
||||
|
@ -64,7 +64,7 @@ impl DebugSession {
|
|||
})],
|
||||
remote_id: None,
|
||||
running_state,
|
||||
label: OnceLock::new(),
|
||||
quirks,
|
||||
stack_trace_view: OnceCell::new(),
|
||||
_worktree_store: project.read(cx).worktree_store().downgrade(),
|
||||
workspace,
|
||||
|
@ -110,65 +110,29 @@ impl DebugSession {
|
|||
.update(cx, |state, cx| state.shutdown(cx));
|
||||
}
|
||||
|
||||
pub(crate) fn label(&self, cx: &App) -> SharedString {
|
||||
if let Some(label) = self.label.get() {
|
||||
return label.clone();
|
||||
}
|
||||
|
||||
let session = self.running_state.read(cx).session();
|
||||
|
||||
self.label
|
||||
.get_or_init(|| session.read(cx).label())
|
||||
.to_owned()
|
||||
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
|
||||
}
|
||||
|
||||
pub(crate) fn label_element(&self, depth: usize, cx: &App) -> AnyElement {
|
||||
const MAX_LABEL_CHARS: usize = 150;
|
||||
|
||||
let label = self.label(cx);
|
||||
let label = truncate_and_trailoff(&label, MAX_LABEL_CHARS);
|
||||
|
||||
let is_terminated = self
|
||||
.running_state
|
||||
.read(cx)
|
||||
.session()
|
||||
.read(cx)
|
||||
.is_terminated();
|
||||
let icon = {
|
||||
if is_terminated {
|
||||
Some(Indicator::dot().color(Color::Error))
|
||||
} else {
|
||||
match self
|
||||
.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 EventEmitter<DebugPanelItemEvent> for DebugSession {}
|
||||
|
|
|
@ -427,7 +427,7 @@ async fn test_handle_start_debugging_request(
|
|||
let sessions = workspace
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
|
||||
debug_panel.read(cx).sessions()
|
||||
debug_panel.read(cx).sessions().collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(sessions.len(), 1);
|
||||
|
@ -451,7 +451,7 @@ async fn test_handle_start_debugging_request(
|
|||
.unwrap()
|
||||
.read(cx)
|
||||
.session(cx);
|
||||
let current_sessions = debug_panel.read(cx).sessions();
|
||||
let current_sessions = debug_panel.read(cx).sessions().collect::<Vec<_>>();
|
||||
assert_eq!(active_session, current_sessions[1].read(cx).session(cx));
|
||||
assert_eq!(
|
||||
active_session.read(cx).parent_session(),
|
||||
|
@ -1796,7 +1796,7 @@ async fn test_debug_adapters_shutdown_on_app_quit(
|
|||
let panel = workspace.panel::<DebugPanel>(cx).unwrap();
|
||||
panel.read_with(cx, |panel, _| {
|
||||
assert!(
|
||||
!panel.sessions().is_empty(),
|
||||
panel.sessions().next().is_some(),
|
||||
"Debug session should be active"
|
||||
);
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ use super::{
|
|||
};
|
||||
use crate::{
|
||||
InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState,
|
||||
debugger::session::SessionQuirks,
|
||||
project_settings::ProjectSettings,
|
||||
terminals::{SshCommand, wrap_for_ssh},
|
||||
worktree_store::WorktreeStore,
|
||||
|
@ -385,10 +386,11 @@ impl DapStore {
|
|||
|
||||
pub fn new_session(
|
||||
&mut self,
|
||||
label: SharedString,
|
||||
label: Option<SharedString>,
|
||||
adapter: DebugAdapterName,
|
||||
task_context: TaskContext,
|
||||
parent_session: Option<Entity<Session>>,
|
||||
quirks: SessionQuirks,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<Session> {
|
||||
let session_id = SessionId(util::post_inc(&mut self.next_session_id));
|
||||
|
@ -406,6 +408,7 @@ impl DapStore {
|
|||
label,
|
||||
adapter,
|
||||
task_context,
|
||||
quirks,
|
||||
cx,
|
||||
);
|
||||
|
||||
|
|
|
@ -151,6 +151,12 @@ pub struct RunningMode {
|
|||
messages_tx: UnboundedSender<Message>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub struct SessionQuirks {
|
||||
pub compact: bool,
|
||||
pub prefer_thread_name: bool,
|
||||
}
|
||||
|
||||
fn client_source(abs_path: &Path) -> dap::Source {
|
||||
dap::Source {
|
||||
name: abs_path
|
||||
|
@ -656,7 +662,7 @@ pub struct OutputToken(pub usize);
|
|||
pub struct Session {
|
||||
pub mode: Mode,
|
||||
id: SessionId,
|
||||
label: SharedString,
|
||||
label: Option<SharedString>,
|
||||
adapter: DebugAdapterName,
|
||||
pub(super) capabilities: Capabilities,
|
||||
child_session_ids: HashSet<SessionId>,
|
||||
|
@ -679,6 +685,7 @@ pub struct Session {
|
|||
background_tasks: Vec<Task<()>>,
|
||||
restart_task: Option<Task<()>>,
|
||||
task_context: TaskContext,
|
||||
quirks: SessionQuirks,
|
||||
}
|
||||
|
||||
trait CacheableCommand: Any + Send + Sync {
|
||||
|
@ -792,9 +799,10 @@ impl Session {
|
|||
breakpoint_store: Entity<BreakpointStore>,
|
||||
session_id: SessionId,
|
||||
parent_session: Option<Entity<Session>>,
|
||||
label: SharedString,
|
||||
label: Option<SharedString>,
|
||||
adapter: DebugAdapterName,
|
||||
task_context: TaskContext,
|
||||
quirks: SessionQuirks,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
cx.new::<Self>(|cx| {
|
||||
|
@ -848,6 +856,7 @@ impl Session {
|
|||
label,
|
||||
adapter,
|
||||
task_context,
|
||||
quirks,
|
||||
};
|
||||
|
||||
this
|
||||
|
@ -1022,7 +1031,7 @@ impl Session {
|
|||
self.adapter.clone()
|
||||
}
|
||||
|
||||
pub fn label(&self) -> SharedString {
|
||||
pub fn label(&self) -> Option<SharedString> {
|
||||
self.label.clone()
|
||||
}
|
||||
|
||||
|
@ -2481,4 +2490,8 @@ impl Session {
|
|||
pub fn thread_state(&self, thread_id: ThreadId) -> Option<ThreadStatus> {
|
||||
self.thread_states.thread_state(thread_id)
|
||||
}
|
||||
|
||||
pub fn quirks(&self) -> SessionQuirks {
|
||||
self.quirks
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4301,6 +4301,7 @@ impl ProjectPanel {
|
|||
.collect::<Vec<_>>();
|
||||
|
||||
let components_len = components.len();
|
||||
// TODO this can underflow
|
||||
let active_index = components_len
|
||||
- 1
|
||||
- folded_ancestors.current_ancestor_depth;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue