diff --git a/assets/icons/bookmark.svg b/assets/icons/bookmark.svg deleted file mode 100644 index 999c9b72a1ead51fe9e899d6a6946e7840bfe742..0000000000000000000000000000000000000000 --- a/assets/icons/bookmark.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/settings/default.json b/assets/settings/default.json index 335dc7543ccd171d0cc91deee364f945186d3868..584c4c4d49d573be8ca600edde638c428bace3e6 100644 --- a/assets/settings/default.json +++ b/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. diff --git a/codebook.toml b/codebook.toml deleted file mode 100644 index 57cdd2569c350bdf35a27195aa6ab86a1b08ab83..0000000000000000000000000000000000000000 --- a/codebook.toml +++ /dev/null @@ -1 +0,0 @@ -words = ["breakpoint"] diff --git a/crates/agent_ui/src/entry_view_state.rs b/crates/agent_ui/src/entry_view_state.rs index 8543b3c96199e7b303971b476ffaade9484384a7..415cd1f3db19df29895d7dd984e7ac4fb4a7b47b 100644 --- a/crates/agent_ui/src/entry_view_state.rs +++ b/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); diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index f4aadb7433f143ba0bcd18e494e4b838da1f5894..48dbd43dd449911a0c40f9e943ad917843941c02 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/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 = diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index c364cdd244752afdda203911653e8a60e54b7871..2c653217716b0218cff0b60eb2bce4ac1ce02e5d 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/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); diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index d1c53203329d738bfd96b2fc0ac89446d7cdcc54..c541257b6d219b56a611f8a3711da287109ef48d 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/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); diff --git a/crates/edit_prediction_ui/src/rate_prediction_modal.rs b/crates/edit_prediction_ui/src/rate_prediction_modal.rs index 6ff7c0e2e46efe1142414e9e5717e1607323636c..eb071bf955cede173e74993c93ab5cd294338474 100644 --- a/crates/edit_prediction_ui/src/rate_prediction_modal.rs +++ b/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); diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 7524c5b01bf090be3661d1af03f918aa3a7449fb..4c0623006953c0aae9c718b2e894ba85b26074e0 100644 --- a/crates/editor/src/actions.rs +++ b/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. diff --git a/crates/editor/src/bookmarks.rs b/crates/editor/src/bookmarks.rs deleted file mode 100644 index fe047e7fa18c22f4dc90e665f1a24c1ee53f1fda..0000000000000000000000000000000000000000 --- a/crates/editor/src/bookmarks.rs +++ /dev/null @@ -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.show_bookmarks = Some(show_bookmarks); - cx.notify(); - } - - pub fn toggle_bookmark( - &mut self, - _: &ToggleBookmark, - window: &mut Window, - cx: &mut Context, - ) { - 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::(&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) { - 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) { - 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.go_to_bookmark_impl(Direction::Next, window, cx); - } - - pub fn go_to_previous_bookmark( - &mut self, - _: &GoToPreviousBookmark, - window: &mut Window, - cx: &mut Context, - ) { - self.go_to_bookmark_impl(Direction::Prev, window, cx); - } - - fn go_to_bookmark_impl( - &mut self, - direction: Direction, - window: &mut Window, - cx: &mut Context, - ) { - let Some(project) = &self.project else { - return; - }; - let Some(bookmark_store) = &self.bookmark_store else { - return; - }; - - let selection = self - .selections - .newest::(&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, - ) { - 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, - multi_buffer_snapshot: &MultiBufferSnapshot, - project: &Entity, - bookmark_store: &Entity, - cx: &mut Context, - ) -> Vec { - 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::>() - }) - .collect() - } -} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 784c05371630d8201396cab3b797cfab037258f0..f3dfef45783240042675667af4207797ae65b743 100644 --- a/crates/editor/src/editor.rs +++ b/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, show_code_actions: Option, show_runnables: Option, - show_bookmarks: Option, show_breakpoints: Option, show_diff_review_button: bool, show_wrap_guides: Option, @@ -1300,9 +1297,8 @@ pub struct Editor { last_position_map: Option>, expect_bounds_change: Option>, runnables: RunnableData, - bookmark_store: Option>, breakpoint_store: Option>, - gutter_hover_button: (Option, Option>), + gutter_breakpoint_indicator: (Option, Option>), pub(crate) gutter_diff_review_indicator: (Option, Option>), pub(crate) diff_review_drag_state: Option, /// Active diff review overlays. Multiple overlays can be open simultaneously @@ -1408,7 +1404,6 @@ pub struct EditorSnapshot { show_code_actions: Option, show_runnables: Option, show_breakpoints: Option, - show_bookmarks: Option, git_blame_gutter_max_author_length: Option, pub display_snapshot: DisplaySnapshot, pub placeholder_display_snapshot: Option, @@ -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, - window: &mut Window, - cx: &mut Context, - ) -> HashSet { - 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, - window: &mut Window, - cx: &mut Context, - ) -> HashSet { - 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) -> 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, ) -> 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, - ) -> 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, buffer: &Entity, @@ -12164,7 +11924,7 @@ impl Editor { } } - fn set_gutter_context_menu( + fn set_breakpoint_context_menu( &mut self, display_row: DisplayRow, position: Option, @@ -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, - ) -> Option { - 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, - ) -> Option { - 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::(&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::(&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; diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index e70dd137ba382049b59691d4252e76ae75cb66d0..548053da7d794de83d99afdfddb098e4cfb2b18e 100644 --- a/crates/editor/src/editor_settings.rs +++ b/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(), }, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 20eb17a2da9ba848030f1fbf26d22bf72498d68b..38f4259b4d4c87f3243e49dec8f35991bd82f246 100644 --- a/crates/editor/src/editor_tests.rs +++ b/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, - editor: Entity, - 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 { - 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, Vec> { - 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) { - 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 = 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::(&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::(&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] diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5d246b7f6ba241d437cbca25b913f300359a2786..392a67bb0d495c3d49063bd4aa6ec87ea6bfd610 100644 --- a/crates/editor/src/element.rs +++ b/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, + line_height: Pixels, + range: Range, + scroll_position: gpui::Point, + gutter_dimensions: &GutterDimensions, + gutter_hitbox: &Hitbox, + snapshot: &EditorSnapshot, + breakpoints: HashMap)>, + row_infos: &[RowInfo], window: &mut Window, cx: &mut App, ) -> Vec { @@ -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 { - 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)>, - window: &mut Window, - cx: &mut App, - ) -> Vec { - 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, - breakpoints: &HashMap)>, + line_height: Pixels, + range: Range, + row_infos: &[RowInfo], + scroll_position: gpui::Point, + gutter_dimensions: &GutterDimensions, + gutter_hitbox: &Hitbox, + snapshot: &EditorSnapshot, + breakpoints: &mut HashMap)>, window: &mut Window, cx: &mut App, ) -> Vec { @@ -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, + rows: Range, + buffer_rows: &[RowInfo], active_rows: &BTreeMap, current_selection_head: Option, + snapshot: &EditorSnapshot, window: &mut Window, cx: &mut App, ) -> Arc> { - 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 = 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, - scroll_position: gpui::Point, - 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 { - 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 { - 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, breadcrumb_font: Option, @@ -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, + 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, selections: Vec<(PlayerColor, Vec)>, test_indicators: Vec, - bookmarks: Vec, breakpoints: Vec, diff_review_button: Option, crease_toggles: Vec>, @@ -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::>(), &BTreeMap::default(), Some(DisplayRow(0)), + &snapshot, window, cx, ) @@ -12688,28 +12635,35 @@ mod tests { assert_eq!(relative_rows[&DisplayRow(1)], 4); assert_eq!(relative_rows[&DisplayRow(2)], 3); - let gutter = Gutter { - row_infos: &(0..6) - .map(|row| RowInfo { - buffer_row: Some(row), - diff_status: (row == DELETED_LINE).then(|| { - DiffHunkStatus::deleted( - buffer_diff::DiffHunkSecondaryStatus::NoSecondaryHunk, - ) - }), - ..Default::default() - }) - .collect::>(), - ..test_gutter(line_height, &snapshot) - }; - const DELETED_LINE: u32 = 3; let layouts = cx .update_window(*window, |_, window, cx| { element.layout_line_numbers( - &gutter, + 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), + diff_status: (row == DELETED_LINE).then(|| { + DiffHunkStatus::deleted( + buffer_diff::DiffHunkSecondaryStatus::NoSecondaryHunk, + ) + }), + ..Default::default() + }) + .collect::>(), &BTreeMap::default(), Some(DisplayRow(0)), + &snapshot, window, cx, ) @@ -12768,9 +12722,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::>(), &BTreeMap::default(), Some(DisplayRow(3)), + &snapshot, window, cx, ) @@ -12825,9 +12796,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::>(), &BTreeMap::default(), Some(DisplayRow(0)), + &snapshot, window, cx, ) @@ -12857,20 +12845,29 @@ mod tests { let layouts = cx .update_window(*window, |_, window, cx| { element.layout_line_numbers( - &Gutter { - row_infos: &(0..6) - .map(|row| RowInfo { - buffer_row: Some(row), - diff_status: Some(DiffHunkStatus::deleted( - buffer_diff::DiffHunkSecondaryStatus::NoSecondaryHunk, - )), - ..Default::default() - }) - .collect::>(), - ..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), + diff_status: Some(DiffHunkStatus::deleted( + buffer_diff::DiffHunkSecondaryStatus::NoSecondaryHunk, + )), + ..Default::default() + }) + .collect::>(), &BTreeMap::from_iter([(DisplayRow(0), LineHighlightSpec::default())]), Some(DisplayRow(0)), + &snapshot, window, cx, ) diff --git a/crates/editor/src/runnables.rs b/crates/editor/src/runnables.rs index dc97a3ea310b5b519c13887996c3bb3c0d6274a8..f451eb7d61d6a2513e1ebf6ec96062b600cbecb6 100644 --- a/crates/editor/src/runnables.rs +++ b/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, row: DisplayRow, + breakpoint: Option<(Anchor, Breakpoint, Option)>, cx: &mut Context, ) -> 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); })) } diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index b3783fe2b70862f04f3eda970e772243a32f5504..aac44c7f9c6eaf6f18c72bea390c0a0b7ad1a4bd 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/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); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index f73a59358c3b06b187fd7357b5351f557d2fd68c..dadc5a6247dc69d1b12fab08b253e1cb1564bd84 100644 --- a/crates/gpui/src/window.rs +++ b/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 diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 568244912460ca750c7e904bca6b903488b0305c..20d7b609d8de07c4de4c489eac90b312fbf9c210 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -48,7 +48,6 @@ pub enum IconName { BellRing, Binary, Blocks, - Bookmark, BoltFilled, BoltOutlined, Book, diff --git a/crates/inspector_ui/src/div_inspector.rs b/crates/inspector_ui/src/div_inspector.rs index e36a72f8ffd15a1edf006db8390e39025eb07ea0..135c8f22116498fbc0db43c88928a365e5607ce5 100644 --- a/crates/inspector_ui/src/div_inspector.rs +++ b/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); diff --git a/crates/language_tools/src/lsp_log_view.rs b/crates/language_tools/src/lsp_log_view.rs index 59571040eac6281fa2b2032a655dafabfa345f0a..97f0676d250cac2cee54b307e7c07d894d3d3128 100644 --- a/crates/language_tools/src/lsp_log_view.rs +++ b/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); diff --git a/crates/project/src/bookmark_store.rs b/crates/project/src/bookmark_store.rs deleted file mode 100644 index 841fb04af2440944ff6bcec4dcf479fa4fc82b6f..0000000000000000000000000000000000000000 --- a/crates/project/src/bookmark_store.rs +++ /dev/null @@ -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, - bookmarks: Vec, - _subscription: Subscription, -} - -impl BufferBookmarks { - pub fn new(buffer: Entity, cx: &mut Context) -> 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 { - &self.buffer - } - - pub fn bookmarks(&self) -> &[BookmarkAnchor] { - &self.bookmarks - } -} - -#[derive(Debug)] -pub enum BookmarkEntry { - Loaded(BufferBookmarks), - Unloaded(Vec), -} - -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, - worktree_store: Entity, - bookmarks: BTreeMap, BookmarkEntry>, -} - -impl BookmarkStore { - pub fn new(worktree_store: Entity, buffer_store: Entity) -> Self { - Self { - buffer_store, - worktree_store, - bookmarks: BTreeMap::new(), - } - } - - pub fn load_serialized_bookmarks( - &mut self, - bookmark_rows: BTreeMap, Vec>, - cx: &mut Context, - ) -> Task> { - 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, - buffer: &Entity, - cx: &mut Context, - ) { - 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 = 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, cx: &App) -> Option> { - worktree::File::from_dyn(buffer.read(cx).file()) - .map(|file| file.worktree.read(cx).absolutize(&file.path)) - .map(Arc::::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, - anchor: text::Anchor, - cx: &mut Context, - ) { - 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::(&snapshot).row == anchor.summary::(&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, - range: Range, - buffer_snapshot: &BufferSnapshot, - cx: &mut Context, - ) -> Vec { - 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, cx: &mut Context) { - 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, Vec> { - 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::(&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, - cx: &mut (impl AppContext + Clone), - ) -> Result, Vec>>> { - 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> = bookmarks - .bookmarks() - .iter() - .map(|anchor| { - let row = snapshot.summary_for_anchor::(&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, cx: &mut (impl AppContext + Clone)) -> Result<()> { - let unloaded_paths: Vec> = 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.bookmarks.clear(); - cx.notify(); - } -} - -async fn open_path( - path: &Path, - worktree_store: &Entity, - buffer_store: &Entity, - mut cx: impl AppContext, -) -> Result> { - 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) -} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a8bd461d3d94839d5222164ef88d536abc1bcaf4..bb128388ae3bd7f29d3bd39260a56eaf38adbebc 100644 --- a/crates/project/src/project.rs +++ b/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, agent_server_store: Entity, - bookmark_store: Entity, breakpoint_store: Entity, collab_client: Arc, 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 { - self.bookmark_store.clone() - } - #[inline] pub fn breakpoint_store(&self) -> Entity { self.breakpoint_store.clone() diff --git a/crates/project/tests/integration/bookmark_store.rs b/crates/project/tests/integration/bookmark_store.rs deleted file mode 100644 index 3b84a0c76b65931e42b93ac8f29df4242d55f006..0000000000000000000000000000000000000000 --- a/crates/project/tests/integration/bookmark_store.rs +++ /dev/null @@ -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 { - Arc::from(Path::new(path)) - } - - async fn open_buffer( - project: &Entity, - path: &str, - cx: &mut TestAppContext, - ) -> Entity { - project - .update(cx, |project, cx| { - project.open_local_buffer(Path::new(path), cx) - }) - .await - .unwrap() - } - - fn add_bookmarks( - project: &Entity, - buffer: &Entity, - 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, - cx: &mut TestAppContext, - ) -> BTreeMap, Vec> { - project.read_with(cx, |project, cx| { - project - .bookmark_store() - .read(cx) - .all_serialized_bookmarks(cx) - }) - } - - fn build_serialized( - entries: &[(&str, &[u32])], - ) -> BTreeMap, Vec> { - 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, - serialized: BTreeMap, Vec>, - 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, 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, Vec>, - 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 = 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 = 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]); - } -} diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index fa7454e5e16f17e4bbdebc87d90fa780c996724e..575c021c3db5c0a8ce1984376e0f00a355977468 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -1,6 +1,5 @@ #![allow(clippy::format_collect)] -mod bookmark_store; mod color_extractor; mod context_server_store; mod debugger; diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 74084421455a9aa4f6032a03810289744a3dd928..c83e56577373aa9834f76b3c32488a069844d249 100644 --- a/crates/settings/src/vscode_import.rs +++ b/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), diff --git a/crates/settings_content/src/editor.rs b/crates/settings_content/src/editor.rs index d6cdf751fdfd413cf234b59bb7e4c32566e3a125..60c2686c084ba428992dfc82a9c18b6c24860a66 100644 --- a/crates/settings_content/src/editor.rs +++ b/crates/settings_content/src/editor.rs @@ -451,10 +451,6 @@ pub struct GutterContent { /// /// Default: true pub breakpoints: Option, - /// Whether to show bookmarks in the gutter. - /// - /// Default: true - pub bookmarks: Option, /// Whether to show fold buttons in the gutter. /// /// Default: true diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 80f2714960a17497bd7556c1b8df6ce90ddc4fa9..0f5679b85f80c418ecc677349689878e7322597a 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/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.", diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index b1617fbc623314a5e27c4c1070b099ceff143600..4dde067c1f74e8eb7570435c587bfba90bea146c 100644 --- a/crates/workspace/src/persistence.rs +++ b/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 { - 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, Vec> { - let bookmarks: Result> = 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.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, Vec> { let breakpoints: Result> = 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(), diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index f0f14cdb591053ef9a048d4fbe4274a5212a9595..c1bfcefc17a4c2735acaebf1fbae3a6b5852ce90 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/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, pub(crate) docks: DockStructure, pub(crate) session_id: Option, - pub(crate) bookmarks: BTreeMap, Vec>, pub(crate) breakpoints: BTreeMap, Vec>, pub(crate) user_toolchains: BTreeMap>, pub(crate) window_id: Option, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 79c7734c379b4c433062b0bb351a6b6c17d8f9ae..d5544dc1aa6ad6bb2fbbd0777eecc393d0fae22c 100644 --- a/crates/workspace/src/workspace.rs +++ b/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.project() - .read(cx) - .bookmark_store() - .update(cx, |bookmark_store, cx| { - bookmark_store.clear_bookmarks(cx); - }); - } - fn adjust_padding(padding: Option) -> f32 { padding .unwrap_or(CenteredPaddingSettings::default().0) diff --git a/crates/zed/src/visual_test_runner.rs b/crates/zed/src/visual_test_runner.rs index 3e07140adcc97d783ce7f04f21b0986f92c6ecc4..d4c0d29ade5c4bd6496509675f9ccb3fc188eb8f 100644 --- a/crates/zed/src/visual_test_runner.rs +++ b/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));