1use crate::buffer::{Buffer, Position};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum Operator {
10 Delete,
11 Change,
12 Yank,
13}
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum Motion {
17 Left,
18 Right,
19 Up,
20 Down,
21 WordForward,
22 WordBack,
23 WordEnd,
24 LineStart,
25 LineEnd,
26 FirstNonBlank,
27 FindForward(char),
29 FindBackward(char),
30 TillForward(char),
31 TillBackward(char),
32 LineDown,
34 LineUp,
35 WholeLine,
37 BufferEnd,
39 BufferStart,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum TextObject {
45 InnerWord,
46 AWord,
47 InnerQuote(char),
48 AQuote(char),
49 InnerBracket(char, char), ABracket(char, char),
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub struct EditRange {
55 pub start: Position,
56 pub end: Position,
57 pub linewise: bool,
58}
59
60impl EditRange {
61 pub fn new(start: Position, end: Position, linewise: bool) -> Self {
62 let (start, end) = order(start, end);
63 Self {
64 start,
65 end,
66 linewise,
67 }
68 }
69}
70
71fn order(a: Position, b: Position) -> (Position, Position) {
72 if a.row < b.row || (a.row == b.row && a.col <= b.col) {
73 (a, b)
74 } else {
75 (b, a)
76 }
77}
78
79pub fn range_for_motion(buf: &Buffer, motion: Motion, count: usize) -> EditRange {
82 let count = count.max(1);
83 let start = buf.cursor();
84 let mut tmp = buf.clone();
85 tmp.cursor = start;
86
87 match motion {
88 Motion::WholeLine => {
89 let row = start.row;
90 let end_row = (row + count - 1).min(buf.line_count().saturating_sub(1));
91 return EditRange {
92 start: Position::new(row, 0),
93 end: Position::new(end_row, buf.line(end_row).chars().count()),
94 linewise: true,
95 };
96 }
97 Motion::LineDown => {
98 let end_row = (start.row + count).min(buf.line_count().saturating_sub(1));
99 return EditRange {
100 start: Position::new(start.row, 0),
101 end: Position::new(end_row, buf.line(end_row).chars().count()),
102 linewise: true,
103 };
104 }
105 Motion::LineUp => {
106 let end_row = start.row.saturating_sub(count);
107 return EditRange {
108 start: Position::new(end_row, 0),
109 end: Position::new(start.row, buf.line(start.row).chars().count()),
110 linewise: true,
111 };
112 }
113 Motion::BufferEnd => {
114 let last = buf.line_count().saturating_sub(1);
115 return EditRange {
116 start: Position::new(start.row, 0),
117 end: Position::new(last, buf.line(last).chars().count()),
118 linewise: true,
119 };
120 }
121 Motion::BufferStart => {
122 return EditRange {
123 start: Position::new(0, 0),
124 end: Position::new(start.row, buf.line(start.row).chars().count()),
125 linewise: true,
126 };
127 }
128 _ => {}
129 }
130
131 for _ in 0..count {
132 match motion {
133 Motion::Left => tmp.move_left(),
134 Motion::Right => tmp.move_right(),
135 Motion::Up => tmp.move_up(),
136 Motion::Down => tmp.move_down(),
137 Motion::WordForward => tmp.move_word_forward(),
138 Motion::WordBack => tmp.move_word_back(),
139 Motion::WordEnd => move_word_end(&mut tmp),
140 Motion::LineStart => tmp.move_to_line_start(),
141 Motion::LineEnd => tmp.move_to_line_end(),
142 Motion::FirstNonBlank => tmp.move_to_first_non_blank(),
143 Motion::FindForward(c) => tmp.find_char_forward(c),
144 Motion::FindBackward(c) => tmp.find_char_backward(c),
145 Motion::TillForward(c) => tmp.till_char_forward(c),
146 Motion::TillBackward(c) => tmp.till_char_backward(c),
147 _ => {}
148 }
149 }
150
151 let end = tmp.cursor();
152 let end = match motion {
154 Motion::FindForward(_) | Motion::WordEnd => Position::new(end.row, end.col + 1),
155 Motion::FindBackward(_) => {
156 return EditRange::new(end, Position::new(start.row, start.col + 1), false);
158 }
159 _ => end,
160 };
161
162 if end.row < start.row || (end.row == start.row && end.col < start.col) {
164 EditRange::new(end, start, false)
165 } else {
166 EditRange {
167 start,
168 end,
169 linewise: false,
170 }
171 }
172}
173
174fn move_word_end(buf: &mut Buffer) {
175 let chars: Vec<char> = buf.line(buf.cursor.row).chars().collect();
176 let mut i = buf.cursor.col;
177 if i >= chars.len() {
178 if buf.cursor.row + 1 < buf.line_count() {
179 buf.cursor.row += 1;
180 buf.cursor.col = 0;
181 move_word_end(buf);
182 }
183 return;
184 }
185 if chars[i].is_whitespace() {
187 while i < chars.len() && chars[i].is_whitespace() {
188 i += 1;
189 }
190 if i >= chars.len() {
191 buf.cursor.col = chars.len();
192 return;
193 }
194 } else if i + 1 < chars.len() {
195 let class = word_class(chars[i]);
197 if i + 1 < chars.len() && word_class(chars[i + 1]) == class {
198 i += 1;
199 } else {
200 i += 1;
201 while i < chars.len() && chars[i].is_whitespace() {
202 i += 1;
203 }
204 }
205 } else {
206 i = chars.len().saturating_sub(1);
207 buf.cursor.col = i;
208 return;
209 }
210 if i >= chars.len() {
211 buf.cursor.col = chars.len().saturating_sub(1);
212 return;
213 }
214 let class = word_class(chars[i]);
215 while i + 1 < chars.len() && word_class(chars[i + 1]) == class {
216 i += 1;
217 }
218 buf.cursor.col = i;
219}
220
221#[derive(Clone, Copy, PartialEq)]
222enum WClass {
223 Space,
224 Word,
225 Punct,
226}
227
228fn word_class(c: char) -> WClass {
229 if c.is_whitespace() {
230 WClass::Space
231 } else if c.is_alphanumeric() || c == '_' {
232 WClass::Word
233 } else {
234 WClass::Punct
235 }
236}
237
238pub fn range_for_textobject(buf: &Buffer, obj: TextObject) -> Option<EditRange> {
239 let cur = buf.cursor();
240 match obj {
241 TextObject::InnerWord | TextObject::AWord => {
242 let line = buf.line(cur.row);
243 let chars: Vec<char> = line.chars().collect();
244 if chars.is_empty() {
245 return None;
246 }
247 let col = cur.col.min(chars.len().saturating_sub(1));
248 if chars[col].is_whitespace() {
249 if obj == TextObject::InnerWord {
250 return None;
251 }
252 let mut s = col;
254 let mut e = col;
255 while s > 0 && chars[s - 1].is_whitespace() {
256 s -= 1;
257 }
258 while e < chars.len() && chars[e].is_whitespace() {
259 e += 1;
260 }
261 return Some(EditRange {
262 start: Position::new(cur.row, s),
263 end: Position::new(cur.row, e),
264 linewise: false,
265 });
266 }
267 let class = word_class(chars[col]);
268 let mut s = col;
269 let mut e = col;
270 while s > 0 && word_class(chars[s - 1]) == class {
271 s -= 1;
272 }
273 while e < chars.len() && word_class(chars[e]) == class {
274 e += 1;
275 }
276 if obj == TextObject::AWord {
277 while e < chars.len() && chars[e].is_whitespace() {
279 e += 1;
280 }
281 if e == col + 1 || (s < e && e == s) {
282 while s > 0 && chars[s - 1].is_whitespace() {
284 s -= 1;
285 }
286 }
287 }
288 Some(EditRange {
289 start: Position::new(cur.row, s),
290 end: Position::new(cur.row, e),
291 linewise: false,
292 })
293 }
294 TextObject::InnerQuote(q) | TextObject::AQuote(q) => {
295 let line = buf.line(cur.row);
296 let chars: Vec<char> = line.chars().collect();
297 let mut opens = Vec::new();
299 for (i, &c) in chars.iter().enumerate() {
300 if c == q {
301 opens.push(i);
302 }
303 }
304 if opens.len() < 2 {
305 return None;
306 }
307 let mut best: Option<(usize, usize)> = None;
309 for pair in opens.chunks(2) {
310 if pair.len() < 2 {
311 break;
312 }
313 let a = pair[0];
314 let b = pair[1];
315 if cur.col >= a && cur.col <= b {
316 best = Some((a, b));
317 break;
318 }
319 }
320 let (a, b) = best.or_else(|| {
322 opens.chunks(2).find_map(|p| {
323 if p.len() == 2 {
324 Some((p[0], p[1]))
325 } else {
326 None
327 }
328 })
329 })?;
330 if matches!(obj, TextObject::InnerQuote(_)) {
331 Some(EditRange {
332 start: Position::new(cur.row, a + 1),
333 end: Position::new(cur.row, b),
334 linewise: false,
335 })
336 } else {
337 Some(EditRange {
338 start: Position::new(cur.row, a),
339 end: Position::new(cur.row, b + 1),
340 linewise: false,
341 })
342 }
343 }
344 TextObject::InnerBracket(open, close) | TextObject::ABracket(open, close) => {
345 find_bracket_range(buf, cur, open, close, matches!(obj, TextObject::ABracket(_, _)))
346 }
347 }
348}
349
350fn find_bracket_range(
351 buf: &Buffer,
352 cur: Position,
353 open: char,
354 close: char,
355 around: bool,
356) -> Option<EditRange> {
357 let mut depth = 0i32;
359 let mut open_pos: Option<Position> = None;
360
361 let mut r = cur.row as isize;
363 let mut started = false;
364 while r >= 0 {
365 let row = r as usize;
366 let chars: Vec<char> = buf.line(row).chars().collect();
367 let mut c_idx = if row == cur.row && !started {
368 started = true;
369 cur.col.min(chars.len())
370 } else {
371 chars.len()
372 };
373 while c_idx > 0 {
374 c_idx -= 1;
375 let ch = chars[c_idx];
376 if ch == close {
377 depth += 1;
378 } else if ch == open {
379 if depth == 0 {
380 open_pos = Some(Position::new(row, c_idx));
381 break;
382 }
383 depth -= 1;
384 }
385 }
386 if open_pos.is_some() {
387 break;
388 }
389 r -= 1;
390 }
391 let op = open_pos?;
392
393 depth = 0;
395 let mut close_pos: Option<Position> = None;
396 for row in op.row..buf.line_count() {
397 let chars: Vec<char> = buf.line(row).chars().collect();
398 let start_col = if row == op.row { op.col + 1 } else { 0 };
399 for (c_idx, &ch) in chars.iter().enumerate().skip(start_col) {
400 if ch == open {
401 depth += 1;
402 } else if ch == close {
403 if depth == 0 {
404 close_pos = Some(Position::new(row, c_idx));
405 break;
406 }
407 depth -= 1;
408 }
409 }
410 if close_pos.is_some() {
411 break;
412 }
413 }
414 let cp = close_pos?;
415
416 if around {
417 Some(EditRange {
418 start: op,
419 end: Position::new(cp.row, cp.col + 1),
420 linewise: false,
421 })
422 } else {
423 Some(EditRange {
424 start: Position::new(op.row, op.col + 1),
425 end: cp,
426 linewise: false,
427 })
428 }
429}
430
431pub fn extract_text(buf: &Buffer, range: EditRange) -> String {
433 if range.linewise {
434 let mut lines = Vec::new();
435 for row in range.start.row..=range.end.row.min(buf.line_count().saturating_sub(1)) {
436 lines.push(buf.line(row).to_string());
437 }
438 return lines.join("\n");
439 }
440 if range.start.row == range.end.row {
441 let chars: Vec<char> = buf.line(range.start.row).chars().collect();
442 let s = range.start.col.min(chars.len());
443 let e = range.end.col.min(chars.len());
444 if s >= e {
445 return String::new();
446 }
447 return chars[s..e].iter().collect();
448 }
449 let mut out = String::new();
450 let first: Vec<char> = buf.line(range.start.row).chars().collect();
451 let s = range.start.col.min(first.len());
452 out.extend(first[s..].iter());
453 out.push('\n');
454 for row in (range.start.row + 1)..range.end.row {
455 out.push_str(buf.line(row));
456 out.push('\n');
457 }
458 let last: Vec<char> = buf.line(range.end.row).chars().collect();
459 let e = range.end.col.min(last.len());
460 out.extend(last[..e].iter());
461 out
462}
463
464pub fn delete_range(buf: &mut Buffer, range: EditRange) -> String {
466 let text = extract_text(buf, range);
467 if range.linewise {
468 let start = range.start.row;
469 let end = range.end.row.min(buf.line_count().saturating_sub(1));
470 if start == 0 && end + 1 >= buf.line_count() {
471 *buf = Buffer::new();
473 return text;
474 }
475 for _ in start..=end {
476 if buf.line_count() == 1 {
477 buf.set_line(0, String::new());
478 break;
479 }
480 buf.cursor.row = start.min(buf.line_count().saturating_sub(1));
481 let _ = buf.delete_line();
482 }
483 buf.cursor.row = start.min(buf.line_count().saturating_sub(1));
484 buf.cursor.col = 0;
485 return text;
486 }
487
488 if range.start.row == range.end.row {
489 let line = buf.line(range.start.row);
490 let chars: Vec<char> = line.chars().collect();
491 let s = range.start.col.min(chars.len());
492 let e = range.end.col.min(chars.len());
493 let new_line: String = chars[..s].iter().chain(chars[e..].iter()).collect();
494 buf.set_line(range.start.row, new_line);
495 buf.cursor = Position::new(range.start.row, s);
496 return text;
497 }
498
499 let first_chars: Vec<char> = buf.line(range.start.row).chars().collect();
500 let last_chars: Vec<char> = buf.line(range.end.row).chars().collect();
501 let prefix: String = first_chars.iter().take(range.start.col).collect();
502 let suffix: String = last_chars.iter().skip(range.end.col).collect();
503 let merged = prefix + &suffix;
504
505 for row in (range.start.row + 1..=range.end.row).rev() {
507 if row < buf.line_count() {
508 buf.cursor.row = row;
509 let _ = buf.delete_line();
510 }
511 }
512 buf.set_line(range.start.row, merged);
513 buf.cursor = Position::new(range.start.row, range.start.col);
514 buf.clamp_col();
515 text
516}
517
518#[derive(Clone, Debug, PartialEq, Eq)]
519pub enum LastChange {
520 Operator {
521 op: Operator,
522 motion: Motion,
523 count: usize,
524 },
525 TextObject {
526 op: Operator,
527 obj: TextObject,
528 count: usize,
529 },
530 DeleteChar {
531 count: usize,
532 },
533 ReplaceChar {
534 ch: char,
535 },
536}
537
538pub fn parse_textobject(mod_char: char, obj_char: char) -> Option<TextObject> {
539 let inner = mod_char == 'i';
540 let around = mod_char == 'a';
541 if !inner && !around {
542 return None;
543 }
544 match obj_char {
545 'w' => Some(if inner {
546 TextObject::InnerWord
547 } else {
548 TextObject::AWord
549 }),
550 '"' | '\'' | '`' => Some(if inner {
551 TextObject::InnerQuote(obj_char)
552 } else {
553 TextObject::AQuote(obj_char)
554 }),
555 '(' | ')' | 'b' => Some(if inner {
556 TextObject::InnerBracket('(', ')')
557 } else {
558 TextObject::ABracket('(', ')')
559 }),
560 '[' | ']' => Some(if inner {
561 TextObject::InnerBracket('[', ']')
562 } else {
563 TextObject::ABracket('[', ']')
564 }),
565 '{' | '}' | 'B' => Some(if inner {
566 TextObject::InnerBracket('{', '}')
567 } else {
568 TextObject::ABracket('{', '}')
569 }),
570 '<' | '>' => Some(if inner {
571 TextObject::InnerBracket('<', '>')
572 } else {
573 TextObject::ABracket('<', '>')
574 }),
575 _ => None,
576 }
577}
578
579pub fn motion_from_char(c: char) -> Option<Motion> {
580 Some(match c {
581 'h' => Motion::Left,
582 'l' => Motion::Right,
583 'j' => Motion::LineDown,
584 'k' => Motion::LineUp,
585 'w' => Motion::WordForward,
586 'b' => Motion::WordBack,
587 'e' => Motion::WordEnd,
588 '0' => Motion::LineStart,
589 '$' => Motion::LineEnd,
590 '^' => Motion::FirstNonBlank,
591 _ => return None,
592 })
593}
594
595#[cfg(test)]
596mod tests {
597 use super::*;
598
599 #[test]
600 fn diw_deletes_word() {
601 let mut buf = Buffer::from_string("hello world");
602 buf.cursor = Position::new(0, 1);
603 let range = range_for_textobject(&buf, TextObject::InnerWord).unwrap();
604 let t = delete_range(&mut buf, range);
605 assert_eq!(t, "hello");
606 assert_eq!(buf.line(0), " world");
607 }
608
609 #[test]
610 fn di_quote() {
611 let mut buf = Buffer::from_string(r#"say "hi" now"#);
612 buf.cursor = Position::new(0, 5); let range = range_for_textobject(&buf, TextObject::InnerQuote('"')).unwrap();
614 let t = delete_range(&mut buf, range);
615 assert_eq!(t, "hi");
616 assert_eq!(buf.line(0), r#"say "" now"#);
617 }
618
619 #[test]
620 fn dw_motion() {
621 let mut buf = Buffer::from_string("foo bar");
622 buf.cursor = Position::new(0, 0);
623 let range = range_for_motion(&buf, Motion::WordForward, 1);
624 let t = delete_range(&mut buf, range);
625 assert_eq!(t, "foo ");
626 assert_eq!(buf.line(0), "bar");
627 }
628
629 #[test]
630 fn dd_linewise() {
631 let mut buf = Buffer::from_string("a\nb\nc");
632 buf.cursor = Position::new(1, 0);
633 let range = range_for_motion(&buf, Motion::WholeLine, 1);
634 let t = delete_range(&mut buf, range);
635 assert_eq!(t, "b");
636 assert_eq!(buf.text(), "a\nc");
637 }
638
639 #[test]
640 fn dib_parens() {
641 let mut buf = Buffer::from_string("x(hello)y");
642 buf.cursor = Position::new(0, 3);
643 let range = range_for_textobject(&buf, TextObject::InnerBracket('(', ')')).unwrap();
644 let t = delete_range(&mut buf, range);
645 assert_eq!(t, "hello");
646 assert_eq!(buf.line(0), "x()y");
647 }
648}