1use std::fmt::Write as _;
2use std::ops::RangeInclusive;
3use std::path::{Path, PathBuf};
4use std::sync::atomic::AtomicBool;
5use std::sync::Arc;
6
7use fuzzy::PathMatch;
8use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, 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::message_editor::MessageEditor;
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 message_editor: WeakView<MessageEditor>,
28 cx: &mut ViewContext<Self>,
29 ) -> Self {
30 let delegate = FileContextPickerDelegate::new(context_picker, workspace, message_editor);
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 message_editor: WeakView<MessageEditor>,
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 message_editor: WeakView<MessageEditor>,
62 ) -> Self {
63 Self {
64 context_picker,
65 workspace,
66 message_editor,
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 entries = workspace.recent_navigation_history(Some(10), cx);
83
84 let entries = entries
85 .into_iter()
86 .map(|entries| entries.0)
87 .chain(project.worktrees(cx).flat_map(|worktree| {
88 let worktree = worktree.read(cx);
89 let id = worktree.id();
90 worktree
91 .child_entries(Path::new(""))
92 .filter(|entry| entry.kind.is_file())
93 .map(move |entry| project::ProjectPath {
94 worktree_id: id,
95 path: entry.path.clone(),
96 })
97 }))
98 .collect::<Vec<_>>();
99
100 let path_prefix: Arc<str> = Arc::default();
101 Task::ready(
102 entries
103 .into_iter()
104 .filter_map(|entry| {
105 let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
106 let mut full_path = PathBuf::from(worktree.read(cx).root_name());
107 full_path.push(&entry.path);
108 Some(PathMatch {
109 score: 0.,
110 positions: Vec::new(),
111 worktree_id: entry.worktree_id.to_usize(),
112 path: full_path.into(),
113 path_prefix: path_prefix.clone(),
114 distance_to_relative_ancestor: 0,
115 is_dir: false,
116 })
117 })
118 .collect(),
119 )
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 mat = &self.matches[self.selected_index];
194
195 let workspace = self.workspace.clone();
196 let Some(project) = workspace
197 .upgrade()
198 .map(|workspace| workspace.read(cx).project().clone())
199 else {
200 return;
201 };
202 let path = mat.path.clone();
203 let worktree_id = WorktreeId::from_usize(mat.worktree_id);
204 cx.spawn(|this, mut cx| async move {
205 let Some(open_buffer_task) = project
206 .update(&mut cx, |project, cx| {
207 project.open_buffer((worktree_id, path.clone()), cx)
208 })
209 .ok()
210 else {
211 return anyhow::Ok(());
212 };
213
214 let buffer = open_buffer_task.await?;
215
216 this.update(&mut cx, |this, cx| {
217 this.delegate
218 .message_editor
219 .update(cx, |message_editor, cx| {
220 let mut text = String::new();
221 text.push_str(&codeblock_fence_for_path(Some(&path), None));
222 text.push_str(&buffer.read(cx).text());
223 if !text.ends_with('\n') {
224 text.push('\n');
225 }
226
227 text.push_str("```\n");
228
229 message_editor.insert_context(
230 ContextKind::File,
231 path.to_string_lossy().to_string(),
232 text,
233 );
234 })
235 })??;
236
237 anyhow::Ok(())
238 })
239 .detach_and_log_err(cx);
240 }
241
242 fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
243 self.context_picker
244 .update(cx, |this, cx| {
245 this.reset_mode();
246 cx.emit(DismissEvent);
247 })
248 .ok();
249 }
250
251 fn render_match(
252 &self,
253 ix: usize,
254 selected: bool,
255 _cx: &mut ViewContext<Picker<Self>>,
256 ) -> Option<Self::ListItem> {
257 let path_match = &self.matches[ix];
258 let file_name = path_match
259 .path
260 .file_name()
261 .unwrap_or_default()
262 .to_string_lossy()
263 .to_string();
264 let directory = path_match
265 .path
266 .parent()
267 .map(|directory| format!("{}/", directory.to_string_lossy()));
268
269 Some(
270 ListItem::new(ix).inset(true).toggle_state(selected).child(
271 h_flex()
272 .gap_2()
273 .child(Label::new(file_name))
274 .children(directory.map(|directory| {
275 Label::new(directory)
276 .size(LabelSize::Small)
277 .color(Color::Muted)
278 })),
279 ),
280 )
281 }
282}
283
284fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option<RangeInclusive<u32>>) -> String {
285 let mut text = String::new();
286 write!(text, "```").unwrap();
287
288 if let Some(path) = path {
289 if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
290 write!(text, "{} ", extension).unwrap();
291 }
292
293 write!(text, "{}", path.display()).unwrap();
294 } else {
295 write!(text, "untitled").unwrap();
296 }
297
298 if let Some(row_range) = row_range {
299 write!(text, ":{}-{}", row_range.start() + 1, row_range.end() + 1).unwrap();
300 }
301
302 text.push('\n');
303 text
304}