| @@ -55,6 +55,15 @@ public final class OrgEditor: NSObject { |
| 55 | 55 | let theme = Theme() |
| 56 | 56 | private var isLoading = false |
| 57 | 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 | 68 | public init(document: DocumentState, frame: NSRect = NSRect(x: 0, y: 0, width: 600, height: 800)) { |
| 60 | 69 | self.document = document |
| @@ -77,10 +86,46 @@ public final class OrgEditor: NSObject { |
| 77 | 86 | textLayoutManager.delegate = self |
| 78 | 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 | 95 | isLoading = true |
| 81 | | textView.string = document.text |
| 96 | textView.textStorage?.setAttributedString(NSAttributedString(string: text, attributes: theme.base)) |
| 82 | 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 | 131 | public var textLayoutManager: NSTextLayoutManager { textView.textLayoutManager! } |
| @@ -107,10 +152,7 @@ public final class OrgEditor: NSObject { |
| 107 | 152 | let outcome = try saver.save(&document, to: url) |
| 108 | 153 | if document.text != before { |
| 109 | 154 | let edits = lineEdits(from: before, to: document.text) |
| 110 | | isLoading = true |
| 111 | | textView.string = document.text |
| 112 | | isLoading = false |
| 113 | | restyleOutsideEditing(0..<utf16Length) |
| 155 | load(document.text) |
| 114 | 156 | setFolds(view.mapped(through: edits).folds) |
| 115 | 157 | } |
| 116 | 158 | return outcome |
| @@ -190,23 +232,30 @@ public final class OrgEditor: NSObject { |
| 190 | 232 | let lineRange = lines.location..<(lines.location + lines.length) |
| 191 | 233 | let around = max(0, lineRange.lowerBound - 1)..<min(text.length, lineRange.upperBound + 1) |
| 192 | 234 | var container = tree.root |
| 193 | | while let child = container.children.first(where: { |
| 194 | | ($0.kind == .section || $0.kind == .zerothSection) |
| 195 | | && $0.range.lowerBound <= lineRange.lowerBound && lineRange.upperBound <= $0.range.upperBound |
| 196 | | }) { |
| 235 | while let child = container.child(containing: lineRange.lowerBound), |
| 236 | child.kind == .section || child.kind == .zerothSection, |
| 237 | lineRange.upperBound <= child.range.upperBound { |
| 197 | 238 | container = child |
| 198 | 239 | } |
| 199 | 240 | var lower = lineRange.lowerBound |
| 200 | 241 | var upper = lineRange.upperBound |
| 201 | | let children = container.children |
| 202 | | for child in children where child.range.overlaps(around) { |
| 242 | // Tables and lists carry no styling of their own, so inside them only the rows or |
| 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 | 252 | lower = min(lower, child.range.lowerBound) |
| 204 | 253 | upper = max(upper, child.range.upperBound) |
| 205 | 254 | } |
| 206 | | let heading = children.first { $0.kind == .heading } |
| 255 | let heading = container.firstChild(.heading) |
| 207 | 256 | if touchedHeadings || heading.map({ $0.range.overlaps(lineRange) }) == true { |
| 208 | 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 | 260 | return lower..<min(upper, text.length) |
| 212 | 261 | } |
| @@ -231,6 +280,13 @@ public final class OrgEditor: NSObject { |
| 231 | 280 | assertionFailure("text view and document disagree: \(error)") |
| 232 | 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 | 290 | let oldHidden = hidden.all |
| 235 | 291 | view = view.mapped(through: [edit]).pruned(to: document.tree) |
| 236 | 292 | folded.set(view.folds) |