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}