docs and tasks

This commit is contained in:
2026-01-24 13:44:20 +01:00
commit 3ca9c10678
16 changed files with 6704 additions and 0 deletions

View File

@@ -0,0 +1,445 @@
# Emacs Lisp Best Practices for Package Development
This document covers best practices for writing Emacs Lisp packages, specifically targeting org-babel language implementations.
## Table of Contents
- [File Structure and Headers](#file-structure-and-headers)
- [Naming Conventions](#naming-conventions)
- [Variables and Customization](#variables-and-customization)
- [Function Definitions](#function-definitions)
- [Error Handling](#error-handling)
- [Documentation](#documentation)
- [Dependencies and Loading](#dependencies-and-loading)
- [References](#references)
---
## File Structure and Headers
Every Emacs Lisp package file should follow a standard structure:
```elisp
;;; ob-elixir.el --- Org Babel functions for Elixir evaluation -*- 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: 1.0.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.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This package provides Org Babel support for evaluating Elixir code blocks.
;;
;; Features:
;; - Execute Elixir code in org-mode source blocks
;; - Support for Mix project context
;; - Session support via IEx
;; - Variable passing between code blocks
;;
;; 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-ref)
(require 'ob-comint)
(require 'ob-eval)
;; ... your code here ...
(provide 'ob-elixir)
;;; ob-elixir.el ends here
```
### Key Header Elements
| Element | Purpose | Example |
|----------------------|--------------------------------------------|-----------------------|
| `lexical-binding: t` | Enable lexical scoping (always use this) | First line comment |
| `Package-Requires` | Declare dependencies with minimum versions | `((emacs "27.1"))` |
| `Keywords` | Help with package discovery | Standard keyword list |
| `Version` | Semantic versioning | `1.0.0` |
---
## Naming Conventions
### Package Prefix
All symbols (functions, variables, constants) must be prefixed with the package name to avoid namespace collisions:
```elisp
;; GOOD - properly prefixed
(defvar ob-elixir-command "elixir")
(defun ob-elixir-evaluate (body params) ...)
(defconst ob-elixir-eoe-indicator "---ob-elixir-eoe---")
;; BAD - no prefix, will collide
(defvar elixir-command "elixir")
(defun evaluate-elixir (body) ...)
```
### Naming Patterns for org-babel
Org-babel uses specific naming conventions that must be followed:
```elisp
;; Required function - note the colon syntax
(defun org-babel-execute:elixir (body params)
"Execute a block of Elixir code with Babel.")
;; Optional session function
(defun org-babel-elixir-initiate-session (&optional session params)
"Create or return an Elixir session buffer.")
;; Variable assignments function
(defun org-babel-variable-assignments:elixir (params)
"Return Elixir code to assign variables.")
;; Default header arguments variable
(defvar org-babel-default-header-args:elixir '()
"Default header arguments for Elixir code blocks.")
;; Prep session function
(defun org-babel-prep-session:elixir (session params)
"Prepare SESSION with PARAMS.")
```
### Private Functions
Use double hyphens for internal/private functions:
```elisp
;; Public API
(defun ob-elixir-evaluate (body params)
"Evaluate BODY with PARAMS.")
;; Internal helper - note the double hyphen
(defun ob-elixir--format-value (value)
"Internal: Format VALUE for Elixir.")
(defun ob-elixir--clean-output (output)
"Internal: Remove ANSI codes from OUTPUT.")
```
---
## Variables and Customization
### User-Customizable Variables
Use `defcustom` for variables users should configure:
```elisp
(defgroup ob-elixir nil
"Org Babel support for Elixir."
:group 'org-babel
:prefix "ob-elixir-")
(defcustom ob-elixir-command "elixir"
"Command to invoke Elixir.
Can be a full path or just the command name if in PATH."
:type 'string
:group 'ob-elixir)
(defcustom ob-elixir-iex-command "iex"
"Command to invoke IEx for session mode."
:type 'string
:group 'ob-elixir)
(defcustom ob-elixir-mix-command "mix"
"Command to invoke Mix."
:type 'string
:group 'ob-elixir)
(defcustom ob-elixir-default-session-timeout 30
"Default timeout in seconds for session operations."
:type 'integer
:group 'ob-elixir)
;; For options with specific choices
(defcustom ob-elixir-output-format 'value
"Default output format for results."
:type '(choice (const :tag "Return value" value)
(const :tag "Standard output" output))
:group 'ob-elixir)
```
### Internal Variables
Use `defvar` for internal state:
```elisp
(defvar ob-elixir--session-buffer nil
"Current active session buffer.
This is internal state and should not be modified directly.")
(defvar ob-elixir--pending-output ""
"Accumulated output waiting to be processed.")
```
### Constants
Use `defconst` for values that never change:
```elisp
(defconst ob-elixir-eoe-indicator "__ob_elixir_eoe__"
"String used to indicate end of evaluation output.")
(defconst ob-elixir-error-regexp "\\*\\* (\\w+Error)"
"Regexp to match Elixir error messages.")
```
---
## Function Definitions
### Function Documentation
Always include comprehensive docstrings:
```elisp
(defun ob-elixir-evaluate (body params)
"Evaluate BODY as Elixir code according to PARAMS.
BODY is the source code string to execute.
PARAMS is an alist of header arguments.
Supported header arguments:
:session - Name of IEx session to use, or \"none\"
:results - How to handle results (value, output)
:var - Variables to inject into the code
Returns the result as a string, or nil if execution failed.
Example:
(ob-elixir-evaluate \"1 + 1\" \\='((:results . \"value\")))"
...)
```
### Interactive Commands
Mark user-facing commands as interactive:
```elisp
(defun ob-elixir-check-installation ()
"Check if Elixir is properly installed and accessible.
Display version information in the minibuffer."
(interactive)
(let ((version (shell-command-to-string
(format "%s --version" ob-elixir-command))))
(message "Elixir: %s" (string-trim version))))
```
### Using `cl-lib` Properly
Prefer `cl-lib` functions over deprecated `cl` package:
```elisp
(require 'cl-lib)
;; Use cl- prefixed versions
(cl-defun ob-elixir-parse-result (output &key (format 'value))
"Parse OUTPUT according to FORMAT."
...)
(cl-loop for pair in params
when (eq (car pair) :var)
collect (cdr pair))
(cl-destructuring-bind (name . value) variable
...)
```
---
## Error Handling
### Signaling Errors
Use appropriate error functions:
```elisp
;; For user errors (incorrect usage)
(user-error "No Elixir session found for %s" session-name)
;; For programming errors
(error "Invalid parameter: %S" param)
;; Custom error types
(define-error 'ob-elixir-error "Ob-elixir error")
(define-error 'ob-elixir-execution-error "Elixir execution error" 'ob-elixir-error)
(define-error 'ob-elixir-session-error "Elixir session error" 'ob-elixir-error)
;; Signaling custom errors
(signal 'ob-elixir-execution-error (list "Compilation failed" output))
```
### Handling Errors Gracefully
```elisp
(defun ob-elixir-safe-evaluate (body params)
"Safely evaluate BODY, handling errors gracefully."
(condition-case err
(ob-elixir-evaluate body params)
(ob-elixir-execution-error
(message "Elixir error: %s" (cadr err))
nil)
(file-error
(user-error "Cannot access Elixir command: %s" (error-message-string err)))
(error
(message "Unexpected error: %s" (error-message-string err))
nil)))
```
### Input Validation
```elisp
(defun ob-elixir-evaluate (body params)
"Evaluate BODY with PARAMS."
;; Validate inputs early
(unless (stringp body)
(error "BODY must be a string, got %S" (type-of body)))
(unless (listp params)
(error "PARAMS must be an alist"))
(when (string-empty-p body)
(user-error "Cannot evaluate empty code block"))
...)
```
---
## Documentation
### Commentary Section
Write helpful commentary:
```elisp
;;; Commentary:
;; ob-elixir provides Org Babel support for the Elixir programming language.
;;
;; Installation:
;;
;; 1. Ensure Elixir is installed and in your PATH
;; 2. Add to your init.el:
;;
;; (require 'ob-elixir)
;; (org-babel-do-load-languages
;; 'org-babel-load-languages
;; '((elixir . t)))
;;
;; Usage:
;;
;; In an org file, create a source block:
;;
;; #+BEGIN_SRC elixir
;; IO.puts("Hello from Elixir!")
;; 1 + 1
;; #+END_SRC
;;
;; Press C-c C-c to evaluate.
;;
;; Header Arguments:
;;
;; - :session NAME Use a persistent IEx session
;; - :results value Return the last expression's value
;; - :results output Capture stdout
;; - :mix PROJECT Execute in Mix project context
;;
;; See the README for complete documentation.
```
### Info Manual Integration (Optional)
For larger packages, consider an info manual:
```elisp
(defun ob-elixir-info ()
"Open the ob-elixir info manual."
(interactive)
(info "(ob-elixir)"))
```
---
## Dependencies and Loading
### Requiring Dependencies
```elisp
;;; Code:
;; Required dependencies
(require 'ob)
(require 'ob-ref)
(require 'ob-comint)
(require 'ob-eval)
;; Soft dependencies (optional features)
(require 'elixir-mode nil t) ; t = noerror
;; Check for optional dependency
(defvar ob-elixir-has-elixir-mode (featurep 'elixir-mode)
"Non-nil if elixir-mode is available.")
```
### Autoloads
For functions that should be available before the package loads:
```elisp
;;;###autoload
(defun org-babel-execute:elixir (body params)
"Execute a block of Elixir code with Babel."
...)
;;;###autoload
(eval-after-load 'org
'(add-to-list 'org-src-lang-modes '("elixir" . elixir)))
```
### Provide Statement
Always end with provide:
```elisp
(provide 'ob-elixir)
;;; ob-elixir.el ends here
```
---
## References
- [GNU Emacs Lisp Reference Manual](https://www.gnu.org/software/emacs/manual/elisp.html)
- [Emacs Lisp Style Guide](https://github.com/bbatsov/emacs-lisp-style-guide)
- [Packaging Guidelines (MELPA)](https://github.com/melpa/melpa/blob/master/CONTRIBUTING.org)
- [Writing GNU Emacs Extensions](https://www.gnu.org/software/emacs/manual/html_node/eintr/)
- [Org Mode Source Code](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/)
- [Elisp Conventions (Emacs Wiki)](https://www.emacswiki.org/emacs/ElispConventions)

View File

@@ -0,0 +1,626 @@
# Testing Emacs Lisp Packages
This document covers testing strategies and tools for Emacs Lisp packages, with focus on org-babel language implementations.
## Table of Contents
- [Testing Frameworks Overview](#testing-frameworks-overview)
- [ERT (Emacs Lisp Regression Testing)](#ert-emacs-lisp-regression-testing)
- [Buttercup](#buttercup)
- [Test Organization](#test-organization)
- [Testing org-babel Specifics](#testing-org-babel-specifics)
- [Mocking and Stubbing](#mocking-and-stubbing)
- [CI/CD Integration](#cicd-integration)
- [Build Tools](#build-tools)
- [References](#references)
---
## Testing Frameworks Overview
| Framework | Style | Best For | Included |
|---------------|------------------|-------------------------|------------------------|
| **ERT** | xUnit | Unit tests, built-in | Yes, in Emacs |
| **Buttercup** | BDD/RSpec | Readable specs, mocking | No, install from MELPA |
| **Ecukes** | Cucumber/Gherkin | Integration/acceptance | No, install from MELPA |
For a package like ob-elixir, **ERT** is recommended as it:
- Comes built into Emacs
- Is well-documented
- Works well with org-mode's own test patterns
- Integrates easily with CI
---
## ERT (Emacs Lisp Regression Testing)
### Basic Test Structure
```elisp
;;; test-ob-elixir.el --- Tests for ob-elixir -*- lexical-binding: t; -*-
(require 'ert)
(require 'ob-elixir)
;; Simple test
(ert-deftest ob-elixir-test-basic-evaluation ()
"Test basic Elixir code evaluation."
(should (equal "2" (ob-elixir-evaluate "1 + 1" '()))))
;; Test with setup
(ert-deftest ob-elixir-test-variable-injection ()
"Test that variables are properly injected."
(let ((params '((:var . ("x" . 5)))))
(should (equal "10"
(ob-elixir-evaluate "x * 2" params)))))
;; Test for expected errors
(ert-deftest ob-elixir-test-syntax-error ()
"Test that syntax errors are handled."
(should-error (ob-elixir-evaluate "def foo(" '())
:type 'ob-elixir-execution-error))
;; Test with negation
(ert-deftest ob-elixir-test-not-nil ()
"Test result is not nil."
(should-not (null (ob-elixir-evaluate ":ok" '()))))
```
### ERT Assertions
```elisp
;; Basic assertions
(should FORM) ; FORM should be non-nil
(should-not FORM) ; FORM should be nil
(should-error FORM) ; FORM should signal an error
(should-error FORM :type 'error-type) ; Specific error type
;; Equality checks
(should (equal expected actual))
(should (string= "expected" actual))
(should (= 42 actual)) ; Numeric equality
(should (eq 'symbol actual)) ; Symbol identity
(should (eql 1.0 actual)) ; Numeric with type
;; Pattern matching
(should (string-match-p "pattern" actual))
(should (cl-every #'integerp list))
;; With custom messages (ERT 28+)
(ert-deftest example-with-message ()
(let ((result (some-function)))
(should (equal expected result)
(format "Expected %s but got %s" expected result))))
```
### Test Fixtures
```elisp
;; Using let for setup
(ert-deftest ob-elixir-test-with-fixture ()
(let ((org-babel-default-header-args:elixir '((:results . "value")))
(test-code "Enum.sum([1, 2, 3])"))
(should (equal "6" (ob-elixir-evaluate test-code '())))))
;; Shared setup with macros
(defmacro ob-elixir-test-with-temp-buffer (&rest body)
"Execute BODY in a temporary org buffer."
`(with-temp-buffer
(org-mode)
,@body))
(ert-deftest ob-elixir-test-in-org-buffer ()
(ob-elixir-test-with-temp-buffer
(insert "#+BEGIN_SRC elixir\n1 + 1\n#+END_SRC")
(goto-char (point-min))
(forward-line 1)
(should (equal "2" (org-babel-execute-src-block)))))
;; Setup/teardown pattern
(defvar ob-elixir-test--saved-command nil)
(defun ob-elixir-test-setup ()
"Setup for ob-elixir tests."
(setq ob-elixir-test--saved-command ob-elixir-command))
(defun ob-elixir-test-teardown ()
"Teardown for ob-elixir tests."
(setq ob-elixir-command ob-elixir-test--saved-command))
(ert-deftest ob-elixir-test-with-setup-teardown ()
(ob-elixir-test-setup)
(unwind-protect
(progn
(setq ob-elixir-command "/custom/path/elixir")
(should (string= "/custom/path/elixir" ob-elixir-command)))
(ob-elixir-test-teardown)))
```
### Running ERT Tests
```elisp
;; Interactive (in Emacs)
M-x ert RET t RET ; Run all tests
M-x ert RET ob-elixir RET ; Run tests matching "ob-elixir"
M-x ert-run-tests-interactively
;; From command line
emacs -batch -l ert -l ob-elixir.el -l test-ob-elixir.el \
-f ert-run-tests-batch-and-exit
;; With specific selector
emacs -batch -l ert -l test-ob-elixir.el \
--eval "(ert-run-tests-batch-and-exit 'ob-elixir-test-basic)"
```
---
## Buttercup
Buttercup provides RSpec-style BDD testing with better mocking support.
### Installation
```elisp
;; In Cask file or package installation
(depends-on "buttercup")
;; Or via use-package
(use-package buttercup :ensure t)
```
### Basic Structure
```elisp
;;; test-ob-elixir-buttercup.el --- Buttercup tests -*- lexical-binding: t; -*-
(require 'buttercup)
(require 'ob-elixir)
(describe "ob-elixir"
(describe "basic evaluation"
(it "evaluates simple arithmetic"
(expect (ob-elixir-evaluate "1 + 1" '())
:to-equal "2"))
(it "returns nil for empty input"
(expect (ob-elixir-evaluate "" '())
:to-be nil)))
(describe "variable injection"
(before-each
(setq test-params '((:var . ("x" . 10)))))
(it "injects variables into code"
(expect (ob-elixir-evaluate "x * 2" test-params)
:to-equal "20"))
(it "handles multiple variables"
(setq test-params '((:var . ("x" . 10))
(:var . ("y" . 5))))
(expect (ob-elixir-evaluate "x + y" test-params)
:to-equal "15")))
(describe "error handling"
(it "signals error on syntax error"
(expect (ob-elixir-evaluate "def foo(" '())
:to-throw 'ob-elixir-execution-error))))
```
### Buttercup Matchers
```elisp
;; Equality
(expect value :to-equal expected)
(expect value :to-be expected) ; eq comparison
(expect value :to-be-truthy)
(expect value :to-be nil)
;; Comparison
(expect value :to-be-greater-than 5)
(expect value :to-be-less-than 10)
;; Strings
(expect string :to-match "regexp")
(expect string :to-contain-string "substring")
;; Errors
(expect (lambda () (error-func)) :to-throw)
(expect (lambda () (error-func)) :to-throw 'error-type)
;; Spies (see Mocking section)
(expect 'function :to-have-been-called)
(expect 'function :to-have-been-called-with args)
(expect 'function :to-have-been-called-times n)
```
### Running Buttercup
```bash
# With Cask
cask exec buttercup -L .
# With Eldev
eldev test buttercup
# From command line directly
emacs -batch -L . -l buttercup \
-f buttercup-run-discover
```
---
## Test Organization
### Directory Structure
```
ob-elixir/
├── ob-elixir.el
├── test/
│ ├── test-ob-elixir.el # Main test file
│ ├── test-ob-elixir-session.el # Session-specific tests
│ ├── test-ob-elixir-vars.el # Variable handling tests
│ └── resources/
│ ├── sample.org # Test org files
│ └── test-project/ # Test Mix project
├── Cask # Or Eldev file
└── Makefile
```
### Test File Template
```elisp
;;; test-ob-elixir.el --- Tests for ob-elixir -*- lexical-binding: t; -*-
;;; Commentary:
;; Tests for ob-elixir package.
;; Run with: make test
;; Or: emacs -batch -l ert -l test-ob-elixir.el -f ert-run-tests-batch-and-exit
;;; Code:
(require 'ert)
;; Add source directory to load path
(let ((dir (file-name-directory (or load-file-name buffer-file-name))))
(add-to-list 'load-path (expand-file-name ".." dir)))
(require 'ob-elixir)
;;; Test Helpers
(defun ob-elixir-test-org-block (code &optional header-args)
"Create org src block with CODE and evaluate it."
(with-temp-buffer
(org-mode)
(insert (format "#+BEGIN_SRC elixir %s\n%s\n#+END_SRC"
(or header-args "")
code))
(goto-char (point-min))
(forward-line 1)
(org-babel-execute-src-block)))
;;; Unit Tests
(ert-deftest ob-elixir-test-command-exists ()
"Test that Elixir command is accessible."
(should (executable-find ob-elixir-command)))
(ert-deftest ob-elixir-test-simple-evaluation ()
"Test simple code evaluation."
(should (equal "2" (ob-elixir-test-org-block "1 + 1"))))
;;; Integration Tests
(ert-deftest ob-elixir-integration-full-block ()
"Test full org-babel workflow."
(skip-unless (executable-find "elixir"))
(should (equal "6" (ob-elixir-test-org-block "Enum.sum([1, 2, 3])"))))
(provide 'test-ob-elixir)
;;; test-ob-elixir.el ends here
```
---
## Testing org-babel Specifics
### Testing with Real Org Buffers
```elisp
(ert-deftest ob-elixir-test-full-org-workflow ()
"Test the complete org-babel evaluation workflow."
(skip-unless (executable-find "elixir"))
(with-temp-buffer
(org-mode)
(insert "#+NAME: test-block
#+BEGIN_SRC elixir :results value
Enum.map([1, 2, 3], &(&1 * 2))
#+END_SRC")
(goto-char (point-min))
(org-babel-goto-named-src-block "test-block")
(let ((result (org-babel-execute-src-block)))
(should (equal '(2 4 6) result)))))
```
### Testing Variable Passing
```elisp
(ert-deftest ob-elixir-test-var-passing ()
"Test :var header argument handling."
(with-temp-buffer
(org-mode)
(insert "#+BEGIN_SRC elixir :var x=5 :var y=10
x + y
#+END_SRC")
(goto-char (point-min))
(forward-line 1)
(should (equal "15" (org-babel-execute-src-block)))))
```
### Testing Table Input/Output
```elisp
(ert-deftest ob-elixir-test-table-input ()
"Test that tables are passed correctly."
(with-temp-buffer
(org-mode)
(insert "#+NAME: test-data
| a | 1 |
| b | 2 |
#+BEGIN_SRC elixir :var data=test-data
Enum.map(data, fn [k, v] -> {k, v * 2} end)
#+END_SRC")
(goto-char (point-min))
(search-forward "BEGIN_SRC")
(let ((result (org-babel-execute-src-block)))
(should (equal '(("a" 2) ("b" 4)) result)))))
```
### Testing Session Mode
```elisp
(ert-deftest ob-elixir-test-session-persistence ()
"Test that session maintains state between evaluations."
(skip-unless (executable-find "iex"))
(unwind-protect
(progn
;; First block: define variable
(with-temp-buffer
(org-mode)
(insert "#+BEGIN_SRC elixir :session test-session
x = 42
#+END_SRC")
(goto-char (point-min))
(forward-line 1)
(org-babel-execute-src-block))
;; Second block: use variable
(with-temp-buffer
(org-mode)
(insert "#+BEGIN_SRC elixir :session test-session
x * 2
#+END_SRC")
(goto-char (point-min))
(forward-line 1)
(should (equal "84" (org-babel-execute-src-block)))))
;; Cleanup
(when-let ((buf (get-buffer "*elixir-test-session*")))
(kill-buffer buf))))
```
---
## Mocking and Stubbing
### ERT with cl-letf
```elisp
(require 'cl-lib)
(ert-deftest ob-elixir-test-mock-shell-command ()
"Test with mocked shell command."
(cl-letf (((symbol-function 'shell-command-to-string)
(lambda (cmd) "42")))
(should (equal "42" (ob-elixir-evaluate "anything" '())))))
(ert-deftest ob-elixir-test-mock-process ()
"Test with mocked external process."
(cl-letf (((symbol-function 'call-process)
(lambda (&rest _args) 0))
((symbol-function 'org-babel-eval)
(lambda (_cmd body) (format "result: %s" body))))
(should (string-match "result:" (ob-elixir-evaluate "code" '())))))
```
### Buttercup Spies
```elisp
(describe "ob-elixir with mocks"
(before-each
(spy-on 'shell-command-to-string :and-return-value "mocked-result")
(spy-on 'message))
(it "uses shell command"
(ob-elixir-evaluate "code" '())
(expect 'shell-command-to-string :to-have-been-called))
(it "passes correct arguments"
(ob-elixir-evaluate "1 + 1" '())
(expect 'shell-command-to-string
:to-have-been-called-with
(string-match-p "elixir" (spy-calls-args-for
'shell-command-to-string 0))))
(it "can call through to original"
(spy-on 'some-func :and-call-through)
;; This will call the real function but track calls
))
```
### Testing Without External Dependencies
```elisp
(ert-deftest ob-elixir-test-without-elixir ()
"Test behavior when Elixir is not installed."
(cl-letf (((symbol-function 'executable-find)
(lambda (_) nil)))
(should-error (ob-elixir-check-installation)
:type 'user-error)))
```
---
## CI/CD Integration
### GitHub Actions Workflow
```yaml
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
emacs-version: ['27.2', '28.2', '29.1']
steps:
- uses: actions/checkout@v4
- name: Set up Emacs
uses: purcell/setup-emacs@master
with:
version: ${{ matrix.emacs-version }}
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: '1.15'
otp-version: '26'
- name: Install Eldev
run: curl -fsSL https://raw.github.com/doublep/eldev/master/webinstall/github-eldev | sh
- name: Run tests
run: eldev test
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Emacs
uses: purcell/setup-emacs@master
with:
version: '29.1'
- name: Install Eldev
run: curl -fsSL https://raw.github.com/doublep/eldev/master/webinstall/github-eldev | sh
- name: Lint
run: eldev lint
```
### Makefile
```makefile
EMACS ?= emacs
BATCH = $(EMACS) -Q -batch -L .
.PHONY: test test-ert compile lint clean
all: compile test
compile:
$(BATCH) -f batch-byte-compile ob-elixir.el
test: test-ert
test-ert:
$(BATCH) -l ert -l test/test-ob-elixir.el \
-f ert-run-tests-batch-and-exit
lint:
$(BATCH) -l package-lint \
--eval "(setq package-lint-main-file \"ob-elixir.el\")" \
-f package-lint-batch-and-exit ob-elixir.el
clean:
rm -f *.elc test/*.elc
```
---
## Build Tools
### Eldev (Recommended)
```elisp
; Eldev file
(eldev-use-package-archive 'gnu)
(eldev-use-package-archive 'melpa)
; Dependencies
(eldev-add-extra-dependencies 'test 'buttercup)
; Test configuration
(setf eldev-test-framework 'ert) ; or 'buttercup
; Lint configuration
(setf eldev-lint-default '(elisp package))
```
```bash
# Common Eldev commands
eldev test # Run tests
eldev lint # Run linters
eldev compile # Byte-compile
eldev clean # Clean build artifacts
eldev doctor # Check project health
```
### Cask
```elisp
; Cask file
(source gnu)
(source melpa)
(package-file "ob-elixir.el")
(development
(depends-on "ert")
(depends-on "buttercup")
(depends-on "package-lint"))
```
```bash
# Common Cask commands
cask install # Install dependencies
cask exec ert-runner # Run ERT tests
cask exec buttercup -L . # Run Buttercup tests
cask build # Byte-compile
```
---
## References
- [ERT Manual](https://www.gnu.org/software/emacs/manual/html_node/ert/)
- [Buttercup Documentation](https://github.com/jorgenschaefer/emacs-buttercup)
- [Eldev Documentation](https://github.com/doublep/eldev)
- [Cask Documentation](https://github.com/cask/cask)
- [GitHub Actions for Emacs](https://github.com/purcell/setup-emacs)
- [Org Mode Test Suite](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/testing)
- [Package-lint](https://github.com/purcell/package-lint)

View File

@@ -0,0 +1,747 @@
# Org Babel Language Implementation Guide
This document provides a comprehensive guide to implementing org-babel support for a new programming language, specifically targeting Elixir.
## Table of Contents
- [Architecture Overview](#architecture-overview)
- [Required Components](#required-components)
- [Optional Components](#optional-components)
- [Header Arguments](#header-arguments)
- [Result Handling](#result-handling)
- [Session Management](#session-management)
- [Variable Handling](#variable-handling)
- [Utility Functions](#utility-functions)
- [Complete Implementation Template](#complete-implementation-template)
- [References](#references)
---
## Architecture Overview
### How Org Babel Executes Code
```
User presses C-c C-c on source block
┌─────────────────────────────┐
│ org-babel-execute-src-block │
└─────────────────────────────┘
┌─────────────────────────────┐
│ Parse header arguments │
│ Get block body │
└─────────────────────────────┘
┌─────────────────────────────┐
│ org-babel-execute:LANG │ ◄── Your implementation
│ (language-specific) │
└─────────────────────────────┘
┌─────────────────────────────┐
│ Process and format results │
└─────────────────────────────┘
┌─────────────────────────────┐
│ Insert results in buffer │
└─────────────────────────────┘
```
### Source Block Structure
```org
#+NAME: block-name
#+HEADER: :var x=5
#+BEGIN_SRC elixir :results value :session my-session
# This is the body
x * 2
#+END_SRC
#+RESULTS: block-name
: 10
```
### Key Data Structures
**params alist** - Association list passed to execute function:
```elisp
((:results . "value")
(:session . "my-session")
(:var . ("x" . 5))
(:var . ("y" . 10))
(:colnames . no)
(:rownames . no)
(:result-params . ("value" "replace"))
(:result-type . value)
...)
```
---
## Required Components
### 1. The Execute Function
This is the **only strictly required** function. It must be named exactly `org-babel-execute:LANG`:
```elisp
(defun org-babel-execute:elixir (body params)
"Execute a block of Elixir code with org-babel.
BODY is the code to execute.
PARAMS is an alist of header arguments.
This function is called by `org-babel-execute-src-block'."
(let* ((session (cdr (assq :session params)))
(result-type (cdr (assq :result-type params)))
(result-params (cdr (assq :result-params params)))
(full-body (org-babel-expand-body:generic
body params
(org-babel-variable-assignments:elixir params))))
;; Execute and return results
(org-babel-reassemble-table
(org-babel-result-cond result-params
;; For :results output - return raw output
(ob-elixir--execute full-body session)
;; For :results value - parse as elisp data
(ob-elixir--table-or-string
(ob-elixir--execute full-body session)))
(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))))))
```
### 2. Default Header Arguments
Define language-specific defaults:
```elisp
(defvar org-babel-default-header-args:elixir
'((:results . "value")
(:session . "none"))
"Default header arguments for Elixir code blocks.")
```
### 3. Language Registration
Enable the language for evaluation:
```elisp
;; Add to tangle extensions (for org-babel-tangle)
(add-to-list 'org-babel-tangle-lang-exts '("elixir" . "ex"))
;; Add to language mode mapping (for syntax highlighting)
(add-to-list 'org-src-lang-modes '("elixir" . elixir))
```
---
## Optional Components
### Language-Specific Header Arguments
Define what header arguments your language supports:
```elisp
(defconst org-babel-header-args:elixir
'((mix-project . :any) ; Path to mix project
(mix-env . :any) ; MIX_ENV value
(iex-args . :any) ; Extra args for iex
(timeout . :any) ; Execution timeout
(async . ((yes no)))) ; Async execution
"Elixir-specific header arguments.")
```
### Initiate Session Function
For persistent REPL sessions:
```elisp
(defun org-babel-elixir-initiate-session (&optional session params)
"Create or return an Elixir session buffer.
SESSION is the session name (string or nil).
PARAMS are the header arguments.
Returns the session buffer, or nil if SESSION is \"none\"."
(unless (string= session "none")
(let ((session-name (or session "default")))
(ob-elixir--get-or-create-session session-name params))))
```
### Prep Session Function
Prepare a session with variables before execution:
```elisp
(defun org-babel-prep-session:elixir (session params)
"Prepare SESSION according to PARAMS.
Injects variables into the session before code execution."
(let ((session-buffer (org-babel-elixir-initiate-session session params))
(var-lines (org-babel-variable-assignments:elixir params)))
(when session-buffer
(org-babel-comint-in-buffer session-buffer
(dolist (var-line var-lines)
(insert var-line)
(comint-send-input nil t)
(org-babel-comint-wait-for-output session-buffer))))
session-buffer))
```
### Load Session Function
Load code into session without executing:
```elisp
(defun org-babel-load-session:elixir (session body params)
"Load BODY into SESSION without executing.
Useful for setting up definitions to be used later."
(save-window-excursion
(let ((buffer (org-babel-prep-session:elixir session params)))
(with-current-buffer buffer
(goto-char (process-mark (get-buffer-process buffer)))
(insert (org-babel-chomp body)))
buffer)))
```
### Variable Assignments Function
Convert Elisp values to language-specific variable assignments:
```elisp
(defun org-babel-variable-assignments:elixir (params)
"Return list of Elixir statements to assign variables from PARAMS."
(mapcar
(lambda (pair)
(format "%s = %s"
(car pair)
(ob-elixir--elisp-to-elixir (cdr pair))))
(org-babel--get-vars params)))
```
### Expand Body Function
Customize how the code body is expanded (optional, generic works for most):
```elisp
(defun org-babel-expand-body:elixir (body params)
"Expand BODY according to PARAMS.
Add variable bindings, prologue, epilogue."
(let ((vars (org-babel-variable-assignments:elixir params))
(prologue (cdr (assq :prologue params)))
(epilogue (cdr (assq :epilogue params))))
(concat
(when prologue (concat prologue "\n"))
(mapconcat #'identity vars "\n")
(when vars "\n")
body
(when epilogue (concat "\n" epilogue)))))
```
---
## Header Arguments
### Standard Header Arguments (Always Available)
| Argument | Values | Description |
|-------------|---------------------------|----------------------|
| `:results` | value, output | What to capture |
| `:session` | name, "none" | Session to use |
| `:var` | name=value | Variable binding |
| `:exports` | code, results, both, none | Export behavior |
| `:file` | path | Save results to file |
| `:dir` | path | Working directory |
| `:prologue` | code | Code to run before |
| `:epilogue` | code | Code to run after |
| `:noweb` | yes, no, tangle | Noweb expansion |
| `:cache` | yes, no | Cache results |
| `:tangle` | yes, no, filename | Tangle behavior |
### Result Types
```elisp
;; Accessing result configuration
(let* ((result-params (cdr (assq :result-params params)))
(result-type (cdr (assq :result-type params))))
;; result-type is 'value or 'output
;; result-params is a list like ("value" "replace" "scalar")
(cond
((eq result-type 'output)
;; Capture stdout
(ob-elixir--execute-output body))
((eq result-type 'value)
;; Return last expression value
(ob-elixir--execute-value body))))
```
### Parsing Header Arguments
```elisp
(defun ob-elixir--parse-params (params)
"Parse PARAMS into a structured format."
(let ((session (cdr (assq :session params)))
(result-type (cdr (assq :result-type params)))
(result-params (cdr (assq :result-params params)))
(dir (cdr (assq :dir params)))
(vars (org-babel--get-vars params))
;; Custom header args
(mix-project (cdr (assq :mix-project params)))
(mix-env (cdr (assq :mix-env params)))
(timeout (cdr (assq :timeout params))))
(list :session session
:result-type result-type
:result-params result-params
:dir dir
:vars vars
:mix-project mix-project
:mix-env mix-env
:timeout (or timeout ob-elixir-default-timeout))))
```
---
## Result Handling
### The `org-babel-result-cond` Macro
This handles result formatting based on `:results` header:
```elisp
(org-babel-result-cond result-params
;; This form is used for :results output/scalar/verbatim
(ob-elixir--raw-output body)
;; This form is used for :results value (default)
;; Should return elisp data for possible table conversion
(ob-elixir--parsed-value body))
```
### Converting Results to Tables
```elisp
(defun ob-elixir--table-or-string (result)
"Convert RESULT to an Emacs table or string.
If RESULT looks like a list/table, parse it.
Otherwise return as string."
(let ((res (org-babel-script-escape result)))
(if (listp res)
(mapcar (lambda (el)
(if (eq el 'nil)
org-babel-elixir-nil-to
el))
res)
res)))
(defvar org-babel-elixir-nil-to 'hline
"Value to use for nil in Elixir results.
When nil values appear in lists/tables, convert to this.")
```
### Using `org-babel-reassemble-table`
Restore column/row names to tables:
```elisp
(org-babel-reassemble-table
result ; The result data
(org-babel-pick-name ; Column names
(cdr (assq :colname-names params))
(cdr (assq :colnames params)))
(org-babel-pick-name ; Row names
(cdr (assq :rowname-names params))
(cdr (assq :rownames params))))
```
### File Results
For `:results file`:
```elisp
(defun ob-elixir--handle-file-result (result params)
"Handle file output for RESULT based on PARAMS."
(let ((file (cdr (assq :file params))))
(when file
(with-temp-file file
(insert result))
file))) ; Return filename for link creation
```
---
## Session Management
### Using Comint for Sessions
Org-babel provides utilities for managing comint-based REPLs:
```elisp
(require 'ob-comint)
(defvar ob-elixir-prompt-regexp "^iex([0-9]+)> "
"Regexp matching the IEx prompt.")
(defun ob-elixir--get-or-create-session (name params)
"Get or create an IEx session named NAME."
(let ((buffer-name (format "*ob-elixir-%s*" name)))
(if (org-babel-comint-buffer-livep (get-buffer buffer-name))
(get-buffer buffer-name)
(ob-elixir--start-session buffer-name params))))
(defun ob-elixir--start-session (buffer-name params)
"Start a new IEx session in BUFFER-NAME."
(let* ((iex-cmd (or (cdr (assq :iex-command params)) "iex"))
(mix-project (cdr (assq :mix-project params)))
(cmd (if mix-project
(format "cd %s && iex -S mix"
(shell-quote-argument mix-project))
iex-cmd)))
(with-current-buffer (get-buffer-create buffer-name)
(make-comint-in-buffer "ob-elixir" (current-buffer)
shell-file-name nil "-c" cmd)
;; Wait for prompt
(sit-for 1)
;; Configure IEx for non-interactive use
(ob-elixir--configure-session (current-buffer))
(current-buffer))))
(defun ob-elixir--configure-session (buffer)
"Configure IEx session in BUFFER for programmatic use."
(org-babel-comint-in-buffer buffer
(insert "IEx.configure(colors: [enabled: false])")
(comint-send-input nil t)
(org-babel-comint-wait-for-output buffer)))
```
### Evaluating in Session
```elisp
(defun ob-elixir--evaluate-in-session (session body)
"Evaluate BODY in SESSION buffer, return output."
(let ((session-buffer (ob-elixir--get-or-create-session session nil))
(eoe-indicator ob-elixir-eoe-indicator))
(org-babel-comint-with-output
(session-buffer ob-elixir-prompt-regexp t eoe-indicator)
(insert body)
(comint-send-input nil t)
;; Send EOE marker
(insert (format "\"%s\"" eoe-indicator))
(comint-send-input nil t))))
```
### Key Comint Utilities
```elisp
;; Check if buffer has live process
(org-babel-comint-buffer-livep buffer)
;; Execute forms in comint buffer context
(org-babel-comint-in-buffer buffer
(insert "code")
(comint-send-input))
;; Wait for output with timeout
(org-babel-comint-wait-for-output buffer)
;; Capture output until EOE marker
(org-babel-comint-with-output (buffer prompt t eoe-marker)
body...)
```
---
## Variable Handling
### Getting Variables from Params
```elisp
;; org-babel--get-vars returns list of (name . value) pairs
(let ((vars (org-babel--get-vars params)))
;; vars = (("x" . 5) ("y" . "hello") ("data" . ((1 2) (3 4))))
(dolist (var vars)
(let ((name (car var))
(value (cdr var)))
;; Process each variable
)))
```
### Converting Elisp to Elixir
```elisp
(defun ob-elixir--elisp-to-elixir (value)
"Convert Elisp VALUE to Elixir literal syntax."
(cond
;; nil -> nil
((null value) "nil")
;; t -> true
((eq value t) "true")
;; Numbers stay as-is
((numberp value) (number-to-string value))
;; Strings get quoted
((stringp value) (format "\"%s\"" (ob-elixir--escape-string value)))
;; Symbols become atoms
((symbolp value) (format ":%s" (symbol-name value)))
;; Lists become Elixir lists
((listp value)
(if (ob-elixir--alist-p value)
;; Association list -> keyword list or map
(ob-elixir--alist-to-elixir value)
;; Regular list
(format "[%s]"
(mapconcat #'ob-elixir--elisp-to-elixir value ", "))))
;; Fallback: convert to string
(t (format "%S" value))))
(defun ob-elixir--escape-string (str)
"Escape special characters in STR for Elixir."
(replace-regexp-in-string
"\\\\" "\\\\\\\\"
(replace-regexp-in-string
"\"" "\\\\\""
(replace-regexp-in-string
"\n" "\\\\n" str))))
(defun ob-elixir--alist-p (list)
"Return t if LIST is an association list."
(and (listp list)
(cl-every (lambda (el)
(and (consp el) (atom (car el))))
list)))
(defun ob-elixir--alist-to-elixir (alist)
"Convert ALIST to Elixir keyword list or map."
(format "[%s]"
(mapconcat
(lambda (pair)
(format "%s: %s"
(car pair)
(ob-elixir--elisp-to-elixir (cdr pair))))
alist
", ")))
```
### Handling Tables (hlines)
```elisp
(defvar org-babel-elixir-hline-to ":hline"
"Elixir representation for org table hlines.")
(defun ob-elixir--table-to-elixir (table)
"Convert org TABLE to Elixir list of lists."
(format "[%s]"
(mapconcat
(lambda (row)
(if (eq row 'hline)
org-babel-elixir-hline-to
(format "[%s]"
(mapconcat #'ob-elixir--elisp-to-elixir row ", "))))
table
", ")))
```
---
## Utility Functions
### From ob.el / ob-core.el
```elisp
;; Execute external command with body as stdin
(org-babel-eval command body)
;; Returns stdout as string
;; Create temporary file
(org-babel-temp-file "prefix-")
;; Returns temp file path
;; Read file contents
(org-babel-eval-read-file filename)
;; Process file path for use in scripts
(org-babel-process-file-name filename)
;; Parse script output as Elisp data
(org-babel-script-escape output)
;; Parses "[1, 2, 3]" -> (1 2 3)
;; Remove trailing newlines
(org-babel-chomp string)
;; Expand generic body with vars
(org-babel-expand-body:generic body params var-lines)
```
### Wrapper Method Pattern
For `:results value`, wrap code to capture return value:
```elisp
(defconst ob-elixir-wrapper-method
"
result = (fn ->
%s
end).()
result
|> inspect(limit: :infinity, printable_limit: :infinity)
|> IO.puts()
"
"Template for wrapping Elixir code to capture value.
%s is replaced with the user's code.")
(defun ob-elixir--wrap-for-value (body)
"Wrap BODY to capture its return value."
(format ob-elixir-wrapper-method body))
```
---
## Complete Implementation Template
Here's a minimal but functional template:
```elisp
;;; ob-elixir.el --- Org Babel functions for Elixir -*- lexical-binding: t; -*-
;; Copyright (C) 2024 Your Name
;; Author: Your Name <email@example.com>
;; Version: 1.0.0
;; Package-Requires: ((emacs "27.1") (org "9.4"))
;; Keywords: literate programming, elixir
;; URL: https://github.com/user/ob-elixir
;;; Commentary:
;; Org Babel support for evaluating Elixir code blocks.
;;; Code:
(require 'ob)
(require 'ob-eval)
;;; 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."
:type 'string
:group 'ob-elixir)
;;; Header Arguments
(defvar org-babel-default-header-args:elixir
'((:results . "value")
(:session . "none"))
"Default header arguments for Elixir code blocks.")
(defconst org-babel-header-args:elixir
'((mix-project . :any))
"Elixir-specific header arguments.")
;;; File Extension
(add-to-list 'org-babel-tangle-lang-exts '("elixir" . "ex"))
;;; Execution
(defun org-babel-execute:elixir (body params)
"Execute Elixir BODY according to PARAMS."
(let* ((result-type (cdr (assq :result-type params)))
(result-params (cdr (assq :result-params params)))
(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))))))
(defun ob-elixir--execute (body result-type)
"Execute BODY and return result based on RESULT-TYPE."
(let* ((tmp-file (org-babel-temp-file "elixir-" ".exs"))
(wrapper (if (eq result-type 'value)
(ob-elixir--wrap-for-value body)
body)))
(with-temp-file tmp-file
(insert wrapper))
(org-babel-eval
(format "%s %s" ob-elixir-command
(org-babel-process-file-name tmp-file))
"")))
(defconst ob-elixir--value-wrapper
"result = (\n%s\n)\nIO.puts(inspect(result, limit: :infinity))"
"Wrapper to capture return value.")
(defun ob-elixir--wrap-for-value (body)
"Wrap BODY to capture its return value."
(format ob-elixir--value-wrapper body))
;;; Variables
(defun org-babel-variable-assignments:elixir (params)
"Return Elixir code to assign variables from PARAMS."
(mapcar
(lambda (pair)
(format "%s = %s"
(car pair)
(ob-elixir--elisp-to-elixir (cdr pair))))
(org-babel--get-vars params)))
(defun ob-elixir--elisp-to-elixir (value)
"Convert Elisp VALUE to Elixir syntax."
(cond
((null value) "nil")
((eq value t) "true")
((numberp value) (number-to-string value))
((stringp value) (format "\"%s\"" value))
((symbolp value) (format ":%s" (symbol-name value)))
((listp value)
(format "[%s]" (mapconcat #'ob-elixir--elisp-to-elixir value ", ")))
(t (format "%S" value))))
(provide 'ob-elixir)
;;; ob-elixir.el ends here
```
---
## References
- [Org Mode Manual - Working with Source Code](https://orgmode.org/manual/Working-with-Source-Code.html)
- [Org Mode Manual - Languages](https://orgmode.org/manual/Languages.html)
- [Org Mode Manual - Results of Evaluation](https://orgmode.org/manual/Results-of-Evaluation.html)
- [Worg - Babel Language Template](https://orgmode.org/worg/org-contrib/babel/languages/ob-doc-template.html)
- [Worg - Python Documentation](https://orgmode.org/worg/org-contrib/babel/languages/ob-doc-python.html)
- [Org Source - ob.el](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/lisp/ob.el)
- [Org Source - ob-python.el](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/lisp/ob-python.el)
- [Org Source - ob-ruby.el](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/lisp/ob-ruby.el)
- [Org Element API](https://orgmode.org/worg/dev/org-element-api.html)

View File

@@ -0,0 +1,644 @@
# Elixir Integration Strategies
This document covers strategies for integrating Elixir with Emacs and org-babel, including execution modes, data conversion, and Mix project support.
## Table of Contents
- [Execution Modes](#execution-modes)
- [One-Shot Execution](#one-shot-execution)
- [IEx Session Management](#iex-session-management)
- [Mix Project Context](#mix-project-context)
- [Remote Shell (remsh)](#remote-shell-remsh)
- [Data Type Conversion](#data-type-conversion)
- [Error Handling](#error-handling)
- [elixir-mode Integration](#elixir-mode-integration)
- [Performance Considerations](#performance-considerations)
- [References](#references)
---
## Execution Modes
### Overview of Execution Strategies
| Mode | Command | Use Case | Startup Time | State |
|--------------|-------------------|----------------|--------------|-------------|
| One-shot | `elixir -e` | Simple scripts | ~500ms | None |
| Script file | `elixir file.exs` | Larger code | ~500ms | None |
| IEx session | `iex` | Interactive | Once | Persistent |
| Mix context | `iex -S mix` | Projects | Slower | Project |
| Remote shell | `iex --remsh` | Production | Fast | Remote node |
### Recommended Approach
For org-babel, we recommend:
1. **Default**: Script file execution (reliable, predictable)
2. **With `:session`**: IEx via comint (persistent state)
3. **With `:mix-project`**: Mix context execution
---
## One-Shot Execution
### Using `elixir -e`
For simple, single expressions:
```elisp
(defun ob-elixir--eval-simple (code)
"Evaluate CODE using elixir -e."
(shell-command-to-string
(format "elixir -e %s"
(shell-quote-argument code))))
```
**Pros**: Simple, no temp files
**Cons**: Limited code size, quoting issues
### Using Script Files (Recommended)
More robust for complex code:
```elisp
(defun ob-elixir--eval-script (code)
"Evaluate CODE using a temporary script file."
(let ((script-file (org-babel-temp-file "elixir-" ".exs")))
(with-temp-file script-file
(insert code))
(org-babel-eval
(format "elixir %s" (org-babel-process-file-name script-file))
"")))
```
### Capturing Return Values vs Output
```elisp
;; For :results output - capture stdout
(defun ob-elixir--eval-output (code)
"Execute CODE and capture stdout."
(ob-elixir--eval-script code))
;; For :results value - wrap to capture return value
(defconst ob-elixir--value-wrapper
"
_result_ = (
%s
)
IO.puts(inspect(_result_, limit: :infinity, printable_limit: :infinity, charlists: :as_lists))
"
"Wrapper to capture the return value of code.")
(defun ob-elixir--eval-value (code)
"Execute CODE and return its value."
(let ((wrapped (format ob-elixir--value-wrapper code)))
(string-trim (ob-elixir--eval-script wrapped))))
```
### Handling Multiline Output
```elisp
(defconst ob-elixir--value-wrapper-with-marker
"
_result_ = (
%s
)
IO.puts(\"__OB_ELIXIR_RESULT_START__\")
IO.puts(inspect(_result_, limit: :infinity, printable_limit: :infinity))
IO.puts(\"__OB_ELIXIR_RESULT_END__\")
"
"Wrapper with markers for reliable output parsing.")
(defun ob-elixir--extract-result (output)
"Extract result from OUTPUT between markers."
(when (string-match "__OB_ELIXIR_RESULT_START__\n\\(.*\\)\n__OB_ELIXIR_RESULT_END__"
output)
(match-string 1 output)))
```
---
## IEx Session Management
### Starting an IEx Session
```elisp
(defvar ob-elixir-iex-buffer-name "*ob-elixir-iex*"
"Buffer name for the IEx process.")
(defvar ob-elixir-prompt-regexp "^iex\\([0-9]+\\)> "
"Regexp matching the IEx prompt.")
(defvar ob-elixir-continued-prompt-regexp "^\\.\\.\\.\\([0-9]+\\)> "
"Regexp matching the IEx continuation prompt.")
(defun ob-elixir--start-iex-session (session-name &optional params)
"Start an IEx session named SESSION-NAME."
(let* ((buffer-name (format "*ob-elixir-%s*" session-name))
(buffer (get-buffer-create buffer-name))
(mix-project (when params (cdr (assq :mix-project params))))
(iex-args (when params (cdr (assq :iex-args params)))))
(unless (comint-check-proc buffer)
(with-current-buffer buffer
;; Set up environment
(setenv "TERM" "dumb")
(setenv "IEX_WITH_WERL" nil)
;; Start the process
(apply #'make-comint-in-buffer
(format "ob-elixir-%s" session-name)
buffer
"iex"
nil
(append
(when mix-project
(list "-S" "mix"))
(when iex-args
(split-string iex-args))))
;; Wait for initial prompt
(ob-elixir--wait-for-prompt buffer 10)
;; Configure IEx for programmatic use
(ob-elixir--configure-iex-session buffer)))
buffer))
```
### Configuring IEx for Non-Interactive Use
```elisp
(defun ob-elixir--configure-iex-session (buffer)
"Configure IEx in BUFFER for non-interactive use."
(with-current-buffer buffer
(let ((config-commands
'("IEx.configure(colors: [enabled: false])"
"IEx.configure(inspect: [limit: :infinity, printable_limit: :infinity])"
"Application.put_env(:elixir, :ansi_enabled, false)")))
(dolist (cmd config-commands)
(ob-elixir--send-to-iex buffer cmd)
(ob-elixir--wait-for-prompt buffer 5)))))
```
### Sending Code to IEx
```elisp
(defvar ob-elixir-eoe-marker "__OB_ELIXIR_EOE__"
"End-of-evaluation marker.")
(defun ob-elixir--send-to-iex (buffer code)
"Send CODE to IEx process in BUFFER."
(with-current-buffer buffer
(goto-char (point-max))
(insert code)
(comint-send-input nil t)))
(defun ob-elixir--eval-in-session (session code)
"Evaluate CODE in SESSION, return output."
(let* ((buffer (ob-elixir--get-or-create-session session))
(start-marker nil))
(with-current-buffer buffer
(goto-char (point-max))
(setq start-marker (point-marker))
;; Send the code
(ob-elixir--send-to-iex buffer code)
(ob-elixir--wait-for-prompt buffer 30)
;; Send EOE marker to clearly delineate output
(ob-elixir--send-to-iex buffer (format "\"%s\"" ob-elixir-eoe-marker))
(ob-elixir--wait-for-prompt buffer 5)
;; Extract output
(ob-elixir--extract-session-output buffer start-marker))))
(defun ob-elixir--wait-for-prompt (buffer timeout)
"Wait for IEx prompt in BUFFER with TIMEOUT seconds."
(with-current-buffer buffer
(let ((end-time (+ (float-time) timeout)))
(while (and (< (float-time) end-time)
(not (ob-elixir--at-prompt-p)))
(accept-process-output (get-buffer-process buffer) 0.1)
(goto-char (point-max))))))
(defun ob-elixir--at-prompt-p ()
"Return t if point is at an IEx prompt."
(save-excursion
(beginning-of-line)
(looking-at ob-elixir-prompt-regexp)))
```
### Cleaning Session Output
```elisp
(defun ob-elixir--clean-iex-output (output)
"Clean OUTPUT from IEx session."
(let ((cleaned output))
;; Remove ANSI escape codes
(setq cleaned (ansi-color-filter-apply cleaned))
;; Remove prompts
(setq cleaned (replace-regexp-in-string
"^iex([0-9]+)> " "" cleaned))
(setq cleaned (replace-regexp-in-string
"^\\.\\.\\.([0-9]+)> " "" cleaned))
;; Remove EOE marker
(setq cleaned (replace-regexp-in-string
(regexp-quote (format "\"%s\"" ob-elixir-eoe-marker))
"" cleaned))
;; Remove trailing whitespace
(string-trim cleaned)))
```
---
## Mix Project Context
### Detecting Mix Projects
```elisp
(defun ob-elixir--find-mix-project (dir)
"Find mix.exs file starting from DIR, searching up."
(let ((mix-file (locate-dominating-file dir "mix.exs")))
(when mix-file
(file-name-directory mix-file))))
(defun ob-elixir--in-mix-project-p ()
"Return t if current buffer is in a Mix project."
(ob-elixir--find-mix-project default-directory))
```
### Executing in Mix Context
```elisp
(defun ob-elixir--eval-with-mix (code project-dir &optional mix-env)
"Evaluate CODE in the context of Mix project at PROJECT-DIR."
(let* ((default-directory project-dir)
(script-file (org-babel-temp-file "elixir-" ".exs"))
(env-vars (when mix-env
(format "MIX_ENV=%s " mix-env))))
(with-temp-file script-file
(insert code))
(shell-command-to-string
(format "%smix run %s"
(or env-vars "")
(org-babel-process-file-name script-file)))))
```
### Compiling Before Execution
```elisp
(defun ob-elixir--ensure-compiled (project-dir)
"Ensure Mix project at PROJECT-DIR is compiled."
(let ((default-directory project-dir))
(shell-command-to-string "mix compile --force-check")))
(defun ob-elixir--eval-with-compilation (code project-dir)
"Compile and evaluate CODE in PROJECT-DIR."
(ob-elixir--ensure-compiled project-dir)
(ob-elixir--eval-with-mix code project-dir))
```
### Using Mix Aliases
```elisp
(defun ob-elixir--run-mix-task (task project-dir &optional args)
"Run Mix TASK in PROJECT-DIR with ARGS."
(let ((default-directory project-dir))
(shell-command-to-string
(format "mix %s %s" task (or args "")))))
```
---
## Remote Shell (remsh)
### Connecting to Running Nodes
```elisp
(defun ob-elixir--start-remsh-session (node-name &optional cookie sname)
"Connect to remote Elixir node NODE-NAME.
COOKIE is the Erlang cookie (optional).
SNAME is the short name for the local node."
(let* ((local-name (or sname (format "ob_elixir_%d" (random 10000))))
(buffer-name (format "*ob-elixir-remsh-%s*" node-name))
(buffer (get-buffer-create buffer-name))
(args (append
(list "--sname" local-name)
(when cookie (list "--cookie" cookie))
(list "--remsh" node-name))))
(unless (comint-check-proc buffer)
(with-current-buffer buffer
(apply #'make-comint-in-buffer
(format "ob-elixir-remsh-%s" node-name)
buffer
"iex"
nil
args)
(ob-elixir--wait-for-prompt buffer 30)
(ob-elixir--configure-iex-session buffer)))
buffer))
```
### Remote Evaluation
```elisp
(defun ob-elixir--eval-remote (code node-name &optional cookie)
"Evaluate CODE on remote NODE-NAME."
(let ((session (ob-elixir--start-remsh-session node-name cookie)))
(ob-elixir--eval-in-session session code)))
```
---
## Data Type Conversion
### Elisp to Elixir
```elisp
(defun ob-elixir--elisp-to-elixir (value)
"Convert Elisp VALUE to Elixir literal syntax."
(pcase value
('nil "nil")
('t "true")
((pred numberp) (number-to-string value))
((pred stringp) (ob-elixir--format-string value))
((pred symbolp) (ob-elixir--format-atom value))
((pred vectorp) (ob-elixir--format-tuple value))
((pred listp)
(cond
((ob-elixir--alist-p value) (ob-elixir--format-keyword-list value))
((ob-elixir--plist-p value) (ob-elixir--format-map value))
(t (ob-elixir--format-list value))))
(_ (format "%S" value))))
(defun ob-elixir--format-string (str)
"Format STR as Elixir string."
(format "\"%s\""
(replace-regexp-in-string
"\\\\" "\\\\\\\\"
(replace-regexp-in-string
"\"" "\\\\\""
(replace-regexp-in-string
"\n" "\\\\n"
str)))))
(defun ob-elixir--format-atom (sym)
"Format symbol SYM as Elixir atom."
(let ((name (symbol-name sym)))
(if (string-match-p "^[a-z_][a-zA-Z0-9_]*[?!]?$" name)
(format ":%s" name)
(format ":\"%s\"" name))))
(defun ob-elixir--format-list (lst)
"Format LST as Elixir list."
(format "[%s]"
(mapconcat #'ob-elixir--elisp-to-elixir lst ", ")))
(defun ob-elixir--format-tuple (vec)
"Format vector VEC as Elixir tuple."
(format "{%s}"
(mapconcat #'ob-elixir--elisp-to-elixir (append vec nil) ", ")))
(defun ob-elixir--format-keyword-list (alist)
"Format ALIST as Elixir keyword list."
(format "[%s]"
(mapconcat
(lambda (pair)
(format "%s: %s"
(car pair)
(ob-elixir--elisp-to-elixir (cdr pair))))
alist ", ")))
(defun ob-elixir--format-map (plist)
"Format PLIST as Elixir map."
(let ((pairs '()))
(while plist
(push (format "%s => %s"
(ob-elixir--elisp-to-elixir (car plist))
(ob-elixir--elisp-to-elixir (cadr plist)))
pairs)
(setq plist (cddr plist)))
(format "%%{%s}" (mapconcat #'identity (nreverse pairs) ", "))))
```
### Elixir to Elisp
```elisp
(defun ob-elixir--parse-result (output)
"Parse Elixir OUTPUT into Elisp value."
(let ((trimmed (string-trim output)))
(cond
;; nil
((string= trimmed "nil") nil)
;; Booleans
((string= trimmed "true") t)
((string= trimmed "false") nil)
;; Numbers
((string-match-p "^-?[0-9]+$" trimmed)
(string-to-number trimmed))
((string-match-p "^-?[0-9]+\\.[0-9]+\\(e[+-]?[0-9]+\\)?$" trimmed)
(string-to-number trimmed))
;; Atoms (convert to symbols)
((string-match "^:\\([a-zA-Z_][a-zA-Z0-9_]*\\)$" trimmed)
(intern (match-string 1 trimmed)))
;; Strings
((string-match "^\"\\(.*\\)\"$" trimmed)
(match-string 1 trimmed))
;; Lists/tuples - use org-babel-script-escape
((string-match-p "^\\[.*\\]$" trimmed)
(org-babel-script-escape trimmed))
((string-match-p "^{.*}$" trimmed)
(org-babel-script-escape
(replace-regexp-in-string "^{\\(.*\\)}$" "[\\1]" trimmed)))
;; Maps - convert to alist
((string-match-p "^%{.*}$" trimmed)
(ob-elixir--parse-map trimmed))
;; Default: return as string
(t trimmed))))
(defun ob-elixir--parse-map (map-string)
"Parse Elixir MAP-STRING to alist."
;; Simplified - for complex maps, use JSON encoding
(let ((content (substring map-string 2 -1))) ; Remove %{ and }
(mapcar
(lambda (pair)
(when (string-match "\\(.+?\\) => \\(.+\\)" pair)
(cons (ob-elixir--parse-result (match-string 1 pair))
(ob-elixir--parse-result (match-string 2 pair)))))
(split-string content ", "))))
```
### Using JSON for Complex Data
For complex nested structures, JSON is more reliable:
```elisp
(defconst ob-elixir--json-wrapper
"
_result_ = (
%s
)
IO.puts(Jason.encode!(_result_))
"
"Wrapper that outputs result as JSON.")
(defun ob-elixir--eval-as-json (code)
"Evaluate CODE and parse result as JSON."
(let* ((wrapped (format ob-elixir--json-wrapper code))
(output (ob-elixir--eval-script wrapped)))
(json-read-from-string (string-trim output))))
```
---
## Error Handling
### Detecting Elixir Errors
```elisp
(defconst ob-elixir-error-patterns
'("^\\*\\* (\\(\\w+Error\\))" ; ** (RuntimeError) ...
"^\\*\\* (\\(\\w+\\)) \\(.+\\)" ; ** (exit) ...
"^\\(CompileError\\)" ; CompileError ...
"^\\(SyntaxError\\)") ; SyntaxError ...
"Patterns matching Elixir error output.")
(defun ob-elixir--error-p (output)
"Return error info if OUTPUT contains an error, nil otherwise."
(catch 'found
(dolist (pattern ob-elixir-error-patterns)
(when (string-match pattern output)
(throw 'found (list :type (match-string 1 output)
:message output))))))
```
### Signaling Errors to Org
```elisp
(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--handle-error (output)
"Handle error in OUTPUT, signaling appropriate condition."
(when-let ((error-info (ob-elixir--error-p output)))
(let ((type (plist-get error-info :type))
(message (plist-get error-info :message)))
(cond
((member type '("CompileError" "SyntaxError" "TokenMissingError"))
(signal 'ob-elixir-compile-error (list message)))
(t
(signal 'ob-elixir-runtime-error (list message)))))))
```
### Timeout Handling
```elisp
(defcustom ob-elixir-timeout 30
"Default timeout in seconds for Elixir evaluation."
:type 'integer
:group 'ob-elixir)
(defun ob-elixir--eval-with-timeout (code timeout)
"Evaluate CODE with TIMEOUT seconds limit."
(with-timeout (timeout
(error "Elixir evaluation timed out after %d seconds" timeout))
(ob-elixir--eval-script code)))
```
---
## elixir-mode Integration
### Syntax Highlighting
```elisp
;; Register with org-src for editing
(add-to-list 'org-src-lang-modes '("elixir" . elixir))
;; If elixir-mode isn't available, use a fallback
(unless (fboundp 'elixir-mode)
(add-to-list 'org-src-lang-modes '("elixir" . prog)))
```
### Using elixir-mode Functions
```elisp
(defun ob-elixir--format-code (code)
"Format CODE using mix format if available."
(when (and (executable-find "mix")
(> (length code) 0))
(with-temp-buffer
(insert code)
(let ((temp-file (make-temp-file "elixir-format" nil ".ex")))
(unwind-protect
(progn
(write-region (point-min) (point-max) temp-file)
(when (= 0 (call-process "mix" nil nil nil
"format" temp-file))
(erase-buffer)
(insert-file-contents temp-file)
(buffer-string)))
(delete-file temp-file))))))
```
---
## Performance Considerations
### Caching Compilation
```elisp
(defvar ob-elixir--module-cache (make-hash-table :test 'equal)
"Cache of compiled Elixir modules.")
(defun ob-elixir--get-cached-module (code)
"Get cached module for CODE, or compile and cache."
(let ((hash (md5 code)))
(or (gethash hash ob-elixir--module-cache)
(let ((module-name (format "ObElixir_%s" (substring hash 0 8))))
(ob-elixir--compile-module module-name code)
(puthash hash module-name ob-elixir--module-cache)
module-name))))
```
### Reusing Sessions
```elisp
(defcustom ob-elixir-reuse-sessions t
"Whether to reuse sessions between evaluations.
When non-nil, sessions persist until explicitly killed."
:type 'boolean
:group 'ob-elixir)
```
### Startup Time Optimization
```elisp
;; Pre-start a session on package load (optional)
(defun ob-elixir-warm-up ()
"Pre-start an IEx session for faster first evaluation."
(interactive)
(ob-elixir--start-iex-session "warmup"))
```
---
## References
- [Elixir Documentation](https://elixir-lang.org/docs.html)
- [IEx Documentation](https://hexdocs.pm/iex/IEx.html)
- [Mix Documentation](https://hexdocs.pm/mix/Mix.html)
- [Erlang Distribution Protocol](https://www.erlang.org/doc/reference_manual/distributed.html)
- [elixir-mode GitHub](https://github.com/elixir-editors/emacs-elixir)
- [inf-elixir for REPL](https://github.com/J3RN/inf-elixir)
- [Elixir LS (Language Server)](https://github.com/elixir-lsp/elixir-ls)

View File

@@ -0,0 +1,435 @@
# Existing Implementations Analysis
This document analyzes existing org-babel Elixir implementations and related packages, identifying gaps and improvement opportunities.
## Table of Contents
- [zweifisch/ob-elixir Analysis](#zweifischob-elixir-analysis)
- [Comparison with Other ob-* Packages](#comparison-with-other-ob--packages)
- [Feature Gap Analysis](#feature-gap-analysis)
- [Recommendations](#recommendations)
- [References](#references)
---
## zweifisch/ob-elixir Analysis
### Repository Information
- **URL**: https://github.com/zweifisch/ob-elixir
- **Stars**: 29 (as of 2024)
- **Last Updated**: Limited recent activity
- **License**: GPL-3.0
### Source Code Review
```elisp
;;; Current implementation (simplified)
(defvar ob-elixir-process-output "")
(defconst org-babel-header-args:elixir
'((cookie . :any)
(name . :any)
(remsh . :any)
(sname . :any))
"elixir header arguments")
(defvar ob-elixir-eoe "\u2029") ; Unicode paragraph separator
(defun org-babel-execute:elixir (body params)
(let ((session (cdr (assoc :session params)))
(tmp (org-babel-temp-file "elixir-")))
(ob-elixir-ensure-session session params)
(with-temp-file tmp (insert body))
(ob-elixir-eval session (format "import_file(\"%s\")" tmp))))
(defun ob-elixir-eval (session body)
(let ((result (ob-elixir-eval-in-repl session body)))
;; Heavy regex cleanup of IEx output
(replace-regexp-in-string
"^\\(import_file([^)]+)\\)+\n" ""
(replace-regexp-in-string
"\r" ""
(replace-regexp-in-string
"\n\\(\\(iex\\|[.]+\\)\\(([^@]+@[^)]+)[0-9]+\\|([0-9]+)\\)> \\)+" ""
(replace-regexp-in-string
"\e\\[[0-9;]*[A-Za-z]" ""
(replace-regexp-in-string
"\"\\\\u2029\"" ""
result)))))))
```
### Strengths
| Feature | Description |
|---------------------|--------------------------------------------------------|
| **Session support** | Uses IEx sessions for persistent state |
| **Remote shell** | Supports `--remsh` for connecting to running nodes |
| **Node naming** | Supports `--sname` and `--name` for distributed Erlang |
| **Cookie support** | Can specify Erlang cookie for authentication |
| **Simple design** | Minimal code, easy to understand |
### Weaknesses
| Issue | Description | Impact |
|----------------------------|-----------------------------------------------------|----------------------------|
| **Always uses sessions** | No one-shot execution mode | Slow for simple scripts |
| **No `:results value`** | Only captures output, not return values | Limited functionality |
| **No `:var` support** | Cannot pass variables to code blocks | Poor org-babel integration |
| **Fragile output parsing** | Multiple regex replacements to clean IEx output | Unreliable |
| **Uses `import_file`** | Relies on IEx-specific command | Tight coupling to IEx |
| **Global state** | Uses `ob-elixir-process-output` global variable | Not thread-safe |
| **No Mix support** | Cannot execute in Mix project context | Limited for real projects |
| **Hardcoded `sit-for`** | Uses fixed delays instead of proper synchronization | Timing issues |
| **No error handling** | Errors not properly detected or reported | Poor UX |
| **No tests** | No test suite | Quality concerns |
| **No `defcustom`** | Settings not customizable via Customize | Poor UX |
### Specific Issues in Code
1. **Timing-based synchronization**:
```elisp
(sit-for 0.5) ; Wait 500ms and hope IEx is ready
;; This is fragile - slow systems may fail
```
2. **No prompt detection**:
```elisp
(defun ob-elixir-wait ()
(while (not (string-match-p ob-elixir-eoe ob-elixir-process-output))
(sit-for 0.2)))
;; Busy-waiting instead of using process filter properly
```
3. **Heavy output cleanup**:
```elisp
;; 5 nested regex replacements - fragile and hard to debug
(replace-regexp-in-string "pattern1" ""
(replace-regexp-in-string "pattern2" ""
(replace-regexp-in-string "pattern3" ""
...)))
```
---
## Comparison with Other ob-* Packages
### ob-python (Standard Library)
**Features we should match:**
```elisp
;; Multiple execution modes
(defcustom org-babel-python-command "python3"
"Command to execute Python code.")
;; Proper result handling
(defun org-babel-execute:python (body params)
(let* ((session (org-babel-python-initiate-session
(cdr (assq :session params)) params))
(result-params (cdr (assq :result-params params)))
(result-type (cdr (assq :result-type params)))
(full-body (org-babel-expand-body:generic ...))
(result (org-babel-python-evaluate ...)))
;; Proper result formatting
(org-babel-reassemble-table
(org-babel-result-cond result-params
result
(org-babel-python-table-or-string result))
...)))
;; Variable injection
(defun org-babel-variable-assignments:python (params)
(mapcar (lambda (pair)
(format "%s=%s" (car pair)
(org-babel-python-var-to-python (cdr pair))))
(org-babel--get-vars params)))
;; Both session and non-session evaluation
(defun org-babel-python-evaluate (session body &optional result-type result-params)
(if session
(org-babel-python-evaluate-session ...)
(org-babel-python-evaluate-external-process ...)))
```
### ob-ruby (Standard Library)
**Patterns to adopt:**
```elisp
;; Clean wrapper for value capture
(defconst org-babel-ruby-wrapper-method
"
def main
%s
end
puts main.inspect
")
;; Proper session management with comint
(defun org-babel-ruby-initiate-session (&optional session params)
(unless (string= session "none")
(let ((session-buffer (or (get-buffer ...) ...)))
(if (org-babel-comint-buffer-livep session-buffer)
session-buffer
(org-babel-ruby-initiate-session session)))))
;; Type conversion
(defun org-babel-ruby-var-to-ruby (var)
(if (listp var)
(concat "[" (mapconcat #'org-babel-ruby-var-to-ruby var ", ") "]")
(format "%S" var)))
```
### ob-shell (Standard Library)
**Useful patterns:**
```elisp
;; Multiple shell types
(defcustom org-babel-shell-names
'("sh" "bash" "zsh" "fish" ...)
"Shells to register with org-babel.")
;; Header arg for shell selection
(defconst org-babel-header-args:shell
'((shebang . :any)))
;; Proper prep-session
(defun org-babel-prep-session:shell (session params)
(let* ((session (org-babel-sh-initiate-session session))
(var-lines (org-babel-variable-assignments:shell params)))
(org-babel-comint-in-buffer session
(mapc (lambda (var)
(insert var) (comint-send-input nil t)
(org-babel-comint-wait-for-output session))
var-lines))
session))
```
### Feature Comparison Matrix
| Feature | ob-python | ob-ruby | ob-shell | ob-elixir (zweifisch) |
|--------------------|-----------|---------|----------|-----------------------|
| One-shot execution | Yes | Yes | Yes | No |
| Session support | Yes | Yes | Yes | Yes |
| `:results value` | Yes | Yes | Yes | No |
| `:results output` | Yes | Yes | Yes | Yes (only) |
| Variable injection | Yes | Yes | Yes | No |
| Table output | Yes | Yes | Yes | No |
| Error handling | Yes | Yes | Yes | No |
| Customization | Yes | Yes | Yes | No |
| Tests | Yes | Yes | Yes | No |
| Graphics support | Yes | No | No | No |
| Async support | Yes | No | No | No |
---
## Feature Gap Analysis
### Critical Gaps (Must Fix)
1. **No `:results value` support**
- Cannot capture return value of expressions
- Users must use `IO.inspect` workarounds
- **Fix**: Implement wrapper method pattern
2. **No variable injection**
- Cannot use `:var` header argument
- Breaks org-babel's data passing paradigm
- **Fix**: Implement `org-babel-variable-assignments:elixir`
3. **No one-shot execution**
- Every evaluation starts/uses IEx session
- Slow for simple scripts
- **Fix**: Add external process execution mode
4. **Fragile output parsing**
- Multiple regex replacements prone to breaking
- **Fix**: Use markers or JSON encoding
### Important Gaps (Should Fix)
1. **No Mix project support**
- Cannot run code in project context
- No access to project dependencies
- **Fix**: Add `:mix-project` header argument
2. **No proper error handling**
- Errors appear as raw output
- No structured error information
- **Fix**: Detect and signal Elixir errors
3. **No customization**
- Hardcoded paths and settings
- **Fix**: Add `defcustom` for all configurable values
4. **No tests**
- No way to verify correctness
- **Fix**: Add comprehensive test suite
### Nice-to-Have Gaps (Could Fix)
1. **No async execution**
- Long-running code blocks UI
- Consider for future versions
2. **No graphics support**
- Elixir has visualization libraries (VegaLite)
- Could add in future
3. **No LSP integration**
- Could integrate with ElixirLS for completions
- Future enhancement
---
## Recommendations
### Approach: New Implementation
Given the significant gaps and architectural issues in zweifisch/ob-elixir, we recommend **creating a new implementation** rather than forking. Reasons:
1. Need to change fundamental architecture (session-only → dual mode)
2. Core data flow needs redesign
3. Limited active maintenance upstream
4. Cleaner API design possible with fresh start
### Proposed Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ org-babel-execute:elixir │
└─────────────────────────────────────────────────────────────────┘
┌──────────┴──────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Session Mode │ │ External Mode │
│ (IEx/comint) │ │ (elixir cmd) │
└─────────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ With Mix │ │ With Mix │
│ (iex -S mix) │ │ (mix run) │
└─────────────────┘ └─────────────────┘
```
### Implementation Phases
#### Phase 1: Core (MVP)
- [x] One-shot execution with `elixir` command
- [x] `:results value` and `:results output` support
- [x] Variable injection with `:var`
- [x] Basic error detection
- [x] Customizable commands
- [x] Test suite foundation
#### Phase 2: Sessions
- [ ] IEx session support via comint
- [ ] Session persistence
- [ ] Proper prompt detection
- [ ] Session cleanup
#### Phase 3: Mix Integration
- [ ] `:mix-project` header argument
- [ ] Automatic Mix project detection
- [ ] `iex -S mix` for sessions
- [ ] `mix run` for one-shot
#### Phase 4: Advanced Features
- [ ] Remote shell (remsh) support
- [ ] Table input/output
- [ ] Async execution
- [ ] Better error messages with line numbers
### Header Arguments to Support
```elisp
(defconst org-babel-header-args:elixir
'(;; Standard org-babel args work automatically
;; Language-specific:
(mix-project . :any) ; Path to mix.exs directory
(mix-env . :any) ; MIX_ENV (dev, test, prod)
(iex-args . :any) ; Extra IEx arguments
(timeout . :any) ; Execution timeout
(node-name . :any) ; --name for distributed
(node-sname . :any) ; --sname for distributed
(cookie . :any) ; Erlang cookie
(remsh . :any)) ; Remote shell node
"Elixir-specific header arguments.")
```
### Example Usage (Target)
```org
* Basic Evaluation
#+BEGIN_SRC elixir
Enum.map([1, 2, 3], &(&1 * 2))
#+END_SRC
#+RESULTS:
| 2 | 4 | 6 |
* With Variables
#+NAME: my-data
| a | 1 |
| b | 2 |
#+BEGIN_SRC elixir :var data=my-data
Enum.map(data, fn [k, v] -> {k, v * 10} end)
#+END_SRC
#+RESULTS:
| a | 10 |
| b | 20 |
* In Mix Project
#+BEGIN_SRC elixir :mix-project ~/my_app :mix-env test
MyApp.some_function()
#+END_SRC
* With Session
#+BEGIN_SRC elixir :session my-session
defmodule Helper do
def double(x), do: x * 2
end
#+END_SRC
#+BEGIN_SRC elixir :session my-session
Helper.double(21)
#+END_SRC
#+RESULTS:
: 42
* Capturing Output
#+BEGIN_SRC elixir :results output
IO.puts("Hello")
IO.puts("World")
#+END_SRC
#+RESULTS:
: Hello
: World
```
---
## References
- [zweifisch/ob-elixir](https://github.com/zweifisch/ob-elixir)
- [Org Mode ob-python.el](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/lisp/ob-python.el)
- [Org Mode ob-ruby.el](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/lisp/ob-ruby.el)
- [Org Mode ob-shell.el](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/lisp/ob-shell.el)
- [Org Mode ob-emacs-lisp.el](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/lisp/ob-emacs-lisp.el)
- [elixir-editors/emacs-elixir](https://github.com/elixir-editors/emacs-elixir)
- [Worg Babel Languages](https://orgmode.org/worg/org-contrib/babel/languages/)