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