| @@ -30,6 +30,18 @@ public struct BabelJob: Sendable, Equatable { |
| 30 | /// Asks every time (`:eval query`) rather than trusting by content. |
30 | /// Asks every time (`:eval query`) rather than trusting by content. |
| 31 | public let alwaysAsk: Bool |
31 | public let alwaysAsk: Bool |
| 32 | public let finish: Finish |
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 | public struct Finish: Sendable, Equatable { |
46 | public struct Finish: Sendable, Equatable { |
| 35 | enum Kind: Sendable, Equatable { case shell(valueIsExitStatus: Bool), python, generic, emacsLisp } |
47 | enum Kind: Sendable, Equatable { case shell(valueIsExitStatus: Bool), python, generic, emacsLisp } |
| @@ -94,19 +106,40 @@ public enum Babel { |
| 94 | |
106 | |
| 95 | /// The run for the src block at the caret, or why it can't run. |
107 | /// The run for the src block at the caret, or why it can't run. |
| 96 | public static func plan(_ context: EditContext, options: Options = Options()) -> Result<BabelJob, Failure> { |
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 | let text = context.text |
122 | let text = context.text |
| 98 | let ns = text as NSString |
123 | let ns = text as NSString |
| 99 | let model = DocumentModel(tree: context.tree) |
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 | do { |
133 | do { |
| 101 | if let block = model.srcBlocks.first(where: { $0.range.contains(context.caret) || $0.range.upperBound == context.caret }) { |
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, |
135 | return located(plan(block, params: try Params(block: block, model: model, text: ns), model: model, text: ns, options: options, |
| 103 | location: block.range, name: block.name) |
136 | location: block.range, name: block.name, refs: refs)) |
| 104 | } |
137 | } |
| 105 | if let call = try callAt(context.caret, model: model, text: ns) { |
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 | if let inline = try inlineAt(context.caret, tree: context.tree, model: model, text: ns) { |
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 | } catch let error as Failure { |
144 | } catch let error as Failure { |
| 112 | return .failure(error) |
145 | return .failure(error) |
| @@ -175,8 +208,22 @@ public enum Babel { |
| 175 | return (block, try Params(block: block, model: model, text: text, extra: extra, inline: true)) |
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 | static func plan(_ block: SrcBlockInfo, params: Params, model: DocumentModel, text ns: NSString, options: Options, |
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 | guard let language = block.language else { return .failure(.message("This block has no language")) } |
227 | guard let language = block.language else { return .failure(.message("This block has no language")) } |
| 181 | let eval = params.single[":eval"]?.lowercased() |
228 | let eval = params.single[":eval"]?.lowercased() |
| 182 | if eval == "never" || eval == "no" || eval == "never-export" || eval == "no-export" { |
229 | if eval == "never" || eval == "no" || eval == "never-export" || eval == "no-export" { |
| @@ -215,7 +262,14 @@ public enum Babel { |
| 215 | } |
262 | } |
| 216 | let vars: [(String, VarValue)] |
263 | let vars: [(String, VarValue)] |
| 217 | do { |
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 | } catch let error as Failure { |
273 | } catch let error as Failure { |
| 220 | return .failure(error) |
274 | return .failure(error) |
| 221 | } catch { |
275 | } catch { |
| @@ -604,14 +658,23 @@ public enum Babel { |
| 604 | |
658 | |
| 605 | // MARK: - Variables |
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 | case scalar(String) |
671 | case scalar(String) |
| 609 | case number(String) |
672 | case number(String) |
| 610 | case table([[Cell]]) |
673 | case table([[Cell]]) |
| 611 | case list([Cell]) |
674 | case list([Cell]) |
| 612 | } |
675 | } |
| 613 | |
676 | |
| 614 | enum Cell: Equatable { |
677 | enum Cell: Equatable, Sendable { |
| 615 | case text(String) |
678 | case text(String) |
| 616 | case number(String) |
679 | case number(String) |
| 617 | } |
680 | } |
| @@ -645,10 +708,46 @@ public enum Babel { |
| 645 | return number(t).map(Cell.number) ?? .text(t) |
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. |
711 | /// `name` or `name(args)` naming a src block in the file: the block and its call arguments. |
| 649 | static func resolve(_ value: String, model: DocumentModel, text: NSString) throws -> VarValue { |
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 | if value.hasPrefix("\""), value.hasSuffix("\""), value.count >= 2 { return .scalar(Params.unescape(String(value.dropFirst().dropLast()))) } |
736 | if value.hasPrefix("\""), value.hasSuffix("\""), value.count >= 2 { return .scalar(Params.unescape(String(value.dropFirst().dropLast()))) } |
| 651 | if let n = number(value) { return .number(n) } |
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 | if value.contains("(") || value.contains("[") { throw Failure.message("References like \(value) aren't supported yet.") } |
751 | if value.contains("(") || value.contains("[") { throw Failure.message("References like \(value) aren't supported yet.") } |
| 653 | // A named element: `#+NAME: value` above a table or list. |
752 | // A named element: `#+NAME: value` above a table or list. |
| 654 | let pattern = "(?im)^[ \\t]*#\\+name:[ \\t]*" + NSRegularExpression.escapedPattern(for: value) + "[ \\t]*$" |
753 | let pattern = "(?im)^[ \\t]*#\\+name:[ \\t]*" + NSRegularExpression.escapedPattern(for: value) + "[ \\t]*$" |