yank.rs

  1use std::{ops::Range, time::Duration};
  2
  3use crate::{
  4    motion::Motion,
  5    object::Object,
  6    state::{Mode, Register},
  7    Vim, VimSettings,
  8};
  9use collections::HashMap;
 10use editor::{ClipboardSelection, Editor};
 11use gpui::Context;
 12use gpui::Window;
 13use language::Point;
 14use multi_buffer::MultiBufferRow;
 15use settings::Settings;
 16
 17struct HighlightOnYank;
 18
 19impl Vim {
 20    pub fn yank_motion(
 21        &mut self,
 22        motion: Motion,
 23        times: Option<usize>,
 24        window: &mut Window,
 25        cx: &mut Context<Self>,
 26    ) {
 27        self.update_editor(window, cx, |vim, editor, window, cx| {
 28            let text_layout_details = editor.text_layout_details(window);
 29            editor.transact(window, cx, |editor, window, cx| {
 30                editor.set_clip_at_line_ends(false, cx);
 31                let mut original_positions: HashMap<_, _> = Default::default();
 32                editor.change_selections(None, window, cx, |s| {
 33                    s.move_with(|map, selection| {
 34                        let original_position = (selection.head(), selection.goal);
 35                        original_positions.insert(selection.id, original_position);
 36                        motion.expand_selection(map, selection, times, true, &text_layout_details);
 37                    });
 38                });
 39                vim.yank_selections_content(editor, motion.linewise(), cx);
 40                editor.change_selections(None, window, cx, |s| {
 41                    s.move_with(|_, selection| {
 42                        let (head, goal) = original_positions.remove(&selection.id).unwrap();
 43                        selection.collapse_to(head, goal);
 44                    });
 45                });
 46            });
 47        });
 48        self.exit_temporary_normal(window, cx);
 49    }
 50
 51    pub fn yank_object(
 52        &mut self,
 53        object: Object,
 54        around: bool,
 55        window: &mut Window,
 56        cx: &mut Context<Self>,
 57    ) {
 58        self.update_editor(window, cx, |vim, editor, window, cx| {
 59            editor.transact(window, cx, |editor, window, cx| {
 60                editor.set_clip_at_line_ends(false, cx);
 61                let mut start_positions: HashMap<_, _> = Default::default();
 62                editor.change_selections(None, window, cx, |s| {
 63                    s.move_with(|map, selection| {
 64                        object.expand_selection(map, selection, around);
 65                        let start_position = (selection.start, selection.goal);
 66                        start_positions.insert(selection.id, start_position);
 67                    });
 68                });
 69                vim.yank_selections_content(editor, false, cx);
 70                editor.change_selections(None, window, cx, |s| {
 71                    s.move_with(|_, selection| {
 72                        let (head, goal) = start_positions.remove(&selection.id).unwrap();
 73                        selection.collapse_to(head, goal);
 74                    });
 75                });
 76            });
 77        });
 78        self.exit_temporary_normal(window, cx);
 79    }
 80
 81    pub fn yank_selections_content(
 82        &mut self,
 83        editor: &mut Editor,
 84        linewise: bool,
 85        cx: &mut Context<Editor>,
 86    ) {
 87        self.copy_ranges(
 88            editor,
 89            linewise,
 90            true,
 91            editor
 92                .selections
 93                .all_adjusted(cx)
 94                .iter()
 95                .map(|s| s.range())
 96                .collect(),
 97            cx,
 98        )
 99    }
100
101    pub fn copy_selections_content(
102        &mut self,
103        editor: &mut Editor,
104        linewise: bool,
105        cx: &mut Context<Editor>,
106    ) {
107        self.copy_ranges(
108            editor,
109            linewise,
110            false,
111            editor
112                .selections
113                .all_adjusted(cx)
114                .iter()
115                .map(|s| s.range())
116                .collect(),
117            cx,
118        )
119    }
120
121    pub(crate) fn copy_ranges(
122        &mut self,
123        editor: &mut Editor,
124        linewise: bool,
125        is_yank: bool,
126        selections: Vec<Range<Point>>,
127        cx: &mut Context<Editor>,
128    ) {
129        let buffer = editor.buffer().read(cx).snapshot(cx);
130        let mut text = String::new();
131        let mut clipboard_selections = Vec::with_capacity(selections.len());
132        let mut ranges_to_highlight = Vec::new();
133
134        self.marks.insert(
135            "[".to_string(),
136            selections
137                .iter()
138                .map(|s| buffer.anchor_before(s.start))
139                .collect(),
140        );
141        self.marks.insert(
142            "]".to_string(),
143            selections
144                .iter()
145                .map(|s| buffer.anchor_after(s.end))
146                .collect(),
147        );
148
149        {
150            let mut is_first = true;
151            for selection in selections.iter() {
152                let mut start = selection.start;
153                let end = selection.end;
154                if is_first {
155                    is_first = false;
156                } else {
157                    text.push('\n');
158                }
159                let initial_len = text.len();
160
161                // if the file does not end with \n, and our line-mode selection ends on
162                // that line, we will have expanded the start of the selection to ensure it
163                // contains a newline (so that delete works as expected). We undo that change
164                // here.
165                let max_point = buffer.max_point();
166                let should_adjust_start = linewise
167                    && end.row == max_point.row
168                    && max_point.column > 0
169                    && start.row < max_point.row
170                    && start == Point::new(start.row, buffer.line_len(MultiBufferRow(start.row)));
171                let should_add_newline =
172                    should_adjust_start || (end == max_point && max_point.column > 0 && linewise);
173
174                if should_adjust_start {
175                    start = Point::new(start.row + 1, 0);
176                }
177
178                let start_anchor = buffer.anchor_after(start);
179                let end_anchor = buffer.anchor_before(end);
180                ranges_to_highlight.push(start_anchor..end_anchor);
181
182                for chunk in buffer.text_for_range(start..end) {
183                    text.push_str(chunk);
184                }
185                if should_add_newline {
186                    text.push('\n');
187                }
188                clipboard_selections.push(ClipboardSelection {
189                    len: text.len() - initial_len,
190                    is_entire_line: linewise,
191                    first_line_indent: buffer.indent_size_for_line(MultiBufferRow(start.row)).len,
192                });
193            }
194        }
195
196        let selected_register = self.selected_register.take();
197        Vim::update_globals(cx, |globals, cx| {
198            globals.write_registers(
199                Register {
200                    text: text.into(),
201                    clipboard_selections: Some(clipboard_selections),
202                },
203                selected_register,
204                is_yank,
205                linewise,
206                cx,
207            )
208        });
209
210        let highlight_duration = VimSettings::get_global(cx).highlight_on_yank_duration;
211        if !is_yank || self.mode == Mode::Visual || highlight_duration == 0 {
212            return;
213        }
214
215        editor.highlight_background::<HighlightOnYank>(
216            &ranges_to_highlight,
217            |colors| colors.editor_document_highlight_read_background,
218            cx,
219        );
220        cx.spawn(|this, mut cx| async move {
221            cx.background_executor()
222                .timer(Duration::from_millis(highlight_duration))
223                .await;
224            this.update(&mut cx, |editor, cx| {
225                editor.clear_background_highlights::<HighlightOnYank>(cx)
226            })
227            .ok();
228        })
229        .detach();
230    }
231}