Revert "editor: Bookmarks MVP" to update its description (#54163)

Yara 🏳️‍⚧️ created

Reverts zed-industries/zed#51404 because I forgot to updated the
squashed commits description ....

Change summary

assets/icons/bookmark.svg                              |   4 
assets/settings/default.json                           |   2 
codebook.toml                                          |   1 
crates/agent_ui/src/entry_view_state.rs                |   1 
crates/collab_ui/src/channel_view.rs                   |   3 
crates/debugger_tools/src/dap_log.rs                   |   1 
crates/debugger_ui/src/session/running/console.rs      |   1 
crates/edit_prediction_ui/src/rate_prediction_modal.rs |   1 
crates/editor/src/actions.rs                           |   8 
crates/editor/src/bookmarks.rs                         | 243 ---
crates/editor/src/editor.rs                            | 475 +------
crates/editor/src/editor_settings.rs                   |   2 
crates/editor/src/editor_tests.rs                      | 616 +--------
crates/editor/src/element.rs                           | 745 +++++------
crates/editor/src/runnables.rs                         |  11 
crates/git_ui/src/commit_view.rs                       |   1 
crates/gpui/src/window.rs                              |  11 
crates/icons/src/icons.rs                              |   1 
crates/inspector_ui/src/div_inspector.rs               |   1 
crates/language_tools/src/lsp_log_view.rs              |   1 
crates/project/src/bookmark_store.rs                   | 444 -------
crates/project/src/project.rs                          |  21 
crates/project/tests/integration/bookmark_store.rs     | 685 -----------
crates/project/tests/integration/project_tests.rs      |   1 
crates/settings/src/vscode_import.rs                   |   1 
crates/settings_content/src/editor.rs                  |   4 
crates/settings_ui/src/page_data.rs                    |  25 
crates/workspace/src/persistence.rs                    | 119 -
crates/workspace/src/persistence/model.rs              |   7 
crates/workspace/src/workspace.rs                      |  29 
crates/zed/src/visual_test_runner.rs                   |   4 
31 files changed, 543 insertions(+), 2,926 deletions(-)

Detailed changes

assets/icons/bookmark.svg 🔗

@@ -1,4 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M11.0885 2.44067C11.4162 2.44067 11.7304 2.57083 11.9621 2.80251C12.1938 3.0342 12.3239 3.34843 12.3239 3.67607V12.9416C12.3239 13.0498 12.2954 13.156 12.2414 13.2497C12.1874 13.3435 12.1098 13.4214 12.0162 13.4757C11.9227 13.53 11.8165 13.5587 11.7083 13.5591C11.6001 13.5594 11.4938 13.5314 11.3998 13.4777L8.61278 11.8853C8.42615 11.7787 8.21495 11.7226 8.00002 11.7226C7.78509 11.7226 7.57389 11.7787 7.38726 11.8853L4.6002 13.4777C4.50627 13.5314 4.3999 13.5594 4.29173 13.5591C4.18356 13.5587 4.07738 13.53 3.98382 13.4757C3.89026 13.4214 3.81259 13.3435 3.75859 13.2497C3.70459 13.156 3.67615 13.0498 3.67612 12.9416V3.67607C3.67612 3.34843 3.80627 3.0342 4.03796 2.80251C4.26964 2.57083 4.58387 2.44067 4.91152 2.44067H11.0885Z" stroke="#C6CAD0" stroke-width="1.11186" stroke-linecap="round" stroke-linejoin="round"/>
-<path opacity="0.12" d="M11.0885 2.44067C11.4162 2.44067 11.7304 2.57083 11.9621 2.80251C12.1938 3.0342 12.3239 3.34843 12.3239 3.67607V12.9416C12.3239 13.0498 12.2954 13.156 12.2414 13.2497C12.1874 13.3435 12.1098 13.4214 12.0162 13.4757C11.9227 13.53 11.8165 13.5587 11.7083 13.5591C11.6001 13.5594 11.4938 13.5314 11.3998 13.4777L8.61278 11.8853C8.42615 11.7787 8.21495 11.7226 8.00002 11.7226C7.78509 11.7226 7.57389 11.7787 7.38726 11.8853L4.6002 13.4777C4.50627 13.5314 4.3999 13.5594 4.29173 13.5591C4.18356 13.5587 4.07738 13.53 3.98382 13.4757C3.89026 13.4214 3.81259 13.3435 3.75859 13.2497C3.70459 13.156 3.67615 13.0498 3.67612 12.9416V3.67607C3.67612 3.34843 3.80627 3.0342 4.03796 2.80251C4.26964 2.57083 4.58387 2.44067 4.91152 2.44067H11.0885Z" fill="#C6CAD0" stroke="#C6CAD0" stroke-width="1.11186" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/settings/default.json 🔗

@@ -614,8 +614,6 @@
     "line_numbers": true,
     // Whether to show runnables buttons in the gutter.
     "runnables": true,
-    // Whether to show bookmarks in the gutter.
-    "bookmarks": true,
     // Whether to show breakpoints in the gutter.
     "breakpoints": true,
     // Whether to show fold buttons in the gutter.

crates/agent_ui/src/entry_view_state.rs 🔗

@@ -458,7 +458,6 @@ fn create_editor_diff(
         editor.set_show_indent_guides(false, cx);
         editor.set_read_only(true);
         editor.set_delegate_open_excerpts(true);
-        editor.set_show_bookmarks(false, cx);
         editor.set_show_breakpoints(false, cx);
         editor.set_show_code_actions(false, cx);
         editor.set_show_git_diff_gutter(false, cx);

crates/collab_ui/src/channel_view.rs 🔗

@@ -216,9 +216,6 @@ impl ChannelView {
                     })
                 }))
             });
-            editor.set_show_bookmarks(false, cx);
-            editor.set_show_breakpoints(false, cx);
-            editor.set_show_runnables(false, cx);
             editor
         });
         let _editor_event_subscription =

crates/debugger_tools/src/dap_log.rs 🔗

@@ -761,7 +761,6 @@ impl DapLogView {
             editor.set_text(log_contents, window, cx);
             editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
             editor.set_show_code_actions(false, cx);
-            editor.set_show_bookmarks(false, cx);
             editor.set_show_breakpoints(false, cx);
             editor.set_show_git_diff_gutter(false, cx);
             editor.set_show_runnables(false, cx);

crates/debugger_ui/src/session/running/console.rs 🔗

@@ -73,7 +73,6 @@ impl Console {
             editor.disable_scrollbars_and_minimap(window, cx);
             editor.set_show_gutter(false, cx);
             editor.set_show_runnables(false, cx);
-            editor.set_show_bookmarks(false, cx);
             editor.set_show_breakpoints(false, cx);
             editor.set_show_code_actions(false, cx);
             editor.set_show_line_numbers(false, cx);

crates/edit_prediction_ui/src/rate_prediction_modal.rs 🔗

@@ -439,7 +439,6 @@ impl RatePredictionsModal {
                     editor.set_show_git_diff_gutter(false, cx);
                     editor.set_show_code_actions(false, cx);
                     editor.set_show_runnables(false, cx);
-                    editor.set_show_bookmarks(false, cx);
                     editor.set_show_breakpoints(false, cx);
                     editor.set_show_wrap_guides(false, cx);
                     editor.set_show_indent_guides(false, cx);

crates/editor/src/actions.rs 🔗

@@ -576,14 +576,10 @@ actions!(
         GoToImplementation,
         /// Goes to implementation in a split pane.
         GoToImplementationSplit,
-        /// Goes to the next bookmark in the file.
-        GoToNextBookmark,
         /// Goes to the next change in the file.
         GoToNextChange,
         /// Goes to the parent module of the current file.
         GoToParentModule,
-        /// Goes to the previous bookmark in the file.
-        GoToPreviousBookmark,
         /// Goes to the previous change in the file.
         GoToPreviousChange,
         /// Goes to the next symbol.
@@ -674,8 +670,6 @@ actions!(
         NextScreen,
         /// Goes to the next snippet tabstop if one exists.
         NextSnippetTabstop,
-        /// Opens a view of all bookmarks in the project.
-        ViewBookmarks,
         /// Opens the context menu at cursor position.
         OpenContextMenu,
         /// Opens excerpts from the current file.
@@ -825,8 +819,6 @@ actions!(
         Tab,
         /// Removes a tab character or outdents.
         Backtab,
-        /// Toggles a bookmark at the current line.
-        ToggleBookmark,
         /// Toggles a breakpoint at the current line.
         ToggleBreakpoint,
         /// Toggles the case of selected text.

crates/editor/src/bookmarks.rs 🔗

@@ -1,243 +0,0 @@
-use std::ops::Range;
-
-use gpui::Entity;
-use multi_buffer::{Anchor, MultiBufferOffset, MultiBufferSnapshot, ToOffset as _};
-use project::{Project, bookmark_store::BookmarkStore};
-use rope::Point;
-use text::Bias;
-use ui::{Context, Window};
-use util::ResultExt as _;
-use workspace::{Workspace, searchable::Direction};
-
-use crate::display_map::DisplayRow;
-use crate::{
-    Editor, GoToNextBookmark, GoToPreviousBookmark, MultibufferSelectionMode, SelectionEffects,
-    ToggleBookmark, ViewBookmarks, scroll::Autoscroll,
-};
-
-impl Editor {
-    pub fn set_show_bookmarks(&mut self, show_bookmarks: bool, cx: &mut Context<Self>) {
-        self.show_bookmarks = Some(show_bookmarks);
-        cx.notify();
-    }
-
-    pub fn toggle_bookmark(
-        &mut self,
-        _: &ToggleBookmark,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let Some(bookmark_store) = self.bookmark_store.clone() else {
-            return;
-        };
-        let Some(project) = self.project() else {
-            return;
-        };
-
-        let snapshot = self.snapshot(window, cx);
-        let multi_buffer_snapshot = snapshot.buffer_snapshot();
-
-        let mut selections = self.selections.all::<Point>(&snapshot.display_snapshot);
-        selections.sort_by_key(|s| s.head());
-        selections.dedup_by_key(|s| s.head().row);
-
-        for selection in &selections {
-            let head = selection.head();
-            let multibuffer_anchor = multi_buffer_snapshot.anchor_before(Point::new(head.row, 0));
-
-            if let Some((buffer_anchor, _)) =
-                multi_buffer_snapshot.anchor_to_buffer_anchor(multibuffer_anchor)
-            {
-                let buffer_id = buffer_anchor.buffer_id;
-                if let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) {
-                    bookmark_store.update(cx, |store, cx| {
-                        store.toggle_bookmark(buffer, buffer_anchor, cx);
-                    });
-                }
-            }
-        }
-
-        cx.notify();
-    }
-
-    pub fn toggle_bookmark_at_row(&mut self, row: DisplayRow, cx: &mut Context<Self>) {
-        let Some(bookmark_store) = &self.bookmark_store else {
-            return;
-        };
-        let display_snapshot = self.display_snapshot(cx);
-        let point = display_snapshot.display_point_to_point(row.as_display_point(), Bias::Left);
-        let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
-        let anchor = buffer_snapshot.anchor_before(point);
-
-        let Some((position, _)) = buffer_snapshot.anchor_to_buffer_anchor(anchor) else {
-            return;
-        };
-        let Some(buffer) = self.buffer.read(cx).buffer(position.buffer_id) else {
-            return;
-        };
-
-        bookmark_store.update(cx, |bookmark_store, cx| {
-            bookmark_store.toggle_bookmark(buffer, position, cx);
-        });
-
-        cx.notify();
-    }
-
-    pub fn toggle_bookmark_at_anchor(&mut self, anchor: Anchor, cx: &mut Context<Self>) {
-        let Some(bookmark_store) = &self.bookmark_store else {
-            return;
-        };
-        let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
-        let Some((position, _)) = buffer_snapshot.anchor_to_buffer_anchor(anchor) else {
-            return;
-        };
-        let Some(buffer) = self.buffer.read(cx).buffer(position.buffer_id) else {
-            return;
-        };
-
-        bookmark_store.update(cx, |bookmark_store, cx| {
-            bookmark_store.toggle_bookmark(buffer, position, cx);
-        });
-
-        cx.notify();
-    }
-
-    pub fn go_to_next_bookmark(
-        &mut self,
-        _: &GoToNextBookmark,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.go_to_bookmark_impl(Direction::Next, window, cx);
-    }
-
-    pub fn go_to_previous_bookmark(
-        &mut self,
-        _: &GoToPreviousBookmark,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.go_to_bookmark_impl(Direction::Prev, window, cx);
-    }
-
-    fn go_to_bookmark_impl(
-        &mut self,
-        direction: Direction,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let Some(project) = &self.project else {
-            return;
-        };
-        let Some(bookmark_store) = &self.bookmark_store else {
-            return;
-        };
-
-        let selection = self
-            .selections
-            .newest::<MultiBufferOffset>(&self.display_snapshot(cx));
-        let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
-
-        let mut all_bookmarks = Self::bookmarks_in_range(
-            MultiBufferOffset(0)..multi_buffer_snapshot.len(),
-            &multi_buffer_snapshot,
-            project,
-            bookmark_store,
-            cx,
-        );
-        all_bookmarks.sort_by_key(|a| a.to_offset(&multi_buffer_snapshot));
-
-        let anchor = match direction {
-            Direction::Next => all_bookmarks
-                .iter()
-                .find(|anchor| anchor.to_offset(&multi_buffer_snapshot) > selection.head())
-                .or_else(|| all_bookmarks.first()),
-            Direction::Prev => all_bookmarks
-                .iter()
-                .rfind(|anchor| anchor.to_offset(&multi_buffer_snapshot) < selection.head())
-                .or_else(|| all_bookmarks.last()),
-        }
-        .cloned();
-
-        if let Some(anchor) = anchor {
-            self.unfold_ranges(&[anchor..anchor], true, false, cx);
-            self.change_selections(
-                SelectionEffects::scroll(Autoscroll::center()),
-                window,
-                cx,
-                |s| {
-                    s.select_anchor_ranges([anchor..anchor]);
-                },
-            );
-        }
-    }
-
-    pub fn view_bookmarks(
-        workspace: &mut Workspace,
-        _: &ViewBookmarks,
-        window: &mut Window,
-        cx: &mut Context<Workspace>,
-    ) {
-        let bookmark_store = workspace.project().read(cx).bookmark_store();
-        cx.spawn_in(window, async move |workspace, cx| {
-            let Some(locations) = BookmarkStore::all_bookmark_locations(bookmark_store, cx)
-                .await
-                .log_err()
-            else {
-                return;
-            };
-
-            workspace
-                .update_in(cx, |workspace, window, cx| {
-                    Editor::open_locations_in_multibuffer(
-                        workspace,
-                        locations,
-                        "Bookmarks".into(),
-                        false,
-                        false,
-                        MultibufferSelectionMode::First,
-                        window,
-                        cx,
-                    );
-                })
-                .log_err();
-        })
-        .detach();
-    }
-
-    fn bookmarks_in_range(
-        range: Range<MultiBufferOffset>,
-        multi_buffer_snapshot: &MultiBufferSnapshot,
-        project: &Entity<Project>,
-        bookmark_store: &Entity<BookmarkStore>,
-        cx: &mut Context<Self>,
-    ) -> Vec<Anchor> {
-        multi_buffer_snapshot
-            .range_to_buffer_ranges(range)
-            .into_iter()
-            .flat_map(|(buffer_snapshot, buffer_range, _excerpt_range)| {
-                let Some(buffer) = project
-                    .read(cx)
-                    .buffer_for_id(buffer_snapshot.remote_id(), cx)
-                else {
-                    return Vec::new();
-                };
-                bookmark_store
-                    .update(cx, |store, cx| {
-                        store.bookmarks_for_buffer(
-                            buffer,
-                            buffer_snapshot.anchor_before(buffer_range.start)
-                                ..buffer_snapshot.anchor_after(buffer_range.end),
-                            &buffer_snapshot,
-                            cx,
-                        )
-                    })
-                    .into_iter()
-                    .filter_map(|bookmark| {
-                        multi_buffer_snapshot.anchor_in_buffer(bookmark.anchor())
-                    })
-                    .collect::<Vec<_>>()
-            })
-            .collect()
-    }
-}

crates/editor/src/editor.rs 🔗

@@ -43,7 +43,6 @@ pub mod semantic_tokens;
 mod split;
 pub mod split_editor_view;
 
-mod bookmarks;
 #[cfg(test)]
 mod code_completion_tests;
 #[cfg(test)]
@@ -163,7 +162,6 @@ use project::{
     CompletionResponse, CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, InlayId,
     InvalidationStrategy, Location, LocationLink, LspAction, PrepareRenameResponse, Project,
     ProjectItem, ProjectPath, ProjectTransaction,
-    bookmark_store::BookmarkStore,
     debugger::{
         breakpoint_store::{
             Breakpoint, BreakpointEditAction, BreakpointSessionState, BreakpointState,
@@ -353,7 +351,6 @@ pub fn init(cx: &mut App) {
             workspace.register_action(Editor::new_file_horizontal);
             workspace.register_action(Editor::cancel_language_server_work);
             workspace.register_action(Editor::toggle_focus);
-            workspace.register_action(Editor::view_bookmarks);
         },
     )
     .detach();
@@ -1029,14 +1026,15 @@ enum ColumnarSelectionState {
     },
 }
 
-/// Represents a button that shows up when hovering over lines in the gutter that don't have
-/// any button on them already (like a bookmark, breakpoint or run indicator).
+/// 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, PartialEq, Eq)]
-struct GutterHoverButton {
+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,
 }
 
 /// Represents a diff review button indicator that shows up when hovering over lines in the gutter
@@ -1192,7 +1190,6 @@ pub struct Editor {
     show_git_diff_gutter: Option<bool>,
     show_code_actions: Option<bool>,
     show_runnables: Option<bool>,
-    show_bookmarks: Option<bool>,
     show_breakpoints: Option<bool>,
     show_diff_review_button: bool,
     show_wrap_guides: Option<bool>,
@@ -1300,9 +1297,8 @@ pub struct Editor {
     last_position_map: Option<Rc<PositionMap>>,
     expect_bounds_change: Option<Bounds<Pixels>>,
     runnables: RunnableData,
-    bookmark_store: Option<Entity<BookmarkStore>>,
     breakpoint_store: Option<Entity<BreakpointStore>>,
-    gutter_hover_button: (Option<GutterHoverButton>, Option<Task<()>>),
+    gutter_breakpoint_indicator: (Option<PhantomBreakpointIndicator>, Option<Task<()>>),
     pub(crate) gutter_diff_review_indicator: (Option<PhantomDiffReviewIndicator>, Option<Task<()>>),
     pub(crate) diff_review_drag_state: Option<DiffReviewDragState>,
     /// Active diff review overlays. Multiple overlays can be open simultaneously
@@ -1408,7 +1404,6 @@ pub struct EditorSnapshot {
     show_code_actions: Option<bool>,
     show_runnables: Option<bool>,
     show_breakpoints: Option<bool>,
-    show_bookmarks: Option<bool>,
     git_blame_gutter_max_author_length: Option<usize>,
     pub display_snapshot: DisplaySnapshot,
     pub placeholder_display_snapshot: Option<DisplaySnapshot>,
@@ -2370,11 +2365,6 @@ impl Editor {
                 None
             };
 
-        let bookmark_store = match (&mode, project.as_ref()) {
-            (EditorMode::Full { .. }, Some(project)) => Some(project.read(cx).bookmark_store()),
-            _ => None,
-        };
-
         let breakpoint_store = match (&mode, project.as_ref()) {
             (EditorMode::Full { .. }, Some(project)) => Some(project.read(cx).breakpoint_store()),
             _ => None,
@@ -2452,7 +2442,6 @@ impl Editor {
             show_git_diff_gutter: None,
             show_code_actions: None,
             show_runnables: None,
-            show_bookmarks: None,
             show_breakpoints: None,
             show_diff_review_button: false,
             show_wrap_guides: None,
@@ -2555,9 +2544,8 @@ impl Editor {
             blame: None,
             blame_subscription: None,
 
-            bookmark_store,
             breakpoint_store,
-            gutter_hover_button: (None, None),
+            gutter_breakpoint_indicator: (None, None),
             gutter_diff_review_indicator: (None, None),
             diff_review_drag_state: None,
             diff_review_overlays: Vec::new(),
@@ -3335,7 +3323,6 @@ impl Editor {
             semantic_tokens_enabled: self.semantic_token_state.enabled(),
             show_code_actions: self.show_code_actions,
             show_runnables: self.show_runnables,
-            show_bookmarks: self.show_bookmarks,
             show_breakpoints: self.show_breakpoints,
             git_blame_gutter_max_author_length,
             scroll_anchor: self.scroll_manager.shared_scroll_anchor(cx),
@@ -8636,9 +8623,6 @@ impl Editor {
 
         let mouse_position = window.mouse_position();
         if !position_map.text_hitbox.is_hovered(window) {
-            if self.gutter_hover_button.0.is_some() {
-                cx.notify();
-            }
             return;
         }
 
@@ -9021,138 +9005,6 @@ impl Editor {
         Some(self.edit_prediction_provider.as_ref()?.provider.clone())
     }
 
-    fn active_run_indicators(
-        &mut self,
-        range: Range<DisplayRow>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> HashSet<DisplayRow> {
-        let snapshot = self.snapshot(window, cx);
-
-        let offset_range_start =
-            snapshot.display_point_to_point(DisplayPoint::new(range.start, 0), Bias::Left);
-
-        let offset_range_end =
-            snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right);
-
-        self.runnables
-            .all_runnables()
-            .filter_map(|tasks| {
-                let multibuffer_point = tasks.offset.to_point(&snapshot.buffer_snapshot());
-                if multibuffer_point < offset_range_start || multibuffer_point > offset_range_end {
-                    return None;
-                }
-                let multibuffer_row = MultiBufferRow(multibuffer_point.row);
-                let buffer_folded = snapshot
-                    .buffer_snapshot()
-                    .buffer_line_for_row(multibuffer_row)
-                    .map(|(buffer_snapshot, _)| buffer_snapshot.remote_id())
-                    .map(|buffer_id| self.is_buffer_folded(buffer_id, cx))
-                    .unwrap_or(false);
-                if buffer_folded {
-                    return None;
-                }
-
-                if snapshot.is_line_folded(multibuffer_row) {
-                    // Skip folded indicators, unless it's the starting line of a fold.
-                    if multibuffer_row
-                        .0
-                        .checked_sub(1)
-                        .is_some_and(|previous_row| {
-                            snapshot.is_line_folded(MultiBufferRow(previous_row))
-                        })
-                    {
-                        return None;
-                    }
-                }
-
-                let display_row = multibuffer_point.to_display_point(&snapshot).row();
-                Some(display_row)
-            })
-            .collect()
-    }
-
-    fn active_bookmarks(
-        &self,
-        range: Range<DisplayRow>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> HashSet<DisplayRow> {
-        let mut bookmark_display_points = HashSet::default();
-
-        let Some(bookmark_store) = self.bookmark_store.clone() else {
-            return bookmark_display_points;
-        };
-
-        let snapshot = self.snapshot(window, cx);
-
-        let multi_buffer_snapshot = snapshot.buffer_snapshot();
-        let Some(project) = self.project() else {
-            return bookmark_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 (buffer_snapshot, range, _excerpt_range) in
-            multi_buffer_snapshot.range_to_buffer_ranges(range.start..range.end)
-        {
-            let Some(buffer) = project
-                .read(cx)
-                .buffer_for_id(buffer_snapshot.remote_id(), cx)
-            else {
-                continue;
-            };
-            let bookmarks = bookmark_store.update(cx, |store, cx| {
-                store.bookmarks_for_buffer(
-                    buffer,
-                    buffer_snapshot.anchor_before(range.start)
-                        ..buffer_snapshot.anchor_after(range.end),
-                    &buffer_snapshot,
-                    cx,
-                )
-            });
-            for bookmark in bookmarks {
-                let Some(multi_buffer_anchor) =
-                    multi_buffer_snapshot.anchor_in_buffer(bookmark.anchor())
-                else {
-                    continue;
-                };
-                let position = multi_buffer_anchor
-                    .to_point(&multi_buffer_snapshot)
-                    .to_display_point(&snapshot);
-
-                bookmark_display_points.insert(position.row());
-            }
-        }
-
-        bookmark_display_points
-    }
-
-    fn render_bookmark(&self, row: DisplayRow, cx: &mut Context<Self>) -> IconButton {
-        let focus_handle = self.focus_handle.clone();
-        IconButton::new(("bookmark indicator", row.0 as usize), IconName::Bookmark)
-            .icon_size(IconSize::XSmall)
-            .size(ui::ButtonSize::None)
-            .icon_color(Color::Info)
-            .style(ButtonStyle::Transparent)
-            .on_click(cx.listener(move |editor, _, _, cx| {
-                editor.toggle_bookmark_at_row(row, cx);
-            }))
-            .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
-                editor.set_gutter_context_menu(row, None, event.position(), window, cx);
-            }))
-            .tooltip(move |_window, cx| {
-                Tooltip::with_meta_in(
-                    "Remove bookmark",
-                    Some(&ToggleBookmark),
-                    SharedString::from("Right-click for more options."),
-                    &focus_handle,
-                    cx,
-                )
-            })
-    }
-
     /// 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.
@@ -9212,7 +9064,7 @@ impl Editor {
         breakpoint_display_points
     }
 
-    fn gutter_context_menu(
+    fn breakpoint_context_menu(
         &self,
         anchor: Anchor,
         window: &mut Window,
@@ -9262,14 +9114,6 @@ impl Editor {
             "Set Breakpoint"
         };
 
-        let bookmark = self.bookmark_at_row(row, window, cx);
-
-        let set_bookmark_msg = if bookmark.as_ref().is_some() {
-            "Remove Bookmark"
-        } else {
-            "Add Bookmark"
-        };
-
         let run_to_cursor = window.is_action_available(&RunToCursor, cx);
 
         let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state {
@@ -9369,28 +9213,16 @@ impl Editor {
                             .log_err();
                     }
                 })
-                .entry(hit_condition_breakpoint_msg, None, {
-                    let breakpoint = breakpoint.clone();
-                    let weak_editor = weak_editor.clone();
-                    move |window, cx| {
-                        weak_editor
-                            .update(cx, |this, cx| {
-                                this.add_edit_breakpoint_block(
-                                    anchor,
-                                    breakpoint.as_ref(),
-                                    BreakpointPromptEditAction::HitCondition,
-                                    window,
-                                    cx,
-                                );
-                            })
-                            .log_err();
-                    }
-                })
-                .separator()
-                .entry(set_bookmark_msg, None, move |_window, cx| {
+                .entry(hit_condition_breakpoint_msg, None, move |window, cx| {
                     weak_editor
                         .update(cx, |this, cx| {
-                            this.toggle_bookmark_at_anchor(anchor, cx);
+                            this.add_edit_breakpoint_block(
+                                anchor,
+                                breakpoint.as_ref(),
+                                BreakpointPromptEditAction::HitCondition,
+                                window,
+                                cx,
+                            );
                         })
                         .log_err();
                 })
@@ -9406,6 +9238,20 @@ impl Editor {
         cx: &mut Context<Self>,
     ) -> IconButton {
         let is_rejected = state.is_some_and(|s| !s.verified);
+        // 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()) {
@@ -9415,7 +9261,19 @@ impl Editor {
                 (true, true) => ui::IconName::DebugDisabledLogBreakpoint,
             };
 
-            let color = if is_rejected {
+            let theme_colors = cx.theme().colors();
+
+            let color = if is_phantom {
+                if collides_with_existing {
+                    Color::Custom(
+                        theme_colors
+                            .debugger_accent
+                            .blend(theme_colors.text.opacity(0.6)),
+                    )
+                } else {
+                    Color::Hint
+                }
+            } else if is_rejected {
                 Color::Disabled
             } else {
                 Color::Debugger
@@ -9430,14 +9288,20 @@ impl Editor {
             modifiers: Modifiers::secondary_key(),
             ..Default::default()
         };
-        let primary_action_text = "Unset breakpoint";
+        let primary_action_text = if breakpoint.is_disabled() {
+            "Enable breakpoint"
+        } else if is_phantom && !collides_with_existing {
+            "Set breakpoint"
+        } else {
+            "Unset breakpoint"
+        };
         let focus_handle = self.focus_handle.clone();
 
         let meta = if is_rejected {
             SharedString::from("No executable code is associated with this line.")
-        } else if !breakpoint.is_disabled() {
+        } else if collides_with_existing && !breakpoint.is_disabled() {
             SharedString::from(format!(
-                "{alt_as_text}click to disable,\nright-click for more options."
+                "{alt_as_text}-click to disable,\nright-click for more options."
             ))
         } else {
             SharedString::from("Right-click for more options.")
@@ -9459,6 +9323,7 @@ impl Editor {
                     };
 
                     window.focus(&editor.focus_handle(cx), cx);
+                    editor.update_breakpoint_collision_on_toggle(row, &edit_action);
                     editor.edit_breakpoint_at_anchor(
                         position,
                         breakpoint.as_ref().clone(),
@@ -9468,7 +9333,13 @@ impl Editor {
                 }
             }))
             .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
-                editor.set_gutter_context_menu(row, Some(position), event.position(), window, cx);
+                editor.set_breakpoint_context_menu(
+                    row,
+                    Some(position),
+                    event.position(),
+                    window,
+                    cx,
+                );
             }))
             .tooltip(move |_window, cx| {
                 Tooltip::with_meta_in(
@@ -9481,117 +9352,6 @@ impl Editor {
             })
     }
 
-    fn render_gutter_hover_button(
-        &self,
-        position: Anchor,
-        row: DisplayRow,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> IconButton {
-        #[derive(Clone, Copy)]
-        enum Intent {
-            SetBookmark,
-            SetBreakpoint,
-        }
-
-        impl Intent {
-            fn as_str(&self) -> &'static str {
-                match self {
-                    Intent::SetBookmark => "Set bookmark",
-                    Intent::SetBreakpoint => "Set breakpoint",
-                }
-            }
-
-            fn icon(&self) -> ui::IconName {
-                match self {
-                    Intent::SetBookmark => ui::IconName::Bookmark,
-                    Intent::SetBreakpoint => ui::IconName::DebugBreakpoint,
-                }
-            }
-
-            fn color(&self) -> Color {
-                match self {
-                    Intent::SetBookmark => Color::Info,
-                    Intent::SetBreakpoint => Color::Hint,
-                }
-            }
-
-            fn secondary_and_options(&self) -> String {
-                let alt_as_text = gpui::Keystroke {
-                    modifiers: Modifiers::secondary_key(),
-                    ..Default::default()
-                };
-                match self {
-                    Intent::SetBookmark => format!(
-                        "{alt_as_text}click to add a breakpoint,\nright-click for more options."
-                    ),
-                    Intent::SetBreakpoint => format!(
-                        "{alt_as_text}click to add a bookmark,\nright-click for more options."
-                    ),
-                }
-            }
-        }
-
-        let gutter_settings = EditorSettings::get_global(cx).gutter;
-        let show_bookmarks = self.show_bookmarks.unwrap_or(gutter_settings.bookmarks);
-        let show_breakpoints = self.show_breakpoints.unwrap_or(gutter_settings.breakpoints);
-
-        let [primary, secondary] = match [show_breakpoints, show_bookmarks] {
-            [true, true] => [Intent::SetBreakpoint, Intent::SetBookmark],
-            [true, false] => [Intent::SetBreakpoint; 2],
-            [false, true] => [Intent::SetBookmark; 2],
-            [false, false] => {
-                log::error!("Trying to place gutter_hover without anything enabled!!");
-                [Intent::SetBookmark; 2]
-            }
-        };
-
-        let intent = if window.modifiers().secondary() {
-            secondary
-        } else {
-            primary
-        };
-
-        let focus_handle = self.focus_handle.clone();
-        IconButton::new(("add_breakpoint_button", row.0 as usize), intent.icon())
-            .icon_size(IconSize::XSmall)
-            .size(ui::ButtonSize::None)
-            .icon_color(intent.color())
-            .style(ButtonStyle::Transparent)
-            .on_click(cx.listener({
-                move |editor, _: &ClickEvent, window, cx| {
-                    window.focus(&editor.focus_handle(cx), cx);
-                    let intent = if window.modifiers().secondary() {
-                        secondary
-                    } else {
-                        primary
-                    };
-
-                    match intent {
-                        Intent::SetBookmark => editor.toggle_bookmark_at_row(row, cx),
-                        Intent::SetBreakpoint => editor.edit_breakpoint_at_anchor(
-                            position,
-                            Breakpoint::new_standard(),
-                            BreakpointEditAction::Toggle,
-                            cx,
-                        ),
-                    }
-                }
-            }))
-            .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
-                editor.set_gutter_context_menu(row, Some(position), event.position(), window, cx);
-            }))
-            .tooltip(move |_window, cx| {
-                Tooltip::with_meta_in(
-                    intent.as_str(),
-                    Some(&ToggleBreakpoint),
-                    intent.secondary_and_options(),
-                    &focus_handle,
-                    cx,
-                )
-            })
-    }
-
     fn build_tasks_context(
         project: &Entity<Project>,
         buffer: &Entity<Buffer>,
@@ -12164,7 +11924,7 @@ impl Editor {
         }
     }
 
-    fn set_gutter_context_menu(
+    fn set_breakpoint_context_menu(
         &mut self,
         display_row: DisplayRow,
         position: Option<Anchor>,
@@ -12178,7 +11938,7 @@ impl Editor {
             .snapshot(cx)
             .anchor_before(Point::new(display_row.0, 0u32));
 
-        let context_menu = self.gutter_context_menu(position.unwrap_or(source), window, cx);
+        let context_menu = self.breakpoint_context_menu(position.unwrap_or(source), window, cx);
 
         self.mouse_context_menu = MouseContextMenu::pinned_to_editor(
             self,
@@ -12295,65 +12055,6 @@ impl Editor {
             })
     }
 
-    pub(crate) fn bookmark_at_row(
-        &self,
-        row: u32,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Option<Anchor> {
-        let snapshot = self.snapshot(window, cx);
-        let bookmark_position = snapshot.buffer_snapshot().anchor_before(Point::new(row, 0));
-
-        self.bookmark_at_anchor(bookmark_position, &snapshot, cx)
-    }
-
-    pub(crate) fn bookmark_at_anchor(
-        &self,
-        bookmark_position: Anchor,
-        snapshot: &EditorSnapshot,
-        cx: &mut Context<Self>,
-    ) -> Option<Anchor> {
-        let (bookmark_position, _) = snapshot
-            .buffer_snapshot()
-            .anchor_to_buffer_anchor(bookmark_position)?;
-        let buffer = self.buffer.read(cx).buffer(bookmark_position.buffer_id)?;
-
-        let buffer_snapshot = buffer.read(cx).snapshot();
-
-        let row = buffer_snapshot
-            .summary_for_anchor::<text::PointUtf16>(&bookmark_position)
-            .row;
-
-        let line_len = buffer_snapshot.line_len(row);
-        let anchor_end = buffer_snapshot.anchor_after(Point::new(row, line_len));
-
-        self.bookmark_store
-            .as_ref()?
-            .update(cx, |bookmark_store, cx| {
-                bookmark_store
-                    .bookmarks_for_buffer(
-                        buffer,
-                        bookmark_position..anchor_end,
-                        &buffer_snapshot,
-                        cx,
-                    )
-                    .first()
-                    .and_then(|bookmark| {
-                        let bookmark_row = buffer_snapshot
-                            .summary_for_anchor::<text::PointUtf16>(&bookmark.anchor())
-                            .row;
-
-                        if bookmark_row == row {
-                            snapshot
-                                .buffer_snapshot()
-                                .anchor_in_excerpt(bookmark.anchor())
-                        } else {
-                            None
-                        }
-                    })
-            })
-    }
-
     pub fn edit_log_breakpoint(
         &mut self,
         _: &EditLogBreakpoint,
@@ -12565,7 +12266,19 @@ impl Editor {
             return;
         }
 
+        let snapshot = self.snapshot(window, cx);
         for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) {
+            if self.gutter_breakpoint_indicator.0.is_some() {
+                let display_row = anchor
+                    .to_point(snapshot.buffer_snapshot())
+                    .to_display_point(&snapshot.display_snapshot)
+                    .row();
+                self.update_breakpoint_collision_on_toggle(
+                    display_row,
+                    &BreakpointEditAction::Toggle,
+                );
+            }
+
             if let Some(breakpoint) = breakpoint {
                 self.edit_breakpoint_at_anchor(
                     anchor,
@@ -12584,6 +12297,21 @@ impl Editor {
         }
     }
 
+    fn update_breakpoint_collision_on_toggle(
+        &mut self,
+        display_row: DisplayRow,
+        edit_action: &BreakpointEditAction,
+    ) {
+        if let Some(ref mut breakpoint_indicator) = self.gutter_breakpoint_indicator.0 {
+            if breakpoint_indicator.display_row == display_row
+                && matches!(edit_action, BreakpointEditAction::Toggle)
+            {
+                breakpoint_indicator.collides_with_existing_breakpoint =
+                    !breakpoint_indicator.collides_with_existing_breakpoint;
+            }
+        }
+    }
+
     pub fn edit_breakpoint_at_anchor(
         &mut self,
         breakpoint_position: Anchor,
@@ -28460,7 +28188,6 @@ impl EditorSnapshot {
 
             let show_runnables = self.show_runnables.unwrap_or(gutter_settings.runnables);
             let show_breakpoints = self.show_breakpoints.unwrap_or(gutter_settings.breakpoints);
-            let show_bookmarks = self.show_bookmarks.unwrap_or(gutter_settings.bookmarks);
 
             let git_blame_entries_width =
                 self.git_blame_gutter_max_author_length
@@ -28481,20 +28208,18 @@ impl EditorSnapshot {
 
             let is_singleton = self.buffer_snapshot().is_singleton();
 
-            let left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO)
-                + if !is_singleton {
-                    ch_width * 4.0
-                // runnables, breakpoints and bookmarks are shown in the same place
-                // if all three are there only the runnable is shown
-                } else if show_runnables || show_breakpoints || show_bookmarks {
-                    ch_width * 3.0
-                } else if show_git_gutter && show_line_numbers {
-                    ch_width * 2.0
-                } else if show_git_gutter || show_line_numbers {
-                    ch_width
-                } else {
-                    px(0.)
-                };
+            let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO);
+            left_padding += if !is_singleton {
+                ch_width * 4.0
+            } else if show_runnables || show_breakpoints {
+                ch_width * 3.0
+            } else if show_git_gutter && show_line_numbers {
+                ch_width * 2.0
+            } else if show_git_gutter || show_line_numbers {
+                ch_width
+            } else {
+                px(0.)
+            };
 
             let shows_folds = is_singleton && gutter_settings.folds;
 

crates/editor/src/editor_settings.rs 🔗

@@ -133,7 +133,6 @@ pub struct Gutter {
     pub line_numbers: bool,
     pub runnables: bool,
     pub breakpoints: bool,
-    pub bookmarks: bool,
     pub folds: bool,
 }
 
@@ -249,7 +248,6 @@ impl Settings for EditorSettings {
                 min_line_number_digits: gutter.min_line_number_digits.unwrap(),
                 line_numbers: gutter.line_numbers.unwrap(),
                 runnables: gutter.runnables.unwrap(),
-                bookmarks: gutter.bookmarks.unwrap(),
                 breakpoints: gutter.breakpoints.unwrap(),
                 folds: gutter.folds.unwrap(),
             },

crates/editor/src/editor_tests.rs 🔗

@@ -41,7 +41,6 @@ use parking_lot::Mutex;
 use pretty_assertions::{assert_eq, assert_ne};
 use project::{
     FakeFs, Project,
-    bookmark_store::SerializedBookmark,
     debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
     project_settings::LspSettings,
     trusted_worktrees::{PathTrust, TrustedWorktrees},
@@ -27571,554 +27570,107 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
     );
 }
 
-struct BookmarkTestContext {
-    project: Entity<Project>,
-    editor: Entity<Editor>,
-    cx: VisualTestContext,
-}
-
-impl BookmarkTestContext {
-    async fn new(sample_text: &str, cx: &mut TestAppContext) -> BookmarkTestContext {
-        init_test(cx, |_| {});
-
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/a"),
-            json!({
-                "main.rs": sample_text,
-            }),
-        )
-        .await;
-        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
-        let window =
-            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
-        let workspace = window
-            .read_with(cx, |mw, _| mw.workspace().clone())
-            .unwrap();
-        let mut visual_cx = VisualTestContext::from_window(*window, cx);
-        let worktree_id = workspace.update_in(&mut visual_cx, |workspace, _window, cx| {
-            workspace.project().update(cx, |project, cx| {
-                project.worktrees(cx).next().unwrap().read(cx).id()
-            })
-        });
-
-        let buffer = project
-            .update(&mut visual_cx, |project, cx| {
-                project.open_buffer((worktree_id, rel_path("main.rs")), cx)
-            })
-            .await
-            .unwrap();
-
-        let (editor, editor_cx) = cx.add_window_view(|window, cx| {
-            Editor::new(
-                EditorMode::full(),
-                MultiBuffer::build_from_buffer(buffer, cx),
-                Some(project.clone()),
-                window,
-                cx,
-            )
-        });
-        let cx = editor_cx.clone();
-
-        BookmarkTestContext {
-            project,
-            editor,
-            cx,
-        }
-    }
-
-    fn abs_path(&self) -> Arc<Path> {
-        let project_path = self
-            .editor
-            .read_with(&self.cx, |editor, cx| editor.project_path(cx).unwrap());
-        self.project.read_with(&self.cx, |project, cx| {
-            project
-                .absolute_path(&project_path, cx)
-                .map(Arc::from)
-                .unwrap()
-        })
-    }
-
-    fn all_bookmarks(&self) -> BTreeMap<Arc<Path>, Vec<SerializedBookmark>> {
-        self.project.read_with(&self.cx, |project, cx| {
-            project
-                .bookmark_store()
-                .read(cx)
-                .all_serialized_bookmarks(cx)
-        })
-    }
-
-    fn assert_bookmark_rows(&self, expected_rows: Vec<u32>) {
-        let abs_path = self.abs_path();
-        let bookmarks = self.all_bookmarks();
-        if expected_rows.is_empty() {
-            assert!(
-                !bookmarks.contains_key(&abs_path),
-                "Expected no bookmarks for {}",
-                abs_path.display()
-            );
-        } else {
-            let mut rows: Vec<u32> = bookmarks
-                .get(&abs_path)
-                .unwrap()
-                .iter()
-                .map(|b| b.0)
-                .collect();
-            rows.sort();
-            assert_eq!(expected_rows, rows);
-        }
-    }
-
-    fn cursor_row(&mut self) -> u32 {
-        self.editor.update(&mut self.cx, |editor, cx| {
-            let snapshot = editor.display_snapshot(cx);
-            editor.selections.newest::<Point>(&snapshot).head().row
-        })
-    }
-
-    fn cursor_point(&mut self) -> Point {
-        self.editor.update(&mut self.cx, |editor, cx| {
-            let snapshot = editor.display_snapshot(cx);
-            editor.selections.newest::<Point>(&snapshot).head()
-        })
-    }
-
-    fn move_to_row(&mut self, row: u32) {
-        self.editor
-            .update_in(&mut self.cx, |editor: &mut Editor, window, cx| {
-                editor.move_to_beginning(&MoveToBeginning, window, cx);
-                for _ in 0..row {
-                    editor.move_down(&MoveDown, window, cx);
-                }
-            });
-    }
-
-    fn toggle_bookmark(&mut self) {
-        self.editor
-            .update_in(&mut self.cx, |editor: &mut Editor, window, cx| {
-                editor.toggle_bookmark(&actions::ToggleBookmark, window, cx);
-            });
-    }
-
-    fn toggle_bookmarks_at_rows(&mut self, rows: &[u32]) {
-        for &row in rows {
-            self.move_to_row(row);
-            self.toggle_bookmark();
-        }
-    }
-
-    fn go_to_next_bookmark(&mut self) {
-        self.editor
-            .update_in(&mut self.cx, |editor: &mut Editor, window, cx| {
-                editor.go_to_next_bookmark(&actions::GoToNextBookmark, window, cx);
-            });
-    }
-
-    fn go_to_previous_bookmark(&mut self) {
-        self.editor
-            .update_in(&mut self.cx, |editor: &mut Editor, window, cx| {
-                editor.go_to_previous_bookmark(&actions::GoToPreviousBookmark, window, cx);
-            });
-    }
-}
-
-#[gpui::test]
-async fn test_bookmark_toggling(cx: &mut TestAppContext) {
-    let mut ctx =
-        BookmarkTestContext::new("First line\nSecond line\nThird line\nFourth line", cx).await;
-
-    ctx.editor
-        .update_in(&mut ctx.cx, |editor: &mut Editor, window, cx| {
-            editor.toggle_bookmark(&actions::ToggleBookmark, window, cx);
-            editor.move_to_end(&MoveToEnd, window, cx);
-            editor.toggle_bookmark(&actions::ToggleBookmark, window, cx);
-        });
-
-    assert_eq!(1, ctx.all_bookmarks().len());
-    ctx.assert_bookmark_rows(vec![0, 3]);
-
-    ctx.editor
-        .update_in(&mut ctx.cx, |editor: &mut Editor, window, cx| {
-            editor.move_to_beginning(&MoveToBeginning, window, cx);
-            editor.toggle_bookmark(&actions::ToggleBookmark, window, cx);
-        });
-
-    assert_eq!(1, ctx.all_bookmarks().len());
-    ctx.assert_bookmark_rows(vec![3]);
-
-    ctx.editor
-        .update_in(&mut ctx.cx, |editor: &mut Editor, window, cx| {
-            editor.move_to_end(&MoveToEnd, window, cx);
-            editor.toggle_bookmark(&actions::ToggleBookmark, window, cx);
-        });
-
-    assert_eq!(0, ctx.all_bookmarks().len());
-    ctx.assert_bookmark_rows(vec![]);
-}
-
-#[gpui::test]
-async fn test_bookmark_toggling_with_multiple_selections(cx: &mut TestAppContext) {
-    let mut ctx =
-        BookmarkTestContext::new("First line\nSecond line\nThird line\nFourth line", cx).await;
-
-    ctx.editor
-        .update_in(&mut ctx.cx, |editor: &mut Editor, window, cx| {
-            editor.move_to_beginning(&MoveToBeginning, window, cx);
-            editor.add_selection_below(&Default::default(), window, cx);
-            editor.add_selection_below(&Default::default(), window, cx);
-            editor.add_selection_below(&Default::default(), window, cx);
-        });
-
-    ctx.toggle_bookmark();
-
-    assert_eq!(1, ctx.all_bookmarks().len());
-    ctx.assert_bookmark_rows(vec![0, 1, 2, 3]);
-
-    ctx.editor
-        .update_in(&mut ctx.cx, |editor: &mut Editor, window, cx| {
-            editor.move_to_beginning(&MoveToBeginning, window, cx);
-            editor.add_selection_below(&Default::default(), window, cx);
-            editor.add_selection_below(&Default::default(), window, cx);
-            editor.add_selection_below(&Default::default(), window, cx);
-            editor.toggle_bookmark(&actions::ToggleBookmark, window, cx);
-        });
-
-    assert_eq!(0, ctx.all_bookmarks().len());
-    ctx.assert_bookmark_rows(vec![]);
-}
-
-#[gpui::test]
-async fn test_bookmark_toggle_deduplicates_by_row(cx: &mut TestAppContext) {
-    let mut ctx =
-        BookmarkTestContext::new("First line\nSecond line\nThird line\nFourth line", cx).await;
-
-    ctx.editor
-        .update_in(&mut ctx.cx, |editor: &mut Editor, window, cx| {
-            editor.move_to_beginning(&MoveToBeginning, window, cx);
-            editor.toggle_bookmark(&actions::ToggleBookmark, window, cx);
-        });
-
-    ctx.assert_bookmark_rows(vec![0]);
-
-    ctx.editor
-        .update_in(&mut ctx.cx, |editor: &mut Editor, window, cx| {
-            editor.move_to_end_of_line(
-                &MoveToEndOfLine {
-                    stop_at_soft_wraps: true,
-                },
-                window,
-                cx,
-            );
-            editor.toggle_bookmark(&actions::ToggleBookmark, window, cx);
-        });
-
-    ctx.assert_bookmark_rows(vec![]);
-}
-
 #[gpui::test]
-async fn test_bookmark_survives_edits(cx: &mut TestAppContext) {
-    let mut ctx =
-        BookmarkTestContext::new("First line\nSecond line\nThird line\nFourth line", cx).await;
-
-    ctx.move_to_row(2);
-    ctx.toggle_bookmark();
-    ctx.assert_bookmark_rows(vec![2]);
-
-    ctx.editor
-        .update_in(&mut ctx.cx, |editor: &mut Editor, window, cx| {
-            editor.move_to_beginning(&MoveToBeginning, window, cx);
-            editor.newline(&Newline, window, cx);
-        });
-
-    ctx.assert_bookmark_rows(vec![3]);
-
-    ctx.move_to_row(3);
-    ctx.toggle_bookmark();
-    ctx.assert_bookmark_rows(vec![]);
-}
+async fn test_breakpoint_phantom_indicator_collision_on_toggle(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
 
-#[gpui::test]
-async fn test_active_bookmarks(cx: &mut TestAppContext) {
-    let mut ctx = BookmarkTestContext::new(
-        "Line 0\nLine 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9",
-        cx,
+    let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        path!("/a"),
+        json!({
+            "main.rs": sample_text,
+        }),
     )
     .await;
+    let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
+    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+    let workspace = window
+        .read_with(cx, |mw, _| mw.workspace().clone())
+        .unwrap();
+    let cx = &mut VisualTestContext::from_window(*window, cx);
+    let worktree_id = workspace.update_in(cx, |workspace, _window, cx| {
+        workspace.project().update(cx, |project, cx| {
+            project.worktrees(cx).next().unwrap().read(cx).id()
+        })
+    });
 
-    ctx.toggle_bookmarks_at_rows(&[1, 3, 5, 8]);
-
-    let active = ctx
-        .editor
-        .update_in(&mut ctx.cx, |editor: &mut Editor, window, cx| {
-            editor.active_bookmarks(DisplayRow(0)..DisplayRow(10), window, cx)
-        });
-    assert!(active.contains(&DisplayRow(1)));
-    assert!(active.contains(&DisplayRow(3)));
-    assert!(active.contains(&DisplayRow(5)));
-    assert!(active.contains(&DisplayRow(8)));
-    assert!(!active.contains(&DisplayRow(0)));
-    assert!(!active.contains(&DisplayRow(2)));
-    assert!(!active.contains(&DisplayRow(9)));
-
-    let active = ctx
-        .editor
-        .update_in(&mut ctx.cx, |editor: &mut Editor, window, cx| {
-            editor.active_bookmarks(DisplayRow(2)..DisplayRow(6), window, cx)
-        });
-    assert!(active.contains(&DisplayRow(3)));
-    assert!(active.contains(&DisplayRow(5)));
-    assert!(!active.contains(&DisplayRow(1)));
-    assert!(!active.contains(&DisplayRow(8)));
-}
+    let buffer = project
+        .update(cx, |project, cx| {
+            project.open_buffer((worktree_id, rel_path("main.rs")), cx)
+        })
+        .await
+        .unwrap();
 
-#[gpui::test]
-async fn test_bookmark_not_available_in_single_line_editor(cx: &mut TestAppContext) {
-    init_test(cx, |_| {});
+    let (editor, cx) = cx.add_window_view(|window, cx| {
+        Editor::new(
+            EditorMode::full(),
+            MultiBuffer::build_from_buffer(buffer, cx),
+            Some(project.clone()),
+            window,
+            cx,
+        )
+    });
 
-    let (editor, _cx) = cx.add_window_view(|window, cx| Editor::single_line(window, cx));
+    // Simulate hovering over row 0 with no existing breakpoint.
+    editor.update(cx, |editor, _cx| {
+        editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
+            display_row: DisplayRow(0),
+            is_active: true,
+            collides_with_existing_breakpoint: false,
+        });
+    });
 
+    // Toggle breakpoint on the same row (row 0) — collision should flip to true.
+    editor.update_in(cx, |editor, window, cx| {
+        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
+    });
     editor.update(cx, |editor, _cx| {
+        let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
         assert!(
-            editor.bookmark_store.is_none(),
-            "Single-line editors should not have a bookmark store"
+            indicator.collides_with_existing_breakpoint,
+            "Adding a breakpoint on the hovered row should set collision to true"
         );
     });
-}
-
-#[gpui::test]
-async fn test_bookmark_navigation_lands_at_column_zero(cx: &mut TestAppContext) {
-    let mut ctx =
-        BookmarkTestContext::new("First line\nSecond line\nThird line\nFourth line", cx).await;
-
-    ctx.editor
-        .update_in(&mut ctx.cx, |editor: &mut Editor, window, cx| {
-            editor.move_to_beginning(&MoveToBeginning, window, cx);
-            editor.move_down(&MoveDown, window, cx);
-            editor.move_to_end_of_line(
-                &MoveToEndOfLine {
-                    stop_at_soft_wraps: true,
-                },
-                window,
-                cx,
-            );
-        });
-
-    let column_before_toggle = ctx.cursor_point().column;
-    assert_eq!(
-        column_before_toggle, 11,
-        "Cursor should be at the 11th column before toggling bookmark, got column {column_before_toggle}"
-    );
-
-    ctx.toggle_bookmark();
-
-    ctx.editor
-        .update_in(&mut ctx.cx, |editor: &mut Editor, window, cx| {
-            editor.move_to_beginning(&MoveToBeginning, window, cx);
-        });
-
-    ctx.go_to_next_bookmark();
-
-    let cursor = ctx.cursor_point();
-    assert_eq!(cursor.row, 1, "Should navigate to the bookmarked row");
-    assert_eq!(
-        cursor.column, 0,
-        "Bookmark navigation should always land at column 0"
-    );
-}
-
-#[gpui::test]
-async fn test_bookmark_set_from_nonzero_column_toggles_off_from_column_zero(
-    cx: &mut TestAppContext,
-) {
-    let mut ctx =
-        BookmarkTestContext::new("First line\nSecond line\nThird line\nFourth line", cx).await;
-
-    ctx.editor
-        .update_in(&mut ctx.cx, |editor: &mut Editor, window, cx| {
-            editor.move_to_beginning(&MoveToBeginning, window, cx);
-            editor.move_down(&MoveDown, window, cx);
-            editor.move_to_end_of_line(
-                &MoveToEndOfLine {
-                    stop_at_soft_wraps: true,
-                },
-                window,
-                cx,
-            );
-            editor.toggle_bookmark(&actions::ToggleBookmark, window, cx);
-        });
-
-    ctx.assert_bookmark_rows(vec![1]);
-
-    ctx.editor
-        .update_in(&mut ctx.cx, |editor: &mut Editor, window, cx| {
-            editor.move_to_beginning_of_line(
-                &MoveToBeginningOfLine {
-                    stop_at_soft_wraps: true,
-                    stop_at_indent: false,
-                },
-                window,
-                cx,
-            );
-            editor.toggle_bookmark(&actions::ToggleBookmark, window, cx);
-        });
-
-    ctx.assert_bookmark_rows(vec![]);
-}
-
-#[gpui::test]
-async fn test_go_to_next_bookmark(cx: &mut TestAppContext) {
-    let mut ctx = BookmarkTestContext::new(
-        "Line 0\nLine 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9",
-        cx,
-    )
-    .await;
-
-    ctx.toggle_bookmarks_at_rows(&[2, 5, 8]);
-
-    ctx.move_to_row(0);
-
-    ctx.go_to_next_bookmark();
-    assert_eq!(
-        ctx.cursor_row(),
-        2,
-        "First next-bookmark should go to row 2"
-    );
-
-    ctx.go_to_next_bookmark();
-    assert_eq!(
-        ctx.cursor_row(),
-        5,
-        "Second next-bookmark should go to row 5"
-    );
-
-    ctx.go_to_next_bookmark();
-    assert_eq!(
-        ctx.cursor_row(),
-        8,
-        "Third next-bookmark should go to row 8"
-    );
-
-    ctx.go_to_next_bookmark();
-    assert_eq!(
-        ctx.cursor_row(),
-        2,
-        "Next-bookmark should wrap around to row 2"
-    );
-}
 
-#[gpui::test]
-async fn test_go_to_previous_bookmark(cx: &mut TestAppContext) {
-    let mut ctx = BookmarkTestContext::new(
-        "Line 0\nLine 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9",
-        cx,
-    )
-    .await;
+    // Toggle again on the same row — breakpoint is removed, collision should flip back to false.
+    editor.update_in(cx, |editor, window, cx| {
+        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
+    });
+    editor.update(cx, |editor, _cx| {
+        let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
+        assert!(
+            !indicator.collides_with_existing_breakpoint,
+            "Removing a breakpoint on the hovered row should set collision to false"
+        );
+    });
 
-    ctx.toggle_bookmarks_at_rows(&[2, 5, 8]);
+    // Now move cursor to row 2 while phantom indicator stays on row 0.
+    editor.update_in(cx, |editor, window, cx| {
+        editor.move_down(&MoveDown, window, cx);
+        editor.move_down(&MoveDown, window, cx);
+    });
 
-    ctx.editor
-        .update_in(&mut ctx.cx, |editor: &mut Editor, window, cx| {
-            editor.move_to_end(&MoveToEnd, window, cx);
+    // Ensure phantom indicator is still on row 0, not colliding.
+    editor.update(cx, |editor, _cx| {
+        editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
+            display_row: DisplayRow(0),
+            is_active: true,
+            collides_with_existing_breakpoint: false,
         });
+    });
 
-    ctx.go_to_previous_bookmark();
-    assert_eq!(
-        ctx.cursor_row(),
-        8,
-        "First prev-bookmark should go to row 8"
-    );
-
-    ctx.go_to_previous_bookmark();
-    assert_eq!(
-        ctx.cursor_row(),
-        5,
-        "Second prev-bookmark should go to row 5"
-    );
-
-    ctx.go_to_previous_bookmark();
-    assert_eq!(
-        ctx.cursor_row(),
-        2,
-        "Third prev-bookmark should go to row 2"
-    );
-
-    ctx.go_to_previous_bookmark();
-    assert_eq!(
-        ctx.cursor_row(),
-        8,
-        "Prev-bookmark should wrap around to row 8"
-    );
-}
-
-#[gpui::test]
-async fn test_go_to_bookmark_when_cursor_on_bookmarked_line(cx: &mut TestAppContext) {
-    let mut ctx = BookmarkTestContext::new(
-        "Line 0\nLine 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9",
-        cx,
-    )
-    .await;
-
-    ctx.toggle_bookmarks_at_rows(&[3, 7]);
-
-    ctx.move_to_row(3);
-
-    ctx.go_to_next_bookmark();
-    assert_eq!(
-        ctx.cursor_row(),
-        7,
-        "Next from bookmarked row 3 should go to row 7"
-    );
-
-    ctx.go_to_previous_bookmark();
-    assert_eq!(
-        ctx.cursor_row(),
-        3,
-        "Previous from bookmarked row 7 should go to row 3"
-    );
-
-    ctx.go_to_next_bookmark();
-    assert_eq!(ctx.cursor_row(), 7, "Next from row 3 should go to row 7");
-
-    ctx.go_to_next_bookmark();
-    assert_eq!(ctx.cursor_row(), 3, "Next from row 7 should wrap to row 3");
-}
-
-#[gpui::test]
-async fn test_go_to_bookmark_with_out_of_order_bookmarks(cx: &mut TestAppContext) {
-    let mut ctx = BookmarkTestContext::new(
-        "Line 0\nLine 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9",
-        cx,
-    )
-    .await;
-
-    ctx.toggle_bookmarks_at_rows(&[8, 1, 5]);
-
-    ctx.move_to_row(0);
-
-    ctx.go_to_next_bookmark();
-    assert_eq!(ctx.cursor_row(), 1, "First next should go to row 1");
-
-    ctx.go_to_next_bookmark();
-    assert_eq!(ctx.cursor_row(), 5, "Second next should go to row 5");
-
-    ctx.go_to_next_bookmark();
-    assert_eq!(ctx.cursor_row(), 8, "Third next should go to row 8");
-
-    ctx.go_to_next_bookmark();
-    assert_eq!(ctx.cursor_row(), 1, "Fourth next should wrap to row 1");
-
-    ctx.go_to_previous_bookmark();
-    assert_eq!(
-        ctx.cursor_row(),
-        8,
-        "Prev from row 1 should wrap around to row 8"
-    );
-
-    ctx.go_to_previous_bookmark();
-    assert_eq!(ctx.cursor_row(), 5, "Prev from row 8 should go to row 5");
-
-    ctx.go_to_previous_bookmark();
-    assert_eq!(ctx.cursor_row(), 1, "Prev from row 5 should go to row 1");
+    // Toggle breakpoint on row 2 (cursor row) — phantom on row 0 should NOT be affected.
+    editor.update_in(cx, |editor, window, cx| {
+        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
+    });
+    editor.update(cx, |editor, _cx| {
+        let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
+        assert!(
+            !indicator.collides_with_existing_breakpoint,
+            "Toggling a breakpoint on a different row should not affect the phantom indicator"
+        );
+    });
 }
 
 #[gpui::test]

crates/editor/src/element.rs 🔗

@@ -4,12 +4,12 @@ use crate::{
     ConflictsOuter, ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape,
     CustomBlockId, DisplayDiffHunk, DisplayPoint, DisplayRow, EditDisplayMode, EditPrediction,
     Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT,
-    FocusedBlock, GutterDimensions, GutterHoverButton, HalfPageDown, HalfPageUp, HandleInput,
-    HoveredCursor, InlayHintRefreshReason, JumpData, LineDown, LineHighlight, LineUp, MAX_LINE_LEN,
+    FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
+    InlayHintRefreshReason, JumpData, LineDown, LineHighlight, LineUp, MAX_LINE_LEN,
     MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, PageUp,
-    PhantomDiffReviewIndicator, Point, RowExt, RowRangeExt, SelectPhase, Selection,
-    SelectionDragState, SelectionEffects, SizingBehavior, SoftWrap, StickyHeaderExcerpt, ToPoint,
-    ToggleFold, ToggleFoldAll,
+    PhantomBreakpointIndicator, PhantomDiffReviewIndicator, Point, RowExt, RowRangeExt,
+    SelectPhase, Selection, SelectionDragState, SelectionEffects, SizingBehavior, SoftWrap,
+    StickyHeaderExcerpt, ToPoint, ToggleFold, ToggleFoldAll,
     code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
     column_pixels,
     display_map::{
@@ -34,7 +34,7 @@ use crate::{
     },
 };
 use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
-use collections::{BTreeMap, HashMap, HashSet};
+use collections::{BTreeMap, HashMap};
 use feature_flags::{DiffReviewFeatureFlag, FeatureFlagAppExt as _};
 use file_icons::FileIcons;
 use git::{Oid, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
@@ -62,7 +62,7 @@ use multi_buffer::{
 };
 
 use project::{
-    DisableAiSettings, Entry,
+    DisableAiSettings, Entry, ProjectPath,
     debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
     project_settings::ProjectSettings,
 };
@@ -652,9 +652,6 @@ impl EditorElement {
         register_action(editor, window, Editor::insert_uuid_v4);
         register_action(editor, window, Editor::insert_uuid_v7);
         register_action(editor, window, Editor::open_selections_in_multibuffer);
-        register_action(editor, window, Editor::toggle_bookmark);
-        register_action(editor, window, Editor::go_to_next_bookmark);
-        register_action(editor, window, Editor::go_to_previous_bookmark);
         register_action(editor, window, Editor::toggle_breakpoint);
         register_action(editor, window, Editor::edit_log_breakpoint);
         register_action(editor, window, Editor::enable_breakpoint);
@@ -893,11 +890,9 @@ impl EditorElement {
             let gutter_right_padding = editor.gutter_dimensions.right_padding;
             let hitbox = &position_map.gutter_hitbox;
 
-            if event.position.x <= hitbox.bounds.right() - gutter_right_padding
-                && editor.collaboration_hub.is_none()
-            {
+            if event.position.x <= hitbox.bounds.right() - gutter_right_padding {
                 let point_for_position = position_map.point_for_position(event.position);
-                editor.set_gutter_context_menu(
+                editor.set_breakpoint_context_menu(
                     point_for_position.previous_valid.row(),
                     None,
                     event.position,
@@ -1401,26 +1396,49 @@ impl EditorElement {
                 .snapshot
                 .display_point_to_anchor(valid_point, Bias::Left);
 
-            if position_map
+            if let Some((buffer_anchor, buffer_snapshot)) = position_map
                 .snapshot
                 .buffer_snapshot()
                 .anchor_to_buffer_anchor(buffer_anchor)
-                .is_some()
+                && let Some(file) = buffer_snapshot.file()
             {
+                let as_point = text::ToPoint::to_point(&buffer_anchor, buffer_snapshot);
+
                 let is_visible = editor
-                    .gutter_hover_button
+                    .gutter_breakpoint_indicator
                     .0
                     .is_some_and(|indicator| indicator.is_active);
 
+                let has_existing_breakpoint =
+                    editor.breakpoint_store.as_ref().is_some_and(|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()
+                    });
+
                 if !is_visible {
-                    editor.gutter_hover_button.1.get_or_insert_with(|| {
+                    editor.gutter_breakpoint_indicator.1.get_or_insert_with(|| {
                         cx.spawn(async move |this, cx| {
                             cx.background_executor()
                                 .timer(Duration::from_millis(200))
                                 .await;
 
                             this.update(cx, |this, cx| {
-                                if let Some(indicator) = this.gutter_hover_button.0.as_mut() {
+                                if let Some(indicator) = this.gutter_breakpoint_indicator.0.as_mut()
+                                {
                                     indicator.is_active = true;
                                     cx.notify();
                                 }
@@ -1430,21 +1448,22 @@ impl EditorElement {
                     });
                 }
 
-                Some(GutterHoverButton {
+                Some(PhantomBreakpointIndicator {
                     display_row: valid_point.row(),
                     is_active: is_visible,
+                    collides_with_existing_breakpoint: has_existing_breakpoint,
                 })
             } else {
-                editor.gutter_hover_button.1 = None;
+                editor.gutter_breakpoint_indicator.1 = None;
                 None
             }
         } else {
-            editor.gutter_hover_button.1 = None;
+            editor.gutter_breakpoint_indicator.1 = None;
             None
         };
 
-        if &breakpoint_indicator != &editor.gutter_hover_button.0 {
-            editor.gutter_hover_button.0 = breakpoint_indicator;
+        if &breakpoint_indicator != &editor.gutter_breakpoint_indicator.0 {
+            editor.gutter_breakpoint_indicator.0 = breakpoint_indicator;
             cx.notify();
         }
 
@@ -3114,10 +3133,16 @@ impl EditorElement {
         (offset_y, length, row_range)
     }
 
-    fn layout_bookmarks(
+    fn layout_breakpoints(
         &self,
-        gutter: &Gutter<'_>,
-        bookmarks: &HashSet<DisplayRow>,
+        line_height: Pixels,
+        range: Range<DisplayRow>,
+        scroll_position: gpui::Point<ScrollOffset>,
+        gutter_dimensions: &GutterDimensions,
+        gutter_hitbox: &Hitbox,
+        snapshot: &EditorSnapshot,
+        breakpoints: HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)>,
+        row_infos: &[RowInfo],
         window: &mut Window,
         cx: &mut App,
     ) -> Vec<AnyElement> {
@@ -3126,71 +3151,44 @@ impl EditorElement {
         }
 
         self.editor.update(cx, |editor, cx| {
-            bookmarks
-                .iter()
-                .filter_map(|row| {
-                    gutter.layout_item_skipping_folds(
-                        *row,
-                        |cx, _| editor.render_bookmark(*row, cx).into_any_element(),
-                        window,
-                        cx,
-                    )
-                })
-                .collect_vec()
-        })
-    }
+            breakpoints
+                .into_iter()
+                .filter_map(|(display_row, (text_anchor, bp, state))| {
+                    if row_infos
+                        .get((display_row.0.saturating_sub(range.start.0)) as usize)
+                        .is_some_and(|row_info| {
+                            row_info.expand_info.is_some()
+                                || row_info
+                                    .diff_status
+                                    .is_some_and(|status| status.is_deleted())
+                        })
+                    {
+                        return None;
+                    }
 
-    fn layout_gutter_hover_button(
-        &self,
-        gutter: &Gutter,
-        position: Anchor,
-        row: DisplayRow,
-        window: &mut Window,
-        cx: &mut App,
-    ) -> Option<AnyElement> {
-        if self.split_side == Some(SplitSide::Left) {
-            return None;
-        }
+                    if range.start > display_row || range.end < display_row {
+                        return None;
+                    }
 
-        self.editor.update(cx, |editor, cx| {
-            gutter.layout_item_skipping_folds(
-                row,
-                |cx, window| {
-                    editor
-                        .render_gutter_hover_button(position, row, window, cx)
-                        .into_any_element()
-                },
-                window,
-                cx,
-            )
-        })
-    }
+                    let row =
+                        MultiBufferRow(DisplayPoint::new(display_row, 0).to_point(snapshot).row);
+                    if snapshot.is_line_folded(row) {
+                        return None;
+                    }
 
-    fn layout_breakpoints(
-        &self,
-        gutter: &Gutter,
-        breakpoints: &HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)>,
-        window: &mut Window,
-        cx: &mut App,
-    ) -> Vec<AnyElement> {
-        if self.split_side == Some(SplitSide::Left) {
-            return Vec::new();
-        }
+                    let button = editor.render_breakpoint(text_anchor, display_row, &bp, state, cx);
 
-        self.editor.update(cx, |editor, cx| {
-            breakpoints
-                .iter()
-                .filter_map(|(row, (text_anchor, bp, state))| {
-                    gutter.layout_item_skipping_folds(
-                        *row,
-                        |cx, _| {
-                            editor
-                                .render_breakpoint(*text_anchor, *row, &bp, *state, cx)
-                                .into_any_element()
-                        },
+                    let button = prepaint_gutter_button(
+                        button.into_any_element(),
+                        display_row,
+                        line_height,
+                        gutter_dimensions,
+                        scroll_position,
+                        gutter_hitbox,
                         window,
                         cx,
-                    )
+                    );
+                    Some(button)
                 })
                 .collect_vec()
         })
@@ -3242,11 +3240,17 @@ impl EditorElement {
         Some((display_row, buffer_row))
     }
 
+    #[allow(clippy::too_many_arguments)]
     fn layout_run_indicators(
         &self,
-        gutter: &Gutter,
-        run_indicators: &HashSet<DisplayRow>,
-        breakpoints: &HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)>,
+        line_height: Pixels,
+        range: Range<DisplayRow>,
+        row_infos: &[RowInfo],
+        scroll_position: gpui::Point<ScrollOffset>,
+        gutter_dimensions: &GutterDimensions,
+        gutter_hitbox: &Hitbox,
+        snapshot: &EditorSnapshot,
+        breakpoints: &mut HashMap<DisplayRow, (Anchor, Breakpoint, Option<BreakpointSessionState>)>,
         window: &mut Window,
         cx: &mut App,
     ) -> Vec<AnyElement> {
@@ -3265,7 +3269,7 @@ impl EditorElement {
                 {
                     actions
                         .tasks()
-                        .map(|tasks| tasks.position.to_display_point(gutter.snapshot).row())
+                        .map(|tasks| tasks.position.to_display_point(snapshot).row())
                         .or_else(|| match deployed_from {
                             Some(CodeActionSource::Indicator(row)) => Some(*row),
                             _ => None,
@@ -3274,25 +3278,77 @@ impl EditorElement {
                     None
                 };
 
-            run_indicators
-                .iter()
-                .filter_map(|display_row| {
-                    gutter.layout_item(
-                        *display_row,
-                        |cx, _| {
-                            editor
-                                .render_run_indicator(
-                                    &self.style,
-                                    Some(*display_row) == active_task_indicator_row,
-                                    breakpoints.get(&display_row).map(|(anchor, _, _)| *anchor),
-                                    *display_row,
-                                    cx,
-                                )
-                                .into_any_element()
-                        },
+            let offset_range_start =
+                snapshot.display_point_to_point(DisplayPoint::new(range.start, 0), Bias::Left);
+
+            let offset_range_end =
+                snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right);
+
+            editor
+                .runnables
+                .all_runnables()
+                .filter_map(|tasks| {
+                    let multibuffer_point = tasks.offset.to_point(&snapshot.buffer_snapshot());
+                    if multibuffer_point < offset_range_start
+                        || multibuffer_point > offset_range_end
+                    {
+                        return None;
+                    }
+                    let multibuffer_row = MultiBufferRow(multibuffer_point.row);
+                    let buffer_folded = snapshot
+                        .buffer_snapshot()
+                        .buffer_line_for_row(multibuffer_row)
+                        .map(|(buffer_snapshot, _)| buffer_snapshot.remote_id())
+                        .map(|buffer_id| editor.is_buffer_folded(buffer_id, cx))
+                        .unwrap_or(false);
+                    if buffer_folded {
+                        return None;
+                    }
+
+                    if snapshot.is_line_folded(multibuffer_row) {
+                        // Skip folded indicators, unless it's the starting line of a fold.
+                        if multibuffer_row
+                            .0
+                            .checked_sub(1)
+                            .is_some_and(|previous_row| {
+                                snapshot.is_line_folded(MultiBufferRow(previous_row))
+                            })
+                        {
+                            return None;
+                        }
+                    }
+
+                    let display_row = multibuffer_point.to_display_point(snapshot).row();
+                    if !range.contains(&display_row) {
+                        return None;
+                    }
+                    if row_infos
+                        .get((display_row - range.start).0 as usize)
+                        .is_some_and(|row_info| row_info.expand_info.is_some())
+                    {
+                        return None;
+                    }
+
+                    let removed_breakpoint = breakpoints.remove(&display_row);
+                    let button = editor.render_run_indicator(
+                        &self.style,
+                        Some(display_row) == active_task_indicator_row,
+                        display_row,
+                        removed_breakpoint,
+                        cx,
+                    );
+
+                    let button = prepaint_gutter_button(
+                        button.into_any_element(),
+                        display_row,
+                        line_height,
+                        gutter_dimensions,
+                        scroll_position,
+                        gutter_hitbox,
                         window,
                         cx,
-                    )
+                    );
+                    Some(button)
                 })
                 .collect_vec()
         })
@@ -3392,14 +3448,19 @@ impl EditorElement {
 
     fn layout_line_numbers(
         &self,
-        gutter: &Gutter<'_>,
+        gutter_hitbox: Option<&Hitbox>,
+        gutter_dimensions: GutterDimensions,
+        line_height: Pixels,
+        scroll_position: gpui::Point<ScrollOffset>,
+        rows: Range<DisplayRow>,
+        buffer_rows: &[RowInfo],
         active_rows: &BTreeMap<DisplayRow, LineHighlightSpec>,
         current_selection_head: Option<DisplayRow>,
+        snapshot: &EditorSnapshot,
         window: &mut Window,
         cx: &mut App,
     ) -> Arc<HashMap<MultiBufferRow, LineNumberLayout>> {
-        let include_line_numbers = gutter
-            .snapshot
+        let include_line_numbers = snapshot
             .show_line_numbers
             .unwrap_or_else(|| EditorSettings::get_global(cx).gutter.line_numbers);
         if !include_line_numbers {
@@ -3412,8 +3473,8 @@ impl EditorElement {
         let relative_rows = if relative_line_numbers_enabled
             && let Some(current_selection_head) = current_selection_head
         {
-            gutter.snapshot.calculate_relative_line_numbers(
-                &gutter.range,
+            snapshot.calculate_relative_line_numbers(
+                &rows,
                 current_selection_head,
                 relative.wrapped(),
             )
@@ -3422,79 +3483,72 @@ impl EditorElement {
         };
 
         let mut line_number = String::new();
-        let segments = gutter
-            .row_infos
-            .iter()
-            .enumerate()
-            .flat_map(|(ix, row_info)| {
-                let display_row = DisplayRow(gutter.range.start.0 + ix as u32);
-                line_number.clear();
-                let non_relative_number = if relative.wrapped() {
-                    row_info.buffer_row.or(row_info.wrapped_buffer_row)? + 1
-                } else {
-                    row_info.buffer_row? + 1
-                };
-                let relative_number = relative_rows.get(&display_row);
-                if !(relative_line_numbers_enabled && relative_number.is_some())
-                    && !gutter.snapshot.number_deleted_lines
-                    && row_info
-                        .diff_status
-                        .is_some_and(|status| status.is_deleted())
-                {
-                    return None;
-                }
+        let segments = buffer_rows.iter().enumerate().flat_map(|(ix, row_info)| {
+            let display_row = DisplayRow(rows.start.0 + ix as u32);
+            line_number.clear();
+            let non_relative_number = if relative.wrapped() {
+                row_info.buffer_row.or(row_info.wrapped_buffer_row)? + 1
+            } else {
+                row_info.buffer_row? + 1
+            };
+            let relative_number = relative_rows.get(&display_row);
+            if !(relative_line_numbers_enabled && relative_number.is_some())
+                && !snapshot.number_deleted_lines
+                && row_info
+                    .diff_status
+                    .is_some_and(|status| status.is_deleted())
+            {
+                return None;
+            }
 
-                let number = relative_number.unwrap_or(&non_relative_number);
-                write!(&mut line_number, "{number}").unwrap();
+            let number = relative_number.unwrap_or(&non_relative_number);
+            write!(&mut line_number, "{number}").unwrap();
 
-                let color = active_rows
-                    .get(&display_row)
-                    .map(|spec| {
-                        if spec.breakpoint {
-                            cx.theme().colors().debugger_accent
-                        } else {
-                            cx.theme().colors().editor_active_line_number
-                        }
-                    })
-                    .unwrap_or_else(|| cx.theme().colors().editor_line_number);
-                let shaped_line =
-                    self.shape_line_number(SharedString::from(&line_number), color, window);
-                let scroll_top =
-                    gutter.scroll_position.y * ScrollPixelOffset::from(gutter.line_height);
-                let line_origin = gutter.hitbox.origin
+            let color = active_rows
+                .get(&display_row)
+                .map(|spec| {
+                    if spec.breakpoint {
+                        cx.theme().colors().debugger_accent
+                    } else {
+                        cx.theme().colors().editor_active_line_number
+                    }
+                })
+                .unwrap_or_else(|| cx.theme().colors().editor_line_number);
+            let shaped_line =
+                self.shape_line_number(SharedString::from(&line_number), color, window);
+            let scroll_top = scroll_position.y * ScrollPixelOffset::from(line_height);
+            let line_origin = gutter_hitbox.map(|hitbox| {
+                hitbox.origin
                     + point(
-                        gutter.hitbox.size.width
-                            - shaped_line.width
-                            - gutter.dimensions.right_padding,
-                        ix as f32 * gutter.line_height
-                            - Pixels::from(
-                                scroll_top % ScrollPixelOffset::from(gutter.line_height),
-                            ),
-                    );
+                        hitbox.size.width - shaped_line.width - gutter_dimensions.right_padding,
+                        ix as f32 * line_height
+                            - Pixels::from(scroll_top % ScrollPixelOffset::from(line_height)),
+                    )
+            });
 
-                #[cfg(not(test))]
-                let hitbox = Some(window.insert_hitbox(
-                    Bounds::new(line_origin, size(shaped_line.width, gutter.line_height)),
+            #[cfg(not(test))]
+            let hitbox = line_origin.map(|line_origin| {
+                window.insert_hitbox(
+                    Bounds::new(line_origin, size(shaped_line.width, line_height)),
                     HitboxBehavior::Normal,
-                ));
-                #[cfg(test)]
-                let hitbox = {
-                    let _ = line_origin;
-                    None
-                };
+                )
+            });
+            #[cfg(test)]
+            let hitbox = {
+                let _ = line_origin;
+                None
+            };
 
-                let segment = LineNumberSegment {
-                    shaped_line,
-                    hitbox,
-                };
+            let segment = LineNumberSegment {
+                shaped_line,
+                hitbox,
+            };
 
-                let buffer_row = DisplayPoint::new(display_row, 0)
-                    .to_point(gutter.snapshot)
-                    .row;
-                let multi_buffer_row = MultiBufferRow(buffer_row);
+            let buffer_row = DisplayPoint::new(display_row, 0).to_point(snapshot).row;
+            let multi_buffer_row = MultiBufferRow(buffer_row);
 
-                Some((multi_buffer_row, segment))
-            });
+            Some((multi_buffer_row, segment))
+        });
 
         let mut line_numbers: HashMap<MultiBufferRow, LineNumberLayout> = HashMap::default();
         for (buffer_row, segment) in segments {
@@ -6373,10 +6427,6 @@ impl EditorElement {
                 }
             });
 
-            for bookmark in layout.bookmarks.iter_mut() {
-                bookmark.paint(window, cx);
-            }
-
             for breakpoint in layout.breakpoints.iter_mut() {
                 breakpoint.paint(window, cx);
             }
@@ -7914,96 +7964,6 @@ impl EditorElement {
     }
 }
 
-struct Gutter<'a> {
-    line_height: Pixels,
-    range: Range<DisplayRow>,
-    scroll_position: gpui::Point<ScrollOffset>,
-    dimensions: &'a GutterDimensions,
-    hitbox: &'a Hitbox,
-    snapshot: &'a EditorSnapshot,
-    row_infos: &'a [RowInfo],
-}
-
-impl Gutter<'_> {
-    fn layout_item_skipping_folds(
-        &self,
-        display_row: DisplayRow,
-        render_item: impl Fn(&mut Context<'_, Editor>, &mut Window) -> AnyElement,
-        window: &mut Window,
-        cx: &mut Context<'_, Editor>,
-    ) -> Option<AnyElement> {
-        let row = MultiBufferRow(
-            DisplayPoint::new(display_row, 0)
-                .to_point(self.snapshot)
-                .row,
-        );
-        if self.snapshot.is_line_folded(row) {
-            return None;
-        }
-
-        self.layout_item(display_row, render_item, window, cx)
-    }
-
-    fn layout_item(
-        &self,
-        display_row: DisplayRow,
-        render_item: impl Fn(&mut Context<'_, Editor>, &mut Window) -> AnyElement,
-        window: &mut Window,
-        cx: &mut Context<'_, Editor>,
-    ) -> Option<AnyElement> {
-        if !self.range.contains(&display_row) {
-            return None;
-        }
-
-        if self
-            .row_infos
-            .get((display_row.0.saturating_sub(self.range.start.0)) as usize)
-            .is_some_and(|row_info| {
-                row_info.expand_info.is_some()
-                    || row_info
-                        .diff_status
-                        .is_some_and(|status| status.is_deleted())
-            })
-        {
-            return None;
-        }
-
-        let button = self.prepaint_button(render_item(cx, window), display_row, window, cx);
-        Some(button)
-    }
-
-    fn prepaint_button(
-        &self,
-        mut button: AnyElement,
-        row: DisplayRow,
-        window: &mut Window,
-        cx: &mut App,
-    ) -> AnyElement {
-        let available_space = size(
-            AvailableSpace::MinContent,
-            AvailableSpace::Definite(self.line_height),
-        );
-        let indicator_size = button.layout_as_root(available_space, window, cx);
-        let git_gutter_width = EditorElement::gutter_strip_width(self.line_height)
-            + self.dimensions.git_blame_entries_width.unwrap_or_default();
-
-        let x = git_gutter_width + px(2.);
-
-        let mut y = Pixels::from(
-            (row.as_f64() - self.scroll_position.y) * ScrollPixelOffset::from(self.line_height),
-        );
-        y += (self.line_height - indicator_size.height) / 2.;
-
-        button.prepaint_as_root(
-            self.hitbox.origin + point(x, y),
-            available_space,
-            window,
-            cx,
-        );
-        button
-    }
-}
-
 pub fn render_breadcrumb_text(
     mut segments: Vec<HighlightedText>,
     breadcrumb_font: Option<Font>,
@@ -8681,6 +8641,41 @@ pub(crate) fn render_buffer_header(
         })
 }
 
+fn prepaint_gutter_button(
+    mut button: AnyElement,
+    row: DisplayRow,
+    line_height: Pixels,
+    gutter_dimensions: &GutterDimensions,
+    scroll_position: gpui::Point<ScrollOffset>,
+    gutter_hitbox: &Hitbox,
+    window: &mut Window,
+    cx: &mut App,
+) -> AnyElement {
+    let available_space = size(
+        AvailableSpace::MinContent,
+        AvailableSpace::Definite(line_height),
+    );
+    let indicator_size = button.layout_as_root(available_space, window, cx);
+    let git_gutter_width = EditorElement::gutter_strip_width(line_height)
+        + gutter_dimensions
+            .git_blame_entries_width
+            .unwrap_or_default();
+
+    let x = git_gutter_width + px(2.);
+
+    let mut y =
+        Pixels::from((row.as_f64() - scroll_position.y) * ScrollPixelOffset::from(line_height));
+    y += (line_height - indicator_size.height) / 2.;
+
+    button.prepaint_as_root(
+        gutter_hitbox.origin + point(x, y),
+        available_space,
+        window,
+        cx,
+    );
+    button
+}
+
 fn render_inline_blame_entry(
     blame_entry: BlameEntry,
     style: &EditorStyle,
@@ -10127,38 +10122,54 @@ impl Element for EditorElement {
                         })
                     });
 
-                    let run_indicator_rows = self.editor.update(cx, |editor, cx| {
-                        editor.active_run_indicators(start_row..end_row, window, cx)
-                    });
-
                     let mut breakpoint_rows = self.editor.update(cx, |editor, cx| {
                         editor.active_breakpoints(start_row..end_row, window, cx)
                     });
-
                     for (display_row, (_, bp, state)) in &breakpoint_rows {
                         if bp.is_enabled() && state.is_none_or(|s| s.verified) {
                             active_rows.entry(*display_row).or_default().breakpoint = true;
                         }
                     }
 
-                    let gutter = Gutter {
+                    let line_numbers = self.layout_line_numbers(
+                        Some(&gutter_hitbox),
+                        gutter_dimensions,
                         line_height,
-                        range: start_row..end_row,
                         scroll_position,
-                        dimensions: &gutter_dimensions,
-                        hitbox: &gutter_hitbox,
-                        snapshot: &snapshot,
-                        row_infos: &row_infos,
-                    };
-
-                    let line_numbers = self.layout_line_numbers(
-                        &gutter,
+                        start_row..end_row,
+                        &row_infos,
                         &active_rows,
                         current_selection_head,
+                        &snapshot,
                         window,
                         cx,
                     );
 
+                    // We add the gutter breakpoint indicator to breakpoint_rows after painting
+                    // 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
+                    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, None)
+                                });
+                        }
+                    });
+
                     let mut expand_toggles =
                         window.with_element_namespace("expand_toggles", |window| {
                             self.layout_expand_toggles(
@@ -10741,9 +10752,14 @@ impl Element for EditorElement {
 
                     let test_indicators = if gutter_settings.runnables {
                         self.layout_run_indicators(
-                            &gutter,
-                            &run_indicator_rows,
-                            &breakpoint_rows,
+                            line_height,
+                            start_row..end_row,
+                            &row_infos,
+                            scroll_position,
+                            &gutter_dimensions,
+                            &gutter_hitbox,
+                            &snapshot,
+                            &mut breakpoint_rows,
                             window,
                             cx,
                         )
@@ -10751,54 +10767,26 @@ impl Element for EditorElement {
                         Vec::new()
                     };
 
-                    let show_bookmarks =
-                        snapshot.show_bookmarks.unwrap_or(gutter_settings.bookmarks);
-
-                    let bookmark_rows = self.editor.update(cx, |editor, cx| {
-                        let mut rows = editor.active_bookmarks(start_row..end_row, window, cx);
-                        rows.retain(|k| !run_indicator_rows.contains(k));
-                        rows.retain(|k| !breakpoint_rows.contains_key(k));
-                        rows
-                    });
-
-                    let bookmarks = if show_bookmarks {
-                        self.layout_bookmarks(&gutter, &bookmark_rows, window, cx)
-                    } else {
-                        Vec::new()
-                    };
-
                     let show_breakpoints = snapshot
                         .show_breakpoints
                         .unwrap_or(gutter_settings.breakpoints);
-
-                    breakpoint_rows.retain(|k, _| !run_indicator_rows.contains(k));
-                    let mut breakpoints = if show_breakpoints {
-                        self.layout_breakpoints(&gutter, &breakpoint_rows, window, cx)
+                    let breakpoints = if show_breakpoints {
+                        self.layout_breakpoints(
+                            line_height,
+                            start_row..end_row,
+                            scroll_position,
+                            &gutter_dimensions,
+                            &gutter_hitbox,
+                            &snapshot,
+                            breakpoint_rows,
+                            &row_infos,
+                            window,
+                            cx,
+                        )
                     } else {
                         Vec::new()
                     };
 
-                    let gutter_hover_button = self
-                        .editor
-                        .read(cx)
-                        .gutter_hover_button
-                        .0
-                        .filter(|phantom| phantom.is_active)
-                        .map(|phantom| phantom.display_row);
-
-                    if let Some(row) = gutter_hover_button
-                        && !breakpoint_rows.contains_key(&row)
-                        && !run_indicator_rows.contains(&row)
-                        && !bookmark_rows.contains(&row)
-                        && (show_bookmarks || show_breakpoints)
-                    {
-                        let position = snapshot
-                            .display_point_to_anchor(DisplayPoint::new(row, 0), Bias::Right);
-                        breakpoints.extend(
-                            self.layout_gutter_hover_button(&gutter, position, row, window, cx),
-                        );
-                    }
-
                     let git_gutter_width = Self::gutter_strip_width(line_height)
                         + gutter_dimensions
                             .git_blame_entries_width
@@ -10842,7 +10830,16 @@ impl Element for EditorElement {
                                     .render_diff_review_button(display_row, button_width, cx)
                                     .into_any_element()
                             });
-                            gutter.prepaint_button(button, display_row, window, cx)
+                            prepaint_gutter_button(
+                                button,
+                                display_row,
+                                line_height,
+                                &gutter_dimensions,
+                                scroll_position,
+                                &gutter_hitbox,
+                                window,
+                                cx,
+                            )
                         });
 
                     self.layout_signature_help(
@@ -11078,7 +11075,6 @@ impl Element for EditorElement {
                         diff_hunk_controls,
                         mouse_context_menu,
                         test_indicators,
-                        bookmarks,
                         breakpoints,
                         diff_review_button,
                         crease_toggles,
@@ -11267,7 +11263,6 @@ pub struct EditorLayout {
     visible_cursors: Vec<CursorLayout>,
     selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
     test_indicators: Vec<AnyElement>,
-    bookmarks: Vec<AnyElement>,
     breakpoints: Vec<AnyElement>,
     diff_review_button: Option<AnyElement>,
     crease_toggles: Vec<Option<AnyElement>>,
@@ -12477,71 +12472,6 @@ mod tests {
     use std::num::NonZeroU32;
     use util::test::sample_text;
 
-    const fn placeholder_hitbox() -> Hitbox {
-        use gpui::HitboxId;
-        let zero_bounds = Bounds {
-            origin: point(Pixels::ZERO, Pixels::ZERO),
-            size: Size {
-                width: Pixels::ZERO,
-                height: Pixels::ZERO,
-            },
-        };
-
-        Hitbox {
-            id: HitboxId::placeholder(),
-            bounds: zero_bounds,
-            content_mask: ContentMask {
-                bounds: zero_bounds,
-            },
-            behavior: HitboxBehavior::Normal,
-        }
-    }
-
-    fn test_gutter(line_height: Pixels, snapshot: &EditorSnapshot) -> Gutter<'_> {
-        const DIMENSIONS: GutterDimensions = GutterDimensions {
-            left_padding: Pixels::ZERO,
-            right_padding: Pixels::ZERO,
-            width: px(30.0),
-            margin: Pixels::ZERO,
-            git_blame_entries_width: None,
-        };
-        const EMPTY_ROW_INFO: RowInfo = RowInfo {
-            buffer_id: None,
-            buffer_row: None,
-            multibuffer_row: None,
-            diff_status: None,
-            expand_info: None,
-            wrapped_buffer_row: None,
-        };
-
-        const fn row_info(row: u32) -> RowInfo {
-            RowInfo {
-                buffer_row: Some(row),
-                ..EMPTY_ROW_INFO
-            }
-        }
-
-        const ROW_INFOS: [RowInfo; 6] = [
-            row_info(0),
-            row_info(1),
-            row_info(2),
-            row_info(3),
-            row_info(4),
-            row_info(5),
-        ];
-
-        const HITBOX: Hitbox = placeholder_hitbox();
-        Gutter {
-            line_height,
-            range: DisplayRow(0)..DisplayRow(6),
-            scroll_position: gpui::Point::default(),
-            dimensions: &DIMENSIONS,
-            hitbox: &HITBOX,
-            snapshot: snapshot,
-            row_infos: &ROW_INFOS,
-        }
-    }
-
     #[gpui::test]
     async fn test_soft_wrap_editor_width_auto_height_editor(cx: &mut TestAppContext) {
         init_test(cx, |_| {});
@@ -12628,9 +12558,26 @@ mod tests {
         let layouts = cx
             .update_window(*window, |_, window, cx| {
                 element.layout_line_numbers(
-                    &test_gutter(line_height, &snapshot),
+                    None,
+                    GutterDimensions {
+                        left_padding: Pixels::ZERO,
+                        right_padding: Pixels::ZERO,
+                        width: px(30.0),
+                        margin: Pixels::ZERO,
+                        git_blame_entries_width: None,
+                    },
+                    line_height,
+                    gpui::Point::default(),
+                    DisplayRow(0)..DisplayRow(6),
+                    &(0..6)
+                        .map(|row| RowInfo {
+                            buffer_row: Some(row),
+                            ..Default::default()
+                        })
+                        .collect::<Vec<_>>(),
                     &BTreeMap::default(),
                     Some(DisplayRow(0)),
+                    &snapshot,
                     window,
                     cx,
                 )

crates/editor/src/runnables.rs 🔗

@@ -9,7 +9,11 @@ use gpui::{
 use language::{Buffer, BufferRow, Runnable};
 use lsp::LanguageServerName;
 use multi_buffer::{Anchor, BufferOffset, MultiBufferRow, MultiBufferSnapshot, ToPoint as _};
-use project::{Location, Project, TaskSourceKind, project_settings::ProjectSettings};
+use project::{
+    Location, Project, TaskSourceKind,
+    debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
+    project_settings::ProjectSettings,
+};
 use settings::Settings as _;
 use smallvec::SmallVec;
 use task::{ResolvedTask, RunnableTag, TaskContext, TaskTemplate, TaskVariables, VariableName};
@@ -515,11 +519,12 @@ impl Editor {
         &self,
         _style: &EditorStyle,
         is_active: bool,
-        active_breakpoint: Option<Anchor>,
         row: DisplayRow,
+        breakpoint: Option<(Anchor, Breakpoint, Option<BreakpointSessionState>)>,
         cx: &mut Context<Self>,
     ) -> IconButton {
         let color = Color::Muted;
+        let position = breakpoint.as_ref().map(|(anchor, _, _)| *anchor);
 
         IconButton::new(
             ("run_indicator", row.0 as usize),
@@ -546,7 +551,7 @@ impl Editor {
             );
         }))
         .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
-            editor.set_gutter_context_menu(row, active_breakpoint, event.position(), window, cx);
+            editor.set_breakpoint_context_menu(row, position, event.position(), window, cx);
         }))
     }
 

crates/git_ui/src/commit_view.rs 🔗

@@ -203,7 +203,6 @@ impl CommitView {
                 Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
 
             editor.disable_inline_diagnostics();
-            editor.set_show_bookmarks(false, cx);
             editor.set_show_breakpoints(false, cx);
             editor.set_show_diff_review_button(true, cx);
             editor.set_expand_all_diff_hunks(cx);

crates/gpui/src/window.rs 🔗

@@ -572,17 +572,6 @@ pub enum WindowControlArea {
 #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
 pub struct HitboxId(u64);
 
-#[cfg(feature = "test-support")]
-impl HitboxId {
-    /// A placeholder HitboxId exclusively for integration testing API's that
-    /// need a hitbox but where the value of the hitbox does not matter. The
-    /// alternative is to make the Hitbox optional but that complicates the
-    /// implementation.
-    pub const fn placeholder() -> Self {
-        Self(0)
-    }
-}
-
 impl HitboxId {
     /// Checks if the hitbox with this ID is currently hovered. Returns `false` during keyboard
     /// input modality so that keyboard navigation suppresses hover highlights. Except when handling

crates/inspector_ui/src/div_inspector.rs 🔗

@@ -495,7 +495,6 @@ impl DivInspector {
             editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
             editor.set_show_line_numbers(false, cx);
             editor.set_show_code_actions(false, cx);
-            editor.set_show_bookmarks(false, cx);
             editor.set_show_breakpoints(false, cx);
             editor.set_show_git_diff_gutter(false, cx);
             editor.set_show_runnables(false, cx);

crates/language_tools/src/lsp_log_view.rs 🔗

@@ -1294,7 +1294,6 @@ fn initialize_new_editor(
         editor.set_text(content, window, cx);
         editor.set_show_git_diff_gutter(false, cx);
         editor.set_show_runnables(false, cx);
-        editor.set_show_bookmarks(false, cx);
         editor.set_show_breakpoints(false, cx);
         editor.set_read_only(true);
         editor.set_show_edit_predictions(Some(false), window, cx);

crates/project/src/bookmark_store.rs 🔗

@@ -1,444 +0,0 @@
-use std::{collections::BTreeMap, ops::Range, path::Path, sync::Arc};
-
-use anyhow::Result;
-use futures::{StreamExt, TryFutureExt, TryStreamExt, stream::FuturesUnordered};
-use gpui::{App, AppContext, Context, Entity, Subscription, Task};
-use itertools::Itertools;
-use language::{Buffer, BufferEvent};
-use std::collections::HashMap;
-use text::{BufferSnapshot, Point};
-
-use crate::{ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore};
-
-#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
-pub struct BookmarkAnchor(text::Anchor);
-
-impl BookmarkAnchor {
-    pub fn anchor(&self) -> text::Anchor {
-        self.0
-    }
-}
-
-#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
-pub struct SerializedBookmark(pub u32);
-
-#[derive(Debug)]
-pub struct BufferBookmarks {
-    buffer: Entity<Buffer>,
-    bookmarks: Vec<BookmarkAnchor>,
-    _subscription: Subscription,
-}
-
-impl BufferBookmarks {
-    pub fn new(buffer: Entity<Buffer>, cx: &mut Context<BookmarkStore>) -> Self {
-        let subscription = cx.subscribe(
-            &buffer,
-            |bookmark_store, buffer, event: &BufferEvent, cx| match event {
-                BufferEvent::FileHandleChanged => {
-                    bookmark_store.handle_file_changed(buffer, cx);
-                }
-                _ => {}
-            },
-        );
-
-        Self {
-            buffer,
-            bookmarks: Vec::new(),
-            _subscription: subscription,
-        }
-    }
-
-    pub fn buffer(&self) -> &Entity<Buffer> {
-        &self.buffer
-    }
-
-    pub fn bookmarks(&self) -> &[BookmarkAnchor] {
-        &self.bookmarks
-    }
-}
-
-#[derive(Debug)]
-pub enum BookmarkEntry {
-    Loaded(BufferBookmarks),
-    Unloaded(Vec<SerializedBookmark>),
-}
-
-impl BookmarkEntry {
-    pub fn is_empty(&self) -> bool {
-        match self {
-            BookmarkEntry::Loaded(buffer_bookmarks) => buffer_bookmarks.bookmarks.is_empty(),
-            BookmarkEntry::Unloaded(rows) => rows.is_empty(),
-        }
-    }
-
-    fn loaded(&self) -> Option<&BufferBookmarks> {
-        match self {
-            BookmarkEntry::Loaded(buffer_bookmarks) => Some(buffer_bookmarks),
-            BookmarkEntry::Unloaded(_) => None,
-        }
-    }
-}
-
-pub struct BookmarkStore {
-    buffer_store: Entity<BufferStore>,
-    worktree_store: Entity<WorktreeStore>,
-    bookmarks: BTreeMap<Arc<Path>, BookmarkEntry>,
-}
-
-impl BookmarkStore {
-    pub fn new(worktree_store: Entity<WorktreeStore>, buffer_store: Entity<BufferStore>) -> Self {
-        Self {
-            buffer_store,
-            worktree_store,
-            bookmarks: BTreeMap::new(),
-        }
-    }
-
-    pub fn load_serialized_bookmarks(
-        &mut self,
-        bookmark_rows: BTreeMap<Arc<Path>, Vec<SerializedBookmark>>,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<()>> {
-        self.bookmarks.clear();
-
-        for (path, rows) in bookmark_rows {
-            if rows.is_empty() {
-                continue;
-            }
-
-            let count = rows.len();
-            log::debug!("Stored {count} unloaded bookmark(s) at {}", path.display());
-
-            self.bookmarks.insert(path, BookmarkEntry::Unloaded(rows));
-        }
-
-        cx.notify();
-        Task::ready(Ok(()))
-    }
-
-    fn resolve_anchors_if_needed(
-        &mut self,
-        abs_path: &Arc<Path>,
-        buffer: &Entity<Buffer>,
-        cx: &mut Context<Self>,
-    ) {
-        let Some(BookmarkEntry::Unloaded(rows)) = self.bookmarks.get(abs_path) else {
-            return;
-        };
-
-        let snapshot = buffer.read(cx).snapshot();
-        let max_point = snapshot.max_point();
-
-        let anchors: Vec<BookmarkAnchor> = rows
-            .iter()
-            .filter_map(|bookmark_row| {
-                let point = Point::new(bookmark_row.0, 0);
-
-                if point > max_point {
-                    log::warn!(
-                        "Skipping out-of-range bookmark: {} row {} (file has {} rows)",
-                        abs_path.display(),
-                        bookmark_row.0,
-                        max_point.row
-                    );
-                    return None;
-                }
-
-                let anchor = snapshot.anchor_after(point);
-                Some(BookmarkAnchor(anchor))
-            })
-            .collect();
-
-        if anchors.is_empty() {
-            self.bookmarks.remove(abs_path);
-        } else {
-            let mut buffer_bookmarks = BufferBookmarks::new(buffer.clone(), cx);
-            buffer_bookmarks.bookmarks = anchors;
-            self.bookmarks
-                .insert(abs_path.clone(), BookmarkEntry::Loaded(buffer_bookmarks));
-        }
-    }
-
-    pub fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
-        worktree::File::from_dyn(buffer.read(cx).file())
-            .map(|file| file.worktree.read(cx).absolutize(&file.path))
-            .map(Arc::<Path>::from)
-    }
-
-    /// Toggle a bookmark at the given anchor in the buffer.
-    /// If a bookmark already exists on the same row, it will be removed.
-    /// Otherwise, a new bookmark will be added.
-    pub fn toggle_bookmark(
-        &mut self,
-        buffer: Entity<Buffer>,
-        anchor: text::Anchor,
-        cx: &mut Context<Self>,
-    ) {
-        let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else {
-            return;
-        };
-
-        self.resolve_anchors_if_needed(&abs_path, &buffer, cx);
-
-        let entry = self
-            .bookmarks
-            .entry(abs_path.clone())
-            .or_insert_with(|| BookmarkEntry::Loaded(BufferBookmarks::new(buffer.clone(), cx)));
-
-        let BookmarkEntry::Loaded(buffer_bookmarks) = entry else {
-            unreachable!("resolve_if_needed should have converted to Loaded");
-        };
-
-        let snapshot = buffer.read(cx).text_snapshot();
-
-        let existing_index = buffer_bookmarks.bookmarks.iter().position(|existing| {
-            existing.0.summary::<Point>(&snapshot).row == anchor.summary::<Point>(&snapshot).row
-        });
-
-        if let Some(index) = existing_index {
-            buffer_bookmarks.bookmarks.remove(index);
-            if buffer_bookmarks.bookmarks.is_empty() {
-                self.bookmarks.remove(&abs_path);
-            }
-        } else {
-            buffer_bookmarks.bookmarks.push(BookmarkAnchor(anchor));
-        }
-
-        cx.notify();
-    }
-
-    /// Returns the bookmarks for a given buffer within an optional range.
-    /// Only returns bookmarks that have been resolved to anchors (loaded).
-    /// Unloaded bookmarks for the given buffer will be resolved first.
-    pub fn bookmarks_for_buffer(
-        &mut self,
-        buffer: Entity<Buffer>,
-        range: Range<text::Anchor>,
-        buffer_snapshot: &BufferSnapshot,
-        cx: &mut Context<Self>,
-    ) -> Vec<BookmarkAnchor> {
-        let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else {
-            return Vec::new();
-        };
-
-        self.resolve_anchors_if_needed(&abs_path, &buffer, cx);
-
-        let Some(BookmarkEntry::Loaded(file_bookmarks)) = self.bookmarks.get(&abs_path) else {
-            return Vec::new();
-        };
-
-        file_bookmarks
-            .bookmarks
-            .iter()
-            .filter_map({
-                move |bookmark| {
-                    if !buffer_snapshot.can_resolve(&bookmark.anchor()) {
-                        return None;
-                    }
-
-                    if bookmark.anchor().cmp(&range.start, buffer_snapshot).is_lt()
-                        || bookmark.anchor().cmp(&range.end, buffer_snapshot).is_gt()
-                    {
-                        return None;
-                    }
-
-                    Some(*bookmark)
-                }
-            })
-            .collect()
-    }
-
-    fn handle_file_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
-        let entity_id = buffer.entity_id();
-
-        if buffer
-            .read(cx)
-            .file()
-            .is_none_or(|f| f.disk_state().is_deleted())
-        {
-            self.bookmarks.retain(|_, entry| match entry {
-                BookmarkEntry::Loaded(buffer_bookmarks) => {
-                    buffer_bookmarks.buffer.entity_id() != entity_id
-                }
-                BookmarkEntry::Unloaded(_) => true,
-            });
-            cx.notify();
-            return;
-        }
-
-        if let Some(new_abs_path) = Self::abs_path_from_buffer(&buffer, cx) {
-            if self.bookmarks.contains_key(&new_abs_path) {
-                return;
-            }
-
-            if let Some(old_path) = self
-                .bookmarks
-                .iter()
-                .find(|(_, entry)| match entry {
-                    BookmarkEntry::Loaded(buffer_bookmarks) => {
-                        buffer_bookmarks.buffer.entity_id() == entity_id
-                    }
-                    BookmarkEntry::Unloaded(_) => false,
-                })
-                .map(|(path, _)| path)
-                .cloned()
-            {
-                let Some(entry) = self.bookmarks.remove(&old_path) else {
-                    log::error!(
-                        "Couldn't get bookmarks from old path during buffer rename handling"
-                    );
-                    return;
-                };
-                self.bookmarks.insert(new_abs_path, entry);
-                cx.notify();
-            }
-        }
-    }
-
-    pub fn all_serialized_bookmarks(
-        &self,
-        cx: &App,
-    ) -> BTreeMap<Arc<Path>, Vec<SerializedBookmark>> {
-        self.bookmarks
-            .iter()
-            .filter_map(|(path, entry)| {
-                let mut rows = match entry {
-                    BookmarkEntry::Unloaded(rows) => rows.clone(),
-                    BookmarkEntry::Loaded(buffer_bookmarks) => {
-                        let snapshot = buffer_bookmarks.buffer.read(cx).snapshot();
-                        buffer_bookmarks
-                            .bookmarks
-                            .iter()
-                            .filter_map(|bookmark| {
-                                if !snapshot.can_resolve(&bookmark.anchor()) {
-                                    return None;
-                                }
-                                let row =
-                                    snapshot.summary_for_anchor::<Point>(&bookmark.anchor()).row;
-                                Some(SerializedBookmark(row))
-                            })
-                            .collect()
-                    }
-                };
-
-                rows.sort();
-                rows.dedup();
-
-                if rows.is_empty() {
-                    None
-                } else {
-                    Some((path.clone(), rows))
-                }
-            })
-            .collect()
-    }
-
-    pub async fn all_bookmark_locations(
-        this: Entity<BookmarkStore>,
-        cx: &mut (impl AppContext + Clone),
-    ) -> Result<HashMap<Entity<Buffer>, Vec<Range<Point>>>> {
-        Self::resolve_all(&this, cx).await?;
-
-        cx.read_entity(&this, |this, cx| {
-            let mut locations: HashMap<_, Vec<_>> = HashMap::new();
-            for bookmarks in this.bookmarks.values().filter_map(BookmarkEntry::loaded) {
-                let snapshot = cx.read_entity(bookmarks.buffer(), |b, _| b.snapshot());
-                let ranges: Vec<Range<Point>> = bookmarks
-                    .bookmarks()
-                    .iter()
-                    .map(|anchor| {
-                        let row = snapshot.summary_for_anchor::<Point>(&anchor.anchor()).row;
-                        Point::row_range(row..row)
-                    })
-                    .collect();
-
-                locations
-                    .entry(bookmarks.buffer().clone())
-                    .or_default()
-                    .extend(ranges);
-            }
-
-            Ok(locations)
-        })
-    }
-
-    /// Opens buffers for all unloaded bookmark entries and resolves them to anchors. This is used to show all bookmarks in a large multi-buffer.
-    async fn resolve_all(this: &Entity<Self>, cx: &mut (impl AppContext + Clone)) -> Result<()> {
-        let unloaded_paths: Vec<Arc<Path>> = cx.read_entity(&this, |this, _| {
-            this.bookmarks
-                .iter()
-                .filter_map(|(path, entry)| match entry {
-                    BookmarkEntry::Unloaded(_) => Some(path.clone()),
-                    BookmarkEntry::Loaded(_) => None,
-                })
-                .collect_vec()
-        });
-
-        if unloaded_paths.is_empty() {
-            return Ok(());
-        }
-
-        let worktree_store = cx.read_entity(&this, |this, _| this.worktree_store.clone());
-        let buffer_store = cx.read_entity(&this, |this, _| this.buffer_store.clone());
-
-        let open_tasks: FuturesUnordered<_> = unloaded_paths
-            .iter()
-            .map(|path| {
-                open_path(path, &worktree_store, &buffer_store, cx.clone())
-                    .map_err(move |e| (path, e))
-                    .map_ok(move |b| (path, b))
-            })
-            .collect();
-
-        let opened: Vec<_> = open_tasks
-            .inspect_err(|(path, error)| {
-                log::warn!(
-                    "Could not open buffer for bookmarked path {}: {error}",
-                    path.display()
-                )
-            })
-            .filter_map(|res| async move { res.ok() })
-            .collect()
-            .await;
-
-        cx.update_entity(&this, |this, cx| {
-            for (path, buffer) in opened {
-                this.resolve_anchors_if_needed(&path, &buffer, cx);
-            }
-            cx.notify();
-        });
-
-        Ok(())
-    }
-
-    pub fn clear_bookmarks(&mut self, cx: &mut Context<Self>) {
-        self.bookmarks.clear();
-        cx.notify();
-    }
-}
-
-async fn open_path(
-    path: &Path,
-    worktree_store: &Entity<WorktreeStore>,
-    buffer_store: &Entity<BufferStore>,
-    mut cx: impl AppContext,
-) -> Result<Entity<Buffer>> {
-    let (worktree, worktree_path) = cx
-        .update_entity(&worktree_store, |worktree_store, cx| {
-            worktree_store.find_or_create_worktree(path, false, cx)
-        })
-        .await?;
-
-    let project_path = ProjectPath {
-        worktree_id: cx.read_entity(&worktree, |worktree, _| worktree.id()),
-        path: worktree_path,
-    };
-
-    let buffer = cx
-        .update_entity(&buffer_store, |buffer_store, cx| {
-            buffer_store.open_buffer(project_path, cx)
-        })
-        .await?;
-
-    Ok(buffer)
-}

crates/project/src/project.rs 🔗

@@ -1,6 +1,5 @@
 pub mod agent_registry_store;
 pub mod agent_server_store;
-pub mod bookmark_store;
 pub mod buffer_store;
 pub mod color_extractor;
 pub mod connection_manager;
@@ -37,7 +36,6 @@ use dap::inline_value::{InlineValueLocation, VariableLookupKind, VariableScope};
 use itertools::{Either, Itertools};
 
 use crate::{
-    bookmark_store::BookmarkStore,
     git_store::GitStore,
     lsp_store::{SymbolLocation, log_store::LogKind},
     project_search::SearchResultsHandle,
@@ -217,7 +215,6 @@ pub struct Project {
     dap_store: Entity<DapStore>,
     agent_server_store: Entity<AgentServerStore>,
 
-    bookmark_store: Entity<BookmarkStore>,
     breakpoint_store: Entity<BreakpointStore>,
     collab_client: Arc<client::Client>,
     join_project_response_message_id: u32,
@@ -1208,9 +1205,6 @@ impl Project {
             cx.subscribe(&buffer_store, Self::on_buffer_store_event)
                 .detach();
 
-            let bookmark_store =
-                cx.new(|_| BookmarkStore::new(worktree_store.clone(), buffer_store.clone()));
-
             let breakpoint_store =
                 cx.new(|_| BreakpointStore::local(worktree_store.clone(), buffer_store.clone()));
 
@@ -1329,7 +1323,6 @@ impl Project {
                 settings_observer,
                 fs,
                 remote_client: None,
-                bookmark_store,
                 breakpoint_store,
                 dap_store,
                 agent_server_store,
@@ -1463,9 +1456,6 @@ impl Project {
             });
             cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
 
-            let bookmark_store =
-                cx.new(|_| BookmarkStore::new(worktree_store.clone(), buffer_store.clone()));
-
             let breakpoint_store = cx.new(|_| {
                 BreakpointStore::remote(
                     REMOTE_SERVER_PROJECT_ID,
@@ -1541,7 +1531,6 @@ impl Project {
                 image_store,
                 lsp_store,
                 context_server_store,
-                bookmark_store,
                 breakpoint_store,
                 dap_store,
                 join_project_response_message_id: 0,
@@ -1724,10 +1713,6 @@ impl Project {
 
         let environment =
             cx.new(|cx| ProjectEnvironment::new(None, worktree_store.downgrade(), None, true, cx));
-
-        let bookmark_store =
-            cx.new(|_| BookmarkStore::new(worktree_store.clone(), buffer_store.clone()));
-
         let breakpoint_store = cx.new(|_| {
             BreakpointStore::remote(
                 remote_id,
@@ -1861,7 +1846,6 @@ impl Project {
                     remote_id,
                     replica_id,
                 },
-                bookmark_store: bookmark_store.clone(),
                 breakpoint_store: breakpoint_store.clone(),
                 dap_store: dap_store.clone(),
                 git_store: git_store.clone(),
@@ -2142,11 +2126,6 @@ impl Project {
         self.dap_store.clone()
     }
 
-    #[inline]
-    pub fn bookmark_store(&self) -> Entity<BookmarkStore> {
-        self.bookmark_store.clone()
-    }
-
     #[inline]
     pub fn breakpoint_store(&self) -> Entity<BreakpointStore> {
         self.breakpoint_store.clone()

crates/project/tests/integration/bookmark_store.rs 🔗

@@ -1,685 +0,0 @@
-use std::{path::Path, sync::Arc};
-
-use collections::BTreeMap;
-use gpui::{Entity, TestAppContext};
-use language::Buffer;
-use project::{Project, bookmark_store::SerializedBookmark};
-use serde_json::json;
-use util::path;
-
-mod integration {
-    use super::*;
-    use fs::Fs as _;
-
-    fn init_test(cx: &mut TestAppContext) {
-        cx.update(|cx| {
-            let settings_store = settings::SettingsStore::test(cx);
-            cx.set_global(settings_store);
-            release_channel::init(semver::Version::new(0, 0, 0), cx);
-        });
-    }
-
-    fn project_path(path: &str) -> Arc<Path> {
-        Arc::from(Path::new(path))
-    }
-
-    async fn open_buffer(
-        project: &Entity<Project>,
-        path: &str,
-        cx: &mut TestAppContext,
-    ) -> Entity<Buffer> {
-        project
-            .update(cx, |project, cx| {
-                project.open_local_buffer(Path::new(path), cx)
-            })
-            .await
-            .unwrap()
-    }
-
-    fn add_bookmarks(
-        project: &Entity<Project>,
-        buffer: &Entity<Buffer>,
-        rows: &[u32],
-        cx: &mut TestAppContext,
-    ) {
-        let buffer = buffer.clone();
-        project.update(cx, |project, cx| {
-            let bookmark_store = project.bookmark_store();
-            let snapshot = buffer.read(cx).snapshot();
-            for &row in rows {
-                let anchor = snapshot.anchor_after(text::Point::new(row, 0));
-                bookmark_store.update(cx, |store, cx| {
-                    store.toggle_bookmark(buffer.clone(), anchor, cx);
-                });
-            }
-        });
-    }
-
-    fn get_all_bookmarks(
-        project: &Entity<Project>,
-        cx: &mut TestAppContext,
-    ) -> BTreeMap<Arc<Path>, Vec<SerializedBookmark>> {
-        project.read_with(cx, |project, cx| {
-            project
-                .bookmark_store()
-                .read(cx)
-                .all_serialized_bookmarks(cx)
-        })
-    }
-
-    fn build_serialized(
-        entries: &[(&str, &[u32])],
-    ) -> BTreeMap<Arc<Path>, Vec<SerializedBookmark>> {
-        let mut map = BTreeMap::new();
-        for &(path_str, rows) in entries {
-            let path = project_path(path_str);
-            map.insert(
-                path.clone(),
-                rows.iter().map(|&row| SerializedBookmark(row)).collect(),
-            );
-        }
-        map
-    }
-
-    async fn restore_bookmarks(
-        project: &Entity<Project>,
-        serialized: BTreeMap<Arc<Path>, Vec<SerializedBookmark>>,
-        cx: &mut TestAppContext,
-    ) {
-        project
-            .update(cx, |project, cx| {
-                project.bookmark_store().update(cx, |store, cx| {
-                    store.load_serialized_bookmarks(serialized, cx)
-                })
-            })
-            .await
-            .expect("with_serialized_bookmarks should succeed");
-    }
-
-    fn clear_bookmarks(project: &Entity<Project>, cx: &mut TestAppContext) {
-        project.update(cx, |project, cx| {
-            project.bookmark_store().update(cx, |store, cx| {
-                store.clear_bookmarks(cx);
-            });
-        });
-    }
-
-    fn assert_bookmark_rows(
-        bookmarks: &BTreeMap<Arc<Path>, Vec<SerializedBookmark>>,
-        path: &str,
-        expected_rows: &[u32],
-    ) {
-        let path = project_path(path);
-        let file_bookmarks = bookmarks
-            .get(&path)
-            .unwrap_or_else(|| panic!("Expected bookmarks for {}", path.display()));
-        let rows: Vec<u32> = file_bookmarks.iter().map(|b| b.0).collect();
-        assert_eq!(rows, expected_rows, "Bookmark rows for {}", path.display());
-    }
-
-    #[gpui::test]
-    async fn test_all_serialized_bookmarks_empty(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.executor().allow_parking();
-
-        let fs = fs::FakeFs::new(cx.executor());
-        fs.insert_tree(path!("/project"), json!({"file1.rs": "line1\nline2\n"}))
-            .await;
-
-        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
-        assert!(get_all_bookmarks(&project, cx).is_empty());
-    }
-
-    #[gpui::test]
-    async fn test_all_serialized_bookmarks_single_file(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.executor().allow_parking();
-
-        let fs = fs::FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project"),
-            json!({"file1.rs": "line1\nline2\nline3\nline4\nline5\n"}),
-        )
-        .await;
-
-        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
-        let buffer = open_buffer(&project, path!("/project/file1.rs"), cx).await;
-
-        add_bookmarks(&project, &buffer, &[0, 2], cx);
-
-        let bookmarks = get_all_bookmarks(&project, cx);
-        assert_eq!(bookmarks.len(), 1);
-        assert_bookmark_rows(&bookmarks, path!("/project/file1.rs"), &[0, 2]);
-    }
-
-    #[gpui::test]
-    async fn test_all_serialized_bookmarks_multiple_files(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.executor().allow_parking();
-
-        let fs = fs::FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project"),
-            json!({
-                "file1.rs": "line1\nline2\nline3\n",
-                "file2.rs": "lineA\nlineB\nlineC\nlineD\n",
-                "file3.rs": "single line"
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
-        let buffer1 = open_buffer(&project, path!("/project/file1.rs"), cx).await;
-        let buffer2 = open_buffer(&project, path!("/project/file2.rs"), cx).await;
-        let _buffer3 = open_buffer(&project, path!("/project/file3.rs"), cx).await;
-
-        add_bookmarks(&project, &buffer1, &[1], cx);
-        add_bookmarks(&project, &buffer2, &[0, 3], cx);
-
-        let bookmarks = get_all_bookmarks(&project, cx);
-        assert_eq!(bookmarks.len(), 2);
-        assert_bookmark_rows(&bookmarks, path!("/project/file1.rs"), &[1]);
-        assert_bookmark_rows(&bookmarks, path!("/project/file2.rs"), &[0, 3]);
-        assert!(
-            !bookmarks.contains_key(&project_path(path!("/project/file3.rs"))),
-            "file3.rs should have no bookmarks"
-        );
-    }
-
-    #[gpui::test]
-    async fn test_all_serialized_bookmarks_after_toggle_off(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.executor().allow_parking();
-
-        let fs = fs::FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project"),
-            json!({"file1.rs": "line1\nline2\nline3\n"}),
-        )
-        .await;
-
-        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
-        let buffer = open_buffer(&project, path!("/project/file1.rs"), cx).await;
-
-        add_bookmarks(&project, &buffer, &[1], cx);
-        assert_eq!(get_all_bookmarks(&project, cx).len(), 1);
-
-        // Toggle same row again to remove it
-        add_bookmarks(&project, &buffer, &[1], cx);
-        assert!(get_all_bookmarks(&project, cx).is_empty());
-    }
-
-    #[gpui::test]
-    async fn test_all_serialized_bookmarks_with_clear(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.executor().allow_parking();
-
-        let fs = fs::FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project"),
-            json!({
-                "file1.rs": "line1\nline2\nline3\n",
-                "file2.rs": "lineA\nlineB\n"
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
-        let buffer1 = open_buffer(&project, path!("/project/file1.rs"), cx).await;
-        let buffer2 = open_buffer(&project, path!("/project/file2.rs"), cx).await;
-
-        add_bookmarks(&project, &buffer1, &[0], cx);
-        add_bookmarks(&project, &buffer2, &[1], cx);
-        assert_eq!(get_all_bookmarks(&project, cx).len(), 2);
-
-        clear_bookmarks(&project, cx);
-        assert!(get_all_bookmarks(&project, cx).is_empty());
-    }
-
-    #[gpui::test]
-    async fn test_all_serialized_bookmarks_returns_sorted_by_path(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.executor().allow_parking();
-
-        let fs = fs::FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project"),
-            json!({"b.rs": "line1\n", "a.rs": "line1\n", "c.rs": "line1\n"}),
-        )
-        .await;
-
-        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
-        let buffer_b = open_buffer(&project, path!("/project/b.rs"), cx).await;
-        let buffer_a = open_buffer(&project, path!("/project/a.rs"), cx).await;
-        let buffer_c = open_buffer(&project, path!("/project/c.rs"), cx).await;
-
-        add_bookmarks(&project, &buffer_b, &[0], cx);
-        add_bookmarks(&project, &buffer_a, &[0], cx);
-        add_bookmarks(&project, &buffer_c, &[0], cx);
-
-        let paths: Vec<_> = get_all_bookmarks(&project, cx).keys().cloned().collect();
-        assert_eq!(
-            paths,
-            [
-                project_path(path!("/project/a.rs")),
-                project_path(path!("/project/b.rs")),
-                project_path(path!("/project/c.rs")),
-            ]
-        );
-    }
-
-    #[gpui::test]
-    async fn test_all_serialized_bookmarks_deduplicates_same_row(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.executor().allow_parking();
-
-        let fs = fs::FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project"),
-            json!({"file1.rs": "line1\nline2\nline3\nline4\n"}),
-        )
-        .await;
-
-        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
-        let buffer = open_buffer(&project, path!("/project/file1.rs"), cx).await;
-
-        add_bookmarks(&project, &buffer, &[1, 2], cx);
-
-        let bookmarks = get_all_bookmarks(&project, cx);
-        assert_bookmark_rows(&bookmarks, path!("/project/file1.rs"), &[1, 2]);
-
-        // Verify no duplicates
-        let rows: Vec<u32> = bookmarks
-            .get(&project_path(path!("/project/file1.rs")))
-            .unwrap()
-            .iter()
-            .map(|b| b.0)
-            .collect();
-        let mut deduped = rows.clone();
-        deduped.dedup();
-        assert_eq!(rows, deduped);
-    }
-
-    #[gpui::test]
-    async fn test_with_serialized_bookmarks_restores_bookmarks(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.executor().allow_parking();
-
-        let fs = fs::FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project"),
-            json!({
-                "file1.rs": "line1\nline2\nline3\nline4\nline5\n",
-                "file2.rs": "aaa\nbbb\nccc\n"
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
-
-        let serialized = build_serialized(&[
-            (path!("/project/file1.rs"), &[0, 3]),
-            (path!("/project/file2.rs"), &[1]),
-        ]);
-
-        restore_bookmarks(&project, serialized, cx).await;
-
-        let restored = get_all_bookmarks(&project, cx);
-        assert_eq!(restored.len(), 2);
-        assert_bookmark_rows(&restored, path!("/project/file1.rs"), &[0, 3]);
-        assert_bookmark_rows(&restored, path!("/project/file2.rs"), &[1]);
-    }
-
-    #[gpui::test]
-    async fn test_with_serialized_bookmarks_skips_out_of_range_rows(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.executor().allow_parking();
-
-        let fs = fs::FakeFs::new(cx.executor());
-        // 3 lines: rows 0, 1, 2
-        fs.insert_tree(
-            path!("/project"),
-            json!({"file1.rs": "line1\nline2\nline3"}),
-        )
-        .await;
-
-        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
-
-        let serialized = build_serialized(&[(path!("/project/file1.rs"), &[1, 100, 2])]);
-        restore_bookmarks(&project, serialized, cx).await;
-
-        // Before resolution, unloaded bookmarks are stored as-is
-        let unresolved = get_all_bookmarks(&project, cx);
-        assert_bookmark_rows(&unresolved, path!("/project/file1.rs"), &[1, 2, 100]);
-
-        // Open the buffer to trigger lazy resolution
-        let buffer = open_buffer(&project, path!("/project/file1.rs"), cx).await;
-        project.update(cx, |project, cx| {
-            let buffer_snapshot = buffer.read(cx).snapshot();
-            project.bookmark_store().update(cx, |store, cx| {
-                store.bookmarks_for_buffer(
-                    buffer.clone(),
-                    buffer_snapshot.anchor_before(0)
-                        ..buffer_snapshot.anchor_after(buffer_snapshot.len()),
-                    &buffer_snapshot,
-                    cx,
-                );
-            });
-        });
-
-        // After resolution, out-of-range rows are filtered
-        let restored = get_all_bookmarks(&project, cx);
-        assert_bookmark_rows(&restored, path!("/project/file1.rs"), &[1, 2]);
-    }
-
-    #[gpui::test]
-    async fn test_with_serialized_bookmarks_skips_empty_entries(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.executor().allow_parking();
-
-        let fs = fs::FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project"),
-            json!({"file1.rs": "line1\nline2\n", "file2.rs": "aaa\nbbb\n"}),
-        )
-        .await;
-
-        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
-
-        let mut serialized = build_serialized(&[(path!("/project/file1.rs"), &[0])]);
-        serialized.insert(project_path(path!("/project/file2.rs")), vec![]);
-
-        restore_bookmarks(&project, serialized, cx).await;
-
-        let restored = get_all_bookmarks(&project, cx);
-        assert_eq!(restored.len(), 1);
-        assert!(restored.contains_key(&project_path(path!("/project/file1.rs"))));
-        assert!(!restored.contains_key(&project_path(path!("/project/file2.rs"))));
-    }
-
-    #[gpui::test]
-    async fn test_with_serialized_bookmarks_all_out_of_range_produces_no_entry(
-        cx: &mut TestAppContext,
-    ) {
-        init_test(cx);
-        cx.executor().allow_parking();
-
-        let fs = fs::FakeFs::new(cx.executor());
-        fs.insert_tree(path!("/project"), json!({"tiny.rs": "x"}))
-            .await;
-
-        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
-
-        let serialized = build_serialized(&[(path!("/project/tiny.rs"), &[5, 10])]);
-        restore_bookmarks(&project, serialized, cx).await;
-
-        // Before resolution, unloaded bookmarks are stored as-is
-        let unresolved = get_all_bookmarks(&project, cx);
-        assert_eq!(unresolved.len(), 1);
-
-        // Open the buffer to trigger lazy resolution
-        let buffer = open_buffer(&project, path!("/project/tiny.rs"), cx).await;
-        project.update(cx, |project, cx| {
-            let buffer_snapshot = buffer.read(cx).snapshot();
-            project.bookmark_store().update(cx, |store, cx| {
-                store.bookmarks_for_buffer(
-                    buffer.clone(),
-                    buffer_snapshot.anchor_before(0)
-                        ..buffer_snapshot.anchor_after(buffer_snapshot.len()),
-                    &buffer_snapshot,
-                    cx,
-                );
-            });
-        });
-
-        // After resolution, all out-of-range rows are filtered away
-        assert!(get_all_bookmarks(&project, cx).is_empty());
-    }
-
-    #[gpui::test]
-    async fn test_with_serialized_bookmarks_replaces_existing(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.executor().allow_parking();
-
-        let fs = fs::FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project"),
-            json!({"file1.rs": "aaa\nbbb\nccc\nddd\n"}),
-        )
-        .await;
-
-        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
-        let buffer = open_buffer(&project, path!("/project/file1.rs"), cx).await;
-
-        add_bookmarks(&project, &buffer, &[0], cx);
-        assert_bookmark_rows(
-            &get_all_bookmarks(&project, cx),
-            path!("/project/file1.rs"),
-            &[0],
-        );
-
-        // Restoring different bookmarks should replace, not merge
-        let serialized = build_serialized(&[(path!("/project/file1.rs"), &[2, 3])]);
-        restore_bookmarks(&project, serialized, cx).await;
-
-        let after = get_all_bookmarks(&project, cx);
-        assert_eq!(after.len(), 1);
-        assert_bookmark_rows(&after, path!("/project/file1.rs"), &[2, 3]);
-    }
-
-    #[gpui::test]
-    async fn test_serialize_deserialize_round_trip(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.executor().allow_parking();
-
-        let fs = fs::FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project"),
-            json!({
-                "alpha.rs": "fn main() {\n    println!(\"hello\");\n    return;\n}\n",
-                "beta.rs": "use std::io;\nfn read() {}\nfn write() {}\n"
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
-        let buffer_alpha = open_buffer(&project, path!("/project/alpha.rs"), cx).await;
-        let buffer_beta = open_buffer(&project, path!("/project/beta.rs"), cx).await;
-
-        add_bookmarks(&project, &buffer_alpha, &[0, 2, 3], cx);
-        add_bookmarks(&project, &buffer_beta, &[1], cx);
-
-        // Serialize
-        let serialized = get_all_bookmarks(&project, cx);
-        assert_eq!(serialized.len(), 2);
-        assert_bookmark_rows(&serialized, path!("/project/alpha.rs"), &[0, 2, 3]);
-        assert_bookmark_rows(&serialized, path!("/project/beta.rs"), &[1]);
-
-        // Clear and restore
-        clear_bookmarks(&project, cx);
-        assert!(get_all_bookmarks(&project, cx).is_empty());
-
-        restore_bookmarks(&project, serialized, cx).await;
-
-        let restored = get_all_bookmarks(&project, cx);
-        assert_eq!(restored.len(), 2);
-        assert_bookmark_rows(&restored, path!("/project/alpha.rs"), &[0, 2, 3]);
-        assert_bookmark_rows(&restored, path!("/project/beta.rs"), &[1]);
-    }
-
-    #[gpui::test]
-    async fn test_round_trip_preserves_bookmarks_after_file_edit(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.executor().allow_parking();
-
-        let fs = fs::FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project"),
-            json!({"file.rs": "aaa\nbbb\nccc\nddd\neee\n"}),
-        )
-        .await;
-
-        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
-        let buffer = open_buffer(&project, path!("/project/file.rs"), cx).await;
-
-        add_bookmarks(&project, &buffer, &[1, 3], cx);
-
-        // Insert a line at the beginning, shifting bookmarks down by 1
-        buffer.update(cx, |buffer, cx| {
-            buffer.edit([(0..0, "new_first_line\n")], None, cx);
-        });
-
-        let serialized = get_all_bookmarks(&project, cx);
-        assert_bookmark_rows(&serialized, path!("/project/file.rs"), &[2, 4]);
-
-        // Clear and restore
-        clear_bookmarks(&project, cx);
-        restore_bookmarks(&project, serialized, cx).await;
-
-        let restored = get_all_bookmarks(&project, cx);
-        assert_bookmark_rows(&restored, path!("/project/file.rs"), &[2, 4]);
-    }
-
-    #[gpui::test]
-    async fn test_file_deletion_removes_bookmarks(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.executor().allow_parking();
-
-        let fs = fs::FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project"),
-            json!({
-                "file1.rs": "aaa\nbbb\nccc\n",
-                "file2.rs": "ddd\neee\nfff\n"
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
-        let buffer1 = open_buffer(&project, path!("/project/file1.rs"), cx).await;
-        let buffer2 = open_buffer(&project, path!("/project/file2.rs"), cx).await;
-
-        add_bookmarks(&project, &buffer1, &[0, 2], cx);
-        add_bookmarks(&project, &buffer2, &[1], cx);
-        assert_eq!(get_all_bookmarks(&project, cx).len(), 2);
-
-        // Delete file1.rs
-        fs.remove_file(path!("/project/file1.rs").as_ref(), Default::default())
-            .await
-            .unwrap();
-        cx.executor().run_until_parked();
-
-        // file1.rs bookmarks should be gone, file2.rs bookmarks preserved
-        let bookmarks = get_all_bookmarks(&project, cx);
-        assert_eq!(bookmarks.len(), 1);
-        assert!(!bookmarks.contains_key(&project_path(path!("/project/file1.rs"))));
-        assert_bookmark_rows(&bookmarks, path!("/project/file2.rs"), &[1]);
-    }
-
-    #[gpui::test]
-    async fn test_deleting_all_bookmarked_files_clears_store(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.executor().allow_parking();
-
-        let fs = fs::FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project"),
-            json!({
-                "file1.rs": "aaa\nbbb\n",
-                "file2.rs": "ccc\nddd\n"
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
-        let buffer1 = open_buffer(&project, path!("/project/file1.rs"), cx).await;
-        let buffer2 = open_buffer(&project, path!("/project/file2.rs"), cx).await;
-
-        add_bookmarks(&project, &buffer1, &[0], cx);
-        add_bookmarks(&project, &buffer2, &[1], cx);
-        assert_eq!(get_all_bookmarks(&project, cx).len(), 2);
-
-        // Delete both files
-        fs.remove_file(path!("/project/file1.rs").as_ref(), Default::default())
-            .await
-            .unwrap();
-        fs.remove_file(path!("/project/file2.rs").as_ref(), Default::default())
-            .await
-            .unwrap();
-        cx.executor().run_until_parked();
-
-        assert!(get_all_bookmarks(&project, cx).is_empty());
-    }
-
-    #[gpui::test]
-    async fn test_file_rename_re_keys_bookmarks(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.executor().allow_parking();
-
-        let fs = fs::FakeFs::new(cx.executor());
-        fs.insert_tree(path!("/project"), json!({"old_name.rs": "aaa\nbbb\nccc\n"}))
-            .await;
-
-        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
-        let buffer = open_buffer(&project, path!("/project/old_name.rs"), cx).await;
-
-        add_bookmarks(&project, &buffer, &[0, 2], cx);
-        assert_bookmark_rows(
-            &get_all_bookmarks(&project, cx),
-            path!("/project/old_name.rs"),
-            &[0, 2],
-        );
-
-        // Rename the file
-        fs.rename(
-            path!("/project/old_name.rs").as_ref(),
-            path!("/project/new_name.rs").as_ref(),
-            Default::default(),
-        )
-        .await
-        .unwrap();
-        cx.executor().run_until_parked();
-
-        let bookmarks = get_all_bookmarks(&project, cx);
-        assert_eq!(bookmarks.len(), 1);
-        assert!(!bookmarks.contains_key(&project_path(path!("/project/old_name.rs"))));
-        assert_bookmark_rows(&bookmarks, path!("/project/new_name.rs"), &[0, 2]);
-    }
-
-    #[gpui::test]
-    async fn test_file_rename_preserves_other_bookmarks(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.executor().allow_parking();
-
-        let fs = fs::FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project"),
-            json!({
-                "rename_me.rs": "aaa\nbbb\n",
-                "untouched.rs": "ccc\nddd\neee\n"
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
-        let buffer_rename = open_buffer(&project, path!("/project/rename_me.rs"), cx).await;
-        let buffer_other = open_buffer(&project, path!("/project/untouched.rs"), cx).await;
-
-        add_bookmarks(&project, &buffer_rename, &[1], cx);
-        add_bookmarks(&project, &buffer_other, &[0, 2], cx);
-
-        fs.rename(
-            path!("/project/rename_me.rs").as_ref(),
-            path!("/project/renamed.rs").as_ref(),
-            Default::default(),
-        )
-        .await
-        .unwrap();
-        cx.executor().run_until_parked();
-
-        let bookmarks = get_all_bookmarks(&project, cx);
-        assert_eq!(bookmarks.len(), 2);
-        assert_bookmark_rows(&bookmarks, path!("/project/renamed.rs"), &[1]);
-        assert_bookmark_rows(&bookmarks, path!("/project/untouched.rs"), &[0, 2]);
-    }
-}

crates/settings/src/vscode_import.rs 🔗

@@ -331,7 +331,6 @@ impl VsCodeSettings {
             min_line_number_digits: None,
             runnables: None,
             breakpoints: None,
-            bookmarks: None,
             folds: self.read_enum("editor.showFoldingControls", |s| match s {
                 "always" | "mouseover" => Some(true),
                 "never" => Some(false),

crates/settings_content/src/editor.rs 🔗

@@ -451,10 +451,6 @@ pub struct GutterContent {
     ///
     /// Default: true
     pub breakpoints: Option<bool>,
-    /// Whether to show bookmarks in the gutter.
-    ///
-    /// Default: true
-    pub bookmarks: Option<bool>,
     /// Whether to show fold buttons in the gutter.
     ///
     /// Default: true

crates/settings_ui/src/page_data.rs 🔗

@@ -1893,7 +1893,7 @@ fn editor_page() -> SettingsPage {
         ]
     }
 
-    fn gutter_section() -> [SettingsPageItem; 9] {
+    fn gutter_section() -> [SettingsPageItem; 8] {
         [
             SettingsPageItem::SectionHeader("Gutter"),
             SettingsPageItem::SettingItem(SettingItem {
@@ -1978,29 +1978,6 @@ fn editor_page() -> SettingsPage {
                 metadata: None,
                 files: USER,
             }),
-            SettingsPageItem::SettingItem(SettingItem {
-                title: "Show Bookmarks",
-                description: "Show bookmarks in the gutter.",
-                field: Box::new(SettingField {
-                    json_path: Some("gutter.bookmarks"),
-                    pick: |settings_content| {
-                        settings_content
-                            .editor
-                            .gutter
-                            .as_ref()
-                            .and_then(|gutter| gutter.bookmarks.as_ref())
-                    },
-                    write: |settings_content, value| {
-                        settings_content
-                            .editor
-                            .gutter
-                            .get_or_insert_default()
-                            .bookmarks = value;
-                    },
-                }),
-                metadata: None,
-                files: USER,
-            }),
             SettingsPageItem::SettingItem(SettingItem {
                 title: "Show Folds",
                 description: "Show code folding controls in the gutter.",

crates/workspace/src/persistence.rs 🔗

@@ -21,7 +21,6 @@ use db::{
 };
 use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size};
 use project::{
-    bookmark_store::SerializedBookmark,
     debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
     trusted_worktrees::{DbTrustedPaths, RemoteHostLocation},
 };
@@ -375,39 +374,6 @@ pub async fn write_default_dock_state(
     Ok(())
 }
 
-#[derive(Debug)]
-pub struct Bookmark {
-    pub row: u32,
-}
-
-impl sqlez::bindable::StaticColumnCount for Bookmark {
-    fn column_count() -> usize {
-        // row
-        1
-    }
-}
-
-impl sqlez::bindable::Bind for Bookmark {
-    fn bind(
-        &self,
-        statement: &sqlez::statement::Statement,
-        start_index: i32,
-    ) -> anyhow::Result<i32> {
-        statement.bind(&self.row, start_index)
-    }
-}
-
-impl Column for Bookmark {
-    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
-        let row = statement
-            .column_int(start_index)
-            .with_context(|| format!("Failed to read bookmark at index {start_index}"))?
-            as u32;
-
-        Ok((Bookmark { row }, start_index + 1))
-    }
-}
-
 #[derive(Debug)]
 pub struct Breakpoint {
     pub position: u32,
@@ -1014,16 +980,6 @@ impl Domain for WorkspaceDb {
         sql!(
             ALTER TABLE remote_connections ADD COLUMN remote_env TEXT;
         ),
-        sql!(
-            CREATE TABLE bookmarks (
-                workspace_id INTEGER NOT NULL,
-                path TEXT NOT NULL,
-                row INTEGER NOT NULL,
-                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
-                ON DELETE CASCADE
-                ON UPDATE CASCADE
-            );
-        ),
     ];
 
     // Allow recovering from bad migration that was initially shipped to nightly
@@ -1158,7 +1114,6 @@ impl WorkspaceDb {
             display,
             docks,
             session_id: None,
-            bookmarks: self.bookmarks(workspace_id),
             breakpoints: self.breakpoints(workspace_id),
             window_id,
             user_toolchains: self.user_toolchains(workspace_id, remote_connection_id),
@@ -1249,47 +1204,12 @@ impl WorkspaceDb {
             display,
             docks,
             session_id: None,
-            bookmarks: self.bookmarks(workspace_id),
             breakpoints: self.breakpoints(workspace_id),
             window_id,
             user_toolchains: self.user_toolchains(workspace_id, remote_connection_id),
         })
     }
 
-    fn bookmarks(&self, workspace_id: WorkspaceId) -> BTreeMap<Arc<Path>, Vec<SerializedBookmark>> {
-        let bookmarks: Result<Vec<(PathBuf, Bookmark)>> = self
-            .select_bound(sql! {
-                SELECT path, row
-                FROM bookmarks
-                WHERE workspace_id = ?
-                ORDER BY path, row
-            })
-            .and_then(|mut prepared_statement| (prepared_statement)(workspace_id));
-
-        match bookmarks {
-            Ok(bookmarks) => {
-                if bookmarks.is_empty() {
-                    log::debug!("Bookmarks are empty after querying database for them");
-                }
-
-                let mut map: BTreeMap<_, Vec<_>> = BTreeMap::default();
-
-                for (path, bookmark) in bookmarks {
-                    let path: Arc<Path> = path.into();
-                    map.entry(path.clone())
-                        .or_default()
-                        .push(SerializedBookmark(bookmark.row))
-                }
-
-                map
-            }
-            Err(e) => {
-                log::error!("Failed to load bookmarks: {}", e);
-                BTreeMap::default()
-            }
-        }
-    }
-
     fn breakpoints(&self, workspace_id: WorkspaceId) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
         let breakpoints: Result<Vec<(PathBuf, Breakpoint)>> = self
             .select_bound(sql! {
@@ -1431,21 +1351,6 @@ impl WorkspaceDb {
                     DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
                     .context("Clearing old panes")?;
 
-                conn.exec_bound(
-                    sql!(
-                        DELETE FROM bookmarks WHERE workspace_id = ?1;
-                    )
-                )?(workspace.id).context("Clearing old bookmarks")?;
-
-                for (path, bookmarks) in workspace.bookmarks {
-                    for bookmark in bookmarks {
-                        conn.exec_bound(sql!(
-                            INSERT INTO bookmarks (workspace_id, path, row)
-                            VALUES (?1, ?2, ?3);
-                        ))?((workspace.id, path.as_ref(), bookmark.0)).context("Inserting bookmark")?;
-                    }
-                }
-
                 conn.exec_bound(
                     sql!(
                         DELETE FROM breakpoints WHERE workspace_id = ?1;
@@ -2760,7 +2665,6 @@ mod tests {
             display: Default::default(),
             docks: Default::default(),
             centered_layout: false,
-            bookmarks: Default::default(),
             breakpoints: {
                 let mut map = collections::BTreeMap::default();
                 map.insert(
@@ -2916,7 +2820,6 @@ mod tests {
             display: Default::default(),
             docks: Default::default(),
             centered_layout: false,
-            bookmarks: Default::default(),
             breakpoints: {
                 let mut map = collections::BTreeMap::default();
                 map.insert(
@@ -2965,7 +2868,6 @@ mod tests {
             display: Default::default(),
             docks: Default::default(),
             centered_layout: false,
-            bookmarks: Default::default(),
             breakpoints: collections::BTreeMap::default(),
             session_id: None,
             window_id: None,
@@ -3064,7 +2966,6 @@ mod tests {
             display: Default::default(),
             docks: Default::default(),
             centered_layout: false,
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             session_id: None,
             window_id: None,
@@ -3080,7 +2981,6 @@ mod tests {
             display: Default::default(),
             docks: Default::default(),
             centered_layout: false,
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             session_id: None,
             window_id: None,
@@ -3185,7 +3085,6 @@ mod tests {
             location: SerializedWorkspaceLocation::Local,
             center_group,
             window_bounds: Default::default(),
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             display: Default::default(),
             docks: Default::default(),
@@ -3220,7 +3119,6 @@ mod tests {
             location: SerializedWorkspaceLocation::Local,
             center_group: Default::default(),
             window_bounds: Default::default(),
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             display: Default::default(),
             docks: Default::default(),
@@ -3239,7 +3137,6 @@ mod tests {
             display: Default::default(),
             docks: Default::default(),
             centered_layout: false,
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             session_id: None,
             window_id: Some(2),
@@ -3279,7 +3176,6 @@ mod tests {
             location: SerializedWorkspaceLocation::Local,
             center_group: Default::default(),
             window_bounds: Default::default(),
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             display: Default::default(),
             docks: Default::default(),
@@ -3321,7 +3217,6 @@ mod tests {
             display: Default::default(),
             docks: Default::default(),
             centered_layout: false,
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             session_id: Some("session-id-1".to_owned()),
             window_id: Some(10),
@@ -3337,7 +3232,6 @@ mod tests {
             display: Default::default(),
             docks: Default::default(),
             centered_layout: false,
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             session_id: Some("session-id-1".to_owned()),
             window_id: Some(20),
@@ -3353,7 +3247,6 @@ mod tests {
             display: Default::default(),
             docks: Default::default(),
             centered_layout: false,
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             session_id: Some("session-id-2".to_owned()),
             window_id: Some(30),
@@ -3369,7 +3262,6 @@ mod tests {
             display: Default::default(),
             docks: Default::default(),
             centered_layout: false,
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             session_id: None,
             window_id: None,
@@ -3396,7 +3288,6 @@ mod tests {
             display: Default::default(),
             docks: Default::default(),
             centered_layout: false,
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             session_id: Some("session-id-2".to_owned()),
             window_id: Some(50),
@@ -3409,7 +3300,6 @@ mod tests {
             location: SerializedWorkspaceLocation::Local,
             center_group: Default::default(),
             window_bounds: Default::default(),
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             display: Default::default(),
             docks: Default::default(),
@@ -3469,7 +3359,6 @@ mod tests {
             window_bounds: Default::default(),
             display: Default::default(),
             docks: Default::default(),
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             centered_layout: false,
             session_id: None,
@@ -3513,7 +3402,6 @@ mod tests {
             docks: Default::default(),
             centered_layout: false,
             session_id: Some("one-session".to_owned()),
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             window_id: Some(window_id),
             user_toolchains: Default::default(),
@@ -3626,7 +3514,6 @@ mod tests {
             docks: Default::default(),
             centered_layout: false,
             session_id: Some("one-session".to_owned()),
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             window_id: Some(window_id),
             user_toolchains: Default::default(),
@@ -3986,7 +3873,6 @@ mod tests {
             window_bounds: None,
             display: None,
             docks: Default::default(),
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             centered_layout: false,
             session_id: None,
@@ -4065,7 +3951,6 @@ mod tests {
                 docks: Default::default(),
                 centered_layout: false,
                 session_id: Some("test-session".to_owned()),
-                bookmarks: Default::default(),
                 breakpoints: Default::default(),
                 window_id: Some(*window_id),
                 user_toolchains: Default::default(),
@@ -4351,7 +4236,6 @@ mod tests {
             docks: Default::default(),
             centered_layout: false,
             session_id: Some(session_id.clone()),
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             window_id: Some(99),
             user_toolchains: Default::default(),
@@ -4447,7 +4331,6 @@ mod tests {
             docks: Default::default(),
             centered_layout: false,
             session_id: Some(session_id.to_owned()),
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             window_id: Some(window_id_val),
             user_toolchains: Default::default(),
@@ -4464,7 +4347,6 @@ mod tests {
             docks: Default::default(),
             centered_layout: false,
             session_id: Some(session_id.to_owned()),
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             window_id: Some(window_id_val),
             user_toolchains: Default::default(),
@@ -4543,7 +4425,6 @@ mod tests {
             docks: Default::default(),
             centered_layout: false,
             session_id: Some(session_id.clone()),
-            bookmarks: Default::default(),
             breakpoints: Default::default(),
             window_id: Some(88),
             user_toolchains: Default::default(),

crates/workspace/src/persistence/model.rs 🔗

@@ -12,11 +12,9 @@ use db::sqlez::{
 };
 use gpui::{AsyncWindowContext, Entity, WeakEntity, WindowId};
 
+use crate::ProjectGroupKey;
 use language::{Toolchain, ToolchainScope};
-use project::{
-    Project, ProjectGroupKey, bookmark_store::SerializedBookmark,
-    debugger::breakpoint_store::SourceBreakpoint,
-};
+use project::{Project, debugger::breakpoint_store::SourceBreakpoint};
 use remote::RemoteConnectionOptions;
 use serde::{Deserialize, Serialize};
 use std::{
@@ -136,7 +134,6 @@ pub(crate) struct SerializedWorkspace {
     pub(crate) display: Option<Uuid>,
     pub(crate) docks: DockStructure,
     pub(crate) session_id: Option<String>,
-    pub(crate) bookmarks: BTreeMap<Arc<Path>, Vec<SerializedBookmark>>,
     pub(crate) breakpoints: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
     pub(crate) user_toolchains: BTreeMap<ToolchainScope, IndexSet<Toolchain>>,
     pub(crate) window_id: Option<u64>,

crates/workspace/src/workspace.rs 🔗

@@ -262,8 +262,6 @@ actions!(
         ActivatePreviousWindow,
         /// Adds a folder to the current project.
         AddFolderToProject,
-        /// Clears all bookmarks in the project.
-        ClearBookmarks,
         /// Clears all notifications.
         ClearAllNotifications,
         /// Clears all navigation history, including forward/backward navigation, recently opened files, and recently closed tabs. **This action is irreversible**.
@@ -6603,13 +6601,6 @@ impl Workspace {
 
         match self.workspace_location(cx) {
             WorkspaceLocation::Location(location, paths) => {
-                let bookmarks = self.project.update(cx, |project, cx| {
-                    project
-                        .bookmark_store()
-                        .read(cx)
-                        .all_serialized_bookmarks(cx)
-                });
-
                 let breakpoints = self.project.update(cx, |project, cx| {
                     project
                         .breakpoint_store()
@@ -6636,7 +6627,6 @@ impl Workspace {
                     docks,
                     centered_layout: self.centered_layout,
                     session_id: self.session_id.clone(),
-                    bookmarks,
                     breakpoints,
                     window_id: Some(window.window_handle().window_id().as_u64()),
                     user_toolchains,
@@ -6846,15 +6836,6 @@ impl Workspace {
                 cx.notify();
             })?;
 
-            project
-                .update(cx, |project, cx| {
-                    project.bookmark_store().update(cx, |bookmark_store, cx| {
-                        bookmark_store.load_serialized_bookmarks(serialized_workspace.bookmarks, cx)
-                    })
-                })
-                .await
-                .log_err();
-
             let _ = project
                 .update(cx, |project, cx| {
                     project
@@ -7327,7 +7308,6 @@ impl Workspace {
             .on_action(cx.listener(|workspace, _: &FocusCenterPane, window, cx| {
                 workspace.focus_center_pane(window, cx);
             }))
-            .on_action(cx.listener(Workspace::clear_bookmarks))
             .on_action(cx.listener(Workspace::cancel))
     }
 
@@ -7455,15 +7435,6 @@ impl Workspace {
         cx.notify();
     }
 
-    pub fn clear_bookmarks(&mut self, _: &ClearBookmarks, _: &mut Window, cx: &mut Context<Self>) {
-        self.project()
-            .read(cx)
-            .bookmark_store()
-            .update(cx, |bookmark_store, cx| {
-                bookmark_store.clear_bookmarks(cx);
-            });
-    }
-
     fn adjust_padding(padding: Option<f32>) -> f32 {
         padding
             .unwrap_or(CenteredPaddingSettings::default().0)

crates/zed/src/visual_test_runner.rs 🔗

@@ -1148,11 +1148,11 @@ fn run_breakpoint_hover_visual_tests(
     //
     // The breakpoint hover requires multiple steps:
     // 1. Draw to register mouse listeners
-    // 2. Mouse move to trigger gutter_hovered and create GutterHoverButton
+    // 2. Mouse move to trigger gutter_hovered and create PhantomBreakpointIndicator
     // 3. Wait 200ms for is_active to become true
     // 4. Draw again to render the indicator
     //
-    // The gutter_position should be in the gutter area to trigger the gutter hover button.
+    // The gutter_position should be in the gutter area to trigger the phantom breakpoint.
     // The button_position should be directly over the breakpoint icon button for tooltip hover.
     // Based on debug output: button is at origin=(3.12, 66.5) with size=(14, 16)
     let gutter_position = point(px(30.0), px(85.0));