krz/orgstar

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

Sources/OrgCore/Syntax/Wrappers.swift

16086b4cf2caff5328774b2cd5ae3ffe1ab65ca4
orgstar/Sources/OrgCore/Syntax/Wrappers.swift history · blame · raw

77 lines · 3295 bytes

 1/// Drawers and blocks, the elements org folds on their own (`org-fold-hide-drawer-toggle`,
 2/// `org-fold-hide-block-toggle`).
 3public struct Wrapper: Sendable, Equatable {
 4    public enum Kind: Sendable, Equatable { case drawer, block }
 5
 6    public let kind: Kind
 7    /// Start of the opening line (`:PROPERTIES:`, `#+begin_src`).
 8    public let start: Int
 9    /// Start of the closing line (`:END:`, `#+end_src`).
10    public let closeLine: Int
11    /// What folding hides: every line after the opening one, through the closing line.
12    public let hidden: Range<Int>
13}
14
15public enum Wrappers {
16    static func kind(_ kind: SyntaxKind) -> Wrapper.Kind? {
17        switch kind {
18        case .drawer, .propertyDrawer: .drawer
19        case .block, .dynamicBlock: .block
20        default: nil
21        }
22    }
23
24    static func wrapper(_ node: SyntaxNode) -> Wrapper? {
25        guard let kind = kind(node.kind) else { return nil }
26        let units = Array(node.text.utf16)
27        // Blank lines after the element belong to it in the tree, not to the fold.
28        var end = units.count
29        while end > 0, [9, 10, 13, 32].contains(units[end - 1]) { end -= 1 }
30        guard let firstNewline = units.firstIndex(of: 10), firstNewline < end else { return nil }
31        var closeStart = end
32        while closeStart > 0, units[closeStart - 1] != 10 { closeStart -= 1 }
33        let hiddenEnd = end < units.count ? end + 1 : end
34        return Wrapper(kind: kind, start: node.range.lowerBound, closeLine: node.range.lowerBound + closeStart,
35                       hidden: (node.range.lowerBound + firstNewline + 1)..<(node.range.lowerBound + hiddenEnd))
36    }
37
38    /// Elements a drawer or block can sit in.
39    static let containers: Set<SyntaxKind> = [.document, .zerothSection, .section, .drawer, .block, .dynamicBlock, .plainList, .item, .footnoteDefinition]
40
41    /// Every drawer and block, in document order, nested ones included.
42    public static func all(_ tree: OrgTree) -> [Wrapper] {
43        var result: [Wrapper] = []
44        func walk(_ node: SyntaxNode) {
45            for child in node.children {
46                if let wrapper = wrapper(child) { result.append(wrapper) }
47                if containers.contains(child.kind) { walk(child) }
48            }
49        }
50        walk(tree.root)
51        return result
52    }
53
54    /// The drawer or block whose opening line starts at `offset`.
55    public static func starting(at offset: Int, in tree: OrgTree) -> Wrapper? {
56        var node = tree.root
57        while let child = node.child(containing: offset) {
58            if child.range.lowerBound == offset, let wrapper = wrapper(child) { return wrapper }
59            node = child
60        }
61        return nil
62    }
63
64    /// The innermost drawer or block whose opening or closing line holds `offset`, the lines
65    /// TAB toggles it from.
66    public static func toggleable(at offset: Int, in tree: OrgTree) -> Wrapper? {
67        var node = tree.root
68        var found: Wrapper?
69        while let child = node.child(containing: offset) {
70            if let wrapper = wrapper(child), offset < wrapper.hidden.lowerBound || (wrapper.closeLine..<wrapper.hidden.upperBound).contains(offset) {
71                found = wrapper
72            }
73            node = child
74        }
75        return found
76    }
77}