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(), window, 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, window, 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        window: &mut Window,
 86        cx: &mut Context<Editor>,
 87    ) {
 88        self.copy_ranges(
 89            editor,
 90            linewise,
 91            true,
 92            editor
 93                .selections
 94                .all_adjusted(cx)
 95                .iter()
 96                .map(|s| s.range())
 97                .collect(),
 98            window,
 99            cx,
100        )
101    }
102
103    pub fn copy_selections_content(
104        &mut self,
105        editor: &mut Editor,
106        linewise: bool,
107        window: &mut Window,
108        cx: &mut Context<Editor>,
109    ) {
110        self.copy_ranges(
111            editor,
112            linewise,
113            false,
114            editor
115                .selections
116                .all_adjusted(cx)
117                .iter()
118                .map(|s| s.range())
119                .collect(),
120            window,
121            cx,
122        )
123    }
124
125    pub(crate) fn copy_ranges(
126        &mut self,
127        editor: &mut Editor,
128        linewise: bool,
129        is_yank: bool,
130        selections: Vec<Range<Point>>,
131        window: &mut Window,
132        cx: &mut Context<Editor>,
133    ) {
134        let buffer = editor.buffer().read(cx).snapshot(cx);
135        self.set_mark(
136            "[".to_string(),
137            selections
138                .iter()
139                .map(|s| buffer.anchor_before(s.start))
140                .collect(),
141            editor.buffer(),
142            window,
143            cx,
144        );
145        self.set_mark(
146            "]".to_string(),
147            selections
148                .iter()
149                .map(|s| buffer.anchor_after(s.end))
150                .collect(),
151            editor.buffer(),
152            window,
153            cx,
154        );
155
156        let mut text = String::new();
157        let mut clipboard_selections = Vec::with_capacity(selections.len());
158        let mut ranges_to_highlight = Vec::new();
159
160        {
161            let mut is_first = true;
162            for selection in selections.iter() {
163                let mut start = selection.start;
164                let end = selection.end;
165                if is_first {
166                    is_first = false;
167                } else {
168                    text.push('\n');
169                }
170                let initial_len = text.len();
171
172                // if the file does not end with \n, and our line-mode selection ends on
173                // that line, we will have expanded the start of the selection to ensure it
174                // contains a newline (so that delete works as expected). We undo that change
175                // here.
176                let max_point = buffer.max_point();
177                let should_adjust_start = linewise
178                    && end.row == max_point.row
179                    && max_point.column > 0
180                    && start.row < max_point.row
181                    && start == Point::new(start.row, buffer.line_len(MultiBufferRow(start.row)));
182                let should_add_newline =
183                    should_adjust_start || (end == max_point && max_point.column > 0 && linewise);
184
185                if should_adjust_start {
186                    start = Point::new(start.row + 1, 0);
187                }
188
189                let start_anchor = buffer.anchor_after(start);
190                let end_anchor = buffer.anchor_before(end);
191                ranges_to_highlight.push(start_anchor..end_anchor);
192
193                for chunk in buffer.text_for_range(start..end) {
194                    text.push_str(chunk);
195                }
196                if should_add_newline {
197                    text.push('\n');
198                }
199                clipboard_selections.push(ClipboardSelection {
200                    len: text.len() - initial_len,
201                    is_entire_line: linewise,
202                    first_line_indent: buffer.indent_size_for_line(MultiBufferRow(start.row)).len,
203                });
204            }
205        }
206
207        let selected_register = self.selected_register.take();
208        Vim::update_globals(cx, |globals, cx| {
209            globals.write_registers(
210                Register {
211                    text: text.into(),
212                    clipboard_selections: Some(clipboard_selections),
213                },
214                selected_register,
215                is_yank,
216                linewise,
217                cx,
218            )
219        });
220
221        let highlight_duration = VimSettings::get_global(cx).highlight_on_yank_duration;
222        if !is_yank || self.mode == Mode::Visual || highlight_duration == 0 {
223            return;
224        }
225
226        editor.highlight_background::<HighlightOnYank>(
227            &ranges_to_highlight,
228            |colors| colors.editor_document_highlight_read_background,
229            cx,
230        );
231        cx.spawn(async move |this, cx| {
232            cx.background_executor()
233                .timer(Duration::from_millis(highlight_duration))
234                .await;
235            this.update(cx, |editor, cx| {
236                editor.clear_background_highlights::<HighlightOnYank>(cx)
237            })
238            .ok();
239        })
240        .detach();
241    }
242}