Sources/OrgEditorAppKit/LineIndex.swift
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