Run only the blocks agreed to when :var runs others !139
3 files changed, +45 −5
Layout: unified · split
Sources/OrgApp/DocumentSession.swift +6
| @@ -716,8 +716,13 @@ public final class DocumentSession { | ||
| 716 | 716 | } |
| 717 | 717 | do { |
| 718 | 718 | // Blocks the `:var`s refer to run first, one at a time. |
| 719 | // Each is one of the blocks agreed to, or the run stops: the text may have changed | |
| 720 | // while a block ran. | |
| 719 | 721 | var job = job |
| 722 | let approved = job.approved | |
| 723 | func agreed(_ block: BabelJob) -> Bool { approved.isEmpty || approved.contains(block.blockText) } | |
| 720 | 724 | while let need = job.needs.first { |
| 725 | guard agreed(need.job) else { return "The blocks changed while running; nothing more was run." } | |
| 721 | 726 | let output = try await run(need.job) |
| 722 | 727 | guard entries.contains(where: { $0 === entry }) else { return "The file was closed before the block finished." } |
| 723 | 728 | let document = entry.buffer.document |
| @@ -726,6 +731,7 @@ public final class DocumentSession { | ||
| 726 | 731 | case .success(let next): job = next |
| 727 | 732 | } |
| 728 | 733 | } |
| 734 | guard agreed(job) else { return "The blocks changed while running; nothing more was run." } | |
| 729 | 735 | let output = try await run(job) |
| 730 | 736 | guard entries.contains(where: { $0 === entry }) else { return "The file was closed before the block finished." } |
| 731 | 737 | if let file = Babel.fileResult(job, output: output) { |
Sources/OrgCore/Compute/Babel.swift +13 −5
| @@ -32,6 +32,9 @@ public struct BabelJob: Sendable, Equatable { | ||
| 32 | 32 | public let finish: Finish |
| 33 | 33 | /// A named block a `:var` refers to: run it first, then `Babel.resume` with its output. |
| 34 | 34 | public var needs: [Need] = [] |
| 35 | /// With `needs`: the text of every block the run may execute, the one asked about and | |
| 36 | /// those its references reach. A later job for a block not among them is refused. | |
| 37 | public internal(set) var approved: [String] = [] | |
| 35 | 38 | /// Values of the references resolved so far, and how to plan again. |
| 36 | 39 | var resolved: [String: Babel.VarValue] = [:] |
| 37 | 40 | var caret = 0 |
| @@ -295,12 +298,17 @@ public enum Babel { | ||
| 295 | 298 | vars = try params.vars.map { ($0.name, try resolve($0.value, model: model, text: ns, refs: refs)) } |
| 296 | 299 | stdin = try params.single[":stdin"].map { try resolve($0, model: model, text: ns, refs: refs) } |
| 297 | 300 | } catch let needed as Needed { |
| 298 | // Asked about once, with every block the references reach. | |
| 299 | let texts = [ns.substring(with: NSRange(location))] + referencedBlocks(params, model: model, text: ns, seen: []).map { ns.substring(with: NSRange($0.range)) } | |
| 300 | return .success(BabelJob( | |
| 301 | // Asked about once, with every block the references reach; asked every time when | |
| 302 | // any of them has `:eval query`. | |
| 303 | let reached = referencedBlocks(params, model: model, text: ns, seen: []) | |
| 304 | let texts = [ns.substring(with: NSRange(location))] + reached.map { ns.substring(with: NSRange($0.range)) } | |
| 305 | let query = eval == "query" || reached.contains { (try? Params(block: $0, model: model, text: ns))?.single[":eval"]?.lowercased() == "query" } | |
| 306 | var job = BabelJob( | |
| 301 | 307 | language: language, runner: .process(executable: "", arguments: []), input: "", directory: params.single[":dir"], |
| 302 | block: location, blockText: texts.joined(separator: "\n"), alwaysAsk: eval == "query", finish: finishBase(.generic), needs: [needed.need] | |
| 303 | )) | |
| 308 | block: location, blockText: texts.joined(separator: "\n"), alwaysAsk: query, finish: finishBase(.generic), needs: [needed.need] | |
| 309 | ) | |
| 310 | job.approved = texts | |
| 311 | return .success(job) | |
| 304 | 312 | } catch let error as Failure { |
| 305 | 313 | return .failure(error) |
| 306 | 314 | } catch { |
Tests/OrgCoreTests/BabelTests.swift +26
| @@ -230,6 +230,32 @@ struct BabelTests { | ||
| 230 | 230 | } |
| 231 | 231 | } |
| 232 | 232 | |
| 233 | @Test func referencedQueryBlocksAskEveryTime() { | |
| 234 | let text = "#+begin_src sh :var x=gen\necho $x\n#+end_src\n\n#+NAME: gen\n#+begin_src sh :eval query\necho 1\n#+end_src\n" | |
| 235 | guard case .success(let job) = Babel.plan(EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])) else { | |
| 236 | Issue.record("not planned") | |
| 237 | return | |
| 238 | } | |
| 239 | #expect(job.alwaysAsk) | |
| 240 | } | |
| 241 | ||
| 242 | @Test func aChangedBufferLeavesTheApprovedBlocks() throws { | |
| 243 | let text = "#+begin_src sh :var x=gen\necho $x\n#+end_src\n\n#+NAME: gen\n#+begin_src sh\necho 1\n#+end_src\n" | |
| 244 | guard case .success(let job) = Babel.plan(EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])), | |
| 245 | let need = job.needs.first else { | |
| 246 | Issue.record("not planned") | |
| 247 | return | |
| 248 | } | |
| 249 | #expect(job.approved.contains(need.job.blockText)) | |
| 250 | // Another block now sits where the approved one was. | |
| 251 | let changed = "#+begin_src sh\nrm -rf nothing\n#+end_src\n" + text | |
| 252 | guard case .success(let next) = Babel.resume(job, need: need, output: BabelOutput(stdout: "1\n"), text: changed, tree: OrgParser.parse(changed)) else { | |
| 253 | Issue.record("not resumed") | |
| 254 | return | |
| 255 | } | |
| 256 | #expect(!job.approved.contains(next.blockText)) | |
| 257 | } | |
| 258 | ||
| 233 | 259 | @Test func asksOnceForEveryReferencedBlock() throws { |
| 234 | 260 | let text = BabelTests.cases.first { $0.contains(":var a=outer") }! |
| 235 | 261 | guard case .success(let job) = Babel.plan(EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])) else { |