mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-10 00:13:29 +00:00
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:
parent
471fb15d7a
commit
f6bfa0915f
8 changed files with 758 additions and 192 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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![]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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")?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue