krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Tests/OrgCoreTests/FillTests.swift

c1295824f55c7bba681fca8338bf08167e110105
orgstar/Tests/OrgCoreTests/FillTests.swift history · blame · raw

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}