krz/orgstar

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

Sources/OrgCore/Commands/ArchiveCommands.swift

3a3dedba062790e1580c66d38606eda4b02cb501
orgstar/Sources/OrgCore/Commands/ArchiveCommands.swift history · blame · raw

94 lines · 5029 bytes

 1import 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.
 8public 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`.
32public 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}