debugger: Add UI for tweaking breakpoint properties directly from breakpoint list (#33097)
Release Notes: - debugger: Breakpoint properties (log/hit condition/condition) can now be set directly from breakpoint list.
This commit is contained in:
parent
41583fb066
commit
e5bcd720e1
11 changed files with 634 additions and 82 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4310,6 +4310,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"alacritty_terminal",
|
||||
"anyhow",
|
||||
"bitflags 2.9.0",
|
||||
"client",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
|
|
1
assets/icons/arrow_down10.svg
Normal file
1
assets/icons/arrow_down10.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-down10-icon lucide-arrow-down-1-0"><path d="m3 16 4 4 4-4"/><path d="M7 20V4"/><path d="M17 10V4h-2"/><path d="M15 10h4"/><rect x="15" y="14" width="4" height="6" ry="2"/></svg>
|
After Width: | Height: | Size: 386 B |
1
assets/icons/scroll_text.svg
Normal file
1
assets/icons/scroll_text.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scroll-text-icon lucide-scroll-text"><path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/></svg>
|
After Width: | Height: | Size: 441 B |
1
assets/icons/split_alt.svg
Normal file
1
assets/icons/split_alt.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-split-icon lucide-split"><path d="M16 3h5v5"/><path d="M8 3H3v5"/><path d="M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3"/><path d="m15 9 6-6"/></svg>
|
After Width: | Height: | Size: 345 B |
|
@ -919,7 +919,9 @@
|
|||
"context": "BreakpointList",
|
||||
"bindings": {
|
||||
"space": "debugger::ToggleEnableBreakpoint",
|
||||
"backspace": "debugger::UnsetBreakpoint"
|
||||
"backspace": "debugger::UnsetBreakpoint",
|
||||
"left": "debugger::PreviousBreakpointProperty",
|
||||
"right": "debugger::NextBreakpointProperty"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -980,7 +980,9 @@
|
|||
"context": "BreakpointList",
|
||||
"bindings": {
|
||||
"space": "debugger::ToggleEnableBreakpoint",
|
||||
"backspace": "debugger::UnsetBreakpoint"
|
||||
"backspace": "debugger::UnsetBreakpoint",
|
||||
"left": "debugger::PreviousBreakpointProperty",
|
||||
"right": "debugger::NextBreakpointProperty"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -28,6 +28,7 @@ test-support = [
|
|||
[dependencies]
|
||||
alacritty_terminal.workspace = true
|
||||
anyhow.workspace = true
|
||||
bitflags.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
|
|
|
@ -100,7 +100,13 @@ impl DebugPanel {
|
|||
sessions: vec![],
|
||||
active_session: None,
|
||||
focus_handle,
|
||||
breakpoint_list: BreakpointList::new(None, workspace.weak_handle(), &project, cx),
|
||||
breakpoint_list: BreakpointList::new(
|
||||
None,
|
||||
workspace.weak_handle(),
|
||||
&project,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
project,
|
||||
workspace: workspace.weak_handle(),
|
||||
context_menu: None,
|
||||
|
|
|
@ -697,8 +697,13 @@ impl RunningState {
|
|||
)
|
||||
});
|
||||
|
||||
let breakpoint_list =
|
||||
BreakpointList::new(Some(session.clone()), workspace.clone(), &project, cx);
|
||||
let breakpoint_list = BreakpointList::new(
|
||||
Some(session.clone()),
|
||||
workspace.clone(),
|
||||
&project,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let _subscriptions = vec![
|
||||
cx.on_app_quit(move |this, cx| {
|
||||
|
|
|
@ -5,11 +5,11 @@ use std::{
|
|||
time::Duration,
|
||||
};
|
||||
|
||||
use dap::ExceptionBreakpointsFilter;
|
||||
use dap::{Capabilities, ExceptionBreakpointsFilter};
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
Action, AppContext, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful,
|
||||
Task, UniformListScrollHandle, WeakEntity, uniform_list,
|
||||
Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy,
|
||||
Stateful, Task, UniformListScrollHandle, WeakEntity, actions, uniform_list,
|
||||
};
|
||||
use language::Point;
|
||||
use project::{
|
||||
|
@ -21,16 +21,20 @@ use project::{
|
|||
worktree_store::WorktreeStore,
|
||||
};
|
||||
use ui::{
|
||||
AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div, FluentBuilder as _,
|
||||
Icon, IconButton, IconName, IconSize, Indicator, InteractiveElement, IntoElement, Label,
|
||||
LabelCommon, LabelSize, ListItem, ParentElement, Render, Scrollbar, ScrollbarState,
|
||||
SharedString, StatefulInteractiveElement, Styled, Toggleable, Tooltip, Window, div, h_flex, px,
|
||||
v_flex,
|
||||
ActiveTheme, AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div,
|
||||
Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, Indicator,
|
||||
InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement,
|
||||
Render, RenderOnce, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement,
|
||||
Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
|
||||
|
||||
actions!(
|
||||
debugger,
|
||||
[PreviousBreakpointProperty, NextBreakpointProperty]
|
||||
);
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub(crate) enum SelectedBreakpointKind {
|
||||
Source,
|
||||
|
@ -48,6 +52,8 @@ pub(crate) struct BreakpointList {
|
|||
focus_handle: FocusHandle,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
selected_ix: Option<usize>,
|
||||
input: Entity<Editor>,
|
||||
strip_mode: Option<ActiveBreakpointStripMode>,
|
||||
}
|
||||
|
||||
impl Focusable for BreakpointList {
|
||||
|
@ -56,11 +62,19 @@ impl Focusable for BreakpointList {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum ActiveBreakpointStripMode {
|
||||
Log,
|
||||
Condition,
|
||||
HitCondition,
|
||||
}
|
||||
|
||||
impl BreakpointList {
|
||||
pub(crate) fn new(
|
||||
session: Option<Entity<Session>>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: &Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
let project = project.read(cx);
|
||||
|
@ -70,7 +84,7 @@ impl BreakpointList {
|
|||
let scroll_handle = UniformListScrollHandle::new();
|
||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||
|
||||
cx.new(|_| Self {
|
||||
cx.new(|cx| Self {
|
||||
breakpoint_store,
|
||||
worktree_store,
|
||||
scrollbar_state,
|
||||
|
@ -82,17 +96,28 @@ impl BreakpointList {
|
|||
focus_handle,
|
||||
scroll_handle,
|
||||
selected_ix: None,
|
||||
input: cx.new(|cx| Editor::single_line(window, cx)),
|
||||
strip_mode: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn edit_line_breakpoint(
|
||||
&mut self,
|
||||
&self,
|
||||
path: Arc<Path>,
|
||||
row: u32,
|
||||
action: BreakpointEditAction,
|
||||
cx: &mut Context<Self>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.breakpoint_store.update(cx, |breakpoint_store, cx| {
|
||||
Self::edit_line_breakpoint_inner(&self.breakpoint_store, path, row, action, cx);
|
||||
}
|
||||
fn edit_line_breakpoint_inner(
|
||||
breakpoint_store: &Entity<BreakpointStore>,
|
||||
path: Arc<Path>,
|
||||
row: u32,
|
||||
action: BreakpointEditAction,
|
||||
cx: &mut App,
|
||||
) {
|
||||
breakpoint_store.update(cx, |breakpoint_store, cx| {
|
||||
if let Some((buffer, breakpoint)) = breakpoint_store.breakpoint_at_row(&path, row, cx) {
|
||||
breakpoint_store.toggle_breakpoint(buffer, breakpoint, action, cx);
|
||||
} else {
|
||||
|
@ -148,16 +173,63 @@ impl BreakpointList {
|
|||
})
|
||||
}
|
||||
|
||||
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
|
||||
fn set_active_breakpoint_property(
|
||||
&mut self,
|
||||
prop: ActiveBreakpointStripMode,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.strip_mode = Some(prop);
|
||||
let placeholder = match prop {
|
||||
ActiveBreakpointStripMode::Log => "Set Log Message",
|
||||
ActiveBreakpointStripMode::Condition => "Set Condition",
|
||||
ActiveBreakpointStripMode::HitCondition => "Set Hit Condition",
|
||||
};
|
||||
let mut is_exception_breakpoint = true;
|
||||
let active_value = self.selected_ix.and_then(|ix| {
|
||||
self.breakpoints.get(ix).and_then(|bp| {
|
||||
if let BreakpointEntryKind::LineBreakpoint(bp) = &bp.kind {
|
||||
is_exception_breakpoint = false;
|
||||
match prop {
|
||||
ActiveBreakpointStripMode::Log => bp.breakpoint.message.clone(),
|
||||
ActiveBreakpointStripMode::Condition => bp.breakpoint.condition.clone(),
|
||||
ActiveBreakpointStripMode::HitCondition => {
|
||||
bp.breakpoint.hit_condition.clone()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
self.input.update(cx, |this, cx| {
|
||||
this.set_placeholder_text(placeholder, cx);
|
||||
this.set_read_only(is_exception_breakpoint);
|
||||
this.set_text(active_value.as_deref().unwrap_or(""), window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn select_ix(&mut self, ix: Option<usize>, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.selected_ix = ix;
|
||||
if let Some(ix) = ix {
|
||||
self.scroll_handle
|
||||
.scroll_to_item(ix, ScrollStrategy::Center);
|
||||
}
|
||||
if let Some(mode) = self.strip_mode {
|
||||
self.set_active_breakpoint_property(mode, window, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.strip_mode.is_some() {
|
||||
if self.input.focus_handle(cx).contains_focused(window, cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
let ix = match self.selected_ix {
|
||||
_ if self.breakpoints.len() == 0 => None,
|
||||
None => Some(0),
|
||||
|
@ -169,15 +241,21 @@ impl BreakpointList {
|
|||
}
|
||||
}
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
self.select_ix(ix, window, cx);
|
||||
}
|
||||
|
||||
fn select_previous(
|
||||
&mut self,
|
||||
_: &menu::SelectPrevious,
|
||||
_window: &mut Window,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.strip_mode.is_some() {
|
||||
if self.input.focus_handle(cx).contains_focused(window, cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
let ix = match self.selected_ix {
|
||||
_ if self.breakpoints.len() == 0 => None,
|
||||
None => Some(self.breakpoints.len() - 1),
|
||||
|
@ -189,37 +267,105 @@ impl BreakpointList {
|
|||
}
|
||||
}
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
self.select_ix(ix, window, cx);
|
||||
}
|
||||
|
||||
fn select_first(
|
||||
&mut self,
|
||||
_: &menu::SelectFirst,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.strip_mode.is_some() {
|
||||
if self.input.focus_handle(cx).contains_focused(window, cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
let ix = if self.breakpoints.len() > 0 {
|
||||
Some(0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
self.select_ix(ix, window, cx);
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.strip_mode.is_some() {
|
||||
if self.input.focus_handle(cx).contains_focused(window, cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
let ix = if self.breakpoints.len() > 0 {
|
||||
Some(self.breakpoints.len() - 1)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.select_ix(ix, cx);
|
||||
self.select_ix(ix, window, cx);
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.input.focus_handle(cx).contains_focused(window, cx) {
|
||||
self.focus_handle.focus(window);
|
||||
} else if self.strip_mode.is_some() {
|
||||
self.strip_mode.take();
|
||||
cx.notify();
|
||||
} else {
|
||||
cx.propagate();
|
||||
}
|
||||
}
|
||||
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(mode) = self.strip_mode {
|
||||
let handle = self.input.focus_handle(cx);
|
||||
if handle.is_focused(window) {
|
||||
// Go back to the main strip. Save the result as well.
|
||||
let text = self.input.read(cx).text(cx);
|
||||
|
||||
match mode {
|
||||
ActiveBreakpointStripMode::Log => match &entry.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
|
||||
Self::edit_line_breakpoint_inner(
|
||||
&self.breakpoint_store,
|
||||
line_breakpoint.breakpoint.path.clone(),
|
||||
line_breakpoint.breakpoint.row,
|
||||
BreakpointEditAction::EditLogMessage(Arc::from(text)),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ActiveBreakpointStripMode::Condition => match &entry.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
|
||||
Self::edit_line_breakpoint_inner(
|
||||
&self.breakpoint_store,
|
||||
line_breakpoint.breakpoint.path.clone(),
|
||||
line_breakpoint.breakpoint.row,
|
||||
BreakpointEditAction::EditCondition(Arc::from(text)),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ActiveBreakpointStripMode::HitCondition => match &entry.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
|
||||
Self::edit_line_breakpoint_inner(
|
||||
&self.breakpoint_store,
|
||||
line_breakpoint.breakpoint.path.clone(),
|
||||
line_breakpoint.breakpoint.row,
|
||||
BreakpointEditAction::EditHitCondition(Arc::from(text)),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
self.focus_handle.focus(window);
|
||||
} else {
|
||||
handle.focus(window);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
match &mut entry.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
|
||||
let path = line_breakpoint.breakpoint.path.clone();
|
||||
|
@ -233,12 +379,18 @@ impl BreakpointList {
|
|||
fn toggle_enable_breakpoint(
|
||||
&mut self,
|
||||
_: &ToggleEnableBreakpoint,
|
||||
_window: &mut Window,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
|
||||
return;
|
||||
};
|
||||
if self.strip_mode.is_some() {
|
||||
if self.input.focus_handle(cx).contains_focused(window, cx) {
|
||||
cx.propagate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
match &mut entry.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
|
||||
|
@ -279,6 +431,50 @@ impl BreakpointList {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
fn previous_breakpoint_property(
|
||||
&mut self,
|
||||
_: &PreviousBreakpointProperty,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let next_mode = match self.strip_mode {
|
||||
Some(ActiveBreakpointStripMode::Log) => None,
|
||||
Some(ActiveBreakpointStripMode::Condition) => Some(ActiveBreakpointStripMode::Log),
|
||||
Some(ActiveBreakpointStripMode::HitCondition) => {
|
||||
Some(ActiveBreakpointStripMode::Condition)
|
||||
}
|
||||
None => Some(ActiveBreakpointStripMode::HitCondition),
|
||||
};
|
||||
if let Some(mode) = next_mode {
|
||||
self.set_active_breakpoint_property(mode, window, cx);
|
||||
} else {
|
||||
self.strip_mode.take();
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
fn next_breakpoint_property(
|
||||
&mut self,
|
||||
_: &NextBreakpointProperty,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let next_mode = match self.strip_mode {
|
||||
Some(ActiveBreakpointStripMode::Log) => Some(ActiveBreakpointStripMode::Condition),
|
||||
Some(ActiveBreakpointStripMode::Condition) => {
|
||||
Some(ActiveBreakpointStripMode::HitCondition)
|
||||
}
|
||||
Some(ActiveBreakpointStripMode::HitCondition) => None,
|
||||
None => Some(ActiveBreakpointStripMode::Log),
|
||||
};
|
||||
if let Some(mode) = next_mode {
|
||||
self.set_active_breakpoint_property(mode, window, cx);
|
||||
} else {
|
||||
self.strip_mode.take();
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
|
||||
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
|
||||
|
@ -294,20 +490,31 @@ impl BreakpointList {
|
|||
}))
|
||||
}
|
||||
|
||||
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render_list(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let selected_ix = self.selected_ix;
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let supported_breakpoint_properties = self
|
||||
.session
|
||||
.as_ref()
|
||||
.map(|session| SupportedBreakpointProperties::from(session.read(cx).capabilities()))
|
||||
.unwrap_or_else(SupportedBreakpointProperties::empty);
|
||||
let strip_mode = self.strip_mode;
|
||||
uniform_list(
|
||||
"breakpoint-list",
|
||||
self.breakpoints.len(),
|
||||
cx.processor(move |this, range: Range<usize>, window, cx| {
|
||||
cx.processor(move |this, range: Range<usize>, _, _| {
|
||||
range
|
||||
.clone()
|
||||
.zip(&mut this.breakpoints[range])
|
||||
.map(|(ix, breakpoint)| {
|
||||
breakpoint
|
||||
.render(ix, focus_handle.clone(), window, cx)
|
||||
.toggle_state(Some(ix) == selected_ix)
|
||||
.render(
|
||||
strip_mode,
|
||||
supported_breakpoint_properties,
|
||||
ix,
|
||||
Some(ix) == selected_ix,
|
||||
focus_handle.clone(),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.collect()
|
||||
|
@ -443,7 +650,6 @@ impl BreakpointList {
|
|||
|
||||
impl Render for BreakpointList {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
||||
// let old_len = self.breakpoints.len();
|
||||
let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
|
||||
self.breakpoints.clear();
|
||||
let weak = cx.weak_entity();
|
||||
|
@ -523,15 +729,46 @@ impl Render for BreakpointList {
|
|||
.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::dismiss))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::toggle_enable_breakpoint))
|
||||
.on_action(cx.listener(Self::unset_breakpoint))
|
||||
.on_action(cx.listener(Self::next_breakpoint_property))
|
||||
.on_action(cx.listener(Self::previous_breakpoint_property))
|
||||
.size_full()
|
||||
.m_0p5()
|
||||
.child(self.render_list(window, cx))
|
||||
.children(self.render_vertical_scrollbar(cx))
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.child(self.render_list(cx))
|
||||
.children(self.render_vertical_scrollbar(cx)),
|
||||
)
|
||||
.when_some(self.strip_mode, |this, _| {
|
||||
this.child(Divider::horizontal()).child(
|
||||
h_flex()
|
||||
// .w_full()
|
||||
.m_0p5()
|
||||
.p_0p5()
|
||||
.border_1()
|
||||
.rounded_sm()
|
||||
.when(
|
||||
self.input.focus_handle(cx).contains_focused(window, cx),
|
||||
|this| {
|
||||
let colors = cx.theme().colors();
|
||||
let border = if self.input.read(cx).read_only(cx) {
|
||||
colors.border_disabled
|
||||
} else {
|
||||
colors.border_focused
|
||||
};
|
||||
this.border_color(border)
|
||||
},
|
||||
)
|
||||
.child(self.input.clone()),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct LineBreakpoint {
|
||||
name: SharedString,
|
||||
|
@ -543,7 +780,10 @@ struct LineBreakpoint {
|
|||
impl LineBreakpoint {
|
||||
fn render(
|
||||
&mut self,
|
||||
props: SupportedBreakpointProperties,
|
||||
strip_mode: Option<ActiveBreakpointStripMode>,
|
||||
ix: usize,
|
||||
is_selected: bool,
|
||||
focus_handle: FocusHandle,
|
||||
weak: WeakEntity<BreakpointList>,
|
||||
) -> ListItem {
|
||||
|
@ -594,15 +834,16 @@ impl LineBreakpoint {
|
|||
})
|
||||
.child(Indicator::icon(Icon::new(icon_name)).color(Color::Debugger))
|
||||
.on_mouse_down(MouseButton::Left, move |_, _, _| {});
|
||||
|
||||
ListItem::new(SharedString::from(format!(
|
||||
"breakpoint-ui-item-{:?}/{}:{}",
|
||||
self.dir, self.name, self.line
|
||||
)))
|
||||
.on_click({
|
||||
let weak = weak.clone();
|
||||
move |_, _, cx| {
|
||||
move |_, window, cx| {
|
||||
weak.update(cx, |breakpoint_list, cx| {
|
||||
breakpoint_list.select_ix(Some(ix), cx);
|
||||
breakpoint_list.select_ix(Some(ix), window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
@ -613,21 +854,26 @@ impl LineBreakpoint {
|
|||
cx.stop_propagation();
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.py_1()
|
||||
h_flex()
|
||||
.w_full()
|
||||
.mr_4()
|
||||
.py_0p5()
|
||||
.gap_1()
|
||||
.min_h(px(26.))
|
||||
.justify_center()
|
||||
.justify_between()
|
||||
.id(SharedString::from(format!(
|
||||
"breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
|
||||
self.dir, self.name, self.line
|
||||
)))
|
||||
.on_click(move |_, window, cx| {
|
||||
.on_click({
|
||||
let weak = weak.clone();
|
||||
move |_, window, cx| {
|
||||
weak.update(cx, |breakpoint_list, cx| {
|
||||
breakpoint_list.select_ix(Some(ix), cx);
|
||||
breakpoint_list.select_ix(Some(ix), window, cx);
|
||||
breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.cursor_pointer()
|
||||
.child(
|
||||
|
@ -644,8 +890,20 @@ impl LineBreakpoint {
|
|||
.size(LabelSize::Small)
|
||||
.line_height_style(ui::LineHeightStyle::UiLabel)
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(BreakpointOptionsStrip {
|
||||
props,
|
||||
breakpoint: BreakpointEntry {
|
||||
kind: BreakpointEntryKind::LineBreakpoint(self.clone()),
|
||||
weak: weak,
|
||||
},
|
||||
is_selected,
|
||||
focus_handle,
|
||||
strip_mode,
|
||||
index: ix,
|
||||
}),
|
||||
)
|
||||
.toggle_state(is_selected)
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -658,7 +916,10 @@ struct ExceptionBreakpoint {
|
|||
impl ExceptionBreakpoint {
|
||||
fn render(
|
||||
&mut self,
|
||||
props: SupportedBreakpointProperties,
|
||||
strip_mode: Option<ActiveBreakpointStripMode>,
|
||||
ix: usize,
|
||||
is_selected: bool,
|
||||
focus_handle: FocusHandle,
|
||||
list: WeakEntity<BreakpointList>,
|
||||
) -> ListItem {
|
||||
|
@ -669,15 +930,15 @@ impl ExceptionBreakpoint {
|
|||
};
|
||||
let id = SharedString::from(&self.id);
|
||||
let is_enabled = self.is_enabled;
|
||||
|
||||
let weak = list.clone();
|
||||
ListItem::new(SharedString::from(format!(
|
||||
"exception-breakpoint-ui-item-{}",
|
||||
self.id
|
||||
)))
|
||||
.on_click({
|
||||
let list = list.clone();
|
||||
move |_, _, cx| {
|
||||
list.update(cx, |list, cx| list.select_ix(Some(ix), cx))
|
||||
move |_, window, cx| {
|
||||
list.update(cx, |list, cx| list.select_ix(Some(ix), window, cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
|
@ -691,7 +952,9 @@ impl ExceptionBreakpoint {
|
|||
"exception-breakpoint-ui-item-{}-click-handler",
|
||||
self.id
|
||||
)))
|
||||
.tooltip(move |window, cx| {
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
if is_enabled {
|
||||
"Disable Exception Breakpoint"
|
||||
|
@ -703,6 +966,7 @@ impl ExceptionBreakpoint {
|
|||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let list = list.clone();
|
||||
|
@ -721,6 +985,12 @@ impl ExceptionBreakpoint {
|
|||
.cursor_pointer()
|
||||
.child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.mr_4()
|
||||
.py_0p5()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.py_1()
|
||||
|
@ -737,6 +1007,19 @@ impl ExceptionBreakpoint {
|
|||
el.tooltip(Tooltip::text(description))
|
||||
}),
|
||||
)
|
||||
.child(BreakpointOptionsStrip {
|
||||
props,
|
||||
breakpoint: BreakpointEntry {
|
||||
kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()),
|
||||
weak: weak,
|
||||
},
|
||||
is_selected,
|
||||
focus_handle,
|
||||
strip_mode,
|
||||
index: ix,
|
||||
}),
|
||||
)
|
||||
.toggle_state(is_selected)
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -754,18 +1037,264 @@ struct BreakpointEntry {
|
|||
impl BreakpointEntry {
|
||||
fn render(
|
||||
&mut self,
|
||||
strip_mode: Option<ActiveBreakpointStripMode>,
|
||||
props: SupportedBreakpointProperties,
|
||||
ix: usize,
|
||||
is_selected: bool,
|
||||
focus_handle: FocusHandle,
|
||||
_: &mut Window,
|
||||
_: &mut App,
|
||||
) -> ListItem {
|
||||
match &mut self.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
|
||||
line_breakpoint.render(ix, focus_handle, self.weak.clone())
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => line_breakpoint.render(
|
||||
props,
|
||||
strip_mode,
|
||||
ix,
|
||||
is_selected,
|
||||
focus_handle,
|
||||
self.weak.clone(),
|
||||
),
|
||||
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => exception_breakpoint
|
||||
.render(
|
||||
props.for_exception_breakpoints(),
|
||||
strip_mode,
|
||||
ix,
|
||||
is_selected,
|
||||
focus_handle,
|
||||
self.weak.clone(),
|
||||
),
|
||||
}
|
||||
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
|
||||
exception_breakpoint.render(ix, focus_handle, self.weak.clone())
|
||||
}
|
||||
|
||||
fn id(&self) -> SharedString {
|
||||
match &self.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => format!(
|
||||
"source-breakpoint-control-strip-{:?}:{}",
|
||||
line_breakpoint.breakpoint.path, line_breakpoint.breakpoint.row
|
||||
)
|
||||
.into(),
|
||||
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => format!(
|
||||
"exception-breakpoint-control-strip--{}",
|
||||
exception_breakpoint.id
|
||||
)
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn has_log(&self) -> bool {
|
||||
match &self.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
|
||||
line_breakpoint.breakpoint.message.is_some()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn has_condition(&self) -> bool {
|
||||
match &self.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
|
||||
line_breakpoint.breakpoint.condition.is_some()
|
||||
}
|
||||
// We don't support conditions on exception breakpoints
|
||||
BreakpointEntryKind::ExceptionBreakpoint(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn has_hit_condition(&self) -> bool {
|
||||
match &self.kind {
|
||||
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
|
||||
line_breakpoint.breakpoint.hit_condition.is_some()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
bitflags::bitflags! {
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SupportedBreakpointProperties: u32 {
|
||||
const LOG = 1 << 0;
|
||||
const CONDITION = 1 << 1;
|
||||
const HIT_CONDITION = 1 << 2;
|
||||
// Conditions for exceptions can be set only when exception filters are supported.
|
||||
const EXCEPTION_FILTER_OPTIONS = 1 << 3;
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Capabilities> for SupportedBreakpointProperties {
|
||||
fn from(caps: &Capabilities) -> Self {
|
||||
let mut this = Self::empty();
|
||||
for (prop, offset) in [
|
||||
(caps.supports_log_points, Self::LOG),
|
||||
(caps.supports_conditional_breakpoints, Self::CONDITION),
|
||||
(
|
||||
caps.supports_hit_conditional_breakpoints,
|
||||
Self::HIT_CONDITION,
|
||||
),
|
||||
(
|
||||
caps.supports_exception_options,
|
||||
Self::EXCEPTION_FILTER_OPTIONS,
|
||||
),
|
||||
] {
|
||||
if prop.unwrap_or_default() {
|
||||
this.insert(offset);
|
||||
}
|
||||
}
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
impl SupportedBreakpointProperties {
|
||||
fn for_exception_breakpoints(self) -> Self {
|
||||
// TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here.
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
#[derive(IntoElement)]
|
||||
struct BreakpointOptionsStrip {
|
||||
props: SupportedBreakpointProperties,
|
||||
breakpoint: BreakpointEntry,
|
||||
is_selected: bool,
|
||||
focus_handle: FocusHandle,
|
||||
strip_mode: Option<ActiveBreakpointStripMode>,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl BreakpointOptionsStrip {
|
||||
fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool {
|
||||
self.is_selected && self.strip_mode == Some(expected_mode)
|
||||
}
|
||||
fn on_click_callback(
|
||||
&self,
|
||||
mode: ActiveBreakpointStripMode,
|
||||
) -> impl for<'a> Fn(&ClickEvent, &mut Window, &'a mut App) + use<> {
|
||||
let list = self.breakpoint.weak.clone();
|
||||
let ix = self.index;
|
||||
move |_, window, cx| {
|
||||
list.update(cx, |this, cx| {
|
||||
if this.strip_mode != Some(mode) {
|
||||
this.set_active_breakpoint_property(mode, window, cx);
|
||||
} else if this.selected_ix == Some(ix) {
|
||||
this.strip_mode.take();
|
||||
} else {
|
||||
cx.propagate();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
fn add_border(
|
||||
&self,
|
||||
kind: ActiveBreakpointStripMode,
|
||||
available: bool,
|
||||
window: &Window,
|
||||
cx: &App,
|
||||
) -> impl Fn(Div) -> Div {
|
||||
move |this: Div| {
|
||||
// Avoid layout shifts in case there's no colored border
|
||||
let this = this.border_2().rounded_sm();
|
||||
if self.is_selected && self.strip_mode == Some(kind) {
|
||||
let theme = cx.theme().colors();
|
||||
if self.focus_handle.is_focused(window) {
|
||||
this.border_color(theme.border_selected)
|
||||
} else {
|
||||
this.border_color(theme.border_disabled)
|
||||
}
|
||||
} else if !available {
|
||||
this.border_color(cx.theme().colors().border_disabled)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl RenderOnce for BreakpointOptionsStrip {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let id = self.breakpoint.id();
|
||||
let supports_logs = self.props.contains(SupportedBreakpointProperties::LOG);
|
||||
let supports_condition = self
|
||||
.props
|
||||
.contains(SupportedBreakpointProperties::CONDITION);
|
||||
let supports_hit_condition = self
|
||||
.props
|
||||
.contains(SupportedBreakpointProperties::HIT_CONDITION);
|
||||
let has_logs = self.breakpoint.has_log();
|
||||
let has_condition = self.breakpoint.has_condition();
|
||||
let has_hit_condition = self.breakpoint.has_hit_condition();
|
||||
let style_for_toggle = |mode, is_enabled| {
|
||||
if is_enabled && self.strip_mode == Some(mode) && self.is_selected {
|
||||
ui::ButtonStyle::Filled
|
||||
} else {
|
||||
ui::ButtonStyle::Subtle
|
||||
}
|
||||
};
|
||||
let color_for_toggle = |is_enabled| {
|
||||
if is_enabled {
|
||||
ui::Color::Default
|
||||
} else {
|
||||
ui::Color::Muted
|
||||
}
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div() .map(self.add_border(ActiveBreakpointStripMode::Log, supports_logs, window, cx))
|
||||
.child(
|
||||
IconButton::new(
|
||||
SharedString::from(format!("{id}-log-toggle")),
|
||||
IconName::ScrollText,
|
||||
)
|
||||
.style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs))
|
||||
.icon_color(color_for_toggle(has_logs))
|
||||
.disabled(!supports_logs)
|
||||
.toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log))
|
||||
.on_click(self.on_click_callback(ActiveBreakpointStripMode::Log)).tooltip(|window, cx| Tooltip::with_meta("Set Log Message", None, "Set log message to display (instead of stopping) when a breakpoint is hit", window, cx))
|
||||
)
|
||||
.when(!has_logs && !self.is_selected, |this| this.invisible()),
|
||||
)
|
||||
.child(
|
||||
div().map(self.add_border(
|
||||
ActiveBreakpointStripMode::Condition,
|
||||
supports_condition,
|
||||
window, cx
|
||||
))
|
||||
.child(
|
||||
IconButton::new(
|
||||
SharedString::from(format!("{id}-condition-toggle")),
|
||||
IconName::SplitAlt,
|
||||
)
|
||||
.style(style_for_toggle(
|
||||
ActiveBreakpointStripMode::Condition,
|
||||
has_condition
|
||||
))
|
||||
.icon_color(color_for_toggle(has_condition))
|
||||
.disabled(!supports_condition)
|
||||
.toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition))
|
||||
.on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition))
|
||||
.tooltip(|window, cx| Tooltip::with_meta("Set Condition", None, "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met", window, cx))
|
||||
)
|
||||
.when(!has_condition && !self.is_selected, |this| this.invisible()),
|
||||
)
|
||||
.child(
|
||||
div() .map(self.add_border(
|
||||
ActiveBreakpointStripMode::HitCondition,
|
||||
supports_hit_condition,window, cx
|
||||
))
|
||||
.child(
|
||||
IconButton::new(
|
||||
SharedString::from(format!("{id}-hit-condition-toggle")),
|
||||
IconName::ArrowDown10,
|
||||
)
|
||||
.style(style_for_toggle(
|
||||
ActiveBreakpointStripMode::HitCondition,
|
||||
has_hit_condition,
|
||||
))
|
||||
.icon_color(color_for_toggle(has_hit_condition))
|
||||
.disabled(!supports_hit_condition)
|
||||
.toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition))
|
||||
.on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition)).tooltip(|window, cx| Tooltip::with_meta("Set Hit Condition", None, "Set expression that controls how many hits of the breakpoint are ignored.", window, cx))
|
||||
)
|
||||
.when(!has_hit_condition && !self.is_selected, |this| {
|
||||
this.invisible()
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ pub enum IconName {
|
|||
AiZed,
|
||||
ArrowCircle,
|
||||
ArrowDown,
|
||||
ArrowDown10,
|
||||
ArrowDownFromLine,
|
||||
ArrowDownRight,
|
||||
ArrowLeft,
|
||||
|
@ -212,6 +213,7 @@ pub enum IconName {
|
|||
Save,
|
||||
Scissors,
|
||||
Screen,
|
||||
ScrollText,
|
||||
SearchCode,
|
||||
SearchSelection,
|
||||
SelectAll,
|
||||
|
@ -231,6 +233,7 @@ pub enum IconName {
|
|||
SparkleFilled,
|
||||
Spinner,
|
||||
Split,
|
||||
SplitAlt,
|
||||
SquareDot,
|
||||
SquareMinus,
|
||||
SquarePlus,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue