Commit d92c7f39e4
Verified · cmc
Layout: unified · split
Sources/OrgCore/Commands/CtrlCCtrlC.swift +2 −2
| @@ -93,12 +93,12 @@ extension EmacsBuffer { | |||
| 93 | match[2] != nil, let first = match[1] else { return } | 93 | match[2] != nil, let first = match[1] else { return } |
| 94 | saveExcursion { | 94 | saveExcursion { |
| 95 | point = first.lowerBound | 95 | point = first.lowerBound |
| 96 | try? changeTimestamp(0, what: .day) | 96 | try? changeTimestamp(0, what: .day, updateClock: false) |
| 97 | } | 97 | } |
| 98 | guard let again = lookingAt("[ \\t]*CLOCK: *[\\[<]([^\\]>]+)[\\]>](-+[\\[<]([^\\]>]+)[\\]>]([ \\t]*=>.*)?)?"), let second = again[3] else { return } | 98 | guard let again = lookingAt("[ \\t]*CLOCK: *[\\[<]([^\\]>]+)[\\]>](-+[\\[<]([^\\]>]+)[\\]>]([ \\t]*=>.*)?)?"), let second = again[3] else { return } |
| 99 | saveExcursion { | 99 | saveExcursion { |
| 100 | point = second.lowerBound | 100 | point = second.lowerBound |
| 101 | try? changeTimestamp(0, what: .day) | 101 | try? changeTimestamp(0, what: .day, updateClock: false) |
| 102 | } | 102 | } |
| 103 | guard let final = lookingAt("[ \\t]*CLOCK: *[\\[<]([^\\]>]+)[\\]>](-+[\\[<]([^\\]>]+)[\\]>]([ \\t]*=>.*)?)?"), | 103 | guard let final = lookingAt("[ \\t]*CLOCK: *[\\[<]([^\\]>]+)[\\]>](-+[\\[<]([^\\]>]+)[\\]>]([ \\t]*=>.*)?)?"), |
| 104 | let start = final[1], let end = final[3] else { return } | 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 | enum TimestampUnit { case minute, hour, day, month, year } | 150 | enum TimestampUnit { case minute, hour, day, month, year } |
| 151 | 151 | ||
| 152 | /// `org-timestamp-change`. `what` nil changes the part at point. | 152 | /// `org-timestamp-change`. `what` nil changes the part at point. On a CLOCK line the |
| 153 | func changeTimestamp(_ n: Int, what: TimestampUnit? = nil, updown: Bool = false, suppressDelay: Bool = false) throws { | 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 | let origin = point | 155 | let origin = point |
| 155 | guard let (category, match) = atTimestamp() else { throw UserError(message: "Not at a timestamp") } | 156 | guard let (category, match) = atTimestamp() else { throw UserError(message: "Not at a timestamp") } |
| 156 | if what == nil, category == .bracket { | 157 | if what == nil, category == .bracket { |
| @@ -215,6 +216,11 @@ extension EmacsBuffer { | |||
| 215 | case .after: point = after[0]!.upperBound | 216 | case .after: point = after[0]!.upperBound |
| 216 | default: point = origin | 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 | /// `org-toggle-timestamp-type`. | 226 | /// `org-toggle-timestamp-type`. |
Tests/OrgCoreTests/TimestampCommandTests.swift +31
| @@ -100,6 +100,37 @@ struct TimestampCommandTests { | |||
| 100 | #expect(mismatches == 0, "\(mismatches) of \(ours.count) differ") | 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 | static let repeating = [ | 134 | static let repeating = [ |
| 104 | "* TODO a\nSCHEDULED: <2026-09-01 Tue +1w>\n", | 135 | "* TODO a\nSCHEDULED: <2026-09-01 Tue +1w>\n", |
| 105 | "* TODO a\nDEADLINE: <2026-09-01 Tue .+1d>\n", | 136 | "* TODO a\nDEADLINE: <2026-09-01 Tue .+1d>\n", |