Compare commits

...

9 Commits

Author SHA1 Message Date
Bennet Bo Fenner
87251198ef Fix errors after merge conflicts 2025-12-08 17:20:50 +01:00
Bennet Bo Fenner
7997c29260 Merge branch 'main' into agent-paste-context 2025-12-08 17:09:13 +01:00
Bennet Bo Fenner
23c1556b9e Fix abs_path, wrong line numbers 2025-12-08 17:08:47 +01:00
Bennet Bo Fenner
67e4361040 Deduplicate logic 2025-12-05 13:11:50 +01:00
ozer
0317d0497d refactor: adaptive to new structure of message editor 2025-11-22 16:11:19 +03:00
ozer
f2476cf44e Merge branch 'agent-paste-context' of github.com:ddoemonn/zed into agent-paste-context 2025-11-22 15:52:19 +03:00
ozer
32ece6eba8 Merge branch 'main' of github.com:ddoemonn/zed into agent-paste-context 2025-11-22 15:51:44 +03:00
ozzy
48e728e086 Merge branch 'main' into agent-paste-context 2025-11-22 15:27:23 +03:00
ozer
302d91ddd9 feat: auto-capture file context on paste to agent panel 2025-11-18 18:08:30 +03:00
4 changed files with 268 additions and 18 deletions

View File

@@ -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<Self>) {
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::<Vec<editor::ClipboardSelection>>()
}
_ => 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)

View File

@@ -1682,6 +1682,98 @@ impl TextThreadEditor {
window: &mut Window,
cx: &mut Context<Self>,
) {
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::<Vec<editor::ClipboardSelection>>()
}
_ => 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);
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() {

View File

@@ -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<PathBuf>,
#[serde(default)]
pub line_range: Option<RangeInclusive<u32>>,
}
impl ClipboardSelection {
pub fn for_buffer(
len: usize,
is_entire_line: bool,
range: Range<Point>,
buffer: &MultiBufferSnapshot,
project: Option<&Entity<Project>>,
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,
));
}
}
}

View File

@@ -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,
));
}
}