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