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