| @@ -0,0 +1,104 @@ |
| |
1 | import 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 without a checkbox have their list repaired, setup keywords refresh, and |
| |
6 | // elsewhere there's nothing to do. |
| |
7 | |
| |
8 | /// `org-ctrl-c-ctrl-c`. |
| |
9 | public struct CtrlCCtrlC: OrgCommand { |
| |
10 | public init() {} |
| |
11 | public var id: String { "org.ctrl-c-ctrl-c" } |
| |
12 | public var title: String { "Act at Point (C-c C-c)" } |
| |
13 | public func applies(in context: EditContext) -> Bool { true } |
| |
14 | |
| |
15 | static let nothing = CommandStep.failed("`C-c C-c' can do nothing useful here") |
| |
16 | |
| |
17 | public func run(in context: EditContext) -> CommandStep { |
| |
18 | let buffer = EmacsBuffer(context.text, point: context.caret, settings: context.tree.settings, options: context.options) |
| |
19 | if KeyContext.holds("src", in: context) { return ExecuteSrcBlock().run(in: context) } |
| |
20 | if KeyContext.holds("table", in: context) || TableRecalculate.tblfmLine(in: context) != nil { |
| |
21 | // At the table's very start the context is the table itself, which org recalculates. |
| |
22 | let atStart = buffer.saveExcursion { buffer.tableBegin() } == context.caret |
| |
23 | return TableRecalculate.tblfmLine(in: context) != nil || atStart ? TableRecalculate(all: true).run(in: context) : TableCommand(.align).run(in: context) |
| |
24 | } |
| |
25 | if buffer.lookingAtLine("^[ \\t]*$") { return Self.nothing } |
| |
26 | // On a clock line, changing a timestamp updates the clock too. |
| |
27 | if buffer.lookingAtLine("^[ \\t]*CLOCK:") { return commitBuffer(context) { $0.clockUpdateTime() } } |
| |
28 | if buffer.lookingAtLine(EmacsBuffer.dblockStart) { return UpdateDynamicBlock().run(in: context) } |
| |
29 | if Self.cookieAtCaret(buffer) { |
| |
30 | return commitBuffer(context) { buffer in |
| |
31 | buffer.saveExcursion { |
| |
32 | buffer.point = buffer.lineStart(buffer.point) |
| |
33 | buffer.updateStatisticsCookies() |
| |
34 | } |
| |
35 | } |
| |
36 | } |
| |
37 | if buffer.atTimestamp() != nil { return commitBuffer(context) { try $0.changeTimestamp(0, what: .day) } } |
| |
38 | if buffer.atHeading { return SetTags().run(in: context) } |
| |
39 | if buffer.atItem { |
| |
40 | // The item's checkbox toggles, and the whole list is repaired, its cookie updated. |
| |
41 | return commitBuffer(context) { buffer in |
| |
42 | buffer.point = buffer.lineStart(buffer.point) |
| |
43 | let item = buffer.point |
| |
44 | var structure = buffer.listStruct() |
| |
45 | let old = structure |
| |
46 | let box = structure.checkbox(item) |
| |
47 | if let box { structure.set(item) { $0.checkbox = box == "[X]" ? "[ ]" : "[X]" } } |
| |
48 | var repaired = structure |
| |
49 | buffer.writeStruct(&repaired, repaired.parents, old: old) |
| |
50 | if box != nil, repaired == old { |
| |
51 | throw EmacsBuffer.UserError(message: "Cannot toggle this checkbox: \(box == "[X]" ? "all subitems checked" : "unchecked subitems")") |
| |
52 | } |
| |
53 | buffer.point = item |
| |
54 | buffer.updateCheckboxCount() |
| |
55 | } |
| |
56 | } |
| |
57 | if buffer.lookingAtLine("^[ \\t]*#\\+[A-Za-z_]+:") { |
| |
58 | return .commit(EditResult(baseRevision: context.revision, edits: [], effects: [.message("Local setup has been refreshed")])) |
| |
59 | } |
| |
60 | if buffer.lookingAtLine("^[ \\t]*:[^\\s:]+:") { return .failed("Property actions aren't supported yet") } |
| |
61 | return Self.nothing |
| |
62 | } |
| |
63 | |
| |
64 | /// A `[n/m]` or `[n%]` cookie holds the caret. |
| |
65 | static func cookieAtCaret(_ buffer: EmacsBuffer) -> Bool { |
| |
66 | let line = buffer.lineStart(buffer.point)..<buffer.lineEnd(buffer.point) |
| |
67 | let text = buffer.substring(line.lowerBound, line.upperBound) as NSString |
| |
68 | let regex = try! NSRegularExpression(pattern: "\\[[0-9]*(?:%|/[0-9]*)\\]") |
| |
69 | return regex.matches(in: text as String, range: NSRange(location: 0, length: text.length)).contains { |
| |
70 | let start = line.lowerBound + $0.range.location |
| |
71 | return start <= buffer.point && buffer.point < start + $0.range.length |
| |
72 | } |
| |
73 | } |
| |
74 | } |
| |
75 | |
| |
76 | extension EmacsBuffer { |
| |
77 | /// `org-clock-update-time-maybe` on a finished clock line: both dates' weekdays fixed and |
| |
78 | /// the duration written again. |
| |
79 | func clockUpdateTime() { |
| |
80 | saveExcursion { |
| |
81 | point = lineStart(point) |
| |
82 | skipForward([32, 9]) |
| |
83 | guard let match = lookingAt("[ \\t]*CLOCK: *[\\[<]([^\\]>]+)[\\]>](-+[\\[<]([^\\]>]+)[\\]>]([ \\t]*=>.*)?)?"), |
| |
84 | match[2] != nil, let first = match[1] else { return } |
| |
85 | saveExcursion { |
| |
86 | point = first.lowerBound |
| |
87 | try? changeTimestamp(0, what: .day) |
| |
88 | } |
| |
89 | guard let again = lookingAt("[ \\t]*CLOCK: *[\\[<]([^\\]>]+)[\\]>](-+[\\[<]([^\\]>]+)[\\]>]([ \\t]*=>.*)?)?"), let second = again[3] else { return } |
| |
90 | saveExcursion { |
| |
91 | point = second.lowerBound |
| |
92 | try? changeTimestamp(0, what: .day) |
| |
93 | } |
| |
94 | guard let final = lookingAt("[ \\t]*CLOCK: *[\\[<]([^\\]>]+)[\\]>](-+[\\[<]([^\\]>]+)[\\]>]([ \\t]*=>.*)?)?"), |
| |
95 | let start = final[1], let end = final[3] else { return } |
| |
96 | if let arrow = final[4] { deleteRegion(arrow.lowerBound, arrow.upperBound) } |
| |
97 | point = lineEnd(point) |
| |
98 | let s = TagsMatcher.seconds(substring(end.lowerBound, end.upperBound), now: now, calendar: calendar) |
| |
99 | - TagsMatcher.seconds(substring(start.lowerBound, start.upperBound), now: now, calendar: calendar) |
| |
100 | let total = abs(Int(s)) |
| |
101 | insert(" => " + String(format: s < 0 ? "-%d:%02d" : "%2d:%02d", total / 3600, total % 3600 / 60)) |
| |
102 | } |
| |
103 | } |
| |
104 | } |