diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 5e70f84d15..ad28b725e6 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -18,9 +18,9 @@ use anyhow::{anyhow, Result}; use assistant_slash_command::{SlashCommandOutput, SlashCommandOutputSection}; use client::telemetry::Telemetry; use collections::{hash_map, BTreeSet, HashMap, HashSet, VecDeque}; -use editor::actions::UnfoldAt; +use editor::actions::ShowCompletions; use editor::{ - actions::{FoldAt, MoveDown, MoveUp}, + actions::{FoldAt, MoveDown, MoveToEndOfLine, MoveUp, Newline, UnfoldAt}, display_map::{ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, Flap, ToDisplayPoint, }, @@ -212,15 +212,20 @@ impl AssistantPanel { let slash_command_registry = SlashCommandRegistry::global(cx); - slash_command_registry.register_command(file_command::FileSlashCommand); + slash_command_registry.register_command(file_command::FileSlashCommand, true); slash_command_registry.register_command( prompt_command::PromptSlashCommand::new(prompt_library.clone()), + true, ); - slash_command_registry.register_command(active_command::ActiveSlashCommand); - slash_command_registry.register_command(tabs_command::TabsSlashCommand); - slash_command_registry.register_command(project_command::ProjectSlashCommand); - slash_command_registry.register_command(search_command::SearchSlashCommand); - slash_command_registry.register_command(rustdoc_command::RustdocSlashCommand); + slash_command_registry + .register_command(active_command::ActiveSlashCommand, true); + slash_command_registry.register_command(tabs_command::TabsSlashCommand, true); + slash_command_registry + .register_command(project_command::ProjectSlashCommand, true); + slash_command_registry + .register_command(search_command::SearchSlashCommand, true); + slash_command_registry + .register_command(rustdoc_command::RustdocSlashCommand, false); Self { workspace: workspace_handle, @@ -943,6 +948,14 @@ impl AssistantPanel { self.model_menu_handle.toggle(cx); } + fn insert_command(&mut self, name: &str, cx: &mut ViewContext) { + if let Some(conversation_editor) = self.active_conversation_editor() { + conversation_editor.update(cx, |conversation_editor, cx| { + conversation_editor.insert_command(name, cx) + }); + } + } + fn active_conversation_editor(&self) -> Option<&View> { Some(&self.active_conversation_editor.as_ref()?.editor) } @@ -980,52 +993,65 @@ impl AssistantPanel { }) } - fn render_inject_context_menu(&self, _cx: &mut ViewContext) -> impl Element { - let workspace = self.workspace.clone(); + fn render_inject_context_menu(&self, cx: &mut ViewContext) -> impl Element { + let commands = self.slash_commands.clone(); + let assistant_panel = cx.view().downgrade(); + let active_editor_focus_handle = self.workspace.upgrade().and_then(|workspace| { + Some( + workspace + .read(cx) + .active_item_as::(cx)? + .focus_handle(cx), + ) + }); popover_menu("inject-context-menu") .trigger(IconButton::new("trigger", IconName::Quote).tooltip(|cx| { - // Tooltip::with_meta("Insert Context", None, "Type # to insert via keyboard", cx) - Tooltip::text("Insert Context", cx) + Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx) })) .menu(move |cx| { - ContextMenu::build(cx, |menu, _cx| { - // menu.entry("Insert Search", None, { - // let assistant = assistant.clone(); - // move |_cx| {} - // }) - // .entry("Insert Docs", None, { - // let assistant = assistant.clone(); - // move |cx| {} - // }) - menu.entry("Quote Selection", None, { - let workspace = workspace.clone(); - move |cx| { - workspace - .update(cx, |workspace, cx| { - ConversationEditor::quote_selection( - workspace, - &Default::default(), - cx, - ) - }) - .ok(); + ContextMenu::build(cx, |mut menu, _cx| { + for command_name in commands.featured_command_names() { + if let Some(command) = commands.command(&command_name) { + let menu_text = SharedString::from(Arc::from(command.menu_text())); + menu = menu.custom_entry( + { + let command_name = command_name.clone(); + move |_cx| { + h_flex() + .w_full() + .justify_between() + .child(Label::new(menu_text.clone())) + .child( + div().ml_4().child( + Label::new(format!("/{command_name}")) + .color(Color::Muted), + ), + ) + .into_any() + } + }, + { + let assistant_panel = assistant_panel.clone(); + move |cx| { + assistant_panel + .update(cx, |assistant_panel, cx| { + assistant_panel.insert_command(&command_name, cx) + }) + .ok(); + } + }, + ) } - }) - // .entry("Insert Active Prompt", None, { - // let workspace = workspace.clone(); - // move |cx| { - // workspace - // .update(cx, |workspace, cx| { - // ConversationEditor::insert_active_prompt( - // workspace, - // &Default::default(), - // cx, - // ) - // }) - // .ok(); - // } - // }) + } + + if let Some(active_editor_focus_handle) = active_editor_focus_handle.clone() { + menu = menu + .context(active_editor_focus_handle) + .action("Quote Selection", Box::new(QuoteSelection)); + } + + menu }) .into() }) @@ -1720,7 +1746,6 @@ impl Conversation { let pending_command = PendingSlashCommand { name: name.to_string(), argument: argument.map(ToString::to_string), - tooltip_text: command.tooltip_text().into(), source_range, status: PendingSlashCommandStatus::Idle, }; @@ -2517,7 +2542,6 @@ struct PendingSlashCommand { argument: Option, status: PendingSlashCommandStatus, source_range: Range, - tooltip_text: SharedString, } #[derive(Clone)] @@ -2690,11 +2714,47 @@ impl ConversationEditor { .collect() } + fn insert_command(&mut self, name: &str, cx: &mut ViewContext) { + if let Some(command) = self.slash_command_registry.command(name) { + self.editor.update(cx, |editor, cx| { + editor.transact(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel()); + let snapshot = editor.buffer().read(cx).snapshot(cx); + let newest_cursor = editor.selections.newest::(cx).head(); + if newest_cursor.column > 0 + || snapshot + .chars_at(newest_cursor) + .next() + .map_or(false, |ch| ch != '\n') + { + editor.move_to_end_of_line( + &MoveToEndOfLine { + stop_at_soft_wraps: false, + }, + cx, + ); + editor.newline(&Newline, cx); + } + + editor.insert(&format!("/{name}"), cx); + if command.requires_argument() { + editor.insert(" ", cx); + editor.show_completions(&ShowCompletions, cx); + } + }); + }); + if !command.requires_argument() { + self.confirm_command(&ConfirmCommand, cx); + } + } + } + pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext) { let selections = self.editor.read(cx).selections.disjoint_anchors(); let mut commands_by_range = HashMap::default(); let workspace = self.workspace.clone(); self.conversation.update(cx, |conversation, cx| { + conversation.reparse_slash_commands(cx); for selection in selections.iter() { if let Some(command) = conversation.pending_command_for_position(selection.head().text_anchor, cx) @@ -2851,9 +2911,8 @@ impl ConversationEditor { let confirm_command = confirm_command.clone(); let command = command.clone(); move |row, _, _, _cx: &mut WindowContext| { - render_pending_slash_command_toggle( + render_pending_slash_command_gutter_decoration( row, - command.tooltip_text.clone(), command.status.clone(), confirm_command.clone(), ) @@ -3680,14 +3739,13 @@ fn render_slash_command_output_toggle( .into_any_element() } -fn render_pending_slash_command_toggle( +fn render_pending_slash_command_gutter_decoration( row: MultiBufferRow, - tooltip_text: SharedString, status: PendingSlashCommandStatus, confirm_command: Arc, ) -> AnyElement { let mut icon = IconButton::new( - ("slash-command-output-fold-indicator", row.0), + ("slash-command-gutter-decoration", row.0), ui::IconName::TriangleRight, ) .on_click(move |_e, cx| confirm_command(cx)) @@ -3696,14 +3754,10 @@ fn render_pending_slash_command_toggle( match status { PendingSlashCommandStatus::Idle => { - icon = icon - .icon_color(Color::Muted) - .tooltip(move |cx| Tooltip::text(tooltip_text.clone(), cx)); + icon = icon.icon_color(Color::Muted); } PendingSlashCommandStatus::Running { .. } => { - icon = icon - .selected(true) - .tooltip(move |cx| Tooltip::text(tooltip_text.clone(), cx)); + icon = icon.selected(true); } PendingSlashCommandStatus::Error(error) => { icon = icon @@ -4126,10 +4180,11 @@ mod tests { let prompt_library = Arc::new(PromptLibrary::default()); let slash_command_registry = SlashCommandRegistry::new(); - slash_command_registry.register_command(file_command::FileSlashCommand); - slash_command_registry.register_command(prompt_command::PromptSlashCommand::new( - prompt_library.clone(), - )); + slash_command_registry.register_command(file_command::FileSlashCommand, false); + slash_command_registry.register_command( + prompt_command::PromptSlashCommand::new(prompt_library.clone()), + false, + ); let registry = Arc::new(LanguageRegistry::test(cx.executor())); let conversation = cx diff --git a/crates/assistant/src/slash_command/active_command.rs b/crates/assistant/src/slash_command/active_command.rs index 779b39b60f..8ae02d1e6c 100644 --- a/crates/assistant/src/slash_command/active_command.rs +++ b/crates/assistant/src/slash_command/active_command.rs @@ -19,8 +19,8 @@ impl SlashCommand for ActiveSlashCommand { "insert active tab".into() } - fn tooltip_text(&self) -> String { - "insert active tab".into() + fn menu_text(&self) -> String { + "Insert Active Tab".into() } fn complete_argument( diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index 48d0ff71ee..da4c7f2fdd 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/crates/assistant/src/slash_command/file_command.rs @@ -86,11 +86,11 @@ impl SlashCommand for FileSlashCommand { } fn description(&self) -> String { - "insert a file".into() + "insert file".into() } - fn tooltip_text(&self) -> String { - "insert file".into() + fn menu_text(&self) -> String { + "Insert File".into() } fn requires_argument(&self) -> bool { diff --git a/crates/assistant/src/slash_command/project_command.rs b/crates/assistant/src/slash_command/project_command.rs index a9a29c1227..f3c1d18b3a 100644 --- a/crates/assistant/src/slash_command/project_command.rs +++ b/crates/assistant/src/slash_command/project_command.rs @@ -94,11 +94,11 @@ impl SlashCommand for ProjectSlashCommand { } fn description(&self) -> String { - "insert current project context".into() + "insert project metadata".into() } - fn tooltip_text(&self) -> String { - "insert current project context".into() + fn menu_text(&self) -> String { + "Insert Project Metadata".into() } fn complete_argument( diff --git a/crates/assistant/src/slash_command/prompt_command.rs b/crates/assistant/src/slash_command/prompt_command.rs index 559fd796d6..6ff3410208 100644 --- a/crates/assistant/src/slash_command/prompt_command.rs +++ b/crates/assistant/src/slash_command/prompt_command.rs @@ -25,11 +25,11 @@ impl SlashCommand for PromptSlashCommand { } fn description(&self) -> String { - "insert a prompt from the library".into() + "insert prompt from library".into() } - fn tooltip_text(&self) -> String { - "insert prompt".into() + fn menu_text(&self) -> String { + "Insert Prompt from Library".into() } fn requires_argument(&self) -> bool { diff --git a/crates/assistant/src/slash_command/rustdoc_command.rs b/crates/assistant/src/slash_command/rustdoc_command.rs index c8e03bcbe4..418caeb758 100644 --- a/crates/assistant/src/slash_command/rustdoc_command.rs +++ b/crates/assistant/src/slash_command/rustdoc_command.rs @@ -51,11 +51,11 @@ impl SlashCommand for RustdocSlashCommand { } fn description(&self) -> String { - "insert the docs for a Rust crate".into() + "insert Rust docs".into() } - fn tooltip_text(&self) -> String { - "insert rustdoc".into() + fn menu_text(&self) -> String { + "Insert Rust Documentation".into() } fn requires_argument(&self) -> bool { diff --git a/crates/assistant/src/slash_command/search_command.rs b/crates/assistant/src/slash_command/search_command.rs index 6e2a9f5116..fee15bc1c0 100644 --- a/crates/assistant/src/slash_command/search_command.rs +++ b/crates/assistant/src/slash_command/search_command.rs @@ -32,11 +32,11 @@ impl SlashCommand for SearchSlashCommand { } fn description(&self) -> String { - "semantically search files".into() + "semantic search".into() } - fn tooltip_text(&self) -> String { - "search".into() + fn menu_text(&self) -> String { + "Semantic Search".into() } fn requires_argument(&self) -> bool { diff --git a/crates/assistant/src/slash_command/tabs_command.rs b/crates/assistant/src/slash_command/tabs_command.rs index 8c480761c2..5c6dc0bb2f 100644 --- a/crates/assistant/src/slash_command/tabs_command.rs +++ b/crates/assistant/src/slash_command/tabs_command.rs @@ -17,11 +17,11 @@ impl SlashCommand for TabsSlashCommand { } fn description(&self) -> String { - "insert content from open tabs".into() + "insert open tabs".into() } - fn tooltip_text(&self) -> String { - "insert open tabs".into() + fn menu_text(&self) -> String { + "Insert Open Tabs".into() } fn requires_argument(&self) -> bool { diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 5765c9eb37..554595ca31 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -20,7 +20,7 @@ pub trait SlashCommand: 'static + Send + Sync { CodeLabel::plain(self.name(), None) } fn description(&self) -> String; - fn tooltip_text(&self) -> String; + fn menu_text(&self) -> String; fn complete_argument( &self, query: String, diff --git a/crates/assistant_slash_command/src/slash_command_registry.rs b/crates/assistant_slash_command/src/slash_command_registry.rs index 68619dc1e1..070e60bc6b 100644 --- a/crates/assistant_slash_command/src/slash_command_registry.rs +++ b/crates/assistant_slash_command/src/slash_command_registry.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use collections::HashMap; +use collections::{BTreeSet, HashMap}; use derive_more::{Deref, DerefMut}; use gpui::Global; use gpui::{AppContext, ReadGlobal}; @@ -16,6 +16,7 @@ impl Global for GlobalSlashCommandRegistry {} #[derive(Default)] struct SlashCommandRegistryState { commands: HashMap, Arc>, + featured_commands: BTreeSet>, } #[derive(Default)] @@ -40,16 +41,19 @@ impl SlashCommandRegistry { Arc::new(Self { state: RwLock::new(SlashCommandRegistryState { commands: HashMap::default(), + featured_commands: BTreeSet::default(), }), }) } /// Registers the provided [`SlashCommand`]. - pub fn register_command(&self, command: impl SlashCommand) { - self.state - .write() - .commands - .insert(command.name().into(), Arc::new(command)); + pub fn register_command(&self, command: impl SlashCommand, is_featured: bool) { + let mut state = self.state.write(); + let command_name: Arc = command.name().into(); + if is_featured { + state.featured_commands.insert(command_name.clone()); + } + state.commands.insert(command_name, Arc::new(command)); } /// Returns the names of registered [`SlashCommand`]s. @@ -57,6 +61,16 @@ impl SlashCommandRegistry { self.state.read().commands.keys().cloned().collect() } + /// Returns the names of registered, featured [`SlashCommand`]s. + pub fn featured_command_names(&self) -> Vec> { + self.state + .read() + .featured_commands + .iter() + .cloned() + .collect() + } + /// Returns the [`SlashCommand`] with the given name. pub fn command(&self, name: &str) -> Option> { self.state.read().commands.get(name).cloned() diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 98c33473e1..f2835d4154 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -41,7 +41,7 @@ pub struct MovePageDown { #[derive(PartialEq, Clone, Deserialize, Default)] pub struct MoveToEndOfLine { #[serde(default = "default_true")] - pub(super) stop_at_soft_wraps: bool, + pub stop_at_soft_wraps: bool, } #[derive(PartialEq, Clone, Deserialize, Default)] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bca2a060b6..ac2f831ad4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3761,7 +3761,7 @@ impl Editor { })) } - fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext) { + pub fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext) { if self.pending_rename.is_some() { return; } diff --git a/crates/extension/src/extension_slash_command.rs b/crates/extension/src/extension_slash_command.rs index ffc81903ae..27805c5d21 100644 --- a/crates/extension/src/extension_slash_command.rs +++ b/crates/extension/src/extension_slash_command.rs @@ -27,7 +27,7 @@ impl SlashCommand for ExtensionSlashCommand { self.command.description.clone() } - fn tooltip_text(&self) -> String { + fn menu_text(&self) -> String { self.command.tooltip_text.clone() } diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs index e40dbb6722..b33e0addaa 100644 --- a/crates/extension/src/extension_store.rs +++ b/crates/extension/src/extension_store.rs @@ -1178,8 +1178,8 @@ impl ExtensionStore { } for (slash_command_name, slash_command) in &manifest.slash_commands { - this.slash_command_registry - .register_command(ExtensionSlashCommand { + this.slash_command_registry.register_command( + ExtensionSlashCommand { command: crate::wit::SlashCommand { name: slash_command_name.to_string(), description: slash_command.description.to_string(), @@ -1188,7 +1188,9 @@ impl ExtensionStore { }, extension: wasm_extension.clone(), host: this.wasm_host.clone(), - }); + }, + false, + ); } } this.wasm_extensions.extend(wasm_extensions); diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index bd1fefc0c3..aa2670a688 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -357,7 +357,7 @@ impl Render for ContextMenu { .unwrap_or_else(|| { KeyBinding::for_action(&**action, cx) }) - .map(|binding| div().ml_1().child(binding)) + .map(|binding| div().ml_4().child(binding)) })), ) .on_click(move |_, cx| { diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 9cec954eda..8c8156337e 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -77,13 +77,14 @@ impl RenderOnce for KeyBinding { .join(" ") ) }) + .gap(rems(0.125)) .flex_none() .children(self.key_binding.keystrokes().iter().map(|keystroke| { let key_icon = Self::icon_for_key(keystroke); h_flex() .flex_none() - .p_0p5() + .py_0p5() .rounded_sm() .text_color(cx.theme().colors().text_muted) .when(keystroke.modifiers.function, |el| {