| @@ -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 | } |