krz/orgstar

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

Sources/OrgEditorAppKit/LineIndex.swift

2fa201330f61c801f777baa3f2943d49d8758cda
orgstar/Sources/OrgEditorAppKit/LineIndex.swift history · blame · raw

72 lines · 2607 bytes

 1#if os(macOS)
 2import Foundation
 3
 4/// Where each line of the text starts (UTF-16), kept up to date edit by edit, so line numbers
 5/// come from a search instead of counting newlines from the top.
 6final class LineIndex {
 7    /// Sorted; the first line starts at 0.
 8    private(set) var starts: [Int] = [0]
 9
10    init(_ text: NSString = "") { reset(text) }
11
12    func reset(_ text: NSString) {
13        starts = [0]
14        appendStarts(in: text, from: 0, to: text.length)
15    }
16
17    private func appendStarts(in text: NSString, from: Int, to end: Int) {
18        var location = from
19        while location < end {
20            let found = text.range(of: "\n", options: .literal, range: NSRange(location: location, length: end - location))
21            guard found.location != NSNotFound else { break }
22            starts.append(found.location + 1)
23            location = found.location + 1
24        }
25    }
26
27    var lineCount: Int { starts.count }
28
29    /// The line (from 1) holding `offset`.
30    func line(of offset: Int) -> Int {
31        var low = 0
32        var high = starts.count
33        while low < high {
34            let mid = (low + high) / 2
35            if starts[mid] <= offset { low = mid + 1 } else { high = mid }
36        }
37        return max(1, low)
38    }
39
40    /// `old` (in the text before) became `newLength` characters of `text` (the text after).
41    func replace(_ old: Range<Int>, newLength: Int, in text: NSString) {
42        let delta = newLength - old.count
43        // Starts after a newline inside the old range go; starts after it move.
44        let first = firstIndex(after: old.lowerBound)
45        let last = firstIndex(after: old.upperBound)
46        var inserted: [Int] = []
47        var location = old.lowerBound
48        let end = old.lowerBound + newLength
49        while location < end {
50            let found = text.range(of: "\n", options: .literal, range: NSRange(location: location, length: end - location))
51            guard found.location != NSNotFound else { break }
52            inserted.append(found.location + 1)
53            location = found.location + 1
54        }
55        if delta != 0 {
56            for i in last..<starts.count { starts[i] += delta }
57        }
58        starts.replaceSubrange(first..<last, with: inserted)
59    }
60
61    /// The index of the first start greater than `offset`.
62    private func firstIndex(after offset: Int) -> Int {
63        var low = 0
64        var high = starts.count
65        while low < high {
66            let mid = (low + high) / 2
67            if starts[mid] <= offset { low = mid + 1 } else { high = mid }
68        }
69        return low
70    }
71}
72#endif