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