krz/orgstar

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

Sources/OrgCore/Commands/Command.swift

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

105 lines · 3855 bytes

  1import Foundation
  2
  3/// Everything a command may read. Commands never read the clock or the file system, so the
  4/// same context always gives the same result.
  5public struct EditContext: Sendable {
  6    /// The revision `text` and `tree` belong to; results are applied only at this revision.
  7    public let revision: Int
  8    public let text: String
  9    public let tree: OrgTree
 10    /// Selections in UTF-16 offsets. The first is the main caret.
 11    public let selection: [Range<Int>]
 12    public let now: Date
 13    /// Includes the time zone.
 14    public let calendar: Calendar
 15    /// Replies to earlier prompts, by prompt key.
 16    public let answers: [String: String]
 17    /// Ranges the view hides (folded text); motion commands skip headings inside them.
 18    public let hidden: [Range<Int>]
 19
 20    public init(
 21        revision: Int, text: String, tree: OrgTree, selection: [Range<Int>],
 22        now: Date = Date(), calendar: Calendar = .current, answers: [String: String] = [:], hidden: [Range<Int>] = []
 23    ) {
 24        self.revision = revision
 25        self.text = text
 26        self.tree = tree
 27        self.selection = selection
 28        self.now = now
 29        self.calendar = calendar
 30        self.answers = answers
 31        self.hidden = hidden
 32    }
 33
 34    public var caret: Int { selection.first?.lowerBound ?? 0 }
 35}
 36
 37public struct Prompt: Sendable, Equatable {
 38    /// The key the answer comes back under in `EditContext.answers`.
 39    public let key: String
 40    public let message: String
 41    /// Text the answer field starts with.
 42    public let initial: String
 43
 44    public init(key: String, message: String, initial: String = "") {
 45        self.key = key
 46        self.message = message
 47        self.initial = initial
 48    }
 49}
 50
 51/// Things a command asks for besides text changes; the platform layer carries them out.
 52public enum Effect: Sendable, Equatable {
 53    case message(String)
 54}
 55
 56public struct EditResult: Sendable, Equatable {
 57    public let baseRevision: Int
 58    /// Non-overlapping, in `baseRevision` coordinates.
 59    public let edits: [TextEdit]
 60    /// Selection after the edits, in new coordinates; nil maps the old selection through.
 61    public let selection: [Range<Int>]?
 62    public let effects: [Effect]
 63
 64    public init(baseRevision: Int, edits: [TextEdit], selection: [Range<Int>]? = nil, effects: [Effect] = []) {
 65        self.baseRevision = baseRevision
 66        self.edits = edits
 67        self.selection = selection
 68        self.effects = effects
 69    }
 70}
 71
 72public enum CommandStep: Sendable, Equatable {
 73    case commit(EditResult)
 74    /// Ask, then run again with the answer in `EditContext.answers`.
 75    case prompt(Prompt)
 76    /// The command can't run here; nothing changes. The message is for the user, as org's
 77    /// `user-error`.
 78    case failed(String)
 79}
 80
 81/// A named operation. Keys, menus, the palette and touch controls all run commands.
 82public protocol OrgCommand: Sendable {
 83    /// Stable identifier, used by keymaps: `org.todo.cycle`.
 84    var id: String { get }
 85    /// Shown in the command palette.
 86    var title: String { get }
 87    /// Whether the command means something at the caret; context dispatch (one key, several
 88    /// commands) runs the first that applies.
 89    func applies(in context: EditContext) -> Bool
 90    func run(in context: EditContext) -> CommandStep
 91}
 92
 93public enum Commands {
 94    public static let all: [any OrgCommand] = [
 95        TodoCycle(), PriorityUp(), PriorityDown(), PromoteHeading(), DemoteHeading(),
 96        InsertHeading(), InsertHeadingAfterSubtree(), InsertTodoHeading(),
 97        MoveSubtreeUp(), MoveSubtreeDown(), PromoteSubtree(), DemoteSubtree(),
 98        NextHeading(), PreviousHeading(), ForwardSameLevel(), BackwardSameLevel(), UpHeading(),
 99        SetTags(),
100    ]
101
102    public static func command(_ id: String) -> (any OrgCommand)? {
103        all.first { $0.id == id }
104    }
105}