Skip to main content

redox_core/buffer/text_buffer/
text_objects.rs

1//! Text-object resolution for operator-pending commands.
2//!
3//! These helpers resolve reusable targets like `iw`, `ap`, and `i]` into
4//! mode-aware edit plans so delete/change/yank can share the same core logic.
5
6use super::TextBuffer;
7use super::selection::VisualModeKind;
8use crate::buffer::util::is_word_char;
9use crate::buffer::{Pos, Selection};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum TextObjectScope {
13    Inner,
14    Around,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum DelimiterKind {
19    Parentheses,
20    Brackets,
21    Braces,
22    SingleQuotes,
23    DoubleQuotes,
24    Backticks,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum TextObjectKind {
29    Word,
30    BigWord,
31    Paragraph,
32    Delimiter(DelimiterKind),
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct TextObjectSpec {
37    pub scope: TextObjectScope,
38    pub kind: TextObjectKind,
39    pub count: usize,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct TextObjectEditPlan {
44    pub delete_ranges: Vec<(Pos, Pos)>,
45    pub text: String,
46    pub mode: VisualModeKind,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50enum RangeMode {
51    Char,
52    Line,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56struct TextObjectRange {
57    start: usize,
58    end: usize,
59    mode: RangeMode,
60}
61
62impl TextBuffer {
63    pub fn text_object_selection(
64        &self,
65        cursor: Pos,
66        spec: TextObjectSpec,
67    ) -> Option<(Selection, VisualModeKind)> {
68        let range = self.text_object_range(cursor, spec)?;
69
70        match range.mode {
71            RangeMode::Char => {
72                if range.start >= range.end {
73                    return None;
74                }
75
76                Some((
77                    Selection::new(
78                        self.char_to_pos(range.start),
79                        self.char_to_pos(range.end.saturating_sub(1)),
80                    ),
81                    VisualModeKind::Char,
82                ))
83            }
84            RangeMode::Line => {
85                let start_line = self.char_to_line(range.start);
86                let end_line = self.char_to_line(range.end.saturating_sub(1));
87                Some((
88                    Selection::new(Pos::new(start_line, 0), Pos::new(end_line, 0)),
89                    VisualModeKind::Line,
90                ))
91            }
92        }
93    }
94
95    pub fn text_object_edit_plan(
96        &self,
97        cursor: Pos,
98        spec: TextObjectSpec,
99    ) -> Option<TextObjectEditPlan> {
100        let range = self.text_object_range(cursor, spec)?;
101
102        Some(match range.mode {
103            RangeMode::Char => TextObjectEditPlan {
104                delete_ranges: vec![(self.char_to_pos(range.start), self.char_to_pos(range.end))],
105                text: self.slice_chars(range.start, range.end),
106                mode: VisualModeKind::Char,
107            },
108            RangeMode::Line => {
109                let start_line = self.char_to_line(range.start);
110                let end_line = self.char_to_line(range.end.saturating_sub(1));
111                let (start, end) = self.line_span_pos_range(start_line, end_line);
112                TextObjectEditPlan {
113                    delete_ranges: vec![(start, end)],
114                    text: self.line_span_text_linewise_register(start_line, end_line),
115                    mode: VisualModeKind::Line,
116                }
117            }
118        })
119    }
120
121    fn text_object_range(&self, cursor: Pos, spec: TextObjectSpec) -> Option<TextObjectRange> {
122        match spec.kind {
123            TextObjectKind::Word => self.word_text_object_range(cursor, spec.scope, spec.count),
124            TextObjectKind::BigWord => {
125                self.big_word_text_object_range(cursor, spec.scope, spec.count)
126            }
127            TextObjectKind::Paragraph => {
128                self.paragraph_text_object_range(cursor, spec.scope, spec.count)
129            }
130            TextObjectKind::Delimiter(kind) => {
131                self.delimiter_text_object_range(cursor, kind, spec.scope, spec.count)
132            }
133        }
134    }
135
136    fn word_text_object_range(
137        &self,
138        cursor: Pos,
139        scope: TextObjectScope,
140        count: usize,
141    ) -> Option<TextObjectRange> {
142        self.run_text_object_range(cursor, scope, count, is_word_char)
143    }
144
145    fn big_word_text_object_range(
146        &self,
147        cursor: Pos,
148        scope: TextObjectScope,
149        count: usize,
150    ) -> Option<TextObjectRange> {
151        self.run_text_object_range(cursor, scope, count, |ch| !ch.is_whitespace())
152    }
153
154    fn run_text_object_range(
155        &self,
156        cursor: Pos,
157        scope: TextObjectScope,
158        count: usize,
159        predicate: impl Fn(char) -> bool + Copy,
160    ) -> Option<TextObjectRange> {
161        let count = count.max(1);
162        let mut start = self.find_seed_char(self.pos_to_char(cursor), predicate)?;
163        let mut end = self.run_end(start, predicate);
164        start = self.run_start(start, predicate);
165
166        for _ in 1..count {
167            if let Some(next_start) = self.find_next_run_start(end, predicate) {
168                end = self.run_end(next_start, predicate);
169            } else {
170                break;
171            }
172        }
173
174        if scope == TextObjectScope::Around {
175            let trailing_end = self.scan_whitespace_forward(end);
176            if trailing_end > end {
177                end = trailing_end;
178            } else {
179                start = self.scan_whitespace_backward(start);
180            }
181        }
182
183        Some(TextObjectRange {
184            start,
185            end,
186            mode: RangeMode::Char,
187        })
188    }
189
190    fn paragraph_text_object_range(
191        &self,
192        cursor: Pos,
193        scope: TextObjectScope,
194        count: usize,
195    ) -> Option<TextObjectRange> {
196        let count = count.max(1);
197        let mut start_line = self.clamp_line(cursor.line);
198
199        if self.line_is_blank(start_line) {
200            return Some(TextObjectRange {
201                start: self.line_to_char(start_line),
202                end: self.line_full_end_char(start_line),
203                mode: RangeMode::Line,
204            });
205        }
206
207        while start_line > 0 && !self.line_is_blank(start_line - 1) {
208            start_line -= 1;
209        }
210
211        let mut end_line = self.clamp_line(cursor.line);
212        while end_line + 1 < self.len_lines() && !self.line_is_blank(end_line + 1) {
213            end_line += 1;
214        }
215
216        for _ in 1..count {
217            let mut next_line = end_line.saturating_add(1);
218            while next_line < self.len_lines() && self.line_is_blank(next_line) {
219                next_line += 1;
220            }
221            if next_line >= self.len_lines() || self.line_is_blank(next_line) {
222                break;
223            }
224            end_line = next_line;
225            while end_line + 1 < self.len_lines() && !self.line_is_blank(end_line + 1) {
226                end_line += 1;
227            }
228        }
229
230        if scope == TextObjectScope::Around {
231            let mut trailing = end_line.saturating_add(1);
232            let mut extended = false;
233            while trailing < self.len_lines() && self.line_is_blank(trailing) {
234                end_line = trailing;
235                trailing += 1;
236                extended = true;
237            }
238            if !extended {
239                while start_line > 0 && self.line_is_blank(start_line - 1) {
240                    start_line -= 1;
241                }
242            }
243        }
244
245        Some(TextObjectRange {
246            start: self.line_to_char(start_line),
247            end: self.line_full_end_char(end_line),
248            mode: RangeMode::Line,
249        })
250    }
251
252    fn delimiter_text_object_range(
253        &self,
254        cursor: Pos,
255        kind: DelimiterKind,
256        scope: TextObjectScope,
257        count: usize,
258    ) -> Option<TextObjectRange> {
259        let count = count.max(1);
260        let cursor_char = self.pos_to_char(cursor);
261        let anchor_before = cursor_char.saturating_sub(1);
262        let (open, close) = delimiter_chars(kind);
263        if open == close {
264            return self.symmetric_delimiter_text_object_range(cursor, open, scope, count);
265        }
266
267        let cursor_line = self.clamp_line(cursor.line);
268
269        let mut containing_pairs = Vec::new();
270        let mut same_line_pairs = Vec::new();
271        let mut stack = Vec::new();
272        for char_idx in 0..self.len_chars() {
273            let ch = self.rope().char(char_idx);
274            if ch == open {
275                stack.push(char_idx);
276            } else if ch == close
277                && let Some(start) = stack.pop()
278            {
279                if pair_contains_cursor(start, char_idx, cursor_char, anchor_before) {
280                    containing_pairs.push((start, char_idx));
281                } else if pair_is_on_line(self, start, char_idx, cursor_line) {
282                    same_line_pairs.push((start, char_idx));
283                }
284            }
285        }
286
287        let (start, end_inclusive) = if !containing_pairs.is_empty() {
288            containing_pairs.sort_by_key(|(start, end)| end.saturating_sub(*start));
289            *containing_pairs.get(count.saturating_sub(1))?
290        } else {
291            same_line_pairs.sort_by_key(|(start, end)| {
292                (
293                    delimiter_pair_distance(*start, *end, cursor_char),
294                    end.saturating_sub(*start),
295                    *start,
296                )
297            });
298            *same_line_pairs.get(count.saturating_sub(1))?
299        };
300
301        let (range_start, range_end) = match scope {
302            TextObjectScope::Inner => (start.saturating_add(1), end_inclusive),
303            TextObjectScope::Around => (start, end_inclusive.saturating_add(1)),
304        };
305
306        Some(TextObjectRange {
307            start: range_start.min(self.len_chars()),
308            end: range_end.min(self.len_chars()),
309            mode: RangeMode::Char,
310        })
311    }
312
313    fn symmetric_delimiter_text_object_range(
314        &self,
315        cursor: Pos,
316        delimiter: char,
317        scope: TextObjectScope,
318        count: usize,
319    ) -> Option<TextObjectRange> {
320        let cursor_line = self.clamp_line(cursor.line);
321        let cursor_char = self.pos_to_char(cursor);
322        let anchor_before = cursor_char.saturating_sub(1);
323        let line_range = self.line_char_range(cursor_line);
324        let mut quote_chars = Vec::new();
325
326        for char_idx in line_range.clone() {
327            if self.rope().char(char_idx) == delimiter && !self.char_is_escaped(char_idx) {
328                quote_chars.push(char_idx);
329            }
330        }
331
332        let mut containing_pairs = Vec::new();
333        let mut same_line_pairs = Vec::new();
334        for pair in quote_chars.chunks_exact(2) {
335            let start = pair[0];
336            let end_inclusive = pair[1];
337            if pair_contains_cursor(start, end_inclusive, cursor_char, anchor_before) {
338                containing_pairs.push((start, end_inclusive));
339            } else {
340                same_line_pairs.push((start, end_inclusive));
341            }
342        }
343
344        let (start, end_inclusive) = if !containing_pairs.is_empty() {
345            containing_pairs.sort_by_key(|(start, end)| end.saturating_sub(*start));
346            *containing_pairs.get(count.saturating_sub(1))?
347        } else {
348            same_line_pairs.sort_by_key(|(start, end)| {
349                (
350                    delimiter_pair_distance(*start, *end, cursor_char),
351                    end.saturating_sub(*start),
352                    *start,
353                )
354            });
355            *same_line_pairs.get(count.saturating_sub(1))?
356        };
357
358        let (range_start, range_end) = match scope {
359            TextObjectScope::Inner => (start.saturating_add(1), end_inclusive),
360            TextObjectScope::Around => (start, end_inclusive.saturating_add(1)),
361        };
362
363        Some(TextObjectRange {
364            start: range_start.min(self.len_chars()),
365            end: range_end.min(self.len_chars()),
366            mode: RangeMode::Char,
367        })
368    }
369
370    fn find_seed_char(
371        &self,
372        cursor_char: usize,
373        predicate: impl Fn(char) -> bool + Copy,
374    ) -> Option<usize> {
375        let maxc = self.len_chars();
376        if maxc == 0 {
377            return None;
378        }
379
380        let clamped = cursor_char.min(maxc.saturating_sub(1));
381        if predicate(self.rope().char(clamped)) {
382            return Some(clamped);
383        }
384
385        if let Some(next) = self.find_next_run_start(clamped, predicate) {
386            return Some(next);
387        }
388
389        if clamped > 0 && predicate(self.rope().char(clamped - 1)) {
390            return Some(clamped - 1);
391        }
392
393        self.find_prev_run_start(clamped, predicate)
394    }
395
396    fn run_start(&self, mut char_idx: usize, predicate: impl Fn(char) -> bool + Copy) -> usize {
397        while char_idx > 0 && predicate(self.rope().char(char_idx - 1)) {
398            char_idx -= 1;
399        }
400        char_idx
401    }
402
403    fn run_end(&self, mut char_idx: usize, predicate: impl Fn(char) -> bool + Copy) -> usize {
404        while char_idx < self.len_chars() && predicate(self.rope().char(char_idx)) {
405            char_idx += 1;
406        }
407        char_idx
408    }
409
410    fn find_next_run_start(
411        &self,
412        mut char_idx: usize,
413        predicate: impl Fn(char) -> bool + Copy,
414    ) -> Option<usize> {
415        while char_idx < self.len_chars() {
416            if predicate(self.rope().char(char_idx)) {
417                return Some(char_idx);
418            }
419            char_idx += 1;
420        }
421        None
422    }
423
424    fn find_prev_run_start(
425        &self,
426        mut char_idx: usize,
427        predicate: impl Fn(char) -> bool + Copy,
428    ) -> Option<usize> {
429        char_idx = char_idx.min(self.len_chars());
430        while char_idx > 0 {
431            char_idx -= 1;
432            if predicate(self.rope().char(char_idx)) {
433                return Some(self.run_start(char_idx, predicate));
434            }
435        }
436        None
437    }
438
439    fn scan_whitespace_forward(&self, mut char_idx: usize) -> usize {
440        while char_idx < self.len_chars() && self.rope().char(char_idx).is_whitespace() {
441            char_idx += 1;
442        }
443        char_idx
444    }
445
446    fn scan_whitespace_backward(&self, mut char_idx: usize) -> usize {
447        while char_idx > 0 && self.rope().char(char_idx - 1).is_whitespace() {
448            char_idx -= 1;
449        }
450        char_idx
451    }
452
453    fn line_is_blank(&self, line_idx: usize) -> bool {
454        self.line_string(line_idx).trim().is_empty()
455    }
456
457    fn char_is_escaped(&self, char_idx: usize) -> bool {
458        let mut backslashes = 0;
459        let mut idx = char_idx;
460        while idx > 0 {
461            idx -= 1;
462            if self.rope().char(idx) != '\\' {
463                break;
464            }
465            backslashes += 1;
466        }
467        backslashes % 2 == 1
468    }
469}
470
471fn delimiter_chars(kind: DelimiterKind) -> (char, char) {
472    match kind {
473        DelimiterKind::Parentheses => ('(', ')'),
474        DelimiterKind::Brackets => ('[', ']'),
475        DelimiterKind::Braces => ('{', '}'),
476        DelimiterKind::SingleQuotes => ('\'', '\''),
477        DelimiterKind::DoubleQuotes => ('"', '"'),
478        DelimiterKind::Backticks => ('`', '`'),
479    }
480}
481
482fn pair_contains_cursor(
483    start: usize,
484    end_inclusive: usize,
485    cursor_char: usize,
486    before: usize,
487) -> bool {
488    (start <= cursor_char && cursor_char <= end_inclusive)
489        || (cursor_char > 0 && start <= before && before <= end_inclusive)
490}
491
492fn pair_is_on_line(buf: &TextBuffer, start: usize, end_inclusive: usize, line: usize) -> bool {
493    buf.char_to_line(start) == line && buf.char_to_line(end_inclusive) == line
494}
495
496fn delimiter_pair_distance(start: usize, end_inclusive: usize, cursor_char: usize) -> usize {
497    if cursor_char < start {
498        start - cursor_char
499    } else if cursor_char > end_inclusive {
500        cursor_char - end_inclusive
501    } else {
502        0
503    }
504}