krz/orgstar

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

Tests/OrgAppTests/EmacsImportTests.swift

c1295824f55c7bba681fca8338bf08167e110105
orgstar/Tests/OrgAppTests/EmacsImportTests.swift history · blame · raw

184 lines · 11478 bytes

  1import Foundation
  2import OrgCore
  3import Testing
  4@testable import OrgApp
  5
  6struct EmacsImportTests {
  7    static let config = #"""
  8    (setq user-full-name "Someone")
  9    (setq doom-theme 'doom-homage-black)
 10    (setq doom-font (font-spec :family "JetBrains Mono" :size 14))
 11    (setq display-line-numbers-type t)
 12    (setq org-support-shift-select t)
 13    (setq org-directory "~/notes/")
 14    (setq org-roam-directory (file-name-concat org-directory "roam"))
 15    (after! org
 16      (setq org-hide-emphasis-markers t
 17            org-pretty-entities t
 18            org-agenda-files '("~/notes/" "~/work/plan.org")
 19            org-tags-column -80))
 20    (setq org-tags-column 4)
 21    (setq org-log-done 'note org-log-into-drawer t org-log-reschedule 'time)
 22    (setq org-agenda-prefix-format '((agenda . " %i %-12:c%?-12t% s%b") (todo . " %-8:c")))
 23    (setq fill-column (+ 70 2))
 24    (setq org-capture-templates
 25          '(("t" "Todo" entry (file+headline "~/notes/todo.org" "Inbox") "* TODO %?\n%a")
 26            ("j" "Journal" entry (file+olp+datetree "~/notes/journal.org") "* %?" :tree-type week :empty-lines 1 :time-prompt t)
 27            ("c" "Clocked" item (clock) "%?" :immediate-finish t)
 28            ("x" "Function" entry (function ignore) "* %?")
 29            ("w" "Work")
 30            ("wn" "Work note" plain (file "~/work/notes.org") "%?" :prepend t)))
 31    (map! :leader :desc "Report" "z t" #'tr/report
 32          :localleader "x" #'org-toggle-checkbox)
 33    (map! :n "g s" #'save-buffer)
 34    (global-set-key (kbd "C-c a") #'org-agenda)
 35    (with-eval-after-load 'org (define-key org-mode-map (kbd "C-c l") (lambda () (interactive))))
 36    """#
 37
 38    static let doomOrg = #"""
 39    (defun +org-init-appearance-h ()
 40      (setq org-tags-column 0
 41            org-agenda-span 10
 42            org-agenda-start-day "-3d"
 43            org-clock-idle-time 15
 44            org-clock-history-length 10
 45            org-todo-keywords '((sequence "TODO(t)" "WAIT(w)" "|" "DONE(d)") (sequence "[ ](T)" "|" "[X](D)") (sequence "BUG" "|" "FIXED"))))
 46    (after! org (setq org-M-RET-may-split-line nil org-list-allow-alphabetical t))
 47    (map! :map org-mode-map "C-c q" #'org-set-tags-command)
 48    """#
 49
 50    func scan() throws -> EmacsImport {
 51        let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-import-\(UUID().uuidString)")
 52        try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
 53        defer { try? FileManager.default.removeItem(at: folder) }
 54        try "(doom! :editor (evil +everywhere) :lang (org +pretty))".write(to: folder.appendingPathComponent("init.el"), atomically: true, encoding: .utf8)
 55        try Self.config.write(to: folder.appendingPathComponent("config.el"), atomically: true, encoding: .utf8)
 56        let doom = folder.appendingPathComponent("doom-org.el")
 57        try Self.doomOrg.write(to: doom, atomically: true, encoding: .utf8)
 58        return EmacsImport.scan(EmacsImport.files(in: folder), doomDefaults: [doom])
 59    }
 60
 61    func settings(_ result: EmacsImport) -> [String: TOML.Value] {
 62        var out: [String: TOML.Value] = [:]
 63        for item in result.items { if case .setting(let key, let value) = item.change { out[key] = value } }
 64        return out
 65    }
 66
 67    @Test func readsSettingsWithDoomDefaultsUnderneath() throws {
 68        let result = try scan()
 69        #expect(result.isDoom)
 70        let s = settings(result)
 71        #expect(s["keymap"] == .string("doom"))
 72        #expect(s["display-line-numbers-type"] == .bool(true))
 73        #expect(s["org-hide-emphasis-markers"] == .bool(true))
 74        #expect(s["org-pretty-entities"] == .bool(true))
 75        #expect(s["org-log-done"] == .string("note"))
 76        #expect(s["org-log-reschedule"] == .string("time"))
 77        #expect(s["org-log-into-drawer"] == .string("LOGBOOK"))
 78        #expect(s["org-agenda-prefix-format.agenda"] == .string(" %i %-12:c%?-12t% s%b"))
 79        #expect(s["org-agenda-prefix-format.todo"] == .string(" %-8:c"))
 80        // after! org runs after the top-level setq.
 81        #expect(s["org-tags-column"] == .integer(-80))
 82        #expect(s["org-agenda-span"] == .integer(10))
 83        #expect(s["org-agenda-start-day"] == .string("-3d"))
 84        #expect(s["org-clock-idle-time"] == .integer(15))
 85        #expect(s["org-clock-history-length"] == .integer(10))
 86        #expect(s["org-M-RET-may-split-line"] == .bool(false))
 87        #expect(s["org-todo-keywords"] == .string("TODO(t) WAIT(w) | DONE(d)\nBUG | FIXED"))
 88        #expect(s["fill-column"] == nil)
 89        #expect(s["font"] == .string("JetBrains Mono") && s["font-size"] == .integer(14))
 90        let folders = result.items.compactMap { if case .folder(let f) = $0.change { f } else { nil } }
 91        #expect(folders == ["~/notes/", "~/work"])
 92        #expect(result.unreadable.map(\.name) == ["fill-column"])
 93        #expect(Set(result.unsupported.map(\.name)) == ["org-support-shift-select", "doom-theme", "org-roam-directory",
 94            "capture \"j\"", "capture \"x\"", "SPC z t → tr/report", "C-c l"])
 95        #expect(result.skipped == 1)
 96        #expect(result.doomDefaultsLeftOut == 1)
 97    }
 98
 99    @Test func readsCaptureTemplatesAndBindings() throws {
100        let result = try scan()
101        let captures = result.items.compactMap { if case .capture(let key, let toml) = $0.change { (key, toml) } else { nil } }
102        #expect(captures.map(\.0) == ["t", "j", "c", "wn"])
103        #expect(captures[1].1 == "[[template]]\nkey = \"j\"\nname = \"Journal\"\ntype = \"entry\"\nfile = \"~/notes/journal.org\"\ndatetree = true\ntree-type = \"week\"\nempty-lines = 1\ntemplate = \"* %?\"\n")
104        #expect(captures[2].1.contains("clock = true\n") && captures[2].1.contains("immediate-finish = true\n"))
105        #expect(result.unsupported.contains { $0.name == "capture \"j\"" && $0.reason == ":time-prompt left out" })
106        #expect(captures[0].1 == "[[template]]\nkey = \"t\"\nname = \"Todo\"\ntype = \"entry\"\nfile = \"~/notes/todo.org\"\nheadline = \"Inbox\"\ntemplate = \"* TODO %?\\n%a\"\n")
107        #expect(captures[3].1.contains("prepend = true\n") && captures[3].1.contains("type = \"plain\""))
108
109        let bindings = result.items.compactMap { if case .binding(let keys, let command, let mode) = $0.change { "\(keys) \(command) \(mode ?? "-")" } else { nil } }
110        #expect(bindings == ["SPC m x org.checkbox.toggle normal", "g s app.save normal", "C-c a app.agenda normal"])
111        #expect(result.unsupported.contains { $0.name == "SPC z t → tr/report" })
112        #expect(result.unsupported.contains { $0.name == "C-c l" && $0.reason == "bound to code, not a command" })
113    }
114
115    @Test func readsCaptureTargetsFromVariablesAndForms() throws {
116        let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-import-\(UUID().uuidString)")
117        try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
118        defer { try? FileManager.default.removeItem(at: folder) }
119        try #"""
120            (defvar my-inbox "~/old.org")
121            (setq org-directory "~/org"
122                  org-default-notes-file (expand-file-name "notes.org" org-directory)
123                  my-inbox (concat org-directory "/inbox.org"))
124            (setq org-capture-templates
125                  `(("n" "Note" entry (file+headline org-default-notes-file "Notes") "* %?")
126                    ("i" "Inbox" entry (file my-inbox) "* TODO %?")
127                    ("w" "Work" entry (file ,(concat org-directory "/work.org")) "* %?")
128                    ("j" "Journal" entry (file+olp+datetree (concat org-directory "/journal.org")) "* %?")))
129            """#.write(to: folder.appendingPathComponent("init.el"), atomically: true, encoding: .utf8)
130        let result = EmacsImport.scan(EmacsImport.files(in: folder))
131        let files = result.items.compactMap { item -> String? in
132            guard case .capture(let key, let toml) = item.change else { return nil }
133            return key + " " + (toml.firstMatch(of: /file = "([^"]*)"/).map { String($0.1) } ?? "-")
134        }
135        #expect(files == ["n ~/org/notes.org", "i ~/org/inbox.org", "w ~/org/work.org", "j ~/org/journal.org"])
136    }
137
138    /// The user's own Doom configuration, when there is one.
139    @Test(.enabled(if: FileManager.default.fileExists(atPath: NSHomeDirectory() + "/.config/doom/config.el")))
140    func readsTheInstalledDoomConfig() {
141        let result = EmacsImport.scan(EmacsImport.files(in: URL(fileURLWithPath: NSHomeDirectory() + "/.config/doom")))
142        #expect(result.problems.isEmpty, "\(result.problems)")
143        let s = settings(result)
144        #expect(s["keymap"] == .string("doom"))
145        #expect(s["org-agenda-start-day"] == .string("-3d"))
146        #expect(s["org-tags-column"] == .integer(0))
147        let report = result.items.map { "\($0.name) | \($0.summary) | \($0.source)" }
148            + result.unsupported.map { "NO \($0.name): \($0.reason) | \($0.source)" }
149            + result.unreadable.map { "?? \($0.name): \($0.reason) | \($0.source)" }
150        print(report.joined(separator: "\n"))
151    }
152
153    @MainActor
154    @Test func appliesToDefaultsAndFiles() async throws {
155        let result = try scan()
156        let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-import-out-\(UUID().uuidString)")
157        defer { try? FileManager.default.removeItem(at: folder) }
158        try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
159        try "[[template]]\nkey = \"t\"\nfile = \"x.org\"\ntemplate = \"* %?\"\n".write(to: folder.appendingPathComponent("capture.toml"), atomically: true, encoding: .utf8)
160        let name = "orgstar-import-\(UUID().uuidString)"
161        let defaults = UserDefaults(suiteName: name)!
162        defer { defaults.removePersistentDomain(forName: name) }
163        let summary = await EmacsImport.apply(result.items, defaults: defaults, workspace: nil, directory: folder)
164        #expect(defaults.string(forKey: "keymap") == "doom")
165        #expect(defaults.integer(forKey: "tagsColumn") == -80)
166        #expect(defaults.integer(forKey: "agendaStartOffset") == -3)
167        #expect(defaults.string(forKey: "todoKeywords") == "TODO(t) WAIT(w) | DONE(d)\nBUG | FIXED")
168        let capture = try String(contentsOf: folder.appendingPathComponent("capture.toml"), encoding: .utf8)
169        #expect(capture.components(separatedBy: "[[template]]").count == 5)
170        #expect(CaptureTemplates.load(userFile: folder.appendingPathComponent("capture.toml")).problems.isEmpty)
171        let keymap = folder.appendingPathComponent("keymap.toml")
172        #expect(KeymapLoader.load(.doom, userFile: keymap).problems.isEmpty)
173        #expect(KeymapLoader.load(.doom, userFile: keymap).keymap.keyLabels(for: "app.agenda", modes: ["normal"]).contains("C-c a"))
174        #expect(summary.hasPrefix("Imported 19 settings, 3 capture templates, 3 key bindings."), "\(summary)")
175        #expect(defaults.string(forKey: "themeFont") == "JetBrains Mono")
176        #expect(defaults.integer(forKey: "themeFontSize") == 14)
177
178        // Importing again adds no bindings or templates.
179        let before = try String(contentsOf: keymap, encoding: .utf8)
180        let again = await EmacsImport.apply(result.items, defaults: defaults, workspace: nil, directory: folder)
181        #expect(try String(contentsOf: keymap, encoding: .utf8) == before)
182        #expect(!again.contains("key binding") && !again.contains("capture template"), "\(again)")
183    }
184}