Skip to main content

hac_core/text_object/
text_object.rs

1use crate::{syntax::highlighter::Highlighter, text_object::cursor::Cursor};
2
3use std::collections::HashMap;
4use std::ops::{Add, Sub};
5
6use ropey::Rope;
7use tree_sitter::Tree;
8
9#[derive(Debug, PartialEq, Clone)]
10pub enum LineBreak {
11    Lf,
12    Crlf,
13}
14
15impl From<LineBreak> for usize {
16    fn from(value: LineBreak) -> usize {
17        match value {
18            LineBreak::Lf => 1,
19            LineBreak::Crlf => 2,
20        }
21    }
22}
23
24impl std::fmt::Display for LineBreak {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match self {
27            Self::Lf => f.write_str("\n"),
28            Self::Crlf => f.write_str("\r\n"),
29        }
30    }
31}
32
33#[derive(Debug, Clone, PartialEq)]
34pub struct Readonly;
35#[derive(Debug, Clone, PartialEq)]
36pub struct Write;
37
38#[derive(Debug, Clone, PartialEq)]
39pub struct TextObject<State = Readonly> {
40    content: Rope,
41    state: std::marker::PhantomData<State>,
42    line_break: LineBreak,
43}
44
45impl<State> Default for TextObject<State> {
46    fn default() -> Self {
47        let content = String::default();
48
49        TextObject {
50            content: Rope::from_str(&content),
51            state: std::marker::PhantomData,
52            line_break: LineBreak::Lf,
53        }
54    }
55}
56
57impl TextObject<Readonly> {
58    pub fn from(content: &str) -> TextObject<Readonly> {
59        let content = Rope::from_str(content);
60        let line_break = match content.line(0).to_string().contains("\r\n") {
61            true => LineBreak::Crlf,
62            false => LineBreak::Lf,
63        };
64        TextObject::<Readonly> {
65            content,
66            state: std::marker::PhantomData::<Readonly>,
67            line_break,
68        }
69    }
70
71    pub fn with_write(self) -> TextObject<Write> {
72        TextObject::<Write> {
73            content: self.content,
74            state: std::marker::PhantomData,
75            line_break: self.line_break,
76        }
77    }
78}
79
80impl TextObject<Write> {
81    pub fn insert_char(&mut self, c: char, cursor: &Cursor) {
82        let line = self.content.line_to_char(cursor.row());
83        let col_offset = line + cursor.col();
84        self.content.insert_char(col_offset, c);
85    }
86
87    pub fn insert_newline(&mut self, cursor: &Cursor) {
88        let line = self.content.line_to_char(cursor.row());
89        let col_offset = line + cursor.col();
90        self.content
91            .insert(col_offset, &self.line_break.to_string());
92    }
93
94    pub fn erase_backwards_up_to_line_start(&mut self, cursor: &Cursor) {
95        if cursor.col().eq(&0) {
96            return;
97        }
98        let line = self.content.line_to_char(cursor.row());
99        let col_offset = line + cursor.col();
100        self.content
101            .try_remove(col_offset.saturating_sub(1)..col_offset)
102            .ok();
103    }
104
105    pub fn erase_previous_char(&mut self, cursor: &Cursor) {
106        let line = self.content.line_to_char(cursor.row());
107        let col_offset = line + cursor.col();
108        self.content
109            .try_remove(col_offset.saturating_sub(1)..col_offset)
110            .ok();
111    }
112
113    pub fn erase_current_char(&mut self, cursor: &Cursor) {
114        let line = self.content.line_to_char(cursor.row());
115        let col_offset = line + cursor.col();
116        self.content.try_remove(col_offset..col_offset.add(1)).ok();
117    }
118
119    pub fn current_line(&self, cursor: &Cursor) -> Option<&str> {
120        self.content.line(cursor.row()).as_str()
121    }
122
123    pub fn line_len_with_linebreak(&self, line: usize) -> usize {
124        self.content
125            .line(line)
126            .as_str()
127            .map(|line| line.len())
128            .unwrap_or_default()
129    }
130
131    pub fn line_len(&self, line: usize) -> usize {
132        self.content
133            .line(line)
134            .as_str()
135            .map(|line| line.len().saturating_sub(self.line_break.clone().into()))
136            .unwrap_or_default()
137    }
138
139    pub fn erase_until_eol(&mut self, cursor: &Cursor) {
140        let line = self.content.line_to_char(cursor.row());
141        let next_line = self.content.line_to_char(cursor.row().add(1));
142        let col_offset = line + cursor.col();
143        self.content
144            .try_remove(col_offset..next_line.saturating_sub(1))
145            .ok();
146    }
147
148    pub fn find_char_after_whitespace(&self, cursor: &Cursor) -> (usize, usize) {
149        let line = self.content.line_to_char(cursor.row());
150        let col_offset = line + cursor.col();
151        let mut end_idx = 0;
152        let mut found = false;
153
154        for char in self.content.chars_at(col_offset) {
155            match (char, found) {
156                (c, false) if c.is_whitespace() => {
157                    found = true;
158                    end_idx = end_idx.add(1);
159                }
160                (c, true) if !c.is_whitespace() => break,
161                _ => end_idx = end_idx.add(1),
162            }
163        }
164        let curr_idx = col_offset.add(end_idx);
165        let curr_row = self.content.char_to_line(col_offset.add(end_idx));
166        let curr_row_start = self.content.line_to_char(curr_row);
167        let curr_col = curr_idx.sub(curr_row_start);
168        (curr_col, curr_row)
169    }
170
171    pub fn find_char_before_whitespace(&self, cursor: &Cursor) -> (usize, usize) {
172        let line = self.content.line_to_char(cursor.row());
173        let col_offset = line + cursor.col();
174        let mut found = false;
175        let mut index = col_offset.saturating_sub(1);
176
177        for _ in (0..col_offset.saturating_sub(1)).rev() {
178            let char = self.content.char(index);
179            match (char, found) {
180                (c, false) if c.is_whitespace() => found = true,
181                (c, true) if !c.is_whitespace() => break,
182                _ => {}
183            }
184            index = index.saturating_sub(1);
185        }
186
187        let curr_row = self.content.char_to_line(index);
188        let curr_row_start = self.content.line_to_char(curr_row);
189        let curr_col = index - curr_row_start;
190
191        (curr_col, curr_row)
192    }
193
194    pub fn find_char_after_separator(&self, cursor: &Cursor) -> (usize, usize) {
195        let start_idx = self.content.line_to_char(cursor.row()).add(cursor.col());
196        let mut end_idx = 0;
197        let mut found_newline = false;
198
199        if let Some(initial_char) = self.content.get_char(start_idx) {
200            for char in self.content.chars_at(start_idx) {
201                match (
202                    initial_char.is_alphanumeric(),
203                    char.is_alphanumeric(),
204                    found_newline,
205                ) {
206                    (_, _, true) if !char.is_whitespace() => break,
207                    (false, true, _) => break,
208                    (true, false, _) => break,
209                    _ if char.is_whitespace() => {
210                        found_newline = true;
211                        end_idx = end_idx.add(1);
212                    }
213                    _ => end_idx = end_idx.add(1),
214                }
215            }
216        }
217
218        let curr_idx = start_idx.add(end_idx);
219        let curr_row = self.content.char_to_line(curr_idx);
220        let curr_row_start = self.content.line_to_char(curr_row);
221        let curr_col = curr_idx.sub(curr_row_start);
222
223        (curr_col, curr_row)
224    }
225
226    pub fn find_char_before_separator(&self, cursor: &Cursor) -> (usize, usize) {
227        let start_idx = self.content.line_to_char(cursor.row()).add(cursor.col());
228        let mut end_idx = start_idx;
229        let mut found_newline = false;
230
231        if let Some(initial_char) = self.content.get_char(start_idx) {
232            for _ in (0..start_idx.saturating_sub(1)).rev() {
233                let char = self.content.char(end_idx);
234
235                match (
236                    initial_char.is_alphanumeric(),
237                    char.is_alphanumeric(),
238                    found_newline,
239                ) {
240                    (_, _, true) if !self.line_break.to_string().contains(char) => break,
241                    (false, true, _) => break,
242                    (true, false, _) => break,
243                    _ if self.line_break.to_string().contains(char) => {
244                        found_newline = true;
245                        end_idx = end_idx.saturating_sub(1);
246                    }
247                    _ => end_idx = end_idx.saturating_sub(1),
248                }
249            }
250        };
251
252        let curr_row = self.content.char_to_line(end_idx);
253        let curr_row_start = self.content.line_to_char(curr_row);
254        let curr_col = end_idx.sub(curr_row_start);
255
256        (curr_col, curr_row)
257    }
258
259    pub fn find_empty_line_above(&self, cursor: &Cursor) -> usize {
260        let mut new_row = cursor.row().saturating_sub(1);
261
262        while let Some(line) = self.content.get_line(new_row) {
263            if line.to_string().eq(&self.line_break.to_string()) {
264                break;
265            }
266
267            if new_row.eq(&0) {
268                break;
269            }
270            new_row = new_row.saturating_sub(1);
271        }
272
273        new_row
274    }
275
276    pub fn find_empty_line_below(&self, cursor: &Cursor) -> usize {
277        let mut new_row = cursor.row().add(1);
278        let len_lines = self.len_lines();
279
280        while let Some(line) = self.content.get_line(new_row) {
281            if line.to_string().eq(&self.line_break.to_string()) {
282                break;
283            }
284            new_row = new_row.add(1);
285        }
286
287        usize::min(new_row, len_lines.saturating_sub(1))
288    }
289
290    pub fn len_lines(&self) -> usize {
291        self.content.len_lines()
292    }
293
294    pub fn delete_line(&mut self, line: usize) {
295        let start = self.content.line_to_char(line);
296        let end = self.content.line_to_char(line.add(1));
297        self.content.try_remove(start..end).ok();
298    }
299
300    /// deletes a word forward in one of two ways:
301    ///
302    /// - if the current character is alphanumeric, then this delete up to the first non
303    /// alphanumeric character
304    /// - if the current character is non alphanumeric, then delete up to the first alphanumeric
305    /// character
306    pub fn delete_word(&mut self, cursor: &Cursor) {
307        let start_idx = self.content.line_to_char(cursor.row()).add(cursor.col());
308        let mut end_idx = start_idx.saturating_sub(1);
309
310        if let Some(initial_char) = self.content.get_char(start_idx) {
311            for char in self.content.chars_at(start_idx) {
312                match (initial_char.is_alphanumeric(), char.is_alphanumeric()) {
313                    (false, _) if self.line_break.to_string().contains(char) => break,
314                    (false, true) => {
315                        end_idx = end_idx.add(1);
316                        break;
317                    }
318                    (true, false) => {
319                        end_idx = end_idx.add(1);
320                        break;
321                    }
322                    _ => end_idx = end_idx.add(1),
323                }
324            }
325
326            self.content.try_remove(start_idx..end_idx).ok();
327        }
328    }
329
330    /// deletes a word backwards in one of two ways:
331    ///
332    /// - if the current character is alphanumeric, then this delete up to the first non
333    /// alphanumeric character
334    /// - if the current character is non alphanumeric, then delete up to the first alphanumeric
335    /// character
336    ///
337    /// will always return how many columns to advance the cursor
338    pub fn delete_word_backwards(&mut self, cursor: &Cursor) -> usize {
339        let start_idx = self.content.line_to_char(cursor.row()).add(cursor.col());
340        let mut end_idx = start_idx.saturating_sub(1);
341
342        if let Some(initial_char) = self.content.get_char(start_idx.saturating_sub(1)) {
343            for _ in (0..start_idx.saturating_sub(1)).rev() {
344                let char = self.content.char(end_idx);
345                match (initial_char.is_alphanumeric(), char.is_alphanumeric()) {
346                    (false, _) if self.line_break.to_string().contains(char) => break,
347                    (false, true) => break,
348                    (true, false) => break,
349                    _ => end_idx = end_idx.saturating_sub(1),
350                }
351            }
352        };
353
354        self.content.try_remove(end_idx.add(1)..start_idx).ok();
355        start_idx.sub(end_idx.add(1))
356    }
357
358    pub fn insert_line_below(&mut self, cursor: &Cursor, tree: Option<&Tree>) {
359        let indentation = self.get_scope_aware_indentation(cursor, tree);
360        let next_line = self.content.line_to_char(cursor.row().add(1));
361        let line_with_indentation = format!("{}{}", indentation, &self.line_break.to_string());
362        self.content.insert(next_line, &line_with_indentation);
363    }
364
365    pub fn insert_line_above(&mut self, cursor: &Cursor, tree: Option<&Tree>) {
366        let indentation = self.get_scope_aware_indentation(cursor, tree);
367        let curr_line = self.content.line_to_char(cursor.row());
368        let line_with_indentation = format!("{}{}", indentation, &self.line_break.to_string());
369        self.content.insert(curr_line, &line_with_indentation);
370    }
371
372    pub fn find_oposing_token(&mut self, cursor: &Cursor) -> (usize, usize) {
373        let start_idx = self.content.line_to_char(cursor.row()).add(cursor.col());
374        let mut combinations = HashMap::new();
375        let pairs = [('<', '>'), ('(', ')'), ('[', ']'), ('{', '}')];
376        pairs.iter().for_each(|pair| {
377            combinations.insert(pair.0, pair.1);
378            combinations.insert(pair.1, pair.0);
379        });
380
381        let mut look_forward = true;
382        let mut token_to_search = char::default();
383        let (mut curr_open, mut walked) = (0, 0);
384
385        if let Some(initial_char) = self.content.get_char(start_idx) {
386            match initial_char {
387                c if is_opening_token(c) => {
388                    token_to_search = *combinations.get(&c).unwrap();
389                    curr_open = curr_open.add(1);
390                }
391                c if is_closing_token(c) => {
392                    token_to_search = *combinations.get(&c).unwrap();
393                    curr_open = curr_open.add(1);
394                    look_forward = false;
395                }
396                _ => {}
397            }
398
399            let range = if look_forward {
400                start_idx.add(1)..self.content.len_chars()
401            } else {
402                0..start_idx
403            };
404
405            for i in range {
406                let char = self
407                    .content
408                    .get_char(if look_forward {
409                        i
410                    } else {
411                        start_idx - walked - 1
412                    })
413                    .unwrap_or_default();
414
415                if token_to_search.eq(&char::default()) {
416                    if !is_opening_token(char) {
417                        walked = walked.add(1);
418                        continue;
419                    }
420                    token_to_search = *combinations.get(&char).unwrap();
421                }
422
423                char.eq(combinations.get(&token_to_search).unwrap())
424                    .then(|| curr_open = curr_open.add(1));
425
426                char.eq(&token_to_search)
427                    .then(|| curr_open = curr_open.sub(1));
428
429                walked = walked.add(1);
430
431                if curr_open.eq(&0) {
432                    break;
433                }
434            }
435        }
436
437        if curr_open.gt(&0) {
438            return (cursor.col(), cursor.row());
439        }
440
441        if look_forward {
442            let curr_row = self.content.char_to_line(start_idx.add(walked));
443            let curr_row_start = self.content.line_to_char(curr_row);
444            let curr_col = start_idx.add(walked).saturating_sub(curr_row_start);
445            (curr_col, curr_row)
446        } else {
447            let curr_row = self.content.char_to_line(start_idx.sub(walked));
448            let curr_row_start = self.content.line_to_char(curr_row);
449            let curr_col = start_idx.sub(walked).sub(curr_row_start);
450            (curr_col, curr_row)
451        }
452    }
453
454    fn get_scope_aware_indentation(&self, cursor: &Cursor, tree: Option<&Tree>) -> String {
455        if let Some(tree) = tree {
456            let line_byte_idx = self.content.line_to_byte(cursor.row());
457            let cursor_byte_idx = line_byte_idx.add(cursor.col());
458            let indentation_level = Highlighter::find_indentation_level(tree, cursor_byte_idx);
459            "  ".repeat(indentation_level)
460        } else {
461            String::new()
462        }
463    }
464}
465
466impl<State> std::fmt::Display for TextObject<State> {
467    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
468        f.write_str(&self.content.to_string())
469    }
470}
471
472fn is_opening_token(char: char) -> bool {
473    matches!(char, '(' | '{' | '[' | '<')
474}
475
476fn is_closing_token(char: char) -> bool {
477    matches!(char, ')' | '}' | ']' | '>')
478}