#+STARTUP indent, hidestars, align; VISIBILITY property !98

merged merged by cmc on 2026-10-06 05:42 UTC · krz/orgstar:startup-options into main

9 files changed, +139 −6

Layout: unified · split

Sources/OrgApp/EmacsImport.swift +2 −1
@@ -448,7 +448,8 @@ 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", "org-use-speed-commands", "org-startup-with-inline-images": 451 "org-cycle-hide-drawer-startup", "org-cycle-hide-block-startup", "org-use-speed-commands", "org-startup-with-inline-images",
452 "org-startup-indented", "org-hide-leading-stars", "org-startup-align-all-tables":
452 add(name, .bool(truthy)) 453 add(name, .bool(truthy))
453 case "org-hide-drawer-startup", "org-hide-block-startup": 454 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)) 455 add(name == "org-hide-drawer-startup" ? "org-cycle-hide-drawer-startup" : "org-cycle-hide-block-startup", .bool(truthy))
Sources/OrgApp/UserConfig.swift +6
@@ -82,6 +82,12 @@ 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-startup-indented", defaultsKey: "startupIndented", kind: .bool, fallback: .bool(true),
86 comment: "indent bodies under their headings (org-indent-mode); #+STARTUP: (no)indent overrides", legacy: ""),
87 Setting(section: "", key: "org-hide-leading-stars", defaultsKey: "hideLeadingStars", kind: .bool, fallback: .bool(false),
88 comment: "show only a heading's last star without indentation; #+STARTUP: hidestars/showstars overrides", legacy: ""),
89 Setting(section: "", key: "org-startup-align-all-tables", defaultsKey: "startupAlignAllTables", kind: .bool, fallback: .bool(false),
90 comment: "align every table when a file opens; #+STARTUP: (no)align overrides", legacy: ""),
85 Setting(section: "", key: "org-startup-with-inline-images", defaultsKey: "startupWithInlineImages", kind: .bool, fallback: .bool(false), 91 Setting(section: "", key: "org-startup-with-inline-images", defaultsKey: "startupWithInlineImages", kind: .bool, fallback: .bool(false),
86 comment: "show image links as images when a file opens; #+STARTUP: (no)inlineimages overrides", legacy: ""), 92 comment: "show image links as images when a file opens; #+STARTUP: (no)inlineimages overrides", legacy: ""),
87 Setting(section: "", key: "org-use-speed-commands", defaultsKey: "useSpeedCommands", kind: .bool, fallback: .bool(false), 93 Setting(section: "", key: "org-use-speed-commands", defaultsKey: "useSpeedCommands", kind: .bool, fallback: .bool(false),
Sources/OrgCore/Commands/Command.swift +11 −1
@@ -26,6 +26,12 @@ public struct EditingOptions: Sendable, Equatable, Codable {
26 public var useSpeedCommands: Bool 26 public var useSpeedCommands: Bool
27 /// `org-startup-with-inline-images`. 27 /// `org-startup-with-inline-images`.
28 public var startupWithInlineImages: Bool 28 public var startupWithInlineImages: Bool
29 /// `org-startup-indented`: org-indent's virtual indentation.
30 public var startupIndented = true
31 /// `org-hide-leading-stars`; org-indent hides them too.
32 public var hideLeadingStars = false
33 /// `org-startup-align-all-tables`.
34 public var startupAlignAllTables = false
29 /// `org-cycle-hide-drawer-startup`: drawers are folded when a file opens. 35 /// `org-cycle-hide-drawer-startup`: drawers are folded when a file opens.
30 public var hideDrawerStartup: Bool 36 public var hideDrawerStartup: Bool
31 /// `org-cycle-hide-block-startup`: blocks are folded when a file opens. 37 /// `org-cycle-hide-block-startup`: blocks are folded when a file opens.
@@ -45,8 +51,12 @@ public struct EditingOptions: Sendable, Equatable, Codable {
45 hideEmphasisMarkers: Bool = false, prettyEntities: Bool = false, subSuperscriptsNeedBraces: Bool = false, 51 hideEmphasisMarkers: Bool = false, prettyEntities: Bool = false, subSuperscriptsNeedBraces: Bool = false,
46 logDone: LogHow? = nil, logIntoDrawer: String? = nil, fastTodoSelection: Bool = true, 52 logDone: LogHow? = nil, logIntoDrawer: String? = nil, fastTodoSelection: Bool = true,
47 hideDrawerStartup: Bool = true, hideBlockStartup: Bool = false, logReschedule: LogHow? = nil, logRedeadline: LogHow? = nil, 53 hideDrawerStartup: Bool = true, hideBlockStartup: Bool = false, logReschedule: LogHow? = nil, logRedeadline: LogHow? = nil,
48 useSpeedCommands: Bool = false, startupWithInlineImages: Bool = false 54 useSpeedCommands: Bool = false, startupWithInlineImages: Bool = false,
55 startupIndented: Bool = true, hideLeadingStars: Bool = false, startupAlignAllTables: Bool = false
49 ) { 56 ) {
57 self.startupIndented = startupIndented
58 self.hideLeadingStars = hideLeadingStars
59 self.startupAlignAllTables = startupAlignAllTables
50 self.useSpeedCommands = useSpeedCommands 60 self.useSpeedCommands = useSpeedCommands
51 self.startupWithInlineImages = startupWithInlineImages 61 self.startupWithInlineImages = startupWithInlineImages
52 self.logReschedule = logReschedule 62 self.logReschedule = logReschedule
Sources/OrgCore/Commands/Table.swift +25
@@ -620,6 +620,31 @@ public struct TableCommand: OrgCommand {
620 } 620 }
621} 621}
622 622
623/// `#+STARTUP: align` (`org-startup-align-all-tables`): every table aligned.
624public struct AlignAllTables: OrgCommand {
625 public init() {}
626 public var id: String { "org.table.align-all" }
627 public var title: String { "Align All Tables" }
628 public func applies(in context: EditContext) -> Bool { true }
629 public func run(in context: EditContext) -> CommandStep {
630 let starts = context.tree.root.descendants().filter { $0.kind == .table }.map(\.range.lowerBound)
631 guard !starts.isEmpty else { return .commit(EditResult(baseRevision: context.revision, edits: [])) }
632 return commitBuffer(context) { buffer in
633 let caret = buffer.marker(buffer.point)
634 defer {
635 buffer.point = caret.position
636 buffer.release(caret)
637 }
638 for start in starts.reversed() {
639 buffer.point = start
640 buffer.skipForward([32, 9])
641 buffer.point += 1
642 buffer.tableAlign()
643 }
644 }
645 }
646}
647
623/// C-c |: `org-table-create` with a size prompt. 648/// C-c |: `org-table-create` with a size prompt.
624public struct CreateTable: OrgCommand { 649public struct CreateTable: OrgCommand {
625 public init() {} 650 public init() {}
Sources/OrgEditorAppKit/OrgEditor.swift +20 −1
@@ -146,12 +146,23 @@ public final class OrgEditor: NSObject {
146 // Files open at their start, as in Emacs; replacing the storage leaves it at the end. 146 // Files open at their start, as in Emacs; replacing the storage leaves it at the end.
147 textView.setSelectedRange(NSRange(location: 0, length: 0)) 147 textView.setSelectedRange(NSRange(location: 0, length: 0))
148 if isOrg { 148 if isOrg {
149 let words = Presentation.startupOptions(document.tree).map { $0.lowercased() }
150 indented = words.contains("indent") ? true : words.contains("noindent") ? false : options.startupIndented
151 hideStars = words.contains("hidestars") ? true : words.contains("showstars") ? false : options.hideLeadingStars
149 var startup = Presentation.startupFoldState(document.tree) ?? FoldState() 152 var startup = Presentation.startupFoldState(document.tree) ?? FoldState()
153 // VISIBILITY properties and archived trees, unless everything shows.
154 if Presentation.startupFoldState(document.tree) != nil, !words.contains("showeverything") {
155 startup = Presentation.applyVisibilityProperties(startup, in: document.tree)
156 }
150 let wrappers = Presentation.startupWrappers(document.tree, hideDrawers: options.hideDrawerStartup, hideBlocks: options.hideBlockStartup) 157 let wrappers = Presentation.startupWrappers(document.tree, hideDrawers: options.hideDrawerStartup, hideBlocks: options.hideBlockStartup)
151 startup.wrapperFolds = Set(wrappers.map(\.start)) 158 startup.wrapperFolds = Set(wrappers.map(\.start))
152 if startup != FoldState() { setFolds(startup, wrappers: wrappers) } 159 if startup != FoldState() { setFolds(startup, wrappers: wrappers) }
153 shrinkAtStartup() 160 shrinkAtStartup()
154 if InlineImages.startup(document.tree) ?? options.startupWithInlineImages { showsImages = true } 161 if InlineImages.startup(document.tree) ?? options.startupWithInlineImages { showsImages = true }
162 if words.contains("align") || (options.startupAlignAllTables && !words.contains("noalign")) {
163 _ = perform(AlignAllTables())
164 }
165 if !indented || !hideStars { restyleOutsideEditing(0..<utf16Length) }
155 } 166 }
156 } 167 }
157 168
@@ -610,6 +621,13 @@ public final class OrgEditor: NSObject {
610 } 621 }
611 } 622 }
612 623
624 // MARK: - STARTUP display
625
626 /// `org-indent-mode`: headings' bodies indented under their titles (`#+STARTUP: indent`).
627 private var indented = true
628 /// `org-hide-leading-stars` (`#+STARTUP: hidestars`); org-indent hides them as well.
629 private var hideStars = false
630
613 // MARK: - Inline images 631 // MARK: - Inline images
614 632
615 /// `org-toggle-inline-images`: lines that are only an image link show the image. 633 /// `org-toggle-inline-images`: lines that are only an image link show the image.
@@ -938,7 +956,7 @@ public final class OrgEditor: NSObject {
938 return 956 return
939 } 957 }
940 let text = storage.string as NSString 958 let text = storage.string as NSString
941 for run in Presentation.indents(document.tree, in: range) { 959 for run in Presentation.indents(document.tree, in: range) where indented {
942 guard let clipped = clip(run.range, to: range) else { continue } 960 guard let clipped = clip(run.range, to: range) else { continue }
943 guard run.firstLine == run.wrapped else { 961 guard run.firstLine == run.wrapped else {
944 storage.addAttribute(.paragraphStyle, value: paragraphStyle(run.firstLine, run.wrapped), range: clipped) 962 storage.addAttribute(.paragraphStyle, value: paragraphStyle(run.firstLine, run.wrapped), range: clipped)
@@ -959,6 +977,7 @@ public final class OrgEditor: NSObject {
959 var scriptExclusions: [Range<Int>] = [] 977 var scriptExclusions: [Range<Int>] = []
960 for run in Presentation.styles(document.tree, in: range, highlighter: highlighter) { 978 for run in Presentation.styles(document.tree, in: range, highlighter: highlighter) {
961 guard let clipped = clip(run.range, to: range) else { continue } 979 guard let clipped = clip(run.range, to: range) else { continue }
980 if run.role == .leadingStars, !indented, !hideStars { continue }
962 if !showMarkup, hides(run.role), NSIntersectionRange(clipped, revealed).length == 0 { 981 if !showMarkup, hides(run.role), NSIntersectionRange(clipped, revealed).length == 0 {
963 storage.addAttributes(theme.hidden, range: clipped) 982 storage.addAttributes(theme.hidden, range: clipped)
964 } else { 983 } else {
Sources/OrgPresentation/Folding.swift +36
@@ -153,6 +153,42 @@ extension Presentation {
153 } 153 }
154 } 154 }
155 155
156 /// `org-cycle-set-visibility-according-to-property` and `org-cycle-hide-archived-subtrees`
157 /// after the startup visibility: each heading's `VISIBILITY` (`folded`, `children`, `content`,
158 /// `all`) in turn, then ARCHIVE-tagged subtrees folded.
159 public static func applyVisibilityProperties(_ state: FoldState, in tree: OrgTree) -> FoldState {
160 let outline = outline(tree)
161 let model = DocumentModel(tree: tree)
162 var next = state
163 for (index, heading) in model.headings.enumerated() {
164 guard index < outline.count,
165 let value = heading.properties.last(where: { $0.key.uppercased() == "VISIBILITY" })?.value.lowercased() else { continue }
166 let entry = outline[index]
167 let descendants = outline.filter { $0.start > entry.start && $0.start < entry.end }.map(\.start)
168 let children = outline.enumerated().filter { $0.element.parent == index }.map(\.element.start)
169 switch value {
170 case "folded":
171 next.folds.insert(entry.start)
172 case "children":
173 next.folds.remove(entry.start)
174 next.bodyFolds.remove(entry.start)
175 next.folds.formUnion(children)
176 case "content":
177 next.folds.subtract([entry.start] + descendants)
178 next.bodyFolds.formUnion([entry.start] + descendants)
179 case "all", "showall":
180 next.folds.subtract([entry.start] + descendants)
181 next.bodyFolds.subtract([entry.start] + descendants)
182 default:
183 break
184 }
185 }
186 for (index, heading) in model.headings.enumerated() where index < outline.count && heading.localTags.contains("ARCHIVE") {
187 next.folds.insert(outline[index].start)
188 }
189 return next
190 }
191
156 /// Headings of levels 1 through `levels` visible, no bodies. 192 /// Headings of levels 1 through `levels` visible, no bodies.
157 public static func foldState(showingLevels levels: Int, in tree: OrgTree) -> FoldState { 193 public static func foldState(showingLevels levels: Int, in tree: OrgTree) -> FoldState {
158 let outline = outline(tree) 194 let outline = outline(tree)
Sources/Orgstar/ContentView.swift +5 −1
@@ -40,6 +40,9 @@ struct ContentView: View {
40 @AppStorage("logRedeadline") private var logRedeadline = "nil" 40 @AppStorage("logRedeadline") private var logRedeadline = "nil"
41 @AppStorage("useSpeedCommands") private var useSpeedCommands = false 41 @AppStorage("useSpeedCommands") private var useSpeedCommands = false
42 @AppStorage("startupWithInlineImages") private var startupWithInlineImages = false 42 @AppStorage("startupWithInlineImages") private var startupWithInlineImages = false
43 @AppStorage("startupIndented") private var startupIndented = true
44 @AppStorage("hideLeadingStars") private var hideLeadingStars = false
45 @AppStorage("startupAlignAllTables") private var startupAlignAllTables = false
43 @AppStorage("hideDrawerStartup") private var hideDrawerStartup = true 46 @AppStorage("hideDrawerStartup") private var hideDrawerStartup = true
44 @AppStorage("hideBlockStartup") private var hideBlockStartup = false 47 @AppStorage("hideBlockStartup") private var hideBlockStartup = false
45 48
@@ -48,7 +51,8 @@ struct ContentView: View {
48 tagsColumn: tagsColumn, headingAfterSubtree: headingAfterSubtree, splitLine: splitLine, alphabeticalLists: alphabeticalLists, 51 tagsColumn: tagsColumn, headingAfterSubtree: headingAfterSubtree, splitLine: splitLine, alphabeticalLists: alphabeticalLists,
49 fillColumn: fillColumn, hiddenMarkers: hiddenMarkers, prettyEntities: prettyEntities, logDone: logDone, logIntoDrawer: logIntoDrawer, 52 fillColumn: fillColumn, hiddenMarkers: hiddenMarkers, prettyEntities: prettyEntities, logDone: logDone, logIntoDrawer: logIntoDrawer,
50 hideDrawerStartup: hideDrawerStartup, hideBlockStartup: hideBlockStartup, logReschedule: logReschedule, logRedeadline: logRedeadline, 53 hideDrawerStartup: hideDrawerStartup, hideBlockStartup: hideBlockStartup, logReschedule: logReschedule, logRedeadline: logRedeadline,
51 useSpeedCommands: useSpeedCommands, startupWithInlineImages: startupWithInlineImages 54 useSpeedCommands: useSpeedCommands, startupWithInlineImages: startupWithInlineImages,
55 startupIndented: startupIndented, hideLeadingStars: hideLeadingStars, startupAlignAllTables: startupAlignAllTables
52 ) 56 )
53 } 57 }
54 /// A block being edited apart (`org-edit-special`). 58 /// A block being edited apart (`org-edit-special`).
Sources/Orgstar/Preferences.swift +4 −2
@@ -20,7 +20,8 @@ enum Preferences {
20 static func options( 20 static func options(
21 tagsColumn: Int, headingAfterSubtree: Bool, splitLine: Bool, alphabeticalLists: Bool, fillColumn: Int, 21 tagsColumn: Int, headingAfterSubtree: Bool, splitLine: Bool, alphabeticalLists: Bool, fillColumn: Int,
22 hiddenMarkers: Bool, prettyEntities: Bool, logDone: String, logIntoDrawer: String, 22 hiddenMarkers: Bool, prettyEntities: Bool, logDone: String, logIntoDrawer: String,
23 hideDrawerStartup: Bool, hideBlockStartup: Bool, logReschedule: String, logRedeadline: String, useSpeedCommands: Bool, startupWithInlineImages: Bool 23 hideDrawerStartup: Bool, hideBlockStartup: Bool, logReschedule: String, logRedeadline: String, useSpeedCommands: Bool, startupWithInlineImages: Bool,
24 startupIndented: Bool, hideLeadingStars: Bool, startupAlignAllTables: Bool
24 ) -> EditingOptions { 25 ) -> EditingOptions {
25 EditingOptions( 26 EditingOptions(
26 tagsColumn: tagsColumn, insertHeadingRespectContent: headingAfterSubtree, 27 tagsColumn: tagsColumn, insertHeadingRespectContent: headingAfterSubtree,
@@ -29,7 +30,8 @@ enum Preferences {
29 logDone: LogHow(rawValue: logDone), logIntoDrawer: logIntoDrawer.isEmpty ? nil : logIntoDrawer, 30 logDone: LogHow(rawValue: logDone), logIntoDrawer: logIntoDrawer.isEmpty ? nil : logIntoDrawer,
30 hideDrawerStartup: hideDrawerStartup, hideBlockStartup: hideBlockStartup, 31 hideDrawerStartup: hideDrawerStartup, hideBlockStartup: hideBlockStartup,
31 logReschedule: LogHow(rawValue: logReschedule), logRedeadline: LogHow(rawValue: logRedeadline), 32 logReschedule: LogHow(rawValue: logReschedule), logRedeadline: LogHow(rawValue: logRedeadline),
32 useSpeedCommands: useSpeedCommands, startupWithInlineImages: startupWithInlineImages 33 useSpeedCommands: useSpeedCommands, startupWithInlineImages: startupWithInlineImages,
34 startupIndented: startupIndented, hideLeadingStars: hideLeadingStars, startupAlignAllTables: startupAlignAllTables
33 ) 35 )
34 } 36 }
35} 37}
Tests/OrgPresentationTests/VisibilityPropertyTests.swift added +30
@@ -0,0 +1,30 @@
1import Foundation
2import Testing
3import OrgCore
4@testable import OrgPresentation
5
6struct VisibilityPropertyTests {
7 static let text = "#+STARTUP: overview\n* A\n:PROPERTIES:\n:VISIBILITY: children\n:END:\nbody a\n** A1\na1 body\n*** A11\n** A2\n* B\n:PROPERTIES:\n:VISIBILITY: content\n:END:\nbody b\n** B1\nb1 body\n*** B11\nb11\n* C\n:PROPERTIES:\n:VISIBILITY: all\n:END:\nbody c\n** C1\nc1\n* D :ARCHIVE:\n** D1\n* E\n:PROPERTIES:\n:VISIBILITY: folded\n:END:\n** E1\n"
8
9 /// Line visibility after opening the file, as Emacs 31.1 / Org 9.8.7 shows it, except that
10 /// Emacs leaves B's `:PROPERTIES:` line (line 12) showing under `content`.
11 static let emacs = "111001100110000101011001111010000"
12
13 @Test func foldsByVisibilityProperties() {
14 let tree = OrgParser.parse(Self.text)
15 var state = Presentation.applyVisibilityProperties(Presentation.startupFoldState(tree)!, in: tree)
16 let wrappers = Presentation.startupWrappers(tree)
17 state.wrapperFolds = Set(wrappers.map(\.start))
18 let hidden = Presentation.hiddenRanges(tree, folds: state.folds, bodyFolds: state.bodyFolds, wrapperRanges: wrappers.map(\.hidden))
19 let ns = Self.text as NSString
20 var visibility = ""
21 var offset = 0
22 while offset < ns.length {
23 visibility += hidden.contains { $0.contains(offset) } ? "0" : "1"
24 offset = NSMaxRange(ns.lineRange(for: NSRange(location: offset, length: 0)))
25 }
26 var expected = Array(Self.emacs)
27 expected[11] = "0"
28 #expect(visibility == String(expected))
29 }
30}