context_picker.rs

  1mod directory_context_picker;
  2mod fetch_context_picker;
  3mod file_context_picker;
  4mod thread_context_picker;
  5
  6use std::path::PathBuf;
  7use std::sync::Arc;
  8
  9use editor::Editor;
 10use file_context_picker::render_file_context_entry;
 11use gpui::{
 12    AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, View, WeakModel, WeakView,
 13};
 14use project::ProjectPath;
 15use thread_context_picker::{render_thread_context_entry, ThreadContextEntry};
 16use ui::{prelude::*, ContextMenu, ContextMenuEntry, ContextMenuItem};
 17use workspace::Workspace;
 18
 19use crate::context::ContextKind;
 20use crate::context_picker::directory_context_picker::DirectoryContextPicker;
 21use crate::context_picker::fetch_context_picker::FetchContextPicker;
 22use crate::context_picker::file_context_picker::FileContextPicker;
 23use crate::context_picker::thread_context_picker::ThreadContextPicker;
 24use crate::context_store::ContextStore;
 25use crate::thread_store::ThreadStore;
 26use crate::AssistantPanel;
 27
 28#[derive(Debug, Clone, Copy)]
 29pub enum ConfirmBehavior {
 30    KeepOpen,
 31    Close,
 32}
 33
 34#[derive(Debug, Clone)]
 35enum ContextPickerMode {
 36    Default(View<ContextMenu>),
 37    File(View<FileContextPicker>),
 38    Directory(View<DirectoryContextPicker>),
 39    Fetch(View<FetchContextPicker>),
 40    Thread(View<ThreadContextPicker>),
 41}
 42
 43pub(super) struct ContextPicker {
 44    mode: ContextPickerMode,
 45    workspace: WeakView<Workspace>,
 46    context_store: WeakModel<ContextStore>,
 47    thread_store: Option<WeakModel<ThreadStore>>,
 48    confirm_behavior: ConfirmBehavior,
 49}
 50
 51impl ContextPicker {
 52    pub fn new(
 53        workspace: WeakView<Workspace>,
 54        thread_store: Option<WeakModel<ThreadStore>>,
 55        context_store: WeakModel<ContextStore>,
 56        confirm_behavior: ConfirmBehavior,
 57        cx: &mut ViewContext<Self>,
 58    ) -> Self {
 59        ContextPicker {
 60            mode: ContextPickerMode::Default(ContextMenu::build(cx, |menu, _cx| menu)),
 61            workspace,
 62            context_store,
 63            thread_store,
 64            confirm_behavior,
 65        }
 66    }
 67
 68    pub fn reset_mode(&mut self, cx: &mut ViewContext<Self>) {
 69        self.mode = ContextPickerMode::Default(self.build(cx));
 70    }
 71
 72    fn build(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
 73        let context_picker = cx.view().clone();
 74
 75        ContextMenu::build(cx, move |menu, cx| {
 76            let kind_entry = |kind: &'static ContextKind| {
 77                let context_picker = context_picker.clone();
 78
 79                ContextMenuEntry::new(kind.label())
 80                    .icon(kind.icon())
 81                    .handler(move |cx| {
 82                        context_picker.update(cx, |this, cx| this.select_kind(*kind, cx))
 83                    })
 84            };
 85
 86            let recent = self.recent_entries(cx);
 87            let has_recent = !recent.is_empty();
 88            let recent_entries = recent
 89                .into_iter()
 90                .enumerate()
 91                .map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
 92
 93            menu.when(has_recent, |menu| menu.label("Recent"))
 94                .extend(recent_entries)
 95                .when(has_recent, |menu| menu.separator())
 96                .extend(ContextKind::all().into_iter().map(kind_entry))
 97        })
 98    }
 99
100    fn select_kind(&mut self, kind: ContextKind, cx: &mut ViewContext<Self>) {
101        let context_picker = cx.view().downgrade();
102
103        match kind {
104            ContextKind::File => {
105                self.mode = ContextPickerMode::File(cx.new_view(|cx| {
106                    FileContextPicker::new(
107                        context_picker.clone(),
108                        self.workspace.clone(),
109                        self.context_store.clone(),
110                        self.confirm_behavior,
111                        cx,
112                    )
113                }));
114            }
115            ContextKind::Directory => {
116                self.mode = ContextPickerMode::Directory(cx.new_view(|cx| {
117                    DirectoryContextPicker::new(
118                        context_picker.clone(),
119                        self.workspace.clone(),
120                        self.context_store.clone(),
121                        self.confirm_behavior,
122                        cx,
123                    )
124                }));
125            }
126            ContextKind::FetchedUrl => {
127                self.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
128                    FetchContextPicker::new(
129                        context_picker.clone(),
130                        self.workspace.clone(),
131                        self.context_store.clone(),
132                        self.confirm_behavior,
133                        cx,
134                    )
135                }));
136            }
137            ContextKind::Thread => {
138                if let Some(thread_store) = self.thread_store.as_ref() {
139                    self.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
140                        ThreadContextPicker::new(
141                            thread_store.clone(),
142                            context_picker.clone(),
143                            self.context_store.clone(),
144                            self.confirm_behavior,
145                            cx,
146                        )
147                    }));
148                }
149            }
150        }
151
152        cx.notify();
153        cx.focus_self();
154    }
155
156    fn recent_menu_item(
157        &self,
158        context_picker: View<ContextPicker>,
159        ix: usize,
160        entry: RecentEntry,
161    ) -> ContextMenuItem {
162        match entry {
163            RecentEntry::File {
164                project_path,
165                path_prefix,
166            } => {
167                let context_store = self.context_store.clone();
168                let path = project_path.path.clone();
169
170                ContextMenuItem::custom_entry(
171                    move |cx| {
172                        render_file_context_entry(
173                            ElementId::NamedInteger("ctx-recent".into(), ix),
174                            &path,
175                            &path_prefix,
176                            context_store.clone(),
177                            cx,
178                        )
179                        .into_any()
180                    },
181                    move |cx| {
182                        context_picker.update(cx, |this, cx| {
183                            this.add_recent_file(project_path.clone(), cx);
184                        })
185                    },
186                )
187            }
188            RecentEntry::Thread(thread) => {
189                let context_store = self.context_store.clone();
190                let view_thread = thread.clone();
191
192                ContextMenuItem::custom_entry(
193                    move |cx| {
194                        render_thread_context_entry(&view_thread, context_store.clone(), cx)
195                            .into_any()
196                    },
197                    move |cx| {
198                        context_picker.update(cx, |this, cx| {
199                            this.add_recent_thread(thread.clone(), cx);
200                        })
201                    },
202                )
203            }
204        }
205    }
206
207    fn add_recent_file(&self, project_path: ProjectPath, cx: &mut ViewContext<Self>) {
208        let Some(context_store) = self.context_store.upgrade() else {
209            return;
210        };
211
212        let task = context_store.update(cx, |context_store, cx| {
213            context_store.add_file_from_path(project_path.clone(), cx)
214        });
215
216        let workspace = self.workspace.clone();
217
218        cx.spawn(|_, mut cx| async move {
219            match task.await {
220                Ok(_) => {
221                    return anyhow::Ok(());
222                }
223                Err(err) => {
224                    let Some(workspace) = workspace.upgrade() else {
225                        return anyhow::Ok(());
226                    };
227
228                    workspace.update(&mut cx, |workspace, cx| {
229                        workspace.show_error(&err, cx);
230                    })
231                }
232            }
233        })
234        .detach_and_log_err(cx);
235
236        cx.notify();
237    }
238
239    fn add_recent_thread(&self, thread: ThreadContextEntry, cx: &mut ViewContext<Self>) {
240        let Some(context_store) = self.context_store.upgrade() else {
241            return;
242        };
243
244        let Some(thread) = self
245            .thread_store
246            .clone()
247            .and_then(|this| this.upgrade())
248            .and_then(|this| this.update(cx, |this, cx| this.open_thread(&thread.id, cx)))
249        else {
250            return;
251        };
252
253        context_store.update(cx, |context_store, cx| {
254            context_store.add_thread(thread, cx);
255        });
256
257        cx.notify();
258    }
259
260    fn recent_entries(&self, cx: &mut WindowContext) -> Vec<RecentEntry> {
261        let Some(workspace) = self.workspace.upgrade().map(|w| w.read(cx)) else {
262            return vec![];
263        };
264
265        let Some(context_store) = self.context_store.upgrade().map(|cs| cs.read(cx)) else {
266            return vec![];
267        };
268
269        let mut recent = Vec::with_capacity(6);
270
271        let mut current_files = context_store.file_paths(cx);
272
273        if let Some(active_path) = Self::active_singleton_buffer_path(&workspace, cx) {
274            current_files.insert(active_path);
275        }
276
277        let project = workspace.project().read(cx);
278
279        recent.extend(
280            workspace
281                .recent_navigation_history_iter(cx)
282                .filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
283                .take(4)
284                .filter_map(|(project_path, _)| {
285                    project
286                        .worktree_for_id(project_path.worktree_id, cx)
287                        .map(|worktree| RecentEntry::File {
288                            project_path,
289                            path_prefix: worktree.read(cx).root_name().into(),
290                        })
291                }),
292        );
293
294        let mut current_threads = context_store.thread_ids();
295
296        if let Some(active_thread) = workspace
297            .panel::<AssistantPanel>(cx)
298            .map(|panel| panel.read(cx).active_thread(cx))
299        {
300            current_threads.insert(active_thread.read(cx).id().clone());
301        }
302
303        let Some(thread_store) = self
304            .thread_store
305            .as_ref()
306            .and_then(|thread_store| thread_store.upgrade())
307        else {
308            return recent;
309        };
310
311        thread_store.update(cx, |thread_store, cx| {
312            recent.extend(
313                thread_store
314                    .threads(cx)
315                    .into_iter()
316                    .filter(|thread| !current_threads.contains(thread.read(cx).id()))
317                    .take(2)
318                    .map(|thread| {
319                        let thread = thread.read(cx);
320
321                        RecentEntry::Thread(ThreadContextEntry {
322                            id: thread.id().clone(),
323                            summary: thread.summary_or_default(),
324                        })
325                    }),
326            )
327        });
328
329        recent
330    }
331
332    fn active_singleton_buffer_path(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
333        let active_item = workspace.active_item(cx)?;
334
335        let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
336        let buffer = editor.buffer().read(cx).as_singleton()?;
337
338        let path = buffer.read(cx).file()?.path().to_path_buf();
339        Some(path)
340    }
341}
342
343impl EventEmitter<DismissEvent> for ContextPicker {}
344
345impl FocusableView for ContextPicker {
346    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
347        match &self.mode {
348            ContextPickerMode::Default(menu) => menu.focus_handle(cx),
349            ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
350            ContextPickerMode::Directory(directory_picker) => directory_picker.focus_handle(cx),
351            ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
352            ContextPickerMode::Thread(thread_picker) => thread_picker.focus_handle(cx),
353        }
354    }
355}
356
357impl Render for ContextPicker {
358    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
359        v_flex()
360            .w(px(400.))
361            .min_w(px(400.))
362            .map(|parent| match &self.mode {
363                ContextPickerMode::Default(menu) => parent.child(menu.clone()),
364                ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
365                ContextPickerMode::Directory(directory_picker) => {
366                    parent.child(directory_picker.clone())
367                }
368                ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
369                ContextPickerMode::Thread(thread_picker) => parent.child(thread_picker.clone()),
370            })
371    }
372}
373enum RecentEntry {
374    File {
375        project_path: ProjectPath,
376        path_prefix: Arc<str>,
377    },
378    Thread(ThreadContextEntry),
379}