1use crate::{ByteOffset, DocumentView, Shape, TextRange, TextSource};
4
5mod objects;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum CharacterMotion {
10 Find,
12 Till,
14 FindBackward,
16 TillBackward,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum TextObjectScope {
23 Inner,
25 Around,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum TextObjectKind {
32 Word,
34 BigWord,
36 Sentence,
38 Paragraph,
40 Delimited {
42 open: char,
44 close: char,
46 },
47 Quote(char),
49}
50
51impl TextObjectKind {
52 pub fn from_key(key: char) -> Option<Self> {
54 Some(match key {
55 'w' => Self::Word,
56 'W' => Self::BigWord,
57 's' => Self::Sentence,
58 'p' => Self::Paragraph,
59 '(' | ')' | 'b' => Self::Delimited {
60 open: '(',
61 close: ')',
62 },
63 '[' | ']' => Self::Delimited {
64 open: '[',
65 close: ']',
66 },
67 '{' | '}' | 'B' => Self::Delimited {
68 open: '{',
69 close: '}',
70 },
71 '"' | '\'' | '`' => Self::Quote(key),
72 _ => return None,
73 })
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum WordDestination {
80 NextStart,
82 NextEnd,
84 PreviousStart,
86 PreviousEnd,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum WordClass {
93 Word,
95 BigWord,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum Motion {
102 Left,
104 Right,
106 Up,
108 Down,
110 LineStart,
112 FirstNonBlank,
114 LineEnd,
116 Word {
118 destination: WordDestination,
120 class: WordClass,
122 },
123 FirstLine,
125 LastLine,
127 Percent,
129 Paragraph {
131 backward: bool,
133 },
134 Sentence {
136 backward: bool,
138 },
139 Character {
141 kind: CharacterMotion,
143 character: char,
145 },
146 RepeatCharacter {
148 kind: CharacterMotion,
150 character: char,
152 },
153 MatchDelimiter,
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub struct Target {
160 pub position: ByteOffset,
162 pub inclusive: bool,
164 pub linewise: bool,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct ResolvedRange {
171 pub ranges: Vec<TextRange>,
173 pub shape: Shape,
175}
176
177impl ResolvedRange {
178 pub fn character(start: usize, end: usize) -> Self {
180 Self {
181 ranges: vec![TextRange::new(start, end)],
182 shape: Shape::Character,
183 }
184 }
185}
186
187#[derive(Clone, Copy)]
189pub struct MotionResolver<'a> {
190 pub(crate) text: &'a dyn TextSource,
191 pub(crate) cursor: usize,
192 pub(crate) opaque: &'a [TextRange],
193 geometry: &'a dyn crate::Geometry,
194}
195
196impl<'a> MotionResolver<'a> {
197 pub fn new(text: &'a dyn TextSource, cursor: ByteOffset) -> Self {
199 Self {
200 text,
201 cursor: cursor.0,
202 opaque: &[],
203 geometry: &crate::DEFAULT_GEOMETRY,
204 }
205 }
206
207 pub fn from_view(view: DocumentView<'a>) -> Self {
209 Self {
210 text: view.text,
211 cursor: view.cursor.0,
212 opaque: view.opaque,
213 geometry: view.geometry,
214 }
215 }
216
217 pub(crate) fn span(&self, p: usize) -> Option<TextRange> {
218 let index = self.opaque.partition_point(|s| s.end.0 <= p);
219 self.opaque.get(index).copied().filter(|s| s.start.0 <= p)
220 }
221
222 pub fn next(&self, p: usize) -> usize {
224 self.span(p)
225 .map_or_else(|| self.text.next_grapheme(p), |s| s.end.0)
226 }
227
228 pub fn previous(&self, p: usize) -> usize {
230 let previous = self.text.prev_grapheme(p);
231 self.span(previous).map_or(previous, |s| s.start.0)
232 }
233
234 pub(crate) fn head(&self, p: usize) -> Option<char> {
235 if self.span(p).is_some() {
236 Some('\u{fffc}')
237 } else {
238 self.text.char_at(p)
239 }
240 }
241
242 pub(crate) fn kind(&self, p: usize, big: bool) -> u8 {
243 match self.head(p) {
244 None => 0,
245 Some(c) if c.is_whitespace() => 0,
246 Some(c) if big || c.is_alphanumeric() || c == '_' => 1,
247 Some(c) if c.is_ascii_punctuation() => 2,
248 _ => 3,
249 }
250 }
251
252 pub fn first_non_blank(&self, p: usize) -> usize {
254 let mut p = self.text.line_start(p);
255 let end = self.text.line_end(p);
256 while p < end && self.head(p).is_some_and(char::is_whitespace) {
257 p = self.next(p);
258 }
259 p
260 }
261
262 pub fn normal_cursor(&self, p: usize) -> usize {
264 let mut p = p.min(self.text.len_bytes());
266 while !self.text.is_char_boundary(p) {
267 p -= 1;
268 }
269 if p > 0 {
270 let previous = self.text.prev_grapheme(p);
271 if self.text.next_grapheme(previous) > p {
272 p = previous;
273 }
274 }
275 let p = self.span(p).map_or(p, |s| s.start.0);
276 if p < self.text.len_bytes() && !matches!(self.head(p), Some('\n' | '\r')) {
277 return p;
278 }
279 if p == 0 {
280 return 0;
281 }
282 let previous = self.previous(p);
283 if matches!(self.head(previous), Some('\n' | '\r')) {
284 p
285 } else {
286 previous
287 }
288 }
289
290 pub fn column(&self, p: usize) -> usize {
292 let mut start = self.text.line_start(p);
293 let mut col = 0;
294 while start < p {
295 let next = self.next(start);
296 col += self.geometry.width(&self.text.slice(start, next), col);
297 start = next;
298 }
299 col
300 }
301
302 pub fn at_column(&self, start: usize, col: usize) -> usize {
304 let end = self.text.line_end(start);
305 let mut p = start;
306 let mut column = 0;
307 while column < col && p < end {
308 let next = self.next(p);
309 let width = self.geometry.width(&self.text.slice(p, next), column);
310 if column + width > col {
311 break;
312 }
313 column += width;
314 p = next;
315 }
316 self.normal_cursor(p)
317 }
318
319 pub fn resolve(&self, motion: Motion, count: usize) -> Option<Target> {
322 let n = count.max(1);
323 let mut p = self.cursor;
324 let mut inclusive = false;
325 let mut linewise = false;
326 match motion {
327 Motion::Left => {
328 for _ in 0..n {
329 if p == 0 {
330 break;
331 }
332 let previous = self.previous(p);
333 if matches!(self.head(previous), Some('\n' | '\r')) {
334 break;
335 }
336 p = previous;
337 }
338 }
339 Motion::Right => {
340 for _ in 0..n {
341 if matches!(self.head(p), None | Some('\n' | '\r')) {
342 break;
343 }
344 let next = self.next(p);
345 if next >= self.text.len_bytes() || matches!(self.head(next), Some('\n' | '\r'))
346 {
347 break;
348 }
349 p = next;
350 }
351 }
352 Motion::Up | Motion::Down => {
353 let col = self.column(p);
354 let mut line = self.text.line_start(p);
355 for _ in 0..n {
356 if motion == Motion::Up {
357 if line == 0 {
358 break;
359 }
360 line = self.text.line_start(line - 1);
361 } else {
362 let next = self.text.next_line(line);
363 if next <= line || next >= self.text.len_bytes() {
364 break;
365 }
366 line = next;
367 }
368 }
369 p = self.at_column(line, col);
370 linewise = true;
371 }
372 Motion::LineStart => p = self.text.line_start(p),
373 Motion::FirstNonBlank => p = self.first_non_blank(p),
374 Motion::LineEnd => {
375 for _ in 1..n {
376 let next = self.text.next_line(p);
377 if next >= self.text.len_bytes() {
378 break;
379 }
380 p = next;
381 }
382 p = self.normal_cursor(self.text.line_end(p));
383 inclusive = true;
384 }
385 Motion::Word { destination, class } => {
386 let backward = matches!(
387 destination,
388 WordDestination::PreviousStart | WordDestination::PreviousEnd
389 );
390 let end = matches!(
391 destination,
392 WordDestination::NextEnd | WordDestination::PreviousEnd
393 );
394 let big = class == WordClass::BigWord;
395 p = self.word(n, backward, end, big);
396 inclusive = end;
397 }
398 Motion::FirstLine | Motion::LastLine | Motion::Percent => {
399 let last = if self.text.is_empty() {
400 0
401 } else {
402 self.text
403 .line_index(self.text.prev_grapheme(self.text.len_bytes()))
404 };
405 let line = match motion {
406 Motion::FirstLine => count.saturating_sub(1),
407 Motion::LastLine if count == 0 => last,
408 Motion::LastLine => count - 1,
409 _ => ((last + 1) * count.min(100))
410 .div_ceil(100)
411 .saturating_sub(1),
412 };
413 p = self.first_non_blank(self.text.line_offset(line.min(last)));
414 linewise = true;
415 }
416 Motion::Character { kind, character } => {
417 let backward = matches!(
418 kind,
419 CharacterMotion::FindBackward | CharacterMotion::TillBackward
420 );
421 let mut remaining = n;
422 let start = self.text.line_start(p);
423 let end = self.text.line_end(p);
424 loop {
425 if backward {
426 if p <= start {
427 return None;
428 }
429 p = self.previous(p);
430 } else {
431 p = self.next(p);
432 if p >= end {
433 return None;
434 }
435 }
436 if self.span(p).is_none() && self.head(p) == Some(character) {
437 remaining -= 1;
438 if remaining == 0 {
439 break;
440 }
441 }
442 }
443 match kind {
444 CharacterMotion::Till => p = self.previous(p),
445 CharacterMotion::TillBackward => p = self.next(p),
446 _ => {}
447 }
448 inclusive = !backward;
449 }
450 Motion::RepeatCharacter { kind, character } => {
451 let mut resolver = *self;
452 let motion = Motion::Character { kind, character };
453 if matches!(kind, CharacterMotion::Till | CharacterMotion::TillBackward)
454 && resolver
455 .resolve(motion, 1)
456 .is_some_and(|t| t.position.0 == self.cursor)
457 {
458 resolver.cursor = if kind == CharacterMotion::Till {
459 self.next(self.cursor)
460 } else {
461 self.previous(self.cursor)
462 };
463 }
464 return resolver.resolve(motion, count);
465 }
466 Motion::MatchDelimiter => {
467 p = self.match_delimiter()?;
468 inclusive = true;
469 }
470 Motion::Paragraph { backward } => {
471 p = self.paragraph_target(n, backward);
472 inclusive = p == self.normal_cursor(self.text.len_bytes()) && !backward;
473 }
474 Motion::Sentence { backward } => {
475 p = self.sentence_target(n, backward);
476 inclusive = p == self.normal_cursor(self.text.len_bytes()) && !backward;
477 }
478 }
479 Some(Target {
480 position: ByteOffset(p),
481 inclusive,
482 linewise,
483 })
484 }
485
486 fn word(&self, count: usize, backward: bool, end: bool, big: bool) -> usize {
487 let len = self.text.len_bytes();
488 let mut p = self.cursor;
489 for _ in 0..count {
490 if backward {
491 if p == 0 {
492 break;
493 }
494 if end && self.kind(p, big) != 0 {
495 let k = self.kind(p, big);
496 while p > 0 && self.kind(self.previous(p), big) == k {
497 p = self.previous(p);
498 }
499 }
500 if p == 0 {
501 break;
502 }
503 p = self.previous(p);
504 while p > 0 && self.kind(p, big) == 0 {
505 p = self.previous(p);
506 }
507 if !end {
508 let k = self.kind(p, big);
509 while p > 0 && self.kind(self.previous(p), big) == k {
510 p = self.previous(p);
511 }
512 }
513 } else {
514 let k = self.kind(p, big);
515 if end {
516 let next = self.next(p);
517 if next < len && k != 0 && self.kind(next, big) == k {
518 p = next;
519 } else {
520 p = next;
521 while p < len && self.kind(p, big) == 0 {
522 p = self.next(p);
523 }
524 }
525 if p < len {
526 let k = self.kind(p, big);
527 while self.next(p) < len && self.kind(self.next(p), big) == k {
528 p = self.next(p);
529 }
530 }
531 } else {
532 if p < len
534 && (p == 0 || self.text.byte_at(p - 1) == Some(b'\n'))
535 && matches!(self.head(p), Some('\n' | '\r') | None)
536 {
537 p = self.next(p);
538 } else {
539 while p < len && self.kind(p, big) == k {
540 p = self.next(p);
541 if k == 0 && (p == 0 || self.text.byte_at(p - 1) == Some(b'\n')) {
542 break;
543 }
544 }
545 }
546 while p < len && self.kind(p, big) == 0 {
547 if (p == 0 || self.text.byte_at(p - 1) == Some(b'\n'))
548 && matches!(self.head(p), Some('\n' | '\r') | None)
549 {
550 break;
551 }
552 p = self.next(p);
553 }
554 }
555 }
556 }
557 p.min(len)
558 }
559
560 pub fn operator_range(
563 &self,
564 motion: Motion,
565 count: usize,
566 operator: crate::Operator,
567 ) -> Option<ResolvedRange> {
568 let change = operator == crate::Operator::Change;
569 if motion == Motion::Right {
570 let mut end = self.cursor;
571 for _ in 0..count.max(1) {
572 if matches!(self.head(end), None | Some('\n' | '\r')) {
573 break;
574 }
575 end = self.next(end);
576 }
577 return (end > self.cursor || change)
578 .then(|| ResolvedRange::character(self.cursor, end));
579 }
580 let target = if let Motion::Word {
581 destination: WordDestination::NextStart,
582 class,
583 } = motion
584 {
585 let big = class == WordClass::BigWord;
586 if change && self.kind(self.cursor, big) != 0 {
587 let next = self.next(self.cursor);
589 let at_end = self.kind(next, big) != self.kind(self.cursor, big);
590 let remaining = count.max(1) - usize::from(at_end);
591 let end = self.word(remaining, false, true, big);
592 return Some(ResolvedRange::character(self.cursor, self.next(end)));
593 }
594 let mut last = *self;
595 last.cursor = self.word(count.max(1) - 1, false, false, big);
596 let mut end = last.word(1, false, false, big);
597 let line_end = self.text.line_end(last.cursor);
598 if end >= line_end
600 && self.text.line_start(last.cursor) > self.cursor
601 && self.cursor <= self.first_non_blank(self.cursor)
602 {
603 return Some(self.lines(self.cursor, last.cursor));
604 }
605 if end > line_end {
606 end = if self.text.line_start(last.cursor) == line_end {
607 self.text.next_line(last.cursor)
608 } else {
609 line_end
610 };
611 }
612 if change && end == self.cursor {
613 return Some(ResolvedRange::character(end, end));
614 }
615 Target {
616 position: ByteOffset(end),
617 inclusive: false,
618 linewise: false,
619 }
620 } else {
621 self.resolve(motion, count)?
622 };
623 if target.linewise {
624 if matches!(motion, Motion::Up | Motion::Down)
625 && self.text.line_start(target.position.0) == self.text.line_start(self.cursor)
626 {
627 return None;
628 }
629 return Some(self.lines(self.cursor, target.position.0));
630 }
631 if !target.inclusive {
632 return self.exclusive_range(target.position);
633 }
634 let start = self.cursor.min(target.position.0);
635 let end = self.next(self.cursor.max(target.position.0));
636 (start < end).then(|| ResolvedRange::character(start, end))
637 }
638
639 pub fn exclusive_range(&self, target: ByteOffset) -> Option<ResolvedRange> {
644 let start = self.cursor.min(target.0);
645 let mut end = self.cursor.max(target.0);
646 if end > start && self.text.line_start(end) == end {
647 if start <= self.first_non_blank(start) {
648 return Some(ResolvedRange {
649 ranges: vec![TextRange::new(self.text.line_start(start), end)],
650 shape: Shape::Line,
651 });
652 }
653 end = self.text.line_end(end - 1);
654 }
655 (start < end).then(|| ResolvedRange::character(start, end))
656 }
657
658 pub fn lines(&self, a: usize, b: usize) -> ResolvedRange {
660 ResolvedRange {
661 ranges: vec![TextRange::new(
662 self.text.line_start(a.min(b)),
663 self.text.next_line(a.max(b)),
664 )],
665 shape: Shape::Line,
666 }
667 }
668
669 pub fn expand_opaque(&self, range: TextRange) -> TextRange {
671 let mut range = range;
672 for span in self.opaque {
673 if span.start < range.end && range.start < span.end {
674 range.start = range.start.min(span.start);
675 range.end = range.end.max(span.end);
676 }
677 }
678 range
679 }
680
681 fn match_delimiter(&self) -> Option<usize> {
682 let mut p = self.cursor;
683 let end = self.text.line_end(p);
684 while p < end && !self.head(p).is_some_and(|c| "()[]{}".contains(c)) {
685 p = self.next(p);
686 }
687 let c = self.head(p)?;
688 let (open, close, backward) = match c {
689 '(' => ('(', ')', false),
690 ')' => ('(', ')', true),
691 '[' => ('[', ']', false),
692 ']' => ('[', ']', true),
693 '{' => ('{', '}', false),
694 '}' => ('{', '}', true),
695 _ => return None,
696 };
697 let mut depth = 1;
698 loop {
699 if backward {
700 if p == 0 {
701 return None;
702 }
703 p = self.previous(p);
704 } else {
705 p = self.next(p);
706 if p >= self.text.len_bytes() {
707 return None;
708 }
709 }
710 let c = self.head(p)?;
711 if c == if backward { close } else { open } {
712 depth += 1;
713 }
714 if c == if backward { open } else { close } {
715 depth -= 1;
716 if depth == 0 {
717 return Some(p);
718 }
719 }
720 }
721 }
722
723 fn paragraph_target(&self, count: usize, backward: bool) -> usize {
724 let mut line = self.text.line_start(self.cursor);
725 for _ in 0..count {
726 let mut skip_empty = self.text.line_end(line) == line;
727 loop {
728 if backward {
729 if line == 0 {
730 return 0;
731 }
732 line = self.text.line_start(line - 1);
733 } else {
734 let next = self.text.next_line(line);
735 if next <= line || next >= self.text.len_bytes() {
736 return self.normal_cursor(self.text.len_bytes());
737 }
738 line = next;
739 }
740 let empty = self.text.line_end(line) == line;
741 if skip_empty && empty {
742 continue;
743 }
744 skip_empty = false;
745 if empty {
746 break;
747 }
748 }
749 }
750 line
751 }
752}