| @@ -55,6 +55,15 @@ public final class OrgEditor: NSObject { |
| 55 | let theme = Theme() |
55 | let theme = Theme() |
| 56 | private var isLoading = false |
56 | private var isLoading = false |
| 57 | private var previousSelection = 0 |
57 | private var previousSelection = 0 |
| |
58 | /// Text before this offset is styled; after it, only base attributes until the background |
| |
59 | /// passes reach it. Always at a line start. |
| |
60 | private(set) var styledUpTo = 0 |
| |
61 | private var stylingScheduled = false |
| |
62 | |
| |
63 | /// Styled before the first render: more than a screenful. |
| |
64 | static let firstStyleChunk = 30_000 |
| |
65 | /// Styled per run-loop turn afterwards. |
| |
66 | static let styleChunk = 200_000 |
| 58 | |
67 | |
| 59 | public init(document: DocumentState, frame: NSRect = NSRect(x: 0, y: 0, width: 600, height: 800)) { |
68 | public init(document: DocumentState, frame: NSRect = NSRect(x: 0, y: 0, width: 600, height: 800)) { |
| 60 | self.document = document |
69 | self.document = document |
| @@ -77,10 +86,46 @@ public final class OrgEditor: NSObject { |
| 77 | textLayoutManager.delegate = self |
86 | textLayoutManager.delegate = self |
| 78 | textContentStorage.delegate = self |
87 | textContentStorage.delegate = self |
| 79 | |
88 | |
| |
89 | load(document.text) |
| |
90 | } |
| |
91 | |
| |
92 | /// Replaces the storage's text. Setting the attributed string directly is about 100x |
| |
93 | /// faster than `NSTextView.string` for large files. |
| |
94 | private func load(_ text: String) { |
| 80 | isLoading = true |
95 | isLoading = true |
| 81 | textView.string = document.text |
96 | textView.textStorage?.setAttributedString(NSAttributedString(string: text, attributes: theme.base)) |
| 82 | isLoading = false |
97 | isLoading = false |
| 83 | restyleOutsideEditing(0..<utf16Length) |
98 | styledUpTo = 0 |
| |
99 | styleNextChunk(Self.firstStyleChunk) |
| |
100 | scheduleStyling() |
| |
101 | } |
| |
102 | |
| |
103 | // MARK: - Initial styling |
| |
104 | |
| |
105 | /// Styles whole lines from the watermark onward, about `size` units. |
| |
106 | private func styleNextChunk(_ size: Int) { |
| |
107 | let text = textView.textStorage!.string as NSString |
| |
108 | guard styledUpTo < text.length else { return } |
| |
109 | let target = min(text.length, styledUpTo + size) |
| |
110 | let end = target == text.length ? target : NSMaxRange(text.lineRange(for: NSRange(location: target, length: 0))) |
| |
111 | restyleOutsideEditing(styledUpTo..<end) |
| |
112 | styledUpTo = end |
| |
113 | } |
| |
114 | |
| |
115 | private func scheduleStyling() { |
| |
116 | guard !stylingScheduled, styledUpTo < utf16Length else { return } |
| |
117 | stylingScheduled = true |
| |
118 | DispatchQueue.main.async { [weak self] in |
| |
119 | guard let self else { return } |
| |
120 | self.stylingScheduled = false |
| |
121 | self.styleNextChunk(Self.styleChunk) |
| |
122 | self.scheduleStyling() |
| |
123 | } |
| |
124 | } |
| |
125 | |
| |
126 | /// Styles everything still waiting for a background pass. |
| |
127 | public func finishStyling() { |
| |
128 | while styledUpTo < utf16Length { styleNextChunk(Self.styleChunk) } |
| 84 | } |
129 | } |
| 85 | |
130 | |
| 86 | public var textLayoutManager: NSTextLayoutManager { textView.textLayoutManager! } |
131 | public var textLayoutManager: NSTextLayoutManager { textView.textLayoutManager! } |
| @@ -107,10 +152,7 @@ public final class OrgEditor: NSObject { |
| 107 | let outcome = try saver.save(&document, to: url) |
152 | let outcome = try saver.save(&document, to: url) |
| 108 | if document.text != before { |
153 | if document.text != before { |
| 109 | let edits = lineEdits(from: before, to: document.text) |
154 | let edits = lineEdits(from: before, to: document.text) |
| 110 | isLoading = true |
155 | load(document.text) |
| 111 | textView.string = document.text |
| |
| 112 | isLoading = false |
| |
| 113 | restyleOutsideEditing(0..<utf16Length) |
| |
| 114 | setFolds(view.mapped(through: edits).folds) |
156 | setFolds(view.mapped(through: edits).folds) |
| 115 | } |
157 | } |
| 116 | return outcome |
158 | return outcome |
| @@ -190,23 +232,30 @@ public final class OrgEditor: NSObject { |
| 190 | let lineRange = lines.location..<(lines.location + lines.length) |
232 | let lineRange = lines.location..<(lines.location + lines.length) |
| 191 | let around = max(0, lineRange.lowerBound - 1)..<min(text.length, lineRange.upperBound + 1) |
233 | let around = max(0, lineRange.lowerBound - 1)..<min(text.length, lineRange.upperBound + 1) |
| 192 | var container = tree.root |
234 | var container = tree.root |
| 193 | while let child = container.children.first(where: { |
235 | while let child = container.child(containing: lineRange.lowerBound), |
| 194 | ($0.kind == .section || $0.kind == .zerothSection) |
236 | child.kind == .section || child.kind == .zerothSection, |
| 195 | && $0.range.lowerBound <= lineRange.lowerBound && lineRange.upperBound <= $0.range.upperBound |
237 | lineRange.upperBound <= child.range.upperBound { |
| 196 | }) { |
| |
| 197 | container = child |
238 | container = child |
| 198 | } |
239 | } |
| 199 | var lower = lineRange.lowerBound |
240 | var lower = lineRange.lowerBound |
| 200 | var upper = lineRange.upperBound |
241 | var upper = lineRange.upperBound |
| 201 | let children = container.children |
242 | // Tables and lists carry no styling of their own, so inside them only the rows or |
| 202 | for child in children where child.range.overlaps(around) { |
243 | // items around the edit change, however large they are. |
| |
244 | func overlapping(_ nodes: [SyntaxNode]) -> [SyntaxNode] { |
| |
245 | nodes.filter { $0.range.overlaps(around) }.flatMap { node -> [SyntaxNode] in |
| |
246 | guard [.table, .plainList, .item].contains(node.kind) else { return [node] } |
| |
247 | let inner = overlapping(node.children(overlapping: around)) |
| |
248 | return inner.isEmpty ? [node] : inner |
| |
249 | } |
| |
250 | } |
| |
251 | for child in overlapping(container.children(overlapping: around)) { |
| 203 | lower = min(lower, child.range.lowerBound) |
252 | lower = min(lower, child.range.lowerBound) |
| 204 | upper = max(upper, child.range.upperBound) |
253 | upper = max(upper, child.range.upperBound) |
| 205 | } |
254 | } |
| 206 | let heading = children.first { $0.kind == .heading } |
255 | let heading = container.firstChild(.heading) |
| 207 | if touchedHeadings || heading.map({ $0.range.overlaps(lineRange) }) == true { |
256 | if touchedHeadings || heading.map({ $0.range.overlaps(lineRange) }) == true { |
| 208 | lower = min(lower, container.range.lowerBound) |
257 | lower = min(lower, container.range.lowerBound) |
| 209 | upper = max(upper, children.first { $0.kind == .section }?.range.lowerBound ?? container.range.upperBound) |
258 | upper = max(upper, container.firstChild(.section)?.range.lowerBound ?? container.range.upperBound) |
| 210 | } |
259 | } |
| 211 | return lower..<min(upper, text.length) |
260 | return lower..<min(upper, text.length) |
| 212 | } |
261 | } |
| @@ -231,6 +280,13 @@ public final class OrgEditor: NSObject { |
| 231 | assertionFailure("text view and document disagree: \(error)") |
280 | assertionFailure("text view and document disagree: \(error)") |
| 232 | return |
281 | return |
| 233 | } |
282 | } |
| |
283 | // Keep the watermark on unstyled text: shift it past the edit, or pull it back to the |
| |
284 | // edited line when the edit reaches into unstyled text. |
| |
285 | if styledUpTo >= edit.range.upperBound { |
| |
286 | styledUpTo += edit.replacement.utf16.count - edit.range.count |
| |
287 | } else if styledUpTo > edit.range.lowerBound { |
| |
288 | styledUpTo = (storage.string as NSString).lineRange(for: NSRange(location: edit.range.lowerBound, length: 0)).location |
| |
289 | } |
| 234 | let oldHidden = hidden.all |
290 | let oldHidden = hidden.all |
| 235 | view = view.mapped(through: [edit]).pruned(to: document.tree) |
291 | view = view.mapped(through: [edit]).pruned(to: document.tree) |
| 236 | folded.set(view.folds) |
292 | folded.set(view.folds) |