diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 0b89969c70806752dcfb1b15ba48f6f4fe91868b..c6964b49e27dbff4d369656d88cd0a000abc5c13 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -199,7 +199,8 @@ "context": "ContextEditor > Editor", "bindings": { "cmd-enter": "assistant::Assist", - "escape": "assistant::CancelLastAssist" + "escape": "assistant::CancelLastAssist", + "cmd-?": "assistant::QuoteSelection" } }, { diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 23baaef04ac8470e0011e2ab7c47ffc0ab86484a..6e76e46c6102cb22de172e0a132d58018883e39c 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -19,11 +19,15 @@ use workspace::{ pane, Pane, Workspace, }; -actions!(assistant, [NewContext, Assist, CancelLastAssist]); +actions!( + assistant, + [NewContext, Assist, CancelLastAssist, QuoteSelection] +); pub fn init(cx: &mut AppContext) { cx.add_action(AssistantEditor::assist); cx.capture_action(AssistantEditor::cancel_last_assist); + cx.add_action(AssistantEditor::quote_selection); } pub enum AssistantPanelEvent { @@ -136,6 +140,12 @@ impl View for AssistantPanel { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { ChildView::new(&self.pane, cx).into_any() } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + cx.focus(&self.pane); + } + } } impl Panel for AssistantPanel { @@ -361,7 +371,7 @@ impl AssistantEditor { editor.set_render_excerpt_header( { let assistant = assistant.clone(); - move |editor, params: editor::RenderExcerptHeaderParams, cx| { + move |_editor, params: editor::RenderExcerptHeaderParams, cx| { let style = &theme::current(cx).assistant; if let Some(message) = assistant.read(cx).messages_by_id.get(¶ms.id) { let sender = match message.role { @@ -421,6 +431,71 @@ impl AssistantEditor { cx.propagate_action(); } } + + fn quote_selection( + workspace: &mut Workspace, + _: &QuoteSelection, + cx: &mut ViewContext, + ) { + let Some(panel) = workspace.panel::(cx) else { + return; + }; + let Some(editor) = workspace.active_item(cx).and_then(|item| item.downcast::()) else { + return; + }; + + let text = editor.read_with(cx, |editor, cx| { + let range = editor.selections.newest::(cx).range(); + let buffer = editor.buffer().read(cx).snapshot(cx); + let start_language = buffer.language_at(range.start); + let end_language = buffer.language_at(range.end); + let language_name = if start_language == end_language { + start_language.map(|language| language.name()) + } else { + None + }; + let language_name = language_name.as_deref().unwrap_or("").to_lowercase(); + + let selected_text = buffer.text_for_range(range).collect::(); + if selected_text.is_empty() { + None + } else { + Some(if language_name == "markdown" { + selected_text + .lines() + .map(|line| format!("> {}", line)) + .collect::>() + .join("\n") + } else { + format!("```{language_name}\n{selected_text}\n```") + }) + } + }); + + // Activate the panel + if !panel.read(cx).has_focus(cx) { + workspace.toggle_panel_focus::(cx); + } + + if let Some(text) = text { + panel.update(cx, |panel, cx| { + if let Some(assistant) = panel + .pane + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + .ok_or_else(|| anyhow!("no active context")) + .log_err() + { + assistant.update(cx, |assistant, cx| { + assistant + .editor + .update(cx, |editor, cx| editor.insert(&text, cx)) + }); + } + }); + } + } } impl Entity for AssistantEditor { @@ -440,6 +515,12 @@ impl View for AssistantEditor { .with_style(theme.container) .into_any() } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + cx.focus(&self.editor); + } + } } impl Item for AssistantEditor { diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 73d4f79399bd73bd52c9b0ecce04076e76b0407e..49651486db3b12526d9e40cc92d01a1a13c3ca19 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -184,6 +184,12 @@ impl Dock { .map_or(false, |panel| panel.has_focus(cx)) } + pub fn panel(&self) -> Option> { + self.panel_entries + .iter() + .find_map(|entry| entry.panel.as_any().clone().downcast()) + } + pub fn panel_index_for_type(&self) -> Option { self.panel_entries .iter() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8afad46ea108c0ab5ef1e6eac4ccf19e1f4661f1..2aa937c9a932ddbc2e2cb9364607693cb2ab5621 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1678,6 +1678,16 @@ impl Workspace { } } + pub fn panel(&self, cx: &WindowContext) -> Option> { + for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { + let dock = dock.read(cx); + if let Some(panel) = dock.panel::() { + return Some(panel); + } + } + None + } + fn zoom_out(&mut self, cx: &mut ViewContext) { for pane in &self.panes { pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));