| @@ -30,6 +30,18 @@ public struct BabelJob: Sendable, Equatable { |
| 30 | 30 | /// Asks every time (`:eval query`) rather than trusting by content. |
| 31 | 31 | public let alwaysAsk: Bool |
| 32 | 32 | public let finish: Finish |
| 33 | /// A named block a `:var` refers to: run it first, then `Babel.resume` with its output. |
| 34 | public var needs: [Need] = [] |
| 35 | /// Values of the references resolved so far, and how to plan again. |
| 36 | var resolved: [String: Babel.VarValue] = [:] |
| 37 | var caret = 0 |
| 38 | var options = Babel.Options() |
| 39 | |
| 40 | public struct Need: Sendable, Equatable { |
| 41 | /// The `:var` value, `name` or `name(args)`. |
| 42 | public let reference: String |
| 43 | public let job: BabelJob |
| 44 | } |
| 33 | 45 | |
| 34 | 46 | public struct Finish: Sendable, Equatable { |
| 35 | 47 | enum Kind: Sendable, Equatable { case shell(valueIsExitStatus: Bool), python, generic, emacsLisp } |
| @@ -94,19 +106,40 @@ public enum Babel { |
| 94 | 106 | |
| 95 | 107 | /// The run for the src block at the caret, or why it can't run. |
| 96 | 108 | public static func plan(_ context: EditContext, options: Options = Options()) -> Result<BabelJob, Failure> { |
| 109 | plan(context, options: options, resolved: [:]) |
| 110 | } |
| 111 | |
| 112 | /// Plans the block again with the value of the block `job` needed: the output of |
| 113 | /// `need.job`, as `org-babel-ref-resolve` reads it. |
| 114 | public static func resume(_ job: BabelJob, need: BabelJob.Need, output: BabelOutput, text: String, tree: OrgTree) -> Result<BabelJob, Failure> { |
| 115 | var resolved = job.resolved |
| 116 | resolved[need.reference] = varValue(need.job.finish, output) |
| 117 | let context = EditContext(revision: 0, text: text, tree: tree, selection: [job.caret..<job.caret]) |
| 118 | return plan(context, options: job.options, resolved: resolved) |
| 119 | } |
| 120 | |
| 121 | static func plan(_ context: EditContext, options: Options, resolved: [String: VarValue]) -> Result<BabelJob, Failure> { |
| 97 | 122 | let text = context.text |
| 98 | 123 | let ns = text as NSString |
| 99 | 124 | let model = DocumentModel(tree: context.tree) |
| 125 | let refs = References(resolved: resolved, options: options) |
| 126 | func located(_ result: Result<BabelJob, Failure>) -> Result<BabelJob, Failure> { |
| 127 | result.map { job in |
| 128 | var job = job |
| 129 | (job.caret, job.resolved, job.options) = (context.caret, resolved, options) |
| 130 | return job |
| 131 | } |
| 132 | } |
| 100 | 133 | do { |
| 101 | 134 | if let block = model.srcBlocks.first(where: { $0.range.contains(context.caret) || $0.range.upperBound == context.caret }) { |
| 102 | | return plan(block, params: try Params(block: block, model: model, text: ns), model: model, text: ns, options: options, |
| 103 | | location: block.range, name: block.name) |
| 135 | return located(plan(block, params: try Params(block: block, model: model, text: ns), model: model, text: ns, options: options, |
| 136 | location: block.range, name: block.name, refs: refs)) |
| 104 | 137 | } |
| 105 | 138 | if let call = try callAt(context.caret, model: model, text: ns) { |
| 106 | | return plan(call.block, params: call.params, model: model, text: ns, options: options, location: call.location, name: call.name) |
| 139 | return located(plan(call.block, params: call.params, model: model, text: ns, options: options, location: call.location, name: call.name, refs: refs)) |
| 107 | 140 | } |
| 108 | 141 | if let inline = try inlineAt(context.caret, tree: context.tree, model: model, text: ns) { |
| 109 | | return plan(inline.block, params: inline.params, model: model, text: ns, options: options, location: inline.block.range, name: nil, inline: true) |
| 142 | return located(plan(inline.block, params: inline.params, model: model, text: ns, options: options, location: inline.block.range, name: nil, inline: true, refs: refs)) |
| 110 | 143 | } |
| 111 | 144 | } catch let error as Failure { |
| 112 | 145 | return .failure(error) |
| @@ -175,8 +208,22 @@ public enum Babel { |
| 175 | 208 | return (block, try Params(block: block, model: model, text: text, extra: extra, inline: true)) |
| 176 | 209 | } |
| 177 | 210 | |
| 211 | /// What `:var` references resolve to while planning. |
| 212 | struct References { |
| 213 | var resolved: [String: VarValue] |
| 214 | var options: Options |
| 215 | /// Blocks being planned for a reference, to stop cycles. |
| 216 | var visiting: Set<String> = [] |
| 217 | } |
| 218 | |
| 219 | /// A reference to a block that has to run first. |
| 220 | struct Needed: Error { |
| 221 | let need: BabelJob.Need |
| 222 | } |
| 223 | |
| 178 | 224 | static func plan(_ block: SrcBlockInfo, params: Params, model: DocumentModel, text ns: NSString, options: Options, |
| 179 | | location: Range<Int>, name: String?, inline: Bool = false) -> Result<BabelJob, Failure> { |
| 225 | location: Range<Int>, name: String?, inline: Bool = false, refs: References? = nil) -> Result<BabelJob, Failure> { |
| 226 | let refs = refs ?? References(resolved: [:], options: options) |
| 180 | 227 | guard let language = block.language else { return .failure(.message("This block has no language")) } |
| 181 | 228 | let eval = params.single[":eval"]?.lowercased() |
| 182 | 229 | if eval == "never" || eval == "no" || eval == "never-export" || eval == "no-export" { |
| @@ -215,7 +262,14 @@ public enum Babel { |
| 215 | 262 | } |
| 216 | 263 | let vars: [(String, VarValue)] |
| 217 | 264 | do { |
| 218 | | vars = try params.vars.map { ($0.name, try resolve($0.value, model: model, text: ns)) } |
| 265 | vars = try params.vars.map { ($0.name, try resolve($0.value, model: model, text: ns, refs: refs)) } |
| 266 | } catch let needed as Needed { |
| 267 | // Asked about once, with every block the references reach. |
| 268 | let texts = [ns.substring(with: NSRange(location))] + referencedBlocks(params, model: model, text: ns, seen: []).map { ns.substring(with: NSRange($0.range)) } |
| 269 | return .success(BabelJob( |
| 270 | language: language, runner: .process(executable: "", arguments: []), input: "", directory: params.single[":dir"], |
| 271 | block: location, blockText: texts.joined(separator: "\n"), alwaysAsk: eval == "query", finish: finishBase(.generic), needs: [needed.need] |
| 272 | )) |
| 219 | 273 | } catch let error as Failure { |
| 220 | 274 | return .failure(error) |
| 221 | 275 | } catch { |
| @@ -604,14 +658,23 @@ public enum Babel { |
| 604 | 658 | |
| 605 | 659 | // MARK: - Variables |
| 606 | 660 | |
| 607 | | enum VarValue: Equatable { |
| 661 | /// A referenced block's result as a `:var` value. |
| 662 | static func varValue(_ finish: BabelJob.Finish, _ output: BabelOutput) -> VarValue { |
| 663 | switch value(finish, output) { |
| 664 | case .none: return .scalar("") |
| 665 | case .string(let s): return number(s).map(VarValue.number) ?? .scalar(s) |
| 666 | case .table(let rows): return .table(rows.compactMap { $0?.map(cell) }) |
| 667 | } |
| 668 | } |
| 669 | |
| 670 | enum VarValue: Equatable, Sendable { |
| 608 | 671 | case scalar(String) |
| 609 | 672 | case number(String) |
| 610 | 673 | case table([[Cell]]) |
| 611 | 674 | case list([Cell]) |
| 612 | 675 | } |
| 613 | 676 | |
| 614 | | enum Cell: Equatable { |
| 677 | enum Cell: Equatable, Sendable { |
| 615 | 678 | case text(String) |
| 616 | 679 | case number(String) |
| 617 | 680 | } |
| @@ -645,10 +708,46 @@ public enum Babel { |
| 645 | 708 | return number(t).map(Cell.number) ?? .text(t) |
| 646 | 709 | } |
| 647 | 710 | |
| 648 | | /// `org-babel-ref-resolve` for literals and named tables and lists in the file. |
| 649 | | static func resolve(_ value: String, model: DocumentModel, text: NSString) throws -> VarValue { |
| 711 | /// `name` or `name(args)` naming a src block in the file: the block and its call arguments. |
| 712 | static func referencedBlock(_ value: String, model: DocumentModel) -> (block: SrcBlockInfo, arguments: String?)? { |
| 713 | guard let m = value.firstMatch(of: /^([^\s()\[\]"]+)(?:\((.*)\))?$/), |
| 714 | let block = model.srcBlocks.first(where: { $0.name == String(m.1) }) else { return nil } |
| 715 | return (block, m.2.map(String.init)) |
| 716 | } |
| 717 | |
| 718 | /// The blocks `params`' references run, directly or through theirs. |
| 719 | static func referencedBlocks(_ params: Params, model: DocumentModel, text: NSString, seen: Set<String>) -> [SrcBlockInfo] { |
| 720 | var found: [SrcBlockInfo] = [] |
| 721 | var seen = seen |
| 722 | for variable in params.vars { |
| 723 | guard let (block, arguments) = referencedBlock(variable.value, model: model), let name = block.name, seen.insert(name).inserted else { continue } |
| 724 | found.append(block) |
| 725 | let extra = arguments.map { [splitArguments($0).map { (":var", $0) }] } ?? [] |
| 726 | if let inner = try? Params(block: block, model: model, text: text, extra: extra) { |
| 727 | found += referencedBlocks(inner, model: model, text: text, seen: seen) |
| 728 | } |
| 729 | } |
| 730 | return found |
| 731 | } |
| 732 | |
| 733 | /// `org-babel-ref-resolve` for literals, named tables and lists in the file, and named src |
| 734 | /// blocks, which run first (`Needed`) and give their result. |
| 735 | static func resolve(_ value: String, model: DocumentModel, text: NSString, refs: References) throws -> VarValue { |
| 650 | 736 | if value.hasPrefix("\""), value.hasSuffix("\""), value.count >= 2 { return .scalar(Params.unescape(String(value.dropFirst().dropLast()))) } |
| 651 | 737 | if let n = number(value) { return .number(n) } |
| 738 | if let known = refs.resolved[value] { return known } |
| 739 | if let (block, arguments) = referencedBlock(value, model: model), let name = block.name { |
| 740 | guard !refs.visiting.contains(name) else { throw Failure.message("\(name) refers to itself through :var") } |
| 741 | let extra = arguments.map { [splitArguments($0).map { (":var", $0) }] } ?? [] |
| 742 | var inner = refs |
| 743 | inner.visiting.insert(name) |
| 744 | switch plan(block, params: try Params(block: block, model: model, text: text, extra: extra), model: model, text: text, |
| 745 | options: refs.options, location: block.range, name: name, refs: inner) { |
| 746 | case .failure(let failure): throw failure |
| 747 | case .success(let job): |
| 748 | throw Needed(need: job.needs.first ?? BabelJob.Need(reference: value, job: job)) |
| 749 | } |
| 750 | } |
| 652 | 751 | if value.contains("(") || value.contains("[") { throw Failure.message("References like \(value) aren't supported yet.") } |
| 653 | 752 | // A named element: `#+NAME: value` above a table or list. |
| 654 | 753 | let pattern = "(?im)^[ \\t]*#\\+name:[ \\t]*" + NSRegularExpression.escapedPattern(for: value) + "[ \\t]*$" |