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