1use std::path::Path;
2use std::sync::atomic::AtomicBool;
3use std::sync::Arc;
4
5use file_icons::FileIcons;
6use fuzzy::PathMatch;
7use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
8use picker::{Picker, PickerDelegate};
9use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
10use ui::{prelude::*, ListItem, Tooltip};
11use util::ResultExt as _;
12use workspace::Workspace;
13
14use crate::context_picker::{ConfirmBehavior, ContextPicker};
15use crate::context_store::{ContextStore, FileInclusion};
16
17pub struct FileContextPicker {
18 picker: View<Picker<FileContextPickerDelegate>>,
19}
20
21impl FileContextPicker {
22 pub fn new(
23 context_picker: WeakView<ContextPicker>,
24 workspace: WeakView<Workspace>,
25 context_store: WeakModel<ContextStore>,
26 confirm_behavior: ConfirmBehavior,
27 cx: &mut ViewContext<Self>,
28 ) -> Self {
29 let delegate = FileContextPickerDelegate::new(
30 context_picker,
31 workspace,
32 context_store,
33 confirm_behavior,
34 );
35 let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
36
37 Self { picker }
38 }
39}
40
41impl FocusableView for FileContextPicker {
42 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
43 self.picker.focus_handle(cx)
44 }
45}
46
47impl Render for FileContextPicker {
48 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
49 self.picker.clone()
50 }
51}
52
53pub struct FileContextPickerDelegate {
54 context_picker: WeakView<ContextPicker>,
55 workspace: WeakView<Workspace>,
56 context_store: WeakModel<ContextStore>,
57 confirm_behavior: ConfirmBehavior,
58 matches: Vec<PathMatch>,
59 selected_index: usize,
60}
61
62impl FileContextPickerDelegate {
63 pub fn new(
64 context_picker: WeakView<ContextPicker>,
65 workspace: WeakView<Workspace>,
66 context_store: WeakModel<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: &View<Workspace>,
84 cx: &mut ViewContext<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 recent_matches = workspace
90 .recent_navigation_history(Some(10), cx)
91 .into_iter()
92 .filter_map(|(project_path, _)| {
93 let worktree = project.worktree_for_id(project_path.worktree_id, cx)?;
94 Some(PathMatch {
95 score: 0.,
96 positions: Vec::new(),
97 worktree_id: project_path.worktree_id.to_usize(),
98 path: project_path.path,
99 path_prefix: worktree.read(cx).root_name().into(),
100 distance_to_relative_ancestor: 0,
101 is_dir: false,
102 })
103 });
104
105 let file_matches = project.worktrees(cx).flat_map(|worktree| {
106 let worktree = worktree.read(cx);
107 let path_prefix: Arc<str> = worktree.root_name().into();
108 worktree.files(true, 0).map(move |entry| PathMatch {
109 score: 0.,
110 positions: Vec::new(),
111 worktree_id: worktree.id().to_usize(),
112 path: entry.path.clone(),
113 path_prefix: path_prefix.clone(),
114 distance_to_relative_ancestor: 0,
115 is_dir: false,
116 })
117 });
118
119 Task::ready(recent_matches.chain(file_matches).collect())
120 } else {
121 let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
122 let candidate_sets = worktrees
123 .into_iter()
124 .map(|worktree| {
125 let worktree = worktree.read(cx);
126
127 PathMatchCandidateSet {
128 snapshot: worktree.snapshot(),
129 include_ignored: worktree
130 .root_entry()
131 .map_or(false, |entry| entry.is_ignored),
132 include_root_name: true,
133 candidates: project::Candidates::Files,
134 }
135 })
136 .collect::<Vec<_>>();
137
138 let executor = cx.background_executor().clone();
139 cx.foreground_executor().spawn(async move {
140 fuzzy::match_path_sets(
141 candidate_sets.as_slice(),
142 query.as_str(),
143 None,
144 false,
145 100,
146 &cancellation_flag,
147 executor,
148 )
149 .await
150 })
151 }
152 }
153}
154
155impl PickerDelegate for FileContextPickerDelegate {
156 type ListItem = ListItem;
157
158 fn match_count(&self) -> usize {
159 self.matches.len()
160 }
161
162 fn selected_index(&self) -> usize {
163 self.selected_index
164 }
165
166 fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
167 self.selected_index = ix;
168 }
169
170 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
171 "Search files…".into()
172 }
173
174 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
175 let Some(workspace) = self.workspace.upgrade() else {
176 return Task::ready(());
177 };
178
179 let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, cx);
180
181 cx.spawn(|this, mut cx| async move {
182 // TODO: This should be probably be run in the background.
183 let paths = search_task.await;
184
185 this.update(&mut cx, |this, _cx| {
186 this.delegate.matches = paths;
187 })
188 .log_err();
189 })
190 }
191
192 fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
193 let Some(mat) = self.matches.get(self.selected_index) else {
194 return;
195 };
196
197 let project_path = ProjectPath {
198 worktree_id: WorktreeId::from_usize(mat.worktree_id),
199 path: mat.path.clone(),
200 };
201
202 let Some(task) = self
203 .context_store
204 .update(cx, |context_store, cx| {
205 context_store.add_file_from_path(project_path, cx)
206 })
207 .ok()
208 else {
209 return;
210 };
211
212 let workspace = self.workspace.clone();
213 let confirm_behavior = self.confirm_behavior;
214 cx.spawn(|this, mut cx| async move {
215 match task.await {
216 Ok(()) => {
217 this.update(&mut cx, |this, cx| match confirm_behavior {
218 ConfirmBehavior::KeepOpen => {}
219 ConfirmBehavior::Close => this.delegate.dismissed(cx),
220 })?;
221 }
222 Err(err) => {
223 let Some(workspace) = workspace.upgrade() else {
224 return anyhow::Ok(());
225 };
226
227 workspace.update(&mut cx, |workspace, cx| {
228 workspace.show_error(&err, cx);
229 })?;
230 }
231 }
232
233 anyhow::Ok(())
234 })
235 .detach_and_log_err(cx);
236 }
237
238 fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
239 self.context_picker
240 .update(cx, |this, cx| {
241 this.reset_mode();
242 cx.emit(DismissEvent);
243 })
244 .ok();
245 }
246
247 fn render_match(
248 &self,
249 ix: usize,
250 selected: bool,
251 cx: &mut ViewContext<Picker<Self>>,
252 ) -> Option<Self::ListItem> {
253 let path_match = &self.matches[ix];
254
255 let (file_name, directory) = if path_match.path.as_ref() == Path::new("") {
256 (SharedString::from(path_match.path_prefix.clone()), None)
257 } else {
258 let file_name = path_match
259 .path
260 .file_name()
261 .unwrap_or_default()
262 .to_string_lossy()
263 .to_string()
264 .into();
265
266 let mut directory = format!("{}/", path_match.path_prefix);
267 if let Some(parent) = path_match
268 .path
269 .parent()
270 .filter(|parent| parent != &Path::new(""))
271 {
272 directory.push_str(&parent.to_string_lossy());
273 directory.push('/');
274 }
275
276 (file_name, Some(directory))
277 };
278
279 let added = self.context_store.upgrade().and_then(|context_store| {
280 context_store
281 .read(cx)
282 .will_include_file_path(&path_match.path, cx)
283 });
284
285 let file_icon = FileIcons::get_icon(&path_match.path.clone(), cx)
286 .map(Icon::from_path)
287 .unwrap_or_else(|| Icon::new(IconName::File));
288
289 Some(
290 ListItem::new(ix)
291 .inset(true)
292 .toggle_state(selected)
293 .child(
294 h_flex()
295 .gap_2()
296 .child(file_icon.size(IconSize::Small))
297 .child(Label::new(file_name))
298 .children(directory.map(|directory| {
299 Label::new(directory)
300 .size(LabelSize::Small)
301 .color(Color::Muted)
302 })),
303 )
304 .when_some(added, |el, added| match added {
305 FileInclusion::Direct(_) => el.end_slot(
306 h_flex()
307 .gap_1()
308 .child(
309 Icon::new(IconName::Check)
310 .size(IconSize::Small)
311 .color(Color::Success),
312 )
313 .child(Label::new("Added").size(LabelSize::Small)),
314 ),
315 FileInclusion::InDirectory(dir_name) => {
316 let dir_name = dir_name.to_string_lossy().into_owned();
317
318 el.end_slot(
319 h_flex()
320 .gap_1()
321 .child(
322 Icon::new(IconName::Check)
323 .size(IconSize::Small)
324 .color(Color::Success),
325 )
326 .child(Label::new("Included").size(LabelSize::Small)),
327 )
328 .tooltip(move |cx| Tooltip::text(format!("in {dir_name}"), cx))
329 }
330 }),
331 )
332 }
333}