krz/orgstar

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

Sources/OrgDocument/ViewState.swift

0271e942ff3d2f72a407b3d9a6463fab8e4b48cf
orgstar/Sources/OrgDocument/ViewState.swift history · blame · raw

52 lines · 2165 bytes

 1import OrgCore
 2
 3/// Per-window state for one document: selection and folds, in UTF-16 offsets.
 4public struct ViewState: Sendable, Equatable {
 5    public var selection: [Range<Int>]
 6    /// Start offsets of folded headings.
 7    public var folds: Set<Int>
 8
 9    public init(selection: [Range<Int>] = [0..<0], folds: Set<Int> = []) {
10        self.selection = selection
11        self.folds = folds
12    }
13
14    /// Maps through non-overlapping edits given in old coordinates.
15    public func mapped(through edits: [TextEdit]) -> ViewState {
16        let sorted = edits.sorted { $0.range.lowerBound < $1.range.lowerBound }
17        return ViewState(
18            selection: selection.map { mapOffset($0.lowerBound, sorted)..<mapOffset($0.upperBound, sorted) },
19            folds: Set(folds.map { mapOffset($0, sorted) })
20        )
21    }
22
23    /// Drops folds that no longer sit at the start of a heading.
24    public func pruned(to tree: OrgTree) -> ViewState {
25        guard !folds.isEmpty else { return self }
26        return ViewState(selection: selection, folds: folds.filter { isHeadingStart($0, tree.root) })
27    }
28}
29
30/// Descends only through the sections containing `offset`.
31func isHeadingStart(_ offset: Int, _ root: SyntaxNode) -> Bool {
32    var node = root
33    while let child = node.child(containing: offset) {
34        if child.kind == .heading { return child.range.lowerBound == offset }
35        guard child.kind == .section || child.kind == .zerothSection else { return false }
36        node = child
37    }
38    return false
39}
40
41/// An offset before an edit stays put; one at the start of a replaced range stays at its start;
42/// one inside it, at its end, or at an insertion point moves past the replacement.
43func mapOffset(_ offset: Int, _ sortedEdits: [TextEdit]) -> Int {
44    var shift = 0
45    for edit in sortedEdits {
46        let lower = edit.range.lowerBound, upper = edit.range.upperBound
47        if offset < lower || (offset == lower && upper > lower) { return offset + shift }
48        if offset <= upper { return lower + shift + edit.replacement.utf16.count }
49        shift += edit.replacement.utf16.count - edit.range.count
50    }
51    return offset + shift
52}