Sources/OrgCore/Commands/ArchiveCommands.swift
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}