krz/orgstar

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

Commit 424b251694

424b251694c057659290bdc45000cd085f341417

parent: d92c7f39e4

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-08 03:49 UTC

Read #+STARTUP from keyword lines only

The parser collects #+STARTUP words outside blocks into
OrgSettings.startup; logging, inline images and folding read them
there. A #+STARTUP line inside a block no longer sets logging or
inline images.

Layout: unified · split

Sources/OrgCore/Commands/InlineImages.swift +3 −5
@@ -34,11 +34,9 @@ public enum InlineImages {
3434 /// `#+STARTUP: inlineimages` or `noinlineimages`, if the file says.
3535 public static func startup(_ tree: OrgTree) -> Bool? {
3636 var shown: Bool?
37 for m in (tree.settings.setupKeywords + tree.root.text).matches(of: #/(?mi)^[ \t]*#\+STARTUP:(.*)$/#) {
38 for word in m.1.split(whereSeparator: \.isWhitespace) {
39 if word.lowercased() == "inlineimages" { shown = true }
40 if word.lowercased() == "noinlineimages" { shown = false }
41 }
37 for word in tree.settings.startup {
38 if word.lowercased() == "inlineimages" { shown = true }
39 if word.lowercased() == "noinlineimages" { shown = false }
4240 }
4341 return shown
4442 }
Sources/OrgCore/Commands/TodoLogging.swift +1 −9
@@ -82,15 +82,7 @@ extension EmacsBuffer {
8282 if enter != nil || leave != nil, log.states[keyword.name] == nil { log.states[keyword.name] = (enter, leave) }
8383 }
8484 }
85 for (key, value) in SetupFile.keywords(settings.setupKeywords) where key == "STARTUP" {
86 for word in value.split(whereSeparator: \.isWhitespace) { log.apply(startup: String(word)) }
87 }
88 let startup = try! NSRegularExpression(pattern: "^[ \\t]*#\\+STARTUP:(.*)$", options: [.anchorsMatchLines, .caseInsensitive])
89 for match in startup.matches(in: string, range: NSRange(location: 0, length: length)) {
90 for word in substring(match.range(at: 1).location, NSMaxRange(match.range(at: 1))).split(whereSeparator: \.isWhitespace) {
91 log.apply(startup: String(word))
92 }
93 }
85 for word in settings.startup { log.apply(startup: word) }
9486 if let logging = inheritedProperty("LOGGING")?.value {
9587 // `org-local-logging`.
9688 log.done = nil
Sources/OrgCore/Parser/Incremental.swift +1 −1
@@ -58,7 +58,7 @@ private struct ReparseContext {
5858 /// Change in UTF-16 length.
5959 var delta: Int { edit.replacement.utf16.count - edit.range.count }
6060
61 static let settingsKeys: Set<String> = ["TODO", "SEQ_TODO", "TYP_TODO", "PRIORITIES"]
61 static let settingsKeys: Set<String> = ["TODO", "SEQ_TODO", "TYP_TODO", "PRIORITIES", "STARTUP"]
6262
6363 /// Elements that parse the same in isolation as in place, as long as their lines keep
6464 /// their classes.
Sources/OrgCore/Parser/Settings.swift +7 −1
@@ -50,6 +50,8 @@ public struct OrgSettings: Sendable, Equatable {
5050 /// Keyword lines from the file's `#+SETUPFILE`s, as `#+KEY: value` lines: they count as
5151 /// the file's own, before them.
5252 public var setupKeywords = ""
53 /// Words of the `#+STARTUP` keyword lines outside blocks, setup files' first.
54 public var startup: [String] = []
5355
5456 public init(todoSequences: [TodoSequence], priorities: Priorities, radioTargets: [String] = []) {
5557 self.todoSequences = todoSequences
@@ -82,12 +84,13 @@ public struct OrgSettings: Sendable, Equatable {
8284}
8385
8486enum SettingsScanner {
85 /// Reads `#+TODO`, `#+SEQ_TODO`, `#+TYP_TODO` and `#+PRIORITIES` outside blocks. Any TODO
87 /// Reads `#+TODO`, `#+SEQ_TODO`, `#+TYP_TODO`, `#+PRIORITIES` and `#+STARTUP` outside blocks. Any TODO
8688 /// line replaces the default sequences; as in org, `TYP_TODO` sequences come first, then
8789 /// `TODO`, then `SEQ_TODO`.
8890 static func scan(lines: [RawLine], info: [ClassifiedLine], blockEnds: [Int: Int], defaults: OrgSettings) -> OrgSettings {
8991 var types: [TodoSequence] = [], todos: [TodoSequence] = [], seqs: [TodoSequence] = []
9092 var priorities = defaults.priorities
93 var startup: [String] = []
9194 func apply(_ key: String, _ value: Substring) {
9295 switch key {
9396 case "TODO":
@@ -96,6 +99,8 @@ enum SettingsScanner {
9699 if let s = todoSequence(value, kind: .sequence) { seqs.append(s) }
97100 case "TYP_TODO":
98101 if let s = todoSequence(value, kind: .type) { types.append(s) }
102 case "STARTUP":
103 startup += value.split(whereSeparator: \.isWhitespace).map(String.init)
99104 case "PRIORITIES":
100105 let words = value.split(whereSeparator: \.isWhitespace)
101106 if words.count == 3 {
@@ -129,6 +134,7 @@ enum SettingsScanner {
129134 radioTargets: radio.sorted { $0.count > $1.count })
130135 settings.listAllowAlphabetical = defaults.listAllowAlphabetical
131136 settings.setupKeywords = defaults.setupKeywords
137 settings.startup = startup
132138 return settings
133139 }
134140
Sources/OrgPresentation/Folding.swift +3 −22
@@ -217,28 +217,9 @@ extension Presentation {
217217 return result
218218 }
219219
220 /// Words of every `#+STARTUP:` line. Only element-level keywords count, so text inside
221 /// blocks and paragraphs is never read as a setting.
220 /// Words of every `#+STARTUP:` keyword line (`OrgSettings.startup`); lines inside blocks
221 /// are not settings.
222222 public static func startupOptions(_ tree: OrgTree) -> [String] {
223 var options = tree.settings.setupKeywords.split(separator: "\n").filter { $0.uppercased().hasPrefix("#+STARTUP:") }
224 .flatMap { $0.dropFirst("#+STARTUP:".count).split(whereSeparator: \.isWhitespace).map(String.init) }
225 func visit(_ node: GreenNode) {
226 for case .node(let child) in node.children {
227 switch child.kind {
228 case .keyword:
229 let text = child.text
230 if text.uppercased().drop(while: { $0 == " " || $0 == "\t" }).hasPrefix("#+STARTUP:"),
231 let colon = text.firstIndex(of: ":") {
232 options += text[text.index(after: colon)...].split(whereSeparator: \.isWhitespace).map(String.init)
233 }
234 case .document, .zerothSection, .section, .drawer:
235 visit(child)
236 default:
237 break
238 }
239 }
240 }
241 visit(tree.green)
242 return options
223 tree.settings.startup
243224 }
244225}
Tests/OrgCoreTests/InlineImagesTests.swift +1
@@ -12,5 +12,6 @@ struct InlineImagesTests {
1212 #expect(InlineImages.startup(OrgParser.parse(text as String)) == true)
1313 #expect(InlineImages.startup(OrgParser.parse("#+STARTUP: overview noinlineimages\n")) == false)
1414 #expect(InlineImages.startup(OrgParser.parse("* x\n")) == nil)
15 #expect(InlineImages.startup(OrgParser.parse("#+begin_src org\n#+STARTUP: inlineimages\n#+end_src\n")) == nil)
1516 }
1617}
Tests/OrgCoreTests/TodoLoggingTests.swift +1
@@ -9,6 +9,7 @@ struct TodoLoggingTests {
99 "* TODO Task\n:LOGBOOK:\n- State \"TODO\" from [2026-01-01 Thu 10:00]\n:END:\n",
1010 "* DONE Task\nCLOSED: [2026-01-01 Thu 10:00]\n",
1111 "#+STARTUP: lognotedone logdrawer\n* TODO Task\n",
12 "#+begin_example\n#+STARTUP: lognotedone logdrawer\n#+end_example\n* TODO Task\n",
1213 "* TODO Task\n:PROPERTIES:\n:LOGGING: DONE(!) logrepeat\n:LOG_INTO_DRAWER: NOTES\n:END:\n",
1314 "* TODO Task\nSCHEDULED: <2026-10-05 Mon +1w>\n",
1415 "* TODO Task\n- item one\n",
Tests/OrgPresentationTests/PresentationTests.swift +1
@@ -128,6 +128,7 @@ struct CyclingTests {
128128 let state = Presentation.startupFoldState(levels)
129129 #expect(state?.folds.count == 1 && state?.bodyFolds.count == 2)
130130 #expect(Presentation.startupOptions(OrgParser.parse("#+begin_example\n#+STARTUP: overview\n#+end_example\n")).isEmpty)
131 #expect(Presentation.startupOptions(OrgParser.parse("#+STARTUP: content\n* a\n:LOGBOOK:\n#+startup: hideblocks\n:END:\n")) == ["content", "hideblocks"])
131132 }
132133}
133134