agent_ui: Improve quote selections to consider message being edited (#39947)
- Update `AcpThreadView.insert_selections` to take into account whether the user is currently editing an existing message and, if it is, insert the selection into that message instead of the thread's message editor - Update Window's default keymap to use the `agent::QuoteSelection` action instead of the deprecated `assistant::QuoteSelection` action - Introduce `AcpThreadView.active_editor` to allow callers to retrieve either the thread view's message editor or the editor for the message being edited, in case `AcpThreadView.editing_message` is not `None` - Improve `AcpThreadView.focus_handle` to focus on the message being currently edited in case the user navigates back to the editor and then to the thread view again, all while editing a message - Add tests for `AcpThreadView.insert_selections`, ensuring that the selection is inserted in the message being currently edited, if a message is being edited, or the thread view's message editor if no message is being edited Closes #39693 Release Notes: - Improved `agent: quote selection` to also work for a message that was already sent but is being edited --------- Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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>) {
|
||||
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<MessageEditor> {
|
||||
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);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user