Commit d97c1b6580
Verified · cmc
Layout: unified · split
Sources/OrgCore/Keymap/Vim.swift +19 −5
| @@ -86,7 +86,9 @@ public struct Vim: Sendable { | |||
| 86 | var anchor = 0 | 86 | var anchor = 0 |
| 87 | var goalColumn: Int? | 87 | var goalColumn: Int? |
| 88 | var lastFind: (kind: Character, char: String)? | 88 | var lastFind: (kind: Character, char: String)? |
| 89 | var lastSnipe: (forward: Bool, chars: String)? | 89 | var lastSnipe: (forward: Bool, chars: String, count: Int)? |
| 90 | /// `;` and `,` repeat a snipe when one came after the last f, F, t or T. | ||
| 91 | var lastWasSnipe = false | ||
| 90 | var lastSearch: (pattern: String, forward: Bool)? | 92 | var lastSearch: (pattern: String, forward: Bool)? |
| 91 | /// The keys and inserted text of the last change, for `.`. | 93 | /// The keys and inserted text of the last change, for `.`. |
| 92 | var lastChange: (keys: [KeyChord], inserted: String?)? | 94 | var lastChange: (keys: [KeyChord], inserted: String?)? |
| @@ -610,8 +612,16 @@ public struct Vim: Sendable { | |||
| 610 | let char = keys[1].key == "SPC" ? " " : keys[1].key | 612 | let char = keys[1].key == "SPC" ? " " : keys[1].key |
| 611 | guard char.count == 1 else { return .invalid } | 613 | guard char.count == 1 else { return .invalid } |
| 612 | lastFind = (Character(key.key), char) | 614 | lastFind = (Character(key.key), char) |
| 615 | lastWasSnipe = false | ||
| 613 | guard let p = find(Character(key.key), char, buffer, c, count: n) else { return .invalid } | 616 | guard let p = find(Character(key.key), char, buffer, c, count: n) else { return .invalid } |
| 614 | return .moved(p, key.key == "f" || key.key == "t" ? .inclusive : .exclusive) | 617 | return .moved(p, key.key == "f" || key.key == "t" ? .inclusive : .exclusive) |
| 618 | case (";", []) where lastWasSnipe, (",", []) where lastWasSnipe: | ||
| 619 | // evil-snipe-repeat, with Doom's `evil-snipe-repeat-scope` visible. | ||
| 620 | guard let last = lastSnipe else { return .invalid } | ||
| 621 | let forward = key.key == ";" ? last.forward : !last.forward | ||
| 622 | let scope = contextVisible.map { min($0.lowerBound, buffer.length)..<min($0.upperBound, buffer.length) } | ||
| 623 | guard let p = snipe(forward: forward, last.chars, buffer, c, count: n * last.count, scope: scope) else { return .invalid } | ||
| 624 | return .moved(p, .exclusive) | ||
| 615 | case (";", []), (",", []): | 625 | case (";", []), (",", []): |
| 616 | guard let last = lastFind else { return .invalid } | 626 | guard let last = lastFind else { return .invalid } |
| 617 | var kind = last.kind | 627 | var kind = last.kind |
| @@ -624,7 +634,8 @@ public struct Vim: Sendable { | |||
| 624 | // evil-snipe with `evil-snipe-scope` line. | 634 | // evil-snipe with `evil-snipe-scope` line. |
| 625 | guard keys.count >= 3 else { return .incomplete } | 635 | guard keys.count >= 3 else { return .incomplete } |
| 626 | let chars = keys[1].key + keys[2].key | 636 | let chars = keys[1].key + keys[2].key |
| 627 | lastSnipe = (key.key == "s", chars) | 637 | lastSnipe = (key.key == "s", chars, n) |
| 638 | lastWasSnipe = true | ||
| 628 | guard let p = snipe(forward: key.key == "s", chars, buffer, c, count: n) else { return .invalid } | 639 | guard let p = snipe(forward: key.key == "s", chars, buffer, c, count: n) else { return .invalid } |
| 629 | return .moved(p, .exclusive) | 640 | return .moved(p, .exclusive) |
| 630 | case ("%", []): | 641 | case ("%", []): |
| @@ -696,9 +707,12 @@ public struct Vim: Sendable { | |||
| 696 | return p | 707 | return p |
| 697 | } | 708 | } |
| 698 | 709 | ||
| 699 | func snipe(forward: Bool, _ chars: String, _ buffer: Buffer, _ c: Int, count n: Int) -> Int? { | 710 | /// Searches the caret's line, or `scope` when given. |
| 700 | let line = buffer.substring(buffer.lineStart(c)..<buffer.lineEnd(c)) as NSString | 711 | func snipe(forward: Bool, _ chars: String, _ buffer: Buffer, _ c: Int, count n: Int, scope: Range<Int>? = nil) -> Int? { |
| 701 | let start = buffer.lineStart(c) | 712 | let range = scope ?? buffer.lineStart(c)..<buffer.lineEnd(c) |
| 713 | let line = buffer.substring(range) as NSString | ||
| 714 | let start = range.lowerBound | ||
| 715 | guard range.lowerBound <= c, c <= range.upperBound else { return nil } | ||
| 702 | let smart = chars == chars.lowercased() | 716 | let smart = chars == chars.lowercased() |
| 703 | var p = c - start | 717 | var p = c - start |
| 704 | for _ in 0..<n { | 718 | for _ in 0..<n { |
Tests/OrgCoreTests/VimTests.swift +22
| @@ -134,6 +134,11 @@ struct VimTests { | |||
| 134 | ("long line here\nab\nlonger line\n", 10, "j j x"), | 134 | ("long line here\nab\nlonger line\n", 10, "j j x"), |
| 135 | ("one two three\n", 0, "s t h x"), | 135 | ("one two three\n", 0, "s t h x"), |
| 136 | ("one two three\n", 12, "S t w x"), | 136 | ("one two three\n", 12, "S t w x"), |
| 137 | ("ab ab ab ab\n", 0, "s a b ; ; x"), | ||
| 138 | ("ab ab ab ab\n", 0, "s a b ; ; , x"), | ||
| 139 | ("ab ab ab ab\n", 10, "S a b ; x"), | ||
| 140 | ("ab ab ab ab ab ab\n", 0, "2 s a b ; x"), | ||
| 141 | ("one tw tw tw\n", 0, "f w s t w ; x"), | ||
| 137 | ("one two three\n", 0, "\" a y w w \" a P"), | 142 | ("one two three\n", 0, "\" a y w w \" a P"), |
| 138 | ("one two three\n", 0, "\" _ d w"), | 143 | ("one two three\n", 0, "\" _ d w"), |
| 139 | (" one\n", 2, "o x ESC"), | 144 | (" one\n", 2, "o x ESC"), |
| @@ -206,6 +211,23 @@ struct VimTests { | |||
| 206 | #expect(vim.feed(KeyChord("Q"), context) == [.ex("q!")]) | 211 | #expect(vim.feed(KeyChord("Q"), context) == [.ex("q!")]) |
| 207 | } | 212 | } |
| 208 | 213 | ||
| 214 | @Test func snipeRepeatsOverTheVisibleText() { | ||
| 215 | let text = "ab x\ncd ab\nab\n" | ||
| 216 | func caret(_ keys: String, visible: Range<Int>?) -> Int { | ||
| 217 | var driver = VimDriver(text, caret: 0) | ||
| 218 | driver.visible = visible | ||
| 219 | driver.type(Self.keys(keys)) | ||
| 220 | return driver.caret | ||
| 221 | } | ||
| 222 | // The snipe itself keeps to the line; repeats search what the window shows. | ||
| 223 | #expect(caret("s a b ;", visible: nil) == 0) | ||
| 224 | #expect(caret("s a b ;", visible: 0..<14) == 8) | ||
| 225 | #expect(caret("s a b ; ;", visible: 0..<14) == 11) | ||
| 226 | #expect(caret("s a b ; ; ,", visible: 0..<14) == 8) | ||
| 227 | // After f, ; repeats the f. | ||
| 228 | #expect(caret("s a b f x ;", visible: 0..<14) == 3) | ||
| 229 | } | ||
| 230 | |||
| 209 | @Test func halfPageScrollsFollowTheWindow() { | 231 | @Test func halfPageScrollsFollowTheWindow() { |
| 210 | #expect(driven(0, "C-d", halfPage: 5) == 35) | 232 | #expect(driven(0, "C-d", halfPage: 5) == 35) |
| 211 | #expect(driven(70, "C-u", halfPage: 5) == 35) | 233 | #expect(driven(70, "C-u", halfPage: 5) == 35) |