Start on `assistant::QuoteSelection`

Antonio Scandurra created

Change summary

assets/keymaps/default.json       |  3 
crates/ai/src/assistant.rs        | 85 ++++++++++++++++++++++++++++++++
crates/workspace/src/dock.rs      |  6 ++
crates/workspace/src/workspace.rs | 10 +++
4 files changed, 101 insertions(+), 3 deletions(-)

Detailed changes

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"
     }
   },
   {

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<Self>) -> AnyElement<Self> {
         ChildView::new(&self.pane, cx).into_any()
     }
+
+    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+        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(&params.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<Workspace>,
+    ) {
+        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
+            return;
+        };
+        let Some(editor) = workspace.active_item(cx).and_then(|item| item.downcast::<Editor>()) else {
+            return;
+        };
+
+        let text = editor.read_with(cx, |editor, cx| {
+            let range = editor.selections.newest::<usize>(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::<String>();
+            if selected_text.is_empty() {
+                None
+            } else {
+                Some(if language_name == "markdown" {
+                    selected_text
+                        .lines()
+                        .map(|line| format!("> {}", line))
+                        .collect::<Vec<_>>()
+                        .join("\n")
+                } else {
+                    format!("```{language_name}\n{selected_text}\n```")
+                })
+            }
+        });
+
+        // Activate the panel
+        if !panel.read(cx).has_focus(cx) {
+            workspace.toggle_panel_focus::<AssistantPanel>(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::<AssistantEditor>())
+                    .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<Self>) {
+        if cx.is_self_focused() {
+            cx.focus(&self.editor);
+        }
+    }
 }
 
 impl Item for AssistantEditor {

crates/workspace/src/dock.rs 🔗

@@ -184,6 +184,12 @@ impl Dock {
             .map_or(false, |panel| panel.has_focus(cx))
     }
 
+    pub fn panel<T: Panel>(&self) -> Option<ViewHandle<T>> {
+        self.panel_entries
+            .iter()
+            .find_map(|entry| entry.panel.as_any().clone().downcast())
+    }
+
     pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
         self.panel_entries
             .iter()

crates/workspace/src/workspace.rs 🔗

@@ -1678,6 +1678,16 @@ impl Workspace {
         }
     }
 
+    pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<ViewHandle<T>> {
+        for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
+            let dock = dock.read(cx);
+            if let Some(panel) = dock.panel::<T>() {
+                return Some(panel);
+            }
+        }
+        None
+    }
+
     fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
         for pane in &self.panes {
             pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));