Sources/OrgCore/Commands/Command.swift
293 lines · 14986 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 /// `org-log-done`: CLOSED when an entry becomes done, and a note with `.note`.
17 public var logDone: LogHow?
18 /// `org-log-into-drawer`: the drawer state notes go in, or nil for none.
19 public var logIntoDrawer: String?
20 /// `org-log-reschedule` and `org-log-redeadline`: a note when a date changes or goes.
21 public var logReschedule: LogHow?
22 public var logRedeadline: LogHow?
23 /// `org-use-fast-todo-selection` `auto`: with keys in the keywords, C-c C-t asks for one.
24 public var fastTodoSelection: Bool
25 /// `org-enforce-todo-dependencies`: an entry can't become done before its TODO
26 /// descendants, or under an `ORDERED` parent before its earlier TODO siblings.
27 public var enforceTodoDependencies = false
28 /// `org-enforce-todo-checkbox-dependencies`: an entry can't become done with unchecked boxes.
29 public var enforceTodoCheckboxDependencies = false
30 /// `org-use-speed-commands`: single keys at the start of a heading line run commands.
31 public var useSpeedCommands: Bool
32 /// `org-startup-with-inline-images`.
33 public var startupWithInlineImages: Bool
34 /// `org-startup-indented`: org-indent's virtual indentation.
35 public var startupIndented = true
36 /// `org-hide-leading-stars`; org-indent hides them too.
37 public var hideLeadingStars = false
38 /// `org-startup-align-all-tables`.
39 public var startupAlignAllTables = false
40 /// `org-startup-truncated`: long lines run off the right edge instead of wrapping.
41 public var startupTruncated = false
42 /// Spell checking while typing, outside code and markup.
43 public var spellCheck = false
44 /// `electric-pair-mode`: brackets and quotes typed in pairs.
45 public var electricPair = false
46 /// `org-cycle-hide-drawer-startup`: drawers are folded when a file opens.
47 public var hideDrawerStartup: Bool
48 /// `org-cycle-hide-block-startup`: blocks are folded when a file opens.
49 public var hideBlockStartup: Bool
50 /// `fill-column` for M-q.
51 public var fillColumn: Int
52 /// `org-hide-emphasis-markers`: table alignment measures emphasis without its markers.
53 public var hideEmphasisMarkers: Bool
54 /// `org-pretty-entities`: table alignment measures `\alpha` as `α`.
55 public var prettyEntities: Bool
56 /// `org-use-sub-superscripts` `{}`: only `_{…}` and `^{…}` are sub- and superscripts.
57 public var subSuperscriptsNeedBraces: Bool
58
59 public init(
60 tagsColumn: Int = -77, insertHeadingRespectContent: Bool = false, metaReturnMaySplitLine: Bool = true,
61 listAllowAlphabetical: Bool = false, logRepeat: Bool = true, fillColumn: Int = 70,
62 hideEmphasisMarkers: Bool = false, prettyEntities: Bool = false, subSuperscriptsNeedBraces: Bool = false,
63 logDone: LogHow? = nil, logIntoDrawer: String? = nil, fastTodoSelection: Bool = true,
64 hideDrawerStartup: Bool = true, hideBlockStartup: Bool = false, logReschedule: LogHow? = nil, logRedeadline: LogHow? = nil,
65 useSpeedCommands: Bool = false, startupWithInlineImages: Bool = false,
66 startupIndented: Bool = true, hideLeadingStars: Bool = false, startupAlignAllTables: Bool = false,
67 startupTruncated: Bool = false, spellCheck: Bool = false
68 ) {
69 self.startupTruncated = startupTruncated
70 self.spellCheck = spellCheck
71 self.startupIndented = startupIndented
72 self.hideLeadingStars = hideLeadingStars
73 self.startupAlignAllTables = startupAlignAllTables
74 self.useSpeedCommands = useSpeedCommands
75 self.startupWithInlineImages = startupWithInlineImages
76 self.logReschedule = logReschedule
77 self.logRedeadline = logRedeadline
78 self.hideDrawerStartup = hideDrawerStartup
79 self.hideBlockStartup = hideBlockStartup
80 self.fastTodoSelection = fastTodoSelection
81 self.logDone = logDone
82 self.logIntoDrawer = logIntoDrawer
83 self.tagsColumn = tagsColumn
84 self.insertHeadingRespectContent = insertHeadingRespectContent
85 self.metaReturnMaySplitLine = metaReturnMaySplitLine
86 self.listAllowAlphabetical = listAllowAlphabetical
87 self.logRepeat = logRepeat
88 self.fillColumn = fillColumn
89 self.hideEmphasisMarkers = hideEmphasisMarkers
90 self.prettyEntities = prettyEntities
91 self.subSuperscriptsNeedBraces = subSuperscriptsNeedBraces
92 }
93
94 /// Org's own defaults.
95 public static let org = EditingOptions()
96}
97
98/// Everything a command may read. Commands never read the clock or the file system, so the
99/// same context always gives the same result.
100public struct EditContext: Sendable {
101 /// The revision `text` and `tree` belong to; results are applied only at this revision.
102 public let revision: Int
103 public let text: String
104 public let tree: OrgTree
105 /// Selections in UTF-16 offsets. The first is the main caret.
106 public let selection: [Range<Int>]
107 public let now: Date
108 /// Includes the time zone.
109 public let calendar: Calendar
110 /// Replies to earlier prompts, by prompt key.
111 public let answers: [String: String]
112 /// Ranges the view hides (folded text); motion commands skip headings inside them.
113 public let hidden: [Range<Int>]
114 public let options: EditingOptions
115 /// Tags used across the workspace, for completion; read when a command needs them.
116 public let knownTags: @Sendable () -> [String]
117 /// The command run just before, with nothing typed since (`last-command`).
118 public let lastCommand: String?
119
120 public init(
121 revision: Int, text: String, tree: OrgTree, selection: [Range<Int>],
122 now: Date = Date(), calendar: Calendar = .current, answers: [String: String] = [:], hidden: [Range<Int>] = [],
123 options: EditingOptions = .org, knownTags: @escaping @Sendable () -> [String] = { [] }, lastCommand: String? = nil
124 ) {
125 self.lastCommand = lastCommand
126 self.knownTags = knownTags
127 self.revision = revision
128 self.text = text
129 self.tree = tree
130 self.selection = selection
131 self.now = now
132 self.calendar = calendar
133 self.answers = answers
134 self.hidden = hidden
135 self.options = options
136 }
137
138 public var caret: Int { selection.first?.lowerBound ?? 0 }
139}
140
141public struct Prompt: Sendable, Equatable {
142 /// The key the answer comes back under in `EditContext.answers`.
143 public let key: String
144 public let message: String
145 /// Text the answer field starts with.
146 public let initial: String
147 /// When set, the answer is one of these, or with `requireMatch` false, these are offered.
148 public let choices: [String]
149 public let requireMatch: Bool
150 /// Choices complete the text after the last of these characters (`:` between tags).
151 public let completionSeparator: Character?
152 /// Single keys answer instead of text.
153 public let fast: FastSelection?
154 /// A date prompt, read by `org-read-date` against this default (nil: today).
155 public let date: DateRead?
156
157 public struct DateRead: Sendable, Equatable {
158 public let defaultDate: Date?
159 /// Whether the answer becomes an inactive timestamp, shown in brackets.
160 public let inactive: Bool
161 public init(defaultDate: Date?, inactive: Bool = false) {
162 self.defaultDate = defaultDate
163 self.inactive = inactive
164 }
165 }
166
167 public init(
168 key: String, message: String, initial: String = "", choices: [String] = [], requireMatch: Bool = true,
169 completionSeparator: Character? = nil, fast: FastSelection? = nil, date: DateRead? = nil
170 ) {
171 self.key = key
172 self.message = message
173 self.initial = initial
174 self.choices = choices
175 self.requireMatch = requireMatch
176 self.completionSeparator = completionSeparator
177 self.fast = fast
178 self.date = date
179 }
180}
181
182/// Edits computed elsewhere (refiling to another file, archiving), applied like any command's
183/// result. Fails if the document changed since they were computed.
184public struct PrecomputedEdit: OrgCommand {
185 public let result: EditResult
186 public init(_ result: EditResult) { self.result = result }
187 public var id: String { "internal.precomputed" }
188 public var title: String { "Apply Edit" }
189 public func applies(in context: EditContext) -> Bool { context.revision == result.baseRevision }
190 public func run(in context: EditContext) -> CommandStep {
191 context.revision == result.baseRevision ? .commit(result) : .failed("The file changed; try again.")
192 }
193}
194
195/// Things a command asks for besides text changes; the platform layer carries them out.
196public enum Effect: Sendable, Equatable {
197 case message(String)
198 /// An inserted link was a stored one; it leaves `org-stored-links`.
199 case storedLinkUsed(String)
200 /// Text for the clipboard (the kill ring).
201 case copy(String)
202 /// Show a sparse tree: the context of each point, the rest folded, and highlights until
203 /// the next edit or C-c C-c.
204 case sparseTree(show: [Int], highlights: [Range<Int>])
205 /// Fold the subtree of the heading starting here (in the new text).
206 case fold(Int)
207 /// Read the file's `#+SETUPFILE`s again (`org-mode-restart`).
208 case refreshSetup
209}
210
211public struct EditResult: Sendable, Equatable {
212 public let baseRevision: Int
213 /// Non-overlapping, in `baseRevision` coordinates.
214 public let edits: [TextEdit]
215 /// Selection after the edits, in new coordinates; nil maps the old selection through.
216 public let selection: [Range<Int>]?
217 public let effects: [Effect]
218
219 public init(baseRevision: Int, edits: [TextEdit], selection: [Range<Int>]? = nil, effects: [Effect] = []) {
220 self.baseRevision = baseRevision
221 self.edits = edits
222 self.selection = selection
223 self.effects = effects
224 }
225}
226
227public enum CommandStep: Sendable, Equatable {
228 case commit(EditResult)
229 /// Ask, then run again with the answer in `EditContext.answers`.
230 case prompt(Prompt)
231 /// The command can't run here; nothing changes. The message is for the user, as org's
232 /// `user-error`.
233 case failed(String)
234 /// Work the platform does outside the text (Emacs, an interpreter); its result comes back
235 /// as an edit.
236 case external(ExternalRequest)
237}
238
239public enum ExternalRequest: Sendable, Equatable {
240 /// Recalculate the table whose text is at `table` in Emacs, running `form` with point at
241 /// `point`, and put Emacs's table text in its place. `lisp` when its formulas run Lisp.
242 case emacsTable(table: Range<Int>, point: Int, form: String, reason: String, lisp: Bool)
243 /// Run a src block; `Babel.finish` turns its output into the result edit.
244 case babel(BabelJob)
245 /// Follow a link: the app opens URLs and files and searches headings.
246 case openLink(OrgLink)
247 /// Edit a block's text apart (`org-edit-special`); `ReplaceBlockLines` puts it back.
248 case editBlock(BlockEdit)
249 /// Show a column view.
250 case columnView(ColumnView.View)
251 /// Show the agenda for `days` days from `start` (a `Days` number), as
252 /// `org-follow-timestamp-link` does.
253 case agenda(start: Int, days: Int)
254 /// Write source blocks to their files (`org-babel-tangle`); the app knows the file's path.
255 case tangle(Tangle.Scope)
256}
257
258/// A named operation. Keys, menus, the palette and touch controls all run commands.
259public protocol OrgCommand: Sendable {
260 /// Stable identifier, used by keymaps: `org.todo.cycle`.
261 var id: String { get }
262 /// Shown in the command palette.
263 var title: String { get }
264 /// Whether the command means something at the caret; context dispatch (one key, several
265 /// commands) runs the first that applies.
266 func applies(in context: EditContext) -> Bool
267 func run(in context: EditContext) -> CommandStep
268}
269
270public enum Commands {
271 public static let all: [any OrgCommand] = [
272 TodoCycle(), TodoShift(right: true), TodoShift(right: false), PriorityUp(), PriorityDown(), PromoteHeading(), DemoteHeading(),
273 InsertHeading(), InsertHeadingAfterSubtree(), InsertTodoHeading(),
274 MoveSubtreeUp(), MoveSubtreeDown(), PromoteSubtree(), DemoteSubtree(),
275 NextHeading(), PreviousHeading(), ForwardSameLevel(), BackwardSameLevel(), UpHeading(),
276 SetTags(),
277 InsertItem(), InsertCheckboxItem(), IndentItem(), OutdentItem(), IndentItemTree(), OutdentItemTree(),
278 MoveItemUp(), MoveItemDown(), ToggleCheckbox(),
279 InsertTimestamp(), InsertTimestamp(inactive: true), SetPlanning(deadline: false), SetPlanning(deadline: true),
280 RemovePlanning(deadline: false), RemovePlanning(deadline: true), FillParagraph(), CreateTable(),
281 TableRecalculate(all: false), TableRecalculate(all: true), ExecuteSrcBlock(), OrgDwim(),
282 ShiftTimestamp(up: true), ShiftTimestamp(up: false), ShiftTimestampDay(later: true), ShiftTimestampDay(later: false),
283 OpenLinkAtPoint(), CreateID(), ToggleComment(), ToggleHeading(), ToggleItem(), MarkSubtree(),
284 CopySubtree(cut: false), CopySubtree(cut: true), CloneSubtree(), SortEntries(), InsertStructureTemplate(), EditSpecial(), TableEditField(), TangleCommand(.all), TangleCommand(.block), TangleCommand(.target),
285 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(), ToggleOrderedProperty(),
286 ]
287
288 public static var everything: [any OrgCommand] { all + TableCommand.Action.allCases.map(TableCommand.init) }
289
290 public static func command(_ id: String) -> (any OrgCommand)? {
291 everything.first { $0.id == id }
292 }
293}