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