assistant2: Start on modal for managing profiles (#27546)

Marshall Bowers created

This PR starts work on a modal for managing profiles.

Release Notes:

- N/A

Change summary

crates/assistant2/src/assistant.rs                                     |   4 
crates/assistant2/src/assistant_configuration.rs                       |   3 
crates/assistant2/src/assistant_configuration/manage_profiles_modal.rs |  79 
crates/assistant2/src/assistant_configuration/profile_picker.rs        | 181 
4 files changed, 266 insertions(+), 1 deletion(-)

Detailed changes

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

@@ -32,7 +32,7 @@ use prompt_store::PromptBuilder;
 use settings::Settings as _;
 
 pub use crate::active_thread::ActiveThread;
-use crate::assistant_configuration::AddContextServerModal;
+use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal};
 pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
 pub use crate::inline_assistant::InlineAssistant;
 pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
@@ -47,6 +47,7 @@ actions!(
         RemoveAllContext,
         OpenHistory,
         OpenConfiguration,
+        ManageProfiles,
         AddContextServer,
         RemoveSelectedThread,
         Chat,
@@ -89,6 +90,7 @@ pub fn init(
         cx,
     );
     cx.observe_new(AddContextServerModal::register).detach();
+    cx.observe_new(ManageProfilesModal::register).detach();
 
     feature_gate_assistant2_actions(cx);
 }

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

@@ -1,4 +1,6 @@
 mod add_context_server_modal;
+mod manage_profiles_modal;
+mod profile_picker;
 
 use std::sync::Arc;
 
@@ -12,6 +14,7 @@ use util::ResultExt as _;
 use zed_actions::ExtensionCategoryFilter;
 
 pub(crate) use add_context_server_modal::AddContextServerModal;
+pub(crate) use manage_profiles_modal::ManageProfilesModal;
 
 use crate::AddContextServer;
 

crates/assistant2/src/assistant_configuration/manage_profiles_modal.rs πŸ”—

@@ -0,0 +1,79 @@
+use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity};
+use ui::prelude::*;
+use workspace::{ModalView, Workspace};
+
+use crate::assistant_configuration::profile_picker::{ProfilePicker, ProfilePickerDelegate};
+use crate::ManageProfiles;
+
+enum Mode {
+    ChooseProfile(Entity<ProfilePicker>),
+}
+
+pub struct ManageProfilesModal {
+    #[allow(dead_code)]
+    workspace: WeakEntity<Workspace>,
+    mode: Mode,
+}
+
+impl ManageProfilesModal {
+    pub fn register(
+        workspace: &mut Workspace,
+        _window: Option<&mut Window>,
+        _cx: &mut Context<Workspace>,
+    ) {
+        workspace.register_action(|workspace, _: &ManageProfiles, window, cx| {
+            let workspace_handle = cx.entity().downgrade();
+            workspace.toggle_modal(window, cx, |window, cx| {
+                Self::new(workspace_handle, window, cx)
+            })
+        });
+    }
+
+    pub fn new(
+        workspace: WeakEntity<Workspace>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        Self {
+            workspace,
+            mode: Mode::ChooseProfile(cx.new(|cx| {
+                let delegate = ProfilePickerDelegate::new(cx);
+                ProfilePicker::new(delegate, window, cx)
+            })),
+        }
+    }
+
+    fn confirm(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
+
+    fn cancel(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
+}
+
+impl ModalView for ManageProfilesModal {}
+
+impl Focusable for ManageProfilesModal {
+    fn focus_handle(&self, cx: &App) -> FocusHandle {
+        match &self.mode {
+            Mode::ChooseProfile(profile_picker) => profile_picker.read(cx).focus_handle(cx),
+        }
+    }
+}
+
+impl EventEmitter<DismissEvent> for ManageProfilesModal {}
+
+impl Render for ManageProfilesModal {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        div()
+            .elevation_3(cx)
+            .w(rems(34.))
+            .key_context("ManageProfilesModal")
+            .on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx)))
+            .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx)))
+            .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
+                this.focus_handle(cx).focus(window);
+            }))
+            .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
+            .child(match &self.mode {
+                Mode::ChooseProfile(profile_picker) => profile_picker.clone().into_any_element(),
+            })
+    }
+}

crates/assistant2/src/assistant_configuration/profile_picker.rs πŸ”—

@@ -0,0 +1,181 @@
+use std::sync::Arc;
+
+use assistant_settings::AssistantSettings;
+use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
+use gpui::{
+    App, Context, DismissEvent, Entity, EventEmitter, Focusable, SharedString, Task, WeakEntity,
+    Window,
+};
+use picker::{Picker, PickerDelegate};
+use settings::Settings;
+use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
+use util::ResultExt as _;
+
+pub struct ProfilePicker {
+    picker: Entity<Picker<ProfilePickerDelegate>>,
+}
+
+impl ProfilePicker {
+    pub fn new(
+        delegate: ProfilePickerDelegate,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
+        Self { picker }
+    }
+}
+
+impl EventEmitter<DismissEvent> for ProfilePicker {}
+
+impl Focusable for ProfilePicker {
+    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
+        self.picker.focus_handle(cx)
+    }
+}
+
+impl Render for ProfilePicker {
+    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+        v_flex().w(rems(34.)).child(self.picker.clone())
+    }
+}
+
+#[derive(Debug)]
+pub struct ProfileEntry {
+    #[allow(dead_code)]
+    pub id: Arc<str>,
+    pub name: SharedString,
+}
+
+pub struct ProfilePickerDelegate {
+    profile_picker: WeakEntity<ProfilePicker>,
+    profiles: Vec<ProfileEntry>,
+    matches: Vec<StringMatch>,
+    selected_index: usize,
+}
+
+impl ProfilePickerDelegate {
+    pub fn new(cx: &mut Context<ProfilePicker>) -> Self {
+        let settings = AssistantSettings::get_global(cx);
+
+        let profiles = settings
+            .profiles
+            .iter()
+            .map(|(id, profile)| ProfileEntry {
+                id: id.clone(),
+                name: profile.name.clone(),
+            })
+            .collect::<Vec<_>>();
+
+        Self {
+            profile_picker: cx.entity().downgrade(),
+            profiles,
+            matches: Vec::new(),
+            selected_index: 0,
+        }
+    }
+}
+
+impl PickerDelegate for ProfilePickerDelegate {
+    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,
+        _window: &mut Window,
+        _cx: &mut Context<Picker<Self>>,
+    ) {
+        self.selected_index = ix;
+    }
+
+    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
+        "Search profiles…".into()
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        window: &mut Window,
+        cx: &mut Context<Picker<Self>>,
+    ) -> Task<()> {
+        let background = cx.background_executor().clone();
+        let candidates = self
+            .profiles
+            .iter()
+            .enumerate()
+            .map(|(id, profile)| StringMatchCandidate::new(id, profile.name.as_ref()))
+            .collect::<Vec<_>>();
+
+        cx.spawn_in(window, async move |this, cx| {
+            let matches = if query.is_empty() {
+                candidates
+                    .into_iter()
+                    .enumerate()
+                    .map(|(index, candidate)| StringMatch {
+                        candidate_id: index,
+                        string: candidate.string,
+                        positions: Vec::new(),
+                        score: 0.,
+                    })
+                    .collect()
+            } else {
+                match_strings(
+                    &candidates,
+                    &query,
+                    false,
+                    100,
+                    &Default::default(),
+                    background,
+                )
+                .await
+            };
+
+            this.update(cx, |this, _cx| {
+                this.delegate.matches = matches;
+                this.delegate.selected_index = this
+                    .delegate
+                    .selected_index
+                    .min(this.delegate.matches.len().saturating_sub(1));
+            })
+            .log_err();
+        })
+    }
+
+    fn confirm(&mut self, _secondary: bool, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {
+    }
+
+    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
+        self.profile_picker
+            .update(cx, |_this, cx| cx.emit(DismissEvent))
+            .log_err();
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        _window: &mut Window,
+        _cx: &mut Context<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        let profile_match = &self.matches[ix];
+
+        Some(
+            ListItem::new(ix)
+                .inset(true)
+                .spacing(ListItemSpacing::Sparse)
+                .toggle_state(selected)
+                .child(HighlightedLabel::new(
+                    profile_match.string.clone(),
+                    profile_match.positions.clone(),
+                )),
+        )
+    }
+}