krz/orgstar

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

Commit 15dd64c4f2

15dd64c4f2752dbdb7d31156ef6fb4d5dc1a60b8

parent: f727fd9a17

Verified · cmc

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

TYP_TODO keywords go to the done keyword on C-c C-t

As org-todo: a type keyword goes to its sequence's first done keyword,
and only a repeated C-c C-t moves to the next type.

Layout: unified · split

Sources/OrgCore/Commands/HeadingCommands.swift +7 −4
@@ -186,8 +186,10 @@ public struct TodoCycle: OrgCommand {
186 entryHeading(at: context.caret, in: context.tree) != nil 186 entryHeading(at: context.caret, in: context.tree) != nil
187 } 187 }
188 188
189 /// No keyword, then each keyword of its sequence in order, then no keyword again. 189 /// No keyword, then each keyword of its sequence in order, then no keyword again. A
190 static func next(after current: String?, in settings: OrgSettings) -> String? { 190 /// `#+TYP_TODO` keyword goes to the sequence's first done keyword unless `repeated` (the
191 /// previous command was also this one, `this-command` eq `last-command` in `org-todo`).
192 static func next(after current: String?, in settings: OrgSettings, repeated: Bool) -> String? {
191 guard let current else { 193 guard let current else {
192 guard let first = settings.todoSequences.first else { return nil } 194 guard let first = settings.todoSequences.first else { return nil }
193 return (first.active + first.done).first?.name 195 return (first.active + first.done).first?.name
@@ -195,7 +197,8 @@ public struct TodoCycle: OrgCommand {
195 for sequence in settings.todoSequences { 197 for sequence in settings.todoSequences {
196 let names = (sequence.active + sequence.done).map(\.name) 198 let names = (sequence.active + sequence.done).map(\.name)
197 if let index = names.firstIndex(of: current) { 199 if let index = names.firstIndex(of: current) {
198 return index + 1 < names.count ? names[index + 1] : nil 200 guard index + 1 < names.count else { return nil }
201 return sequence.kind == .type && !repeated ? sequence.done.first?.name : names[index + 1]
199 } 202 }
200 } 203 }
201 return nil 204 return nil
@@ -229,7 +232,7 @@ public struct TodoCycle: OrgCommand {
229 return .failed("Before first headline") 232 return .failed("Before first headline")
230 } 233 }
231 let line = HeadingLine(heading) 234 let line = HeadingLine(heading)
232 let next = Self.next(after: line.todo?.text, in: context.tree.settings) 235 let next = Self.next(after: line.todo?.text, in: context.tree.settings, repeated: context.lastCommand == id)
233 return line.commit(context) { buffer in 236 return line.commit(context) { buffer in
234 let text = buffer.string as NSString 237 let text = buffer.string as NSString
235 let regionStart = line.stars.upperBound - line.start 238 let regionStart = line.stars.upperBound - line.start
Tests/OrgCoreTests/HeadingCommandTests.swift +29
@@ -13,6 +13,35 @@ struct HeadingCommandTests {
13 #expect(runCommand(cycle, "text\n", caret: 1).failure != nil) 13 #expect(runCommand(cycle, "text\n", caret: 1).failure != nil)
14 } 14 }
15 15
16 /// `#+TYP_TODO`: a first C-c C-t goes to the done keyword, a repeated one to the next type.
17 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
18 func typeKeywordsMatchEmacs() throws {
19 let texts = [
20 "#+TYP_TODO: Fred Sara Lucy | DONE\n* Fred a\n", "#+TYP_TODO: Fred Sara Lucy | DONE\n* Sara a\n",
21 "#+TYP_TODO: Fred Sara Lucy | DONE\n* Lucy a\n", "#+TYP_TODO: Fred Sara Lucy | DONE\n* DONE a\n",
22 "#+TYP_TODO: Fred Sara | DONE GONE\n* Sara a\n", "#+TYP_TODO: Fred Sara | DONE GONE\n* DONE a\n",
23 "#+TYP_TODO: Fred Sara | DONE\n* a\n", "#+TODO: TODO | DONE\n#+TYP_TODO: Fred Sara | OK\n* Fred a\n",
24 ]
25 var cases: [EmacsOracle.Case] = []
26 var ours: [String] = []
27 for text in texts {
28 let caret = (text as NSString).range(of: "\n*").location + 2
29 for repeated in [false, true] {
30 let last = repeated ? "'org-todo" : "'other"
31 cases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(caret, in: text), form: "(let ((this-command 'org-todo) (last-command \(last))) (org-todo))"))
32 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [caret..<caret], lastCommand: repeated ? TodoCycle().id : "org.other")
33 guard case .commit(let result) = TodoCycle().run(in: context) else { ours.append("failed"); continue }
34 ours.append(result.edits.sorted { $0.range.lowerBound > $1.range.lowerBound }.reduce(text) { $1.apply(to: $0) })
35 }
36 }
37 let emacs = try EmacsOracle.run(cases)
38 #expect(emacs.count == ours.count)
39 for (mine, emacs) in zip(ours, emacs) {
40 #expect(mine == emacs.text)
41 }
42 #expect(ours[0].hasSuffix("* DONE a\n") && ours[1].hasSuffix("* Sara a\n"))
43 }
44
16 @Test func caretMovesToTheTitleWhenBeforeIt() { 45 @Test func caretMovesToTheTitleWhenBeforeIt() {
17 #expect(runCommand(TodoCycle(), "* a\n", caret: 2).caret == 7) 46 #expect(runCommand(TodoCycle(), "* a\n", caret: 2).caret == 7)
18 #expect(runCommand(TodoCycle(), "* TODO abc\nbody\n", caret: 13).caret == 13) 47 #expect(runCommand(TodoCycle(), "* TODO abc\nbody\n", caret: 13).caret == 13)