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