| @@ -36,6 +36,8 @@ public struct BabelJob: Sendable, Equatable { |
| 36 | let output: Bool |
36 | let output: Bool |
| 37 | let wrap: String? |
37 | let wrap: String? |
| 38 | let name: String? |
38 | let name: String? |
| |
39 | /// An inline block: the result goes after it, in `{{{results(…)}}}`. |
| |
40 | var inline = false |
| 39 | } |
41 | } |
| 40 | } |
42 | } |
| 41 | |
43 | |
| @@ -87,18 +89,87 @@ public enum Babel { |
| 87 | let text = context.text |
89 | let text = context.text |
| 88 | let ns = text as NSString |
90 | let ns = text as NSString |
| 89 | let model = DocumentModel(tree: context.tree) |
91 | let model = DocumentModel(tree: context.tree) |
| 90 | guard let block = model.srcBlocks.first(where: { $0.range.contains(context.caret) || $0.range.upperBound == context.caret }) else { |
| |
| 91 | return .failure(.message("Not in a source block")) |
| |
| 92 | } |
| |
| 93 | guard let language = block.language else { return .failure(.message("This block has no language")) } |
| |
| 94 | let params: Params |
| |
| 95 | do { |
92 | do { |
| 96 | params = try Params(block: block, model: model, text: ns) |
93 | if let block = model.srcBlocks.first(where: { $0.range.contains(context.caret) || $0.range.upperBound == context.caret }) { |
| |
94 | return plan(block, params: try Params(block: block, model: model, text: ns), model: model, text: ns, options: options, |
| |
95 | location: block.range, name: block.name) |
| |
96 | } |
| |
97 | if let call = try callAt(context.caret, model: model, text: ns) { |
| |
98 | return plan(call.block, params: call.params, model: model, text: ns, options: options, location: call.location, name: call.name) |
| |
99 | } |
| |
100 | if let inline = try inlineAt(context.caret, tree: context.tree, model: model, text: ns) { |
| |
101 | return plan(inline.block, params: inline.params, model: model, text: ns, options: options, location: inline.block.range, name: nil, inline: true) |
| |
102 | } |
| 97 | } catch let error as Failure { |
103 | } catch let error as Failure { |
| 98 | return .failure(error) |
104 | return .failure(error) |
| 99 | } catch { |
105 | } catch { |
| 100 | return .failure(.message("\(error)")) |
106 | return .failure(.message("\(error)")) |
| 101 | } |
107 | } |
| |
108 | return .failure(.message("Not in a source block")) |
| |
109 | } |
| |
110 | |
| |
111 | /// The `#+CALL:` line at `offset`: the block it names, with the call's inside header, its |
| |
112 | /// arguments as `:var` and its end header over the block's own (`org-babel-lob-get-info`). |
| |
113 | static func callAt(_ offset: Int, model: DocumentModel, text: NSString) throws -> (block: SrcBlockInfo, params: Params, location: Range<Int>, name: String?)? { |
| |
114 | let lineRange = text.lineRange(for: NSRange(location: min(offset, text.length), length: 0)) |
| |
115 | let line = text.substring(with: lineRange) |
| |
116 | guard let m = line.firstMatch(of: #/(?i)^[ \t]*#\+CALL:[ \t]*([^\s(\[]+)(?:\[([^\]]*)\])?\((.*?)\)[ \t]*(.*?)[ \t]*\n?$/#) else { return nil } |
| |
117 | let name = String(m.1) |
| |
118 | guard let block = model.srcBlocks.first(where: { $0.name == name }) else { throw Failure.message("Reference '\(name)' not found in this buffer") } |
| |
119 | var extra: [[(String, String)]] = [] |
| |
120 | if let inside = m.2 { extra.append(Params.parse(String(inside))) } |
| |
121 | extra.append(splitArguments(String(m.3)).map { (":var", $0) }) |
| |
122 | if !m.4.isEmpty { extra.append(Params.parse(String(m.4))) } |
| |
123 | // The call's own #+NAME names its results. |
| |
124 | var callName: String? |
| |
125 | if lineRange.location > 0 { |
| |
126 | let previous = text.substring(with: text.lineRange(for: NSRange(location: lineRange.location - 1, length: 0))) |
| |
127 | callName = previous.firstMatch(of: #/(?i)^[ \t]*#\+NAME:[ \t]*(\S+)/#).map { String($0.1) } |
| |
128 | } |
| |
129 | let location = lineRange.location..<NSMaxRange(lineRange) |
| |
130 | return (block, try Params(block: block, model: model, text: text, extra: extra), location, callName) |
| |
131 | } |
| |
132 | |
| |
133 | /// Call arguments split on commas outside quotes and parentheses. |
| |
134 | static func splitArguments(_ s: String) -> [String] { |
| |
135 | var parts: [String] = [] |
| |
136 | var current = "" |
| |
137 | var depth = 0 |
| |
138 | var quoted = false |
| |
139 | for c in s { |
| |
140 | if c == "\"" { quoted.toggle() } |
| |
141 | if !quoted, c == "(" { depth += 1 } |
| |
142 | if !quoted, c == ")" { depth -= 1 } |
| |
143 | if c == ",", !quoted, depth == 0 { |
| |
144 | parts.append(current) |
| |
145 | current = "" |
| |
146 | } else { |
| |
147 | current.append(c) |
| |
148 | } |
| |
149 | } |
| |
150 | parts.append(current) |
| |
151 | return parts.map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty } |
| |
152 | } |
| |
153 | |
| |
154 | /// The inline `src_LANG[HEADERS]{BODY}` at `offset`. |
| |
155 | static func inlineAt(_ offset: Int, tree: OrgTree, model: DocumentModel, text: NSString) throws -> (block: SrcBlockInfo, params: Params)? { |
| |
156 | var node = tree.root |
| |
157 | var found: SyntaxNode? |
| |
158 | while let child = node.child(containing: offset) { |
| |
159 | if child.kind == .inlineSourceBlock { found = child } |
| |
160 | node = child |
| |
161 | } |
| |
162 | guard let object = found, let m = object.text.firstMatch(of: /^src_([^\s\[{]+)(?:\[([^\]]*)\])?\{(.*)\}$/) else { return nil } |
| |
163 | let heading = model.headings.lastIndex { $0.headingRange.lowerBound <= object.range.lowerBound } |
| |
164 | let block = SrcBlockInfo(range: object.range, heading: heading, name: nil, language: String(m.1), switches: [], body: String(m.3), headerArguments: [:]) |
| |
165 | var extra: [[(String, String)]] = [[(":exports", "results"), (":hlines", "yes")]] |
| |
166 | if let headers = m.2 { extra.append(Params.parse(String(headers))) } |
| |
167 | return (block, try Params(block: block, model: model, text: text, extra: extra, inline: true)) |
| |
168 | } |
| |
169 | |
| |
170 | static func plan(_ block: SrcBlockInfo, params: Params, model: DocumentModel, text ns: NSString, options: Options, |
| |
171 | location: Range<Int>, name: String?, inline: Bool = false) -> Result<BabelJob, Failure> { |
| |
172 | guard let language = block.language else { return .failure(.message("This block has no language")) } |
| 102 | let eval = params.single[":eval"]?.lowercased() |
173 | let eval = params.single[":eval"]?.lowercased() |
| 103 | if eval == "never" || eval == "no" || eval == "never-export" || eval == "no-export" { |
174 | if eval == "never" || eval == "no" || eval == "never-export" || eval == "no-export" { |
| 104 | return .failure(.message("Evaluation of this \(language) code block\(block.name.map { " (\($0)) " } ?? " ")is disabled.")) |
175 | return .failure(.message("Evaluation of this \(language) code block\(block.name.map { " (\($0)) " } ?? " ")is disabled.")) |
| @@ -107,15 +178,21 @@ public enum Babel { |
| 107 | return .failure(.message("\(key) isn't supported yet; nothing was run.")) |
178 | return .failure(.message("\(key) isn't supported yet; nothing was run.")) |
| 108 | } |
179 | } |
| 109 | if let session = params.single[":session"], session != "none" { return .failure(.message(":session isn't supported yet; nothing was run.")) } |
180 | if let session = params.single[":session"], session != "none" { return .failure(.message(":session isn't supported yet; nothing was run.")) } |
| 110 | if let noweb = params.single[":noweb"], !["no", "strip-export", "tangle"].contains(noweb) { |
| |
| 111 | return .failure(.message(":noweb isn't supported yet; nothing was run.")) |
| |
| 112 | } |
| |
| 113 | if params.single[":cache"] == "yes" { return .failure(.message(":cache isn't supported yet; nothing was run.")) } |
181 | if params.single[":cache"] == "yes" { return .failure(.message(":cache isn't supported yet; nothing was run.")) } |
| 114 | |
182 | |
| 115 | let body = normalizedBody(block) |
183 | var body = normalizedBody(block) |
| |
184 | if nowebEvaluates(params.single[":noweb"]) { |
| |
185 | do { |
| |
186 | body = try expandNoweb(block, params: params, model: model, text: ns) |
| |
187 | } catch let error as Failure { |
| |
188 | return .failure(error) |
| |
189 | } catch { |
| |
190 | return .failure(.message("\(error)")) |
| |
191 | } |
| |
192 | } |
| 116 | let output = params.results.contains("output") |
193 | let output = params.results.contains("output") |
| 117 | let finishBase = { (kind: BabelJob.Finish.Kind) in |
194 | let finishBase = { (kind: BabelJob.Finish.Kind) in |
| 118 | BabelJob.Finish(kind: kind, language: language, params: params.results, output: output, wrap: params.wrap, name: block.name) |
195 | BabelJob.Finish(kind: kind, language: language, params: params.results, output: output, wrap: params.wrap, name: name, inline: inline) |
| 119 | } |
196 | } |
| 120 | let vars: [(String, VarValue)] |
197 | let vars: [(String, VarValue)] |
| 121 | do { |
198 | do { |
| @@ -125,7 +202,7 @@ public enum Babel { |
| 125 | } catch { |
202 | } catch { |
| 126 | return .failure(.message("\(error)")) |
203 | return .failure(.message("\(error)")) |
| 127 | } |
204 | } |
| 128 | let blockText = ns.substring(with: NSRange(block.range)) |
205 | let blockText = ns.substring(with: NSRange(location)) |
| 129 | let dir = params.single[":dir"] |
206 | let dir = params.single[":dir"] |
| 130 | let alwaysAsk = eval == "query" |
207 | let alwaysAsk = eval == "query" |
| 131 | |
208 | |
| @@ -145,7 +222,7 @@ public enum Babel { |
| 145 | return .success(BabelJob( |
222 | return .success(BabelJob( |
| 146 | language: language, runner: .process(executable: executable, arguments: []), |
223 | language: language, runner: .process(executable: executable, arguments: []), |
| 147 | input: full.trimmingCharacters(in: .whitespacesAndNewlines) + "\n", directory: dir, |
224 | input: full.trimmingCharacters(in: .whitespacesAndNewlines) + "\n", directory: dir, |
| 148 | block: block.range, blockText: blockText, alwaysAsk: alwaysAsk, |
225 | block: location, blockText: blockText, alwaysAsk: alwaysAsk, |
| 149 | finish: finishBase(.shell(valueIsExitStatus: valueIsExitStatus)) |
226 | finish: finishBase(.shell(valueIsExitStatus: valueIsExitStatus)) |
| 150 | )) |
227 | )) |
| 151 | } |
228 | } |
| @@ -162,7 +239,7 @@ public enum Babel { |
| 162 | } |
239 | } |
| 163 | return .success(BabelJob( |
240 | return .success(BabelJob( |
| 164 | language: language, runner: .process(executable: params.single[":python"] ?? options.python, arguments: []), |
241 | language: language, runner: .process(executable: params.single[":python"] ?? options.python, arguments: []), |
| 165 | input: input, directory: dir, block: block.range, blockText: blockText, alwaysAsk: alwaysAsk, |
242 | input: input, directory: dir, block: location, blockText: blockText, alwaysAsk: alwaysAsk, |
| 166 | finish: finishBase(.python) |
243 | finish: finishBase(.python) |
| 167 | )) |
244 | )) |
| 168 | } |
245 | } |
| @@ -172,15 +249,15 @@ public enum Babel { |
| 172 | let form = output ? "(with-output-to-string \(expanded))" : "(progn \(expanded)\n)" |
249 | let form = output ? "(with-output-to-string \(expanded))" : "(progn \(expanded)\n)" |
| 173 | return .success(BabelJob( |
250 | return .success(BabelJob( |
| 174 | language: language, runner: .emacs, input: emacsScript(form: form, params: params.results), |
251 | language: language, runner: .emacs, input: emacsScript(form: form, params: params.results), |
| 175 | directory: dir, block: block.range, blockText: blockText, alwaysAsk: alwaysAsk, finish: finishBase(.emacsLisp) |
252 | directory: dir, block: location, blockText: blockText, alwaysAsk: alwaysAsk, finish: finishBase(.emacsLisp) |
| 176 | )) |
253 | )) |
| 177 | } |
254 | } |
| 178 | if let (executable, arguments) = generic[language] { |
255 | if let (executable, arguments) = generic[language] { |
| 179 | guard vars.isEmpty else { return .failure(.message(":var isn't supported for \(language) yet.")) } |
256 | guard vars.isEmpty else { return .failure(.message(":var isn't supported for \(language) yet.")) } |
| 180 | return .success(BabelJob( |
257 | return .success(BabelJob( |
| 181 | language: language, runner: .process(executable: params.single[":cmd"] ?? executable, arguments: arguments), |
258 | language: language, runner: .process(executable: params.single[":cmd"] ?? executable, arguments: arguments), |
| 182 | input: body + "\n", directory: dir, block: block.range, blockText: blockText, alwaysAsk: alwaysAsk, |
259 | input: body + "\n", directory: dir, block: location, blockText: blockText, alwaysAsk: alwaysAsk, |
| 183 | finish: BabelJob.Finish(kind: .generic, language: language, params: params.results + (output ? [] : ["output"]), output: true, wrap: params.wrap, name: block.name) |
260 | finish: BabelJob.Finish(kind: .generic, language: language, params: params.results + (output ? [] : ["output"]), output: true, wrap: params.wrap, name: name, inline: inline) |
| 184 | )) |
261 | )) |
| 185 | } |
262 | } |
| 186 | return .failure(.message("No way to run \(language) blocks yet.")) |
263 | return .failure(.message("No way to run \(language) blocks yet.")) |
| @@ -190,6 +267,78 @@ public enum Babel { |
| 190 | case message(String) |
267 | case message(String) |
| 191 | } |
268 | } |
| 192 | |
269 | |
| |
270 | // MARK: - Noweb |
| |
271 | |
| |
272 | /// `org-babel-noweb-p` in the `:eval` context. |
| |
273 | static func nowebEvaluates(_ value: String?) -> Bool { |
| |
274 | (value ?? "").split(whereSeparator: \.isWhitespace).contains { ["yes", "no-export", "strip-export", "eval", "strip-tangle"].contains(String($0)) } |
| |
275 | } |
| |
276 | |
| |
277 | /// `org-babel-expand-noweb-references` for evaluation: each `<<name>>` becomes the body of |
| |
278 | /// the block named so, or of every block with that `:noweb-ref`, joined by `:noweb-sep`, or |
| |
279 | /// the contents of the heading with that ID; the text before it on its line goes before |
| |
280 | /// each line it brings. |
| |
281 | static func expandNoweb(_ block: SrcBlockInfo, params: Params, model: DocumentModel, text: NSString, depth: Int = 0) throws -> String { |
| |
282 | guard depth < 32 else { throw Failure.message("Noweb references nest too deeply") } |
| |
283 | let body = normalizedBody(block) |
| |
284 | let prefixing = params.single[":noweb-prefix"].map { $0 != "no" && $0 != "nil" } ?? true |
| |
285 | func expand(_ other: SrcBlockInfo) throws -> String { |
| |
286 | let otherParams = try Params(block: other, model: model, text: text) |
| |
287 | return nowebEvaluates(otherParams.single[":noweb"]) |
| |
288 | ? try expandNoweb(other, params: otherParams, model: model, text: text, depth: depth + 1) |
| |
289 | : normalizedBody(other) |
| |
290 | } |
| |
291 | func inCommentedHeading(_ b: SrcBlockInfo) -> Bool { |
| |
292 | guard let h = b.heading else { return false } |
| |
293 | return ([h] + model.ancestors(of: h)).contains { model.headings[$0].title == "COMMENT" || model.headings[$0].title.hasPrefix("COMMENT ") } |
| |
294 | } |
| |
295 | let regex = try! NSRegularExpression(pattern: "(.*?)(<<([^ \\t\\n](?:.*?[^ \\t\\n])?)>>)") |
| |
296 | let ns = body as NSString |
| |
297 | var out = "" |
| |
298 | var at = 0 |
| |
299 | for m in regex.matches(in: body, range: NSRange(location: 0, length: ns.length)) { |
| |
300 | let prefix = ns.substring(with: m.range(at: 1)) |
| |
301 | let id = ns.substring(with: m.range(at: 3)) |
| |
302 | out += ns.substring(with: NSRange(location: at, length: m.range(at: 2).location - at)) |
| |
303 | at = NSMaxRange(m.range) |
| |
304 | var expansion = "" |
| |
305 | if id.range(of: "\\(.*\\)", options: .regularExpression) != nil { |
| |
306 | throw Failure.message("Noweb references that run a block (\(id)) aren't supported yet; nothing was run.") |
| |
307 | } else if let heading = model.headings.firstIndex(where: { h in |
| |
308 | h.properties.contains { ($0.key.uppercased() == "ID" && $0.value == id) || ($0.key.uppercased() == "CUSTOM_ID" && "#" + $0.value == id) } |
| |
309 | }) { |
| |
310 | expansion = headlineBody(heading, model: model, text: text) |
| |
311 | } else if let named = model.srcBlocks.first(where: { $0.name == id }), !inCommentedHeading(named) { |
| |
312 | expansion = try expand(named) |
| |
313 | } else { |
| |
314 | var parts: [String] = [] |
| |
315 | var separators: [String] = [] |
| |
316 | for other in model.srcBlocks where !inCommentedHeading(other) { |
| |
317 | let p = try Params(block: other, model: model, text: text) |
| |
318 | guard p.single[":noweb-ref"] == id else { continue } |
| |
319 | parts.append(try expand(other)) |
| |
320 | separators.append(p.single[":noweb-sep"] ?? "\n") |
| |
321 | } |
| |
322 | for (i, part) in parts.enumerated() { |
| |
323 | if i > 0 { expansion += separators[i - 1] } |
| |
324 | expansion += part |
| |
325 | } |
| |
326 | } |
| |
327 | out += prefixing ? expansion.components(separatedBy: CharacterSet(charactersIn: "\n\r")).joined(separator: "\n" + prefix) : expansion |
| |
328 | } |
| |
329 | return out + ns.substring(from: at) |
| |
330 | } |
| |
331 | |
| |
332 | /// `org-babel-ref-headline-body`: the entry after its planning and drawers. |
| |
333 | static func headlineBody(_ index: Int, model: DocumentModel, text: NSString) -> String { |
| |
334 | let heading = model.headings[index] |
| |
335 | let buffer = EmacsBuffer(text as String, point: heading.headingRange.lowerBound) |
| |
336 | buffer.endOfMetaData() |
| |
337 | let start = buffer.point |
| |
338 | let end = model.headings[(index + 1)...].first.map { $0.headingRange.lowerBound } ?? text.length |
| |
339 | return start < end ? text.substring(with: NSRange(location: start, length: end - start)).trimmingCharacters(in: .whitespacesAndNewlines) : "" |
| |
340 | } |
| |
341 | |
| 193 | /// `org-babel--normalize-body`: without the final newline, and without the indentation |
342 | /// `org-babel--normalize-body`: without the final newline, and without the indentation |
| 194 | /// common to its lines unless `-i` keeps it. |
343 | /// common to its lines unless `-i` keeps it. |
| 195 | static func normalizedBody(_ block: SrcBlockInfo) -> String { |
344 | static func normalizedBody(_ block: SrcBlockInfo) -> String { |
| @@ -220,7 +369,7 @@ public enum Babel { |
| 220 | ["output", "value"], |
369 | ["output", "value"], |
| 221 | ] |
370 | ] |
| 222 | |
371 | |
| 223 | init(block: SrcBlockInfo, model: DocumentModel, text: NSString) throws { |
372 | init(block: SrcBlockInfo, model: DocumentModel, text: NSString, extra: [[(String, String)]] = [], inline: Bool = false) throws { |
| 224 | var layers: [[(String, String)]] = [] |
373 | var layers: [[(String, String)]] = [] |
| 225 | let language = block.language ?? "" |
374 | let language = block.language ?? "" |
| 226 | func properties(_ entries: [Property]) { |
375 | func properties(_ entries: [Property]) { |
| @@ -235,7 +384,7 @@ public enum Babel { |
| 235 | if let heading = block.heading { |
384 | if let heading = block.heading { |
| 236 | for index in model.ancestors(of: heading) + [heading] { properties(model.headings[index].properties) } |
385 | for index in model.ancestors(of: heading) + [heading] { properties(model.headings[index].properties) } |
| 237 | } |
386 | } |
| 238 | let firstLine = text.substring(with: text.lineRange(for: NSRange(location: block.range.lowerBound, length: 0))) |
387 | let firstLine = inline ? "" : text.substring(with: text.lineRange(for: NSRange(location: block.range.lowerBound, length: 0))) |
| 239 | if let r = firstLine.range(of: "(?i)#\\+begin_src[ \\t]+\\S+", options: .regularExpression) { |
388 | if let r = firstLine.range(of: "(?i)#\\+begin_src[ \\t]+\\S+", options: .regularExpression) { |
| 240 | var rest = String(firstLine[r.upperBound...]).trimmingCharacters(in: .whitespacesAndNewlines) |
389 | var rest = String(firstLine[r.upperBound...]).trimmingCharacters(in: .whitespacesAndNewlines) |
| 241 | while let first = rest.split(separator: " ").first, first.hasPrefix("-") || first.hasPrefix("+") { |
390 | while let first = rest.split(separator: " ").first, first.hasPrefix("-") || first.hasPrefix("+") { |
| @@ -246,7 +395,7 @@ public enum Babel { |
| 246 | // `#+HEADER:` lines above the block. |
395 | // `#+HEADER:` lines above the block. |
| 247 | var headers: [String] = [] |
396 | var headers: [String] = [] |
| 248 | var location = block.range.lowerBound |
397 | var location = block.range.lowerBound |
| 249 | while location > 0 { |
398 | while !inline, location > 0 { |
| 250 | let previous = text.lineRange(for: NSRange(location: location - 1, length: 0)) |
399 | let previous = text.lineRange(for: NSRange(location: location - 1, length: 0)) |
| 251 | let line = text.substring(with: previous).trimmingCharacters(in: .whitespacesAndNewlines) |
400 | let line = text.substring(with: previous).trimmingCharacters(in: .whitespacesAndNewlines) |
| 252 | guard line.range(of: "(?i)^#\\+(header|name|caption|attr_[a-z]+|results|plot|label):", options: .regularExpression) != nil else { break } |
401 | guard line.range(of: "(?i)^#\\+(header|name|caption|attr_[a-z]+|results|plot|label):", options: .regularExpression) != nil else { break } |
| @@ -254,6 +403,7 @@ public enum Babel { |
| 254 | location = previous.location |
403 | location = previous.location |
| 255 | } |
404 | } |
| 256 | for header in headers { layers.append(Params.parse(header)) } |
405 | for header in headers { layers.append(Params.parse(header)) } |
| |
406 | layers += extra |
| 257 | |
407 | |
| 258 | for layer in layers { |
408 | for layer in layers { |
| 259 | for (key, value) in layer { |
409 | for (key, value) in layer { |
| @@ -546,6 +696,16 @@ public enum Babel { |
| 546 | let value = Self.value(job.finish, output) |
696 | let value = Self.value(job.finish, output) |
| 547 | if job.finish.params.contains("silent") || job.finish.params.contains("none") || job.finish.params.contains("discard") { return .success([]) } |
697 | if job.finish.params.contains("silent") || job.finish.params.contains("none") || job.finish.params.contains("discard") { return .success([]) } |
| 548 | let buffer = EmacsBuffer(text, point: 0, settings: settings) |
698 | let buffer = EmacsBuffer(text, point: 0, settings: settings) |
| |
699 | if job.finish.inline { |
| |
700 | do { |
| |
701 | try insertInlineResult(value, job.finish, at: blockRange, buffer: buffer) |
| |
702 | } catch let error as Failure { |
| |
703 | return .failure(error) |
| |
704 | } catch { |
| |
705 | return .failure(.message("\(error)")) |
| |
706 | } |
| |
707 | return .success(TextEdit.between(text, buffer.text as String)) |
| |
708 | } |
| 549 | insertResult(value, job.finish, block: blockRange, buffer: buffer) |
709 | insertResult(value, job.finish, block: blockRange, buffer: buffer) |
| 550 | return .success(TextEdit.between(text, buffer.text as String)) |
710 | return .success(TextEdit.between(text, buffer.text as String)) |
| 551 | } |
711 | } |
| @@ -818,6 +978,31 @@ public enum Babel { |
| 818 | } |
978 | } |
| 819 | |
979 | |
| 820 | /// `org-babel-insert-result` for a src block. |
980 | /// `org-babel-insert-result` for a src block. |
| |
981 | /// An inline block's result, after it: ` {{{results(=value=)}}}`, replacing one already |
| |
982 | /// there; raw results go in as they are. |
| |
983 | static func insertInlineResult(_ value: Value, _ finish: BabelJob.Finish, at object: Range<Int>, buffer: EmacsBuffer) throws { |
| |
984 | var string: String |
| |
985 | switch value { |
| |
986 | case .none: string = "" |
| |
987 | case .string(let s): string = s |
| |
988 | case .table(let rows): |
| |
989 | let cells = rows.compactMap { $0 } |
| |
990 | guard cells.count == 1, cells[0].count == 1 else { throw Failure.message("Inline source block results can't be tables or lists") } |
| |
991 | string = cells[0][0] |
| |
992 | } |
| |
993 | buffer.point = object.upperBound |
| |
994 | if let existing = buffer.lookingAt("[ \\t]*\\{\\{\\{results\\(.*?\\)\\}\\}\\}") { |
| |
995 | buffer.deleteRegion(existing[0]!.lowerBound, existing[0]!.upperBound) |
| |
996 | } |
| |
997 | if finish.params.contains("raw") { |
| |
998 | buffer.insert(" " + string) |
| |
999 | return |
| |
1000 | } |
| |
1001 | if string.hasSuffix("\n") { string.removeLast() } |
| |
1002 | guard !string.contains("\n") else { throw Failure.message("Inline source block results must be one line") } |
| |
1003 | buffer.insert(" {{{results(=" + string + "=)}}}") |
| |
1004 | } |
| |
1005 | |
| 821 | static func insertResult(_ value: Value, _ finish: BabelJob.Finish, block: Range<Int>, buffer: EmacsBuffer) { |
1006 | static func insertResult(_ value: Value, _ finish: BabelJob.Finish, block: Range<Int>, buffer: EmacsBuffer) { |
| 822 | let params = finish.params |
1007 | let params = finish.params |
| 823 | let keyword = resultsLine(finish, block: block, buffer: buffer) |
1008 | let keyword = resultsLine(finish, block: block, buffer: buffer) |
| @@ -942,7 +1127,21 @@ public struct ExecuteSrcBlock: OrgCommand { |
| 942 | public init() {} |
1127 | public init() {} |
| 943 | public var id: String { "org.babel.execute" } |
1128 | public var id: String { "org.babel.execute" } |
| 944 | public var title: String { "Run Source Block" } |
1129 | public var title: String { "Run Source Block" } |
| 945 | public func applies(in context: EditContext) -> Bool { KeyContext.holds("src", in: context) } |
1130 | public func applies(in context: EditContext) -> Bool { KeyContext.holds("src", in: context) || Self.atCallOrInline(context) } |
| |
1131 | |
| |
1132 | /// On a `#+CALL:` line or an inline `src_` block. |
| |
1133 | static func atCallOrInline(_ context: EditContext) -> Bool { |
| |
1134 | let ns = context.text as NSString |
| |
1135 | let line = ns.substring(with: ns.lineRange(for: NSRange(location: min(context.caret, ns.length), length: 0))) |
| |
1136 | if line.range(of: "(?i)^[ \\t]*#\\+CALL:", options: .regularExpression) != nil { return true } |
| |
1137 | var node = context.tree.root |
| |
1138 | while let child = node.child(containing: context.caret) { |
| |
1139 | if child.kind == .inlineSourceBlock { return true } |
| |
1140 | node = child |
| |
1141 | } |
| |
1142 | return false |
| |
1143 | } |
| |
1144 | |
| 946 | public func run(in context: EditContext) -> CommandStep { |
1145 | public func run(in context: EditContext) -> CommandStep { |
| 947 | switch Babel.plan(context) { |
1146 | switch Babel.plan(context) { |
| 948 | case .success(let job): return .external(.babel(job)) |
1147 | case .success(let job): return .external(.babel(job)) |