Add named bookmark support (#57491)

Adds support for named bookmarks while preserving the existing unnamed
bookmark workflow.

Main changes:
- Keeps `ToggleBookmark` as the quick unnamed bookmark action.
- Adds `ToggleNamedBookmark`, which prompts for a bookmark name when
adding a new bookmark and removes an existing bookmark on the same line,
including unnamed ones.
- Adds `EditBookmark` support for renaming existing bookmarks.
- Persists bookmark names in workspace storage.
- Adds a project bookmarks picker that supports filtering by bookmark
name and relative path, with highlighted matches.
- Updates bookmark storage and tests to carry bookmark names through
serialization, restoration, navigation, and project-level listing.

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #ISSUE

Video
toggle:


https://github.com/user-attachments/assets/a725b9cf-ac37-4ebc-85cc-ee820452c043

rename:


https://github.com/user-attachments/assets/a6d0d0c1-511b-4fb7-ae5d-22c55d1a075f


Release Notes:

- Add named bookmark support

---------

Co-authored-by: Yara 🏳️‍⚧️ <git@yara.blue>
This commit is contained in:
guopenghui 2026-06-23 04:47:44 +08:00 committed by GitHub
parent 471fb15d7a
commit f6bfa0915f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 758 additions and 192 deletions

View file

@ -856,6 +856,8 @@ actions!(
Backtab,
/// Toggles a bookmark at the current line.
ToggleBookmark,
/// Edits the bookmark's label at the current line.
EditBookmark,
/// Toggles a breakpoint at the current line.
ToggleBreakpoint,
/// Toggles the case of selected text.

View file

@ -1,6 +1,7 @@
use std::ops::Range;
use gpui::Entity;
use language::Buffer;
use multi_buffer::{Anchor, MultiBufferOffset, MultiBufferSnapshot, ToOffset as _};
use project::{Project, bookmark_store::BookmarkStore};
use rope::Point;
@ -11,11 +12,30 @@ use workspace::{Workspace, searchable::Direction};
use crate::display_map::DisplayRow;
use crate::{
Editor, GoToNextBookmark, GoToPreviousBookmark, MultibufferSelectionMode, SelectionEffects,
ToggleBookmark, ViewBookmarks, scroll::Autoscroll,
EditBookmark, Editor, GoToNextBookmark, GoToPreviousBookmark, MultibufferSelectionMode,
SelectionEffects, ToggleBookmark, ViewBookmarks, scroll::Autoscroll,
};
#[derive(Clone, Debug)]
struct BookmarkTarget {
buffer: Entity<Buffer>,
anchor: Anchor,
buffer_anchor: text::Anchor,
}
impl Editor {
fn bookmark_exists_for_target(
bookmark_store: &Entity<BookmarkStore>,
target: &BookmarkTarget,
cx: &mut Context<Self>,
) -> bool {
bookmark_store.update(cx, |bookmark_store, cx| {
bookmark_store
.find_bookmark(&target.buffer, target.buffer_anchor, cx)
.is_some()
})
}
pub fn set_show_bookmarks(&mut self, show_bookmarks: bool, cx: &mut Context<Self>) {
self.show_bookmarks = Some(show_bookmarks);
cx.notify();
@ -41,6 +61,9 @@ impl Editor {
selections.sort_by_key(|s| s.head());
selections.dedup_by_key(|s| s.head().row);
let mut exist_targets: Vec<BookmarkTarget> = vec![];
let mut absent_targets: Vec<BookmarkTarget> = vec![];
for selection in &selections {
let head = selection.head();
let multibuffer_anchor = multi_buffer_snapshot.anchor_before(Point::new(head.row, 0));
@ -50,43 +73,52 @@ impl Editor {
{
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);
});
let target = BookmarkTarget {
buffer,
anchor: multibuffer_anchor,
buffer_anchor,
};
if Self::bookmark_exists_for_target(&bookmark_store, &target, cx) {
exist_targets.push(target);
} else {
absent_targets.push(target);
}
}
}
}
if absent_targets.is_empty() {
// All cursors are on existing bookmarks, remove all bookmarks.
self.toggle_bookmarks(exist_targets, String::new(), cx);
} else {
// Only add new ones and leave existing ones unchanged.
self.add_toggle_bookmark_blocks(absent_targets, bookmark_store, window, cx);
}
cx.notify();
}
pub fn toggle_bookmark_at_row(&mut self, row: DisplayRow, cx: &mut Context<Self>) {
let Some(bookmark_store) = &self.bookmark_store else {
return;
};
pub fn toggle_bookmark_at_row(
&mut self,
row: DisplayRow,
window: &mut Window,
cx: &mut Context<Self>,
) {
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();
self.toggle_bookmark_at_anchor(anchor, window, cx);
}
pub fn toggle_bookmark_at_anchor(&mut self, anchor: Anchor, cx: &mut Context<Self>) {
let Some(bookmark_store) = &self.bookmark_store else {
return;
};
pub fn toggle_bookmark_at_anchor(
&mut self,
anchor: Anchor,
window: &mut Window,
cx: &mut Context<Self>,
) {
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let Some((position, _)) = buffer_snapshot.anchor_to_buffer_anchor(anchor) else {
return;
@ -95,13 +127,142 @@ impl Editor {
return;
};
bookmark_store.update(cx, |bookmark_store, cx| {
bookmark_store.toggle_bookmark(buffer, position, cx);
});
let Some(bookmark_store) = self.bookmark_store.clone() else {
return;
};
let target = BookmarkTarget {
buffer,
anchor,
buffer_anchor: position,
};
if Self::bookmark_exists_for_target(&bookmark_store, &target, cx) {
bookmark_store.update(cx, |bookmark_store, cx| {
bookmark_store.toggle_bookmark(target.buffer, position, String::new(), cx);
});
} else {
self.add_toggle_bookmark_blocks(vec![target], bookmark_store, window, cx)
}
cx.notify();
}
pub fn edit_bookmark(&mut self, _: &EditBookmark, window: &mut Window, cx: &mut Context<Self>) {
let snapshot = self.snapshot(window, cx);
let multi_buffer_snapshot = snapshot.buffer_snapshot();
let selection = self
.selections
.newest::<Point>(&snapshot.display_snapshot)
.head();
let anchor = multi_buffer_snapshot.anchor_before(Point::new(selection.row, 0));
self.edit_bookmark_at_anchor(anchor, window, cx);
}
pub fn edit_bookmark_at_anchor(
&mut self,
anchor: Anchor,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(bookmark_store) = self.bookmark_store.clone() else {
return;
};
let Some(project) = self.project() else {
return;
};
let editor_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let Some((buffer_anchor, _)) = editor_buffer_snapshot.anchor_to_buffer_anchor(anchor)
else {
return;
};
let Some(buffer) = project.read(cx).buffer_for_id(buffer_anchor.buffer_id, cx) else {
return;
};
let Some(label) = bookmark_store.update(cx, |store, cx| {
store
.find_bookmark(&buffer, buffer_anchor, cx)
.map(|bookmark| bookmark.label.clone())
}) else {
return;
};
self.add_edit_bookmark_block(
BookmarkTarget {
anchor,
buffer,
buffer_anchor,
},
&label,
bookmark_store,
window,
cx,
);
}
fn add_edit_bookmark_block(
&mut self,
target: BookmarkTarget,
label: &str,
bookmark_store: Entity<BookmarkStore>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.add_edit_block(
target.anchor,
label,
"Enter bookmark label (Optional)",
Some(Box::new(move |label, _, cx| {
bookmark_store.update(cx, |store, cx| {
store.edit_bookmark(&target.buffer, target.buffer_anchor, label, cx)
});
})),
None,
window,
cx,
);
}
fn add_toggle_bookmark_blocks(
&mut self,
targets: Vec<BookmarkTarget>,
bookmark_store: Entity<BookmarkStore>,
window: &mut Window,
cx: &mut Context<Self>,
) {
for target in targets {
let bookmark_store = bookmark_store.clone();
self.add_edit_block(
target.anchor,
"",
"Enter bookmark label (Optional)",
Some(Box::new(move |label: String, _, cx| {
bookmark_store.update(cx, |store, cx| {
store.toggle_bookmark(target.buffer, target.buffer_anchor, label, cx);
});
})),
None,
window,
cx,
);
}
}
fn toggle_bookmarks(
&mut self,
targets: Vec<BookmarkTarget>,
label: String,
cx: &mut Context<Self>,
) {
if let Some(bookmark_store) = self.bookmark_store.clone() {
bookmark_store.update(cx, |store, cx| {
for target in targets {
store.toggle_bookmark(target.buffer, target.buffer_anchor, label.clone(), cx);
}
});
}
}
pub fn go_to_next_bookmark(
&mut self,
_: &GoToNextBookmark,
@ -233,9 +394,7 @@ impl Editor {
)
})
.into_iter()
.filter_map(|bookmark| {
multi_buffer_snapshot.anchor_in_buffer(bookmark.anchor())
})
.filter_map(|bookmark| multi_buffer_snapshot.anchor_in_buffer(bookmark.anchor))
.collect::<Vec<_>>()
})
.collect()

View file

@ -3939,7 +3939,7 @@ impl Editor {
});
for bookmark in bookmarks {
let Some(multi_buffer_anchor) =
multi_buffer_snapshot.anchor_in_buffer(bookmark.anchor())
multi_buffer_snapshot.anchor_in_buffer(bookmark.anchor)
else {
continue;
};
@ -3961,8 +3961,8 @@ impl Editor {
.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_click(cx.listener(move |editor, _, window, cx| {
editor.toggle_bookmark_at_row(row, window, cx);
}))
.on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
editor.set_gutter_context_menu(row, None, event.position(), window, cx);
@ -4100,6 +4100,7 @@ impl Editor {
} else {
"Add Bookmark"
};
let has_bookmark = bookmark.as_ref().is_some();
let run_to_cursor = window.is_action_available(&RunToCursor, cx);
@ -4250,12 +4251,28 @@ impl Editor {
}
})
.separator()
.entry(set_bookmark_msg, None, move |_window, cx| {
weak_editor
.update(cx, |this, cx| {
this.toggle_bookmark_at_anchor(anchor, cx);
})
.log_err();
.entry(set_bookmark_msg, Some(ToggleBookmark.boxed_clone()), {
let weak_editor = weak_editor.clone();
move |window, cx| {
weak_editor
.update(cx, |this, cx| {
this.toggle_bookmark_at_anchor(anchor, window, cx);
})
.log_err();
}
})
.when(has_bookmark, |this| {
this.entry(
"Edit Bookmark",
Some(EditBookmark.boxed_clone()),
move |window, cx| {
weak_editor
.update(cx, |this, cx| {
this.edit_bookmark_at_anchor(anchor, window, cx);
})
.log_err();
},
)
})
})
}
@ -4435,7 +4452,7 @@ impl Editor {
};
match intent {
Intent::SetBookmark => editor.toggle_bookmark_at_row(row, cx),
Intent::SetBookmark => editor.toggle_bookmark_at_row(row, window, cx),
Intent::SetBreakpoint => editor.edit_breakpoint_at_anchor(
position,
Breakpoint::new_standard(),
@ -5751,24 +5768,29 @@ impl Editor {
);
}
fn add_edit_breakpoint_block(
fn add_edit_block(
&mut self,
anchor: Anchor,
breakpoint: &Breakpoint,
edit_action: BreakpointPromptEditAction,
base_text: &str,
placeholder_text: &str,
confirm: Option<PromptEditorCallback>,
cancel: Option<PromptEditorCallback>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let weak_editor = cx.weak_entity();
let bp_prompt = cx.new(|cx| {
BreakpointPromptEditor::new(
weak_editor,
anchor,
breakpoint.clone(),
edit_action,
window,
cx,
)
let mut prompt_editor =
PromptEditor::new(weak_editor, placeholder_text, base_text, window, cx);
if let Some(callback) = confirm {
prompt_editor = prompt_editor.on_confirm(callback);
}
if let Some(callback) = cancel {
prompt_editor = prompt_editor.on_cancel(callback);
}
prompt_editor
});
let height = bp_prompt.update(cx, |this, cx| {
@ -5796,6 +5818,61 @@ impl Editor {
});
}
fn add_edit_breakpoint_block(
&mut self,
anchor: Anchor,
breakpoint: &Breakpoint,
edit_action: BreakpointPromptEditAction,
window: &mut Window,
cx: &mut Context<Self>,
) {
let base_text: &str = match edit_action {
BreakpointPromptEditAction::Log => breakpoint.message.as_ref(),
BreakpointPromptEditAction::Condition => breakpoint.condition.as_ref(),
BreakpointPromptEditAction::HitCondition => breakpoint.hit_condition.as_ref(),
}
.map(|msg| msg.as_ref())
.unwrap_or_default();
let placeholder_text = match edit_action {
BreakpointPromptEditAction::Log => {
"Message to log when a breakpoint is hit. Expressions within {} are interpolated."
}
BreakpointPromptEditAction::Condition => {
"Condition when a breakpoint is hit. Expressions within {} are interpolated."
}
BreakpointPromptEditAction::HitCondition => "How many breakpoint hits to ignore",
};
let breakpoint = breakpoint.clone();
self.add_edit_block(
anchor,
base_text,
placeholder_text,
Some(Box::new(move |message: String, editor: &mut Self, cx| {
editor.edit_breakpoint_at_anchor(
anchor,
breakpoint,
match edit_action {
BreakpointPromptEditAction::Log => {
BreakpointEditAction::EditLogMessage(message.into())
}
BreakpointPromptEditAction::Condition => {
BreakpointEditAction::EditCondition(message.into())
}
BreakpointPromptEditAction::HitCondition => {
BreakpointEditAction::EditHitCondition(message.into())
}
},
cx,
);
})),
None,
window,
cx,
);
}
pub(crate) fn breakpoint_at_row(
&self,
row: u32,
@ -5901,13 +5978,13 @@ impl Editor {
.first()
.and_then(|bookmark| {
let bookmark_row = buffer_snapshot
.summary_for_anchor::<text::PointUtf16>(&bookmark.anchor())
.summary_for_anchor::<text::PointUtf16>(&bookmark.anchor)
.row;
if bookmark_row == row {
snapshot
.buffer_snapshot()
.anchor_in_excerpt(bookmark.anchor())
.anchor_in_excerpt(bookmark.anchor)
} else {
None
}
@ -12048,42 +12125,35 @@ fn collapse_multiline_range(range: Range<Point>) -> Range<Point> {
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
#[derive(Copy, Clone, Debug)]
enum BreakpointPromptEditAction {
Log,
Condition,
HitCondition,
}
struct BreakpointPromptEditor {
type PromptEditorCallback = Box<dyn FnOnce(String, &mut Editor, &mut Context<Editor>) + 'static>;
struct PromptEditor {
pub(crate) prompt: Entity<Editor>,
editor: WeakEntity<Editor>,
breakpoint_anchor: Anchor,
breakpoint: Breakpoint,
edit_action: BreakpointPromptEditAction,
confirm_callback: Option<PromptEditorCallback>,
cancel_callback: Option<PromptEditorCallback>,
block_ids: HashSet<CustomBlockId>,
editor_margins: Arc<Mutex<EditorMargins>>,
_subscriptions: Vec<Subscription>,
}
impl BreakpointPromptEditor {
impl PromptEditor {
const MAX_LINES: u8 = 4;
fn new(
editor: WeakEntity<Editor>,
breakpoint_anchor: Anchor,
breakpoint: Breakpoint,
edit_action: BreakpointPromptEditAction,
placeholder_text: &str,
base_text: &str,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let base_text = match edit_action {
BreakpointPromptEditAction::Log => breakpoint.message.as_ref(),
BreakpointPromptEditAction::Condition => breakpoint.condition.as_ref(),
BreakpointPromptEditAction::HitCondition => breakpoint.hit_condition.as_ref(),
}
.map(|msg| msg.to_string())
.unwrap_or_default();
let buffer = cx.new(|cx| Buffer::local(base_text, cx));
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
@ -12100,15 +12170,7 @@ impl BreakpointPromptEditor {
);
prompt.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
prompt.set_show_cursor_when_unfocused(false, cx);
prompt.set_placeholder_text(
match edit_action {
BreakpointPromptEditAction::Log => "Message to log when a breakpoint is hit. Expressions within {} are interpolated.",
BreakpointPromptEditAction::Condition => "Condition when a breakpoint is hit. Expressions within {} are interpolated.",
BreakpointPromptEditAction::HitCondition => "How many breakpoint hits to ignore",
},
window,
cx,
);
prompt.set_placeholder_text(placeholder_text, window, cx);
prompt
});
@ -12116,15 +12178,24 @@ impl BreakpointPromptEditor {
Self {
prompt,
editor,
breakpoint_anchor,
breakpoint,
edit_action,
confirm_callback: None,
cancel_callback: None,
editor_margins: Arc::new(Mutex::new(EditorMargins::default())),
block_ids: Default::default(),
_subscriptions: vec![],
}
}
fn on_confirm(mut self, confirm: PromptEditorCallback) -> Self {
self.confirm_callback = Some(confirm);
self
}
fn on_cancel(mut self, cancel: PromptEditorCallback) -> Self {
self.cancel_callback = Some(cancel);
self
}
pub(crate) fn add_block_ids(&mut self, block_ids: Vec<CustomBlockId>) {
self.block_ids.extend(block_ids)
}
@ -12137,28 +12208,15 @@ impl BreakpointPromptEditor {
.buffer
.read(cx)
.as_singleton()
.expect("A multi buffer in breakpoint prompt isn't possible")
.expect("A multi buffer in prompt isn't possible")
.read(cx)
.as_rope()
.to_string();
editor.update(cx, |editor, cx| {
editor.edit_breakpoint_at_anchor(
self.breakpoint_anchor,
self.breakpoint.clone(),
match self.edit_action {
BreakpointPromptEditAction::Log => {
BreakpointEditAction::EditLogMessage(message.into())
}
BreakpointPromptEditAction::Condition => {
BreakpointEditAction::EditCondition(message.into())
}
BreakpointPromptEditAction::HitCondition => {
BreakpointEditAction::EditHitCondition(message.into())
}
},
cx,
);
if let Some(confirm) = self.confirm_callback.take() {
confirm(message, editor, cx);
}
editor.remove_blocks(self.block_ids.clone(), None, cx);
cx.focus_self(window);
@ -12169,6 +12227,21 @@ impl BreakpointPromptEditor {
fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
self.editor
.update(cx, |editor, cx| {
let message = self
.prompt
.read(cx)
.buffer
.read(cx)
.as_singleton()
.expect("A multi buffer in prompt isn't possible")
.read(cx)
.as_rope()
.to_string();
if let Some(cancel) = self.cancel_callback.take() {
cancel(message, editor, cx);
}
editor.remove_blocks(self.block_ids.clone(), None, cx);
window.focus(&editor.focus_handle, cx);
})
@ -12228,7 +12301,7 @@ impl BreakpointPromptEditor {
}
}
impl Render for BreakpointPromptEditor {
impl Render for PromptEditor {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
let editor_margins = *self.editor_margins.lock();
@ -12273,7 +12346,7 @@ impl Render for BreakpointPromptEditor {
}
}
impl Focusable for BreakpointPromptEditor {
impl Focusable for PromptEditor {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.prompt.focus_handle(cx)
}

View file

@ -28853,6 +28853,10 @@ impl BookmarkTestContext {
})
}
fn assert_bookmarked_file_count(&self, expected_count: usize) {
assert_eq!(expected_count, self.all_bookmarks().len());
}
fn assert_bookmark_rows(&self, expected_rows: Vec<u32>) {
let abs_path = self.abs_path();
let bookmarks = self.all_bookmarks();
@ -28867,20 +28871,134 @@ impl BookmarkTestContext {
.get(&abs_path)
.unwrap()
.iter()
.map(|b| b.0)
.map(|b| b.row)
.collect();
rows.sort();
assert_eq!(expected_rows, rows);
}
}
fn cursor_row(&mut self) -> u32 {
fn assert_bookmark_labels(&self, expected_labels: Vec<(u32, &str)>) {
let abs_path = self.abs_path();
let bookmarks = self.all_bookmarks();
let mut labels: Vec<(u32, &str)> = bookmarks
.get(&abs_path)
.unwrap()
.iter()
.map(|bookmark| (bookmark.row, bookmark.label.as_str()))
.collect();
labels.sort_by_key(|(row, _)| *row);
assert_eq!(expected_labels, labels);
}
fn confirm_action_available(&mut self) -> bool {
self.cx
.update(|window, cx| window.is_action_available(&menu::Confirm, cx))
}
fn select_rows(&mut self, rows: &[u32]) {
assert!(!rows.is_empty(), "expected at least one row to select");
self.editor
.update_in(&mut self.cx, |editor: &mut Editor, window, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges(
rows.iter()
.copied()
.map(|row| Point::new(row, 0)..Point::new(row, 0)),
)
});
});
}
fn prompt_blocks(&mut self) -> Vec<(DisplayRow, u32)> {
self.editor.update(&mut self.cx, |editor, cx| {
let snapshot = editor.display_snapshot(cx);
editor.selections.newest::<Point>(&snapshot).head().row
let max_row = snapshot.max_point().row().next_row();
snapshot
.blocks_in_range(DisplayRow(0)..max_row)
.filter_map(|(row, block)| match block {
crate::display_map::Block::Custom(_) => Some((row, block.height())),
_ => None,
})
.collect()
})
}
fn assert_prompt_block_count(&mut self, expected_count: usize) {
assert_eq!(expected_count, self.prompt_blocks().len());
}
fn draw_window(&mut self) {
self.cx.update(|window, cx| {
window.refresh();
let _ = window.draw(cx);
});
}
fn focus_bookmark_prompt_block(&mut self, block_index: usize) {
self.draw_window();
let prompt_blocks = self.prompt_blocks();
let (block_row, block_height) = *prompt_blocks
.get(block_index)
.expect("expected bookmark prompt block");
let click_position =
self.editor
.update_in(&mut self.cx, |editor: &mut Editor, window, cx| {
let snapshot = editor.snapshot(window, cx);
let block_top = DisplayPoint::new(block_row, 0);
let relative_block_top = editor
.display_to_pixel_point(block_top, &snapshot, window, cx)
.expect("expected prompt block to be visible");
let line_height = editor
.style(cx)
.text
.line_height_in_pixels(window.rem_size());
let editor_origin = editor
.last_position_map
.as_ref()
.expect("expected editor position map")
.text_hitbox
.bounds
.origin;
let editor_center_x = editor
.last_bounds
.expect("expected editor bounds")
.center()
.x;
gpui::Point {
x: editor_center_x,
y: editor_origin.y
+ relative_block_top.y
+ line_height * (block_height as f32 / 2.),
}
});
self.cx
.simulate_click(click_position, gpui::Modifiers::none());
self.cx.run_until_parked();
assert!(
self.confirm_action_available(),
"expected bookmark prompt block to be focused"
);
}
fn confirm_bookmark_prompt_at_block_index(&mut self, block_index: usize, label: &str) {
// Confirming a PromptEditor returns focus to the parent editor, so each remaining
// prompt block must be focused explicitly before typing into it.
self.focus_bookmark_prompt_block(block_index);
self.confirm_bookmark_prompt(label);
}
fn cursor_row(&mut self) -> u32 {
self.cursor_point().row
}
fn cursor_point(&mut self) -> Point {
self.editor.update(&mut self.cx, |editor, cx| {
let snapshot = editor.display_snapshot(cx);
@ -28905,10 +29023,23 @@ impl BookmarkTestContext {
});
}
fn confirm_bookmark_prompt(&mut self, label: &str) {
if !label.is_empty() {
self.cx.simulate_input(label);
}
self.cx.dispatch_action(menu::Confirm);
self.cx.run_until_parked();
}
fn add_bookmark_with_label(&mut self, label: &str) {
self.toggle_bookmark();
self.confirm_bookmark_prompt(label);
}
fn toggle_bookmarks_at_rows(&mut self, rows: &[u32]) {
for &row in rows {
self.move_to_row(row);
self.toggle_bookmark();
self.add_bookmark_with_label("");
}
}
@ -28932,32 +29063,26 @@ async fn test_bookmark_toggling(cx: &mut TestAppContext) {
let mut ctx =
BookmarkTestContext::new("First line\nSecond line\nThird line\nFourth line", cx).await;
ctx.add_bookmark_with_label("");
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);
});
ctx.add_bookmark_with_label("");
assert_eq!(1, ctx.all_bookmarks().len());
ctx.assert_bookmarked_file_count(1);
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);
});
ctx.move_to_row(0);
ctx.toggle_bookmark();
assert_eq!(1, ctx.all_bookmarks().len());
ctx.assert_bookmarked_file_count(1);
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);
});
ctx.move_to_row(3);
ctx.toggle_bookmark();
assert_eq!(0, ctx.all_bookmarks().len());
ctx.assert_bookmarked_file_count(0);
ctx.assert_bookmark_rows(vec![]);
}
@ -28966,29 +29091,51 @@ 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.select_rows(&[0, 1, 2]);
ctx.toggle_bookmark();
assert_eq!(1, ctx.all_bookmarks().len());
ctx.assert_bookmark_rows(vec![0, 1, 2, 3]);
ctx.assert_prompt_block_count(3);
ctx.assert_bookmarked_file_count(0);
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);
});
ctx.confirm_bookmark_prompt_at_block_index(0, "first label");
ctx.assert_prompt_block_count(2);
ctx.confirm_bookmark_prompt_at_block_index(0, "second label");
ctx.assert_prompt_block_count(1);
ctx.confirm_bookmark_prompt_at_block_index(0, "third label");
ctx.assert_prompt_block_count(0);
assert_eq!(0, ctx.all_bookmarks().len());
ctx.assert_bookmarked_file_count(1);
ctx.assert_bookmark_labels(vec![
(0, "first label"),
(1, "second label"),
(2, "third label"),
]);
ctx.select_rows(&[0, 1, 2, 3]);
ctx.toggle_bookmark();
ctx.assert_prompt_block_count(1);
ctx.assert_bookmark_labels(vec![
(0, "first label"),
(1, "second label"),
(2, "third label"),
]);
ctx.confirm_bookmark_prompt_at_block_index(0, "fourth label");
ctx.assert_prompt_block_count(0);
ctx.assert_bookmark_labels(vec![
(0, "first label"),
(1, "second label"),
(2, "third label"),
(3, "fourth label"),
]);
ctx.select_rows(&[0, 1, 2, 3]);
ctx.toggle_bookmark();
ctx.assert_prompt_block_count(0);
ctx.assert_bookmarked_file_count(0);
ctx.assert_bookmark_rows(vec![]);
}
@ -29000,8 +29147,8 @@ async fn test_bookmark_toggle_deduplicates_by_row(cx: &mut TestAppContext) {
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.add_bookmark_with_label("");
ctx.assert_bookmark_rows(vec![0]);
@ -29014,8 +29161,8 @@ async fn test_bookmark_toggle_deduplicates_by_row(cx: &mut TestAppContext) {
window,
cx,
);
editor.toggle_bookmark(&actions::ToggleBookmark, window, cx);
});
ctx.toggle_bookmark();
ctx.assert_bookmark_rows(vec![]);
}
@ -29026,7 +29173,7 @@ async fn test_bookmark_survives_edits(cx: &mut TestAppContext) {
BookmarkTestContext::new("First line\nSecond line\nThird line\nFourth line", cx).await;
ctx.move_to_row(2);
ctx.toggle_bookmark();
ctx.add_bookmark_with_label("");
ctx.assert_bookmark_rows(vec![2]);
ctx.editor
@ -29090,6 +29237,45 @@ async fn test_bookmark_not_available_in_single_line_editor(cx: &mut TestAppConte
});
}
#[gpui::test]
async fn test_edit_bookmark_does_not_open_prompt_without_existing_bookmark(
cx: &mut TestAppContext,
) {
let mut ctx =
BookmarkTestContext::new("First line\nSecond line\nThird line\nFourth line", cx).await;
assert!(!ctx.confirm_action_available());
ctx.editor
.update_in(&mut ctx.cx, |editor: &mut Editor, window, cx| {
editor.edit_bookmark(&actions::EditBookmark, window, cx);
});
assert!(!ctx.confirm_action_available());
ctx.assert_bookmark_rows(vec![]);
}
#[gpui::test]
async fn test_edit_bookmark_updates_label_after_confirmation(cx: &mut TestAppContext) {
let mut ctx =
BookmarkTestContext::new("First line\nSecond line\nThird line\nFourth line", cx).await;
ctx.add_bookmark_with_label("old label");
ctx.assert_bookmark_labels(vec![(0, "old label")]);
ctx.editor
.update_in(&mut ctx.cx, |editor: &mut Editor, window, cx| {
editor.edit_bookmark(&actions::EditBookmark, window, cx);
});
assert!(ctx.confirm_action_available());
ctx.cx.dispatch_action(SelectAll);
ctx.cx.simulate_input("new label");
ctx.cx.dispatch_action(menu::Confirm);
ctx.assert_bookmark_labels(vec![(0, "new label")]);
}
#[gpui::test]
async fn test_bookmark_navigation_lands_at_column_zero(cx: &mut TestAppContext) {
let mut ctx =
@ -29114,7 +29300,7 @@ async fn test_bookmark_navigation_lands_at_column_zero(cx: &mut TestAppContext)
"Cursor should be at the 11th column before toggling bookmark, got column {column_before_toggle}"
);
ctx.toggle_bookmark();
ctx.add_bookmark_with_label("");
ctx.editor
.update_in(&mut ctx.cx, |editor: &mut Editor, window, cx| {
@ -29149,8 +29335,8 @@ async fn test_bookmark_set_from_nonzero_column_toggles_off_from_column_zero(
window,
cx,
);
editor.toggle_bookmark(&actions::ToggleBookmark, window, cx);
});
ctx.add_bookmark_with_label("");
ctx.assert_bookmark_rows(vec![1]);
@ -29164,8 +29350,8 @@ async fn test_bookmark_set_from_nonzero_column_toggles_off_from_column_zero(
window,
cx,
);
editor.toggle_bookmark(&actions::ToggleBookmark, window, cx);
});
ctx.toggle_bookmark();
ctx.assert_bookmark_rows(vec![]);
}

View file

@ -504,6 +504,7 @@ impl EditorElement {
register_action(editor, window, Editor::spawn_nearest_task);
register_action(editor, window, Editor::open_selections_in_multibuffer);
register_action(editor, window, Editor::toggle_bookmark);
register_action(editor, window, Editor::edit_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);

View file

@ -10,22 +10,22 @@ 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, Debug)]
pub struct Bookmark {
pub anchor: text::Anchor,
pub label: String,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct SerializedBookmark(pub u32);
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct SerializedBookmark {
pub row: u32,
pub label: String,
}
#[derive(Debug)]
pub struct BufferBookmarks {
buffer: Entity<Buffer>,
bookmarks: Vec<BookmarkAnchor>,
bookmarks: Vec<Bookmark>,
_subscription: Subscription,
}
@ -52,7 +52,7 @@ impl BufferBookmarks {
&self.buffer
}
pub fn bookmarks(&self) -> &[BookmarkAnchor] {
pub fn bookmarks(&self) -> &[Bookmark] {
&self.bookmarks
}
}
@ -122,38 +122,41 @@ impl BookmarkStore {
buffer: &Entity<Buffer>,
cx: &mut Context<Self>,
) {
let Some(BookmarkEntry::Unloaded(rows)) = self.bookmarks.get(abs_path) else {
let Some(BookmarkEntry::Unloaded(bookmarks)) = self.bookmarks.get(abs_path) else {
return;
};
let snapshot = buffer.read(cx).snapshot();
let max_point = snapshot.max_point();
let anchors: Vec<BookmarkAnchor> = rows
let bookmarks: Vec<Bookmark> = bookmarks
.iter()
.filter_map(|bookmark_row| {
let point = Point::new(bookmark_row.0, 0);
.filter_map(|bookmark| {
let point = Point::new(bookmark.row, 0);
if point > max_point {
log::warn!(
"Skipping out-of-range bookmark: {} row {} (file has {} rows)",
abs_path.display(),
bookmark_row.0,
bookmark.row,
max_point.row
);
return None;
}
let anchor = snapshot.anchor_after(point);
Some(BookmarkAnchor(anchor))
Some(Bookmark {
anchor,
label: bookmark.label.clone(),
})
})
.collect();
if anchors.is_empty() {
if bookmarks.is_empty() {
self.bookmarks.remove(abs_path);
} else {
let mut buffer_bookmarks = BufferBookmarks::new(buffer.clone(), cx);
buffer_bookmarks.bookmarks = anchors;
buffer_bookmarks.bookmarks = bookmarks;
self.bookmarks
.insert(abs_path.clone(), BookmarkEntry::Loaded(buffer_bookmarks));
}
@ -167,11 +170,12 @@ impl BookmarkStore {
/// 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.
/// Otherwise, a new bookmark will be added with the given label.
pub fn toggle_bookmark(
&mut self,
buffer: Entity<Buffer>,
anchor: text::Anchor,
label: String,
cx: &mut Context<Self>,
) {
let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else {
@ -192,7 +196,8 @@ impl BookmarkStore {
let snapshot = buffer.read(cx).text_snapshot();
let existing_index = buffer_bookmarks.bookmarks.iter().position(|existing| {
existing.0.summary::<Point>(&snapshot).row == anchor.summary::<Point>(&snapshot).row
existing.anchor.summary::<Point>(&snapshot).row
== anchor.summary::<Point>(&snapshot).row
});
if let Some(index) = existing_index {
@ -201,12 +206,72 @@ impl BookmarkStore {
self.bookmarks.remove(&abs_path);
}
} else {
buffer_bookmarks.bookmarks.push(BookmarkAnchor(anchor));
buffer_bookmarks.bookmarks.push(Bookmark { anchor, label });
}
cx.notify();
}
pub fn find_bookmark(
&mut self,
buffer: &Entity<Buffer>,
anchor: text::Anchor,
cx: &mut Context<Self>,
) -> Option<&Bookmark> {
let Some(abs_path) = Self::abs_path_from_buffer(buffer, cx) else {
return None;
};
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();
buffer_bookmarks.bookmarks.iter().find(|existing| {
existing.anchor.summary::<Point>(&snapshot).row
== anchor.summary::<Point>(&snapshot).row
})
}
pub fn edit_bookmark(
&mut self,
buffer: &Entity<Buffer>,
anchor: text::Anchor,
label: String,
cx: &mut Context<Self>,
) {
let Some(abs_path) = Self::abs_path_from_buffer(buffer, cx) else {
return;
};
self.resolve_anchors_if_needed(&abs_path, buffer, cx);
let Some(BookmarkEntry::Loaded(buffer_bookmarks)) = self.bookmarks.get_mut(&abs_path)
else {
return;
};
let snapshot = buffer.read(cx).text_snapshot();
let row = anchor.summary::<Point>(&snapshot).row;
if let Some(bookmark) = buffer_bookmarks
.bookmarks
.iter_mut()
.find(|existing| existing.anchor.summary::<Point>(&snapshot).row == row)
{
bookmark.label = label;
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.
@ -216,7 +281,7 @@ impl BookmarkStore {
range: Range<text::Anchor>,
buffer_snapshot: &BufferSnapshot,
cx: &mut Context<Self>,
) -> Vec<BookmarkAnchor> {
) -> Vec<Bookmark> {
let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else {
return Vec::new();
};
@ -232,17 +297,17 @@ impl BookmarkStore {
.iter()
.filter_map({
move |bookmark| {
if !buffer_snapshot.can_resolve(&bookmark.anchor()) {
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()
if bookmark.anchor.cmp(&range.start, buffer_snapshot).is_lt()
|| bookmark.anchor.cmp(&range.end, buffer_snapshot).is_gt()
{
return None;
}
Some(*bookmark)
Some(bookmark.clone())
}
})
.collect()
@ -310,19 +375,22 @@ impl BookmarkStore {
.bookmarks
.iter()
.filter_map(|bookmark| {
if !snapshot.can_resolve(&bookmark.anchor()) {
if !snapshot.can_resolve(&bookmark.anchor) {
return None;
}
let row =
snapshot.summary_for_anchor::<Point>(&bookmark.anchor()).row;
Some(SerializedBookmark(row))
snapshot.summary_for_anchor::<Point>(&bookmark.anchor).row;
Some(SerializedBookmark {
row,
label: bookmark.label.clone(),
})
})
.collect()
}
};
rows.sort_unstable();
rows.dedup();
rows.sort_unstable_by_key(|a| a.row);
rows.dedup_by_key(|a| a.row);
if rows.is_empty() {
None
@ -346,8 +414,8 @@ impl BookmarkStore {
let ranges: Vec<Range<Point>> = bookmarks
.bookmarks()
.iter()
.map(|anchor| {
let row = snapshot.summary_for_anchor::<Point>(&anchor.anchor()).row;
.map(|bookmark| {
let row = snapshot.summary_for_anchor::<Point>(&bookmark.anchor).row;
Point::row_range(row..row)
})
.collect();

View file

@ -23,6 +23,13 @@ mod integration {
Arc::from(Path::new(path))
}
fn serialized_bookmark(row: u32) -> SerializedBookmark {
SerializedBookmark {
row,
label: String::new(),
}
}
async fn open_buffer(
project: &Entity<Project>,
path: &str,
@ -49,12 +56,30 @@ mod integration {
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);
store.toggle_bookmark(buffer.clone(), anchor, String::new(), cx);
});
}
});
}
fn add_labeled_bookmark(
project: &Entity<Project>,
buffer: &Entity<Buffer>,
row: u32,
label: &str,
cx: &mut TestAppContext,
) {
let buffer = buffer.clone();
project.update(cx, |project, cx| {
let bookmark_store = project.bookmark_store();
let snapshot = buffer.read(cx).snapshot();
let anchor = snapshot.anchor_after(text::Point::new(row, 0));
bookmark_store.update(cx, |store, cx| {
store.toggle_bookmark(buffer.clone(), anchor, label.to_string(), cx);
});
});
}
fn get_all_bookmarks(
project: &Entity<Project>,
cx: &mut TestAppContext,
@ -75,7 +100,7 @@ mod integration {
let path = project_path(path_str);
map.insert(
path.clone(),
rows.iter().map(|&row| SerializedBookmark(row)).collect(),
rows.iter().map(|&row| serialized_bookmark(row)).collect(),
);
}
map
@ -113,10 +138,26 @@ mod integration {
let file_bookmarks = bookmarks
.get(&path)
.unwrap_or_else(|| panic!("Expected bookmarks for {}", path.display()));
let rows: Vec<u32> = file_bookmarks.iter().map(|b| b.0).collect();
let rows: Vec<u32> = file_bookmarks.iter().map(|b| b.row).collect();
assert_eq!(rows, expected_rows, "Bookmark rows for {}", path.display());
}
fn assert_bookmark_labels(
bookmarks: &BTreeMap<Arc<Path>, Vec<SerializedBookmark>>,
path: &str,
expected: &[(u32, &str)],
) {
let path = project_path(path);
let file_bookmarks = bookmarks
.get(&path)
.unwrap_or_else(|| panic!("Expected bookmarks for {}", path.display()));
let actual: Vec<_> = file_bookmarks
.iter()
.map(|bookmark| (bookmark.row, bookmark.label.as_str()))
.collect();
assert_eq!(actual, expected, "Bookmark labels for {}", path.display());
}
#[gpui::test]
async fn test_all_serialized_bookmarks_empty(cx: &mut TestAppContext) {
init_test(cx);
@ -152,6 +193,32 @@ mod integration {
assert_bookmark_rows(&bookmarks, path!("/project/file1.rs"), &[0, 2]);
}
#[gpui::test]
async fn test_all_serialized_bookmarks_includes_labels(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_labeled_bookmark(&project, &buffer, 0, "first", cx);
add_labeled_bookmark(&project, &buffer, 2, " keeps inner spaces ", cx);
let bookmarks = get_all_bookmarks(&project, cx);
assert_bookmark_labels(
&bookmarks,
path!("/project/file1.rs"),
&[(0, "first"), (2, " keeps inner spaces ")],
);
}
#[gpui::test]
async fn test_all_serialized_bookmarks_multiple_files(cx: &mut TestAppContext) {
init_test(cx);
@ -293,7 +360,7 @@ mod integration {
.get(&project_path(path!("/project/file1.rs")))
.unwrap()
.iter()
.map(|b| b.0)
.map(|b| b.row)
.collect();
let mut deduped = rows.clone();
deduped.dedup();

View file

@ -394,12 +394,13 @@ pub async fn write_default_dock_state(
#[derive(Debug)]
pub struct Bookmark {
pub row: u32,
pub label: String,
}
impl sqlez::bindable::StaticColumnCount for Bookmark {
fn column_count() -> usize {
// row
1
// row, label
2
}
}
@ -409,7 +410,8 @@ impl sqlez::bindable::Bind for Bookmark {
statement: &sqlez::statement::Statement,
start_index: i32,
) -> anyhow::Result<i32> {
statement.bind(&self.row, start_index)
let next_index = statement.bind(&self.row, start_index)?;
statement.bind(&self.label, next_index)
}
}
@ -420,7 +422,9 @@ impl Column for Bookmark {
.with_context(|| format!("Failed to read bookmark at index {start_index}"))?
as u32;
Ok((Bookmark { row }, start_index + 1))
let (label, next_index) = String::column(statement, start_index + 1)?;
Ok((Bookmark { row, label }, next_index))
}
}
@ -1044,6 +1048,9 @@ impl Domain for WorkspaceDb {
ALTER TABLE workspaces ADD COLUMN identity_paths TEXT;
ALTER TABLE workspaces ADD COLUMN identity_paths_order TEXT;
),
sql!(
ALTER TABLE bookmarks ADD COLUMN label TEXT NOT NULL DEFAULT "";
),
];
// Allow recovering from bad migration that was initially shipped to nightly
@ -1305,7 +1312,7 @@ impl WorkspaceDb {
fn bookmarks(&self, workspace_id: WorkspaceId) -> BTreeMap<Arc<Path>, Vec<SerializedBookmark>> {
let bookmarks: Result<Vec<(PathBuf, Bookmark)>> = self
.select_bound(sql! {
SELECT path, row
SELECT path, row, label
FROM bookmarks
WHERE workspace_id = ?
ORDER BY path, row
@ -1324,7 +1331,10 @@ impl WorkspaceDb {
let path: Arc<Path> = path.into();
map.entry(path.clone())
.or_default()
.push(SerializedBookmark(bookmark.row))
.push(SerializedBookmark {
row: bookmark.row,
label: bookmark.label,
})
}
map
@ -1485,9 +1495,9 @@ impl WorkspaceDb {
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")?;
INSERT INTO bookmarks (workspace_id, path, row, label)
VALUES (?1, ?2, ?3, ?4);
))?((workspace.id, path.as_ref(), bookmark.row, bookmark.label)).context("Inserting bookmark")?;
}
}