Sources/OrgCore/Commands/CtrlCCtrlC.swift
123 lines · 7075 bytes
5 symbols in this file
1import Foundation
2
3// C-c C-c (`org-ctrl-c-ctrl-c`, Org 9.8.7) for the contexts no more specific binding takes:
4// headings set tags, clock lines get their duration again, timestamps their weekday, cookies
5// update, items toggle their checkbox, or without one have their list repaired, setup keywords
6// refresh, tables evaluate a typed formula, then align or recalculate a row marked `#`, and
7// elsewhere there's nothing to do.
8
9/// `org-ctrl-c-ctrl-c`.
10public struct CtrlCCtrlC: OrgCommand {
11 public init() {}
12 public var id: String { "org.ctrl-c-ctrl-c" }
13 public var title: String { "Act at Point (C-c C-c)" }
14 public func applies(in context: EditContext) -> Bool { true }
15
16 static let nothing = CommandStep.failed("`C-c C-c' can do nothing useful here")
17
18 public func run(in context: EditContext) -> CommandStep {
19 let buffer = EmacsBuffer(context.text, point: context.caret, settings: context.tree.settings, options: context.options)
20 if KeyContext.holds("src", in: context) || ExecuteSrcBlock.atCallOrInline(context) { return ExecuteSrcBlock().run(in: context) }
21 if KeyContext.holds("table", in: context) || TableRecalculate.tblfmLine(in: context) != nil {
22 // At the table's very start the context is the table itself, which org recalculates.
23 let atStart = buffer.saveExcursion { buffer.tableBegin() } == context.caret
24 if TableRecalculate.tblfmLine(in: context) != nil || atStart { return TableRecalculate(all: true).run(in: context) }
25 // `org-table-maybe-eval-formula`, then `org-table-maybe-recalculate-line` on a row
26 // marked `#`, which aligns, or else `org-table-align`.
27 let step = commitBuffer(context) { buffer in
28 try buffer.tableMaybeEvalFormula()
29 guard buffer.lookingAtLine("^[ \\t]*\\| *# *(\\||$)") else { return buffer.tableAlign() }
30 do {
31 try buffer.tableRecalculate(all: false)
32 } catch let error as Calc.Unsupported {
33 throw EmacsBuffer.UserError(message: TableRecalculate.emacsMessage + error.reason)
34 }
35 }
36 return TableRecalculate.inEmacs(step, form: "(org-ctrl-c-ctrl-c)", in: context)
37 }
38 if buffer.footnoteReferenceAtPoint() != nil
39 || buffer.footnoteDefinitions().contains(where: { $0.begin <= context.caret && context.caret < ($0.contentsBegin ?? $0.end) }) {
40 return FootnoteAction().run(in: context)
41 }
42 if buffer.lookingAtLine("^[ \\t]*$") { return Self.nothing }
43 // On a clock line, changing a timestamp updates the clock too.
44 if buffer.lookingAtLine("^[ \\t]*CLOCK:") { return commitBuffer(context) { $0.clockUpdateTime() } }
45 if buffer.lookingAtLine(EmacsBuffer.dblockStart) { return UpdateDynamicBlock().run(in: context) }
46 if Self.cookieAtCaret(buffer) {
47 return commitBuffer(context) { buffer in
48 buffer.saveExcursion {
49 buffer.point = buffer.lineStart(buffer.point)
50 buffer.updateStatisticsCookies()
51 }
52 }
53 }
54 if buffer.atTimestamp() != nil { return commitBuffer(context) { try $0.changeTimestamp(0, what: .day) } }
55 if buffer.atHeading { return SetTags().run(in: context) }
56 if buffer.atItem {
57 // The item's checkbox toggles, and the whole list is repaired, its cookie updated.
58 return commitBuffer(context) { buffer in
59 buffer.point = buffer.lineStart(buffer.point)
60 let item = buffer.point
61 var structure = buffer.listStruct()
62 let old = structure
63 let box = structure.checkbox(item)
64 if let box { structure.set(item) { $0.checkbox = box == "[X]" ? "[ ]" : "[X]" } }
65 var repaired = structure
66 buffer.writeStruct(&repaired, repaired.parents, old: old)
67 if box != nil, repaired == old {
68 throw EmacsBuffer.UserError(message: "Cannot toggle this checkbox: \(box == "[X]" ? "all subitems checked" : "unchecked subitems")")
69 }
70 buffer.point = item
71 buffer.updateCheckboxCount()
72 }
73 }
74 if buffer.lookingAtLine("^[ \\t]*#\\+[A-Za-z_]+:") {
75 return .commit(EditResult(baseRevision: context.revision, edits: [], effects: [.refreshSetup, .message("Local setup has been refreshed")]))
76 }
77 if buffer.propertyAtPoint() != nil || buffer.lookingAtLine("^[ \\t]*:(?:PROPERTIES|END):[ \\t]*$") && buffer.existingPropertyBlock() != nil {
78 return PropertyAction().run(in: context)
79 }
80 return Self.nothing
81 }
82
83 /// A `[n/m]` or `[n%]` cookie holds the caret.
84 static func cookieAtCaret(_ buffer: EmacsBuffer) -> Bool {
85 let line = buffer.lineStart(buffer.point)..<buffer.lineEnd(buffer.point)
86 let text = buffer.substring(line.lowerBound, line.upperBound) as NSString
87 let regex = try! NSRegularExpression(pattern: "\\[[0-9]*(?:%|/[0-9]*)\\]")
88 return regex.matches(in: text as String, range: NSRange(location: 0, length: text.length)).contains {
89 let start = line.lowerBound + $0.range.location
90 return start <= buffer.point && buffer.point < start + $0.range.length
91 }
92 }
93}
94
95extension EmacsBuffer {
96 /// `org-clock-update-time-maybe` on a finished clock line: both dates' weekdays fixed and
97 /// the duration written again.
98 func clockUpdateTime() {
99 saveExcursion {
100 point = lineStart(point)
101 skipForward([32, 9])
102 guard let match = lookingAt("[ \\t]*CLOCK: *[\\[<]([^\\]>]+)[\\]>](-+[\\[<]([^\\]>]+)[\\]>]([ \\t]*=>.*)?)?"),
103 match[2] != nil, let first = match[1] else { return }
104 saveExcursion {
105 point = first.lowerBound
106 try? changeTimestamp(0, what: .day, updateClock: false)
107 }
108 guard let again = lookingAt("[ \\t]*CLOCK: *[\\[<]([^\\]>]+)[\\]>](-+[\\[<]([^\\]>]+)[\\]>]([ \\t]*=>.*)?)?"), let second = again[3] else { return }
109 saveExcursion {
110 point = second.lowerBound
111 try? changeTimestamp(0, what: .day, updateClock: false)
112 }
113 guard let final = lookingAt("[ \\t]*CLOCK: *[\\[<]([^\\]>]+)[\\]>](-+[\\[<]([^\\]>]+)[\\]>]([ \\t]*=>.*)?)?"),
114 let start = final[1], let end = final[3] else { return }
115 if let arrow = final[4] { deleteRegion(arrow.lowerBound, arrow.upperBound) }
116 point = lineEnd(point)
117 let s = TagsMatcher.seconds(substring(end.lowerBound, end.upperBound), now: now, calendar: calendar)
118 - TagsMatcher.seconds(substring(start.lowerBound, start.upperBound), now: now, calendar: calendar)
119 let total = abs(Int(s))
120 insert(" => " + String(format: s < 0 ? "-%d:%02d" : "%2d:%02d", total / 3600, total % 3600 / 60))
121 }
122 }
123}