krz/orgstar

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

Sources/OrgCore/Commands/Command.swift

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

95 lines · 3279 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
18    public init(
19        revision: Int, text: String, tree: OrgTree, selection: [Range<Int>],
20        now: Date = Date(), calendar: Calendar = .current, answers: [String: String] = [:]
21    ) {
22        self.revision = revision
23        self.text = text
24        self.tree = tree
25        self.selection = selection
26        self.now = now
27        self.calendar = calendar
28        self.answers = answers
29    }
30
31    public var caret: Int { selection.first?.lowerBound ?? 0 }
32}
33
34public struct Prompt: Sendable, Equatable {
35    /// The key the answer comes back under in `EditContext.answers`.
36    public let key: String
37    public let message: String
38
39    public init(key: String, message: String) {
40        self.key = key
41        self.message = message
42    }
43}
44
45/// Things a command asks for besides text changes; the platform layer carries them out.
46public enum Effect: Sendable, Equatable {
47    case message(String)
48}
49
50public struct EditResult: Sendable, Equatable {
51    public let baseRevision: Int
52    /// Non-overlapping, in `baseRevision` coordinates.
53    public let edits: [TextEdit]
54    /// Selection after the edits, in new coordinates; nil maps the old selection through.
55    public let selection: [Range<Int>]?
56    public let effects: [Effect]
57
58    public init(baseRevision: Int, edits: [TextEdit], selection: [Range<Int>]? = nil, effects: [Effect] = []) {
59        self.baseRevision = baseRevision
60        self.edits = edits
61        self.selection = selection
62        self.effects = effects
63    }
64}
65
66public enum CommandStep: Sendable, Equatable {
67    case commit(EditResult)
68    /// Ask, then run again with the answer in `EditContext.answers`.
69    case prompt(Prompt)
70    /// The command can't run here; nothing changes. The message is for the user, as org's
71    /// `user-error`.
72    case failed(String)
73}
74
75/// A named operation. Keys, menus, the palette and touch controls all run commands.
76public protocol OrgCommand: Sendable {
77    /// Stable identifier, used by keymaps: `org.todo.cycle`.
78    var id: String { get }
79    /// Shown in the command palette.
80    var title: String { get }
81    /// Whether the command means something at the caret; context dispatch (one key, several
82    /// commands) runs the first that applies.
83    func applies(in context: EditContext) -> Bool
84    func run(in context: EditContext) -> CommandStep
85}
86
87public enum Commands {
88    public static let all: [any OrgCommand] = [
89        TodoCycle(), PriorityUp(), PriorityDown(), PromoteHeading(), DemoteHeading(),
90    ]
91
92    public static func command(_ id: String) -> (any OrgCommand)? {
93        all.first { $0.id == id }
94    }
95}