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 { |
| 93 | 93 | match[2] != nil, let first = match[1] else { return } |
| 94 | 94 | saveExcursion { |
| 95 | 95 | point = first.lowerBound |
| 96 | | try? changeTimestamp(0, what: .day) |
| 96 | try? changeTimestamp(0, what: .day, updateClock: false) |
| 97 | 97 | } |
| 98 | 98 | guard let again = lookingAt("[ \\t]*CLOCK: *[\\[<]([^\\]>]+)[\\]>](-+[\\[<]([^\\]>]+)[\\]>]([ \\t]*=>.*)?)?"), let second = again[3] else { return } |
| 99 | 99 | saveExcursion { |
| 100 | 100 | point = second.lowerBound |
| 101 | | try? changeTimestamp(0, what: .day) |
| 101 | try? changeTimestamp(0, what: .day, updateClock: false) |
| 102 | 102 | } |
| 103 | 103 | guard let final = lookingAt("[ \\t]*CLOCK: *[\\[<]([^\\]>]+)[\\]>](-+[\\[<]([^\\]>]+)[\\]>]([ \\t]*=>.*)?)?"), |
| 104 | 104 | let start = final[1], let end = final[3] else { return } |
Sources/OrgCore/Commands/Timestamps.swift
+8 −2
| @@ -149,8 +149,9 @@ extension EmacsBuffer { |
| 149 | 149 | |
| 150 | 150 | enum TimestampUnit { case minute, hour, day, month, year } |
| 151 | 151 | |
| 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 { |
| 154 | 155 | let origin = point |
| 155 | 156 | guard let (category, match) = atTimestamp() else { throw UserError(message: "Not at a timestamp") } |
| 156 | 157 | if what == nil, category == .bracket { |
| @@ -215,6 +216,11 @@ extension EmacsBuffer { |
| 215 | 216 | case .after: point = after[0]!.upperBound |
| 216 | 217 | default: point = origin |
| 217 | 218 | } |
| 219 | if updateClock { |
| 220 | let before = point |
| 221 | clockUpdateTime() |
| 222 | if point >= before { point = before } |
| 223 | } |
| 218 | 224 | } |
| 219 | 225 | |
| 220 | 226 | /// `org-toggle-timestamp-type`. |
Tests/OrgCoreTests/TimestampCommandTests.swift
+31
| @@ -100,6 +100,37 @@ struct TimestampCommandTests { |
| 100 | 100 | #expect(mismatches == 0, "\(mismatches) of \(ours.count) differ") |
| 101 | 101 | } |
| 102 | 102 | |
| 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 | |
| 103 | 134 | static let repeating = [ |
| 104 | 135 | "* TODO a\nSCHEDULED: <2026-09-01 Tue +1w>\n", |
| 105 | 136 | "* TODO a\nDEADLINE: <2026-09-01 Tue .+1d>\n", |