Agenda, date and TODO fixes from the manual review !159
24 files changed, +163 −64
Layout: unified · split
Sources/OrgCore/Agenda/AgendaFilter.swift +13 −4
| @@ -13,10 +13,19 @@ public struct AgendaFilter: Sendable, Equatable { | |||
| 13 | 13 | ||
| 14 | public var isEmpty: Bool { categories.isEmpty && tags.isEmpty && efforts.isEmpty && regexps.isEmpty } | 14 | public var isEmpty: Bool { categories.isEmpty && tags.isEmpty && efforts.isEmpty && regexps.isEmpty } |
| 15 | 15 | ||
| 16 | /// The filter as `/` shows it for editing. | 16 | /// The filter as `/` shows it for editing; `parse` reads it back to the same filter. |
| 17 | /// Categories holding `-` are quoted. | ||
| 17 | public var description: String { | 18 | public var description: String { |
| 18 | let regexp = regexps.first.map { "/" + $0.dropFirst().replacingOccurrences(of: "^\\+", with: "", options: .regularExpression) + "/" } ?? "" | 19 | func unsigned(_ term: String) -> (sign: String, body: String) { |
| 19 | return categories.joined() + tags.joined() + (efforts.first.map { String($0.dropFirst()) } ?? "") + regexp | 20 | (term.hasPrefix("-") ? "-" : "", String(term.dropFirst())) |
| 21 | } | ||
| 22 | let cats = categories.map { term -> String in | ||
| 23 | let (sign, body) = unsigned(term) | ||
| 24 | return (sign.isEmpty ? "+" : sign) + (body.contains("-") ? "\"\(body)\"" : body) | ||
| 25 | } | ||
| 26 | let efforts = efforts.map { unsigned($0).sign + unsigned($0).body } | ||
| 27 | let regexps = regexps.map { unsigned($0).sign + "/" + unsigned($0).body + "/" } | ||
| 28 | return (cats + tags + efforts + regexps).joined() | ||
| 20 | } | 29 | } |
| 21 | 30 | ||
| 22 | /// Reads `+cat-tag<0:10-/regexp/`: a word is a tag if a shown line has it, else a category | 31 | /// Reads `+cat-tag<0:10-/regexp/`: a word is a tag if a shown line has it, else a category |
| @@ -43,7 +52,7 @@ public struct AgendaFilter: Sendable, Equatable { | |||
| 43 | let word = ns.substring(with: m.range(at: 3)).replacingOccurrences(of: "~~~", with: "-") | 52 | let word = ns.substring(with: m.range(at: 3)).replacingOccurrences(of: "~~~", with: "-") |
| 44 | if shownTags.contains(word) { | 53 | if shownTags.contains(word) { |
| 45 | append(sign + word, to: &result.tags) | 54 | append(sign + word, to: &result.tags) |
| 46 | } else if shownCategories.contains(word) { | 55 | } else if shownCategories.contains(word.replacingOccurrences(of: "^\"(.*)\"$", with: "$1", options: .regularExpression)) { |
| 47 | append(sign + word.replacingOccurrences(of: "^\"(.*)\"$", with: "$1", options: .regularExpression), to: &result.categories) | 56 | append(sign + word.replacingOccurrences(of: "^\"(.*)\"$", with: "$1", options: .regularExpression), to: &result.categories) |
| 48 | } else { | 57 | } else { |
| 49 | ignored.append(sign + word) | 58 | ignored.append(sign + word) |
Sources/OrgCore/Agenda/AgendaSearch.swift +3 −1
| @@ -231,7 +231,9 @@ public struct TagsMatcher: Sendable { | |||
| 231 | let a = Self.seconds(actual, now: now, calendar: calendar) | 231 | let a = Self.seconds(actual, now: now, calendar: calendar) |
| 232 | let b = Self.matcherTime(time, now: now, calendar: calendar) | 232 | let b = Self.matcherTime(time, now: now, calendar: calendar) |
| 233 | guard a > 0, b > 0 else { return false } | 233 | guard a > 0, b > 0 else { return false } |
| 234 | // `org-time<>` compares with `=`. | 234 | // `org-time<>` in Org 9.8.7 (org-macs.el) is `(\= a b)`, which reads as `=`: |
| 235 | // `DEADLINE<>"<2026-10-07>"` keeps the entries due that day. | ||
| 236 | // AgendaTests.listsMatchEmacs checks this against Emacs. | ||
| 235 | return op == "<>" || op == "!=" || op == "/=" ? a == b : Self.compare(op, a, b) | 237 | return op == "<>" || op == "!=" || op == "/=" ? a == b : Self.compare(op, a, b) |
| 236 | } | 238 | } |
| 237 | } | 239 | } |
Sources/OrgCore/Agenda/PrefixFormat.swift +4 −3
| @@ -1,8 +1,9 @@ | |||
| 1 | import Foundation | 1 | import Foundation |
| 2 | 2 | ||
| 3 | /// `org-agenda-prefix-format` for one view, compiled as `org-compile-prefix-format` does: | 3 | /// `org-agenda-prefix-format` for one view, compiled as `org-compile-prefix-format` does: |
| 4 | /// `%c` category, `%t` time, `%l` level, `%s` extra, `%e` effort, `%b` breadcrumbs, `%i` icon | 4 | /// `%c` category, `%t` time, `%l` level, `%s` extra, `%e` effort, `%b` breadcrumbs, `%T` last |
| 5 | /// (none in Orgstar), each with an optional `?` (empty when the value is), a `%-12`-style | 5 | /// tag (Org's regexp lists only lower-case keys but matches `T` under `case-fold-search`), |
| 6 | /// `%i` icon (none in Orgstar), each with an optional `?` (empty when the value is), a `%-12`-style | ||
| 6 | /// width with an optional `.N` limit on categories, and a punctuation character added after a | 7 | /// width with an optional `.N` limit on categories, and a punctuation character added after a |
| 7 | /// non-empty value. `%(…)` forms run Lisp; they are empty here. | 8 | /// non-empty value. `%(…)` forms run Lisp; they are empty here. |
| 8 | public struct PrefixFormat: Sendable, Equatable { | 9 | public struct PrefixFormat: Sendable, Equatable { |
| @@ -18,7 +19,7 @@ public struct PrefixFormat: Sendable, Equatable { | |||
| 18 | let categoryMaxLength: Int? | 19 | let categoryMaxLength: Int? |
| 19 | 20 | ||
| 20 | public init(_ format: String) { | 21 | public init(_ format: String) { |
| 21 | let regex = try! NSRegularExpression(pattern: "%(\\?)?([-+]?[0-9.]*)([ .;,:!?=|/<>]?)([cltseib]|\\(.+?\\))") | 22 | let regex = try! NSRegularExpression(pattern: "%(\\?)?([-+]?[0-9.]*)([ .;,:!?=|/<>]?)([cltseibT]|\\(.+?\\))") |
| 22 | let ns = format as NSString | 23 | let ns = format as NSString |
| 23 | var pieces: [Piece] = [] | 24 | var pieces: [Piece] = [] |
| 24 | var hasTime = false | 25 | var hasTime = false |
Sources/OrgCore/Commands/ClockReport.swift +3 −3
| @@ -70,9 +70,9 @@ public enum ClockReport { | |||
| 70 | return String(format: "%04d-W%02d", Days.date(thursday).year, Days.isoWeek(day)) | 70 | return String(format: "%04d-W%02d", Days.date(thursday).year, Days.isoWeek(day)) |
| 71 | } | 71 | } |
| 72 | 72 | ||
| 73 | /// The time report your Doom configuration writes (`tr/generate-report`): minutes per | 73 | /// An Org table of clocked time per date and heading between `start` and `end` |
| 74 | /// date and heading between `start` and `end` (inclusive, `YYYY-MM-DD`), with a total | 74 | /// (inclusive, `YYYY-MM-DD`), with a total after each ISO week. The heading column is |
| 75 | /// after each ISO week. | 75 | /// titled Code, for headings named by charge code. |
| 76 | public static func timeReport(_ entries: [Entry], start: String? = nil, end: String? = nil) -> String { | 76 | public static func timeReport(_ entries: [Entry], start: String? = nil, end: String? = nil) -> String { |
| 77 | var totals: [String: [String: Int]] = [:] | 77 | var totals: [String: [String: Int]] = [:] |
| 78 | for entry in entries { | 78 | for entry in entries { |
Sources/OrgCore/Commands/CtrlCCtrlC.swift +4 −4
| @@ -2,8 +2,8 @@ import Foundation | |||
| 2 | 2 | ||
| 3 | // C-c C-c (`org-ctrl-c-ctrl-c`, Org 9.8.7) for the contexts no more specific binding takes: | 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 | 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 | 5 | // update, items toggle their checkbox, or without one have their list repaired, setup keywords |
| 6 | // elsewhere there's nothing to do. | 6 | // refresh, and elsewhere there's nothing to do. |
| 7 | 7 | ||
| 8 | /// `org-ctrl-c-ctrl-c`. | 8 | /// `org-ctrl-c-ctrl-c`. |
| 9 | public struct CtrlCCtrlC: OrgCommand { | 9 | public struct CtrlCCtrlC: OrgCommand { |
| @@ -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/Footnotes.swift +1 −1
| @@ -513,7 +513,7 @@ public struct FootnoteAction: OrgCommand { | |||
| 513 | return .commit(EditResult(baseRevision: result.baseRevision, edits: result.edits, selection: result.selection, effects: result.effects + [.message(message)])) | 513 | return .commit(EditResult(baseRevision: result.baseRevision, edits: result.edits, selection: result.selection, effects: result.effects + [.message(message)])) |
| 514 | } | 514 | } |
| 515 | 515 | ||
| 516 | static let definitionMessage = "Edit definition and go back with `C-c &' or, if unique, with `C-c C-c'." | 516 | static let definitionMessage = "Edit definition and go back with `C-c C-c' or `C-c C-x f' on its label." |
| 517 | 517 | ||
| 518 | private func move(_ context: EditContext, to offset: Int, message: String? = nil) -> CommandStep { | 518 | private func move(_ context: EditContext, to offset: Int, message: String? = nil) -> CommandStep { |
| 519 | .commit(EditResult(baseRevision: context.revision, edits: [], selection: [offset..<offset], effects: message.map { [.message($0)] } ?? [])) | 519 | .commit(EditResult(baseRevision: context.revision, edits: [], selection: [offset..<offset], effects: message.map { [.message($0)] } ?? [])) |
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 |
Sources/OrgCore/Commands/InlineImages.swift +3 −5
| @@ -34,11 +34,9 @@ public enum InlineImages { | |||
| 34 | /// `#+STARTUP: inlineimages` or `noinlineimages`, if the file says. | 34 | /// `#+STARTUP: inlineimages` or `noinlineimages`, if the file says. |
| 35 | public static func startup(_ tree: OrgTree) -> Bool? { | 35 | public static func startup(_ tree: OrgTree) -> Bool? { |
| 36 | var shown: Bool? | 36 | var shown: Bool? |
| 37 | for m in (tree.settings.setupKeywords + tree.root.text).matches(of: #/(?mi)^[ \t]*#\+STARTUP:(.*)$/#) { | 37 | for word in tree.settings.startup { |
| 38 | for word in m.1.split(whereSeparator: \.isWhitespace) { | 38 | if word.lowercased() == "inlineimages" { shown = true } |
| 39 | if word.lowercased() == "inlineimages" { shown = true } | 39 | if word.lowercased() == "noinlineimages" { shown = false } |
| 40 | if word.lowercased() == "noinlineimages" { shown = false } | ||
| 41 | } | ||
| 42 | } | 40 | } |
| 43 | return shown | 41 | return shown |
| 44 | } | 42 | } |
Sources/OrgCore/Commands/Repeat.swift +3 −1
| @@ -81,7 +81,9 @@ extension EmacsBuffer { | |||
| 81 | if let kind = parts.1 { | 81 | if let kind = parts.1 { |
| 82 | let time = Self.parseTimeString(stamp).map(date)! | 82 | let time = Self.parseTimeString(stamp).map(date)! |
| 83 | if kind == "." { | 83 | if kind == "." { |
| 84 | if what != "h" { | 84 | if what == "h" { |
| 85 | try changeTimestamp(Int((now.timeIntervalSince(time) / 60).rounded(.down)), what: .minute) | ||
| 86 | } else { | ||
| 85 | let days = calendar.dateComponents([.day], from: calendar.startOfDay(for: time), to: today).day! | 87 | let days = calendar.dateComponents([.day], from: calendar.startOfDay(for: time), to: today).day! |
| 86 | try changeTimestamp(days, what: .day) | 88 | try changeTimestamp(days, what: .day) |
| 87 | } | 89 | } |
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`. |
Sources/OrgCore/Commands/TodoLogging.swift +1 −9
| @@ -82,15 +82,7 @@ extension EmacsBuffer { | |||
| 82 | if enter != nil || leave != nil, log.states[keyword.name] == nil { log.states[keyword.name] = (enter, leave) } | 82 | if enter != nil || leave != nil, log.states[keyword.name] == nil { log.states[keyword.name] = (enter, leave) } |
| 83 | } | 83 | } |
| 84 | } | 84 | } |
| 85 | for (key, value) in SetupFile.keywords(settings.setupKeywords) where key == "STARTUP" { | 85 | for word in settings.startup { log.apply(startup: word) } |
| 86 | for word in value.split(whereSeparator: \.isWhitespace) { log.apply(startup: String(word)) } | ||
| 87 | } | ||
| 88 | let startup = try! NSRegularExpression(pattern: "^[ \\t]*#\\+STARTUP:(.*)$", options: [.anchorsMatchLines, .caseInsensitive]) | ||
| 89 | for match in startup.matches(in: string, range: NSRange(location: 0, length: length)) { | ||
| 90 | for word in substring(match.range(at: 1).location, NSMaxRange(match.range(at: 1))).split(whereSeparator: \.isWhitespace) { | ||
| 91 | log.apply(startup: String(word)) | ||
| 92 | } | ||
| 93 | } | ||
| 94 | if let logging = inheritedProperty("LOGGING")?.value { | 86 | if let logging = inheritedProperty("LOGGING")?.value { |
| 95 | // `org-local-logging`. | 87 | // `org-local-logging`. |
| 96 | log.done = nil | 88 | log.done = nil |
Sources/OrgCore/Commands/ToggleCommands.swift +1 −1
| @@ -326,7 +326,7 @@ extension EmacsBuffer { | |||
| 326 | } | 326 | } |
| 327 | } | 327 | } |
| 328 | 328 | ||
| 329 | /// C-c @ and M-h on a heading: `org-mark-subtree`, the subtree selected from its heading. | 329 | /// C-c @ (and the speed key @) on a heading: `org-mark-subtree`, the subtree selected from its heading. |
| 330 | public struct MarkSubtree: OrgCommand { | 330 | public struct MarkSubtree: OrgCommand { |
| 331 | public init() {} | 331 | public init() {} |
| 332 | public var id: String { "org.subtree.mark" } | 332 | public var id: String { "org.subtree.mark" } |
Sources/OrgCore/Parser/Incremental.swift +1 −1
| @@ -58,7 +58,7 @@ private struct ReparseContext { | |||
| 58 | /// Change in UTF-16 length. | 58 | /// Change in UTF-16 length. |
| 59 | var delta: Int { edit.replacement.utf16.count - edit.range.count } | 59 | var delta: Int { edit.replacement.utf16.count - edit.range.count } |
| 60 | 60 | ||
| 61 | static let settingsKeys: Set<String> = ["TODO", "SEQ_TODO", "TYP_TODO", "PRIORITIES"] | 61 | static let settingsKeys: Set<String> = ["TODO", "SEQ_TODO", "TYP_TODO", "PRIORITIES", "STARTUP"] |
| 62 | 62 | ||
| 63 | /// Elements that parse the same in isolation as in place, as long as their lines keep | 63 | /// Elements that parse the same in isolation as in place, as long as their lines keep |
| 64 | /// their classes. | 64 | /// their classes. |
Sources/OrgCore/Parser/Settings.swift +7 −1
| @@ -50,6 +50,8 @@ public struct OrgSettings: Sendable, Equatable { | |||
| 50 | /// Keyword lines from the file's `#+SETUPFILE`s, as `#+KEY: value` lines: they count as | 50 | /// Keyword lines from the file's `#+SETUPFILE`s, as `#+KEY: value` lines: they count as |
| 51 | /// the file's own, before them. | 51 | /// the file's own, before them. |
| 52 | public var setupKeywords = "" | 52 | public var setupKeywords = "" |
| 53 | /// Words of the `#+STARTUP` keyword lines outside blocks, setup files' first. | ||
| 54 | public var startup: [String] = [] | ||
| 53 | 55 | ||
| 54 | public init(todoSequences: [TodoSequence], priorities: Priorities, radioTargets: [String] = []) { | 56 | public init(todoSequences: [TodoSequence], priorities: Priorities, radioTargets: [String] = []) { |
| 55 | self.todoSequences = todoSequences | 57 | self.todoSequences = todoSequences |
| @@ -82,12 +84,13 @@ public struct OrgSettings: Sendable, Equatable { | |||
| 82 | } | 84 | } |
| 83 | 85 | ||
| 84 | enum SettingsScanner { | 86 | enum SettingsScanner { |
| 85 | /// Reads `#+TODO`, `#+SEQ_TODO`, `#+TYP_TODO` and `#+PRIORITIES` outside blocks. Any TODO | 87 | /// Reads `#+TODO`, `#+SEQ_TODO`, `#+TYP_TODO`, `#+PRIORITIES` and `#+STARTUP` outside blocks. Any TODO |
| 86 | /// line replaces the default sequences; as in org, `TYP_TODO` sequences come first, then | 88 | /// line replaces the default sequences; as in org, `TYP_TODO` sequences come first, then |
| 87 | /// `TODO`, then `SEQ_TODO`. | 89 | /// `TODO`, then `SEQ_TODO`. |
| 88 | static func scan(lines: [RawLine], info: [ClassifiedLine], blockEnds: [Int: Int], defaults: OrgSettings) -> OrgSettings { | 90 | static func scan(lines: [RawLine], info: [ClassifiedLine], blockEnds: [Int: Int], defaults: OrgSettings) -> OrgSettings { |
| 89 | var types: [TodoSequence] = [], todos: [TodoSequence] = [], seqs: [TodoSequence] = [] | 91 | var types: [TodoSequence] = [], todos: [TodoSequence] = [], seqs: [TodoSequence] = [] |
| 90 | var priorities = defaults.priorities | 92 | var priorities = defaults.priorities |
| 93 | var startup: [String] = [] | ||
| 91 | func apply(_ key: String, _ value: Substring) { | 94 | func apply(_ key: String, _ value: Substring) { |
| 92 | switch key { | 95 | switch key { |
| 93 | case "TODO": | 96 | case "TODO": |
| @@ -96,6 +99,8 @@ enum SettingsScanner { | |||
| 96 | if let s = todoSequence(value, kind: .sequence) { seqs.append(s) } | 99 | if let s = todoSequence(value, kind: .sequence) { seqs.append(s) } |
| 97 | case "TYP_TODO": | 100 | case "TYP_TODO": |
| 98 | if let s = todoSequence(value, kind: .type) { types.append(s) } | 101 | if let s = todoSequence(value, kind: .type) { types.append(s) } |
| 102 | case "STARTUP": | ||
| 103 | startup += value.split(whereSeparator: \.isWhitespace).map(String.init) | ||
| 99 | case "PRIORITIES": | 104 | case "PRIORITIES": |
| 100 | let words = value.split(whereSeparator: \.isWhitespace) | 105 | let words = value.split(whereSeparator: \.isWhitespace) |
| 101 | if words.count == 3 { | 106 | if words.count == 3 { |
| @@ -129,6 +134,7 @@ enum SettingsScanner { | |||
| 129 | radioTargets: radio.sorted { $0.count > $1.count }) | 134 | radioTargets: radio.sorted { $0.count > $1.count }) |
| 130 | settings.listAllowAlphabetical = defaults.listAllowAlphabetical | 135 | settings.listAllowAlphabetical = defaults.listAllowAlphabetical |
| 131 | settings.setupKeywords = defaults.setupKeywords | 136 | settings.setupKeywords = defaults.setupKeywords |
| 137 | settings.startup = startup | ||
| 132 | return settings | 138 | return settings |
| 133 | } | 139 | } |
| 134 | 140 | ||
Sources/OrgPresentation/Folding.swift +3 −22
| @@ -217,28 +217,9 @@ extension Presentation { | |||
| 217 | return result | 217 | return result |
| 218 | } | 218 | } |
| 219 | 219 | ||
| 220 | /// Words of every `#+STARTUP:` line. Only element-level keywords count, so text inside | 220 | /// Words of every `#+STARTUP:` keyword line (`OrgSettings.startup`); lines inside blocks |
| 221 | /// blocks and paragraphs is never read as a setting. | 221 | /// are not settings. |
| 222 | public static func startupOptions(_ tree: OrgTree) -> [String] { | 222 | public static func startupOptions(_ tree: OrgTree) -> [String] { |
| 223 | var options = tree.settings.setupKeywords.split(separator: "\n").filter { $0.uppercased().hasPrefix("#+STARTUP:") } | 223 | tree.settings.startup |
| 224 | .flatMap { $0.dropFirst("#+STARTUP:".count).split(whereSeparator: \.isWhitespace).map(String.init) } | ||
| 225 | func visit(_ node: GreenNode) { | ||
| 226 | for case .node(let child) in node.children { | ||
| 227 | switch child.kind { | ||
| 228 | case .keyword: | ||
| 229 | let text = child.text | ||
| 230 | if text.uppercased().drop(while: { $0 == " " || $0 == "\t" }).hasPrefix("#+STARTUP:"), | ||
| 231 | let colon = text.firstIndex(of: ":") { | ||
| 232 | options += text[text.index(after: colon)...].split(whereSeparator: \.isWhitespace).map(String.init) | ||
| 233 | } | ||
| 234 | case .document, .zerothSection, .section, .drawer: | ||
| 235 | visit(child) | ||
| 236 | default: | ||
| 237 | break | ||
| 238 | } | ||
| 239 | } | ||
| 240 | } | ||
| 241 | visit(tree.green) | ||
| 242 | return options | ||
| 243 | } | 224 | } |
| 244 | } | 225 | } |
Sources/Orgstar/ClockViews.swift +1 −2
| @@ -62,8 +62,7 @@ final class ClockStatusItem: NSObject { | |||
| 62 | } | 62 | } |
| 63 | } | 63 | } |
| 64 | 64 | ||
| 65 | /// Time per day and heading, as your `tr/generate-report` writes it, for chosen files and | 65 | /// Clocked time per day and heading (`ClockReport.timeReport`) for chosen files and dates. |
| 66 | /// dates. | ||
| 67 | struct ClockReportView: View { | 66 | struct ClockReportView: View { |
| 68 | let workspace: WorkspaceModel | 67 | let workspace: WorkspaceModel |
| 69 | @State private var start = Calendar.current.date(byAdding: .day, value: -6, to: Date()) ?? Date() | 68 | @State private var start = Calendar.current.date(byAdding: .day, value: -6, to: Date()) ?? Date() |
Tests/OrgCoreTests/AgendaTests.swift +31
| @@ -287,6 +287,9 @@ struct AgendaTests { | |||
| 287 | "CATEGORY=\"cat\"", "DUE<\"<2026-10-15>\"", "DUE>\"<today>\"", "SCHEDULED<\"<today>\"", "TIMESTAMP=\"<2026-10-03 Sat>\"", | 287 | "CATEGORY=\"cat\"", "DUE<\"<2026-10-15>\"", "DUE>\"<today>\"", "SCHEDULED<\"<today>\"", "TIMESTAMP=\"<2026-10-03 Sat>\"", |
| 288 | "ITEM={lph}", "work/TODO", "/!", "+work/!TODO|WAIT", "EFFORT>\"1:00\"", "ALLTAGS={proj}", "TAGS=\":work:urgent:\"", | 288 | "ITEM={lph}", "work/TODO", "/!", "+work/!TODO|WAIT", "EFFORT>\"1:00\"", "ALLTAGS={proj}", "TAGS=\":work:urgent:\"", |
| 289 | "x@y", "/-TODO", "/!{^[HW]}", "work&urgent", "SIZE>=3+SIZE<=3", "LEVEL<>1", | 289 | "x@y", "/-TODO", "/!{^[HW]}", "work&urgent", "SIZE>=3+SIZE<=3", "LEVEL<>1", |
| 290 | // `org-time<>` is `=` in Org 9.8.7. | ||
| 291 | "DUE<>\"<2026-10-10>\"", "DUE<>\"<2026-10-11>\"", "SCHEDULED<>\"<2026-09-30>\"", "TIMESTAMP!=\"<2026-10-03 Sat>\"", | ||
| 292 | "DEADLINE/=\"<2026-10-05>\"", "DEADLINE<>\"<2026-10-07>\"", | ||
| 290 | ] | 293 | ] |
| 291 | 294 | ||
| 292 | @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil)) | 295 | @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil)) |
| @@ -437,6 +440,24 @@ struct AgendaPrefixFormatTests { | |||
| 437 | } | 440 | } |
| 438 | } | 441 | } |
| 439 | 442 | ||
| 443 | /// `%T` is the last tag: Org's field regexp lists only lower-case keys, but matches it | ||
| 444 | /// under `case-fold-search`. | ||
| 445 | @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil)) | ||
| 446 | func lastTagMatchesEmacs() throws { | ||
| 447 | let today = Days.absolute(year: 2026, month: 10, day: 5) | ||
| 448 | let formats = ["agenda": " %-10:c%-8T%?-12t% s", "todo": " %?-6T|", "tags": "%c %-6:T", "search": " %i %-12:c"] | ||
| 449 | try AgendaOracle.compare(files: [("p.org", Self.file)], start: today, span: 2, today: today, prefix: formats) | ||
| 450 | var options = AgendaOptions() | ||
| 451 | options.prefixFormat = formats | ||
| 452 | let lisp = AgendaOracle.prefixLisp(formats) | ||
| 453 | try AgendaOracle.compareList(files: [("p.org", Self.file)], form: "(let ((org-agenda-prefix-format \(lisp))) (org-todo-list))", today: today) { | ||
| 454 | Agenda.todoList($0, options: options) | ||
| 455 | } | ||
| 456 | try AgendaOracle.compareList(files: [("p.org", Self.file)], form: "(let ((org-agenda-prefix-format \(lisp))) (org-tags-view nil \"work|proj\"))", today: today) { | ||
| 457 | Agenda.tagsMatch($0, match: "work|proj", options: options) | ||
| 458 | } | ||
| 459 | } | ||
| 460 | |||
| 440 | @Test func compilesFields() { | 461 | @Test func compilesFields() { |
| 441 | let format = PrefixFormat(" %-8.6c|%?-4t%e") | 462 | let format = PrefixFormat(" %-8.6c|%?-4t%e") |
| 442 | #expect(format.render(["c": "category", "e": "1:00"]) == " categ |1:00") | 463 | #expect(format.render(["c": "category", "e": "1:00"]) == " categ |1:00") |
| @@ -536,4 +557,14 @@ struct AgendaFilterTests { | |||
| 536 | let kept = filter.parse("++urgent", tags: ["urgent"], categories: []).filter | 557 | let kept = filter.parse("++urgent", tags: ["urgent"], categories: []).filter |
| 537 | #expect(kept.tags == ["-phone", "+urgent"]) | 558 | #expect(kept.tags == ["-phone", "+urgent"]) |
| 538 | } | 559 | } |
| 560 | |||
| 561 | @Test func descriptionRoundTrips() { | ||
| 562 | let tags: Set<String> = ["phone", "urgent"], categories: Set<String> = ["work", "my-cat"] | ||
| 563 | for text in ["-/call/", "-work+phone-<0:10>1:00/a/-/b/", "\"my-cat\"-urgent", "-\"my-cat\""] { | ||
| 564 | let filter = AgendaFilter().parse(text, tags: tags, categories: categories).filter | ||
| 565 | #expect(AgendaFilter().parse(filter.description, tags: tags, categories: categories).filter == filter, "\(text)") | ||
| 566 | } | ||
| 567 | let negative = AgendaFilter().parse("-/call/", tags: [], categories: []).filter | ||
| 568 | #expect(negative.description == "-/call/") | ||
| 569 | } | ||
| 539 | } | 570 | } |
Tests/OrgCoreTests/ClockTableTests.swift +1
| @@ -31,6 +31,7 @@ struct ClockTableTests { | |||
| 31 | ":block today :maxlevel 3", ":block 2026-09", ":tstart \"<2026-09-08>\" :tend \"<2026-09-10>\"", | 31 | ":block today :maxlevel 3", ":block 2026-09", ":tstart \"<2026-09-08>\" :tend \"<2026-09-10>\"", |
| 32 | ":link t :maxlevel 3", ":narrow 20! :maxlevel 3", ":tags t :level t :maxlevel 3", ":properties (\"Effort\") :formula %", | 32 | ":link t :maxlevel 3", ":narrow 20! :maxlevel 3", ":tags t :level t :maxlevel 3", ":properties (\"Effort\") :formula %", |
| 33 | ":indent nil :maxlevel 3", ":block yesterday", ":block 2026-W36 :maxlevel 3", ":fileskip0 t :block 2025", | 33 | ":indent nil :maxlevel 3", ":block yesterday", ":block 2026-W36 :maxlevel 3", ":fileskip0 t :block 2025", |
| 34 | ":maxlevel nil", | ||
| 34 | ] | 35 | ] |
| 35 | 36 | ||
| 36 | static func ours(_ text: String, caret: Int, command: any OrgCommand, answers: [String: String] = [:]) -> String { | 37 | static func ours(_ text: String, caret: Int, command: any OrgCommand, answers: [String: String] = [:]) -> String { |
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) |
Tests/OrgCoreTests/InlineImagesTests.swift +1
| @@ -12,5 +12,6 @@ struct InlineImagesTests { | |||
| 12 | #expect(InlineImages.startup(OrgParser.parse(text as String)) == true) | 12 | #expect(InlineImages.startup(OrgParser.parse(text as String)) == true) |
| 13 | #expect(InlineImages.startup(OrgParser.parse("#+STARTUP: overview noinlineimages\n")) == false) | 13 | #expect(InlineImages.startup(OrgParser.parse("#+STARTUP: overview noinlineimages\n")) == false) |
| 14 | #expect(InlineImages.startup(OrgParser.parse("* x\n")) == nil) | 14 | #expect(InlineImages.startup(OrgParser.parse("* x\n")) == nil) |
| 15 | #expect(InlineImages.startup(OrgParser.parse("#+begin_src org\n#+STARTUP: inlineimages\n#+end_src\n")) == nil) | ||
| 15 | } | 16 | } |
| 16 | } | 17 | } |
Tests/OrgCoreTests/ReadDateTests.swift +2
| @@ -11,11 +11,13 @@ func frozenClock(_ seconds: Int, _ body: String) -> String { | |||
| 11 | (otl (symbol-function 'time-less-p)) | 11 | (otl (symbol-function 'time-less-p)) |
| 12 | (ots (symbol-function 'time-subtract)) | 12 | (ots (symbol-function 'time-subtract)) |
| 13 | (ota (symbol-function 'time-add)) | 13 | (ota (symbol-function 'time-add)) |
| 14 | (oflt (symbol-function 'float-time)) | ||
| 14 | ((symbol-function 'current-time) (lambda () frozen)) | 15 | ((symbol-function 'current-time) (lambda () frozen)) |
| 15 | ((symbol-function 'org-today) (lambda () (time-to-days frozen))) | 16 | ((symbol-function 'org-today) (lambda () (time-to-days frozen))) |
| 16 | ((symbol-function 'time-less-p) (lambda (a b) (funcall otl (or a frozen) (or b frozen)))) | 17 | ((symbol-function 'time-less-p) (lambda (a b) (funcall otl (or a frozen) (or b frozen)))) |
| 17 | ((symbol-function 'time-subtract) (lambda (a b) (funcall ots (or a frozen) (or b frozen)))) | 18 | ((symbol-function 'time-subtract) (lambda (a b) (funcall ots (or a frozen) (or b frozen)))) |
| 18 | ((symbol-function 'time-add) (lambda (a b) (funcall ota (or a frozen) (or b frozen)))) | 19 | ((symbol-function 'time-add) (lambda (a b) (funcall ota (or a frozen) (or b frozen)))) |
| 20 | ((symbol-function 'float-time) (lambda (&optional time) (funcall oflt (or time frozen)))) | ||
| 19 | ((symbol-function 'decode-time) (lambda (&optional time zone form) (funcall odt (or time frozen) zone form))) | 21 | ((symbol-function 'decode-time) (lambda (&optional time zone form) (funcall odt (or time frozen) zone form))) |
| 20 | ((symbol-function 'format-time-string) (lambda (f &optional time zone) (funcall oft f (or time frozen) zone)))) | 22 | ((symbol-function 'format-time-string) (lambda (f &optional time zone) (funcall oft f (or time frozen) zone)))) |
| 21 | \(body)) | 23 | \(body)) |
Tests/OrgCoreTests/TimestampCommandTests.swift +34
| @@ -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", |
| @@ -111,6 +142,9 @@ struct TimestampCommandTests { | |||
| 111 | "* P [0/1]\n** TODO a\nSCHEDULED: <2026-09-01 Tue +1w>\n* Q\n", | 142 | "* P [0/1]\n** TODO a\nSCHEDULED: <2026-09-01 Tue +1w>\n* Q\n", |
| 112 | "* TODO no repeat\nSCHEDULED: <2026-09-01 Tue>\n", | 143 | "* TODO no repeat\nSCHEDULED: <2026-09-01 Tue>\n", |
| 113 | "* TODO hourly\nSCHEDULED: <2026-09-10 Thu 00:00 +1h>\n", | 144 | "* TODO hourly\nSCHEDULED: <2026-09-10 Thu 00:00 +1h>\n", |
| 145 | "* TODO from now\nSCHEDULED: <2026-09-08 Tue 10:00 .+2h>\n", | ||
| 146 | "* TODO from now\n<2026-09-10 Thu 03:15-04:00 .+1h>\n", | ||
| 147 | "* TODO catch up\nSCHEDULED: <2026-09-09 Wed 23:00 ++2h>\n", | ||
| 114 | ] | 148 | ] |
| 115 | 149 | ||
| 116 | @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil)) | 150 | @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil)) |
Tests/OrgCoreTests/TodoLoggingTests.swift +1
| @@ -9,6 +9,7 @@ struct TodoLoggingTests { | |||
| 9 | "* TODO Task\n:LOGBOOK:\n- State \"TODO\" from [2026-01-01 Thu 10:00]\n:END:\n", | 9 | "* TODO Task\n:LOGBOOK:\n- State \"TODO\" from [2026-01-01 Thu 10:00]\n:END:\n", |
| 10 | "* DONE Task\nCLOSED: [2026-01-01 Thu 10:00]\n", | 10 | "* DONE Task\nCLOSED: [2026-01-01 Thu 10:00]\n", |
| 11 | "#+STARTUP: lognotedone logdrawer\n* TODO Task\n", | 11 | "#+STARTUP: lognotedone logdrawer\n* TODO Task\n", |
| 12 | "#+begin_example\n#+STARTUP: lognotedone logdrawer\n#+end_example\n* TODO Task\n", | ||
| 12 | "* TODO Task\n:PROPERTIES:\n:LOGGING: DONE(!) logrepeat\n:LOG_INTO_DRAWER: NOTES\n:END:\n", | 13 | "* TODO Task\n:PROPERTIES:\n:LOGGING: DONE(!) logrepeat\n:LOG_INTO_DRAWER: NOTES\n:END:\n", |
| 13 | "* TODO Task\nSCHEDULED: <2026-10-05 Mon +1w>\n", | 14 | "* TODO Task\nSCHEDULED: <2026-10-05 Mon +1w>\n", |
| 14 | "* TODO Task\n- item one\n", | 15 | "* TODO Task\n- item one\n", |
Tests/OrgPresentationTests/PresentationTests.swift +1
| @@ -128,6 +128,7 @@ struct CyclingTests { | |||
| 128 | let state = Presentation.startupFoldState(levels) | 128 | let state = Presentation.startupFoldState(levels) |
| 129 | #expect(state?.folds.count == 1 && state?.bodyFolds.count == 2) | 129 | #expect(state?.folds.count == 1 && state?.bodyFolds.count == 2) |
| 130 | #expect(Presentation.startupOptions(OrgParser.parse("#+begin_example\n#+STARTUP: overview\n#+end_example\n")).isEmpty) | 130 | #expect(Presentation.startupOptions(OrgParser.parse("#+begin_example\n#+STARTUP: overview\n#+end_example\n")).isEmpty) |
| 131 | #expect(Presentation.startupOptions(OrgParser.parse("#+STARTUP: content\n* a\n:LOGBOOK:\n#+startup: hideblocks\n:END:\n")) == ["content", "hideblocks"]) | ||
| 131 | } | 132 | } |
| 132 | } | 133 | } |
| 133 | 134 | ||