import Foundation /// Modal editing as Doom's evil gives it: normal, insert, visual and visual-line states; /// counts, registers, operators with motions and text objects, `.` repeat, evil-snipe's `s` /// and `S`, `Y` to the end of the line, and the unnamed register on the system clipboard. /// Keys go in; edits, moves and commands come out for the editor to apply. public struct Vim: Sendable { public enum Mode: String, Sendable { case normal, insert, visual case visualLine = "visual-line" /// The keymap mode name for bindings. public var keymapMode: String { switch self { case .normal: "normal" case .insert: "insert" case .visual, .visualLine: "visual" } } } public enum Action: Equatable, Sendable { /// Replace text and put the caret (and, in visual states, the selection) somewhere. case edit([TextEdit], caret: Int) case move(caret: Int) /// The selection to show in a visual state, `lowerBound..) case command(String) case undo(Int) case redo(Int) case setClipboard(String) /// Ask for a search pattern (`/`, `?`) or an ex command (`:`). case prompt(String) case message(String) } public struct Context: Sendable { public var text: String public var caret: Int public var clipboard: String? /// Lines in a half page, for C-d and C-u. public var halfPage: Int public init(text: String, caret: Int, clipboard: String? = nil, halfPage: Int = 15) { self.text = text self.caret = caret self.clipboard = clipboard self.halfPage = halfPage } } public private(set) var mode: Mode = .normal /// Keys of the command being typed. public private(set) var pending: [KeyChord] = [] var registers: [Character: (text: String, linewise: Bool)] = [:] /// What the clipboard held when this engine last wrote it, and whether that was whole lines. var clipboardRecord: (text: String, linewise: Bool)? var anchor = 0 var goalColumn: Int? var lastFind: (kind: Character, char: String)? var lastSnipe: (forward: Bool, chars: String)? var lastSearch: (pattern: String, forward: Bool)? /// The keys and inserted text of the last change, for `.`. var lastChange: (keys: [KeyChord], inserted: String?)? var changeKeys: [KeyChord]? var insertStart = 0 /// The caret when the command began, for commands that keep its column. var caretForApply = 0 /// Typed in insert state since it began, kept by `inserted(_:)`. var insertedText = "" public init() {} // MARK: - Feeding keys public mutating func feed(_ key: KeyChord, _ context: Context) -> [Action] { switch mode { case .insert: return insertKey(key, context) case .normal, .visual, .visualLine: pending.append(key) let keys = pending caretForApply = context.caret var buffer = Buffer(context) switch parse(keys, &buffer, context) { case .incomplete: return [] case .invalid: pending = [] return [] case .done(let actions): pending = [] return actions } } } /// Text typed in insert state, recorded for `.`. public mutating func inserted(_ text: String) { insertedText += text } public mutating func deletedBackward() { if !insertedText.isEmpty { insertedText.removeLast() } } private mutating func insertKey(_ key: KeyChord, _ context: Context) -> [Action] { let buffer = Buffer(context) if key == KeyChord("ESC") || key == KeyChord("[", .control) || key == KeyChord("g", .control) { mode = .normal if var keys = changeKeys { keys.append(KeyChord("ESC")) lastChange = (keys, insertedText) changeKeys = nil } let caret = context.caret let lineStart = buffer.lineStart(caret) return [.move(caret: caret > lineStart ? caret - 1 : caret)] } if key == KeyChord("w", .control) { let start = buffer.wordBackward(context.caret, bigWord: false, stopAtLine: true) guard start < context.caret else { return [] } return [.edit([TextEdit(range: start.. start ? start : buffer.lineStart(context.caret) guard from < context.caret else { return [] } return [.edit([TextEdit(range: from.. Bool { guard mode == .insert else { return false } return !(key == KeyChord("ESC") || key == KeyChord("[", .control) || key == KeyChord("g", .control) || key == KeyChord("w", .control) || key == KeyChord("u", .control)) } public mutating func enterInsert(at caret: Int, keys: [KeyChord]) { mode = .insert insertStart = caret insertedText = "" changeKeys = keys } // MARK: - Parsing enum Parse { case incomplete case invalid case done([Action]) } struct Command { var register: Character? var count: Int? var keys: ArraySlice } /// `evil-shift-width`, which Doom keeps equal to `tab-width`: 8 in Org. static let shiftWidth = 8 /// What `indent-according-to-mode` gives a line after `line` in Org: under a list item, /// its text's column; under a heading, none; otherwise `line`'s own indentation. static func indentation(for buffer: Buffer, line: Int) -> String { let text = buffer.substring(buffer.lineStart(line).. Int? { guard key.modifiers.isEmpty, key.key.count == 1, let d = Int(key.key) else { return nil } return d } /// Reads `"r` and a count off the front of `keys`. static func prefix(_ keys: [KeyChord]) -> (register: Character?, count: Int?, rest: ArraySlice)? { var rest = keys[...] var register: Character? if rest.first?.key == "\"", rest.first?.modifiers.isEmpty == true { guard rest.count >= 2 else { return nil } register = rest[rest.startIndex + 1].key == "SPC" ? " " : rest[rest.startIndex + 1].key.first rest = rest.dropFirst(2) } var count: Int? while let key = rest.first, let d = digit(key), !(d == 0 && count == nil) { count = (count ?? 0) * 10 + d rest = rest.dropFirst() } return (register, count, rest) } mutating func parse(_ keys: [KeyChord], _ buffer: inout Buffer, _ context: Context) -> Parse { guard let (register, count, rest) = Self.prefix(keys) else { return .incomplete } guard let first = rest.first else { return .incomplete } let visual = mode == .visual || mode == .visualLine let n = count ?? 1 let caret = context.caret // Operators. let operatorKeys: [String: String] = ["d": "d", "c": "c", "y": "y", ">": ">", "<": "<"] var op: String? var afterOp = rest.dropFirst() if first.modifiers.isEmpty, let o = operatorKeys[first.key] { op = o } else if first.key == "g", first.modifiers.isEmpty { guard let second = rest.dropFirst().first else { return .incomplete } if ["~", "u", "U"].contains(second.key), second.modifiers.isEmpty { op = "g" + second.key afterOp = rest.dropFirst(2) } } if let op { if visual { let range = visualRange(buffer, caret) return .done(apply(op, range: range.range, linewise: range.linewise, buffer, register: register, keys: keys, count: n)) } // A second count and the motion. guard let (_, count2, motionKeys) = Self.prefix(Array(afterOp)), let motionFirst = motionKeys.first else { return .incomplete } let total = n * (count2 ?? 1) let doubled = op.count == 1 ? op : String(op.last!) if motionFirst.key == doubled, motionFirst.modifiers.isEmpty, motionKeys.count == 1 || (op.count == 2 && motionFirst.key == String(op.last!)) { // dd, cc, yy, >>, <<, g~~, guu, gUU: whole lines. let startLine = buffer.lineStart(caret) var end = startLine for _ in 0..= buffer.length { break } } return .done(apply(op, range: startLine..= 2 else { return .incomplete } guard let object = textObject(motionFirst.key == "i", motionKeys[motionKeys.startIndex + 1], buffer, caret, count: total) else { return .invalid } return .done(apply(op, range: object.range, linewise: object.linewise, buffer, register: register, keys: keys, count: total)) } var motionCaret = caret if op == "c", motionFirst.key == "w" || motionFirst.key == "W", motionFirst.modifiers.isEmpty, let c = buffer.char(caret), !Buffer.isBlank(c) { // cw acts as ce on a word. var end = caret for _ in 0.. var linewise = false switch kind { case .linewise: let a = buffer.lineStart(min(caret, target)) let b = buffer.lineEnd(max(caret, target)) range = a.. range.lowerBound { var end = buffer.lineEnd(range.lowerBound) if end == range.lowerBound, total == 1 { end = min(buffer.length, end + 1) } let upper = buffer.firstNonBlankOrEnd(buffer.lineStart(range.upperBound)) == range.upperBound ? max(range.lowerBound, min(end, range.upperBound)) : range.upperBound range = range.lowerBound..<(upper == range.lowerBound ? end : upper) if buffer.lineStart(range.upperBound) != buffer.lineStart(range.lowerBound) { range = range.lowerBound..= 2 else { return .incomplete } guard let object = textObject(first.key == "i", rest[rest.startIndex + 1], buffer, caret, count: n) else { return .invalid } if object.linewise { mode = .visualLine } anchor = object.range.lowerBound let end = max(object.range.lowerBound, object.range.upperBound - 1) return .done(visualActions(buffer, end)) default: break } } if !visual { switch (first.key, first.modifiers) { case ("i", []), ("a", []), ("I", []), ("A", []), ("o", []), ("O", []): return .done(enterInsert(first.key, buffer, caret, keys: keys)) case ("v", []): mode = .visual anchor = caret return .done(visualActions(buffer, caret)) case ("V", []): mode = .visualLine anchor = caret return .done(visualActions(buffer, caret)) case ("x", []), ("", []): guard let c = buffer.char(caret), c != 10 else { return .done([]) } let end = min(buffer.lineEnd(caret), caret + n) return .done(apply("d", range: caret..= buffer.length { let insertion = "\n" + String(body.dropLast()) return .done(change([TextEdit(range: end.. caret else { return .done([]) } let flipped = String(buffer.substring(caret..= 2 else { return .incomplete } let target = rest[rest.startIndex + 1] let char = target.key == "SPC" ? " " : target.key == "RET" ? "\n" : target.key guard char.count == 1, caret + n <= buffer.lineEnd(caret) else { return .done([]) } let replacement = char == "\n" ? "\n" : String(repeating: char, count: n) return .done(change([TextEdit(range: caret..<(caret + n), replacement: replacement)], caret: char == "\n" ? caret + 1 : caret + n - 1, keys: keys)) case ("u", []): return .done([.undo(n)]) case ("r", [.control]): return .done([.redo(n)]) case (".", []): return .done(repeatChange(n, context)) case (":", []): return .done([.prompt(":")]) case ("/", []), ("?", []): return .done([.prompt(first.key)]) case ("Z", []): guard rest.count >= 2 else { return .incomplete } switch rest[rest.startIndex + 1].key { case "Z": return .done([.command("app.save")]) case "Q": return .done([.message("ZQ isn't available; use the window's close button")]) default: return .invalid } default: break } } // Motions move the caret, or extend the selection in visual states. var target = caret switch motion(Array(rest), &buffer, &target, count: n, operatorPending: false) { case .incomplete: return .incomplete case .invalid: return .invalid case .moved(let to, _): if visual { return .done(visualActions(buffer, to)) } return .done([.move(caret: buffer.clampNormal(to))]) } } // MARK: - Motions enum MotionKind { case exclusive, inclusive, linewise } enum MotionResult { case incomplete case invalid case moved(Int, MotionKind) } mutating func motion(_ keys: [KeyChord], _ buffer: inout Buffer, _ caret: inout Int, count n: Int, operatorPending: Bool) -> MotionResult { guard let key = keys.first else { return .incomplete } let c = caret func column(_ position: Int) -> Int { position - buffer.lineStart(position) } switch (key.key, key.modifiers) { case ("h", []), ("", []), ("DEL", []): goalColumn = nil return .moved(max(buffer.lineStart(c), c - n), .exclusive) case ("l", []), ("", []), ("SPC", []): goalColumn = nil let end = buffer.lineEnd(c) let limit = operatorPending ? end : max(buffer.lineStart(c), end - 1) return .moved(min(limit, c + n), .exclusive) case ("j", []), ("k", []), ("", []), ("", []), ("d", [.control]), ("u", [.control]), ("+", []), ("-", []), ("RET", []): let down = ["j", "", "+", "RET"].contains(key.key) || key == KeyChord("d", .control) let lines = key.modifiers.contains(.control) ? (n == 1 ? 15 : n) : n let goal = goalColumn ?? column(c) var line = buffer.lineStart(c) var moved = 0 for _ in 0.. 0 else { break } line = buffer.lineStart(line - 1) } moved += 1 } if moved == 0, !key.modifiers.contains(.control) { return .invalid } if ["+", "-", "RET"].contains(key.key) { return .moved(buffer.firstNonBlank(line), .linewise) } goalColumn = goal let length = buffer.lineEnd(line) - line return .moved(line + min(goal, operatorPending ? length : max(0, length - 1)), .linewise) case ("w", []), ("W", []): goalColumn = nil var p = c for _ in 0..", []): goalColumn = nil return .moved(buffer.lineStart(c), .exclusive) case ("^", []): goalColumn = nil return .moved(buffer.firstNonBlank(buffer.lineStart(c)), .exclusive) case ("$", []), ("", []): var line = buffer.lineStart(c) for _ in 1.. 1 || keys.count > 1 ? buffer.lineOffset(n) : buffer.lineStart(buffer.length) let column = c - buffer.lineStart(c) let length = buffer.lineEnd(line) - line return .moved(line + min(column, operatorPending ? length : max(0, length - 1)), .linewise) case ("g", []): guard keys.count >= 2 else { return .incomplete } switch keys[1].key { case "g": let line = buffer.lineOffset(n) let column = c - buffer.lineStart(c) let length = buffer.lineEnd(line) - line return .moved(line + min(column, operatorPending ? length : max(0, length - 1)), .linewise) case "e", "E": var p = c for _ in 0.. buffer.lineStart(c), let ch = buffer.char(p - 1), Buffer.isBlank(ch) { p -= 1 } return .moved(max(buffer.lineStart(c), p - 1), .inclusive) default: return .invalid } case ("f", []), ("F", []), ("t", []), ("T", []): guard keys.count >= 2 else { return .incomplete } let char = keys[1].key == "SPC" ? " " : keys[1].key guard char.count == 1 else { return .invalid } lastFind = (Character(key.key), char) guard let p = find(Character(key.key), char, buffer, c, count: n) else { return .invalid } return .moved(p, key.key == "f" || key.key == "t" ? .inclusive : .exclusive) case (";", []), (",", []): guard let last = lastFind else { return .invalid } var kind = last.kind if key.key == "," { kind = ["f": "F", "F": "f", "t": "T", "T": "t"][kind]! } guard let p = find(kind, last.char, buffer, c, count: n, repeating: true) else { return .invalid } return .moved(p, kind == "f" || kind == "t" ? .inclusive : .exclusive) case ("s", []), ("S", []) where !operatorPending: // evil-snipe with `evil-snipe-scope` line. guard keys.count >= 3 else { return .incomplete } let chars = keys[1].key + keys[2].key lastSnipe = (key.key == "s", chars) guard let p = snipe(forward: key.key == "s", chars, buffer, c, count: n) else { return .invalid } return .moved(p, .exclusive) case ("%", []): guard let p = buffer.matchingBracket(c) else { return .invalid } return .moved(p, .inclusive) case ("}", []), ("{", []): var p = c for _ in 0.. Int? { let target = char.utf16.first! let forward = kind == "f" || kind == "t" var p = c for i in 0..= buffer.lineStart(c), buffer.char(q) != target { q -= 1 } guard q >= buffer.lineStart(c) else { return nil } } p = q } if kind == "t" { return p - 1 } if kind == "T" { return p + 1 } return p } func snipe(forward: Bool, _ chars: String, _ buffer: Buffer, _ c: Int, count n: Int) -> Int? { let line = buffer.substring(buffer.lineStart(c).. 0 else { return nil } range = line.range(of: chars, options: smart ? [.caseInsensitive, .backwards] : .backwards, range: NSRange(location: 0, length: p)) } guard range.location != NSNotFound else { return nil } p = range.location } return start + p } // MARK: - Text objects func textObject(_ inner: Bool, _ key: KeyChord, _ buffer: Buffer, _ c: Int, count: Int) -> (range: Range, linewise: Bool)? { switch key.key { case "w", "W": return buffer.wordObject(c, inner: inner, bigWord: key.key == "W", count: count).map { ($0, false) } case "\"", "'", "`": return buffer.quoteObject(c, quote: key.key.utf16.first!, inner: inner).map { ($0, false) } case "(", ")", "b": return buffer.bracketObject(c, open: 40, close: 41, inner: inner).map { ($0, false) } case "[", "]": return buffer.bracketObject(c, open: 91, close: 93, inner: inner).map { ($0, false) } case "{", "}", "B": return buffer.bracketObject(c, open: 123, close: 125, inner: inner).map { ($0, false) } case "<", ">": return buffer.bracketObject(c, open: 60, close: 62, inner: inner).map { ($0, false) } case "p": return buffer.paragraphObject(c, inner: inner).map { ($0, true) } default: return nil } } // MARK: - Applying func visualRange(_ buffer: Buffer, _ caret: Int) -> (range: Range, linewise: Bool) { let a = min(anchor, caret) let b = max(anchor, caret) if mode == .visualLine { let end = buffer.lineEnd(b) return (buffer.lineStart(a).. [Action] { let a = min(anchor, caret) let b = max(anchor, caret) let range = mode == .visualLine ? buffer.lineStart(a).. [Action] { if register == "_" { return [] } if let register, register.isLetter { let key = Character(register.lowercased()) if register.isUppercase, let existing = registers[key] { registers[key] = (existing.text + text, existing.linewise || linewise) } else { registers[key] = (text, linewise) } return [] } if yank { registers["0"] = (text, linewise) } clipboardRecord = (text, linewise) return [.setClipboard(text)] } func paste(_ register: Character?, _ context: Context) -> (String, Bool)? { if let register, register != "\"", register != "+", register != "*" { return registers[Character(register.lowercased())] } guard let clip = context.clipboard ?? clipboardRecord?.text else { return nil } if let record = clipboardRecord, record.text == clip { return (clip, record.linewise) } return (clip, false) } mutating func change(_ edits: [TextEdit], caret: Int, keys: [KeyChord]) -> [Action] { lastChange = (keys, nil) return [.edit(edits, caret: caret)] } mutating func apply(_ op: String, range: Range, linewise: Bool, _ buffer: Buffer, register: Character?, keys: [KeyChord], count: Int) -> [Action] { let wasVisual = mode == .visual || mode == .visualLine let context0Column = max(0, caretForApply - buffer.lineStart(caretForApply)) mode = .normal let text = buffer.substring(range) switch op { case "y": var actions = store(text, linewise: linewise, register: register, yank: true) let caret = linewise && !wasVisual ? min(range.lowerBound, buffer.length) : range.lowerBound actions.append(.move(caret: linewise && wasVisual ? buffer.lineStart(range.lowerBound) : caret)) return actions case "d": var actions = store(text, linewise: linewise, register: register, yank: false) var deletion = range if linewise, range.upperBound >= buffer.length, range.lowerBound > 0, buffer.char(range.upperBound - 1) != 10 || range.upperBound == buffer.length { // The last line takes the newline before it. if buffer.char(range.lowerBound - 1) == 10, range.upperBound == buffer.length, !(text.hasSuffix("\n")) { deletion = (range.lowerBound - 1)..", "<": let start = buffer.lineStart(range.lowerBound) var edits: [TextEdit] = [] var line = start let end = linewise ? range.upperBound : max(range.upperBound, buffer.lineEnd(max(range.lowerBound, range.upperBound - 1))) while line < end || (line == start && line <= buffer.length) { let lineEnd = buffer.lineEnd(line) if lineEnd > line { if op == ">" { edits.append(TextEdit(range: line.. 0 { edits.append(TextEdit(range: line..<(line + remove), replacement: "")) } } } guard lineEnd < buffer.length else { break } line = lineEnd + 1 if line >= end { break } } lastChange = (wasVisual ? [] : keys, nil) return [.edit(edits, caret: start)] default: let transformed: String switch op { case "gu": transformed = text.lowercased() case "gU": transformed = text.uppercased() default: transformed = String(text.map { $0.isUppercase ? Character($0.lowercased()) : Character($0.uppercased()) }) } lastChange = (wasVisual ? [] : keys, nil) return [.edit([TextEdit(range: range, replacement: transformed)], caret: range.lowerBound)] } } mutating func enterInsert(_ key: String, _ buffer: Buffer, _ caret: Int, keys: [KeyChord]) -> [Action] { switch key { case "i": enterInsert(at: caret, keys: keys) return [.move(caret: caret)] case "a": let p = buffer.char(caret).map { $0 != 10 } == true ? caret + 1 : caret enterInsert(at: p, keys: keys) return [.move(caret: p)] case "I": let p = buffer.firstNonBlank(buffer.lineStart(caret)) enterInsert(at: p, keys: keys) return [.move(caret: p)] case "A": let p = buffer.lineEnd(caret) enterInsert(at: p, keys: keys) return [.move(caret: p)] case "o": let end = buffer.lineEnd(caret) let indent = Self.indentation(for: buffer, line: caret) enterInsert(at: end + 1 + indent.utf16.count, keys: keys) return [.edit([TextEdit(range: end.. [Action] { var edits: [TextEdit] = [] var line = buffer.lineStart(caret) var position = caret for _ in 1.. line && buffer.char(end - 1) == 32 ? "" : " ") var from = end while from > line, let c = buffer.char(from - 1), c == 32 || c == 9 { from -= 1 } if !separator.isEmpty || from == end { from = end } edits.append(TextEdit(range: from.. [Action] { guard let change = lastChange, !change.keys.isEmpty else { return [] } var copy = self copy.lastChange = nil copy.pending = [] var keys = change.keys if count > 1, let (_, _, rest) = Self.prefix(keys) { keys = Array("\(count)".map { KeyChord(String($0)) }) + Array(rest) } var actions: [Action] = [] var text = context.text var caret = context.caret for key in keys { if copy.mode == .insert, key == KeyChord("ESC") { if let inserted = change.inserted, !inserted.isEmpty { let edit = TextEdit(range: caret.. $1.range.lowerBound }) { new = edit.apply(to: new) } text = new caret = to case .move(let to): caret = to default: break } actions.append(action) } } let preserved = (registers, clipboardRecord) self = copy registers = preserved.0 clipboardRecord = copy.clipboardRecord ?? preserved.1 lastChange = change return actions } /// `/pattern` or `?pattern` from the prompt: the caret's new place. public mutating func search(_ pattern: String, forward: Bool, _ context: Context) -> [Action] { lastSearch = (pattern, forward) let buffer = Buffer(context) guard let p = buffer.search(pattern, from: context.caret, forward: forward, count: 1) else { return [.message("Search failed: \(pattern)")] } if mode == .visual || mode == .visualLine { return visualActions(buffer, p) } return [.move(caret: p)] } /// Leave a visual state without acting, as `ESC` does. public mutating func reset() { pending = [] if mode == .visual || mode == .visualLine { mode = .normal } } // MARK: - Text struct Buffer { let ns: NSString var length: Int { ns.length } init(_ context: Context) { ns = context.text as NSString } init(_ text: String) { ns = text as NSString } func char(_ i: Int) -> unichar? { i >= 0 && i < ns.length ? ns.character(at: i) : nil } func substring(_ r: Range) -> String { ns.substring(with: NSRange(location: r.lowerBound, length: r.count)) } func lineStart(_ i: Int) -> Int { ns.lineRange(for: NSRange(location: min(i, ns.length), length: 0)).location } func lineEnd(_ i: Int) -> Int { var end = 0 ns.getLineStart(nil, end: nil, contentsEnd: &end, for: NSRange(location: min(i, ns.length), length: 0)) return end } func removing(_ r: Range) -> Buffer { Buffer(ns.replacingCharacters(in: NSRange(location: r.lowerBound, length: r.count), with: "")) } func applying(_ edits: [TextEdit]) -> Buffer { var text = ns as String for edit in edits.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) { text = edit.apply(to: text) } return Buffer(text) } static func isBlank(_ c: unichar) -> Bool { c == 32 || c == 9 } static func isSpace(_ c: unichar) -> Bool { c == 32 || c == 9 || c == 10 || c == 13 } static func isWord(_ c: unichar) -> Bool { guard let scalar = Unicode.Scalar(c) else { return true } return CharacterSet.alphanumerics.contains(scalar) || c == 95 } /// 0 blank, 1 word, 2 other. func kind(_ i: Int, bigWord: Bool) -> Int { guard let c = char(i) else { return 0 } if Self.isSpace(c) { return 0 } if bigWord { return 1 } return Self.isWord(c) ? 1 : 2 } func clampNormal(_ i: Int) -> Int { clampNormal(i, in: lineStart(i)) } func clampNormal(_ i: Int, in line: Int) -> Int { let end = lineEnd(line) return max(line, min(i, end > line ? end - 1 : line)) } func firstNonBlank(_ line: Int) -> Int { var p = line let end = lineEnd(line) while p < end, let c = char(p), Self.isBlank(c) { p += 1 } return p == end && end > line ? end - 1 : (p == end ? line : p) } func firstNonBlankOrEnd(_ line: Int) -> Int { var p = line let end = lineEnd(line) while p < end, let c = char(p), Self.isBlank(c) { p += 1 } return p } func firstNonBlankIn(_ inserted: String, at start: Int) -> Int { start + inserted.prefix { $0 == " " || $0 == "\t" }.utf16.count } func lineOffset(_ number: Int) -> Int { var line = 0 for _ in 1..) -> Int { substring(r).filter { $0 == "\n" }.count + (r.upperBound > 0 && char(r.upperBound - 1) == 10 ? 0 : 1) } func wordForward(_ c: Int, bigWord: Bool) -> Int { var p = c let k = kind(p, bigWord: bigWord) if k != 0 { while p < length, kind(p, bigWord: bigWord) == k { p += 1 } } // Skip blanks; an empty line is a word. while p < length, kind(p, bigWord: bigWord) == 0 { if char(p) == 10, p > c, char(p - 1) == 10 { return p } if char(p) == 10, p + 1 < length, char(p + 1) == 10, p + 1 > c { return p + 1 } p += 1 } return min(p, length) } func wordBackward(_ c: Int, bigWord: Bool, stopAtLine: Bool) -> Int { var p = c guard p > 0 else { return 0 } p -= 1 while p > 0, kind(p, bigWord: bigWord) == 0 { if char(p) == 10, char(p - 1) == 10, !stopAtLine { return p } if stopAtLine, char(p) == 10 { return p + 1 } p -= 1 } let k = kind(p, bigWord: bigWord) while p > 0, kind(p - 1, bigWord: bigWord) == k, k != 0 { p -= 1 } return p } func wordEnd(_ c: Int, bigWord: Bool, fromInside: Bool) -> Int { var p = fromInside ? c : c + 1 while p < length, kind(p, bigWord: bigWord) == 0 { p += 1 } let k = kind(p, bigWord: bigWord) while p + 1 < length, kind(p + 1, bigWord: bigWord) == k, k != 0 { p += 1 } return min(p, max(0, length - 1)) } func wordEndBackward(_ c: Int, bigWord: Bool) -> Int { var p = c let k = kind(p, bigWord: bigWord) while p > 0, kind(p, bigWord: bigWord) == k, k != 0 { p -= 1 } while p > 0, kind(p, bigWord: bigWord) == 0 { p -= 1 } return p } func wordStart(_ c: Int) -> Int { var p = c while p > 0, let ch = char(p - 1), Self.isWord(ch) { p -= 1 } return p } func wordAt(_ c: Int) -> String? { var p = c while p < lineEnd(c), let ch = char(p), !Self.isWord(ch) { p += 1 } guard let ch = char(p), Self.isWord(ch) else { return nil } let start = wordStart(p) var end = p while end < length, let e = char(end), Self.isWord(e) { end += 1 } return substring(start.. Int? { let pairs: [unichar: (unichar, Bool)] = [40: (41, true), 91: (93, true), 123: (125, true), 41: (40, false), 93: (91, false), 125: (123, false)] var p = c let end = lineEnd(c) while p < end, let ch = char(p), pairs[ch] == nil { p += 1 } guard let open = char(p), let (close, forward) = pairs[open] else { return nil } var depth = 0 var q = p while q >= 0, q < length { if char(q) == open { depth += 1 } if char(q) == close { depth -= 1 if depth == 0 { return q } } q += forward ? 1 : -1 } return nil } func paragraph(_ c: Int, forward: Bool) -> Int { func blank(_ line: Int) -> Bool { substring(line.. 0 && char(length - 1) == 10 ? 1 : 0)) } while line > 0, blank(line) { line = lineStart(line - 1) } while line > 0 { line = lineStart(line - 1) if blank(line) { return line } } return 0 } func search(_ pattern: String, from c: Int, forward: Bool, count: Int) -> Int? { guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil } let all = regex.matches(in: ns as String, range: NSRange(location: 0, length: length)).map(\.range.location) guard !all.isEmpty else { return nil } var p = c for _ in 0.. p } ?? all[0] } else { p = all.last { $0 < p } ?? all[all.count - 1] } } return p } func wordObject(_ c: Int, inner: Bool, bigWord: Bool, count: Int) -> Range? { guard c < length else { return nil } var start = c var end = c let k = kind(c, bigWord: bigWord) let lineS = lineStart(c) let lineE = lineEnd(c) while start > lineS, kind(start - 1, bigWord: bigWord) == k, char(start - 1) != 10 { start -= 1 } while end < lineE, kind(end, bigWord: bigWord) == k { end += 1 } if inner { for _ in 1.. end { return start.. lineS, let ch = char(start - 1), Self.isBlank(ch) { start -= 1 } return start.. Range? { let s = lineStart(c) let e = lineEnd(c) var positions: [Int] = [] var p = s while p < e { if char(p) == quote, p == s || char(p - 1) != 92 { positions.append(p) } p += 1 } var pair: (Int, Int)? var i = 0 while i + 1 < positions.count { if positions[i] <= c, c <= positions[i + 1] { pair = (positions[i], positions[i + 1]); break } i += 2 } guard let (open, close) = pair else { return nil } if inner { return (open + 1).. s, let ch = char(start - 1), Self.isBlank(ch) { start -= 1 } } return start.. Range? { var depth = 0 var p = c var start: Int? if char(c) == open { start = c } else { if char(c) == close { p = c - 1 } while p >= 0 { if char(p) == close { depth += 1 } if char(p) == open { if depth == 0 { start = p; break } depth -= 1 } p -= 1 } } if start == nil { var f = c while f < lineEnd(c), char(f) != open { f += 1 } if f < lineEnd(c) { start = f } } guard let s = start else { return nil } depth = 0 var q = s var end: Int? while q < length { if char(q) == open { depth += 1 } if char(q) == close { depth -= 1 if depth == 0 { end = q; break } } q += 1 } guard let e = end else { return nil } if !inner { return s..<(e + 1) } var innerStart = s + 1 var innerEnd = e // A block that starts and ends on its own lines keeps them. if char(innerStart) == 10, lineStart(innerEnd) == innerEnd || substring(lineStart(innerEnd).. Range? { func blank(_ line: Int) -> Bool { substring(line.. 0, blank(lineStart(start - 1)) == isBlank { start = lineStart(start - 1) } var end = lineEnd(c) while end < length, end + 1 <= length, end + 1 < length || !isBlank, blank(end + 1) == isBlank, end + 1 < length { end = lineEnd(end + 1) } var upper = min(length, end + 1) if !inner { var after = upper while after < length, blank(after) { let next = lineEnd(after) after = min(length, next + 1) if next >= length { break } } upper = after } return start..