Remove headers from prompt library picker (#12889)

Antonio Scandurra created

Also, as a drive-by, we're fixing up/down not working in inline
assistant editor.

Release Notes:

- N/A

Change summary

crates/assistant/src/inline_assistant.rs |  47 +++-
crates/assistant/src/prompt_library.rs   | 244 +++++++++----------------
crates/editor/src/editor.rs              |  17 +
crates/text/src/selection.rs             |   2 
4 files changed, 136 insertions(+), 174 deletions(-)

Detailed changes

crates/assistant/src/inline_assistant.rs 🔗

@@ -1026,8 +1026,16 @@ impl InlineAssistEditor {
     ) {
         match event {
             EditorEvent::Edited => {
+                let prompt = self.prompt_editor.read(cx).text(cx);
+                if self
+                    .prompt_history_ix
+                    .map_or(true, |ix| self.prompt_history[ix] != prompt)
+                {
+                    self.prompt_history_ix.take();
+                    self.pending_prompt = prompt;
+                }
+
                 self.edited_since_done = true;
-                self.pending_prompt = self.prompt_editor.read(cx).text(cx);
                 cx.notify();
             }
             EditorEvent::Blurred => {
@@ -1102,13 +1110,19 @@ impl InlineAssistEditor {
         if let Some(ix) = self.prompt_history_ix {
             if ix > 0 {
                 self.prompt_history_ix = Some(ix - 1);
-                let prompt = self.prompt_history[ix - 1].clone();
-                self.set_prompt(&prompt, cx);
+                let prompt = self.prompt_history[ix - 1].as_str();
+                self.prompt_editor.update(cx, |editor, cx| {
+                    editor.set_text(prompt, cx);
+                    editor.move_to_beginning(&Default::default(), cx);
+                });
             }
         } else if !self.prompt_history.is_empty() {
             self.prompt_history_ix = Some(self.prompt_history.len() - 1);
-            let prompt = self.prompt_history[self.prompt_history.len() - 1].clone();
-            self.set_prompt(&prompt, cx);
+            let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
+            self.prompt_editor.update(cx, |editor, cx| {
+                editor.set_text(prompt, cx);
+                editor.move_to_beginning(&Default::default(), cx);
+            });
         }
     }
 
@@ -1116,25 +1130,22 @@ impl InlineAssistEditor {
         if let Some(ix) = self.prompt_history_ix {
             if ix < self.prompt_history.len() - 1 {
                 self.prompt_history_ix = Some(ix + 1);
-                let prompt = self.prompt_history[ix + 1].clone();
-                self.set_prompt(&prompt, cx);
+                let prompt = self.prompt_history[ix + 1].as_str();
+                self.prompt_editor.update(cx, |editor, cx| {
+                    editor.set_text(prompt, cx);
+                    editor.move_to_end(&Default::default(), cx)
+                });
             } else {
                 self.prompt_history_ix = None;
-                let pending_prompt = self.pending_prompt.clone();
-                self.set_prompt(&pending_prompt, cx);
+                let prompt = self.pending_prompt.as_str();
+                self.prompt_editor.update(cx, |editor, cx| {
+                    editor.set_text(prompt, cx);
+                    editor.move_to_end(&Default::default(), cx)
+                });
             }
         }
     }
 
-    fn set_prompt(&mut self, prompt: &str, cx: &mut ViewContext<Self>) {
-        self.prompt_editor.update(cx, |editor, cx| {
-            editor.buffer().update(cx, |buffer, cx| {
-                let len = buffer.len(cx);
-                buffer.edit([(0..len, prompt)], None, cx);
-            });
-        });
-    }
-
     fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let settings = ThemeSettings::get_global(cx);
         let text_style = TextStyle {

crates/assistant/src/prompt_library.rs 🔗

@@ -13,10 +13,9 @@ use futures::{
 };
 use fuzzy::StringMatchCandidate;
 use gpui::{
-    actions, percentage, point, size, Animation, AnimationExt, AnyElement, AppContext,
-    BackgroundExecutor, Bounds, DevicePixels, EventEmitter, Global, PromptLevel, ReadGlobal,
-    Subscription, Task, TitlebarOptions, Transformation, UpdateGlobal, View, WindowBounds,
-    WindowHandle, WindowOptions,
+    actions, percentage, point, size, Animation, AnimationExt, AppContext, BackgroundExecutor,
+    Bounds, DevicePixels, EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task,
+    TitlebarOptions, Transformation, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
 };
 use heed::{types::SerdeBincode, Database, RoTxn};
 use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
@@ -26,6 +25,7 @@ use rope::Rope;
 use serde::{Deserialize, Serialize};
 use settings::Settings;
 use std::{
+    cmp::Reverse,
     future::Future,
     path::PathBuf,
     sync::{atomic::AtomicBool, Arc},
@@ -33,8 +33,8 @@ use std::{
 };
 use theme::ThemeSettings;
 use ui::{
-    div, prelude::*, IconButtonShape, ListHeader, ListItem, ListItemSpacing, ListSubHeader,
-    ParentElement, Render, SharedString, Styled, TitleBar, Tooltip, ViewContext, VisualContext,
+    div, prelude::*, IconButtonShape, ListItem, ListItemSpacing, ParentElement, Render,
+    SharedString, Styled, TitleBar, Tooltip, ViewContext, VisualContext,
 };
 use util::{paths::PROMPTS_DIR, ResultExt, TryFutureExt};
 use uuid::Uuid;
@@ -124,41 +124,23 @@ struct PromptEditor {
 struct PromptPickerDelegate {
     store: Arc<PromptStore>,
     selected_index: usize,
-    entries: Vec<PromptPickerEntry>,
+    matches: Vec<PromptMetadata>,
 }
 
 enum PromptPickerEvent {
-    Selected { prompt_id: Option<PromptId> },
+    Selected { prompt_id: PromptId },
     Confirmed { prompt_id: PromptId },
     Deleted { prompt_id: PromptId },
     ToggledDefault { prompt_id: PromptId },
 }
 
-#[derive(Debug)]
-enum PromptPickerEntry {
-    DefaultPromptsHeader,
-    DefaultPromptsEmpty,
-    AllPromptsHeader,
-    AllPromptsEmpty,
-    Prompt(PromptMetadata),
-}
-
-impl PromptPickerEntry {
-    fn prompt_id(&self) -> Option<PromptId> {
-        match self {
-            PromptPickerEntry::Prompt(metadata) => Some(metadata.id),
-            _ => None,
-        }
-    }
-}
-
 impl EventEmitter<PromptPickerEvent> for Picker<PromptPickerDelegate> {}
 
 impl PickerDelegate for PromptPickerDelegate {
-    type ListItem = AnyElement;
+    type ListItem = ListItem;
 
     fn match_count(&self) -> usize {
-        self.entries.len()
+        self.matches.len()
     }
 
     fn selected_index(&self) -> usize {
@@ -167,14 +149,11 @@ impl PickerDelegate for PromptPickerDelegate {
 
     fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
         self.selected_index = ix;
-        let prompt_id = if let Some(PromptPickerEntry::Prompt(prompt)) =
-            self.entries.get(self.selected_index)
-        {
-            Some(prompt.id)
-        } else {
-            None
-        };
-        cx.emit(PromptPickerEvent::Selected { prompt_id });
+        if let Some(prompt) = self.matches.get(self.selected_index) {
+            cx.emit(PromptPickerEvent::Selected {
+                prompt_id: prompt.id,
+            });
+        }
     }
 
     fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
@@ -183,48 +162,24 @@ impl PickerDelegate for PromptPickerDelegate {
 
     fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
         let search = self.store.search(query);
-        let prev_prompt_id = self
-            .entries
-            .get(self.selected_index)
-            .and_then(|mat| mat.prompt_id());
+        let prev_prompt_id = self.matches.get(self.selected_index).map(|mat| mat.id);
         cx.spawn(|this, mut cx| async move {
-            let (entries, selected_index) = cx
+            let (matches, selected_index) = cx
                 .background_executor()
                 .spawn(async move {
-                    let prompts = search.await;
-                    let (default_prompts, prompts) = prompts
-                        .into_iter()
-                        .partition::<Vec<_>, _>(|prompt| prompt.default);
-
-                    let mut entries = Vec::new();
-                    entries.push(PromptPickerEntry::DefaultPromptsHeader);
-                    if default_prompts.is_empty() {
-                        entries.push(PromptPickerEntry::DefaultPromptsEmpty);
-                    } else {
-                        entries.extend(default_prompts.into_iter().map(PromptPickerEntry::Prompt));
-                    }
-
-                    entries.push(PromptPickerEntry::AllPromptsHeader);
-                    if prompts.is_empty() {
-                        entries.push(PromptPickerEntry::AllPromptsEmpty);
-                    } else {
-                        entries.extend(prompts.into_iter().map(PromptPickerEntry::Prompt));
-                    }
+                    let matches = search.await;
 
                     let selected_index = prev_prompt_id
                         .and_then(|prev_prompt_id| {
-                            entries
-                                .iter()
-                                .position(|entry| entry.prompt_id() == Some(prev_prompt_id))
+                            matches.iter().position(|entry| entry.id == prev_prompt_id)
                         })
-                        .or_else(|| entries.iter().position(|entry| entry.prompt_id().is_some()))
                         .unwrap_or(0);
-                    (entries, selected_index)
+                    (matches, selected_index)
                 })
                 .await;
 
             this.update(&mut cx, |this, cx| {
-                this.delegate.entries = entries;
+                this.delegate.matches = matches;
                 this.delegate.set_selected_index(selected_index, cx);
                 cx.notify();
             })
@@ -233,7 +188,7 @@ impl PickerDelegate for PromptPickerDelegate {
     }
 
     fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
-        if let Some(PromptPickerEntry::Prompt(prompt)) = self.entries.get(self.selected_index) {
+        if let Some(prompt) = self.matches.get(self.selected_index) {
             cx.emit(PromptPickerEvent::Confirmed {
                 prompt_id: prompt.id,
             });
@@ -248,82 +203,59 @@ impl PickerDelegate for PromptPickerDelegate {
         selected: bool,
         cx: &mut ViewContext<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let prompt = self.entries.get(ix)?;
-        let element = match prompt {
-            PromptPickerEntry::DefaultPromptsHeader => ListHeader::new("Default Prompts")
-                .inset(true)
-                .start_slot(
-                    Icon::new(IconName::Sparkle)
-                        .color(Color::Muted)
-                        .size(IconSize::XSmall),
-                )
-                .selected(selected)
-                .into_any_element(),
-            PromptPickerEntry::DefaultPromptsEmpty => {
-                ListSubHeader::new("Star a prompt to add it to the default context")
-                    .inset(true)
-                    .selected(selected)
-                    .into_any_element()
-            }
-            PromptPickerEntry::AllPromptsHeader => ListHeader::new("All Prompts")
-                .inset(true)
-                .start_slot(
-                    Icon::new(IconName::Library)
-                        .color(Color::Muted)
-                        .size(IconSize::XSmall),
-                )
-                .selected(selected)
-                .into_any_element(),
-            PromptPickerEntry::AllPromptsEmpty => ListSubHeader::new("No prompts")
-                .inset(true)
-                .selected(selected)
-                .into_any_element(),
-            PromptPickerEntry::Prompt(prompt) => {
-                let default = prompt.default;
-                let prompt_id = prompt.id;
-                ListItem::new(ix)
-                    .inset(true)
-                    .spacing(ListItemSpacing::Sparse)
-                    .selected(selected)
-                    .child(h_flex().h_5().line_height(relative(1.)).child(Label::new(
-                        prompt.title.clone().unwrap_or("Untitled".into()),
-                    )))
-                    .end_hover_slot(
-                        h_flex()
-                            .gap_2()
-                            .child(
-                                IconButton::new("delete-prompt", IconName::Trash)
-                                    .icon_color(Color::Muted)
-                                    .shape(IconButtonShape::Square)
-                                    .tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
-                                    .on_click(cx.listener(move |_, _, cx| {
-                                        cx.emit(PromptPickerEvent::Deleted { prompt_id })
-                                    })),
-                            )
-                            .child(
-                                IconButton::new("toggle-default-prompt", IconName::Sparkle)
-                                    .selected(default)
-                                    .selected_icon(IconName::SparkleFilled)
-                                    .icon_color(if default { Color::Accent } else { Color::Muted })
-                                    .shape(IconButtonShape::Square)
-                                    .tooltip(move |cx| {
-                                        Tooltip::text(
-                                            if default {
-                                                "Remove from Default Prompt"
-                                            } else {
-                                                "Add to Default Prompt"
-                                            },
-                                            cx,
-                                        )
-                                    })
-                                    .on_click(cx.listener(move |_, _, cx| {
-                                        cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
-                                    })),
-                            ),
+        let prompt = self.matches.get(ix)?;
+        let default = prompt.default;
+        let prompt_id = prompt.id;
+        let element = ListItem::new(ix)
+            .inset(true)
+            .spacing(ListItemSpacing::Sparse)
+            .selected(selected)
+            .child(h_flex().h_5().line_height(relative(1.)).child(Label::new(
+                prompt.title.clone().unwrap_or("Untitled".into()),
+            )))
+            .end_slot::<IconButton>(default.then(|| {
+                IconButton::new("toggle-default-prompt", IconName::SparkleFilled)
+                    .selected(true)
+                    .icon_color(Color::Accent)
+                    .shape(IconButtonShape::Square)
+                    .tooltip(move |cx| Tooltip::text("Remove from Default Prompt", cx))
+                    .on_click(cx.listener(move |_, _, cx| {
+                        cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
+                    }))
+            }))
+            .end_hover_slot(
+                h_flex()
+                    .gap_2()
+                    .child(
+                        IconButton::new("delete-prompt", IconName::Trash)
+                            .icon_color(Color::Muted)
+                            .shape(IconButtonShape::Square)
+                            .tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
+                            .on_click(cx.listener(move |_, _, cx| {
+                                cx.emit(PromptPickerEvent::Deleted { prompt_id })
+                            })),
                     )
-                    .into_any_element()
-            }
-        };
+                    .child(
+                        IconButton::new("toggle-default-prompt", IconName::Sparkle)
+                            .selected(default)
+                            .selected_icon(IconName::SparkleFilled)
+                            .icon_color(if default { Color::Accent } else { Color::Muted })
+                            .shape(IconButtonShape::Square)
+                            .tooltip(move |cx| {
+                                Tooltip::text(
+                                    if default {
+                                        "Remove from Default Prompt"
+                                    } else {
+                                        "Add to Default Prompt"
+                                    },
+                                    cx,
+                                )
+                            })
+                            .on_click(cx.listener(move |_, _, cx| {
+                                cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
+                            })),
+                    ),
+            );
         Some(element)
     }
 
@@ -349,11 +281,13 @@ impl PromptLibrary {
         let delegate = PromptPickerDelegate {
             store: store.clone(),
             selected_index: 0,
-            entries: Vec::new(),
+            matches: Vec::new(),
         };
 
         let picker = cx.new_view(|cx| {
-            let picker = Picker::list(delegate, cx).modal(false).max_height(None);
+            let picker = Picker::uniform_list(delegate, cx)
+                .modal(false)
+                .max_height(None);
             picker.focus(cx);
             picker
         });
@@ -376,11 +310,7 @@ impl PromptLibrary {
     ) {
         match event {
             PromptPickerEvent::Selected { prompt_id } => {
-                if let Some(prompt_id) = *prompt_id {
-                    self.load_prompt(prompt_id, false, cx);
-                } else {
-                    self.focus_picker(&Default::default(), cx);
-                }
+                self.load_prompt(*prompt_id, false, cx);
             }
             PromptPickerEvent::Confirmed { prompt_id } => {
                 self.load_prompt(*prompt_id, true, cx);
@@ -567,21 +497,23 @@ impl PromptLibrary {
             if let Some(prompt_id) = prompt_id {
                 if picker
                     .delegate
-                    .entries
+                    .matches
                     .get(picker.delegate.selected_index())
                     .map_or(true, |old_selected_prompt| {
-                        old_selected_prompt.prompt_id() != Some(prompt_id)
+                        old_selected_prompt.id != prompt_id
                     })
                 {
                     if let Some(ix) = picker
                         .delegate
-                        .entries
+                        .matches
                         .iter()
-                        .position(|mat| mat.prompt_id() == Some(prompt_id))
+                        .position(|mat| mat.id == prompt_id)
                     {
                         picker.set_selected_index(ix, true, cx);
                     }
                 }
+            } else {
+                picker.focus(cx);
             }
         });
         cx.notify();
@@ -1105,7 +1037,7 @@ impl PromptStore {
         let cached_metadata = self.metadata_cache.read().metadata.clone();
         let executor = self.executor.clone();
         self.executor.spawn(async move {
-            if query.is_empty() {
+            let mut matches = if query.is_empty() {
                 cached_metadata
             } else {
                 let candidates = cached_metadata
@@ -1131,7 +1063,9 @@ impl PromptStore {
                     .into_iter()
                     .map(|mat| cached_metadata[mat.candidate_id].clone())
                     .collect()
-            }
+            };
+            matches.sort_by_key(|metadata| Reverse(metadata.default));
+            matches
         })
     }
 

crates/editor/src/editor.rs 🔗

@@ -6540,6 +6540,8 @@ impl Editor {
         }
 
         let text_layout_details = &self.text_layout_details(cx);
+        let selection_count = self.selections.count();
+        let first_selection = self.selections.first_anchor();
 
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             let line_mode = s.line_mode;
@@ -6556,7 +6558,12 @@ impl Editor {
                 );
                 selection.collapse_to(cursor, goal);
             });
-        })
+        });
+
+        if selection_count == 1 && first_selection.range() == self.selections.first_anchor().range()
+        {
+            cx.propagate();
+        }
     }
 
     pub fn move_up_by_lines(&mut self, action: &MoveUpByLines, cx: &mut ViewContext<Self>) {
@@ -6700,6 +6707,9 @@ impl Editor {
         }
 
         let text_layout_details = &self.text_layout_details(cx);
+        let selection_count = self.selections.count();
+        let first_selection = self.selections.first_anchor();
+
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             let line_mode = s.line_mode;
             s.move_with(|map, selection| {
@@ -6716,6 +6726,11 @@ impl Editor {
                 selection.collapse_to(cursor, goal);
             });
         });
+
+        if selection_count == 1 && first_selection.range() == self.selections.first_anchor().range()
+        {
+            cx.propagate();
+        }
     }
 
     pub fn move_page_down(&mut self, action: &MovePageDown, cx: &mut ViewContext<Self>) {

crates/text/src/selection.rs 🔗

@@ -84,7 +84,9 @@ impl<T: Copy + Ord> Selection<T> {
         }
         self.goal = new_goal;
     }
+}
 
+impl<T: Copy> Selection<T> {
     pub fn range(&self) -> Range<T> {
         self.start..self.end
     }