Archive tag toggle, archive sibling, datetree archive locations !91
9 files changed, +213 −18
Layout: unified · split
Sources/OrgCore/Commands/Archive.swift +44 −12
| @@ -239,8 +239,24 @@ public enum Archive { | |||
| 239 | let step = commitBuffer(context) { source in | 239 | let step = commitBuffer(context) { source in |
| 240 | let target = archive ?? source | 240 | let target = archive ?? source |
| 241 | let level: Int | 241 | let level: Int |
| 242 | let headingText: String? | 242 | var headingText: String? |
| 243 | if let heading = destination.heading, let stars = heading.range(of: "^\\*+", options: .regularExpression) { | 243 | var heading = destination.heading |
| 244 | // `datetree/`: three levels of date headings, stars after it a heading below them. | ||
| 245 | var datetree: DateComponents? | ||
| 246 | var datetreeSubheading = false | ||
| 247 | if let h = heading, let m = h.firstMatch(of: /^datetree\/(\**)/) { | ||
| 248 | let nsub = m.1.count | ||
| 249 | heading = String(repeating: "*", count: 3 + nsub) + h[m.range.upperBound...] | ||
| 250 | datetreeSubheading = nsub > 0 | ||
| 251 | let closed = source.saveExcursion { () -> String? in | ||
| 252 | _ = try? source.backToHeading() | ||
| 253 | source.forwardLine(1) | ||
| 254 | guard let line = source.lookingAt("^[ \\t]*(?:SCHEDULED|DEADLINE|CLOSED):.*$") else { return nil } | ||
| 255 | return source.substring(line[0]!.lowerBound, line[0]!.upperBound).firstMatch(of: /CLOSED: *\[([^\]]+)\]/).map { String($0.1) } | ||
| 256 | } | ||
| 257 | datetree = EmacsBuffer.parseTimeString(closed ?? time) | ||
| 258 | } | ||
| 259 | if let heading, let stars = heading.range(of: "^\\*+", options: .regularExpression) { | ||
| 244 | level = heading[stars].count | 260 | level = heading[stars].count |
| 245 | headingText = heading | 261 | headingText = heading |
| 246 | } else { | 262 | } else { |
| @@ -264,28 +280,44 @@ public enum Archive { | |||
| 264 | target.point = target.length | 280 | target.point = target.length |
| 265 | target.insert("\nArchived entries from file \(sourcePath)\n\n") | 281 | target.insert("\nArchived entries from file \(sourcePath)\n\n") |
| 266 | } | 282 | } |
| 267 | target.point = 0 | 283 | // The date's entry, as `org-datetree-find-date-create` then `org-narrow-to-subtree`. |
| 268 | if let headingText { | 284 | let low = target.marker(0) |
| 285 | let high = target.marker(target.length, advances: true) | ||
| 286 | defer { | ||
| 287 | target.release(low) | ||
| 288 | target.release(high) | ||
| 289 | } | ||
| 290 | if let date = datetree { | ||
| 291 | let day = Datetree.findCreate(target, under: nil, tree: .day, date: date) | ||
| 292 | low.position = day | ||
| 293 | high.position = Datetree.narrowEnd(target, day, limit: target.length) | ||
| 294 | } | ||
| 295 | target.point = low.position | ||
| 296 | if let headingText, !(datetree != nil && !datetreeSubheading) { | ||
| 269 | let pattern = "^" + NSRegularExpression.escapedPattern(for: headingText) + "([ \\t]+:([[:alnum:]_@#%]+:)+)?[ \\t]*$" | 297 | let pattern = "^" + NSRegularExpression.escapedPattern(for: headingText) + "([ \\t]+:([[:alnum:]_@#%]+:)+)?[ \\t]*$" |
| 270 | if let match = target.searchForward(pattern) { | 298 | if let match = target.searchForward(pattern, bound: high.position) { |
| 271 | target.point = match[0]!.upperBound | 299 | target.point = match[0]!.upperBound |
| 272 | } else { | 300 | } else { |
| 273 | target.point = target.length | 301 | target.point = high.position |
| 274 | if !target.bolp { target.insert("\n") } | 302 | if !target.bolp { target.insert("\n") } |
| 275 | target.insert("\n" + headingText + "\n") | 303 | target.insert((datetree != nil ? "" : "\n") + headingText + "\n") |
| 276 | target.forwardLine(-1) | 304 | target.forwardLine(-1) |
| 277 | target.endOfLine() | 305 | target.endOfLine() |
| 278 | } | 306 | } |
| 279 | target.endOfSubtree() | 307 | target.endOfSubtree() |
| 308 | target.point = min(target.point, high.position) | ||
| 280 | target.skipBackward(EmacsBuffer.whitespace) | 309 | target.skipBackward(EmacsBuffer.whitespace) |
| 310 | target.point = max(target.point, low.position) | ||
| 281 | let blanks = target.looking("[ \\t\\r\\n]*")! | 311 | let blanks = target.looking("[ \\t\\r\\n]*")! |
| 282 | target.replace(blanks[0]!, with: "\n\n") | 312 | let clipped = blanks[0]!.lowerBound..<min(blanks[0]!.upperBound, high.position) |
| 283 | target.point = blanks[0]!.lowerBound + 2 | 313 | let separator = datetree != nil ? "\n" : "\n\n" |
| 314 | target.replace(clipped, with: separator) | ||
| 315 | target.point = clipped.lowerBound + separator.count | ||
| 284 | } else { | 316 | } else { |
| 285 | target.point = target.length | 317 | target.point = high.position |
| 286 | target.insert("\n") | 318 | if !(datetree != nil && target.bolp) { target.insert("\n") } |
| 287 | } | 319 | } |
| 288 | try target.pasteSubtree(level: max(1, level + (headingText == nil ? 0 : 1)), tree: tree) | 320 | try target.pasteSubtree(level: max(1, level + (headingText == nil ? 0 : 1)), tree: tree, limit: datetree != nil ? high.position : nil) |
| 289 | if !inherited.isEmpty, destination.isSameFile { | 321 | if !inherited.isEmpty, destination.isSameFile { |
| 290 | let all = inherited + local | 322 | let all = inherited + local |
| 291 | target.saveExcursion { | 323 | target.saveExcursion { |
Sources/OrgCore/Commands/ArchiveCommands.swift added +94
| @@ -0,0 +1,94 @@ | |||
| 1 | import Foundation | ||
| 2 | |||
| 3 | // Archiving in place (org-archive.el, Org 9.8.7): `org-toggle-archive-tag` and | ||
| 4 | // `org-archive-to-archive-sibling` with `org-archive-sibling-heading` "Archive" and | ||
| 5 | // `org-archive-reversed-order` nil. | ||
| 6 | |||
| 7 | /// C-c C-x a: `org-toggle-archive-tag`. Setting the tag folds the subtree. | ||
| 8 | public struct ToggleArchiveTag: OrgCommand { | ||
| 9 | public init() {} | ||
| 10 | public var id: String { "org.archive.toggle-tag" } | ||
| 11 | public var title: String { "Toggle ARCHIVE Tag" } | ||
| 12 | public func applies(in context: EditContext) -> Bool { entryHeading(at: context.caret, in: context.tree) != nil } | ||
| 13 | public func run(in context: EditContext) -> CommandStep { | ||
| 14 | guard let heading = entryHeading(at: context.caret, in: context.tree) else { return .failed("Before first headline") } | ||
| 15 | var tags = HeadingLine(heading).tags.map { (context.text as NSString).substring(with: NSRange($0.range)).split(separator: ":").map(String.init) } ?? [] | ||
| 16 | let set = !tags.contains("ARCHIVE") | ||
| 17 | if set { tags.append("ARCHIVE") } else { tags.removeAll { $0 == "ARCHIVE" } } | ||
| 18 | guard case .commit(let result) = SetTags.set(tags, in: context) else { return SetTags.set(tags, in: context) } | ||
| 19 | let start = heading.range.lowerBound | ||
| 20 | var effects = result.effects + [.message("Subtree \(set ? "archived" : "unarchived")")] | ||
| 21 | var selection = [context.caret..<context.caret] | ||
| 22 | if set { | ||
| 23 | effects.insert(.fold(start), at: 0) | ||
| 24 | let line = (context.text as NSString).lineRange(for: NSRange(location: context.caret, length: 0)).location | ||
| 25 | selection = [line..<line] | ||
| 26 | } | ||
| 27 | return .commit(EditResult(baseRevision: result.baseRevision, edits: result.edits, selection: selection, effects: effects)) | ||
| 28 | } | ||
| 29 | } | ||
| 30 | |||
| 31 | /// C-c C-x A: `org-archive-to-archive-sibling`. | ||
| 32 | public struct ArchiveToSibling: OrgCommand { | ||
| 33 | public init() {} | ||
| 34 | public var id: String { "org.archive.to-sibling" } | ||
| 35 | public var title: String { "Archive to Archive Sibling" } | ||
| 36 | public func applies(in context: EditContext) -> Bool { entryHeading(at: context.caret, in: context.tree) != nil } | ||
| 37 | |||
| 38 | public func run(in context: EditContext) -> CommandStep { | ||
| 39 | let formatter = DateFormatter() | ||
| 40 | formatter.locale = Locale(identifier: "en_US_POSIX") | ||
| 41 | formatter.calendar = context.calendar | ||
| 42 | formatter.timeZone = context.calendar.timeZone | ||
| 43 | formatter.dateFormat = "yyyy-MM-dd EEE HH:mm" | ||
| 44 | let time = formatter.string(from: context.now) | ||
| 45 | var sibling = 0 | ||
| 46 | let step = commitBuffer(context) { buffer in | ||
| 47 | try buffer.backToHeading() | ||
| 48 | let level = buffer.outlineLevel | ||
| 49 | let leader = String(repeating: "*", count: level) + " " | ||
| 50 | let pos = buffer.marker(buffer.point, advances: true) | ||
| 51 | defer { buffer.release(pos) } | ||
| 52 | var end: Int? | ||
| 53 | if buffer.upHeadingSafe() == nil { | ||
| 54 | end = buffer.length | ||
| 55 | buffer.point = 0 | ||
| 56 | } | ||
| 57 | let begin = buffer.point | ||
| 58 | let e = buffer.marker(end ?? buffer.saveExcursion { buffer.endOfSubtree(toHeading: true) }) | ||
| 59 | defer { buffer.release(e) } | ||
| 60 | buffer.point = begin | ||
| 61 | let pattern = "^" + NSRegularExpression.escapedPattern(for: leader) + "[ \\t]*Archive[ \\t]*:ARCHIVE:" | ||
| 62 | if buffer.searchForward(pattern, bound: e.position, caseInsensitive: false) == nil { | ||
| 63 | buffer.point = e.position | ||
| 64 | if !buffer.bolp { buffer.insert("\n") } | ||
| 65 | buffer.insert(leader + "Archive\n") | ||
| 66 | buffer.forwardLine(-1) | ||
| 67 | buffer.endOfLine() | ||
| 68 | buffer.insert(" :ARCHIVE:") | ||
| 69 | buffer.alignTags(onLineAt: buffer.point) | ||
| 70 | } | ||
| 71 | buffer.beginningOfLine() | ||
| 72 | let archive = buffer.marker(buffer.point) | ||
| 73 | defer { buffer.release(archive) } | ||
| 74 | buffer.endOfSubtree(toHeading: true) | ||
| 75 | let target = buffer.marker(buffer.point) | ||
| 76 | defer { buffer.release(target) } | ||
| 77 | // `org-cut-subtree` at the entry, then `org-paste-subtree` one level below. | ||
| 78 | buffer.point = pos.position | ||
| 79 | let tree = try buffer.copySubtree() | ||
| 80 | let range = try buffer.subtreeRange() | ||
| 81 | buffer.deleteRegion(range.lowerBound, range.upperBound) | ||
| 82 | buffer.point = target.position | ||
| 83 | try buffer.pasteSubtree(level: level + 1, tree: tree) | ||
| 84 | buffer.entryPut("ARCHIVE_TIME", time) | ||
| 85 | buffer.point = archive.position | ||
| 86 | sibling = archive.position | ||
| 87 | buffer.updateParentTodoStatistics() | ||
| 88 | buffer.point = pos.position | ||
| 89 | if buffer.lookingAt("[ \\t]*$") != nil, let next = buffer.nextHeadingStart(after: buffer.point) { buffer.point = next } | ||
| 90 | } | ||
| 91 | guard case .commit(let result) = step else { return step } | ||
| 92 | return .commit(EditResult(baseRevision: result.baseRevision, edits: result.edits, selection: result.selection, effects: result.effects + [.fold(sibling)])) | ||
| 93 | } | ||
| 94 | } | ||
Sources/OrgCore/Commands/Command.swift +3 −1
| @@ -170,6 +170,8 @@ public enum Effect: Sendable, Equatable { | |||
| 170 | /// Show a sparse tree: the context of each point, the rest folded, and highlights until | 170 | /// Show a sparse tree: the context of each point, the rest folded, and highlights until |
| 171 | /// the next edit or C-c C-c. | 171 | /// the next edit or C-c C-c. |
| 172 | case sparseTree(show: [Int], highlights: [Range<Int>]) | 172 | case sparseTree(show: [Int], highlights: [Range<Int>]) |
| 173 | /// Fold the subtree of the heading starting here (in the new text). | ||
| 174 | case fold(Int) | ||
| 173 | } | 175 | } |
| 174 | 176 | ||
| 175 | public struct EditResult: Sendable, Equatable { | 177 | public struct EditResult: Sendable, Equatable { |
| @@ -241,7 +243,7 @@ public enum Commands { | |||
| 241 | ShiftTimestamp(up: true), ShiftTimestamp(up: false), ShiftTimestampDay(later: true), ShiftTimestampDay(later: false), | 243 | ShiftTimestamp(up: true), ShiftTimestamp(up: false), ShiftTimestampDay(later: true), ShiftTimestampDay(later: false), |
| 242 | OpenLinkAtPoint(), CreateID(), ToggleComment(), ToggleHeading(), ToggleItem(), MarkSubtree(), | 244 | OpenLinkAtPoint(), CreateID(), ToggleComment(), ToggleHeading(), ToggleItem(), MarkSubtree(), |
| 243 | CopySubtree(cut: false), CopySubtree(cut: true), CloneSubtree(), SortEntries(), InsertStructureTemplate(), EditSpecial(), | 245 | CopySubtree(cut: false), CopySubtree(cut: true), CloneSubtree(), SortEntries(), InsertStructureTemplate(), EditSpecial(), |
| 244 | UpdateDynamicBlock(), ClockReportTable(), SetEffort(), CtrlCCtrlC(), EvaluateTimeRange(), FootnoteAction(), FootnoteAction(special: true), TableEvalFormula(named: false), TableEvalFormula(named: true), SortTableLines(), TransposeTable(), GotoHeading(), SparseTreeCommand(), SetPriority(), SetPriority(value: "A"), SetPriority(value: "B"), SetPriority(value: "C"), SetPriority(value: " "), ReadAndSetProperty(), DeleteProperty(), DeleteProperty(globally: true), PropertyAction(), NextAllowedValue(previous: false), NextAllowedValue(previous: true), InsertColumnView(), ShowColumnView(), ShowColumnView(global: true), | 246 | UpdateDynamicBlock(), ClockReportTable(), SetEffort(), CtrlCCtrlC(), EvaluateTimeRange(), FootnoteAction(), FootnoteAction(special: true), TableEvalFormula(named: false), TableEvalFormula(named: true), SortTableLines(), TransposeTable(), GotoHeading(), SparseTreeCommand(), SetPriority(), SetPriority(value: "A"), SetPriority(value: "B"), SetPriority(value: "C"), SetPriority(value: " "), ReadAndSetProperty(), DeleteProperty(), DeleteProperty(globally: true), PropertyAction(), NextAllowedValue(previous: false), NextAllowedValue(previous: true), InsertColumnView(), ShowColumnView(), ShowColumnView(global: true), ToggleArchiveTag(), ArchiveToSibling(), |
| 245 | ] | 247 | ] |
| 246 | 248 | ||
| 247 | public static var everything: [any OrgCommand] { all + TableCommand.Action.allCases.map(TableCommand.init) } | 249 | public static var everything: [any OrgCommand] { all + TableCommand.Action.allCases.map(TableCommand.init) } |
Sources/OrgCore/Commands/Refile.swift +3 −3
| @@ -32,13 +32,13 @@ extension EmacsBuffer { | |||
| 32 | } | 32 | } |
| 33 | } | 33 | } |
| 34 | 34 | ||
| 35 | /// `org-paste-subtree` with a numeric level. | 35 | /// `org-paste-subtree` with a numeric level; `limit` ends the buffer there, as narrowing does. |
| 36 | func pasteSubtree(level: Int, tree: String) throws { | 36 | func pasteSubtree(level: Int, tree: String, limit: Int? = nil) throws { |
| 37 | let oldLevel = tree.range(of: "(?m)^\\*+ ", options: .regularExpression).map { tree[$0].count - 1 } ?? -1 | 37 | let oldLevel = tree.range(of: "(?m)^\\*+ ", options: .regularExpression).map { tree[$0].count - 1 } ?? -1 |
| 38 | let shift = oldLevel == -1 || oldLevel == level ? 0 : level - oldLevel | 38 | let shift = oldLevel == -1 || oldLevel == level ? 0 : level - oldLevel |
| 39 | if !(bolp && atHeading) { | 39 | if !(bolp && atHeading) { |
| 40 | endOfLine() | 40 | endOfLine() |
| 41 | point = nextHeadingStart(after: point) ?? length | 41 | point = min(nextHeadingStart(after: point) ?? length, limit ?? length) |
| 42 | if !bolp { insert("\n") } | 42 | if !bolp { insert("\n") } |
| 43 | } | 43 | } |
| 44 | var begin = point | 44 | var begin = point |
Sources/OrgCore/Keymap/Presets.swift +10
| @@ -326,6 +326,10 @@ public enum KeymapPreset: String, Sendable, CaseIterable { | |||
| 326 | command = "app.archive" | 326 | command = "app.archive" |
| 327 | mode = "normal" | 327 | mode = "normal" |
| 328 | [[bind]] | 328 | [[bind]] |
| 329 | keys = "SPC m s a" | ||
| 330 | command = "org.archive.toggle-tag" | ||
| 331 | mode = "normal" | ||
| 332 | [[bind]] | ||
| 329 | keys = "SPC m s h" | 333 | keys = "SPC m s h" |
| 330 | command = "org.subtree.promote" | 334 | command = "org.subtree.promote" |
| 331 | mode = "normal" | 335 | mode = "normal" |
| @@ -719,6 +723,12 @@ public enum KeymapPreset: String, Sendable, CaseIterable { | |||
| 719 | keys = "C-c C-x i" | 723 | keys = "C-c C-x i" |
| 720 | command = "org.columns.insert-dblock" | 724 | command = "org.columns.insert-dblock" |
| 721 | [[bind]] | 725 | [[bind]] |
| 726 | keys = "C-c C-x a" | ||
| 727 | command = "org.archive.toggle-tag" | ||
| 728 | [[bind]] | ||
| 729 | keys = "C-c C-x A" | ||
| 730 | command = "org.archive.to-sibling" | ||
| 731 | [[bind]] | ||
| 722 | keys = "C-c C-x C-c" | 732 | keys = "C-c C-x C-c" |
| 723 | command = "org.columns" | 733 | command = "org.columns" |
| 724 | [[bind]] | 734 | [[bind]] |
Sources/OrgEditorAppKit/OrgEditor.swift +4
| @@ -460,6 +460,10 @@ public final class OrgEditor: NSObject { | |||
| 460 | switch effect { | 460 | switch effect { |
| 461 | case .message(let text): onMessage?(text) | 461 | case .message(let text): onMessage?(text) |
| 462 | case .sparseTree(let points, let highlights): showSparseTree(points, highlights: highlights) | 462 | case .sparseTree(let points, let highlights): showSparseTree(points, highlights: highlights) |
| 463 | case .fold(let start): | ||
| 464 | var state = foldState | ||
| 465 | state.folds.insert(start) | ||
| 466 | setFolds(state) | ||
| 463 | default: onEffect?(effect) | 467 | default: onEffect?(effect) |
| 464 | } | 468 | } |
| 465 | } | 469 | } |
Sources/Orgstar/EditorView.swift +1 −1
| @@ -103,7 +103,7 @@ struct EditorView: NSViewRepresentable { | |||
| 103 | case .copy(let text): | 103 | case .copy(let text): |
| 104 | NSPasteboard.general.clearContents() | 104 | NSPasteboard.general.clearContents() |
| 105 | NSPasteboard.general.setString(text, forType: .string) | 105 | NSPasteboard.general.setString(text, forType: .string) |
| 106 | case .message, .sparseTree: break | 106 | case .message, .sparseTree, .fold: break |
| 107 | } | 107 | } |
| 108 | } | 108 | } |
| 109 | let scroll = editor.makeScrollView() | 109 | let scroll = editor.makeScrollView() |
Tests/OrgCoreTests/ArchiveCommandTests.swift added +49
| @@ -0,0 +1,49 @@ | |||
| 1 | import Foundation | ||
| 2 | import Testing | ||
| 3 | @testable import OrgCore | ||
| 4 | |||
| 5 | struct ArchiveCommandTests { | ||
| 6 | static let oracle = ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil | ||
| 7 | |||
| 8 | @Test(.enabled(if: oracle)) | ||
| 9 | func archivesInPlaceLikeOrg() throws { | ||
| 10 | let texts = [ | ||
| 11 | "* P [0/2]\n** TODO a :x:\nbody\n** TODO b\n* Q\n", | ||
| 12 | "* P\n** a\n** Archive :ARCHIVE:\n*** old\n** b\n", | ||
| 13 | "* top\n* other :ARCHIVE:\n", | ||
| 14 | "* P\n** a\n\n** b\n", | ||
| 15 | ] | ||
| 16 | var cases: [EmacsOracle.Case] = [] | ||
| 17 | var ours: [(String, Int, String?)] = [] | ||
| 18 | for text in texts { | ||
| 19 | let ns = text as NSString | ||
| 20 | var starts: [Int] = [] | ||
| 21 | var offset = 0 | ||
| 22 | while offset < ns.length { | ||
| 23 | if ns.character(at: offset) == 42 { starts.append(offset) } | ||
| 24 | offset = NSMaxRange(ns.lineRange(for: NSRange(location: offset, length: 0))) | ||
| 25 | } | ||
| 26 | for start in starts { | ||
| 27 | for (command, form) in [(ToggleArchiveTag() as any OrgCommand, "(org-toggle-archive-tag)"), (ArchiveToSibling(), "(org-archive-to-archive-sibling)")] { | ||
| 28 | cases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(start + 1, in: text), form: frozenClock(1_789_000_000, form))) | ||
| 29 | var utc = Calendar(identifier: .gregorian) | ||
| 30 | utc.timeZone = TimeZone(identifier: "UTC")! | ||
| 31 | let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [(start + 1)..<(start + 1)], | ||
| 32 | now: Date(timeIntervalSince1970: 1_789_000_000), calendar: utc) | ||
| 33 | switch command.run(in: context) { | ||
| 34 | case .commit(let result): | ||
| 35 | let new = result.edits.sorted { $0.range.lowerBound > $1.range.lowerBound }.reduce(text) { $1.apply(to: $0) } | ||
| 36 | ours.append((new, result.selection?.first?.lowerBound ?? start + 1, nil)) | ||
| 37 | case .failed(let message): ours.append((text, start + 1, message)) | ||
| 38 | default: ours.append((text, start + 1, "other")) | ||
| 39 | } | ||
| 40 | } | ||
| 41 | } | ||
| 42 | } | ||
| 43 | for (i, theirs) in try EmacsOracle.run(cases).enumerated() { | ||
| 44 | let caret = EmacsOracle.offset(theirs.point, in: theirs.text) | ||
| 45 | #expect(ours[i].0 == theirs.text && ours[i].1 == caret && (ours[i].2 != nil) == !theirs.error.isEmpty, | ||
| 46 | "\(cases[i].form) on \(cases[i].text.debugDescription) @\(cases[i].point)\nours: \(ours[i].0.debugDescription) @\(ours[i].1) \(ours[i].2 ?? "")\nemacs: \(theirs.text.debugDescription) @\(caret) \(theirs.error)") | ||
| 47 | } | ||
| 48 | } | ||
| 49 | } | ||
Tests/OrgCoreTests/ArchiveTests.swift +5 −1
| @@ -43,8 +43,12 @@ struct ArchiveTests { | |||
| 43 | "#+ARCHIVE: ::* Archive\n* a :x:\n* Archive\n** old\n", | 43 | "#+ARCHIVE: ::* Archive\n* a :x:\n* Archive\n** old\n", |
| 44 | "* P\n:PROPERTIES:\n:ARCHIVE: ::* Old :t:\n:CATEGORY: c\n:END:\n** a\n* Old :t:\n", | 44 | "* P\n:PROPERTIES:\n:ARCHIVE: ::* Old :t:\n:CATEGORY: c\n:END:\n** a\n* Old :t:\n", |
| 45 | "* a\nSCHEDULED: <2026-01-01 Thu>\n:PROPERTIES:\n:ID: 1\n:END:\n* b", | 45 | "* a\nSCHEDULED: <2026-01-01 Thu>\n:PROPERTIES:\n:ID: 1\n:END:\n* b", |
| 46 | "#+ARCHIVE: ::datetree/\n* a\nbody\n* b\n", | ||
| 47 | "#+ARCHIVE: %s_archive::datetree/\n* DONE a\nCLOSED: [2026-08-15 Sat 09:00]\n* b\n", | ||
| 48 | "#+ARCHIVE: ::datetree/* Notes\n* a\n** sub\n* b\n", | ||
| 46 | ] | 49 | ] |
| 47 | static let archives: [String?] = [nil, "", "* existing\n", "\n#+TITLE: arch\n\n* x\n:PROPERTIES:\n:ARCHIVE_TIME: t\n:END:\n\n"] | 50 | static let archives: [String?] = [nil, "", "* existing\n", "\n#+TITLE: arch\n\n* x\n:PROPERTIES:\n:ARCHIVE_TIME: t\n:END:\n\n", |
| 51 | "* 2026\n** 2026-09 September\n*** 2026-09-10 Thursday\n**** prior\n*** 2026-09-12 Saturday\n"] | ||
| 48 | 52 | ||
| 49 | @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil)) | 53 | @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil)) |
| 50 | func archiveMatchesEmacs() throws { | 54 | func archiveMatchesEmacs() throws { |