Sources/OrgCore/Commands/ElectricPair.swift
436 lines · 18230 bytes
32 symbols in this file
1import Foundation
2
3// `electric-pair-mode` (Emacs 31.1 elec-pair.el) in an Org buffer, with its defaults:
4// `electric-pair-preserve-balance`, skipping whitespace before a closer, deleting adjacent
5// pairs and opening a line between them. Delimiters are what `org-mode-syntax-table` makes
6// them: () [] {} <> and "; inside a string, `prog-mode-syntax-table`'s.
7
8/// A syntax table's view of a character, as far as pairing goes.
9private enum SyntaxClass: Equatable {
10 case open(unichar), close(unichar), string, escape, other
11}
12
13private enum SyntaxTable {
14 case org, text
15
16 func classify(_ c: unichar) -> SyntaxClass {
17 switch c {
18 case 40: return .open(41)
19 case 91: return .open(93)
20 case 123: return .open(125)
21 case 41: return .close(40)
22 case 93: return .close(91)
23 case 125: return .close(123)
24 case 34: return .string
25 case 60: return self == .org ? .open(62) : .other
26 case 62: return self == .org ? .close(60) : .other
27 // Org makes the backslash a symbol constituent; the standard table escapes with it.
28 case 92: return self == .org ? .other : .escape
29 default: return .other
30 }
31 }
32
33 /// `matching-paren`.
34 func matching(_ c: unichar) -> unichar? {
35 switch classify(c) {
36 case .open(let m), .close(let m): return m
37 default: return nil
38 }
39 }
40}
41
42/// The text being typed into, with point, as the hook functions see the buffer.
43private final class PairBuffer {
44 let s: NSMutableString
45 var point: Int
46
47 init(_ text: String, point: Int) {
48 s = NSMutableString(string: text)
49 self.point = point
50 }
51
52 var length: Int { s.length }
53 func char(_ i: Int) -> unichar? { i >= 0 && i < s.length ? s.character(at: i) : nil }
54 var charAfter: unichar? { char(point) }
55 var charBefore: unichar? { char(point - 1) }
56
57 func insert(_ text: String, at i: Int) {
58 s.insert(text, at: i)
59 if point >= i { point += (text as NSString).length }
60 }
61
62 func delete(_ range: Range<Int>) {
63 s.deleteCharacters(in: NSRange(location: range.lowerBound, length: range.count))
64 if point > range.upperBound { point -= range.count } else if point > range.lowerBound { point = range.lowerBound }
65 }
66
67 // MARK: Parsing, as `parse-partial-sexp` and `scan-sexps`
68
69 struct State {
70 var depth = 0
71 /// Positions of the open parens enclosing the end, innermost last (nth 9).
72 var opens: [Int] = []
73 /// The string's delimiter while inside one (nth 3).
74 var stringChar: unichar?
75 /// Where that string starts (nth 8).
76 var stringStart: Int?
77 }
78
79 func parse(from: Int, to: Int, table: SyntaxTable) -> State {
80 var state = State()
81 var i = from
82 while i < to, let c = char(i) {
83 let cls = table.classify(c)
84 if let delimiter = state.stringChar {
85 if cls == .escape { i += 2; continue }
86 if c == delimiter { state.stringChar = nil; state.stringStart = nil }
87 } else {
88 switch cls {
89 case .open: state.depth += 1; state.opens.append(i)
90 case .close: state.depth -= 1; if !state.opens.isEmpty { state.opens.removeLast() }
91 case .string: state.stringChar = c; state.stringStart = i
92 case .escape: i += 2; continue
93 case .other: break
94 }
95 }
96 i += 1
97 }
98 return state
99 }
100
101 /// `syntax-ppss`: the buffer's table, from the start.
102 func ppss(_ pos: Int) -> State { parse(from: 0, to: pos, table: .org) }
103
104 /// `electric-pair--syntax-ppss` with WHERE `(string comment)`: inside a string, its text
105 /// parsed with the text table.
106 func pairPpss(_ pos: Int) -> State {
107 let quick = ppss(pos)
108 if quick.stringChar != nil, let start = quick.stringStart {
109 return parse(from: start + 1, to: pos, table: .text)
110 }
111 return quick
112 }
113
114 enum ScanResult {
115 case done
116 /// "Containing expression ends prematurely", and where it stopped.
117 case premature(Int)
118 case unbalanced
119 }
120
121 /// `scan-sexps` from `from` over as many sexps as there are, forward or back.
122 func scan(from: Int, forward: Bool, table: SyntaxTable) -> ScanResult {
123 var depth = 0
124 if forward {
125 var i = from
126 while let c = char(i) {
127 switch table.classify(c) {
128 case .open: depth += 1
129 case .close:
130 depth -= 1
131 if depth < 0 { return .premature(i + 1) }
132 case .string:
133 var j = i + 1
134 while let d = char(j), d != c { j += table.classify(d) == .escape ? 2 : 1 }
135 if j >= length { return .unbalanced }
136 i = j
137 case .escape: i += 1
138 case .other: break
139 }
140 i += 1
141 }
142 } else {
143 var i = from - 1
144 while i >= 0, let c = char(i) {
145 if quoted(i, table) { i -= 1; continue }
146 switch table.classify(c) {
147 case .close: depth += 1
148 case .open:
149 depth -= 1
150 if depth < 0 { return .premature(i) }
151 case .string:
152 var j = i - 1
153 while j >= 0, !(char(j) == c && !quoted(j, table)) { j -= 1 }
154 if j < 0 { return .unbalanced }
155 i = j
156 default: break
157 }
158 i -= 1
159 }
160 }
161 return depth > 0 ? .unbalanced : .done
162 }
163
164 /// `char-quoted`: an odd number of escapes before `i`.
165 private func quoted(_ i: Int, _ table: SyntaxTable) -> Bool {
166 var n = 0
167 var j = i - 1
168 while j >= 0, let c = char(j), table.classify(c) == .escape { n += 1; j -= 1 }
169 return n % 2 == 1
170 }
171
172 /// `scan-sexps` forward by one from the open paren at `from`: past its closer, or nil on an
173 /// error.
174 func scanOne(from: Int, table: SyntaxTable) -> Int? {
175 var depth = 0
176 var i = from
177 while let c = char(i) {
178 switch table.classify(c) {
179 case .open: depth += 1
180 case .close:
181 depth -= 1
182 if depth < 0 { return nil }
183 if depth == 0 { return i + 1 }
184 case .string:
185 var j = i + 1
186 while let d = char(j), d != c { j += table.classify(d) == .escape ? 2 : 1 }
187 if j >= length { return nil }
188 i = j
189 if depth == 0 { return i + 1 }
190 case .escape: i += 1
191 case .other: break
192 }
193 i += 1
194 }
195 return nil
196 }
197
198 // MARK: elec-pair.el
199
200 struct Info {
201 enum Syntax { case open, close, string }
202 let syntax: Syntax
203 let pair: unichar
204 let unconditional: Bool
205 /// Where the string point is in starts.
206 let stringStart: Int?
207 }
208
209 /// `electric-quote-chars` pairs, from `electric-pair-pairs` (and the text pairs, the same).
210 static let fallback: [(unichar, unichar)] = [(34, 34), (0x2018, 0x2019), (0x201C, 0x201D)]
211
212 /// `electric-pair-syntax-info`.
213 func info(_ c: unichar) -> Info? {
214 let pre = point == 0 || ppss(point - 1).stringStart != nil
215 let post = ppss(point).stringStart
216 let stringStart = pre ? post : nil
217 let table: SyntaxTable = stringStart == nil ? .org : .text
218 switch table.classify(c) {
219 case .open(let m): return Info(syntax: .open, pair: m, unconditional: false, stringStart: stringStart)
220 case .close(let m): return Info(syntax: .close, pair: m, unconditional: false, stringStart: stringStart)
221 case .string: return Info(syntax: .string, pair: c, unconditional: false, stringStart: stringStart)
222 default: break
223 }
224 if let direct = Self.fallback.first(where: { $0.0 == c }) {
225 return direct.0 == direct.1
226 ? Info(syntax: .string, pair: c, unconditional: true, stringStart: stringStart)
227 : Info(syntax: .open, pair: direct.1, unconditional: true, stringStart: stringStart)
228 }
229 if let reverse = Self.fallback.first(where: { $0.1 == c }) {
230 return Info(syntax: .close, pair: reverse.0, unconditional: true, stringStart: stringStart)
231 }
232 return nil
233 }
234
235 struct Side {
236 var matched: Bool
237 var pair: unichar?
238 }
239
240 /// `electric-pair--balance-info`: the innermost and outermost lists around point.
241 func balance(forward: Bool, stringStart: Int?) -> (innermost: Side, outermost: Side) {
242 let table: SyntaxTable = stringStart == nil ? .org : .text
243 var innermost: Side?
244 var outermost: Side?
245 let saved = point
246 defer { point = saved }
247 while outermost == nil {
248 switch scan(from: point, forward: forward, table: table) {
249 case .done, .unbalanced:
250 outermost = Side(matched: true)
251 if innermost == nil { innermost = Side(matched: true) }
252 case .premature(let pos):
253 if stringStart != nil, ppss(point).stringStart == nil {
254 outermost = Side(matched: true)
255 if innermost == nil { innermost = Side(matched: true) }
256 break
257 }
258 point = pos
259 let matched: Bool
260 if forward {
261 let state = pairPpss(point - 1)
262 if let start = state.opens.last, let opener = char(start) {
263 matched = charBefore == (table.matching(opener) ?? opener)
264 } else {
265 matched = false
266 }
267 } else if let end = scanOne(from: point, table: table), let closer = char(end - 1) {
268 matched = charAfter == table.matching(closer)
269 } else {
270 matched = false
271 }
272 let actual = forward ? charBefore : charAfter
273 if innermost == nil { innermost = Side(matched: matched, pair: actual) }
274 if !matched { outermost = Side(matched: matched, pair: actual) }
275 }
276 }
277 return (innermost!, outermost!)
278 }
279
280 /// `electric-pair-inhibit-if-helps-balance`, with the typed character before point.
281 func inhibit(_ c: unichar) -> Bool {
282 guard let info = info(c) else { return false }
283 let at = point - 1
284 delete(at..<point)
285 defer { insert(String(utf16CodeUnits: [c], count: 1), at: at) }
286 switch info.syntax {
287 case .open:
288 let outermost = balance(forward: true, stringStart: info.stringStart).outermost
289 return outermost.matched ? false : outermost.pair == info.pair
290 case .string:
291 return ppss(length).stringChar == c
292 case .close:
293 return false
294 }
295 }
296
297 /// `electric-pair-skip-if-helps-balance`, with the typed character before point.
298 func skip(_ c: unichar) -> Bool {
299 guard let info = info(c) else { return false }
300 let at = point - 1
301 delete(at..<point)
302 defer { insert(String(utf16CodeUnits: [c], count: 1), at: at) }
303 switch info.syntax {
304 case .close:
305 let (innermost, outermost) = balance(forward: false, stringStart: info.stringStart)
306 if outermost.matched { return innermost.matched }
307 return innermost.matched && outermost.pair != info.pair
308 case .string:
309 return ppss(point).stringChar == c
310 case .open:
311 return false
312 }
313 }
314
315 /// `electric-pair--skip-whitespace`, unless that leaves or enters a string.
316 func skipWhitespace() {
317 let saved = point
318 let before = ppss(point).stringStart
319 while let c = charAfter, c == 9 || c == 32 || c == 10 { point += 1 }
320 if ppss(point).stringStart != before { point = saved }
321 }
322}
323
324public enum ElectricPair {
325 /// Typing `c` into `text` with `selection` (point at its upper end, mark at its lower):
326 /// the text and point that `self-insert-command` and `electric-pair-post-self-insert-function`
327 /// leave.
328 public static func type(_ c: unichar, text: String, selection: Range<Int>) -> (text: String, point: Int) {
329 let b = PairBuffer(text, point: selection.upperBound)
330 let typed = String(utf16CodeUnits: [c], count: 1)
331 b.insert(typed, at: b.point)
332 let pos = b.point
333 guard let info = b.info(c) else { return (b.s as String, b.point) }
334 if !selection.isEmpty {
335 // `use-region-p`: the region is wrapped.
336 let mark = selection.lowerBound
337 let pair = String(utf16CodeUnits: [info.pair], count: 1)
338 if info.syntax == .string || info.syntax == .close {
339 b.insert(pair, at: mark)
340 } else {
341 b.delete(pos - 1..<pos)
342 b.insert(pair, at: b.point)
343 b.point = mark
344 b.insert(typed, at: b.point)
345 }
346 return (b.s as String, b.point)
347 }
348 if info.syntax == .close || info.syntax == .string {
349 let skipsSelf = info.unconditional || b.skip(c)
350 if skipsSelf {
351 let skipsSpace = !(info.unconditional && info.syntax == .string)
352 let saved = b.point
353 if skipsSpace { b.skipWhitespace() }
354 let lands = b.charAfter == c
355 b.point = saved
356 if lands {
357 if skipsSpace { b.skipWhitespace() }
358 b.delete(pos - 1..<pos)
359 b.point += 1
360 return (b.s as String, b.point)
361 }
362 }
363 }
364 if info.syntax == .open || info.syntax == .string, info.unconditional || !b.inhibit(c) {
365 b.insert(String(utf16CodeUnits: [info.pair], count: 1), at: b.point)
366 b.point = pos
367 }
368 return (b.s as String, b.point)
369 }
370
371 /// DEL at `caret` between an opening delimiter and its pair: both go
372 /// (`electric-pair-delete-pair`). Nil leaves DEL as it is.
373 public static func deletePair(text: String, caret: Int) -> Range<Int>? {
374 let b = PairBuffer(text, point: caret)
375 guard let prev = b.charBefore, let next = b.charAfter, let info = b.info(prev),
376 info.syntax != .close, info.pair == next else { return nil }
377 return caret - 1..<caret + 1
378 }
379
380 /// RET at `caret` between an open paren and its closer: a second newline after point
381 /// (`electric-pair-open-newline-between-pairs`).
382 public static func opensLine(text: String, caret: Int) -> Bool {
383 let b = PairBuffer(text, point: caret)
384 guard caret > 0, caret < b.length, let prev = b.charBefore, let next = b.charAfter else { return false }
385 return SyntaxTable.org.matching(next) == prev
386 }
387}
388
389/// A key typed with `electric-pair-mode` on: a pairing character, DEL or RET. It doesn't
390/// apply where the key types as it would without the mode.
391public struct ElectricPairKey: OrgCommand {
392 public let key: String
393 public init(key: String) { self.key = key }
394 public var id: String { "internal.electric-pair" }
395 public var title: String { "Electric Pair" }
396
397 /// The characters `electric-pair-syntax-info` knows in an Org buffer.
398 public static let characters: Set<String> = ["(", ")", "[", "]", "{", "}", "<", ">", "\"", "\u{2018}", "\u{2019}", "\u{201C}", "\u{201D}"]
399
400 public func applies(in context: EditContext) -> Bool { result(context) != nil }
401
402 public func run(in context: EditContext) -> CommandStep {
403 guard let result = result(context) else { return .failed("") }
404 return .commit(result)
405 }
406
407 private func result(_ context: EditContext) -> EditResult? {
408 guard context.options.electricPair else { return nil }
409 let selection = context.selection.first ?? context.caret..<context.caret
410 let ns = context.text as NSString
411 switch key {
412 case "DEL":
413 guard selection.isEmpty, let range = ElectricPair.deletePair(text: context.text, caret: selection.lowerBound) else { return nil }
414 return EditResult(baseRevision: context.revision, edits: [TextEdit(range: range, replacement: "")], selection: [range.lowerBound..<range.lowerBound])
415 case "RET":
416 guard selection.isEmpty, ElectricPair.opensLine(text: context.text, caret: selection.lowerBound) else { return nil }
417 let at = selection.lowerBound
418 return EditResult(baseRevision: context.revision, edits: [TextEdit(range: at..<at, replacement: "\n\n")], selection: [at + 1..<at + 1])
419 default:
420 guard Self.characters.contains(key), let unit = key.utf16.first else { return nil }
421 let typed = ElectricPair.type(unit, text: context.text, selection: selection)
422 let plain = ns.replacingCharacters(in: NSRange(location: selection.lowerBound, length: selection.count), with: key)
423 if typed.text == plain, typed.point == selection.lowerBound + 1 { return nil }
424 // One replacement: what lies between the common start and end.
425 let new = typed.text as NSString
426 var start = 0
427 while start < ns.length, start < new.length, ns.character(at: start) == new.character(at: start) { start += 1 }
428 var end = 0
429 while end < ns.length - start, end < new.length - start,
430 ns.character(at: ns.length - 1 - end) == new.character(at: new.length - 1 - end) { end += 1 }
431 let replacement = new.substring(with: NSRange(location: start, length: new.length - start - end))
432 return EditResult(baseRevision: context.revision, edits: [TextEdit(range: start..<ns.length - end, replacement: replacement)],
433 selection: [typed.point..<typed.point])
434 }
435 }
436}