Sources/OrgCore/Commands/StructureCommands.swift
438 lines · 16877 bytes
1import Foundation
2
3// Structure commands, ported from Org 9.8.7 onto `EmacsBuffer` and matched to Emacs by the
4// oracle tests. Defaults as in org: `org-blank-before-new-entry` heading `auto`,
5// `org-M-RET-may-split-line` t, `org-auto-align-tags` t.
6
7extension EmacsBuffer {
8 /// `org--blank-before-heading-p` for headings.
9 var blankBeforeHeading: Bool {
10 saveExcursion {
11 if beforeFirstHeading, !outlineNextHeading() { return false }
12 _ = try? backToHeading()
13 if !bobp { return previousLineEmpty }
14 if outlineNextHeading() { return previousLineEmpty }
15 skipBackward(Self.blanks)
16 return bolp ? previousLineEmpty : false
17 }
18 }
19
20 /// `org-N-empty-lines-before-current`.
21 func emptyLinesBeforeCurrent(_ n: Int) {
22 let column = currentColumn
23 forwardLine(0)
24 if !bobp {
25 let start = saveExcursion {
26 skipBackward(Self.whitespace)
27 return lineEnd(point)
28 }
29 deleteRegion(start, point - 1)
30 }
31 insert(String(repeating: "\n", count: n))
32 moveToColumn(column)
33 }
34
35 /// The blank line `org-insert-heading` keeps before the next heading.
36 func addBlankAfter(_ blank: Bool) {
37 saveExcursion {
38 endOfLine()
39 guard !eobp else { return }
40 point += 1
41 if blank, atHeading { insert("\n") }
42 }
43 }
44
45 /// `org-insert-heading` with no prefix, or with `respectContent` as C-RET.
46 func insertHeading(respectContent: Bool, hidden: [Range<Int>]) throws {
47 let blank = blankBeforeHeading
48 let level = currentLevel
49 let stars = String(repeating: "*", count: level ?? 1)
50 let invisible = hidden.contains { $0.contains(max(point - 1, 0)) }
51 if respectContent || invisible {
52 if level == nil {
53 outlineNextHeading()
54 } else {
55 try backToHeading()
56 endOfSubtree(toHeading: true)
57 }
58 if !bolp { insert("\n") }
59 if blank, saveExcursion({ point -= 1; return beforeFirstHeading }) {
60 insert("\n")
61 point -= 1
62 }
63 if level == nil, !eobp, !bobp {
64 if atHeading { insert("\n") }
65 point -= 1
66 }
67 if !(blank && previousLineEmpty) { emptyLinesBeforeCurrent(blank ? 1 : 0) }
68 insert(stars + " \n")
69 point -= 1
70 addBlankAfter(blank)
71 } else if atHeading {
72 if bolp {
73 if blank { saveExcursion { insert("\n") } }
74 saveExcursion { insert(stars + " \n") }
75 if !(blank && previousLineEmpty) { emptyLinesBeforeCurrent(blank ? 1 : 0) }
76 endOfLine()
77 } else if let match = saveExcursion({ () -> [Range<Int>?]? in beginningOfLine(); return lookingAt(complexHeadingPattern) }),
78 let title = match[4], title.lowerBound <= point, point <= title.upperBound {
79 let split = substring(point, title.upperBound)
80 deleteRegion(point, title.upperBound)
81 if let blanks = lookingAt("[ \\t]*$") {
82 deleteRegion(blanks[0]!.lowerBound, blanks[0]!.upperBound)
83 } else {
84 alignTags(onLineAt: point)
85 }
86 endOfLine()
87 if blank { insert("\n") }
88 insert("\n" + stars + " ")
89 addBlankAfter(blank)
90 if split.contains(where: { !$0.isWhitespace }) { insert(split) }
91 } else {
92 endOfLine()
93 if blank { insert("\n") }
94 insert("\n" + stars + " ")
95 addBlankAfter(blank)
96 }
97 } else if bolp {
98 insert(stars + " ")
99 if !(blank && previousLineEmpty) { emptyLinesBeforeCurrent(blank ? 1 : 0) }
100 addBlankAfter(blank)
101 } else {
102 insert("\n" + stars + " ")
103 if !(blank && previousLineEmpty) { emptyLinesBeforeCurrent(blank ? 1 : 0) }
104 addBlankAfter(blank)
105 }
106 }
107
108 /// `org-forward-heading-same-level` over all headings, visible or not.
109 func forwardHeadingSameLevel(_ count: Int) throws {
110 let backward = count < 0
111 if beforeFirstHeading {
112 if backward { point = 0 } else { outlineNextHeading() }
113 return
114 }
115 try backToHeading()
116 if !backward { endOfLine() }
117 let level = currentLevel ?? 0
118 var remaining = abs(count)
119 var result = point
120 while remaining > 0, let found = backward ? searchHeadingBackward() : searchHeadingForward() {
121 let l = headingLevel(atLine: found)!
122 if l < level {
123 remaining = 0
124 } else if l == level {
125 remaining -= 1
126 result = point
127 }
128 }
129 point = result
130 forwardLine(0)
131 }
132
133 /// `re-search-forward "^\\*+ " nil 'move`: point to the match end, or the buffer end.
134 /// Returns the heading's line start.
135 func searchHeadingForward() -> Int? {
136 var line = bolp ? point : lineEnd(point) + 1
137 while line <= length {
138 if let level = headingLevel(atLine: line) {
139 point = line + level + 1
140 return line
141 }
142 line = lineEnd(line) + 1
143 }
144 point = length
145 return nil
146 }
147
148 /// `re-search-backward "^\\*+ " nil 'move`: point to the match start, or the buffer start.
149 /// The match must end by point.
150 func searchHeadingBackward() -> Int? {
151 var line = lineStart(point)
152 if let level = headingLevel(atLine: line), line + level + 1 <= point {
153 point = line
154 return line
155 }
156 while line > 0 {
157 line = lineStart(line - 1)
158 if headingLevel(atLine: line) != nil {
159 point = line
160 return line
161 }
162 }
163 point = 0
164 return nil
165 }
166
167 /// `org-promote` or `org-demote` on the heading at point.
168 func changeLevel(by delta: Int) throws {
169 try saveExcursion {
170 try backToHeading()
171 let level = headingLevel(atLine: point)!
172 guard level + delta >= 1 else { throw UserError(message: "Cannot promote to level 0. UNDO to recover if necessary") }
173 replace(point..<(point + level + 1), with: String(repeating: "*", count: level + delta) + " ")
174 alignTags(onLineAt: point)
175 }
176 }
177
178 /// `org-fix-position-after-promote`.
179 func fixPositionAfterPromote() {
180 let position = point
181 let match = saveExcursion { () -> [Range<Int>?]? in
182 forwardLine(0)
183 return lookingAt(todoLinePattern)
184 }
185 guard let match, match[1]?.upperBound == position || match[2]?.upperBound == position else { return }
186 if eobp || eolp {
187 insert(" ")
188 } else if character(at: point) == 32 {
189 point += 1
190 }
191 }
192
193 /// `org-map-tree` over the subtree at point.
194 func mapTree(_ body: () throws -> Void) throws {
195 try backToHeading()
196 let level = outlineLevel
197 try saveExcursion {
198 try body()
199 while true {
200 outlineNextHeading()
201 guard outlineLevel > level, !eobp else { break }
202 try body()
203 }
204 }
205 }
206
207 /// `org-back-over-empty-lines` with blank lines before headings on (`auto`).
208 func backOverEmptyLines() {
209 let position = point
210 skipBackward(Self.whitespace)
211 forwardLine(1)
212 point = min(point, position)
213 }
214
215 /// `org-move-subtree-down`; negative counts move up.
216 func moveSubtree(_ count: Int) throws {
217 let column = currentColumn
218 try backToHeading()
219 let begin = point
220 endOfSubtree(toHeading: true)
221 if !eobp { point -= 1 }
222 outlineNextHeading()
223 let end = point
224 point = begin
225 for _ in 0..<abs(count) {
226 let found = count > 0 ? nextSibling() : previousSibling()
227 guard found != nil, atHeading, bolp else {
228 point = begin
229 throw UserError(message: "Cannot move past superior level or buffer limit")
230 }
231 }
232 if count > 0 {
233 endOfSubtree(toHeading: true)
234 saveExcursion {
235 backOverEmptyLines()
236 if !bolp { insert("\n") }
237 }
238 }
239 let insertion = marker(point)
240 defer { release(insertion) }
241 let subtree = substring(begin, end)
242 deleteRegion(begin, end)
243 if !bolp, character(at: point) == Self.newline { point += 1 }
244 let at = point
245 insertBeforeMarkers(subtree)
246 insertion.position = at
247 if !bolp { insert("\n") }
248 point = insertion.position
249 skipForward(Self.whitespace)
250 moveToColumn(column)
251 }
252}
253
254private func headingCommand(_ context: EditContext, _ body: (EmacsBuffer) throws -> Void) -> CommandStep {
255 commitBuffer(context, body)
256}
257
258// MARK: - Inserting
259
260/// M-RET on a heading or text: `org-insert-heading`.
261public struct InsertHeading: OrgCommand {
262 public init() {}
263 public var id: String { "org.heading.insert" }
264 public var title: String { "Insert Heading" }
265 public func applies(in context: EditContext) -> Bool { true }
266 public func run(in context: EditContext) -> CommandStep {
267 headingCommand(context) { try $0.insertHeading(respectContent: false, hidden: context.hidden) }
268 }
269}
270
271/// C-RET: `org-insert-heading-respect-content`.
272public struct InsertHeadingAfterSubtree: OrgCommand {
273 public init() {}
274 public var id: String { "org.heading.insert-after-subtree" }
275 public var title: String { "Insert Heading After Subtree" }
276 public func applies(in context: EditContext) -> Bool { true }
277 public func run(in context: EditContext) -> CommandStep {
278 headingCommand(context) { try $0.insertHeading(respectContent: true, hidden: context.hidden) }
279 }
280}
281
282/// M-S-RET on a heading or text: `org-insert-todo-heading`.
283public struct InsertTodoHeading: OrgCommand {
284 public init() {}
285 public var id: String { "org.heading.insert-todo" }
286 public var title: String { "Insert TODO Heading" }
287 public func applies(in context: EditContext) -> Bool { true }
288 public func run(in context: EditContext) -> CommandStep {
289 headingCommand(context) { buffer in
290 try buffer.insertHeading(respectContent: false, hidden: context.hidden)
291 let previous = buffer.saveExcursion { () -> String? in
292 try? buffer.forwardHeadingSameLevel(-1)
293 guard let match = buffer.lookingAt(buffer.todoLinePattern), let keyword = match[2] else { return nil }
294 return buffer.substring(keyword.lowerBound, keyword.upperBound)
295 }
296 let keyword = previous.flatMap { context.tree.settings.isDone($0) ? nil : $0 } ?? buffer.keywordsInOrder.first ?? "TODO"
297 buffer.forwardLine(0)
298 if let stars = buffer.lookingAt("\\*+ ") {
299 buffer.point = stars[0]!.upperBound
300 buffer.insert(keyword + " ")
301 }
302 }
303 }
304}
305
306// MARK: - Moving and levels
307
308/// M-up on a heading: `org-move-subtree-up`.
309public struct MoveSubtreeUp: OrgCommand {
310 public init() {}
311 public var id: String { "org.subtree.up" }
312 public var title: String { "Move Subtree Up" }
313 public func applies(in context: EditContext) -> Bool { headingOnLine(at: context.caret, in: context.tree) != nil }
314 public func run(in context: EditContext) -> CommandStep { headingCommand(context) { try $0.moveSubtree(-1) } }
315}
316
317/// M-down on a heading: `org-move-subtree-down`.
318public struct MoveSubtreeDown: OrgCommand {
319 public init() {}
320 public var id: String { "org.subtree.down" }
321 public var title: String { "Move Subtree Down" }
322 public func applies(in context: EditContext) -> Bool { headingOnLine(at: context.caret, in: context.tree) != nil }
323 public func run(in context: EditContext) -> CommandStep { headingCommand(context) { try $0.moveSubtree(1) } }
324}
325
326/// M-S-left on a heading: `org-promote-subtree`.
327public struct PromoteSubtree: OrgCommand {
328 public init() {}
329 public var id: String { "org.subtree.promote" }
330 public var title: String { "Promote Subtree" }
331 public func applies(in context: EditContext) -> Bool { headingOnLine(at: context.caret, in: context.tree) != nil }
332 public func run(in context: EditContext) -> CommandStep {
333 headingCommand(context) { buffer in
334 try buffer.saveExcursion {
335 try buffer.backToHeading()
336 try buffer.mapTree { try buffer.changeLevel(by: -1) }
337 }
338 buffer.fixPositionAfterPromote()
339 }
340 }
341}
342
343/// M-S-right on a heading: `org-demote-subtree`.
344public struct DemoteSubtree: OrgCommand {
345 public init() {}
346 public var id: String { "org.subtree.demote" }
347 public var title: String { "Demote Subtree" }
348 public func applies(in context: EditContext) -> Bool { headingOnLine(at: context.caret, in: context.tree) != nil }
349 public func run(in context: EditContext) -> CommandStep {
350 headingCommand(context) { buffer in
351 try buffer.saveExcursion {
352 try buffer.backToHeading()
353 try buffer.mapTree { try buffer.changeLevel(by: 1) }
354 }
355 buffer.fixPositionAfterPromote()
356 }
357 }
358}
359
360// MARK: - Motion
361
362/// C-c C-n: `org-next-visible-heading`.
363public struct NextHeading: OrgCommand {
364 public init() {}
365 public var id: String { "org.heading.next" }
366 public var title: String { "Next Heading" }
367 public func applies(in context: EditContext) -> Bool { true }
368 public func run(in context: EditContext) -> CommandStep {
369 headingCommand(context) { buffer in
370 buffer.endOfLine()
371 var found = buffer.nextHeadingStart(after: buffer.point)
372 while let start = found, context.hidden.contains(where: { $0.contains(start) }) {
373 found = buffer.nextHeadingStart(after: start)
374 }
375 buffer.point = found ?? buffer.length
376 }
377 }
378}
379
380/// C-c C-p: `org-previous-visible-heading`.
381public struct PreviousHeading: OrgCommand {
382 public init() {}
383 public var id: String { "org.heading.previous" }
384 public var title: String { "Previous Heading" }
385 public func applies(in context: EditContext) -> Bool { true }
386 public func run(in context: EditContext) -> CommandStep {
387 headingCommand(context) { buffer in
388 buffer.forwardLine(0)
389 var found = buffer.previousHeadingStart(before: buffer.point)
390 while let start = found, context.hidden.contains(where: { $0.contains(start) }) {
391 found = buffer.previousHeadingStart(before: start)
392 }
393 buffer.point = found ?? 0
394 }
395 }
396}
397
398/// C-c C-f: `org-forward-heading-same-level`.
399public struct ForwardSameLevel: OrgCommand {
400 public init() {}
401 public var id: String { "org.heading.forward-same-level" }
402 public var title: String { "Next Heading at Same Level" }
403 public func applies(in context: EditContext) -> Bool { true }
404 public func run(in context: EditContext) -> CommandStep { headingCommand(context) { try $0.forwardHeadingSameLevel(1) } }
405}
406
407/// C-c C-b: `org-backward-heading-same-level`.
408public struct BackwardSameLevel: OrgCommand {
409 public init() {}
410 public var id: String { "org.heading.backward-same-level" }
411 public var title: String { "Previous Heading at Same Level" }
412 public func applies(in context: EditContext) -> Bool { true }
413 public func run(in context: EditContext) -> CommandStep { headingCommand(context) { try $0.forwardHeadingSameLevel(-1) } }
414}
415
416/// C-c C-u: `outline-up-heading`.
417public struct UpHeading: OrgCommand {
418 public init() {}
419 public var id: String { "org.heading.up" }
420 public var title: String { "Up to Parent Heading" }
421 public func applies(in context: EditContext) -> Bool { true }
422 public func run(in context: EditContext) -> CommandStep {
423 headingCommand(context) { buffer in
424 do {
425 try buffer.backToHeading()
426 } catch {
427 throw EmacsBuffer.UserError(message: "before first heading")
428 }
429 let start = buffer.outlineLevel
430 guard start > 1 else { throw EmacsBuffer.UserError(message: "Already at top level of the outline") }
431 var level = start
432 while level >= start, !buffer.bobp {
433 buffer.outlinePreviousHeading()
434 level = buffer.outlineLevel
435 }
436 }
437 }
438}