diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 225a68b0718b5e2d8c7dc8cbc0725aec9d48b551..a0aca0c51bd0afe1ea61f6a58c3585d68172ec24 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -21,8 +21,8 @@ use editor::{ }; use futures::{FutureExt as _, future::join_all}; use gpui::{ - AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageFormat, KeyContext, - SharedString, Subscription, Task, TextStyle, WeakEntity, + AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageFormat, + KeyContext, SharedString, Subscription, Task, TextStyle, WeakEntity, }; use language::{Buffer, Language, language_settings::InlayHintKind}; use project::{CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, Worktree}; @@ -543,6 +543,120 @@ impl MessageEditor { } fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context) { + let editor_clipboard_selections = cx + .read_from_clipboard() + .and_then(|item| item.entries().first().cloned()) + .and_then(|entry| match entry { + ClipboardEntry::String(text) => { + text.metadata_json::>() + } + _ => None, + }); + + let has_file_context = editor_clipboard_selections + .as_ref() + .is_some_and(|selections| { + selections + .iter() + .any(|sel| sel.file_path.is_some() && sel.line_range.is_some()) + }); + + if has_file_context { + if let Some((workspace, selections)) = + self.workspace.upgrade().zip(editor_clipboard_selections) + { + cx.stop_propagation(); + + let project = workspace.read(cx).project().clone(); + for selection in selections { + if let (Some(file_path), Some(line_range)) = + (selection.file_path, selection.line_range) + { + let crease_text = + acp_thread::selection_name(Some(file_path.as_ref()), &line_range); + + let mention_uri = MentionUri::Selection { + abs_path: Some(file_path.clone()), + line_range: line_range.clone(), + }; + + let mention_text = mention_uri.as_link().to_string(); + let (excerpt_id, text_anchor, content_len) = + self.editor.update(cx, |editor, cx| { + let buffer = editor.buffer().read(cx); + let snapshot = buffer.snapshot(cx); + let (excerpt_id, _, buffer_snapshot) = + snapshot.as_singleton().unwrap(); + let start_offset = buffer_snapshot.len(); + let text_anchor = buffer_snapshot.anchor_before(start_offset); + + editor.insert(&mention_text, window, cx); + editor.insert(" ", window, cx); + + (*excerpt_id, text_anchor, mention_text.len()) + }); + + let Some((crease_id, tx)) = insert_crease_for_mention( + excerpt_id, + text_anchor, + content_len, + crease_text.into(), + mention_uri.icon_path(cx), + None, + self.editor.clone(), + window, + cx, + ) else { + continue; + }; + drop(tx); + + let mention_task = cx + .spawn({ + let project = project.clone(); + async move |_, cx| { + let project_path = project + .update(cx, |project, cx| { + project.project_path_for_absolute_path(&file_path, cx) + }) + .map_err(|e| e.to_string())? + .ok_or_else(|| "project path not found".to_string())?; + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer(project_path, cx) + }) + .map_err(|e| e.to_string())? + .await + .map_err(|e| e.to_string())?; + + buffer + .update(cx, |buffer, cx| { + let start = Point::new(*line_range.start(), 0) + .min(buffer.max_point()); + let end = Point::new(*line_range.end() + 1, 0) + .min(buffer.max_point()); + let content = + buffer.text_for_range(start..end).collect(); + Mention::Text { + content, + tracked_buffers: vec![cx.entity()], + } + }) + .map_err(|e| e.to_string()) + } + }) + .shared(); + + self.mention_set.update(cx, |mention_set, _cx| { + mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task) + }); + } + } + return; + } + } + if self.prompt_capabilities.borrow().image && let Some(task) = paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx) diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 161fad95e68c015f720df825b1f0ca32f5d79124..fb9ee5e49e22fe7b70c02537a3e9a60394ddcc6f 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -1682,6 +1682,98 @@ impl TextThreadEditor { window: &mut Window, cx: &mut Context, ) { + let editor_clipboard_selections = cx + .read_from_clipboard() + .and_then(|item| item.entries().first().cloned()) + .and_then(|entry| match entry { + ClipboardEntry::String(text) => { + text.metadata_json::>() + } + _ => None, + }); + + let has_file_context = editor_clipboard_selections + .as_ref() + .is_some_and(|selections| { + selections + .iter() + .any(|sel| sel.file_path.is_some() && sel.line_range.is_some()) + }); + + if has_file_context { + if let Some(clipboard_item) = cx.read_from_clipboard() { + if let Some(ClipboardEntry::String(clipboard_text)) = + clipboard_item.entries().first() + { + if let Some(selections) = editor_clipboard_selections { + cx.stop_propagation(); + + let text = clipboard_text.text(); + self.editor.update(cx, |editor, cx| { + let mut current_offset = 0; + let weak_editor = cx.entity().downgrade(); + + for selection in selections { + if let (Some(file_path), Some(line_range)) = + (selection.file_path, selection.line_range) + { + let selected_text = + &text[current_offset..current_offset + selection.len]; + let fence = assistant_slash_commands::codeblock_fence_for_path( + file_path.to_str(), + Some(line_range.clone()), + ); + let formatted_text = format!("{fence}{selected_text}\n```"); + + let insert_point = editor + .selections + .newest::(&editor.display_snapshot(cx)) + .head(); + let start_row = MultiBufferRow(insert_point.row); + + editor.insert(&formatted_text, window, cx); + + let snapshot = editor.buffer().read(cx).snapshot(cx); + let anchor_before = snapshot.anchor_after(insert_point); + let anchor_after = editor + .selections + .newest_anchor() + .head() + .bias_left(&snapshot); + + editor.insert("\n", window, cx); + + let crease_text = acp_thread::selection_name( + Some(file_path.as_ref()), + &line_range, + ); + + let fold_placeholder = quote_selection_fold_placeholder( + crease_text, + weak_editor.clone(), + ); + let crease = Crease::inline( + anchor_before..anchor_after, + fold_placeholder, + render_quote_selection_output_toggle, + |_, _, _, _| Empty.into_any(), + ); + editor.insert_creases(vec![crease], cx); + editor.fold_at(start_row, window, cx); + + current_offset += selection.len; + if !selection.is_entire_line && current_offset < text.len() { + current_offset += 1; + } + } + } + }); + return; + } + } + } + } + cx.stop_propagation(); let mut images = if let Some(item) = cx.read_from_clipboard() { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d5ff85e5714a5efe255144fb46fbf7a7be29d5e8..223c0e574cff7daa9f50b07735b40e291185a37c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1592,6 +1592,45 @@ pub struct ClipboardSelection { pub is_entire_line: bool, /// The indentation of the first line when this content was originally copied. pub first_line_indent: u32, + #[serde(default)] + pub file_path: Option, + #[serde(default)] + pub line_range: Option>, +} + +impl ClipboardSelection { + pub fn for_buffer( + len: usize, + is_entire_line: bool, + range: Range, + buffer: &MultiBufferSnapshot, + project: Option<&Entity>, + cx: &App, + ) -> Self { + let first_line_indent = buffer + .indent_size_for_line(MultiBufferRow(range.start.row)) + .len; + + let file_path = util::maybe!({ + let project = project?.read(cx); + let file = buffer.file_at(range.start)?; + let project_path = ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path().clone(), + }; + project.absolute_path(&project_path, cx) + }); + + let line_range = file_path.as_ref().map(|_| range.start.row..=range.end.row); + + Self { + len, + is_entire_line, + first_line_indent, + file_path, + line_range, + } + } } // selections, scroll behavior, was newest selection reversed @@ -12812,13 +12851,15 @@ impl Editor { text.push_str(chunk); len += chunk.len(); } - clipboard_selections.push(ClipboardSelection { + + clipboard_selections.push(ClipboardSelection::for_buffer( len, is_entire_line, - first_line_indent: buffer - .indent_size_for_line(MultiBufferRow(selection.start.row)) - .len, - }); + selection.range(), + &buffer, + self.project.as_ref(), + cx, + )); } } @@ -12961,13 +13002,14 @@ impl Editor { text.push('\n'); len += 1; } - clipboard_selections.push(ClipboardSelection { + clipboard_selections.push(ClipboardSelection::for_buffer( len, is_entire_line, - first_line_indent: buffer - .indent_size_for_line(MultiBufferRow(trimmed_range.start.row)) - .len, - }); + trimmed_range, + &buffer, + self.project.as_ref(), + cx, + )); } } } diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index 81350d780a507a6e1d2502cf0f05115dc19abcdf..9920b8fc88d86625a1eb6642f59c894730905c77 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -11,7 +11,6 @@ use editor::{ClipboardSelection, Editor, SelectionEffects}; use gpui::Context; use gpui::Window; use language::Point; -use multi_buffer::MultiBufferRow; use settings::Settings; struct HighlightOnYank; @@ -198,11 +197,14 @@ impl Vim { if kind.linewise() { text.push('\n'); } - clipboard_selections.push(ClipboardSelection { - len: text.len() - initial_len, - is_entire_line: false, - first_line_indent: buffer.indent_size_for_line(MultiBufferRow(start.row)).len, - }); + clipboard_selections.push(ClipboardSelection::for_buffer( + text.len() - initial_len, + false, + start..end, + &buffer, + editor.project(), + cx, + )); } }