| @@ -0,0 +1,976 @@ |
| 1 | import Foundation |
| 2 | |
| 3 | // Org's plain list machinery (org-list.el, Org 9.8.7) on `EmacsBuffer`: the list structure, |
| 4 | // its repairs (bullets, indentation, checkboxes) and how changes are written back. Defaults as |
| 5 | // in org: `org-list-allow-alphabetical` nil, `org-plain-list-ordered-item-terminator` t, |
| 6 | // `org-list-indent-offset` 0, `org-list-demote-modify-bullet` nil, |
| 7 | // `org-list-two-spaces-after-bullet-regexp` nil, `org-blank-before-new-entry` items `auto`, |
| 8 | // `org-checkbox-hierarchical-statistics` t, `indent-tabs-mode` nil. |
| 9 | |
| 10 | /// One item of `org-list-struct`. |
| 11 | struct ListItem: Equatable { |
| 12 | var pos: Int |
| 13 | var ind: Int |
| 14 | /// With its trailing whitespace. |
| 15 | var bullet: String |
| 16 | var counter: String? |
| 17 | var checkbox: String? |
| 18 | var tag: String? |
| 19 | var end: Int |
| 20 | } |
| 21 | |
| 22 | /// Items in buffer order. |
| 23 | typealias ListStruct = [ListItem] |
| 24 | |
| 25 | extension ListStruct { |
| 26 | func item(_ pos: Int) -> ListItem? { first { $0.pos == pos } } |
| 27 | func index(of pos: Int) -> Int? { firstIndex { $0.pos == pos } } |
| 28 | func ind(_ pos: Int) -> Int { item(pos)!.ind } |
| 29 | func bullet(_ pos: Int) -> String { item(pos)!.bullet } |
| 30 | func checkbox(_ pos: Int) -> String? { item(pos)?.checkbox } |
| 31 | func end(_ pos: Int) -> Int { item(pos)!.end } |
| 32 | mutating func set(_ pos: Int, _ change: (inout ListItem) -> Void) { |
| 33 | if let i = index(of: pos) { change(&self[i]) } |
| 34 | } |
| 35 | |
| 36 | var top: Int { self[0].pos } |
| 37 | var bottom: Int { map(\.end).max()! } |
| 38 | |
| 39 | /// `org-list-prevs-alist`: item to previous item in its sub-list. |
| 40 | var prevs: [Int: Int] { |
| 41 | var byEnd: [Int: Int] = [:] |
| 42 | for item in self where byEnd[item.end] == nil { byEnd[item.end] = item.pos } |
| 43 | var result: [Int: Int] = [:] |
| 44 | for item in self { if let previous = byEnd[item.pos] { result[item.pos] = previous } } |
| 45 | return result |
| 46 | } |
| 47 | |
| 48 | /// `org-list-parents-alist`, in buffer order. |
| 49 | var parents: [(item: Int, parent: Int?)] { |
| 50 | var indToOri: [(ind: Int, origin: Int?)] = [(self[0].ind, nil)] |
| 51 | var previousPositions = [top] |
| 52 | var result: [(Int, Int?)] = [(top, nil)] |
| 53 | for item in dropFirst() { |
| 54 | let previousInd = indToOri[0].ind |
| 55 | previousPositions.insert(item.pos, at: 0) |
| 56 | if previousInd > item.ind { |
| 57 | if let i = indToOri.firstIndex(where: { $0.ind == item.ind }) { |
| 58 | indToOri.removeFirst(i) |
| 59 | } else if let i = indToOri.firstIndex(where: { $0.ind < item.ind }) { |
| 60 | indToOri.removeFirst(i) |
| 61 | } else { |
| 62 | indToOri = [(item.ind, nil)] |
| 63 | } |
| 64 | result.append((item.pos, indToOri[0].origin)) |
| 65 | } else if previousInd < item.ind { |
| 66 | let origin = previousPositions[1] |
| 67 | indToOri.insert((item.ind, origin), at: 0) |
| 68 | result.append((item.pos, origin)) |
| 69 | } else { |
| 70 | result.append((item.pos, indToOri[0].origin)) |
| 71 | } |
| 72 | } |
| 73 | return result |
| 74 | } |
| 75 | |
| 76 | func nextItem(_ pos: Int, _ prevs: [Int: Int]) -> Int? { first { prevs[$0.pos] == pos }?.pos } |
| 77 | func previousItem(_ pos: Int, _ prevs: [Int: Int]) -> Int? { prevs[pos] } |
| 78 | |
| 79 | func hasChild(_ pos: Int) -> Int? { |
| 80 | guard let i = index(of: pos), i + 1 < count, self[i + 1].ind > self[i].ind else { return nil } |
| 81 | return self[i + 1].pos |
| 82 | } |
| 83 | |
| 84 | func subtree(_ pos: Int) -> [Int] { |
| 85 | guard let i = index(of: pos) else { return [] } |
| 86 | let end = self[i].end |
| 87 | return self[(i + 1)...].prefix { $0.pos < end }.map(\.pos) |
| 88 | } |
| 89 | |
| 90 | func children(_ pos: Int, _ parents: [(item: Int, parent: Int?)]) -> [Int] { |
| 91 | parents.filter { $0.parent == pos }.map(\.item) |
| 92 | } |
| 93 | |
| 94 | func parent(_ pos: Int, _ parents: [(item: Int, parent: Int?)]) -> Int? { |
| 95 | parents.first { $0.item == pos }?.parent ?? nil |
| 96 | } |
| 97 | |
| 98 | func listBegin(_ pos: Int, _ prevs: [Int: Int]) -> Int { |
| 99 | var first = pos |
| 100 | while let previous = prevs[first] { first = previous } |
| 101 | return first |
| 102 | } |
| 103 | |
| 104 | func lastItem(_ pos: Int, _ prevs: [Int: Int]) -> Int { |
| 105 | var last = pos |
| 106 | while let next = nextItem(last, prevs) { last = next } |
| 107 | return last |
| 108 | } |
| 109 | |
| 110 | func allItems(_ pos: Int, _ prevs: [Int: Int]) -> [Int] { |
| 111 | var before: [Int] = [] |
| 112 | var item = pos |
| 113 | while let previous = prevs[item] { |
| 114 | before.insert(previous, at: 0) |
| 115 | item = previous |
| 116 | } |
| 117 | var after: [Int] = [] |
| 118 | item = pos |
| 119 | while let next = nextItem(item, prevs) { |
| 120 | after.append(next) |
| 121 | item = next |
| 122 | } |
| 123 | return before + [pos] + after |
| 124 | } |
| 125 | |
| 126 | enum ListType { case ordered, descriptive, unordered } |
| 127 | |
| 128 | func listType(_ pos: Int, _ prevs: [Int: Int]) -> ListType { |
| 129 | let first = item(listBegin(pos, prevs))! |
| 130 | if first.bullet.contains(where: { $0.isLetter || $0.isNumber }) { return .ordered } |
| 131 | return first.tag != nil ? .descriptive : .unordered |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | /// `org-list-struct-assoc-end`: gives each item the first ending at its indentation or less. |
| 136 | private func assocEnd(_ structure: inout ListStruct, _ endList: [(ind: Int, pos: Int)]) { |
| 137 | var endings = endList[...] |
| 138 | for i in structure.indices { |
| 139 | let item = structure[i] |
| 140 | while let first = endings.first, first.pos <= item.pos { endings = endings.dropFirst() } |
| 141 | structure[i].end = endings.first { $0.ind <= item.ind }?.pos ?? structure[i].end |
| 142 | } |
| 143 | } |
| 144 | |
| 145 | /// `org-list-bullet-string`: one space after the bullet's text. |
| 146 | func bulletString(_ bullet: String) -> String { |
| 147 | guard let range = bullet.range(of: "\\S+([ \\t]*)", options: .regularExpression) else { return bullet } |
| 148 | let text = bullet[range] |
| 149 | let core = text.prefix { !$0.isWhitespace } |
| 150 | return bullet[..<range.lowerBound] + core + " " + bullet[range.upperBound...] |
| 151 | } |
| 152 | |
| 153 | extension EmacsBuffer { |
| 154 | // MARK: - Lines and searching |
| 155 | |
| 156 | /// `org-current-text-indentation` of the line at point. |
| 157 | var currentTextIndentation: Int { |
| 158 | var at = lineStart(point) |
| 159 | var column = 0 |
| 160 | while let c = character(at: at), c == 32 || c == 9 { |
| 161 | column = c == 9 ? (column / 8 + 1) * 8 : column + 1 |
| 162 | at += 1 |
| 163 | } |
| 164 | return column |
| 165 | } |
| 166 | |
| 167 | private func regex(_ pattern: String, caseInsensitive: Bool) -> NSRegularExpression { |
| 168 | EmacsBuffer.regex(pattern, caseInsensitive: caseInsensitive) |
| 169 | } |
| 170 | |
| 171 | /// `looking-at`; with `setsMatch` false, `looking-at-p` or inside `save-match-data`. |
| 172 | func looking(_ pattern: String, caseInsensitive: Bool = true, setsMatch: Bool = true) -> [Range<Int>?]? { |
| 173 | let match = regex(pattern, caseInsensitive: caseInsensitive).firstMatch(in: string, options: Self.lookingOptions, range: NSRange(point..<length)) |
| 174 | let groups = match.map(ranges) |
| 175 | if let groups, setsMatch { lastMatch = groups } |
| 176 | return groups |
| 177 | } |
| 178 | |
| 179 | private func ranges(_ match: NSTextCheckingResult) -> [Range<Int>?] { |
| 180 | (0..<match.numberOfRanges).map { i in |
| 181 | let range = match.range(at: i) |
| 182 | return range.location == NSNotFound ? nil : range.location..<NSMaxRange(range) |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | /// `re-search-forward` with a bound and no error: point to the match end; nil leaves point. |
| 187 | @discardableResult |
| 188 | func searchForward(_ pattern: String, bound: Int? = nil, caseInsensitive: Bool = true, move: Bool = false) -> [Range<Int>?]? { |
| 189 | let limit = min(bound ?? length, length) |
| 190 | guard point <= limit else { return nil } |
| 191 | guard let match = regex(pattern, caseInsensitive: caseInsensitive).firstMatch(in: string, options: Self.searchOptions, range: NSRange(point..<limit)) else { |
| 192 | if move { point = limit } |
| 193 | return nil |
| 194 | } |
| 195 | let groups = ranges(match) |
| 196 | point = groups[0]!.upperBound |
| 197 | lastMatch = groups |
| 198 | return groups |
| 199 | } |
| 200 | |
| 201 | /// `re-search-backward` with a bound and no error: the match closest before point that |
| 202 | /// ends by point; point to its start. |
| 203 | @discardableResult |
| 204 | func searchBackward(_ pattern: String, bound: Int? = nil, caseInsensitive: Bool = true) -> [Range<Int>?]? { |
| 205 | let lower = max(bound ?? 0, 0) |
| 206 | guard lower <= point else { return nil } |
| 207 | let expression = regex(pattern, caseInsensitive: caseInsensitive) |
| 208 | var start = point |
| 209 | while start >= lower { |
| 210 | if let match = expression.firstMatch(in: string, options: Self.lookingOptions, range: NSRange(start..<point)) { |
| 211 | let groups = ranges(match) |
| 212 | point = start |
| 213 | lastMatch = groups |
| 214 | return groups |
| 215 | } |
| 216 | start -= 1 |
| 217 | } |
| 218 | return nil |
| 219 | } |
| 220 | |
| 221 | /// `indent-to` with spaces. |
| 222 | func indentTo(_ column: Int) { |
| 223 | let current = currentColumn |
| 224 | if column > current { insert(String(repeating: " ", count: column - current)) } |
| 225 | } |
| 226 | |
| 227 | /// `indent-line-to` with spaces and a tab width of 8. |
| 228 | func indentLineTo(_ column: Int) { |
| 229 | beginningOfLine() |
| 230 | skipForward(Self.blanks) |
| 231 | let current = currentColumn |
| 232 | if current < column { |
| 233 | if column - (current / 8) * 8 >= 8 { |
| 234 | let end = point |
| 235 | skipBackward([32]) |
| 236 | deleteRegion(point, end) |
| 237 | } |
| 238 | indentTo(column) |
| 239 | } else if current > column { |
| 240 | moveToColumn(column, force: true) |
| 241 | let start = point |
| 242 | beginningOfLine() |
| 243 | skipForward(Self.blanks) |
| 244 | deleteRegion(start, point) |
| 245 | } |
| 246 | } |
| 247 | |
| 248 | // MARK: - Context |
| 249 | |
| 250 | static let itemPattern = "([ \\t]*([-+]|(([0-9]+)[.)]))|[ \\t]+\\*)([ \\t]+|$)" |
| 251 | static let itemBeginningPattern = "^" + itemPattern |
| 252 | static let fullItemPattern = "^[ \\t]*((?:[-+*]|(?:[0-9]+|[A-Za-z])[.)])(?:[ \\t]+|$))(?:\\[@(?:start:)?([0-9]+|[A-Za-z])\\][ \\t]*)?(?:(\\[[ X-]\\])(?:[ \\t]+|$))?(?:(.*)[ \\t]+::(?:[ \\t]+|$))?" |
| 253 | static let listEndPattern = "^[ \\t]*\\n[ \\t]*\\n" |
| 254 | static let drawerPattern = "^[ \\t]*:([-_\\w]+):[ \\t]*$" |
| 255 | static let forbiddenBlocks = ["example", "verse", "src", "export"] |
| 256 | |
| 257 | enum ListContextKind: Equatable { case none, drawer, block(String), invalid } |
| 258 | |
| 259 | /// `org-list-context`. |
| 260 | func listContext() -> (up: Int, down: Int, kind: ListContextKind) { |
| 261 | let savedMatch = lastMatch |
| 262 | defer { lastMatch = savedMatch } |
| 263 | return saveExcursion { |
| 264 | beginningOfLine() |
| 265 | let pos = point |
| 266 | var up = saveExcursion { (try? backToHeading()) ?? 0 } |
| 267 | var down = saveExcursion { outlineNextHeading() ? point : length } |
| 268 | var kind = ListContextKind.none |
| 269 | let drawerEnd = "^[ \\t]*:END:" |
| 270 | if looking(Self.drawerPattern) == nil, looking(drawerEnd) == nil { |
| 271 | let found = saveExcursion { () -> (Int, Int)? in |
| 272 | guard searchBackward(Self.drawerPattern, bound: up) != nil else { return nil } |
| 273 | let begin = lineEnd(point) + 1 |
| 274 | let end = searchForward(drawerEnd, bound: down).map { $0[0]!.lowerBound - 1 } ?? down |
| 275 | return end >= pos ? (begin, end) : nil |
| 276 | } |
| 277 | if let (begin, end) = found { |
| 278 | (up, down, kind) = (begin, end, .drawer) |
| 279 | } |
| 280 | } |
| 281 | let blockPattern = "^[ \\t]*#\\+(begin|end)_" |
| 282 | if looking(blockPattern) == nil { |
| 283 | let found = saveExcursion { () -> (Int, Int, String)? in |
| 284 | guard searchBackward(blockPattern, bound: up) != nil else { return nil } |
| 285 | let begin = lineEnd(point) + 1 |
| 286 | guard let open = looking("^[ \\t]*#\\+begin_(\\S+)") else { return nil } |
| 287 | let type = substring(open[1]!.lowerBound, open[1]!.upperBound).lowercased() |
| 288 | point = begin |
| 289 | let close = searchForward(blockPattern, bound: down) |
| 290 | let end = close.map { _ in lineStart(point) - 1 } ?? down |
| 291 | guard end >= pos else { return nil } |
| 292 | let word = close.map { substring($0[1]!.lowerBound, $0[1]!.upperBound) } ?? type |
| 293 | return word.lowercased() == "end" ? (begin, end, type) : nil |
| 294 | } |
| 295 | if let (begin, end, type) = found { |
| 296 | (up, down, kind) = (begin, end, Self.forbiddenBlocks.contains(type) ? .invalid : .block(type)) |
| 297 | } |
| 298 | } |
| 299 | return (up, down, kind) |
| 300 | } |
| 301 | } |
| 302 | |
| 303 | /// `org-in-item-p`: the start of the item holding point, or nil. |
| 304 | func inItem() -> Int? { |
| 305 | saveExcursion { |
| 306 | beginningOfLine() |
| 307 | let context = listContext() |
| 308 | var reference = looking("^[ \\t]*$") != nil ? 10000 : currentTextIndentation |
| 309 | if context.kind == .invalid { return nil } |
| 310 | if looking(Self.itemPattern) != nil { return point } |
| 311 | // Inside `org-list-end-re`: start above it. |
| 312 | let windowStart = saveExcursion { forwardLine(-2); return point } |
| 313 | let windowEnd = saveExcursion { forwardLine(2); return lineEnd(point) } |
| 314 | let expression = regex(Self.listEndPattern, caseInsensitive: true) |
| 315 | for match in expression.matches(in: string, options: Self.searchOptions, range: NSRange(windowStart..<windowEnd)) { |
| 316 | let range = match.range.location..<NSMaxRange(match.range) |
| 317 | if point >= range.lowerBound, point < range.upperBound { |
| 318 | point = range.lowerBound |
| 319 | forwardLine(-1) |
| 320 | break |
| 321 | } |
| 322 | } |
| 323 | while true { |
| 324 | let ind = currentTextIndentation |
| 325 | if looking(Self.itemPattern) != nil, ind < reference { return point } |
| 326 | if point <= context.up { return nil } |
| 327 | if looking(Self.listEndPattern) != nil { return nil } |
| 328 | if looking("^[ \\t]*#\\+end_") != nil, searchBackward("^[ \\t]*#\\+begin_", bound: context.up) != nil { |
| 329 | continue |
| 330 | } |
| 331 | if looking("^[ \\t]*:END:") != nil, searchBackward(Self.drawerPattern, bound: context.up) != nil { |
| 332 | beginningOfLine() |
| 333 | continue |
| 334 | } |
| 335 | if looking("^[ \\t]*$") != nil { |
| 336 | forwardLine(-1) |
| 337 | continue |
| 338 | } |
| 339 | if ind == 0 { return nil } |
| 340 | if ind < reference { reference = ind } |
| 341 | forwardLine(-1) |
| 342 | } |
| 343 | } |
| 344 | } |
| 345 | |
| 346 | /// `org-at-item-p`: an item line where the element parser sees one, so not in blocks |
| 347 | /// whose contents aren't elements. |
| 348 | var atItem: Bool { |
| 349 | saveExcursion { |
| 350 | beginningOfLine() |
| 351 | switch listContext().kind { |
| 352 | case .invalid, .block("comment"), .block("verse"): return false |
| 353 | default: return looking(Self.itemPattern) != nil |
| 354 | } |
| 355 | } |
| 356 | } |
| 357 | |
| 358 | // MARK: - Structure |
| 359 | |
| 360 | private func assocAtPoint(_ ind: Int) -> ListItem { |
| 361 | let match = looking(Self.fullItemPattern)! |
| 362 | func group(_ i: Int) -> String? { match[i].map { substring($0.lowerBound, $0.upperBound) } } |
| 363 | let bullet = group(1) ?? "" |
| 364 | let tag = bullet.range(of: "[-+*]", options: .regularExpression) != nil ? group(4) : nil |
| 365 | return ListItem(pos: point, ind: ind, bullet: bullet, counter: group(2), checkbox: group(3), tag: tag, end: 0) |
| 366 | } |
| 367 | |
| 368 | /// `org-list-struct` for the list holding point, which is at an item. |
| 369 | func listStruct() -> ListStruct { |
| 370 | saveExcursion { |
| 371 | beginningOfLine() |
| 372 | let context = listContext() |
| 373 | var textMinInd = 10000 |
| 374 | var beginCell = (pos: point, ind: currentTextIndentation) |
| 375 | var items: [ListItem] = [] |
| 376 | var ends: [(ind: Int, pos: Int)] = [] |
| 377 | func endBeforeBlank() -> Int { |
| 378 | skipBackward(Self.whitespace) |
| 379 | return min(lineEnd(point) + 1, context.down) |
| 380 | } |
| 381 | func trimToBegin() { |
| 382 | if let i = items.firstIndex(where: { $0.pos == beginCell.pos }) { items = Array(items[i...]) } else { items = [] } |
| 383 | } |
| 384 | saveExcursion { |
| 385 | while true { |
| 386 | let ind = currentTextIndentation |
| 387 | if point <= context.up { |
| 388 | if looking(Self.itemPattern) == nil { |
| 389 | trimToBegin() |
| 390 | } else { |
| 391 | beginCell = (point, ind) |
| 392 | items.insert(assocAtPoint(ind), at: 0) |
| 393 | } |
| 394 | return |
| 395 | } |
| 396 | if looking(Self.listEndPattern) != nil { |
| 397 | trimToBegin() |
| 398 | return |
| 399 | } |
| 400 | if looking(Self.itemPattern) != nil { |
| 401 | items.insert(assocAtPoint(ind), at: 0) |
| 402 | ends.insert((ind, point), at: 0) |
| 403 | if ind < textMinInd { beginCell = (point, ind) } |
| 404 | forwardLine(-1) |
| 405 | } else if looking("^[ \\t]*#\\+end_") != nil, searchBackward("^[ \\t]*#\\+begin_", bound: context.up) != nil { |
| 406 | } else if looking("^[ \\t]*:END:") != nil, searchBackward(Self.drawerPattern, bound: context.up) != nil { |
| 407 | beginningOfLine() |
| 408 | } else if looking("^[ \\t]*$") != nil { |
| 409 | forwardLine(-1) |
| 410 | } else if ind == 0 { |
| 411 | trimToBegin() |
| 412 | return |
| 413 | } else { |
| 414 | if ind < textMinInd { textMinInd = ind } |
| 415 | ends.insert((ind, point), at: 0) |
| 416 | forwardLine(-1) |
| 417 | } |
| 418 | } |
| 419 | } |
| 420 | var items2: [ListItem] = [] |
| 421 | var ends2: [(ind: Int, pos: Int)] = [] |
| 422 | while true { |
| 423 | let ind = currentTextIndentation |
| 424 | if point >= context.down { |
| 425 | ends2.append((0, endBeforeBlank())) |
| 426 | break |
| 427 | } |
| 428 | if looking(Self.listEndPattern) != nil { |
| 429 | ends2.append((0, point)) |
| 430 | break |
| 431 | } |
| 432 | if looking(Self.itemPattern) != nil { |
| 433 | items2.append(assocAtPoint(ind)) |
| 434 | ends2.append((ind, point)) |
| 435 | forwardLine(1) |
| 436 | } else if looking("^[ \\t]*$") != nil { |
| 437 | forwardLine(1) |
| 438 | } else if ind <= beginCell.ind { |
| 439 | ends2.append((0, endBeforeBlank())) |
| 440 | break |
| 441 | } else { |
| 442 | if let last = items2.last, ind <= last.ind { ends2.append((ind, point)) } |
| 443 | if looking("^[ \\t]*#\\+begin_") != nil, searchForward("^[ \\t]*#\\+end_", bound: context.down) != nil { |
| 444 | } else if looking(Self.drawerPattern) != nil, searchForward("^[ \\t]*:END:", bound: context.down) != nil { |
| 445 | } |
| 446 | forwardLine(1) |
| 447 | } |
| 448 | } |
| 449 | var structure = items + Array(items2.dropFirst()) |
| 450 | assocEnd(&structure, ends + Array(ends2.dropFirst())) |
| 451 | return structure |
| 452 | } |
| 453 | } |
| 454 | |
| 455 | func itemEndBeforeBlank(_ pos: Int, _ structure: ListStruct) -> Int { |
| 456 | saveExcursion { |
| 457 | point = structure.end(pos) |
| 458 | skipBackward(Self.whitespace) |
| 459 | return lineEnd(point) |
| 460 | } |
| 461 | } |
| 462 | |
| 463 | // MARK: - Editing structures |
| 464 | |
| 465 | /// `org-list-swap-items`. |
| 466 | func swapItems(_ a: Int, _ b: Int, _ structure: ListStruct) -> ListStruct { |
| 467 | saveExcursion { |
| 468 | let endANoBlank = itemEndBeforeBlank(a, structure) |
| 469 | let endBNoBlank = itemEndBeforeBlank(b, structure) |
| 470 | let endA = structure.end(a), endB = structure.end(b) |
| 471 | let sizeA = endANoBlank - a, sizeB = endBNoBlank - b |
| 472 | let bodyA = substring(a, endANoBlank), bodyB = substring(b, endBNoBlank) |
| 473 | let between = substring(endANoBlank, b) |
| 474 | let subA = [a] + structure.subtree(a), subB = [b] + structure.subtree(b) |
| 475 | point = a |
| 476 | deleteRegion(a, endBNoBlank) |
| 477 | insert(bodyB + between + bodyA) |
| 478 | var result = structure |
| 479 | for i in result.indices { |
| 480 | let pos = result[i].pos, end = result[i].end |
| 481 | if pos < a { |
| 482 | } else if subA.contains(pos) { |
| 483 | result[i].pos = pos + (endBNoBlank - endANoBlank) |
| 484 | result[i].end = end + (endBNoBlank - endANoBlank) |
| 485 | if end == endA { result[i].end = endB } |
| 486 | } else if subB.contains(pos) { |
| 487 | result[i].pos = pos + a - b |
| 488 | result[i].end = end + (a - b) |
| 489 | if end == endB { result[i].end = a + sizeB + (endA - endANoBlank) } |
| 490 | } else if pos < b { |
| 491 | result[i].pos = pos + (sizeB - sizeA) |
| 492 | result[i].end = end + (sizeB - sizeA) |
| 493 | } |
| 494 | } |
| 495 | return result.sorted { $0.pos < $1.pos } |
| 496 | } |
| 497 | } |
| 498 | |
| 499 | /// `org-list-separating-blank-lines-number`, with point at the item. |
| 500 | func separatingBlankLines(_ pos: Int, _ structure: ListStruct, _ prevs: [Int: Int]) -> Int { |
| 501 | saveExcursion { |
| 502 | let item = point |
| 503 | func countBlanks() -> Int { |
| 504 | saveExcursion { |
| 505 | point = lineStart(point) |
| 506 | let start = point |
| 507 | skipBackward(Self.whitespace) |
| 508 | forwardLine(1) |
| 509 | return countLines(point, start) |
| 510 | } |
| 511 | } |
| 512 | if let next = structure.nextItem(item, prevs) { |
| 513 | point = next |
| 514 | return countBlanks() |
| 515 | } |
| 516 | if structure.previousItem(item, prevs) != nil { return countBlanks() } |
| 517 | if pos > itemEndBeforeBlank(item, structure) { |
| 518 | let blanks = saveExcursion { point = pos; return countBlanks() } |
| 519 | if blanks > 0 { return blanks } |
| 520 | } |
| 521 | point = structure.top |
| 522 | if searchForward("^[ \\t]*$", bound: itemEndBeforeBlank(item, structure)) != nil { return 1 } |
| 523 | return 0 |
| 524 | } |
| 525 | } |
| 526 | |
| 527 | /// `count-lines` between two positions. |
| 528 | func countLines(_ start: Int, _ end: Int) -> Int { |
| 529 | let (lower, upper) = (min(start, end), max(start, end)) |
| 530 | guard upper > lower else { return 0 } |
| 531 | let newlines = substring(lower, upper).utf16.filter { $0 == 10 }.count |
| 532 | return character(at: upper - 1) == Self.newline ? newlines : newlines + 1 |
| 533 | } |
| 534 | |
| 535 | /// `org-list-insert-item`. Leaves point at the new item. |
| 536 | func insertItem(at position: Int, _ structure: ListStruct, _ prevs: [Int: Int], checkbox: Bool, afterBullet: String?, splitLine: Bool = true) -> ListStruct { |
| 537 | var pos = position |
| 538 | var found: Int? |
| 539 | var stopped = false |
| 540 | for entry in structure { |
| 541 | if entry.pos > pos { |
| 542 | stopped = true |
| 543 | break |
| 544 | } |
| 545 | if entry.end < pos { continue } |
| 546 | found = entry.pos |
| 547 | } |
| 548 | guard let item = stopped ? found : (found ?? structure.last?.pos) else { return structure } |
| 549 | let itemEnd = structure.end(item) |
| 550 | let itemEndNoBlank = itemEndBeforeBlank(item, structure) |
| 551 | point = item |
| 552 | let match = looking(Self.fullItemPattern)! |
| 553 | let bodyStart: Int |
| 554 | if match[4] == nil { |
| 555 | bodyStart = match[0]!.upperBound |
| 556 | } else if substring(match[1]!.lowerBound, match[1]!.upperBound).contains(where: { $0 == "." || $0 == ")" }) { |
| 557 | bodyStart = match[4]!.lowerBound |
| 558 | } else { |
| 559 | bodyStart = saveExcursion { point = match[4]!.upperBound; skipForward(Self.blanks); return point } |
| 560 | } |
| 561 | let before = pos <= bodyStart |
| 562 | let blanks = separatingBlankLines(pos, structure, prevs) |
| 563 | let ind = structure.ind(item) |
| 564 | let bullet = bulletString(structure.bullet(item)) |
| 565 | let box = checkbox ? "[ ]" : nil |
| 566 | var textCut: String? |
| 567 | if !before, splitLine { |
| 568 | point = pos |
| 569 | if itemEnd < pos { deleteRegion(itemEnd - 1, lineEnd(point)) } |
| 570 | skipBackward(Self.whitespace) |
| 571 | pos = saveExcursion { skipForward(Self.blanks); return point } |
| 572 | textCut = substring(point, itemEndNoBlank) |
| 573 | deleteRegion(point, itemEndNoBlank) |
| 574 | } |
| 575 | var body = bullet + (box.map { $0 + " " } ?? "") + (afterBullet ?? "") |
| 576 | if let textCut { |
| 577 | // `string-match' sets the match data to the string's blanks. |
| 578 | let blanks = textCut.prefix { $0 == " " || $0 == "\t" }.utf16.count |
| 579 | if blanks > 0 { lastMatch = [0..<blanks] } |
| 580 | body += textCut.replacingOccurrences(of: "^[ \\t]+", with: "", options: .regularExpression) |
| 581 | } |
| 582 | let separator = String(repeating: "\n", count: blanks + 1) |
| 583 | let itemSize = ind + (body as NSString).length + (separator as NSString).length |
| 584 | let sizeOffset = itemSize - ((textCut ?? "") as NSString).length |
| 585 | point = item |
| 586 | indentTo(ind) |
| 587 | insert(body + separator) |
| 588 | var result = structure |
| 589 | for i in result.indices { |
| 590 | let p = result[i].pos, end = result[i].end |
| 591 | if p < item { |
| 592 | if end > item { result[i].end = end + sizeOffset } |
| 593 | } else if p == item, !before, splitLine { |
| 594 | result[i].pos = p + itemSize |
| 595 | result[i].end = end + sizeOffset |
| 596 | } else if splitLine, !before, p >= pos, p <= itemEndNoBlank { |
| 597 | let offset = pos - item - ind - (bullet as NSString).length - ((afterBullet ?? "") as NSString).length |
| 598 | result[i].pos = p - offset |
| 599 | result[i].end = end - offset |
| 600 | } else { |
| 601 | result[i].pos = p + sizeOffset |
| 602 | result[i].end = end + sizeOffset |
| 603 | } |
| 604 | } |
| 605 | result.append(ListItem(pos: item, ind: ind, bullet: bullet, counter: nil, checkbox: box, tag: nil, end: item + itemSize)) |
| 606 | result.sort { $0.pos < $1.pos } |
| 607 | if before { |
| 608 | point = item |
| 609 | } else { |
| 610 | result = swapItems(item, item + itemSize, result) |
| 611 | point = result.nextItem(item, result.prevs)! |
| 612 | } |
| 613 | return result |
| 614 | } |
| 615 | |
| 616 | /// `org-list-delete-item`. |
| 617 | func deleteItem(_ item: Int, _ structure: ListStruct) -> ListStruct { |
| 618 | let end = structure.end(item) |
| 619 | let begin = structure.bottom == end ? saveExcursion { () -> Int in |
| 620 | point = item |
| 621 | skipBackward(Self.whitespace) |
| 622 | return min(lineEnd(point) + 1, length) |
| 623 | } : item |
| 624 | deleteRegion(begin, end) |
| 625 | let size = end - begin |
| 626 | return structure.compactMap { entry in |
| 627 | var entry = entry |
| 628 | if entry.pos < item { |
| 629 | if entry.end == item { entry.end = begin } else if entry.end > item { entry.end -= size } |
| 630 | return entry |
| 631 | } |
| 632 | if entry.pos < end { return nil } |
| 633 | entry.pos -= size |
| 634 | entry.end -= size |
| 635 | return entry |
| 636 | } |
| 637 | } |
| 638 | |
| 639 | enum SendDestination { case begin, end } |
| 640 | |
| 641 | /// `org-list-send-item` to the start or end of the item's sub-list. |
| 642 | func sendItem(_ item: Int, to destination: SendDestination, _ structure: ListStruct) -> ListStruct { |
| 643 | let prevs = structure.prevs |
| 644 | let itemEnd = structure.end(item) |
| 645 | let bulletEnd = saveExcursion { () -> Int in |
| 646 | point = item |
| 647 | return looking("[ \\t]*" + NSRegularExpression.escapedPattern(for: structure.bullet(item)))![0]!.upperBound |
| 648 | } |
| 649 | let body = substring(bulletEnd, itemEnd).trimmingCharacters(in: .whitespacesAndNewlines) |
| 650 | let insertion: Int |
| 651 | switch destination { |
| 652 | case .begin: |
| 653 | insertion = structure.listBegin(item, prevs) |
| 654 | case .end: |
| 655 | insertion = saveExcursion { () -> Int in |
| 656 | point = structure.lastItem(item, prevs) |
| 657 | return lineEnd(point) |
| 658 | } |
| 659 | } |
| 660 | guard item != insertion else { return structure } |
| 661 | let itemMarker = marker(item) |
| 662 | defer { release(itemMarker) } |
| 663 | var result = insertItem(at: insertion, structure, prevs, checkbox: false, afterBullet: body, splitLine: false) |
| 664 | let moved = [itemMarker.position] + result.subtree(itemMarker.position) |
| 665 | let newEnd = result.end(point) |
| 666 | let oldEnd = result.end(itemMarker.position) |
| 667 | let newItem = point |
| 668 | let shift = point - itemMarker.position |
| 669 | result.removeAll { $0.pos == newItem } |
| 670 | let copies = moved.map { pos -> ListItem in |
| 671 | var cell = result.item(pos)! |
| 672 | cell.end = cell.end == oldEnd ? newEnd : cell.end + shift |
| 673 | cell.pos = pos + shift |
| 674 | return cell |
| 675 | } |
| 676 | result = (result + copies).sorted { $0.pos < $1.pos } |
| 677 | return deleteItem(itemMarker.position, result) |
| 678 | } |
| 679 | |
| 680 | // MARK: - Indentation changes |
| 681 | |
| 682 | /// `org-list-struct-outdent`. |
| 683 | func structOutdent(_ start: Int, _ end: Int, _ structure: ListStruct, _ parents: [(item: Int, parent: Int?)]) throws -> [(item: Int, parent: Int?)] { |
| 684 | var acc: [Int: Int] = [:] |
| 685 | return try parents.map { cell in |
| 686 | let (item, parent) = cell |
| 687 | if item < start { return cell } |
| 688 | if item >= end { |
| 689 | if let parent, let convert = acc[parent] { return (item, convert) } |
| 690 | return cell |
| 691 | } |
| 692 | guard let parent else { throw UserError(message: "Cannot outdent top-level items") } |
| 693 | if parent >= start { |
| 694 | acc[parent] = item |
| 695 | return cell |
| 696 | } |
| 697 | acc[parent] = item |
| 698 | return (item, structure.parent(parent, parents)) |
| 699 | } |
| 700 | } |
| 701 | |
| 702 | /// `org-list-struct-indent`. |
| 703 | func structIndent(_ start: Int, _ end: Int, _ structure: ListStruct, _ parents: [(item: Int, parent: Int?)], _ prevs: [Int: Int]) throws -> [(item: Int, parent: Int?)] { |
| 704 | var acc: [(item: Int, parent: Int?)] = [] |
| 705 | func remember(_ cell: (item: Int, parent: Int?)) -> (item: Int, parent: Int?) { |
| 706 | acc.insert(cell, at: 0) |
| 707 | return cell |
| 708 | } |
| 709 | return try parents.map { cell in |
| 710 | let (item, parent) = cell |
| 711 | if item < start { return cell } |
| 712 | if item >= end { |
| 713 | if let convert = acc.first(where: { $0.item == parent }) { return (item, convert.parent) } |
| 714 | return cell |
| 715 | } |
| 716 | let previous = structure.previousItem(item, prevs) |
| 717 | guard let previous else { |
| 718 | if parent == nil || parent! < start { throw UserError(message: "Cannot indent the first item of a list") } |
| 719 | return remember(cell) |
| 720 | } |
| 721 | if previous < start { return remember((item, previous)) } |
| 722 | return remember((item, acc.first { $0.item == previous }?.parent ?? nil)) |
| 723 | } |
| 724 | } |
| 725 | |
| 726 | // MARK: - Repairs |
| 727 | |
| 728 | /// Records a successful `string-match' on a string in the match data. |
| 729 | private func stringMatched(_ string: String, _ range: Range<String.Index>) { |
| 730 | let start = string.utf16.distance(from: string.startIndex, to: range.lowerBound) |
| 731 | lastMatch = [start..<(start + string[range].utf16.count)] |
| 732 | } |
| 733 | |
| 734 | /// `org-list-inc-bullet-maybe`. |
| 735 | func incrementBullet(_ bullet: String) -> String { |
| 736 | if let range = bullet.range(of: "[0-9]+", options: .regularExpression) { |
| 737 | stringMatched(bullet, range) |
| 738 | return bullet.replacingCharacters(in: range, with: String(Int(bullet[range])! + 1)) |
| 739 | } |
| 740 | if let range = bullet.range(of: "[A-Za-z]", options: .regularExpression) { |
| 741 | stringMatched(bullet, range) |
| 742 | let scalar = bullet[range].unicodeScalars.first!.value + 1 |
| 743 | return bullet.replacingCharacters(in: range, with: String(Character(Unicode.Scalar(scalar)!))) |
| 744 | } |
| 745 | return bullet |
| 746 | } |
| 747 | |
| 748 | /// `org-list-struct-fix-bul` (alphabetical bullets off). |
| 749 | func fixBullets(_ structure: inout ListStruct, _ prevs: [Int: Int]) { |
| 750 | for pos in structure.map(\.pos) { |
| 751 | let previous = prevs[pos] |
| 752 | let counter = structure.item(pos)!.counter |
| 753 | let bullet = structure.bullet(pos) |
| 754 | let new: String |
| 755 | if let previous, let counter, counter.range(of: "[a-zA-Z]", options: .regularExpression) != nil, |
| 756 | structure.bullet(previous).range(of: "[a-zA-Z]", options: .regularExpression) != nil { |
| 757 | let previousBullet = structure.bullet(previous) |
| 758 | let real = previousBullet.range(of: "[a-z]", options: .regularExpression) != nil ? counter.lowercased() : counter.uppercased() |
| 759 | let range = previousBullet.range(of: "[a-zA-Z]", options: .regularExpression)! |
| 760 | new = previousBullet.replacingCharacters(in: range, with: real) |
| 761 | } else if let previous, let counter, let counterRange = counter.range(of: "[0-9]+", options: .regularExpression), |
| 762 | let range = structure.bullet(previous).range(of: "[0-9]+", options: .regularExpression) { |
| 763 | new = structure.bullet(previous).replacingCharacters(in: range, with: counter[counterRange]) |
| 764 | } else if let previous { |
| 765 | new = incrementBullet(structure.bullet(previous)) |
| 766 | } else if let counter, let counterRange = counter.range(of: "[0-9]+", options: .regularExpression), |
| 767 | let range = bullet.range(of: "[0-9]+", options: .regularExpression) { |
| 768 | new = bullet.replacingCharacters(in: range, with: counter[counterRange]) |
| 769 | } else if let range = bullet.range(of: "([0-9]+|[A-Za-z])", options: .regularExpression) { |
| 770 | stringMatched(bullet, range) |
| 771 | new = bullet.replacingCharacters(in: range, with: "1") |
| 772 | } else { |
| 773 | new = bullet |
| 774 | } |
| 775 | structure.set(pos) { $0.bullet = bulletString(new) } |
| 776 | } |
| 777 | } |
| 778 | |
| 779 | /// `org-list-struct-fix-ind`. |
| 780 | func fixIndentation(_ structure: inout ListStruct, _ parents: [(item: Int, parent: Int?)], bulletSize: Int? = nil) { |
| 781 | let topInd = structure.ind(structure.top) |
| 782 | for pos in structure.dropFirst().map(\.pos) { |
| 783 | if let parent = structure.parent(pos, parents) { |
| 784 | let ind = (bulletSize ?? (structure.bullet(parent) as NSString).length) + structure.ind(parent) |
| 785 | structure.set(pos) { $0.ind = ind } |
| 786 | } else { |
| 787 | structure.set(pos) { $0.ind = topInd } |
| 788 | } |
| 789 | } |
| 790 | } |
| 791 | |
| 792 | /// `org-list-struct-fix-box` without ORDERED. |
| 793 | func fixCheckboxes(_ structure: inout ListStruct, _ parents: [(item: Int, parent: Int?)]) { |
| 794 | var parentList: [Int] = [] |
| 795 | for pos in structure.map(\.pos) { |
| 796 | guard let parent = structure.parent(pos, parents), structure.checkbox(parent) != nil, !parentList.contains(parent) else { continue } |
| 797 | parentList.insert(parent, at: 0) |
| 798 | } |
| 799 | // `sort` in Emacs is stable; ties keep their order. |
| 800 | let ordered = parentList.enumerated().sorted { lhs, rhs in |
| 801 | let (a, b) = (structure.ind(lhs.element), structure.ind(rhs.element)) |
| 802 | return a != b ? a > b : lhs.offset < rhs.offset |
| 803 | }.map(\.element) |
| 804 | for item in ordered { |
| 805 | let boxes = structure.children(item, parents).map { structure.checkbox($0) } |
| 806 | let new: String? |
| 807 | if boxes.contains("[ ]"), boxes.contains("[X]") { |
| 808 | new = "[-]" |
| 809 | } else if boxes.contains("[-]") { |
| 810 | new = "[-]" |
| 811 | } else if boxes.contains("[X]") { |
| 812 | new = "[X]" |
| 813 | } else if boxes.contains("[ ]") { |
| 814 | new = "[ ]" |
| 815 | } else { |
| 816 | new = structure.checkbox(item) |
| 817 | } |
| 818 | structure.set(item) { $0.checkbox = new } |
| 819 | } |
| 820 | } |
| 821 | |
| 822 | /// `org-list-struct-fix-item-end`. |
| 823 | func fixItemEnds(_ structure: inout ListStruct) { |
| 824 | var endList: [(ind: Int, pos: Int)] = [] |
| 825 | var accEnd: [(end: Int, pos: Int)] = [] |
| 826 | for item in structure { |
| 827 | if structure.item(item.end) == nil { |
| 828 | let up = accEnd.first { item.end < $0.end }?.pos |
| 829 | endList.insert((up.map { structure.ind($0) + 2 } ?? 0, item.end), at: 0) |
| 830 | } |
| 831 | endList.insert((item.ind, item.pos), at: 0) |
| 832 | accEnd.insert((item.end, item.pos), at: 0) |
| 833 | } |
| 834 | let sorted = endList.enumerated().sorted { $0.element.pos != $1.element.pos ? $0.element.pos < $1.element.pos : $0.offset < $1.offset }.map(\.element) |
| 835 | assocEnd(&structure, sorted) |
| 836 | } |
| 837 | |
| 838 | /// The previous match data, with groups it lacks as unmatched. |
| 839 | private var staleMatch: [Range<Int>?] { |
| 840 | lastMatch + Array(repeating: nil, count: max(0, 6 - lastMatch.count)) |
| 841 | } |
| 842 | |
| 843 | /// `org-list-struct-apply-struct`. |
| 844 | func applyStruct(_ structure: ListStruct, _ old: ListStruct) { |
| 845 | let origin = marker(point) |
| 846 | defer { |
| 847 | point = origin.position |
| 848 | release(origin) |
| 849 | } |
| 850 | func shiftBody(end: Int, begin: Int, delta: Int, ind: Int?) { |
| 851 | point = end |
| 852 | skipBackward(Self.whitespace) |
| 853 | beginningOfLine() |
| 854 | while point > begin || (point == begin && looking(Self.itemPattern) == nil) { |
| 855 | if looking("^[ \\t]*\\S", setsMatch: false) != nil { |
| 856 | indentLineTo(max(currentTextIndentation + delta, ind.map { $0 + 1 } ?? -1)) |
| 857 | } |
| 858 | if point == 0 { break } |
| 859 | forwardLine(-1) |
| 860 | } |
| 861 | } |
| 862 | func modify(_ item: Int) { |
| 863 | point = item |
| 864 | let newInd = structure.ind(item) |
| 865 | let oldInd = currentTextIndentation |
| 866 | let newBullet = bulletString(structure.bullet(item)) |
| 867 | let oldBullet = old.bullet(item) |
| 868 | let newBox = structure.checkbox(item) |
| 869 | // A failed `looking-at' leaves the previous match data, which org then uses; this |
| 870 | // happens when a split item takes a sub-list with it. |
| 871 | var match = looking(Self.fullItemPattern) ?? staleMatch |
| 872 | if oldBullet != newBullet { |
| 873 | var keepSpace = "" |
| 874 | saveExcursion { |
| 875 | let bulletRange = match[1]! |
| 876 | if bulletRange.lowerBound <= origin.position, origin.position <= bulletRange.upperBound { |
| 877 | let spaces = saveExcursion { () -> String? in |
| 878 | point = origin.position |
| 879 | return looking("[ \\t]+", setsMatch: false).map { substring($0[0]!.lowerBound, $0[0]!.upperBound) } |
| 880 | } |
| 881 | keepSpace = spaces ?? "" |
| 882 | } |
| 883 | replace(bulletRange, with: "") |
| 884 | point = bulletRange.lowerBound |
| 885 | insertBeforeMarkers(newBullet) |
| 886 | insert(keepSpace) |
| 887 | } |
| 888 | } |
| 889 | point = item |
| 890 | match = looking(Self.fullItemPattern) ?? staleMatch |
| 891 | let oldBox = match[3].map { substring($0.lowerBound, $0.upperBound) } |
| 892 | if oldBox == newBox { |
| 893 | } else if let boxRange = match[3], let newBox { |
| 894 | replace(boxRange, with: newBox) |
| 895 | } else if oldBox != nil { |
| 896 | let box = looking(".*?([ \\t]*\\[[ X-]\\])")! |
| 897 | replace(box[1]!, with: "") |
| 898 | } else if let newBox { |
| 899 | let counterEnd = match[2]?.upperBound |
| 900 | point = counterEnd.map { $0 + 1 } ?? match[1]!.upperBound |
| 901 | insert(newBox + (counterEnd == nil ? " " : "")) |
| 902 | } |
| 903 | if newInd != oldInd { |
| 904 | point = lineStart(point) |
| 905 | let start = point |
| 906 | skipForward(Self.blanks) |
| 907 | deleteRegion(start, point) |
| 908 | indentTo(newInd) |
| 909 | } |
| 910 | } |
| 911 | var endList: [(end: Int, up: Int?)] = [] |
| 912 | var accEnd: [(end: Int, pos: Int)] = [] |
| 913 | var shifts: [Int: (delta: Int, ind: Int)] = [:] |
| 914 | for entry in old { |
| 915 | let pos = entry.pos |
| 916 | let indNew = structure.ind(pos), indOld = entry.ind |
| 917 | let delta = (indNew + (structure.bullet(pos) as NSString).length) - (indOld + (entry.bullet as NSString).length) |
| 918 | shifts[pos] = (delta, indNew) |
| 919 | if old.item(entry.end) == nil { |
| 920 | endList.insert((entry.end, accEnd.first { entry.end < $0.end }?.pos), at: 0) |
| 921 | } |
| 922 | accEnd.insert((entry.end, pos), at: 0) |
| 923 | } |
| 924 | var uniqueEnds: [Int] = [] |
| 925 | for end in endList.map(\.end) where !uniqueEnds.contains(end) { uniqueEnds.append(end) } |
| 926 | var allEnds = (old.map(\.pos) + uniqueEnds).sorted() |
| 927 | accEnd.reverse() |
| 928 | var slices: [(down: Int, up: Int, delta: Int, ind: Int?)] = [] |
| 929 | while allEnds.count > 1 { |
| 930 | let up = allEnds.removeFirst() |
| 931 | let down = allEnds[0] |
| 932 | let isItem = structure.item(up) != nil |
| 933 | let delta: Int |
| 934 | if isItem { |
| 935 | delta = shifts[up]!.delta |
| 936 | } else { |
| 937 | let child = accEnd.first { $0.end == up }!.pos |
| 938 | let ind = structure.ind(child) |
| 939 | var minInd = Int.max |
| 940 | saveExcursion { |
| 941 | point = up |
| 942 | while point < down { |
| 943 | if looking("[ \\t]*$", setsMatch: false) == nil { |
| 944 | minInd = min(currentTextIndentation, minInd) |
| 945 | if let open = looking("#\\+BEGIN(:|_\\S+)") { |
| 946 | let name = NSRegularExpression.escapedPattern(for: substring(open[1]!.lowerBound, open[1]!.upperBound)) |
| 947 | searchForward("^[ \\t]*#\\+END\(name)[ \\t]*$", bound: down) |
| 948 | } else if looking(Self.drawerPattern) != nil { |
| 949 | searchForward("^[ \\t]*:END:[ \\t]*$", bound: down) |
| 950 | } |
| 951 | } |
| 952 | if forwardLine(1) != 0 { break } |
| 953 | } |
| 954 | } |
| 955 | delta = ind - minInd |
| 956 | } |
| 957 | slices.insert((down, up, delta, isItem ? shifts[up]!.ind : nil), at: 0) |
| 958 | } |
| 959 | for slice in slices { |
| 960 | if slice.delta != 0 { shiftBody(end: slice.down, begin: slice.up, delta: slice.delta, ind: slice.ind) } |
| 961 | if let cell = structure.item(slice.up), cell != old.item(slice.up) { modify(slice.up) } |
| 962 | } |
| 963 | } |
| 964 | |
| 965 | /// `org-list-write-struct`. |
| 966 | func writeStruct(_ structure: inout ListStruct, _ parents: [(item: Int, parent: Int?)], old: ListStruct? = nil) { |
| 967 | let old = old ?? structure |
| 968 | fixIndentation(&structure, parents, bulletSize: 2) |
| 969 | fixItemEnds(&structure) |
| 970 | let prevs = structure.prevs |
| 971 | fixBullets(&structure, prevs) |
| 972 | fixIndentation(&structure, parents) |
| 973 | fixCheckboxes(&structure, parents) |
| 974 | applyStruct(structure, old) |
| 975 | } |
| 976 | } |