Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:40:19.235Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

duplicate.rs

357 lines · 10.1 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.