Sources/OrgCore/Commands/Command.swift
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}