krz/orgstar

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

Sources/OrgCore/Keymap/Keymap.swift

056befa97fffa110959cb6010c1986461c09dc0b
orgstar/Sources/OrgCore/Keymap/Keymap.swift history · blame · raw

173 lines · 7041 bytes

  1import Foundation
  2
  3public struct KeyBinding: Sendable, Equatable {
  4    public let keys: [KeyChord]
  5    /// A command id, or `none` to unbind keys an earlier layer bound.
  6    public let command: String
  7    /// A context name (`KeyContext`); nil binds everywhere.
  8    public let when: String?
  9    /// A modal state; nil binds outside modal editing.
 10    public let mode: String?
 11
 12    public init(keys: [KeyChord], command: String, when: String? = nil, mode: String? = nil) {
 13        self.keys = keys
 14        self.command = command
 15        self.when = when
 16        self.mode = mode
 17    }
 18
 19    public var unbinds: Bool { command == "none" }
 20}
 21
 22/// Bindings in priority order: later bindings win over earlier ones for the same keys.
 23/// Layers are concatenated: preset, then the user's file.
 24public struct Keymap: Sendable, Equatable {
 25    public private(set) var bindings: [KeyBinding]
 26
 27    public init(_ bindings: [KeyBinding] = []) {
 28        self.bindings = bindings
 29    }
 30
 31    /// Reads `[[bind]]` tables. Entries that can't be used are skipped and reported in
 32    /// `problems`; a file that isn't valid TOML throws.
 33    public init(toml: String, problems: inout [String]) throws {
 34        bindings = []
 35        for table in try TOML.parse(toml) where !(table.name.isEmpty && table.values.isEmpty) {
 36            guard table.name == "bind", table.isArrayElement else {
 37                problems.append("line \(table.line): unknown table [\(table.name)]")
 38                continue
 39            }
 40            guard let keyText = table.values["keys"]?.string, let keys = KeySequence.parse(keyText) else {
 41                problems.append("line \(table.line): bad or missing keys")
 42                continue
 43            }
 44            guard let command = table.values["command"]?.string, !command.isEmpty else {
 45                problems.append("line \(table.line): missing command")
 46                continue
 47            }
 48            let when = table.values["when"]?.string
 49            if let when, !KeyContext.names.contains(when) {
 50                problems.append("line \(table.line): unknown context \(when)")
 51                continue
 52            }
 53            bindings.append(KeyBinding(keys: keys, command: command, when: when, mode: table.values["mode"]?.string))
 54        }
 55    }
 56
 57    public static func layered(_ layers: [Keymap]) -> Keymap {
 58        Keymap(layers.flatMap(\.bindings))
 59    }
 60
 61    /// Bindings for exactly `keys` in `mode`, highest priority first. An unbinding with no
 62    /// context hides everything below it.
 63    public func candidates(for keys: [KeyChord], mode: String? = nil) -> [KeyBinding] {
 64        var result: [KeyBinding] = []
 65        for binding in bindings.reversed() where binding.mode == mode && binding.keys == keys {
 66            if binding.unbinds {
 67                if binding.when == nil { break }
 68                continue
 69            }
 70            result.append(binding)
 71        }
 72        return result
 73    }
 74
 75    /// Whether some live binding in `mode` starts with `keys` and is longer.
 76    public func isPrefix(_ keys: [KeyChord], mode: String? = nil) -> Bool {
 77        let longer = Set(bindings.filter { $0.mode == mode && $0.keys.count > keys.count && Array($0.keys.prefix(keys.count)) == keys }.map(\.keys))
 78        return longer.contains { !candidates(for: $0, mode: mode).isEmpty }
 79    }
 80
 81    /// What can follow `prefix`: each next key with the command it runs, or nil when it
 82    /// leads to a longer sequence. Sorted by key.
 83    public func continuations(of prefix: [KeyChord], mode: String? = nil) -> [(key: KeyChord, command: String?)] {
 84        var seen: [KeyChord: String?] = [:]
 85        for binding in bindings where binding.mode == mode && binding.keys.count > prefix.count && Array(binding.keys.prefix(prefix.count)) == prefix {
 86            let next = binding.keys[prefix.count]
 87            let sequence = prefix + [next]
 88            guard seen[next] == nil else { continue }
 89            if isPrefix(sequence, mode: mode) {
 90                seen[next] = .some(nil)
 91            } else if let command = candidates(for: sequence, mode: mode).first?.command {
 92                seen[next] = .some(command)
 93            }
 94        }
 95        return seen.map { (key: $0.key, command: $0.value) }.sorted { $0.key.description < $1.key.description }
 96    }
 97
 98    /// The first live binding of `command`, for showing its keys.
 99    public func keys(for command: String, mode: String? = nil) -> [KeyChord]? {
100        bindings.reversed().first { $0.mode == mode && $0.command == command && candidates(for: $0.keys, mode: mode).contains($0) }?.keys
101    }
102}
103
104/// Context names a binding's `when` can use.
105public enum KeyContext {
106    public static let names: Set<String> = ["heading", "table", "item", "region"]
107
108    public static func holds(_ name: String, in context: EditContext) -> Bool {
109        switch name {
110        case "heading": return headingOnLine(at: context.caret, in: context.tree) != nil
111        case "table": return enclosing(.table, context) != nil
112        case "item": return enclosing(.item, context) != nil
113        case "region": return context.selection.contains { !$0.isEmpty }
114        default: return false
115        }
116    }
117
118    private static func enclosing(_ kind: SyntaxKind, _ context: EditContext) -> SyntaxNode? {
119        var node = context.tree.root
120        while let child = node.child(containing: context.caret) {
121            if child.kind == kind { return child }
122            node = child
123        }
124        return nil
125    }
126}
127
128/// Turns key presses into bindings, holding a pending prefix between presses.
129public struct KeyDispatcher: Sendable {
130    public enum Outcome: Sendable, Equatable {
131        /// A prefix; waiting for the next key.
132        case pending([KeyChord])
133        /// Bindings for the completed sequence, highest priority first.
134        case complete([KeyChord], [KeyBinding])
135        /// A sequence of two or more keys that nothing binds.
136        case undefined([KeyChord])
137        /// A single key nothing binds: let the text system have it.
138        case unbound
139        /// C-g during a prefix.
140        case cancelled
141    }
142
143    public var keymap: Keymap
144    public var mode: String?
145    public private(set) var pending: [KeyChord] = []
146
147    public init(keymap: Keymap, mode: String? = nil) {
148        self.keymap = keymap
149        self.mode = mode
150    }
151
152    public static let quit = KeyChord("g", .control)
153
154    public mutating func feed(_ chord: KeyChord) -> Outcome {
155        if !pending.isEmpty, chord == Self.quit {
156            pending = []
157            return .cancelled
158        }
159        let sequence = pending + [chord]
160        if keymap.isPrefix(sequence, mode: mode) {
161            pending = sequence
162            return .pending(sequence)
163        }
164        pending = []
165        let candidates = keymap.candidates(for: sequence, mode: mode)
166        if !candidates.isEmpty { return .complete(sequence, candidates) }
167        return sequence.count == 1 ? .unbound : .undefined(sequence)
168    }
169
170    public mutating func reset() {
171        pending = []
172    }
173}