Tests/OrgCoreTests/FillTests.swift
110 lines · 6916 bytes
1import Foundation
2import Testing
3@testable import OrgCore
4
5struct FillTests {
6 @Test func fillsAParagraph() {
7 let text = "one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen\n"
8 let filled = runCommand(FillParagraph(), text, caret: 0).text
9 #expect(filled == "one two three four five six seven eight nine ten eleven twelve\nthirteen fourteen fifteen\n")
10 }
11
12 static let long = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris."
13 static let variants = [
14 long + "\n",
15 "Short\nlines that\nshould join. Two spaces after.\n",
16 "- item " + long + "\n - nested " + long + "\n",
17 "1. [ ] " + long + "\n",
18 "# comment " + long + "\n# more\n",
19 "* Heading\n" + long + "\n\nSecond " + long + "\n",
20 " indented " + long + "\n continued\n",
21 "> quoted " + long + "\n> more quoted text\n",
22 "Has a line break \\\\\n" + long + "\n",
23 "Date <2026-09-10 Thu 10:00> inside a long sentence that keeps going and going and going past the column.\n",
24 "A [[https://example.com/a/very/long/path][link with a description]] that runs past the fill column for sure.\n",
25 "Ends with - a dash that would start a list if it began a line and continues further on.\n",
26 "Words with extra spaces\tand\ttabs that should collapse into single spaces when filled together.\n",
27 "#+CAPTION: cap\nCaptioned paragraph " + long + "\n",
28 "*bold start* of a line with " + long + "\n",
29 "Unicode ünïcödé wörds ñ " + long + "\n",
30 ]
31
32 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
33 func fillMatchesEmacs() throws {
34 let commands: [(command: any OrgCommand, form: String, headingLineOnly: Bool)] = [(FillParagraph(), "(org-fill-paragraph)", false)]
35 let inputs = Self.variants.map { ($0, EmacsOracle.positions($0).enumerated().filter { $0.offset % 7 == 0 || $0.element == 0 }.map(\.element)) }
36 try compareWithEmacs(inputs, commands)
37 try compareWithEmacs(inputs, commands, options: doomOptions)
38 }
39
40 /// M-q with an active region: `org-fill-paragraph` called interactively.
41 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
42 func regionFillMatchesEmacs() throws {
43 try #require(EmacsOracle.isAvailable)
44 let texts = [
45 "Short\nlines\n\n" + Self.long + "\nmore\n\nlast\npara\n",
46 "* H\nintro\ntext\n- item " + Self.long + "\n- b\n c\n# comment\n# lines\n#\n# again\n",
47 "a\nb\n#+begin_comment\none\ntwo\n\nthree\nfour\n#+end_comment\n:NOTES:\nx\ny\n:END:\n#+begin_quote\nq\nr\n#+end_quote\nz\nw\n",
48 "Words with extra spaces\n\n and more\n",
49 ]
50 var cases: [EmacsOracle.Case] = []
51 var ours: [(label: String, text: String, caret: Int)] = []
52 for text in texts {
53 let positions = EmacsOracle.positions(text)
54 for (i, from) in positions.enumerated() where i % 3 == 0 {
55 for to in positions where to > from && (to - from) % 5 == 1 {
56 let form = "(progn (transient-mark-mode 1) (set-mark \(EmacsOracle.point(to, in: text))) (activate-mark) (org-fill-paragraph nil t))"
57 cases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(from, in: text), form: withOptions(.org, form)))
58 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [from..<to], options: .org)
59 guard case .commit(let result) = FillParagraph().run(in: context) else {
60 ours.append(("\(from)..<\(to) failed", text, -1))
61 continue
62 }
63 let new = result.edits.sorted { $0.range.lowerBound > $1.range.lowerBound }.reduce(text) { $1.apply(to: $0) }
64 ours.append(("\(from)..<\(to) in \(text.debugDescription)", new, result.selection?.first?.lowerBound ?? from))
65 }
66 }
67 }
68 var mismatches = 0
69 for (mine, emacs) in zip(ours, try EmacsOracle.run(cases)) {
70 let caret = EmacsOracle.offset(emacs.point, in: emacs.text)
71 guard !emacs.error.isEmpty || mine.text != emacs.text || mine.caret != caret else { continue }
72 mismatches += 1
73 if mismatches <= 12 {
74 Issue.record("\(mine.label)\n ours: \(mine.text.debugDescription) @\(mine.caret)\n emacs: \(emacs.error.isEmpty ? "\(emacs.text.debugDescription) @\(caret)" : emacs.error)")
75 }
76 }
77 #expect(mismatches == 0, "\(mismatches) of \(ours.count) differ")
78 }
79
80 /// Real paragraphs: `ORGSTAR_ORACLE_CORPUS=<folder>` fills the first 30 paragraph-like lines
81 /// of up to 60 files.
82 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_ORACLE_CORPUS"] != nil))
83 func fillMatchesEmacsOnACorpus() throws {
84 let root = URL(fileURLWithPath: ProcessInfo.processInfo.environment["ORGSTAR_ORACLE_CORPUS"]!)
85 let files = FileManager.default.enumerator(at: root, includingPropertiesForKeys: nil)!
86 .compactMap { $0 as? URL }.filter { $0.pathExtension == "org" }.sorted { $0.path < $1.path }
87 var samples: [(String, [Int])] = []
88 for file in files where samples.count < 60 {
89 guard let text = try? String(contentsOf: file, encoding: .utf8), text.utf16.count <= 20_000, !text.contains("\\r") else { continue }
90 let ns = text as NSString
91 var carets: [Int] = []
92 var location = 0
93 var inSource = false
94 while location < ns.length, carets.count < 30 {
95 let line = ns.lineRange(for: NSRange(location: location, length: 0))
96 let content = ns.substring(with: line)
97 let lower = content.lowercased().trimmingCharacters(in: .whitespaces)
98 // M-q in a src block fills through the language's mode in Emacs; Orgstar leaves code alone.
99 if lower.hasPrefix("#+begin_src") { inSource = true }
100 if lower.hasPrefix("#+end_src") { inSource = false }
101 if !inSource, content.count > 20, !content.hasPrefix("*"), !content.hasPrefix("#+"), !content.hasPrefix("|") { carets.append(line.location + 3) }
102 location = NSMaxRange(line)
103 }
104 if !carets.isEmpty { samples.append((text, carets)) }
105 }
106 let commands: [(command: any OrgCommand, form: String, headingLineOnly: Bool)] = [(FillParagraph(), "(org-fill-paragraph)", false)]
107 try compareWithEmacs(samples, commands)
108 try compareWithEmacs(samples, commands, options: doomOptions)
109 }
110}