Sources/OrgCore/Syntax/Wrappers.swift
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}