assistant2: Sketch in directory context picker (#22148)

Marshall Bowers created

This PR sketches in the structure for the directory context picker.

Waiting on implementing the actual behavior until we fix the issues with
the file context picker.

Release Notes:

- N/A

Change summary

crates/assistant2/src/context.rs                                 |  11 
crates/assistant2/src/context_picker.rs                          |  23 
crates/assistant2/src/context_picker/directory_context_picker.rs | 117 ++
3 files changed, 151 insertions(+)

Detailed changes

crates/assistant2/src/context.rs πŸ”—

@@ -24,6 +24,7 @@ pub struct Context {
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub enum ContextKind {
     File,
+    Directory,
     FetchedUrl,
     Thread,
 }
@@ -33,6 +34,7 @@ pub fn attach_context_to_message(
     context: impl IntoIterator<Item = Context>,
 ) {
     let mut file_context = String::new();
+    let mut directory_context = String::new();
     let mut fetch_context = String::new();
     let mut thread_context = String::new();
 
@@ -42,6 +44,10 @@ pub fn attach_context_to_message(
                 file_context.push_str(&context.text);
                 file_context.push('\n');
             }
+            ContextKind::Directory => {
+                directory_context.push_str(&context.text);
+                directory_context.push('\n');
+            }
             ContextKind::FetchedUrl => {
                 fetch_context.push_str(&context.name);
                 fetch_context.push('\n');
@@ -63,6 +69,11 @@ pub fn attach_context_to_message(
         context_text.push_str(&file_context);
     }
 
+    if !directory_context.is_empty() {
+        context_text.push_str("The following directories are available:\n");
+        context_text.push_str(&directory_context);
+    }
+
     if !fetch_context.is_empty() {
         context_text.push_str("The following fetched results are available\n");
         context_text.push_str(&fetch_context);

crates/assistant2/src/context_picker.rs πŸ”—

@@ -1,3 +1,4 @@
+mod directory_context_picker;
 mod fetch_context_picker;
 mod file_context_picker;
 mod thread_context_picker;
@@ -14,6 +15,7 @@ use util::ResultExt;
 use workspace::Workspace;
 
 use crate::context::ContextKind;
+use crate::context_picker::directory_context_picker::DirectoryContextPicker;
 use crate::context_picker::fetch_context_picker::FetchContextPicker;
 use crate::context_picker::file_context_picker::FileContextPicker;
 use crate::context_picker::thread_context_picker::ThreadContextPicker;
@@ -24,6 +26,7 @@ use crate::thread_store::ThreadStore;
 enum ContextPickerMode {
     Default,
     File(View<FileContextPicker>),
+    Directory(View<DirectoryContextPicker>),
     Fetch(View<FetchContextPicker>),
     Thread(View<ThreadContextPicker>),
 }
@@ -46,6 +49,11 @@ impl ContextPicker {
                 kind: ContextKind::File,
                 icon: IconName::File,
             },
+            ContextPickerEntry {
+                name: "Folder".into(),
+                kind: ContextKind::Directory,
+                icon: IconName::Folder,
+            },
             ContextPickerEntry {
                 name: "Fetch".into(),
                 kind: ContextKind::FetchedUrl,
@@ -92,6 +100,7 @@ impl FocusableView for ContextPicker {
         match &self.mode {
             ContextPickerMode::Default => self.picker.focus_handle(cx),
             ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
+            ContextPickerMode::Directory(directory_picker) => directory_picker.focus_handle(cx),
             ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
             ContextPickerMode::Thread(thread_picker) => thread_picker.focus_handle(cx),
         }
@@ -106,6 +115,9 @@ impl Render for ContextPicker {
             .map(|parent| match &self.mode {
                 ContextPickerMode::Default => parent.child(self.picker.clone()),
                 ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
+                ContextPickerMode::Directory(directory_picker) => {
+                    parent.child(directory_picker.clone())
+                }
                 ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
                 ContextPickerMode::Thread(thread_picker) => parent.child(thread_picker.clone()),
             })
@@ -167,6 +179,16 @@ impl PickerDelegate for ContextPickerDelegate {
                                 )
                             }));
                         }
+                        ContextKind::Directory => {
+                            this.mode = ContextPickerMode::Directory(cx.new_view(|cx| {
+                                DirectoryContextPicker::new(
+                                    self.context_picker.clone(),
+                                    self.workspace.clone(),
+                                    self.context_store.clone(),
+                                    cx,
+                                )
+                            }));
+                        }
                         ContextKind::FetchedUrl => {
                             this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
                                 FetchContextPicker::new(
@@ -202,6 +224,7 @@ impl PickerDelegate for ContextPickerDelegate {
             .update(cx, |this, cx| match this.mode {
                 ContextPickerMode::Default => cx.emit(DismissEvent),
                 ContextPickerMode::File(_)
+                | ContextPickerMode::Directory(_)
                 | ContextPickerMode::Fetch(_)
                 | ContextPickerMode::Thread(_) => {}
             })

crates/assistant2/src/context_picker/directory_context_picker.rs πŸ”—

@@ -0,0 +1,117 @@
+// TODO: Remove this once we've implemented the functionality.
+#![allow(unused)]
+
+use std::sync::Arc;
+
+use fuzzy::PathMatch;
+use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
+use picker::{Picker, PickerDelegate};
+use project::{PathMatchCandidateSet, WorktreeId};
+use ui::{prelude::*, ListItem};
+use util::ResultExt as _;
+use workspace::Workspace;
+
+use crate::context_picker::ContextPicker;
+use crate::context_store::ContextStore;
+
+pub struct DirectoryContextPicker {
+    picker: View<Picker<DirectoryContextPickerDelegate>>,
+}
+
+impl DirectoryContextPicker {
+    pub fn new(
+        context_picker: WeakView<ContextPicker>,
+        workspace: WeakView<Workspace>,
+        context_store: WeakModel<ContextStore>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let delegate =
+            DirectoryContextPickerDelegate::new(context_picker, workspace, context_store);
+        let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
+
+        Self { picker }
+    }
+}
+
+impl FocusableView for DirectoryContextPicker {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.picker.focus_handle(cx)
+    }
+}
+
+impl Render for DirectoryContextPicker {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+        self.picker.clone()
+    }
+}
+
+pub struct DirectoryContextPickerDelegate {
+    context_picker: WeakView<ContextPicker>,
+    workspace: WeakView<Workspace>,
+    context_store: WeakModel<ContextStore>,
+    matches: Vec<PathMatch>,
+    selected_index: usize,
+}
+
+impl DirectoryContextPickerDelegate {
+    pub fn new(
+        context_picker: WeakView<ContextPicker>,
+        workspace: WeakView<Workspace>,
+        context_store: WeakModel<ContextStore>,
+    ) -> Self {
+        Self {
+            context_picker,
+            workspace,
+            context_store,
+            matches: Vec::new(),
+            selected_index: 0,
+        }
+    }
+}
+
+impl PickerDelegate for DirectoryContextPickerDelegate {
+    type ListItem = ListItem;
+
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
+
+    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
+        self.selected_index = ix;
+    }
+
+    fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
+        "Search folders…".into()
+    }
+
+    fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
+        // TODO: Implement this once we fix the issues with the file context picker.
+        Task::ready(())
+    }
+
+    fn confirm(&mut self, _secondary: bool, _cx: &mut ViewContext<Picker<Self>>) {
+        // TODO: Implement this once we fix the issues with the file context picker.
+    }
+
+    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+        self.context_picker
+            .update(cx, |this, cx| {
+                this.reset_mode();
+                cx.emit(DismissEvent);
+            })
+            .ok();
+    }
+
+    fn render_match(
+        &self,
+        _ix: usize,
+        _selected: bool,
+        _cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        None
+    }
+}