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 original_positions: HashMap<_, _> = Default::default();
62 editor.change_selections(None, window, cx, |s| {
63 s.move_with(|map, selection| {
64 let original_position = (selection.head(), selection.goal);
65 object.expand_selection(map, selection, around);
66 original_positions.insert(selection.id, original_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) = original_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 is_last_line = linewise
166 && end.row == buffer.max_row().0
167 && buffer.max_point().column > 0
168 && start.row < buffer.max_row().0
169 && start == Point::new(start.row, buffer.line_len(MultiBufferRow(start.row)));
170
171 if is_last_line {
172 start = Point::new(start.row + 1, 0);
173 }
174
175 let start_anchor = buffer.anchor_after(start);
176 let end_anchor = buffer.anchor_before(end);
177 ranges_to_highlight.push(start_anchor..end_anchor);
178
179 for chunk in buffer.text_for_range(start..end) {
180 text.push_str(chunk);
181 }
182 if is_last_line {
183 text.push('\n');
184 }
185 clipboard_selections.push(ClipboardSelection {
186 len: text.len() - initial_len,
187 is_entire_line: linewise,
188 first_line_indent: buffer.indent_size_for_line(MultiBufferRow(start.row)).len,
189 });
190 }
191 }
192
193 let selected_register = self.selected_register.take();
194 Vim::update_globals(cx, |globals, cx| {
195 globals.write_registers(
196 Register {
197 text: text.into(),
198 clipboard_selections: Some(clipboard_selections),
199 },
200 selected_register,
201 is_yank,
202 linewise,
203 cx,
204 )
205 });
206
207 let highlight_duration = VimSettings::get_global(cx).highlight_on_yank_duration;
208 if !is_yank || self.mode == Mode::Visual || highlight_duration == 0 {
209 return;
210 }
211
212 editor.highlight_background::<HighlightOnYank>(
213 &ranges_to_highlight,
214 |colors| colors.editor_document_highlight_read_background,
215 cx,
216 );
217 cx.spawn(|this, mut cx| async move {
218 cx.background_executor()
219 .timer(Duration::from_millis(highlight_duration))
220 .await;
221 this.update(&mut cx, |editor, cx| {
222 editor.clear_background_highlights::<HighlightOnYank>(cx)
223 })
224 .ok();
225 })
226 .detach();
227 }
228}