bookmark_store.rs

  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}