Sources/OrgCore/Keymap/Vim.swift
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}