Debugger implementation (#13433)
### DISCLAIMER > As of 6th March 2025, debugger is still in development. We plan to merge it behind a staff-only feature flag for staff use only, followed by non-public release and then finally a public one (akin to how Git panel release was handled). This is done to ensure the best experience when it gets released. ### END OF DISCLAIMER **The current state of the debugger implementation:** https://github.com/user-attachments/assets/c4deff07-80dd-4dc6-ad2e-0c252a478fe9 https://github.com/user-attachments/assets/e1ed2345-b750-4bb6-9c97-50961b76904f ---- All the todo's are in the following channel, so it's easier to work on this together: https://zed.dev/channel/zed-debugger-11370 If you are on Linux, you can use the following command to join the channel: ```cli zed https://zed.dev/channel/zed-debugger-11370 ``` ## Current Features - Collab - Breakpoints - Sync when you (re)join a project - Sync when you add/remove a breakpoint - Sync active debug line - Stack frames - Click on stack frame - View variables that belong to the stack frame - Visit the source file - Restart stack frame (if adapter supports this) - Variables - Loaded sources - Modules - Controls - Continue - Step back - Stepping granularity (configurable) - Step into - Stepping granularity (configurable) - Step over - Stepping granularity (configurable) - Step out - Stepping granularity (configurable) - Debug console - Breakpoints - Log breakpoints - line breakpoints - Persistent between zed sessions (configurable) - Multi buffer support - Toggle disable/enable all breakpoints - Stack frames - Click on stack frame - View variables that belong to the stack frame - Visit the source file - Show collapsed stack frames - Restart stack frame (if adapter supports this) - Loaded sources - View all used loaded sources if supported by adapter. - Modules - View all used modules (if adapter supports this) - Variables - Copy value - Copy name - Copy memory reference - Set value (if adapter supports this) - keyboard navigation - Debug Console - See logs - View output that was sent from debug adapter - Output grouping - Evaluate code - Updates the variable list - Auto completion - If not supported by adapter, we will show auto-completion for existing variables - Debug Terminal - Run custom commands and change env values right inside your Zed terminal - Attach to process (if adapter supports this) - Process picker - Controls - Continue - Step back - Stepping granularity (configurable) - Step into - Stepping granularity (configurable) - Step over - Stepping granularity (configurable) - Step out - Stepping granularity (configurable) - Disconnect - Restart - Stop - Warning when a debug session exited without hitting any breakpoint - Debug view to see Adapter/RPC log messages - Testing - Fake debug adapter - Fake requests & events --- Release Notes: - N/A --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Co-authored-by: Anthony Eid <hello@anthonyeid.me> Co-authored-by: Anthony <anthony@zed.dev> Co-authored-by: Piotr Osiewicz <peterosiewicz@gmail.com> Co-authored-by: Piotr <piotr@zed.dev>
This commit is contained in:
parent
ed4e654fdf
commit
41a60ffecf
156 changed files with 25840 additions and 451 deletions
|
@ -68,6 +68,7 @@ use element::{layout_line, AcceptEditPredictionBinding, LineWithInvisibles, Posi
|
|||
pub use element::{
|
||||
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
|
||||
};
|
||||
use feature_flags::{Debugger, FeatureFlagAppExt};
|
||||
use futures::{
|
||||
future::{self, join, Shared},
|
||||
FutureExt,
|
||||
|
@ -83,7 +84,7 @@ use git::blame::GitBlame;
|
|||
use gpui::{
|
||||
div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, Action, Animation,
|
||||
AnimationExt, AnyElement, App, AppContext, AsyncWindowContext, AvailableSpace, Background,
|
||||
Bounds, ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity,
|
||||
Bounds, ClickEvent, ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity,
|
||||
EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight,
|
||||
Global, HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad,
|
||||
ParentElement, Pixels, Render, SharedString, Size, Stateful, Styled, StyledText, Subscription,
|
||||
|
@ -91,6 +92,7 @@ use gpui::{
|
|||
WeakEntity, WeakFocusHandle, Window,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
use indent_guides::ActiveIndentGuidesState;
|
||||
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
||||
|
@ -112,6 +114,11 @@ use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange}
|
|||
use linked_editing_ranges::refresh_linked_ranges;
|
||||
use mouse_context_menu::MouseContextMenu;
|
||||
use persistence::DB;
|
||||
use project::{
|
||||
debugger::breakpoint_store::{BreakpointEditAction, BreakpointStore, BreakpointStoreEvent},
|
||||
ProjectPath,
|
||||
};
|
||||
|
||||
pub use proposed_changes_editor::{
|
||||
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
|
||||
};
|
||||
|
@ -119,7 +126,6 @@ use smallvec::smallvec;
|
|||
use std::iter::Peekable;
|
||||
use task::{ResolvedTask, TaskTemplate, TaskVariables};
|
||||
|
||||
use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
|
||||
pub use lsp::CompletionContext;
|
||||
use lsp::{
|
||||
CodeActionKind, CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity,
|
||||
|
@ -136,7 +142,9 @@ use multi_buffer::{
|
|||
ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow,
|
||||
MultiOrSingleBufferOffsetRange, ToOffsetUtf16,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use project::{
|
||||
debugger::breakpoint_store::{Breakpoint, BreakpointKind},
|
||||
lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
|
||||
project_settings::{GitGutterSetting, ProjectSettings},
|
||||
CodeAction, Completion, CompletionIntent, CompletionSource, DocumentHighlight, InlayHint,
|
||||
|
@ -153,6 +161,7 @@ use serde::{Deserialize, Serialize};
|
|||
use settings::{update_settings_file, Settings, SettingsLocation, SettingsStore};
|
||||
use smallvec::SmallVec;
|
||||
use snippet::Snippet;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
borrow::Cow,
|
||||
|
@ -163,7 +172,6 @@ use std::{
|
|||
ops::{ControlFlow, Deref, DerefMut, Not as _, Range, RangeInclusive},
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
pub use sum_tree::Bias;
|
||||
|
@ -236,6 +244,7 @@ impl InlayId {
|
|||
}
|
||||
}
|
||||
|
||||
pub enum DebugCurrentRowHighlight {}
|
||||
enum DocumentHighlightRead {}
|
||||
enum DocumentHighlightWrite {}
|
||||
enum InputComposition {}
|
||||
|
@ -589,6 +598,7 @@ struct ResolvedTasks {
|
|||
templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
|
||||
position: Anchor,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
|
||||
struct BufferOffset(usize);
|
||||
|
||||
|
@ -659,6 +669,7 @@ pub struct Editor {
|
|||
show_git_diff_gutter: Option<bool>,
|
||||
show_code_actions: Option<bool>,
|
||||
show_runnables: Option<bool>,
|
||||
show_breakpoints: Option<bool>,
|
||||
show_wrap_guides: Option<bool>,
|
||||
show_indent_guides: Option<bool>,
|
||||
placeholder_text: Option<Arc<str>>,
|
||||
|
@ -749,6 +760,11 @@ pub struct Editor {
|
|||
expect_bounds_change: Option<Bounds<Pixels>>,
|
||||
tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
|
||||
tasks_update_task: Option<Task<()>>,
|
||||
pub breakpoint_store: Option<Entity<BreakpointStore>>,
|
||||
/// Allow's a user to create a breakpoint by selecting this indicator
|
||||
/// It should be None while a user is not hovering over the gutter
|
||||
/// Otherwise it represents the point that the breakpoint will be shown
|
||||
pub gutter_breakpoint_indicator: Option<DisplayPoint>,
|
||||
in_project_search: bool,
|
||||
previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
|
||||
breadcrumb_header: Option<String>,
|
||||
|
@ -789,6 +805,7 @@ pub struct EditorSnapshot {
|
|||
show_git_diff_gutter: Option<bool>,
|
||||
show_code_actions: Option<bool>,
|
||||
show_runnables: Option<bool>,
|
||||
show_breakpoints: Option<bool>,
|
||||
git_blame_gutter_max_author_length: Option<usize>,
|
||||
pub display_snapshot: DisplaySnapshot,
|
||||
pub placeholder_text: Option<Arc<str>>,
|
||||
|
@ -1279,7 +1296,18 @@ impl Editor {
|
|||
editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));
|
||||
},
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
project_subscriptions.push(cx.subscribe_in(
|
||||
&project.read(cx).breakpoint_store(),
|
||||
window,
|
||||
|editor, _, event, window, cx| match event {
|
||||
BreakpointStoreEvent::ActiveDebugLineChanged => {
|
||||
editor.go_to_active_debug_line(window, cx);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1303,6 +1331,11 @@ impl Editor {
|
|||
None
|
||||
};
|
||||
|
||||
let breakpoint_store = match (mode, project.as_ref()) {
|
||||
(EditorMode::Full, Some(project)) => Some(project.read(cx).breakpoint_store()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let mut code_action_providers = Vec::new();
|
||||
let mut load_uncommitted_diff = None;
|
||||
if let Some(project) = project.clone() {
|
||||
|
@ -1356,6 +1389,7 @@ impl Editor {
|
|||
show_git_diff_gutter: None,
|
||||
show_code_actions: None,
|
||||
show_runnables: None,
|
||||
show_breakpoints: None,
|
||||
show_wrap_guides: None,
|
||||
show_indent_guides,
|
||||
placeholder_text: None,
|
||||
|
@ -1440,6 +1474,9 @@ impl Editor {
|
|||
blame: None,
|
||||
blame_subscription: None,
|
||||
tasks: Default::default(),
|
||||
|
||||
breakpoint_store,
|
||||
gutter_breakpoint_indicator: None,
|
||||
_subscriptions: vec![
|
||||
cx.observe(&buffer, Self::on_buffer_changed),
|
||||
cx.subscribe_in(&buffer, window, Self::on_buffer_event),
|
||||
|
@ -1474,6 +1511,12 @@ impl Editor {
|
|||
text_style_refinement: None,
|
||||
load_diff_task: load_uncommitted_diff,
|
||||
};
|
||||
if let Some(breakpoints) = this.breakpoint_store.as_ref() {
|
||||
this._subscriptions
|
||||
.push(cx.observe(breakpoints, |_, _, cx| {
|
||||
cx.notify();
|
||||
}));
|
||||
}
|
||||
this.tasks_update_task = Some(this.refresh_runnables(window, cx));
|
||||
this._subscriptions.extend(project_subscriptions);
|
||||
|
||||
|
@ -1490,6 +1533,8 @@ impl Editor {
|
|||
this.start_git_blame_inline(false, window, cx);
|
||||
}
|
||||
|
||||
this.go_to_active_debug_line(window, cx);
|
||||
|
||||
if let Some(buffer) = buffer.read(cx).as_singleton() {
|
||||
if let Some(project) = this.project.as_ref() {
|
||||
let handle = project.update(cx, |project, cx| {
|
||||
|
@ -1764,6 +1809,7 @@ impl Editor {
|
|||
show_git_diff_gutter: self.show_git_diff_gutter,
|
||||
show_code_actions: self.show_code_actions,
|
||||
show_runnables: self.show_runnables,
|
||||
show_breakpoints: self.show_breakpoints,
|
||||
git_blame_gutter_max_author_length,
|
||||
display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
|
||||
scroll_anchor: self.scroll_manager.anchor(),
|
||||
|
@ -5857,14 +5903,28 @@ impl Editor {
|
|||
_style: &EditorStyle,
|
||||
row: DisplayRow,
|
||||
is_active: bool,
|
||||
breakpoint: Option<&(Anchor, Breakpoint)>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<IconButton> {
|
||||
let color = if breakpoint.is_some() {
|
||||
Color::Debugger
|
||||
} else {
|
||||
Color::Muted
|
||||
};
|
||||
|
||||
let position = breakpoint.as_ref().map(|(anchor, _)| *anchor);
|
||||
let bp_kind = Arc::new(
|
||||
breakpoint
|
||||
.map(|(_, bp)| bp.kind.clone())
|
||||
.unwrap_or(BreakpointKind::Standard),
|
||||
);
|
||||
|
||||
if self.available_code_actions.is_some() {
|
||||
Some(
|
||||
IconButton::new("code_actions_indicator", ui::IconName::Bolt)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_color(color)
|
||||
.toggle_state(is_active)
|
||||
.tooltip({
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
@ -5889,6 +5949,16 @@ impl Editor {
|
|||
window,
|
||||
cx,
|
||||
);
|
||||
}))
|
||||
.on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
|
||||
editor.set_breakpoint_context_menu(
|
||||
row,
|
||||
position,
|
||||
bp_kind.clone(),
|
||||
event.down.position,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
|
@ -5907,6 +5977,198 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get all display points of breakpoints that will be rendered within editor
|
||||
///
|
||||
/// This function is used to handle overlaps between breakpoints and Code action/runner symbol.
|
||||
/// It's also used to set the color of line numbers with breakpoints to the breakpoint color.
|
||||
/// TODO debugger: Use this function to color toggle symbols that house nested breakpoints
|
||||
fn active_breakpoints(
|
||||
&mut self,
|
||||
range: Range<DisplayRow>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> HashMap<DisplayRow, (Anchor, Breakpoint)> {
|
||||
let mut breakpoint_display_points = HashMap::default();
|
||||
|
||||
let Some(breakpoint_store) = self.breakpoint_store.clone() else {
|
||||
return breakpoint_display_points;
|
||||
};
|
||||
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
|
||||
let multi_buffer_snapshot = &snapshot.display_snapshot.buffer_snapshot;
|
||||
let Some(project) = self.project.as_ref() else {
|
||||
return breakpoint_display_points;
|
||||
};
|
||||
|
||||
if let Some(buffer) = self.buffer.read(cx).as_singleton() {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
for breakpoint in
|
||||
breakpoint_store
|
||||
.read(cx)
|
||||
.breakpoints(&buffer, None, buffer_snapshot.clone(), cx)
|
||||
{
|
||||
let point = buffer_snapshot.summary_for_anchor::<Point>(&breakpoint.0);
|
||||
let anchor = multi_buffer_snapshot.anchor_before(point);
|
||||
breakpoint_display_points.insert(
|
||||
snapshot
|
||||
.point_to_display_point(
|
||||
MultiBufferPoint {
|
||||
row: point.row,
|
||||
column: point.column,
|
||||
},
|
||||
Bias::Left,
|
||||
)
|
||||
.row(),
|
||||
(anchor, breakpoint.1.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
return breakpoint_display_points;
|
||||
}
|
||||
|
||||
let range = snapshot.display_point_to_point(DisplayPoint::new(range.start, 0), Bias::Left)
|
||||
..snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right);
|
||||
for excerpt_boundary in multi_buffer_snapshot.excerpt_boundaries_in_range(range) {
|
||||
let info = excerpt_boundary.next;
|
||||
|
||||
let Some(excerpt_ranges) = multi_buffer_snapshot.range_for_excerpt(info.id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(buffer) =
|
||||
project.read_with(cx, |this, cx| this.buffer_for_id(info.buffer_id, cx))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let breakpoints = breakpoint_store.read(cx).breakpoints(
|
||||
&buffer,
|
||||
Some(info.range.context.start..info.range.context.end),
|
||||
info.buffer.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
// To translate a breakpoint's position within a singular buffer to a multi buffer
|
||||
// position we need to know it's excerpt starting location, it's position within
|
||||
// the singular buffer, and if that position is within the excerpt's range.
|
||||
let excerpt_head = excerpt_ranges
|
||||
.start
|
||||
.to_display_point(&snapshot.display_snapshot);
|
||||
|
||||
let buffer_start = info
|
||||
.buffer
|
||||
.summary_for_anchor::<Point>(&info.range.context.start);
|
||||
|
||||
for (anchor, breakpoint) in breakpoints {
|
||||
let as_row = info.buffer.summary_for_anchor::<Point>(&anchor).row;
|
||||
let delta = as_row - buffer_start.row;
|
||||
|
||||
let position = excerpt_head + DisplayPoint::new(DisplayRow(delta), 0);
|
||||
|
||||
let anchor = snapshot.display_point_to_anchor(position, Bias::Left);
|
||||
|
||||
breakpoint_display_points.insert(position.row(), (anchor, breakpoint.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
breakpoint_display_points
|
||||
}
|
||||
|
||||
fn breakpoint_context_menu(
|
||||
&self,
|
||||
anchor: Anchor,
|
||||
kind: Arc<BreakpointKind>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ui::ContextMenu> {
|
||||
let weak_editor = cx.weak_entity();
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
let second_entry_msg = if kind.log_message().is_some() {
|
||||
"Edit Log Breakpoint"
|
||||
} else {
|
||||
"Add Log Breakpoint"
|
||||
};
|
||||
|
||||
ui::ContextMenu::build(window, cx, |menu, _, _cx| {
|
||||
menu.on_blur_subscription(Subscription::new(|| {}))
|
||||
.context(focus_handle)
|
||||
.entry("Toggle Breakpoint", None, {
|
||||
let weak_editor = weak_editor.clone();
|
||||
move |_window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
BreakpointKind::Standard,
|
||||
BreakpointEditAction::Toggle,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.entry(second_entry_msg, None, move |window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.add_edit_breakpoint_block(anchor, kind.as_ref(), window, cx);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn render_breakpoint(
|
||||
&self,
|
||||
position: Anchor,
|
||||
row: DisplayRow,
|
||||
kind: &BreakpointKind,
|
||||
cx: &mut Context<Self>,
|
||||
) -> IconButton {
|
||||
let color = if self
|
||||
.gutter_breakpoint_indicator
|
||||
.is_some_and(|gutter_bp| gutter_bp.row() == row)
|
||||
{
|
||||
Color::Hint
|
||||
} else {
|
||||
Color::Debugger
|
||||
};
|
||||
|
||||
let icon = match &kind {
|
||||
BreakpointKind::Standard => ui::IconName::DebugBreakpoint,
|
||||
BreakpointKind::Log(_) => ui::IconName::DebugLogBreakpoint,
|
||||
};
|
||||
let arc_kind = Arc::new(kind.clone());
|
||||
let arc_kind2 = arc_kind.clone();
|
||||
|
||||
IconButton::new(("breakpoint_indicator", row.0 as usize), icon)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.size(ui::ButtonSize::None)
|
||||
.icon_color(color)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.on_click(cx.listener(move |editor, _e, window, cx| {
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.edit_breakpoint_at_anchor(
|
||||
position,
|
||||
arc_kind.as_ref().clone(),
|
||||
BreakpointEditAction::Toggle,
|
||||
cx,
|
||||
);
|
||||
}))
|
||||
.on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
|
||||
editor.set_breakpoint_context_menu(
|
||||
row,
|
||||
Some(position),
|
||||
arc_kind2.clone(),
|
||||
event.down.position,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}))
|
||||
}
|
||||
|
||||
fn build_tasks_context(
|
||||
project: &Entity<Project>,
|
||||
buffer: &Entity<Buffer>,
|
||||
|
@ -6043,12 +6305,26 @@ impl Editor {
|
|||
_style: &EditorStyle,
|
||||
is_active: bool,
|
||||
row: DisplayRow,
|
||||
breakpoint: Option<(Anchor, Breakpoint)>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> IconButton {
|
||||
let color = if breakpoint.is_some() {
|
||||
Color::Debugger
|
||||
} else {
|
||||
Color::Muted
|
||||
};
|
||||
|
||||
let position = breakpoint.as_ref().map(|(anchor, _)| *anchor);
|
||||
let bp_kind = Arc::new(
|
||||
breakpoint
|
||||
.map(|(_, bp)| bp.kind)
|
||||
.unwrap_or(BreakpointKind::Standard),
|
||||
);
|
||||
|
||||
IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_color(color)
|
||||
.toggle_state(is_active)
|
||||
.on_click(cx.listener(move |editor, _e, window, cx| {
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
|
@ -6060,6 +6336,16 @@ impl Editor {
|
|||
cx,
|
||||
);
|
||||
}))
|
||||
.on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
|
||||
editor.set_breakpoint_context_menu(
|
||||
row,
|
||||
position,
|
||||
bp_kind.clone(),
|
||||
event.down.position,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn context_menu_visible(&self) -> bool {
|
||||
|
@ -8032,6 +8318,235 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
|
||||
fn set_breakpoint_context_menu(
|
||||
&mut self,
|
||||
row: DisplayRow,
|
||||
position: Option<Anchor>,
|
||||
kind: Arc<BreakpointKind>,
|
||||
clicked_point: gpui::Point<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if !cx.has_flag::<Debugger>() {
|
||||
return;
|
||||
}
|
||||
let source = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.snapshot(cx)
|
||||
.anchor_before(Point::new(row.0, 0u32));
|
||||
|
||||
let context_menu =
|
||||
self.breakpoint_context_menu(position.unwrap_or(source), kind, window, cx);
|
||||
|
||||
self.mouse_context_menu = MouseContextMenu::pinned_to_editor(
|
||||
self,
|
||||
source,
|
||||
clicked_point,
|
||||
context_menu,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn add_edit_breakpoint_block(
|
||||
&mut self,
|
||||
anchor: Anchor,
|
||||
kind: &BreakpointKind,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let weak_editor = cx.weak_entity();
|
||||
let bp_prompt =
|
||||
cx.new(|cx| BreakpointPromptEditor::new(weak_editor, anchor, kind.clone(), window, cx));
|
||||
|
||||
let height = bp_prompt.update(cx, |this, cx| {
|
||||
this.prompt
|
||||
.update(cx, |prompt, cx| prompt.max_point(cx).row().0 + 1 + 2)
|
||||
});
|
||||
let cloned_prompt = bp_prompt.clone();
|
||||
let blocks = vec![BlockProperties {
|
||||
style: BlockStyle::Sticky,
|
||||
placement: BlockPlacement::Above(anchor),
|
||||
height,
|
||||
render: Arc::new(move |cx| {
|
||||
*cloned_prompt.read(cx).gutter_dimensions.lock() = *cx.gutter_dimensions;
|
||||
cloned_prompt.clone().into_any_element()
|
||||
}),
|
||||
priority: 0,
|
||||
}];
|
||||
|
||||
let focus_handle = bp_prompt.focus_handle(cx);
|
||||
window.focus(&focus_handle);
|
||||
|
||||
let block_ids = self.insert_blocks(blocks, None, cx);
|
||||
bp_prompt.update(cx, |prompt, _| {
|
||||
prompt.add_block_ids(block_ids);
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn breakpoint_at_cursor_head(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<(Anchor, Breakpoint)> {
|
||||
let cursor_position: Point = self.selections.newest(cx).head();
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
// We Set the column position to zero so this function interacts correctly
|
||||
// between calls by clicking on the gutter & using an action to toggle a
|
||||
// breakpoint. Otherwise, toggling a breakpoint through an action wouldn't
|
||||
// untoggle a breakpoint that was added through clicking on the gutter
|
||||
let cursor_position = snapshot
|
||||
.display_snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(Point::new(cursor_position.row, 0));
|
||||
|
||||
let project = self.project.clone();
|
||||
|
||||
let buffer_id = cursor_position.text_anchor.buffer_id?;
|
||||
let enclosing_excerpt = snapshot
|
||||
.buffer_snapshot
|
||||
.excerpt_ids_for_range(cursor_position..cursor_position)
|
||||
.next()?;
|
||||
let buffer = project?.read_with(cx, |project, cx| project.buffer_for_id(buffer_id, cx))?;
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
let row = buffer_snapshot
|
||||
.summary_for_anchor::<text::PointUtf16>(&cursor_position.text_anchor)
|
||||
.row;
|
||||
|
||||
let bp = self
|
||||
.breakpoint_store
|
||||
.as_ref()?
|
||||
.read_with(cx, |breakpoint_store, cx| {
|
||||
breakpoint_store
|
||||
.breakpoints(
|
||||
&buffer,
|
||||
Some(cursor_position.text_anchor..(text::Anchor::MAX)),
|
||||
buffer_snapshot.clone(),
|
||||
cx,
|
||||
)
|
||||
.next()
|
||||
.and_then(move |(anchor, bp)| {
|
||||
let breakpoint_row = buffer_snapshot
|
||||
.summary_for_anchor::<text::PointUtf16>(anchor)
|
||||
.row;
|
||||
|
||||
if breakpoint_row == row {
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(enclosing_excerpt, *anchor)
|
||||
.map(|anchor| (anchor, bp.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
bp
|
||||
}
|
||||
|
||||
pub fn edit_log_breakpoint(
|
||||
&mut self,
|
||||
_: &EditLogBreakpoint,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let (anchor, bp) = self
|
||||
.breakpoint_at_cursor_head(window, cx)
|
||||
.unwrap_or_else(|| {
|
||||
let cursor_position: Point = self.selections.newest(cx).head();
|
||||
|
||||
let breakpoint_position = self
|
||||
.snapshot(window, cx)
|
||||
.display_snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(Point::new(cursor_position.row, 0));
|
||||
|
||||
(
|
||||
breakpoint_position,
|
||||
Breakpoint {
|
||||
kind: BreakpointKind::Standard,
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
self.add_edit_breakpoint_block(anchor, &bp.kind, window, cx);
|
||||
}
|
||||
|
||||
pub fn toggle_breakpoint(
|
||||
&mut self,
|
||||
_: &crate::actions::ToggleBreakpoint,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let edit_action = BreakpointEditAction::Toggle;
|
||||
|
||||
if let Some((anchor, breakpoint)) = self.breakpoint_at_cursor_head(window, cx) {
|
||||
self.edit_breakpoint_at_anchor(anchor, breakpoint.kind, edit_action, cx);
|
||||
} else {
|
||||
let cursor_position: Point = self.selections.newest(cx).head();
|
||||
|
||||
let breakpoint_position = self
|
||||
.snapshot(window, cx)
|
||||
.display_snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(Point::new(cursor_position.row, 0));
|
||||
|
||||
self.edit_breakpoint_at_anchor(
|
||||
breakpoint_position,
|
||||
BreakpointKind::Standard,
|
||||
edit_action,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn edit_breakpoint_at_anchor(
|
||||
&mut self,
|
||||
breakpoint_position: Anchor,
|
||||
kind: BreakpointKind,
|
||||
edit_action: BreakpointEditAction,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(breakpoint_store) = &self.breakpoint_store else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(buffer_id) = breakpoint_position.buffer_id.or_else(|| {
|
||||
if breakpoint_position == Anchor::min() {
|
||||
self.buffer()
|
||||
.read(cx)
|
||||
.excerpt_buffer_ids()
|
||||
.into_iter()
|
||||
.next()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
breakpoint_store.update(cx, |breakpoint_store, cx| {
|
||||
breakpoint_store.toggle_breakpoint(
|
||||
buffer,
|
||||
(breakpoint_position.text_anchor, Breakpoint { kind }),
|
||||
edit_action,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn breakpoint_store(&self) -> Option<Entity<BreakpointStore>> {
|
||||
self.breakpoint_store.clone()
|
||||
}
|
||||
|
||||
pub fn prepare_restore_change(
|
||||
&self,
|
||||
revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
|
||||
|
@ -11866,6 +12381,33 @@ impl Editor {
|
|||
.or_else(|| snapshot.buffer_snapshot.diff_hunk_before(Point::MAX))
|
||||
}
|
||||
|
||||
fn go_to_line<T: 'static>(
|
||||
&mut self,
|
||||
position: Anchor,
|
||||
highlight_color: Option<Hsla>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let snapshot = self.snapshot(window, cx).display_snapshot;
|
||||
let position = position.to_point(&snapshot.buffer_snapshot);
|
||||
let start = snapshot
|
||||
.buffer_snapshot
|
||||
.clip_point(Point::new(position.row, 0), Bias::Left);
|
||||
let end = start + Point::new(1, 0);
|
||||
let start = snapshot.buffer_snapshot.anchor_before(start);
|
||||
let end = snapshot.buffer_snapshot.anchor_before(end);
|
||||
|
||||
self.clear_row_highlights::<T>();
|
||||
self.highlight_rows::<T>(
|
||||
start..end,
|
||||
highlight_color
|
||||
.unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background),
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
self.request_autoscroll(Autoscroll::center(), cx);
|
||||
}
|
||||
|
||||
pub fn go_to_definition(
|
||||
&mut self,
|
||||
_: &GoToDefinition,
|
||||
|
@ -14532,6 +15074,11 @@ impl Editor {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_show_breakpoints(&mut self, show_breakpoints: bool, cx: &mut Context<Self>) {
|
||||
self.show_breakpoints = Some(show_breakpoints);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_masked(&mut self, masked: bool, cx: &mut Context<Self>) {
|
||||
if self.display_map.read(cx).masked != masked {
|
||||
self.display_map.update(cx, |map, _| map.masked = masked);
|
||||
|
@ -14634,6 +15181,61 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn project_path(&self, cx: &mut Context<Self>) -> Option<ProjectPath> {
|
||||
if let Some(buffer) = self.buffer.read(cx).as_singleton() {
|
||||
buffer.read_with(cx, |buffer, cx| buffer.project_path(cx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn go_to_active_debug_line(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let _ = maybe!({
|
||||
let breakpoint_store = self.breakpoint_store.as_ref()?;
|
||||
|
||||
let Some((_, _, active_position)) =
|
||||
breakpoint_store.read(cx).active_position().cloned()
|
||||
else {
|
||||
self.clear_row_highlights::<DebugCurrentRowHighlight>();
|
||||
return None;
|
||||
};
|
||||
|
||||
let snapshot = self
|
||||
.project
|
||||
.as_ref()?
|
||||
.read(cx)
|
||||
.buffer_for_id(active_position.buffer_id?, cx)?
|
||||
.read(cx)
|
||||
.snapshot();
|
||||
|
||||
for (id, ExcerptRange { context, .. }) in self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.excerpts_for_buffer(active_position.buffer_id?, cx)
|
||||
{
|
||||
if context.start.cmp(&active_position, &snapshot).is_ge()
|
||||
|| context.end.cmp(&active_position, &snapshot).is_lt()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let multibuffer_anchor = snapshot.anchor_in_excerpt(id, active_position)?;
|
||||
|
||||
self.clear_row_highlights::<DebugCurrentRowHighlight>();
|
||||
self.go_to_line::<DebugCurrentRowHighlight>(
|
||||
multibuffer_anchor,
|
||||
Some(cx.theme().colors().editor_debugger_active_line_background),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn copy_file_name_without_extension(
|
||||
&mut self,
|
||||
_: &CopyFileNameWithoutExtension,
|
||||
|
@ -17645,6 +18247,7 @@ impl EditorSnapshot {
|
|||
.unwrap_or(gutter_settings.code_actions);
|
||||
|
||||
let show_runnables = self.show_runnables.unwrap_or(gutter_settings.runnables);
|
||||
let show_breakpoints = self.show_breakpoints.unwrap_or(gutter_settings.breakpoints);
|
||||
|
||||
let git_blame_entries_width =
|
||||
self.git_blame_gutter_max_author_length
|
||||
|
@ -17668,7 +18271,7 @@ impl EditorSnapshot {
|
|||
let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO);
|
||||
left_padding += if !is_singleton {
|
||||
em_width * 4.0
|
||||
} else if show_code_actions || show_runnables {
|
||||
} else if show_code_actions || show_runnables || show_breakpoints {
|
||||
em_width * 3.0
|
||||
} else if show_git_gutter && show_line_numbers {
|
||||
em_width * 2.0
|
||||
|
@ -18668,6 +19271,157 @@ impl Global for KillRing {}
|
|||
|
||||
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
|
||||
struct BreakpointPromptEditor {
|
||||
pub(crate) prompt: Entity<Editor>,
|
||||
editor: WeakEntity<Editor>,
|
||||
breakpoint_anchor: Anchor,
|
||||
kind: BreakpointKind,
|
||||
block_ids: HashSet<CustomBlockId>,
|
||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl BreakpointPromptEditor {
|
||||
const MAX_LINES: u8 = 4;
|
||||
|
||||
fn new(
|
||||
editor: WeakEntity<Editor>,
|
||||
breakpoint_anchor: Anchor,
|
||||
kind: BreakpointKind,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let buffer = cx.new(|cx| {
|
||||
Buffer::local(
|
||||
kind.log_message()
|
||||
.map(|msg| msg.to_string())
|
||||
.unwrap_or_default(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
|
||||
let prompt = cx.new(|cx| {
|
||||
let mut prompt = Editor::new(
|
||||
EditorMode::AutoHeight {
|
||||
max_lines: Self::MAX_LINES as usize,
|
||||
},
|
||||
buffer,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
prompt.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
|
||||
prompt.set_show_cursor_when_unfocused(false, cx);
|
||||
prompt.set_placeholder_text(
|
||||
"Message to log when breakpoint is hit. Expressions within {} are interpolated.",
|
||||
cx,
|
||||
);
|
||||
|
||||
prompt
|
||||
});
|
||||
|
||||
Self {
|
||||
prompt,
|
||||
editor,
|
||||
breakpoint_anchor,
|
||||
kind,
|
||||
gutter_dimensions: Arc::new(Mutex::new(GutterDimensions::default())),
|
||||
block_ids: Default::default(),
|
||||
_subscriptions: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add_block_ids(&mut self, block_ids: Vec<CustomBlockId>) {
|
||||
self.block_ids.extend(block_ids)
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(editor) = self.editor.upgrade() {
|
||||
let log_message = self
|
||||
.prompt
|
||||
.read(cx)
|
||||
.buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.expect("A multi buffer in breakpoint prompt isn't possible")
|
||||
.read(cx)
|
||||
.as_rope()
|
||||
.to_string();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.edit_breakpoint_at_anchor(
|
||||
self.breakpoint_anchor,
|
||||
self.kind.clone(),
|
||||
BreakpointEditAction::EditLogMessage(log_message.into()),
|
||||
cx,
|
||||
);
|
||||
|
||||
editor.remove_blocks(self.block_ids.clone(), None, cx);
|
||||
cx.focus_self(window);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.remove_blocks(self.block_ids.clone(), None, cx);
|
||||
window.focus(&editor.focus_handle);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn render_prompt_editor(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: if self.prompt.read(cx).read_only(cx) {
|
||||
cx.theme().colors().text_disabled
|
||||
} else {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_fallbacks: settings.buffer_font.fallbacks.clone(),
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
..Default::default()
|
||||
};
|
||||
EditorElement::new(
|
||||
&self.prompt,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for BreakpointPromptEditor {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let gutter_dimensions = *self.gutter_dimensions.lock();
|
||||
h_flex()
|
||||
.key_context("Editor")
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_y_1()
|
||||
.border_color(cx.theme().status().info_border)
|
||||
.size_full()
|
||||
.py(window.line_height() / 2.5)
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.child(h_flex().w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0)))
|
||||
.child(div().flex_1().child(self.render_prompt_editor(cx)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for BreakpointPromptEditor {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.prompt.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn all_edits_insertions_or_deletions(
|
||||
edits: &Vec<(Range<Anchor>, String)>,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue