use std::{collections::BTreeMap, ops::Range, path::Path, sync::Arc}; use anyhow::Result;use futures::{StreamExt, TryFutureExt, TryStreamExt, stream::FuturesUnordered};use gpui::{App, AppContext, Context, Entity, Subscription, Task};use itertools::Itertools;use language::{Buffer, BufferEvent};use std::collections::HashMap;use text::{BufferSnapshot, Point}; use crate::{ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore}; #[derive(Clone, Debug)]pub struct Bookmark { pub anchor: text::Anchor, pub label: String,} #[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<Bookmark>, _subscription: Subscription,} impl BufferBookmarks { pub fn new(buffer: Entity<Buffer>, cx: &mut Context<BookmarkStore>) -> Self { let subscription = cx.subscribe( &buffer, |bookmark_store, buffer, event: &BufferEvent, cx| match event { BufferEvent::FileHandleChanged => { bookmark_store.handle_file_changed(buffer, cx); } _ => {} }, ); Self { buffer, bookmarks: Vec::new(), _subscription: subscription, } } pub fn buffer(&self) -> &Entity<Buffer> { &self.buffer } pub fn bookmarks(&self) -> &[Bookmark] { &self.bookmarks }} #[derive(Debug)]pub enum BookmarkEntry { Loaded(BufferBookmarks), Unloaded(Vec<SerializedBookmark>),} impl BookmarkEntry { pub fn is_empty(&self) -> bool { match self { BookmarkEntry::Loaded(buffer_bookmarks) => buffer_bookmarks.bookmarks.is_empty(), BookmarkEntry::Unloaded(rows) => rows.is_empty(), } } fn loaded(&self) -> Option<&BufferBookmarks> { match self { BookmarkEntry::Loaded(buffer_bookmarks) => Some(buffer_bookmarks), BookmarkEntry::Unloaded(_) => None, } }} pub struct BookmarkStore { buffer_store: Entity<BufferStore>, worktree_store: Entity<WorktreeStore>, bookmarks: BTreeMap<Arc<Path>, BookmarkEntry>,} impl BookmarkStore { pub fn new(worktree_store: Entity<WorktreeStore>, buffer_store: Entity<BufferStore>) -> Self { Self { buffer_store, worktree_store, bookmarks: BTreeMap::new(), } } pub fn load_serialized_bookmarks( &mut self, bookmark_rows: BTreeMap<Arc<Path>, Vec<SerializedBookmark>>, cx: &mut Context<Self>, ) -> Task<Result<()>> { self.bookmarks.clear(); for (path, rows) in bookmark_rows { if rows.is_empty() { continue; } let count = rows.len(); log::debug!("Stored {count} unloaded bookmark(s) at {}", path.display()); self.bookmarks.insert(path, BookmarkEntry::Unloaded(rows)); } cx.notify(); Task::ready(Ok(())) } fn resolve_anchors_if_needed( &mut self, abs_path: &Arc<Path>, buffer: &Entity<Buffer>, cx: &mut Context<Self>, ) { 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 bookmarks: Vec<Bookmark> = bookmarks .iter() .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, max_point.row ); return None; } let anchor = snapshot.anchor_after(point); Some(Bookmark { anchor, label: bookmark.label.clone(), }) }) .collect(); if bookmarks.is_empty() { self.bookmarks.remove(abs_path); } else { let mut buffer_bookmarks = BufferBookmarks::new(buffer.clone(), cx); buffer_bookmarks.bookmarks = bookmarks; self.bookmarks .insert(abs_path.clone(), BookmarkEntry::Loaded(buffer_bookmarks)); } } pub fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> { worktree::File::from_dyn(buffer.read(cx).file()) .map(|file| file.worktree.read(cx).absolutize(&file.path)) .map(Arc::<Path>::from) } /// 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 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 { return; }; 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(); let existing_index = buffer_bookmarks.bookmarks.iter().position(|existing| { existing.anchor.summary::<Point>(&snapshot).row == anchor.summary::<Point>(&snapshot).row }); if let Some(index) = existing_index { buffer_bookmarks.bookmarks.remove(index); if buffer_bookmarks.bookmarks.is_empty() { self.bookmarks.remove(&abs_path); } } else { 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. pub fn bookmarks_for_buffer( &mut self, buffer: Entity<Buffer>, range: Range<text::Anchor>, buffer_snapshot: &BufferSnapshot, cx: &mut Context<Self>, ) -> Vec<Bookmark> { let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else { return Vec::new(); }; self.resolve_anchors_if_needed(&abs_path, &buffer, cx); let Some(BookmarkEntry::Loaded(file_bookmarks)) = self.bookmarks.get(&abs_path) else { return Vec::new(); }; file_bookmarks .bookmarks .iter() .filter_map({ move |bookmark| { 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() { return None; } Some(bookmark.clone()) } }) .collect() } fn handle_file_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) { let entity_id = buffer.entity_id(); if buffer .read(cx) .file() .is_none_or(|f| f.disk_state().is_deleted()) { self.bookmarks.retain(|_, entry| match entry { BookmarkEntry::Loaded(buffer_bookmarks) => { buffer_bookmarks.buffer.entity_id() != entity_id } BookmarkEntry::Unloaded(_) => true, }); cx.notify(); return; } if let Some(new_abs_path) = Self::abs_path_from_buffer(&buffer, cx) { if self.bookmarks.contains_key(&new_abs_path) { return; } if let Some(old_path) = self .bookmarks .iter() .find(|(_, entry)| match entry { BookmarkEntry::Loaded(buffer_bookmarks) => { buffer_bookmarks.buffer.entity_id() == entity_id } BookmarkEntry::Unloaded(_) => false, }) .map(|(path, _)| path) .cloned() { let Some(entry) = self.bookmarks.remove(&old_path) else { log::error!( "Couldn't get bookmarks from old path during buffer rename handling" ); return; }; self.bookmarks.insert(new_abs_path, entry); cx.notify(); } } } pub fn all_serialized_bookmarks( &self, cx: &App, ) -> BTreeMap<Arc<Path>, Vec<SerializedBookmark>> { self.bookmarks .iter() .filter_map(|(path, entry)| { let mut rows = match entry { BookmarkEntry::Unloaded(rows) => rows.clone(), BookmarkEntry::Loaded(buffer_bookmarks) => { let snapshot = buffer_bookmarks.buffer.read(cx).snapshot(); buffer_bookmarks .bookmarks .iter() .filter_map(|bookmark| { if !snapshot.can_resolve(&bookmark.anchor) { return None; } let row = snapshot.summary_for_anchor::<Point>(&bookmark.anchor).row; Some(SerializedBookmark { row, label: bookmark.label.clone(), }) }) .collect() } }; rows.sort_unstable_by_key(|a| a.row); rows.dedup_by_key(|a| a.row); if rows.is_empty() { None } else { Some((path.clone(), rows)) } }) .collect() } pub async fn all_bookmark_locations( this: Entity<BookmarkStore>, cx: &mut (impl AppContext + Clone), ) -> Result<HashMap<Entity<Buffer>, Vec<Range<Point>>>> { Self::resolve_all(&this, cx).await?; cx.read_entity(&this, |this, cx| { let mut locations: HashMap<_, Vec<_>> = HashMap::new(); for bookmarks in this.bookmarks.values().filter_map(BookmarkEntry::loaded) { let snapshot = cx.read_entity(bookmarks.buffer(), |b, _| b.snapshot()); let ranges: Vec<Range<Point>> = bookmarks .bookmarks() .iter() .map(|bookmark| { let row = snapshot.summary_for_anchor::<Point>(&bookmark.anchor).row; Point::row_range(row..row) }) .collect(); locations .entry(bookmarks.buffer().clone()) .or_default() .extend(ranges); } Ok(locations) }) } /// Opens buffers for all unloaded bookmark entries and resolves them to anchors. This is used to show all bookmarks in a large multi-buffer. async fn resolve_all(this: &Entity<Self>, cx: &mut (impl AppContext + Clone)) -> Result<()> { let unloaded_paths: Vec<Arc<Path>> = cx.read_entity(&this, |this, _| { this.bookmarks .iter() .filter_map(|(path, entry)| match entry { BookmarkEntry::Unloaded(_) => Some(path.clone()), BookmarkEntry::Loaded(_) => None, }) .collect_vec() }); if unloaded_paths.is_empty() { return Ok(()); } let worktree_store = cx.read_entity(&this, |this, _| this.worktree_store.clone()); let buffer_store = cx.read_entity(&this, |this, _| this.buffer_store.clone()); let open_tasks: FuturesUnordered<_> = unloaded_paths .iter() .map(|path| { open_path(path, &worktree_store, &buffer_store, cx.clone()) .map_err(move |e| (path, e)) .map_ok(move |b| (path, b)) }) .collect(); let opened: Vec<_> = open_tasks .inspect_err(|(path, error)| { log::warn!( "Could not open buffer for bookmarked path {}: {error}", path.display() ) }) .filter_map(|res| async move { res.ok() }) .collect() .await; cx.update_entity(&this, |this, cx| { for (path, buffer) in opened { this.resolve_anchors_if_needed(&path, &buffer, cx); } cx.notify(); }); Ok(()) } pub fn clear_bookmarks(&mut self, cx: &mut Context<Self>) { self.bookmarks.clear(); cx.notify(); }} async fn open_path( path: &Path, worktree_store: &Entity<WorktreeStore>, buffer_store: &Entity<BufferStore>, mut cx: impl AppContext,) -> Result<Entity<Buffer>> { let (worktree, worktree_path) = cx .update_entity(&worktree_store, |worktree_store, cx| { worktree_store.find_or_create_worktree(path, false, cx) }) .await?; let project_path = ProjectPath {Lines 1–500 of 512. Load more
use std::{collections::BTreeMap, ops::Range, path::Path, sync::Arc}; use anyhow::Result; use futures::{StreamExt, TryFutureExt, TryStreamExt, stream::FuturesUnordered}; use gpui::{App, AppContext, Context, Entity, Subscription, Task}; use itertools::Itertools; use language::{Buffer, BufferEvent}; use std::collections::HashMap; use text::{BufferSnapshot, Point}; use crate::{ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore}; #[derive(Clone, Debug)] pub struct Bookmark { pub anchor: text::Anchor, pub label: String, } #[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<Bookmark>, _subscription: Subscription, } impl BufferBookmarks { pub fn new(buffer: Entity<Buffer>, cx: &mut Context<BookmarkStore>) -> Self { let subscription = cx.subscribe( &buffer, |bookmark_store, buffer, event: &BufferEvent, cx| match event { BufferEvent::FileHandleChanged => { bookmark_store.handle_file_changed(buffer, cx); } _ => {} }, ); Self { buffer, bookmarks: Vec::new(), _subscription: subscription, } } pub fn buffer(&self) -> &Entity<Buffer> { &self.buffer } pub fn bookmarks(&self) -> &[Bookmark] { &self.bookmarks } } #[derive(Debug)] pub enum BookmarkEntry { Loaded(BufferBookmarks), Unloaded(Vec<SerializedBookmark>), } impl BookmarkEntry { pub fn is_empty(&self) -> bool { match self { BookmarkEntry::Loaded(buffer_bookmarks) => buffer_bookmarks.bookmarks.is_empty(), BookmarkEntry::Unloaded(rows) => rows.is_empty(), } } fn loaded(&self) -> Option<&BufferBookmarks> { match self { BookmarkEntry::Loaded(buffer_bookmarks) => Some(buffer_bookmarks), BookmarkEntry::Unloaded(_) => None, } } } pub struct BookmarkStore { buffer_store: Entity<BufferStore>, worktree_store: Entity<WorktreeStore>, bookmarks: BTreeMap<Arc<Path>, BookmarkEntry>, } impl BookmarkStore { pub fn new(worktree_store: Entity<WorktreeStore>, buffer_store: Entity<BufferStore>) -> Self { Self { buffer_store, worktree_store, bookmarks: BTreeMap::new(), } } pub fn load_serialized_bookmarks( &mut self, bookmark_rows: BTreeMap<Arc<Path>, Vec<SerializedBookmark>>, cx: &mut Context<Self>, ) -> Task<Result<()>> { self.bookmarks.clear(); for (path, rows) in bookmark_rows { if rows.is_empty() { continue; } let count = rows.len(); log::debug!("Stored {count} unloaded bookmark(s) at {}", path.display()); self.bookmarks.insert(path, BookmarkEntry::Unloaded(rows)); } cx.notify(); Task::ready(Ok(())) } fn resolve_anchors_if_needed( &mut self, abs_path: &Arc<Path>, buffer: &Entity<Buffer>, cx: &mut Context<Self>, ) { 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 bookmarks: Vec<Bookmark> = bookmarks .iter() .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, max_point.row ); return None; } let anchor = snapshot.anchor_after(point); Some(Bookmark { anchor, label: bookmark.label.clone(), }) }) .collect(); if bookmarks.is_empty() { self.bookmarks.remove(abs_path); } else { let mut buffer_bookmarks = BufferBookmarks::new(buffer.clone(), cx); buffer_bookmarks.bookmarks = bookmarks; self.bookmarks .insert(abs_path.clone(), BookmarkEntry::Loaded(buffer_bookmarks)); } } pub fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> { worktree::File::from_dyn(buffer.read(cx).file()) .map(|file| file.worktree.read(cx).absolutize(&file.path)) .map(Arc::<Path>::from) } /// 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 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 { return; }; 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(); let existing_index = buffer_bookmarks.bookmarks.iter().position(|existing| { existing.anchor.summary::<Point>(&snapshot).row == anchor.summary::<Point>(&snapshot).row }); if let Some(index) = existing_index { buffer_bookmarks.bookmarks.remove(index); if buffer_bookmarks.bookmarks.is_empty() { self.bookmarks.remove(&abs_path); } } else { 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. pub fn bookmarks_for_buffer( &mut self, buffer: Entity<Buffer>, range: Range<text::Anchor>, buffer_snapshot: &BufferSnapshot, cx: &mut Context<Self>, ) -> Vec<Bookmark> { let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else { return Vec::new(); }; self.resolve_anchors_if_needed(&abs_path, &buffer, cx); let Some(BookmarkEntry::Loaded(file_bookmarks)) = self.bookmarks.get(&abs_path) else { return Vec::new(); }; file_bookmarks .bookmarks .iter() .filter_map({ move |bookmark| { 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() { return None; } Some(bookmark.clone()) } }) .collect() } fn handle_file_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) { let entity_id = buffer.entity_id(); if buffer .read(cx) .file() .is_none_or(|f| f.disk_state().is_deleted()) { self.bookmarks.retain(|_, entry| match entry { BookmarkEntry::Loaded(buffer_bookmarks) => { buffer_bookmarks.buffer.entity_id() != entity_id } BookmarkEntry::Unloaded(_) => true, }); cx.notify(); return; } if let Some(new_abs_path) = Self::abs_path_from_buffer(&buffer, cx) { if self.bookmarks.contains_key(&new_abs_path) { return; } if let Some(old_path) = self .bookmarks .iter() .find(|(_, entry)| match entry { BookmarkEntry::Loaded(buffer_bookmarks) => { buffer_bookmarks.buffer.entity_id() == entity_id } BookmarkEntry::Unloaded(_) => false, }) .map(|(path, _)| path) .cloned() { let Some(entry) = self.bookmarks.remove(&old_path) else { log::error!( "Couldn't get bookmarks from old path during buffer rename handling" ); return; }; self.bookmarks.insert(new_abs_path, entry); cx.notify(); } } } pub fn all_serialized_bookmarks( &self, cx: &App, ) -> BTreeMap<Arc<Path>, Vec<SerializedBookmark>> { self.bookmarks .iter() .filter_map(|(path, entry)| { let mut rows = match entry { BookmarkEntry::Unloaded(rows) => rows.clone(), BookmarkEntry::Loaded(buffer_bookmarks) => { let snapshot = buffer_bookmarks.buffer.read(cx).snapshot(); buffer_bookmarks .bookmarks .iter() .filter_map(|bookmark| { if !snapshot.can_resolve(&bookmark.anchor) { return None; } let row = snapshot.summary_for_anchor::<Point>(&bookmark.anchor).row; Some(SerializedBookmark { row, label: bookmark.label.clone(), }) }) .collect() } }; rows.sort_unstable_by_key(|a| a.row); rows.dedup_by_key(|a| a.row); if rows.is_empty() { None } else { Some((path.clone(), rows)) } }) .collect() } pub async fn all_bookmark_locations( this: Entity<BookmarkStore>, cx: &mut (impl AppContext + Clone), ) -> Result<HashMap<Entity<Buffer>, Vec<Range<Point>>>> { Self::resolve_all(&this, cx).await?; cx.read_entity(&this, |this, cx| { let mut locations: HashMap<_, Vec<_>> = HashMap::new(); for bookmarks in this.bookmarks.values().filter_map(BookmarkEntry::loaded) { let snapshot = cx.read_entity(bookmarks.buffer(), |b, _| b.snapshot()); let ranges: Vec<Range<Point>> = bookmarks .bookmarks() .iter() .map(|bookmark| { let row = snapshot.summary_for_anchor::<Point>(&bookmark.anchor).row; Point::row_range(row..row) }) .collect(); locations .entry(bookmarks.buffer().clone()) .or_default() .extend(ranges); } Ok(locations) }) } /// Opens buffers for all unloaded bookmark entries and resolves them to anchors. This is used to show all bookmarks in a large multi-buffer. async fn resolve_all(this: &Entity<Self>, cx: &mut (impl AppContext + Clone)) -> Result<()> { let unloaded_paths: Vec<Arc<Path>> = cx.read_entity(&this, |this, _| { this.bookmarks .iter() .filter_map(|(path, entry)| match entry { BookmarkEntry::Unloaded(_) => Some(path.clone()), BookmarkEntry::Loaded(_) => None, }) .collect_vec() }); if unloaded_paths.is_empty() { return Ok(()); } let worktree_store = cx.read_entity(&this, |this, _| this.worktree_store.clone()); let buffer_store = cx.read_entity(&this, |this, _| this.buffer_store.clone()); let open_tasks: FuturesUnordered<_> = unloaded_paths .iter() .map(|path| { open_path(path, &worktree_store, &buffer_store, cx.clone()) .map_err(move |e| (path, e)) .map_ok(move |b| (path, b)) }) .collect(); let opened: Vec<_> = open_tasks .inspect_err(|(path, error)| { log::warn!( "Could not open buffer for bookmarked path {}: {error}", path.display() ) }) .filter_map(|res| async move { res.ok() }) .collect() .await; cx.update_entity(&this, |this, cx| { for (path, buffer) in opened { this.resolve_anchors_if_needed(&path, &buffer, cx); } cx.notify(); }); Ok(()) } pub fn clear_bookmarks(&mut self, cx: &mut Context<Self>) { self.bookmarks.clear(); cx.notify(); } } async fn open_path( path: &Path, worktree_store: &Entity<WorktreeStore>, buffer_store: &Entity<BufferStore>, mut cx: impl AppContext, ) -> Result<Entity<Buffer>> { let (worktree, worktree_path) = cx .update_entity(&worktree_store, |worktree_store, cx| { worktree_store.find_or_create_worktree(path, false, cx) }) .await?; let project_path = ProjectPath {