Sources/OrgCore/Keymap/Keymap.swift
176 lines · 7341 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", "timestamp", "tblfm", "src"]
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 case "timestamp": return EmacsBuffer(context.text, point: context.caret).atTimestamp() != nil
115 case "tblfm": return TableRecalculate.tblfmLine(in: context) != nil
116 case "src": return enclosing(.block, context).map(DocumentModel.isSrcBlock) ?? false
117 default: return false
118 }
119 }
120
121 private static func enclosing(_ kind: SyntaxKind, _ context: EditContext) -> SyntaxNode? {
122 var node = context.tree.root
123 while let child = node.child(containing: context.caret) {
124 if child.kind == kind { return child }
125 node = child
126 }
127 return nil
128 }
129}
130
131/// Turns key presses into bindings, holding a pending prefix between presses.
132public struct KeyDispatcher: Sendable {
133 public enum Outcome: Sendable, Equatable {
134 /// A prefix; waiting for the next key.
135 case pending([KeyChord])
136 /// Bindings for the completed sequence, highest priority first.
137 case complete([KeyChord], [KeyBinding])
138 /// A sequence of two or more keys that nothing binds.
139 case undefined([KeyChord])
140 /// A single key nothing binds: let the text system have it.
141 case unbound
142 /// C-g during a prefix.
143 case cancelled
144 }
145
146 public var keymap: Keymap
147 public var mode: String?
148 public private(set) var pending: [KeyChord] = []
149
150 public init(keymap: Keymap, mode: String? = nil) {
151 self.keymap = keymap
152 self.mode = mode
153 }
154
155 public static let quit = KeyChord("g", .control)
156
157 public mutating func feed(_ chord: KeyChord) -> Outcome {
158 if !pending.isEmpty, chord == Self.quit {
159 pending = []
160 return .cancelled
161 }
162 let sequence = pending + [chord]
163 if keymap.isPrefix(sequence, mode: mode) {
164 pending = sequence
165 return .pending(sequence)
166 }
167 pending = []
168 let candidates = keymap.candidates(for: sequence, mode: mode)
169 if !candidates.isEmpty { return .complete(sequence, candidates) }
170 return sequence.count == 1 ? .unbound : .undefined(sequence)
171 }
172
173 public mutating func reset() {
174 pending = []
175 }
176}