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, 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 mat = &self.matches[self.selected_index];
196
197 let workspace = self.workspace.clone();
198 let Some(project) = workspace
199 .upgrade()
200 .map(|workspace| workspace.read(cx).project().clone())
201 else {
202 return;
203 };
204 let path = mat.path.clone();
205 let worktree_id = WorktreeId::from_usize(mat.worktree_id);
206 let confirm_behavior = self.confirm_behavior;
207 cx.spawn(|this, mut cx| async move {
208 let Some(open_buffer_task) = project
209 .update(&mut cx, |project, cx| {
210 project.open_buffer((worktree_id, path.clone()), cx)
211 })
212 .ok()
213 else {
214 return anyhow::Ok(());
215 };
216
217 let buffer = open_buffer_task.await?;
218
219 this.update(&mut cx, |this, cx| {
220 this.delegate
221 .context_store
222 .update(cx, |context_store, cx| {
223 let mut text = String::new();
224 text.push_str(&codeblock_fence_for_path(Some(&path), None));
225 text.push_str(&buffer.read(cx).text());
226 if !text.ends_with('\n') {
227 text.push('\n');
228 }
229
230 text.push_str("```\n");
231
232 context_store.insert_context(
233 ContextKind::File,
234 path.to_string_lossy().to_string(),
235 text,
236 );
237 })?;
238
239 match confirm_behavior {
240 ConfirmBehavior::KeepOpen => {}
241 ConfirmBehavior::Close => this.delegate.dismissed(cx),
242 }
243
244 anyhow::Ok(())
245 })??;
246
247 anyhow::Ok(())
248 })
249 .detach_and_log_err(cx);
250 }
251
252 fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
253 self.context_picker
254 .update(cx, |this, cx| {
255 this.reset_mode();
256 cx.emit(DismissEvent);
257 })
258 .ok();
259 }
260
261 fn render_match(
262 &self,
263 ix: usize,
264 selected: bool,
265 _cx: &mut ViewContext<Picker<Self>>,
266 ) -> Option<Self::ListItem> {
267 let path_match = &self.matches[ix];
268
269 let (file_name, directory) = if path_match.path.as_ref() == Path::new("") {
270 (SharedString::from(path_match.path_prefix.clone()), None)
271 } else {
272 let file_name = path_match
273 .path
274 .file_name()
275 .unwrap_or_default()
276 .to_string_lossy()
277 .to_string()
278 .into();
279
280 let mut directory = format!("{}/", path_match.path_prefix);
281 if let Some(parent) = path_match
282 .path
283 .parent()
284 .filter(|parent| parent != &Path::new(""))
285 {
286 directory.push_str(&parent.to_string_lossy());
287 directory.push('/');
288 }
289
290 (file_name, Some(directory))
291 };
292
293 Some(
294 ListItem::new(ix).inset(true).toggle_state(selected).child(
295 h_flex()
296 .gap_2()
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 )
305 }
306}
307
308fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option<RangeInclusive<u32>>) -> String {
309 let mut text = String::new();
310 write!(text, "```").unwrap();
311
312 if let Some(path) = path {
313 if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
314 write!(text, "{} ", extension).unwrap();
315 }
316
317 write!(text, "{}", path.display()).unwrap();
318 } else {
319 write!(text, "untitled").unwrap();
320 }
321
322 if let Some(row_range) = row_range {
323 write!(text, ":{}-{}", row_range.start() + 1, row_range.end() + 1).unwrap();
324 }
325
326 text.push('\n');
327 text
328}