| @@ -0,0 +1,520 @@ |
| |
1 | import Foundation |
| |
2 | |
| |
3 | // Footnotes (`org-footnote.el`, Org 9.8.7) with the defaults: `org-footnote-section` |
| |
4 | // "Footnotes", `org-footnote-auto-label` t, `org-footnote-define-inline` and |
| |
5 | // `org-footnote-auto-adjust` nil. |
| |
6 | |
| |
7 | extension EmacsBuffer { |
| |
8 | struct FootnoteReference: Equatable { |
| |
9 | /// Nil for anonymous references. |
| |
10 | var label: String? |
| |
11 | var begin: Int |
| |
12 | /// After the closing bracket. |
| |
13 | var end: Int |
| |
14 | /// An inline definition's text. |
| |
15 | var contents: Range<Int>? |
| |
16 | /// The label of the definition the reference sits in. |
| |
17 | var definition: String? |
| |
18 | } |
| |
19 | |
| |
20 | struct FootnoteDefinition { |
| |
21 | var label: String |
| |
22 | var begin: Int |
| |
23 | var contentsBegin: Int? |
| |
24 | /// The line after its last non-blank line. |
| |
25 | var end: Int |
| |
26 | } |
| |
27 | |
| |
28 | static let footnoteSection = "Footnotes" |
| |
29 | |
| |
30 | private func footnoteTree() -> OrgTree { OrgParser.parse(string) } |
| |
31 | |
| |
32 | /// Definitions in buffer order, ending as `org-element--footnote-separator` ends them. |
| |
33 | func footnoteDefinitions(_ tree: OrgTree? = nil) -> [FootnoteDefinition] { |
| |
34 | let tree = tree ?? footnoteTree() |
| |
35 | return tree.root.descendants().filter { $0.kind == .footnoteDefinition }.compactMap { node in |
| |
36 | saveExcursion { |
| |
37 | point = node.range.lowerBound |
| |
38 | guard let match = lookingAt("\\[fn:([-_\\w]+)\\]") else { return nil } |
| |
39 | let label = substring(match[1]!.lowerBound, match[1]!.upperBound) |
| |
40 | point = lineEnd(point) |
| |
41 | let separator = searchForward("^(?:\\*+ |\\[fn:[-_\\w]+\\]|([ \\t]*\\n){2,})", caseInsensitive: false) |
| |
42 | var raw = length |
| |
43 | if let separator { |
| |
44 | raw = separator[0]!.lowerBound |
| |
45 | if separator[1] != nil { |
| |
46 | point = raw |
| |
47 | skipForward([32, 13, 9, 10]) |
| |
48 | raw = eobp ? length : lineStart(point) |
| |
49 | } |
| |
50 | } |
| |
51 | point = match[0]!.upperBound |
| |
52 | skipForward([32, 13, 9, 10]) |
| |
53 | let contentsBegin: Int? = point >= raw ? nil : lineStart(point) == node.range.lowerBound ? point : lineStart(point) |
| |
54 | point = raw |
| |
55 | skipBackward([32, 13, 9, 10]) |
| |
56 | forwardLine(1) |
| |
57 | return FootnoteDefinition(label: label, begin: node.range.lowerBound, contentsBegin: contentsBegin, end: point) |
| |
58 | } |
| |
59 | } |
| |
60 | } |
| |
61 | |
| |
62 | /// References in buffer order. |
| |
63 | func footnoteReferences(_ tree: OrgTree? = nil) -> [FootnoteReference] { |
| |
64 | let tree = tree ?? footnoteTree() |
| |
65 | let definitions = footnoteDefinitions(tree) |
| |
66 | return tree.root.descendants().filter { $0.kind == .footnoteReference }.compactMap { node in |
| |
67 | saveExcursion { |
| |
68 | point = node.range.lowerBound |
| |
69 | guard let match = lookingAt("\\[fn:([-_\\w]*)(:)?") else { return nil } |
| |
70 | let label = match[1]!.isEmpty ? nil : substring(match[1]!.lowerBound, match[1]!.upperBound) |
| |
71 | let contents = match[2] != nil ? match[0]!.upperBound..<(node.range.upperBound - 1) : nil |
| |
72 | let holder = definitions.last { $0.begin <= node.range.lowerBound && node.range.lowerBound < $0.end } |
| |
73 | return FootnoteReference(label: label, begin: node.range.lowerBound, end: node.range.upperBound, contents: contents, definition: holder?.label) |
| |
74 | } |
| |
75 | } |
| |
76 | } |
| |
77 | |
| |
78 | /// `org-footnote-at-reference-p`. |
| |
79 | func footnoteReferenceAtPoint() -> FootnoteReference? { |
| |
80 | footnoteReferences().first { $0.begin <= point && point < $0.end } |
| |
81 | } |
| |
82 | |
| |
83 | /// `org-footnote-at-definition-p`: anywhere in a definition. |
| |
84 | func footnoteDefinitionAtPoint() -> FootnoteDefinition? { |
| |
85 | footnoteDefinitions().first { $0.begin <= point && point < max($0.end, $0.begin + 1) } |
| |
86 | } |
| |
87 | |
| |
88 | /// `org-footnote-all-labels`. |
| |
89 | func footnoteLabels() -> [String] { |
| |
90 | let tree = footnoteTree() |
| |
91 | var all: [String] = [] |
| |
92 | for label in footnoteReferences(tree).compactMap(\.label) + footnoteDefinitions(tree).map(\.label) where !all.contains(label) { |
| |
93 | all.append(label) |
| |
94 | } |
| |
95 | return all |
| |
96 | } |
| |
97 | |
| |
98 | /// `org-footnote-unique-label`. |
| |
99 | func footnoteUniqueLabel(_ current: [String]) -> String { |
| |
100 | var count = 1 |
| |
101 | while current.contains(String(count)) { count += 1 } |
| |
102 | return String(count) |
| |
103 | } |
| |
104 | |
| |
105 | /// Where `org-footnote-get-definition` finds LABEL: its definition, or an inline reference. |
| |
106 | func footnoteDefinitionStart(_ label: String) -> Int? { |
| |
107 | let tree = footnoteTree() |
| |
108 | let definition = footnoteDefinitions(tree).first { $0.label == label }?.begin |
| |
109 | let inline = footnoteReferences(tree).first { $0.label == label && $0.contents != nil && $0.begin > 0 }?.begin |
| |
110 | return [definition, inline].compactMap { $0 }.min() |
| |
111 | } |
| |
112 | |
| |
113 | /// `org-footnote--allow-reference-p` for a labeled reference. |
| |
114 | func footnoteReferenceAllowed() -> Bool { |
| |
115 | if bolp { return false } |
| |
116 | let tree = footnoteTree() |
| |
117 | var chain: [SyntaxNode] = [] |
| |
118 | var node = tree.root |
| |
119 | while let child = node.child(containing: point) { |
| |
120 | chain.append(child) |
| |
121 | node = child |
| |
122 | } |
| |
123 | if chain.contains(where: { $0.kind == .heading }), let heading = chain.first(where: { $0.kind == .heading }), |
| |
124 | lineStart(point) == heading.range.lowerBound { |
| |
125 | guard let title = heading.firstChild(.title) else { return false } |
| |
126 | let tags = heading.tokens.first { $0.kind == .tags } |
| |
127 | return point >= title.range.lowerBound && tags.map { point < $0.range.lowerBound } ?? true |
| |
128 | } |
| |
129 | let objects: Set<SyntaxKind> = [.bold, .italic, .underline, .strikeThrough, .verbatim, .code, .link, .linkDescription, .timestamp, |
| |
130 | .footnoteReference, .statisticsCookie, .target, .macro, .inlineSourceBlock, .latexFragment, .lineBreak, .superscript] |
| |
131 | let containers: Set<SyntaxKind> = [.document, .zerothSection, .section, .plainList, .item, .drawer] |
| |
132 | let element = chain.last { !objects.contains($0.kind) && !containers.contains($0.kind) } |
| |
133 | switch element?.kind { |
| |
134 | case nil, .paragraph, .tableRow, .tableCell: |
| |
135 | break |
| |
136 | case .footnoteDefinition: |
| |
137 | let definition = footnoteDefinitions(tree).first { $0.begin == element!.range.lowerBound } |
| |
138 | if let begin = definition?.contentsBegin, point >= begin { break } |
| |
139 | return character(at: point).map { [32, 9, 10, 13].contains($0) } ?? false |
| |
140 | case .block: |
| |
141 | let block = element! |
| |
142 | let opening = saveExcursion { () -> Bool in |
| |
143 | point = block.range.lowerBound |
| |
144 | return lookingAt("[ \\t]*#\\+begin_verse") != nil |
| |
145 | } |
| |
146 | guard opening, let wrapper = Wrappers.starting(at: block.range.lowerBound, in: tree) else { return false } |
| |
147 | let contentsBegin = lineEnd(block.range.lowerBound) + 1 |
| |
148 | return point >= contentsBegin && point < wrapper.closeLine |
| |
149 | default: |
| |
150 | // Blank lines after an element. |
| |
151 | let after = saveExcursion { () -> Int in |
| |
152 | point = element!.range.upperBound |
| |
153 | skipBackward([32, 13, 9, 10]) |
| |
154 | forwardLine(1) |
| |
155 | return point |
| |
156 | } |
| |
157 | return point >= after |
| |
158 | } |
| |
159 | guard let object = chain.last(where: { objects.contains($0.kind) }) else { return true } |
| |
160 | if point == object.range.lowerBound { return true } |
| |
161 | if chain.contains(where: { $0.kind == .link }) { return false } |
| |
162 | switch object.kind { |
| |
163 | case .bold, .italic, .underline, .strikeThrough, .superscript: |
| |
164 | return point > object.range.lowerBound && point <= object.range.upperBound - 1 |
| |
165 | default: |
| |
166 | return false |
| |
167 | } |
| |
168 | } |
| |
169 | |
| |
170 | /// `org-back-over-empty-lines`, returning the number of empty lines passed. |
| |
171 | @discardableResult |
| |
172 | func backOverEmptyLinesCount() -> Int { |
| |
173 | let position = point |
| |
174 | backOverEmptyLines() |
| |
175 | return countLines(point, position) |
| |
176 | } |
| |
177 | |
| |
178 | /// `org-end-of-meta-data` with FULL t. |
| |
179 | func endOfMetaDataFull() { |
| |
180 | endOfMetaData() |
| |
181 | guard !atHeading else { return } |
| |
182 | let end = saveExcursion { () -> Int in outlineNextHeading(); return point } |
| |
183 | while !eobp { |
| |
184 | if lookingAt("[ \\t]*$|^[ \\t]*CLOCK:") != nil { |
| |
185 | forwardLine(1) |
| |
186 | } else if lookingAt("[ \\t]*:[-_\\w]+:[ \\t]*$") != nil { |
| |
187 | guard searchForward("^[ \\t]*:END:[ \\t]*$", bound: end, caseInsensitive: true) != nil else { return } |
| |
188 | forwardLine(1) |
| |
189 | } else { |
| |
190 | return |
| |
191 | } |
| |
192 | } |
| |
193 | } |
| |
194 | |
| |
195 | /// `org-footnote--clear-footnote-section`. |
| |
196 | func clearFootnoteSection() { |
| |
197 | point = 0 |
| |
198 | let pattern = "^\\*+ +" + NSRegularExpression.escapedPattern(for: Self.footnoteSection) + "[ \\t]*$" |
| |
199 | while let match = searchForward(pattern, caseInsensitive: false) { |
| |
200 | point = match[0]!.lowerBound |
| |
201 | let end = endOfSubtree(toHeading: true) |
| |
202 | deleteRegion(match[0]!.lowerBound, end) |
| |
203 | point = match[0]!.lowerBound |
| |
204 | } |
| |
205 | point = length |
| |
206 | skipBackward([32, 13, 9, 10]) |
| |
207 | if !bobp { |
| |
208 | forwardLine(1) |
| |
209 | if eolp { insert("\n") } |
| |
210 | } |
| |
211 | deleteRegion(point, length) |
| |
212 | if saveExcursion({ backOverEmptyLinesCount() }) == 0 { insert("\n") } |
| |
213 | insert("* " + Self.footnoteSection + "\n") |
| |
214 | } |
| |
215 | |
| |
216 | /// `org-footnote-create-definition`: the definition's start. |
| |
217 | func createFootnoteDefinition(_ label: String) -> Int { |
| |
218 | saveExcursion { |
| |
219 | let pattern = "^\\*+[ \\t]+" + NSRegularExpression.escapedPattern(for: Self.footnoteSection) + "[ \\t]*$" |
| |
220 | point = 0 |
| |
221 | if let match = searchForward(pattern, caseInsensitive: false) { |
| |
222 | point = match[0]!.upperBound |
| |
223 | endOfMetaDataFull() |
| |
224 | if !bolp { insert("\n") } |
| |
225 | } else { |
| |
226 | clearFootnoteSection() |
| |
227 | } |
| |
228 | if backOverEmptyLinesCount() == 0 { insert("\n") } |
| |
229 | insert("[fn:\(label)] \n") |
| |
230 | forwardLine(-1) |
| |
231 | return point |
| |
232 | } |
| |
233 | } |
| |
234 | |
| |
235 | /// `org-footnote-goto-definition`. |
| |
236 | func gotoFootnoteDefinition(_ label: String, at start: Int) { |
| |
237 | point = start |
| |
238 | if let match = lookingAt("\\[fn:" + NSRegularExpression.escapedPattern(for: label) + "[]:]") { point = match[0]!.upperBound } |
| |
239 | } |
| |
240 | |
| |
241 | /// `org-footnote-new`. |
| |
242 | func newFootnote() throws { |
| |
243 | let all = footnoteLabels() |
| |
244 | let label = footnoteUniqueLabel(all) |
| |
245 | guard footnoteReferenceAllowed() else { throw UserError(message: "Cannot insert a footnote here") } |
| |
246 | insert("[fn:\(label)]") |
| |
247 | gotoFootnoteDefinition(label, at: createFootnoteDefinition(label)) |
| |
248 | } |
| |
249 | |
| |
250 | /// `org-footnote-goto-previous-reference`. |
| |
251 | func gotoPreviousFootnoteReference(_ label: String) throws { |
| |
252 | let references = footnoteReferences().filter { $0.label == label } |
| |
253 | let opening = "[fn:\(label)".utf16.count + 1 |
| |
254 | guard let reference = references.last(where: { $0.begin + opening <= point }) ?? references.first(where: { $0.begin >= point }) else { |
| |
255 | throw UserError(message: "Cannot find reference of footnote \"\(label)\"") |
| |
256 | } |
| |
257 | point = reference.begin |
| |
258 | } |
| |
259 | |
| |
260 | /// `org-footnote--set-label`, with point at the reference or definition. |
| |
261 | func setFootnoteLabel(_ label: String) { |
| |
262 | point += 4 |
| |
263 | if character(at: point) == 58 { |
| |
264 | insert(label) |
| |
265 | } else if let match = lookingAt("([-_\\w]+)") { |
| |
266 | replace(match[1]!, with: label) |
| |
267 | point = match[1]!.lowerBound + label.utf16.count |
| |
268 | } |
| |
269 | } |
| |
270 | |
| |
271 | /// `org-footnote--collect-references`, in org's reading order: nested references follow |
| |
272 | /// the definition they sit in. |
| |
273 | func collectFootnoteReferences(anonymous: Bool = false) -> [FootnoteReference] { |
| |
274 | let references = footnoteReferences().filter { anonymous || $0.label != nil } |
| |
275 | var nested: [String: [String?]] = [:] |
| |
276 | for reference in references { if let holder = reference.definition { nested[holder, default: []].append(reference.label) } } |
| |
277 | var ordered: [FootnoteReference] = [] |
| |
278 | func add(_ reference: FootnoteReference, allowNested: Bool) { |
| |
279 | guard allowNested || reference.definition == nil else { return } |
| |
280 | ordered.append(reference) |
| |
281 | for label in nested[reference.label ?? ""] ?? [] where reference.label != nil { |
| |
282 | guard let r = references.last(where: { $0.label == label }), !ordered.contains(r) else { continue } |
| |
283 | add(r, allowNested: true) |
| |
284 | } |
| |
285 | } |
| |
286 | for reference in references { add(reference, allowNested: false) } |
| |
287 | return ordered |
| |
288 | } |
| |
289 | |
| |
290 | /// `org-footnote--collect-definitions`: label and text, last first, as org's alist. |
| |
291 | func collectFootnoteDefinitions(delete: Bool) -> [(label: String, text: String)] { |
| |
292 | var seen: Set<String> = [] |
| |
293 | var found: [(label: String, text: String, range: Range<Int>)] = [] |
| |
294 | for definition in footnoteDefinitions() where !seen.contains(definition.label) { |
| |
295 | seen.insert(definition.label) |
| |
296 | let begin = saveExcursion { () -> Int in |
| |
297 | point = definition.begin |
| |
298 | skipBackward([32, 13, 9, 10]) |
| |
299 | if bobp { return point } |
| |
300 | forwardLine(1) |
| |
301 | return point |
| |
302 | } |
| |
303 | let text = substring(begin, definition.end).trimmingCharacters(in: CharacterSet(charactersIn: " \t\n\r")) |
| |
304 | found.append((definition.label, text, begin..<definition.end)) |
| |
305 | } |
| |
306 | if delete { for f in found.reversed() { deleteRegion(f.range.lowerBound, f.range.upperBound) } } |
| |
307 | return found.reversed().map { ($0.label, $0.text) } |
| |
308 | } |
| |
309 | |
| |
310 | /// `org-footnote-renumber-fn:N`. |
| |
311 | func renumberFootnotes() { |
| |
312 | var c = 0 |
| |
313 | let references = collectFootnoteReferences().filter { $0.label!.range(of: "^[0-9]+$", options: .regularExpression) != nil } |
| |
314 | var alist: [String: String] = [:] |
| |
315 | for label in references.map({ $0.label! }) where alist[label] == nil { |
| |
316 | c += 1 |
| |
317 | alist[label] = String(c) |
| |
318 | } |
| |
319 | let markers = references.map { marker($0.begin) } |
| |
320 | for (reference, m) in zip(references, markers) { |
| |
321 | point = m.position |
| |
322 | setFootnoteLabel(alist[reference.label!]!) |
| |
323 | } |
| |
324 | markers.forEach(release) |
| |
325 | point = 0 |
| |
326 | while let match = searchForward("^\\[fn:([0-9]+)\\]", caseInsensitive: false) { |
| |
327 | let old = substring(match[1]!.lowerBound, match[1]!.upperBound) |
| |
328 | let new = alist[old] ?? { c += 1; return String(c) }() |
| |
329 | replace(match[1]!, with: new) |
| |
330 | point = match[1]!.lowerBound + new.utf16.count + 1 |
| |
331 | } |
| |
332 | } |
| |
333 | |
| |
334 | /// `org-footnote-sort`. |
| |
335 | func sortFootnotes() { |
| |
336 | let references = collectFootnoteReferences() |
| |
337 | let definitions = collectFootnoteDefinitions(delete: true) |
| |
338 | clearFootnoteSection() |
| |
339 | var inserted: [String] = [] |
| |
340 | for reference in references where !inserted.contains(reference.label!) && reference.contents == nil { |
| |
341 | inserted.append(reference.label!) |
| |
342 | insert("\n" + (definitions.first { $0.label == reference.label }?.text ?? "[fn:\(reference.label!)] DEFINITION NOT FOUND.") + "\n") |
| |
343 | } |
| |
344 | for definition in definitions where !inserted.contains(definition.label) { |
| |
345 | insert("\n" + definition.text + "\n") |
| |
346 | } |
| |
347 | } |
| |
348 | |
| |
349 | /// `org-footnote-normalize`. |
| |
350 | func normalizeFootnotes() { |
| |
351 | enum Key: Equatable { |
| |
352 | case label(String) |
| |
353 | case anonymous(Int) |
| |
354 | } |
| |
355 | var n = 0 |
| |
356 | var translations: [String: String] = [:] |
| |
357 | var definitions: [(key: Key, text: String)] = [] |
| |
358 | let collected = collectFootnoteReferences(anonymous: true) |
| |
359 | let markers = collected.map { marker($0.begin) } |
| |
360 | var keys: [Key] = [] |
| |
361 | for (reference, m) in zip(collected, markers) { |
| |
362 | let new: String |
| |
363 | let key: Key |
| |
364 | if let label = reference.label { |
| |
365 | if let known = translations[label] { |
| |
366 | new = known |
| |
367 | } else { |
| |
368 | n += 1 |
| |
369 | new = String(n) |
| |
370 | translations[label] = new |
| |
371 | } |
| |
372 | key = .label(label) |
| |
373 | } else { |
| |
374 | n += 1 |
| |
375 | new = String(n) |
| |
376 | key = .anonymous(n) |
| |
377 | } |
| |
378 | keys.append(key) |
| |
379 | point = m.position |
| |
380 | setFootnoteLabel(new) |
| |
381 | if let contents = reference.contents { |
| |
382 | let size = contents.count |
| |
383 | let removed = substring(point, point + size + 1) |
| |
384 | deleteRegion(point, point + size + 1) |
| |
385 | let text = "[fn:\(new)] " + String(removed.dropFirst()).trimmingCharacters(in: CharacterSet(charactersIn: " \t\n\r")) |
| |
386 | definitions.insert((key, text), at: 0) |
| |
387 | } |
| |
388 | } |
| |
389 | markers.forEach(release) |
| |
390 | definitions += collectFootnoteDefinitions(delete: true).map { (Key.label($0.label), $0.text) } |
| |
391 | clearFootnoteSection() |
| |
392 | var inserted: [Key] = [] |
| |
393 | for key in keys where !inserted.contains(key) { |
| |
394 | inserted.append(key) |
| |
395 | let stored = definitions.first { $0.key == key }?.text |
| |
396 | let text: String |
| |
397 | switch key { |
| |
398 | case .anonymous(let number): |
| |
399 | text = stored ?? "[fn:\(number)] DEFINITION NOT FOUND." |
| |
400 | case .label(let label): |
| |
401 | let new = translations[label]! |
| |
402 | if let stored, let match = stored.firstMatch(of: /^\[fn:(.*?)\]/) { |
| |
403 | text = stored.replacingCharacters(in: match.1.startIndex..<match.1.endIndex, with: new) |
| |
404 | } else { |
| |
405 | text = stored ?? "[fn:\(new)] DEFINITION NOT FOUND." |
| |
406 | } |
| |
407 | } |
| |
408 | insert("\n" + text + "\n") |
| |
409 | } |
| |
410 | for definition in definitions where !inserted.contains(definition.key) { |
| |
411 | n += 1 |
| |
412 | let number = n |
| |
413 | let text = definition.text.replacingOccurrences(of: "(?m)^\\[fn:[-_\\w]+\\]", with: "[fn:\(number)]", options: .regularExpression) |
| |
414 | insert("\n" + text + "\n") |
| |
415 | } |
| |
416 | } |
| |
417 | |
| |
418 | /// `org-footnote-delete`: the message to show. |
| |
419 | func deleteFootnote() throws -> String { |
| |
420 | let label: String |
| |
421 | if let reference = footnoteReferenceAtPoint() { |
| |
422 | guard let l = reference.label else { |
| |
423 | deleteRegion(reference.begin, reference.end) |
| |
424 | return "Anonymous footnote removed" |
| |
425 | } |
| |
426 | label = l |
| |
427 | } else if let definition = footnoteDefinitionAtPoint() { |
| |
428 | label = definition.label |
| |
429 | } else { |
| |
430 | throw UserError(message: "Don't know which footnote to remove") |
| |
431 | } |
| |
432 | var references = 0 |
| |
433 | while let reference = footnoteReferences().first(where: { $0.label == label }) { |
| |
434 | deleteRegion(reference.begin, reference.end) |
| |
435 | references += 1 |
| |
436 | } |
| |
437 | var definitions = 0 |
| |
438 | while let definition = footnoteDefinitions().first(where: { $0.label == label }) { |
| |
439 | let begin = saveExcursion { () -> Int in |
| |
440 | point = definition.begin |
| |
441 | skipBackward([32, 13, 9, 10]) |
| |
442 | if bobp { return point } |
| |
443 | forwardLine(1) |
| |
444 | return point |
| |
445 | } |
| |
446 | deleteRegion(begin, definition.end) |
| |
447 | definitions += 1 |
| |
448 | } |
| |
449 | return "\(definitions) definition(s) of and \(references) reference(s) of footnote \(label) removed" |
| |
450 | } |
| |
451 | } |
| |
452 | |
| |
453 | /// C-c C-x f: `org-footnote-action`. On a reference, goes to its definition; on a definition's |
| |
454 | /// label, back to a reference; elsewhere inserts a new footnote, or where one can't go, offers |
| |
455 | /// the footnote menu. |
| |
456 | public struct FootnoteAction: OrgCommand { |
| |
457 | let special: Bool |
| |
458 | public init(special: Bool = false) { self.special = special } |
| |
459 | public var id: String { special ? "org.footnote.menu" : "org.footnote.action" } |
| |
460 | public var title: String { special ? "Footnote Menu" : "Footnote Action" } |
| |
461 | public func applies(in context: EditContext) -> Bool { true } |
| |
462 | |
| |
463 | static let menu = FastSelection(items: [ |
| |
464 | .option(key: "s", name: "sort"), .option(key: "r", name: "renumber fn:N"), .option(key: "S", name: "renumber and sort"), |
| |
465 | .option(key: "n", name: "normalize"), .option(key: "d", name: "delete"), |
| |
466 | ], multiple: false) |
| |
467 | |
| |
468 | public func run(in context: EditContext) -> CommandStep { |
| |
469 | let buffer = EmacsBuffer(context.text, point: context.caret, settings: context.tree.settings, options: context.options) |
| |
470 | if !special { |
| |
471 | if let reference = buffer.footnoteReferenceAtPoint() { |
| |
472 | guard let label = reference.label else { return move(context, to: reference.contents!.lowerBound) } |
| |
473 | if let start = buffer.footnoteDefinitionStart(label) { |
| |
474 | buffer.gotoFootnoteDefinition(label, at: start) |
| |
475 | return move(context, to: buffer.point, message: Self.definitionMessage) |
| |
476 | } |
| |
477 | guard let answer = context.answers["create"] else { |
| |
478 | return .prompt(Prompt(key: "create", message: "No definition for \(label). Create one?", choices: ["yes", "no"])) |
| |
479 | } |
| |
480 | guard answer == "yes" else { return .failed("Quit") } |
| |
481 | return commitBuffer(context) { buffer in |
| |
482 | buffer.gotoFootnoteDefinition(label, at: buffer.createFootnoteDefinition(label)) |
| |
483 | } |
| |
484 | } |
| |
485 | if let definition = buffer.footnoteDefinitions().first(where: { $0.begin <= context.caret && context.caret < ($0.contentsBegin ?? $0.end) }) { |
| |
486 | return commitBuffer(context) { try $0.gotoPreviousFootnoteReference(definition.label) } |
| |
487 | } |
| |
488 | if buffer.footnoteReferenceAllowed() { return commitBuffer(context) { try $0.newFootnote() } } |
| |
489 | } |
| |
490 | guard let key = context.answers["footnote"] else { |
| |
491 | return .prompt(Prompt(key: "footnote", message: "Footnotes: [s]ort | [r]enumber fn:N | [S]=r+s | [n]ormalize | [d]elete", fast: Self.menu)) |
| |
492 | } |
| |
493 | var message: String? |
| |
494 | let step = commitBuffer(context) { buffer in |
| |
495 | let caret = buffer.marker(buffer.point) |
| |
496 | defer { |
| |
497 | buffer.point = caret.position |
| |
498 | buffer.release(caret) |
| |
499 | } |
| |
500 | switch key { |
| |
501 | case "s": buffer.sortFootnotes() |
| |
502 | case "r": buffer.renumberFootnotes() |
| |
503 | case "S": |
| |
504 | buffer.renumberFootnotes() |
| |
505 | buffer.sortFootnotes() |
| |
506 | case "n": buffer.normalizeFootnotes() |
| |
507 | case "d": message = try buffer.deleteFootnote() |
| |
508 | default: throw EmacsBuffer.UserError(message: "No such footnote command \(key)") |
| |
509 | } |
| |
510 | } |
| |
511 | guard case .commit(let result) = step, let message else { return step } |
| |
512 | return .commit(EditResult(baseRevision: result.baseRevision, edits: result.edits, selection: result.selection, effects: result.effects + [.message(message)])) |
| |
513 | } |
| |
514 | |
| |
515 | static let definitionMessage = "Edit definition and go back with `C-c &' or, if unique, with `C-c C-c'." |
| |
516 | |
| |
517 | private func move(_ context: EditContext, to offset: Int, message: String? = nil) -> CommandStep { |
| |
518 | .commit(EditResult(baseRevision: context.revision, edits: [], selection: [offset..<offset], effects: message.map { [.message($0)] } ?? [])) |
| |
519 | } |
| |
520 | } |