Sources/OrgCore/Commands/Fill.swift
593 lines · 27451 bytes
1import Foundation
2
3// M-q: `org-fill-paragraph` (Org 9.8.7) over Emacs's `fill-region-as-paragraph` (fill.el), for
4// paragraphs, comments and comment blocks, with `sentence-end-double-space` nil,
5// `colon-double-space` nil, `left-margin` 0 and left justification.
6
7extension EmacsBuffer {
8 /// `org-element-paragraph-separate`, which org uses as `paragraph-start`.
9 var paragraphStartPattern: String {
10 let letters = options.listAllowAlphabetical ? "|[A-Za-z]" : ""
11 return "(?:\\*+ |\\[fn:[-_\\w]+\\]|%%\\(|[ \\t]*(?:$|\\||\\+(?:-+\\+)+[ \\t]*$|#(?: |$|\\+(?:BEGIN_\\S+|\\S+(?:\\[.*\\])?:[ \\t]*))|:(?: |$|[-_\\w]+:[ \\t]*$)|-{5,}[ \\t]*$|\\\\begin\\{([A-Za-z0-9*]+)\\}|(?:[-+*]|(?:[0-9]+\(letters))[.)])(?:[ \\t]|$)))"
12 }
13
14 static let adaptiveFillPattern = "[-–!|#%;>*·•‣⁃◦ \\t]*"
15 static let sentenceEndPattern = "([.?!…‽][\\]\"'”’)}»›]*($|[\\t \\u00A0])|[。.?!]+)[ \\u00A0\\t\\n]*"
16
17 /// An element as `org-element-at-point` sees it, with offsets in the buffer.
18 struct Element {
19 let kind: SyntaxKind
20 let range: Range<Int>
21 /// The enclosing element, for paragraphs in items.
22 let parentKind: SyntaxKind?
23 let parentStart: Int?
24 /// For blocks: the word after `#+begin_`, lowercased.
25 let blockType: String?
26 /// For plain lists: the first item's start.
27 let firstItem: Int?
28 }
29
30 static let elementKinds: Set<SyntaxKind> = [.paragraph, .comment, .block, .table, .tableRow, .item, .plainList, .heading,
31 .keyword, .fixedWidth, .drawer, .propertyDrawer, .planning, .dynamicBlock,
32 .footnoteDefinition, .horizontalRule, .clock, .nodeProperty]
33
34 /// The innermost element (not object) holding `offset`, as `org-element-at-point`. Greater
35 /// blocks (quote, center, special) are parsed into their elements.
36 func element(at offset: Int) -> Element? {
37 // Elements never cross a heading, so the section around `offset` is enough to parse.
38 let lineBegin = lineStart(offset)
39 let from = headingLevel(atLine: lineBegin) != nil ? lineBegin : (previousHeadingStart(before: lineBegin) ?? 0)
40 let to = nextHeadingStart(after: lineBegin) ?? length
41 return element(at: offset, in: substring(from, to), base: from)
42 }
43
44 private func element(at offset: Int, in text: String, base: Int) -> Element? {
45 let tree = OrgParser.parse(text, defaults: settings)
46 let local = offset - base
47 var node = tree.root
48 var found: SyntaxNode?
49 while true {
50 let child = node.child(containing: local) ?? (local == node.range.upperBound ? node.children.last : nil)
51 guard let child, Self.elementKinds.contains(child.kind) || child.kind == .section || child.kind == .zerothSection else { break }
52 if Self.elementKinds.contains(child.kind) { found = child }
53 if [.heading, .paragraph, .comment, .tableRow, .block].contains(child.kind) { break }
54 node = child
55 }
56 guard let found else { return nil }
57 let range = (found.range.lowerBound + base)..<(found.range.upperBound + base)
58 var blockType: String?
59 if found.kind == .block {
60 let first = found.text.prefix { $0 != "\n" }.lowercased()
61 blockType = first.firstMatch(of: /#\+begin_(\S+)/).map { String($0.1) }
62 if let type = blockType, !["src", "example", "export", "comment", "verse"].contains(type) {
63 let contentsStart = lineEnd(range.lowerBound) + 1
64 let closing = saveExcursion { () -> Int in
65 point = range.upperBound
66 skipBackward(Self.whitespace)
67 return lineStart(point)
68 }
69 if offset >= contentsStart, offset < closing, contentsStart < closing {
70 return element(at: offset, in: substring(contentsStart, closing), base: contentsStart)
71 }
72 }
73 }
74 let parent = found.parent.flatMap { Self.elementKinds.contains($0.kind) ? $0 : nil }
75 return Element(
76 kind: found.kind, range: range, parentKind: parent?.kind, parentStart: parent.map { $0.range.lowerBound + base },
77 blockType: blockType, firstItem: found.children.first { $0.kind == .item }.map { $0.range.lowerBound + base }
78 )
79 }
80
81 /// Where an element's contents end: after its last non-blank line.
82 func contentsEnd(_ node: Element) -> Int {
83 saveExcursion {
84 point = node.range.upperBound
85 skipBackward(Self.whitespace)
86 return min(lineEnd(point) + 1, length)
87 }
88 }
89
90 /// `org-list-item-body-column`.
91 func itemBodyColumn(_ item: Int) -> Int {
92 saveExcursion {
93 point = item
94 let match = looking("[ \\t]*(\\S+)", setsMatch: false)!
95 point = match[1]!.upperBound
96 return currentColumn + 1
97 }
98 }
99
100 /// `org-adaptive-fill-function` at point's line.
101 func orgAdaptiveFillPrefix() -> String? {
102 saveExcursion {
103 guard !atHeading else { return nil }
104 let lineBegin = lineStart(point)
105 guard let element = element(at: lineBegin) else { return nil }
106 switch element.kind {
107 case .comment:
108 let indent = substring(lineBegin, lineBegin + (looking("[ \\t]*", setsMatch: false).map { $0[0]!.count } ?? 0))
109 return indent + "# "
110 case .item, .plainList:
111 let item = element.kind == .item ? element.range.lowerBound : (element.firstItem ?? element.range.lowerBound)
112 return String(repeating: " ", count: itemBodyColumn(item))
113 case .block where element.blockType == "comment":
114 let begin = lineEnd(element.range.lowerBound) + 1
115 let end = saveExcursion { () -> Int in
116 point = element.range.upperBound
117 skipBackward(Self.whitespace)
118 return lineStart(point)
119 }
120 guard lineBegin >= begin, lineBegin < end else { return nil }
121 point = lineBegin
122 return looking("[ \\t]+", setsMatch: false).map { substring($0[0]!.lowerBound, $0[0]!.upperBound) } ?? ""
123 case .paragraph:
124 point = lineBegin
125 if element.parentKind == .item, let parent = element.parentStart {
126 return String(repeating: " ", count: itemBodyColumn(parent))
127 }
128 if let prefix = contextPrefix(from: postAffiliated(element), to: element.range.upperBound, paragraphOnly: true) {
129 return prefix
130 }
131 return looking("[ \\t]+", setsMatch: false).map { substring($0[0]!.lowerBound, $0[0]!.upperBound) } ?? ""
132 default:
133 return nil
134 }
135 }
136 }
137
138 /// Start of an element after its affiliated keywords.
139 func postAffiliated(_ node: Element) -> Int {
140 saveExcursion {
141 point = node.range.lowerBound
142 while looking("[ \\t]*#\\+(?:CAPTION|DATA|HEADERS?|LABEL|NAME|PLOT|RESNAME|RESULTS?|SOURCE|SRCNAME|TBLNAME|ATTR_[-_\\w]+)(?:\\[.*\\])?:", setsMatch: false) != nil {
143 if forwardLine(1) != 0 { break }
144 }
145 return point
146 }
147 }
148
149 /// `fill-match-adaptive-prefix`, with org's function or, in paragraphs, the regexp after
150 /// any markup at the line start.
151 func matchAdaptivePrefix(paragraphOnly: Bool) -> String? {
152 let prefix: String?
153 if paragraphOnly {
154 let start = point
155 prefix = saveExcursion {
156 skipLineStartMarkup()
157 return looking(Self.adaptiveFillPattern, setsMatch: false).map { substring($0[0]!.lowerBound, $0[0]!.upperBound) }
158 }
159 point = start
160 } else {
161 prefix = orgAdaptiveFillPrefix() ?? looking(Self.adaptiveFillPattern, setsMatch: false).map { substring($0[0]!.lowerBound, $0[0]!.upperBound) }
162 }
163 guard let prefix else { return nil }
164 return (prefix as NSString).length >= options.fillColumn ? nil : prefix
165 }
166
167 /// Moves past the opening markup of an object that starts at point, as
168 /// `org-adaptive-fill-paragraph-function` does.
169 private func skipLineStartMarkup() {
170 let line = substring(point, lineEnd(point)) + "\n"
171 let tree = OrgParser.parse(line, defaults: settings)
172 var node: SyntaxNode? = tree.root
173 while let current = node, let child = current.child(containing: 0), child.range.lowerBound == 0 {
174 if [.bold, .italic, .underline, .strikeThrough].contains(child.kind) {
175 point += 1
176 return
177 }
178 if child.kind == .link, let description = child.firstChild(.linkDescription) {
179 point += description.range.lowerBound
180 return
181 }
182 node = child
183 }
184 }
185
186 /// `fill-context-prefix`.
187 func contextPrefix(from: Int, to: Int, paragraphOnly: Bool = false) -> String? {
188 saveExcursion {
189 point = from
190 if eolp { forwardLine(1) }
191 point = lineStart(point)
192 var first = matchAdaptivePrefix(paragraphOnly: paragraphOnly)
193 forwardLine(1)
194 if point < to {
195 point = lineStart(point)
196 let second: String? = looking(paragraphStartPattern, setsMatch: false) != nil ? nil : matchAdaptivePrefix(paragraphOnly: paragraphOnly)
197 if let second {
198 if first == nil { first = "" }
199 var pattern = "^"
200 var rest = Substring(second)
201 while let match = rest.firstMatch(of: /^[ \t]*([^ \t]+)/) {
202 pattern += ".*" + NSRegularExpression.escapedPattern(for: String(match.1))
203 rest = rest[match.range.upperBound...]
204 }
205 if first!.range(of: pattern, options: .regularExpression) != nil { return second }
206 let common = zip(first!, second).prefix { $0 == $1 }.count
207 return common == 0 ? nil : String(first!.prefix(common))
208 }
209 }
210 guard let firstPrefix = first else { return nil }
211 let keep = firstPrefix.allSatisfy({ $0 == " " || $0 == "\t" }) || firstPrefix.range(of: "^\\s*#( |$)", options: .regularExpression) != nil
212 let result = keep ? firstPrefix : String(repeating: " ", count: displayWidth(firstPrefix))
213 let probe = result + "a"
214 let regex = Self.regex(paragraphStartPattern, caseInsensitive: false)
215 if let match = regex.firstMatch(in: probe, options: [.anchored], range: NSRange(location: 0, length: (probe as NSString).length)), match.range.location == 0 {
216 return nil
217 }
218 return result
219 }
220 }
221
222 /// `fill-nobreak-p` with org's predicates.
223 func fillNoBreak() -> Bool {
224 if bolp { return false }
225 let startsParagraph = saveExcursion { () -> Bool in
226 skipForward(Self.blanks)
227 return !eolp && looking(paragraphStartPattern, setsMatch: false) != nil
228 }
229 if startsParagraph { return true }
230 let lineBreak = saveExcursion { () -> Bool in
231 skipBackward(Self.blanks)
232 skipBackward([92])
233 return looking("\\\\\\\\($|[^\\\\])", setsMatch: false) != nil
234 }
235 if lineBreak { return true }
236 if looking("[ \\t]*\\{\\{\\{n(?:\\([^\\n)]*\\))?\\}\\}\\}[.)](?:$| )", setsMatch: false) != nil { return true }
237 let saved = lastMatch
238 defer { lastMatch = saved }
239 if atTimestamp() != nil, looking("[\\[<](" + Self.timestampCore + ")[^\\]>\\n]*[\\]>]", setsMatch: false) == nil { return true }
240 return false
241 }
242
243 /// `fill-move-to-break-point` for text without CJK characters.
244 func moveToBreakPoint(_ lineBegin: Int) {
245 if lineBegin > point { point = lineBegin }
246 while true {
247 guard let match = searchBackward("[ \\t]", bound: lineBegin, caseInsensitive: false) else {
248 point = lineBegin
249 break
250 }
251 point = match[0]!.lowerBound + 1
252 if fillNoBreak() {
253 skipBackward(Self.blanks, bound: lineBegin)
254 } else {
255 break
256 }
257 }
258 skipBackward(Self.blanks)
259 if lineBegin >= point {
260 let end = lineEnd(point)
261 var first = true
262 point = lineBegin
263 while point < end, first || fillNoBreak() {
264 skipForward(Self.blanks)
265 while point < length, let c = character(at: point), c != 32, c != 10, c != 9 { point += 1 }
266 first = false
267 }
268 }
269 }
270
271 /// `skip-chars-backward` stopping at `bound`.
272 func skipBackward(_ set: Set<unichar>, bound: Int) {
273 while point > bound, let c = character(at: point - 1), set.contains(c) { point -= 1 }
274 }
275
276 /// `canonically-space-region` with single spaces after sentences.
277 func canonicallySpace(_ begin: Int, _ endOffset: Int) {
278 saveExcursion {
279 let end = marker(endOffset, advances: true)
280 defer { release(end) }
281 for offset in begin..<end.position where character(at: offset) == 9 { replace(offset..<(offset + 1), with: " ") }
282 point = begin
283 let pattern = "(" + Self.sentenceEndPattern + ") *| +"
284 while point < end.position, let match = searchForward(pattern, bound: end.position, caseInsensitive: false) {
285 let matchEnd = match[0]!.upperBound
286 let start: Int
287 if let sentence = match[1], matchEnd > sentence.upperBound {
288 start = sentence.upperBound
289 } else if match[1] != nil {
290 let lastNonSpace = saveExcursion { () -> Int in point = matchEnd; skipBackward([32]); return point }
291 start = min(matchEnd, lastNonSpace + 1)
292 } else {
293 let before = saveExcursion { () -> unichar? in
294 point = match[0]!.lowerBound
295 skipBackward(Set(" ]})\"'".utf16))
296 return character(at: point - 1)
297 }
298 start = match[0]!.lowerBound + (before == 10 ? 0 : 1)
299 }
300 if start < matchEnd { deleteRegion(start, matchEnd) }
301 }
302 }
303 }
304
305 /// `fill-region-as-paragraph` with left justification and adaptive fill. With `nosqueeze`,
306 /// spaces between words and at the end stay.
307 func fillRegionAsParagraph(_ fromOffset: Int, _ toOffset: Int, prefix fixedPrefix: String? = nil, nosqueeze: Bool = false) {
308 point = min(fromOffset, toOffset)
309 let to = max(fromOffset, toOffset)
310 skipForward(Self.whitespace)
311 let fromPlusIndent = point
312 point = lineStart(point)
313 var from = max(point, fromOffset)
314 point = to
315 var oneLeft = false
316 while point > from, character(at: point - 1) == 10 {
317 if oneLeft {
318 deleteRegion(point - 1, point)
319 } else {
320 point -= 1
321 oneLeft = true
322 }
323 }
324 let toMarker = marker(point, advances: true)
325 defer { release(toMarker) }
326 point = fromPlusIndent
327 guard toMarker.position > point else { return }
328 var prefix = fixedPrefix
329 if prefix == nil || prefix == "" { prefix = contextPrefix(from: from, to: toMarker.position) }
330 point = lineStart(from)
331 // `fill-delete-prefix`.
332 let prefixPattern: String
333 if let prefix, !prefix.allSatisfy({ $0 == " " || $0 == "\t" }) {
334 prefixPattern = "[ \\t]*(" + NSRegularExpression.escapedPattern(for: prefix).replacingOccurrences(of: "[ \\t]+", with: "[ \\t]*", options: .regularExpression) + ")?[ \\t]*"
335 } else {
336 prefixPattern = "[ \\t]*"
337 }
338 point = from
339 forwardLine(1)
340 while point < toMarker.position {
341 if let match = looking(prefixPattern, setsMatch: false) { deleteRegion(point, match[0]!.upperBound) }
342 if forwardLine(1) != 0 { break }
343 }
344 point = from
345 if let match = looking(prefixPattern, setsMatch: false) { point = match[0]!.upperBound }
346 from = point
347 // `fill-delete-newlines`.
348 var at = from
349 while at < toMarker.position {
350 if character(at: at) == 10 { replace(at..<(at + 1), with: " ") }
351 at += 1
352 }
353 if !nosqueeze {
354 point = from
355 skipForward(Self.blanks)
356 canonicallySpace(point, toMarker.position)
357 point = toMarker.position
358 let end = point
359 skipBackward(Self.blanks)
360 if point < end { deleteRegion(point, end) }
361 }
362 point = from
363 while point < toMarker.position {
364 let lineBegin = point
365 moveToColumn(options.fillColumn)
366 var breaks = false
367 if point < toMarker.position, lineBegin < toMarker.position {
368 if currentColumn <= options.fillColumn { point += 1 }
369 moveToBreakPoint(lineBegin)
370 skipForward(Self.blanks)
371 breaks = point < toMarker.position
372 }
373 guard breaks else {
374 point = toMarker.position
375 break
376 }
377 // `fill-newline`.
378 skipBackward(Self.blanks)
379 insert("\n")
380 indentLineTo(0)
381 if let prefix, !prefix.isEmpty { insertBeforeMarkers(prefix) }
382 }
383 point = toMarker.position
384 if !eobp { point += 1 }
385 }
386
387 /// `org-fill-paragraph` without a region or justification.
388 func fillParagraph() {
389 saveExcursion {
390 if lookingAt("^[ \\t]*$") != nil || saveExcursion({ beginningOfLine(); return lookingAt("[ \\t]*$") != nil }) {
391 skipForward(Self.whitespace)
392 }
393 let eol = lineEnd(point)
394 guard let element = element(at: eol) else { return }
395 switch element.kind {
396 case .paragraph:
397 let begin = paragraphContentsBegin(element)
398 var end = contentsEnd(element)
399 guard lineEnd(point) >= begin else { return }
400 var cuts = [begin]
401 saveExcursion {
402 point = begin
403 while let match = searchForward("\\\\\\\\[ \\t]*\\n", bound: end, caseInsensitive: false) {
404 cuts.insert(match[0]!.upperBound, at: 0)
405 }
406 }
407 for cut in cuts where cut != end {
408 fillRegionAsParagraph(cut, end)
409 end = cut
410 }
411 case .comment:
412 let begin = postAffiliated(element)
413 let elementEnd = element.range.upperBound
414 guard point >= begin, point <= elementEnd else { return }
415 let from = saveExcursion { () -> Int in
416 endOfLine()
417 if searchBackward("^[ \\t]*#[ \\t]*$", bound: begin, caseInsensitive: false) != nil {
418 forwardLine(1)
419 return point
420 }
421 return begin
422 }
423 let to = saveExcursion { () -> Int in
424 endOfLine()
425 if searchForward("^[ \\t]*#[ \\t]*$", bound: elementEnd, caseInsensitive: false) != nil {
426 return lineStart(point) - 1
427 }
428 point = elementEnd
429 skipBackward(Self.whitespace)
430 return lineEnd(point)
431 }
432 guard to > from else { return }
433 let prefix = saveExcursion { () -> String in
434 beginningOfLine()
435 let comment = looking("[ \\t]*#", setsMatch: false)!
436 point = comment[0]!.upperBound
437 let marker = substring(comment[0]!.lowerBound, comment[0]!.upperBound)
438 if let rest = looking(Self.adaptiveFillPattern, setsMatch: false) {
439 return marker + substring(rest[0]!.lowerBound, rest[0]!.upperBound)
440 }
441 return marker + " "
442 }
443 saveExcursion { fillRegionAsParagraph(from, to, prefix: prefix) }
444 case .block where element.blockType == "comment":
445 let begin = saveExcursion { () -> Int in
446 point = element.range.lowerBound
447 forwardLine(1)
448 return point
449 }
450 let end = saveExcursion { () -> Int in
451 point = element.range.upperBound
452 skipBackward(Self.whitespace)
453 return lineStart(point)
454 }
455 guard point >= begin, point <= end else { return }
456 let from = saveExcursion { () -> Int in
457 endOfLine()
458 if searchBackward("^[ \\t]*$", bound: begin, caseInsensitive: false) == nil { point = begin }
459 return lineStart(point)
460 }
461 let to = saveExcursion { () -> Int in
462 beginningOfLine()
463 if searchForward("^[ \\t]*$", bound: end, caseInsensitive: false) == nil { point = end }
464 return lineStart(point)
465 }
466 fillRegionAsParagraph(from, to)
467 default:
468 return
469 }
470 }
471 }
472
473 /// `org-fill-paragraph` with an active region: from the region's end back to `start`, M-q at
474 /// the end of each paragraph's text, moving back a paragraph at a time.
475 func fillParagraphs(_ start: Int, _ end: Int) {
476 saveExcursion {
477 point = end
478 skipBackward(Self.whitespace)
479 while point > start {
480 let before = point
481 fillParagraph()
482 point = paragraphBackwardStop()
483 skipBackward(Self.whitespace)
484 if point >= before { break }
485 }
486 }
487 }
488
489 /// Where `org--backward-paragraph-once` goes from the end of a line's text.
490 private func paragraphBackwardStop() -> Int {
491 guard let element = element(at: point) else { return lineStart(point) }
492 switch element.kind {
493 case .block where ["comment", "example", "export", "src", "verse"].contains(element.blockType ?? ""):
494 // Between empty lines in the block.
495 let contentsStart = lineEnd(element.range.lowerBound) + 1
496 guard point > contentsStart else { return element.range.lowerBound }
497 let contentsEnd = saveExcursion { () -> Int in
498 point = element.range.upperBound
499 skipBackward(Self.whitespace)
500 return lineStart(point)
501 }
502 if point > contentsEnd { return contentsEnd }
503 return saveExcursion {
504 skipBackward(Self.whitespace, bound: element.range.lowerBound)
505 return searchBackward("^[ \t]*\n", bound: contentsStart, caseInsensitive: false)?[0]!.lowerBound ?? contentsStart
506 }
507 case .block, .drawer, .propertyDrawer, .dynamicBlock:
508 // On the closing line: inside, to the contents' end.
509 return lineStart(point) > element.range.lowerBound ? lineStart(point) : element.range.lowerBound
510 default:
511 return lineStart(element.range.lowerBound)
512 }
513 }
514
515 /// `fill-region` over whole lines: each paragraph's part of `from..<to` filled as a
516 /// paragraph. Other elements stay.
517 func fillRegion(_ from: Int, _ to: Int, nosqueeze: Bool = false) {
518 var spans: [Range<Int>] = []
519 var line = lineStart(from)
520 while line < to, line < length {
521 var next = lineEnd(line) + 1
522 let blank = substring(line, lineEnd(line)).allSatisfy { $0 == " " || $0 == "\t" }
523 if !blank, let element = element(at: lineEnd(line)) {
524 if element.kind == .paragraph {
525 let span = max(paragraphContentsBegin(element), from)..<min(contentsEnd(element), to)
526 if !span.isEmpty { spans.append(span) }
527 }
528 next = max(next, lineStart(min(length, max(element.range.lowerBound, contentsEnd(element)))))
529 if next <= line { next = lineEnd(line) + 1 }
530 }
531 line = next
532 }
533 // Back to front, so the spans before stay where they are.
534 for span in spans.reversed() { fillRegionAsParagraph(span.lowerBound, span.upperBound, nosqueeze: nosqueeze) }
535 }
536
537 /// Where a paragraph's text starts: past affiliated keywords, and for a paragraph that
538 /// opens an item, past the bullet.
539 func paragraphContentsBegin(_ node: Element) -> Int {
540 max(postAffiliated(node), node.range.lowerBound)
541 }
542}
543
544/// M-q: `org-fill-paragraph`. With a selection, as with an active region, every paragraph
545/// in it; `point` is where the caret is in it, and stays.
546public struct FillParagraph: OrgCommand {
547 public let point: Int?
548
549 public init(point: Int? = nil) { self.point = point }
550 public var id: String { "org.fill-paragraph" }
551 public var title: String { "Fill Paragraph" }
552 public func applies(in context: EditContext) -> Bool { true }
553 public func run(in context: EditContext) -> CommandStep {
554 commitBuffer(context) { buffer in
555 if let selection = context.selection.first, !selection.isEmpty {
556 if let point { buffer.point = point }
557 buffer.fillParagraphs(selection.lowerBound, selection.upperBound)
558 } else {
559 buffer.fillParagraph()
560 }
561 }
562 }
563}
564
565/// evil's `gq` and `gw` operators: the paragraphs in `range`'s lines filled, as `fill-region`.
566/// With `keep` (`gw`) the caret stays there; without (`gq`) it goes to the first non-blank
567/// of the range's last line. Spaces are not squeezed, as Doom's `+evil--no-squeeze-on-fill-a`
568/// has it.
569public struct FillRegion: OrgCommand {
570 public let range: Range<Int>
571 public let keep: Int?
572
573 public init(range: Range<Int>, keep: Int?) {
574 self.range = range
575 self.keep = keep
576 }
577
578 public var id: String { "org.fill-region" }
579 public var title: String { "Fill Region" }
580 public func applies(in context: EditContext) -> Bool { true }
581 public func run(in context: EditContext) -> CommandStep {
582 commitBuffer(context) { buffer in
583 let target = buffer.marker(keep ?? max(range.lowerBound, range.upperBound - 1))
584 defer { buffer.release(target) }
585 buffer.fillRegion(range.lowerBound, range.upperBound, nosqueeze: true)
586 buffer.point = target.position
587 if keep == nil {
588 buffer.beginningOfLine()
589 buffer.skipForward(EmacsBuffer.blanks)
590 }
591 }
592 }
593}