1use crate::mm::{Buffer, Position};
9
10use super::direction::WordBoundary;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum TextObject {
30 InnerWord(WordBoundary),
32 AWord(WordBoundary),
34 InnerParagraph,
36 AParagraph,
38 InnerQuote(char),
40 AQuote(char),
42 InnerBracket(char),
44 ABracket(char),
46}
47
48impl TextObject {
49 #[must_use]
51 pub const fn is_inner(&self) -> bool {
52 matches!(
53 self,
54 Self::InnerWord(_) | Self::InnerParagraph | Self::InnerQuote(_) | Self::InnerBracket(_)
55 )
56 }
57
58 #[must_use]
60 pub const fn is_around(&self) -> bool {
61 !self.is_inner()
62 }
63
64 #[must_use]
75 pub const fn from_chars(scope: char, object: char) -> Option<Self> {
76 let is_inner = match scope {
77 'i' => true,
78 'a' => false,
79 _ => return None,
80 };
81
82 match object {
83 'w' => Some(if is_inner {
84 Self::InnerWord(WordBoundary::Word)
85 } else {
86 Self::AWord(WordBoundary::Word)
87 }),
88 'W' => Some(if is_inner {
89 Self::InnerWord(WordBoundary::BigWord)
90 } else {
91 Self::AWord(WordBoundary::BigWord)
92 }),
93 'p' => Some(if is_inner {
94 Self::InnerParagraph
95 } else {
96 Self::AParagraph
97 }),
98 '(' | ')' | 'b' => Some(if is_inner {
99 Self::InnerBracket('(')
100 } else {
101 Self::ABracket('(')
102 }),
103 '[' | ']' => Some(if is_inner {
104 Self::InnerBracket('[')
105 } else {
106 Self::ABracket('[')
107 }),
108 '{' | '}' | 'B' => Some(if is_inner {
109 Self::InnerBracket('{')
110 } else {
111 Self::ABracket('{')
112 }),
113 '<' | '>' => Some(if is_inner {
114 Self::InnerBracket('<')
115 } else {
116 Self::ABracket('<')
117 }),
118 '"' => Some(if is_inner {
119 Self::InnerQuote('"')
120 } else {
121 Self::AQuote('"')
122 }),
123 '\'' => Some(if is_inner {
124 Self::InnerQuote('\'')
125 } else {
126 Self::AQuote('\'')
127 }),
128 '`' => Some(if is_inner {
129 Self::InnerQuote('`')
130 } else {
131 Self::AQuote('`')
132 }),
133 _ => None,
134 }
135 }
136}
137
138pub struct TextObjectEngine;
160
161impl TextObjectEngine {
162 #[must_use]
178 pub fn range(
179 buffer: &Buffer,
180 position: Position,
181 text_object: TextObject,
182 count: usize,
183 ) -> Option<(Position, Position)> {
184 let count = count.max(1);
185
186 match text_object {
187 TextObject::InnerWord(boundary) => Self::inner_word(buffer, position, boundary),
188 TextObject::AWord(boundary) => Self::a_word(buffer, position, boundary),
189 TextObject::InnerParagraph => Self::inner_paragraph(buffer, position),
190 TextObject::AParagraph => Self::a_paragraph(buffer, position),
191 TextObject::InnerQuote(quote) => Self::inner_quote(buffer, position, quote),
192 TextObject::AQuote(quote) => Self::a_quote(buffer, position, quote),
193 TextObject::InnerBracket(bracket) => {
194 Self::inner_bracket(buffer, position, bracket, count)
195 }
196 TextObject::ABracket(bracket) => Self::a_bracket(buffer, position, bracket, count),
197 }
198 }
199
200 #[cfg_attr(coverage_nightly, coverage(off))]
203 fn inner_word(
204 buffer: &Buffer,
205 pos: Position,
206 boundary: WordBoundary,
207 ) -> Option<(Position, Position)> {
208 let line = buffer.line(pos.line)?;
209 let chars: Vec<char> = line.chars().collect();
210
211 if chars.is_empty() {
212 return Some((pos, pos));
213 }
214
215 let col = pos.column.min(chars.len().saturating_sub(1));
216 let current_char = chars.get(col)?;
217
218 let is_word_char = boundary.is_word_char(*current_char);
220 let is_whitespace = current_char.is_whitespace();
221
222 let mut start = col;
224 let mut end = col;
225
226 if is_whitespace {
227 while start > 0 && chars.get(start - 1).is_some_and(|c| c.is_whitespace()) {
229 start -= 1;
230 }
231 while end + 1 < chars.len() && chars.get(end + 1).is_some_and(|c| c.is_whitespace()) {
232 end += 1;
233 }
234 } else if boundary == WordBoundary::Word {
235 if is_word_char {
237 while start > 0
238 && chars
239 .get(start - 1)
240 .is_some_and(|c| boundary.is_word_char(*c))
241 {
242 start -= 1;
243 }
244 while end + 1 < chars.len()
245 && chars
246 .get(end + 1)
247 .is_some_and(|c| boundary.is_word_char(*c))
248 {
249 end += 1;
250 }
251 } else {
252 while start > 0
254 && chars
255 .get(start - 1)
256 .is_some_and(|c| !c.is_whitespace() && !boundary.is_word_char(*c))
257 {
258 start -= 1;
259 }
260 while end + 1 < chars.len()
261 && chars
262 .get(end + 1)
263 .is_some_and(|c| !c.is_whitespace() && !boundary.is_word_char(*c))
264 {
265 end += 1;
266 }
267 }
268 } else {
269 while start > 0 && chars.get(start - 1).is_some_and(|c| !c.is_whitespace()) {
271 start -= 1;
272 }
273 while end + 1 < chars.len() && chars.get(end + 1).is_some_and(|c| !c.is_whitespace()) {
274 end += 1;
275 }
276 }
277
278 Some((Position::new(pos.line, start), Position::new(pos.line, end)))
279 }
280
281 #[cfg_attr(coverage_nightly, coverage(off))]
282 fn a_word(
283 buffer: &Buffer,
284 pos: Position,
285 boundary: WordBoundary,
286 ) -> Option<(Position, Position)> {
287 let (inner_start, inner_end) = Self::inner_word(buffer, pos, boundary)?;
288 let line = buffer.line(pos.line)?;
289 let chars: Vec<char> = line.chars().collect();
290
291 let mut start = inner_start.column;
292 let mut end = inner_end.column;
293
294 let mut has_trailing = false;
296 while end + 1 < chars.len() && chars.get(end + 1).is_some_and(|c| c.is_whitespace()) {
297 end += 1;
298 has_trailing = true;
299 }
300
301 if !has_trailing {
303 while start > 0 && chars.get(start - 1).is_some_and(|c| c.is_whitespace()) {
304 start -= 1;
305 }
306 }
307
308 Some((Position::new(pos.line, start), Position::new(pos.line, end)))
309 }
310
311 fn inner_paragraph(buffer: &Buffer, pos: Position) -> Option<(Position, Position)> {
314 let line_count = buffer.line_count();
315 if line_count == 0 {
316 return None;
317 }
318
319 let current_line = buffer.line(pos.line)?;
320 let is_empty_line = current_line.trim().is_empty();
321
322 let mut start = pos.line;
323 let mut end = pos.line;
324
325 let predicate = |l: &str| {
327 if is_empty_line {
328 l.trim().is_empty()
329 } else {
330 !l.trim().is_empty()
331 }
332 };
333
334 while start > 0 && buffer.line(start - 1).is_some_and(&predicate) {
335 start -= 1;
336 }
337 while end + 1 < line_count && buffer.line(end + 1).is_some_and(&predicate) {
338 end += 1;
339 }
340
341 let end_col = buffer.line_len(end).unwrap_or(0).saturating_sub(1);
342 Some((Position::new(start, 0), Position::new(end, end_col)))
343 }
344
345 fn a_paragraph(buffer: &Buffer, pos: Position) -> Option<(Position, Position)> {
346 let (inner_start, inner_end) = Self::inner_paragraph(buffer, pos)?;
347 let line_count = buffer.line_count();
348
349 let mut start = inner_start.line;
350 let mut end = inner_end.line;
351
352 while end + 1 < line_count && buffer.line(end + 1).is_some_and(|l| l.trim().is_empty()) {
354 end += 1;
355 }
356
357 if end == inner_end.line {
359 while start > 0 && buffer.line(start - 1).is_some_and(|l| l.trim().is_empty()) {
360 start -= 1;
361 }
362 }
363
364 let end_col = buffer.line_len(end).unwrap_or(0).saturating_sub(1);
365 Some((Position::new(start, 0), Position::new(end, end_col)))
366 }
367
368 fn inner_quote(buffer: &Buffer, pos: Position, quote: char) -> Option<(Position, Position)> {
371 let line = buffer.line(pos.line)?;
372 let chars: Vec<char> = line.chars().collect();
373
374 let (open, close) = Self::find_quote_pair(&chars, pos.column, quote)?;
376
377 let start = open + 1;
379 let end = close.saturating_sub(1);
380
381 if start > end {
382 Some((Position::new(pos.line, start), Position::new(pos.line, start)))
384 } else {
385 Some((Position::new(pos.line, start), Position::new(pos.line, end)))
386 }
387 }
388
389 fn a_quote(buffer: &Buffer, pos: Position, quote: char) -> Option<(Position, Position)> {
390 let line = buffer.line(pos.line)?;
391 let chars: Vec<char> = line.chars().collect();
392
393 let (open, close) = Self::find_quote_pair(&chars, pos.column, quote)?;
394
395 Some((Position::new(pos.line, open), Position::new(pos.line, close)))
396 }
397
398 #[cfg_attr(coverage_nightly, coverage(off))]
399 fn find_quote_pair(chars: &[char], col: usize, quote: char) -> Option<(usize, usize)> {
400 let quotes: Vec<usize> = chars
402 .iter()
403 .enumerate()
404 .filter(|&(_, c)| *c == quote)
405 .map(|(i, _)| i)
406 .collect();
407
408 if quotes.len() < 2 {
409 return None;
410 }
411
412 for pair in quotes.chunks(2) {
414 if pair.len() == 2 {
415 let (open, close) = (pair[0], pair[1]);
416 if col >= open && col <= close {
417 return Some((open, close));
418 }
419 }
420 }
421
422 if col < quotes[0] && quotes.len() >= 2 {
424 return Some((quotes[0], quotes[1]));
425 }
426
427 if quotes.len() >= 2 && col > quotes[quotes.len() - 1] {
429 let len = quotes.len();
430 return Some((quotes[len - 2], quotes[len - 1]));
431 }
432
433 None
434 }
435
436 fn inner_bracket(
439 buffer: &Buffer,
440 pos: Position,
441 bracket: char,
442 count: usize,
443 ) -> Option<(Position, Position)> {
444 let (open, close) = Self::get_bracket_pair(bracket)?;
445 let (open_pos, close_pos) = Self::find_bracket_pair(buffer, pos, open, close, count)?;
446
447 let start = Self::next_position(buffer, open_pos)?;
450 let end = Self::prev_position(buffer, close_pos)?;
452
453 if start > end {
454 Some((start, start))
456 } else {
457 Some((start, end))
458 }
459 }
460
461 fn a_bracket(
462 buffer: &Buffer,
463 pos: Position,
464 bracket: char,
465 count: usize,
466 ) -> Option<(Position, Position)> {
467 let (open, close) = Self::get_bracket_pair(bracket)?;
468 Self::find_bracket_pair(buffer, pos, open, close, count)
469 }
470
471 const fn get_bracket_pair(bracket: char) -> Option<(char, char)> {
472 match bracket {
473 '(' | ')' => Some(('(', ')')),
474 '[' | ']' => Some(('[', ']')),
475 '{' | '}' => Some(('{', '}')),
476 '<' | '>' => Some(('<', '>')),
477 _ => None,
478 }
479 }
480
481 fn find_bracket_pair(
482 buffer: &Buffer,
483 pos: Position,
484 open: char,
485 close: char,
486 count: usize,
487 ) -> Option<(Position, Position)> {
488 let open_pos = Self::find_opening_bracket(buffer, pos, open, close, count)?;
490
491 let close_pos = Self::find_closing_bracket(buffer, open_pos, open, close)?;
493
494 Some((open_pos, close_pos))
495 }
496
497 #[cfg_attr(coverage_nightly, coverage(off))]
498 fn find_opening_bracket(
499 buffer: &Buffer,
500 pos: Position,
501 open: char,
502 close: char,
503 count: usize,
504 ) -> Option<Position> {
505 let mut depth: isize = 0;
506 let mut found_count = 0;
507 let mut line_idx = pos.line;
508 let mut last_open = None;
509
510 if let Some(line) = buffer.line(line_idx) {
512 let chars: Vec<char> = line.chars().collect();
513 let start_col = pos.column.min(chars.len().saturating_sub(1));
514
515 for col in (0..=start_col).rev() {
516 if let Some(&c) = chars.get(col) {
517 if c == close {
518 depth += 1;
519 } else if c == open {
520 if depth > 0 {
521 depth -= 1;
522 } else {
523 found_count += 1;
524 last_open = Some(Position::new(line_idx, col));
525 if found_count >= count {
526 return last_open;
527 }
528 }
529 }
530 }
531 }
532 }
533
534 while line_idx > 0 {
536 line_idx -= 1;
537 if let Some(line) = buffer.line(line_idx) {
538 let chars: Vec<char> = line.chars().collect();
539 for col in (0..chars.len()).rev() {
540 if let Some(&c) = chars.get(col) {
541 if c == close {
542 depth += 1;
543 } else if c == open {
544 if depth > 0 {
545 depth -= 1;
546 } else {
547 found_count += 1;
548 last_open = Some(Position::new(line_idx, col));
549 if found_count >= count {
550 return last_open;
551 }
552 }
553 }
554 }
555 }
556 }
557 }
558
559 last_open
560 }
561
562 #[cfg_attr(coverage_nightly, coverage(off))]
563 fn find_closing_bracket(
564 buffer: &Buffer,
565 open_pos: Position,
566 open: char,
567 close: char,
568 ) -> Option<Position> {
569 let mut depth = 1;
570 let mut line_idx = open_pos.line;
571 let mut col = open_pos.column + 1;
572
573 while line_idx < buffer.line_count() {
574 if let Some(line) = buffer.line(line_idx) {
575 let chars: Vec<char> = line.chars().collect();
576
577 while col < chars.len() {
578 let c = chars[col];
579 if c == open {
580 depth += 1;
581 } else if c == close {
582 depth -= 1;
583 if depth == 0 {
584 return Some(Position::new(line_idx, col));
585 }
586 }
587 col += 1;
588 }
589 }
590
591 line_idx += 1;
592 col = 0;
593 }
594
595 None
596 }
597
598 #[cfg_attr(coverage_nightly, coverage(off))]
599 fn next_position(buffer: &Buffer, pos: Position) -> Option<Position> {
600 let line_len = buffer.line_len(pos.line)?;
601 if pos.column + 1 < line_len {
602 Some(Position::new(pos.line, pos.column + 1))
603 } else if pos.line + 1 < buffer.line_count() {
604 Some(Position::new(pos.line + 1, 0))
605 } else {
606 Some(Position::new(pos.line, pos.column))
607 }
608 }
609
610 #[cfg_attr(coverage_nightly, coverage(off))]
611 fn prev_position(buffer: &Buffer, pos: Position) -> Option<Position> {
612 if pos.column > 0 {
613 Some(Position::new(pos.line, pos.column - 1))
614 } else if pos.line > 0 {
615 let prev_len = buffer.line_len(pos.line - 1)?;
616 Some(Position::new(pos.line - 1, prev_len.saturating_sub(1)))
617 } else {
618 Some(Position::new(0, 0))
619 }
620 }
621}