Cargo.lock 🔗
@@ -5680,7 +5680,6 @@ dependencies = [
"serde_derive",
"serde_json",
"settings",
- "smallvec",
"strum",
"telemetry",
"theme",
Piotr Osiewicz created
- Collapse Launch and Attach into a single split button
- Fix code actions indicator being colored red.
Release Notes:
- N/A
Cargo.lock | 1
crates/debugger_ui/src/session/inert.rs | 146 +++++++++++----
crates/editor/src/editor.rs | 6
crates/git_ui/Cargo.toml | 1
crates/git_ui/src/git_ui.rs | 180 +++++++-----------
crates/ui/src/components/button.rs | 2
crates/ui/src/components/button/split_button.rs | 45 ++++
7 files changed, 226 insertions(+), 155 deletions(-)
@@ -5680,7 +5680,6 @@ dependencies = [
"serde_derive",
"serde_json",
"settings",
- "smallvec",
"strum",
"telemetry",
"theme",
@@ -7,20 +7,39 @@ use settings::Settings as _;
use task::TCPHost;
use theme::ThemeSettings;
use ui::{
- h_flex, relative, v_flex, ActiveTheme as _, Button, ButtonCommon, ButtonStyle, Clickable,
- Context, ContextMenu, Disableable, DropdownMenu, InteractiveElement, IntoElement,
- ParentElement, Render, SharedString, Styled, Window,
+ h_flex, relative, v_flex, ActiveTheme as _, ButtonLike, Clickable, Context, ContextMenu,
+ Disableable, Disclosure, DropdownMenu, FluentBuilder, InteractiveElement, IntoElement, Label,
+ LabelCommon, LabelSize, ParentElement, PopoverMenu, PopoverMenuHandle, Render, SharedString,
+ SplitButton, Styled, Window,
};
use workspace::Workspace;
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 {
focus_handle: FocusHandle,
selected_debugger: Option<SharedString>,
program_editor: Entity<Editor>,
cwd_editor: Entity<Editor>,
workspace: WeakEntity<Workspace>,
+ spawn_mode: SpawnMode,
+ popover_handle: PopoverMenuHandle<ContextMenu>,
}
impl InertState {
@@ -47,6 +66,8 @@ impl InertState {
program_editor,
selected_debugger: None,
focus_handle: cx.focus_handle(),
+ spawn_mode: SpawnMode::default(),
+ popover_handle: Default::default(),
}
}
}
@@ -72,19 +93,46 @@ impl Render for InertState {
) -> impl ui::IntoElement {
let weak = cx.weak_entity();
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()
.track_focus(&self.focus_handle)
.size_full()
.gap_1()
.p_2()
-
.child(
- v_flex().gap_1()
+ v_flex()
+ .gap_1()
.child(
h_flex()
.w_full()
.gap_2()
-
.child(Self::render_editor(&self.program_editor, cx))
.child(
h_flex().child(DropdownMenu::new(
@@ -109,45 +157,63 @@ impl Render for InertState {
.entry("Delve", None, setter_for_name("Delve"))
.entry("LLDB", None, setter_for_name("LLDB"))
.entry("PHP", None, setter_for_name("PHP"))
- .entry("JavaScript", None, setter_for_name("JavaScript"))
+ .entry(
+ "JavaScript",
+ None,
+ setter_for_name("JavaScript"),
+ )
.entry("Debugpy", None, setter_for_name("Debugpy"))
}),
)),
- )
+ ),
)
.child(
- h_flex().gap_2().child(
- Self::render_editor(&self.cwd_editor, cx),
- ).child(h_flex()
- .gap_4()
- .pl_2()
- .child(
- Button::new("launch-dap", "Launch")
- .style(ButtonStyle::Filled)
- .disabled(disable_buttons)
- .on_click(cx.listener(|this, _, _, cx| {
- let program = this.program_editor.read(cx).text(cx);
- let cwd = PathBuf::from(this.cwd_editor.read(cx).text(cx));
- let kind = kind_for_label(this.selected_debugger.as_deref().unwrap_or_else(|| unimplemented!("Automatic selection of a debugger based on users project")));
- cx.emit(InertEvent::Spawned {
- config: DebugAdapterConfig {
- label: "hard coded".into(),
- kind,
- request: DebugRequestType::Launch,
- program: Some(program),
- cwd: Some(cwd),
- initialize_args: None,
- supports_attach: false,
- },
- });
- })),
- )
- .child(Button::new("attach-dap", "Attach")
- .style(ButtonStyle::Filled)
- .disabled(disable_buttons)
- .on_click(cx.listener(|this, _, window, cx| this.attach(window, cx)))
- ))
- )
+ h_flex()
+ .gap_2()
+ .child(Self::render_editor(&self.cwd_editor, cx))
+ .map(|this| {
+ let entity = cx.weak_entity();
+ this.child(SplitButton {
+ left: spawn_button,
+ right: PopoverMenu::new("debugger-select-spawn-mode")
+ .trigger(Disclosure::new(
+ "debugger-spawn-button-disclosure",
+ self.popover_handle.is_deployed(),
+ ))
+ .menu(move |window, cx| {
+ Some(ContextMenu::build(window, cx, {
+ let entity = entity.clone();
+ move |this, _, _| {
+ this.entry("Launch", None, {
+ let entity = entity.clone();
+ move |_, cx| {
+ let _ =
+ entity.update(cx, |this, cx| {
+ this.spawn_mode =
+ SpawnMode::Launch;
+ cx.notify();
+ });
+ }
+ })
+ .entry("Attach", None, {
+ let entity = entity.clone();
+ move |_, cx| {
+ let _ =
+ entity.update(cx, |this, cx| {
+ this.spawn_mode =
+ SpawnMode::Attach;
+ cx.notify();
+ });
+ }
+ })
+ }
+ }))
+ })
+ .with_handle(self.popover_handle.clone())
+ .into_any_element(),
+ })
+ }),
+ ),
)
}
}
@@ -5918,11 +5918,7 @@ impl Editor {
breakpoint: Option<&(Anchor, Breakpoint)>,
cx: &mut Context<Self>,
) -> Option<IconButton> {
- let color = if breakpoint.is_some() {
- Color::Debugger
- } else {
- Color::Muted
- };
+ let color = Color::Muted;
let position = breakpoint.as_ref().map(|(anchor, _)| *anchor);
let bp_kind = Arc::new(
@@ -49,7 +49,6 @@ serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
settings.workspace = true
-smallvec.workspace = true
strum.workspace = true
telemetry.workspace = true
theme.workspace = true
@@ -163,19 +163,18 @@ fn render_remote_button(
}
mod remote_button {
- use gpui::{hsla, point, Action, AnyView, BoxShadow, ClickEvent, Corner, FocusHandle};
+ use gpui::{Action, AnyView, ClickEvent, Corner, FocusHandle};
use ui::{
- div, h_flex, px, rems, ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, Clickable,
- ContextMenu, ElementId, ElevationIndex, FluentBuilder, Icon, IconName, IconSize,
- IntoElement, Label, LabelCommon, LabelSize, LineHeightStyle, ParentElement, PopoverMenu,
- RenderOnce, SharedString, Styled, Tooltip, Window,
+ div, h_flex, rems, App, ButtonCommon, Clickable, ContextMenu, ElementId, FluentBuilder,
+ Icon, IconName, IconSize, IntoElement, Label, LabelCommon, LabelSize, LineHeightStyle,
+ ParentElement, PopoverMenu, SharedString, SplitButton, Styled, Tooltip, Window,
};
pub fn render_fetch_button(
keybinding_target: Option<FocusHandle>,
id: SharedString,
) -> SplitButton {
- SplitButton::new(
+ split_button(
id,
"Fetch",
0,
@@ -203,7 +202,7 @@ mod remote_button {
id: SharedString,
ahead: u32,
) -> SplitButton {
- SplitButton::new(
+ split_button(
id,
"Push",
ahead as usize,
@@ -232,7 +231,7 @@ mod remote_button {
ahead: u32,
behind: u32,
) -> SplitButton {
- SplitButton::new(
+ split_button(
id,
"Pull",
ahead as usize,
@@ -259,7 +258,7 @@ mod remote_button {
keybinding_target: Option<FocusHandle>,
id: SharedString,
) -> SplitButton {
- SplitButton::new(
+ split_button(
id,
"Publish",
0,
@@ -286,7 +285,7 @@ mod remote_button {
keybinding_target: Option<FocusHandle>,
id: SharedString,
) -> SplitButton {
- SplitButton::new(
+ split_button(
id,
"Republish",
0,
@@ -364,111 +363,76 @@ mod remote_button {
})
.anchor(Corner::TopRight)
}
+ #[allow(clippy::too_many_arguments)]
+ fn split_button(
+ id: SharedString,
+ left_label: impl Into<SharedString>,
+ 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,
+ ) -> SplitButton {
+ 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),
+ )
+ }
- #[derive(IntoElement)]
- pub struct SplitButton {
- pub left: ButtonLike,
- pub right: AnyElement,
- }
-
- impl SplitButton {
- #[allow(clippy::too_many_arguments)]
- fn new(
- id: impl Into<SharedString>,
- left_label: impl Into<SharedString>,
- 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();
+ let should_render_counts = left_icon.is_none() && (ahead_count > 0 || behind_count > 0);
- fn count(count: usize) -> impl IntoElement {
+ 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_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(),
+ .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))
+ }),
)
- .on_click(left_on_click)
- .tooltip(tooltip);
-
- let right = render_git_action_menu(
- ElementId::Name(format!("split-button-right-{}", id).into()),
- keybinding_target,
+ })
+ .when_some(left_icon, |this, left_icon| {
+ this.child(
+ h_flex()
+ .ml_neg_0p5()
+ .mr_1()
+ .child(Icon::new(left_icon).size(IconSize::XSmall)),
)
- .into_any_element();
+ })
+ .child(
+ div()
+ .child(Label::new(left_label).size(LabelSize::Small))
+ .mr_0p5(),
+ )
+ .on_click(left_on_click)
+ .tooltip(tooltip);
- Self { left, right }
- }
- }
+ let right = render_git_action_menu(
+ ElementId::Name(format!("split-button-right-{}", id).into()),
+ keybinding_target,
+ )
+ .into_any_element();
- 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.),
- }])
- }
+ SplitButton { left, right }
}
}
@@ -2,9 +2,11 @@ mod button;
mod button_icon;
mod button_like;
mod icon_button;
+mod split_button;
mod toggle_button;
pub use button::*;
pub use button_like::*;
pub use icon_button::*;
+pub use split_button::*;
pub use toggle_button::*;
@@ -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.),
+ }])
+ }
+}