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}