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