1use std::path::Path;
2use std::sync::atomic::AtomicBool;
3use std::sync::Arc;
4
5use fuzzy::PathMatch;
6use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
7use picker::{Picker, PickerDelegate};
8use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
9use ui::{prelude::*, ListItem};
10use util::ResultExt as _;
11use workspace::{notifications::NotifyResultExt, Workspace};
12
13use crate::context_picker::{ConfirmBehavior, ContextPicker};
14use crate::context_store::ContextStore;
15
16pub struct DirectoryContextPicker {
17 picker: Entity<Picker<DirectoryContextPickerDelegate>>,
18}
19
20impl DirectoryContextPicker {
21 pub fn new(
22 context_picker: WeakEntity<ContextPicker>,
23 workspace: WeakEntity<Workspace>,
24 context_store: WeakEntity<ContextStore>,
25 confirm_behavior: ConfirmBehavior,
26 window: &mut Window,
27 cx: &mut Context<Self>,
28 ) -> Self {
29 let delegate = DirectoryContextPickerDelegate::new(
30 context_picker,
31 workspace,
32 context_store,
33 confirm_behavior,
34 );
35 let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
36
37 Self { picker }
38 }
39}
40
41impl Focusable for DirectoryContextPicker {
42 fn focus_handle(&self, cx: &App) -> FocusHandle {
43 self.picker.focus_handle(cx)
44 }
45}
46
47impl Render for DirectoryContextPicker {
48 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
49 self.picker.clone()
50 }
51}
52
53pub struct DirectoryContextPickerDelegate {
54 context_picker: WeakEntity<ContextPicker>,
55 workspace: WeakEntity<Workspace>,
56 context_store: WeakEntity<ContextStore>,
57 confirm_behavior: ConfirmBehavior,
58 matches: Vec<PathMatch>,
59 selected_index: usize,
60}
61
62impl DirectoryContextPickerDelegate {
63 pub fn new(
64 context_picker: WeakEntity<ContextPicker>,
65 workspace: WeakEntity<Workspace>,
66 context_store: WeakEntity<ContextStore>,
67 confirm_behavior: ConfirmBehavior,
68 ) -> Self {
69 Self {
70 context_picker,
71 workspace,
72 context_store,
73 confirm_behavior,
74 matches: Vec::new(),
75 selected_index: 0,
76 }
77 }
78
79 fn search(
80 &mut self,
81 query: String,
82 cancellation_flag: Arc<AtomicBool>,
83 workspace: &Entity<Workspace>,
84 cx: &mut Context<Picker<Self>>,
85 ) -> Task<Vec<PathMatch>> {
86 if query.is_empty() {
87 let workspace = workspace.read(cx);
88 let project = workspace.project().read(cx);
89 let directory_matches = project.worktrees(cx).flat_map(|worktree| {
90 let worktree = worktree.read(cx);
91 let path_prefix: Arc<str> = worktree.root_name().into();
92 worktree.directories(false, 0).map(move |entry| PathMatch {
93 score: 0.,
94 positions: Vec::new(),
95 worktree_id: worktree.id().to_usize(),
96 path: entry.path.clone(),
97 path_prefix: path_prefix.clone(),
98 distance_to_relative_ancestor: 0,
99 is_dir: true,
100 })
101 });
102
103 Task::ready(directory_matches.collect())
104 } else {
105 let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
106 let candidate_sets = worktrees
107 .into_iter()
108 .map(|worktree| {
109 let worktree = worktree.read(cx);
110
111 PathMatchCandidateSet {
112 snapshot: worktree.snapshot(),
113 include_ignored: worktree
114 .root_entry()
115 .map_or(false, |entry| entry.is_ignored),
116 include_root_name: true,
117 candidates: project::Candidates::Directories,
118 }
119 })
120 .collect::<Vec<_>>();
121
122 let executor = cx.background_executor().clone();
123 cx.foreground_executor().spawn(async move {
124 fuzzy::match_path_sets(
125 candidate_sets.as_slice(),
126 query.as_str(),
127 None,
128 false,
129 100,
130 &cancellation_flag,
131 executor,
132 )
133 .await
134 })
135 }
136 }
137}
138
139impl PickerDelegate for DirectoryContextPickerDelegate {
140 type ListItem = ListItem;
141
142 fn match_count(&self) -> usize {
143 self.matches.len()
144 }
145
146 fn selected_index(&self) -> usize {
147 self.selected_index
148 }
149
150 fn set_selected_index(
151 &mut self,
152 ix: usize,
153 _window: &mut Window,
154 _cx: &mut Context<Picker<Self>>,
155 ) {
156 self.selected_index = ix;
157 }
158
159 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
160 "Search folders…".into()
161 }
162
163 fn update_matches(
164 &mut self,
165 query: String,
166 _window: &mut Window,
167 cx: &mut Context<Picker<Self>>,
168 ) -> Task<()> {
169 let Some(workspace) = self.workspace.upgrade() else {
170 return Task::ready(());
171 };
172
173 let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, cx);
174
175 cx.spawn(|this, mut cx| async move {
176 let mut paths = search_task.await;
177 let empty_path = Path::new("");
178 paths.retain(|path_match| path_match.path.as_ref() != empty_path);
179
180 this.update(&mut cx, |this, _cx| {
181 this.delegate.matches = paths;
182 })
183 .log_err();
184 })
185 }
186
187 fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
188 let Some(mat) = self.matches.get(self.selected_index) else {
189 return;
190 };
191
192 let project_path = ProjectPath {
193 worktree_id: WorktreeId::from_usize(mat.worktree_id),
194 path: mat.path.clone(),
195 };
196
197 let Some(task) = self
198 .context_store
199 .update(cx, |context_store, cx| {
200 context_store.add_directory(project_path, cx)
201 })
202 .ok()
203 else {
204 return;
205 };
206
207 let confirm_behavior = self.confirm_behavior;
208 cx.spawn_in(window, |this, mut cx| async move {
209 match task.await.notify_async_err(&mut cx) {
210 None => anyhow::Ok(()),
211 Some(()) => this.update_in(&mut cx, |this, window, cx| match confirm_behavior {
212 ConfirmBehavior::KeepOpen => {}
213 ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
214 }),
215 }
216 })
217 .detach_and_log_err(cx);
218 }
219
220 fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
221 self.context_picker
222 .update(cx, |_, cx| {
223 cx.emit(DismissEvent);
224 })
225 .ok();
226 }
227
228 fn render_match(
229 &self,
230 ix: usize,
231 selected: bool,
232 _window: &mut Window,
233 cx: &mut Context<Picker<Self>>,
234 ) -> Option<Self::ListItem> {
235 let path_match = &self.matches[ix];
236 let directory_name = path_match.path.to_string_lossy().to_string();
237
238 let added = self.context_store.upgrade().map_or(false, |context_store| {
239 context_store
240 .read(cx)
241 .includes_directory(&path_match.path)
242 .is_some()
243 });
244
245 Some(
246 ListItem::new(ix)
247 .inset(true)
248 .toggle_state(selected)
249 .start_slot(
250 Icon::new(IconName::Folder)
251 .size(IconSize::XSmall)
252 .color(Color::Muted),
253 )
254 .child(Label::new(directory_name))
255 .when(added, |el| {
256 el.end_slot(
257 h_flex()
258 .gap_1()
259 .child(
260 Icon::new(IconName::Check)
261 .size(IconSize::Small)
262 .color(Color::Success),
263 )
264 .child(Label::new("Added").size(LabelSize::Small)),
265 )
266 }),
267 )
268 }
269}