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/// A precomputed plan for visual selection operations.
8///
9/// This bundles mode-aware text capture and deletion bounds so callers can
10/// perform yank/delete flows without duplicating selection maths.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct VisualSelectionEditPlan {
13    pub delete_start: Pos,
14    pub delete_end: Pos,
15    pub text: String,
16    pub line_mode: bool,
17}
18
19impl TextBuffer {
20    /// Return a char range spanning full lines from `start_line..=end_line_inclusive`.
21    ///
22    /// The end of the range includes a trailing newline when one exists.
23    pub fn line_span_char_range(
24        &self,
25        start_line: usize,
26        end_line_inclusive: usize,
27    ) -> std::ops::Range<usize> {
28        let start_line = self.clamp_line(start_line);
29        let end_line = self.clamp_line(end_line_inclusive.max(start_line));
30        let start = self.line_to_char(start_line);
31        let end = self.line_full_end_char(end_line);
32        start..end
33    }
34
35    /// Return a position range spanning full lines from `start_line..=end_line_inclusive`.
36    pub fn line_span_pos_range(&self, start_line: usize, end_line_inclusive: usize) -> (Pos, Pos) {
37        let range = self.line_span_char_range(start_line, end_line_inclusive);
38        (self.char_to_pos(range.start), self.char_to_pos(range.end))
39    }
40
41    /// Return text spanning full lines from `start_line..=end_line_inclusive`.
42    ///
43    /// This preserves the underlying buffer content exactly.
44    pub fn line_span_text(&self, start_line: usize, end_line_inclusive: usize) -> String {
45        let range = self.line_span_char_range(start_line, end_line_inclusive);
46        self.slice_chars(range.start, range.end)
47    }
48
49    /// Return line-span text suitable for a line-wise register.
50    ///
51    /// Ensures the returned text ends with `'\n'`.
52    pub fn line_span_text_linewise_register(
53        &self,
54        start_line: usize,
55        end_line_inclusive: usize,
56    ) -> String {
57        let mut text = self.line_span_text(start_line, end_line_inclusive);
58        if !text.ends_with('\n') {
59            text.push('\n');
60        }
61        text
62    }
63
64    /// Return the charwise visual selection as an order-normalized deletion range.
65    ///
66    /// Visual charwise mode is inclusive of the cursor endpoint.
67    pub fn visual_charwise_pos_range(&self, selection: Selection) -> (Pos, Pos) {
68        let (start, end_inclusive) = selection.ordered();
69        let end_char = self.pos_to_char(end_inclusive);
70        let end_exclusive = if end_char < self.len_chars() {
71            self.char_to_pos(end_char + 1)
72        } else {
73            end_inclusive
74        };
75        (start, end_exclusive)
76    }
77
78    /// Return the linewise visual selection as a full-line deletion range.
79    pub fn visual_linewise_pos_range(&self, selection: Selection) -> (Pos, Pos) {
80        let (start, end_inclusive) = selection.ordered();
81        self.line_span_pos_range(start.line, end_inclusive.line)
82    }
83
84    /// Return visual charwise selection text (inclusive endpoint semantics).
85    pub fn visual_charwise_text(&self, selection: Selection) -> String {
86        let (start, end_exclusive) = self.visual_charwise_pos_range(selection);
87        self.slice_pos_range(start, end_exclusive)
88    }
89
90    /// Return visual linewise selection text for line-wise register semantics.
91    pub fn visual_linewise_text(&self, selection: Selection) -> String {
92        let (start, end) = selection.ordered();
93        self.line_span_text_linewise_register(start.line, end.line)
94    }
95
96    /// Return visual selection deletion bounds for the current visual mode.
97    pub fn visual_selection_pos_range(&self, selection: Selection, line_mode: bool) -> (Pos, Pos) {
98        if line_mode {
99            self.visual_linewise_pos_range(selection)
100        } else {
101            self.visual_charwise_pos_range(selection)
102        }
103    }
104
105    /// Return visual selection text for the current visual mode.
106    pub fn visual_selection_text(&self, selection: Selection, line_mode: bool) -> String {
107        if line_mode {
108            self.visual_linewise_text(selection)
109        } else {
110            self.visual_charwise_text(selection)
111        }
112    }
113
114    /// Build a mode-aware visual selection edit plan.
115    pub fn visual_selection_edit_plan(
116        &self,
117        selection: Selection,
118        line_mode: bool,
119    ) -> VisualSelectionEditPlan {
120        let (delete_start, delete_end) = self.visual_selection_pos_range(selection, line_mode);
121        let text = self.visual_selection_text(selection, line_mode);
122        VisualSelectionEditPlan {
123            delete_start,
124            delete_end,
125            text,
126            line_mode,
127        }
128    }
129
130    /// Return selection char bounds for a specific `line_idx`.
131    ///
132    /// The returned range is half-open (`start..end`) in char-column units for
133    /// that line and follows visual-mode inclusive endpoint behaviour.
134    pub fn visual_selection_char_range_on_line(
135        &self,
136        selection: Selection,
137        line_mode: bool,
138        line_idx: usize,
139    ) -> Option<std::ops::Range<usize>> {
140        let line_len = self.line_len_chars(line_idx);
141        if line_mode {
142            let (start_line, end_line) = selection.line_range();
143            if line_idx < start_line || line_idx > end_line {
144                return None;
145            }
146            return Some(0..line_len);
147        }
148
149        let (start, end) = selection.ordered();
150        if line_idx < start.line || line_idx > end.line {
151            return None;
152        }
153        if line_len == 0 {
154            return None;
155        }
156
157        let max_char = line_len.saturating_sub(1);
158        let sel_start = if line_idx == start.line {
159            start.col.min(max_char)
160        } else {
161            0
162        };
163        let sel_end_inclusive = if line_idx == end.line {
164            end.col.min(max_char)
165        } else {
166            max_char
167        };
168        if sel_start > sel_end_inclusive {
169            return None;
170        }
171
172        Some(sel_start..sel_end_inclusive.saturating_add(1))
173    }
174}