Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:47:33.382Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

bookmark_store.rs

513 lines · 16.4 KB · rust
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(Clone, Debug)]
14pub struct Bookmark {
15    pub anchor: text::Anchor,
16    pub label: String,
17}
18
19#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
20pub struct SerializedBookmark {
21    pub row: u32,
22    pub label: String,
23}
24
25#[derive(Debug)]
26pub struct BufferBookmarks {
27    buffer: Entity<Buffer>,
28    bookmarks: Vec<Bookmark>,
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) -> &[Bookmark] {
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(bookmarks)) = 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 bookmarks: Vec<Bookmark> = bookmarks
133            .iter()
134            .filter_map(|bookmark| {
135                let point = Point::new(bookmark.row, 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,
142                        max_point.row
143                    );
144                    return None;
145                }
146
147                let anchor = snapshot.anchor_after(point);
148                Some(Bookmark {
149                    anchor,
150                    label: bookmark.label.clone(),
151                })
152            })
153            .collect();
154
155        if bookmarks.is_empty() {
156            self.bookmarks.remove(abs_path);
157        } else {
158            let mut buffer_bookmarks = BufferBookmarks::new(buffer.clone(), cx);
159            buffer_bookmarks.bookmarks = bookmarks;
160            self.bookmarks
161                .insert(abs_path.clone(), BookmarkEntry::Loaded(buffer_bookmarks));
162        }
163    }
164
165    pub fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
166        worktree::File::from_dyn(buffer.read(cx).file())
167            .map(|file| file.worktree.read(cx).absolutize(&file.path))
168            .map(Arc::<Path>::from)
169    }
170
171    /// Toggle a bookmark at the given anchor in the buffer.
172    /// If a bookmark already exists on the same row, it will be removed.
173    /// Otherwise, a new bookmark will be added with the given label.
174    pub fn toggle_bookmark(
175        &mut self,
176        buffer: Entity<Buffer>,
177        anchor: text::Anchor,
178        label: String,
179        cx: &mut Context<Self>,
180    ) {
181        let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else {
182            return;
183        };
184
185        self.resolve_anchors_if_needed(&abs_path, &buffer, cx);
186
187        let entry = self
188            .bookmarks
189            .entry(abs_path.clone())
190            .or_insert_with(|| BookmarkEntry::Loaded(BufferBookmarks::new(buffer.clone(), cx)));
191
192        let BookmarkEntry::Loaded(buffer_bookmarks) = entry else {
193            unreachable!("resolve_if_needed should have converted to Loaded");
194        };
195
196        let snapshot = buffer.read(cx).text_snapshot();
197
198        let existing_index = buffer_bookmarks.bookmarks.iter().position(|existing| {
199            existing.anchor.summary::<Point>(&snapshot).row
200                == anchor.summary::<Point>(&snapshot).row
201        });
202
203        if let Some(index) = existing_index {
204            buffer_bookmarks.bookmarks.remove(index);
205            if buffer_bookmarks.bookmarks.is_empty() {
206                self.bookmarks.remove(&abs_path);
207            }
208        } else {
209            buffer_bookmarks.bookmarks.push(Bookmark { anchor, label });
210        }
211
212        cx.notify();
213    }
214
215    pub fn find_bookmark(
216        &mut self,
217        buffer: &Entity<Buffer>,
218        anchor: text::Anchor,
219        cx: &mut Context<Self>,
220    ) -> Option<&Bookmark> {
221        let Some(abs_path) = Self::abs_path_from_buffer(buffer, cx) else {
222            return None;
223        };
224
225        self.resolve_anchors_if_needed(&abs_path, buffer, cx);
226
227        let entry = self
228            .bookmarks
229            .entry(abs_path.clone())
230            .or_insert_with(|| BookmarkEntry::Loaded(BufferBookmarks::new(buffer.clone(), cx)));
231
232        let BookmarkEntry::Loaded(buffer_bookmarks) = entry else {
233            unreachable!("resolve_if_needed should have converted to Loaded");
234        };
235
236        let snapshot = buffer.read(cx).text_snapshot();
237
238        buffer_bookmarks.bookmarks.iter().find(|existing| {
239            existing.anchor.summary::<Point>(&snapshot).row
240                == anchor.summary::<Point>(&snapshot).row
241        })
242    }
243
244    pub fn edit_bookmark(
245        &mut self,
246        buffer: &Entity<Buffer>,
247        anchor: text::Anchor,
248        label: String,
249        cx: &mut Context<Self>,
250    ) {
251        let Some(abs_path) = Self::abs_path_from_buffer(buffer, cx) else {
252            return;
253        };
254
255        self.resolve_anchors_if_needed(&abs_path, buffer, cx);
256
257        let Some(BookmarkEntry::Loaded(buffer_bookmarks)) = self.bookmarks.get_mut(&abs_path)
258        else {
259            return;
260        };
261
262        let snapshot = buffer.read(cx).text_snapshot();
263        let row = anchor.summary::<Point>(&snapshot).row;
264
265        if let Some(bookmark) = buffer_bookmarks
266            .bookmarks
267            .iter_mut()
268            .find(|existing| existing.anchor.summary::<Point>(&snapshot).row == row)
269        {
270            bookmark.label = label;
271            cx.notify();
272        }
273    }
274
275    /// Returns the bookmarks for a given buffer within an optional range.
276    /// Only returns bookmarks that have been resolved to anchors (loaded).
277    /// Unloaded bookmarks for the given buffer will be resolved first.
278    pub fn bookmarks_for_buffer(
279        &mut self,
280        buffer: Entity<Buffer>,
281        range: Range<text::Anchor>,
282        buffer_snapshot: &BufferSnapshot,
283        cx: &mut Context<Self>,
284    ) -> Vec<Bookmark> {
285        let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else {
286            return Vec::new();
287        };
288
289        self.resolve_anchors_if_needed(&abs_path, &buffer, cx);
290
291        let Some(BookmarkEntry::Loaded(file_bookmarks)) = self.bookmarks.get(&abs_path) else {
292            return Vec::new();
293        };
294
295        file_bookmarks
296            .bookmarks
297            .iter()
298            .filter_map({
299                move |bookmark| {
300                    if !buffer_snapshot.can_resolve(&bookmark.anchor) {
301                        return None;
302                    }
303
304                    if bookmark.anchor.cmp(&range.start, buffer_snapshot).is_lt()
305                        || bookmark.anchor.cmp(&range.end, buffer_snapshot).is_gt()
306                    {
307                        return None;
308                    }
309
310                    Some(bookmark.clone())
311                }
312            })
313            .collect()
314    }
315
316    fn handle_file_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
317        let entity_id = buffer.entity_id();
318
319        if buffer
320            .read(cx)
321            .file()
322            .is_none_or(|f| f.disk_state().is_deleted())
323        {
324            self.bookmarks.retain(|_, entry| match entry {
325                BookmarkEntry::Loaded(buffer_bookmarks) => {
326                    buffer_bookmarks.buffer.entity_id() != entity_id
327                }
328                BookmarkEntry::Unloaded(_) => true,
329            });
330            cx.notify();
331            return;
332        }
333
334        if let Some(new_abs_path) = Self::abs_path_from_buffer(&buffer, cx) {
335            if self.bookmarks.contains_key(&new_abs_path) {
336                return;
337            }
338
339            if let Some(old_path) = self
340                .bookmarks
341                .iter()
342                .find(|(_, entry)| match entry {
343                    BookmarkEntry::Loaded(buffer_bookmarks) => {
344                        buffer_bookmarks.buffer.entity_id() == entity_id
345                    }
346                    BookmarkEntry::Unloaded(_) => false,
347                })
348                .map(|(path, _)| path)
349                .cloned()
350            {
351                let Some(entry) = self.bookmarks.remove(&old_path) else {
352                    log::error!(
353                        "Couldn't get bookmarks from old path during buffer rename handling"
354                    );
355                    return;
356                };
357                self.bookmarks.insert(new_abs_path, entry);
358                cx.notify();
359            }
360        }
361    }
362
363    pub fn all_serialized_bookmarks(
364        &self,
365        cx: &App,
366    ) -> BTreeMap<Arc<Path>, Vec<SerializedBookmark>> {
367        self.bookmarks
368            .iter()
369            .filter_map(|(path, entry)| {
370                let mut rows = match entry {
371                    BookmarkEntry::Unloaded(rows) => rows.clone(),
372                    BookmarkEntry::Loaded(buffer_bookmarks) => {
373                        let snapshot = buffer_bookmarks.buffer.read(cx).snapshot();
374                        buffer_bookmarks
375                            .bookmarks
376                            .iter()
377                            .filter_map(|bookmark| {
378                                if !snapshot.can_resolve(&bookmark.anchor) {
379                                    return None;
380                                }
381                                let row =
382                                    snapshot.summary_for_anchor::<Point>(&bookmark.anchor).row;
383                                Some(SerializedBookmark {
384                                    row,
385                                    label: bookmark.label.clone(),
386                                })
387                            })
388                            .collect()
389                    }
390                };
391
392                rows.sort_unstable_by_key(|a| a.row);
393                rows.dedup_by_key(|a| a.row);
394
395                if rows.is_empty() {
396                    None
397                } else {
398                    Some((path.clone(), rows))
399                }
400            })
401            .collect()
402    }
403
404    pub async fn all_bookmark_locations(
405        this: Entity<BookmarkStore>,
406        cx: &mut (impl AppContext + Clone),
407    ) -> Result<HashMap<Entity<Buffer>, Vec<Range<Point>>>> {
408        Self::resolve_all(&this, cx).await?;
409
410        cx.read_entity(&this, |this, cx| {
411            let mut locations: HashMap<_, Vec<_>> = HashMap::new();
412            for bookmarks in this.bookmarks.values().filter_map(BookmarkEntry::loaded) {
413                let snapshot = cx.read_entity(bookmarks.buffer(), |b, _| b.snapshot());
414                let ranges: Vec<Range<Point>> = bookmarks
415                    .bookmarks()
416                    .iter()
417                    .map(|bookmark| {
418                        let row = snapshot.summary_for_anchor::<Point>(&bookmark.anchor).row;
419                        Point::row_range(row..row)
420                    })
421                    .collect();
422
423                locations
424                    .entry(bookmarks.buffer().clone())
425                    .or_default()
426                    .extend(ranges);
427            }
428
429            Ok(locations)
430        })
431    }
432
433    /// Opens buffers for all unloaded bookmark entries and resolves them to anchors. This is used to show all bookmarks in a large multi-buffer.
434    async fn resolve_all(this: &Entity<Self>, cx: &mut (impl AppContext + Clone)) -> Result<()> {
435        let unloaded_paths: Vec<Arc<Path>> = cx.read_entity(&this, |this, _| {
436            this.bookmarks
437                .iter()
438                .filter_map(|(path, entry)| match entry {
439                    BookmarkEntry::Unloaded(_) => Some(path.clone()),
440                    BookmarkEntry::Loaded(_) => None,
441                })
442                .collect_vec()
443        });
444
445        if unloaded_paths.is_empty() {
446            return Ok(());
447        }
448
449        let worktree_store = cx.read_entity(&this, |this, _| this.worktree_store.clone());
450        let buffer_store = cx.read_entity(&this, |this, _| this.buffer_store.clone());
451
452        let open_tasks: FuturesUnordered<_> = unloaded_paths
453            .iter()
454            .map(|path| {
455                open_path(path, &worktree_store, &buffer_store, cx.clone())
456                    .map_err(move |e| (path, e))
457                    .map_ok(move |b| (path, b))
458            })
459            .collect();
460
461        let opened: Vec<_> = open_tasks
462            .inspect_err(|(path, error)| {
463                log::warn!(
464                    "Could not open buffer for bookmarked path {}: {error}",
465                    path.display()
466                )
467            })
468            .filter_map(|res| async move { res.ok() })
469            .collect()
470            .await;
471
472        cx.update_entity(&this, |this, cx| {
473            for (path, buffer) in opened {
474                this.resolve_anchors_if_needed(&path, &buffer, cx);
475            }
476            cx.notify();
477        });
478
479        Ok(())
480    }
481
482    pub fn clear_bookmarks(&mut self, cx: &mut Context<Self>) {
483        self.bookmarks.clear();
484        cx.notify();
485    }
486}
487
488async fn open_path(
489    path: &Path,
490    worktree_store: &Entity<WorktreeStore>,
491    buffer_store: &Entity<BufferStore>,
492    mut cx: impl AppContext,
493) -> Result<Entity<Buffer>> {
494    let (worktree, worktree_path) = cx
495        .update_entity(&worktree_store, |worktree_store, cx| {
496            worktree_store.find_or_create_worktree(path, false, cx)
497        })
498        .await?;
499
500    let project_path = ProjectPath {
501        worktree_id: cx.read_entity(&worktree, |worktree, _| worktree.id()),
502        path: worktree_path,
503    };
504
505    let buffer = cx
506        .update_entity(&buffer_store, |buffer_store, cx| {
507            buffer_store.open_buffer(project_path, cx)
508        })
509        .await?;
510
511    Ok(buffer)
512}
513
Served at tenant.openagents/omega Member data and write actions are omitted.