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}