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                    true,
148                    100,
149                    &Default::default(),
150                    background,
151                )
152                .await
153                .into_iter()
154                .map(|mat| BookmarkEntry {
155                    bookmark: all_bookmarks[mat.candidate_id].clone(),
156                    positions: mat.positions,
157                })
158                .collect()
159            };
160
161            this.update(cx, |this, _cx| {
162                this.delegate.matches = matches;
163            })
164            .log_err();
165        })
166    }
167
168    fn confirm(&mut self, _secondary: bool, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {
169        //
170    }
171
172    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
173        self.picker
174            .update(cx, |_, cx| cx.emit(DismissEvent))
175            .log_err();
176    }
177
178    fn render_match(
179        &self,
180        ix: usize,
181        selected: bool,
182        _window: &mut Window,
183        _cx: &mut Context<Picker<Self>>,
184    ) -> Option<Self::ListItem> {
185        let entry = &self.matches[ix];
186
187        Some(
188            ListItem::new(ix)
189                .inset(true)
190                .spacing(ListItemSpacing::Sparse)
191                .toggle_state(selected)
192                .child(HighlightedLabel::new(
193                    entry.bookmark.ref_name.clone(),
194                    entry.positions.clone(),
195                )),
196        )
197    }
198}