Detailed changes
@@ -225,7 +225,8 @@
"ctrl-k c": "assistant::CopyCode",
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPreviousMatch",
- "ctrl-k l": "agent::OpenRulesLibrary"
+ "ctrl-k l": "agent::OpenRulesLibrary",
+ "ctrl-shift-v": "agent::PasteRaw"
}
},
{
@@ -290,7 +291,8 @@
"ctrl-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
- "ctrl-shift-n": "agent::RejectAll"
+ "ctrl-shift-n": "agent::RejectAll",
+ "ctrl-shift-v": "agent::PasteRaw"
}
},
{
@@ -301,7 +303,8 @@
"ctrl-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
- "ctrl-shift-n": "agent::RejectAll"
+ "ctrl-shift-n": "agent::RejectAll",
+ "ctrl-shift-v": "agent::PasteRaw"
}
},
{
@@ -264,7 +264,8 @@
"cmd-k c": "assistant::CopyCode",
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPreviousMatch",
- "cmd-k l": "agent::OpenRulesLibrary"
+ "cmd-k l": "agent::OpenRulesLibrary",
+ "cmd-shift-v": "agent::PasteRaw"
}
},
{
@@ -331,7 +332,8 @@
"cmd-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
- "cmd-shift-n": "agent::RejectAll"
+ "cmd-shift-n": "agent::RejectAll",
+ "cmd-shift-v": "agent::PasteRaw"
}
},
{
@@ -343,7 +345,8 @@
"cmd-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
- "cmd-shift-n": "agent::RejectAll"
+ "cmd-shift-n": "agent::RejectAll",
+ "cmd-shift-v": "agent::PasteRaw"
}
},
{
@@ -226,7 +226,8 @@
"ctrl-k c": "assistant::CopyCode",
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPreviousMatch",
- "ctrl-k l": "agent::OpenRulesLibrary"
+ "ctrl-k l": "agent::OpenRulesLibrary",
+ "ctrl-shift-v": "agent::PasteRaw"
}
},
{
@@ -294,7 +295,8 @@
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
- "ctrl-shift-n": "agent::RejectAll"
+ "ctrl-shift-n": "agent::RejectAll",
+ "ctrl-shift-v": "agent::PasteRaw"
}
},
{
@@ -306,7 +308,8 @@
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
- "ctrl-shift-n": "agent::RejectAll"
+ "ctrl-shift-n": "agent::RejectAll",
+ "ctrl-shift-v": "agent::PasteRaw"
}
},
{
@@ -34,7 +34,7 @@ use theme::ThemeSettings;
use ui::prelude::*;
use util::{ResultExt, debug_panic};
use workspace::{CollaboratorId, Workspace};
-use zed_actions::agent::Chat;
+use zed_actions::agent::{Chat, PasteRaw};
pub struct MessageEditor {
mention_set: Entity<MentionSet>,
@@ -543,6 +543,9 @@ impl MessageEditor {
}
fn paste(&mut self, _: &Paste, 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().first().cloned())
@@ -553,133 +556,127 @@ impl MessageEditor {
_ => 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())
- });
+ // Insert creases for pasted clipboard selections that:
+ // 1. Contain exactly one selection
+ // 2. Have an associated file path
+ // 3. Span multiple lines (not single-line selections)
+ // 4. Belong to a file that exists in the current project
+ let should_insert_creases = util::maybe!({
+ let selections = editor_clipboard_selections.as_ref()?;
+ if selections.len() > 1 {
+ return Some(false);
+ }
+ let selection = selections.first()?;
+ let file_path = selection.file_path.as_ref()?;
+ let line_range = selection.line_range.as_ref()?;
- if has_file_context {
- if let Some((workspace, selections)) =
- self.workspace.upgrade().zip(editor_clipboard_selections)
- {
- let Some(first_selection) = selections.first() else {
- return;
- };
- if let Some(file_path) = &first_selection.file_path {
- // In case someone pastes selections from another window
- // with a different project, we don't want to insert the
- // crease (containing the absolute path) since the agent
- // cannot access files outside the project.
- let is_in_project = workspace
- .read(cx)
- .project()
- .read(cx)
- .project_path_for_absolute_path(file_path, cx)
- .is_some();
- if !is_in_project {
- return;
- }
- }
+ if line_range.start() == line_range.end() {
+ return Some(false);
+ }
- cx.stop_propagation();
- let insertion_target = self
- .editor
+ Some(
+ workspace
.read(cx)
- .selections
- .newest_anchor()
- .start
- .text_anchor;
-
- 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);
+ .project()
+ .read(cx)
+ .project_path_for_absolute_path(file_path, cx)
+ .is_some(),
+ )
+ })
+ .unwrap_or(false);
- let mention_uri = MentionUri::Selection {
- abs_path: Some(file_path.clone()),
- line_range: line_range.clone(),
- };
+ if should_insert_creases && let Some(selections) = editor_clipboard_selections {
+ cx.stop_propagation();
+ let insertion_target = self
+ .editor
+ .read(cx)
+ .selections
+ .newest_anchor()
+ .start
+ .text_anchor;
+
+ 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_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 text_anchor = insertion_target.bias_left(&buffer_snapshot);
-
- 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();
+ 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 text_anchor = insertion_target.bias_left(&buffer_snapshot);
+
+ editor.insert(&mention_text, window, cx);
+ editor.insert(" ", window, cx);
- self.mention_set.update(cx, |mention_set, _cx| {
- mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
+ (*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;
}
+ return;
}
if self.prompt_capabilities.borrow().image
@@ -690,6 +687,13 @@ impl MessageEditor {
}
}
+ fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
+ let editor = self.editor.clone();
+ window.defer(cx, move |window, cx| {
+ editor.update(cx, |editor, cx| editor.paste(&Paste, window, cx));
+ });
+ }
+
pub fn insert_dragged_files(
&mut self,
paths: Vec<project::ProjectPath>,
@@ -967,6 +971,7 @@ impl Render for MessageEditor {
.on_action(cx.listener(Self::chat))
.on_action(cx.listener(Self::chat_with_follow))
.on_action(cx.listener(Self::cancel))
+ .on_action(cx.listener(Self::paste_raw))
.capture_action(cx.listener(Self::paste))
.flex_1()
.child({
@@ -71,7 +71,7 @@ use workspace::{
pane,
searchable::{SearchEvent, SearchableItem},
};
-use zed_actions::agent::{AddSelectionToThread, ToggleModelSelector};
+use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector};
use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};
use assistant_text_thread::{
@@ -1682,6 +1682,9 @@ impl TextThreadEditor {
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().first().cloned())
@@ -1692,84 +1695,101 @@ impl TextThreadEditor {
_ => 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::<Point>(&editor.display_snapshot(cx))
- .head();
- let start_row = MultiBufferRow(insert_point.row);
-
- editor.insert(&formatted_text, window, cx);
+ // Insert creases for pasted clipboard selections that:
+ // 1. Contain exactly one selection
+ // 2. Have an associated file path
+ // 3. Span multiple lines (not single-line selections)
+ // 4. Belong to a file that exists in the current project
+ let should_insert_creases = util::maybe!({
+ let selections = editor_clipboard_selections.as_ref()?;
+ if selections.len() > 1 {
+ return Some(false);
+ }
+ let selection = selections.first()?;
+ let file_path = selection.file_path.as_ref()?;
+ let line_range = selection.line_range.as_ref()?;
- 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);
+ if line_range.start() == line_range.end() {
+ return Some(false);
+ }
- editor.insert("\n", window, cx);
+ Some(
+ workspace
+ .read(cx)
+ .project()
+ .read(cx)
+ .project_path_for_absolute_path(file_path, cx)
+ .is_some(),
+ )
+ })
+ .unwrap_or(false);
- let crease_text = acp_thread::selection_name(
- Some(file_path.as_ref()),
- &line_range,
- );
+ if should_insert_creases && 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 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);
+ let text = clipboard_text.text();
+ self.editor.update(cx, |editor, cx| {
+ let mut current_offset = 0;
+ let weak_editor = cx.entity().downgrade();
- current_offset += selection.len;
- if !selection.is_entire_line && current_offset < text.len() {
- current_offset += 1;
- }
+ 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::<Point>(&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;
- }
+ }
+ });
+ return;
}
}
}
@@ -1928,6 +1948,12 @@ impl TextThreadEditor {
}
}
+ fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
+ self.editor.update(cx, |editor, cx| {
+ editor.paste(&editor::actions::Paste, window, cx);
+ });
+ }
+
fn update_image_blocks(&mut self, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
@@ -2572,6 +2598,7 @@ impl Render for TextThreadEditor {
.capture_action(cx.listener(TextThreadEditor::copy))
.capture_action(cx.listener(TextThreadEditor::cut))
.capture_action(cx.listener(TextThreadEditor::paste))
+ .on_action(cx.listener(TextThreadEditor::paste_raw))
.capture_action(cx.listener(TextThreadEditor::cycle_message_role))
.capture_action(cx.listener(TextThreadEditor::confirm_command))
.on_action(cx.listener(TextThreadEditor::assist))
@@ -350,6 +350,8 @@ pub mod agent {
AddSelectionToThread,
/// Resets the agent panel zoom levels (agent UI and buffer font sizes).
ResetAgentZoom,
+ /// Pastes clipboard content without any formatting.
+ PasteRaw,
]
);
}