krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/OrgCore/Compute/Babel.swift

b8d091bdcbf2889c220b12b2d0640d50cea17c24
orgstar/Sources/OrgCore/Compute/Babel.swift history · blame · raw

935 lines · 44277 bytes

  1import Foundation
  2
  3// Babel (ob-core.el, ob-shell.el, ob-python.el, ob-emacs-lisp.el, Org 9.8.7): executing a src
  4// block is planned here, run by the platform, and finished here. The plan resolves header
  5// arguments and refuses anything that would make the run differ from Emacs's; the finish turns
  6// the output into a result the way `org-babel-insert-result` writes it.
  7
  8/// A src block run for the platform to carry out.
  9public struct BabelJob: Sendable, Equatable {
 10    public enum Runner: Sendable, Equatable {
 11        /// An interpreter reading the program on standard input.
 12        case process(executable: String, arguments: [String])
 13        /// `emacs --batch` loading `input` as a script.
 14        case emacs
 15    }
 16
 17    public let language: String
 18    public let runner: Runner
 19    /// Standard input, or the Emacs script. `Babel.resultFileToken` stands for a file the runner
 20    /// creates; its contents come back as `BabelOutput.resultFile`.
 21    public let input: String
 22    /// `:dir`, relative to the file's folder; nil for the folder itself.
 23    public let directory: String?
 24    /// The block as it was planned, to find it again when the run ends.
 25    public let block: Range<Int>
 26    public let blockText: String
 27    /// Asks every time (`:eval query`) rather than trusting by content.
 28    public let alwaysAsk: Bool
 29    public let finish: Finish
 30
 31    public struct Finish: Sendable, Equatable {
 32        enum Kind: Sendable, Equatable { case shell(valueIsExitStatus: Bool), python, generic, emacsLisp }
 33        let kind: Kind
 34        var language = ""
 35        public let params: [String]
 36        let output: Bool
 37        let wrap: String?
 38        let name: String?
 39    }
 40}
 41
 42public struct BabelOutput: Sendable, Equatable {
 43    public var stdout: String
 44    public var resultFile: String?
 45    public init(stdout: String, resultFile: String? = nil) {
 46        self.stdout = stdout
 47        self.resultFile = resultFile
 48    }
 49}
 50
 51public enum Babel {
 52    public static let resultFileToken = "__ORGSTAR_RESULT_FILE__"
 53    static let shells: Set<String> = ["sh", "bash", "zsh", "shell", "fish", "ksh", "dash", "ash", "csh", "mksh", "posh"]
 54    static let generic: [String: (String, [String])] = [
 55        "ruby": ("ruby", []), "js": ("node", []), "javascript": ("node", []), "R": ("Rscript", ["-"]),
 56        "awk": ("awk", ["-f", "/dev/stdin"]),
 57    ]
 58
 59    public struct Options: Sendable, Equatable {
 60        public var python = "python3"
 61        /// `shell-file-name`, for blocks in the `shell` language.
 62        public var shell = "/bin/sh"
 63        public init() {}
 64    }
 65
 66    // MARK: - Planning
 67
 68    /// The run for the src block at the caret, or why it can't run.
 69    public static func plan(_ context: EditContext, options: Options = Options()) -> Result<BabelJob, Failure> {
 70        let text = context.text
 71        let ns = text as NSString
 72        let model = DocumentModel(tree: context.tree)
 73        guard let block = model.srcBlocks.first(where: { $0.range.contains(context.caret) || $0.range.upperBound == context.caret }) else {
 74            return .failure(.message("Not in a source block"))
 75        }
 76        guard let language = block.language else { return .failure(.message("This block has no language")) }
 77        let params: Params
 78        do {
 79            params = try Params(block: block, model: model, text: ns)
 80        } catch let error as Failure {
 81            return .failure(error)
 82        } catch {
 83            return .failure(.message("\(error)"))
 84        }
 85        let eval = params.single[":eval"]?.lowercased()
 86        if eval == "never" || eval == "no" || eval == "never-export" || eval == "no-export" {
 87            return .failure(.message("Evaluation of this \(language) code block\(block.name.map { " (\($0)) " } ?? " ")is disabled."))
 88        }
 89        for key in [":prologue", ":epilogue", ":file", ":post", ":stdin", ":cmdline", ":shebang"] where params.single[key] != nil {
 90            return .failure(.message("\(key) isn't supported yet; nothing was run."))
 91        }
 92        if let session = params.single[":session"], session != "none" { return .failure(.message(":session isn't supported yet; nothing was run.")) }
 93        if let noweb = params.single[":noweb"], !["no", "strip-export", "tangle"].contains(noweb) {
 94            return .failure(.message(":noweb isn't supported yet; nothing was run."))
 95        }
 96        if params.single[":cache"] == "yes" { return .failure(.message(":cache isn't supported yet; nothing was run.")) }
 97
 98        let body = normalizedBody(block)
 99        let output = params.results.contains("output")
100        let finishBase = { (kind: BabelJob.Finish.Kind) in
101            BabelJob.Finish(kind: kind, language: language, params: params.results, output: output, wrap: params.wrap, name: block.name)
102        }
103        let vars: [(String, VarValue)]
104        do {
105            vars = try params.vars.map { ($0.name, try resolve($0.value, model: model, text: ns)) }
106        } catch let error as Failure {
107            return .failure(error)
108        } catch {
109            return .failure(.message("\(error)"))
110        }
111        let blockText = ns.substring(with: NSRange(block.range))
112        let dir = params.single[":dir"]
113        let alwaysAsk = eval == "query"
114
115        if shells.contains(language) {
116            let valueIsExitStatus = params.results.contains("value")
117            var lines: [String] = []
118            for (name, value) in vars {
119                let s: String
120                switch value {
121                case .scalar(let v), .number(let v): s = v
122                default: return .failure(.message("Tables and lists in :var aren't supported for \(language) yet."))
123                }
124                lines.append("\(name)='" + s.replacingOccurrences(of: "'", with: "'\"'\"'") + "'")
125            }
126            let full = (lines + [body]).joined(separator: "\n") + (valueIsExitStatus ? "\necho $?" : "")
127            let executable = language == "shell" ? options.shell : language
128            return .success(BabelJob(
129                language: language, runner: .process(executable: executable, arguments: []),
130                input: full.trimmingCharacters(in: .whitespacesAndNewlines) + "\n", directory: dir,
131                block: block.range, blockText: blockText, alwaysAsk: alwaysAsk,
132                finish: finishBase(.shell(valueIsExitStatus: valueIsExitStatus))
133            ))
134        }
135        if language == "python" {
136            let lines = vars.map { "\($0.0)=\(pythonLiteral($0.1))" }
137            var full = (lines + [body]).joined(separator: "\n")
138            if let returnValue = params.single[":return"], !output { full += "\nreturn \(returnValue)" }
139            let input: String
140            if output {
141                input = full + "\n"
142            } else {
143                input = pythonFormatter + "def main():\n" + shiftRight(full) + "\n\n__orgstar_format(main(), '\(resultFileToken)', ["
144                    + params.results.map { "\"\($0)\"" }.joined(separator: ", ") + "])\n"
145            }
146            return .success(BabelJob(
147                language: language, runner: .process(executable: params.single[":python"] ?? options.python, arguments: []),
148                input: input, directory: dir, block: block.range, blockText: blockText, alwaysAsk: alwaysAsk,
149                finish: finishBase(.python)
150            ))
151        }
152        if language == "emacs-lisp" || language == "elisp" {
153            let bindings = vars.map { "(\($0.0) '\(lispLiteral($0.1)))" }
154            let expanded = bindings.isEmpty ? body + "\n" : "(let (\(bindings.joined(separator: "\n      ")))\n\(body)\n)"
155            let form = output ? "(with-output-to-string \(expanded))" : "(progn \(expanded)\n)"
156            return .success(BabelJob(
157                language: language, runner: .emacs, input: emacsScript(form: form, params: params.results),
158                directory: dir, block: block.range, blockText: blockText, alwaysAsk: alwaysAsk, finish: finishBase(.emacsLisp)
159            ))
160        }
161        if let (executable, arguments) = generic[language] {
162            guard vars.isEmpty else { return .failure(.message(":var isn't supported for \(language) yet.")) }
163            return .success(BabelJob(
164                language: language, runner: .process(executable: params.single[":cmd"] ?? executable, arguments: arguments),
165                input: body + "\n", directory: dir, block: block.range, blockText: blockText, alwaysAsk: alwaysAsk,
166                finish: BabelJob.Finish(kind: .generic, language: language, params: params.results + (output ? [] : ["output"]), output: true, wrap: params.wrap, name: block.name)
167            ))
168        }
169        return .failure(.message("No way to run \(language) blocks yet."))
170    }
171
172    public enum Failure: Error, Equatable {
173        case message(String)
174    }
175
176    /// `org-babel--normalize-body`: without the final newline, and without the indentation
177    /// common to its lines unless `-i` keeps it.
178    static func normalizedBody(_ block: SrcBlockInfo) -> String {
179        var lines = block.body.components(separatedBy: "\n")
180        if lines.last == "" { lines.removeLast() }
181        // Org escapes lines starting with `*` or `#+` inside blocks with a comma.
182        lines = lines.map { $0.replacingOccurrences(of: "^([ \\t]*),([,*]|#\\+)", with: "$1$2", options: .regularExpression) }
183        if !block.switches.contains("-i") {
184            let indents = lines.filter { $0.contains(where: { !$0.isWhitespace }) }.map { $0.prefix { $0 == " " || $0 == "\t" }.count }
185            let common = indents.min() ?? 0
186            lines = lines.map { String($0.dropFirst(min(common, $0.prefix { $0 == " " || $0 == "\t" }.count))) }
187        }
188        return lines.joined(separator: "\n")
189    }
190
191    // MARK: - Header arguments
192
193    struct Params {
194        var results: [String] = ["replace"]
195        var single: [String: String] = [:]
196        var vars: [(name: String, value: String)] = []
197        var wrap: String?
198
199        static let resultGroups: [Set<String>] = [
200            ["file", "list", "vector", "table", "scalar", "verbatim"],
201            ["raw", "html", "latex", "org", "code", "pp", "drawer", "link", "graphics"],
202            ["replace", "silent", "none", "discard", "append", "prepend"],
203            ["output", "value"],
204        ]
205
206        init(block: SrcBlockInfo, model: DocumentModel, text: NSString) throws {
207            var layers: [[(String, String)]] = []
208            let language = block.language ?? ""
209            func properties(_ entries: [Property]) {
210                for key in ["header-args", "header-args:\(language)"] {
211                    for entry in entries where entry.key.lowercased() == key {
212                        let pairs = Params.parse(entry.value)
213                        layers.append(pairs)
214                    }
215                }
216            }
217            properties(model.fileProperties)
218            if let heading = block.heading {
219                for index in model.ancestors(of: heading) + [heading] { properties(model.headings[index].properties) }
220            }
221            let firstLine = text.substring(with: text.lineRange(for: NSRange(location: block.range.lowerBound, length: 0)))
222            if let r = firstLine.range(of: "(?i)#\\+begin_src[ \\t]+\\S+", options: .regularExpression) {
223                var rest = String(firstLine[r.upperBound...]).trimmingCharacters(in: .whitespacesAndNewlines)
224                while let first = rest.split(separator: " ").first, first.hasPrefix("-") || first.hasPrefix("+") {
225                    rest = String(rest.dropFirst(first.count)).trimmingCharacters(in: .whitespaces)
226                }
227                layers.append(Params.parse(rest))
228            }
229            // `#+HEADER:` lines above the block.
230            var headers: [String] = []
231            var location = block.range.lowerBound
232            while location > 0 {
233                let previous = text.lineRange(for: NSRange(location: location - 1, length: 0))
234                let line = text.substring(with: previous).trimmingCharacters(in: .whitespacesAndNewlines)
235                guard line.range(of: "(?i)^#\\+(header|name|caption|attr_[a-z]+|results|plot|label):", options: .regularExpression) != nil else { break }
236                if line.lowercased().hasPrefix("#+header:") { headers.insert(String(line.dropFirst("#+header:".count)), at: 0) }
237                location = previous.location
238            }
239            for header in headers { layers.append(Params.parse(header)) }
240
241            for layer in layers {
242                for (key, value) in layer {
243                    switch key {
244                    case ":results":
245                        for word in value.split(whereSeparator: \.isWhitespace).map(String.init) {
246                            if let group = Params.resultGroups.first(where: { $0.contains(word) }) { results.removeAll { group.contains($0) } }
247                            results.removeAll { $0 == word }
248                            results.insert(word, at: 0)
249                        }
250                    case ":var":
251                        guard let eq = value.firstIndex(of: "=") else { throw Failure.message(":var \(value) needs a name and a value") }
252                        let name = value[..<eq].trimmingCharacters(in: .whitespaces)
253                        let assigned = value[value.index(after: eq)...].trimmingCharacters(in: .whitespaces)
254                        vars.removeAll { $0.name == name }
255                        vars.append((name, assigned))
256                    case ":wrap":
257                        wrap = value
258                    default:
259                        single[key] = value
260                    }
261                }
262            }
263        }
264
265        /// `org-babel-parse-header-arguments`: split before each `:key` outside quotes and
266        /// parentheses.
267        static func parse(_ text: String) -> [(String, String)] {
268            var parts: [String] = []
269            var current = ""
270            var depth = 0
271            var quoted = false
272            var previous: Character = " "
273            for c in text {
274                if c == "\"" { quoted.toggle() }
275                if !quoted {
276                    if c == "(" { depth += 1 }
277                    if c == ")" { depth = max(0, depth - 1) }
278                }
279                if c == ":", !quoted, depth == 0, previous == " " || previous == "\t" {
280                    parts.append(current)
281                    current = ""
282                }
283                current.append(c)
284                previous = c
285            }
286            parts.append(current)
287            var pairs: [(String, String)] = []
288            for part in parts {
289                let trimmed = part.trimmingCharacters(in: .whitespaces)
290                guard trimmed.hasPrefix(":") else { continue }
291                let key = String(trimmed.prefix { !$0.isWhitespace }).lowercased()
292                var value = trimmed.dropFirst(key.count).trimmingCharacters(in: .whitespaces)
293                if key != ":var", value.count >= 2, value.hasPrefix("\""), value.hasSuffix("\"") {
294                    value = String(value.dropFirst().dropLast())
295                }
296                pairs.append((key, value))
297            }
298            return pairs
299        }
300    }
301
302    // MARK: - Variables
303
304    enum VarValue: Equatable {
305        case scalar(String)
306        case number(String)
307        case table([[Cell]])
308        case list([Cell])
309    }
310
311    enum Cell: Equatable {
312        case text(String)
313        case number(String)
314    }
315
316    /// `org-babel--string-to-number`, keeping Emacs's printed form.
317    static func number(_ s: String) -> String? {
318        let t = s.trimmingCharacters(in: .whitespaces)
319        guard !t.isEmpty, !t.contains(where: \.isWhitespace), t.range(of: "^[0-9e.+ -]+$", options: .regularExpression) != nil else { return nil }
320        if t.range(of: "^[-+]?[0-9]+\\.?$", options: .regularExpression) != nil {
321            var digits = t.hasSuffix(".") ? String(t.dropLast()) : t
322            let negative = digits.hasPrefix("-")
323            if digits.hasPrefix("-") || digits.hasPrefix("+") { digits.removeFirst() }
324            digits = String(digits.drop { $0 == "0" })
325            if digits.isEmpty { return "0" }
326            return (negative ? "-" : "") + digits
327        }
328        guard t.range(of: "^[-+]?([0-9]+\\.[0-9]+|[0-9]*\\.[0-9]+|[0-9]+(\\.[0-9]*)?e[-+]?[0-9]+|[0-9]*\\.[0-9]+e[-+]?[0-9]+)$", options: .regularExpression) != nil,
329              let d = Double(t) else { return nil }
330        return emacsFloat(d)
331    }
332
333    /// How Emacs prints a float.
334    static func emacsFloat(_ d: Double) -> String {
335        if d == d.rounded(), abs(d) < 1e16 { return String(format: "%.1f", d) }
336        return "\(d)"
337    }
338
339    static func cell(_ s: String) -> Cell {
340        var t = s
341        if let m = s.firstMatch(of: /^\s*"(.+)"\s*$/) { t = String(m.1) }
342        return number(t).map(Cell.number) ?? .text(t)
343    }
344
345    /// `org-babel-ref-resolve` for literals and named tables and lists in the file.
346    static func resolve(_ value: String, model: DocumentModel, text: NSString) throws -> VarValue {
347        if value.hasPrefix("\""), value.hasSuffix("\""), value.count >= 2 { return .scalar(String(value.dropFirst().dropLast())) }
348        if let n = number(value) { return .number(n) }
349        if value.contains("(") || value.contains("[") { throw Failure.message("References like \(value) aren't supported yet.") }
350        // A named element: `#+NAME: value` above a table or list.
351        let pattern = "(?im)^[ \\t]*#\\+name:[ \\t]*" + NSRegularExpression.escapedPattern(for: value) + "[ \\t]*$"
352        guard let r = (text as String).range(of: pattern, options: .regularExpression) else {
353            throw Failure.message("Can't find \(value) for :var.")
354        }
355        let start = NSMaxRange(text.lineRange(for: NSRange(r, in: text as String)))
356        var location = start
357        var rows: [[Cell]] = []
358        var items: [Cell] = []
359        while location < text.length {
360            let lineRange = text.lineRange(for: NSRange(location: location, length: 0))
361            let line = text.substring(with: lineRange).trimmingCharacters(in: .newlines)
362            let trimmed = line.trimmingCharacters(in: .whitespaces)
363            if trimmed.hasPrefix("|") {
364                if trimmed.hasPrefix("|-") { throw Failure.message("Tables with rules in :var aren't supported yet.") }
365                let cells = trimmed.dropFirst().split(separator: "|", omittingEmptySubsequences: false).map { $0.trimmingCharacters(in: .whitespaces) }
366                rows.append((trimmed.hasSuffix("|") ? Array(cells.dropLast()) : cells).map(cell))
367            } else if let m = trimmed.firstMatch(of: /^[-+*] (.*)$/), rows.isEmpty {
368                items.append(cell(String(m.1)))
369            } else if trimmed.hasPrefix("#+") || (!rows.isEmpty || !items.isEmpty) {
370                if rows.isEmpty, items.isEmpty, trimmed.lowercased().hasPrefix("#+begin_src") {
371                    throw Failure.message("Using a src block's result in :var isn't supported yet.")
372                }
373                break
374            } else {
375                break
376            }
377            location = NSMaxRange(lineRange)
378        }
379        if !rows.isEmpty { return .table(rows) }
380        if !items.isEmpty { return .list(items) }
381        throw Failure.message("\(value) isn't a table or list.")
382    }
383
384    /// `org-babel-python-var-to-python`.
385    static func pythonLiteral(_ value: VarValue) -> String {
386        func cell(_ c: Cell) -> String {
387            switch c {
388            case .number(let n): return n
389            case .text(let s): return lispString(s, triple: s.contains("\n") || s.contains("\r"))
390            }
391        }
392        switch value {
393        case .scalar(let s): return lispString(s, triple: s.contains("\n") || s.contains("\r"))
394        case .number(let n): return n
395        case .list(let items): return "[" + items.map(cell).joined(separator: ", ") + "]"
396        case .table(let rows): return "[" + rows.map { "[" + $0.map(cell).joined(separator: ", ") + "]" }.joined(separator: ", ") + "]"
397        }
398    }
399
400    /// `prin1` of a string.
401    static func lispString(_ s: String, triple: Bool = false) -> String {
402        let quoted = "\"" + s.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") + "\""
403        return triple ? "\"\"" + quoted + "\"\"" : quoted
404    }
405
406    static func lispLiteral(_ value: VarValue) -> String {
407        func cell(_ c: Cell) -> String {
408            switch c {
409            case .number(let n): return n
410            case .text(let s): return lispString(s)
411            }
412        }
413        switch value {
414        case .scalar(let s): return lispString(s)
415        case .number(let n): return n
416        case .list(let items): return "(" + items.map(cell).joined(separator: " ") + ")"
417        case .table(let rows): return "(" + rows.map { "(" + $0.map(cell).joined(separator: " ") + ")" }.joined(separator: " ") + ")"
418        }
419    }
420
421    // MARK: - Python
422
423    /// Writes the value of `main()` to the result file as org's Python wrapper does: `pprint`
424    /// for `pp`, `key :: value` items for a dict as a list, dicts as rows for tables,
425    /// otherwise `str`.
426    static let pythonFormatter = """
427    def __orgstar_format(value, path, params):
428        def rows(v):
429            if isinstance(v, dict):
430                return [(k, rows(x)) for k, x in v.items()]
431            if isinstance(v, (list, tuple)):
432                return [rows(x) for x in v]
433            return v
434        with open(path, 'w') as out:
435            if 'pp' in params:
436                import pprint
437                out.write(pprint.pformat(value))
438            elif 'list' in params and isinstance(value, dict):
439                out.write(str(['{} :: {}'.format(k, v) for k, v in value.items()]))
440            else:
441                if not set(params) & {'scalar', 'verbatim', 'raw'} and 'table' in params:
442                    value = rows(value)
443                out.write(str(value))
444
445    """
446
447    /// `org-babel-python--shift-right`: four spaces more on every line outside strings.
448    static func shiftRight(_ body: String) -> String {
449        var inString: String?
450        var lines: [String] = []
451        for line in body.components(separatedBy: "\n") {
452            lines.append(inString == nil ? "    " + line : line)
453            var i = line.startIndex
454            while i < line.endIndex {
455                if let delimiter = inString {
456                    if line[i...].hasPrefix(delimiter) {
457                        inString = nil
458                        i = line.index(i, offsetBy: 3)
459                        continue
460                    }
461                } else if line[i] == "#" {
462                    break
463                } else if line[i...].hasPrefix("\"\"\"") || line[i...].hasPrefix("'''") {
464                    inString = String(line[i...].prefix(3))
465                    i = line.index(i, offsetBy: 3)
466                    continue
467                } else if line[i] == "\"" || line[i] == "'" {
468                    let quote = line[i]
469                    i = line.index(after: i)
470                    while i < line.endIndex, line[i] != quote {
471                        if line[i] == "\\" { i = line.index(after: i) }
472                        if i < line.endIndex { i = line.index(after: i) }
473                    }
474                }
475                if i < line.endIndex { i = line.index(after: i) }
476            }
477        }
478        return lines.joined(separator: "\n")
479    }
480
481    // MARK: - Emacs Lisp
482
483    /// Evaluates `form` and prints the result as JSON: its `%s` and `%S` forms, and its rows
484    /// when it is a table.
485    static func emacsScript(form: String, params: [String]) -> String {
486        """
487        ;;; -*- lexical-binding: t -*-
488        (require 'cl-lib)
489        (require 'json)
490        (let* ((result \(form))
491               (print-level nil)
492               (print-length nil)
493               (cell (lambda (c) (if (stringp c) c (format "%s" c))))
494               (tabulable (and (proper-list-p result)
495                               (cl-every (lambda (e) (or (atom e) (proper-list-p e))) result)))
496               (rows (and tabulable result
497                          (if (cl-every (lambda (e) (or (eq e 'hline) (listp e))) result)
498                              (mapcar (lambda (r) (if (eq r 'hline) "hline" (vconcat (mapcar cell r)))) result)
499                            (list (vconcat (mapcar cell result)))))))
500          (princ (json-encode `((s . ,(format "%s" result))
501                                (S . ,(format "%S" result))
502                                (string . ,(if (stringp result) t :json-false))
503                                (rows . ,(if rows (vconcat rows) :json-null))))))
504        """
505    }
506
507    // MARK: - Finishing
508
509    /// What the run produced, as Babel holds it before inserting.
510    enum Value: Equatable {
511        case none
512        case string(String)
513        /// Rows of cells; nil rows are rules.
514        case table([[String]?])
515    }
516
517    /// The edit that writes the run's result under the block, or why it can't.
518    public static func finish(_ job: BabelJob, output: BabelOutput, text: String, settings: OrgSettings = .default) -> Result<[TextEdit], Failure> {
519        let ns = text as NSString
520        var blockRange = job.block
521        if blockRange.upperBound > ns.length || ns.substring(with: NSRange(blockRange)) != job.blockText {
522            let found = ns.range(of: job.blockText)
523            guard found.location != NSNotFound,
524                  ns.range(of: job.blockText, range: NSRange(location: NSMaxRange(found), length: ns.length - NSMaxRange(found))).location == NSNotFound else {
525                return .failure(.message("The block changed while it ran; the result wasn't written."))
526            }
527            blockRange = found.location..<NSMaxRange(found)
528        }
529        let value = Self.value(job.finish, output)
530        if job.finish.params.contains("silent") || job.finish.params.contains("none") || job.finish.params.contains("discard") { return .success([]) }
531        let buffer = EmacsBuffer(text, point: 0, settings: settings)
532        insertResult(value, job.finish, block: blockRange, buffer: buffer)
533        return .success(TextEdit.between(text, buffer.text as String))
534    }
535
536    /// The language's result handling: `org-babel-result-cond` and the table import.
537    static func value(_ finish: BabelJob.Finish, _ output: BabelOutput) -> Value {
538        let params = finish.params
539        let scalar = params.contains("scalar") || params.contains("verbatim") || params.contains("html") || params.contains("code")
540            || params.contains("pp") || params.contains("file")
541            || ((params.contains("output") || params.contains("raw") || params.contains("org")) && !params.contains("table"))
542        switch finish.kind {
543        case .shell(let valueIsExitStatus):
544            var results = output.stdout
545            if valueIsExitStatus {
546                results = results.split(separator: "\n", omittingEmptySubsequences: true).last.map(String.init) ?? ""
547            }
548            if scalar { return .string(results) }
549            return importTable(results)
550        case .generic:
551            return .string(output.stdout)
552        case .python:
553            let raw = finish.output ? output.stdout : (output.resultFile ?? "")
554            if scalar { return .string(raw) }
555            return pythonValue(raw)
556        case .emacsLisp:
557            guard let data = output.stdout.data(using: .utf8),
558                  let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return .string(output.stdout) }
559            if scalar {
560                return .string((params.contains("scalar") || params.contains("verbatim") ? object["S"] : object["s"]) as? String ?? "")
561            }
562            if object["string"] as? Bool == true { return .string(object["s"] as? String ?? "") }
563            if let rows = object["rows"] as? [Any] {
564                return .table(rows.map { $0 as? String == "hline" ? nil : ($0 as? [String] ?? []) })
565            }
566            let printed = object["S"] as? String ?? ""
567            return printed == "nil" ? .none : .string(printed)
568        }
569    }
570
571    /// `org-babel-import-elisp-from-file`.
572    static func importTable(_ results: String) -> Value {
573        guard !results.isEmpty else { return .none }
574        var lines = results.components(separatedBy: "\n")
575        if lines.last == "" { lines.removeLast() }
576        let nonEmpty = lines.filter { !$0.isEmpty }
577        let rows: [[String]]
578        if !nonEmpty.isEmpty, nonEmpty.allSatisfy({ $0.contains("\t") }) {
579            rows = lines.map { $0.components(separatedBy: "\t") }
580        } else if !nonEmpty.isEmpty, nonEmpty.allSatisfy({ $0.contains(",") }) {
581            rows = lines.map(csvFields)
582        } else if lines.count == 1 {
583            rows = [[lines[0]]]
584        } else {
585            rows = lines.map { line in
586                let trimmed = line.drop { $0 == " " }
587                return trimmed.split(separator: " ", omittingEmptySubsequences: true).map(String.init).isEmpty
588                    ? [""] : trimmed.split(separator: " ", omittingEmptySubsequences: true).map(String.init)
589            }
590        }
591        let cells = rows.map { row in row.map { field -> String in
592            let trimmed = field.trimmingCharacters(in: .whitespaces)
593            switch cell(trimmed) {
594            case .number(let n): return n
595            case .text(let t): return t
596            }
597        } }
598        if cells.count == 1, cells[0].count == 1 { return .string(cells[0][0]) }
599        return .table(cells)
600    }
601
602    static func csvFields(_ line: String) -> [String] {
603        var fields: [String] = []
604        var current = ""
605        var quoted = false
606        for c in line {
607            if c == "\"" {
608                quoted.toggle()
609            } else if c == ",", !quoted {
610                fields.append(current.trimmingCharacters(in: .whitespaces))
611                current = ""
612            } else {
613                current.append(c)
614            }
615        }
616        fields.append(current.trimmingCharacters(in: .whitespaces))
617        return fields
618    }
619
620    /// `org-babel-python-table-or-string`.
621    static func pythonValue(_ raw: String) -> Value {
622        guard let first = raw.first, let last = raw.last, raw.count >= 2,
623              (first == "[" && last == "]") || (first == "(" && last == ")") else {
624            if raw.isEmpty { return .string("") }
625            if raw.count > 2, (raw.hasPrefix("'") && raw.hasSuffix("'")) || (raw.hasPrefix("\"") && raw.hasSuffix("\"")) {
626                return .string(String(raw.dropFirst().dropLast()))
627            }
628            if let n = number(raw) { return .string(n) }
629            return .string(raw)
630        }
631        var parser = PythonLiteral(Array(raw))
632        guard let parsed = parser.parse(), parser.atEnd, case .list(let items) = parsed else { return .string(raw) }
633        func text(_ v: PythonLiteral.Node) -> String {
634            switch v {
635            case .atom(let s): return s
636            case .string(let s): return s
637            case .list(let xs): return "(" + xs.map(text).joined(separator: " ") + ")"
638            }
639        }
640        if items.allSatisfy({ if case .list = $0 { return true }; if case .atom("None") = $0 { return true }; return false }) {
641            return .table(items.map { item -> [String]? in
642                if case .list(let cells) = item { return cells.map(text) }
643                return nil
644            })
645        }
646        return .table([items.map { if case .atom("None") = $0 { return "hline" }; return text($0) }])
647    }
648
649    struct PythonLiteral {
650        indirect enum Node: Equatable {
651            case atom(String)
652            case string(String)
653            case list([Node])
654        }
655
656        let chars: [Character]
657        var i = 0
658        init(_ chars: [Character]) { self.chars = chars }
659        var atEnd: Bool { mutating get { skip(); return i == chars.count } }
660
661        mutating func skip() { while i < chars.count, chars[i] == " " || chars[i] == "\n" { i += 1 } }
662
663        mutating func parse() -> Node? {
664            skip()
665            guard i < chars.count else { return nil }
666            let c = chars[i]
667            if c == "[" || c == "(" {
668                let close: Character = c == "[" ? "]" : ")"
669                i += 1
670                var items: [Node] = []
671                skip()
672                if i < chars.count, chars[i] == close {
673                    i += 1
674                    return .list(items)
675                }
676                while true {
677                    guard let item = parse() else { return nil }
678                    items.append(item)
679                    skip()
680                    guard i < chars.count else { return nil }
681                    if chars[i] == "," {
682                        i += 1
683                        skip()
684                        if i < chars.count, chars[i] == close {
685                            i += 1
686                            return .list(items)
687                        }
688                        continue
689                    }
690                    if chars[i] == close {
691                        i += 1
692                        return .list(items)
693                    }
694                    return nil
695                }
696            }
697            if c == "'" || c == "\"" {
698                i += 1
699                var s = ""
700                while i < chars.count, chars[i] != c {
701                    if chars[i] == "\\", i + 1 < chars.count {
702                        i += 1
703                        switch chars[i] {
704                        case "n": s.append("\n")
705                        case "t": s.append("\t")
706                        default: s.append(chars[i])
707                        }
708                    } else {
709                        s.append(chars[i])
710                    }
711                    i += 1
712                }
713                guard i < chars.count else { return nil }
714                i += 1
715                return .string(s)
716            }
717            var s = ""
718            while i < chars.count, !",)] \n".contains(chars[i]) {
719                s.append(chars[i])
720                i += 1
721            }
722            guard !s.isEmpty else { return nil }
723            return .atom(number(s) ?? s)
724        }
725    }
726
727    // MARK: - Inserting
728
729    static let resultKeyword = "(?i)^[ \\t]*#\\+results(\\[[^\\]]*\\])?:"
730
731    /// `org-babel-where-is-src-block-result` with `insert`: the line of the block's results
732    /// keyword, added when there is none.
733    static func resultsLine(_ finish: BabelJob.Finish, block: Range<Int>, buffer: EmacsBuffer) -> Int {
734        let ns = buffer.text
735        if let name = finish.name {
736            let pattern = "(?im)^[ \\t]*#\\+results(\\[[^\\]]*\\])?:[ \\t]*" + NSRegularExpression.escapedPattern(for: name) + "[ \\t]*$"
737            let regex = try! NSRegularExpression(pattern: pattern)
738            for m in regex.matches(in: ns as String, range: NSRange(location: 0, length: ns.length)) {
739                // Not the name of another block.
740                return m.range.location
741            }
742        } else {
743            // The element after the block, past blank lines.
744            var location = block.upperBound
745            while location < ns.length {
746                let line = ns.lineRange(for: NSRange(location: location, length: 0))
747                let content = ns.substring(with: line)
748                if content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
749                    location = NSMaxRange(line)
750                    continue
751                }
752                if content.range(of: "(?i)^[ \\t]*#\\+results(\\[[^\\]]*\\])?:[ \\t]*\\n?$", options: .regularExpression) != nil {
753                    return line.location
754                }
755                break
756            }
757        }
758        // A new keyword after the block.
759        buffer.point = min(block.upperBound, buffer.length)
760        buffer.skipBackward([32, 9, 10, 13])
761        buffer.forwardLine(1)
762        if !buffer.bolp { buffer.insert("\n") }
763        let indent = buffer.saveExcursion { () -> String in
764            buffer.point = block.lowerBound
765            let line = buffer.substring(buffer.lineStart(buffer.point), buffer.lineEnd(buffer.point))
766            return String(line.prefix { $0 == " " || $0 == "\t" })
767        }
768        buffer.insert("\n")
769        let keywordLine = buffer.point
770        buffer.insert(indent + "#+RESULTS:" + (finish.name.map { " " + $0 } ?? "") + "\n")
771        if buffer.looking("^[ \\t]*$", caseInsensitive: false, setsMatch: false) == nil {
772            buffer.saveExcursion { buffer.insert("\n") }
773        }
774        return keywordLine
775    }
776
777    /// `org-babel-result-end` with point at the start of the result.
778    static func resultEnd(_ buffer: EmacsBuffer) -> Int {
779        let start = buffer.point
780        let line = buffer.substring(buffer.lineStart(start), buffer.lineEnd(start))
781        if line.trimmingCharacters(in: .whitespaces).isEmpty { return start }
782        if line.range(of: "^[ \\t]*\\[\\[[^\\]]+\\](\\[[^\\]]*\\])?\\][ \\t]*$", options: .regularExpression) != nil {
783            return min(buffer.length, buffer.lineEnd(start) + 1)
784        }
785        let tree = OrgParser.parse(buffer.text as String)
786        var node = tree.root
787        var found: SyntaxNode?
788        while let child = node.child(containing: start) {
789            if child.range.lowerBound == start || (buffer.lineStart(child.range.lowerBound) == start) {
790                if [.fixedWidth, .table, .plainList, .drawer, .block, .dynamicBlock, .latexFragment].contains(child.kind) {
791                    found = child
792                    break
793                }
794            }
795            node = child
796        }
797        guard let element = found else { return start }
798        var end = element.range.upperBound
799        while end > start, let c = buffer.character(at: end - 1), c == 32 || c == 9 || c == 10 || c == 13 { end -= 1 }
800        return min(buffer.length, buffer.lineEnd(end) + 1)
801    }
802
803    /// `org-babel-insert-result` for a src block.
804    static func insertResult(_ value: Value, _ finish: BabelJob.Finish, block: Range<Int>, buffer: EmacsBuffer) {
805        let params = finish.params
806        let keyword = resultsLine(finish, block: block, buffer: buffer)
807        buffer.point = keyword
808        let indent = buffer.substring(buffer.lineStart(keyword), buffer.lineEnd(keyword)).prefix { $0 == " " || $0 == "\t" }.count
809        buffer.forwardLine(1)
810        if buffer.point == buffer.lineEnd(keyword) { buffer.insert("\n") }
811        var beg = buffer.point
812        if params.contains("replace") {
813            buffer.deleteRegion(beg, resultEnd(buffer))
814            buffer.point = beg
815        } else if params.contains("append") {
816            buffer.point = resultEnd(buffer)
817            beg = buffer.point
818        }
819        var tabulated = false
820        switch value {
821        case .none:
822            break
823        case .string(var s) where params.contains("list"):
824            if s.last != "\n" { s += "\n" }
825            let items = s.split(separator: "\n", omittingEmptySubsequences: true)
826            buffer.insert(items.map { "- " + $0 }.joined(separator: "\n") + "\n")
827        case .table(let rows) where params.contains("list"):
828            let items = rows.count == 1 ? (rows[0] ?? []) : rows.compactMap { $0?.joined(separator: " ") }
829            buffer.insert(items.map { "- " + $0 }.joined(separator: "\n") + "\n")
830        case .table(let rows):
831            tabulated = true
832            let lines = rows.map { row in row.map { "| " + $0.map { " " + $0 + " " }.joined(separator: "|").dropFirst() + " |" } ?? "|-" }
833            let start = buffer.point
834            buffer.insert(lines.joined(separator: "\n") + "\n")
835            buffer.point = start
836            buffer.tableAlign()
837            buffer.point = buffer.tableEnd()
838        case .string(var s):
839            if !s.isEmpty, s.last != "\n" { s += "\n" }
840            buffer.insert(s)
841        }
842        let end = buffer.marker(buffer.point, advances: true)
843        defer { buffer.release(end) }
844        func wrap(_ open: String, _ close: String, escape: Bool) {
845            buffer.point = end.position
846            buffer.insert(close + "\n")
847            buffer.point = beg
848            buffer.insert(open + "\n")
849            if escape { escapeCode(buffer, from: buffer.point, to: end.position - close.utf16.count - 1) }
850        }
851        if let wrapValue = finish.wrap {
852            let full = wrapValue.isEmpty ? "results" : wrapValue
853            let type = String(full.split(separator: " ").first ?? "results")
854            if type.lowercased() != "nil", type.lowercased() != "no" {
855                wrap("#+begin_" + full, "#+end_" + type, escape: !["example", "src", "export"].contains(type.lowercased()) ? false : true)
856            }
857        } else if params.contains("html") {
858            wrap("#+begin_export html", "#+end_export", escape: true)
859        } else if params.contains("latex") {
860            wrap("#+begin_export latex", "#+end_export", escape: true)
861        } else if params.contains("org") {
862            wrap("#+begin_src org", "#+end_src", escape: true)
863        } else if params.contains("code") {
864            wrap("#+begin_src \(finish.language.isEmpty ? "none" : finish.language)", "#+end_src", escape: true)
865        } else if params.contains("raw") {
866            // As inserted.
867        } else if params.contains("drawer") {
868            wrap(":results:", ":end:", escape: false)
869        } else if !tabulated, !params.contains("list") {
870            examplify(buffer, beg, end.position)
871        }
872        if indent > 0, !(tabulated && params.contains("append")) {
873            buffer.point = beg
874            while buffer.point < end.position {
875                if !buffer.eolp { buffer.insert(String(repeating: " ", count: indent)) }
876                if buffer.forwardLine(1) != 0 { break }
877            }
878        }
879    }
880
881    /// `org-escape-code-in-region`.
882    static func escapeCode(_ buffer: EmacsBuffer, from start: Int, to end: Int) {
883        buffer.point = start
884        let stop = buffer.marker(end)
885        defer { buffer.release(stop) }
886        while buffer.point < stop.position {
887            let line = buffer.substring(buffer.point, buffer.lineEnd(buffer.point))
888            if let m = line.firstMatch(of: /^[ \t]*(,*(\*|#\+))/) {
889                buffer.point += line[..<m.1.startIndex].utf16.count
890                buffer.insert(",")
891            }
892            if buffer.forwardLine(1) != 0 { break }
893        }
894    }
895
896    /// `org-babel-examplify-region`: fewer than ten lines get `: `, more an example block.
897    static func examplify(_ buffer: EmacsBuffer, _ beg: Int, _ end: Int) {
898        let text = buffer.substring(beg, end)
899        var size = text.components(separatedBy: "\n").count - (text.hasSuffix("\n") ? 1 : 0)
900        if text.isEmpty { size = 0 }
901        guard size > 0 else { return }
902        if size < 10 {
903            buffer.point = beg
904            for _ in 0..<size {
905                buffer.point = buffer.lineStart(buffer.point)
906                buffer.insert(": ")
907                buffer.forwardLine(1)
908            }
909            return
910        }
911        let endMarker = buffer.marker(end)
912        defer { buffer.release(endMarker) }
913        buffer.point = beg
914        buffer.insert("#+begin_example\n")
915        escapeCode(buffer, from: buffer.point, to: endMarker.position)
916        buffer.point = endMarker.position
917        if !buffer.bolp { buffer.insert("\n") }
918        buffer.insert("#+end_example")
919        if !buffer.eolp { buffer.insert("\n") }
920    }
921}
922
923/// C-c C-c in a src block: `org-babel-execute-src-block`.
924public struct ExecuteSrcBlock: OrgCommand {
925    public init() {}
926    public var id: String { "org.babel.execute" }
927    public var title: String { "Run Source Block" }
928    public func applies(in context: EditContext) -> Bool { KeyContext.holds("src", in: context) }
929    public func run(in context: EditContext) -> CommandStep {
930        switch Babel.plan(context) {
931        case .success(let job): return .external(.babel(job))
932        case .failure(.message(let message)): return .failed(message)
933        }
934    }
935}