assistant: Support copy/pasting creases (#17490)
https://github.com/user-attachments/assets/78a2572d-8e8f-4206-9680-dcd884e7bbbd Release Notes: - Added support for copying and pasting slash commands in the assistant panel --------- Co-authored-by: Thorsten <thorsten@zed.dev>
This commit is contained in:
parent
66ef318823
commit
fcf79c0f1d
2 changed files with 283 additions and 48 deletions
|
@ -26,8 +26,8 @@ use collections::{BTreeSet, HashMap, HashSet};
|
||||||
use editor::{
|
use editor::{
|
||||||
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
|
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
|
||||||
display_map::{
|
display_map::{
|
||||||
BlockDisposition, BlockId, BlockProperties, BlockStyle, Crease, CustomBlockId, FoldId,
|
BlockDisposition, BlockId, BlockProperties, BlockStyle, Crease, CreaseMetadata,
|
||||||
RenderBlock, ToDisplayPoint,
|
CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
|
||||||
},
|
},
|
||||||
scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor},
|
scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor},
|
||||||
Anchor, Editor, EditorEvent, ExcerptRange, MultiBuffer, RowExt, ToOffset as _, ToPoint,
|
Anchor, Editor, EditorEvent, ExcerptRange, MultiBuffer, RowExt, ToOffset as _, ToPoint,
|
||||||
|
@ -54,13 +54,13 @@ use multi_buffer::MultiBufferRow;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::{Project, ProjectLspAdapterDelegate, Worktree};
|
use project::{Project, ProjectLspAdapterDelegate, Worktree};
|
||||||
use search::{buffer_search::DivRegistrar, BufferSearchBar};
|
use search::{buffer_search::DivRegistrar, BufferSearchBar};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{update_settings_file, Settings};
|
use settings::{update_settings_file, Settings};
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
cmp,
|
cmp,
|
||||||
collections::hash_map,
|
collections::hash_map,
|
||||||
fmt::Write,
|
|
||||||
ops::{ControlFlow, Range},
|
ops::{ControlFlow, Range},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
@ -2491,7 +2491,8 @@ impl ContextEditor {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
|
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
|
||||||
buffer_rows_to_fold.insert(buffer_row);
|
buffer_rows_to_fold.insert(buffer_row);
|
||||||
creases.push(Crease::new(
|
creases.push(
|
||||||
|
Crease::new(
|
||||||
start..end,
|
start..end,
|
||||||
FoldPlaceholder {
|
FoldPlaceholder {
|
||||||
render: render_fold_icon_button(
|
render: render_fold_icon_button(
|
||||||
|
@ -2504,7 +2505,12 @@ impl ContextEditor {
|
||||||
},
|
},
|
||||||
render_slash_command_output_toggle,
|
render_slash_command_output_toggle,
|
||||||
|_, _, _| Empty.into_any_element(),
|
|_, _, _| Empty.into_any_element(),
|
||||||
));
|
)
|
||||||
|
.with_metadata(CreaseMetadata {
|
||||||
|
icon: section.icon,
|
||||||
|
label: section.label,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.insert_creases(creases, cx);
|
editor.insert_creases(creases, cx);
|
||||||
|
@ -3318,12 +3324,92 @@ impl ContextEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
|
fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
|
||||||
let editor = self.editor.read(cx);
|
if self.editor.read(cx).selections.count() == 1 {
|
||||||
|
let (copied_text, metadata) = self.get_clipboard_contents(cx);
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
|
||||||
|
copied_text,
|
||||||
|
metadata,
|
||||||
|
));
|
||||||
|
cx.stop_propagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.propagate();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cut(&mut self, _: &editor::actions::Cut, cx: &mut ViewContext<Self>) {
|
||||||
|
if self.editor.read(cx).selections.count() == 1 {
|
||||||
|
let (copied_text, metadata) = self.get_clipboard_contents(cx);
|
||||||
|
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
let selections = editor.selections.all::<Point>(cx);
|
||||||
|
|
||||||
|
editor.transact(cx, |this, cx| {
|
||||||
|
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
|
s.select(selections);
|
||||||
|
});
|
||||||
|
this.insert("", cx);
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
|
||||||
|
copied_text,
|
||||||
|
metadata,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.stop_propagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.propagate();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_clipboard_contents(&mut self, cx: &mut ViewContext<Self>) -> (String, CopyMetadata) {
|
||||||
|
let creases = self.editor.update(cx, |editor, cx| {
|
||||||
|
let selection = editor.selections.newest::<Point>(cx);
|
||||||
|
let selection_start = editor.selections.newest::<usize>(cx).start;
|
||||||
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
|
editor.display_map.update(cx, |display_map, cx| {
|
||||||
|
display_map
|
||||||
|
.snapshot(cx)
|
||||||
|
.crease_snapshot
|
||||||
|
.creases_in_range(
|
||||||
|
MultiBufferRow(selection.start.row)..MultiBufferRow(selection.end.row + 1),
|
||||||
|
&snapshot,
|
||||||
|
)
|
||||||
|
.filter_map(|crease| {
|
||||||
|
if let Some(metadata) = &crease.metadata {
|
||||||
|
let start = crease
|
||||||
|
.range
|
||||||
|
.start
|
||||||
|
.to_offset(&snapshot)
|
||||||
|
.saturating_sub(selection_start);
|
||||||
|
let end = crease
|
||||||
|
.range
|
||||||
|
.end
|
||||||
|
.to_offset(&snapshot)
|
||||||
|
.saturating_sub(selection_start);
|
||||||
|
|
||||||
|
let range_relative_to_selection = start..end;
|
||||||
|
|
||||||
|
if range_relative_to_selection.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(SelectedCreaseMetadata {
|
||||||
|
range_relative_to_selection,
|
||||||
|
crease: metadata.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
let context = self.context.read(cx);
|
let context = self.context.read(cx);
|
||||||
if editor.selections.count() == 1 {
|
let selection = self.editor.read(cx).selections.newest::<usize>(cx);
|
||||||
let selection = editor.selections.newest::<usize>(cx);
|
let mut text = String::new();
|
||||||
let mut copied_text = String::new();
|
|
||||||
let mut spanned_messages = 0;
|
|
||||||
for message in context.messages(cx) {
|
for message in context.messages(cx) {
|
||||||
if message.offset_range.start >= selection.range().end {
|
if message.offset_range.start >= selection.range().end {
|
||||||
break;
|
break;
|
||||||
|
@ -3331,26 +3417,20 @@ impl ContextEditor {
|
||||||
let range = cmp::max(message.offset_range.start, selection.range().start)
|
let range = cmp::max(message.offset_range.start, selection.range().start)
|
||||||
..cmp::min(message.offset_range.end, selection.range().end);
|
..cmp::min(message.offset_range.end, selection.range().end);
|
||||||
if !range.is_empty() {
|
if !range.is_empty() {
|
||||||
spanned_messages += 1;
|
|
||||||
write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
|
|
||||||
for chunk in context.buffer().read(cx).text_for_range(range) {
|
for chunk in context.buffer().read(cx).text_for_range(range) {
|
||||||
copied_text.push_str(chunk);
|
text.push_str(chunk);
|
||||||
}
|
}
|
||||||
copied_text.push('\n');
|
text.push('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if spanned_messages > 1 {
|
(text, CopyMetadata { creases })
|
||||||
cx.write_to_clipboard(ClipboardItem::new_string(copied_text));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.propagate();
|
fn paste(&mut self, action: &editor::actions::Paste, cx: &mut ViewContext<Self>) {
|
||||||
}
|
cx.stop_propagation();
|
||||||
|
|
||||||
fn paste(&mut self, _: &editor::actions::Paste, cx: &mut ViewContext<Self>) {
|
|
||||||
let images = if let Some(item) = cx.read_from_clipboard() {
|
let images = if let Some(item) = cx.read_from_clipboard() {
|
||||||
item.into_entries()
|
item.into_entries()
|
||||||
.filter_map(|entry| {
|
.filter_map(|entry| {
|
||||||
|
@ -3365,9 +3445,62 @@ impl ContextEditor {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let metadata = if let Some(item) = cx.read_from_clipboard() {
|
||||||
|
item.entries().first().and_then(|entry| {
|
||||||
|
if let ClipboardEntry::String(text) = entry {
|
||||||
|
text.metadata_json::<CopyMetadata>()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
if images.is_empty() {
|
if images.is_empty() {
|
||||||
// If we didn't find any valid image data to paste, propagate to let normal pasting happen.
|
self.editor.update(cx, |editor, cx| {
|
||||||
cx.propagate();
|
let paste_position = editor.selections.newest::<usize>(cx).head();
|
||||||
|
editor.paste(action, cx);
|
||||||
|
|
||||||
|
if let Some(metadata) = metadata {
|
||||||
|
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||||
|
|
||||||
|
let mut buffer_rows_to_fold = BTreeSet::new();
|
||||||
|
let weak_editor = cx.view().downgrade();
|
||||||
|
editor.insert_creases(
|
||||||
|
metadata.creases.into_iter().map(|metadata| {
|
||||||
|
let start = buffer.anchor_after(
|
||||||
|
paste_position + metadata.range_relative_to_selection.start,
|
||||||
|
);
|
||||||
|
let end = buffer.anchor_before(
|
||||||
|
paste_position + metadata.range_relative_to_selection.end,
|
||||||
|
);
|
||||||
|
|
||||||
|
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
|
||||||
|
buffer_rows_to_fold.insert(buffer_row);
|
||||||
|
Crease::new(
|
||||||
|
start..end,
|
||||||
|
FoldPlaceholder {
|
||||||
|
constrain_width: false,
|
||||||
|
render: render_fold_icon_button(
|
||||||
|
weak_editor.clone(),
|
||||||
|
metadata.crease.icon,
|
||||||
|
metadata.crease.label.clone(),
|
||||||
|
),
|
||||||
|
merge_adjacent: false,
|
||||||
|
},
|
||||||
|
render_slash_command_output_toggle,
|
||||||
|
|_, _, _| Empty.into_any(),
|
||||||
|
)
|
||||||
|
.with_metadata(metadata.crease.clone())
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
for buffer_row in buffer_rows_to_fold.into_iter().rev() {
|
||||||
|
editor.fold_at(&FoldAt { buffer_row }, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
let mut image_positions = Vec::new();
|
let mut image_positions = Vec::new();
|
||||||
self.editor.update(cx, |editor, cx| {
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
@ -4037,6 +4170,17 @@ fn render_fold_icon_button(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct CopyMetadata {
|
||||||
|
creases: Vec<SelectedCreaseMetadata>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct SelectedCreaseMetadata {
|
||||||
|
range_relative_to_selection: Range<usize>,
|
||||||
|
crease: CreaseMetadata,
|
||||||
|
}
|
||||||
|
|
||||||
impl EventEmitter<EditorEvent> for ContextEditor {}
|
impl EventEmitter<EditorEvent> for ContextEditor {}
|
||||||
impl EventEmitter<SearchEvent> for ContextEditor {}
|
impl EventEmitter<SearchEvent> for ContextEditor {}
|
||||||
|
|
||||||
|
@ -4062,6 +4206,7 @@ impl Render for ContextEditor {
|
||||||
.capture_action(cx.listener(ContextEditor::cancel))
|
.capture_action(cx.listener(ContextEditor::cancel))
|
||||||
.capture_action(cx.listener(ContextEditor::save))
|
.capture_action(cx.listener(ContextEditor::save))
|
||||||
.capture_action(cx.listener(ContextEditor::copy))
|
.capture_action(cx.listener(ContextEditor::copy))
|
||||||
|
.capture_action(cx.listener(ContextEditor::cut))
|
||||||
.capture_action(cx.listener(ContextEditor::paste))
|
.capture_action(cx.listener(ContextEditor::paste))
|
||||||
.capture_action(cx.listener(ContextEditor::cycle_message_role))
|
.capture_action(cx.listener(ContextEditor::cycle_message_role))
|
||||||
.capture_action(cx.listener(ContextEditor::confirm_command))
|
.capture_action(cx.listener(ContextEditor::confirm_command))
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use gpui::{AnyElement, IntoElement};
|
use gpui::{AnyElement, IntoElement};
|
||||||
use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToPoint};
|
use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToPoint};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{cmp::Ordering, ops::Range, sync::Arc};
|
use std::{cmp::Ordering, ops::Range, sync::Arc};
|
||||||
use sum_tree::{Bias, SeekTarget, SumTree};
|
use sum_tree::{Bias, SeekTarget, SumTree};
|
||||||
use text::Point;
|
use text::Point;
|
||||||
use ui::WindowContext;
|
use ui::{IconName, SharedString, WindowContext};
|
||||||
|
|
||||||
use crate::FoldPlaceholder;
|
use crate::FoldPlaceholder;
|
||||||
|
|
||||||
|
@ -49,6 +50,31 @@ impl CreaseSnapshot {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn creases_in_range<'a>(
|
||||||
|
&'a self,
|
||||||
|
range: Range<MultiBufferRow>,
|
||||||
|
snapshot: &'a MultiBufferSnapshot,
|
||||||
|
) -> impl '_ + Iterator<Item = &'a Crease> {
|
||||||
|
let start = snapshot.anchor_before(Point::new(range.start.0, 0));
|
||||||
|
let mut cursor = self.creases.cursor::<ItemSummary>();
|
||||||
|
cursor.seek(&start, Bias::Left, snapshot);
|
||||||
|
|
||||||
|
std::iter::from_fn(move || {
|
||||||
|
while let Some(item) = cursor.item() {
|
||||||
|
cursor.next(snapshot);
|
||||||
|
let crease_start = item.crease.range.start.to_point(snapshot);
|
||||||
|
let crease_end = item.crease.range.end.to_point(snapshot);
|
||||||
|
if crease_end.row > range.end.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if crease_start.row >= range.start.0 && crease_end.row < range.end.0 {
|
||||||
|
return Some(&item.crease);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn crease_items_with_offsets(
|
pub fn crease_items_with_offsets(
|
||||||
&self,
|
&self,
|
||||||
snapshot: &MultiBufferSnapshot,
|
snapshot: &MultiBufferSnapshot,
|
||||||
|
@ -87,6 +113,14 @@ pub struct Crease {
|
||||||
pub placeholder: FoldPlaceholder,
|
pub placeholder: FoldPlaceholder,
|
||||||
pub render_toggle: RenderToggleFn,
|
pub render_toggle: RenderToggleFn,
|
||||||
pub render_trailer: RenderTrailerFn,
|
pub render_trailer: RenderTrailerFn,
|
||||||
|
pub metadata: Option<CreaseMetadata>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Metadata about a [`Crease`], that is used for serialization.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct CreaseMetadata {
|
||||||
|
pub icon: IconName,
|
||||||
|
pub label: SharedString,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Crease {
|
impl Crease {
|
||||||
|
@ -124,8 +158,14 @@ impl Crease {
|
||||||
render_trailer: Arc::new(move |row, folded, cx| {
|
render_trailer: Arc::new(move |row, folded, cx| {
|
||||||
render_trailer(row, folded, cx).into_any_element()
|
render_trailer(row, folded, cx).into_any_element()
|
||||||
}),
|
}),
|
||||||
|
metadata: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_metadata(mut self, metadata: CreaseMetadata) -> Self {
|
||||||
|
self.metadata = Some(metadata);
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Crease {
|
impl std::fmt::Debug for Crease {
|
||||||
|
@ -304,4 +344,54 @@ mod test {
|
||||||
.query_row(MultiBufferRow(3), &snapshot)
|
.query_row(MultiBufferRow(3), &snapshot)
|
||||||
.is_none());
|
.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_creases_in_range(cx: &mut AppContext) {
|
||||||
|
let text = "line1\nline2\nline3\nline4\nline5\nline6\nline7";
|
||||||
|
let buffer = MultiBuffer::build_simple(text, cx);
|
||||||
|
let snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
|
||||||
|
let mut crease_map = CreaseMap::default();
|
||||||
|
|
||||||
|
let creases = [
|
||||||
|
Crease::new(
|
||||||
|
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 5)),
|
||||||
|
FoldPlaceholder::test(),
|
||||||
|
|_row, _folded, _toggle, _cx| div(),
|
||||||
|
|_row, _folded, _cx| div(),
|
||||||
|
),
|
||||||
|
Crease::new(
|
||||||
|
snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_after(Point::new(3, 5)),
|
||||||
|
FoldPlaceholder::test(),
|
||||||
|
|_row, _folded, _toggle, _cx| div(),
|
||||||
|
|_row, _folded, _cx| div(),
|
||||||
|
),
|
||||||
|
Crease::new(
|
||||||
|
snapshot.anchor_before(Point::new(5, 0))..snapshot.anchor_after(Point::new(5, 5)),
|
||||||
|
FoldPlaceholder::test(),
|
||||||
|
|_row, _folded, _toggle, _cx| div(),
|
||||||
|
|_row, _folded, _cx| div(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
crease_map.insert(creases, &snapshot);
|
||||||
|
|
||||||
|
let crease_snapshot = crease_map.snapshot();
|
||||||
|
|
||||||
|
let range = MultiBufferRow(0)..MultiBufferRow(7);
|
||||||
|
let creases: Vec<_> = crease_snapshot.creases_in_range(range, &snapshot).collect();
|
||||||
|
assert_eq!(creases.len(), 3);
|
||||||
|
|
||||||
|
let range = MultiBufferRow(2)..MultiBufferRow(5);
|
||||||
|
let creases: Vec<_> = crease_snapshot.creases_in_range(range, &snapshot).collect();
|
||||||
|
assert_eq!(creases.len(), 1);
|
||||||
|
assert_eq!(creases[0].range.start.to_point(&snapshot).row, 3);
|
||||||
|
|
||||||
|
let range = MultiBufferRow(0)..MultiBufferRow(2);
|
||||||
|
let creases: Vec<_> = crease_snapshot.creases_in_range(range, &snapshot).collect();
|
||||||
|
assert_eq!(creases.len(), 1);
|
||||||
|
assert_eq!(creases[0].range.start.to_point(&snapshot).row, 1);
|
||||||
|
|
||||||
|
let range = MultiBufferRow(6)..MultiBufferRow(7);
|
||||||
|
let creases: Vec<_> = crease_snapshot.creases_in_range(range, &snapshot).collect();
|
||||||
|
assert_eq!(creases.len(), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue