Files
ob-elixir/ob-elixir.el

330 lines
9.9 KiB
EmacsLisp

;;; ob-elixir.el --- Org Babel functions for Elixir -*- lexical-binding: t; -*-
;; Copyright (C) 2024 Your Name
;; Author: Your Name <your.email@example.com>
;; URL: https://github.com/username/ob-elixir
;; Keywords: literate programming, reproducible research, elixir
;; Version: 0.1.0
;; Package-Requires: ((emacs "27.1") (org "9.4"))
;; This file is not part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;; Commentary:
;; Org Babel support for evaluating Elixir code blocks.
;;
;; Features:
;; - Execute Elixir code in org-mode source blocks
;; - Support for :results value and :results output
;; - Variable passing with :var header argument
;; - Mix project context support
;;
;; Usage:
;; Add (elixir . t) to `org-babel-load-languages':
;;
;; (org-babel-do-load-languages
;; 'org-babel-load-languages
;; '((elixir . t)))
;;; Code:
(require 'ob)
(require 'ob-eval)
(require 'cl-lib)
;;; Customization
(defgroup ob-elixir nil
"Org Babel support for Elixir."
:group 'org-babel
:prefix "ob-elixir-")
(defcustom ob-elixir-command "elixir"
"Command to execute Elixir code.
Can be a full path or command name if in PATH."
:type 'string
:group 'ob-elixir
:safe #'stringp)
(defcustom ob-elixir-signal-errors t
"Whether to signal Emacs errors on Elixir execution failure.
When non-nil, Elixir errors will be signaled as Emacs errors.
When nil, errors are returned as the result string."
:type 'boolean
:group 'ob-elixir)
(defcustom ob-elixir-show-warnings t
"Whether to include warnings in output.
When non-nil, Elixir warnings are included in the result.
When nil, warnings are stripped from the output."
:type 'boolean
:group 'ob-elixir)
;;; Header Arguments
(defvar org-babel-default-header-args:elixir
'((:results . "value")
(:session . "none"))
"Default header arguments for Elixir code blocks.")
;;; Language Registration
;; File extension for tangling
(add-to-list 'org-babel-tangle-lang-exts '("elixir" . "ex"))
;; Associate with elixir-mode for syntax highlighting (if available)
(with-eval-after-load 'org-src
(add-to-list 'org-src-lang-modes '("elixir" . elixir)))
;;; Error Handling
(defconst ob-elixir--error-regexp
"^\\*\\* (\\([A-Za-z.]+Error\\))\\(.*\\)"
"Regexp matching Elixir runtime errors.
Group 1 is the error type, group 2 is the message.")
(defconst ob-elixir--compile-error-regexp
"^\\*\\* (\\(CompileError\\|TokenMissingError\\|SyntaxError\\))\\(.*\\)"
"Regexp matching Elixir compile-time errors.")
(defconst ob-elixir--warning-regexp
"^warning: \\(.*\\)"
"Regexp matching Elixir warnings.")
(define-error 'ob-elixir-error
"Elixir evaluation error")
(define-error 'ob-elixir-compile-error
"Elixir compilation error"
'ob-elixir-error)
(define-error 'ob-elixir-runtime-error
"Elixir runtime error"
'ob-elixir-error)
(defun ob-elixir--detect-error (output)
"Check OUTPUT for Elixir errors.
Returns a plist with :type, :message, and :line if an error is found.
Returns nil if no error detected."
(cond
;; Compile-time error
((string-match ob-elixir--compile-error-regexp output)
(list :type 'compile
:error-type (match-string 1 output)
:message (string-trim (match-string 2 output))
:full-output output))
;; Runtime error
((string-match ob-elixir--error-regexp output)
(list :type 'runtime
:error-type (match-string 1 output)
:message (string-trim (match-string 2 output))
:full-output output))
;; No error
(t nil)))
(defun ob-elixir--format-error (error-info)
"Format ERROR-INFO into a user-friendly message."
(let ((type (plist-get error-info :type))
(error-type (plist-get error-info :error-type))
(message (plist-get error-info :message)))
(format "Elixir %s: (%s) %s"
(if (eq type 'compile) "Compile Error" "Runtime Error")
error-type
message)))
(defun ob-elixir--strip-warnings (output)
"Remove warning lines from OUTPUT if configured."
(if ob-elixir-show-warnings
output
(let ((lines (split-string output "\n")))
(mapconcat #'identity
(cl-remove-if (lambda (line)
(string-match-p ob-elixir--warning-regexp line))
lines)
"\n"))))
(defun ob-elixir--process-result (result)
"Process RESULT from Elixir execution.
Checks for errors and handles them according to `ob-elixir-signal-errors'.
Returns the cleaned result string."
(let ((trimmed (string-trim result))
(error-info (ob-elixir--detect-error result)))
(if error-info
(if ob-elixir-signal-errors
(signal (if (eq (plist-get error-info :type) 'compile)
'ob-elixir-compile-error
'ob-elixir-runtime-error)
(list (ob-elixir--format-error error-info)))
;; Return error as result
(plist-get error-info :full-output))
;; No error, return trimmed result
trimmed)))
;;; Type Conversion
(defun ob-elixir--escape-string (str)
"Escape special characters in STR for Elixir string literal."
(let ((result str))
;; Escape backslashes first
(setq result (replace-regexp-in-string "\\\\" "\\\\\\\\" result))
;; Escape double quotes
(setq result (replace-regexp-in-string "\"" "\\\\\"" result))
;; Escape newlines
(setq result (replace-regexp-in-string "\n" "\\\\n" result))
;; Escape tabs
(setq result (replace-regexp-in-string "\t" "\\\\t" result))
result))
(defun ob-elixir--elisp-to-elixir (value)
"Convert Elisp VALUE to Elixir literal syntax.
Handles:
- nil -> nil
- t -> true
- numbers -> numbers
- strings -> quoted strings
- symbols -> atoms
- lists -> Elixir lists
- vectors -> tuples"
(cond
;; nil
((null value) "nil")
;; Boolean true
((eq value t) "true")
;; Numbers
((numberp value)
(number-to-string value))
;; Strings
((stringp value)
(format "\"%s\"" (ob-elixir--escape-string value)))
;; Symbols become atoms (except special ones)
((symbolp value)
(let ((name (symbol-name value)))
(cond
((string= name "hline") ":hline")
((string-match-p "^[a-z_][a-zA-Z0-9_]*[?!]?$" name)
(format ":%s" name))
(t (format ":\"%s\"" name)))))
;; Vectors become tuples
((vectorp value)
(format "{%s}"
(mapconcat #'ob-elixir--elisp-to-elixir
(append value nil) ", ")))
;; Lists
((listp value)
(format "[%s]"
(mapconcat #'ob-elixir--elisp-to-elixir value ", ")))
;; Fallback
(t (format "%S" value))))
;;; Variable Handling
(defun ob-elixir--var-name (name)
"Convert NAME to a valid Elixir variable name.
Elixir variables must start with lowercase or underscore."
(let ((str (if (symbolp name) (symbol-name name) name)))
;; Ensure starts with lowercase or underscore
(if (string-match-p "^[a-z_]" str)
str
(concat "_" str))))
(defun org-babel-variable-assignments:elixir (params)
"Return list of Elixir statements assigning variables from PARAMS.
Each statement has the form: var_name = value"
(mapcar
(lambda (pair)
(let ((name (car pair))
(value (cdr pair)))
(format "%s = %s"
(ob-elixir--var-name name)
(ob-elixir--elisp-to-elixir value))))
(org-babel--get-vars params)))
;;; Execution
(defconst ob-elixir--value-wrapper
"result = (
%s
)
IO.puts(inspect(result, limit: :infinity, printable_limit: :infinity, charlists: :as_lists))
"
"Wrapper template for capturing Elixir expression value.
%s is replaced with the user's code.")
(defun ob-elixir--wrap-for-value (body)
"Wrap BODY to capture its return value.
The wrapper evaluates BODY, then prints the result using
`inspect/2` with infinite limits to avoid truncation."
(format ob-elixir--value-wrapper body))
(defun ob-elixir--execute (body result-type)
"Execute BODY as Elixir code.
RESULT-TYPE is either `value' or `output'.
For `value', wraps code to capture return value.
For `output', captures stdout directly.
Returns the result as a string.
May signal `ob-elixir-error' if execution fails and
`ob-elixir-signal-errors' is non-nil."
(let* ((tmp-file (org-babel-temp-file "ob-elixir-" ".exs"))
(code (if (eq result-type 'value)
(ob-elixir--wrap-for-value body)
body)))
(with-temp-file tmp-file
(insert code))
(let ((result (with-temp-buffer
(call-process ob-elixir-command nil t nil
(org-babel-process-file-name tmp-file))
;; Capture both stdout and stderr
(buffer-string))))
(ob-elixir--process-result result))))
(defun org-babel-execute:elixir (body params)
"Execute a block of Elixir code with org-babel.
BODY is the Elixir code to execute.
PARAMS is an alist of header arguments.
This function is called by `org-babel-execute-src-block'."
(let* ((result-type (cdr (assq :result-type params)))
(result-params (cdr (assq :result-params params)))
;; Expand body with variable assignments
(full-body (org-babel-expand-body:generic
body params
(org-babel-variable-assignments:elixir params)))
(result (ob-elixir--execute full-body result-type)))
(org-babel-reassemble-table
(org-babel-result-cond result-params
result
(org-babel-script-escape result))
(org-babel-pick-name (cdr (assq :colname-names params))
(cdr (assq :colnames params)))
(org-babel-pick-name (cdr (assq :rowname-names params))
(cdr (assq :rownames params))))))
(provide 'ob-elixir)
;;; ob-elixir.el ends here