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/)

144
tasks/00-index.md Normal file
View File

@@ -0,0 +1,144 @@
# ob-elixir Implementation Tasks
This directory contains step-by-step implementation tasks for building the ob-elixir package.
## Overview
The implementation is organized into 4 phases:
| Phase | Description | Tasks | Priority |
|-------|-------------|-------|----------|
| **Phase 1** | Core MVP | 01-06 | Critical |
| **Phase 2** | Sessions | 07 | High |
| **Phase 3** | Mix Integration | 08 | High |
| **Phase 4** | Advanced Features | 09-10 | Medium/Low |
## Task List
### Phase 1: Core (MVP)
These tasks implement the minimum viable product - basic Elixir code execution in org-mode.
| Task | Title | Time | Status |
|------|-------|------|--------|
| [01](01-project-setup.md) | Project Setup | 30 min | Pending |
| [02](02-basic-execution.md) | Basic Code Execution | 1-2 hrs | Pending |
| [03](03-variable-injection.md) | Variable Injection | 1-2 hrs | Pending |
| [04](04-error-handling.md) | Error Handling | 1 hr | Pending |
| [05](05-result-formatting.md) | Result Formatting | 1-2 hrs | Pending |
| [06](06-test-suite.md) | Comprehensive Test Suite | 2-3 hrs | Pending |
**Phase 1 Deliverables:**
- Execute Elixir code with `C-c C-c`
- `:results value` and `:results output` work
- `:var` header arguments work
- Errors are properly reported
- Lists become org tables
- Comprehensive test coverage
### Phase 2: Sessions
| Task | Title | Time | Status |
|------|-------|------|--------|
| [07](07-session-support.md) | IEx Session Support | 3-4 hrs | Pending |
**Phase 2 Deliverables:**
- `:session name` creates persistent IEx sessions
- Variables and modules persist across blocks
- Session cleanup commands
### Phase 3: Mix Integration
| Task | Title | Time | Status |
|------|-------|------|--------|
| [08](08-mix-project-support.md) | Mix Project Support | 2-3 hrs | Pending |
**Phase 3 Deliverables:**
- `:mix-project path` executes in project context
- Auto-detection of Mix projects
- `:mix-env` sets MIX_ENV
- Sessions with Mix (`iex -S mix`)
### Phase 4: Advanced Features
| Task | Title | Time | Status |
|------|-------|------|--------|
| [09](09-remote-shell.md) | Remote Shell (remsh) | 2-3 hrs | Pending |
| [10](10-async-execution.md) | Async Execution | 3-4 hrs | Pending |
**Phase 4 Deliverables:**
- `:remsh node@host` connects to running nodes
- `:async yes` for non-blocking execution
## Implementation Order
```
Phase 1 (Must complete in order)
├── 01-project-setup
├── 02-basic-execution
├── 03-variable-injection
├── 04-error-handling
├── 05-result-formatting
└── 06-test-suite
Phase 2 (After Phase 1)
└── 07-session-support
Phase 3 (After Phase 1, can parallel with Phase 2)
└── 08-mix-project-support
Phase 4 (After relevant dependencies)
├── 09-remote-shell (after 07)
└── 10-async-execution (after Phase 1)
```
## Time Estimates
| Phase | Estimated Time |
|-------|----------------|
| Phase 1 | 8-12 hours |
| Phase 2 | 3-4 hours |
| Phase 3 | 2-3 hours |
| Phase 4 | 5-7 hours |
| **Total** | **18-26 hours** |
## Getting Started
1. Start with [Task 01: Project Setup](01-project-setup.md)
2. Complete Phase 1 tasks in order
3. Phases 2-4 can be done based on your priorities
## Task Template
Each task file includes:
- **Objective**: What the task accomplishes
- **Prerequisites**: What must be done first
- **Steps**: Detailed implementation steps with code
- **Tests**: Test cases to verify the implementation
- **Acceptance Criteria**: Checklist of requirements
- **Troubleshooting**: Common issues and solutions
## Testing
Run tests after each task:
```bash
make test
```
For integration tests with org-mode:
```bash
make test-integration
```
## Documentation References
| Document | Content |
|----------|---------|
| [01-emacs-elisp-best-practices.md](../docs/01-emacs-elisp-best-practices.md) | Elisp conventions |
| [02-testing-emacs-elisp.md](../docs/02-testing-emacs-elisp.md) | Testing strategies |
| [03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md) | Org-babel internals |
| [04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) | Elixir execution |
| [05-existing-implementations-analysis.md](../docs/05-existing-implementations-analysis.md) | Prior art analysis |

216
tasks/01-project-setup.md Normal file
View File

@@ -0,0 +1,216 @@
# Task 01: Project Setup
**Phase**: 1 - Core (MVP)
**Priority**: Critical
**Estimated Time**: 30 minutes
**Dependencies**: None
## Objective
Set up the project structure with proper Emacs Lisp package conventions, including file headers, licensing, and build tooling.
## Prerequisites
- Emacs 27.1+ installed
- Elixir installed and in PATH
- Git repository initialized
## Steps
### Step 1: Create the main package file
Create `ob-elixir.el` with proper headers:
```elisp
;;; 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)
(provide 'ob-elixir)
;;; ob-elixir.el ends here
```
### Step 2: Create the Eldev file for build tooling
Create `Eldev` file:
```elisp
; -*- mode: emacs-lisp; lexical-binding: t; -*-
(eldev-use-package-archive 'gnu)
(eldev-use-package-archive 'melpa)
;; Test dependencies
(eldev-add-extra-dependencies 'test 'ert)
;; Use ERT for testing
(setf eldev-test-framework 'ert)
;; Lint configuration
(setf eldev-lint-default '(elisp package))
```
### Step 3: Create test directory structure
```bash
mkdir -p test
```
Create `test/test-ob-elixir.el`:
```elisp
;;; test-ob-elixir.el --- Tests for ob-elixir -*- lexical-binding: t; -*-
;;; Commentary:
;; Test suite for ob-elixir package.
;;; Code:
(require 'ert)
;; Add parent 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)
(ert-deftest ob-elixir-test-package-loads ()
"Test that the package loads successfully."
(should (featurep 'ob-elixir)))
(provide 'test-ob-elixir)
;;; test-ob-elixir.el ends here
```
### Step 4: Create Makefile
Create `Makefile`:
```makefile
EMACS ?= emacs
BATCH = $(EMACS) -Q -batch -L .
.PHONY: all compile test lint clean
all: compile test
compile:
$(BATCH) -f batch-byte-compile ob-elixir.el
test:
$(BATCH) -l ert -l test/test-ob-elixir.el \
-f ert-run-tests-batch-and-exit
lint:
$(BATCH) --eval "(require 'package)" \
--eval "(package-initialize)" \
--eval "(package-refresh-contents)" \
--eval "(package-install 'package-lint)" \
-l package-lint \
-f package-lint-batch-and-exit ob-elixir.el
clean:
rm -f *.elc test/*.elc
```
### Step 5: Create .gitignore
Create `.gitignore`:
```
# Byte-compiled files
*.elc
# Eldev
.eldev/
Eldev-local
# Package archives
/packages/
# Test artifacts
/test/tmp/
# Editor
*~
\#*\#
.#*
# OS
.DS_Store
```
### Step 6: Verify setup
Run the following commands to verify:
```bash
# Check Emacs version
emacs --version
# Check Elixir version
elixir --version
# Run tests
make test
# Compile
make compile
```
## Acceptance Criteria
- [ ] `ob-elixir.el` exists with proper headers
- [ ] Package loads without errors: `(require 'ob-elixir)`
- [ ] `make test` runs successfully
- [ ] `make compile` produces no warnings
- [ ] All files follow Emacs Lisp conventions
## Files Created
- `ob-elixir.el` - Main package file
- `Eldev` - Build tool configuration
- `Makefile` - Make targets
- `test/test-ob-elixir.el` - Test file
- `.gitignore` - Git ignore rules
## References
- [docs/01-emacs-elisp-best-practices.md](../docs/01-emacs-elisp-best-practices.md)
- [docs/02-testing-emacs-elisp.md](../docs/02-testing-emacs-elisp.md)

242
tasks/02-basic-execution.md Normal file
View File

@@ -0,0 +1,242 @@
# Task 02: Basic Code Execution
**Phase**: 1 - Core (MVP)
**Priority**: Critical
**Estimated Time**: 1-2 hours
**Dependencies**: Task 01 (Project Setup)
## Objective
Implement the core `org-babel-execute:elixir` function that can execute Elixir code blocks using external process (one-shot execution).
## Prerequisites
- Task 01 completed
- Elixir installed and accessible via `elixir` command
## Steps
### Step 1: Add customization group and variables
Add to `ob-elixir.el`:
```elisp
;;; 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)
```
### Step 2: Add default header arguments
```elisp
;;; Header Arguments
(defvar org-babel-default-header-args:elixir
'((:results . "value")
(:session . "none"))
"Default header arguments for Elixir code blocks.")
```
### Step 3: Register the language
```elisp
;;; 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)))
```
### Step 4: Implement the execute function
```elisp
;;; Execution
(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)))
(result (ob-elixir--execute 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))))))
```
### Step 5: Implement the internal execute function
```elisp
(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."
(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 (org-babel-eval
(format "%s %s"
ob-elixir-command
(org-babel-process-file-name tmp-file))
"")))
(string-trim result))))
```
### Step 6: Implement the value wrapper
```elisp
(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))
```
### Step 7: Add tests
Add to `test/test-ob-elixir.el`:
```elisp
(ert-deftest ob-elixir-test-elixir-available ()
"Test that Elixir is available."
(should (executable-find ob-elixir-command)))
(ert-deftest ob-elixir-test-simple-value ()
"Test simple value evaluation."
(skip-unless (executable-find ob-elixir-command))
(let ((result (ob-elixir--execute "1 + 1" 'value)))
(should (equal "2" result))))
(ert-deftest ob-elixir-test-simple-output ()
"Test simple output evaluation."
(skip-unless (executable-find ob-elixir-command))
(let ((result (ob-elixir--execute "IO.puts(\"hello\")" 'output)))
(should (equal "hello" result))))
(ert-deftest ob-elixir-test-multiline-value ()
"Test multiline code value evaluation."
(skip-unless (executable-find ob-elixir-command))
(let ((result (ob-elixir--execute "x = 10\ny = 20\nx + y" 'value)))
(should (equal "30" result))))
(ert-deftest ob-elixir-test-list-result ()
"Test list result."
(skip-unless (executable-find ob-elixir-command))
(let ((result (ob-elixir--execute "[1, 2, 3]" 'value)))
(should (equal "[1, 2, 3]" result))))
(ert-deftest ob-elixir-test-map-result ()
"Test map result."
(skip-unless (executable-find ob-elixir-command))
(let ((result (ob-elixir--execute "%{a: 1, b: 2}" 'value)))
(should (string-match-p "%{a: 1, b: 2}" result))))
```
### Step 8: Test in an org buffer
Create a test org file `test.org`:
```org
* Test ob-elixir
** Basic arithmetic (value)
#+BEGIN_SRC elixir
1 + 1
#+END_SRC
** Output test
#+BEGIN_SRC elixir :results output
IO.puts("Hello, World!")
#+END_SRC
** List manipulation
#+BEGIN_SRC elixir
Enum.map([1, 2, 3], fn x -> x * 2 end)
#+END_SRC
```
Press `C-c C-c` on each block to test.
## Acceptance Criteria
- [ ] `org-babel-execute:elixir` function exists
- [ ] Simple expressions evaluate correctly: `1 + 1` returns `2`
- [ ] `:results value` captures return value (default)
- [ ] `:results output` captures stdout
- [ ] Multiline code executes correctly
- [ ] Lists and maps are returned in Elixir format
- [ ] All tests pass: `make test`
## Troubleshooting
### "Cannot find elixir"
Ensure Elixir is in PATH:
```bash
which elixir
elixir --version
```
Or set the full path:
```elisp
(setq ob-elixir-command "/usr/local/bin/elixir")
```
### Results are truncated
The wrapper uses `limit: :infinity` to prevent truncation. If still truncated, check for very large outputs.
### ANSI codes in output
We'll handle this in a later task. For now, output should be clean with the current approach.
## Files Modified
- `ob-elixir.el` - Add execution functions
- `test/test-ob-elixir.el` - Add execution tests
## References
- [docs/03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md)
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md)

View File

@@ -0,0 +1,334 @@
# Task 03: Variable Injection
**Phase**: 1 - Core (MVP)
**Priority**: High
**Estimated Time**: 1-2 hours
**Dependencies**: Task 02 (Basic Execution)
## Objective
Implement variable injection so that `:var` header arguments work correctly, allowing data to be passed from org-mode into Elixir code blocks.
## Prerequisites
- Task 02 completed
- Basic execution working
## Background
Org-babel allows passing variables to code blocks:
```org
#+BEGIN_SRC elixir :var x=5 :var name="Alice"
"Hello, #{name}! x = #{x}"
#+END_SRC
```
We need to:
1. Convert Elisp values to Elixir syntax
2. Generate Elixir variable assignment statements
3. Prepend these to the code before execution
## Steps
### Step 1: Implement Elisp to Elixir conversion
Add to `ob-elixir.el`:
```elisp
;;; Type Conversion
(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))))
```
### Step 2: Implement string escaping
```elisp
(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))
```
### Step 3: Implement variable assignments function
```elisp
;;; Variable Handling
(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)))
(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))))
```
### Step 4: Update the execute function
Modify `org-babel-execute:elixir` to use variable assignments:
```elisp
(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))))))
```
### Step 5: Add tests for type conversion
Add to `test/test-ob-elixir.el`:
```elisp
;;; Type Conversion Tests
(ert-deftest ob-elixir-test-convert-nil ()
"Test nil conversion."
(should (equal "nil" (ob-elixir--elisp-to-elixir nil))))
(ert-deftest ob-elixir-test-convert-true ()
"Test t conversion."
(should (equal "true" (ob-elixir--elisp-to-elixir t))))
(ert-deftest ob-elixir-test-convert-integer ()
"Test integer conversion."
(should (equal "42" (ob-elixir--elisp-to-elixir 42)))
(should (equal "-10" (ob-elixir--elisp-to-elixir -10))))
(ert-deftest ob-elixir-test-convert-float ()
"Test float conversion."
(should (equal "3.14" (ob-elixir--elisp-to-elixir 3.14))))
(ert-deftest ob-elixir-test-convert-string ()
"Test string conversion."
(should (equal "\"hello\"" (ob-elixir--elisp-to-elixir "hello"))))
(ert-deftest ob-elixir-test-convert-string-escaping ()
"Test string escaping."
(should (equal "\"say \\\"hi\\\"\""
(ob-elixir--elisp-to-elixir "say \"hi\"")))
(should (equal "\"line1\\nline2\""
(ob-elixir--elisp-to-elixir "line1\nline2"))))
(ert-deftest ob-elixir-test-convert-symbol ()
"Test symbol conversion to atom."
(should (equal ":foo" (ob-elixir--elisp-to-elixir 'foo)))
(should (equal ":ok" (ob-elixir--elisp-to-elixir 'ok))))
(ert-deftest ob-elixir-test-convert-list ()
"Test list conversion."
(should (equal "[1, 2, 3]" (ob-elixir--elisp-to-elixir '(1 2 3))))
(should (equal "[\"a\", \"b\"]" (ob-elixir--elisp-to-elixir '("a" "b")))))
(ert-deftest ob-elixir-test-convert-nested-list ()
"Test nested list conversion."
(should (equal "[[1, 2], [3, 4]]"
(ob-elixir--elisp-to-elixir '((1 2) (3 4))))))
(ert-deftest ob-elixir-test-convert-vector ()
"Test vector to tuple conversion."
(should (equal "{1, 2, 3}" (ob-elixir--elisp-to-elixir [1 2 3]))))
```
### Step 6: Add tests for variable injection
```elisp
;;; Variable Injection Tests
(ert-deftest ob-elixir-test-variable-assignments ()
"Test variable assignment generation."
(let ((params '((:var . ("x" . 5))
(:var . ("name" . "Alice")))))
(let ((assignments (org-babel-variable-assignments:elixir params)))
(should (member "x = 5" assignments))
(should (member "name = \"Alice\"" assignments)))))
(ert-deftest ob-elixir-test-var-execution ()
"Test code execution with variables."
(skip-unless (executable-find ob-elixir-command))
(let* ((params '((:var . ("x" . 10))))
(var-lines (org-babel-variable-assignments:elixir params))
(full-body (concat (mapconcat #'identity var-lines "\n")
"\nx * 2")))
(should (equal "20" (ob-elixir--execute full-body 'value)))))
(ert-deftest ob-elixir-test-var-list ()
"Test passing list as variable."
(skip-unless (executable-find ob-elixir-command))
(let* ((params '((:var . ("data" . (1 2 3)))))
(var-lines (org-babel-variable-assignments:elixir params))
(full-body (concat (mapconcat #'identity var-lines "\n")
"\nEnum.sum(data)")))
(should (equal "6" (ob-elixir--execute full-body 'value)))))
```
### Step 7: Test in an org buffer
Update `test.org`:
```org
* Variable Injection Tests
** Simple variable
#+BEGIN_SRC elixir :var x=42
x * 2
#+END_SRC
#+RESULTS:
: 84
** String variable
#+BEGIN_SRC elixir :var name="World"
"Hello, #{name}!"
#+END_SRC
#+RESULTS:
: Hello, World!
** Multiple variables
#+BEGIN_SRC elixir :var x=10 :var y=20
x + y
#+END_SRC
#+RESULTS:
: 30
** List variable
#+BEGIN_SRC elixir :var numbers='(1 2 3 4 5)
Enum.sum(numbers)
#+END_SRC
#+RESULTS:
: 15
** Table as variable
#+NAME: my-data
| a | 1 |
| b | 2 |
| c | 3 |
#+BEGIN_SRC elixir :var data=my-data
Enum.map(data, fn [k, v] -> "#{k}=#{v}" end)
#+END_SRC
```
## Acceptance Criteria
- [ ] `ob-elixir--elisp-to-elixir` correctly converts all Elisp types
- [ ] `org-babel-variable-assignments:elixir` generates valid Elixir code
- [ ] `:var x=5` works in org blocks
- [ ] `:var name="string"` works with string values
- [ ] Multiple `:var` arguments work
- [ ] Lists and tables can be passed as variables
- [ ] All tests pass: `make test`
## Edge Cases to Consider
1. **Variable name conflicts**: Elixir variables must start with lowercase
2. **Special characters in strings**: Quotes, newlines, backslashes
3. **Empty lists**: Should produce `[]`
4. **Mixed type lists**: `[1, "two", :three]`
5. **hline in tables**: Special symbol for table separators
## Files Modified
- `ob-elixir.el` - Add variable handling functions
- `test/test-ob-elixir.el` - Add variable tests
## References
- [docs/03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md) - Variable Handling section
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Data Type Conversion section

296
tasks/04-error-handling.md Normal file
View File

@@ -0,0 +1,296 @@
# Task 04: Error Handling
**Phase**: 1 - Core (MVP)
**Priority**: High
**Estimated Time**: 1 hour
**Dependencies**: Task 02 (Basic Execution)
## Objective
Implement proper error detection and reporting for Elixir code execution, so users get meaningful feedback when their code fails.
## Prerequisites
- Task 02 completed
- Basic execution working
## Background
Currently, Elixir errors are returned as raw output. We need to:
1. Detect when Elixir reports an error
2. Extract useful error information
3. Present errors clearly to the user
4. Optionally signal Emacs error conditions
## Steps
### Step 1: Define error patterns
Add to `ob-elixir.el`:
```elisp
;;; 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.")
```
### Step 2: Define custom error types
```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)
```
### Step 3: Implement error detection
```elisp
(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)))
```
### Step 4: Implement error formatting
```elisp
(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)))
(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)
```
### Step 5: Update the execute function
Modify `ob-elixir--execute`:
```elisp
(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 (org-babel-eval
(format "%s %s"
ob-elixir-command
(org-babel-process-file-name tmp-file))
"")))
(ob-elixir--process-result result))))
(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)))
```
### Step 6: Handle warnings
```elisp
(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)
(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"))))
```
### Step 7: Add tests
Add to `test/test-ob-elixir.el`:
```elisp
;;; Error Handling Tests
(ert-deftest ob-elixir-test-detect-runtime-error ()
"Test runtime error detection."
(let ((output "** (RuntimeError) something went wrong"))
(let ((error-info (ob-elixir--detect-error output)))
(should error-info)
(should (eq 'runtime (plist-get error-info :type)))
(should (equal "RuntimeError" (plist-get error-info :error-type))))))
(ert-deftest ob-elixir-test-detect-compile-error ()
"Test compile error detection."
(let ((output "** (CompileError) test.exs:1: undefined function foo/0"))
(let ((error-info (ob-elixir--detect-error output)))
(should error-info)
(should (eq 'compile (plist-get error-info :type)))
(should (equal "CompileError" (plist-get error-info :error-type))))))
(ert-deftest ob-elixir-test-no-error ()
"Test that valid output is not detected as error."
(should-not (ob-elixir--detect-error "42"))
(should-not (ob-elixir--detect-error "[1, 2, 3]"))
(should-not (ob-elixir--detect-error "\"hello\"")))
(ert-deftest ob-elixir-test-error-execution ()
"Test that errors are properly handled during execution."
(skip-unless (executable-find ob-elixir-command))
(let ((ob-elixir-signal-errors nil))
(let ((result (ob-elixir--execute "raise \"test error\"" 'value)))
(should (string-match-p "RuntimeError" result)))))
(ert-deftest ob-elixir-test-error-signaling ()
"Test that errors are signaled when configured."
(skip-unless (executable-find ob-elixir-command))
(let ((ob-elixir-signal-errors t))
(should-error (ob-elixir--execute "raise \"test error\"" 'value)
:type 'ob-elixir-runtime-error)))
(ert-deftest ob-elixir-test-undefined-function ()
"Test handling of undefined function error."
(skip-unless (executable-find ob-elixir-command))
(let ((ob-elixir-signal-errors nil))
(let ((result (ob-elixir--execute "undefined_function()" 'value)))
(should (string-match-p "\\(UndefinedFunctionError\\|CompileError\\)" result)))))
```
### Step 8: Test in an org buffer
Add to `test.org`:
```org
* Error Handling Tests
** Runtime Error
#+BEGIN_SRC elixir
raise "This is a test error"
#+END_SRC
** Compile Error
#+BEGIN_SRC elixir
def incomplete_function(
#+END_SRC
** Undefined Function
#+BEGIN_SRC elixir
this_function_does_not_exist()
#+END_SRC
** Warning (should still execute)
#+BEGIN_SRC elixir
x = 1
y = 2
x # y is unused, may generate warning
#+END_SRC
```
## Acceptance Criteria
- [ ] Runtime errors are detected (e.g., `raise "error"`)
- [ ] Compile errors are detected (e.g., syntax errors)
- [ ] Errors are formatted with type and message
- [ ] `ob-elixir-signal-errors` controls error behavior
- [ ] Warnings are handled according to `ob-elixir-show-warnings`
- [ ] Valid output is not mistakenly detected as errors
- [ ] All tests pass: `make test`
## Error Types to Handle
| Error Type | Example | Detection |
|------------|---------|-----------|
| RuntimeError | `raise "msg"` | `** (RuntimeError)` |
| ArgumentError | Bad function arg | `** (ArgumentError)` |
| ArithmeticError | Division by zero | `** (ArithmeticError)` |
| CompileError | Syntax error | `** (CompileError)` |
| SyntaxError | Invalid syntax | `** (SyntaxError)` |
| TokenMissingError | Missing end | `** (TokenMissingError)` |
| UndefinedFunctionError | Unknown function | `** (UndefinedFunctionError)` |
## Files Modified
- `ob-elixir.el` - Add error handling functions
- `test/test-ob-elixir.el` - Add error handling tests
## References
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Error Handling section

View File

@@ -0,0 +1,330 @@
# Task 05: Result Formatting and Table Support
**Phase**: 1 - Core (MVP)
**Priority**: Medium
**Estimated Time**: 1-2 hours
**Dependencies**: Task 02 (Basic Execution), Task 03 (Variable Injection)
## Objective
Implement proper result formatting so Elixir lists become org tables and results are properly parsed back into Elisp data structures.
## Prerequisites
- Task 02 and 03 completed
- Basic execution and variables working
## Background
Org-babel can display results as:
- Scalar values (`:results scalar`)
- Tables (`:results table`)
- Raw org markup (`:results raw`)
- Verbatim (`:results verbatim`)
When Elixir returns a list like `[[1, 2], [3, 4]]`, org should display it as a table:
```
| 1 | 2 |
| 3 | 4 |
```
## Steps
### Step 1: Implement result parsing
Add to `ob-elixir.el`:
```elisp
;;; Result Formatting
(defun ob-elixir--table-or-string (result)
"Convert RESULT to Emacs table or string.
If RESULT looks like a list, parse it into an Elisp list.
Otherwise return as string.
Uses `org-babel-script-escape' for parsing."
(let ((trimmed (string-trim result)))
(cond
;; Empty result
((string-empty-p trimmed) nil)
;; Looks like a list - try to parse
((string-match-p "^\\[.*\\]$" trimmed)
(condition-case nil
(let ((parsed (org-babel-script-escape trimmed)))
(ob-elixir--sanitize-table parsed))
(error trimmed)))
;; Looks like a tuple - convert to list first
((string-match-p "^{.*}$" trimmed)
(condition-case nil
(let* ((as-list (replace-regexp-in-string
"^{\\(.*\\)}$" "[\\1]" trimmed))
(parsed (org-babel-script-escape as-list)))
(ob-elixir--sanitize-table parsed))
(error trimmed)))
;; Scalar value
(t trimmed))))
```
### Step 2: Implement table sanitization
```elisp
(defvar ob-elixir-nil-to 'hline
"Elisp value to use for Elixir nil in table cells.
When nil appears in an Elixir list that becomes a table,
it is replaced with this value. Use `hline' for org table
horizontal lines, or nil for empty cells.")
(defun ob-elixir--sanitize-table (data)
"Sanitize DATA for use as an org table.
Replaces nil values according to `ob-elixir-nil-to'.
Ensures consistent structure for table rendering."
(cond
;; Not a list - return as-is
((not (listp data)) data)
;; Empty list
((null data) nil)
;; List of lists - could be table
((and (listp (car data)) (not (null (car data))))
(mapcar #'ob-elixir--sanitize-row data))
;; Simple list - single row
(t (ob-elixir--sanitize-row data))))
(defun ob-elixir--sanitize-row (row)
"Sanitize a single ROW for table display."
(if (listp row)
(mapcar (lambda (cell)
(cond
((null cell) ob-elixir-nil-to)
((eq cell 'nil) ob-elixir-nil-to)
(t cell)))
row)
row))
```
### Step 3: Handle keyword lists and maps
```elisp
(defun ob-elixir--parse-keyword-list (str)
"Parse STR as Elixir keyword list into alist.
Handles format like: [a: 1, b: 2]"
(when (string-match "^\\[\\(.*\\)\\]$" str)
(let ((content (match-string 1 str)))
(when (string-match-p "^[a-z_]+:" content)
(let ((pairs '()))
(dolist (part (split-string content ", "))
(when (string-match "^\\([a-z_]+\\):\\s-*\\(.+\\)$" part)
(push (cons (intern (match-string 1 part))
(ob-elixir--parse-value (match-string 2 part)))
pairs)))
(nreverse pairs))))))
(defun ob-elixir--parse-value (str)
"Parse STR as a simple Elixir value."
(let ((trimmed (string-trim str)))
(cond
((string= trimmed "nil") nil)
((string= trimmed "true") t)
((string= trimmed "false") nil)
((string-match-p "^[0-9]+$" trimmed)
(string-to-number trimmed))
((string-match-p "^[0-9]+\\.[0-9]+$" trimmed)
(string-to-number trimmed))
((string-match-p "^\".*\"$" trimmed)
(substring trimmed 1 -1))
((string-match-p "^:.*$" trimmed)
(intern (substring trimmed 1)))
(t trimmed))))
```
### Step 4: Update the execute function
Ensure `org-babel-execute:elixir` uses the parsing:
```elisp
(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."
(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
;; For output/scalar/verbatim - return as-is
result
;; For value - parse into Elisp data
(ob-elixir--table-or-string 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))))))
```
### Step 5: Support column names
```elisp
(defun ob-elixir--maybe-add-colnames (result params)
"Add column names to RESULT if specified in PARAMS."
(let ((colnames (cdr (assq :colnames params))))
(if (and colnames (listp result) (listp (car result)))
(cons (if (listp colnames) colnames (car result))
(if (listp colnames) result (cdr result)))
result)))
```
### Step 6: Add tests
Add to `test/test-ob-elixir.el`:
```elisp
;;; Result Formatting Tests
(ert-deftest ob-elixir-test-parse-simple-list ()
"Test parsing simple list result."
(should (equal '(1 2 3) (ob-elixir--table-or-string "[1, 2, 3]"))))
(ert-deftest ob-elixir-test-parse-nested-list ()
"Test parsing nested list (table) result."
(should (equal '((1 2) (3 4))
(ob-elixir--table-or-string "[[1, 2], [3, 4]]"))))
(ert-deftest ob-elixir-test-parse-tuple ()
"Test parsing tuple result."
(should (equal '(1 2 3) (ob-elixir--table-or-string "{1, 2, 3}"))))
(ert-deftest ob-elixir-test-parse-scalar ()
"Test that scalars are returned as strings."
(should (equal "42" (ob-elixir--table-or-string "42")))
(should (equal ":ok" (ob-elixir--table-or-string ":ok"))))
(ert-deftest ob-elixir-test-parse-string ()
"Test parsing string result."
(should (equal "\"hello\"" (ob-elixir--table-or-string "\"hello\""))))
(ert-deftest ob-elixir-test-sanitize-table-nil ()
"Test that nil values are sanitized in tables."
(let ((ob-elixir-nil-to 'hline))
(should (equal '((1 hline) (hline 2))
(ob-elixir--sanitize-table '((1 nil) (nil 2)))))))
(ert-deftest ob-elixir-test-execution-returns-table ()
"Test that list results become tables."
(skip-unless (executable-find ob-elixir-command))
(let ((result (ob-elixir--table-or-string
(ob-elixir--execute "[[1, 2], [3, 4]]" 'value))))
(should (equal '((1 2) (3 4)) result))))
(ert-deftest ob-elixir-test-mixed-list ()
"Test parsing mixed-type list."
(skip-unless (executable-find ob-elixir-command))
(let ((result (ob-elixir--table-or-string
(ob-elixir--execute "[1, \"two\", :three]" 'value))))
(should (listp result))
(should (= 3 (length result)))))
```
### Step 7: Test in an org buffer
Add to `test.org`:
```org
* Result Formatting Tests
** Simple list as table row
#+BEGIN_SRC elixir
[1, 2, 3, 4, 5]
#+END_SRC
#+RESULTS:
| 1 | 2 | 3 | 4 | 5 |
** Nested list as table
#+BEGIN_SRC elixir
[
["Alice", 30],
["Bob", 25],
["Charlie", 35]
]
#+END_SRC
#+RESULTS:
| Alice | 30 |
| Bob | 25 |
| Charlie | 35 |
** Map result (verbatim)
#+BEGIN_SRC elixir :results verbatim
%{name: "Alice", age: 30}
#+END_SRC
#+RESULTS:
: %{age: 30, name: "Alice"}
** Enum operations returning lists
#+BEGIN_SRC elixir
Enum.map(1..5, fn x -> [x, x * x] end)
#+END_SRC
#+RESULTS:
| 1 | 1 |
| 2 | 4 |
| 3 | 9 |
| 4 | 16 |
| 5 | 25 |
** Tuple result
#+BEGIN_SRC elixir
{:ok, "success", 123}
#+END_SRC
```
## Acceptance Criteria
- [ ] Simple lists `[1, 2, 3]` become table rows
- [ ] Nested lists `[[1, 2], [3, 4]]` become tables
- [ ] Tuples are handled (converted to lists)
- [ ] Scalar values remain as strings
- [ ] `:results verbatim` bypasses table conversion
- [ ] nil values in tables are handled according to config
- [ ] All tests pass: `make test`
## Result Format Reference
| Elixir Value | Org Display |
|--------------|-------------|
| `[1, 2, 3]` | `\| 1 \| 2 \| 3 \|` |
| `[[1], [2]]` | Multi-row table |
| `{:ok, 1}` | `\| ok \| 1 \|` |
| `42` | `: 42` |
| `"hello"` | `: "hello"` |
| `%{a: 1}` | `: %{a: 1}` |
## Files Modified
- `ob-elixir.el` - Add result formatting functions
- `test/test-ob-elixir.el` - Add formatting tests
## References
- [docs/03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md) - Result Handling section
- [Org Manual - Results of Evaluation](https://orgmode.org/manual/Results-of-Evaluation.html)

623
tasks/06-test-suite.md Normal file
View File

@@ -0,0 +1,623 @@
# Task 06: Comprehensive Test Suite
**Phase**: 1 - Core (MVP)
**Priority**: High
**Estimated Time**: 2-3 hours
**Dependencies**: Tasks 01-05 (All Core Tasks)
## Objective
Create a comprehensive test suite that covers all implemented functionality, including unit tests, integration tests, and org-buffer tests.
## Prerequisites
- All Phase 1 tasks completed
- Basic functionality working
## Background
A good test suite should:
1. Test each function in isolation (unit tests)
2. Test the integration with org-mode (integration tests)
3. Be runnable in CI/CD (batch mode tests)
4. Provide good coverage of edge cases
## Steps
### Step 1: Organize test file structure
Create the test directory structure:
```
test/
├── test-ob-elixir.el # Main test file (loads all)
├── test-ob-elixir-core.el # Core execution tests
├── test-ob-elixir-vars.el # Variable handling tests
├── test-ob-elixir-results.el # Result formatting tests
├── test-ob-elixir-errors.el # Error handling tests
└── test-ob-elixir-org.el # Org integration tests
```
### Step 2: Create main test file
`test/test-ob-elixir.el`:
```elisp
;;; test-ob-elixir.el --- Tests for ob-elixir -*- lexical-binding: t; -*-
;;; Commentary:
;; Main test file that loads all test modules.
;; Run with: make test
;; Or: emacs -batch -l ert -l test/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))
(add-to-list 'load-path dir))
;; Load the package
(require 'ob-elixir)
;; Load test modules
(require 'test-ob-elixir-core)
(require 'test-ob-elixir-vars)
(require 'test-ob-elixir-results)
(require 'test-ob-elixir-errors)
(require 'test-ob-elixir-org)
;;; Test Helpers
(defvar ob-elixir-test--elixir-available
(executable-find ob-elixir-command)
"Non-nil if Elixir is available for testing.")
(defmacro ob-elixir-test-with-elixir (&rest body)
"Execute BODY only if Elixir is available."
`(if ob-elixir-test--elixir-available
(progn ,@body)
(ert-skip "Elixir not available")))
(defmacro ob-elixir-test-with-temp-org-buffer (&rest body)
"Execute BODY in a temporary org-mode buffer."
`(with-temp-buffer
(org-mode)
(ob-elixir--ensure-org-babel-loaded)
,@body))
(defun ob-elixir--ensure-org-babel-loaded ()
"Ensure org-babel is loaded with Elixir support."
(require 'org)
(require 'ob)
(org-babel-do-load-languages
'org-babel-load-languages
'((elixir . t))))
;;; Smoke Test
(ert-deftest ob-elixir-test-smoke ()
"Basic smoke test - package loads and Elixir is available."
(should (featurep 'ob-elixir))
(should (fboundp 'org-babel-execute:elixir))
(should (boundp 'org-babel-default-header-args:elixir)))
(provide 'test-ob-elixir)
;;; test-ob-elixir.el ends here
```
### Step 3: Create core execution tests
`test/test-ob-elixir-core.el`:
```elisp
;;; test-ob-elixir-core.el --- Core execution tests -*- lexical-binding: t; -*-
;;; Code:
(require 'ert)
(require 'ob-elixir)
;;; Command Tests
(ert-deftest ob-elixir-test-command-exists ()
"Test that the Elixir command is configured."
(should (stringp ob-elixir-command))
(should (not (string-empty-p ob-elixir-command))))
(ert-deftest ob-elixir-test-command-executable ()
"Test that the Elixir command is executable."
(skip-unless (executable-find ob-elixir-command))
(should (executable-find ob-elixir-command)))
;;; Basic Execution Tests
(ert-deftest ob-elixir-test-execute-simple-value ()
"Test simple value evaluation."
(skip-unless (executable-find ob-elixir-command))
(should (equal "2" (ob-elixir--execute "1 + 1" 'value))))
(ert-deftest ob-elixir-test-execute-simple-output ()
"Test simple output evaluation."
(skip-unless (executable-find ob-elixir-command))
(should (equal "hello" (ob-elixir--execute "IO.puts(\"hello\")" 'output))))
(ert-deftest ob-elixir-test-execute-multiline ()
"Test multiline code execution."
(skip-unless (executable-find ob-elixir-command))
(let ((code "x = 10\ny = 20\nx + y"))
(should (equal "30" (ob-elixir--execute code 'value)))))
(ert-deftest ob-elixir-test-execute-function-def ()
"Test function definition and call."
(skip-unless (executable-find ob-elixir-command))
(let ((code "
defmodule Test do
def double(x), do: x * 2
end
Test.double(21)"))
(should (equal "42" (ob-elixir--execute code 'value)))))
(ert-deftest ob-elixir-test-execute-enum ()
"Test Enum module usage."
(skip-unless (executable-find ob-elixir-command))
(should (equal "15"
(ob-elixir--execute "Enum.sum([1, 2, 3, 4, 5])" 'value))))
(ert-deftest ob-elixir-test-execute-pipe ()
"Test pipe operator."
(skip-unless (executable-find ob-elixir-command))
(let ((code "[1, 2, 3] |> Enum.map(&(&1 * 2)) |> Enum.sum()"))
(should (equal "12" (ob-elixir--execute code 'value)))))
;;; Data Type Tests
(ert-deftest ob-elixir-test-execute-list ()
"Test list result."
(skip-unless (executable-find ob-elixir-command))
(should (equal "[1, 2, 3]" (ob-elixir--execute "[1, 2, 3]" 'value))))
(ert-deftest ob-elixir-test-execute-tuple ()
"Test tuple result."
(skip-unless (executable-find ob-elixir-command))
(should (equal "{:ok, 42}" (ob-elixir--execute "{:ok, 42}" 'value))))
(ert-deftest ob-elixir-test-execute-map ()
"Test map result."
(skip-unless (executable-find ob-elixir-command))
(let ((result (ob-elixir--execute "%{a: 1, b: 2}" 'value)))
(should (string-match-p "%{" result))
(should (string-match-p "a:" result))
(should (string-match-p "b:" result))))
(ert-deftest ob-elixir-test-execute-string ()
"Test string result."
(skip-unless (executable-find ob-elixir-command))
(should (equal "\"hello world\""
(ob-elixir--execute "\"hello world\"" 'value))))
(ert-deftest ob-elixir-test-execute-atom ()
"Test atom result."
(skip-unless (executable-find ob-elixir-command))
(should (equal ":ok" (ob-elixir--execute ":ok" 'value))))
(ert-deftest ob-elixir-test-execute-boolean ()
"Test boolean result."
(skip-unless (executable-find ob-elixir-command))
(should (equal "true" (ob-elixir--execute "true" 'value)))
(should (equal "false" (ob-elixir--execute "false" 'value))))
(ert-deftest ob-elixir-test-execute-nil ()
"Test nil result."
(skip-unless (executable-find ob-elixir-command))
(should (equal "nil" (ob-elixir--execute "nil" 'value))))
;;; Wrapper Tests
(ert-deftest ob-elixir-test-wrap-for-value ()
"Test value wrapper generation."
(let ((wrapped (ob-elixir--wrap-for-value "1 + 1")))
(should (string-match-p "result = " wrapped))
(should (string-match-p "IO\\.puts" wrapped))
(should (string-match-p "inspect" wrapped))))
(provide 'test-ob-elixir-core)
;;; test-ob-elixir-core.el ends here
```
### Step 4: Create variable tests
`test/test-ob-elixir-vars.el`:
```elisp
;;; test-ob-elixir-vars.el --- Variable handling tests -*- lexical-binding: t; -*-
;;; Code:
(require 'ert)
(require 'ob-elixir)
;;; Type Conversion Tests
(ert-deftest ob-elixir-test-convert-nil ()
(should (equal "nil" (ob-elixir--elisp-to-elixir nil))))
(ert-deftest ob-elixir-test-convert-true ()
(should (equal "true" (ob-elixir--elisp-to-elixir t))))
(ert-deftest ob-elixir-test-convert-integer ()
(should (equal "42" (ob-elixir--elisp-to-elixir 42)))
(should (equal "-10" (ob-elixir--elisp-to-elixir -10)))
(should (equal "0" (ob-elixir--elisp-to-elixir 0))))
(ert-deftest ob-elixir-test-convert-float ()
(should (equal "3.14" (ob-elixir--elisp-to-elixir 3.14)))
(should (equal "-2.5" (ob-elixir--elisp-to-elixir -2.5))))
(ert-deftest ob-elixir-test-convert-string ()
(should (equal "\"hello\"" (ob-elixir--elisp-to-elixir "hello")))
(should (equal "\"\"" (ob-elixir--elisp-to-elixir ""))))
(ert-deftest ob-elixir-test-convert-string-escaping ()
(should (equal "\"say \\\"hi\\\"\""
(ob-elixir--elisp-to-elixir "say \"hi\"")))
(should (equal "\"line1\\nline2\""
(ob-elixir--elisp-to-elixir "line1\nline2")))
(should (equal "\"tab\\there\""
(ob-elixir--elisp-to-elixir "tab\there"))))
(ert-deftest ob-elixir-test-convert-symbol ()
(should (equal ":foo" (ob-elixir--elisp-to-elixir 'foo)))
(should (equal ":ok" (ob-elixir--elisp-to-elixir 'ok)))
(should (equal ":error" (ob-elixir--elisp-to-elixir 'error))))
(ert-deftest ob-elixir-test-convert-list ()
(should (equal "[1, 2, 3]" (ob-elixir--elisp-to-elixir '(1 2 3))))
(should (equal "[]" (ob-elixir--elisp-to-elixir '())))
(should (equal "[\"a\", \"b\"]" (ob-elixir--elisp-to-elixir '("a" "b")))))
(ert-deftest ob-elixir-test-convert-nested-list ()
(should (equal "[[1, 2], [3, 4]]"
(ob-elixir--elisp-to-elixir '((1 2) (3 4))))))
(ert-deftest ob-elixir-test-convert-vector ()
(should (equal "{1, 2, 3}" (ob-elixir--elisp-to-elixir [1 2 3]))))
(ert-deftest ob-elixir-test-convert-mixed ()
(should (equal "[1, \"two\", :three]"
(ob-elixir--elisp-to-elixir '(1 "two" three)))))
;;; Variable Assignment Tests
(ert-deftest ob-elixir-test-var-assignments-single ()
(let ((params '((:var . ("x" . 5)))))
(should (equal '("x = 5")
(org-babel-variable-assignments:elixir params)))))
(ert-deftest ob-elixir-test-var-assignments-multiple ()
(let ((params '((:var . ("x" . 5))
(:var . ("y" . 10)))))
(let ((assignments (org-babel-variable-assignments:elixir params)))
(should (= 2 (length assignments)))
(should (member "x = 5" assignments))
(should (member "y = 10" assignments)))))
(ert-deftest ob-elixir-test-var-assignments-string ()
(let ((params '((:var . ("name" . "Alice")))))
(should (equal '("name = \"Alice\"")
(org-babel-variable-assignments:elixir params)))))
(ert-deftest ob-elixir-test-var-assignments-list ()
(let ((params '((:var . ("data" . (1 2 3))))))
(should (equal '("data = [1, 2, 3]")
(org-babel-variable-assignments:elixir params)))))
;;; Execution with Variables
(ert-deftest ob-elixir-test-execute-with-var ()
(skip-unless (executable-find ob-elixir-command))
(let* ((params '((:var . ("x" . 10))))
(var-lines (org-babel-variable-assignments:elixir params))
(full-body (concat (mapconcat #'identity var-lines "\n")
"\nx * 2")))
(should (equal "20" (ob-elixir--execute full-body 'value)))))
(ert-deftest ob-elixir-test-execute-with-list-var ()
(skip-unless (executable-find ob-elixir-command))
(let* ((params '((:var . ("nums" . (1 2 3 4 5)))))
(var-lines (org-babel-variable-assignments:elixir params))
(full-body (concat (mapconcat #'identity var-lines "\n")
"\nEnum.sum(nums)")))
(should (equal "15" (ob-elixir--execute full-body 'value)))))
(ert-deftest ob-elixir-test-execute-with-string-var ()
(skip-unless (executable-find ob-elixir-command))
(let* ((params '((:var . ("name" . "World"))))
(var-lines (org-babel-variable-assignments:elixir params))
(full-body (concat (mapconcat #'identity var-lines "\n")
"\n\"Hello, #{name}!\"")))
(should (equal "\"Hello, World!\""
(ob-elixir--execute full-body 'value)))))
(provide 'test-ob-elixir-vars)
;;; test-ob-elixir-vars.el ends here
```
### Step 5: Create result formatting tests
`test/test-ob-elixir-results.el`:
```elisp
;;; test-ob-elixir-results.el --- Result formatting tests -*- lexical-binding: t; -*-
;;; Code:
(require 'ert)
(require 'ob-elixir)
;;; Parsing Tests
(ert-deftest ob-elixir-test-parse-simple-list ()
(should (equal '(1 2 3) (ob-elixir--table-or-string "[1, 2, 3]"))))
(ert-deftest ob-elixir-test-parse-nested-list ()
(should (equal '((1 2) (3 4))
(ob-elixir--table-or-string "[[1, 2], [3, 4]]"))))
(ert-deftest ob-elixir-test-parse-empty-list ()
(should (equal '() (ob-elixir--table-or-string "[]"))))
(ert-deftest ob-elixir-test-parse-tuple ()
(should (equal '(1 2 3) (ob-elixir--table-or-string "{1, 2, 3}"))))
(ert-deftest ob-elixir-test-parse-scalar-number ()
(should (equal "42" (ob-elixir--table-or-string "42"))))
(ert-deftest ob-elixir-test-parse-scalar-atom ()
(should (equal ":ok" (ob-elixir--table-or-string ":ok"))))
(ert-deftest ob-elixir-test-parse-scalar-string ()
(should (equal "\"hello\"" (ob-elixir--table-or-string "\"hello\""))))
(ert-deftest ob-elixir-test-parse-empty ()
(should (null (ob-elixir--table-or-string "")))
(should (null (ob-elixir--table-or-string " "))))
;;; Table Sanitization Tests
(ert-deftest ob-elixir-test-sanitize-nil-values ()
(let ((ob-elixir-nil-to 'hline))
(should (equal '((1 hline) (hline 2))
(ob-elixir--sanitize-table '((1 nil) (nil 2)))))))
(ert-deftest ob-elixir-test-sanitize-nested ()
(let ((ob-elixir-nil-to 'hline))
(should (equal '((1 2) (3 4))
(ob-elixir--sanitize-table '((1 2) (3 4)))))))
(ert-deftest ob-elixir-test-sanitize-simple ()
(should (equal '(1 2 3)
(ob-elixir--sanitize-table '(1 2 3)))))
;;; Integration Tests
(ert-deftest ob-elixir-test-full-result-list ()
(skip-unless (executable-find ob-elixir-command))
(let ((result (ob-elixir--table-or-string
(ob-elixir--execute "[1, 2, 3]" 'value))))
(should (equal '(1 2 3) result))))
(ert-deftest ob-elixir-test-full-result-table ()
(skip-unless (executable-find ob-elixir-command))
(let ((result (ob-elixir--table-or-string
(ob-elixir--execute "[[1, 2], [3, 4]]" 'value))))
(should (equal '((1 2) (3 4)) result))))
(provide 'test-ob-elixir-results)
;;; test-ob-elixir-results.el ends here
```
### Step 6: Create error handling tests
`test/test-ob-elixir-errors.el`:
```elisp
;;; test-ob-elixir-errors.el --- Error handling tests -*- lexical-binding: t; -*-
;;; Code:
(require 'ert)
(require 'ob-elixir)
;;; Error Detection Tests
(ert-deftest ob-elixir-test-detect-runtime-error ()
(let ((output "** (RuntimeError) something went wrong"))
(should (ob-elixir--detect-error output))))
(ert-deftest ob-elixir-test-detect-compile-error ()
(let ((output "** (CompileError) test.exs:1: undefined function"))
(should (ob-elixir--detect-error output))))
(ert-deftest ob-elixir-test-detect-no-error ()
(should-not (ob-elixir--detect-error "42"))
(should-not (ob-elixir--detect-error "[1, 2, 3]"))
(should-not (ob-elixir--detect-error ":ok")))
(ert-deftest ob-elixir-test-error-type-runtime ()
(let* ((output "** (RuntimeError) test error")
(info (ob-elixir--detect-error output)))
(should (eq 'runtime (plist-get info :type)))))
(ert-deftest ob-elixir-test-error-type-compile ()
(let* ((output "** (CompileError) syntax error")
(info (ob-elixir--detect-error output)))
(should (eq 'compile (plist-get info :type)))))
;;; Error Execution Tests
(ert-deftest ob-elixir-test-runtime-error-no-signal ()
(skip-unless (executable-find ob-elixir-command))
(let ((ob-elixir-signal-errors nil))
(let ((result (ob-elixir--execute "raise \"test\"" 'value)))
(should (string-match-p "RuntimeError" result)))))
(ert-deftest ob-elixir-test-runtime-error-signal ()
(skip-unless (executable-find ob-elixir-command))
(let ((ob-elixir-signal-errors t))
(should-error (ob-elixir--execute "raise \"test\"" 'value)
:type 'ob-elixir-runtime-error)))
(ert-deftest ob-elixir-test-compile-error ()
(skip-unless (executable-find ob-elixir-command))
(let ((ob-elixir-signal-errors nil))
(let ((result (ob-elixir--execute "def incomplete(" 'value)))
(should (string-match-p "\\(SyntaxError\\|TokenMissingError\\)" result)))))
(provide 'test-ob-elixir-errors)
;;; test-ob-elixir-errors.el ends here
```
### Step 7: Create org integration tests
`test/test-ob-elixir-org.el`:
```elisp
;;; test-ob-elixir-org.el --- Org integration tests -*- lexical-binding: t; -*-
;;; Code:
(require 'ert)
(require 'org)
(require 'ob)
(require 'ob-elixir)
;;; Helper Functions
(defun ob-elixir-test--execute-src-block (code &optional header-args)
"Execute CODE as an Elixir src block with HEADER-ARGS."
(with-temp-buffer
(org-mode)
(insert (format "#+BEGIN_SRC elixir%s\n%s\n#+END_SRC"
(if header-args (concat " " header-args) "")
code))
(goto-char (point-min))
(forward-line 1)
(org-babel-execute-src-block)))
;;; Basic Org Tests
(ert-deftest ob-elixir-test-org-simple ()
(skip-unless (executable-find ob-elixir-command))
(should (equal "2" (ob-elixir-test--execute-src-block "1 + 1"))))
(ert-deftest ob-elixir-test-org-with-var ()
(skip-unless (executable-find ob-elixir-command))
(should (equal "20" (ob-elixir-test--execute-src-block "x * 2" ":var x=10"))))
(ert-deftest ob-elixir-test-org-results-output ()
(skip-unless (executable-find ob-elixir-command))
(should (equal "hello"
(ob-elixir-test--execute-src-block
"IO.puts(\"hello\")"
":results output"))))
(ert-deftest ob-elixir-test-org-results-value ()
(skip-unless (executable-find ob-elixir-command))
(should (equal '(1 2 3)
(ob-elixir-test--execute-src-block
"[1, 2, 3]"
":results value"))))
(ert-deftest ob-elixir-test-org-results-verbatim ()
(skip-unless (executable-find ob-elixir-command))
(should (stringp (ob-elixir-test--execute-src-block
"[1, 2, 3]"
":results verbatim"))))
;;; Table Tests
(ert-deftest ob-elixir-test-org-table-result ()
(skip-unless (executable-find ob-elixir-command))
(let ((result (ob-elixir-test--execute-src-block "[[1, 2], [3, 4]]")))
(should (equal '((1 2) (3 4)) result))))
(provide 'test-ob-elixir-org)
;;; test-ob-elixir-org.el ends here
```
### Step 8: Update Makefile
```makefile
EMACS ?= emacs
BATCH = $(EMACS) -Q -batch -L . -L test
.PHONY: all compile test test-unit test-integration lint clean
all: compile test
compile:
$(BATCH) -f batch-byte-compile ob-elixir.el
test: test-unit
test-unit:
$(BATCH) -l ert \
-l test/test-ob-elixir.el \
-f ert-run-tests-batch-and-exit
test-integration:
$(BATCH) -l ert \
-l org -l ob \
-l test/test-ob-elixir.el \
-f ert-run-tests-batch-and-exit
lint:
$(BATCH) --eval "(require 'package)" \
--eval "(package-initialize)" \
--eval "(unless (package-installed-p 'package-lint) \
(package-refresh-contents) \
(package-install 'package-lint))" \
-l package-lint \
-f package-lint-batch-and-exit ob-elixir.el
clean:
rm -f *.elc test/*.elc
```
## Acceptance Criteria
- [ ] All test files created and organized
- [ ] `make test` runs all tests
- [ ] Tests cover core execution, variables, results, and errors
- [ ] Org integration tests work
- [ ] Tests can run in CI (batch mode)
- [ ] Test coverage is comprehensive (major code paths)
## Test Coverage Goals
| Component | Tests | Coverage |
|-----------|-------|----------|
| Type conversion | 12+ tests | All Elisp types |
| Execution | 10+ tests | Value/output, types |
| Variables | 8+ tests | All var scenarios |
| Results | 8+ tests | Parsing, tables |
| Errors | 6+ tests | Detection, signaling |
| Org integration | 6+ tests | Full workflow |
## Files Created
- `test/test-ob-elixir.el` - Main test file
- `test/test-ob-elixir-core.el` - Core tests
- `test/test-ob-elixir-vars.el` - Variable tests
- `test/test-ob-elixir-results.el` - Result tests
- `test/test-ob-elixir-errors.el` - Error tests
- `test/test-ob-elixir-org.el` - Org integration tests
## References
- [docs/02-testing-emacs-elisp.md](../docs/02-testing-emacs-elisp.md)
- [ERT Manual](https://www.gnu.org/software/emacs/manual/html_node/ert/)

442
tasks/07-session-support.md Normal file
View File

@@ -0,0 +1,442 @@
# Task 07: IEx Session Support
**Phase**: 2 - Sessions
**Priority**: High
**Estimated Time**: 3-4 hours
**Dependencies**: Phase 1 Complete
## Objective
Implement IEx session support so code blocks can share state when using `:session` header argument.
## Prerequisites
- Phase 1 complete
- Understanding of Emacs comint mode
- IEx (Elixir REPL) available
## Background
Sessions allow multiple code blocks to share state:
```org
#+BEGIN_SRC elixir :session my-session
x = 42
#+END_SRC
#+BEGIN_SRC elixir :session my-session
x * 2 # Can use x from previous block
#+END_SRC
```
This requires:
1. Starting an IEx process
2. Managing the process via comint
3. Sending code and capturing output
4. Proper prompt detection
5. Session cleanup
## Steps
### Step 1: Add session configuration
Add to `ob-elixir.el`:
```elisp
;;; Session Configuration
(defcustom ob-elixir-iex-command "iex"
"Command to start IEx session."
:type 'string
:group 'ob-elixir
:safe #'stringp)
(defconst ob-elixir--prompt-regexp
"^\\(?:iex\\|\\.\\.\\)([0-9]+)> "
"Regexp matching IEx prompt.
Matches both regular prompt 'iex(N)> ' and continuation '...(N)> '.")
(defconst ob-elixir--eoe-marker
"__ob_elixir_eoe_marker__"
"End-of-evaluation marker for session output.")
(defvar ob-elixir--sessions (make-hash-table :test 'equal)
"Hash table mapping session names to buffer names.")
```
### Step 2: Implement session initialization
```elisp
;;; Session Management
(require 'ob-comint)
(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 (or (not session) (string= session "none"))
(let* ((session-name (if (stringp session) session "default"))
(buffer (ob-elixir--get-or-create-session session-name params)))
(when buffer
(puthash session-name (buffer-name buffer) ob-elixir--sessions))
buffer)))
(defun ob-elixir--get-or-create-session (name params)
"Get or create an IEx session named NAME with PARAMS."
(let* ((buffer-name (format "*ob-elixir:%s*" name))
(existing (get-buffer buffer-name)))
(if (and existing (org-babel-comint-buffer-livep existing))
existing
(ob-elixir--start-session buffer-name name params))))
(defun ob-elixir--start-session (buffer-name session-name params)
"Start a new IEx session in BUFFER-NAME."
(let* ((buffer (get-buffer-create buffer-name))
(process-environment (cons "TERM=dumb" process-environment)))
(with-current-buffer buffer
;; Start the IEx process
(make-comint-in-buffer
(format "ob-elixir-%s" session-name)
buffer
ob-elixir-iex-command
nil)
;; Wait for initial prompt
(ob-elixir--wait-for-prompt buffer 10)
;; Configure IEx for programmatic use
(ob-elixir--configure-session buffer)
buffer)))
(defun ob-elixir--configure-session (buffer)
"Configure IEx session in BUFFER for programmatic use."
(let ((config-commands
'("IEx.configure(colors: [enabled: false])"
"IEx.configure(inspect: [limit: :infinity, printable_limit: :infinity])")))
(dolist (cmd config-commands)
(ob-elixir--send-command buffer cmd)
(ob-elixir--wait-for-prompt buffer 5))))
```
### Step 3: Implement prompt detection
```elisp
(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)))
(ob-elixir--at-prompt-p))))
(defun ob-elixir--at-prompt-p ()
"Return t if the last line in buffer looks like an IEx prompt."
(save-excursion
(goto-char (point-max))
(forward-line 0)
(looking-at ob-elixir--prompt-regexp)))
```
### Step 4: Implement command sending
```elisp
(defun ob-elixir--send-command (buffer command)
"Send COMMAND to IEx process in BUFFER."
(with-current-buffer buffer
(goto-char (point-max))
(insert command)
(comint-send-input nil t)))
(defun ob-elixir--evaluate-in-session (session body result-type)
"Evaluate BODY in SESSION, return result.
RESULT-TYPE is 'value or 'output."
(let* ((buffer (org-babel-elixir-initiate-session session nil))
(code (if (eq result-type 'value)
(ob-elixir--session-wrap-for-value body)
body))
(start-marker nil)
output)
(unless buffer
(error "Failed to create Elixir session: %s" session))
(with-current-buffer buffer
;; Mark position before output
(goto-char (point-max))
(setq start-marker (point-marker))
;; Send the code
(ob-elixir--send-command buffer code)
(ob-elixir--wait-for-prompt buffer 30)
;; Send EOE marker
(ob-elixir--send-command buffer
(format "\"%s\"" ob-elixir--eoe-marker))
(ob-elixir--wait-for-prompt buffer 5)
;; Extract output
(setq output (ob-elixir--extract-session-output
buffer start-marker)))
(ob-elixir--clean-session-output output)))
(defconst ob-elixir--session-value-wrapper
"_ob_result_ = (
%s
)
IO.puts(\"__ob_value_start__\")
IO.puts(inspect(_ob_result_, limit: :infinity, printable_limit: :infinity))
IO.puts(\"__ob_value_end__\")
:ok
"
"Wrapper for capturing value in session mode.")
(defun ob-elixir--session-wrap-for-value (body)
"Wrap BODY to capture its value in session mode."
(format ob-elixir--session-value-wrapper body))
```
### Step 5: Implement output extraction
```elisp
(defun ob-elixir--extract-session-output (buffer start-marker)
"Extract output from BUFFER since START-MARKER."
(with-current-buffer buffer
(let ((end-pos (point-max)))
(buffer-substring-no-properties start-marker end-pos))))
(defun ob-elixir--clean-session-output (output)
"Clean OUTPUT from IEx session."
(let ((result output))
;; Remove ANSI escape codes
(setq result (ansi-color-filter-apply result))
;; Remove prompts
(setq result (replace-regexp-in-string
ob-elixir--prompt-regexp "" result))
;; Remove the input echo
(setq result (replace-regexp-in-string
"^.*\n" "" result nil nil nil 1))
;; Remove EOE marker
(setq result (replace-regexp-in-string
(format "\"%s\"" ob-elixir--eoe-marker) "" result))
;; Extract value if using value wrapper
(when (string-match "__ob_value_start__\n\\(.*\\)\n__ob_value_end__" result)
(setq result (match-string 1 result)))
;; Remove :ok from wrapper
(setq result (replace-regexp-in-string ":ok\n*$" "" result))
(string-trim result)))
```
### Step 6: Update execute function
Modify `org-babel-execute:elixir`:
```elisp
(defun org-babel-execute:elixir (body params)
"Execute a block of Elixir code with org-babel."
(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)))
(result (if (and session (not (string= session "none")))
;; Session mode
(ob-elixir--evaluate-in-session session full-body result-type)
;; External process mode
(ob-elixir--execute full-body result-type))))
(org-babel-reassemble-table
(org-babel-result-cond result-params
result
(ob-elixir--table-or-string 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))))))
```
### Step 7: Implement prep-session
```elisp
(defun org-babel-prep-session:elixir (session params)
"Prepare SESSION according to PARAMS.
Sends variable assignments to the session."
(let ((buffer (org-babel-elixir-initiate-session session params))
(var-lines (org-babel-variable-assignments:elixir params)))
(when (and buffer var-lines)
(dolist (var-line var-lines)
(ob-elixir--send-command buffer var-line)
(ob-elixir--wait-for-prompt buffer 5)))
buffer))
```
### Step 8: Implement session cleanup
```elisp
(defun ob-elixir-kill-session (session)
"Kill the Elixir session named SESSION."
(interactive
(list (completing-read "Kill session: "
(hash-table-keys ob-elixir--sessions))))
(let ((buffer-name (gethash session ob-elixir--sessions)))
(when buffer-name
(let ((buffer (get-buffer buffer-name)))
(when buffer
(let ((process (get-buffer-process buffer)))
(when process
(delete-process process)))
(kill-buffer buffer)))
(remhash session ob-elixir--sessions))))
(defun ob-elixir-kill-all-sessions ()
"Kill all Elixir sessions."
(interactive)
(maphash (lambda (name _buffer)
(ob-elixir-kill-session name))
ob-elixir--sessions))
```
### Step 9: Add tests
Add to `test/test-ob-elixir-sessions.el`:
```elisp
;;; test-ob-elixir-sessions.el --- Session tests -*- lexical-binding: t; -*-
(require 'ert)
(require 'ob-elixir)
(ert-deftest ob-elixir-test-session-creation ()
"Test session creation."
(skip-unless (executable-find ob-elixir-iex-command))
(unwind-protect
(let ((buffer (org-babel-elixir-initiate-session "test-create" nil)))
(should buffer)
(should (org-babel-comint-buffer-livep buffer)))
(ob-elixir-kill-session "test-create")))
(ert-deftest ob-elixir-test-session-persistence ()
"Test that sessions persist state."
(skip-unless (executable-find ob-elixir-iex-command))
(unwind-protect
(progn
;; First evaluation - define variable
(ob-elixir--evaluate-in-session "test-persist" "x = 42" 'value)
;; Second evaluation - use variable
(let ((result (ob-elixir--evaluate-in-session
"test-persist" "x * 2" 'value)))
(should (equal "84" result))))
(ob-elixir-kill-session "test-persist")))
(ert-deftest ob-elixir-test-session-none ()
"Test that :session none uses external process."
(skip-unless (executable-find ob-elixir-command))
(should (null (org-babel-elixir-initiate-session "none" nil))))
(ert-deftest ob-elixir-test-session-module-def ()
"Test defining module in session."
(skip-unless (executable-find ob-elixir-iex-command))
(unwind-protect
(progn
(ob-elixir--evaluate-in-session
"test-module"
"defmodule TestMod do\n def double(x), do: x * 2\nend"
'value)
(let ((result (ob-elixir--evaluate-in-session
"test-module" "TestMod.double(21)" 'value)))
(should (equal "42" result))))
(ob-elixir-kill-session "test-module")))
(provide 'test-ob-elixir-sessions)
```
### Step 10: Test in org buffer
Create session tests in `test.org`:
```org
* Session Tests
** Define variable in session
#+BEGIN_SRC elixir :session my-session
x = 42
#+END_SRC
** Use variable from session
#+BEGIN_SRC elixir :session my-session
x * 2
#+END_SRC
#+RESULTS:
: 84
** Define module in session
#+BEGIN_SRC elixir :session my-session
defmodule Helper do
def greet(name), do: "Hello, #{name}!"
end
#+END_SRC
** Use module from session
#+BEGIN_SRC elixir :session my-session
Helper.greet("World")
#+END_SRC
#+RESULTS:
: "Hello, World!"
```
## Acceptance Criteria
- [ ] `:session name` creates persistent IEx session
- [ ] Variables persist across blocks in same session
- [ ] Module definitions persist
- [ ] `:session none` uses external process (default)
- [ ] Multiple named sessions work independently
- [ ] Sessions can be killed with `ob-elixir-kill-session`
- [ ] Proper prompt detection
- [ ] Output is clean (no prompts, ANSI codes)
- [ ] All tests pass
## Troubleshooting
### Session hangs
Check for proper prompt detection. IEx prompts can vary.
### ANSI codes in output
The `ansi-color-filter-apply` should remove them. Check TERM environment variable.
### Process dies unexpectedly
Check for Elixir errors. May need to handle compilation errors in session context.
## Files Modified
- `ob-elixir.el` - Add session support
- `test/test-ob-elixir-sessions.el` - Add session tests
## References
- [docs/03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md) - Session Management
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - IEx Session Management
- [Emacs Comint Mode](https://www.gnu.org/software/emacs/manual/html_node/emacs/Shell-Mode.html)

View File

@@ -0,0 +1,421 @@
# Task 08: Mix Project Support
**Phase**: 3 - Mix Integration
**Priority**: High
**Estimated Time**: 2-3 hours
**Dependencies**: Task 07 (Session Support) or Phase 1 complete
## Objective
Implement Mix project support so Elixir code can be executed within the context of a Mix project, with access to project dependencies and modules.
## Prerequisites
- Phase 1 complete (or Phase 2 for session+mix)
- Understanding of Mix build tool
- A test Mix project
## Background
Mix projects have:
- Dependencies in `mix.exs`
- Compiled modules in `_build/`
- Configuration in `config/`
To execute code in project context, we need:
1. Run code from the project directory
2. Use `mix run` for one-shot execution
3. Use `iex -S mix` for sessions
## Steps
### Step 1: Add Mix configuration
Add to `ob-elixir.el`:
```elisp
;;; Mix Configuration
(defcustom ob-elixir-mix-command "mix"
"Command to run Mix."
:type 'string
:group 'ob-elixir
:safe #'stringp)
(defcustom ob-elixir-auto-detect-mix t
"Whether to automatically detect Mix projects.
When non-nil and no :mix-project is specified, ob-elixir will
search upward from the org file for a mix.exs file."
:type 'boolean
:group 'ob-elixir)
(defconst org-babel-header-args:elixir
'((mix-project . :any) ; Path to Mix project root
(mix-env . :any) ; MIX_ENV (dev, test, prod)
(mix-target . :any)) ; MIX_TARGET for Nerves, etc.
"Elixir-specific header arguments.")
```
### Step 2: Implement Mix project detection
```elisp
;;; Mix Project Detection
(defun ob-elixir--find-mix-project (&optional start-dir)
"Find Mix project root by searching for mix.exs.
Starts from START-DIR (default: current directory) and searches
upward. Returns the directory containing mix.exs, or nil."
(let* ((dir (or start-dir default-directory))
(found (locate-dominating-file dir "mix.exs")))
(when found
(file-name-directory found))))
(defun ob-elixir--resolve-mix-project (params)
"Resolve Mix project path from PARAMS or auto-detection.
Returns project path or nil."
(let ((explicit (cdr (assq :mix-project params))))
(cond
;; Explicit project path
((and explicit (not (eq explicit 'no)))
(expand-file-name explicit))
;; Explicitly disabled
((eq explicit 'no)
nil)
;; Auto-detect if enabled
(ob-elixir-auto-detect-mix
(ob-elixir--find-mix-project))
;; No project
(t nil))))
(defun ob-elixir--in-mix-project-p (params)
"Return t if execution should happen in Mix project context."
(not (null (ob-elixir--resolve-mix-project params))))
```
### Step 3: Implement Mix execution
```elisp
;;; Mix Execution
(defun ob-elixir--execute-with-mix (body result-type params)
"Execute BODY in Mix project context.
RESULT-TYPE is 'value or 'output.
PARAMS contains header arguments including :mix-project."
(let* ((project-dir (ob-elixir--resolve-mix-project params))
(mix-env (cdr (assq :mix-env params)))
(mix-target (cdr (assq :mix-target params)))
(default-directory project-dir)
(tmp-file (org-babel-temp-file "ob-elixir-mix-" ".exs"))
(code (if (eq result-type 'value)
(ob-elixir--wrap-for-value body)
body))
(env-vars (ob-elixir--build-mix-env mix-env mix-target)))
;; Write code to temp file
(with-temp-file tmp-file
(insert code))
;; Execute with mix run
(let ((command (format "%s%s run %s"
env-vars
ob-elixir-mix-command
(org-babel-process-file-name tmp-file))))
(ob-elixir--process-result
(shell-command-to-string command)))))
(defun ob-elixir--build-mix-env (mix-env mix-target)
"Build environment variable prefix for Mix execution."
(let ((vars '()))
(when mix-env
(push (format "MIX_ENV=%s" mix-env) vars))
(when mix-target
(push (format "MIX_TARGET=%s" mix-target) vars))
(if vars
(concat (mapconcat #'identity vars " ") " ")
"")))
```
### Step 4: Update execute function
Modify `org-babel-execute:elixir`:
```elisp
(defun org-babel-execute:elixir (body params)
"Execute a block of Elixir code with org-babel."
(let* ((session (cdr (assq :session params)))
(result-type (cdr (assq :result-type params)))
(result-params (cdr (assq :result-params params)))
(mix-project (ob-elixir--resolve-mix-project params))
(full-body (org-babel-expand-body:generic
body params
(org-babel-variable-assignments:elixir params)))
(result (cond
;; Session mode
((and session (not (string= session "none")))
(ob-elixir--evaluate-in-session
session full-body result-type params))
;; Mix project mode
(mix-project
(ob-elixir--execute-with-mix
full-body result-type params))
;; Plain execution
(t
(ob-elixir--execute full-body result-type)))))
(org-babel-reassemble-table
(org-babel-result-cond result-params
result
(ob-elixir--table-or-string 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))))))
```
### Step 5: Update session for Mix projects
Modify session creation to support Mix:
```elisp
(defun ob-elixir--start-session (buffer-name session-name params)
"Start a new IEx session in BUFFER-NAME."
(let* ((mix-project (ob-elixir--resolve-mix-project params))
(mix-env (cdr (assq :mix-env params)))
(buffer (get-buffer-create buffer-name))
(default-directory (or mix-project default-directory))
(process-environment
(append
(list "TERM=dumb")
(when mix-env (list (format "MIX_ENV=%s" mix-env)))
process-environment)))
(with-current-buffer buffer
(if mix-project
;; Start with mix
(make-comint-in-buffer
(format "ob-elixir-%s" session-name)
buffer
ob-elixir-iex-command
nil
"-S" "mix")
;; Start plain IEx
(make-comint-in-buffer
(format "ob-elixir-%s" session-name)
buffer
ob-elixir-iex-command
nil))
;; Wait for prompt
(ob-elixir--wait-for-prompt buffer 30)
;; Configure IEx
(ob-elixir--configure-session buffer)
buffer)))
```
### Step 6: Add compilation support
```elisp
(defcustom ob-elixir-compile-before-run nil
"Whether to run mix compile before execution.
When non-nil, ensures project is compiled before running code.
This adds overhead but catches compilation errors early."
:type 'boolean
:group 'ob-elixir)
(defun ob-elixir--ensure-compiled (project-dir)
"Ensure Mix project at PROJECT-DIR is compiled."
(let ((default-directory project-dir))
(shell-command-to-string
(format "%s compile --force-check" ob-elixir-mix-command))))
(defun ob-elixir--execute-with-mix (body result-type params)
"Execute BODY in Mix project context."
(let* ((project-dir (ob-elixir--resolve-mix-project params))
(default-directory project-dir))
;; Optionally compile first
(when ob-elixir-compile-before-run
(ob-elixir--ensure-compiled project-dir))
;; ... rest of execution
))
```
### Step 7: Add tests
Create `test/test-ob-elixir-mix.el`:
```elisp
;;; test-ob-elixir-mix.el --- Mix project tests -*- lexical-binding: t; -*-
(require 'ert)
(require 'ob-elixir)
(defvar ob-elixir-test--mix-project-dir nil
"Temporary Mix project directory for testing.")
(defun ob-elixir-test--setup-mix-project ()
"Create a temporary Mix project for testing."
(let ((dir (make-temp-file "ob-elixir-test-" t)))
(setq ob-elixir-test--mix-project-dir dir)
(let ((default-directory dir))
;; Create mix.exs
(with-temp-file (expand-file-name "mix.exs" dir)
(insert "defmodule TestProject.MixProject do
use Mix.Project
def project do
[app: :test_project, version: \"0.1.0\", elixir: \"~> 1.14\"]
end
end"))
;; Create lib directory and module
(make-directory (expand-file-name "lib" dir))
(with-temp-file (expand-file-name "lib/test_project.ex" dir)
(insert "defmodule TestProject do
def hello, do: \"Hello from TestProject!\"
def add(a, b), do: a + b
end")))
dir))
(defun ob-elixir-test--cleanup-mix-project ()
"Clean up temporary Mix project."
(when ob-elixir-test--mix-project-dir
(delete-directory ob-elixir-test--mix-project-dir t)
(setq ob-elixir-test--mix-project-dir nil)))
;;; Tests
(ert-deftest ob-elixir-test-find-mix-project ()
"Test Mix project detection."
(skip-unless (executable-find ob-elixir-mix-command))
(unwind-protect
(let* ((project-dir (ob-elixir-test--setup-mix-project))
(sub-dir (expand-file-name "lib" project-dir))
(default-directory sub-dir))
(should (equal project-dir
(ob-elixir--find-mix-project))))
(ob-elixir-test--cleanup-mix-project)))
(ert-deftest ob-elixir-test-mix-project-execution ()
"Test code execution in Mix project context."
(skip-unless (and (executable-find ob-elixir-mix-command)
(executable-find ob-elixir-command)))
(unwind-protect
(let* ((project-dir (ob-elixir-test--setup-mix-project))
(params `((:mix-project . ,project-dir))))
;; Compile first
(let ((default-directory project-dir))
(shell-command-to-string "mix compile"))
;; Test execution
(let ((result (ob-elixir--execute-with-mix
"TestProject.hello()" 'value params)))
(should (string-match-p "Hello from TestProject" result))))
(ob-elixir-test--cleanup-mix-project)))
(ert-deftest ob-elixir-test-mix-env ()
"Test MIX_ENV handling."
(skip-unless (executable-find ob-elixir-mix-command))
(let ((env-str (ob-elixir--build-mix-env "test" nil)))
(should (string-match-p "MIX_ENV=test" env-str))))
(ert-deftest ob-elixir-test-explicit-no-mix ()
"Test disabling Mix with :mix-project no."
(let ((params '((:mix-project . no))))
(should (null (ob-elixir--resolve-mix-project params)))))
(provide 'test-ob-elixir-mix)
```
### Step 8: Test in org buffer
Create Mix tests in `test.org`:
```org
* Mix Project Tests
** Using project module (explicit path)
#+BEGIN_SRC elixir :mix-project ~/my_elixir_project
MyApp.hello()
#+END_SRC
** Using project module (auto-detect)
When this org file is inside a Mix project:
#+BEGIN_SRC elixir
MyApp.some_function()
#+END_SRC
** With specific MIX_ENV
#+BEGIN_SRC elixir :mix-project ~/my_elixir_project :mix-env test
Application.get_env(:my_app, :some_config)
#+END_SRC
** Session with Mix
#+BEGIN_SRC elixir :session mix-session :mix-project ~/my_elixir_project
# Has access to project modules
alias MyApp.SomeModule
#+END_SRC
** Disable auto-detect
#+BEGIN_SRC elixir :mix-project no
# Plain Elixir, no project context
1 + 1
#+END_SRC
```
## Acceptance Criteria
- [ ] `:mix-project path` executes in specified project
- [ ] Auto-detection finds `mix.exs` in parent directories
- [ ] `:mix-project no` disables auto-detection
- [ ] `:mix-env` sets MIX_ENV correctly
- [ ] Project modules are accessible
- [ ] Sessions with `:mix-project` use `iex -S mix`
- [ ] Compilation errors are reported properly
- [ ] All tests pass
## Header Arguments Reference
| Argument | Values | Description |
|----------|--------|-------------|
| `:mix-project` | path, `no` | Project path or disable |
| `:mix-env` | dev, test, prod | MIX_ENV value |
| `:mix-target` | host, target | MIX_TARGET for Nerves |
## Troubleshooting
### Module not found
Ensure project is compiled:
```bash
cd /path/to/project && mix compile
```
### Dependencies not available
Check that `mix deps.get` has been run.
### Wrong MIX_ENV
Explicitly set `:mix-env` header argument.
## Files Modified
- `ob-elixir.el` - Add Mix support
- `test/test-ob-elixir-mix.el` - Add Mix tests
## References
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Mix Project Context
- [Mix Documentation](https://hexdocs.pm/mix/Mix.html)

401
tasks/09-remote-shell.md Normal file
View File

@@ -0,0 +1,401 @@
# Task 09: Remote Shell (remsh) Support
**Phase**: 4 - Advanced Features
**Priority**: Medium
**Estimated Time**: 2-3 hours
**Dependencies**: Task 07 (Session Support)
## Objective
Implement remote shell support to connect to running Elixir/Erlang nodes and execute code against them.
## Prerequisites
- Session support implemented (Task 07)
- Understanding of Erlang distribution
- Running Elixir node for testing
## Background
Elixir/Erlang nodes can connect to each other for distributed computing. The `--remsh` flag allows IEx to connect to a running node:
```bash
iex --sname console --remsh my_app@hostname --cookie secret
```
This is useful for:
- Inspecting production systems
- Running code against a live application
- Debugging distributed systems
## Steps
### Step 1: Add remote shell configuration
Add to `ob-elixir.el`:
```elisp
;;; Remote Shell Configuration
(defconst org-babel-header-args:elixir
'((mix-project . :any)
(mix-env . :any)
(remsh . :any) ; Remote node to connect to
(node-name . :any) ; --name for local node
(node-sname . :any) ; --sname for local node
(cookie . :any)) ; Erlang cookie
"Elixir-specific header arguments.")
(defcustom ob-elixir-default-cookie nil
"Default Erlang cookie for remote connections.
Set this if all your nodes use the same cookie.
Can be overridden with :cookie header argument."
:type '(choice (const nil) string)
:group 'ob-elixir)
```
### Step 2: Implement remote session creation
```elisp
;;; Remote Shell Sessions
(defun ob-elixir--start-remote-session (buffer-name session-name params)
"Start a remote shell session in BUFFER-NAME.
Connects to the node specified in PARAMS."
(let* ((remsh (cdr (assq :remsh params)))
(node-name (cdr (assq :node-name params)))
(node-sname (cdr (assq :node-sname params)))
(cookie (or (cdr (assq :cookie params))
ob-elixir-default-cookie))
(buffer (get-buffer-create buffer-name))
(local-name (or node-sname
node-name
(format "ob_elixir_%d" (random 99999))))
(process-environment (cons "TERM=dumb" process-environment)))
(unless remsh
(error "No remote node specified. Use :remsh header argument"))
(with-current-buffer buffer
;; Build command arguments
(let ((args (append
;; Local node name
(if node-name
(list "--name" node-name)
(list "--sname" local-name))
;; Cookie
(when cookie
(list "--cookie" cookie))
;; Remote shell
(list "--remsh" remsh))))
(apply #'make-comint-in-buffer
(format "ob-elixir-remsh-%s" session-name)
buffer
ob-elixir-iex-command
nil
args))
;; Wait for connection
(unless (ob-elixir--wait-for-prompt buffer 30)
(error "Failed to connect to remote node: %s" remsh))
;; Configure session
(ob-elixir--configure-session buffer)
buffer)))
(defun ob-elixir--is-remote-session-p (params)
"Return t if PARAMS specify a remote shell connection."
(not (null (cdr (assq :remsh params)))))
```
### Step 3: Update session creation dispatcher
Modify `ob-elixir--start-session`:
```elisp
(defun ob-elixir--start-session (buffer-name session-name params)
"Start a new IEx session in BUFFER-NAME."
(cond
;; Remote shell
((ob-elixir--is-remote-session-p params)
(ob-elixir--start-remote-session buffer-name session-name params))
;; Mix project
((ob-elixir--resolve-mix-project params)
(ob-elixir--start-mix-session buffer-name session-name params))
;; Plain IEx
(t
(ob-elixir--start-plain-session buffer-name session-name params))))
(defun ob-elixir--start-plain-session (buffer-name session-name params)
"Start a plain IEx session in BUFFER-NAME."
(let* ((buffer (get-buffer-create buffer-name))
(process-environment (cons "TERM=dumb" process-environment)))
(with-current-buffer buffer
(make-comint-in-buffer
(format "ob-elixir-%s" session-name)
buffer
ob-elixir-iex-command
nil)
(ob-elixir--wait-for-prompt buffer 10)
(ob-elixir--configure-session buffer)
buffer)))
(defun ob-elixir--start-mix-session (buffer-name session-name params)
"Start an IEx session with Mix in BUFFER-NAME."
(let* ((mix-project (ob-elixir--resolve-mix-project params))
(mix-env (cdr (assq :mix-env params)))
(buffer (get-buffer-create buffer-name))
(default-directory mix-project)
(process-environment
(append
(list "TERM=dumb")
(when mix-env (list (format "MIX_ENV=%s" mix-env)))
process-environment)))
(with-current-buffer buffer
(make-comint-in-buffer
(format "ob-elixir-%s" session-name)
buffer
ob-elixir-iex-command
nil
"-S" "mix")
(ob-elixir--wait-for-prompt buffer 30)
(ob-elixir--configure-session buffer)
buffer)))
```
### Step 4: Add connection verification
```elisp
(defun ob-elixir--verify-remote-connection (buffer remsh)
"Verify that BUFFER is connected to remote node REMSH."
(with-current-buffer buffer
(let ((result (ob-elixir--send-and-receive buffer "Node.self()")))
(when (string-match-p (regexp-quote remsh) result)
t))))
(defun ob-elixir--send-and-receive (buffer command)
"Send COMMAND to BUFFER and return the response."
(let ((start-pos nil)
(result nil))
(with-current-buffer buffer
(goto-char (point-max))
(setq start-pos (point))
(ob-elixir--send-command buffer command)
(ob-elixir--wait-for-prompt buffer 10)
(setq result (buffer-substring-no-properties start-pos (point))))
(ob-elixir--clean-session-output result)))
```
### Step 5: Add safety checks
```elisp
(defcustom ob-elixir-remsh-confirm t
"Whether to confirm before connecting to remote nodes.
When non-nil, ask for confirmation before connecting.
This is a safety measure for production systems."
:type 'boolean
:group 'ob-elixir)
(defun ob-elixir--confirm-remsh (node)
"Confirm remote shell connection to NODE."
(or (not ob-elixir-remsh-confirm)
(yes-or-no-p
(format "Connect to remote Elixir node '%s'? " node))))
(defun ob-elixir--start-remote-session (buffer-name session-name params)
"Start a remote shell session in BUFFER-NAME."
(let ((remsh (cdr (assq :remsh params))))
(unless (ob-elixir--confirm-remsh remsh)
(user-error "Remote shell connection cancelled"))
;; ... rest of implementation
))
```
### Step 6: Add helper commands
```elisp
(defun ob-elixir-connect-to-node (node &optional cookie)
"Interactively connect to a remote Elixir NODE.
Optional COOKIE specifies the Erlang cookie."
(interactive
(list (read-string "Remote node: ")
(when current-prefix-arg
(read-string "Cookie: "))))
(let ((params `((:remsh . ,node)
,@(when cookie `((:cookie . ,cookie))))))
(org-babel-elixir-initiate-session "remote" params)))
(defun ob-elixir-list-sessions ()
"List all active ob-elixir sessions."
(interactive)
(if (= 0 (hash-table-count ob-elixir--sessions))
(message "No active sessions")
(with-output-to-temp-buffer "*ob-elixir sessions*"
(princ "Active ob-elixir sessions:\n\n")
(maphash (lambda (name buffer-name)
(let ((buffer (get-buffer buffer-name)))
(princ (format " %s -> %s (%s)\n"
name
buffer-name
(if (and buffer
(get-buffer-process buffer))
"running"
"dead")))))
ob-elixir--sessions))))
```
### Step 7: Add tests
Create `test/test-ob-elixir-remsh.el`:
```elisp
;;; test-ob-elixir-remsh.el --- Remote shell tests -*- lexical-binding: t; -*-
(require 'ert)
(require 'ob-elixir)
;; Note: These tests require a running Elixir node
;; Start one with: iex --sname testnode --cookie testcookie
(defvar ob-elixir-test-remote-node "testnode@localhost"
"Remote node for testing. Set to your test node.")
(defvar ob-elixir-test-remote-cookie "testcookie"
"Cookie for test node.")
(ert-deftest ob-elixir-test-is-remote-session ()
"Test remote session detection."
(should (ob-elixir--is-remote-session-p
'((:remsh . "node@host"))))
(should-not (ob-elixir--is-remote-session-p
'((:session . "test")))))
(ert-deftest ob-elixir-test-remote-session-creation ()
"Test remote session creation."
(skip-unless (executable-find ob-elixir-iex-command))
(skip-unless (getenv "OB_ELIXIR_TEST_REMSH")) ; Only run if explicitly enabled
(let ((ob-elixir-remsh-confirm nil))
(unwind-protect
(let* ((params `((:remsh . ,ob-elixir-test-remote-node)
(:cookie . ,ob-elixir-test-remote-cookie)))
(buffer (org-babel-elixir-initiate-session "test-remote" params)))
(should buffer)
(should (org-babel-comint-buffer-livep buffer)))
(ob-elixir-kill-session "test-remote"))))
(ert-deftest ob-elixir-test-remote-execution ()
"Test code execution on remote node."
(skip-unless (executable-find ob-elixir-iex-command))
(skip-unless (getenv "OB_ELIXIR_TEST_REMSH"))
(let ((ob-elixir-remsh-confirm nil))
(unwind-protect
(let* ((params `((:remsh . ,ob-elixir-test-remote-node)
(:cookie . ,ob-elixir-test-remote-cookie)))
(result (ob-elixir--evaluate-in-session
"test-remote-exec"
"Node.self() |> to_string()"
'value
params)))
(should (string-match-p ob-elixir-test-remote-node result)))
(ob-elixir-kill-session "test-remote-exec"))))
(provide 'test-ob-elixir-remsh)
```
### Step 8: Document usage
Add to documentation:
```org
* Remote Shell Usage
** Connecting to a running node
#+BEGIN_SRC elixir :session remote :remsh myapp@localhost :cookie secret
Node.self()
#+END_SRC
** With explicit node name
#+BEGIN_SRC elixir :session remote :remsh myapp@localhost :node-sname console :cookie secret
# Local node will be named 'console'
Node.list()
#+END_SRC
** With distributed name
#+BEGIN_SRC elixir :session remote :remsh myapp@myhost.example.com :node-name console@myhost.example.com :cookie secret
# Use full node names for cross-machine connections
Application.get_env(:my_app, :some_setting)
#+END_SRC
```
## Acceptance Criteria
- [ ] `:remsh node@host` connects to remote node
- [ ] `:cookie` sets the Erlang cookie
- [ ] `:node-sname` and `:node-name` set local node name
- [ ] Connection failures produce clear error messages
- [ ] Safety confirmation before connecting (configurable)
- [ ] `ob-elixir-connect-to-node` interactive command works
- [ ] Remote session appears in `ob-elixir-list-sessions`
- [ ] All tests pass
## Header Arguments Reference
| Argument | Values | Description |
|----------|--------|-------------|
| `:remsh` | node@host | Remote node to connect to |
| `:cookie` | string | Erlang cookie for authentication |
| `:node-sname` | name | Short name for local node |
| `:node-name` | name@host | Full name for local node |
## Security Considerations
1. **Cookies**: Never commit cookies to version control
2. **Confirmation**: Enable `ob-elixir-remsh-confirm` for production
3. **Network**: Ensure proper firewall rules for Erlang distribution port (4369 + dynamic)
4. **Scope**: Consider what code could be executed on production systems
## Troubleshooting
### Cannot connect to node
1. Verify node is running: `epmd -names`
2. Check cookie matches
3. Verify network connectivity on port 4369
4. Check that node allows remote connections
### Connection times out
Increase timeout or check for network issues:
```elisp
(setq ob-elixir-session-timeout 60)
```
### Node not found
Ensure using correct node name format:
- Short names: `node@hostname` (same machine or subnet)
- Long names: `node@hostname.domain.com` (cross-network)
## Files Modified
- `ob-elixir.el` - Add remote shell support
- `test/test-ob-elixir-remsh.el` - Add remote shell tests
## References
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Remote Shell section
- [Erlang Distribution](https://www.erlang.org/doc/reference_manual/distributed.html)
- [IEx Remote Shell](https://hexdocs.pm/iex/IEx.html#module-remote-shells)

358
tasks/10-async-execution.md Normal file
View File

@@ -0,0 +1,358 @@
# Task 10: Async Execution Support
**Phase**: 4 - Advanced Features
**Priority**: Low
**Estimated Time**: 3-4 hours
**Dependencies**: Phase 1 Complete
## Objective
Implement asynchronous execution so long-running Elixir code blocks don't freeze Emacs.
## Prerequisites
- Phase 1 complete
- Understanding of Emacs async processes
## Background
Some Elixir operations can take a long time:
- Database migrations
- Large data processing
- Network operations
- Build tasks
Async execution allows:
- Continue editing while code runs
- Visual indicator of running blocks
- Cancel long-running operations
## Steps
### Step 1: Add async configuration
Add to `ob-elixir.el`:
```elisp
;;; Async Configuration
(defcustom ob-elixir-async-timeout 300
"Timeout in seconds for async execution.
After this time, async execution will be cancelled."
:type 'integer
:group 'ob-elixir)
(defvar ob-elixir--async-processes (make-hash-table :test 'equal)
"Hash table mapping buffer positions to async processes.")
(defconst org-babel-header-args:elixir
'((mix-project . :any)
(mix-env . :any)
(remsh . :any)
(node-name . :any)
(node-sname . :any)
(cookie . :any)
(async . ((yes no)))) ; NEW: async execution
"Elixir-specific header arguments.")
```
### Step 2: Implement async execution
```elisp
;;; Async Execution
(defun ob-elixir--execute-async (body result-type callback)
"Execute BODY asynchronously.
RESULT-TYPE is 'value or 'output.
CALLBACK is called with the result when complete."
(let* ((tmp-file (org-babel-temp-file "ob-elixir-async-" ".exs"))
(code (if (eq result-type 'value)
(ob-elixir--wrap-for-value body)
body))
(output-buffer (generate-new-buffer " *ob-elixir-async*"))
process)
;; Write code to temp file
(with-temp-file tmp-file
(insert code))
;; Start async process
(setq process
(start-process
"ob-elixir-async"
output-buffer
ob-elixir-command
tmp-file))
;; Set up process sentinel
(set-process-sentinel
process
(lambda (proc event)
(when (memq (process-status proc) '(exit signal))
(let ((result (with-current-buffer (process-buffer proc)
(buffer-string))))
;; Clean up
(kill-buffer (process-buffer proc))
(delete-file tmp-file)
;; Call callback with result
(funcall callback (ob-elixir--process-result
(string-trim result)))))))
;; Set up timeout
(run-at-time ob-elixir-async-timeout nil
(lambda ()
(when (process-live-p process)
(kill-process process)
(funcall callback "Error: Async execution timed out"))))
process))
```
### Step 3: Integrate with org-babel
```elisp
(defun ob-elixir--async-p (params)
"Return t if PARAMS specify async execution."
(string= "yes" (cdr (assq :async params))))
(defun org-babel-execute:elixir (body params)
"Execute a block of Elixir code with org-babel."
(let* ((session (cdr (assq :session params)))
(result-type (cdr (assq :result-type params)))
(result-params (cdr (assq :result-params params)))
(async (ob-elixir--async-p params))
(full-body (org-babel-expand-body:generic
body params
(org-babel-variable-assignments:elixir params))))
(if async
;; Async execution
(ob-elixir--execute-async-block full-body result-type params)
;; Sync execution (existing code)
(let ((result (ob-elixir--execute-sync full-body result-type params)))
(org-babel-reassemble-table
(org-babel-result-cond result-params
result
(ob-elixir--table-or-string 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-sync (body result-type params)
"Execute BODY synchronously."
(cond
((and (cdr (assq :session params))
(not (string= (cdr (assq :session params)) "none")))
(ob-elixir--evaluate-in-session
(cdr (assq :session params)) body result-type params))
((ob-elixir--resolve-mix-project params)
(ob-elixir--execute-with-mix body result-type params))
(t
(ob-elixir--execute body result-type))))
```
### Step 4: Implement async block handling
```elisp
(defun ob-elixir--execute-async-block (body result-type params)
"Execute BODY asynchronously and insert results when done."
(let ((buffer (current-buffer))
(point (point))
(result-params (cdr (assq :result-params params)))
(marker (copy-marker (point))))
;; Show placeholder
(ob-elixir--insert-async-placeholder marker)
;; Execute async
(ob-elixir--execute-async
body
result-type
(lambda (result)
(ob-elixir--insert-async-result
buffer marker result result-params params)))
;; Return placeholder message
"Executing asynchronously..."))
(defun ob-elixir--insert-async-placeholder (marker)
"Insert a placeholder at MARKER indicating async execution."
(save-excursion
(goto-char marker)
(end-of-line)
(insert "\n")
(insert "#+RESULTS:\n")
(insert ": [Executing...]\n")))
(defun ob-elixir--insert-async-result (buffer marker result result-params params)
"Insert RESULT at MARKER in BUFFER."
(when (buffer-live-p buffer)
(with-current-buffer buffer
(save-excursion
(goto-char marker)
;; Find and remove placeholder
(when (search-forward ": [Executing...]" nil t)
(beginning-of-line)
(let ((start (point)))
(forward-line 1)
(delete-region start (point))))
;; Insert real result
(let ((formatted (org-babel-result-cond result-params
result
(ob-elixir--table-or-string result))))
(org-babel-insert-result formatted result-params))))))
```
### Step 5: Add cancellation support
```elisp
(defun ob-elixir-cancel-async ()
"Cancel the async execution at point."
(interactive)
(let* ((pos (point))
(process (gethash pos ob-elixir--async-processes)))
(if (and process (process-live-p process))
(progn
(kill-process process)
(remhash pos ob-elixir--async-processes)
(message "Async execution cancelled"))
(message "No async execution at point"))))
(defun ob-elixir-cancel-all-async ()
"Cancel all running async executions."
(interactive)
(maphash (lambda (_pos process)
(when (process-live-p process)
(kill-process process)))
ob-elixir--async-processes)
(clrhash ob-elixir--async-processes)
(message "All async executions cancelled"))
```
### Step 6: Add visual indicators
```elisp
(defface ob-elixir-async-running
'((t :background "yellow" :foreground "black"))
"Face for source blocks with running async execution."
:group 'ob-elixir)
(defun ob-elixir--highlight-async-block (start end)
"Highlight the region from START to END as running."
(let ((overlay (make-overlay start end)))
(overlay-put overlay 'face 'ob-elixir-async-running)
(overlay-put overlay 'ob-elixir-async t)
overlay))
(defun ob-elixir--remove-async-highlight ()
"Remove async highlighting from current block."
(dolist (ov (overlays-in (point-min) (point-max)))
(when (overlay-get ov 'ob-elixir-async)
(delete-overlay ov))))
```
### Step 7: Add tests
Create `test/test-ob-elixir-async.el`:
```elisp
;;; test-ob-elixir-async.el --- Async execution tests -*- lexical-binding: t; -*-
(require 'ert)
(require 'ob-elixir)
(ert-deftest ob-elixir-test-async-detection ()
"Test async header argument detection."
(should (ob-elixir--async-p '((:async . "yes"))))
(should-not (ob-elixir--async-p '((:async . "no"))))
(should-not (ob-elixir--async-p '())))
(ert-deftest ob-elixir-test-async-execution ()
"Test async execution completion."
(skip-unless (executable-find ob-elixir-command))
(let ((result nil)
(done nil))
(ob-elixir--execute-async
"1 + 1"
'value
(lambda (r)
(setq result r)
(setq done t)))
;; Wait for completion
(with-timeout (10 (error "Async test timed out"))
(while (not done)
(accept-process-output nil 0.1)))
(should (equal "2" result))))
(ert-deftest ob-elixir-test-async-timeout ()
"Test async timeout handling."
(skip-unless (executable-find ob-elixir-command))
(let ((ob-elixir-async-timeout 1)
(result nil)
(done nil))
(ob-elixir--execute-async
":timer.sleep(5000)" ; Sleep for 5 seconds
'value
(lambda (r)
(setq result r)
(setq done t)))
;; Wait for timeout
(with-timeout (3 (error "Test timed out"))
(while (not done)
(accept-process-output nil 0.1)))
(should (string-match-p "timed out" result))))
(provide 'test-ob-elixir-async)
```
### Step 8: Document usage
Add to documentation:
```org
* Async Execution
** Long-running computation
#+BEGIN_SRC elixir :async yes
# This won't block Emacs
Enum.reduce(1..1000000, 0, &+/2)
#+END_SRC
** Async with Mix project
#+BEGIN_SRC elixir :async yes :mix-project ~/my_app
MyApp.expensive_operation()
#+END_SRC
** Cancel with M-x ob-elixir-cancel-async
```
## Acceptance Criteria
- [ ] `:async yes` executes code asynchronously
- [ ] Placeholder shown while executing
- [ ] Results inserted when complete
- [ ] Timeout handled gracefully
- [ ] `ob-elixir-cancel-async` cancels execution
- [ ] Visual indicator for running blocks
- [ ] All tests pass
## Limitations
- Sessions cannot be async (they're inherently stateful)
- Multiple async blocks may have ordering issues
- Async results may not integrate perfectly with noweb
## Files Modified
- `ob-elixir.el` - Add async support
- `test/test-ob-elixir-async.el` - Add async tests
## References
- [Emacs Async Processes](https://www.gnu.org/software/emacs/manual/html_node/elisp/Asynchronous-Processes.html)
- [Process Sentinels](https://www.gnu.org/software/emacs/manual/html_node/elisp/Sentinels.html)