#+STARTUP indent, hidestars, align; VISIBILITY property !98
9 files changed, +139 −6
Layout: unified · split
Sources/OrgApp/EmacsImport.swift +2 −1
| @@ -448,7 +448,8 @@ public struct EmacsImport: Sendable { | ||
| 448 | 448 | guard let n = value.integer else { return note("not a number") } |
| 449 | 449 | add(name, .integer(n)) |
| 450 | 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 | 453 | add(name, .bool(truthy)) |
| 453 | 454 | case "org-hide-drawer-startup", "org-hide-block-startup": |
| 454 | 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 | 82 | fallback: .string("nil"), comment: "nil, time or note: log changing or removing a DEADLINE", legacy: ""), |
| 83 | 83 | Setting(section: "", key: "org-log-into-drawer", defaultsKey: "logIntoDrawer", kind: .string, fallback: .string(""), |
| 84 | 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 | 91 | Setting(section: "", key: "org-startup-with-inline-images", defaultsKey: "startupWithInlineImages", kind: .bool, fallback: .bool(false), |
| 86 | 92 | comment: "show image links as images when a file opens; #+STARTUP: (no)inlineimages overrides", legacy: ""), |
| 87 | 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 | 26 | public var useSpeedCommands: Bool |
| 27 | 27 | /// `org-startup-with-inline-images`. |
| 28 | 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 | 35 | /// `org-cycle-hide-drawer-startup`: drawers are folded when a file opens. |
| 30 | 36 | public var hideDrawerStartup: Bool |
| 31 | 37 | /// `org-cycle-hide-block-startup`: blocks are folded when a file opens. |
| @@ -45,8 +51,12 @@ public struct EditingOptions: Sendable, Equatable, Codable { | ||
| 45 | 51 | hideEmphasisMarkers: Bool = false, prettyEntities: Bool = false, subSuperscriptsNeedBraces: Bool = false, |
| 46 | 52 | logDone: LogHow? = nil, logIntoDrawer: String? = nil, fastTodoSelection: Bool = true, |
| 47 | 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 | 60 | self.useSpeedCommands = useSpeedCommands |
| 51 | 61 | self.startupWithInlineImages = startupWithInlineImages |
| 52 | 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. | |
| 624 | public 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 | 648 | /// C-c |: `org-table-create` with a size prompt. |
| 624 | 649 | public struct CreateTable: OrgCommand { |
| 625 | 650 | public init() {} |
Sources/OrgEditorAppKit/OrgEditor.swift +20 −1
| @@ -146,12 +146,23 @@ public final class OrgEditor: NSObject { | ||
| 146 | 146 | // Files open at their start, as in Emacs; replacing the storage leaves it at the end. |
| 147 | 147 | textView.setSelectedRange(NSRange(location: 0, length: 0)) |
| 148 | 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 | 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 | 157 | let wrappers = Presentation.startupWrappers(document.tree, hideDrawers: options.hideDrawerStartup, hideBlocks: options.hideBlockStartup) |
| 151 | 158 | startup.wrapperFolds = Set(wrappers.map(\.start)) |
| 152 | 159 | if startup != FoldState() { setFolds(startup, wrappers: wrappers) } |
| 153 | 160 | shrinkAtStartup() |
| 154 | 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 | 631 | // MARK: - Inline images |
| 614 | 632 | |
| 615 | 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 | 956 | return |
| 939 | 957 | } |
| 940 | 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 | 960 | guard let clipped = clip(run.range, to: range) else { continue } |
| 943 | 961 | guard run.firstLine == run.wrapped else { |
| 944 | 962 | storage.addAttribute(.paragraphStyle, value: paragraphStyle(run.firstLine, run.wrapped), range: clipped) |
| @@ -959,6 +977,7 @@ public final class OrgEditor: NSObject { | ||
| 959 | 977 | var scriptExclusions: [Range<Int>] = [] |
| 960 | 978 | for run in Presentation.styles(document.tree, in: range, highlighter: highlighter) { |
| 961 | 979 | guard let clipped = clip(run.range, to: range) else { continue } |
| 980 | if run.role == .leadingStars, !indented, !hideStars { continue } | |
| 962 | 981 | if !showMarkup, hides(run.role), NSIntersectionRange(clipped, revealed).length == 0 { |
| 963 | 982 | storage.addAttributes(theme.hidden, range: clipped) |
| 964 | 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 | 192 | /// Headings of levels 1 through `levels` visible, no bodies. |
| 157 | 193 | public static func foldState(showingLevels levels: Int, in tree: OrgTree) -> FoldState { |
| 158 | 194 | let outline = outline(tree) |
Sources/Orgstar/ContentView.swift +5 −1
| @@ -40,6 +40,9 @@ struct ContentView: View { | ||
| 40 | 40 | @AppStorage("logRedeadline") private var logRedeadline = "nil" |
| 41 | 41 | @AppStorage("useSpeedCommands") private var useSpeedCommands = false |
| 42 | 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 | 46 | @AppStorage("hideDrawerStartup") private var hideDrawerStartup = true |
| 44 | 47 | @AppStorage("hideBlockStartup") private var hideBlockStartup = false |
| 45 | 48 | |
| @@ -48,7 +51,8 @@ struct ContentView: View { | ||
| 48 | 51 | tagsColumn: tagsColumn, headingAfterSubtree: headingAfterSubtree, splitLine: splitLine, alphabeticalLists: alphabeticalLists, |
| 49 | 52 | fillColumn: fillColumn, hiddenMarkers: hiddenMarkers, prettyEntities: prettyEntities, logDone: logDone, logIntoDrawer: logIntoDrawer, |
| 50 | 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 | 58 | /// A block being edited apart (`org-edit-special`). |
Sources/Orgstar/Preferences.swift +4 −2
| @@ -20,7 +20,8 @@ enum Preferences { | ||
| 20 | 20 | static func options( |
| 21 | 21 | tagsColumn: Int, headingAfterSubtree: Bool, splitLine: Bool, alphabeticalLists: Bool, fillColumn: Int, |
| 22 | 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 | 25 | ) -> EditingOptions { |
| 25 | 26 | EditingOptions( |
| 26 | 27 | tagsColumn: tagsColumn, insertHeadingRespectContent: headingAfterSubtree, |
| @@ -29,7 +30,8 @@ enum Preferences { | ||
| 29 | 30 | logDone: LogHow(rawValue: logDone), logIntoDrawer: logIntoDrawer.isEmpty ? nil : logIntoDrawer, |
| 30 | 31 | hideDrawerStartup: hideDrawerStartup, hideBlockStartup: hideBlockStartup, |
| 31 | 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 @@ | ||
| 1 | import Foundation | |
| 2 | import Testing | |
| 3 | import OrgCore | |
| 4 | @testable import OrgPresentation | |
| 5 | ||
| 6 | struct 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 | } | |