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