diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index fd5f62e1881f99fa4fa6a624b35480f6f9b385b7..0828b9b9916725842aa2f70a56d3ae5a6e7abcfc 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -26,8 +26,8 @@ use collections::{BTreeSet, HashMap, HashSet}; use editor::{ actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt}, display_map::{ - BlockDisposition, BlockId, BlockProperties, BlockStyle, Crease, CustomBlockId, FoldId, - RenderBlock, ToDisplayPoint, + BlockDisposition, BlockId, BlockProperties, BlockStyle, Crease, CreaseMetadata, + CustomBlockId, FoldId, RenderBlock, ToDisplayPoint, }, scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor}, Anchor, Editor, EditorEvent, ExcerptRange, MultiBuffer, RowExt, ToOffset as _, ToPoint, @@ -54,13 +54,13 @@ use multi_buffer::MultiBufferRow; use picker::{Picker, PickerDelegate}; use project::{Project, ProjectLspAdapterDelegate, Worktree}; use search::{buffer_search::DivRegistrar, BufferSearchBar}; +use serde::{Deserialize, Serialize}; use settings::{update_settings_file, Settings}; use smol::stream::StreamExt; use std::{ borrow::Cow, cmp, collections::hash_map, - fmt::Write, ops::{ControlFlow, Range}, path::PathBuf, sync::Arc, @@ -2491,20 +2491,26 @@ impl ContextEditor { .unwrap(); let buffer_row = MultiBufferRow(start.to_point(&buffer).row); buffer_rows_to_fold.insert(buffer_row); - creases.push(Crease::new( - start..end, - FoldPlaceholder { - render: render_fold_icon_button( - cx.view().downgrade(), - section.icon, - section.label.clone(), - ), - constrain_width: false, - merge_adjacent: false, - }, - render_slash_command_output_toggle, - |_, _, _| Empty.into_any_element(), - )); + creases.push( + Crease::new( + start..end, + FoldPlaceholder { + render: render_fold_icon_button( + cx.view().downgrade(), + section.icon, + section.label.clone(), + ), + constrain_width: false, + merge_adjacent: false, + }, + render_slash_command_output_toggle, + |_, _, _| Empty.into_any_element(), + ) + .with_metadata(CreaseMetadata { + icon: section.icon, + label: section.label, + }), + ); } editor.insert_creases(creases, cx); @@ -3318,39 +3324,113 @@ impl ContextEditor { } fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext) { - let editor = self.editor.read(cx); - let context = self.context.read(cx); - if editor.selections.count() == 1 { - let selection = editor.selections.newest::(cx); - let mut copied_text = String::new(); - let mut spanned_messages = 0; - for message in context.messages(cx) { - if message.offset_range.start >= selection.range().end { - break; - } else if message.offset_range.end >= selection.range().start { - let range = cmp::max(message.offset_range.start, selection.range().start) - ..cmp::min(message.offset_range.end, selection.range().end); - 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) { - copied_text.push_str(chunk); + 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) { + 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::(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) -> (String, CopyMetadata) { + let creases = self.editor.update(cx, |editor, cx| { + let selection = editor.selections.newest::(cx); + let selection_start = editor.selections.newest::(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 } - copied_text.push('\n'); + }) + .collect::>() + }) + }); + + let context = self.context.read(cx); + let selection = self.editor.read(cx).selections.newest::(cx); + let mut text = String::new(); + for message in context.messages(cx) { + if message.offset_range.start >= selection.range().end { + break; + } else if message.offset_range.end >= selection.range().start { + let range = cmp::max(message.offset_range.start, selection.range().start) + ..cmp::min(message.offset_range.end, selection.range().end); + if !range.is_empty() { + for chunk in context.buffer().read(cx).text_for_range(range) { + text.push_str(chunk); } + text.push('\n'); } } - - if spanned_messages > 1 { - cx.write_to_clipboard(ClipboardItem::new_string(copied_text)); - return; - } } - cx.propagate(); + (text, CopyMetadata { creases }) } - fn paste(&mut self, _: &editor::actions::Paste, cx: &mut ViewContext) { + fn paste(&mut self, action: &editor::actions::Paste, cx: &mut ViewContext) { + cx.stop_propagation(); + let images = if let Some(item) = cx.read_from_clipboard() { item.into_entries() .filter_map(|entry| { @@ -3365,9 +3445,62 @@ impl ContextEditor { 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::() + } else { + None + } + }) + } else { + None + }; + if images.is_empty() { - // If we didn't find any valid image data to paste, propagate to let normal pasting happen. - cx.propagate(); + self.editor.update(cx, |editor, cx| { + let paste_position = editor.selections.newest::(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 { let mut image_positions = Vec::new(); self.editor.update(cx, |editor, cx| { @@ -4037,6 +4170,17 @@ fn render_fold_icon_button( }) } +#[derive(Debug, Clone, Serialize, Deserialize)] +struct CopyMetadata { + creases: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SelectedCreaseMetadata { + range_relative_to_selection: Range, + crease: CreaseMetadata, +} + impl EventEmitter for ContextEditor {} impl EventEmitter for ContextEditor {} @@ -4062,6 +4206,7 @@ impl Render for ContextEditor { .capture_action(cx.listener(ContextEditor::cancel)) .capture_action(cx.listener(ContextEditor::save)) .capture_action(cx.listener(ContextEditor::copy)) + .capture_action(cx.listener(ContextEditor::cut)) .capture_action(cx.listener(ContextEditor::paste)) .capture_action(cx.listener(ContextEditor::cycle_message_role)) .capture_action(cx.listener(ContextEditor::confirm_command)) diff --git a/crates/editor/src/display_map/crease_map.rs b/crates/editor/src/display_map/crease_map.rs index 9aa2728dca8c451af67c0de53d5920aad79e4f01..10ee125b3237ac830dfb80c537acc43722c67880 100644 --- a/crates/editor/src/display_map/crease_map.rs +++ b/crates/editor/src/display_map/crease_map.rs @@ -1,10 +1,11 @@ use collections::HashMap; use gpui::{AnyElement, IntoElement}; use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToPoint}; +use serde::{Deserialize, Serialize}; use std::{cmp::Ordering, ops::Range, sync::Arc}; use sum_tree::{Bias, SeekTarget, SumTree}; use text::Point; -use ui::WindowContext; +use ui::{IconName, SharedString, WindowContext}; use crate::FoldPlaceholder; @@ -49,6 +50,31 @@ impl CreaseSnapshot { None } + pub fn creases_in_range<'a>( + &'a self, + range: Range, + snapshot: &'a MultiBufferSnapshot, + ) -> impl '_ + Iterator { + let start = snapshot.anchor_before(Point::new(range.start.0, 0)); + let mut cursor = self.creases.cursor::(); + 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( &self, snapshot: &MultiBufferSnapshot, @@ -87,6 +113,14 @@ pub struct Crease { pub placeholder: FoldPlaceholder, pub render_toggle: RenderToggleFn, pub render_trailer: RenderTrailerFn, + pub metadata: Option, +} + +/// 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 { @@ -124,8 +158,14 @@ impl Crease { render_trailer: Arc::new(move |row, folded, cx| { 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 { @@ -304,4 +344,54 @@ mod test { .query_row(MultiBufferRow(3), &snapshot) .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); + } }