krz/orgstar

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

Sources/OrgCore/Keymap/Vim.swift

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

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