diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6e1d3fc443..bd55083bd7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -815,6 +815,16 @@ struct InlineBlamePopover { popover_state: InlineBlamePopoverState, } +/// Represents a breakpoint indicator that shows up when hovering over lines in the gutter that don't have +/// a breakpoint on them. +#[derive(Clone, Copy, Debug)] +struct PhantomBreakpointIndicator { + display_row: DisplayRow, + /// There's a small debounce between hovering over the line and showing the indicator. + /// We don't want to show the indicator when moving the mouse from editor to e.g. project panel. + is_active: bool, + collides_with_existing_breakpoint: bool, +} /// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`]. /// /// See the [module level documentation](self) for more information. @@ -963,10 +973,7 @@ pub struct Editor { tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, tasks_update_task: Option>, breakpoint_store: Option>, - /// 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 - gutter_breakpoint_indicator: (Option<(DisplayPoint, bool)>, Option>), + gutter_breakpoint_indicator: (Option, Option>), in_project_search: bool, previous_search_ranges: Option]>>, breadcrumb_header: Option, @@ -6965,6 +6972,21 @@ impl Editor { breakpoint: &Breakpoint, cx: &mut Context, ) -> IconButton { + // Is it a breakpoint that shows up when hovering over gutter? + let (is_phantom, collides_with_existing) = self.gutter_breakpoint_indicator.0.map_or( + (false, false), + |PhantomBreakpointIndicator { + is_active, + display_row, + collides_with_existing_breakpoint, + }| { + ( + is_active && display_row == row, + collides_with_existing_breakpoint, + ) + }, + ); + let (color, icon) = { let icon = match (&breakpoint.message.is_some(), breakpoint.is_disabled()) { (false, false) => ui::IconName::DebugBreakpoint, @@ -6973,11 +6995,7 @@ impl Editor { (true, true) => ui::IconName::DebugDisabledLogBreakpoint, }; - let color = if self - .gutter_breakpoint_indicator - .0 - .is_some_and(|(point, is_visible)| is_visible && point.row() == row) - { + let color = if is_phantom { Color::Hint } else { Color::Debugger @@ -6988,6 +7006,24 @@ impl Editor { let breakpoint = Arc::from(breakpoint.clone()); + let alt_as_text = gpui::Keystroke { + modifiers: Modifiers::secondary_key(), + ..Default::default() + }; + let primary_action_text = if breakpoint.is_disabled() { + "enable" + } else if is_phantom && !collides_with_existing { + "set" + } else { + "unset" + }; + let mut primary_text = format!("Click to {primary_action_text}"); + if collides_with_existing && !breakpoint.is_disabled() { + use std::fmt::Write; + write!(primary_text, ", {alt_as_text}-click to disable").ok(); + } + let primary_text = SharedString::from(primary_text); + let focus_handle = self.focus_handle.clone(); IconButton::new(("breakpoint_indicator", row.0 as usize), icon) .icon_size(IconSize::XSmall) .size(ui::ButtonSize::None) @@ -7021,6 +7057,16 @@ impl Editor { cx, ); })) + .tooltip(move |window, cx| { + Tooltip::with_meta_in( + primary_text.clone(), + None, + "Right-click for more options", + &focus_handle, + window, + cx, + ) + }) } fn build_tasks_context( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 0ecbdab07c..78b5adff5f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -7,8 +7,8 @@ use crate::{ FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, - PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, - SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, + PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase, + SelectedTextHighlight, Selection, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP}, display_map::{ Block, BlockContext, BlockStyle, DisplaySnapshot, FoldId, HighlightedChunk, ToDisplayPoint, @@ -59,6 +59,7 @@ use multi_buffer::{ MultiBufferRow, RowInfo, }; use project::{ + ProjectPath, debugger::breakpoint_store::Breakpoint, project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings}, }; @@ -946,18 +947,45 @@ impl EditorElement { .snapshot .display_point_to_anchor(new_point, Bias::Left); - if position_map + if let Some((buffer_snapshot, file)) = position_map .snapshot .buffer_snapshot .buffer_for_excerpt(buffer_anchor.excerpt_id) - .is_some_and(|buffer| buffer.file().is_some()) + .and_then(|buffer| buffer.file().map(|file| (buffer, file))) { let was_hovered = editor.gutter_breakpoint_indicator.0.is_some(); + let as_point = text::ToPoint::to_point(&buffer_anchor.text_anchor, buffer_snapshot); + let is_visible = editor .gutter_breakpoint_indicator .0 - .map_or(false, |(_, is_active)| is_active); - editor.gutter_breakpoint_indicator.0 = Some((new_point, is_visible)); + .map_or(false, |indicator| indicator.is_active); + + let has_existing_breakpoint = + editor.breakpoint_store.as_ref().map_or(false, |store| { + let Some(project) = &editor.project else { + return false; + }; + let Some(abs_path) = project.read(cx).absolute_path( + &ProjectPath { + path: file.path().clone(), + worktree_id: file.worktree_id(cx), + }, + cx, + ) else { + return false; + }; + store + .read(cx) + .breakpoint_at_row(&abs_path, as_point.row, cx) + .is_some() + }); + + editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator { + display_row: new_point.row(), + is_active: is_visible, + collides_with_existing_breakpoint: has_existing_breakpoint, + }); editor.gutter_breakpoint_indicator.1.get_or_insert_with(|| { cx.spawn(async move |this, cx| { @@ -968,10 +996,8 @@ impl EditorElement { } this.update(cx, |this, cx| { - if let Some((_, is_active)) = - this.gutter_breakpoint_indicator.0.as_mut() - { - *is_active = true; + if let Some(indicator) = this.gutter_breakpoint_indicator.0.as_mut() { + indicator.is_active = true; } cx.notify(); @@ -7063,23 +7089,28 @@ impl Element for EditorElement { // line numbers so we don't paint a line number debug accent color if a user // has their mouse over that line when a breakpoint isn't there if cx.has_flag::() { - let gutter_breakpoint_indicator = - self.editor.read(cx).gutter_breakpoint_indicator.0; - if let Some((gutter_breakpoint_point, _)) = - gutter_breakpoint_indicator.filter(|(_, is_active)| *is_active) - { - breakpoint_rows - .entry(gutter_breakpoint_point.row()) - .or_insert_with(|| { - let position = snapshot.display_point_to_anchor( - gutter_breakpoint_point, - Bias::Right, - ); - let breakpoint = Breakpoint::new_standard(); - - (position, breakpoint) - }); - } + self.editor.update(cx, |editor, _| { + if let Some(phantom_breakpoint) = &mut editor + .gutter_breakpoint_indicator + .0 + .filter(|phantom_breakpoint| phantom_breakpoint.is_active) + { + // Is there a non-phantom breakpoint on this line? + phantom_breakpoint.collides_with_existing_breakpoint = true; + breakpoint_rows + .entry(phantom_breakpoint.display_row) + .or_insert_with(|| { + let position = snapshot.display_point_to_anchor( + DisplayPoint::new(phantom_breakpoint.display_row, 0), + Bias::Right, + ); + let breakpoint = Breakpoint::new_standard(); + phantom_breakpoint.collides_with_existing_breakpoint = + false; + (position, breakpoint) + }); + } + }) } let mut expand_toggles =