| @@ -22,6 +22,8 @@ public struct TextEdit: Sendable, Equatable { |
| 22 | 22 | enum ReparseStrategy: Equatable { |
| 23 | 23 | /// One element reparsed in place; everything else reused. |
| 24 | 24 | case element |
| 25 | /// A run of elements in one section's body reparsed; everything else reused. |
| 26 | case region |
| 25 | 27 | /// A run of top-level sections reparsed; the sections before and after reused. |
| 26 | 28 | case sections |
| 27 | 29 | case full |
| @@ -41,6 +43,7 @@ extension OrgParser { |
| 41 | 43 | let context = EditContext(oldText: oldText, newText: newText, edit: edit) |
| 42 | 44 | if context.touchesSettings() { return (parse(newText, defaults: defaults), .full) } |
| 43 | 45 | if let tree = context.reparseElement(old) { return (tree, .element) } |
| 46 | if let tree = context.reparseRegion(old) { return (tree, .region) } |
| 44 | 47 | if let tree = context.reparseSections(old) { return (tree, .sections) } |
| 45 | 48 | return (parse(newText, defaults: defaults), .full) |
| 46 | 49 | } |
| @@ -113,10 +116,7 @@ private struct EditContext { |
| 113 | 116 | func leaf(in root: SyntaxNode) -> SyntaxNode? { |
| 114 | 117 | let range = edit.range |
| 115 | 118 | var current = root |
| 116 | | while let child = current.children.first(where: { |
| 117 | | $0.range.lowerBound <= range.lowerBound && range.lowerBound < $0.range.upperBound |
| 118 | | && range.upperBound <= $0.range.upperBound |
| 119 | | }) { |
| 119 | while let child = current.child(containing: range.lowerBound), range.upperBound <= child.range.upperBound { |
| 120 | 120 | if Self.leafKinds.contains(child.kind) { return child } |
| 121 | 121 | current = child |
| 122 | 122 | } |
| @@ -140,6 +140,151 @@ private struct EditContext { |
| 140 | 140 | return replacement |
| 141 | 141 | } |
| 142 | 142 | |
| 143 | // MARK: - Body region |
| 144 | |
| 145 | /// Reparses a window of the innermost section's own body (the elements between its heading |
| 146 | /// lines and its first child section) and splices it into the old section. |
| 147 | /// |
| 148 | /// The window starts one element before the element holding the line before the edit. It ends at the first |
| 149 | /// later element boundary that the reparse reproduces at the same shifted offset. The |
| 150 | /// window is parsed one element past that boundary, so every decision about where an element |
| 151 | /// ends was made with the following line in view; from a reproduced boundary on, elements |
| 152 | /// parse the same as before. Edits that touch heading lines, the first line after a heading |
| 153 | /// (where planning lines and property drawers are only recognized), or that add an end |
| 154 | /// delimiter (which could pair with a begin line before the window) are left to the |
| 155 | /// section strategy. |
| 156 | func reparseRegion(_ old: OrgTree) -> OrgTree? { |
| 157 | let start = lineStart(oldText, edit.range.lowerBound) |
| 158 | let oldLines = classes(slice(oldText, start, lineEnd(oldText, edit.range.upperBound))) |
| 159 | let newLines = classes(slice(newText, start, lineEnd(newText, edit.range.lowerBound + edit.replacement.utf16.count))) |
| 160 | for line in oldLines + newLines { |
| 161 | if case .heading = line.cls { return nil } |
| 162 | } |
| 163 | for line in newLines { |
| 164 | switch line.cls { |
| 165 | case .blockEnd, .dynamicEnd, .drawerEnd: return nil |
| 166 | default: break |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | // The innermost section or zeroth section holding the edited lines. |
| 171 | let damaged = start..<max(lineEnd(oldText, edit.range.upperBound), start + 1) |
| 172 | var container = old.root |
| 173 | while let child = container.child(containing: damaged.lowerBound), |
| 174 | child.kind == .section || child.kind == .zerothSection, |
| 175 | damaged.upperBound <= child.range.upperBound { |
| 176 | container = child |
| 177 | } |
| 178 | guard container.kind == .section || container.kind == .zerothSection else { return nil } |
| 179 | |
| 180 | // Body children with their offsets. |
| 181 | var body: [(element: GreenElement, offset: Int)] = [] |
| 182 | var bodyStart = container.range.lowerBound |
| 183 | var bodyEnd = container.range.upperBound |
| 184 | var offset = container.range.lowerBound |
| 185 | var prefixCount = 0 |
| 186 | for (index, element) in container.green.children.enumerated() { |
| 187 | defer { offset += element.length } |
| 188 | if case .node(let node) = element { |
| 189 | if [.heading, .planning, .propertyDrawer].contains(node.kind), body.isEmpty { |
| 190 | prefixCount = index + 1 |
| 191 | bodyStart = offset + node.length |
| 192 | continue |
| 193 | } |
| 194 | if node.kind == .section { |
| 195 | bodyEnd = offset |
| 196 | break |
| 197 | } |
| 198 | } |
| 199 | body.append((element, offset)) |
| 200 | } |
| 201 | // The first body line of a heading section can become a planning line or drawer. |
| 202 | let firstEditable = container.kind == .section ? lineEnd(oldText, bodyStart) : bodyStart |
| 203 | guard start >= firstEditable, edit.range.upperBound <= bodyEnd, !body.isEmpty else { return nil } |
| 204 | |
| 205 | // Window start: the body element holding the line before the edit, and one more before |
| 206 | // it, which could absorb the window's first line if that line changes meaning (a drawer |
| 207 | // that loses its end turns into paragraph text). |
| 208 | let anchor = max(bodyStart, start - 1) |
| 209 | guard var first = body.lastIndex(where: { $0.offset <= anchor }) else { return nil } |
| 210 | while first > 0, case .token = body[first].element { first -= 1 } |
| 211 | if first > 0 { |
| 212 | first -= 1 |
| 213 | while first > 0, case .token = body[first].element { first -= 1 } |
| 214 | } |
| 215 | let windowStart = body[first].offset |
| 216 | |
| 217 | var next = body.firstIndex { $0.offset > edit.range.upperBound } ?? body.count |
| 218 | var attempts = 0 |
| 219 | while attempts < 8 { |
| 220 | attempts += 1 |
| 221 | // Parse through one node past the candidate boundary. |
| 222 | var verifyEnd = next |
| 223 | while verifyEnd < body.count, case .token = body[verifyEnd].element { verifyEnd += 1 } |
| 224 | let boundary = next < body.count ? body[next].offset : bodyEnd |
| 225 | let oldWindowEnd = verifyEnd < body.count ? body[verifyEnd].offset + body[verifyEnd].element.length : bodyEnd |
| 226 | let rows = reusableRows(body[first..<min(verifyEnd + 1, body.count)].map(\.element)) |
| 227 | guard let window = parseBody(slice(newText, windowStart, oldWindowEnd + delta), settings: old.settings, rows: rows) else { return nil } |
| 228 | if window.unmatchedBegin, oldWindowEnd < bodyEnd { |
| 229 | next = body.count |
| 230 | continue |
| 231 | } |
| 232 | // The reparsed elements up to the boundary, if the reparse has one there. |
| 233 | let target = boundary + delta - windowStart |
| 234 | var reparsed: [GreenElement] = [] |
| 235 | var at = 0 |
| 236 | for element in window.elements where at < target { |
| 237 | reparsed.append(element) |
| 238 | at += element.length |
| 239 | } |
| 240 | guard at == target else { |
| 241 | next += 1 |
| 242 | if next > body.count { return nil } |
| 243 | continue |
| 244 | } |
| 245 | let before = container.green.children[..<(prefixCount + first)] |
| 246 | let after = next < body.count ? Array(body[next...].map(\.element)) : [] |
| 247 | let tail = container.green.children[(prefixCount + body.count)...] |
| 248 | let green = GreenNode(kind: container.kind, children: Array(before) + reparsed + after + Array(tail)) |
| 249 | // A full parse has no zeroth section for empty text. |
| 250 | if green.children.isEmpty { return nil } |
| 251 | return OrgTree(green: replacing(container, with: green), settings: old.settings) |
| 252 | } |
| 253 | return nil |
| 254 | } |
| 255 | |
| 256 | /// Rows of the tables among `elements`, keyed by their text. |
| 257 | func reusableRows(_ elements: [GreenElement]) -> [Substring: GreenNode] { |
| 258 | var rows: [Substring: GreenNode] = [:] |
| 259 | for case .node(let table) in elements where table.kind == .table { |
| 260 | for case .node(let row) in table.children where row.kind == .tableRow { |
| 261 | rows[Substring(row.text)] = row |
| 262 | } |
| 263 | } |
| 264 | return rows |
| 265 | } |
| 266 | |
| 267 | /// Body elements parsed from `text`, or nil if the text holds a heading. |
| 268 | func parseBody(_ text: Substring, settings: OrgSettings, rows: [Substring: GreenNode] = [:]) -> (elements: [GreenElement], unmatchedBegin: Bool)? { |
| 269 | var parser = Parser(text: String(text), settings: settings) |
| 270 | parser.reusableRows = rows |
| 271 | let count = parser.lines.count |
| 272 | parser.builder.start(.zerothSection) |
| 273 | parser.parseContent(limit: count) |
| 274 | guard parser.i == count else { return nil } |
| 275 | parser.builder.finish() |
| 276 | var unmatched = false |
| 277 | for (index, line) in parser.info.enumerated() { |
| 278 | switch line.cls { |
| 279 | case .blockBegin, .dynamicBegin, .drawerBegin: |
| 280 | if parser.blockEnds[index] == nil { unmatched = true } |
| 281 | default: |
| 282 | break |
| 283 | } |
| 284 | } |
| 285 | return (parser.builder.build().children, unmatched) |
| 286 | } |
| 287 | |
| 143 | 288 | // MARK: - Sections |
| 144 | 289 | |
| 145 | 290 | /// Reparses from the top-level section before the edit up to the first later top-level |