Sources/OrgCore/Commands/Command.swift
185 lines · 8234 bytes
1import Foundation
2
3/// Org options that change what commands write, as the user's Emacs has them.
4public struct EditingOptions: Sendable, Equatable, Codable {
5 /// `org-tags-column`: negative aligns tags to end at that column, 0 puts them one space
6 /// after the title.
7 public var tagsColumn: Int
8 /// `org-insert-heading-respect-content`: M-RET adds the heading after the subtree.
9 public var insertHeadingRespectContent: Bool
10 /// `org-M-RET-may-split-line`: M-RET in the middle of a line splits it.
11 public var metaReturnMaySplitLine: Bool
12 /// `org-list-allow-alphabetical`: `a.` and `A)` are list bullets.
13 public var listAllowAlphabetical: Bool
14 /// `org-log-repeat` `time`: completing a repeating entry records LAST_REPEAT and a state note.
15 public var logRepeat: Bool
16 /// `fill-column` for M-q.
17 public var fillColumn: Int
18 /// `org-hide-emphasis-markers`: table alignment measures emphasis without its markers.
19 public var hideEmphasisMarkers: Bool
20 /// `org-pretty-entities`: table alignment measures `\alpha` as `α`.
21 public var prettyEntities: Bool
22 /// `org-use-sub-superscripts` `{}`: only `_{…}` and `^{…}` are sub- and superscripts.
23 public var subSuperscriptsNeedBraces: Bool
24
25 public init(
26 tagsColumn: Int = -77, insertHeadingRespectContent: Bool = false, metaReturnMaySplitLine: Bool = true,
27 listAllowAlphabetical: Bool = false, logRepeat: Bool = true, fillColumn: Int = 70,
28 hideEmphasisMarkers: Bool = false, prettyEntities: Bool = false, subSuperscriptsNeedBraces: Bool = false
29 ) {
30 self.tagsColumn = tagsColumn
31 self.insertHeadingRespectContent = insertHeadingRespectContent
32 self.metaReturnMaySplitLine = metaReturnMaySplitLine
33 self.listAllowAlphabetical = listAllowAlphabetical
34 self.logRepeat = logRepeat
35 self.fillColumn = fillColumn
36 self.hideEmphasisMarkers = hideEmphasisMarkers
37 self.prettyEntities = prettyEntities
38 self.subSuperscriptsNeedBraces = subSuperscriptsNeedBraces
39 }
40
41 /// Org's own defaults.
42 public static let org = EditingOptions()
43}
44
45/// Everything a command may read. Commands never read the clock or the file system, so the
46/// same context always gives the same result.
47public struct EditContext: Sendable {
48 /// The revision `text` and `tree` belong to; results are applied only at this revision.
49 public let revision: Int
50 public let text: String
51 public let tree: OrgTree
52 /// Selections in UTF-16 offsets. The first is the main caret.
53 public let selection: [Range<Int>]
54 public let now: Date
55 /// Includes the time zone.
56 public let calendar: Calendar
57 /// Replies to earlier prompts, by prompt key.
58 public let answers: [String: String]
59 /// Ranges the view hides (folded text); motion commands skip headings inside them.
60 public let hidden: [Range<Int>]
61 public let options: EditingOptions
62
63 public init(
64 revision: Int, text: String, tree: OrgTree, selection: [Range<Int>],
65 now: Date = Date(), calendar: Calendar = .current, answers: [String: String] = [:], hidden: [Range<Int>] = [],
66 options: EditingOptions = .org
67 ) {
68 self.revision = revision
69 self.text = text
70 self.tree = tree
71 self.selection = selection
72 self.now = now
73 self.calendar = calendar
74 self.answers = answers
75 self.hidden = hidden
76 self.options = options
77 }
78
79 public var caret: Int { selection.first?.lowerBound ?? 0 }
80}
81
82public struct Prompt: Sendable, Equatable {
83 /// The key the answer comes back under in `EditContext.answers`.
84 public let key: String
85 public let message: String
86 /// Text the answer field starts with.
87 public let initial: String
88 /// When set, the answer is one of these.
89 public let choices: [String]
90
91 public init(key: String, message: String, initial: String = "", choices: [String] = []) {
92 self.key = key
93 self.message = message
94 self.initial = initial
95 self.choices = choices
96 }
97}
98
99/// Edits computed elsewhere (refiling to another file, archiving), applied like any command's
100/// result. Fails if the document changed since they were computed.
101public struct PrecomputedEdit: OrgCommand {
102 public let result: EditResult
103 public init(_ result: EditResult) { self.result = result }
104 public var id: String { "internal.precomputed" }
105 public var title: String { "Apply Edit" }
106 public func applies(in context: EditContext) -> Bool { context.revision == result.baseRevision }
107 public func run(in context: EditContext) -> CommandStep {
108 context.revision == result.baseRevision ? .commit(result) : .failed("The file changed; try again.")
109 }
110}
111
112/// Things a command asks for besides text changes; the platform layer carries them out.
113public enum Effect: Sendable, Equatable {
114 case message(String)
115}
116
117public struct EditResult: Sendable, Equatable {
118 public let baseRevision: Int
119 /// Non-overlapping, in `baseRevision` coordinates.
120 public let edits: [TextEdit]
121 /// Selection after the edits, in new coordinates; nil maps the old selection through.
122 public let selection: [Range<Int>]?
123 public let effects: [Effect]
124
125 public init(baseRevision: Int, edits: [TextEdit], selection: [Range<Int>]? = nil, effects: [Effect] = []) {
126 self.baseRevision = baseRevision
127 self.edits = edits
128 self.selection = selection
129 self.effects = effects
130 }
131}
132
133public enum CommandStep: Sendable, Equatable {
134 case commit(EditResult)
135 /// Ask, then run again with the answer in `EditContext.answers`.
136 case prompt(Prompt)
137 /// The command can't run here; nothing changes. The message is for the user, as org's
138 /// `user-error`.
139 case failed(String)
140 /// Work the platform does outside the text (Emacs, an interpreter); its result comes back
141 /// as an edit.
142 case external(ExternalRequest)
143}
144
145public enum ExternalRequest: Sendable, Equatable {
146 /// Recalculate the table whose text is at `table` in Emacs, running `form` with point at
147 /// `point`, and put Emacs's table text in its place. `lisp` when its formulas run Lisp.
148 case emacsTable(table: Range<Int>, point: Int, form: String, reason: String, lisp: Bool)
149 /// Run a src block; `Babel.finish` turns its output into the result edit.
150 case babel(BabelJob)
151}
152
153/// A named operation. Keys, menus, the palette and touch controls all run commands.
154public protocol OrgCommand: Sendable {
155 /// Stable identifier, used by keymaps: `org.todo.cycle`.
156 var id: String { get }
157 /// Shown in the command palette.
158 var title: String { get }
159 /// Whether the command means something at the caret; context dispatch (one key, several
160 /// commands) runs the first that applies.
161 func applies(in context: EditContext) -> Bool
162 func run(in context: EditContext) -> CommandStep
163}
164
165public enum Commands {
166 public static let all: [any OrgCommand] = [
167 TodoCycle(), PriorityUp(), PriorityDown(), PromoteHeading(), DemoteHeading(),
168 InsertHeading(), InsertHeadingAfterSubtree(), InsertTodoHeading(),
169 MoveSubtreeUp(), MoveSubtreeDown(), PromoteSubtree(), DemoteSubtree(),
170 NextHeading(), PreviousHeading(), ForwardSameLevel(), BackwardSameLevel(), UpHeading(),
171 SetTags(),
172 InsertItem(), InsertCheckboxItem(), IndentItem(), OutdentItem(), IndentItemTree(), OutdentItemTree(),
173 MoveItemUp(), MoveItemDown(), ToggleCheckbox(),
174 InsertTimestamp(), InsertTimestamp(inactive: true), SetPlanning(deadline: false), SetPlanning(deadline: true),
175 RemovePlanning(deadline: false), RemovePlanning(deadline: true), FillParagraph(), CreateTable(),
176 TableRecalculate(all: false), TableRecalculate(all: true), ExecuteSrcBlock(), OrgDwim(),
177 ShiftTimestamp(up: true), ShiftTimestamp(up: false), ShiftTimestampDay(later: true), ShiftTimestampDay(later: false),
178 ]
179
180 public static var everything: [any OrgCommand] { all + TableCommand.Action.allCases.map(TableCommand.init) }
181
182 public static func command(_ id: String) -> (any OrgCommand)? {
183 everything.first { $0.id == id }
184 }
185}