Run Emacs Lisp blocks on iOS !135

merged merged by cmc on 2026-10-07 07:29 UTC · krz/orgstar:ios-elisp into main

6 files changed, +207 −15

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 {