krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/OrgCore/Keymap/Vim.swift

322a87d47526d505d903e05d14e65a5464e1468f
orgstar/Sources/OrgCore/Keymap/Vim.swift history · blame · raw

1353 lines · 66016 bytes

   1import Foundation
   2
   3/// Modal editing as Doom's evil gives it: normal, insert, visual and visual-line states;
   4/// counts, registers, operators with motions and text objects, `.` repeat, evil-snipe's `s`
   5/// and `S`, `Y` to the end of the line, and the unnamed register on the system clipboard.
   6/// Keys go in; edits, moves and commands come out for the editor to apply.
   7public struct Vim: Sendable {
   8    public enum Mode: String, Sendable {
   9        case normal, insert, visual
  10        case visualLine = "visual-line"
  11        case visualBlock = "visual-block"
  12
  13        /// The keymap mode name for bindings.
  14        public var keymapMode: String {
  15            switch self {
  16            case .normal: "normal"
  17            case .insert: "insert"
  18            case .visual, .visualLine, .visualBlock: "visual"
  19            }
  20        }
  21    }
  22
  23    public enum Action: Equatable, Sendable {
  24        /// Replace text and put the caret (and, in visual states, the selection) somewhere.
  25        case edit([TextEdit], caret: Int)
  26        case move(caret: Int)
  27        /// The selection to show in a visual state, `lowerBound..<upperBound`.
  28        case select(Range<Int>)
  29        case command(String)
  30        case undo(Int)
  31        case redo(Int)
  32        case setClipboard(String)
  33        /// Ask for a search pattern (`/`, `?`) or an ex command (`:`).
  34        case prompt(String)
  35        /// `gq` and `gw`: fill these lines' paragraphs (`FillRegion`); the caret stays at
  36        /// `keep`, or without it goes to the last line's first non-blank.
  37        case fill(Range<Int>, keep: Int?)
  38        /// An ex command to run, as if typed after `:` (`ZZ` is `:wq`, `ZQ` is `:q!`).
  39        case ex(String)
  40        case message(String)
  41        /// `m`: a mark at this offset, for the editor to keep through edits.
  42        case setMark(Character, Int)
  43        /// A jump left this offset: the editor adds it to the jump list.
  44        case jumped(from: Int)
  45        /// C-o and C-i through the jump list.
  46        case jumpBack(Int)
  47        case jumpForward(Int)
  48        /// `q`: start recording keys into a register, or with nil stop.
  49        case recordMacro(Character?)
  50        /// `@`: play a register's keys, this many times.
  51        case playMacro(Character, Int)
  52        /// A block selection: one range per line.
  53        case selectBlock([Range<Int>])
  54        /// `gj`, `gk`: down (or, negative, up) by screen lines, which only the editor knows.
  55        case visualLines(Int)
  56    }
  57
  58    public struct Context: Sendable {
  59        public var text: String
  60        public var caret: Int
  61        public var clipboard: String?
  62        /// Lines in a half page, for C-d and C-u.
  63        public var halfPage: Int
  64        /// Marks where the editor keeps them now; `'` and `` ` `` are the last jump's start.
  65        public var marks: [Character: Int]
  66        /// The text the window shows, for H, M and L; nil when unknown.
  67        public var visible: Range<Int>?
  68        /// An Org buffer, where evil-org's keys apply.
  69        public var org: Bool
  70
  71        public init(text: String, caret: Int, clipboard: String? = nil, halfPage: Int = 15, marks: [Character: Int] = [:], visible: Range<Int>? = nil, org: Bool = false) {
  72            self.org = org
  73            self.text = text
  74            self.caret = caret
  75            self.clipboard = clipboard
  76            self.halfPage = halfPage
  77            self.marks = marks
  78            self.visible = visible
  79        }
  80    }
  81
  82    public internal(set) var mode: Mode = .normal
  83    /// Keys of the command being typed.
  84    public private(set) var pending: [KeyChord] = []
  85    /// Named registers. Evil's are global: an editor with several engines shares them.
  86    public var registers: [Character: (text: String, linewise: Bool)] = [:]
  87    /// What the clipboard held when this engine last wrote it, and whether that was whole lines.
  88    public var clipboardRecord: (text: String, linewise: Bool)?
  89    var anchor = 0
  90    var goalColumn: Int?
  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)?
  96    var lastSearch: (pattern: String, forward: Bool)?
  97    /// The keys and inserted text of the last change, for `.`.
  98    var lastChange: (keys: [KeyChord], inserted: String?)?
  99    var changeKeys: [KeyChord]?
 100    var insertStart = 0
 101    /// The caret when the command began, for commands that keep its column.
 102    var caretForApply = 0
 103    /// Typed in insert state since it began, kept by `inserted(_:)`.
 104    var insertedText = ""
 105    /// The last visual selection, for `gv`.
 106    var lastVisual: (anchor: Int, caret: Int, mode: Mode)?
 107    /// Marks the engine set this run, for a context without the editor's.
 108    var marks: [Character: Int] = [:]
 109    /// Where block `I`, `A` and `c` copy what was typed when insert state ends: the other lines,
 110    /// by number from 1, the column, and whether short lines are padded to it.
 111    var blockInsert: (lines: [Int], column: Int, pad: Bool)?
 112    /// The editor's marks for this key.
 113    var contextMarks: [Character: Int] = [:]
 114    /// The context's half page and visible text for this key.
 115    var contextHalfPage = 15
 116    var contextVisible: Range<Int>?
 117    var contextOrg = false
 118    /// Keys are going into a macro register.
 119    public internal(set) var recording = false
 120    var lastMacro: Character?
 121    /// `$` in block visual state: every line to its end.
 122    var blockToEnd = false
 123
 124    public init() {}
 125
 126    // MARK: - Feeding keys
 127
 128    public mutating func feed(_ key: KeyChord, _ context: Context) -> [Action] {
 129        switch mode {
 130        case .insert:
 131            return insertKey(key, context)
 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 }
 138            pending.append(key)
 139            let keys = pending
 140            caretForApply = context.caret
 141            contextMarks = context.marks
 142            contextHalfPage = context.halfPage
 143            contextVisible = context.visible
 144            contextOrg = context.org
 145            var buffer = Buffer(context)
 146            switch parse(keys, &buffer, context) {
 147            case .incomplete:
 148                return []
 149            case .invalid:
 150                pending = []
 151                return []
 152            case .done(let actions):
 153                pending = []
 154                return actions
 155            }
 156        }
 157    }
 158
 159    /// Text typed in insert state, recorded for `.`.
 160    public mutating func inserted(_ text: String) {
 161        insertedText += text
 162    }
 163
 164    public mutating func deletedBackward() {
 165        if !insertedText.isEmpty { insertedText.removeLast() }
 166    }
 167
 168    private mutating func insertKey(_ key: KeyChord, _ context: Context) -> [Action] {
 169        let buffer = Buffer(context)
 170        if key == KeyChord("ESC") || key == KeyChord("[", .control) || key == KeyChord("g", .control) {
 171            mode = .normal
 172            if var keys = changeKeys {
 173                keys.append(KeyChord("ESC"))
 174                lastChange = (keys, insertedText)
 175                changeKeys = nil
 176            }
 177            let caret = context.caret
 178            let lineStart = buffer.lineStart(caret)
 179            if let block = blockInsert {
 180                blockInsert = nil
 181                if !insertedText.isEmpty, !insertedText.contains("\n") { return replicateBlockInsert(block, buffer, caret: caret) }
 182            }
 183            return [.move(caret: caret > lineStart ? caret - 1 : caret)]
 184        }
 185        if key == KeyChord("w", .control) {
 186            let start = buffer.wordBackward(context.caret, bigWord: false, stopAtLine: true)
 187            guard start < context.caret else { return [] }
 188            return [.edit([TextEdit(range: start..<context.caret, replacement: "")], caret: start)]
 189        }
 190        if key == KeyChord("u", .control) {
 191            let start = buffer.firstNonBlank(buffer.lineStart(context.caret))
 192            let from = context.caret > start ? start : buffer.lineStart(context.caret)
 193            guard from < context.caret else { return [] }
 194            return [.edit([TextEdit(range: from..<context.caret, replacement: "")], caret: from)]
 195        }
 196        return []
 197    }
 198
 199    /// Whether a key in insert state belongs to the text system.
 200    public func passesThrough(_ key: KeyChord) -> Bool {
 201        guard mode == .insert else { return false }
 202        return !(key == KeyChord("ESC") || key == KeyChord("[", .control) || key == KeyChord("g", .control)
 203            || key == KeyChord("w", .control) || key == KeyChord("u", .control))
 204    }
 205
 206    public mutating func enterInsert(at caret: Int, keys: [KeyChord]) {
 207        mode = .insert
 208        insertStart = caret
 209        insertedText = ""
 210        changeKeys = keys
 211    }
 212
 213    // MARK: - Parsing
 214
 215    enum Parse {
 216        case incomplete
 217        case invalid
 218        case done([Action])
 219    }
 220
 221    struct Command {
 222        var register: Character?
 223        var count: Int?
 224        var keys: ArraySlice<KeyChord>
 225    }
 226
 227    /// `evil-shift-width`, which Doom keeps equal to `tab-width`: 8 in Org.
 228    static let shiftWidth = 8
 229
 230    /// What `indent-according-to-mode` gives a line after `line` in Org: under a list item,
 231    /// its text's column; under a heading, none; otherwise `line`'s own indentation.
 232    static func indentation(for buffer: Buffer, line: Int) -> String {
 233        let text = buffer.substring(buffer.lineStart(line)..<buffer.lineEnd(line))
 234        let item = try! NSRegularExpression(pattern: "^[ \\t]*([-+*]|[0-9]+[.)])[ \\t]+")
 235        let ns = text as NSString
 236        if text.hasPrefix("*") { return "" }
 237        if let m = item.firstMatch(in: text, range: NSRange(location: 0, length: ns.length)) {
 238            return String(repeating: " ", count: m.range.length)
 239        }
 240        return String(text.prefix { $0 == " " || $0 == "\t" })
 241    }
 242
 243    static func digit(_ key: KeyChord) -> Int? {
 244        guard key.modifiers.isEmpty, key.key.count == 1, let d = Int(key.key) else { return nil }
 245        return d
 246    }
 247
 248    /// Reads `"r` and a count off the front of `keys`.
 249    static func prefix(_ keys: [KeyChord]) -> (register: Character?, count: Int?, rest: ArraySlice<KeyChord>)? {
 250        var rest = keys[...]
 251        var register: Character?
 252        if rest.first?.key == "\"", rest.first?.modifiers.isEmpty == true {
 253            guard rest.count >= 2 else { return nil }
 254            register = rest[rest.startIndex + 1].key == "SPC" ? " " : rest[rest.startIndex + 1].key.first
 255            rest = rest.dropFirst(2)
 256        }
 257        var count: Int?
 258        while let key = rest.first, let d = digit(key), !(d == 0 && count == nil) {
 259            count = (count ?? 0) * 10 + d
 260            rest = rest.dropFirst()
 261        }
 262        return (register, count, rest)
 263    }
 264
 265    mutating func parse(_ keys: [KeyChord], _ buffer: inout Buffer, _ context: Context) -> Parse {
 266        guard let (register, count, rest) = Self.prefix(keys) else { return .incomplete }
 267        guard let first = rest.first else { return .incomplete }
 268        let visual = mode == .visual || mode == .visualLine
 269        let n = count ?? 1
 270        let caret = context.caret
 271
 272        if mode == .visualBlock { return parseBlock(rest, &buffer, context, register: register, count: n, keys: keys) }
 273        if let result = parseExtras(rest, &buffer, context, register: register, count: count, keys: keys) { return result }
 274
 275        // Operators.
 276        let operatorKeys: [String: String] = ["d": "d", "c": "c", "y": "y", ">": ">", "<": "<"]
 277        var op: String?
 278        var afterOp = rest.dropFirst()
 279        if first.modifiers.isEmpty, let o = operatorKeys[first.key] {
 280            op = o
 281        } else if first.key == "g", first.modifiers.isEmpty {
 282            guard let second = rest.dropFirst().first else { return .incomplete }
 283            if ["~", "u", "U", "c", "q", "w"].contains(second.key), second.modifiers.isEmpty {
 284                op = "g" + second.key
 285                afterOp = rest.dropFirst(2)
 286            }
 287        }
 288        if let op {
 289            if visual {
 290                let range = visualRange(buffer, caret)
 291                return .done(apply(op, range: range.range, linewise: range.linewise, buffer, register: register, keys: keys, count: n))
 292            }
 293            // A second count and the motion.
 294            guard let (_, count2, motionKeys) = Self.prefix(Array(afterOp)), let motionFirst = motionKeys.first else { return .incomplete }
 295            let total = n * (count2 ?? 1)
 296            let doubled = op.count == 1 ? op : String(op.last!)
 297            if motionFirst.key == doubled, motionFirst.modifiers.isEmpty, motionKeys.count == 1 || (op.count == 2 && motionFirst.key == String(op.last!)) {
 298                // dd, cc, yy, >>, <<, g~~, guu, gUU: whole lines.
 299                let startLine = buffer.lineStart(caret)
 300                var end = startLine
 301                for _ in 0..<total {
 302                    let next = buffer.lineEnd(end)
 303                    end = next < buffer.length ? next + 1 : next
 304                    if end >= buffer.length { break }
 305                }
 306                return .done(apply(op, range: startLine..<max(end, buffer.lineEnd(startLine)), linewise: true, buffer, register: register, keys: keys, count: total))
 307            }
 308            if motionFirst.key == "i" || motionFirst.key == "a", motionFirst.modifiers.isEmpty {
 309                guard motionKeys.count >= 2 else { return .incomplete }
 310                guard let object = textObject(motionFirst.key == "i", motionKeys[motionKeys.startIndex + 1], buffer, caret, count: total) else { return .invalid }
 311                // evil-org's subtree objects move point to the heading first.
 312                if motionKeys[motionKeys.startIndex + 1].key == "R" { caretForApply = buffer.lineStart(object.range.lowerBound) }
 313                return .done(apply(op, range: object.range, linewise: object.linewise, buffer, register: register, keys: keys, count: total))
 314            }
 315            var motionCaret = caret
 316            if op == "c", motionFirst.key == "w" || motionFirst.key == "W", motionFirst.modifiers.isEmpty,
 317               let c = buffer.char(caret), !Buffer.isBlank(c) {
 318                // cw acts as ce on a word.
 319                var end = caret
 320                for _ in 0..<total { end = buffer.wordEnd(end, bigWord: motionFirst.key == "W", fromInside: true) }
 321                return .done(apply(op, range: caret..<min(buffer.length, end + 1), linewise: false, buffer, register: register, keys: keys, count: total))
 322            }
 323            switch motion(Array(motionKeys), &buffer, &motionCaret, count: total, operatorPending: true) {
 324            case .incomplete: return .incomplete
 325            case .invalid: return .invalid
 326            case .moved(let target, let kind):
 327                var range: Range<Int>
 328                var linewise = false
 329                switch kind {
 330                case .linewise:
 331                    let a = buffer.lineStart(min(caret, target))
 332                    let b = buffer.lineEnd(max(caret, target))
 333                    range = a..<min(buffer.length, b + 1)
 334                    linewise = true
 335                case .inclusive:
 336                    range = min(caret, target)..<min(buffer.length, max(caret, target) + 1)
 337                case .exclusive:
 338                    range = min(caret, target)..<max(caret, target)
 339                    // dw at the end of a line stops at the line's end.
 340                    if motionFirst.key == "w" || motionFirst.key == "W", buffer.lineStart(range.upperBound) != buffer.lineStart(range.lowerBound), range.upperBound > range.lowerBound {
 341                        var end = buffer.lineEnd(range.lowerBound)
 342                        if end == range.lowerBound, total == 1 { end = min(buffer.length, end + 1) }
 343                        let upper = buffer.firstNonBlankOrEnd(buffer.lineStart(range.upperBound)) == range.upperBound
 344                            ? max(range.lowerBound, min(end, range.upperBound)) : range.upperBound
 345                        range = range.lowerBound..<(upper == range.lowerBound ? end : upper)
 346                        if buffer.lineStart(range.upperBound) != buffer.lineStart(range.lowerBound) {
 347                            range = range.lowerBound..<buffer.lineEnd(range.lowerBound)
 348                        }
 349                    }
 350                }
 351                return .done(apply(op, range: range, linewise: linewise, buffer, register: register, keys: keys, count: total))
 352            }
 353        }
 354
 355        if visual, first.modifiers.isEmpty {
 356            switch first.key {
 357            case "ESC":
 358                lastVisual = (anchor, caret, mode)
 359                mode = .normal
 360                return .done([.move(caret: buffer.clampNormal(caret))])
 361            case "o":
 362                let old = anchor
 363                anchor = caret
 364                return .done(visualActions(buffer, old))
 365            case "x":
 366                let range = visualRange(buffer, caret)
 367                return .done(apply("d", range: range.range, linewise: range.linewise, buffer, register: register, keys: keys, count: n))
 368            case "~", "u", "U":
 369                let range = visualRange(buffer, caret)
 370                return .done(apply(first.key == "~" ? "g~" : "g" + first.key, range: range.range, linewise: range.linewise, buffer, register: register, keys: keys, count: n))
 371            case "J":
 372                let range = visualRange(buffer, caret)
 373                let lines = max(2, buffer.lineCount(range.range))
 374                mode = .normal
 375                return .done(join(buffer, at: range.range.lowerBound, lines: lines, keys: keys))
 376            case "p", "P":
 377                let range = visualRange(buffer, caret)
 378                guard let (text, linewise) = paste(register, context) else { return .done([]) }
 379                mode = .normal
 380                var replacement = text
 381                if linewise, !range.linewise { replacement = "\n" + text }
 382                if !linewise, range.linewise { replacement += "\n" }
 383                let removed = buffer.substring(range.range)
 384                var actions: [Action] = [.edit([TextEdit(range: range.range, replacement: replacement)], caret: range.range.lowerBound + (linewise ? 0 : max(0, replacement.utf16.count - 1)))]
 385                actions += store(removed, linewise: range.linewise, register: nil, yank: false)
 386                return .done(actions)
 387            case "v":
 388                if mode == .visual {
 389                    mode = .normal
 390                    return .done([.move(caret: buffer.clampNormal(caret))])
 391                }
 392                mode = .visual
 393                return .done(visualActions(buffer, caret))
 394            case "V":
 395                if mode == .visualLine {
 396                    mode = .normal
 397                    return .done([.move(caret: buffer.clampNormal(caret))])
 398                }
 399                mode = .visualLine
 400                return .done(visualActions(buffer, caret))
 401            case "i", "a":
 402                guard rest.count >= 2 else { return .incomplete }
 403                guard let object = textObject(first.key == "i", rest[rest.startIndex + 1], buffer, caret, count: n) else { return .invalid }
 404                if object.linewise { mode = .visualLine }
 405                anchor = object.range.lowerBound
 406                let end = max(object.range.lowerBound, object.range.upperBound - 1)
 407                return .done(visualActions(buffer, end))
 408            default:
 409                break
 410            }
 411        }
 412
 413        if !visual {
 414            switch (first.key, first.modifiers) {
 415            case ("i", []), ("a", []), ("I", []), ("A", []), ("o", []), ("O", []):
 416                return .done(enterInsert(first.key, buffer, caret, keys: keys))
 417            case ("v", []):
 418                mode = .visual
 419                anchor = caret
 420                return .done(visualActions(buffer, caret))
 421            case ("V", []):
 422                mode = .visualLine
 423                anchor = caret
 424                return .done(visualActions(buffer, caret))
 425            case ("x", []), ("<delete>", []):
 426                guard let c = buffer.char(caret), c != 10 else { return .done([]) }
 427                let end = min(buffer.lineEnd(caret), caret + n)
 428                return .done(apply("d", range: caret..<end, linewise: false, buffer, register: register, keys: keys, count: n))
 429            case ("X", []):
 430                let start = max(buffer.lineStart(caret), caret - n)
 431                guard start < caret else { return .done([]) }
 432                return .done(apply("d", range: start..<caret, linewise: false, buffer, register: register, keys: keys, count: n))
 433            case ("D", []), ("C", []):
 434                var end = buffer.lineEnd(caret)
 435                for _ in 1..<max(1, n) where end < buffer.length { end = buffer.lineEnd(end + 1) }
 436                return .done(apply(first.key == "D" ? "d" : "c", range: caret..<end, linewise: false, buffer, register: register, keys: keys, count: n))
 437            case ("Y", []):
 438                // `evil-want-Y-yank-to-eol`.
 439                return .done(apply("y", range: caret..<buffer.lineEnd(caret), linewise: false, buffer, register: register, keys: keys, count: n))
 440            case ("p", []), ("P", []):
 441                guard let (text, linewise) = paste(register, context) else { return .done([]) }
 442                let repeated = String(repeating: text, count: n)
 443                if linewise {
 444                    let body = repeated.hasSuffix("\n") ? repeated : repeated + "\n"
 445                    if first.key == "p" {
 446                        let end = buffer.lineEnd(caret)
 447                        if end >= buffer.length {
 448                            let insertion = "\n" + String(body.dropLast())
 449                            return .done(change([TextEdit(range: end..<end, replacement: insertion)], caret: buffer.firstNonBlankIn(insertion, at: end + 1), keys: keys))
 450                        }
 451                        return .done(change([TextEdit(range: (end + 1)..<(end + 1), replacement: body)], caret: buffer.firstNonBlankIn(body, at: end + 1), keys: keys))
 452                    }
 453                    let start = buffer.lineStart(caret)
 454                    return .done(change([TextEdit(range: start..<start, replacement: body)], caret: buffer.firstNonBlankIn(body, at: start), keys: keys))
 455                }
 456                let at = first.key == "p" && buffer.char(caret).map({ $0 != 10 }) == true ? caret + 1 : caret
 457                return .done(change([TextEdit(range: at..<at, replacement: repeated)], caret: at + max(0, repeated.utf16.count - 1), keys: keys))
 458            case ("J", []):
 459                return .done(join(buffer, at: caret, lines: max(2, n), keys: keys))
 460            case ("~", []):
 461                let end = min(buffer.lineEnd(caret), caret + n)
 462                guard end > caret else { return .done([]) }
 463                let flipped = String(buffer.substring(caret..<end).map { $0.isUppercase ? Character($0.lowercased()) : Character($0.uppercased()) })
 464                return .done(change([TextEdit(range: caret..<end, replacement: flipped)], caret: buffer.clampNormal(end, in: buffer.lineStart(caret)), keys: keys))
 465            case ("r", []):
 466                guard rest.count >= 2 else { return .incomplete }
 467                let target = rest[rest.startIndex + 1]
 468                let char = target.key == "SPC" ? " " : target.key == "RET" ? "\n" : target.key
 469                guard char.count == 1, caret + n <= buffer.lineEnd(caret) else { return .done([]) }
 470                let replacement = char == "\n" ? "\n" : String(repeating: char, count: n)
 471                return .done(change([TextEdit(range: caret..<(caret + n), replacement: replacement)], caret: char == "\n" ? caret + 1 : caret + n - 1, keys: keys))
 472            case ("u", []):
 473                return .done([.undo(n)])
 474            case ("r", [.control]):
 475                return .done([.redo(n)])
 476            case (".", []):
 477                return .done(repeatChange(n, context))
 478            case (":", []):
 479                return .done([.prompt(":")])
 480            case ("/", []), ("?", []):
 481                return .done([.prompt(first.key)])
 482            case ("Z", []):
 483                guard rest.count >= 2 else { return .incomplete }
 484                switch rest[rest.startIndex + 1].key {
 485                case "Z": return .done([.ex("wq")])
 486                case "Q": return .done([.ex("q!")])
 487                default: return .invalid
 488                }
 489            default:
 490                break
 491            }
 492        }
 493
 494        if mode == .normal, !contextOrg, rest.count == 2, first == KeyChord("g"), rest.last == KeyChord("j") || rest.last == KeyChord("k") {
 495            return .done([.visualLines(rest.last == KeyChord("j") ? n : -n)])
 496        }
 497
 498        // Motions move the caret, or extend the selection in visual states.
 499        var target = caret
 500        switch motion(Array(rest), &buffer, &target, count: n, operatorPending: false) {
 501        case .incomplete: return .incomplete
 502        case .invalid: return .invalid
 503        case .moved(let to, _):
 504            if visual { return .done(visualActions(buffer, to)) }
 505            let jump: [Action] = Self.isJump(Array(rest)) && to != caret ? [.jumped(from: caret)] : []
 506            return .done(jump + [.move(caret: buffer.clampNormal(to))])
 507        }
 508    }
 509
 510    // MARK: - Motions
 511
 512    enum MotionKind { case exclusive, inclusive, linewise }
 513
 514    enum MotionResult {
 515        case incomplete
 516        case invalid
 517        case moved(Int, MotionKind)
 518    }
 519
 520    mutating func motion(_ keys: [KeyChord], _ buffer: inout Buffer, _ caret: inout Int, count n: Int, operatorPending: Bool) -> MotionResult {
 521        guard let key = keys.first else { return .incomplete }
 522        let c = caret
 523        func column(_ position: Int) -> Int { position - buffer.lineStart(position) }
 524        switch (key.key, key.modifiers) {
 525        case ("h", []), ("<left>", []), ("DEL", []):
 526            goalColumn = nil
 527            return .moved(max(buffer.lineStart(c), c - n), .exclusive)
 528        case ("l", []), ("<right>", []), ("SPC", []):
 529            goalColumn = nil
 530            let end = buffer.lineEnd(c)
 531            let limit = operatorPending ? end : max(buffer.lineStart(c), end - 1)
 532            return .moved(min(limit, c + n), .exclusive)
 533        case ("j", []), ("k", []), ("<down>", []), ("<up>", []), ("d", [.control]), ("u", [.control]), ("+", []), ("-", []), ("RET", []):
 534            let down = ["j", "<down>", "+", "RET"].contains(key.key) || key == KeyChord("d", .control)
 535            let lines = key.modifiers.contains(.control) ? (n == 1 ? contextHalfPage : n) : n
 536            let goal = goalColumn ?? column(c)
 537            var line = buffer.lineStart(c)
 538            var moved = 0
 539            for _ in 0..<lines {
 540                if down {
 541                    let end = buffer.lineEnd(line)
 542                    guard end < buffer.length, end + 1 <= buffer.length else { break }
 543                    if end + 1 == buffer.length { break }
 544                    line = end + 1
 545                } else {
 546                    guard line > 0 else { break }
 547                    line = buffer.lineStart(line - 1)
 548                }
 549                moved += 1
 550            }
 551            if moved == 0, !key.modifiers.contains(.control) { return .invalid }
 552            if ["+", "-", "RET"].contains(key.key) { return .moved(buffer.firstNonBlank(line), .linewise) }
 553            goalColumn = goal
 554            let length = buffer.lineEnd(line) - line
 555            return .moved(line + min(goal, operatorPending ? length : max(0, length - 1)), .linewise)
 556        case ("w", []), ("W", []):
 557            goalColumn = nil
 558            var p = c
 559            for _ in 0..<n { p = buffer.wordForward(p, bigWord: key.key == "W") }
 560            return .moved(p, .exclusive)
 561        case ("b", []), ("B", []):
 562            goalColumn = nil
 563            var p = c
 564            for _ in 0..<n { p = buffer.wordBackward(p, bigWord: key.key == "B", stopAtLine: false) }
 565            return .moved(p, .exclusive)
 566        case ("e", []), ("E", []):
 567            goalColumn = nil
 568            var p = c
 569            for _ in 0..<n { p = buffer.wordEnd(p, bigWord: key.key == "E", fromInside: false) }
 570            return .moved(p, .inclusive)
 571        case ("0", []), ("<home>", []):
 572            goalColumn = nil
 573            return .moved(buffer.lineStart(c), .exclusive)
 574        case ("^", []):
 575            goalColumn = nil
 576            return .moved(buffer.firstNonBlank(buffer.lineStart(c)), .exclusive)
 577        case ("$", []), ("<end>", []):
 578            var line = buffer.lineStart(c)
 579            for _ in 1..<max(1, n) where buffer.lineEnd(line) < buffer.length { line = buffer.lineEnd(line) + 1 }
 580            goalColumn = Int.max
 581            let end = buffer.lineEnd(line)
 582            if mode == .visual { return .moved(end, .inclusive) }
 583            return .moved(max(line, end - 1), .inclusive)
 584        case ("G", []):
 585            // `evil-start-of-line` nil: the column stays.
 586            let line = n > 1 || keys.count > 1 ? buffer.lineOffset(n) : buffer.lineStart(buffer.length)
 587            let column = c - buffer.lineStart(c)
 588            let length = buffer.lineEnd(line) - line
 589            return .moved(line + min(column, operatorPending ? length : max(0, length - 1)), .linewise)
 590        case ("g", []):
 591            guard keys.count >= 2 else { return .incomplete }
 592            switch keys[1].key {
 593            case "g":
 594                let line = buffer.lineOffset(n)
 595                let column = c - buffer.lineStart(c)
 596                let length = buffer.lineEnd(line) - line
 597                return .moved(line + min(column, operatorPending ? length : max(0, length - 1)), .linewise)
 598            case "e", "E":
 599                var p = c
 600                for _ in 0..<n { p = buffer.wordEndBackward(p, bigWord: keys[1].key == "E") }
 601                return .moved(p, .inclusive)
 602            case "j" where contextOrg, "k" where contextOrg:
 603                // evil-org: org-forward-element and org-backward-element, which take no count.
 604                guard let p = Self.orgObject(keys[1].key, inner: false, buffer, c)?.range.lowerBound, p != c else { return .invalid }
 605                goalColumn = nil
 606                return .moved(p, .exclusive)
 607            case "j":
 608                return motion([KeyChord("j")], &buffer, &caret, count: n, operatorPending: operatorPending)
 609            case "k":
 610                return motion([KeyChord("k")], &buffer, &caret, count: n, operatorPending: operatorPending)
 611            case "_":
 612                let end = buffer.lineEnd(c)
 613                var p = end
 614                while p > buffer.lineStart(c), let ch = buffer.char(p - 1), Buffer.isBlank(ch) { p -= 1 }
 615                return .moved(max(buffer.lineStart(c), p - 1), .inclusive)
 616            default:
 617                return .invalid
 618            }
 619        case ("f", []), ("F", []), ("t", []), ("T", []):
 620            // evil-snipe-override-mode: one-character snipes.
 621            guard keys.count >= 2 else { return .incomplete }
 622            let char = keys[1].key == "SPC" ? " " : keys[1].key == "TAB" ? "\t" : keys[1].key
 623            guard char.count == 1 else { return .invalid }
 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 (";", []), (",", []):
 628            // evil-snipe-repeat, with Doom's `evil-snipe-repeat-scope` visible.
 629            guard let last = lastSnipe else { return .invalid }
 630            let forward = key.key == ";" ? last.forward : !last.forward
 631            return snipe(forward: forward, count: n * last.count, buffer, c, operatorPending: operatorPending, repeating: true)
 632        case ("s", []) where !operatorPending, ("S", []) where !operatorPending:
 633            guard keys.count >= 3 else { return .incomplete }
 634            let chars = keys[1...2].map { $0.key == "SPC" ? " " : $0.key == "TAB" ? "\t" : $0.key }.joined()
 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)
 637        case ("%", []):
 638            guard let p = buffer.matchingBracket(c) else { return .invalid }
 639            return .moved(p, .inclusive)
 640        case ("'", []), ("`", []):
 641            guard keys.count >= 2 else { return .incomplete }
 642            let name = keys[1].key == "'" || keys[1].key == "`" ? Character("'") : keys[1].key.first
 643            guard let name, let mark = currentMarks[name] else { return .invalid }
 644            goalColumn = nil
 645            let p = min(mark, buffer.length)
 646            return key.key == "'" ? .moved(buffer.firstNonBlank(buffer.lineStart(p)), .linewise) : .moved(p, .exclusive)
 647        case ("}", []), ("{", []):
 648            var p = c
 649            for _ in 0..<n { p = buffer.paragraph(p, forward: key.key == "}") }
 650            return .moved(p, .exclusive)
 651        case ("n", []), ("N", []):
 652            guard let search = lastSearch else { return .invalid }
 653            let forward = key.key == "n" ? search.forward : !search.forward
 654            guard let p = buffer.search(search.pattern, from: c, forward: forward, count: n) else { return .invalid }
 655            return .moved(p, .exclusive)
 656        case ("*", []), ("#", []):
 657            guard let word = buffer.wordAt(c) else { return .invalid }
 658            let pattern = "\\b" + NSRegularExpression.escapedPattern(for: word) + "\\b"
 659            lastSearch = (pattern, key.key == "*")
 660            guard let p = buffer.search(pattern, from: buffer.wordStart(c), forward: key.key == "*", count: n) else { return .invalid }
 661            return .moved(p, .exclusive)
 662        case ("H", []), ("M", []), ("L", []):
 663            // The visible lines: H the count-th from the top, L from the bottom, M the middle.
 664            guard let visible = contextVisible else { return .invalid }
 665            goalColumn = nil
 666            let lower = min(visible.lowerBound, buffer.length)
 667            let last = buffer.lineStart(max(lower, min(visible.upperBound, buffer.length) - 1))
 668            var lines: [Int] = []
 669            var line = buffer.lineStart(lower)
 670            while true {
 671                lines.append(line)
 672                guard line < last, buffer.lineEnd(line) < buffer.length else { break }
 673                line = buffer.lineEnd(line) + 1
 674            }
 675            let index = key.key == "H" ? min(n, lines.count) - 1 : key.key == "L" ? max(0, lines.count - n) : (lines.count - 1) / 2
 676            return .moved(buffer.firstNonBlank(lines[index]), .linewise)
 677        default:
 678            return .invalid
 679        }
 680    }
 681
 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,
 684    /// Doom's `evil-snipe-char-fold` and `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        let regexps = last.chars.map(CharFold.regexp)
 696        var pattern = regexps.map(CharFold.icu).joined()
 697        if blankKeys, let ch = buffer.char(start), Buffer.isBlank(ch) { pattern += "[^ \t]" }
 698        // `evil-snipe--case-p`: case matters when the regexps hold a capital letter.
 699        let caseFold = !regexps.joined().unicodeScalars.contains { ("A"..."Z").contains($0) }
 700        let regex = try! NSRegularExpression(pattern: pattern, options: caseFold ? .caseInsensitive : [])
 701        var match: NSRange?
 702        if forward, start <= bound {
 703            var from = start
 704            for _ in 0..<n {
 705                match = regex.firstMatch(in: buffer.ns as String, range: NSRange(location: from, length: bound - from))?.range
 706                guard let found = match else { break }
 707                from = NSMaxRange(found)
 708            }
 709        } else if !forward {
 710            var from = start
 711            for _ in 0..<n {
 712                match = nil
 713                var p = from - 1
 714                while p >= bound, match == nil {
 715                    match = regex.firstMatch(in: buffer.ns as String, options: .anchored, range: NSRange(location: p, length: from - p))?.range
 716                    p -= 1
 717                }
 718                guard let found = match else { break }
 719                from = found.location
 720            }
 721        }
 722        guard let found = match else {
 723            if repeating { snipeRepeatKeys = last.keys }
 724            return .invalid
 725        }
 726        if !operatorPending { snipeRepeatKeys = last.keys }
 727        var end = NSMaxRange(found)
 728        var length = found.length
 729        if blankKeys {
 730            end -= 1
 731            length -= 1
 732        }
 733        let visual = mode == .visual || mode == .visualLine || mode == .visualBlock
 734        var p: Int
 735        if !forward {
 736            p = last.consume ? found.location : end
 737        } else {
 738            p = visual ? (last.consume ? end : found.location) : operatorPending ? end : found.location
 739            if last.consume {
 740                if visual { p -= 1 }
 741            } else {
 742                p -= length
 743                if length > 1, !operatorPending { p += 1 }
 744            }
 745        }
 746        return .moved(p, .exclusive)
 747    }
 748
 749    // MARK: - Text objects
 750
 751    func textObject(_ inner: Bool, _ key: KeyChord, _ buffer: Buffer, _ c: Int, count: Int) -> (range: Range<Int>, linewise: Bool)? {
 752        switch key.key {
 753        case "w", "W":
 754            return buffer.wordObject(c, inner: inner, bigWord: key.key == "W", count: count).map { ($0, false) }
 755        case "\"", "'", "`":
 756            return buffer.quoteObject(c, quote: key.key.utf16.first!, inner: inner).map { ($0, false) }
 757        case "(", ")", "b":
 758            return buffer.bracketObject(c, open: 40, close: 41, inner: inner).map { ($0, false) }
 759        case "[", "]":
 760            return buffer.bracketObject(c, open: 91, close: 93, inner: inner).map { ($0, false) }
 761        case "{", "}", "B":
 762            return buffer.bracketObject(c, open: 123, close: 125, inner: inner).map { ($0, false) }
 763        case "<", ">":
 764            return buffer.bracketObject(c, open: 60, close: 62, inner: inner).map { ($0, false) }
 765        case "p":
 766            return buffer.paragraphObject(c, inner: inner).map { ($0, true) }
 767        case "e", "E", "r", "R":
 768            return Self.orgObject(key.key, inner: inner, buffer, c)
 769        default:
 770            return nil
 771        }
 772    }
 773
 774    // MARK: - Applying
 775
 776    func visualRange(_ buffer: Buffer, _ caret: Int) -> (range: Range<Int>, linewise: Bool) {
 777        let a = min(anchor, caret)
 778        let b = max(anchor, caret)
 779        if mode == .visualLine {
 780            let end = buffer.lineEnd(b)
 781            return (buffer.lineStart(a)..<min(buffer.length, end + 1), true)
 782        }
 783        return (a..<min(buffer.length, b + 1), false)
 784    }
 785
 786    func visualActions(_ buffer: Buffer, _ caret: Int) -> [Action] {
 787        let a = min(anchor, caret)
 788        let b = max(anchor, caret)
 789        let range = mode == .visualLine ? buffer.lineStart(a)..<buffer.lineEnd(b) : a..<min(buffer.length, b + 1)
 790        return [.move(caret: caret), .select(range)]
 791    }
 792
 793    mutating func store(_ text: String, linewise: Bool, register: Character?, yank: Bool) -> [Action] {
 794        if register == "_" { return [] }
 795        if let register, register.isLetter {
 796            let key = Character(register.lowercased())
 797            if register.isUppercase, let existing = registers[key] {
 798                registers[key] = (existing.text + text, existing.linewise || linewise)
 799            } else {
 800                registers[key] = (text, linewise)
 801            }
 802            return []
 803        }
 804        if yank { registers["0"] = (text, linewise) }
 805        clipboardRecord = (text, linewise)
 806        return [.setClipboard(text)]
 807    }
 808
 809    func paste(_ register: Character?, _ context: Context) -> (String, Bool)? {
 810        if let register, register != "\"", register != "+", register != "*" {
 811            return registers[Character(register.lowercased())]
 812        }
 813        guard let clip = context.clipboard ?? clipboardRecord?.text else { return nil }
 814        if let record = clipboardRecord, record.text == clip { return (clip, record.linewise) }
 815        return (clip, false)
 816    }
 817
 818    mutating func change(_ edits: [TextEdit], caret: Int, keys: [KeyChord]) -> [Action] {
 819        lastChange = (keys, nil)
 820        return [.edit(edits, caret: caret)]
 821    }
 822
 823    mutating func apply(_ op: String, range: Range<Int>, linewise: Bool, _ buffer: Buffer, register: Character?, keys: [KeyChord], count: Int) -> [Action] {
 824        let wasVisual = mode == .visual || mode == .visualLine
 825        if wasVisual { lastVisual = (anchor, caretForApply, mode) }
 826        let context0Column = max(0, caretForApply - buffer.lineStart(caretForApply))
 827        mode = .normal
 828        let text = buffer.substring(range)
 829        switch op {
 830        case "y":
 831            var actions = store(text, linewise: linewise, register: register, yank: true)
 832            let caret = linewise && !wasVisual ? min(range.lowerBound, buffer.length) : range.lowerBound
 833            actions.append(.move(caret: linewise && wasVisual ? buffer.lineStart(range.lowerBound) : caret))
 834            return actions
 835        case "d":
 836            var actions = store(text, linewise: linewise, register: register, yank: false)
 837            var deletion = range
 838            if linewise, range.upperBound >= buffer.length, range.lowerBound > 0, buffer.char(range.upperBound - 1) != 10 || range.upperBound == buffer.length {
 839                // The last line takes the newline before it.
 840                if buffer.char(range.lowerBound - 1) == 10, range.upperBound == buffer.length, !(text.hasSuffix("\n")) {
 841                    deletion = (range.lowerBound - 1)..<range.upperBound
 842                }
 843            }
 844            let after = buffer.removing(deletion)
 845            var caret = deletion.lowerBound
 846            if linewise {
 847                let column = context0Column
 848                let line = after.lineStart(min(deletion.lowerBound, after.length))
 849                let length = after.lineEnd(line) - line
 850                caret = line + min(column, max(0, length - 1))
 851            } else {
 852                caret = after.clampNormal(caret)
 853            }
 854            lastChange = (wasVisual ? [] : keys, nil)
 855            actions.insert(.edit([TextEdit(range: deletion, replacement: "")], caret: caret), at: 0)
 856            return actions
 857        case "c":
 858            var actions = store(text, linewise: linewise, register: register, yank: false)
 859            var deletion = range
 860            var replacement = ""
 861            if linewise {
 862                // cc keeps the newline and indents as the line before it calls for.
 863                replacement = range.lowerBound == 0 ? "" : Self.indentation(for: buffer, line: range.lowerBound - 1)
 864                if buffer.char(range.upperBound - 1) == 10 { deletion = range.lowerBound..<(range.upperBound - 1) }
 865            }
 866            let caret = deletion.lowerBound + replacement.utf16.count
 867            actions.insert(.edit([TextEdit(range: deletion, replacement: replacement)], caret: caret), at: 0)
 868            enterInsert(at: caret, keys: wasVisual ? [] : keys)
 869            return actions
 870        case ">", "<":
 871            let start = buffer.lineStart(range.lowerBound)
 872            var edits: [TextEdit] = []
 873            var line = start
 874            let end = linewise ? range.upperBound : max(range.upperBound, buffer.lineEnd(max(range.lowerBound, range.upperBound - 1)))
 875            while line < end || (line == start && line <= buffer.length) {
 876                let lineEnd = buffer.lineEnd(line)
 877                if lineEnd > line {
 878                    if op == ">" {
 879                        edits.append(TextEdit(range: line..<line, replacement: String(repeating: " ", count: Self.shiftWidth)))
 880                    } else {
 881                        let indent = buffer.substring(line..<buffer.firstNonBlankOrEnd(line))
 882                        let remove = min(Self.shiftWidth, indent.count)
 883                        if remove > 0 { edits.append(TextEdit(range: line..<(line + remove), replacement: "")) }
 884                    }
 885                }
 886                guard lineEnd < buffer.length else { break }
 887                line = lineEnd + 1
 888                if line >= end { break }
 889            }
 890            lastChange = (wasVisual ? [] : keys, nil)
 891            return [.edit(edits, caret: start)]
 892        case "gc":
 893            lastChange = (wasVisual ? [] : keys, nil)
 894            return [.edit(Self.toggleComments(buffer, range), caret: buffer.lineStart(range.lowerBound))]
 895        case "gq", "gw":
 896            // Whole lines, as evil's fill operators take them.
 897            lastChange = (wasVisual ? [] : keys, nil)
 898            let end = buffer.lineEnd(max(range.lowerBound, range.upperBound - 1))
 899            let lines = buffer.lineStart(range.lowerBound)..<min(buffer.length, end + 1)
 900            return [.fill(lines, keep: op == "gw" ? caretForApply : nil)]
 901        default:
 902            let transformed: String
 903            switch op {
 904            case "gu": transformed = text.lowercased()
 905            case "gU": transformed = text.uppercased()
 906            default: transformed = String(text.map { $0.isUppercase ? Character($0.lowercased()) : Character($0.uppercased()) })
 907            }
 908            lastChange = (wasVisual ? [] : keys, nil)
 909            return [.edit([TextEdit(range: range, replacement: transformed)], caret: range.lowerBound)]
 910        }
 911    }
 912
 913    mutating func enterInsert(_ key: String, _ buffer: Buffer, _ caret: Int, keys: [KeyChord]) -> [Action] {
 914        switch key {
 915        case "i":
 916            enterInsert(at: caret, keys: keys)
 917            return [.move(caret: caret)]
 918        case "a":
 919            let p = buffer.char(caret).map { $0 != 10 } == true ? caret + 1 : caret
 920            enterInsert(at: p, keys: keys)
 921            return [.move(caret: p)]
 922        case "I":
 923            let p = buffer.firstNonBlank(buffer.lineStart(caret))
 924            enterInsert(at: p, keys: keys)
 925            return [.move(caret: p)]
 926        case "A":
 927            let p = buffer.lineEnd(caret)
 928            enterInsert(at: p, keys: keys)
 929            return [.move(caret: p)]
 930        case "o":
 931            let end = buffer.lineEnd(caret)
 932            let indent = Self.indentation(for: buffer, line: caret)
 933            enterInsert(at: end + 1 + indent.utf16.count, keys: keys)
 934            return [.edit([TextEdit(range: end..<end, replacement: "\n" + indent)], caret: end + 1 + indent.utf16.count)]
 935        default:
 936            let start = buffer.lineStart(caret)
 937            let indent = Self.indentation(for: buffer, line: max(0, start - 1)) == "" || start == 0 ? "" : Self.indentation(for: buffer, line: start - 1)
 938            enterInsert(at: start + indent.utf16.count, keys: keys)
 939            return [.edit([TextEdit(range: start..<start, replacement: indent + "\n")], caret: start + indent.utf16.count)]
 940        }
 941    }
 942
 943    mutating func join(_ buffer: Buffer, at caret: Int, lines: Int, keys: [KeyChord]) -> [Action] {
 944        var edits: [TextEdit] = []
 945        var line = buffer.lineStart(caret)
 946        var position = caret
 947        for _ in 1..<lines {
 948            let end = buffer.lineEnd(line)
 949            guard end < buffer.length else { break }
 950            let nextStart = end + 1
 951            let nextText = buffer.firstNonBlankOrEnd(nextStart)
 952            let joinsEmpty = buffer.lineEnd(nextStart) == nextText
 953            let separator = joinsEmpty || buffer.char(nextText) == 41 ? "" : (end > line && buffer.char(end - 1) == 32 ? "" : " ")
 954            var from = end
 955            while from > line, let c = buffer.char(from - 1), c == 32 || c == 9 { from -= 1 }
 956            if !separator.isEmpty || from == end { from = end }
 957            edits.append(TextEdit(range: from..<nextText, replacement: separator))
 958            position = from
 959            line = nextStart
 960        }
 961        guard !edits.isEmpty else { return [] }
 962        lastChange = (keys, nil)
 963        // Positions in edits refer to the original text; apply back to front.
 964        let shift = edits.dropLast().reduce(0) { $0 + $1.replacement.utf16.count - $1.range.count }
 965        return [.edit(edits, caret: position + shift)]
 966    }
 967
 968    mutating func repeatChange(_ count: Int, _ context: Context) -> [Action] {
 969        guard let change = lastChange, !change.keys.isEmpty else { return [] }
 970        var copy = self
 971        copy.lastChange = nil
 972        copy.pending = []
 973        var keys = change.keys
 974        if count > 1, let (_, _, rest) = Self.prefix(keys) {
 975            keys = Array("\(count)".map { KeyChord(String($0)) }) + Array(rest)
 976        }
 977        var actions: [Action] = []
 978        var text = context.text
 979        var caret = context.caret
 980        for key in keys {
 981            if copy.mode == .insert, key == KeyChord("ESC") {
 982                if let inserted = change.inserted, !inserted.isEmpty {
 983                    let edit = TextEdit(range: caret..<caret, replacement: inserted)
 984                    actions.append(.edit([edit], caret: caret + inserted.utf16.count))
 985                    text = edit.apply(to: text)
 986                    caret += inserted.utf16.count
 987                }
 988            }
 989            let produced = copy.feed(key, Context(text: text, caret: caret, clipboard: context.clipboard, halfPage: context.halfPage, visible: context.visible, org: context.org))
 990            for action in produced {
 991                switch action {
 992                case .edit(let edits, let to):
 993                    var new = text
 994                    for edit in edits.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) { new = edit.apply(to: new) }
 995                    text = new
 996                    caret = to
 997                case .move(let to):
 998                    caret = to
 999                default:
1000                    break
1001                }
1002                actions.append(action)
1003            }
1004        }
1005        let preserved = (registers, clipboardRecord)
1006        self = copy
1007        registers = preserved.0
1008        clipboardRecord = copy.clipboardRecord ?? preserved.1
1009        lastChange = change
1010        return actions
1011    }
1012
1013    /// `/pattern` or `?pattern` from the prompt: the caret's new place.
1014    public mutating func search(_ pattern: String, forward: Bool, _ context: Context) -> [Action] {
1015        lastSearch = (pattern, forward)
1016        let buffer = Buffer(context)
1017        guard let p = buffer.search(pattern, from: context.caret, forward: forward, count: 1) else { return [.message("Search failed: \(pattern)")] }
1018        if mode == .visual || mode == .visualLine { return visualActions(buffer, p) }
1019        return [.jumped(from: context.caret), .move(caret: p)]
1020    }
1021
1022    /// Leave a visual state without acting, as `ESC` does.
1023    public mutating func reset() {
1024        pending = []
1025        if mode == .visual || mode == .visualLine || mode == .visualBlock { mode = .normal }
1026    }
1027
1028    // MARK: - Text
1029
1030    struct Buffer {
1031        let ns: NSString
1032        var length: Int { ns.length }
1033
1034        init(_ context: Context) { ns = context.text as NSString }
1035        init(_ text: String) { ns = text as NSString }
1036
1037        func char(_ i: Int) -> unichar? { i >= 0 && i < ns.length ? ns.character(at: i) : nil }
1038        func substring(_ r: Range<Int>) -> String { ns.substring(with: NSRange(location: r.lowerBound, length: r.count)) }
1039        func lineStart(_ i: Int) -> Int { ns.lineRange(for: NSRange(location: min(i, ns.length), length: 0)).location }
1040        func lineEnd(_ i: Int) -> Int {
1041            var end = 0
1042            ns.getLineStart(nil, end: nil, contentsEnd: &end, for: NSRange(location: min(i, ns.length), length: 0))
1043            return end
1044        }
1045        func removing(_ r: Range<Int>) -> Buffer { Buffer(ns.replacingCharacters(in: NSRange(location: r.lowerBound, length: r.count), with: "")) }
1046        func applying(_ edits: [TextEdit]) -> Buffer {
1047            var text = ns as String
1048            for edit in edits.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) { text = edit.apply(to: text) }
1049            return Buffer(text)
1050        }
1051
1052        static func isBlank(_ c: unichar) -> Bool { c == 32 || c == 9 }
1053        static func isSpace(_ c: unichar) -> Bool { c == 32 || c == 9 || c == 10 || c == 13 }
1054        static func isWord(_ c: unichar) -> Bool {
1055            guard let scalar = Unicode.Scalar(c) else { return true }
1056            return CharacterSet.alphanumerics.contains(scalar) || c == 95
1057        }
1058
1059        /// 0 blank, 1 word, 2 other.
1060        func kind(_ i: Int, bigWord: Bool) -> Int {
1061            guard let c = char(i) else { return 0 }
1062            if Self.isSpace(c) { return 0 }
1063            if bigWord { return 1 }
1064            return Self.isWord(c) ? 1 : 2
1065        }
1066
1067        func clampNormal(_ i: Int) -> Int { clampNormal(i, in: lineStart(i)) }
1068        func clampNormal(_ i: Int, in line: Int) -> Int {
1069            let end = lineEnd(line)
1070            return max(line, min(i, end > line ? end - 1 : line))
1071        }
1072
1073        func firstNonBlank(_ line: Int) -> Int {
1074            var p = line
1075            let end = lineEnd(line)
1076            while p < end, let c = char(p), Self.isBlank(c) { p += 1 }
1077            return p == end && end > line ? end - 1 : (p == end ? line : p)
1078        }
1079
1080        func firstNonBlankOrEnd(_ line: Int) -> Int {
1081            var p = line
1082            let end = lineEnd(line)
1083            while p < end, let c = char(p), Self.isBlank(c) { p += 1 }
1084            return p
1085        }
1086
1087        func firstNonBlankIn(_ inserted: String, at start: Int) -> Int {
1088            start + inserted.prefix { $0 == " " || $0 == "\t" }.utf16.count
1089        }
1090
1091        func lineOffset(_ number: Int) -> Int {
1092            var line = 0
1093            for _ in 1..<max(1, number) {
1094                let end = lineEnd(line)
1095                guard end < length, end + 1 < length else { break }
1096                line = end + 1
1097            }
1098            return line
1099        }
1100
1101        /// The number, from 1, of the line holding `i`.
1102        func lineNumber(_ i: Int) -> Int {
1103            var count = 1
1104            var p = 0
1105            while p < min(i, length) {
1106                if char(p) == 10 { count += 1 }
1107                p += 1
1108            }
1109            return count
1110        }
1111
1112        func lineCount(_ r: Range<Int>) -> Int {
1113            substring(r).filter { $0 == "\n" }.count + (r.upperBound > 0 && char(r.upperBound - 1) == 10 ? 0 : 1)
1114        }
1115
1116        func wordForward(_ c: Int, bigWord: Bool) -> Int {
1117            var p = c
1118            let k = kind(p, bigWord: bigWord)
1119            if k != 0 { while p < length, kind(p, bigWord: bigWord) == k { p += 1 } }
1120            // Skip blanks; an empty line is a word.
1121            while p < length, kind(p, bigWord: bigWord) == 0 {
1122                if char(p) == 10, p > c, char(p - 1) == 10 { return p }
1123                if char(p) == 10, p + 1 < length, char(p + 1) == 10, p + 1 > c { return p + 1 }
1124                p += 1
1125            }
1126            return min(p, length)
1127        }
1128
1129        func wordBackward(_ c: Int, bigWord: Bool, stopAtLine: Bool) -> Int {
1130            var p = c
1131            guard p > 0 else { return 0 }
1132            p -= 1
1133            while p > 0, kind(p, bigWord: bigWord) == 0 {
1134                if char(p) == 10, char(p - 1) == 10, !stopAtLine { return p }
1135                if stopAtLine, char(p) == 10 { return p + 1 }
1136                p -= 1
1137            }
1138            let k = kind(p, bigWord: bigWord)
1139            while p > 0, kind(p - 1, bigWord: bigWord) == k, k != 0 { p -= 1 }
1140            return p
1141        }
1142
1143        func wordEnd(_ c: Int, bigWord: Bool, fromInside: Bool) -> Int {
1144            var p = fromInside ? c : c + 1
1145            while p < length, kind(p, bigWord: bigWord) == 0 { p += 1 }
1146            let k = kind(p, bigWord: bigWord)
1147            while p + 1 < length, kind(p + 1, bigWord: bigWord) == k, k != 0 { p += 1 }
1148            return min(p, max(0, length - 1))
1149        }
1150
1151        func wordEndBackward(_ c: Int, bigWord: Bool) -> Int {
1152            var p = c
1153            let k = kind(p, bigWord: bigWord)
1154            while p > 0, kind(p, bigWord: bigWord) == k, k != 0 { p -= 1 }
1155            while p > 0, kind(p, bigWord: bigWord) == 0 { p -= 1 }
1156            return p
1157        }
1158
1159        func wordStart(_ c: Int) -> Int {
1160            var p = c
1161            while p > 0, let ch = char(p - 1), Self.isWord(ch) { p -= 1 }
1162            return p
1163        }
1164
1165        func wordAt(_ c: Int) -> String? {
1166            var p = c
1167            while p < lineEnd(c), let ch = char(p), !Self.isWord(ch) { p += 1 }
1168            guard let ch = char(p), Self.isWord(ch) else { return nil }
1169            let start = wordStart(p)
1170            var end = p
1171            while end < length, let e = char(end), Self.isWord(e) { end += 1 }
1172            return substring(start..<end)
1173        }
1174
1175        func matchingBracket(_ c: Int) -> Int? {
1176            let pairs: [unichar: (unichar, Bool)] = [40: (41, true), 91: (93, true), 123: (125, true), 41: (40, false), 93: (91, false), 125: (123, false)]
1177            var p = c
1178            let end = lineEnd(c)
1179            while p < end, let ch = char(p), pairs[ch] == nil { p += 1 }
1180            guard let open = char(p), let (close, forward) = pairs[open] else { return nil }
1181            var depth = 0
1182            var q = p
1183            while q >= 0, q < length {
1184                if char(q) == open { depth += 1 }
1185                if char(q) == close {
1186                    depth -= 1
1187                    if depth == 0 { return q }
1188                }
1189                q += forward ? 1 : -1
1190            }
1191            return nil
1192        }
1193
1194        func paragraph(_ c: Int, forward: Bool) -> Int {
1195            func blank(_ line: Int) -> Bool { substring(line..<lineEnd(line)).allSatisfy { $0 == " " || $0 == "\t" } }
1196            var line = lineStart(c)
1197            if forward {
1198                while lineEnd(line) < length, blank(line) { line = lineEnd(line) + 1 }
1199                while lineEnd(line) < length {
1200                    line = lineEnd(line) + 1
1201                    if blank(line) { return line }
1202                }
1203                return max(0, length - (length > 0 && char(length - 1) == 10 ? 1 : 0))
1204            }
1205            while line > 0, blank(line) { line = lineStart(line - 1) }
1206            while line > 0 {
1207                line = lineStart(line - 1)
1208                if blank(line) { return line }
1209            }
1210            return 0
1211        }
1212
1213        func search(_ pattern: String, from c: Int, forward: Bool, count: Int) -> Int? {
1214            guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil }
1215            let all = regex.matches(in: ns as String, range: NSRange(location: 0, length: length)).map(\.range.location)
1216            guard !all.isEmpty else { return nil }
1217            var p = c
1218            for _ in 0..<count {
1219                if forward {
1220                    p = all.first { $0 > p } ?? all[0]
1221                } else {
1222                    p = all.last { $0 < p } ?? all[all.count - 1]
1223                }
1224            }
1225            return p
1226        }
1227
1228        func wordObject(_ c: Int, inner: Bool, bigWord: Bool, count: Int) -> Range<Int>? {
1229            guard c < length else { return nil }
1230            var start = c
1231            var end = c
1232            let k = kind(c, bigWord: bigWord)
1233            let lineS = lineStart(c)
1234            let lineE = lineEnd(c)
1235            while start > lineS, kind(start - 1, bigWord: bigWord) == k, char(start - 1) != 10 { start -= 1 }
1236            while end < lineE, kind(end, bigWord: bigWord) == k { end += 1 }
1237            if inner {
1238                for _ in 1..<max(1, count) {
1239                    let k2 = kind(end, bigWord: bigWord)
1240                    while end < lineE, kind(end, bigWord: bigWord) == k2 { end += 1 }
1241                }
1242                return start..<end
1243            }
1244            for _ in 1..<max(1, count) {
1245                while end < lineE, let ch = char(end), Self.isBlank(ch) { end += 1 }
1246                let k2 = kind(end, bigWord: bigWord)
1247                while end < lineE, kind(end, bigWord: bigWord) == k2 { end += 1 }
1248            }
1249            if k == 0 {
1250                let k2 = kind(end, bigWord: bigWord)
1251                while end < lineE, kind(end, bigWord: bigWord) == k2 { end += 1 }
1252                return start..<end
1253            }
1254            var trailing = end
1255            while trailing < lineE, let ch = char(trailing), Self.isBlank(ch) { trailing += 1 }
1256            if trailing > end { return start..<trailing }
1257            while start > lineS, let ch = char(start - 1), Self.isBlank(ch) { start -= 1 }
1258            return start..<end
1259        }
1260
1261        func quoteObject(_ c: Int, quote: unichar, inner: Bool) -> Range<Int>? {
1262            let s = lineStart(c)
1263            let e = lineEnd(c)
1264            var positions: [Int] = []
1265            var p = s
1266            while p < e {
1267                if char(p) == quote, p == s || char(p - 1) != 92 { positions.append(p) }
1268                p += 1
1269            }
1270            var pair: (Int, Int)?
1271            var i = 0
1272            while i + 1 < positions.count {
1273                if positions[i] <= c, c <= positions[i + 1] { pair = (positions[i], positions[i + 1]); break }
1274                i += 2
1275            }
1276            guard let (open, close) = pair else { return nil }
1277            if inner { return (open + 1)..<close }
1278            var end = close + 1
1279            while end < e, let ch = char(end), Self.isBlank(ch) { end += 1 }
1280            var start = open
1281            if end == close + 1 { while start > s, let ch = char(start - 1), Self.isBlank(ch) { start -= 1 } }
1282            return start..<end
1283        }
1284
1285        func bracketObject(_ c: Int, open: unichar, close: unichar, inner: Bool) -> Range<Int>? {
1286            var depth = 0
1287            var p = c
1288            var start: Int?
1289            if char(c) == open {
1290                start = c
1291            } else {
1292                if char(c) == close { p = c - 1 }
1293                while p >= 0 {
1294                    if char(p) == close { depth += 1 }
1295                    if char(p) == open {
1296                        if depth == 0 { start = p; break }
1297                        depth -= 1
1298                    }
1299                    p -= 1
1300                }
1301            }
1302            if start == nil {
1303                var f = c
1304                while f < lineEnd(c), char(f) != open { f += 1 }
1305                if f < lineEnd(c) { start = f }
1306            }
1307            guard let s = start else { return nil }
1308            depth = 0
1309            var q = s
1310            var end: Int?
1311            while q < length {
1312                if char(q) == open { depth += 1 }
1313                if char(q) == close {
1314                    depth -= 1
1315                    if depth == 0 { end = q; break }
1316                }
1317                q += 1
1318            }
1319            guard let e = end else { return nil }
1320            if !inner { return s..<(e + 1) }
1321            var innerStart = s + 1
1322            var innerEnd = e
1323            // A block that starts and ends on its own lines keeps them.
1324            if char(innerStart) == 10, lineStart(innerEnd) == innerEnd || substring(lineStart(innerEnd)..<innerEnd).allSatisfy({ $0 == " " || $0 == "\t" }) {
1325                innerStart += 1
1326                innerEnd = lineStart(innerEnd)
1327            }
1328            return innerStart..<innerEnd
1329        }
1330
1331        func paragraphObject(_ c: Int, inner: Bool) -> Range<Int>? {
1332            func blank(_ line: Int) -> Bool { substring(line..<lineEnd(line)).allSatisfy { $0 == " " || $0 == "\t" } }
1333            var start = lineStart(c)
1334            let isBlank = blank(start)
1335            while start > 0, blank(lineStart(start - 1)) == isBlank { start = lineStart(start - 1) }
1336            var end = lineEnd(c)
1337            while end < length, end + 1 <= length, end + 1 < length || !isBlank, blank(end + 1) == isBlank, end + 1 < length {
1338                end = lineEnd(end + 1)
1339            }
1340            var upper = min(length, end + 1)
1341            if !inner {
1342                var after = upper
1343                while after < length, blank(after) {
1344                    let next = lineEnd(after)
1345                    after = min(length, next + 1)
1346                    if next >= length { break }
1347                }
1348                upper = after
1349            }
1350            return start..<upper
1351        }
1352    }
1353}