krz/orgstar

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

Sources/OrgCore/Commands/Command.swift

b98a6509c5ae75e5172dd333d1b6105fd6ceee0e
orgstar/Sources/OrgCore/Commands/Command.swift history · blame · raw

293 lines · 14986 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-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}