krz/orgstar

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

Sources/OrgCore/Keymap/VimExtras.swift

ab6ccec56eca03d0dadf2c9332aab10c4490eb62
orgstar/Sources/OrgCore/Keymap/VimExtras.swift history · blame · raw

661 lines · 35081 bytes

  1import Foundation
  2
  3// Doom's evil beyond the basics: marks and the jump list, macros, `gv`, block visual state,
  4// evil-surround (`ys`, `cs`, `ds`, visual `S`), commenting (`gc`) and evil-org's text objects
  5// (`e` object, `E` element, `r` greater element, `R` subtree).
  6
  7extension Vim {
  8    /// Marks from the editor, over the ones set in this engine.
  9    var currentMarks: [Character: Int] { marks.merging(contextMarks) { $1 } }
 10
 11    /// Motions evil records in the jump list.
 12    static func isJump(_ keys: [KeyChord]) -> Bool {
 13        guard let key = keys.first, key.modifiers.isEmpty else { return false }
 14        if ["G", "%", "{", "}", "n", "N", "*", "#", "'", "`", "H", "M", "L", "s", "S", "f", "F", "t", "T"].contains(key.key) { return true }
 15        return key.key == "g" && keys.count > 1 && keys[1].key == "g"
 16    }
 17
 18    /// Keys evil handles here before operators and motions, or nil.
 19    mutating func parseExtras(_ rest: ArraySlice<KeyChord>, _ buffer: inout Buffer, _ context: Context, register: Character?, count: Int?, keys: [KeyChord]) -> Parse? {
 20        guard let first = rest.first else { return .incomplete }
 21        let visual = mode == .visual || mode == .visualLine
 22        let n = count ?? 1
 23        let caret = context.caret
 24        let second = rest.dropFirst().first
 25        func needSecond() -> Parse? { second == nil ? .incomplete : nil }
 26
 27        if first.modifiers == [.control] {
 28            switch first.key {
 29            case "o" where !visual: return .done([.jumpBack(n)])
 30            case "i" where !visual: return .done([.jumpForward(n)])
 31            case "v":
 32                if visual { lastVisual = (anchor, caret, mode) }
 33                mode = .visualBlock
 34                if !visual { anchor = caret }
 35                return .done(blockActions(buffer, caret))
 36            default: return nil
 37            }
 38        }
 39        guard first.modifiers.isEmpty else { return nil }
 40
 41        if visual, first.key == "S" {
 42            if let wait = needSecond() { return wait }
 43            let range = visualRange(buffer, caret)
 44            lastVisual = (anchor, caret, mode)
 45            mode = .normal
 46            return .done(surround(range.range, with: second!, linewise: range.linewise, buffer, keys: []))
 47        }
 48        guard !visual else { return nil }
 49
 50        switch first.key {
 51        case "m":
 52            if let wait = needSecond() { return wait }
 53            guard let name = second!.key.first, second!.key.count == 1, name.isLetter else { return .invalid }
 54            marks[name] = caret
 55            return .done([.setMark(name, caret)])
 56        case "q":
 57            if recording {
 58                recording = false
 59                return .done([.recordMacro(nil)])
 60            }
 61            if let wait = needSecond() { return wait }
 62            guard let name = second!.key.first, second!.key.count == 1, name.isLetter || name.isNumber else { return .invalid }
 63            recording = true
 64            return .done([.recordMacro(name)])
 65        case "@":
 66            if let wait = needSecond() { return wait }
 67            let name: Character
 68            if second!.key == "@" {
 69                guard let last = lastMacro else { return .done([.message("No previous macro")]) }
 70                name = last
 71            } else {
 72                guard let c = second!.key.first, second!.key.count == 1 else { return .invalid }
 73                name = c
 74            }
 75            lastMacro = name
 76            return .done([.playMacro(name, n)])
 77        case "g":
 78            if let wait = needSecond() { return wait }
 79            guard second!.key == "v", second!.modifiers.isEmpty else { return nil }
 80            guard let last = lastVisual else { return .done([]) }
 81            mode = last.mode
 82            anchor = min(last.anchor, buffer.length)
 83            let to = min(last.caret, buffer.length)
 84            return .done(mode == .visualBlock ? blockActions(buffer, to) : visualActions(buffer, to))
 85        case "y", "c", "d":
 86            guard second?.key == "s", second?.modifiers.isEmpty == true else { return nil }
 87            return surroundCommand(first.key, Array(rest.dropFirst(2)), &buffer, caret: caret, count: n, keys: keys)
 88        default:
 89            return nil
 90        }
 91    }
 92
 93    // MARK: - Surround
 94
 95    /// The delimiters evil-surround puts around text for `key`: an opening bracket adds a space
 96    /// inside, a closing one doesn't.
 97    static func delimiters(_ key: KeyChord) -> (open: String, close: String)? {
 98        let pairs: [String: (String, String)] = [
 99            "(": ("( ", " )"), ")": ("(", ")"), "b": ("(", ")"), "[": ("[ ", " ]"), "]": ("[", "]"), "r": ("[", "]"),
100            "{": ("{ ", " }"), "}": ("{", "}"), "B": ("{", "}"), "<": ("< ", " >"), ">": ("<", ">"), "a": ("<", ">"), "SPC": (" ", " "),
101        ]
102        if let pair = pairs[key.key] { return pair }
103        guard key.modifiers.isEmpty, key.key.count == 1 else { return nil }
104        return (key.key, key.key)
105    }
106
107    /// `ys{motion}{char}`, `yss{char}`, `cs{old}{new}` and `ds{char}`.
108    mutating func surroundCommand(_ op: String, _ after: [KeyChord], _ buffer: inout Buffer, caret: Int, count: Int, keys: [KeyChord]) -> Parse {
109        switch op {
110        case "y":
111            guard let (_, count2, motionKeys) = Self.prefix(after), let motionFirst = motionKeys.first else { return .incomplete }
112            let total = count * (count2 ?? 1)
113            if motionFirst.key == "s", motionFirst.modifiers.isEmpty {
114                guard motionKeys.count >= 2 else { return .incomplete }
115                let line = buffer.lineStart(caret)
116                let range = buffer.firstNonBlank(line)..<buffer.lineEnd(line)
117                return .done(surround(range, with: motionKeys[motionKeys.startIndex + 1], linewise: false, buffer, keys: keys))
118            }
119            if motionFirst.key == "i" || motionFirst.key == "a", motionFirst.modifiers.isEmpty {
120                guard motionKeys.count >= 3 else { return .incomplete }
121                guard let object = textObject(motionFirst.key == "i", motionKeys[motionKeys.startIndex + 1], buffer, caret, count: total) else { return .invalid }
122                return .done(surround(object.range, with: motionKeys[motionKeys.startIndex + 2], linewise: object.linewise, buffer, keys: keys))
123            }
124            var target = caret
125            switch motion(Array(motionKeys), &buffer, &target, count: total, operatorPending: true) {
126            case .incomplete: return .incomplete
127            case .invalid: return .invalid
128            case .moved(let to, let kind):
129                // The surround character follows the motion's keys.
130                let used = Self.motionLength(Array(motionKeys))
131                guard motionKeys.count > used else { return .incomplete }
132                let char = motionKeys[motionKeys.startIndex + used]
133                var range: Range<Int>
134                switch kind {
135                case .linewise: range = buffer.lineStart(min(caret, to))..<buffer.lineEnd(max(caret, to))
136                case .inclusive: range = min(caret, to)..<min(buffer.length, max(caret, to) + 1)
137                case .exclusive: range = min(caret, to)..<max(caret, to)
138                }
139                // ysw leaves the space after the word outside.
140                while range.upperBound > range.lowerBound, let c = buffer.char(range.upperBound - 1), Buffer.isBlank(c) { range = range.lowerBound..<(range.upperBound - 1) }
141                return .done(surround(range, with: char, linewise: kind == .linewise, buffer, keys: keys))
142            }
143        case "d", "c":
144            guard let target = after.first else { return .incomplete }
145            if op == "c", after.count < 2 { return .incomplete }
146            guard let found = surrounding(target, buffer, caret) else { return .invalid }
147            var edits: [TextEdit]
148            if op == "d" {
149                edits = [TextEdit(range: found.close, replacement: ""), TextEdit(range: found.open, replacement: "")]
150            } else {
151                guard let (open, close) = Self.delimiters(after[after.startIndex + 1]) else { return .invalid }
152                edits = [TextEdit(range: found.close, replacement: close), TextEdit(range: found.open, replacement: open)]
153            }
154            lastChange = (keys, nil)
155            return .done([.edit(edits, caret: found.open.lowerBound)])
156        default:
157            return .invalid
158        }
159    }
160
161    /// How many keys a motion takes: two for `f`, `t`, `'`, `` ` `` and `g`, otherwise one.
162    static func motionLength(_ keys: [KeyChord]) -> Int {
163        guard let first = keys.first, first.modifiers.isEmpty else { return 1 }
164        return ["f", "F", "t", "T", "'", "`", "g"].contains(first.key) ? 2 : 1
165    }
166
167    /// Wraps `range` in the delimiters for `key`; linewise, on lines of their own.
168    mutating func surround(_ range: Range<Int>, with key: KeyChord, linewise: Bool, _ buffer: Buffer, keys: [KeyChord]) -> [Action] {
169        guard let (open, close) = Self.delimiters(key) else { return [] }
170        var edits: [TextEdit]
171        if linewise {
172            let start = buffer.lineStart(range.lowerBound)
173            let end = buffer.lineEnd(max(range.lowerBound, range.upperBound - 1))
174            edits = [TextEdit(range: end..<end, replacement: "\n" + close.trimmingCharacters(in: .whitespaces)),
175                     TextEdit(range: start..<start, replacement: open.trimmingCharacters(in: .whitespaces) + "\n")]
176        } else {
177            edits = [TextEdit(range: range.upperBound..<range.upperBound, replacement: close), TextEdit(range: range.lowerBound..<range.lowerBound, replacement: open)]
178        }
179        if !keys.isEmpty { lastChange = (keys, nil) }
180        return [.edit(edits, caret: range.lowerBound)]
181    }
182
183    /// The delimiters around `caret` for `key`, as `ds` and `cs` find them; with an opening
184    /// bracket, the blanks inside go too.
185    func surrounding(_ key: KeyChord, _ buffer: Buffer, _ caret: Int) -> (open: Range<Int>, close: Range<Int>)? {
186        let pairs: [String: (unichar, unichar)] = ["(": (40, 41), ")": (40, 41), "b": (40, 41), "[": (91, 93), "]": (91, 93), "r": (91, 93),
187                                                    "{": (123, 125), "}": (123, 125), "B": (123, 125), "<": (60, 62), ">": (60, 62), "a": (60, 62)]
188        let outer: Range<Int>
189        if let (open, close) = pairs[key.key] {
190            guard let r = buffer.bracketObject(caret, open: open, close: close, inner: false) else { return nil }
191            outer = r
192        } else if key.key.count == 1, let quote = key.key.utf16.first {
193            guard let r = buffer.quoteObject(caret, quote: quote, inner: false) else { return nil }
194            outer = r
195            // `a"` takes blanks after the closing quote; the delimiter is the quote itself.
196            var end = r.upperBound
197            while end > r.lowerBound, buffer.char(end - 1) != quote { end -= 1 }
198            return (r.lowerBound..<(r.lowerBound + 1), (end - 1)..<end)
199        } else {
200            return nil
201        }
202        var open = outer.lowerBound..<(outer.lowerBound + 1)
203        var close = (outer.upperBound - 1)..<outer.upperBound
204        if ["(", "[", "{", "<"].contains(key.key) {
205            while open.upperBound < close.lowerBound, let c = buffer.char(open.upperBound), Buffer.isBlank(c) { open = open.lowerBound..<(open.upperBound + 1) }
206            while close.lowerBound > open.upperBound, let c = buffer.char(close.lowerBound - 1), Buffer.isBlank(c) { close = (close.lowerBound - 1)..<close.upperBound }
207        }
208        return (open, close)
209    }
210
211    // MARK: - Commenting
212
213    /// `comment-or-uncomment-region` in Org: `# ` at the shallowest indentation of the lines'
214    /// text, blank lines left alone; when every line is a comment, the comments come off.
215    static func toggleComments(_ buffer: Buffer, _ range: Range<Int>) -> [TextEdit] {
216        var lines: [Int] = []
217        var line = buffer.lineStart(range.lowerBound)
218        let last = buffer.lineStart(max(range.lowerBound, range.upperBound - 1))
219        while true {
220            lines.append(line)
221            let end = buffer.lineEnd(line)
222            guard line < last, end < buffer.length else { break }
223            line = end + 1
224        }
225        let content = lines.filter { buffer.firstNonBlankOrEnd($0) < buffer.lineEnd($0) }
226        guard !content.isEmpty else { return [] }
227        let commented = content.allSatisfy { start in
228            let text = buffer.substring(buffer.firstNonBlankOrEnd(start)..<buffer.lineEnd(start))
229            return text == "#" || text.hasPrefix("# ")
230        }
231        if commented {
232            return content.reversed().map { start in
233                let at = buffer.firstNonBlankOrEnd(start)
234                let length = buffer.lineEnd(start) - at >= 2 && buffer.char(at + 1) == 32 ? 2 : 1
235                return TextEdit(range: at..<(at + length), replacement: "")
236            }
237        }
238        let column = content.map { buffer.firstNonBlankOrEnd($0) - $0 }.min() ?? 0
239        return content.reversed().map { TextEdit(range: ($0 + column)..<($0 + column), replacement: "# ") }
240    }
241
242    // MARK: - Org text objects
243
244    /// evil-org's text objects: `e` the object at point, `E` the element, `r` the greater element
245    /// around it, `R` the subtree. `a` takes the blanks after; `i` the contents.
246    /// An org-element: `end` takes the blanks after it, `contents` is nil for elements without any.
247    struct OrgElement {
248        var begin: Int
249        var end: Int
250        var contents: Range<Int>?
251        var greater: Bool
252        var block = false
253        var node: SyntaxNode?
254    }
255
256    /// `j` and `k` are evil-org's `gj` and `gk` (`org-forward-element`, `org-backward-element`):
257    /// an empty range where they go, nil where they can't.
258    static func orgObject(_ key: String, inner: Bool, _ buffer: Buffer, _ position: Int) -> (range: Range<Int>, linewise: Bool)? {
259        let tree = OrgParser.parse(buffer.ns as String)
260        /// The point the element functions look at, and the nodes containing it.
261        var c = position
262        var chain: [SyntaxNode] = []
263        func look(at p: Int) {
264            c = p
265            chain = []
266            var node = tree.root
267            while let child = node.child(containing: c) {
268                chain.append(child)
269                node = child
270            }
271        }
272        look(at: position)
273        /// The end of the line ending at or after `p`, without trailing blank lines.
274        func trimmed(_ lower: Int, _ upper: Int) -> Int {
275            var end = upper
276            while end > lower, let ch = buffer.char(end - 1), ch == 10 || Buffer.isBlank(ch) { end -= 1 }
277            if end > lower, buffer.char(end) == 10 { end += 1 }
278            return max(lower, end)
279        }
280        /// The org-element for a node of ours; a `section` here is Org's headline.
281        func element(_ n: SyntaxNode) -> OrgElement {
282            let r = n.range
283            var post = r.upperBound
284            let limit = n.parent?.range.upperBound ?? buffer.length
285            while post < limit {
286                let lineEnd = buffer.lineEnd(post)
287                guard (post..<lineEnd).allSatisfy({ buffer.char($0).map(Buffer.isBlank) ?? true }), lineEnd < buffer.length else { break }
288                post = min(limit, lineEnd + 1)
289            }
290            switch n.kind {
291            case .section:
292                let body = min(r.upperBound, buffer.lineEnd(r.lowerBound) + 1)
293                let contentsEnd = trimmed(body, r.upperBound)
294                return OrgElement(begin: r.lowerBound, end: r.upperBound, contents: body < contentsEnd ? body..<contentsEnd : nil, greater: true, node: n)
295            case .plainList, .table, .drawer, .propertyDrawer, .dynamicBlock, .footnoteDefinition, .inlineTask:
296                return OrgElement(begin: r.lowerBound, end: post, contents: r.lowerBound..<trimmed(r.lowerBound, r.upperBound), greater: true, node: n)
297            case .item:
298                let first = n.children.first { ![.bullet, .checkbox, .itemTag].contains($0.kind) }?.range.lowerBound ?? r.upperBound
299                return OrgElement(begin: r.lowerBound, end: post, contents: min(first, r.upperBound)..<trimmed(first, r.upperBound), greater: true, node: n)
300            case .block:
301                let name = buffer.substring(r.lowerBound..<buffer.lineEnd(r.lowerBound)).lowercased()
302                let greater = !["src", "example", "export", "verse", "comment"].contains { name.hasPrefix("#+begin_\($0)") }
303                return OrgElement(begin: r.lowerBound, end: post, contents: nil, greater: greater, block: true, node: n)
304            case .latexEnvironment:
305                return OrgElement(begin: r.lowerBound, end: post, contents: nil, greater: false, block: true, node: n)
306            case .paragraph:
307                return OrgElement(begin: r.lowerBound, end: post, contents: r.lowerBound..<trimmed(r.lowerBound, r.upperBound), greater: false, node: n)
308            default:
309                return OrgElement(begin: r.lowerBound, end: post, contents: nil, greater: false, node: n)
310            }
311        }
312        /// Org's section: the text under a heading before its first child, or before the first heading.
313        func section(_ n: SyntaxNode) -> OrgElement? {
314            let begin = n.kind == .zerothSection ? n.range.lowerBound : min(n.range.upperBound, buffer.lineEnd(n.range.lowerBound) + 1)
315            let end = n.children.first { $0.kind == .section && $0.range.lowerBound >= begin }?.range.lowerBound ?? n.range.upperBound
316            guard begin < end else { return nil }
317            return OrgElement(begin: begin, end: end, contents: begin..<trimmed(begin, end), greater: true)
318        }
319        func parent(_ e: OrgElement) -> OrgElement? {
320            guard let n = e.node else {
321                // A section's parent is its headline.
322                return chain.last { $0.kind == .section && $0.range.lowerBound < e.begin }.map(element)
323            }
324            guard let up = n.parent else { return nil }
325            switch up.kind {
326            case .section where n.kind != .section: return section(up)
327            case .zerothSection: return section(up)
328            case .document: return nil
329            default: return element(up)
330            }
331        }
332        /// org-element-at-point.
333        func atPoint() -> OrgElement? {
334            let objects: Set<SyntaxKind> = [.title, .heading, .bold, .italic, .underline, .strikeThrough, .verbatim, .code, .link, .linkDescription,
335                                            .timestamp, .footnoteReference, .statisticsCookie, .target, .radioTarget, .macro, .inlineSourceBlock,
336                                            .latexFragment, .lineBreak, .superscript, .subscript, .entity, .tableCell, .nodeProperty,
337                                            .citation, .citationReference, .exportSnippet, .inlineBabelCall, .itemTag]
338            var elements = chain.filter { !objects.contains($0.kind) }
339            if chain.contains(where: { $0.kind == .heading }), let i = elements.lastIndex(where: { $0.kind == .section }) {
340                elements = Array(elements[...i])
341            }
342            guard var n = elements.last else { return nil }
343            if [.section, .zerothSection, .plainList, .item].contains(n.kind), !chain.contains(where: { $0.kind == .heading }) {
344                // On a blank line: the element before it, whose post-blank it is.
345                if let before = n.children.last(where: { $0.range.upperBound <= c && $0.kind != .heading && $0.kind != .bullet }), !n.children.contains(where: { $0.range.contains(c) }) {
346                    n = before
347                } else if n.kind == .zerothSection || n.kind == .section {
348                    return section(n)
349                }
350            }
351            if let up = n.parent, up.range.lowerBound == n.range.lowerBound, (n.kind == .item && up.kind == .plainList) || (n.kind == .tableRow && up.kind == .table) {
352                n = up
353            }
354            return element(n)
355        }
356        func innerRange(_ e: OrgElement) -> Range<Int> {
357            if e.block {
358                let first = min(e.end, buffer.lineEnd(e.begin) + 1)
359                let closing = buffer.lineStart(max(e.begin, trimmed(e.begin, e.end) - 1))
360                return min(first, closing)..<closing
361            }
362            if let contents = e.contents { return contents }
363            return e.begin..<max(e.begin, trimmed(e.begin, e.end) - (buffer.char(trimmed(e.begin, e.end) - 1) == 10 ? 1 : 0))
364        }
365        /// The stars of the heading on the line holding `p`, or nil.
366        func headingLevel(_ p: Int) -> Int? {
367            let line = buffer.substring(buffer.lineStart(p)..<buffer.lineEnd(p))
368            guard let stars = line.range(of: "^\\*+(?=[ \\t]|$)", options: .regularExpression) else { return nil }
369            return line.distance(from: stars.lowerBound, to: stars.upperBound)
370        }
371        func at(_ p: Int) -> (range: Range<Int>, linewise: Bool) { (p..<p, false) }
372        switch key {
373        case "j":
374            guard position < buffer.length else { return nil }
375            if headingLevel(position) != nil {
376                // To the end of the subtree, when a heading is there.
377                guard let section = chain.last(where: { $0.kind == .section }) else { return nil }
378                let end = section.range.upperBound
379                return end < buffer.length && headingLevel(end) != nil ? at(end) : nil
380            }
381            guard let e = atPoint() else { return nil }
382            if let up = parent(e), up.contents?.upperBound == e.end { return at(up.end) }
383            return at(e.end)
384        case "k":
385            guard position > 0 else { return nil }
386            if let level = headingLevel(position) {
387                // The previous heading at this level, or else the parent.
388                var line = buffer.lineStart(position)
389                while line > 0 {
390                    line = buffer.lineStart(line - 1)
391                    if let other = headingLevel(line), other <= level { return at(line) }
392                }
393                return nil
394            }
395            guard let e = atPoint() else { return nil }
396            if position != e.begin { return at(e.begin) }
397            var p = e.begin
398            while p > 0, let ch = buffer.char(p - 1), Buffer.isSpace(ch) { p -= 1 }
399            guard p > 0 else { return at(0) }
400            look(at: p)
401            guard var prev = atPoint() else { return at(e.begin) }
402            var target = prev.begin
403            while let up = parent(prev), up.end <= e.begin {
404                target = up.begin
405                prev = up
406            }
407            return at(target)
408        case "R":
409            guard let headline = chain.last(where: { $0.kind == .section }).map(element) else { return nil }
410            return (inner ? (headline.contents ?? headline.begin..<headline.begin) : headline.begin..<headline.end, true)
411        case "E":
412            guard let e = atPoint() else { return nil }
413            return (inner ? innerRange(e) : e.begin..<e.end, false)
414        case "r":
415            guard var e = atPoint() else { return nil }
416            if !e.greater, let up = parent(e) { e = up }
417            return (inner ? innerRange(e) : e.begin..<e.end, !inner)
418        default:
419            let objects: Set<SyntaxKind> = [.bold, .italic, .underline, .strikeThrough, .verbatim, .code, .link, .timestamp, .footnoteReference,
420                                            .statisticsCookie, .target, .radioTarget, .macro, .inlineSourceBlock, .latexFragment, .entity, .superscript, .subscript,
421                                            .citation, .citationReference, .exportSnippet, .inlineBabelCall]
422            guard let found = chain.last(where: { objects.contains($0.kind) }) else {
423                guard let e = atPoint() else { return nil }
424                return (inner ? innerRange(e) : e.begin..<e.end, false)
425            }
426            var range = found.range
427            var end = range.upperBound
428            while let ch = buffer.char(end), Buffer.isBlank(ch) { end += 1 }
429            if inner {
430                switch found.kind {
431                case .bold, .italic, .underline, .strikeThrough, .verbatim, .code:
432                    range = (range.lowerBound + 1)..<(range.upperBound - 1)
433                case .link:
434                    if let description = found.firstChild(.linkDescription) { range = description.range }
435                default:
436                    break
437                }
438                return (range, false)
439            }
440            return (range.lowerBound..<end, false)
441        }
442    }
443
444    // MARK: - Block visual
445
446    /// The block's columns and line starts, from `anchor` to `caret`; `$` stretches it to each
447    /// line's end.
448    func block(_ buffer: Buffer, _ caret: Int) -> (lines: [Int], left: Int, right: Int) {
449        let a = min(anchor, caret), b = max(anchor, caret)
450        let left = min(anchor - buffer.lineStart(anchor), caret - buffer.lineStart(caret))
451        let right = blockToEnd ? Int.max : max(anchor - buffer.lineStart(anchor), caret - buffer.lineStart(caret))
452        var lines: [Int] = []
453        var line = buffer.lineStart(a)
454        while true {
455            lines.append(line)
456            let end = buffer.lineEnd(line)
457            guard end < buffer.length, line < buffer.lineStart(b) else { break }
458            line = end + 1
459        }
460        return (lines, left, right)
461    }
462
463    /// Each line's part of the block.
464    func blockRanges(_ buffer: Buffer, _ caret: Int) -> [Range<Int>] {
465        let (lines, left, right) = block(buffer, caret)
466        return lines.map { line in
467            let end = buffer.lineEnd(line)
468            let lower = min(line + left, end)
469            let upper = right == Int.max ? end : min(line + right + 1, end)
470            return lower..<max(lower, upper)
471        }
472    }
473
474    func blockActions(_ buffer: Buffer, _ caret: Int) -> [Action] {
475        [.move(caret: caret), .selectBlock(blockRanges(buffer, caret))]
476    }
477
478    /// Keys in block visual state.
479    mutating func parseBlock(_ rest: ArraySlice<KeyChord>, _ buffer: inout Buffer, _ context: Context, register: Character?, count n: Int, keys: [KeyChord]) -> Parse {
480        guard let first = rest.first else { return .incomplete }
481        let caret = context.caret
482        let ranges = blockRanges(buffer, caret)
483        func leave() {
484            lastVisual = (anchor, caret, .visualBlock)
485            mode = .normal
486            blockToEnd = false
487        }
488        if first == KeyChord("v", .control) || (first.modifiers.isEmpty && first.key == "ESC") {
489            leave()
490            return .done([.move(caret: buffer.clampNormal(caret))])
491        }
492        guard first.modifiers.isEmpty else { return .invalid }
493        switch first.key {
494        case "v", "V":
495            lastVisual = (anchor, caret, .visualBlock)
496            mode = first.key == "v" ? .visual : .visualLine
497            blockToEnd = false
498            return .done(visualActions(buffer, caret))
499        case "o", "O":
500            let old = anchor
501            anchor = caret
502            return .done(blockActions(buffer, old))
503        case "$":
504            blockToEnd = true
505            return .done(blockActions(buffer, buffer.lineEnd(caret) > buffer.lineStart(caret) ? buffer.lineEnd(caret) - 1 : caret))
506        case "d", "x", "y", "c":
507            let removed = ranges.map { buffer.substring($0) }.joined(separator: "\n")
508            leave()
509            var actions = store(removed, linewise: false, register: register, yank: first.key == "y")
510            let top = ranges.first?.lowerBound ?? caret
511            if first.key == "y" { return .done(actions + [.move(caret: top)]) }
512            let edits = ranges.reversed().map { TextEdit(range: $0, replacement: "") }
513            if first.key == "c" {
514                let (lines, left, _) = block(buffer, caret)
515                blockInsert = (lines.dropFirst().map { buffer.lineNumber($0) }, left, false)
516                actions.insert(.edit(edits, caret: top), at: 0)
517                enterInsert(at: top, keys: [])
518                return .done(actions)
519            }
520            actions.insert(.edit(edits, caret: buffer.removing(top..<top).clampNormal(top)), at: 0)
521            return .done(actions)
522        case "I", "A":
523            let (lines, left, right) = block(buffer, caret)
524            leave()
525            let column = first.key == "I" ? left : (right == Int.max ? Int.max : right + 1)
526            let top = lines.first ?? caret
527            let at: Int
528            if column == Int.max {
529                at = buffer.lineEnd(top)
530            } else {
531                at = min(top + column, buffer.lineEnd(top))
532            }
533            blockInsert = (lines.dropFirst().map { buffer.lineNumber($0) }, column, first.key == "A")
534            var actions: [Action] = []
535            // `A` past a short first line pads it with spaces.
536            if first.key == "A", column != Int.max, buffer.lineEnd(top) < top + column {
537                let pad = top + column - buffer.lineEnd(top)
538                actions.append(.edit([TextEdit(range: buffer.lineEnd(top)..<buffer.lineEnd(top), replacement: String(repeating: " ", count: pad))], caret: top + column))
539                enterInsert(at: top + column, keys: [])
540                return .done(actions)
541            }
542            enterInsert(at: at, keys: [])
543            return .done([.move(caret: at)])
544        case "r":
545            guard rest.count >= 2 else { return .incomplete }
546            let char = rest[rest.startIndex + 1].key == "SPC" ? " " : rest[rest.startIndex + 1].key
547            guard char.count == 1 else { return .invalid }
548            leave()
549            let edits = ranges.reversed().map { TextEdit(range: $0, replacement: String(repeating: char, count: $0.count)) }
550            return .done([.edit(edits, caret: ranges.first?.lowerBound ?? caret)])
551        case "~", "u", "U":
552            leave()
553            let edits = ranges.reversed().map { r -> TextEdit in
554                let s = buffer.substring(r)
555                let t = first.key == "u" ? s.lowercased() : first.key == "U" ? s.uppercased() : String(s.map { $0.isUppercase ? Character($0.lowercased()) : Character($0.uppercased()) })
556                return TextEdit(range: r, replacement: t)
557            }
558            return .done([.edit(edits, caret: ranges.first?.lowerBound ?? caret)])
559        case ">", "<":
560            leave()
561            let lines = block(buffer, caret).lines
562            let range = (lines.first ?? caret)..<min(buffer.length, buffer.lineEnd(lines.last ?? caret) + 1)
563            mode = .normal
564            return .done(apply(first.key, range: range, linewise: true, buffer, register: register, keys: [], count: n))
565        default:
566            var target = caret
567            switch motion(Array(rest), &buffer, &target, count: n, operatorPending: false) {
568            case .incomplete: return .incomplete
569            case .invalid: return .invalid
570            case .moved(let to, _): return .done(blockActions(buffer, to))
571            }
572        }
573    }
574
575    /// What block `I`, `A` or `c` typed, put on the block's other lines at its column.
576    func replicateBlockInsert(_ block: (lines: [Int], column: Int, pad: Bool), _ buffer: Buffer, caret: Int) -> [Action] {
577        var edits: [TextEdit] = []
578        for number in block.lines.reversed() {
579            let line = buffer.lineOffset(number)
580            let end = buffer.lineEnd(line)
581            if block.column == Int.max {
582                edits.append(TextEdit(range: end..<end, replacement: insertedText))
583            } else if line + block.column <= end {
584                edits.append(TextEdit(range: (line + block.column)..<(line + block.column), replacement: insertedText))
585            } else if block.pad {
586                edits.append(TextEdit(range: end..<end, replacement: String(repeating: " ", count: line + block.column - end) + insertedText))
587            }
588        }
589        let lineStart = buffer.lineStart(caret)
590        let back = caret > lineStart ? caret - 1 : caret
591        return edits.isEmpty ? [.move(caret: back)] : [.edit(edits, caret: back)]
592    }
593}
594
595/// `char-fold-to-regexp` for one snipe key, as evil-snipe's `evil-snipe--process-key` uses it
596/// with `evil-snipe-char-fold`: a letter also matches its accented and other variants.
597enum CharFold {
598    /// `char-fold-table` by code point.
599    static let regexps: [UInt32: String] = {
600        var regexps: [UInt32: String] = [:]
601        // Scalars, not characters: a tab before a combining mark is one character.
602        for line in table.unicodeScalars.split(separator: "\n") {
603            guard let tab = line.firstIndex(of: "\t"), let code = UInt32(String(line[..<tab]), radix: 16) else { continue }
604            regexps[code] = String(line[line.index(after: tab)...])
605        }
606        return regexps
607    }()
608
609    /// The Emacs regexp `key` matches: its table entry, else the key quoted.
610    static func regexp(_ key: Character) -> String {
611        let scalars = key.unicodeScalars
612        if scalars.count == 1, let entry = regexps[scalars.first!.value] { return entry }
613        return scalars.map { "[*.\\?+^$".unicodeScalars.contains($0) ? "\\" + String($0) : String($0) }.joined()
614    }
615
616    /// An Emacs regexp of shy groups, alternatives, bracket expressions and literals, which is
617    /// all `char-fold-table` and `regexp-quote` produce, as an ICU pattern.
618    static func icu(_ emacs: String) -> String {
619        let s = Array(emacs.unicodeScalars)
620        func literal(_ c: Unicode.Scalar) -> String { String(format: "\\x{%X}", c.value) }
621        var out = ""
622        var i = 0
623        while i < s.count {
624            if s[i] == "\\", i + 1 < s.count {
625                switch s[i + 1] {
626                case "(":
627                    out += "(?:"
628                    i += i + 3 < s.count && s[i + 2] == "?" && s[i + 3] == ":" ? 4 : 2
629                case "|":
630                    out += "|"
631                    i += 2
632                case ")":
633                    out += ")"
634                    i += 2
635                default:
636                    out += literal(s[i + 1])
637                    i += 2
638                }
639            } else if s[i] == "[", let close = s[(i + 2)...].firstIndex(of: "]") {
640                // A `]` right after `[` is a member; `-` between two members is a range.
641                var j = i + 1
642                out += "["
643                while j < close {
644                    if j + 2 < close, s[j + 1] == "-" {
645                        out += literal(s[j]) + "-" + literal(s[j + 2])
646                        j += 3
647                    } else {
648                        out += literal(s[j])
649                        j += 1
650                    }
651                }
652                out += "]"
653                i = close + 1
654            } else {
655                out += literal(s[i])
656                i += 1
657            }
658        }
659        return out
660    }
661}