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 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 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}