debugger: First slight pass at UI (#27034)

- Collapse Launch and Attach into a single split button
- Fix code actions indicator being colored red.

Release Notes:

- N/A
This commit is contained in:
Piotr Osiewicz 2025-03-19 01:15:48 +01:00 committed by GitHub
parent 73ac3d9a99
commit c042a02cf4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 229 additions and 158 deletions

1
Cargo.lock generated
View file

@ -5680,7 +5680,6 @@ dependencies = [
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"settings", "settings",
"smallvec",
"strum", "strum",
"telemetry", "telemetry",
"theme", "theme",

View file

@ -7,20 +7,39 @@ use settings::Settings as _;
use task::TCPHost; use task::TCPHost;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{ use ui::{
h_flex, relative, v_flex, ActiveTheme as _, Button, ButtonCommon, ButtonStyle, Clickable, h_flex, relative, v_flex, ActiveTheme as _, ButtonLike, Clickable, Context, ContextMenu,
Context, ContextMenu, Disableable, DropdownMenu, InteractiveElement, IntoElement, Disableable, Disclosure, DropdownMenu, FluentBuilder, InteractiveElement, IntoElement, Label,
ParentElement, Render, SharedString, Styled, Window, LabelCommon, LabelSize, ParentElement, PopoverMenu, PopoverMenuHandle, Render, SharedString,
SplitButton, Styled, Window,
}; };
use workspace::Workspace; use workspace::Workspace;
use crate::attach_modal::AttachModal; use crate::attach_modal::AttachModal;
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
enum SpawnMode {
#[default]
Launch,
Attach,
}
impl SpawnMode {
fn label(&self) -> &'static str {
match self {
SpawnMode::Launch => "Launch",
SpawnMode::Attach => "Attach",
}
}
}
pub(crate) struct InertState { pub(crate) struct InertState {
focus_handle: FocusHandle, focus_handle: FocusHandle,
selected_debugger: Option<SharedString>, selected_debugger: Option<SharedString>,
program_editor: Entity<Editor>, program_editor: Entity<Editor>,
cwd_editor: Entity<Editor>, cwd_editor: Entity<Editor>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
spawn_mode: SpawnMode,
popover_handle: PopoverMenuHandle<ContextMenu>,
} }
impl InertState { impl InertState {
@ -47,6 +66,8 @@ impl InertState {
program_editor, program_editor,
selected_debugger: None, selected_debugger: None,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
spawn_mode: SpawnMode::default(),
popover_handle: Default::default(),
} }
} }
} }
@ -72,19 +93,46 @@ impl Render for InertState {
) -> impl ui::IntoElement { ) -> impl ui::IntoElement {
let weak = cx.weak_entity(); let weak = cx.weak_entity();
let disable_buttons = self.selected_debugger.is_none(); let disable_buttons = self.selected_debugger.is_none();
let spawn_button = ButtonLike::new_rounded_left("spawn-debug-session")
.child(Label::new(self.spawn_mode.label()).size(LabelSize::Small))
.on_click(cx.listener(|this, _, window, cx| {
if this.spawn_mode == SpawnMode::Launch {
let program = this.program_editor.read(cx).text(cx);
let cwd = PathBuf::from(this.cwd_editor.read(cx).text(cx));
let kind =
kind_for_label(this.selected_debugger.as_deref().unwrap_or_else(|| {
unimplemented!(
"Automatic selection of a debugger based on users project"
)
}));
cx.emit(InertEvent::Spawned {
config: DebugAdapterConfig {
label: "hard coded".into(),
kind,
request: DebugRequestType::Launch,
program: Some(program),
cwd: Some(cwd),
initialize_args: None,
supports_attach: false,
},
});
} else {
this.attach(window, cx)
}
}))
.disabled(disable_buttons);
v_flex() v_flex()
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.size_full() .size_full()
.gap_1() .gap_1()
.p_2() .p_2()
.child( .child(
v_flex().gap_1() v_flex()
.gap_1()
.child( .child(
h_flex() h_flex()
.w_full() .w_full()
.gap_2() .gap_2()
.child(Self::render_editor(&self.program_editor, cx)) .child(Self::render_editor(&self.program_editor, cx))
.child( .child(
h_flex().child(DropdownMenu::new( h_flex().child(DropdownMenu::new(
@ -109,45 +157,63 @@ impl Render for InertState {
.entry("Delve", None, setter_for_name("Delve")) .entry("Delve", None, setter_for_name("Delve"))
.entry("LLDB", None, setter_for_name("LLDB")) .entry("LLDB", None, setter_for_name("LLDB"))
.entry("PHP", None, setter_for_name("PHP")) .entry("PHP", None, setter_for_name("PHP"))
.entry("JavaScript", None, setter_for_name("JavaScript")) .entry(
"JavaScript",
None,
setter_for_name("JavaScript"),
)
.entry("Debugpy", None, setter_for_name("Debugpy")) .entry("Debugpy", None, setter_for_name("Debugpy"))
}), }),
)), )),
) ),
) )
.child( .child(
h_flex().gap_2().child( h_flex()
Self::render_editor(&self.cwd_editor, cx), .gap_2()
).child(h_flex() .child(Self::render_editor(&self.cwd_editor, cx))
.gap_4() .map(|this| {
.pl_2() let entity = cx.weak_entity();
.child( this.child(SplitButton {
Button::new("launch-dap", "Launch") left: spawn_button,
.style(ButtonStyle::Filled) right: PopoverMenu::new("debugger-select-spawn-mode")
.disabled(disable_buttons) .trigger(Disclosure::new(
.on_click(cx.listener(|this, _, _, cx| { "debugger-spawn-button-disclosure",
let program = this.program_editor.read(cx).text(cx); self.popover_handle.is_deployed(),
let cwd = PathBuf::from(this.cwd_editor.read(cx).text(cx)); ))
let kind = kind_for_label(this.selected_debugger.as_deref().unwrap_or_else(|| unimplemented!("Automatic selection of a debugger based on users project"))); .menu(move |window, cx| {
cx.emit(InertEvent::Spawned { Some(ContextMenu::build(window, cx, {
config: DebugAdapterConfig { let entity = entity.clone();
label: "hard coded".into(), move |this, _, _| {
kind, this.entry("Launch", None, {
request: DebugRequestType::Launch, let entity = entity.clone();
program: Some(program), move |_, cx| {
cwd: Some(cwd), let _ =
initialize_args: None, entity.update(cx, |this, cx| {
supports_attach: false, this.spawn_mode =
}, SpawnMode::Launch;
}); cx.notify();
})), });
) }
.child(Button::new("attach-dap", "Attach") })
.style(ButtonStyle::Filled) .entry("Attach", None, {
.disabled(disable_buttons) let entity = entity.clone();
.on_click(cx.listener(|this, _, window, cx| this.attach(window, cx))) move |_, cx| {
)) let _ =
) entity.update(cx, |this, cx| {
this.spawn_mode =
SpawnMode::Attach;
cx.notify();
});
}
})
}
}))
})
.with_handle(self.popover_handle.clone())
.into_any_element(),
})
}),
),
) )
} }
} }

View file

@ -5918,11 +5918,7 @@ impl Editor {
breakpoint: Option<&(Anchor, Breakpoint)>, breakpoint: Option<&(Anchor, Breakpoint)>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Option<IconButton> { ) -> Option<IconButton> {
let color = if breakpoint.is_some() { let color = Color::Muted;
Color::Debugger
} else {
Color::Muted
};
let position = breakpoint.as_ref().map(|(anchor, _)| *anchor); let position = breakpoint.as_ref().map(|(anchor, _)| *anchor);
let bp_kind = Arc::new( let bp_kind = Arc::new(

View file

@ -49,7 +49,6 @@ serde.workspace = true
serde_derive.workspace = true serde_derive.workspace = true
serde_json.workspace = true serde_json.workspace = true
settings.workspace = true settings.workspace = true
smallvec.workspace = true
strum.workspace = true strum.workspace = true
telemetry.workspace = true telemetry.workspace = true
theme.workspace = true theme.workspace = true

View file

@ -163,19 +163,18 @@ fn render_remote_button(
} }
mod remote_button { mod remote_button {
use gpui::{hsla, point, Action, AnyView, BoxShadow, ClickEvent, Corner, FocusHandle}; use gpui::{Action, AnyView, ClickEvent, Corner, FocusHandle};
use ui::{ use ui::{
div, h_flex, px, rems, ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, Clickable, div, h_flex, rems, App, ButtonCommon, Clickable, ContextMenu, ElementId, FluentBuilder,
ContextMenu, ElementId, ElevationIndex, FluentBuilder, Icon, IconName, IconSize, Icon, IconName, IconSize, IntoElement, Label, LabelCommon, LabelSize, LineHeightStyle,
IntoElement, Label, LabelCommon, LabelSize, LineHeightStyle, ParentElement, PopoverMenu, ParentElement, PopoverMenu, SharedString, SplitButton, Styled, Tooltip, Window,
RenderOnce, SharedString, Styled, Tooltip, Window,
}; };
pub fn render_fetch_button( pub fn render_fetch_button(
keybinding_target: Option<FocusHandle>, keybinding_target: Option<FocusHandle>,
id: SharedString, id: SharedString,
) -> SplitButton { ) -> SplitButton {
SplitButton::new( split_button(
id, id,
"Fetch", "Fetch",
0, 0,
@ -203,7 +202,7 @@ mod remote_button {
id: SharedString, id: SharedString,
ahead: u32, ahead: u32,
) -> SplitButton { ) -> SplitButton {
SplitButton::new( split_button(
id, id,
"Push", "Push",
ahead as usize, ahead as usize,
@ -232,7 +231,7 @@ mod remote_button {
ahead: u32, ahead: u32,
behind: u32, behind: u32,
) -> SplitButton { ) -> SplitButton {
SplitButton::new( split_button(
id, id,
"Pull", "Pull",
ahead as usize, ahead as usize,
@ -259,7 +258,7 @@ mod remote_button {
keybinding_target: Option<FocusHandle>, keybinding_target: Option<FocusHandle>,
id: SharedString, id: SharedString,
) -> SplitButton { ) -> SplitButton {
SplitButton::new( split_button(
id, id,
"Publish", "Publish",
0, 0,
@ -286,7 +285,7 @@ mod remote_button {
keybinding_target: Option<FocusHandle>, keybinding_target: Option<FocusHandle>,
id: SharedString, id: SharedString,
) -> SplitButton { ) -> SplitButton {
SplitButton::new( split_button(
id, id,
"Republish", "Republish",
0, 0,
@ -364,111 +363,76 @@ mod remote_button {
}) })
.anchor(Corner::TopRight) .anchor(Corner::TopRight)
} }
#[allow(clippy::too_many_arguments)]
#[derive(IntoElement)] fn split_button(
pub struct SplitButton { id: SharedString,
pub left: ButtonLike, left_label: impl Into<SharedString>,
pub right: AnyElement, ahead_count: usize,
} behind_count: usize,
left_icon: Option<IconName>,
impl SplitButton { keybinding_target: Option<FocusHandle>,
#[allow(clippy::too_many_arguments)] left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
fn new( tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
id: impl Into<SharedString>, ) -> SplitButton {
left_label: impl Into<SharedString>, fn count(count: usize) -> impl IntoElement {
ahead_count: usize,
behind_count: usize,
left_icon: Option<IconName>,
keybinding_target: Option<FocusHandle>,
left_on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
) -> Self {
let id = id.into();
fn count(count: usize) -> impl IntoElement {
h_flex()
.ml_neg_px()
.h(rems(0.875))
.items_center()
.overflow_hidden()
.px_0p5()
.child(
Label::new(count.to_string())
.size(LabelSize::XSmall)
.line_height_style(LineHeightStyle::UiLabel),
)
}
let should_render_counts = left_icon.is_none() && (ahead_count > 0 || behind_count > 0);
let left = ui::ButtonLike::new_rounded_left(ElementId::Name(
format!("split-button-left-{}", id).into(),
))
.layer(ui::ElevationIndex::ModalSurface)
.size(ui::ButtonSize::Compact)
.when(should_render_counts, |this| {
this.child(
h_flex()
.ml_neg_0p5()
.mr_1()
.when(behind_count > 0, |this| {
this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall))
.child(count(behind_count))
})
.when(ahead_count > 0, |this| {
this.child(Icon::new(IconName::ArrowUp).size(IconSize::XSmall))
.child(count(ahead_count))
}),
)
})
.when_some(left_icon, |this, left_icon| {
this.child(
h_flex()
.ml_neg_0p5()
.mr_1()
.child(Icon::new(left_icon).size(IconSize::XSmall)),
)
})
.child(
div()
.child(Label::new(left_label).size(LabelSize::Small))
.mr_0p5(),
)
.on_click(left_on_click)
.tooltip(tooltip);
let right = render_git_action_menu(
ElementId::Name(format!("split-button-right-{}", id).into()),
keybinding_target,
)
.into_any_element();
Self { left, right }
}
}
impl RenderOnce for SplitButton {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
h_flex() h_flex()
.rounded_sm() .ml_neg_px()
.border_1() .h(rems(0.875))
.border_color(cx.theme().colors().text_muted.alpha(0.12)) .items_center()
.child(div().flex_grow().child(self.left)) .overflow_hidden()
.px_0p5()
.child( .child(
div() Label::new(count.to_string())
.h_full() .size(LabelSize::XSmall)
.w_px() .line_height_style(LineHeightStyle::UiLabel),
.bg(cx.theme().colors().text_muted.alpha(0.16)),
) )
.child(self.right)
.bg(ElevationIndex::Surface.on_elevation_bg(cx))
.shadow(smallvec::smallvec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.16),
offset: point(px(0.), px(1.)),
blur_radius: px(0.),
spread_radius: px(0.),
}])
} }
let should_render_counts = left_icon.is_none() && (ahead_count > 0 || behind_count > 0);
let left = ui::ButtonLike::new_rounded_left(ElementId::Name(
format!("split-button-left-{}", id).into(),
))
.layer(ui::ElevationIndex::ModalSurface)
.size(ui::ButtonSize::Compact)
.when(should_render_counts, |this| {
this.child(
h_flex()
.ml_neg_0p5()
.mr_1()
.when(behind_count > 0, |this| {
this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall))
.child(count(behind_count))
})
.when(ahead_count > 0, |this| {
this.child(Icon::new(IconName::ArrowUp).size(IconSize::XSmall))
.child(count(ahead_count))
}),
)
})
.when_some(left_icon, |this, left_icon| {
this.child(
h_flex()
.ml_neg_0p5()
.mr_1()
.child(Icon::new(left_icon).size(IconSize::XSmall)),
)
})
.child(
div()
.child(Label::new(left_label).size(LabelSize::Small))
.mr_0p5(),
)
.on_click(left_on_click)
.tooltip(tooltip);
let right = render_git_action_menu(
ElementId::Name(format!("split-button-right-{}", id).into()),
keybinding_target,
)
.into_any_element();
SplitButton { left, right }
} }
} }

View file

@ -2,9 +2,11 @@ mod button;
mod button_icon; mod button_icon;
mod button_like; mod button_like;
mod icon_button; mod icon_button;
mod split_button;
mod toggle_button; mod toggle_button;
pub use button::*; pub use button::*;
pub use button_like::*; pub use button_like::*;
pub use icon_button::*; pub use icon_button::*;
pub use split_button::*;
pub use toggle_button::*; pub use toggle_button::*;

View file

@ -0,0 +1,45 @@
use gpui::{
div, hsla, point, px, AnyElement, App, BoxShadow, IntoElement, ParentElement, RenderOnce,
Styled, Window,
};
use theme::ActiveTheme;
use crate::{h_flex, ElevationIndex};
use super::ButtonLike;
/// /// A button with two parts: a primary action on the left and a secondary action on the right.
///
/// The left side is a [`ButtonLike`] with the main action, while the right side can contain
/// any element (typically a dropdown trigger or similar).
///
/// The two sections are visually separated by a divider, but presented as a unified control.
#[derive(IntoElement)]
pub struct SplitButton {
pub left: ButtonLike,
pub right: AnyElement,
}
impl RenderOnce for SplitButton {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
h_flex()
.rounded_sm()
.border_1()
.border_color(cx.theme().colors().text_muted.alpha(0.12))
.child(div().flex_grow().child(self.left))
.child(
div()
.h_full()
.w_px()
.bg(cx.theme().colors().text_muted.alpha(0.16)),
)
.child(self.right)
.bg(ElevationIndex::Surface.on_elevation_bg(cx))
.shadow(smallvec::smallvec![BoxShadow {
color: hsla(0.0, 0.0, 0.0, 0.16),
offset: point(px(0.), px(1.)),
blur_radius: px(0.),
spread_radius: px(0.),
}])
}
}