Sources/OrgCore/Commands/OrgList.swift
1009 lines · 44859 bytes
61 symbols in this file
ListItemListStructitemindexindbulletcheckboxendsetnextItempreviousItemhasChildsubtreechildrenparentlistBeginlastItemallItemsListTypelistTypeassocEndbulletStringregexlookingrangessearchForwardsearchBackwardindentToindentLineToListContextKindlistContextinItemassocAtPointgrouplistStructendBeforeBlanktrimToBeginitemEndBeforeBlankswapItemsseparatingBlankLinescountBlankscountLinesinsertItemdeleteItemSendDestinationsendItemstructOutdentstructIndentrememberstringMatchedincrementBulletfixBulletsfinduseAlphabeticalBulletsfixIndentationfixCheckboxesfixItemEndsapplyStructshiftBodymodifywriteStruct
1import 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`.
11struct 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.
23typealias ListStruct = [ListItem]
24
25extension 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.
136private 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.
146func 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
153extension 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 /// `org-item-re`.
251 var itemPattern: String {
252 let alpha = options.listAllowAlphabetical ? "|[A-Za-z]" : ""
253 return "([ \\t]*([-+]|(([0-9]+\(alpha))[.)]))|[ \\t]+\\*)([ \\t]+|$)"
254 }
255
256 var itemBeginningPattern: String { "^" + itemPattern }
257 static let fullItemPattern = "^[ \\t]*((?:[-+*]|(?:[0-9]+|[A-Za-z])[.)])(?:[ \\t]+|$))(?:\\[@(?:start:)?([0-9]+|[A-Za-z])\\][ \\t]*)?(?:(\\[[ X-]\\])(?:[ \\t]+|$))?(?:(.*)[ \\t]+::(?:[ \\t]+|$))?"
258 static let listEndPattern = "^[ \\t]*\\n[ \\t]*\\n"
259 static let drawerPattern = "^[ \\t]*:([-_\\w]+):[ \\t]*$"
260 static let forbiddenBlocks = ["example", "verse", "src", "export"]
261
262 enum ListContextKind: Equatable { case none, drawer, block(String), invalid }
263
264 /// `org-list-context`.
265 func listContext() -> (up: Int, down: Int, kind: ListContextKind) {
266 let savedMatch = lastMatch
267 defer { lastMatch = savedMatch }
268 return saveExcursion {
269 beginningOfLine()
270 let pos = point
271 var up = saveExcursion { (try? backToHeading()) ?? 0 }
272 var down = saveExcursion { outlineNextHeading() ? point : length }
273 var kind = ListContextKind.none
274 let drawerEnd = "^[ \\t]*:END:"
275 if looking(Self.drawerPattern) == nil, looking(drawerEnd) == nil {
276 let found = saveExcursion { () -> (Int, Int)? in
277 guard searchBackward(Self.drawerPattern, bound: up) != nil else { return nil }
278 let begin = lineEnd(point) + 1
279 let end = searchForward(drawerEnd, bound: down).map { $0[0]!.lowerBound - 1 } ?? down
280 return end >= pos ? (begin, end) : nil
281 }
282 if let (begin, end) = found {
283 (up, down, kind) = (begin, end, .drawer)
284 }
285 }
286 let blockPattern = "^[ \\t]*#\\+(begin|end)_"
287 if looking(blockPattern) == nil {
288 let found = saveExcursion { () -> (Int, Int, String)? in
289 guard searchBackward(blockPattern, bound: up) != nil else { return nil }
290 let begin = lineEnd(point) + 1
291 guard let open = looking("^[ \\t]*#\\+begin_(\\S+)") else { return nil }
292 let type = substring(open[1]!.lowerBound, open[1]!.upperBound).lowercased()
293 point = begin
294 let close = searchForward(blockPattern, bound: down)
295 let end = close.map { _ in lineStart(point) - 1 } ?? down
296 guard end >= pos else { return nil }
297 let word = close.map { substring($0[1]!.lowerBound, $0[1]!.upperBound) } ?? type
298 return word.lowercased() == "end" ? (begin, end, type) : nil
299 }
300 if let (begin, end, type) = found {
301 (up, down, kind) = (begin, end, Self.forbiddenBlocks.contains(type) ? .invalid : .block(type))
302 }
303 }
304 return (up, down, kind)
305 }
306 }
307
308 /// `org-in-item-p`: the start of the item holding point, or nil.
309 func inItem() -> Int? {
310 saveExcursion {
311 beginningOfLine()
312 let context = listContext()
313 var reference = looking("^[ \\t]*$") != nil ? 10000 : currentTextIndentation
314 if context.kind == .invalid { return nil }
315 if looking(itemPattern) != nil { return point }
316 // Inside `org-list-end-re`: start above it.
317 let windowStart = saveExcursion { forwardLine(-2); return point }
318 let windowEnd = saveExcursion { forwardLine(2); return lineEnd(point) }
319 let expression = regex(Self.listEndPattern, caseInsensitive: true)
320 for match in expression.matches(in: string, options: Self.searchOptions, range: NSRange(windowStart..<windowEnd)) {
321 let range = match.range.location..<NSMaxRange(match.range)
322 if point >= range.lowerBound, point < range.upperBound {
323 point = range.lowerBound
324 forwardLine(-1)
325 break
326 }
327 }
328 while true {
329 let ind = currentTextIndentation
330 if looking(itemPattern) != nil, ind < reference { return point }
331 if point <= context.up { return nil }
332 if looking(Self.listEndPattern) != nil { return nil }
333 if looking("^[ \\t]*#\\+end_") != nil, searchBackward("^[ \\t]*#\\+begin_", bound: context.up) != nil {
334 continue
335 }
336 if looking("^[ \\t]*:END:") != nil, searchBackward(Self.drawerPattern, bound: context.up) != nil {
337 beginningOfLine()
338 continue
339 }
340 if looking("^[ \\t]*$") != nil {
341 forwardLine(-1)
342 continue
343 }
344 if ind == 0 { return nil }
345 if ind < reference { reference = ind }
346 forwardLine(-1)
347 }
348 }
349 }
350
351 /// `org-at-item-p`: an item line where the element parser sees one, so not in blocks
352 /// whose contents aren't elements.
353 var atItem: Bool {
354 saveExcursion {
355 beginningOfLine()
356 switch listContext().kind {
357 case .invalid, .block("comment"), .block("verse"): return false
358 default: return looking(itemPattern) != nil
359 }
360 }
361 }
362
363 // MARK: - Structure
364
365 private func assocAtPoint(_ ind: Int) -> ListItem {
366 let match = looking(Self.fullItemPattern)!
367 func group(_ i: Int) -> String? { match[i].map { substring($0.lowerBound, $0.upperBound) } }
368 let bullet = group(1) ?? ""
369 let tag = bullet.range(of: "[-+*]", options: .regularExpression) != nil ? group(4) : nil
370 return ListItem(pos: point, ind: ind, bullet: bullet, counter: group(2), checkbox: group(3), tag: tag, end: 0)
371 }
372
373 /// `org-list-struct` for the list holding point, which is at an item.
374 func listStruct() -> ListStruct {
375 saveExcursion {
376 beginningOfLine()
377 let context = listContext()
378 var textMinInd = 10000
379 var beginCell = (pos: point, ind: currentTextIndentation)
380 var items: [ListItem] = []
381 var ends: [(ind: Int, pos: Int)] = []
382 func endBeforeBlank() -> Int {
383 skipBackward(Self.whitespace)
384 return min(lineEnd(point) + 1, context.down)
385 }
386 func trimToBegin() {
387 if let i = items.firstIndex(where: { $0.pos == beginCell.pos }) { items = Array(items[i...]) } else { items = [] }
388 }
389 saveExcursion {
390 while true {
391 let ind = currentTextIndentation
392 if point <= context.up {
393 if looking(itemPattern) == nil {
394 trimToBegin()
395 } else {
396 beginCell = (point, ind)
397 items.insert(assocAtPoint(ind), at: 0)
398 }
399 return
400 }
401 if looking(Self.listEndPattern) != nil {
402 trimToBegin()
403 return
404 }
405 if looking(itemPattern) != nil {
406 items.insert(assocAtPoint(ind), at: 0)
407 ends.insert((ind, point), at: 0)
408 if ind < textMinInd { beginCell = (point, ind) }
409 forwardLine(-1)
410 } else if looking("^[ \\t]*#\\+end_") != nil, searchBackward("^[ \\t]*#\\+begin_", bound: context.up) != nil {
411 } else if looking("^[ \\t]*:END:") != nil, searchBackward(Self.drawerPattern, bound: context.up) != nil {
412 beginningOfLine()
413 } else if looking("^[ \\t]*$") != nil {
414 forwardLine(-1)
415 } else if ind == 0 {
416 trimToBegin()
417 return
418 } else {
419 if ind < textMinInd { textMinInd = ind }
420 ends.insert((ind, point), at: 0)
421 forwardLine(-1)
422 }
423 }
424 }
425 var items2: [ListItem] = []
426 var ends2: [(ind: Int, pos: Int)] = []
427 while true {
428 let ind = currentTextIndentation
429 if point >= context.down {
430 ends2.append((0, endBeforeBlank()))
431 break
432 }
433 if looking(Self.listEndPattern) != nil {
434 ends2.append((0, point))
435 break
436 }
437 if looking(itemPattern) != nil {
438 items2.append(assocAtPoint(ind))
439 ends2.append((ind, point))
440 forwardLine(1)
441 } else if looking("^[ \\t]*$") != nil {
442 forwardLine(1)
443 } else if ind <= beginCell.ind {
444 ends2.append((0, endBeforeBlank()))
445 break
446 } else {
447 if let last = items2.last, ind <= last.ind { ends2.append((ind, point)) }
448 if looking("^[ \\t]*#\\+begin_") != nil, searchForward("^[ \\t]*#\\+end_", bound: context.down) != nil {
449 } else if looking(Self.drawerPattern) != nil, searchForward("^[ \\t]*:END:", bound: context.down) != nil {
450 }
451 forwardLine(1)
452 }
453 }
454 var structure = items + Array(items2.dropFirst())
455 assocEnd(&structure, ends + Array(ends2.dropFirst()))
456 return structure
457 }
458 }
459
460 func itemEndBeforeBlank(_ pos: Int, _ structure: ListStruct) -> Int {
461 saveExcursion {
462 point = structure.end(pos)
463 skipBackward(Self.whitespace)
464 return lineEnd(point)
465 }
466 }
467
468 // MARK: - Editing structures
469
470 /// `org-list-swap-items`.
471 func swapItems(_ a: Int, _ b: Int, _ structure: ListStruct) -> ListStruct {
472 saveExcursion {
473 let endANoBlank = itemEndBeforeBlank(a, structure)
474 let endBNoBlank = itemEndBeforeBlank(b, structure)
475 let endA = structure.end(a), endB = structure.end(b)
476 let sizeA = endANoBlank - a, sizeB = endBNoBlank - b
477 let bodyA = substring(a, endANoBlank), bodyB = substring(b, endBNoBlank)
478 let between = substring(endANoBlank, b)
479 let subA = [a] + structure.subtree(a), subB = [b] + structure.subtree(b)
480 point = a
481 deleteRegion(a, endBNoBlank)
482 insert(bodyB + between + bodyA)
483 var result = structure
484 for i in result.indices {
485 let pos = result[i].pos, end = result[i].end
486 if pos < a {
487 } else if subA.contains(pos) {
488 result[i].pos = pos + (endBNoBlank - endANoBlank)
489 result[i].end = end + (endBNoBlank - endANoBlank)
490 if end == endA { result[i].end = endB }
491 } else if subB.contains(pos) {
492 result[i].pos = pos + a - b
493 result[i].end = end + (a - b)
494 if end == endB { result[i].end = a + sizeB + (endA - endANoBlank) }
495 } else if pos < b {
496 result[i].pos = pos + (sizeB - sizeA)
497 result[i].end = end + (sizeB - sizeA)
498 }
499 }
500 return result.sorted { $0.pos < $1.pos }
501 }
502 }
503
504 /// `org-list-separating-blank-lines-number`, with point at the item.
505 func separatingBlankLines(_ pos: Int, _ structure: ListStruct, _ prevs: [Int: Int]) -> Int {
506 saveExcursion {
507 let item = point
508 func countBlanks() -> Int {
509 saveExcursion {
510 point = lineStart(point)
511 let start = point
512 skipBackward(Self.whitespace)
513 forwardLine(1)
514 return countLines(point, start)
515 }
516 }
517 if let next = structure.nextItem(item, prevs) {
518 point = next
519 return countBlanks()
520 }
521 if structure.previousItem(item, prevs) != nil { return countBlanks() }
522 if pos > itemEndBeforeBlank(item, structure) {
523 let blanks = saveExcursion { point = pos; return countBlanks() }
524 if blanks > 0 { return blanks }
525 }
526 point = structure.top
527 if searchForward("^[ \\t]*$", bound: itemEndBeforeBlank(item, structure)) != nil { return 1 }
528 return 0
529 }
530 }
531
532 /// `count-lines` between two positions.
533 func countLines(_ start: Int, _ end: Int) -> Int {
534 let (lower, upper) = (min(start, end), max(start, end))
535 guard upper > lower else { return 0 }
536 let newlines = substring(lower, upper).utf16.filter { $0 == 10 }.count
537 return character(at: upper - 1) == Self.newline ? newlines : newlines + 1
538 }
539
540 /// `org-list-insert-item`. Leaves point at the new item.
541 func insertItem(at position: Int, _ structure: ListStruct, _ prevs: [Int: Int], checkbox: Bool, afterBullet: String?, splitLine: Bool = true) -> ListStruct {
542 var pos = position
543 var found: Int?
544 var stopped = false
545 for entry in structure {
546 if entry.pos > pos {
547 stopped = true
548 break
549 }
550 if entry.end < pos { continue }
551 found = entry.pos
552 }
553 guard let item = stopped ? found : (found ?? structure.last?.pos) else { return structure }
554 let itemEnd = structure.end(item)
555 let itemEndNoBlank = itemEndBeforeBlank(item, structure)
556 point = item
557 let match = looking(Self.fullItemPattern)!
558 let bodyStart: Int
559 if match[4] == nil {
560 bodyStart = match[0]!.upperBound
561 } else if substring(match[1]!.lowerBound, match[1]!.upperBound).contains(where: { $0 == "." || $0 == ")" }) {
562 bodyStart = match[4]!.lowerBound
563 } else {
564 bodyStart = saveExcursion { point = match[4]!.upperBound; skipForward(Self.blanks); return point }
565 }
566 let before = pos <= bodyStart
567 let blanks = separatingBlankLines(pos, structure, prevs)
568 let ind = structure.ind(item)
569 let bullet = bulletString(structure.bullet(item))
570 let box = checkbox ? "[ ]" : nil
571 var textCut: String?
572 if !before, splitLine {
573 point = pos
574 if itemEnd < pos { deleteRegion(itemEnd - 1, lineEnd(point)) }
575 skipBackward(Self.whitespace)
576 pos = saveExcursion { skipForward(Self.blanks); return point }
577 textCut = substring(point, itemEndNoBlank)
578 deleteRegion(point, itemEndNoBlank)
579 }
580 var body = bullet + (box.map { $0 + " " } ?? "") + (afterBullet ?? "")
581 if let textCut {
582 // `string-match' sets the match data to the string's blanks.
583 let blanks = textCut.prefix { $0 == " " || $0 == "\t" }.utf16.count
584 if blanks > 0 { lastMatch = [0..<blanks] }
585 body += textCut.replacingOccurrences(of: "^[ \\t]+", with: "", options: .regularExpression)
586 }
587 let separator = String(repeating: "\n", count: blanks + 1)
588 let itemSize = ind + (body as NSString).length + (separator as NSString).length
589 let sizeOffset = itemSize - ((textCut ?? "") as NSString).length
590 point = item
591 indentTo(ind)
592 insert(body + separator)
593 var result = structure
594 for i in result.indices {
595 let p = result[i].pos, end = result[i].end
596 if p < item {
597 if end > item { result[i].end = end + sizeOffset }
598 } else if p == item, !before, splitLine {
599 result[i].pos = p + itemSize
600 result[i].end = end + sizeOffset
601 } else if splitLine, !before, p >= pos, p <= itemEndNoBlank {
602 let offset = pos - item - ind - (bullet as NSString).length - ((afterBullet ?? "") as NSString).length
603 result[i].pos = p - offset
604 result[i].end = end - offset
605 } else {
606 result[i].pos = p + sizeOffset
607 result[i].end = end + sizeOffset
608 }
609 }
610 result.append(ListItem(pos: item, ind: ind, bullet: bullet, counter: nil, checkbox: box, tag: nil, end: item + itemSize))
611 result.sort { $0.pos < $1.pos }
612 if before {
613 point = item
614 } else {
615 result = swapItems(item, item + itemSize, result)
616 point = result.nextItem(item, result.prevs)!
617 }
618 return result
619 }
620
621 /// `org-list-delete-item`.
622 func deleteItem(_ item: Int, _ structure: ListStruct) -> ListStruct {
623 let end = structure.end(item)
624 let begin = structure.bottom == end ? saveExcursion { () -> Int in
625 point = item
626 skipBackward(Self.whitespace)
627 return min(lineEnd(point) + 1, length)
628 } : item
629 deleteRegion(begin, end)
630 let size = end - begin
631 return structure.compactMap { entry in
632 var entry = entry
633 if entry.pos < item {
634 if entry.end == item { entry.end = begin } else if entry.end > item { entry.end -= size }
635 return entry
636 }
637 if entry.pos < end { return nil }
638 entry.pos -= size
639 entry.end -= size
640 return entry
641 }
642 }
643
644 enum SendDestination { case begin, end }
645
646 /// `org-list-send-item` to the start or end of the item's sub-list.
647 func sendItem(_ item: Int, to destination: SendDestination, _ structure: ListStruct) -> ListStruct {
648 let prevs = structure.prevs
649 let itemEnd = structure.end(item)
650 let bulletEnd = saveExcursion { () -> Int in
651 point = item
652 return looking("[ \\t]*" + NSRegularExpression.escapedPattern(for: structure.bullet(item)))![0]!.upperBound
653 }
654 let body = substring(bulletEnd, itemEnd).trimmingCharacters(in: .whitespacesAndNewlines)
655 let insertion: Int
656 switch destination {
657 case .begin:
658 insertion = structure.listBegin(item, prevs)
659 case .end:
660 insertion = saveExcursion { () -> Int in
661 point = structure.lastItem(item, prevs)
662 return lineEnd(point)
663 }
664 }
665 guard item != insertion else { return structure }
666 let itemMarker = marker(item)
667 defer { release(itemMarker) }
668 var result = insertItem(at: insertion, structure, prevs, checkbox: false, afterBullet: body, splitLine: false)
669 let moved = [itemMarker.position] + result.subtree(itemMarker.position)
670 let newEnd = result.end(point)
671 let oldEnd = result.end(itemMarker.position)
672 let newItem = point
673 let shift = point - itemMarker.position
674 result.removeAll { $0.pos == newItem }
675 let copies = moved.map { pos -> ListItem in
676 var cell = result.item(pos)!
677 cell.end = cell.end == oldEnd ? newEnd : cell.end + shift
678 cell.pos = pos + shift
679 return cell
680 }
681 result = (result + copies).sorted { $0.pos < $1.pos }
682 return deleteItem(itemMarker.position, result)
683 }
684
685 // MARK: - Indentation changes
686
687 /// `org-list-struct-outdent`.
688 func structOutdent(_ start: Int, _ end: Int, _ structure: ListStruct, _ parents: [(item: Int, parent: Int?)]) throws -> [(item: Int, parent: Int?)] {
689 var acc: [Int: Int] = [:]
690 return try parents.map { cell in
691 let (item, parent) = cell
692 if item < start { return cell }
693 if item >= end {
694 if let parent, let convert = acc[parent] { return (item, convert) }
695 return cell
696 }
697 guard let parent else { throw UserError(message: "Cannot outdent top-level items") }
698 if parent >= start {
699 acc[parent] = item
700 return cell
701 }
702 acc[parent] = item
703 return (item, structure.parent(parent, parents))
704 }
705 }
706
707 /// `org-list-struct-indent`.
708 func structIndent(_ start: Int, _ end: Int, _ structure: ListStruct, _ parents: [(item: Int, parent: Int?)], _ prevs: [Int: Int]) throws -> [(item: Int, parent: Int?)] {
709 var acc: [(item: Int, parent: Int?)] = []
710 func remember(_ cell: (item: Int, parent: Int?)) -> (item: Int, parent: Int?) {
711 acc.insert(cell, at: 0)
712 return cell
713 }
714 return try parents.map { cell in
715 let (item, parent) = cell
716 if item < start { return cell }
717 if item >= end {
718 if let convert = acc.first(where: { $0.item == parent }) { return (item, convert.parent) }
719 return cell
720 }
721 let previous = structure.previousItem(item, prevs)
722 guard let previous else {
723 if parent == nil || parent! < start { throw UserError(message: "Cannot indent the first item of a list") }
724 return remember(cell)
725 }
726 if previous < start { return remember((item, previous)) }
727 return remember((item, acc.first { $0.item == previous }?.parent ?? nil))
728 }
729 }
730
731 // MARK: - Repairs
732
733 /// Records a successful `string-match' on a string in the match data.
734 private func stringMatched(_ string: String, _ range: Range<String.Index>) {
735 let start = string.utf16.distance(from: string.startIndex, to: range.lowerBound)
736 lastMatch = [start..<(start + string[range].utf16.count)]
737 }
738
739 /// `org-list-inc-bullet-maybe`.
740 func incrementBullet(_ bullet: String) -> String {
741 if let range = bullet.range(of: "[0-9]+", options: .regularExpression) {
742 stringMatched(bullet, range)
743 return bullet.replacingCharacters(in: range, with: String(Int(bullet[range])! + 1))
744 }
745 if let range = bullet.range(of: "[A-Za-z]", options: .regularExpression) {
746 stringMatched(bullet, range)
747 let scalar = bullet[range].unicodeScalars.first!.value + 1
748 return bullet.replacingCharacters(in: range, with: String(Character(Unicode.Scalar(scalar)!)))
749 }
750 return bullet
751 }
752
753 /// `org-list-struct-fix-bul` (alphabetical bullets off).
754 func fixBullets(_ structure: inout ListStruct, _ prevs: [Int: Int]) {
755 func find(_ pattern: String, _ string: String) -> Range<String.Index>? {
756 guard let range = string.range(of: pattern, options: .regularExpression) else { return nil }
757 stringMatched(string, range)
758 return range
759 }
760 for pos in structure.map(\.pos) {
761 let previous = prevs[pos]
762 let counter = structure.item(pos)!.counter
763 let bullet = structure.bullet(pos)
764 let previousBullet = previous.map { structure.bullet($0) }
765 let alphap = previous == nil && useAlphabeticalBullets(pos, structure, prevs)
766 let new: String
767 if previous != nil, let counter, let previousBullet, find("[a-zA-Z]", counter) != nil, find("[a-zA-Z]", previousBullet) != nil {
768 let lower = find("[a-z]", previousBullet)
769 let range = lower ?? find("[A-Z]", previousBullet)!
770 new = previousBullet.replacingCharacters(in: range, with: lower != nil ? counter.lowercased() : counter.uppercased())
771 } else if previous != nil, let counter, let previousBullet, let counterRange = find("[0-9]+", counter),
772 let range = find("[0-9]+", previousBullet) {
773 new = previousBullet.replacingCharacters(in: range, with: counter[counterRange])
774 } else if let previousBullet {
775 new = incrementBullet(previousBullet)
776 } else if let counter, useAlphabeticalBullets(pos, structure, prevs), find("[A-Za-z]", counter) != nil, find("[A-Za-z]", bullet) != nil {
777 let lower = find("[a-z]", bullet)
778 let range = lower ?? find("[A-Z]", bullet)!
779 new = bullet.replacingCharacters(in: range, with: lower != nil ? counter.lowercased() : counter.uppercased())
780 } else if let counter, let counterRange = find("[0-9]+", counter), let range = find("[0-9]+", bullet) {
781 new = bullet.replacingCharacters(in: range, with: counter[counterRange])
782 } else if alphap, let range = find("[A-Z]", bullet) {
783 new = bullet.replacingCharacters(in: range, with: "A")
784 } else if alphap, let range = find("[a-z]", bullet) {
785 new = bullet.replacingCharacters(in: range, with: "a")
786 } else if let range = find("([0-9]+|[A-Za-z])", bullet) {
787 new = bullet.replacingCharacters(in: range, with: "1")
788 } else {
789 new = bullet
790 }
791 structure.set(pos) { $0.bullet = bulletString(new) }
792 }
793 }
794
795 /// `org-list-use-alpha-bul-p`: the list starting at `first` fits in the alphabet.
796 func useAlphabeticalBullets(_ first: Int, _ structure: ListStruct, _ prevs: [Int: Int]) -> Bool {
797 guard options.listAllowAlphabetical else { return false }
798 var item: Int? = first
799 var ascii = 64
800 while let current = item {
801 if let counter = structure.item(current)?.counter, let letter = counter.first, letter.isASCII, letter.isLetter {
802 ascii = Int(letter.uppercased().unicodeScalars.first!.value)
803 } else {
804 ascii += 1
805 }
806 if ascii > 90 { return false }
807 item = structure.nextItem(current, prevs)
808 }
809 return true
810 }
811
812 /// `org-list-struct-fix-ind`.
813 func fixIndentation(_ structure: inout ListStruct, _ parents: [(item: Int, parent: Int?)], bulletSize: Int? = nil) {
814 let topInd = structure.ind(structure.top)
815 for pos in structure.dropFirst().map(\.pos) {
816 if let parent = structure.parent(pos, parents) {
817 let ind = (bulletSize ?? (structure.bullet(parent) as NSString).length) + structure.ind(parent)
818 structure.set(pos) { $0.ind = ind }
819 } else {
820 structure.set(pos) { $0.ind = topInd }
821 }
822 }
823 }
824
825 /// `org-list-struct-fix-box` without ORDERED.
826 func fixCheckboxes(_ structure: inout ListStruct, _ parents: [(item: Int, parent: Int?)]) {
827 var parentList: [Int] = []
828 for pos in structure.map(\.pos) {
829 guard let parent = structure.parent(pos, parents), structure.checkbox(parent) != nil, !parentList.contains(parent) else { continue }
830 parentList.insert(parent, at: 0)
831 }
832 // `sort` in Emacs is stable; ties keep their order.
833 let ordered = parentList.enumerated().sorted { lhs, rhs in
834 let (a, b) = (structure.ind(lhs.element), structure.ind(rhs.element))
835 return a != b ? a > b : lhs.offset < rhs.offset
836 }.map(\.element)
837 for item in ordered {
838 let boxes = structure.children(item, parents).map { structure.checkbox($0) }
839 let new: String?
840 if boxes.contains("[ ]"), boxes.contains("[X]") {
841 new = "[-]"
842 } else if boxes.contains("[-]") {
843 new = "[-]"
844 } else if boxes.contains("[X]") {
845 new = "[X]"
846 } else if boxes.contains("[ ]") {
847 new = "[ ]"
848 } else {
849 new = structure.checkbox(item)
850 }
851 structure.set(item) { $0.checkbox = new }
852 }
853 }
854
855 /// `org-list-struct-fix-item-end`.
856 func fixItemEnds(_ structure: inout ListStruct) {
857 var endList: [(ind: Int, pos: Int)] = []
858 var accEnd: [(end: Int, pos: Int)] = []
859 for item in structure {
860 if structure.item(item.end) == nil {
861 let up = accEnd.first { item.end < $0.end }?.pos
862 endList.insert((up.map { structure.ind($0) + 2 } ?? 0, item.end), at: 0)
863 }
864 endList.insert((item.ind, item.pos), at: 0)
865 accEnd.insert((item.end, item.pos), at: 0)
866 }
867 let sorted = endList.enumerated().sorted { $0.element.pos != $1.element.pos ? $0.element.pos < $1.element.pos : $0.offset < $1.offset }.map(\.element)
868 assocEnd(&structure, sorted)
869 }
870
871 /// The previous match data, with groups it lacks as unmatched.
872 private var staleMatch: [Range<Int>?] {
873 lastMatch + Array(repeating: nil, count: max(0, 6 - lastMatch.count))
874 }
875
876 /// `org-list-struct-apply-struct`.
877 func applyStruct(_ structure: ListStruct, _ old: ListStruct) {
878 let origin = marker(point)
879 defer {
880 point = origin.position
881 release(origin)
882 }
883 func shiftBody(end: Int, begin: Int, delta: Int, ind: Int?) {
884 point = end
885 skipBackward(Self.whitespace)
886 beginningOfLine()
887 while point > begin || (point == begin && looking(itemPattern) == nil) {
888 if looking("^[ \\t]*\\S", setsMatch: false) != nil {
889 indentLineTo(max(currentTextIndentation + delta, ind.map { $0 + 1 } ?? -1))
890 }
891 if point == 0 { break }
892 forwardLine(-1)
893 }
894 }
895 func modify(_ item: Int) {
896 point = item
897 let newInd = structure.ind(item)
898 let oldInd = currentTextIndentation
899 let newBullet = bulletString(structure.bullet(item))
900 let oldBullet = old.bullet(item)
901 let newBox = structure.checkbox(item)
902 // A failed `looking-at' leaves the previous match data, which org then uses; this
903 // happens when a split item takes a sub-list with it.
904 var match = looking(Self.fullItemPattern) ?? staleMatch
905 if oldBullet != newBullet {
906 var keepSpace = ""
907 saveExcursion {
908 let bulletRange = match[1]!
909 if bulletRange.lowerBound <= origin.position, origin.position <= bulletRange.upperBound {
910 let spaces = saveExcursion { () -> String? in
911 point = origin.position
912 return looking("[ \\t]+", setsMatch: false).map { substring($0[0]!.lowerBound, $0[0]!.upperBound) }
913 }
914 keepSpace = spaces ?? ""
915 }
916 replace(bulletRange, with: "")
917 point = bulletRange.lowerBound
918 insertBeforeMarkers(newBullet)
919 insert(keepSpace)
920 }
921 }
922 point = item
923 match = looking(Self.fullItemPattern) ?? staleMatch
924 let oldBox = match[3].map { substring($0.lowerBound, $0.upperBound) }
925 if oldBox == newBox {
926 } else if let boxRange = match[3], let newBox {
927 replace(boxRange, with: newBox)
928 } else if oldBox != nil {
929 let box = looking(".*?([ \\t]*\\[[ X-]\\])")!
930 replace(box[1]!, with: "")
931 } else if let newBox {
932 let counterEnd = match[2]?.upperBound
933 point = counterEnd.map { $0 + 1 } ?? match[1]!.upperBound
934 insert(newBox + (counterEnd == nil ? " " : ""))
935 }
936 if newInd != oldInd {
937 point = lineStart(point)
938 let start = point
939 skipForward(Self.blanks)
940 deleteRegion(start, point)
941 indentTo(newInd)
942 }
943 }
944 var endList: [(end: Int, up: Int?)] = []
945 var accEnd: [(end: Int, pos: Int)] = []
946 var shifts: [Int: (delta: Int, ind: Int)] = [:]
947 for entry in old {
948 let pos = entry.pos
949 let indNew = structure.ind(pos), indOld = entry.ind
950 let delta = (indNew + (structure.bullet(pos) as NSString).length) - (indOld + (entry.bullet as NSString).length)
951 shifts[pos] = (delta, indNew)
952 if old.item(entry.end) == nil {
953 endList.insert((entry.end, accEnd.first { entry.end < $0.end }?.pos), at: 0)
954 }
955 accEnd.insert((entry.end, pos), at: 0)
956 }
957 var uniqueEnds: [Int] = []
958 for end in endList.map(\.end) where !uniqueEnds.contains(end) { uniqueEnds.append(end) }
959 var allEnds = (old.map(\.pos) + uniqueEnds).sorted()
960 accEnd.reverse()
961 var slices: [(down: Int, up: Int, delta: Int, ind: Int?)] = []
962 while allEnds.count > 1 {
963 let up = allEnds.removeFirst()
964 let down = allEnds[0]
965 let isItem = structure.item(up) != nil
966 let delta: Int
967 if isItem {
968 delta = shifts[up]!.delta
969 } else {
970 let child = accEnd.first { $0.end == up }!.pos
971 let ind = structure.ind(child)
972 var minInd = Int.max
973 saveExcursion {
974 point = up
975 while point < down {
976 if looking("[ \\t]*$", setsMatch: false) == nil {
977 minInd = min(currentTextIndentation, minInd)
978 if let open = looking("#\\+BEGIN(:|_\\S+)") {
979 let name = NSRegularExpression.escapedPattern(for: substring(open[1]!.lowerBound, open[1]!.upperBound))
980 searchForward("^[ \\t]*#\\+END\(name)[ \\t]*$", bound: down)
981 } else if looking(Self.drawerPattern) != nil {
982 searchForward("^[ \\t]*:END:[ \\t]*$", bound: down)
983 }
984 }
985 if forwardLine(1) != 0 { break }
986 }
987 }
988 delta = ind - minInd
989 }
990 slices.insert((down, up, delta, isItem ? shifts[up]!.ind : nil), at: 0)
991 }
992 for slice in slices {
993 if slice.delta != 0 { shiftBody(end: slice.down, begin: slice.up, delta: slice.delta, ind: slice.ind) }
994 if let cell = structure.item(slice.up), cell != old.item(slice.up) { modify(slice.up) }
995 }
996 }
997
998 /// `org-list-write-struct`.
999 func writeStruct(_ structure: inout ListStruct, _ parents: [(item: Int, parent: Int?)], old: ListStruct? = nil) {
1000 let old = old ?? structure
1001 fixIndentation(&structure, parents, bulletSize: 2)
1002 fixItemEnds(&structure)
1003 let prevs = structure.prevs
1004 fixBullets(&structure, prevs)
1005 fixIndentation(&structure, parents)
1006 fixCheckboxes(&structure, parents)
1007 applyStruct(structure, old)
1008 }
1009}