krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Commit d92c7f39e4

d92c7f39e46e785d7ec0f4297d1f0747c076f58e

parent: 15dd64c4f2

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-08 03:43 UTC

Update the CLOCK duration after shifting its timestamps

org-timestamp-change ends with org-clock-update-time-maybe, so
S-up/S-down and the day shifts on a CLOCK line rewrite => H:MM.

Layout: unified · split

Sources/OrgCore/Commands/CtrlCCtrlC.swift +2 −2
@@ -93,12 +93,12 @@ extension EmacsBuffer {
9393 match[2] != nil, let first = match[1] else { return }
9494 saveExcursion {
9595 point = first.lowerBound
96 try? changeTimestamp(0, what: .day)
96 try? changeTimestamp(0, what: .day, updateClock: false)
9797 }
9898 guard let again = lookingAt("[ \\t]*CLOCK: *[\\[<]([^\\]>]+)[\\]>](-+[\\[<]([^\\]>]+)[\\]>]([ \\t]*=>.*)?)?"), let second = again[3] else { return }
9999 saveExcursion {
100100 point = second.lowerBound
101 try? changeTimestamp(0, what: .day)
101 try? changeTimestamp(0, what: .day, updateClock: false)
102102 }
103103 guard let final = lookingAt("[ \\t]*CLOCK: *[\\[<]([^\\]>]+)[\\]>](-+[\\[<]([^\\]>]+)[\\]>]([ \\t]*=>.*)?)?"),
104104 let start = final[1], let end = final[3] else { return }
Sources/OrgCore/Commands/Timestamps.swift +8 −2
@@ -149,8 +149,9 @@ extension EmacsBuffer {
149149
150150 enum TimestampUnit { case minute, hour, day, month, year }
151151
152 /// `org-timestamp-change`. `what` nil changes the part at point.
153 func changeTimestamp(_ n: Int, what: TimestampUnit? = nil, updown: Bool = false, suppressDelay: Bool = false) throws {
152 /// `org-timestamp-change`. `what` nil changes the part at point. On a CLOCK line the
153 /// duration is then updated, unless `updateClock` is false.
154 func changeTimestamp(_ n: Int, what: TimestampUnit? = nil, updown: Bool = false, suppressDelay: Bool = false, updateClock: Bool = true) throws {
154155 let origin = point
155156 guard let (category, match) = atTimestamp() else { throw UserError(message: "Not at a timestamp") }
156157 if what == nil, category == .bracket {
@@ -215,6 +216,11 @@ extension EmacsBuffer {
215216 case .after: point = after[0]!.upperBound
216217 default: point = origin
217218 }
219 if updateClock {
220 let before = point
221 clockUpdateTime()
222 if point >= before { point = before }
223 }
218224 }
219225
220226 /// `org-toggle-timestamp-type`.
Tests/OrgCoreTests/TimestampCommandTests.swift +31
@@ -100,6 +100,37 @@ struct TimestampCommandTests {
100100 #expect(mismatches == 0, "\(mismatches) of \(ours.count) differ")
101101 }
102102
103 /// Shifting a timestamp on a CLOCK line rewrites the duration (`org-clock-update-time-maybe`).
104 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
105 func clockShiftsMatchEmacs() throws {
106 let text = "* a\n:LOGBOOK:\nCLOCK: [2026-09-01 Tue 10:00]--[2026-09-01 Tue 11:30] => 1:30\n CLOCK: [2026-09-02 Wed 23:58]--[2026-09-03 Thu 00:02]\nCLOCK: [2026-09-04 Fri 09:00]\n:END:\n"
107 let commands: [(any OrgCommand, String)] = [
108 (ShiftTimestamp(up: true), "(org-timestamp-up)"), (ShiftTimestamp(up: false), "(org-timestamp-down)"),
109 (ShiftTimestampDay(later: true), "(org-timestamp-up-day)"), (ShiftTimestampDay(later: false), "(org-timestamp-down-day)"),
110 ]
111 var cases: [EmacsOracle.Case] = []
112 var ours: [(label: String, text: String, caret: Int, failed: Bool)] = []
113 for caret in EmacsOracle.positions(text) where EmacsBuffer(text, point: caret).atTimestamp() != nil {
114 for (command, form) in commands {
115 cases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(caret, in: text), form: frozenClock(1_789_000_000, form)))
116 let result = run(command, text, caret: caret)
117 ours.append(("\(command.id) at \(caret)", result.text, result.caret, result.failure != nil))
118 }
119 }
120 var mismatches = 0
121 for (mine, emacs) in zip(ours, try EmacsOracle.run(cases)) {
122 let failed = !emacs.error.isEmpty
123 let caret = EmacsOracle.offset(emacs.point, in: emacs.text)
124 guard mine.failed != failed || (!failed && (mine.text != emacs.text || mine.caret != caret)) else { continue }
125 mismatches += 1
126 if mismatches <= 12 {
127 Issue.record("\(mine.label)\n ours: \(mine.failed ? "failed" : "\(mine.text.debugDescription) @\(mine.caret)")\n emacs: \(failed ? emacs.error : "\(emacs.text.debugDescription) @\(caret)")")
128 }
129 }
130 #expect(mismatches == 0, "\(mismatches) of \(ours.count) differ")
131 #expect(run(ShiftTimestamp(up: true), text, caret: (text as NSString).range(of: "11:30").location + 4).text.contains("11:35] => 1:35\n"))
132 }
133
103134 static let repeating = [
104135 "* TODO a\nSCHEDULED: <2026-09-01 Tue +1w>\n",
105136 "* TODO a\nDEADLINE: <2026-09-01 Tue .+1d>\n",