| @@ -88,10 +88,11 @@ public struct Vim: Sendable { |
| 88 | public var clipboardRecord: (text: String, linewise: Bool)? |
88 | public var clipboardRecord: (text: String, linewise: Bool)? |
| 89 | var anchor = 0 |
89 | var anchor = 0 |
| 90 | var goalColumn: Int? |
90 | var goalColumn: Int? |
| 91 | var lastFind: (kind: Character, char: String)? |
91 | /// The last snipe: s, S, f, F, t or T. `consume` lands on the match (s, f, F) rather |
| 92 | var lastSnipe: (forward: Bool, chars: String, count: Int)? |
92 | /// than next to it (t, T); `keys` repeat it right after it, forward and back. |
| 93 | /// `;` and `,` repeat a snipe when one came after the last f, F, t or T. |
93 | var lastSnipe: (forward: Bool, chars: String, count: Int, consume: Bool, keys: (forward: KeyChord, backward: KeyChord))? |
| 94 | var lastWasSnipe = false |
94 | /// Set after a snipe: its keys repeat it as the next key, as evil-snipe's transient map has it. |
| |
95 | var snipeRepeatKeys: (forward: KeyChord, backward: KeyChord)? |
| 95 | var lastSearch: (pattern: String, forward: Bool)? |
96 | var lastSearch: (pattern: String, forward: Bool)? |
| 96 | /// The keys and inserted text of the last change, for `.`. |
97 | /// The keys and inserted text of the last change, for `.`. |
| 97 | var lastChange: (keys: [KeyChord], inserted: String?)? |
98 | var lastChange: (keys: [KeyChord], inserted: String?)? |
| @@ -129,6 +130,11 @@ public struct Vim: Sendable { |
| 129 | case .insert: |
130 | case .insert: |
| 130 | return insertKey(key, context) |
131 | return insertKey(key, context) |
| 131 | case .normal, .visual, .visualLine, .visualBlock: |
132 | case .normal, .visual, .visualLine, .visualBlock: |
| |
133 | var key = key |
| |
134 | if pending.isEmpty, let repeatKeys = snipeRepeatKeys { |
| |
135 | if key == repeatKeys.forward { key = KeyChord(";") } else if key == repeatKeys.backward { key = KeyChord(",") } |
| |
136 | } |
| |
137 | if pending.isEmpty { snipeRepeatKeys = nil } |
| 132 | pending.append(key) |
138 | pending.append(key) |
| 133 | let keys = pending |
139 | let keys = pending |
| 134 | caretForApply = context.caret |
140 | caretForApply = context.caret |
| @@ -611,36 +617,23 @@ public struct Vim: Sendable { |
| 611 | return .invalid |
617 | return .invalid |
| 612 | } |
618 | } |
| 613 | case ("f", []), ("F", []), ("t", []), ("T", []): |
619 | case ("f", []), ("F", []), ("t", []), ("T", []): |
| |
620 | // evil-snipe-override-mode: one-character snipes. |
| 614 | guard keys.count >= 2 else { return .incomplete } |
621 | guard keys.count >= 2 else { return .incomplete } |
| 615 | let char = keys[1].key == "SPC" ? " " : keys[1].key |
622 | let char = keys[1].key == "SPC" ? " " : keys[1].key == "TAB" ? "\t" : keys[1].key |
| 616 | guard char.count == 1 else { return .invalid } |
623 | guard char.count == 1 else { return .invalid } |
| 617 | lastFind = (Character(key.key), char) |
624 | let forward = key.key == "f" || key.key == "t" |
| 618 | lastWasSnipe = false |
625 | lastSnipe = (forward, char, n, key.key == "f" || key.key == "F", (KeyChord(key.key.lowercased()), KeyChord(key.key.uppercased()))) |
| 619 | guard let p = find(Character(key.key), char, buffer, c, count: n) else { return .invalid } |
626 | return snipe(forward: forward, count: n, buffer, c, operatorPending: operatorPending, repeating: false) |
| 620 | return .moved(p, key.key == "f" || key.key == "t" ? .inclusive : .exclusive) |
627 | case (";", []), (",", []): |
| 621 | case (";", []) where lastWasSnipe, (",", []) where lastWasSnipe: |
| |
| 622 | // evil-snipe-repeat, with Doom's `evil-snipe-repeat-scope` visible. |
628 | // evil-snipe-repeat, with Doom's `evil-snipe-repeat-scope` visible. |
| 623 | guard let last = lastSnipe else { return .invalid } |
629 | guard let last = lastSnipe else { return .invalid } |
| 624 | let forward = key.key == ";" ? last.forward : !last.forward |
630 | let forward = key.key == ";" ? last.forward : !last.forward |
| 625 | let scope = contextVisible.map { min($0.lowerBound, buffer.length)..<min($0.upperBound, buffer.length) } |
631 | return snipe(forward: forward, count: n * last.count, buffer, c, operatorPending: operatorPending, repeating: true) |
| 626 | guard let p = snipe(forward: forward, last.chars, buffer, c, count: n * last.count, scope: scope) else { return .invalid } |
| |
| 627 | return .moved(p, .exclusive) |
| |
| 628 | case (";", []), (",", []): |
| |
| 629 | guard let last = lastFind else { return .invalid } |
| |
| 630 | var kind = last.kind |
| |
| 631 | if key.key == "," { |
| |
| 632 | kind = ["f": "F", "F": "f", "t": "T", "T": "t"][kind]! |
| |
| 633 | } |
| |
| 634 | guard let p = find(kind, last.char, buffer, c, count: n, repeating: true) else { return .invalid } |
| |
| 635 | return .moved(p, kind == "f" || kind == "t" ? .inclusive : .exclusive) |
| |
| 636 | case ("s", []) where !operatorPending, ("S", []) where !operatorPending: |
632 | case ("s", []) where !operatorPending, ("S", []) where !operatorPending: |
| 637 | // evil-snipe with `evil-snipe-scope` line. |
| |
| 638 | guard keys.count >= 3 else { return .incomplete } |
633 | guard keys.count >= 3 else { return .incomplete } |
| 639 | let chars = keys[1].key + keys[2].key |
634 | let chars = keys[1].key + keys[2].key |
| 640 | lastSnipe = (key.key == "s", chars, n) |
635 | lastSnipe = (key.key == "s", chars, n, true, (KeyChord("s"), KeyChord("S"))) |
| 641 | lastWasSnipe = true |
636 | return snipe(forward: key.key == "s", count: n, buffer, c, operatorPending: operatorPending, repeating: false) |
| 642 | guard let p = snipe(forward: key.key == "s", chars, buffer, c, count: n) else { return .invalid } |
| |
| 643 | return .moved(p, .exclusive) |
| |
| 644 | case ("%", []): |
637 | case ("%", []): |
| 645 | guard let p = buffer.matchingBracket(c) else { return .invalid } |
638 | guard let p = buffer.matchingBracket(c) else { return .invalid } |
| 646 | return .moved(p, .inclusive) |
639 | return .moved(p, .inclusive) |
| @@ -686,51 +679,68 @@ public struct Vim: Sendable { |
| 686 | } |
679 | } |
| 687 | } |
680 | } |
| 688 | |
681 | |
| 689 | func find(_ kind: Character, _ char: String, _ buffer: Buffer, _ c: Int, count n: Int, repeating: Bool = false) -> Int? { |
682 | /// `evil-snipe--seek` for the last snipe: the `n`th match from `c` in the line after (or |
| 690 | let target = char.utf16.first! |
683 | /// before) the search start, or for a repeat in the visible text, with smart case and |
| 691 | let forward = kind == "f" || kind == "t" |
684 | /// `evil-snipe-skip-leading-whitespace`. |
| 692 | var p = c |
685 | mutating func snipe(forward: Bool, count n: Int, _ buffer: Buffer, _ c: Int, operatorPending: Bool, repeating: Bool) -> MotionResult { |
| 693 | for i in 0..<n { |
686 | guard let last = lastSnipe else { return .invalid } |
| 694 | var q = p |
687 | // f, s and F search from the next or this character, t and T one further. |
| 695 | // Repeating t/T starts past the character it stopped before. |
688 | let start = forward ? c + (last.consume ? 1 : 2) : c - (last.consume ? 0 : 1) |
| 696 | let skip = repeating && i == 0 && (kind == "t" || kind == "T") ? 1 : 0 |
689 | guard start >= 0, start <= buffer.length else { return .invalid } |
| 697 | if forward { |
690 | var bound = forward ? buffer.lineEnd(start) : buffer.lineStart(start) |
| 698 | q += 1 + skip |
691 | if repeating, let visible = contextVisible { |
| 699 | while q < buffer.lineEnd(c), buffer.char(q) != target { q += 1 } |
692 | bound = forward ? min(visible.upperBound, buffer.length) - 1 : min(visible.lowerBound, start) |
| 700 | guard q < buffer.lineEnd(c) else { return nil } |
693 | } |
| 701 | } else { |
694 | let blankKeys = last.chars.first == " " || last.chars.first == "\t" |
| 702 | q -= 1 + skip |
695 | var pattern = NSRegularExpression.escapedPattern(for: last.chars) |
| 703 | while q >= buffer.lineStart(c), buffer.char(q) != target { q -= 1 } |
696 | if blankKeys, let ch = buffer.char(start), Buffer.isBlank(ch) { pattern += "[^ \t]" } |
| 704 | guard q >= buffer.lineStart(c) else { return nil } |
697 | let regex = try! NSRegularExpression(pattern: pattern, options: last.chars == last.chars.lowercased() ? .caseInsensitive : []) |
| |
698 | var match: NSRange? |
| |
699 | if forward, start <= bound { |
| |
700 | var from = start |
| |
701 | for _ in 0..<n { |
| |
702 | match = regex.firstMatch(in: buffer.ns as String, range: NSRange(location: from, length: bound - from))?.range |
| |
703 | guard let found = match else { break } |
| |
704 | from = NSMaxRange(found) |
| |
705 | } |
| |
706 | } else if !forward { |
| |
707 | var from = start |
| |
708 | for _ in 0..<n { |
| |
709 | match = nil |
| |
710 | var p = from - 1 |
| |
711 | while p >= bound, match == nil { |
| |
712 | match = regex.firstMatch(in: buffer.ns as String, options: .anchored, range: NSRange(location: p, length: from - p))?.range |
| |
713 | p -= 1 |
| |
714 | } |
| |
715 | guard let found = match else { break } |
| |
716 | from = found.location |
| 705 | } |
717 | } |
| 706 | p = q |
| |
| 707 | } |
718 | } |
| 708 | if kind == "t" { return p - 1 } |
719 | guard let found = match else { |
| 709 | if kind == "T" { return p + 1 } |
720 | if repeating { snipeRepeatKeys = last.keys } |
| 710 | return p |
721 | return .invalid |
| 711 | } |
722 | } |
| 712 | |
723 | if !operatorPending { snipeRepeatKeys = last.keys } |
| 713 | /// Searches the caret's line, or `scope` when given. |
724 | var end = NSMaxRange(found) |
| 714 | func snipe(forward: Bool, _ chars: String, _ buffer: Buffer, _ c: Int, count n: Int, scope: Range<Int>? = nil) -> Int? { |
725 | var length = found.length |
| 715 | let range = scope ?? buffer.lineStart(c)..<buffer.lineEnd(c) |
726 | if blankKeys { |
| 716 | let line = buffer.substring(range) as NSString |
727 | end -= 1 |
| 717 | let start = range.lowerBound |
728 | length -= 1 |
| 718 | guard range.lowerBound <= c, c <= range.upperBound else { return nil } |
729 | } |
| 719 | let smart = chars == chars.lowercased() |
730 | let visual = mode == .visual || mode == .visualLine || mode == .visualBlock |
| 720 | var p = c - start |
731 | var p: Int |
| 721 | for _ in 0..<n { |
732 | if !forward { |
| 722 | let range: NSRange |
733 | p = last.consume ? found.location : end |
| 723 | if forward { |
734 | } else { |
| 724 | guard p + 1 < line.length else { return nil } |
735 | p = visual ? (last.consume ? end : found.location) : operatorPending ? end : found.location |
| 725 | range = line.range(of: chars, options: smart ? .caseInsensitive : [], range: NSRange(location: p + 1, length: line.length - p - 1)) |
736 | if last.consume { |
| |
737 | if visual { p -= 1 } |
| 726 | } else { |
738 | } else { |
| 727 | guard p > 0 else { return nil } |
739 | p -= length |
| 728 | range = line.range(of: chars, options: smart ? [.caseInsensitive, .backwards] : .backwards, range: NSRange(location: 0, length: p)) |
740 | if length > 1, !operatorPending { p += 1 } |
| 729 | } |
741 | } |
| 730 | guard range.location != NSNotFound else { return nil } |
| |
| 731 | p = range.location |
| |
| 732 | } |
742 | } |
| 733 | return start + p |
743 | return .moved(p, .exclusive) |
| 734 | } |
744 | } |
| 735 | |
745 | |
| 736 | // MARK: - Text objects |
746 | // MARK: - Text objects |