Skip to main content

zsh/zle/
textobjects.rs

1//! ZLE text objects
2//!
3//! Direct port from zsh/Src/Zle/zle_thingy.c text object support
4//!
5//! Text objects for vi mode operations (e.g., "iw" for inner word, "a)" for a-parenthesis)
6
7use super::main::Zle;
8
9/// Text object type
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum TextObjectType {
12    /// Inner (inside delimiters)
13    Inner,
14    /// A (including delimiters)
15    A,
16}
17
18/// Text object kind
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum TextObjectKind {
21    Word,
22    BigWord,
23    Sentence,
24    Paragraph,
25    Parenthesis,
26    Bracket,
27    Brace,
28    Angle,
29    SingleQuote,
30    DoubleQuote,
31    BackQuote,
32}
33
34/// A text object selection (start and end positions)
35#[derive(Debug, Clone, Copy)]
36pub struct TextObject {
37    pub start: usize,
38    pub end: usize,
39}
40
41impl Zle {
42    /// Select a text object
43    pub fn select_text_object(
44        &self,
45        obj_type: TextObjectType,
46        kind: TextObjectKind,
47    ) -> Option<TextObject> {
48        match kind {
49            TextObjectKind::Word => self.select_word_object(obj_type, false),
50            TextObjectKind::BigWord => self.select_word_object(obj_type, true),
51            TextObjectKind::Sentence => self.select_sentence_object(obj_type),
52            TextObjectKind::Paragraph => self.select_paragraph_object(obj_type),
53            TextObjectKind::Parenthesis => self.select_pair_object(obj_type, '(', ')'),
54            TextObjectKind::Bracket => self.select_pair_object(obj_type, '[', ']'),
55            TextObjectKind::Brace => self.select_pair_object(obj_type, '{', '}'),
56            TextObjectKind::Angle => self.select_pair_object(obj_type, '<', '>'),
57            TextObjectKind::SingleQuote => self.select_quote_object(obj_type, '\''),
58            TextObjectKind::DoubleQuote => self.select_quote_object(obj_type, '"'),
59            TextObjectKind::BackQuote => self.select_quote_object(obj_type, '`'),
60        }
61    }
62
63    fn select_word_object(&self, obj_type: TextObjectType, big_word: bool) -> Option<TextObject> {
64        if self.zlell == 0 {
65            return None;
66        }
67
68        let is_word_char = if big_word {
69            |c: char| !c.is_whitespace()
70        } else {
71            |c: char| c.is_alphanumeric() || c == '_'
72        };
73
74        let mut start = self.zlecs;
75        let mut end = self.zlecs;
76
77        // Determine if we're on a word or whitespace
78        let on_word = if self.zlecs < self.zlell {
79            is_word_char(self.zleline[self.zlecs])
80        } else {
81            false
82        };
83
84        if on_word {
85            // Find word boundaries
86            while start > 0 && is_word_char(self.zleline[start - 1]) {
87                start -= 1;
88            }
89            while end < self.zlell && is_word_char(self.zleline[end]) {
90                end += 1;
91            }
92
93            // For "a word", include trailing whitespace
94            if obj_type == TextObjectType::A {
95                while end < self.zlell && self.zleline[end].is_whitespace() {
96                    end += 1;
97                }
98            }
99        } else {
100            // On whitespace - select whitespace
101            while start > 0 && self.zleline[start - 1].is_whitespace() {
102                start -= 1;
103            }
104            while end < self.zlell && self.zleline[end].is_whitespace() {
105                end += 1;
106            }
107
108            // For "a whitespace", include adjacent word
109            if obj_type == TextObjectType::A && end < self.zlell {
110                while end < self.zlell && is_word_char(self.zleline[end]) {
111                    end += 1;
112                }
113            }
114        }
115
116        if start < end {
117            Some(TextObject { start, end })
118        } else {
119            None
120        }
121    }
122
123    fn select_sentence_object(&self, obj_type: TextObjectType) -> Option<TextObject> {
124        // Simplified sentence detection
125        let mut start = self.zlecs;
126        let mut end = self.zlecs;
127
128        // Find sentence start (after previous . ! ?)
129        while start > 0 {
130            let c = self.zleline[start - 1];
131            if c == '.' || c == '!' || c == '?' {
132                break;
133            }
134            start -= 1;
135        }
136
137        // Skip whitespace at start (for inner)
138        if obj_type == TextObjectType::Inner {
139            while start < self.zlell && self.zleline[start].is_whitespace() {
140                start += 1;
141            }
142        }
143
144        // Find sentence end
145        while end < self.zlell {
146            let c = self.zleline[end];
147            end += 1;
148            if c == '.' || c == '!' || c == '?' {
149                break;
150            }
151        }
152
153        // Include trailing whitespace for "a sentence"
154        if obj_type == TextObjectType::A {
155            while end < self.zlell && self.zleline[end].is_whitespace() {
156                end += 1;
157            }
158        }
159
160        if start < end {
161            Some(TextObject { start, end })
162        } else {
163            None
164        }
165    }
166
167    fn select_paragraph_object(&self, obj_type: TextObjectType) -> Option<TextObject> {
168        let mut start = self.zlecs;
169        let mut end = self.zlecs;
170
171        // Find paragraph start (blank line)
172        while start > 0 {
173            if start >= 2 && self.zleline[start - 1] == '\n' && self.zleline[start - 2] == '\n' {
174                break;
175            }
176            start -= 1;
177        }
178
179        // Find paragraph end
180        while end < self.zlell {
181            if end + 1 < self.zlell && self.zleline[end] == '\n' && self.zleline[end + 1] == '\n' {
182                if obj_type == TextObjectType::A {
183                    end += 2;
184                }
185                break;
186            }
187            end += 1;
188        }
189
190        if start < end {
191            Some(TextObject { start, end })
192        } else {
193            None
194        }
195    }
196
197    fn select_pair_object(
198        &self,
199        obj_type: TextObjectType,
200        open: char,
201        close: char,
202    ) -> Option<TextObject> {
203        let mut depth = 0;
204        let mut start = None;
205        let mut end = None;
206
207        // Find opening bracket
208        for i in (0..=self.zlecs).rev() {
209            let c = self.zleline[i];
210            if c == close {
211                depth += 1;
212            } else if c == open {
213                if depth == 0 {
214                    start = Some(i);
215                    break;
216                }
217                depth -= 1;
218            }
219        }
220
221        // Find closing bracket
222        depth = 0;
223        for i in self.zlecs..self.zlell {
224            let c = self.zleline[i];
225            if c == open {
226                depth += 1;
227            } else if c == close {
228                if depth == 0 {
229                    end = Some(i + 1);
230                    break;
231                }
232                depth -= 1;
233            }
234        }
235
236        match (start, end) {
237            (Some(s), Some(e)) => {
238                if obj_type == TextObjectType::Inner {
239                    Some(TextObject {
240                        start: s + 1,
241                        end: e - 1,
242                    })
243                } else {
244                    Some(TextObject { start: s, end: e })
245                }
246            }
247            _ => None,
248        }
249    }
250
251    fn select_quote_object(&self, obj_type: TextObjectType, quote: char) -> Option<TextObject> {
252        let mut start = None;
253        let mut end = None;
254
255        // Find opening quote (searching backward)
256        for i in (0..=self.zlecs).rev() {
257            if self.zleline[i] == quote {
258                start = Some(i);
259                break;
260            }
261        }
262
263        // Find closing quote (searching forward)
264        if let Some(s) = start {
265            for i in (s + 1)..self.zlell {
266                if self.zleline[i] == quote {
267                    end = Some(i + 1);
268                    break;
269                }
270            }
271        }
272
273        match (start, end) {
274            (Some(s), Some(e)) => {
275                if obj_type == TextObjectType::Inner {
276                    Some(TextObject {
277                        start: s + 1,
278                        end: e - 1,
279                    })
280                } else {
281                    Some(TextObject { start: s, end: e })
282                }
283            }
284            _ => None,
285        }
286    }
287}