diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index c45de4a1fd..0c050bfe53 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/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.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) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); let text_style = TextStyle { diff --git a/crates/assistant/src/prompt_library.rs b/crates/assistant/src/prompt_library.rs index e8322a9285..d8dc610b83 100644 --- a/crates/assistant/src/prompt_library.rs +++ b/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, selected_index: usize, - entries: Vec, + matches: Vec, } enum PromptPickerEvent { - Selected { prompt_id: Option }, + 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 { - match self { - PromptPickerEntry::Prompt(metadata) => Some(metadata.id), - _ => None, - } - } -} - impl EventEmitter for Picker {} 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>) { 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 { @@ -183,48 +162,24 @@ impl PickerDelegate for PromptPickerDelegate { fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> 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::, _>(|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>) { - 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>, ) -> Option { - 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::(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 }) } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0500e6643e..9b129ad9f5 100644 --- a/crates/editor/src/editor.rs +++ b/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) { @@ -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) { diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index 480cb99d74..94c373d630 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -84,7 +84,9 @@ impl Selection { } self.goal = new_goal; } +} +impl Selection { pub fn range(&self) -> Range { self.start..self.end }