1use std::{collections::BTreeMap, ops::Range, path::Path, sync::Arc};
2
3use anyhow::Result;
4use futures::{StreamExt, TryFutureExt, TryStreamExt, stream::FuturesUnordered};
5use gpui::{App, AppContext, Context, Entity, Subscription, Task};
6use itertools::Itertools;
7use language::{Buffer, BufferEvent};
8use std::collections::HashMap;
9use text::{BufferSnapshot, Point};
10
11use crate::{ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore};
12
13#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
14pub struct BookmarkAnchor(text::Anchor);
15
16impl BookmarkAnchor {
17 pub fn anchor(&self) -> text::Anchor {
18 self.0
19 }
20}
21
22#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
23pub struct SerializedBookmark(pub u32);
24
25#[derive(Debug)]
26pub struct BufferBookmarks {
27 buffer: Entity<Buffer>,
28 bookmarks: Vec<BookmarkAnchor>,
29 _subscription: Subscription,
30}
31
32impl BufferBookmarks {
33 pub fn new(buffer: Entity<Buffer>, cx: &mut Context<BookmarkStore>) -> Self {
34 let subscription = cx.subscribe(
35 &buffer,
36 |bookmark_store, buffer, event: &BufferEvent, cx| match event {
37 BufferEvent::FileHandleChanged => {
38 bookmark_store.handle_file_changed(buffer, cx);
39 }
40 _ => {}
41 },
42 );
43
44 Self {
45 buffer,
46 bookmarks: Vec::new(),
47 _subscription: subscription,
48 }
49 }
50
51 pub fn buffer(&self) -> &Entity<Buffer> {
52 &self.buffer
53 }
54
55 pub fn bookmarks(&self) -> &[BookmarkAnchor] {
56 &self.bookmarks
57 }
58}
59
60#[derive(Debug)]
61pub enum BookmarkEntry {
62 Loaded(BufferBookmarks),
63 Unloaded(Vec<SerializedBookmark>),
64}
65
66impl BookmarkEntry {
67 pub fn is_empty(&self) -> bool {
68 match self {
69 BookmarkEntry::Loaded(buffer_bookmarks) => buffer_bookmarks.bookmarks.is_empty(),
70 BookmarkEntry::Unloaded(rows) => rows.is_empty(),
71 }
72 }
73
74 fn loaded(&self) -> Option<&BufferBookmarks> {
75 match self {
76 BookmarkEntry::Loaded(buffer_bookmarks) => Some(buffer_bookmarks),
77 BookmarkEntry::Unloaded(_) => None,
78 }
79 }
80}
81
82pub struct BookmarkStore {
83 buffer_store: Entity<BufferStore>,
84 worktree_store: Entity<WorktreeStore>,
85 bookmarks: BTreeMap<Arc<Path>, BookmarkEntry>,
86}
87
88impl BookmarkStore {
89 pub fn new(worktree_store: Entity<WorktreeStore>, buffer_store: Entity<BufferStore>) -> Self {
90 Self {
91 buffer_store,
92 worktree_store,
93 bookmarks: BTreeMap::new(),
94 }
95 }
96
97 pub fn load_serialized_bookmarks(
98 &mut self,
99 bookmark_rows: BTreeMap<Arc<Path>, Vec<SerializedBookmark>>,
100 cx: &mut Context<Self>,
101 ) -> Task<Result<()>> {
102 self.bookmarks.clear();
103
104 for (path, rows) in bookmark_rows {
105 if rows.is_empty() {
106 continue;
107 }
108
109 let count = rows.len();
110 log::debug!("Stored {count} unloaded bookmark(s) at {}", path.display());
111
112 self.bookmarks.insert(path, BookmarkEntry::Unloaded(rows));
113 }
114
115 cx.notify();
116 Task::ready(Ok(()))
117 }
118
119 fn resolve_anchors_if_needed(
120 &mut self,
121 abs_path: &Arc<Path>,
122 buffer: &Entity<Buffer>,
123 cx: &mut Context<Self>,
124 ) {
125 let Some(BookmarkEntry::Unloaded(rows)) = self.bookmarks.get(abs_path) else {
126 return;
127 };
128
129 let snapshot = buffer.read(cx).snapshot();
130 let max_point = snapshot.max_point();
131
132 let anchors: Vec<BookmarkAnchor> = rows
133 .iter()
134 .filter_map(|bookmark_row| {
135 let point = Point::new(bookmark_row.0, 0);
136
137 if point > max_point {
138 log::warn!(
139 "Skipping out-of-range bookmark: {} row {} (file has {} rows)",
140 abs_path.display(),
141 bookmark_row.0,
142 max_point.row
143 );
144 return None;
145 }
146
147 let anchor = snapshot.anchor_after(point);
148 Some(BookmarkAnchor(anchor))
149 })
150 .collect();
151
152 if anchors.is_empty() {
153 self.bookmarks.remove(abs_path);
154 } else {
155 let mut buffer_bookmarks = BufferBookmarks::new(buffer.clone(), cx);
156 buffer_bookmarks.bookmarks = anchors;
157 self.bookmarks
158 .insert(abs_path.clone(), BookmarkEntry::Loaded(buffer_bookmarks));
159 }
160 }
161
162 pub fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
163 worktree::File::from_dyn(buffer.read(cx).file())
164 .map(|file| file.worktree.read(cx).absolutize(&file.path))
165 .map(Arc::<Path>::from)
166 }
167
168 /// Toggle a bookmark at the given anchor in the buffer.
169 /// If a bookmark already exists on the same row, it will be removed.
170 /// Otherwise, a new bookmark will be added.
171 pub fn toggle_bookmark(
172 &mut self,
173 buffer: Entity<Buffer>,
174 anchor: text::Anchor,
175 cx: &mut Context<Self>,
176 ) {
177 let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else {
178 return;
179 };
180
181 self.resolve_anchors_if_needed(&abs_path, &buffer, cx);
182
183 let entry = self
184 .bookmarks
185 .entry(abs_path.clone())
186 .or_insert_with(|| BookmarkEntry::Loaded(BufferBookmarks::new(buffer.clone(), cx)));
187
188 let BookmarkEntry::Loaded(buffer_bookmarks) = entry else {
189 unreachable!("resolve_if_needed should have converted to Loaded");
190 };
191
192 let snapshot = buffer.read(cx).text_snapshot();
193
194 let existing_index = buffer_bookmarks.bookmarks.iter().position(|existing| {
195 existing.0.summary::<Point>(&snapshot).row == anchor.summary::<Point>(&snapshot).row
196 });
197
198 if let Some(index) = existing_index {
199 buffer_bookmarks.bookmarks.remove(index);
200 if buffer_bookmarks.bookmarks.is_empty() {
201 self.bookmarks.remove(&abs_path);
202 }
203 } else {
204 buffer_bookmarks.bookmarks.push(BookmarkAnchor(anchor));
205 }
206
207 cx.notify();
208 }
209
210 /// Returns the bookmarks for a given buffer within an optional range.
211 /// Only returns bookmarks that have been resolved to anchors (loaded).
212 /// Unloaded bookmarks for the given buffer will be resolved first.
213 pub fn bookmarks_for_buffer(
214 &mut self,
215 buffer: Entity<Buffer>,
216 range: Range<text::Anchor>,
217 buffer_snapshot: &BufferSnapshot,
218 cx: &mut Context<Self>,
219 ) -> Vec<BookmarkAnchor> {
220 let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else {
221 return Vec::new();
222 };
223
224 self.resolve_anchors_if_needed(&abs_path, &buffer, cx);
225
226 let Some(BookmarkEntry::Loaded(file_bookmarks)) = self.bookmarks.get(&abs_path) else {
227 return Vec::new();
228 };
229
230 file_bookmarks
231 .bookmarks
232 .iter()
233 .filter_map({
234 move |bookmark| {
235 if !buffer_snapshot.can_resolve(&bookmark.anchor()) {
236 return None;
237 }
238
239 if bookmark.anchor().cmp(&range.start, buffer_snapshot).is_lt()
240 || bookmark.anchor().cmp(&range.end, buffer_snapshot).is_gt()
241 {
242 return None;
243 }
244
245 Some(*bookmark)
246 }
247 })
248 .collect()
249 }
250
251 fn handle_file_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
252 let entity_id = buffer.entity_id();
253
254 if buffer
255 .read(cx)
256 .file()
257 .is_none_or(|f| f.disk_state().is_deleted())
258 {
259 self.bookmarks.retain(|_, entry| match entry {
260 BookmarkEntry::Loaded(buffer_bookmarks) => {
261 buffer_bookmarks.buffer.entity_id() != entity_id
262 }
263 BookmarkEntry::Unloaded(_) => true,
264 });
265 cx.notify();
266 return;
267 }
268
269 if let Some(new_abs_path) = Self::abs_path_from_buffer(&buffer, cx) {
270 if self.bookmarks.contains_key(&new_abs_path) {
271 return;
272 }
273
274 if let Some(old_path) = self
275 .bookmarks
276 .iter()
277 .find(|(_, entry)| match entry {
278 BookmarkEntry::Loaded(buffer_bookmarks) => {
279 buffer_bookmarks.buffer.entity_id() == entity_id
280 }
281 BookmarkEntry::Unloaded(_) => false,
282 })
283 .map(|(path, _)| path)
284 .cloned()
285 {
286 let Some(entry) = self.bookmarks.remove(&old_path) else {
287 log::error!(
288 "Couldn't get bookmarks from old path during buffer rename handling"
289 );
290 return;
291 };
292 self.bookmarks.insert(new_abs_path, entry);
293 cx.notify();
294 }
295 }
296 }
297
298 pub fn all_serialized_bookmarks(
299 &self,
300 cx: &App,
301 ) -> BTreeMap<Arc<Path>, Vec<SerializedBookmark>> {
302 self.bookmarks
303 .iter()
304 .filter_map(|(path, entry)| {
305 let mut rows = match entry {
306 BookmarkEntry::Unloaded(rows) => rows.clone(),
307 BookmarkEntry::Loaded(buffer_bookmarks) => {
308 let snapshot = buffer_bookmarks.buffer.read(cx).snapshot();
309 buffer_bookmarks
310 .bookmarks
311 .iter()
312 .filter_map(|bookmark| {
313 if !snapshot.can_resolve(&bookmark.anchor()) {
314 return None;
315 }
316 let row =
317 snapshot.summary_for_anchor::<Point>(&bookmark.anchor()).row;
318 Some(SerializedBookmark(row))
319 })
320 .collect()
321 }
322 };
323
324 rows.sort();
325 rows.dedup();
326
327 if rows.is_empty() {
328 None
329 } else {
330 Some((path.clone(), rows))
331 }
332 })
333 .collect()
334 }
335
336 pub async fn all_bookmark_locations(
337 this: Entity<BookmarkStore>,
338 cx: &mut (impl AppContext + Clone),
339 ) -> Result<HashMap<Entity<Buffer>, Vec<Range<Point>>>> {
340 Self::resolve_all(&this, cx).await?;
341
342 cx.read_entity(&this, |this, cx| {
343 let mut locations: HashMap<_, Vec<_>> = HashMap::new();
344 for bookmarks in this.bookmarks.values().filter_map(BookmarkEntry::loaded) {
345 let snapshot = cx.read_entity(bookmarks.buffer(), |b, _| b.snapshot());
346 let ranges: Vec<Range<Point>> = bookmarks
347 .bookmarks()
348 .iter()
349 .map(|anchor| {
350 let row = snapshot.summary_for_anchor::<Point>(&anchor.anchor()).row;
351 Point::row_range(row..row)
352 })
353 .collect();
354
355 locations
356 .entry(bookmarks.buffer().clone())
357 .or_default()
358 .extend(ranges);
359 }
360
361 Ok(locations)
362 })
363 }
364
365 /// Opens buffers for all unloaded bookmark entries and resolves them to anchors. This is used to show all bookmarks in a large multi-buffer.
366 async fn resolve_all(this: &Entity<Self>, cx: &mut (impl AppContext + Clone)) -> Result<()> {
367 let unloaded_paths: Vec<Arc<Path>> = cx.read_entity(&this, |this, _| {
368 this.bookmarks
369 .iter()
370 .filter_map(|(path, entry)| match entry {
371 BookmarkEntry::Unloaded(_) => Some(path.clone()),
372 BookmarkEntry::Loaded(_) => None,
373 })
374 .collect_vec()
375 });
376
377 if unloaded_paths.is_empty() {
378 return Ok(());
379 }
380
381 let worktree_store = cx.read_entity(&this, |this, _| this.worktree_store.clone());
382 let buffer_store = cx.read_entity(&this, |this, _| this.buffer_store.clone());
383
384 let open_tasks: FuturesUnordered<_> = unloaded_paths
385 .iter()
386 .map(|path| {
387 open_path(path, &worktree_store, &buffer_store, cx.clone())
388 .map_err(move |e| (path, e))
389 .map_ok(move |b| (path, b))
390 })
391 .collect();
392
393 let opened: Vec<_> = open_tasks
394 .inspect_err(|(path, error)| {
395 log::warn!(
396 "Could not open buffer for bookmarked path {}: {error}",
397 path.display()
398 )
399 })
400 .filter_map(|res| async move { res.ok() })
401 .collect()
402 .await;
403
404 cx.update_entity(&this, |this, cx| {
405 for (path, buffer) in opened {
406 this.resolve_anchors_if_needed(&path, &buffer, cx);
407 }
408 cx.notify();
409 });
410
411 Ok(())
412 }
413
414 pub fn clear_bookmarks(&mut self, cx: &mut Context<Self>) {
415 self.bookmarks.clear();
416 cx.notify();
417 }
418}
419
420async fn open_path(
421 path: &Path,
422 worktree_store: &Entity<WorktreeStore>,
423 buffer_store: &Entity<BufferStore>,
424 mut cx: impl AppContext,
425) -> Result<Entity<Buffer>> {
426 let (worktree, worktree_path) = cx
427 .update_entity(&worktree_store, |worktree_store, cx| {
428 worktree_store.find_or_create_worktree(path, false, cx)
429 })
430 .await?;
431
432 let project_path = ProjectPath {
433 worktree_id: cx.read_entity(&worktree, |worktree, _| worktree.id()),
434 path: worktree_path,
435 };
436
437 let buffer = cx
438 .update_entity(&buffer_store, |buffer_store, cx| {
439 buffer_store.open_buffer(project_path, cx)
440 })
441 .await?;
442
443 Ok(buffer)
444}