bookmark_picker.rs

  1use std::sync::Arc;
  2
  3use fuzzy::{StringMatchCandidate, match_strings};
  4use gpui::{
  5    App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, Window,
  6    prelude::*,
  7};
  8use jj::{Bookmark, JujutsuStore};
  9use picker::{Picker, PickerDelegate};
 10use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
 11use util::ResultExt as _;
 12use workspace::{ModalView, Workspace};
 13
 14pub fn register(workspace: &mut Workspace) {
 15    workspace.register_action(open);
 16}
 17
 18fn open(
 19    workspace: &mut Workspace,
 20    _: &zed_actions::jj::BookmarkList,
 21    window: &mut Window,
 22    cx: &mut Context<Workspace>,
 23) {
 24    let Some(jj_store) = JujutsuStore::try_global(cx) else {
 25        return;
 26    };
 27
 28    workspace.toggle_modal(window, cx, |window, cx| {
 29        let delegate = BookmarkPickerDelegate::new(cx.entity().downgrade(), jj_store, cx);
 30        BookmarkPicker::new(delegate, window, cx)
 31    });
 32}
 33
 34pub struct BookmarkPicker {
 35    picker: Entity<Picker<BookmarkPickerDelegate>>,
 36}
 37
 38impl BookmarkPicker {
 39    pub fn new(
 40        delegate: BookmarkPickerDelegate,
 41        window: &mut Window,
 42        cx: &mut Context<Self>,
 43    ) -> Self {
 44        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
 45        Self { picker }
 46    }
 47}
 48
 49impl ModalView for BookmarkPicker {}
 50
 51impl EventEmitter<DismissEvent> for BookmarkPicker {}
 52
 53impl Focusable for BookmarkPicker {
 54    fn focus_handle(&self, cx: &App) -> FocusHandle {
 55        self.picker.focus_handle(cx)
 56    }
 57}
 58
 59impl Render for BookmarkPicker {
 60    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
 61        v_flex().w(rems(34.)).child(self.picker.clone())
 62    }
 63}
 64
 65#[derive(Debug, Clone)]
 66struct BookmarkEntry {
 67    bookmark: Bookmark,
 68    positions: Vec<usize>,
 69}
 70
 71pub struct BookmarkPickerDelegate {
 72    picker: WeakEntity<BookmarkPicker>,
 73    matches: Vec<BookmarkEntry>,
 74    all_bookmarks: Vec<Bookmark>,
 75    selected_index: usize,
 76}
 77
 78impl BookmarkPickerDelegate {
 79    fn new(
 80        picker: WeakEntity<BookmarkPicker>,
 81        jj_store: Entity<JujutsuStore>,
 82        cx: &mut Context<BookmarkPicker>,
 83    ) -> Self {
 84        let bookmarks = jj_store.read(cx).repository().list_bookmarks();
 85
 86        Self {
 87            picker,
 88            matches: Vec::new(),
 89            all_bookmarks: bookmarks,
 90            selected_index: 0,
 91        }
 92    }
 93}
 94
 95impl PickerDelegate for BookmarkPickerDelegate {
 96    type ListItem = ListItem;
 97
 98    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
 99        "Select Bookmark…".into()
100    }
101
102    fn match_count(&self) -> usize {
103        self.matches.len()
104    }
105
106    fn selected_index(&self) -> usize {
107        self.selected_index
108    }
109
110    fn set_selected_index(
111        &mut self,
112        ix: usize,
113        _window: &mut Window,
114        _cx: &mut Context<Picker<Self>>,
115    ) {
116        self.selected_index = ix;
117    }
118
119    fn update_matches(
120        &mut self,
121        query: String,
122        window: &mut Window,
123        cx: &mut Context<Picker<Self>>,
124    ) -> Task<()> {
125        let background = cx.background_executor().clone();
126        let all_bookmarks = self.all_bookmarks.clone();
127
128        cx.spawn_in(window, async move |this, cx| {
129            let matches = if query.is_empty() {
130                all_bookmarks
131                    .into_iter()
132                    .map(|bookmark| BookmarkEntry {
133                        bookmark,
134                        positions: Vec::new(),
135                    })
136                    .collect()
137            } else {
138                let candidates = all_bookmarks
139                    .iter()
140                    .enumerate()
141                    .map(|(ix, bookmark)| StringMatchCandidate::new(ix, &bookmark.ref_name))
142                    .collect::<Vec<_>>();
143                match_strings(
144                    &candidates,
145                    &query,
146                    false,
147                    100,
148                    &Default::default(),
149                    background,
150                )
151                .await
152                .into_iter()
153                .map(|mat| BookmarkEntry {
154                    bookmark: all_bookmarks[mat.candidate_id].clone(),
155                    positions: mat.positions,
156                })
157                .collect()
158            };
159
160            this.update(cx, |this, _cx| {
161                this.delegate.matches = matches;
162            })
163            .log_err();
164        })
165    }
166
167    fn confirm(&mut self, _secondary: bool, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {
168        //
169    }
170
171    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
172        self.picker
173            .update(cx, |_, cx| cx.emit(DismissEvent))
174            .log_err();
175    }
176
177    fn render_match(
178        &self,
179        ix: usize,
180        selected: bool,
181        _window: &mut Window,
182        _cx: &mut Context<Picker<Self>>,
183    ) -> Option<Self::ListItem> {
184        let entry = &self.matches[ix];
185
186        Some(
187            ListItem::new(ix)
188                .inset(true)
189                .spacing(ListItemSpacing::Sparse)
190                .toggle_state(selected)
191                .child(HighlightedLabel::new(
192                    entry.bookmark.ref_name.clone(),
193                    entry.positions.clone(),
194                )),
195        )
196    }
197}