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