krz/orgstar

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

Commit 46d6a046f5

46d6a046f519e5ab5a9338ff643341bd53e7788f

parent: 5bb13b8565

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-07 07:29 UTC

Run Emacs Lisp blocks on iOS

Emacs Lisp blocks run in the Lisp interpreter on iOS, which writes
what the Emacs script would print, so results are inserted the same
way; a Lisp error fails the run as in Emacs. The interpreter gains
princ, prin1, print, terpri, message, prin1-to-string and
with-output-to-string. Other languages, and Lisp the interpreter
doesn't have, say they need the Mac. Runs are confirmed as on the
Mac.

The command palette now runs the chosen command once it has closed,
not after a fixed delay, which crashed when the command's prompt
arrived while the palette and its keyboard were still going.

Layout: unified · split

Sources/OrgApp/DocumentSession.swift +17 −6
@@ -688,14 +688,26 @@ public final class DocumentSession {
688 /// Runs a src block and writes its result under it, unless the block changed while it 688 /// Runs a src block and writes its result under it, unless the block changed while it
689 /// ran. Returns a message for the echo area. 689 /// ran. Returns a message for the echo area.
690 public func runBabel(_ job: BabelJob) async -> String { 690 public func runBabel(_ job: BabelJob) async -> String {
691 #if os(macOS)
692 guard let entry = current else { return "No file is open." } 691 guard let entry = current else { return "No file is open." }
693 let directory = URL(fileURLWithPath: entry.path).deletingLastPathComponent() 692 let directory = URL(fileURLWithPath: entry.path).deletingLastPathComponent()
693 /// A program on the Mac; on iOS, Emacs Lisp in `Elisp`, and nothing else.
694 func run(_ job: BabelJob) async throws -> BabelOutput {
695 #if os(macOS)
696 return try await BabelRunner.run(job, fileDirectory: directory)
697 #else
698 guard let output = try Babel.evaluateNatively(job) else {
699 throw Babel.Failure.message(job.lispForm == nil
700 ? "\(job.language) blocks need the Mac to run."
701 : "This block uses Emacs Lisp that runs only in Emacs, on the Mac.")
702 }
703 return output
704 #endif
705 }
694 do { 706 do {
695 // Blocks the `:var`s refer to run first, one at a time. 707 // Blocks the `:var`s refer to run first, one at a time.
696 var job = job 708 var job = job
697 while let need = job.needs.first { 709 while let need = job.needs.first {
698 let output = try await BabelRunner.run(need.job, fileDirectory: directory) 710 let output = try await run(need.job)
699 guard entries.contains(where: { $0 === entry }) else { return "The file was closed before the block finished." } 711 guard entries.contains(where: { $0 === entry }) else { return "The file was closed before the block finished." }
700 let document = entry.buffer.document 712 let document = entry.buffer.document
701 switch Babel.resume(job, need: need, output: output, text: document.text, tree: document.tree) { 713 switch Babel.resume(job, need: need, output: output, text: document.text, tree: document.tree) {
@@ -703,7 +715,7 @@ public final class DocumentSession {
703 case .success(let next): job = next 715 case .success(let next): job = next
704 } 716 }
705 } 717 }
706 let output = try await BabelRunner.run(job, fileDirectory: directory) 718 let output = try await run(job)
707 guard entries.contains(where: { $0 === entry }) else { return "The file was closed before the block finished." } 719 guard entries.contains(where: { $0 === entry }) else { return "The file was closed before the block finished." }
708 if let file = Babel.fileResult(job, output: output) { 720 if let file = Babel.fileResult(job, output: output) {
709 let runDirectory = job.directory.map { URL(fileURLWithPath: $0, relativeTo: directory).standardizedFileURL } ?? directory 721 let runDirectory = job.directory.map { URL(fileURLWithPath: $0, relativeTo: directory).standardizedFileURL } ?? directory
@@ -729,12 +741,11 @@ public final class DocumentSession {
729 } 741 }
730 } catch is CancellationError { 742 } catch is CancellationError {
731 return "Code block canceled." 743 return "Code block canceled."
744 } catch Babel.Failure.message(let message) {
745 return message
732 } catch { 746 } catch {
733 return "\(error)" 747 return "\(error)"
734 } 748 }
735 #else
736 return "Source blocks don't run on iOS."
737 #endif
738 } 749 }
739 750
740 /// The block running now, to cancel. 751 /// The block running now, to cancel.
Sources/OrgCore/Compute/Babel.swift +46 −2
@@ -36,6 +36,8 @@ public struct BabelJob: Sendable, Equatable {
36 var resolved: [String: Babel.VarValue] = [:] 36 var resolved: [String: Babel.VarValue] = [:]
37 var caret = 0 37 var caret = 0
38 var options = Babel.Options() 38 var options = Babel.Options()
39 /// An Emacs Lisp block's form, for `Babel.evaluateNatively`.
40 public internal(set) var lispForm: String?
39 41
40 public struct Need: Sendable, Equatable { 42 public struct Need: Sendable, Equatable {
41 /// The `:var` value, `name` or `name(args)`. 43 /// The `:var` value, `name` or `name(args)`.
@@ -367,10 +369,12 @@ public enum Babel {
367 let bindings = vars.map { "(\($0.0) '\(lispLiteral($0.1)))" } 369 let bindings = vars.map { "(\($0.0) '\(lispLiteral($0.1)))" }
368 let expanded = bindings.isEmpty ? body + "\n" : "(let (\(bindings.joined(separator: "\n ")))\n\(body)\n)" 370 let expanded = bindings.isEmpty ? body + "\n" : "(let (\(bindings.joined(separator: "\n ")))\n\(body)\n)"
369 let form = output ? "(with-output-to-string \(expanded))" : "(progn \(expanded)\n)" 371 let form = output ? "(with-output-to-string \(expanded))" : "(progn \(expanded)\n)"
370 return .success(BabelJob( 372 var job = BabelJob(
371 language: language, runner: .emacs, input: emacsScript(form: form, params: params.results), 373 language: language, runner: .emacs, input: emacsScript(form: form, params: params.results),
372 directory: dir, block: location, blockText: blockText, alwaysAsk: alwaysAsk, finish: finishBase(.emacsLisp) 374 directory: dir, block: location, blockText: blockText, alwaysAsk: alwaysAsk, finish: finishBase(.emacsLisp)
373 )) 375 )
376 job.lispForm = form
377 return .success(job)
374 } 378 }
375 if ["dot", "plantuml", "mermaid"].contains(language) { 379 if ["dot", "plantuml", "mermaid"].contains(language) {
376 // ob-dot, ob-plantuml and ob-mermaid: the program draws into `:file`. 380 // ob-dot, ob-plantuml and ob-mermaid: the program draws into `:file`.
@@ -939,6 +943,46 @@ public enum Babel {
939 """ 943 """
940 } 944 }
941 945
946 /// An Emacs Lisp job run by `Elisp` instead of Emacs: the output `emacsScript` would print.
947 /// A Lisp error fails the run, as it does in Emacs; nil when the block uses what `Elisp`
948 /// doesn't have.
949 public static func evaluateNatively(_ job: BabelJob) throws(Failure) -> BabelOutput? {
950 guard let form = job.lispForm, let sexp = try? LispReader.readFirst(form).sexp else { return nil }
951 let lisp = Elisp()
952 let result: Sexp
953 do {
954 result = try lisp.eval(sexp)
955 } catch is Elisp.Unsupported {
956 return nil
957 } catch {
958 throw .message("\(error)")
959 }
960 func cell(_ c: Sexp) -> String { Elisp.printed(c) }
961 func isProper(_ v: Sexp) -> Bool {
962 if case .list = v { return true }
963 return Elisp.isNil(v)
964 }
965 func isAtom(_ v: Sexp) -> Bool {
966 switch v {
967 case .list(let items): items.isEmpty
968 case .dotted: false
969 default: true
970 }
971 }
972 var rows: [Any] = []
973 if case .list(let items) = result, items.allSatisfy({ isAtom($0) || isProper($0) }) {
974 if items.allSatisfy({ $0 == .symbol("hline") || isProper($0) }) {
975 rows = items.map { $0 == .symbol("hline") ? "hline" as Any : ((try? Elisp.elements($0)) ?? []).map(cell) as Any }
976 } else {
977 rows = [items.map(cell)]
978 }
979 }
980 var object: [String: Any] = ["s": Elisp.printed(result), "S": Elisp.printed(result, escape: true), "string": result.string != nil]
981 object["rows"] = rows.isEmpty ? NSNull() : rows
982 guard let data = try? JSONSerialization.data(withJSONObject: object), let json = String(data: data, encoding: .utf8) else { return nil }
983 return BabelOutput(stdout: lisp.standardOutput + json, stderr: lisp.messages)
984 }
985
942 // MARK: - Finishing 986 // MARK: - Finishing
943 987
944 /// What the run produced, as Babel holds it before inserting. 988 /// What the run produced, as Babel holds it before inserting.
Sources/OrgCore/Config/Elisp.swift +41 −1
@@ -20,6 +20,13 @@ public final class Elisp {
20 private var functions: [String: Builtin] = [:] 20 private var functions: [String: Builtin] = [:]
21 private var steps = 0 21 private var steps = 0
22 static let stepLimit = 1_000_000 22 static let stepLimit = 1_000_000
23 /// What `princ` and the other printers wrote: the innermost `with-output-to-string`, or
24 /// standard output.
25 private var outputs: [String] = [""]
26 /// What went to standard output.
27 public var standardOutput: String { outputs[0] }
28 /// What `message` wrote.
29 public private(set) var messages = ""
23 30
24 public init() { 31 public init() {
25 Self.core(self) 32 Self.core(self)
@@ -135,7 +142,7 @@ public final class Elisp {
135 /// `prin1-to-string` with `princ` for strings when `escape` is false. 142 /// `prin1-to-string` with `princ` for strings when `escape` is false.
136 public static func printed(_ v: Sexp, escape: Bool = false) -> String { 143 public static func printed(_ v: Sexp, escape: Bool = false) -> String {
137 switch v { 144 switch v {
138 case .string(let s): return escape ? v.description : s 145 case .string(let s): return escape ? "\"" + s.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") + "\"" : s
139 case .float(let d): return floatText(d) 146 case .float(let d): return floatText(d)
140 case .character(let c): return String(c) 147 case .character(let c): return String(c)
141 case .list(let items): return "(" + items.map { printed($0, escape: true) }.joined(separator: " ") + ")" 148 case .list(let items): return "(" + items.map { printed($0, escape: true) }.joined(separator: " ") + ")"
@@ -290,6 +297,15 @@ public final class Elisp {
290 return try eval(spec[2]) 297 return try eval(spec[2])
291 } 298 }
292 return .nil 299 return .nil
300 case "with-output-to-string":
301 outputs.append("")
302 do {
303 _ = try progn(args)
304 } catch {
305 outputs.removeLast()
306 throw error
307 }
308 return .string(outputs.removeLast())
293 case "ignore-errors": 309 case "ignore-errors":
294 do { return try progn(args) } catch is Signal { return .nil } 310 do { return try progn(args) } catch is Signal { return .nil }
295 case "condition-case": 311 case "condition-case":
@@ -623,6 +639,30 @@ public final class Elisp {
623 throw Signal(message: args.isEmpty ? "" : try format(args)) 639 throw Signal(message: args.isEmpty ? "" : try format(args))
624 } 640 }
625 641
642 // Printing.
643 for (name, escape) in [("princ", false), ("prin1", true), ("print", true)] {
644 lisp.define(name) { lisp, args in
645 try arity(args, 1...2, name)
646 let text = printed(args[0], escape: escape)
647 lisp.outputs[lisp.outputs.count - 1] += name == "print" ? "\n" + text + "\n" : text
648 return args[0]
649 }
650 }
651 lisp.define("terpri") { lisp, _ in
652 lisp.outputs[lisp.outputs.count - 1] += "\n"
653 return t
654 }
655 lisp.define("message") { lisp, args in
656 guard !args.isEmpty, !isNil(args[0]) else { return .nil }
657 let text = try format(args)
658 lisp.messages += text + "\n"
659 return .string(text)
660 }
661 lisp.define("prin1-to-string") { _, args in
662 try arity(args, 1...2, "prin1-to-string")
663 return .string(printed(args[0], escape: args.count < 2 || isNil(args[1])))
664 }
665
626 // Strings. 666 // Strings.
627 lisp.define("concat") { _, args in .string(try args.map(printedString).joined()) } 667 lisp.define("concat") { _, args in .string(try args.map(printedString).joined()) }
628 lisp.define("format") { _, args in .string(try format(args)) } 668 lisp.define("format") { _, args in .string(try format(args)) }
Sources/OrgstarMobile/EditorScreen.swift +34 −4
@@ -17,6 +17,9 @@ struct EditorScreen: View {
17 @State private var prompt: PromptRequest? 17 @State private var prompt: PromptRequest?
18 @State private var message: String? 18 @State private var message: String?
19 @State private var sheet: Sheet? 19 @State private var sheet: Sheet?
20 @State private var trust = TrustStore()
21 /// The command chosen in the palette, run once the palette is gone.
22 @State private var chosen: String?
20 @Environment(ClockModel.self) private var clock 23 @Environment(ClockModel.self) private var clock
21 @Environment(MobileModel.self) private var model 24 @Environment(MobileModel.self) private var model
22 @Environment(\.openURL) private var openURL 25 @Environment(\.openURL) private var openURL
@@ -86,9 +89,15 @@ struct EditorScreen: View {
86 request.reply(answer) 89 request.reply(answer)
87 } 90 }
88 } 91 }
89 .sheet(item: $sheet) { sheet in 92 .sheet(item: $sheet, onDismiss: {
93 // After the sheet has gone, so a command's own prompt can show.
94 if let id = chosen {
95 chosen = nil
96 editor?.run(id)
97 }
98 }) { sheet in
90 switch sheet { 99 switch sheet {
91 case .palette: if let editor { CommandPalette(editor: editor) } 100 case .palette: if let editor { CommandPalette(editor: editor) { chosen = $0 } }
92 case .conflict: ConflictSheet(session: session) 101 case .conflict: ConflictSheet(session: session)
93 case .syncCopies: SyncConflictSheet(session: session) { message = $0 } 102 case .syncCopies: SyncConflictSheet(session: session) { message = $0 }
94 case .recovery: RecoverySheet(session: session) { message = $0 } 103 case .recovery: RecoverySheet(session: session) { message = $0 }
@@ -115,6 +124,25 @@ struct EditorScreen: View {
115 } 124 }
116 } 125 }
117 126
127 /// Runs a block once it's trusted, asking as `org-confirm-babel-evaluate` does.
128 private func run(_ job: BabelJob) {
129 let go = {
130 message = "Running \(job.language) block…"
131 session.startBabel(job) { message = $0 }
132 }
133 if !job.alwaysAsk, trust.isTrusted(path: path, content: job.blockText) { return go() }
134 let choices = job.alwaysAsk ? ["yes", "no"] : ["yes", "no", "always"]
135 prompt = PromptRequest(prompt: Prompt(key: "confirm", message: "Run this \(job.language) block?", choices: choices)) { answer in
136 switch answer {
137 case "always":
138 trust.trust(path: path, content: job.blockText)
139 go()
140 case "yes": go()
141 default: message = "Not run"
142 }
143 }
144 }
145
118 private func start() { 146 private func start() {
119 if session.path != path { session.open(path) } 147 if session.path != path { session.open(path) }
120 if editor == nil, let document = session.document { 148 if editor == nil, let document = session.document {
@@ -133,6 +161,7 @@ struct EditorScreen: View {
133 } 161 }
134 editor.onEditBlock = { edit in sheet = .block(edit) } 162 editor.onEditBlock = { edit in sheet = .block(edit) }
135 editor.onTangle = { scope in message = session.tangle(scope) } 163 editor.onTangle = { scope in message = session.tangle(scope) }
164 editor.onBabel = { job in run(job) }
136 editor.onAgenda = { start, count in model.agendaRequest = .init(start: start, count: count) } 165 editor.onAgenda = { start, count in model.agendaRequest = .init(start: start, count: count) }
137 editor.onAppCommand = { id in 166 editor.onAppCommand = { id in
138 switch id { 167 switch id {
@@ -362,6 +391,8 @@ struct BlockEditSheet: View {
362/// Every command that applies at the caret, by name. 391/// Every command that applies at the caret, by name.
363struct CommandPalette: View { 392struct CommandPalette: View {
364 let editor: MobileEditor 393 let editor: MobileEditor
394 /// Called with the chosen command before the palette closes.
395 let choose: (String) -> Void
365 @State private var query = "" 396 @State private var query = ""
366 @Environment(\.dismiss) private var dismiss 397 @Environment(\.dismiss) private var dismiss
367 398
@@ -379,9 +410,8 @@ struct CommandPalette: View {
379 NavigationStack { 410 NavigationStack {
380 List(shown, id: \.id) { command in 411 List(shown, id: \.id) { command in
381 Button { 412 Button {
413 choose(command.id)
382 dismiss() 414 dismiss()
383 // After the sheet goes, so a command's own prompt can show.
384 DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { editor.run(command.id) }
385 } label: { 415 } label: {
386 HStack { 416 HStack {
387 Text(command.title).foregroundStyle(.primary) 417 Text(command.title).foregroundStyle(.primary)
Sources/OrgstarMobile/MobileEditor.swift +4
@@ -64,6 +64,8 @@ final class MobileEditor: NSObject, DocumentBuffer {
64 private var applyingEdits = false 64 private var applyingEdits = false
65 /// Source blocks to write to their files (C-c C-v t). 65 /// Source blocks to write to their files (C-c C-v t).
66 var onTangle: ((Tangle.Scope) -> Void)? 66 var onTangle: ((Tangle.Scope) -> Void)?
67 /// A source block to run (C-c C-c).
68 var onBabel: ((BabelJob) -> Void)?
67 /// A block, a table's formulas or a field to edit apart (C-c ', C-c `). 69 /// A block, a table's formulas or a field to edit apart (C-c ', C-c `).
68 var onEditBlock: ((BlockEdit) -> Void)? 70 var onEditBlock: ((BlockEdit) -> Void)?
69 /// A followed timestamp's agenda days: the first (a `Days` number) and how many. 71 /// A followed timestamp's agenda days: the first (a `Days` number) and how many.
@@ -168,6 +170,8 @@ final class MobileEditor: NSObject, DocumentBuffer {
168 } 170 }
169 case .external(.editBlock(let edit)) where onEditBlock != nil: 171 case .external(.editBlock(let edit)) where onEditBlock != nil:
170 onEditBlock?(edit) 172 onEditBlock?(edit)
173 case .external(.babel(let job)) where onBabel != nil:
174 onBabel?(job)
171 case .external(.tangle(let scope)) where onTangle != nil: 175 case .external(.tangle(let scope)) where onTangle != nil:
172 onTangle?(scope) 176 onTangle?(scope)
173 case .external(.agenda(let start, let count)) where onAgenda != nil: 177 case .external(.agenda(let start, let count)) where onAgenda != nil:
Tests/OrgCoreTests/BabelTests.swift +65 −2
@@ -3,6 +3,8 @@ import Testing
3@testable import OrgCore 3@testable import OrgCore
4 4
5/// Runs a Babel job the way the app does, for tests. 5/// Runs a Babel job the way the app does, for tests.
6struct NativeUnsupported: Error {}
7
6enum TestBabelRunner { 8enum TestBabelRunner {
7 static func run(_ job: BabelJob, directory: URL) throws -> BabelOutput { 9 static func run(_ job: BabelJob, directory: URL) throws -> BabelOutput {
8 let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-babel-\(UUID().uuidString)") 10 let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-babel-\(UUID().uuidString)")
@@ -37,7 +39,13 @@ enum TestBabelRunner {
37 } 39 }
38 40
39 /// Plans, runs and finishes the block at `caret`: the new text, or the failure. 41 /// Plans, runs and finishes the block at `caret`: the new text, or the failure.
40 static func execute(_ text: String, caret: Int, directory: URL) throws -> (text: String, failure: String?) { 42 /// `native` runs Emacs Lisp blocks with `Babel.evaluateNatively` instead of Emacs.
43 static func execute(_ text: String, caret: Int, directory: URL, native: Bool = false) throws -> (text: String, failure: String?) {
44 func run(_ job: BabelJob, directory: URL) throws -> BabelOutput {
45 guard native else { return try TestBabelRunner.run(job, directory: directory) }
46 guard let output = try Babel.evaluateNatively(job) else { throw NativeUnsupported() }
47 return output
48 }
41 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [caret..<caret]) 49 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [caret..<caret])
42 switch Babel.plan(context) { 50 switch Babel.plan(context) {
43 case .failure(.message(let message)): return (text, message) 51 case .failure(.message(let message)): return (text, message)
@@ -48,7 +56,13 @@ enum TestBabelRunner {
48 case .success(let next): job = next 56 case .success(let next): job = next
49 } 57 }
50 } 58 }
51 let output = try run(job, directory: directory) 59 let output: BabelOutput
60 do {
61 output = try run(job, directory: directory)
62 } catch let failure as Babel.Failure {
63 guard case .message(let message) = failure else { return (text, nil) }
64 return (text, message)
65 }
52 if let file = Babel.fileResult(job, output: output) { 66 if let file = Babel.fileResult(job, output: output) {
53 try file.contents.write(to: directory.appendingPathComponent(file.path), atomically: true, encoding: .utf8) 67 try file.contents.write(to: directory.appendingPathComponent(file.path), atomically: true, encoding: .utf8)
54 } 68 }
@@ -153,6 +167,55 @@ struct BabelTests {
153 } 167 }
154 } 168 }
155 169
170 static let lispCases = [
171 "#+begin_src emacs-lisp\n(list 1 2.5 \"three\" 'four)\n#+end_src\n",
172 "#+begin_src emacs-lisp\n(list '(a 1) 'hline '(b 2))\n#+end_src\n",
173 "#+begin_src emacs-lisp\n(format \"%05.2f|%s\" 3.14159 'sym)\n#+end_src\n",
174 "#+begin_src emacs-lisp\n\"line one\\nline \\\"two\\\"\"\n#+end_src\n",
175 "#+begin_src emacs-lisp :results verbatim\n\"with \\\"quotes\\\" and \\\\ slash\"\n#+end_src\n",
176 "#+begin_src emacs-lisp\nnil\n#+end_src\n",
177 "#+begin_src emacs-lisp\nt\n#+end_src\n",
178 "#+begin_src emacs-lisp\n[1 2 3]\n#+end_src\n",
179 "#+begin_src emacs-lisp\n(cons 1 2)\n#+end_src\n",
180 "#+begin_src emacs-lisp\n(/ 10 4.0)\n#+end_src\n",
181 "#+begin_src emacs-lisp\n(let ((s 0)) (dotimes (i 5) (setq s (+ s i))) s)\n#+end_src\n",
182 "#+begin_src emacs-lisp\n(mapcar (lambda (x) (* x x)) '(1 2 3))\n#+end_src\n",
183 "#+begin_src emacs-lisp :results output\n(princ \"a\")\n(terpri)\n(prin1 \"b\")\n(print 'c)\n#+end_src\n",
184 "#+begin_src emacs-lisp :results value scalar\n'(1 2)\n#+end_src\n",
185 "#+begin_src emacs-lisp :results list\n'(\"x\" \"y\")\n#+end_src\n",
186 "#+NAME: tbl\n| 1 | 2 |\n| 3 | 4 |\n\n#+begin_src emacs-lisp :var rows=tbl\n(mapcar (lambda (r) (apply #'+ r)) rows)\n#+end_src\n",
187 "#+begin_src emacs-lisp :var n=6 :var s=\"hi\"\n(concat s (number-to-string (* n 7)))\n#+end_src\n",
188 "#+begin_src emacs-lisp\n(car 1)\n#+end_src\n",
189 "#+begin_src emacs-lisp :results raw\n\"*bold*\"\n#+end_src\n",
190 "#+begin_src emacs-lisp :wrap example\n(upcase \"wrapped\")\n#+end_src\n",
191 ]
192
193 @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
194 func lispRunsNativelyLikeEmacs() throws {
195 let folder = FileManager.default.temporaryDirectory
196 let texts = Self.lispCases + Self.cases.filter { ($0.contains("emacs-lisp") || $0.contains("begin_src elisp")) && !$0.contains("begin_src sh") }
197 var emacsCases: [EmacsOracle.Case] = []
198 var ours: [(text: String, failure: String?)] = []
199 for text in texts {
200 let caret = (text as NSString).range(of: "#+begin_src").location + 2
201 do {
202 ours.append(try TestBabelRunner.execute(text, caret: caret, directory: folder, native: true))
203 } catch is NativeUnsupported {
204 Issue.record("not run natively:\n\(text)")
205 ours.append((text, "unsupported"))
206 }
207 emacsCases.append(EmacsOracle.Case(text: text, point: EmacsOracle.point(caret, in: text), form: """
208 (progn
209 (setq org-confirm-babel-evaluate nil)
210 (org-babel-execute-src-block))
211 """))
212 }
213 let emacs = try EmacsOracle.run(emacsCases)
214 for (i, (mine, theirs)) in zip(ours, emacs).enumerated() {
215 #expect(mine.text == theirs.text, "case \(i):\n\(texts[i])\nours (\(mine.failure ?? "")):\n\(mine.text)\nemacs (\(theirs.error)):\n\(theirs.text)")
216 }
217 }
218
156 @Test func asksOnceForEveryReferencedBlock() throws { 219 @Test func asksOnceForEveryReferencedBlock() throws {
157 let text = BabelTests.cases.first { $0.contains(":var a=outer") }! 220 let text = BabelTests.cases.first { $0.contains(":var a=outer") }!
158 guard case .success(let job) = Babel.plan(EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])) else { 221 guard case .success(let job) = Babel.plan(EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])) else {