Skip to repository content357 lines · 10.1 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:40:19.235Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
duplicate.rs
1use std::ops::Range;
2
3use editor::{DisplayPoint, MultiBufferOffset, display_map::DisplaySnapshot};
4use gpui::Context;
5use language::PointUtf16;
6use multi_buffer::{MultiBufferPoint, MultiBufferRow};
7use text::Bias;
8use ui::Window;
9
10use crate::Vim;
11
12#[derive(Copy, Clone)]
13enum Direction {
14 Above,
15 Below,
16}
17
18impl Vim {
19 /// Creates a duplicate of every selection below it in the first place that has both its start
20 /// and end
21 pub(super) fn helix_duplicate_selections_below(
22 &mut self,
23 times: Option<usize>,
24 window: &mut Window,
25 cx: &mut Context<Self>,
26 ) {
27 self.duplicate_selections(times, window, cx, Direction::Below);
28 }
29
30 /// Creates a duplicate of every selection above it in the first place that has both its start
31 /// and end
32 pub(super) fn helix_duplicate_selections_above(
33 &mut self,
34 times: Option<usize>,
35 window: &mut Window,
36 cx: &mut Context<Self>,
37 ) {
38 self.duplicate_selections(times, window, cx, Direction::Above);
39 }
40
41 fn duplicate_selections(
42 &mut self,
43 times: Option<usize>,
44 window: &mut Window,
45 cx: &mut Context<Self>,
46 direction: Direction,
47 ) {
48 let times = times.unwrap_or(1);
49 self.update_editor(cx, |_, editor, cx| {
50 let mut selections = Vec::new();
51 let map = editor.display_snapshot(cx);
52 let mut original_selections = editor.selections.all_display(&map);
53 // The order matters, because it is recorded when the selections are added.
54 if matches!(direction, Direction::Above) {
55 original_selections.reverse();
56 }
57
58 for origin in original_selections {
59 let origin = origin.tail()..origin.head();
60 selections.push(display_point_range_to_offset_range(&origin, &map));
61 let mut last_origin = origin;
62 for _ in 1..=times {
63 if let Some(duplicate) =
64 find_next_valid_duplicate_space(last_origin.clone(), &map, direction)
65 {
66 selections.push(display_point_range_to_offset_range(&duplicate, &map));
67 last_origin = duplicate;
68 } else {
69 break;
70 }
71 }
72 }
73
74 editor.change_selections(Default::default(), window, cx, |s| {
75 s.select_ranges(selections);
76 });
77 });
78 }
79}
80
81fn find_next_valid_duplicate_space(
82 origin: Range<DisplayPoint>,
83 map: &DisplaySnapshot,
84 direction: Direction,
85) -> Option<Range<DisplayPoint>> {
86 let buffer = map.buffer_snapshot();
87 let start_col_utf16 = buffer
88 .point_to_point_utf16(origin.start.to_point(map))
89 .column;
90 let end_col_utf16 = buffer.point_to_point_utf16(origin.end.to_point(map)).column;
91
92 let mut candidate = origin;
93 loop {
94 match direction {
95 Direction::Below => {
96 if candidate.end.row() >= map.max_point().row() {
97 return None;
98 }
99 *candidate.start.row_mut() += 1;
100 *candidate.end.row_mut() += 1;
101 }
102 Direction::Above => {
103 if candidate.start.row() == DisplayPoint::zero().row() {
104 return None;
105 }
106 *candidate.start.row_mut() = candidate.start.row().0.saturating_sub(1);
107 *candidate.end.row_mut() = candidate.end.row().0.saturating_sub(1);
108 }
109 }
110
111 let start_row = DisplayPoint::new(candidate.start.row(), 0)
112 .to_point(map)
113 .row;
114 let end_row = DisplayPoint::new(candidate.end.row(), 0).to_point(map).row;
115
116 if start_col_utf16 > buffer.line_len_utf16(MultiBufferRow(start_row))
117 || end_col_utf16 > buffer.line_len_utf16(MultiBufferRow(end_row))
118 {
119 continue;
120 }
121
122 let start_col = buffer
123 .point_utf16_to_point(PointUtf16::new(start_row, start_col_utf16))
124 .column;
125 let end_col = buffer
126 .point_utf16_to_point(PointUtf16::new(end_row, end_col_utf16))
127 .column;
128
129 let candidate_start =
130 map.point_to_display_point(MultiBufferPoint::new(start_row, start_col), Bias::Left);
131 let candidate_end =
132 map.point_to_display_point(MultiBufferPoint::new(end_row, end_col), Bias::Right);
133
134 if map.clip_point(candidate_start, Bias::Left) == candidate_start
135 && map.clip_point(candidate_end, Bias::Right) == candidate_end
136 {
137 return Some(candidate_start..candidate_end);
138 }
139 }
140}
141
142fn display_point_range_to_offset_range(
143 range: &Range<DisplayPoint>,
144 map: &DisplaySnapshot,
145) -> Range<MultiBufferOffset> {
146 range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right)
147}
148
149#[cfg(test)]
150mod tests {
151 use db::indoc;
152 use editor::{Inlay, MultiBufferOffset};
153
154 use crate::{state::Mode, test::VimTestContext};
155
156 #[gpui::test]
157 async fn test_selection_duplication(cx: &mut gpui::TestAppContext) {
158 let mut cx = VimTestContext::new(cx, true).await;
159 cx.enable_helix();
160
161 cx.set_state(
162 indoc! {"
163 The quick brown
164 fox «jumpsˇ»
165 over the
166 lazy dog."},
167 Mode::HelixNormal,
168 );
169
170 cx.simulate_keystrokes("C");
171
172 cx.assert_state(
173 indoc! {"
174 The quick brown
175 fox «jumpsˇ»
176 over the
177 lazy« dog.ˇ»"},
178 Mode::HelixNormal,
179 );
180
181 cx.simulate_keystrokes("C");
182
183 cx.assert_state(
184 indoc! {"
185 The quick brown
186 fox «jumpsˇ»
187 over the
188 lazy« dog.ˇ»"},
189 Mode::HelixNormal,
190 );
191
192 cx.simulate_keystrokes("alt-C");
193
194 cx.assert_state(
195 indoc! {"
196 The «quickˇ» brown
197 fox «jumpsˇ»
198 over the
199 lazy« dog.ˇ»"},
200 Mode::HelixNormal,
201 );
202
203 cx.simulate_keystrokes(",");
204
205 cx.assert_state(
206 indoc! {"
207 The «quickˇ» brown
208 fox jumps
209 over the
210 lazy dog."},
211 Mode::HelixNormal,
212 );
213 }
214
215 #[gpui::test]
216 async fn test_selection_duplication_backwards(cx: &mut gpui::TestAppContext) {
217 let mut cx = VimTestContext::new(cx, true).await;
218 cx.enable_helix();
219
220 cx.set_state(
221 indoc! {"
222 The quick brown
223 «ˇfox» jumps
224 over the
225 lazy dog."},
226 Mode::HelixNormal,
227 );
228
229 cx.simulate_keystrokes("C C alt-C");
230
231 cx.assert_state(
232 indoc! {"
233 «ˇThe» quick brown
234 «ˇfox» jumps
235 «ˇove»r the
236 «ˇlaz»y dog."},
237 Mode::HelixNormal,
238 );
239 }
240
241 #[gpui::test]
242 async fn test_selection_duplication_count(cx: &mut gpui::TestAppContext) {
243 let mut cx = VimTestContext::new(cx, true).await;
244 cx.enable_helix();
245
246 cx.set_state(
247 indoc! {"
248 The «qˇ»uick brown
249 fox jumps
250 over the
251 lazy dog."},
252 Mode::HelixNormal,
253 );
254
255 cx.simulate_keystrokes("9 C");
256
257 cx.assert_state(
258 indoc! {"
259 The «qˇ»uick brown
260 fox «jˇ»umps
261 over« ˇ»the
262 lazy« ˇ»dog."},
263 Mode::HelixNormal,
264 );
265 }
266
267 #[gpui::test]
268 async fn test_selection_duplication_multiline_multibyte(cx: &mut gpui::TestAppContext) {
269 let mut cx = VimTestContext::new(cx, true).await;
270 cx.enable_helix();
271
272 // Multiline selection on rows with multibyte chars should preserve
273 // the visual column on both start and end rows.
274 cx.set_state(
275 indoc! {"
276 «H䡻llo
277 Hëllo
278 Hallo"},
279 Mode::HelixNormal,
280 );
281
282 cx.simulate_keystrokes("C");
283
284 cx.assert_state(
285 indoc! {"
286 «H䡻llo
287 «H롻llo
288 Hallo"},
289 Mode::HelixNormal,
290 );
291 }
292
293 #[gpui::test]
294 async fn test_selection_duplication_multibyte(cx: &mut gpui::TestAppContext) {
295 let mut cx = VimTestContext::new(cx, true).await;
296 cx.enable_helix();
297
298 // Selection on a line with multibyte chars should duplicate to the
299 // same character column on the next line, not skip it.
300 cx.set_state(
301 indoc! {"
302 H«äˇ»llo
303 Hallo"},
304 Mode::HelixNormal,
305 );
306
307 cx.simulate_keystrokes("C");
308
309 cx.assert_state(
310 indoc! {"
311 H«äˇ»llo
312 H«aˇ»llo"},
313 Mode::HelixNormal,
314 );
315 }
316
317 #[gpui::test]
318 async fn test_selection_duplication_with_inlay_hints(cx: &mut gpui::TestAppContext) {
319 let mut cx = VimTestContext::new(cx, true).await;
320 cx.enable_helix();
321
322 cx.set_state(
323 indoc! {"
324 let x = «1ˇ»;
325 let y = 2;"},
326 Mode::HelixNormal,
327 );
328
329 cx.update_editor(|editor, window, cx| {
330 let buffer = &editor.snapshot(window, cx).buffer;
331 editor.splice_inlays(
332 &[],
333 vec![
334 Inlay::mock_hint(0, buffer.anchor_after(MultiBufferOffset(5)), ": i32"),
335 Inlay::mock_hint(1, buffer.anchor_after(MultiBufferOffset(16)), ": i32"),
336 ],
337 cx,
338 );
339 });
340
341 cx.simulate_keystrokes("C");
342
343 assert_eq!(
344 cx.display_text(),
345 "let x: i32 = 1;
346let y: i32 = 2;",
347 );
348
349 cx.assert_state(
350 indoc! {"
351 let x = «1ˇ»;
352 let y = «2ˇ»;"},
353 Mode::HelixNormal,
354 );
355 }
356}
357