Skip to main content

redox_core/buffer/text_buffer/
selection.rs

1//! Selection and line-span helpers for `TextBuffer`.
2//!
3//! These methods are editor-logic primitives that are independent of input.
4
5use crate::buffer::{Pos, Selection, TextBuffer};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum VisualModeKind {
9    Char,
10    Line,
11    Block,
12}
13
14/// A precomputed plan for visual selection operations.
15///
16/// This bundles mode-aware text capture and deletion bounds so callers can
17/// perform yank/delete flows without duplicating selection maths.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct VisualSelectionEditPlan {
20    pub delete_ranges: Vec<(Pos, Pos)>,
21    pub text: String,
22    pub mode: VisualModeKind,
23}
24
25impl TextBuffer {
26    /// Return a char range spanning full lines from `start_line..=end_line_inclusive`.
27    ///
28    /// The end of the range includes a trailing newline when one exists.
29    pub fn line_span_char_range(
30        &self,
31        start_line: usize,
32        end_line_inclusive: usize,
33    ) -> std::ops::Range<usize> {
34        let start_line = self.clamp_line(start_line);
35        let end_line = self.clamp_line(end_line_inclusive.max(start_line));
36        let start = self.line_to_char(start_line);
37        let end = self.line_full_end_char(end_line);
38        start..end
39    }
40
41    /// Return a position range spanning full lines from `start_line..=end_line_inclusive`.
42    pub fn line_span_pos_range(&self, start_line: usize, end_line_inclusive: usize) -> (Pos, Pos) {
43        let range = self.line_span_char_range(start_line, end_line_inclusive);
44        (self.char_to_pos(range.start), self.char_to_pos(range.end))
45    }
46
47    /// Return text spanning full lines from `start_line..=end_line_inclusive`.
48    ///
49    /// This preserves the underlying buffer content exactly.
50    pub fn line_span_text(&self, start_line: usize, end_line_inclusive: usize) -> String {
51        let range = self.line_span_char_range(start_line, end_line_inclusive);
52        self.slice_chars(range.start, range.end)
53    }
54
55    /// Return line-span text suitable for a line-wise register.
56    ///
57    /// Ensures the returned text ends with `'\n'`.
58    pub fn line_span_text_linewise_register(
59        &self,
60        start_line: usize,
61        end_line_inclusive: usize,
62    ) -> String {
63        let mut text = self.line_span_text(start_line, end_line_inclusive);
64        if !text.ends_with('\n') {
65            text.push('\n');
66        }
67        text
68    }
69
70    /// Return the charwise visual selection as an order-normalized deletion range.
71    ///
72    /// Visual charwise mode is inclusive of the cursor endpoint.
73    pub fn visual_charwise_pos_range(&self, selection: Selection) -> (Pos, Pos) {
74        let (start, end_inclusive) = selection.ordered();
75        let end_char = self.pos_to_char(end_inclusive);
76        let end_exclusive = if end_char < self.len_chars() {
77            self.char_to_pos(end_char + 1)
78        } else {
79            end_inclusive
80        };
81        (start, end_exclusive)
82    }
83
84    /// Return the linewise visual selection as a full-line deletion range.
85    pub fn visual_linewise_pos_range(&self, selection: Selection) -> (Pos, Pos) {
86        let (start, end_inclusive) = selection.ordered();
87        self.line_span_pos_range(start.line, end_inclusive.line)
88    }
89
90    /// Return visual charwise selection text (inclusive endpoint semantics).
91    pub fn visual_charwise_text(&self, selection: Selection) -> String {
92        let (start, end_exclusive) = self.visual_charwise_pos_range(selection);
93        self.slice_pos_range(start, end_exclusive)
94    }
95
96    /// Return visual linewise selection text for line-wise register semantics.
97    pub fn visual_linewise_text(&self, selection: Selection) -> String {
98        let (start, end) = selection.ordered();
99        self.line_span_text_linewise_register(start.line, end.line)
100    }
101
102    /// Return visual block selection text/deletion slices without line collapsing.
103    pub fn visual_blockwise_pos_ranges(&self, selection: Selection) -> Vec<(Pos, Pos)> {
104        let (start, end) = selection.ordered();
105        let left = start.col.min(end.col);
106        let right_exclusive = start.col.max(end.col).saturating_add(1);
107        let mut ranges = Vec::new();
108
109        for line_idx in start.line..=end.line {
110            let line_len = self.line_len_chars(line_idx);
111            let range_start = left.min(line_len);
112            let range_end = right_exclusive.min(line_len);
113            if range_start < range_end {
114                ranges.push((
115                    Pos::new(line_idx, range_start),
116                    Pos::new(line_idx, range_end),
117                ));
118            }
119        }
120
121        ranges
122    }
123
124    /// Return deletion ranges for visual block mode.
125    ///
126    /// When the block fully covers a line's content, delete the whole logical
127    /// line so trailing text is pulled upward instead of leaving empty rows.
128    pub fn visual_blockwise_delete_ranges(&self, selection: Selection) -> Vec<(Pos, Pos)> {
129        let (start, end) = selection.ordered();
130        let left = start.col.min(end.col);
131        let right_exclusive = start.col.max(end.col).saturating_add(1);
132        let mut ranges = Vec::new();
133
134        for line_idx in start.line..=end.line {
135            let line_len = self.line_len_chars(line_idx);
136            if left == 0 && right_exclusive >= line_len {
137                let full_line = self.line_span_pos_range(line_idx, line_idx);
138                if full_line.0 != full_line.1 {
139                    ranges.push(full_line);
140                }
141                continue;
142            }
143
144            let range_start = left.min(line_len);
145            let range_end = right_exclusive.min(line_len);
146            if range_start < range_end {
147                ranges.push((
148                    Pos::new(line_idx, range_start),
149                    Pos::new(line_idx, range_end),
150                ));
151            }
152        }
153
154        ranges
155    }
156
157    /// Return visual selection text for the current visual mode.
158    pub fn visual_blockwise_text(&self, selection: Selection) -> String {
159        self.visual_blockwise_pos_ranges(selection)
160            .into_iter()
161            .map(|(start, end)| self.slice_pos_range(start, end))
162            .collect::<Vec<_>>()
163            .join("\n")
164    }
165
166    /// Return visual selection deletion bounds for the current visual mode.
167    pub fn visual_selection_pos_ranges(
168        &self,
169        selection: Selection,
170        mode: VisualModeKind,
171    ) -> Vec<(Pos, Pos)> {
172        match mode {
173            VisualModeKind::Char => vec![self.visual_charwise_pos_range(selection)],
174            VisualModeKind::Line => vec![self.visual_linewise_pos_range(selection)],
175            VisualModeKind::Block => self.visual_blockwise_delete_ranges(selection),
176        }
177    }
178
179    /// Return visual selection text for the current visual mode.
180    pub fn visual_selection_text(&self, selection: Selection, mode: VisualModeKind) -> String {
181        match mode {
182            VisualModeKind::Char => self.visual_charwise_text(selection),
183            VisualModeKind::Line => self.visual_linewise_text(selection),
184            VisualModeKind::Block => self.visual_blockwise_text(selection),
185        }
186    }
187
188    /// Build a mode-aware visual selection edit plan.
189    pub fn visual_selection_edit_plan(
190        &self,
191        selection: Selection,
192        mode: VisualModeKind,
193    ) -> VisualSelectionEditPlan {
194        let delete_ranges = self.visual_selection_pos_ranges(selection, mode);
195        let text = self.visual_selection_text(selection, mode);
196        VisualSelectionEditPlan {
197            delete_ranges,
198            text,
199            mode,
200        }
201    }
202
203    /// Return selection char bounds for a specific `line_idx`.
204    ///
205    /// The returned range is half-open (`start..end`) in char-column units for
206    /// that line and follows visual-mode inclusive endpoint behaviour.
207    pub fn visual_selection_char_range_on_line(
208        &self,
209        selection: Selection,
210        mode: VisualModeKind,
211        line_idx: usize,
212    ) -> Option<std::ops::Range<usize>> {
213        let line_len = self.line_len_chars(line_idx);
214        match mode {
215            VisualModeKind::Line => {
216                let (start_line, end_line) = selection.line_range();
217                if line_idx < start_line || line_idx > end_line {
218                    return None;
219                }
220                Some(0..line_len)
221            }
222            VisualModeKind::Block => {
223                let (start, end) = selection.ordered();
224                if line_idx < start.line || line_idx > end.line {
225                    return None;
226                }
227                let left = start.col.min(end.col).min(line_len);
228                let right = start.col.max(end.col).saturating_add(1).min(line_len);
229                if left < right {
230                    Some(left..right)
231                } else {
232                    None
233                }
234            }
235            VisualModeKind::Char => {
236                let (start, end) = selection.ordered();
237                if line_idx < start.line || line_idx > end.line {
238                    return None;
239                }
240                if line_len == 0 {
241                    return None;
242                }
243
244                let max_char = line_len.saturating_sub(1);
245                let sel_start = if line_idx == start.line {
246                    start.col.min(max_char)
247                } else {
248                    0
249                };
250                let sel_end_inclusive = if line_idx == end.line {
251                    end.col.min(max_char)
252                } else {
253                    max_char
254                };
255                if sel_start > sel_end_inclusive {
256                    return None;
257                }
258
259                Some(sel_start..sel_end_inclusive.saturating_add(1))
260            }
261        }
262    }
263}