docs and tasks
This commit is contained in:
445
docs/01-emacs-elisp-best-practices.md
Normal file
445
docs/01-emacs-elisp-best-practices.md
Normal 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)
|
||||
626
docs/02-testing-emacs-elisp.md
Normal file
626
docs/02-testing-emacs-elisp.md
Normal 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)
|
||||
747
docs/03-org-babel-implementation-guide.md
Normal file
747
docs/03-org-babel-implementation-guide.md
Normal 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)
|
||||
644
docs/04-elixir-integration-strategies.md
Normal file
644
docs/04-elixir-integration-strategies.md
Normal 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)
|
||||
435
docs/05-existing-implementations-analysis.md
Normal file
435
docs/05-existing-implementations-analysis.md
Normal 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/)
|
||||
Reference in New Issue
Block a user