| @@ -0,0 +1,480 @@ |
| |
1 | import 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 | |
| |
7 | extension 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 | element(at: offset, in: string, base: 0) |
| |
38 | } |
| |
39 | |
| |
40 | private func element(at offset: Int, in text: String, base: Int) -> Element? { |
| |
41 | let tree = OrgParser.parse(text, defaults: settings) |
| |
42 | let local = offset - base |
| |
43 | var node = tree.root |
| |
44 | var found: SyntaxNode? |
| |
45 | while true { |
| |
46 | let child = node.child(containing: local) ?? (local == node.range.upperBound ? node.children.last : nil) |
| |
47 | guard let child, Self.elementKinds.contains(child.kind) || child.kind == .section || child.kind == .zerothSection else { break } |
| |
48 | if Self.elementKinds.contains(child.kind) { found = child } |
| |
49 | if [.heading, .paragraph, .comment, .tableRow, .block].contains(child.kind) { break } |
| |
50 | node = child |
| |
51 | } |
| |
52 | guard let found else { return nil } |
| |
53 | let range = (found.range.lowerBound + base)..<(found.range.upperBound + base) |
| |
54 | var blockType: String? |
| |
55 | if found.kind == .block { |
| |
56 | let first = found.text.prefix { $0 != "\n" }.lowercased() |
| |
57 | blockType = first.firstMatch(of: /#\+begin_(\S+)/).map { String($0.1) } |
| |
58 | if let type = blockType, !["src", "example", "export", "comment", "verse"].contains(type) { |
| |
59 | let contentsStart = lineEnd(range.lowerBound) + 1 |
| |
60 | let closing = saveExcursion { () -> Int in |
| |
61 | point = range.upperBound |
| |
62 | skipBackward(Self.whitespace) |
| |
63 | return lineStart(point) |
| |
64 | } |
| |
65 | if offset >= contentsStart, offset < closing, contentsStart < closing { |
| |
66 | return element(at: offset, in: substring(contentsStart, closing), base: contentsStart) |
| |
67 | } |
| |
68 | } |
| |
69 | } |
| |
70 | let parent = found.parent.flatMap { Self.elementKinds.contains($0.kind) ? $0 : nil } |
| |
71 | return Element( |
| |
72 | kind: found.kind, range: range, parentKind: parent?.kind, parentStart: parent.map { $0.range.lowerBound + base }, |
| |
73 | blockType: blockType, firstItem: found.children.first { $0.kind == .item }.map { $0.range.lowerBound + base } |
| |
74 | ) |
| |
75 | } |
| |
76 | |
| |
77 | /// Where an element's contents end: after its last non-blank line. |
| |
78 | func contentsEnd(_ node: Element) -> Int { |
| |
79 | saveExcursion { |
| |
80 | point = node.range.upperBound |
| |
81 | skipBackward(Self.whitespace) |
| |
82 | return min(lineEnd(point) + 1, length) |
| |
83 | } |
| |
84 | } |
| |
85 | |
| |
86 | /// `org-list-item-body-column`. |
| |
87 | func itemBodyColumn(_ item: Int) -> Int { |
| |
88 | saveExcursion { |
| |
89 | point = item |
| |
90 | let match = looking("[ \\t]*(\\S+)", setsMatch: false)! |
| |
91 | point = match[1]!.upperBound |
| |
92 | return currentColumn + 1 |
| |
93 | } |
| |
94 | } |
| |
95 | |
| |
96 | /// `org-adaptive-fill-function` at point's line. |
| |
97 | func orgAdaptiveFillPrefix() -> String? { |
| |
98 | saveExcursion { |
| |
99 | guard !atHeading else { return nil } |
| |
100 | let lineBegin = lineStart(point) |
| |
101 | guard let element = element(at: lineBegin) else { return nil } |
| |
102 | switch element.kind { |
| |
103 | case .comment: |
| |
104 | let indent = substring(lineBegin, lineBegin + (looking("[ \\t]*", setsMatch: false).map { $0[0]!.count } ?? 0)) |
| |
105 | return indent + "# " |
| |
106 | case .item, .plainList: |
| |
107 | let item = element.kind == .item ? element.range.lowerBound : (element.firstItem ?? element.range.lowerBound) |
| |
108 | return String(repeating: " ", count: itemBodyColumn(item)) |
| |
109 | case .block where element.blockType == "comment": |
| |
110 | let begin = lineEnd(element.range.lowerBound) + 1 |
| |
111 | let end = saveExcursion { () -> Int in |
| |
112 | point = element.range.upperBound |
| |
113 | skipBackward(Self.whitespace) |
| |
114 | return lineStart(point) |
| |
115 | } |
| |
116 | guard lineBegin >= begin, lineBegin < end else { return nil } |
| |
117 | point = lineBegin |
| |
118 | return looking("[ \\t]+", setsMatch: false).map { substring($0[0]!.lowerBound, $0[0]!.upperBound) } ?? "" |
| |
119 | case .paragraph: |
| |
120 | point = lineBegin |
| |
121 | if element.parentKind == .item, let parent = element.parentStart { |
| |
122 | return String(repeating: " ", count: itemBodyColumn(parent)) |
| |
123 | } |
| |
124 | if let prefix = contextPrefix(from: postAffiliated(element), to: element.range.upperBound, paragraphOnly: true) { |
| |
125 | return prefix |
| |
126 | } |
| |
127 | return looking("[ \\t]+", setsMatch: false).map { substring($0[0]!.lowerBound, $0[0]!.upperBound) } ?? "" |
| |
128 | default: |
| |
129 | return nil |
| |
130 | } |
| |
131 | } |
| |
132 | } |
| |
133 | |
| |
134 | /// Start of an element after its affiliated keywords. |
| |
135 | func postAffiliated(_ node: Element) -> Int { |
| |
136 | saveExcursion { |
| |
137 | point = node.range.lowerBound |
| |
138 | while looking("[ \\t]*#\\+(?:CAPTION|DATA|HEADERS?|LABEL|NAME|PLOT|RESNAME|RESULTS?|SOURCE|SRCNAME|TBLNAME|ATTR_[-_\\w]+)(?:\\[.*\\])?:", setsMatch: false) != nil { |
| |
139 | if forwardLine(1) != 0 { break } |
| |
140 | } |
| |
141 | return point |
| |
142 | } |
| |
143 | } |
| |
144 | |
| |
145 | /// `fill-match-adaptive-prefix`, with org's function or, in paragraphs, the regexp after |
| |
146 | /// any markup at the line start. |
| |
147 | func matchAdaptivePrefix(paragraphOnly: Bool) -> String? { |
| |
148 | let prefix: String? |
| |
149 | if paragraphOnly { |
| |
150 | let start = point |
| |
151 | prefix = saveExcursion { |
| |
152 | skipLineStartMarkup() |
| |
153 | return looking(Self.adaptiveFillPattern, setsMatch: false).map { substring($0[0]!.lowerBound, $0[0]!.upperBound) } |
| |
154 | } |
| |
155 | point = start |
| |
156 | } else { |
| |
157 | prefix = orgAdaptiveFillPrefix() ?? looking(Self.adaptiveFillPattern, setsMatch: false).map { substring($0[0]!.lowerBound, $0[0]!.upperBound) } |
| |
158 | } |
| |
159 | guard let prefix else { return nil } |
| |
160 | return (prefix as NSString).length >= options.fillColumn ? nil : prefix |
| |
161 | } |
| |
162 | |
| |
163 | /// Moves past the opening markup of an object that starts at point, as |
| |
164 | /// `org-adaptive-fill-paragraph-function` does. |
| |
165 | private func skipLineStartMarkup() { |
| |
166 | let line = substring(point, lineEnd(point)) + "\n" |
| |
167 | let tree = OrgParser.parse(line, defaults: settings) |
| |
168 | var node: SyntaxNode? = tree.root |
| |
169 | while let current = node, let child = current.child(containing: 0), child.range.lowerBound == 0 { |
| |
170 | if [.bold, .italic, .underline, .strikeThrough].contains(child.kind) { |
| |
171 | point += 1 |
| |
172 | return |
| |
173 | } |
| |
174 | if child.kind == .link, let description = child.firstChild(.linkDescription) { |
| |
175 | point += description.range.lowerBound |
| |
176 | return |
| |
177 | } |
| |
178 | node = child |
| |
179 | } |
| |
180 | } |
| |
181 | |
| |
182 | /// `fill-context-prefix`. |
| |
183 | func contextPrefix(from: Int, to: Int, paragraphOnly: Bool = false) -> String? { |
| |
184 | saveExcursion { |
| |
185 | point = from |
| |
186 | if eolp { forwardLine(1) } |
| |
187 | point = lineStart(point) |
| |
188 | var first = matchAdaptivePrefix(paragraphOnly: paragraphOnly) |
| |
189 | forwardLine(1) |
| |
190 | if point < to { |
| |
191 | point = lineStart(point) |
| |
192 | let second: String? = looking(paragraphStartPattern, setsMatch: false) != nil ? nil : matchAdaptivePrefix(paragraphOnly: paragraphOnly) |
| |
193 | if let second { |
| |
194 | if first == nil { first = "" } |
| |
195 | var pattern = "^" |
| |
196 | var rest = Substring(second) |
| |
197 | while let match = rest.firstMatch(of: /^[ \t]*([^ \t]+)/) { |
| |
198 | pattern += ".*" + NSRegularExpression.escapedPattern(for: String(match.1)) |
| |
199 | rest = rest[match.range.upperBound...] |
| |
200 | } |
| |
201 | if first!.range(of: pattern, options: .regularExpression) != nil { return second } |
| |
202 | let common = zip(first!, second).prefix { $0 == $1 }.count |
| |
203 | return common == 0 ? nil : String(first!.prefix(common)) |
| |
204 | } |
| |
205 | } |
| |
206 | guard let firstPrefix = first else { return nil } |
| |
207 | let keep = firstPrefix.allSatisfy({ $0 == " " || $0 == "\t" }) || firstPrefix.range(of: "^\\s*#( |$)", options: .regularExpression) != nil |
| |
208 | let result = keep ? firstPrefix : String(repeating: " ", count: displayWidth(firstPrefix)) |
| |
209 | let probe = result + "a" |
| |
210 | let regex = Self.regex(paragraphStartPattern, caseInsensitive: false) |
| |
211 | if let match = regex.firstMatch(in: probe, options: [.anchored], range: NSRange(location: 0, length: (probe as NSString).length)), match.range.location == 0 { |
| |
212 | return nil |
| |
213 | } |
| |
214 | return result |
| |
215 | } |
| |
216 | } |
| |
217 | |
| |
218 | /// `fill-nobreak-p` with org's predicates. |
| |
219 | func fillNoBreak() -> Bool { |
| |
220 | if bolp { return false } |
| |
221 | let startsParagraph = saveExcursion { () -> Bool in |
| |
222 | skipForward(Self.blanks) |
| |
223 | return !eolp && looking(paragraphStartPattern, setsMatch: false) != nil |
| |
224 | } |
| |
225 | if startsParagraph { return true } |
| |
226 | let lineBreak = saveExcursion { () -> Bool in |
| |
227 | skipBackward(Self.blanks) |
| |
228 | skipBackward([92]) |
| |
229 | return looking("\\\\\\\\($|[^\\\\])", setsMatch: false) != nil |
| |
230 | } |
| |
231 | if lineBreak { return true } |
| |
232 | if looking("[ \\t]*\\{\\{\\{n(?:\\([^\\n)]*\\))?\\}\\}\\}[.)](?:$| )", setsMatch: false) != nil { return true } |
| |
233 | let saved = lastMatch |
| |
234 | defer { lastMatch = saved } |
| |
235 | if atTimestamp() != nil, looking("[\\[<](" + Self.timestampCore + ")[^\\]>\\n]*[\\]>]", setsMatch: false) == nil { return true } |
| |
236 | return false |
| |
237 | } |
| |
238 | |
| |
239 | /// `fill-move-to-break-point` for text without CJK characters. |
| |
240 | func moveToBreakPoint(_ lineBegin: Int) { |
| |
241 | if lineBegin > point { point = lineBegin } |
| |
242 | while true { |
| |
243 | guard let match = searchBackward("[ \\t]", bound: lineBegin, caseInsensitive: false) else { |
| |
244 | point = lineBegin |
| |
245 | break |
| |
246 | } |
| |
247 | point = match[0]!.lowerBound + 1 |
| |
248 | if fillNoBreak() { |
| |
249 | skipBackward(Self.blanks, bound: lineBegin) |
| |
250 | } else { |
| |
251 | break |
| |
252 | } |
| |
253 | } |
| |
254 | skipBackward(Self.blanks) |
| |
255 | if lineBegin >= point { |
| |
256 | let end = lineEnd(point) |
| |
257 | var first = true |
| |
258 | point = lineBegin |
| |
259 | while point < end, first || fillNoBreak() { |
| |
260 | skipForward(Self.blanks) |
| |
261 | while point < length, let c = character(at: point), c != 32, c != 10, c != 9 { point += 1 } |
| |
262 | first = false |
| |
263 | } |
| |
264 | } |
| |
265 | } |
| |
266 | |
| |
267 | /// `skip-chars-backward` stopping at `bound`. |
| |
268 | func skipBackward(_ set: Set<unichar>, bound: Int) { |
| |
269 | while point > bound, let c = character(at: point - 1), set.contains(c) { point -= 1 } |
| |
270 | } |
| |
271 | |
| |
272 | /// `canonically-space-region` with single spaces after sentences. |
| |
273 | func canonicallySpace(_ begin: Int, _ endOffset: Int) { |
| |
274 | saveExcursion { |
| |
275 | let end = marker(endOffset, advances: true) |
| |
276 | defer { release(end) } |
| |
277 | for offset in begin..<end.position where character(at: offset) == 9 { replace(offset..<(offset + 1), with: " ") } |
| |
278 | point = begin |
| |
279 | let pattern = "(" + Self.sentenceEndPattern + ") *| +" |
| |
280 | while point < end.position, let match = searchForward(pattern, bound: end.position, caseInsensitive: false) { |
| |
281 | let matchEnd = match[0]!.upperBound |
| |
282 | let start: Int |
| |
283 | if let sentence = match[1], matchEnd > sentence.upperBound { |
| |
284 | start = sentence.upperBound |
| |
285 | } else if match[1] != nil { |
| |
286 | let lastNonSpace = saveExcursion { () -> Int in point = matchEnd; skipBackward([32]); return point } |
| |
287 | start = min(matchEnd, lastNonSpace + 1) |
| |
288 | } else { |
| |
289 | let before = saveExcursion { () -> unichar? in |
| |
290 | point = match[0]!.lowerBound |
| |
291 | skipBackward(Set(" ]})\"'".utf16)) |
| |
292 | return character(at: point - 1) |
| |
293 | } |
| |
294 | start = match[0]!.lowerBound + (before == 10 ? 0 : 1) |
| |
295 | } |
| |
296 | if start < matchEnd { deleteRegion(start, matchEnd) } |
| |
297 | } |
| |
298 | } |
| |
299 | } |
| |
300 | |
| |
301 | /// `fill-region-as-paragraph` with left justification and adaptive fill. |
| |
302 | func fillRegionAsParagraph(_ fromOffset: Int, _ toOffset: Int, prefix fixedPrefix: String? = nil) { |
| |
303 | point = min(fromOffset, toOffset) |
| |
304 | var to = max(fromOffset, toOffset) |
| |
305 | skipForward(Self.whitespace) |
| |
306 | let fromPlusIndent = point |
| |
307 | point = lineStart(point) |
| |
308 | var from = max(point, fromOffset) |
| |
309 | point = to |
| |
310 | var oneLeft = false |
| |
311 | while point > from, character(at: point - 1) == 10 { |
| |
312 | if oneLeft { |
| |
313 | deleteRegion(point - 1, point) |
| |
314 | } else { |
| |
315 | point -= 1 |
| |
316 | oneLeft = true |
| |
317 | } |
| |
318 | } |
| |
319 | let toMarker = marker(point, advances: true) |
| |
320 | defer { release(toMarker) } |
| |
321 | point = fromPlusIndent |
| |
322 | guard toMarker.position > point else { return } |
| |
323 | var prefix = fixedPrefix |
| |
324 | if prefix == nil || prefix == "" { prefix = contextPrefix(from: from, to: toMarker.position) } |
| |
325 | point = lineStart(from) |
| |
326 | // `fill-delete-prefix`. |
| |
327 | let prefixPattern: String |
| |
328 | if let prefix, !prefix.allSatisfy({ $0 == " " || $0 == "\t" }) { |
| |
329 | prefixPattern = "[ \\t]*(" + NSRegularExpression.escapedPattern(for: prefix).replacingOccurrences(of: "[ \\t]+", with: "[ \\t]*", options: .regularExpression) + ")?[ \\t]*" |
| |
330 | } else { |
| |
331 | prefixPattern = "[ \\t]*" |
| |
332 | } |
| |
333 | point = from |
| |
334 | forwardLine(1) |
| |
335 | while point < toMarker.position { |
| |
336 | if let match = looking(prefixPattern, setsMatch: false) { deleteRegion(point, match[0]!.upperBound) } |
| |
337 | if forwardLine(1) != 0 { break } |
| |
338 | } |
| |
339 | point = from |
| |
340 | if let match = looking(prefixPattern, setsMatch: false) { point = match[0]!.upperBound } |
| |
341 | from = point |
| |
342 | // `fill-delete-newlines`. |
| |
343 | var at = from |
| |
344 | while at < toMarker.position { |
| |
345 | if character(at: at) == 10 { replace(at..<(at + 1), with: " ") } |
| |
346 | at += 1 |
| |
347 | } |
| |
348 | point = from |
| |
349 | skipForward(Self.blanks) |
| |
350 | canonicallySpace(point, toMarker.position) |
| |
351 | point = toMarker.position |
| |
352 | let end = point |
| |
353 | skipBackward(Self.blanks) |
| |
354 | if point < end { deleteRegion(point, end) } |
| |
355 | point = from |
| |
356 | while point < toMarker.position { |
| |
357 | let lineBegin = point |
| |
358 | moveToColumn(options.fillColumn) |
| |
359 | var breaks = false |
| |
360 | if point < toMarker.position, lineBegin < toMarker.position { |
| |
361 | if currentColumn <= options.fillColumn { point += 1 } |
| |
362 | moveToBreakPoint(lineBegin) |
| |
363 | skipForward(Self.blanks) |
| |
364 | breaks = point < toMarker.position |
| |
365 | } |
| |
366 | guard breaks else { |
| |
367 | point = toMarker.position |
| |
368 | break |
| |
369 | } |
| |
370 | // `fill-newline`. |
| |
371 | skipBackward(Self.blanks) |
| |
372 | insert("\n") |
| |
373 | indentLineTo(0) |
| |
374 | if let prefix, !prefix.isEmpty { insertBeforeMarkers(prefix) } |
| |
375 | } |
| |
376 | point = toMarker.position |
| |
377 | if !eobp { point += 1 } |
| |
378 | } |
| |
379 | |
| |
380 | /// `org-fill-paragraph` without a region or justification. |
| |
381 | func fillParagraph() { |
| |
382 | saveExcursion { |
| |
383 | if lookingAt("^[ \\t]*$") != nil || saveExcursion({ beginningOfLine(); return lookingAt("[ \\t]*$") != nil }) { |
| |
384 | skipForward(Self.whitespace) |
| |
385 | } |
| |
386 | let eol = lineEnd(point) |
| |
387 | guard let element = element(at: eol) else { return } |
| |
388 | switch element.kind { |
| |
389 | case .paragraph: |
| |
390 | let begin = paragraphContentsBegin(element) |
| |
391 | var end = contentsEnd(element) |
| |
392 | guard lineEnd(point) >= begin else { return } |
| |
393 | var cuts = [begin] |
| |
394 | saveExcursion { |
| |
395 | point = begin |
| |
396 | while let match = searchForward("\\\\\\\\[ \\t]*\\n", bound: end, caseInsensitive: false) { |
| |
397 | cuts.insert(match[0]!.upperBound, at: 0) |
| |
398 | } |
| |
399 | } |
| |
400 | for cut in cuts where cut != end { |
| |
401 | fillRegionAsParagraph(cut, end) |
| |
402 | end = cut |
| |
403 | } |
| |
404 | case .comment: |
| |
405 | let begin = postAffiliated(element) |
| |
406 | let elementEnd = element.range.upperBound |
| |
407 | guard point >= begin, point <= elementEnd else { return } |
| |
408 | let from = saveExcursion { () -> Int in |
| |
409 | endOfLine() |
| |
410 | if searchBackward("^[ \\t]*#[ \\t]*$", bound: begin, caseInsensitive: false) != nil { |
| |
411 | forwardLine(1) |
| |
412 | return point |
| |
413 | } |
| |
414 | return begin |
| |
415 | } |
| |
416 | let to = saveExcursion { () -> Int in |
| |
417 | endOfLine() |
| |
418 | if searchForward("^[ \\t]*#[ \\t]*$", bound: elementEnd, caseInsensitive: false) != nil { |
| |
419 | return lineStart(point) - 1 |
| |
420 | } |
| |
421 | point = elementEnd |
| |
422 | skipBackward(Self.whitespace) |
| |
423 | return lineEnd(point) |
| |
424 | } |
| |
425 | guard to > from else { return } |
| |
426 | let prefix = saveExcursion { () -> String in |
| |
427 | beginningOfLine() |
| |
428 | let comment = looking("[ \\t]*#", setsMatch: false)! |
| |
429 | point = comment[0]!.upperBound |
| |
430 | let marker = substring(comment[0]!.lowerBound, comment[0]!.upperBound) |
| |
431 | if let rest = looking(Self.adaptiveFillPattern, setsMatch: false) { |
| |
432 | return marker + substring(rest[0]!.lowerBound, rest[0]!.upperBound) |
| |
433 | } |
| |
434 | return marker + " " |
| |
435 | } |
| |
436 | saveExcursion { fillRegionAsParagraph(from, to, prefix: prefix) } |
| |
437 | case .block where element.blockType == "comment": |
| |
438 | let begin = saveExcursion { () -> Int in |
| |
439 | point = element.range.lowerBound |
| |
440 | forwardLine(1) |
| |
441 | return point |
| |
442 | } |
| |
443 | let end = saveExcursion { () -> Int in |
| |
444 | point = element.range.upperBound |
| |
445 | skipBackward(Self.whitespace) |
| |
446 | return lineStart(point) |
| |
447 | } |
| |
448 | guard point >= begin, point <= end else { return } |
| |
449 | let from = saveExcursion { () -> Int in |
| |
450 | endOfLine() |
| |
451 | if searchBackward("^[ \\t]*$", bound: begin, caseInsensitive: false) == nil { point = begin } |
| |
452 | return lineStart(point) |
| |
453 | } |
| |
454 | let to = saveExcursion { () -> Int in |
| |
455 | beginningOfLine() |
| |
456 | if searchForward("^[ \\t]*$", bound: end, caseInsensitive: false) == nil { point = end } |
| |
457 | return lineStart(point) |
| |
458 | } |
| |
459 | fillRegionAsParagraph(from, to) |
| |
460 | default: |
| |
461 | return |
| |
462 | } |
| |
463 | } |
| |
464 | } |
| |
465 | |
| |
466 | /// Where a paragraph's text starts: past affiliated keywords, and for a paragraph that |
| |
467 | /// opens an item, past the bullet. |
| |
468 | func paragraphContentsBegin(_ node: Element) -> Int { |
| |
469 | max(postAffiliated(node), node.range.lowerBound) |
| |
470 | } |
| |
471 | } |
| |
472 | |
| |
473 | /// M-q: `org-fill-paragraph`. |
| |
474 | public struct FillParagraph: OrgCommand { |
| |
475 | public init() {} |
| |
476 | public var id: String { "org.fill-paragraph" } |
| |
477 | public var title: String { "Fill Paragraph" } |
| |
478 | public func applies(in context: EditContext) -> Bool { true } |
| |
479 | public func run(in context: EditContext) -> CommandStep { commitBuffer(context) { $0.fillParagraph() } } |
| |
480 | } |