mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Merge branch 'main' into fix-worktree-drag-reorder
This commit is contained in:
commit
017c649340
5 changed files with 227 additions and 78 deletions
|
|
@ -299,10 +299,15 @@ impl ToolCall {
|
|||
|
||||
let subagent_session_info = subagent_session_info_from_meta(&tool_call.meta);
|
||||
|
||||
let label = if tool_call.kind == acp::ToolKind::Execute {
|
||||
cx.new(|cx| Markdown::new_text(title.into(), cx))
|
||||
} else {
|
||||
cx.new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx))
|
||||
};
|
||||
|
||||
let result = Self {
|
||||
id: tool_call.tool_call_id,
|
||||
label: cx
|
||||
.new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)),
|
||||
label,
|
||||
kind: tool_call.kind,
|
||||
content,
|
||||
locations: tool_call.locations,
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ use crate::agent_connection_store::{
|
|||
};
|
||||
use crate::agent_diff::AgentDiff;
|
||||
use crate::entry_view_state::{EntryViewEvent, ViewEvent};
|
||||
use crate::message_editor::{MessageEditor, MessageEditorEvent};
|
||||
use crate::message_editor::{InputAttempt, MessageEditor, MessageEditorEvent};
|
||||
use crate::profile_selector::{ProfileProvider, ProfileSelector};
|
||||
|
||||
use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore};
|
||||
|
|
@ -1387,7 +1387,7 @@ impl ConversationView {
|
|||
fn move_queued_message_to_main_editor(
|
||||
&mut self,
|
||||
index: usize,
|
||||
inserted_text: Option<&str>,
|
||||
attempt: Option<InputAttempt>,
|
||||
cursor_offset: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
|
@ -1396,7 +1396,7 @@ impl ConversationView {
|
|||
active.update(cx, |active, cx| {
|
||||
active.move_queued_message_to_main_editor(
|
||||
index,
|
||||
inserted_text,
|
||||
attempt,
|
||||
cursor_offset,
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -2384,15 +2384,17 @@ impl ConversationView {
|
|||
window,
|
||||
move |this, _editor, event, window, cx| match event {
|
||||
MessageEditorEvent::InputAttempted {
|
||||
text,
|
||||
attempt,
|
||||
cursor_offset,
|
||||
} => this.move_queued_message_to_main_editor(
|
||||
index,
|
||||
Some(text.as_ref()),
|
||||
Some(*cursor_offset),
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
} => {
|
||||
this.move_queued_message_to_main_editor(
|
||||
index,
|
||||
Some(attempt.clone()),
|
||||
Some(*cursor_offset),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
MessageEditorEvent::LostFocus => {
|
||||
this.save_queued_message_at_index(index, cx);
|
||||
}
|
||||
|
|
@ -2958,8 +2960,9 @@ pub(crate) mod tests {
|
|||
use agent::{AgentTool, EditFileTool, FetchTool, TerminalTool, ToolPermissionContext};
|
||||
use agent_servers::FakeAcpAgentServer;
|
||||
use editor::MultiBufferOffset;
|
||||
use editor::actions::Paste;
|
||||
use fs::FakeFs;
|
||||
use gpui::{EventEmitter, TestAppContext, VisualTestContext};
|
||||
use gpui::{ClipboardItem, EventEmitter, TestAppContext, VisualTestContext};
|
||||
use parking_lot::Mutex;
|
||||
use project::Project;
|
||||
use serde_json::json;
|
||||
|
|
@ -7405,6 +7408,107 @@ pub(crate) mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_paste_text_into_queued_message_promotes_to_main_editor(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let (conversation_view, cx) =
|
||||
paste_into_queued_message(cx, ClipboardItem::new_string("PASTED".to_string())).await;
|
||||
|
||||
let queue_len = active_thread(&conversation_view, cx)
|
||||
.read_with(cx, |thread, _cx| thread.local_queued_messages.len());
|
||||
assert_eq!(queue_len, 0);
|
||||
|
||||
let text = message_editor(&conversation_view, cx).update(cx, |editor, cx| editor.text(cx));
|
||||
assert_eq!(text, "queued PASTEDmessage");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_paste_image_into_queued_message_promotes_to_main_editor(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
use base64::Engine as _;
|
||||
use std::io::Write as _;
|
||||
let png_bytes = base64::prelude::BASE64_STANDARD
|
||||
.decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==")
|
||||
.unwrap();
|
||||
let mut image_file = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
|
||||
image_file.write_all(&png_bytes).unwrap();
|
||||
|
||||
let (conversation_view, cx) = paste_into_queued_message(
|
||||
cx,
|
||||
ClipboardItem {
|
||||
entries: vec![gpui::ClipboardEntry::ExternalPaths(gpui::ExternalPaths(
|
||||
vec![image_file.path().to_path_buf()].into(),
|
||||
))],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let queue_len = active_thread(&conversation_view, cx)
|
||||
.read_with(cx, |thread, _cx| thread.local_queued_messages.len());
|
||||
assert_eq!(queue_len, 0);
|
||||
|
||||
let text = message_editor(&conversation_view, cx).update(cx, |editor, cx| editor.text(cx));
|
||||
let image_name = image_file.path().file_name().unwrap().to_string_lossy();
|
||||
let expected_uri = acp_thread::MentionUri::PastedImage {
|
||||
name: image_name.to_string(),
|
||||
}
|
||||
.to_uri()
|
||||
.to_string();
|
||||
assert_eq!(
|
||||
text,
|
||||
format!("queued [@{image_name}]({expected_uri}) message"),
|
||||
);
|
||||
}
|
||||
|
||||
async fn paste_into_queued_message(
|
||||
cx: &mut TestAppContext,
|
||||
clipboard: ClipboardItem,
|
||||
) -> (Entity<ConversationView>, &mut VisualTestContext) {
|
||||
let (conversation_view, cx) =
|
||||
setup_conversation_view(StubAgentServer::default_response(), cx).await;
|
||||
add_to_workspace(conversation_view.clone(), cx);
|
||||
|
||||
active_thread(&conversation_view, cx).update_in(cx, |thread, _window, cx| {
|
||||
thread
|
||||
.session_capabilities
|
||||
.write()
|
||||
.set_prompt_capabilities(acp::PromptCapabilities::new().image(true));
|
||||
thread.add_to_queue(
|
||||
vec![acp::ContentBlock::Text(acp::TextContent::new(
|
||||
"queued message".to_string(),
|
||||
))],
|
||||
vec![],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
conversation_view.update(cx, |_, cx| cx.notify());
|
||||
cx.run_until_parked();
|
||||
|
||||
let queued_editor = active_thread(&conversation_view, cx).read_with(cx, |thread, _cx| {
|
||||
thread
|
||||
.queued_message_editors
|
||||
.first()
|
||||
.cloned()
|
||||
.expect("queued message editor not synced")
|
||||
});
|
||||
|
||||
cx.write_to_clipboard(clipboard);
|
||||
|
||||
queued_editor.update_in(cx, |message_editor, window, cx| {
|
||||
message_editor.editor().update(cx, |editor, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
|
||||
selections.select_ranges([MultiBufferOffset(7)..MultiBufferOffset(7)]);
|
||||
});
|
||||
});
|
||||
message_editor.paste(&Paste, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
(conversation_view, cx)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_close_all_sessions_skips_when_unsupported(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
|
|
|||
|
|
@ -1539,7 +1539,7 @@ impl ThreadView {
|
|||
pub fn move_queued_message_to_main_editor(
|
||||
&mut self,
|
||||
index: usize,
|
||||
inserted_text: Option<&str>,
|
||||
attempt: Option<InputAttempt>,
|
||||
cursor_offset: Option<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
|
@ -1549,36 +1549,35 @@ impl ThreadView {
|
|||
};
|
||||
let queued_content = queued_message.content;
|
||||
let message_editor = self.message_editor.clone();
|
||||
let inserted_text = inserted_text.map(ToOwned::to_owned);
|
||||
|
||||
window.focus(&message_editor.focus_handle(cx), cx);
|
||||
|
||||
if message_editor.read(cx).is_empty(cx) {
|
||||
let adjusted_cursor_offset = if message_editor.read(cx).is_empty(cx) {
|
||||
message_editor.update(cx, |editor, cx| {
|
||||
editor.set_message(queued_content, window, cx);
|
||||
if let Some(offset) = cursor_offset {
|
||||
editor.set_cursor_offset(offset, window, cx);
|
||||
}
|
||||
if let Some(inserted_text) = inserted_text.as_deref() {
|
||||
editor.insert_text(inserted_text, window, cx);
|
||||
}
|
||||
});
|
||||
cx.notify();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Adjust cursor offset accounting for existing content
|
||||
let existing_len = message_editor.read(cx).text(cx).len();
|
||||
let separator = "\n\n";
|
||||
cursor_offset
|
||||
} else {
|
||||
let existing_len = message_editor.read(cx).text(cx).len();
|
||||
let separator = "\n\n";
|
||||
message_editor.update(cx, |editor, cx| {
|
||||
editor.append_message(queued_content, Some(separator), window, cx);
|
||||
});
|
||||
cursor_offset.map(|offset| existing_len + separator.len() + offset)
|
||||
};
|
||||
|
||||
message_editor.update(cx, |editor, cx| {
|
||||
editor.append_message(queued_content, Some(separator), window, cx);
|
||||
if let Some(offset) = cursor_offset {
|
||||
let adjusted_offset = existing_len + separator.len() + offset;
|
||||
editor.set_cursor_offset(adjusted_offset, window, cx);
|
||||
if let Some(offset) = adjusted_cursor_offset {
|
||||
editor.set_cursor_offset(offset, window, cx);
|
||||
}
|
||||
if let Some(inserted_text) = inserted_text.as_deref() {
|
||||
editor.insert_text(inserted_text, window, cx);
|
||||
match attempt {
|
||||
Some(InputAttempt::Text(text)) => {
|
||||
editor.insert_text(&text, window, cx);
|
||||
}
|
||||
Some(InputAttempt::Paste(clipboard)) => {
|
||||
editor.paste_item(&clipboard, window, cx);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -145,6 +145,12 @@ pub struct MessageEditor {
|
|||
_parse_slash_command_task: Task<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum InputAttempt {
|
||||
Text(Arc<str>),
|
||||
Paste(ClipboardItem),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum MessageEditorEvent {
|
||||
Send,
|
||||
|
|
@ -153,7 +159,7 @@ pub enum MessageEditorEvent {
|
|||
Focus,
|
||||
LostFocus,
|
||||
InputAttempted {
|
||||
text: Arc<str>,
|
||||
attempt: InputAttempt,
|
||||
cursor_offset: usize,
|
||||
},
|
||||
}
|
||||
|
|
@ -494,7 +500,7 @@ impl MessageEditor {
|
|||
.to_offset(&editor.buffer().read(cx).snapshot(cx))
|
||||
.0;
|
||||
cx.emit(MessageEditorEvent::InputAttempted {
|
||||
text: text.clone(),
|
||||
attempt: InputAttempt::Text(text.clone()),
|
||||
cursor_offset,
|
||||
});
|
||||
}
|
||||
|
|
@ -954,18 +960,47 @@ impl MessageEditor {
|
|||
cx.emit(MessageEditorEvent::Cancel)
|
||||
}
|
||||
|
||||
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(clipboard) = cx.read_from_clipboard() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if self.editor.read(cx).read_only(cx) {
|
||||
let editor = self.editor.read(cx);
|
||||
let cursor_offset = editor
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.head()
|
||||
.to_offset(&editor.buffer().read(cx).snapshot(cx))
|
||||
.0;
|
||||
cx.emit(MessageEditorEvent::InputAttempted {
|
||||
attempt: InputAttempt::Paste(clipboard),
|
||||
cursor_offset,
|
||||
});
|
||||
cx.stop_propagation();
|
||||
return;
|
||||
}
|
||||
|
||||
cx.stop_propagation();
|
||||
self.paste_item(&clipboard, window, cx);
|
||||
}
|
||||
|
||||
pub fn paste_item(
|
||||
&mut self,
|
||||
clipboard: &ClipboardItem,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let editor_clipboard_selections = cx.read_from_clipboard().and_then(|item| {
|
||||
item.entries().iter().find_map(|entry| match entry {
|
||||
let editor_clipboard_selections =
|
||||
clipboard.entries().iter().find_map(|entry| match entry {
|
||||
ClipboardEntry::String(text) => {
|
||||
text.metadata_json::<Vec<editor::ClipboardSelection>>()
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
// Insert creases for pasted clipboard selections that:
|
||||
// 1. Contain exactly one selection
|
||||
|
|
@ -997,7 +1032,6 @@ impl MessageEditor {
|
|||
.unwrap_or(false);
|
||||
|
||||
if should_insert_creases && let Some(selections) = editor_clipboard_selections {
|
||||
cx.stop_propagation();
|
||||
let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
let (insertion_target, _) = snapshot
|
||||
.anchor_to_buffer_anchor(self.editor.read(cx).selections.newest_anchor().start)
|
||||
|
|
@ -1085,14 +1119,12 @@ impl MessageEditor {
|
|||
}
|
||||
// Handle text paste with potential markdown mention links before
|
||||
// clipboard context entries so markdown text still pastes as text.
|
||||
if let Some(clipboard_text) = cx.read_from_clipboard().and_then(|item| {
|
||||
item.entries().iter().find_map(|entry| match entry {
|
||||
ClipboardEntry::String(text) => Some(text.text().to_string()),
|
||||
_ => None,
|
||||
})
|
||||
}) {
|
||||
let clipboard_text = clipboard.entries().iter().find_map(|entry| match entry {
|
||||
ClipboardEntry::String(text) => Some(text.text().to_string()),
|
||||
_ => None,
|
||||
});
|
||||
if let Some(clipboard_text) = clipboard_text.as_deref() {
|
||||
if clipboard_text.contains("[@") {
|
||||
cx.stop_propagation();
|
||||
let selections_before = self.editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
editor
|
||||
|
|
@ -1109,7 +1141,7 @@ impl MessageEditor {
|
|||
});
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.insert(&clipboard_text, window, cx);
|
||||
editor.insert(clipboard_text, window, cx);
|
||||
});
|
||||
|
||||
let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
|
|
@ -1180,12 +1212,13 @@ impl MessageEditor {
|
|||
}
|
||||
}
|
||||
|
||||
if self.handle_pasted_context(window, cx) {
|
||||
if self.handle_pasted_context(clipboard, window, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall through to default editor paste
|
||||
cx.propagate();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.paste_item(clipboard, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
|
||||
|
|
@ -1205,11 +1238,12 @@ impl MessageEditor {
|
|||
});
|
||||
}
|
||||
|
||||
fn handle_pasted_context(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
|
||||
let Some(clipboard) = cx.read_from_clipboard() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
fn handle_pasted_context(
|
||||
&mut self,
|
||||
clipboard: &ClipboardItem,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
if matches!(
|
||||
clipboard.entries().first(),
|
||||
Some(ClipboardEntry::String(_)) | None
|
||||
|
|
@ -1229,9 +1263,7 @@ impl MessageEditor {
|
|||
let editor = self.editor.clone();
|
||||
let mention_set = self.mention_set.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
let entries = clipboard.into_entries().collect::<Vec<_>>();
|
||||
|
||||
cx.stop_propagation();
|
||||
let entries = clipboard.clone().into_entries().collect::<Vec<_>>();
|
||||
|
||||
window
|
||||
.spawn(cx, async move |mut cx| {
|
||||
|
|
|
|||
|
|
@ -14756,24 +14756,33 @@ impl Editor {
|
|||
}
|
||||
|
||||
pub fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(item) = cx.read_from_clipboard() {
|
||||
self.paste_item(&item, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paste_item(
|
||||
&mut self,
|
||||
item: &ClipboardItem,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.read_only(cx) {
|
||||
return;
|
||||
}
|
||||
if let Some(item) = cx.read_from_clipboard() {
|
||||
let clipboard_string = item.entries().iter().find_map(|entry| match entry {
|
||||
ClipboardEntry::String(s) => Some(s),
|
||||
_ => None,
|
||||
});
|
||||
match clipboard_string {
|
||||
Some(clipboard_string) => self.do_paste(
|
||||
clipboard_string.text(),
|
||||
clipboard_string.metadata_json::<Vec<ClipboardSelection>>(),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
_ => self.do_paste(&item.text().unwrap_or_default(), None, true, window, cx),
|
||||
}
|
||||
let clipboard_string = item.entries().iter().find_map(|entry| match entry {
|
||||
ClipboardEntry::String(s) => Some(s),
|
||||
_ => None,
|
||||
});
|
||||
match clipboard_string {
|
||||
Some(clipboard_string) => self.do_paste(
|
||||
clipboard_string.text(),
|
||||
clipboard_string.metadata_json::<Vec<ClipboardSelection>>(),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
_ => self.do_paste(&item.text().unwrap_or_default(), None, true, window, cx),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue