diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index fc70ed68217f3520d0b8ac6aeb3affc82664d818..c0412152d70790ad89bcf1374cb315affb8fce36 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -134,7 +134,7 @@ "ctrl-k z": "editor::ToggleSoftWrap", "ctrl-f": "buffer_search::Deploy", "ctrl-h": "buffer_search::DeployReplace", - "ctrl-shift-.": "assistant::QuoteSelection", + "ctrl-shift-.": "agent::QuoteSelection", "ctrl-shift-,": "assistant::InsertIntoEditor", "shift-alt-e": "editor::SelectEnclosingSymbol", "ctrl-shift-backspace": "editor::GoToPreviousChange", @@ -244,7 +244,7 @@ "ctrl-shift-i": "agent::ToggleOptionsMenu", // "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu", "shift-alt-escape": "agent::ExpandMessageEditor", - "ctrl-shift-.": "assistant::QuoteSelection", + "ctrl-shift-.": "agent::QuoteSelection", "shift-alt-e": "agent::RemoveAllContext", "ctrl-shift-e": "project_panel::ToggleFocus", "ctrl-shift-enter": "agent::ContinueThread", diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index b7521517575a9831635f5e08a954c0e2bd493a80..626c9f3bb7bc3d9c146f584bb9acbc999c25c879 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1030,6 +1030,7 @@ impl MessageEditor { ) else { return; }; + self.editor.update(cx, |message_editor, cx| { message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx); }); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 07a2366f989bb1cf454b4dc2ab3c99b7da0ad528..8b8f127764ad36a9fb9afa029b22b2a113991d34 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -4972,10 +4972,12 @@ impl AcpThreadView { }) } + /// Inserts the selected text into the message editor or the message being + /// edited, if any. pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context) { - self.message_editor.update(cx, |message_editor, cx| { - message_editor.insert_selections(window, cx); - }) + self.active_editor(cx).update(cx, |editor, cx| { + editor.insert_selections(window, cx); + }); } fn render_thread_retry_status_callout( @@ -5386,6 +5388,23 @@ impl AcpThreadView { }; task.detach_and_log_err(cx); } + + /// Returns the currently active editor, either for a message that is being + /// edited or the editor for a new message. + fn active_editor(&self, cx: &App) -> Entity { + if let Some(index) = self.editing_message + && let Some(editor) = self + .entry_view_state + .read(cx) + .entry(index) + .and_then(|e| e.message_editor()) + .cloned() + { + editor + } else { + self.message_editor.clone() + } + } } fn loading_contents_spinner(size: IconSize) -> AnyElement { @@ -5400,7 +5419,7 @@ impl Focusable for AcpThreadView { fn focus_handle(&self, cx: &App) -> FocusHandle { match self.thread_state { ThreadState::Loading { .. } | ThreadState::Ready { .. } => { - self.message_editor.focus_handle(cx) + self.active_editor(cx).focus_handle(cx) } ThreadState::LoadError(_) | ThreadState::Unauthenticated { .. } => { self.focus_handle.clone() @@ -6661,4 +6680,146 @@ pub(crate) mod tests { ) }); } + + #[gpui::test] + async fn test_message_editing_insert_selections(cx: &mut TestAppContext) { + init_test(cx); + + let connection = StubAgentConnection::new(); + connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk { + content: acp::ContentBlock::Text(acp::TextContent { + text: "Response".into(), + annotations: None, + meta: None, + }), + }]); + + let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; + add_to_workspace(thread_view.clone(), cx); + + let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Original message to edit", window, cx) + }); + thread_view.update_in(cx, |thread_view, window, cx| thread_view.send(window, cx)); + cx.run_until_parked(); + + let user_message_editor = thread_view.read_with(cx, |thread_view, cx| { + thread_view + .entry_view_state + .read(cx) + .entry(0) + .expect("Should have at least one entry") + .message_editor() + .expect("Should have message editor") + .clone() + }); + + cx.focus(&user_message_editor); + thread_view.read_with(cx, |thread_view, _cx| { + assert_eq!(thread_view.editing_message, Some(0)); + }); + + // Ensure to edit the focused message before proceeding otherwise, since + // its content is not different from what was sent, focus will be lost. + user_message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Original message to edit with ", window, cx) + }); + + // Create a simple buffer with some text so we can create a selection + // that will then be added to the message being edited. + let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| { + (thread_view.workspace.clone(), thread_view.project.clone()) + }); + let buffer = project.update(cx, |project, cx| { + project.create_local_buffer("let a = 10 + 10;", None, false, cx) + }); + + workspace + .update_in(cx, |workspace, window, cx| { + let editor = cx.new(|cx| { + let mut editor = + Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx); + + editor.change_selections(Default::default(), window, cx, |selections| { + selections.select_ranges([8..15]); + }); + + editor + }); + workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx); + }) + .unwrap(); + + thread_view.update_in(cx, |thread_view, window, cx| { + assert_eq!(thread_view.editing_message, Some(0)); + thread_view.insert_selections(window, cx); + }); + + user_message_editor.read_with(cx, |editor, cx| { + let text = editor.editor().read(cx).text(cx); + let expected_text = String::from("Original message to edit with selection "); + + assert_eq!(text, expected_text); + }); + } + + #[gpui::test] + async fn test_insert_selections(cx: &mut TestAppContext) { + init_test(cx); + + let connection = StubAgentConnection::new(); + connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk { + content: acp::ContentBlock::Text(acp::TextContent { + text: "Response".into(), + annotations: None, + meta: None, + }), + }]); + + let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; + add_to_workspace(thread_view.clone(), cx); + + let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); + message_editor.update_in(cx, |editor, window, cx| { + editor.set_text("Can you review this snippet ", window, cx) + }); + + // Create a simple buffer with some text so we can create a selection + // that will then be added to the message being edited. + let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| { + (thread_view.workspace.clone(), thread_view.project.clone()) + }); + let buffer = project.update(cx, |project, cx| { + project.create_local_buffer("let a = 10 + 10;", None, false, cx) + }); + + workspace + .update_in(cx, |workspace, window, cx| { + let editor = cx.new(|cx| { + let mut editor = + Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx); + + editor.change_selections(Default::default(), window, cx, |selections| { + selections.select_ranges([8..15]); + }); + + editor + }); + workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx); + }) + .unwrap(); + + thread_view.update_in(cx, |thread_view, window, cx| { + assert_eq!(thread_view.editing_message, None); + thread_view.insert_selections(window, cx); + }); + + thread_view.read_with(cx, |thread_view, cx| { + let text = thread_view.message_editor.read(cx).text(cx); + let expected_txt = String::from("Can you review this snippet selection "); + + assert_eq!(text, expected_txt); + }) + } }