Speed keys and org-priority !89

merged merged by cmc on 2026-10-06 04:24 UTC · krz/orgstar:speed-keys into main

12 files changed, +166 −15

Layout: unified · split

Sources/OrgApp/EmacsImport.swift +1 −1
@@ -448,7 +448,7 @@ public struct EmacsImport: Sendable {
448 guard let n = value.integer else { return note("not a number") } 448 guard let n = value.integer else { return note("not a number") }
449 add(name, .integer(n)) 449 add(name, .integer(n))
450 case "org-insert-heading-respect-content", "org-list-allow-alphabetical", "org-hide-emphasis-markers", "org-pretty-entities", 450 case "org-insert-heading-respect-content", "org-list-allow-alphabetical", "org-hide-emphasis-markers", "org-pretty-entities",
451 "org-cycle-hide-drawer-startup", "org-cycle-hide-block-startup": 451 "org-cycle-hide-drawer-startup", "org-cycle-hide-block-startup", "org-use-speed-commands":
452 add(name, .bool(truthy)) 452 add(name, .bool(truthy))
453 case "org-hide-drawer-startup", "org-hide-block-startup": 453 case "org-hide-drawer-startup", "org-hide-block-startup":
454 add(name == "org-hide-drawer-startup" ? "org-cycle-hide-drawer-startup" : "org-cycle-hide-block-startup", .bool(truthy)) 454 add(name == "org-hide-drawer-startup" ? "org-cycle-hide-drawer-startup" : "org-cycle-hide-block-startup", .bool(truthy))
Sources/OrgApp/UserConfig.swift +2
@@ -82,6 +82,8 @@ public enum UserConfig {
82 fallback: .string("nil"), comment: "nil, time or note: log changing or removing a DEADLINE", legacy: ""), 82 fallback: .string("nil"), comment: "nil, time or note: log changing or removing a DEADLINE", legacy: ""),
83 Setting(section: "", key: "org-log-into-drawer", defaultsKey: "logIntoDrawer", kind: .string, fallback: .string(""), 83 Setting(section: "", key: "org-log-into-drawer", defaultsKey: "logIntoDrawer", kind: .string, fallback: .string(""),
84 comment: "the drawer state notes go in (\"LOGBOOK\" is Emacs's t); \"\" for none", legacy: ""), 84 comment: "the drawer state notes go in (\"LOGBOOK\" is Emacs's t); \"\" for none", legacy: ""),
85 Setting(section: "", key: "org-use-speed-commands", defaultsKey: "useSpeedCommands", kind: .bool, fallback: .bool(false),
86 comment: "single keys at the start of a heading line run commands (n, p, t, c, …)", legacy: ""),
85 Setting(section: "", key: "org-cycle-hide-drawer-startup", defaultsKey: "hideDrawerStartup", kind: .bool, fallback: .bool(true), 87 Setting(section: "", key: "org-cycle-hide-drawer-startup", defaultsKey: "hideDrawerStartup", kind: .bool, fallback: .bool(true),
86 comment: "fold drawers when a file opens; #+STARTUP: nohidedrawers overrides", legacy: ""), 88 comment: "fold drawers when a file opens; #+STARTUP: nohidedrawers overrides", legacy: ""),
87 Setting(section: "", key: "org-cycle-hide-block-startup", defaultsKey: "hideBlockStartup", kind: .bool, fallback: .bool(false), 89 Setting(section: "", key: "org-cycle-hide-block-startup", defaultsKey: "hideBlockStartup", kind: .bool, fallback: .bool(false),
Sources/OrgCore/Commands/Command.swift +6 −2
@@ -22,6 +22,8 @@ public struct EditingOptions: Sendable, Equatable, Codable {
22 public var logRedeadline: 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. 23 /// `org-use-fast-todo-selection` `auto`: with keys in the keywords, C-c C-t asks for one.
24 public var fastTodoSelection: Bool 24 public var fastTodoSelection: Bool
25 /// `org-use-speed-commands`: single keys at the start of a heading line run commands.
26 public var useSpeedCommands: Bool
25 /// `org-cycle-hide-drawer-startup`: drawers are folded when a file opens. 27 /// `org-cycle-hide-drawer-startup`: drawers are folded when a file opens.
26 public var hideDrawerStartup: Bool 28 public var hideDrawerStartup: Bool
27 /// `org-cycle-hide-block-startup`: blocks are folded when a file opens. 29 /// `org-cycle-hide-block-startup`: blocks are folded when a file opens.
@@ -40,8 +42,10 @@ public struct EditingOptions: Sendable, Equatable, Codable {
40 listAllowAlphabetical: Bool = false, logRepeat: Bool = true, fillColumn: Int = 70, 42 listAllowAlphabetical: Bool = false, logRepeat: Bool = true, fillColumn: Int = 70,
41 hideEmphasisMarkers: Bool = false, prettyEntities: Bool = false, subSuperscriptsNeedBraces: Bool = false, 43 hideEmphasisMarkers: Bool = false, prettyEntities: Bool = false, subSuperscriptsNeedBraces: Bool = false,
42 logDone: LogHow? = nil, logIntoDrawer: String? = nil, fastTodoSelection: Bool = true, 44 logDone: LogHow? = nil, logIntoDrawer: String? = nil, fastTodoSelection: Bool = true,
43 hideDrawerStartup: Bool = true, hideBlockStartup: Bool = false, logReschedule: LogHow? = nil, logRedeadline: LogHow? = nil 45 hideDrawerStartup: Bool = true, hideBlockStartup: Bool = false, logReschedule: LogHow? = nil, logRedeadline: LogHow? = nil,
46 useSpeedCommands: Bool = false
44 ) { 47 ) {
48 self.useSpeedCommands = useSpeedCommands
45 self.logReschedule = logReschedule 49 self.logReschedule = logReschedule
46 self.logRedeadline = logRedeadline 50 self.logRedeadline = logRedeadline
47 self.hideDrawerStartup = hideDrawerStartup 51 self.hideDrawerStartup = hideDrawerStartup
@@ -235,7 +239,7 @@ public enum Commands {
235 ShiftTimestamp(up: true), ShiftTimestamp(up: false), ShiftTimestampDay(later: true), ShiftTimestampDay(later: false), 239 ShiftTimestamp(up: true), ShiftTimestamp(up: false), ShiftTimestampDay(later: true), ShiftTimestampDay(later: false),
236 OpenLinkAtPoint(), CreateID(), ToggleComment(), ToggleHeading(), ToggleItem(), MarkSubtree(), 240 OpenLinkAtPoint(), CreateID(), ToggleComment(), ToggleHeading(), ToggleItem(), MarkSubtree(),
237 CopySubtree(cut: false), CopySubtree(cut: true), CloneSubtree(), SortEntries(), InsertStructureTemplate(), EditSpecial(), 241 CopySubtree(cut: false), CopySubtree(cut: true), CloneSubtree(), SortEntries(), InsertStructureTemplate(), EditSpecial(),
238 UpdateDynamicBlock(), ClockReportTable(), SetEffort(), CtrlCCtrlC(), EvaluateTimeRange(), FootnoteAction(), FootnoteAction(special: true), TableEvalFormula(named: false), TableEvalFormula(named: true), SortTableLines(), TransposeTable(), GotoHeading(), SparseTreeCommand(), 242 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: " "),
239 ] 243 ]
240 244
241 public static var everything: [any OrgCommand] { all + TableCommand.Action.allCases.map(TableCommand.init) } 245 public static var everything: [any OrgCommand] { all + TableCommand.Action.allCases.map(TableCommand.init) }
Sources/OrgCore/Commands/HeadingCommands.swift +62 −2
@@ -296,8 +296,12 @@ struct PriorityChange {
296 } else { 296 } else {
297 new = Self.value(priorities.default) 297 new = Self.value(priorities.default)
298 } 298 }
299 let cookie = new.map { "[#\(Self.label($0, numeric: numeric))]" } 299 return Self.write(new.map { "[#\(Self.label($0, numeric: numeric))]" }, line: line, context: context)
300 return line.commit(context) { buffer in 300 }
301
302 /// Puts `cookie` on the heading, or with nil removes it.
303 static func write(_ cookie: String?, line: HeadingLine, context: EditContext) -> CommandStep {
304 line.commit(context) { buffer in
301 if let existing = line.priority { 305 if let existing = line.priority {
302 let range = line.local(existing.range) 306 let range = line.local(existing.range)
303 if let cookie { 307 if let cookie {
@@ -331,6 +335,62 @@ struct PriorityChange {
331 } 335 }
332} 336}
333 337
338/// C-c ,: `org-priority` with `set`, asking for the priority (SPC removes it); with `value`,
339/// that priority, or with " " none, as the speed keys 1, 2, 3 and 0 do.
340public struct SetPriority: OrgCommand {
341 let value: Character?
342 public init(value: Character? = nil) { self.value = value }
343 public var id: String {
344 switch value {
345 case nil: "org.priority.set"
346 case " ": "org.priority.remove"
347 case let v?: "org.priority.set-\(v.lowercased())"
348 }
349 }
350 public var title: String {
351 switch value {
352 case nil: "Set Priority…"
353 case " ": "Remove Priority"
354 case let v?: "Set Priority \(v)"
355 }
356 }
357 public func applies(in context: EditContext) -> Bool { headingOnLine(at: context.caret, in: context.tree) != nil }
358
359 public func run(in context: EditContext) -> CommandStep {
360 guard let heading = headingOnLine(at: context.caret, in: context.tree) else { return .failed("Not on a heading") }
361 let priorities = context.tree.settings.priorities
362 let numeric = Int(priorities.lowest) != nil
363 let answer: String
364 if let value {
365 answer = String(value)
366 } else if let given = context.answers["priority"] {
367 answer = given
368 } else {
369 let message = "Priority \(priorities.highest)-\(priorities.lowest), SPC to remove:"
370 if numeric, (Int(priorities.lowest) ?? 0) >= 10 { return .prompt(Prompt(key: "priority", message: message)) }
371 guard let high = PriorityChange.value(priorities.highest), let low = PriorityChange.value(priorities.lowest), high <= low else {
372 return .failed("Unsupported priority range")
373 }
374 let items: [FastSelection.Item] = (high...low).map { v in
375 let label = PriorityChange.label(v, numeric: numeric)
376 return .option(key: Character(label.lowercased()), name: label)
377 } + [.option(key: " ", name: "remove")]
378 return .prompt(Prompt(key: "priority", message: message, fast: FastSelection(items: items, multiple: false)))
379 }
380 let line = HeadingLine(heading)
381 if answer == " " {
382 guard line.priority != nil else { return .failed("No priority cookie found in line") }
383 return PriorityChange.write(nil, line: line, context: context)
384 }
385 let new = numeric ? Int(answer.trimmingCharacters(in: .whitespaces)) : PriorityChange.value(answer.uppercased())
386 guard let new, let high = PriorityChange.value(priorities.highest), let low = PriorityChange.value(priorities.lowest), new >= high, new <= low else {
387 return .failed(numeric ? "Priority must be a number between `\(priorities.highest)' and `\(priorities.lowest)'"
388 : "Priority must be between `\(priorities.highest)' and `\(priorities.lowest)'")
389 }
390 return PriorityChange.write("[#\(PriorityChange.label(new, numeric: numeric))]", line: line, context: context)
391 }
392}
393
334public struct PriorityUp: OrgCommand { 394public struct PriorityUp: OrgCommand {
335 public init() {} 395 public init() {}
336 public var id: String { "org.priority.up" } 396 public var id: String { "org.priority.up" }
Sources/OrgCore/Keymap/Keymap.swift +10 −2
@@ -101,7 +101,8 @@ public struct Keymap: Sendable, Equatable {
101 public func keyLabels(for command: String, modes: [String?] = [nil], limit: Int = 2) -> [String] { 101 public func keyLabels(for command: String, modes: [String?] = [nil], limit: Int = 2) -> [String] {
102 var found: [(keys: [KeyChord], rank: [Int])] = [] 102 var found: [(keys: [KeyChord], rank: [Int])] = []
103 for (order, mode) in modes.enumerated() { 103 for (order, mode) in modes.enumerated() {
104 for binding in bindings.reversed() where binding.mode == mode && binding.command == command && isLive(binding) { 104 // Speed keys are labels for nothing: they act only at a heading's start.
105 for binding in bindings.reversed() where binding.mode == mode && binding.command == command && isLive(binding) && binding.when != "speed" {
105 found.append((binding.keys, [order, binding.when == nil ? 0 : 1, binding.keys.count, found.count])) 106 found.append((binding.keys, [order, binding.when == nil ? 0 : 1, binding.keys.count, found.count]))
106 } 107 }
107 } 108 }
@@ -130,7 +131,7 @@ public struct Keymap: Sendable, Equatable {
130 131
131/// Context names a binding's `when` can use. 132/// Context names a binding's `when` can use.
132public enum KeyContext { 133public enum KeyContext {
133 public static let names: Set<String> = ["heading", "table", "item", "region", "timestamp", "tblfm", "src", "fold-line", "dblock"] 134 public static let names: Set<String> = ["heading", "table", "item", "region", "timestamp", "tblfm", "src", "fold-line", "dblock", "speed"]
134 135
135 public static func holds(_ name: String, in context: EditContext) -> Bool { 136 public static func holds(_ name: String, in context: EditContext) -> Bool {
136 switch name { 137 switch name {
@@ -147,6 +148,13 @@ public enum KeyContext {
147 case "dblock": 148 case "dblock":
148 let buffer = EmacsBuffer(context.text, point: context.caret, settings: context.tree.settings) 149 let buffer = EmacsBuffer(context.text, point: context.caret, settings: context.tree.settings)
149 return buffer.lookingAtLine(EmacsBuffer.dblockStart) 150 return buffer.lookingAtLine(EmacsBuffer.dblockStart)
151 // `org-use-speed-commands`: at the very start of a heading line.
152 case "speed":
153 guard context.options.useSpeedCommands, context.selection.allSatisfy(\.isEmpty) else { return false }
154 let ns = context.text as NSString
155 let caret = context.caret
156 guard caret == 0 || (caret <= ns.length && ns.character(at: caret - 1) == 10) else { return false }
157 return ns.substring(from: caret).range(of: "^\\*+ ", options: .regularExpression) != nil
150 default: return false 158 default: return false
151 } 159 }
152 } 160 }
Sources/OrgCore/Keymap/Presets.swift +30 −3
@@ -22,12 +22,32 @@ public enum KeymapPreset: String, Sendable, CaseIterable {
22 22
23 public var toml: String { 23 public var toml: String {
24 switch self { 24 switch self {
25 case .emacs: Self.common + Self.emacsBindings 25 case .emacs: Self.common + Self.speedBindings(mode: nil) + Self.emacsBindings
26 case .mac: Self.common + Self.macBindings 26 case .mac: Self.common + Self.speedBindings(mode: nil) + Self.macBindings
27 case .doom: Self.doomToml 27 case .doom: Self.doomToml + Self.speedBindings(mode: "insert")
28 } 28 }
29 } 29 }
30 30
31 /// `org-speed-commands`, for the start of a heading line when `org-use-speed-commands` is on.
32 static let speedCommands: [(key: String, command: String)] = [
33 ("n", "org.heading.next"), ("p", "org.heading.previous"), ("f", "org.heading.forward-same-level"),
34 ("b", "org.heading.backward-same-level"), ("u", "org.heading.up"), ("j", "org.goto"),
35 ("c", "org.cycle"), ("C", "org.cycle-global"), ("s", "org.narrow.toggle"), ("k", "org.subtree.cut"),
36 ("U", "org.subtree.up"), ("D", "org.subtree.down"), ("r", "org.heading.demote"), ("l", "org.heading.promote"),
37 ("R", "org.subtree.demote"), ("L", "org.subtree.promote"), ("i", "org.heading.insert-after-subtree"),
38 ("^", "org.sort"), ("w", "app.refile"), ("a", "app.archive"), ("@", "org.subtree.mark"),
39 ("#", "org.heading.toggle-comment"), ("I", "app.clock.in"), ("O", "app.clock.out"), ("t", "org.todo.cycle"),
40 (",", "org.priority.set"), ("0", "org.priority.remove"), ("1", "org.priority.set-a"), ("2", "org.priority.set-b"),
41 ("3", "org.priority.set-c"), (":", "org.tags.set"), ("e", "org.effort.set"), ("v", "app.agenda"),
42 ("/", "org.sparse-tree"), ("o", "org.link.open"),
43 ]
44
45 static func speedBindings(mode: String?) -> String {
46 speedCommands.map { key, command in
47 "[[bind]]\nkeys = \"\(key)\"\ncommand = \"\(command)\"\nwhen = \"speed\"\n" + (mode.map { "mode = \"\($0)\"\n" } ?? "")
48 }.joined()
49 }
50
31 /// Doom: the Emacs preset's org bindings in every evil state (as org-mode-map's are), with 51 /// Doom: the Emacs preset's org bindings in every evil state (as org-mode-map's are), with
32 /// evil-org's `M-h/j/k/l` forms, then Doom's leader, localleader and evil-org keys. 52 /// evil-org's `M-h/j/k/l` forms, then Doom's leader, localleader and evil-org keys.
33 static var doomToml: String { 53 static var doomToml: String {
@@ -346,6 +366,10 @@ public enum KeymapPreset: String, Sendable, CaseIterable {
346 command = "org.effort.set" 366 command = "org.effort.set"
347 mode = "normal" 367 mode = "normal"
348 [[bind]] 368 [[bind]]
369 keys = "SPC m p p"
370 command = "org.priority.set"
371 mode = "normal"
372 [[bind]]
349 keys = "SPC m p u" 373 keys = "SPC m p u"
350 command = "org.priority.up" 374 command = "org.priority.up"
351 mode = "normal" 375 mode = "normal"
@@ -682,6 +706,9 @@ public enum KeymapPreset: String, Sendable, CaseIterable {
682 keys = "C-c /" 706 keys = "C-c /"
683 command = "org.sparse-tree" 707 command = "org.sparse-tree"
684 [[bind]] 708 [[bind]]
709 keys = "C-c ,"
710 command = "org.priority.set"
711 [[bind]]
685 keys = "C-c C-x C-r" 712 keys = "C-c C-x C-r"
686 command = "org.clock.report" 713 command = "org.clock.report"
687 [[bind]] 714 [[bind]]
Sources/OrgEditorAppKit/EditorCommands.swift +1
@@ -40,6 +40,7 @@ public enum EditorCommands {
40 EditorCommand(id: "org.narrow.subtree", title: "Narrow to Subtree", action: .view), 40 EditorCommand(id: "org.narrow.subtree", title: "Narrow to Subtree", action: .view),
41 EditorCommand(id: "org.narrow.block", title: "Narrow to Block", action: .view), 41 EditorCommand(id: "org.narrow.block", title: "Narrow to Block", action: .view),
42 EditorCommand(id: "org.widen", title: "Widen", action: .view), 42 EditorCommand(id: "org.widen", title: "Widen", action: .view),
43 EditorCommand(id: "org.narrow.toggle", title: "Narrow to Subtree or Widen", action: .view),
43 EditorCommand(id: "org.table.toggle-column-width", title: "Shrink or Expand Table Column", action: .view), 44 EditorCommand(id: "org.table.toggle-column-width", title: "Shrink or Expand Table Column", action: .view),
44 EditorCommand(id: "org.table.shrink", title: "Shrink Table Columns with Widths", action: .view), 45 EditorCommand(id: "org.table.shrink", title: "Shrink Table Columns with Widths", action: .view),
45 EditorCommand(id: "org.table.expand", title: "Expand Table Columns", action: .view), 46 EditorCommand(id: "org.table.expand", title: "Expand Table Columns", action: .view),
Sources/OrgEditorAppKit/OrgEditor.swift +3 −1
@@ -405,7 +405,7 @@ public final class OrgEditor: NSObject {
405 switch id { 405 switch id {
406 case "org.cycle": cycleAtCaret() 406 case "org.cycle": cycleAtCaret()
407 case "org.cycle-global": cycleGlobal() 407 case "org.cycle-global": cycleGlobal()
408 case "org.narrow.subtree", "org.narrow.block", "org.widen": narrow(id) 408 case "org.narrow.subtree", "org.narrow.block", "org.narrow.toggle", "org.widen": narrow(id)
409 default: tableColumnWidth(id) 409 default: tableColumnWidth(id)
410 } 410 }
411 case .edit(let selector, let extending): 411 case .edit(let selector, let extending):
@@ -615,6 +615,8 @@ public final class OrgEditor: NSObject {
615 do { 615 do {
616 switch id { 616 switch id {
617 case "org.narrow.subtree": narrowed = try Narrowing.subtree(at: caret, in: textView.string) 617 case "org.narrow.subtree": narrowed = try Narrowing.subtree(at: caret, in: textView.string)
618 // `org-toggle-narrow-to-subtree`.
619 case "org.narrow.toggle": narrowed = narrowed == nil ? try Narrowing.subtree(at: caret, in: textView.string) : nil
618 case "org.narrow.block": narrowed = try Narrowing.block(at: caret, in: textView.string) 620 case "org.narrow.block": narrowed = try Narrowing.block(at: caret, in: textView.string)
619 default: narrowed = nil 621 default: narrowed = nil
620 } 622 }
Sources/Orgstar/ContentView.swift +3 −1
@@ -38,6 +38,7 @@ struct ContentView: View {
38 @AppStorage("logIntoDrawer") private var logIntoDrawer = "" 38 @AppStorage("logIntoDrawer") private var logIntoDrawer = ""
39 @AppStorage("logReschedule") private var logReschedule = "nil" 39 @AppStorage("logReschedule") private var logReschedule = "nil"
40 @AppStorage("logRedeadline") private var logRedeadline = "nil" 40 @AppStorage("logRedeadline") private var logRedeadline = "nil"
41 @AppStorage("useSpeedCommands") private var useSpeedCommands = false
41 @AppStorage("hideDrawerStartup") private var hideDrawerStartup = true 42 @AppStorage("hideDrawerStartup") private var hideDrawerStartup = true
42 @AppStorage("hideBlockStartup") private var hideBlockStartup = false 43 @AppStorage("hideBlockStartup") private var hideBlockStartup = false
43 44
@@ -45,7 +46,8 @@ struct ContentView: View {
45 Preferences.options( 46 Preferences.options(
46 tagsColumn: tagsColumn, headingAfterSubtree: headingAfterSubtree, splitLine: splitLine, alphabeticalLists: alphabeticalLists, 47 tagsColumn: tagsColumn, headingAfterSubtree: headingAfterSubtree, splitLine: splitLine, alphabeticalLists: alphabeticalLists,
47 fillColumn: fillColumn, hiddenMarkers: hiddenMarkers, prettyEntities: prettyEntities, logDone: logDone, logIntoDrawer: logIntoDrawer, 48 fillColumn: fillColumn, hiddenMarkers: hiddenMarkers, prettyEntities: prettyEntities, logDone: logDone, logIntoDrawer: logIntoDrawer,
48 hideDrawerStartup: hideDrawerStartup, hideBlockStartup: hideBlockStartup, logReschedule: logReschedule, logRedeadline: logRedeadline 49 hideDrawerStartup: hideDrawerStartup, hideBlockStartup: hideBlockStartup, logReschedule: logReschedule, logRedeadline: logRedeadline,
50 useSpeedCommands: useSpeedCommands
49 ) 51 )
50 } 52 }
51 /// A block being edited apart (`org-edit-special`). 53 /// A block being edited apart (`org-edit-special`).
Sources/Orgstar/Preferences.swift +3 −2
@@ -18,7 +18,7 @@ enum Preferences {
18 static func options( 18 static func options(
19 tagsColumn: Int, headingAfterSubtree: Bool, splitLine: Bool, alphabeticalLists: Bool, fillColumn: Int, 19 tagsColumn: Int, headingAfterSubtree: Bool, splitLine: Bool, alphabeticalLists: Bool, fillColumn: Int,
20 hiddenMarkers: Bool, prettyEntities: Bool, logDone: String, logIntoDrawer: String, 20 hiddenMarkers: Bool, prettyEntities: Bool, logDone: String, logIntoDrawer: String,
21 hideDrawerStartup: Bool, hideBlockStartup: Bool, logReschedule: String, logRedeadline: String 21 hideDrawerStartup: Bool, hideBlockStartup: Bool, logReschedule: String, logRedeadline: String, useSpeedCommands: Bool
22 ) -> EditingOptions { 22 ) -> EditingOptions {
23 EditingOptions( 23 EditingOptions(
24 tagsColumn: tagsColumn, insertHeadingRespectContent: headingAfterSubtree, 24 tagsColumn: tagsColumn, insertHeadingRespectContent: headingAfterSubtree,
@@ -26,7 +26,8 @@ enum Preferences {
26 hideEmphasisMarkers: hiddenMarkers, prettyEntities: prettyEntities, subSuperscriptsNeedBraces: true, 26 hideEmphasisMarkers: hiddenMarkers, prettyEntities: prettyEntities, subSuperscriptsNeedBraces: true,
27 logDone: LogHow(rawValue: logDone), logIntoDrawer: logIntoDrawer.isEmpty ? nil : logIntoDrawer, 27 logDone: LogHow(rawValue: logDone), logIntoDrawer: logIntoDrawer.isEmpty ? nil : logIntoDrawer,
28 hideDrawerStartup: hideDrawerStartup, hideBlockStartup: hideBlockStartup, 28 hideDrawerStartup: hideDrawerStartup, hideBlockStartup: hideBlockStartup,
29 logReschedule: LogHow(rawValue: logReschedule), logRedeadline: LogHow(rawValue: logRedeadline) 29 logReschedule: LogHow(rawValue: logReschedule), logRedeadline: LogHow(rawValue: logRedeadline),
30 useSpeedCommands: useSpeedCommands
30 ) 31 )
31 } 32 }
32} 33}
Tests/OrgCoreTests/KeymapTests.swift +1 −1
@@ -135,7 +135,7 @@ struct KeymapTests {
135 #expect(dispatcher.feed(z) == .undefined([c, z])) 135 #expect(dispatcher.feed(z) == .undefined([c, z]))
136 #expect(dispatcher.feed(c) == .pending([c])) 136 #expect(dispatcher.feed(c) == .pending([c]))
137 #expect(dispatcher.feed(KeyDispatcher.quit) == .cancelled) 137 #expect(dispatcher.feed(KeyDispatcher.quit) == .cancelled)
138 #expect(dispatcher.feed(KeyChord("a")) == .unbound) 138 #expect(dispatcher.feed(KeyChord("z")) == .unbound)
139 } 139 }
140 140
141 @Test func continuationsListNextKeys() { 141 @Test func continuationsListNextKeys() {
Tests/OrgCoreTests/PrioritySetTests.swift added +44
@@ -0,0 +1,44 @@
1import Foundation
2import Testing
3@testable import OrgCore
4
5struct PrioritySetTests {
6 static let oracle = ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil
7
8 @Test(.enabled(if: oracle))
9 func setsPrioritiesLikeOrg() throws {
10 let texts = ["* TODO Task :tag:\nbody\n", "* [#B] Plain\n", "* DONE [#A] Done one :x:y:\n", "* Heading\n"]
11 // Org reads SPC and lower case only at its prompt.
12 let values: [(Character, String)] = [("A", "(org-priority ?A)"), ("C", "(org-priority ?C)"),
13 (" ", "(cl-letf (((symbol-function 'read-char-exclusive) (lambda (&rest _) ?\\s))) (org-priority))"),
14 ("b", "(cl-letf (((symbol-function 'read-char-exclusive) (lambda (&rest _) ?b))) (org-priority))")]
15 var cases: [EmacsOracle.Case] = []
16 var ours: [(String, String?)] = []
17 for text in texts {
18 for (value, form) in values {
19 cases.append(EmacsOracle.Case(text: text, point: 3, form: form))
20 let result = runCommand(SetPriority(), text, caret: 2, answers: ["priority": String(value)])
21 ours.append((result.text, result.failure))
22 }
23 cases.append(EmacsOracle.Case(text: text, point: 3, form: "(org-priority ?D)"))
24 let result = runCommand(SetPriority(), text, caret: 2, answers: ["priority": "D"])
25 ours.append((result.text, result.failure))
26 }
27 for (i, theirs) in try EmacsOracle.run(cases).enumerated() {
28 #expect(ours[i].0 == theirs.text, "\(cases[i].form) on \(cases[i].text.debugDescription)")
29 #expect((ours[i].1 != nil) == !theirs.error.isEmpty, "\(cases[i].form): \(ours[i].1 ?? "") vs \(theirs.error)")
30 }
31 }
32
33 @Test func speedKeysHoldAtHeadingStarts() {
34 let text = "* One\nbody\n** Two\n"
35 func holds(_ caret: Int, _ on: Bool) -> Bool {
36 KeyContext.holds("speed", in: EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [caret..<caret], options: EditingOptions(useSpeedCommands: on)))
37 }
38 #expect(holds(0, true))
39 #expect(holds(11, true))
40 #expect(!holds(1, true))
41 #expect(!holds(6, true))
42 #expect(!holds(0, false))
43 }
44}