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