| @@ -22,6 +22,8 @@ public struct TextEdit: Sendable, Equatable { |
| 22 | enum ReparseStrategy: Equatable { |
22 | enum ReparseStrategy: Equatable { |
| 23 | /// One element reparsed in place; everything else reused. |
23 | /// One element reparsed in place; everything else reused. |
| 24 | case element |
24 | case element |
| |
25 | /// A run of elements in one section's body reparsed; everything else reused. |
| |
26 | case region |
| 25 | /// A run of top-level sections reparsed; the sections before and after reused. |
27 | /// A run of top-level sections reparsed; the sections before and after reused. |
| 26 | case sections |
28 | case sections |
| 27 | case full |
29 | case full |
| @@ -41,6 +43,7 @@ extension OrgParser { |
| 41 | let context = EditContext(oldText: oldText, newText: newText, edit: edit) |
43 | let context = EditContext(oldText: oldText, newText: newText, edit: edit) |
| 42 | if context.touchesSettings() { return (parse(newText, defaults: defaults), .full) } |
44 | if context.touchesSettings() { return (parse(newText, defaults: defaults), .full) } |
| 43 | if let tree = context.reparseElement(old) { return (tree, .element) } |
45 | if let tree = context.reparseElement(old) { return (tree, .element) } |
| |
46 | if let tree = context.reparseRegion(old) { return (tree, .region) } |
| 44 | if let tree = context.reparseSections(old) { return (tree, .sections) } |
47 | if let tree = context.reparseSections(old) { return (tree, .sections) } |
| 45 | return (parse(newText, defaults: defaults), .full) |
48 | return (parse(newText, defaults: defaults), .full) |
| 46 | } |
49 | } |
| @@ -113,10 +116,7 @@ private struct EditContext { |
| 113 | func leaf(in root: SyntaxNode) -> SyntaxNode? { |
116 | func leaf(in root: SyntaxNode) -> SyntaxNode? { |
| 114 | let range = edit.range |
117 | let range = edit.range |
| 115 | var current = root |
118 | var current = root |
| 116 | while let child = current.children.first(where: { |
119 | while let child = current.child(containing: range.lowerBound), range.upperBound <= child.range.upperBound { |
| 117 | $0.range.lowerBound <= range.lowerBound && range.lowerBound < $0.range.upperBound |
| |
| 118 | && range.upperBound <= $0.range.upperBound |
| |
| 119 | }) { |
| |
| 120 | if Self.leafKinds.contains(child.kind) { return child } |
120 | if Self.leafKinds.contains(child.kind) { return child } |
| 121 | current = child |
121 | current = child |
| 122 | } |
122 | } |
| @@ -140,6 +140,151 @@ private struct EditContext { |
| 140 | return replacement |
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 | // MARK: - Sections |
288 | // MARK: - Sections |
| 144 | |
289 | |
| 145 | /// Reparses from the top-level section before the edit up to the first later top-level |
290 | /// Reparses from the top-level section before the edit up to the first later top-level |