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/)
|
||||||
144
tasks/00-index.md
Normal file
144
tasks/00-index.md
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# ob-elixir Implementation Tasks
|
||||||
|
|
||||||
|
This directory contains step-by-step implementation tasks for building the ob-elixir package.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The implementation is organized into 4 phases:
|
||||||
|
|
||||||
|
| Phase | Description | Tasks | Priority |
|
||||||
|
|-------|-------------|-------|----------|
|
||||||
|
| **Phase 1** | Core MVP | 01-06 | Critical |
|
||||||
|
| **Phase 2** | Sessions | 07 | High |
|
||||||
|
| **Phase 3** | Mix Integration | 08 | High |
|
||||||
|
| **Phase 4** | Advanced Features | 09-10 | Medium/Low |
|
||||||
|
|
||||||
|
## Task List
|
||||||
|
|
||||||
|
### Phase 1: Core (MVP)
|
||||||
|
|
||||||
|
These tasks implement the minimum viable product - basic Elixir code execution in org-mode.
|
||||||
|
|
||||||
|
| Task | Title | Time | Status |
|
||||||
|
|------|-------|------|--------|
|
||||||
|
| [01](01-project-setup.md) | Project Setup | 30 min | Pending |
|
||||||
|
| [02](02-basic-execution.md) | Basic Code Execution | 1-2 hrs | Pending |
|
||||||
|
| [03](03-variable-injection.md) | Variable Injection | 1-2 hrs | Pending |
|
||||||
|
| [04](04-error-handling.md) | Error Handling | 1 hr | Pending |
|
||||||
|
| [05](05-result-formatting.md) | Result Formatting | 1-2 hrs | Pending |
|
||||||
|
| [06](06-test-suite.md) | Comprehensive Test Suite | 2-3 hrs | Pending |
|
||||||
|
|
||||||
|
**Phase 1 Deliverables:**
|
||||||
|
- Execute Elixir code with `C-c C-c`
|
||||||
|
- `:results value` and `:results output` work
|
||||||
|
- `:var` header arguments work
|
||||||
|
- Errors are properly reported
|
||||||
|
- Lists become org tables
|
||||||
|
- Comprehensive test coverage
|
||||||
|
|
||||||
|
### Phase 2: Sessions
|
||||||
|
|
||||||
|
| Task | Title | Time | Status |
|
||||||
|
|------|-------|------|--------|
|
||||||
|
| [07](07-session-support.md) | IEx Session Support | 3-4 hrs | Pending |
|
||||||
|
|
||||||
|
**Phase 2 Deliverables:**
|
||||||
|
- `:session name` creates persistent IEx sessions
|
||||||
|
- Variables and modules persist across blocks
|
||||||
|
- Session cleanup commands
|
||||||
|
|
||||||
|
### Phase 3: Mix Integration
|
||||||
|
|
||||||
|
| Task | Title | Time | Status |
|
||||||
|
|------|-------|------|--------|
|
||||||
|
| [08](08-mix-project-support.md) | Mix Project Support | 2-3 hrs | Pending |
|
||||||
|
|
||||||
|
**Phase 3 Deliverables:**
|
||||||
|
- `:mix-project path` executes in project context
|
||||||
|
- Auto-detection of Mix projects
|
||||||
|
- `:mix-env` sets MIX_ENV
|
||||||
|
- Sessions with Mix (`iex -S mix`)
|
||||||
|
|
||||||
|
### Phase 4: Advanced Features
|
||||||
|
|
||||||
|
| Task | Title | Time | Status |
|
||||||
|
|------|-------|------|--------|
|
||||||
|
| [09](09-remote-shell.md) | Remote Shell (remsh) | 2-3 hrs | Pending |
|
||||||
|
| [10](10-async-execution.md) | Async Execution | 3-4 hrs | Pending |
|
||||||
|
|
||||||
|
**Phase 4 Deliverables:**
|
||||||
|
- `:remsh node@host` connects to running nodes
|
||||||
|
- `:async yes` for non-blocking execution
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 1 (Must complete in order)
|
||||||
|
├── 01-project-setup
|
||||||
|
├── 02-basic-execution
|
||||||
|
├── 03-variable-injection
|
||||||
|
├── 04-error-handling
|
||||||
|
├── 05-result-formatting
|
||||||
|
└── 06-test-suite
|
||||||
|
|
||||||
|
Phase 2 (After Phase 1)
|
||||||
|
└── 07-session-support
|
||||||
|
|
||||||
|
Phase 3 (After Phase 1, can parallel with Phase 2)
|
||||||
|
└── 08-mix-project-support
|
||||||
|
|
||||||
|
Phase 4 (After relevant dependencies)
|
||||||
|
├── 09-remote-shell (after 07)
|
||||||
|
└── 10-async-execution (after Phase 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Time Estimates
|
||||||
|
|
||||||
|
| Phase | Estimated Time |
|
||||||
|
|-------|----------------|
|
||||||
|
| Phase 1 | 8-12 hours |
|
||||||
|
| Phase 2 | 3-4 hours |
|
||||||
|
| Phase 3 | 2-3 hours |
|
||||||
|
| Phase 4 | 5-7 hours |
|
||||||
|
| **Total** | **18-26 hours** |
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. Start with [Task 01: Project Setup](01-project-setup.md)
|
||||||
|
2. Complete Phase 1 tasks in order
|
||||||
|
3. Phases 2-4 can be done based on your priorities
|
||||||
|
|
||||||
|
## Task Template
|
||||||
|
|
||||||
|
Each task file includes:
|
||||||
|
|
||||||
|
- **Objective**: What the task accomplishes
|
||||||
|
- **Prerequisites**: What must be done first
|
||||||
|
- **Steps**: Detailed implementation steps with code
|
||||||
|
- **Tests**: Test cases to verify the implementation
|
||||||
|
- **Acceptance Criteria**: Checklist of requirements
|
||||||
|
- **Troubleshooting**: Common issues and solutions
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run tests after each task:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
For integration tests with org-mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test-integration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation References
|
||||||
|
|
||||||
|
| Document | Content |
|
||||||
|
|----------|---------|
|
||||||
|
| [01-emacs-elisp-best-practices.md](../docs/01-emacs-elisp-best-practices.md) | Elisp conventions |
|
||||||
|
| [02-testing-emacs-elisp.md](../docs/02-testing-emacs-elisp.md) | Testing strategies |
|
||||||
|
| [03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md) | Org-babel internals |
|
||||||
|
| [04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) | Elixir execution |
|
||||||
|
| [05-existing-implementations-analysis.md](../docs/05-existing-implementations-analysis.md) | Prior art analysis |
|
||||||
216
tasks/01-project-setup.md
Normal file
216
tasks/01-project-setup.md
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
# Task 01: Project Setup
|
||||||
|
|
||||||
|
**Phase**: 1 - Core (MVP)
|
||||||
|
**Priority**: Critical
|
||||||
|
**Estimated Time**: 30 minutes
|
||||||
|
**Dependencies**: None
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Set up the project structure with proper Emacs Lisp package conventions, including file headers, licensing, and build tooling.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Emacs 27.1+ installed
|
||||||
|
- Elixir installed and in PATH
|
||||||
|
- Git repository initialized
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### Step 1: Create the main package file
|
||||||
|
|
||||||
|
Create `ob-elixir.el` with proper headers:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; ob-elixir.el --- Org Babel functions for Elixir -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;; Copyright (C) 2024 Your Name
|
||||||
|
|
||||||
|
;; Author: Your Name <your.email@example.com>
|
||||||
|
;; URL: https://github.com/username/ob-elixir
|
||||||
|
;; Keywords: literate programming, reproducible research, elixir
|
||||||
|
;; Version: 0.1.0
|
||||||
|
;; Package-Requires: ((emacs "27.1") (org "9.4"))
|
||||||
|
|
||||||
|
;; This file is not part of GNU Emacs.
|
||||||
|
|
||||||
|
;; This program is free software; you can redistribute it and/or modify
|
||||||
|
;; it under the terms of the GNU General Public License as published by
|
||||||
|
;; the Free Software Foundation, either version 3 of the License, or
|
||||||
|
;; (at your option) any later version.
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
|
||||||
|
;; Org Babel support for evaluating Elixir code blocks.
|
||||||
|
;;
|
||||||
|
;; Features:
|
||||||
|
;; - Execute Elixir code in org-mode source blocks
|
||||||
|
;; - Support for :results value and :results output
|
||||||
|
;; - Variable passing with :var header argument
|
||||||
|
;; - Mix project context support
|
||||||
|
;;
|
||||||
|
;; Usage:
|
||||||
|
;; Add (elixir . t) to `org-babel-load-languages':
|
||||||
|
;;
|
||||||
|
;; (org-babel-do-load-languages
|
||||||
|
;; 'org-babel-load-languages
|
||||||
|
;; '((elixir . t)))
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ob)
|
||||||
|
(require 'ob-eval)
|
||||||
|
|
||||||
|
(provide 'ob-elixir)
|
||||||
|
;;; ob-elixir.el ends here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Create the Eldev file for build tooling
|
||||||
|
|
||||||
|
Create `Eldev` file:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
; -*- mode: emacs-lisp; lexical-binding: t; -*-
|
||||||
|
|
||||||
|
(eldev-use-package-archive 'gnu)
|
||||||
|
(eldev-use-package-archive 'melpa)
|
||||||
|
|
||||||
|
;; Test dependencies
|
||||||
|
(eldev-add-extra-dependencies 'test 'ert)
|
||||||
|
|
||||||
|
;; Use ERT for testing
|
||||||
|
(setf eldev-test-framework 'ert)
|
||||||
|
|
||||||
|
;; Lint configuration
|
||||||
|
(setf eldev-lint-default '(elisp package))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Create test directory structure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p test
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `test/test-ob-elixir.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; test-ob-elixir.el --- Tests for ob-elixir -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
|
||||||
|
;; Test suite for ob-elixir package.
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
|
||||||
|
;; Add parent directory to load path
|
||||||
|
(let ((dir (file-name-directory (or load-file-name buffer-file-name))))
|
||||||
|
(add-to-list 'load-path (expand-file-name ".." dir)))
|
||||||
|
|
||||||
|
(require 'ob-elixir)
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-package-loads ()
|
||||||
|
"Test that the package loads successfully."
|
||||||
|
(should (featurep 'ob-elixir)))
|
||||||
|
|
||||||
|
(provide 'test-ob-elixir)
|
||||||
|
;;; test-ob-elixir.el ends here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Create Makefile
|
||||||
|
|
||||||
|
Create `Makefile`:
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
EMACS ?= emacs
|
||||||
|
BATCH = $(EMACS) -Q -batch -L .
|
||||||
|
|
||||||
|
.PHONY: all compile test lint clean
|
||||||
|
|
||||||
|
all: compile test
|
||||||
|
|
||||||
|
compile:
|
||||||
|
$(BATCH) -f batch-byte-compile ob-elixir.el
|
||||||
|
|
||||||
|
test:
|
||||||
|
$(BATCH) -l ert -l test/test-ob-elixir.el \
|
||||||
|
-f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
|
lint:
|
||||||
|
$(BATCH) --eval "(require 'package)" \
|
||||||
|
--eval "(package-initialize)" \
|
||||||
|
--eval "(package-refresh-contents)" \
|
||||||
|
--eval "(package-install 'package-lint)" \
|
||||||
|
-l package-lint \
|
||||||
|
-f package-lint-batch-and-exit ob-elixir.el
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f *.elc test/*.elc
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Create .gitignore
|
||||||
|
|
||||||
|
Create `.gitignore`:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Byte-compiled files
|
||||||
|
*.elc
|
||||||
|
|
||||||
|
# Eldev
|
||||||
|
.eldev/
|
||||||
|
Eldev-local
|
||||||
|
|
||||||
|
# Package archives
|
||||||
|
/packages/
|
||||||
|
|
||||||
|
# Test artifacts
|
||||||
|
/test/tmp/
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
*~
|
||||||
|
\#*\#
|
||||||
|
.#*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Verify setup
|
||||||
|
|
||||||
|
Run the following commands to verify:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Emacs version
|
||||||
|
emacs --version
|
||||||
|
|
||||||
|
# Check Elixir version
|
||||||
|
elixir --version
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Compile
|
||||||
|
make compile
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] `ob-elixir.el` exists with proper headers
|
||||||
|
- [ ] Package loads without errors: `(require 'ob-elixir)`
|
||||||
|
- [ ] `make test` runs successfully
|
||||||
|
- [ ] `make compile` produces no warnings
|
||||||
|
- [ ] All files follow Emacs Lisp conventions
|
||||||
|
|
||||||
|
## Files Created
|
||||||
|
|
||||||
|
- `ob-elixir.el` - Main package file
|
||||||
|
- `Eldev` - Build tool configuration
|
||||||
|
- `Makefile` - Make targets
|
||||||
|
- `test/test-ob-elixir.el` - Test file
|
||||||
|
- `.gitignore` - Git ignore rules
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [docs/01-emacs-elisp-best-practices.md](../docs/01-emacs-elisp-best-practices.md)
|
||||||
|
- [docs/02-testing-emacs-elisp.md](../docs/02-testing-emacs-elisp.md)
|
||||||
242
tasks/02-basic-execution.md
Normal file
242
tasks/02-basic-execution.md
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
# Task 02: Basic Code Execution
|
||||||
|
|
||||||
|
**Phase**: 1 - Core (MVP)
|
||||||
|
**Priority**: Critical
|
||||||
|
**Estimated Time**: 1-2 hours
|
||||||
|
**Dependencies**: Task 01 (Project Setup)
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Implement the core `org-babel-execute:elixir` function that can execute Elixir code blocks using external process (one-shot execution).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Task 01 completed
|
||||||
|
- Elixir installed and accessible via `elixir` command
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### Step 1: Add customization group and variables
|
||||||
|
|
||||||
|
Add to `ob-elixir.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Customization
|
||||||
|
|
||||||
|
(defgroup ob-elixir nil
|
||||||
|
"Org Babel support for Elixir."
|
||||||
|
:group 'org-babel
|
||||||
|
:prefix "ob-elixir-")
|
||||||
|
|
||||||
|
(defcustom ob-elixir-command "elixir"
|
||||||
|
"Command to execute Elixir code.
|
||||||
|
Can be a full path or command name if in PATH."
|
||||||
|
:type 'string
|
||||||
|
:group 'ob-elixir
|
||||||
|
:safe #'stringp)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Add default header arguments
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Header Arguments
|
||||||
|
|
||||||
|
(defvar org-babel-default-header-args:elixir
|
||||||
|
'((:results . "value")
|
||||||
|
(:session . "none"))
|
||||||
|
"Default header arguments for Elixir code blocks.")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Register the language
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Language Registration
|
||||||
|
|
||||||
|
;; File extension for tangling
|
||||||
|
(add-to-list 'org-babel-tangle-lang-exts '("elixir" . "ex"))
|
||||||
|
|
||||||
|
;; Associate with elixir-mode for syntax highlighting (if available)
|
||||||
|
(with-eval-after-load 'org-src
|
||||||
|
(add-to-list 'org-src-lang-modes '("elixir" . elixir)))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Implement the execute function
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Execution
|
||||||
|
|
||||||
|
(defun org-babel-execute:elixir (body params)
|
||||||
|
"Execute a block of Elixir code with org-babel.
|
||||||
|
|
||||||
|
BODY is the Elixir code to execute.
|
||||||
|
PARAMS is an alist of header arguments.
|
||||||
|
|
||||||
|
This function is called by `org-babel-execute-src-block'."
|
||||||
|
(let* ((result-type (cdr (assq :result-type params)))
|
||||||
|
(result-params (cdr (assq :result-params params)))
|
||||||
|
(result (ob-elixir--execute body result-type)))
|
||||||
|
(org-babel-reassemble-table
|
||||||
|
(org-babel-result-cond result-params
|
||||||
|
result
|
||||||
|
(org-babel-script-escape result))
|
||||||
|
(org-babel-pick-name (cdr (assq :colname-names params))
|
||||||
|
(cdr (assq :colnames params)))
|
||||||
|
(org-babel-pick-name (cdr (assq :rowname-names params))
|
||||||
|
(cdr (assq :rownames params))))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Implement the internal execute function
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun ob-elixir--execute (body result-type)
|
||||||
|
"Execute BODY as Elixir code.
|
||||||
|
|
||||||
|
RESULT-TYPE is either `value' or `output'.
|
||||||
|
For `value', wraps code to capture return value.
|
||||||
|
For `output', captures stdout directly.
|
||||||
|
|
||||||
|
Returns the result as a string."
|
||||||
|
(let* ((tmp-file (org-babel-temp-file "ob-elixir-" ".exs"))
|
||||||
|
(code (if (eq result-type 'value)
|
||||||
|
(ob-elixir--wrap-for-value body)
|
||||||
|
body)))
|
||||||
|
(with-temp-file tmp-file
|
||||||
|
(insert code))
|
||||||
|
(let ((result (org-babel-eval
|
||||||
|
(format "%s %s"
|
||||||
|
ob-elixir-command
|
||||||
|
(org-babel-process-file-name tmp-file))
|
||||||
|
"")))
|
||||||
|
(string-trim result))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Implement the value wrapper
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defconst ob-elixir--value-wrapper
|
||||||
|
"result = (
|
||||||
|
%s
|
||||||
|
)
|
||||||
|
IO.puts(inspect(result, limit: :infinity, printable_limit: :infinity, charlists: :as_lists))
|
||||||
|
"
|
||||||
|
"Wrapper template for capturing Elixir expression value.
|
||||||
|
%s is replaced with the user's code.")
|
||||||
|
|
||||||
|
(defun ob-elixir--wrap-for-value (body)
|
||||||
|
"Wrap BODY to capture its return value.
|
||||||
|
|
||||||
|
The wrapper evaluates BODY, then prints the result using
|
||||||
|
`inspect/2` with infinite limits to avoid truncation."
|
||||||
|
(format ob-elixir--value-wrapper body))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Add tests
|
||||||
|
|
||||||
|
Add to `test/test-ob-elixir.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ert-deftest ob-elixir-test-elixir-available ()
|
||||||
|
"Test that Elixir is available."
|
||||||
|
(should (executable-find ob-elixir-command)))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-simple-value ()
|
||||||
|
"Test simple value evaluation."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((result (ob-elixir--execute "1 + 1" 'value)))
|
||||||
|
(should (equal "2" result))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-simple-output ()
|
||||||
|
"Test simple output evaluation."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((result (ob-elixir--execute "IO.puts(\"hello\")" 'output)))
|
||||||
|
(should (equal "hello" result))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-multiline-value ()
|
||||||
|
"Test multiline code value evaluation."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((result (ob-elixir--execute "x = 10\ny = 20\nx + y" 'value)))
|
||||||
|
(should (equal "30" result))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-list-result ()
|
||||||
|
"Test list result."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((result (ob-elixir--execute "[1, 2, 3]" 'value)))
|
||||||
|
(should (equal "[1, 2, 3]" result))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-map-result ()
|
||||||
|
"Test map result."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((result (ob-elixir--execute "%{a: 1, b: 2}" 'value)))
|
||||||
|
(should (string-match-p "%{a: 1, b: 2}" result))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 8: Test in an org buffer
|
||||||
|
|
||||||
|
Create a test org file `test.org`:
|
||||||
|
|
||||||
|
```org
|
||||||
|
* Test ob-elixir
|
||||||
|
|
||||||
|
** Basic arithmetic (value)
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir
|
||||||
|
1 + 1
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** Output test
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :results output
|
||||||
|
IO.puts("Hello, World!")
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** List manipulation
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir
|
||||||
|
Enum.map([1, 2, 3], fn x -> x * 2 end)
|
||||||
|
#+END_SRC
|
||||||
|
```
|
||||||
|
|
||||||
|
Press `C-c C-c` on each block to test.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] `org-babel-execute:elixir` function exists
|
||||||
|
- [ ] Simple expressions evaluate correctly: `1 + 1` returns `2`
|
||||||
|
- [ ] `:results value` captures return value (default)
|
||||||
|
- [ ] `:results output` captures stdout
|
||||||
|
- [ ] Multiline code executes correctly
|
||||||
|
- [ ] Lists and maps are returned in Elixir format
|
||||||
|
- [ ] All tests pass: `make test`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Cannot find elixir"
|
||||||
|
|
||||||
|
Ensure Elixir is in PATH:
|
||||||
|
```bash
|
||||||
|
which elixir
|
||||||
|
elixir --version
|
||||||
|
```
|
||||||
|
|
||||||
|
Or set the full path:
|
||||||
|
```elisp
|
||||||
|
(setq ob-elixir-command "/usr/local/bin/elixir")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Results are truncated
|
||||||
|
|
||||||
|
The wrapper uses `limit: :infinity` to prevent truncation. If still truncated, check for very large outputs.
|
||||||
|
|
||||||
|
### ANSI codes in output
|
||||||
|
|
||||||
|
We'll handle this in a later task. For now, output should be clean with the current approach.
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
- `ob-elixir.el` - Add execution functions
|
||||||
|
- `test/test-ob-elixir.el` - Add execution tests
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [docs/03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md)
|
||||||
|
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md)
|
||||||
334
tasks/03-variable-injection.md
Normal file
334
tasks/03-variable-injection.md
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
# Task 03: Variable Injection
|
||||||
|
|
||||||
|
**Phase**: 1 - Core (MVP)
|
||||||
|
**Priority**: High
|
||||||
|
**Estimated Time**: 1-2 hours
|
||||||
|
**Dependencies**: Task 02 (Basic Execution)
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Implement variable injection so that `:var` header arguments work correctly, allowing data to be passed from org-mode into Elixir code blocks.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Task 02 completed
|
||||||
|
- Basic execution working
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Org-babel allows passing variables to code blocks:
|
||||||
|
|
||||||
|
```org
|
||||||
|
#+BEGIN_SRC elixir :var x=5 :var name="Alice"
|
||||||
|
"Hello, #{name}! x = #{x}"
|
||||||
|
#+END_SRC
|
||||||
|
```
|
||||||
|
|
||||||
|
We need to:
|
||||||
|
1. Convert Elisp values to Elixir syntax
|
||||||
|
2. Generate Elixir variable assignment statements
|
||||||
|
3. Prepend these to the code before execution
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### Step 1: Implement Elisp to Elixir conversion
|
||||||
|
|
||||||
|
Add to `ob-elixir.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Type Conversion
|
||||||
|
|
||||||
|
(defun ob-elixir--elisp-to-elixir (value)
|
||||||
|
"Convert Elisp VALUE to Elixir literal syntax.
|
||||||
|
|
||||||
|
Handles:
|
||||||
|
- nil -> nil
|
||||||
|
- t -> true
|
||||||
|
- numbers -> numbers
|
||||||
|
- strings -> quoted strings
|
||||||
|
- symbols -> atoms
|
||||||
|
- lists -> Elixir lists
|
||||||
|
- vectors -> tuples"
|
||||||
|
(cond
|
||||||
|
;; nil
|
||||||
|
((null value) "nil")
|
||||||
|
|
||||||
|
;; Boolean true
|
||||||
|
((eq value t) "true")
|
||||||
|
|
||||||
|
;; Numbers
|
||||||
|
((numberp value)
|
||||||
|
(number-to-string value))
|
||||||
|
|
||||||
|
;; Strings
|
||||||
|
((stringp value)
|
||||||
|
(format "\"%s\"" (ob-elixir--escape-string value)))
|
||||||
|
|
||||||
|
;; Symbols become atoms (except special ones)
|
||||||
|
((symbolp value)
|
||||||
|
(let ((name (symbol-name value)))
|
||||||
|
(cond
|
||||||
|
((string= name "hline") ":hline")
|
||||||
|
((string-match-p "^[a-z_][a-zA-Z0-9_]*[?!]?$" name)
|
||||||
|
(format ":%s" name))
|
||||||
|
(t (format ":\"%s\"" name)))))
|
||||||
|
|
||||||
|
;; Vectors become tuples
|
||||||
|
((vectorp value)
|
||||||
|
(format "{%s}"
|
||||||
|
(mapconcat #'ob-elixir--elisp-to-elixir
|
||||||
|
(append value nil) ", ")))
|
||||||
|
|
||||||
|
;; Lists
|
||||||
|
((listp value)
|
||||||
|
(format "[%s]"
|
||||||
|
(mapconcat #'ob-elixir--elisp-to-elixir value ", ")))
|
||||||
|
|
||||||
|
;; Fallback
|
||||||
|
(t (format "%S" value))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Implement string escaping
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun ob-elixir--escape-string (str)
|
||||||
|
"Escape special characters in STR for Elixir string literal."
|
||||||
|
(let ((result str))
|
||||||
|
;; Escape backslashes first
|
||||||
|
(setq result (replace-regexp-in-string "\\\\" "\\\\\\\\" result))
|
||||||
|
;; Escape double quotes
|
||||||
|
(setq result (replace-regexp-in-string "\"" "\\\\\"" result))
|
||||||
|
;; Escape newlines
|
||||||
|
(setq result (replace-regexp-in-string "\n" "\\\\n" result))
|
||||||
|
;; Escape tabs
|
||||||
|
(setq result (replace-regexp-in-string "\t" "\\\\t" result))
|
||||||
|
result))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Implement variable assignments function
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Variable Handling
|
||||||
|
|
||||||
|
(defun org-babel-variable-assignments:elixir (params)
|
||||||
|
"Return list of Elixir statements assigning variables from PARAMS.
|
||||||
|
|
||||||
|
Each statement has the form: var_name = value"
|
||||||
|
(mapcar
|
||||||
|
(lambda (pair)
|
||||||
|
(let ((name (car pair))
|
||||||
|
(value (cdr pair)))
|
||||||
|
(format "%s = %s"
|
||||||
|
(ob-elixir--var-name name)
|
||||||
|
(ob-elixir--elisp-to-elixir value))))
|
||||||
|
(org-babel--get-vars params)))
|
||||||
|
|
||||||
|
(defun ob-elixir--var-name (name)
|
||||||
|
"Convert NAME to a valid Elixir variable name.
|
||||||
|
|
||||||
|
Elixir variables must start with lowercase or underscore."
|
||||||
|
(let ((str (if (symbolp name) (symbol-name name) name)))
|
||||||
|
;; Ensure starts with lowercase or underscore
|
||||||
|
(if (string-match-p "^[a-z_]" str)
|
||||||
|
str
|
||||||
|
(concat "_" str))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Update the execute function
|
||||||
|
|
||||||
|
Modify `org-babel-execute:elixir` to use variable assignments:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun org-babel-execute:elixir (body params)
|
||||||
|
"Execute a block of Elixir code with org-babel.
|
||||||
|
|
||||||
|
BODY is the Elixir code to execute.
|
||||||
|
PARAMS is an alist of header arguments.
|
||||||
|
|
||||||
|
This function is called by `org-babel-execute-src-block'."
|
||||||
|
(let* ((result-type (cdr (assq :result-type params)))
|
||||||
|
(result-params (cdr (assq :result-params params)))
|
||||||
|
;; Expand body with variable assignments
|
||||||
|
(full-body (org-babel-expand-body:generic
|
||||||
|
body params
|
||||||
|
(org-babel-variable-assignments:elixir params)))
|
||||||
|
(result (ob-elixir--execute full-body result-type)))
|
||||||
|
(org-babel-reassemble-table
|
||||||
|
(org-babel-result-cond result-params
|
||||||
|
result
|
||||||
|
(org-babel-script-escape result))
|
||||||
|
(org-babel-pick-name (cdr (assq :colname-names params))
|
||||||
|
(cdr (assq :colnames params)))
|
||||||
|
(org-babel-pick-name (cdr (assq :rowname-names params))
|
||||||
|
(cdr (assq :rownames params))))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Add tests for type conversion
|
||||||
|
|
||||||
|
Add to `test/test-ob-elixir.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Type Conversion Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-nil ()
|
||||||
|
"Test nil conversion."
|
||||||
|
(should (equal "nil" (ob-elixir--elisp-to-elixir nil))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-true ()
|
||||||
|
"Test t conversion."
|
||||||
|
(should (equal "true" (ob-elixir--elisp-to-elixir t))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-integer ()
|
||||||
|
"Test integer conversion."
|
||||||
|
(should (equal "42" (ob-elixir--elisp-to-elixir 42)))
|
||||||
|
(should (equal "-10" (ob-elixir--elisp-to-elixir -10))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-float ()
|
||||||
|
"Test float conversion."
|
||||||
|
(should (equal "3.14" (ob-elixir--elisp-to-elixir 3.14))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-string ()
|
||||||
|
"Test string conversion."
|
||||||
|
(should (equal "\"hello\"" (ob-elixir--elisp-to-elixir "hello"))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-string-escaping ()
|
||||||
|
"Test string escaping."
|
||||||
|
(should (equal "\"say \\\"hi\\\"\""
|
||||||
|
(ob-elixir--elisp-to-elixir "say \"hi\"")))
|
||||||
|
(should (equal "\"line1\\nline2\""
|
||||||
|
(ob-elixir--elisp-to-elixir "line1\nline2"))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-symbol ()
|
||||||
|
"Test symbol conversion to atom."
|
||||||
|
(should (equal ":foo" (ob-elixir--elisp-to-elixir 'foo)))
|
||||||
|
(should (equal ":ok" (ob-elixir--elisp-to-elixir 'ok))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-list ()
|
||||||
|
"Test list conversion."
|
||||||
|
(should (equal "[1, 2, 3]" (ob-elixir--elisp-to-elixir '(1 2 3))))
|
||||||
|
(should (equal "[\"a\", \"b\"]" (ob-elixir--elisp-to-elixir '("a" "b")))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-nested-list ()
|
||||||
|
"Test nested list conversion."
|
||||||
|
(should (equal "[[1, 2], [3, 4]]"
|
||||||
|
(ob-elixir--elisp-to-elixir '((1 2) (3 4))))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-vector ()
|
||||||
|
"Test vector to tuple conversion."
|
||||||
|
(should (equal "{1, 2, 3}" (ob-elixir--elisp-to-elixir [1 2 3]))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Add tests for variable injection
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Variable Injection Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-variable-assignments ()
|
||||||
|
"Test variable assignment generation."
|
||||||
|
(let ((params '((:var . ("x" . 5))
|
||||||
|
(:var . ("name" . "Alice")))))
|
||||||
|
(let ((assignments (org-babel-variable-assignments:elixir params)))
|
||||||
|
(should (member "x = 5" assignments))
|
||||||
|
(should (member "name = \"Alice\"" assignments)))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-var-execution ()
|
||||||
|
"Test code execution with variables."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let* ((params '((:var . ("x" . 10))))
|
||||||
|
(var-lines (org-babel-variable-assignments:elixir params))
|
||||||
|
(full-body (concat (mapconcat #'identity var-lines "\n")
|
||||||
|
"\nx * 2")))
|
||||||
|
(should (equal "20" (ob-elixir--execute full-body 'value)))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-var-list ()
|
||||||
|
"Test passing list as variable."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let* ((params '((:var . ("data" . (1 2 3)))))
|
||||||
|
(var-lines (org-babel-variable-assignments:elixir params))
|
||||||
|
(full-body (concat (mapconcat #'identity var-lines "\n")
|
||||||
|
"\nEnum.sum(data)")))
|
||||||
|
(should (equal "6" (ob-elixir--execute full-body 'value)))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Test in an org buffer
|
||||||
|
|
||||||
|
Update `test.org`:
|
||||||
|
|
||||||
|
```org
|
||||||
|
* Variable Injection Tests
|
||||||
|
|
||||||
|
** Simple variable
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :var x=42
|
||||||
|
x * 2
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS:
|
||||||
|
: 84
|
||||||
|
|
||||||
|
** String variable
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :var name="World"
|
||||||
|
"Hello, #{name}!"
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS:
|
||||||
|
: Hello, World!
|
||||||
|
|
||||||
|
** Multiple variables
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :var x=10 :var y=20
|
||||||
|
x + y
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS:
|
||||||
|
: 30
|
||||||
|
|
||||||
|
** List variable
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :var numbers='(1 2 3 4 5)
|
||||||
|
Enum.sum(numbers)
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS:
|
||||||
|
: 15
|
||||||
|
|
||||||
|
** Table as variable
|
||||||
|
|
||||||
|
#+NAME: my-data
|
||||||
|
| a | 1 |
|
||||||
|
| b | 2 |
|
||||||
|
| c | 3 |
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :var data=my-data
|
||||||
|
Enum.map(data, fn [k, v] -> "#{k}=#{v}" end)
|
||||||
|
#+END_SRC
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] `ob-elixir--elisp-to-elixir` correctly converts all Elisp types
|
||||||
|
- [ ] `org-babel-variable-assignments:elixir` generates valid Elixir code
|
||||||
|
- [ ] `:var x=5` works in org blocks
|
||||||
|
- [ ] `:var name="string"` works with string values
|
||||||
|
- [ ] Multiple `:var` arguments work
|
||||||
|
- [ ] Lists and tables can be passed as variables
|
||||||
|
- [ ] All tests pass: `make test`
|
||||||
|
|
||||||
|
## Edge Cases to Consider
|
||||||
|
|
||||||
|
1. **Variable name conflicts**: Elixir variables must start with lowercase
|
||||||
|
2. **Special characters in strings**: Quotes, newlines, backslashes
|
||||||
|
3. **Empty lists**: Should produce `[]`
|
||||||
|
4. **Mixed type lists**: `[1, "two", :three]`
|
||||||
|
5. **hline in tables**: Special symbol for table separators
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
- `ob-elixir.el` - Add variable handling functions
|
||||||
|
- `test/test-ob-elixir.el` - Add variable tests
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [docs/03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md) - Variable Handling section
|
||||||
|
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Data Type Conversion section
|
||||||
296
tasks/04-error-handling.md
Normal file
296
tasks/04-error-handling.md
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
# Task 04: Error Handling
|
||||||
|
|
||||||
|
**Phase**: 1 - Core (MVP)
|
||||||
|
**Priority**: High
|
||||||
|
**Estimated Time**: 1 hour
|
||||||
|
**Dependencies**: Task 02 (Basic Execution)
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Implement proper error detection and reporting for Elixir code execution, so users get meaningful feedback when their code fails.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Task 02 completed
|
||||||
|
- Basic execution working
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Currently, Elixir errors are returned as raw output. We need to:
|
||||||
|
1. Detect when Elixir reports an error
|
||||||
|
2. Extract useful error information
|
||||||
|
3. Present errors clearly to the user
|
||||||
|
4. Optionally signal Emacs error conditions
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### Step 1: Define error patterns
|
||||||
|
|
||||||
|
Add to `ob-elixir.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Error Handling
|
||||||
|
|
||||||
|
(defconst ob-elixir--error-regexp
|
||||||
|
"^\\*\\* (\\([A-Za-z.]+Error\\))\\(.*\\)"
|
||||||
|
"Regexp matching Elixir runtime errors.
|
||||||
|
Group 1 is the error type, group 2 is the message.")
|
||||||
|
|
||||||
|
(defconst ob-elixir--compile-error-regexp
|
||||||
|
"^\\*\\* (\\(CompileError\\|TokenMissingError\\|SyntaxError\\))\\(.*\\)"
|
||||||
|
"Regexp matching Elixir compile-time errors.")
|
||||||
|
|
||||||
|
(defconst ob-elixir--warning-regexp
|
||||||
|
"^warning: \\(.*\\)"
|
||||||
|
"Regexp matching Elixir warnings.")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Define custom error types
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(define-error 'ob-elixir-error
|
||||||
|
"Elixir evaluation error")
|
||||||
|
|
||||||
|
(define-error 'ob-elixir-compile-error
|
||||||
|
"Elixir compilation error"
|
||||||
|
'ob-elixir-error)
|
||||||
|
|
||||||
|
(define-error 'ob-elixir-runtime-error
|
||||||
|
"Elixir runtime error"
|
||||||
|
'ob-elixir-error)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Implement error detection
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun ob-elixir--detect-error (output)
|
||||||
|
"Check OUTPUT for Elixir errors.
|
||||||
|
|
||||||
|
Returns a plist with :type, :message, and :line if an error is found.
|
||||||
|
Returns nil if no error detected."
|
||||||
|
(cond
|
||||||
|
;; Compile-time error
|
||||||
|
((string-match ob-elixir--compile-error-regexp output)
|
||||||
|
(list :type 'compile
|
||||||
|
:error-type (match-string 1 output)
|
||||||
|
:message (string-trim (match-string 2 output))
|
||||||
|
:full-output output))
|
||||||
|
|
||||||
|
;; Runtime error
|
||||||
|
((string-match ob-elixir--error-regexp output)
|
||||||
|
(list :type 'runtime
|
||||||
|
:error-type (match-string 1 output)
|
||||||
|
:message (string-trim (match-string 2 output))
|
||||||
|
:full-output output))
|
||||||
|
|
||||||
|
;; No error
|
||||||
|
(t nil)))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Implement error formatting
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun ob-elixir--format-error (error-info)
|
||||||
|
"Format ERROR-INFO into a user-friendly message."
|
||||||
|
(let ((type (plist-get error-info :type))
|
||||||
|
(error-type (plist-get error-info :error-type))
|
||||||
|
(message (plist-get error-info :message)))
|
||||||
|
(format "Elixir %s: (%s) %s"
|
||||||
|
(if (eq type 'compile) "Compile Error" "Runtime Error")
|
||||||
|
error-type
|
||||||
|
message)))
|
||||||
|
|
||||||
|
(defcustom ob-elixir-signal-errors t
|
||||||
|
"Whether to signal Emacs errors on Elixir execution failure.
|
||||||
|
|
||||||
|
When non-nil, Elixir errors will be signaled as Emacs errors.
|
||||||
|
When nil, errors are returned as the result string."
|
||||||
|
:type 'boolean
|
||||||
|
:group 'ob-elixir)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Update the execute function
|
||||||
|
|
||||||
|
Modify `ob-elixir--execute`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun ob-elixir--execute (body result-type)
|
||||||
|
"Execute BODY as Elixir code.
|
||||||
|
|
||||||
|
RESULT-TYPE is either `value' or `output'.
|
||||||
|
For `value', wraps code to capture return value.
|
||||||
|
For `output', captures stdout directly.
|
||||||
|
|
||||||
|
Returns the result as a string.
|
||||||
|
May signal `ob-elixir-error' if execution fails and
|
||||||
|
`ob-elixir-signal-errors' is non-nil."
|
||||||
|
(let* ((tmp-file (org-babel-temp-file "ob-elixir-" ".exs"))
|
||||||
|
(code (if (eq result-type 'value)
|
||||||
|
(ob-elixir--wrap-for-value body)
|
||||||
|
body)))
|
||||||
|
(with-temp-file tmp-file
|
||||||
|
(insert code))
|
||||||
|
(let ((result (org-babel-eval
|
||||||
|
(format "%s %s"
|
||||||
|
ob-elixir-command
|
||||||
|
(org-babel-process-file-name tmp-file))
|
||||||
|
"")))
|
||||||
|
(ob-elixir--process-result result))))
|
||||||
|
|
||||||
|
(defun ob-elixir--process-result (result)
|
||||||
|
"Process RESULT from Elixir execution.
|
||||||
|
|
||||||
|
Checks for errors and handles them according to `ob-elixir-signal-errors'.
|
||||||
|
Returns the cleaned result string."
|
||||||
|
(let ((trimmed (string-trim result))
|
||||||
|
(error-info (ob-elixir--detect-error result)))
|
||||||
|
(if error-info
|
||||||
|
(if ob-elixir-signal-errors
|
||||||
|
(signal (if (eq (plist-get error-info :type) 'compile)
|
||||||
|
'ob-elixir-compile-error
|
||||||
|
'ob-elixir-runtime-error)
|
||||||
|
(list (ob-elixir--format-error error-info)))
|
||||||
|
;; Return error as result
|
||||||
|
(plist-get error-info :full-output))
|
||||||
|
;; No error, return trimmed result
|
||||||
|
trimmed)))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Handle warnings
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defcustom ob-elixir-show-warnings t
|
||||||
|
"Whether to include warnings in output.
|
||||||
|
|
||||||
|
When non-nil, Elixir warnings are included in the result.
|
||||||
|
When nil, warnings are stripped from the output."
|
||||||
|
:type 'boolean
|
||||||
|
:group 'ob-elixir)
|
||||||
|
|
||||||
|
(defun ob-elixir--strip-warnings (output)
|
||||||
|
"Remove warning lines from OUTPUT if configured."
|
||||||
|
(if ob-elixir-show-warnings
|
||||||
|
output
|
||||||
|
(let ((lines (split-string output "\n")))
|
||||||
|
(mapconcat #'identity
|
||||||
|
(cl-remove-if (lambda (line)
|
||||||
|
(string-match-p ob-elixir--warning-regexp line))
|
||||||
|
lines)
|
||||||
|
"\n"))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Add tests
|
||||||
|
|
||||||
|
Add to `test/test-ob-elixir.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Error Handling Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-detect-runtime-error ()
|
||||||
|
"Test runtime error detection."
|
||||||
|
(let ((output "** (RuntimeError) something went wrong"))
|
||||||
|
(let ((error-info (ob-elixir--detect-error output)))
|
||||||
|
(should error-info)
|
||||||
|
(should (eq 'runtime (plist-get error-info :type)))
|
||||||
|
(should (equal "RuntimeError" (plist-get error-info :error-type))))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-detect-compile-error ()
|
||||||
|
"Test compile error detection."
|
||||||
|
(let ((output "** (CompileError) test.exs:1: undefined function foo/0"))
|
||||||
|
(let ((error-info (ob-elixir--detect-error output)))
|
||||||
|
(should error-info)
|
||||||
|
(should (eq 'compile (plist-get error-info :type)))
|
||||||
|
(should (equal "CompileError" (plist-get error-info :error-type))))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-no-error ()
|
||||||
|
"Test that valid output is not detected as error."
|
||||||
|
(should-not (ob-elixir--detect-error "42"))
|
||||||
|
(should-not (ob-elixir--detect-error "[1, 2, 3]"))
|
||||||
|
(should-not (ob-elixir--detect-error "\"hello\"")))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-error-execution ()
|
||||||
|
"Test that errors are properly handled during execution."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((ob-elixir-signal-errors nil))
|
||||||
|
(let ((result (ob-elixir--execute "raise \"test error\"" 'value)))
|
||||||
|
(should (string-match-p "RuntimeError" result)))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-error-signaling ()
|
||||||
|
"Test that errors are signaled when configured."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((ob-elixir-signal-errors t))
|
||||||
|
(should-error (ob-elixir--execute "raise \"test error\"" 'value)
|
||||||
|
:type 'ob-elixir-runtime-error)))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-undefined-function ()
|
||||||
|
"Test handling of undefined function error."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((ob-elixir-signal-errors nil))
|
||||||
|
(let ((result (ob-elixir--execute "undefined_function()" 'value)))
|
||||||
|
(should (string-match-p "\\(UndefinedFunctionError\\|CompileError\\)" result)))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 8: Test in an org buffer
|
||||||
|
|
||||||
|
Add to `test.org`:
|
||||||
|
|
||||||
|
```org
|
||||||
|
* Error Handling Tests
|
||||||
|
|
||||||
|
** Runtime Error
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir
|
||||||
|
raise "This is a test error"
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** Compile Error
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir
|
||||||
|
def incomplete_function(
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** Undefined Function
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir
|
||||||
|
this_function_does_not_exist()
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** Warning (should still execute)
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir
|
||||||
|
x = 1
|
||||||
|
y = 2
|
||||||
|
x # y is unused, may generate warning
|
||||||
|
#+END_SRC
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] Runtime errors are detected (e.g., `raise "error"`)
|
||||||
|
- [ ] Compile errors are detected (e.g., syntax errors)
|
||||||
|
- [ ] Errors are formatted with type and message
|
||||||
|
- [ ] `ob-elixir-signal-errors` controls error behavior
|
||||||
|
- [ ] Warnings are handled according to `ob-elixir-show-warnings`
|
||||||
|
- [ ] Valid output is not mistakenly detected as errors
|
||||||
|
- [ ] All tests pass: `make test`
|
||||||
|
|
||||||
|
## Error Types to Handle
|
||||||
|
|
||||||
|
| Error Type | Example | Detection |
|
||||||
|
|------------|---------|-----------|
|
||||||
|
| RuntimeError | `raise "msg"` | `** (RuntimeError)` |
|
||||||
|
| ArgumentError | Bad function arg | `** (ArgumentError)` |
|
||||||
|
| ArithmeticError | Division by zero | `** (ArithmeticError)` |
|
||||||
|
| CompileError | Syntax error | `** (CompileError)` |
|
||||||
|
| SyntaxError | Invalid syntax | `** (SyntaxError)` |
|
||||||
|
| TokenMissingError | Missing end | `** (TokenMissingError)` |
|
||||||
|
| UndefinedFunctionError | Unknown function | `** (UndefinedFunctionError)` |
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
- `ob-elixir.el` - Add error handling functions
|
||||||
|
- `test/test-ob-elixir.el` - Add error handling tests
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Error Handling section
|
||||||
330
tasks/05-result-formatting.md
Normal file
330
tasks/05-result-formatting.md
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
# Task 05: Result Formatting and Table Support
|
||||||
|
|
||||||
|
**Phase**: 1 - Core (MVP)
|
||||||
|
**Priority**: Medium
|
||||||
|
**Estimated Time**: 1-2 hours
|
||||||
|
**Dependencies**: Task 02 (Basic Execution), Task 03 (Variable Injection)
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Implement proper result formatting so Elixir lists become org tables and results are properly parsed back into Elisp data structures.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Task 02 and 03 completed
|
||||||
|
- Basic execution and variables working
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Org-babel can display results as:
|
||||||
|
- Scalar values (`:results scalar`)
|
||||||
|
- Tables (`:results table`)
|
||||||
|
- Raw org markup (`:results raw`)
|
||||||
|
- Verbatim (`:results verbatim`)
|
||||||
|
|
||||||
|
When Elixir returns a list like `[[1, 2], [3, 4]]`, org should display it as a table:
|
||||||
|
|
||||||
|
```
|
||||||
|
| 1 | 2 |
|
||||||
|
| 3 | 4 |
|
||||||
|
```
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### Step 1: Implement result parsing
|
||||||
|
|
||||||
|
Add to `ob-elixir.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Result Formatting
|
||||||
|
|
||||||
|
(defun ob-elixir--table-or-string (result)
|
||||||
|
"Convert RESULT to Emacs table or string.
|
||||||
|
|
||||||
|
If RESULT looks like a list, parse it into an Elisp list.
|
||||||
|
Otherwise return as string.
|
||||||
|
|
||||||
|
Uses `org-babel-script-escape' for parsing."
|
||||||
|
(let ((trimmed (string-trim result)))
|
||||||
|
(cond
|
||||||
|
;; Empty result
|
||||||
|
((string-empty-p trimmed) nil)
|
||||||
|
|
||||||
|
;; Looks like a list - try to parse
|
||||||
|
((string-match-p "^\\[.*\\]$" trimmed)
|
||||||
|
(condition-case nil
|
||||||
|
(let ((parsed (org-babel-script-escape trimmed)))
|
||||||
|
(ob-elixir--sanitize-table parsed))
|
||||||
|
(error trimmed)))
|
||||||
|
|
||||||
|
;; Looks like a tuple - convert to list first
|
||||||
|
((string-match-p "^{.*}$" trimmed)
|
||||||
|
(condition-case nil
|
||||||
|
(let* ((as-list (replace-regexp-in-string
|
||||||
|
"^{\\(.*\\)}$" "[\\1]" trimmed))
|
||||||
|
(parsed (org-babel-script-escape as-list)))
|
||||||
|
(ob-elixir--sanitize-table parsed))
|
||||||
|
(error trimmed)))
|
||||||
|
|
||||||
|
;; Scalar value
|
||||||
|
(t trimmed))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Implement table sanitization
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defvar ob-elixir-nil-to 'hline
|
||||||
|
"Elisp value to use for Elixir nil in table cells.
|
||||||
|
|
||||||
|
When nil appears in an Elixir list that becomes a table,
|
||||||
|
it is replaced with this value. Use `hline' for org table
|
||||||
|
horizontal lines, or nil for empty cells.")
|
||||||
|
|
||||||
|
(defun ob-elixir--sanitize-table (data)
|
||||||
|
"Sanitize DATA for use as an org table.
|
||||||
|
|
||||||
|
Replaces nil values according to `ob-elixir-nil-to'.
|
||||||
|
Ensures consistent structure for table rendering."
|
||||||
|
(cond
|
||||||
|
;; Not a list - return as-is
|
||||||
|
((not (listp data)) data)
|
||||||
|
|
||||||
|
;; Empty list
|
||||||
|
((null data) nil)
|
||||||
|
|
||||||
|
;; List of lists - could be table
|
||||||
|
((and (listp (car data)) (not (null (car data))))
|
||||||
|
(mapcar #'ob-elixir--sanitize-row data))
|
||||||
|
|
||||||
|
;; Simple list - single row
|
||||||
|
(t (ob-elixir--sanitize-row data))))
|
||||||
|
|
||||||
|
(defun ob-elixir--sanitize-row (row)
|
||||||
|
"Sanitize a single ROW for table display."
|
||||||
|
(if (listp row)
|
||||||
|
(mapcar (lambda (cell)
|
||||||
|
(cond
|
||||||
|
((null cell) ob-elixir-nil-to)
|
||||||
|
((eq cell 'nil) ob-elixir-nil-to)
|
||||||
|
(t cell)))
|
||||||
|
row)
|
||||||
|
row))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Handle keyword lists and maps
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun ob-elixir--parse-keyword-list (str)
|
||||||
|
"Parse STR as Elixir keyword list into alist.
|
||||||
|
|
||||||
|
Handles format like: [a: 1, b: 2]"
|
||||||
|
(when (string-match "^\\[\\(.*\\)\\]$" str)
|
||||||
|
(let ((content (match-string 1 str)))
|
||||||
|
(when (string-match-p "^[a-z_]+:" content)
|
||||||
|
(let ((pairs '()))
|
||||||
|
(dolist (part (split-string content ", "))
|
||||||
|
(when (string-match "^\\([a-z_]+\\):\\s-*\\(.+\\)$" part)
|
||||||
|
(push (cons (intern (match-string 1 part))
|
||||||
|
(ob-elixir--parse-value (match-string 2 part)))
|
||||||
|
pairs)))
|
||||||
|
(nreverse pairs))))))
|
||||||
|
|
||||||
|
(defun ob-elixir--parse-value (str)
|
||||||
|
"Parse STR as a simple Elixir value."
|
||||||
|
(let ((trimmed (string-trim str)))
|
||||||
|
(cond
|
||||||
|
((string= trimmed "nil") nil)
|
||||||
|
((string= trimmed "true") t)
|
||||||
|
((string= trimmed "false") nil)
|
||||||
|
((string-match-p "^[0-9]+$" trimmed)
|
||||||
|
(string-to-number trimmed))
|
||||||
|
((string-match-p "^[0-9]+\\.[0-9]+$" trimmed)
|
||||||
|
(string-to-number trimmed))
|
||||||
|
((string-match-p "^\".*\"$" trimmed)
|
||||||
|
(substring trimmed 1 -1))
|
||||||
|
((string-match-p "^:.*$" trimmed)
|
||||||
|
(intern (substring trimmed 1)))
|
||||||
|
(t trimmed))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Update the execute function
|
||||||
|
|
||||||
|
Ensure `org-babel-execute:elixir` uses the parsing:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun org-babel-execute:elixir (body params)
|
||||||
|
"Execute a block of Elixir code with org-babel.
|
||||||
|
|
||||||
|
BODY is the Elixir code to execute.
|
||||||
|
PARAMS is an alist of header arguments."
|
||||||
|
(let* ((result-type (cdr (assq :result-type params)))
|
||||||
|
(result-params (cdr (assq :result-params params)))
|
||||||
|
(full-body (org-babel-expand-body:generic
|
||||||
|
body params
|
||||||
|
(org-babel-variable-assignments:elixir params)))
|
||||||
|
(result (ob-elixir--execute full-body result-type)))
|
||||||
|
(org-babel-reassemble-table
|
||||||
|
(org-babel-result-cond result-params
|
||||||
|
;; For output/scalar/verbatim - return as-is
|
||||||
|
result
|
||||||
|
;; For value - parse into Elisp data
|
||||||
|
(ob-elixir--table-or-string result))
|
||||||
|
(org-babel-pick-name (cdr (assq :colname-names params))
|
||||||
|
(cdr (assq :colnames params)))
|
||||||
|
(org-babel-pick-name (cdr (assq :rowname-names params))
|
||||||
|
(cdr (assq :rownames params))))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Support column names
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun ob-elixir--maybe-add-colnames (result params)
|
||||||
|
"Add column names to RESULT if specified in PARAMS."
|
||||||
|
(let ((colnames (cdr (assq :colnames params))))
|
||||||
|
(if (and colnames (listp result) (listp (car result)))
|
||||||
|
(cons (if (listp colnames) colnames (car result))
|
||||||
|
(if (listp colnames) result (cdr result)))
|
||||||
|
result)))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Add tests
|
||||||
|
|
||||||
|
Add to `test/test-ob-elixir.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Result Formatting Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-parse-simple-list ()
|
||||||
|
"Test parsing simple list result."
|
||||||
|
(should (equal '(1 2 3) (ob-elixir--table-or-string "[1, 2, 3]"))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-parse-nested-list ()
|
||||||
|
"Test parsing nested list (table) result."
|
||||||
|
(should (equal '((1 2) (3 4))
|
||||||
|
(ob-elixir--table-or-string "[[1, 2], [3, 4]]"))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-parse-tuple ()
|
||||||
|
"Test parsing tuple result."
|
||||||
|
(should (equal '(1 2 3) (ob-elixir--table-or-string "{1, 2, 3}"))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-parse-scalar ()
|
||||||
|
"Test that scalars are returned as strings."
|
||||||
|
(should (equal "42" (ob-elixir--table-or-string "42")))
|
||||||
|
(should (equal ":ok" (ob-elixir--table-or-string ":ok"))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-parse-string ()
|
||||||
|
"Test parsing string result."
|
||||||
|
(should (equal "\"hello\"" (ob-elixir--table-or-string "\"hello\""))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-sanitize-table-nil ()
|
||||||
|
"Test that nil values are sanitized in tables."
|
||||||
|
(let ((ob-elixir-nil-to 'hline))
|
||||||
|
(should (equal '((1 hline) (hline 2))
|
||||||
|
(ob-elixir--sanitize-table '((1 nil) (nil 2)))))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-execution-returns-table ()
|
||||||
|
"Test that list results become tables."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((result (ob-elixir--table-or-string
|
||||||
|
(ob-elixir--execute "[[1, 2], [3, 4]]" 'value))))
|
||||||
|
(should (equal '((1 2) (3 4)) result))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-mixed-list ()
|
||||||
|
"Test parsing mixed-type list."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((result (ob-elixir--table-or-string
|
||||||
|
(ob-elixir--execute "[1, \"two\", :three]" 'value))))
|
||||||
|
(should (listp result))
|
||||||
|
(should (= 3 (length result)))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Test in an org buffer
|
||||||
|
|
||||||
|
Add to `test.org`:
|
||||||
|
|
||||||
|
```org
|
||||||
|
* Result Formatting Tests
|
||||||
|
|
||||||
|
** Simple list as table row
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir
|
||||||
|
[1, 2, 3, 4, 5]
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS:
|
||||||
|
| 1 | 2 | 3 | 4 | 5 |
|
||||||
|
|
||||||
|
** Nested list as table
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir
|
||||||
|
[
|
||||||
|
["Alice", 30],
|
||||||
|
["Bob", 25],
|
||||||
|
["Charlie", 35]
|
||||||
|
]
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS:
|
||||||
|
| Alice | 30 |
|
||||||
|
| Bob | 25 |
|
||||||
|
| Charlie | 35 |
|
||||||
|
|
||||||
|
** Map result (verbatim)
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :results verbatim
|
||||||
|
%{name: "Alice", age: 30}
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS:
|
||||||
|
: %{age: 30, name: "Alice"}
|
||||||
|
|
||||||
|
** Enum operations returning lists
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir
|
||||||
|
Enum.map(1..5, fn x -> [x, x * x] end)
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS:
|
||||||
|
| 1 | 1 |
|
||||||
|
| 2 | 4 |
|
||||||
|
| 3 | 9 |
|
||||||
|
| 4 | 16 |
|
||||||
|
| 5 | 25 |
|
||||||
|
|
||||||
|
** Tuple result
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir
|
||||||
|
{:ok, "success", 123}
|
||||||
|
#+END_SRC
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] Simple lists `[1, 2, 3]` become table rows
|
||||||
|
- [ ] Nested lists `[[1, 2], [3, 4]]` become tables
|
||||||
|
- [ ] Tuples are handled (converted to lists)
|
||||||
|
- [ ] Scalar values remain as strings
|
||||||
|
- [ ] `:results verbatim` bypasses table conversion
|
||||||
|
- [ ] nil values in tables are handled according to config
|
||||||
|
- [ ] All tests pass: `make test`
|
||||||
|
|
||||||
|
## Result Format Reference
|
||||||
|
|
||||||
|
| Elixir Value | Org Display |
|
||||||
|
|--------------|-------------|
|
||||||
|
| `[1, 2, 3]` | `\| 1 \| 2 \| 3 \|` |
|
||||||
|
| `[[1], [2]]` | Multi-row table |
|
||||||
|
| `{:ok, 1}` | `\| ok \| 1 \|` |
|
||||||
|
| `42` | `: 42` |
|
||||||
|
| `"hello"` | `: "hello"` |
|
||||||
|
| `%{a: 1}` | `: %{a: 1}` |
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
- `ob-elixir.el` - Add result formatting functions
|
||||||
|
- `test/test-ob-elixir.el` - Add formatting tests
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [docs/03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md) - Result Handling section
|
||||||
|
- [Org Manual - Results of Evaluation](https://orgmode.org/manual/Results-of-Evaluation.html)
|
||||||
623
tasks/06-test-suite.md
Normal file
623
tasks/06-test-suite.md
Normal file
@@ -0,0 +1,623 @@
|
|||||||
|
# Task 06: Comprehensive Test Suite
|
||||||
|
|
||||||
|
**Phase**: 1 - Core (MVP)
|
||||||
|
**Priority**: High
|
||||||
|
**Estimated Time**: 2-3 hours
|
||||||
|
**Dependencies**: Tasks 01-05 (All Core Tasks)
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Create a comprehensive test suite that covers all implemented functionality, including unit tests, integration tests, and org-buffer tests.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- All Phase 1 tasks completed
|
||||||
|
- Basic functionality working
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
A good test suite should:
|
||||||
|
1. Test each function in isolation (unit tests)
|
||||||
|
2. Test the integration with org-mode (integration tests)
|
||||||
|
3. Be runnable in CI/CD (batch mode tests)
|
||||||
|
4. Provide good coverage of edge cases
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### Step 1: Organize test file structure
|
||||||
|
|
||||||
|
Create the test directory structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
test/
|
||||||
|
├── test-ob-elixir.el # Main test file (loads all)
|
||||||
|
├── test-ob-elixir-core.el # Core execution tests
|
||||||
|
├── test-ob-elixir-vars.el # Variable handling tests
|
||||||
|
├── test-ob-elixir-results.el # Result formatting tests
|
||||||
|
├── test-ob-elixir-errors.el # Error handling tests
|
||||||
|
└── test-ob-elixir-org.el # Org integration tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Create main test file
|
||||||
|
|
||||||
|
`test/test-ob-elixir.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; test-ob-elixir.el --- Tests for ob-elixir -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Commentary:
|
||||||
|
|
||||||
|
;; Main test file that loads all test modules.
|
||||||
|
;; Run with: make test
|
||||||
|
;; Or: emacs -batch -l ert -l test/test-ob-elixir.el -f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
|
||||||
|
;; Add source directory to load path
|
||||||
|
(let ((dir (file-name-directory (or load-file-name buffer-file-name))))
|
||||||
|
(add-to-list 'load-path (expand-file-name ".." dir))
|
||||||
|
(add-to-list 'load-path dir))
|
||||||
|
|
||||||
|
;; Load the package
|
||||||
|
(require 'ob-elixir)
|
||||||
|
|
||||||
|
;; Load test modules
|
||||||
|
(require 'test-ob-elixir-core)
|
||||||
|
(require 'test-ob-elixir-vars)
|
||||||
|
(require 'test-ob-elixir-results)
|
||||||
|
(require 'test-ob-elixir-errors)
|
||||||
|
(require 'test-ob-elixir-org)
|
||||||
|
|
||||||
|
;;; Test Helpers
|
||||||
|
|
||||||
|
(defvar ob-elixir-test--elixir-available
|
||||||
|
(executable-find ob-elixir-command)
|
||||||
|
"Non-nil if Elixir is available for testing.")
|
||||||
|
|
||||||
|
(defmacro ob-elixir-test-with-elixir (&rest body)
|
||||||
|
"Execute BODY only if Elixir is available."
|
||||||
|
`(if ob-elixir-test--elixir-available
|
||||||
|
(progn ,@body)
|
||||||
|
(ert-skip "Elixir not available")))
|
||||||
|
|
||||||
|
(defmacro ob-elixir-test-with-temp-org-buffer (&rest body)
|
||||||
|
"Execute BODY in a temporary org-mode buffer."
|
||||||
|
`(with-temp-buffer
|
||||||
|
(org-mode)
|
||||||
|
(ob-elixir--ensure-org-babel-loaded)
|
||||||
|
,@body))
|
||||||
|
|
||||||
|
(defun ob-elixir--ensure-org-babel-loaded ()
|
||||||
|
"Ensure org-babel is loaded with Elixir support."
|
||||||
|
(require 'org)
|
||||||
|
(require 'ob)
|
||||||
|
(org-babel-do-load-languages
|
||||||
|
'org-babel-load-languages
|
||||||
|
'((elixir . t))))
|
||||||
|
|
||||||
|
;;; Smoke Test
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-smoke ()
|
||||||
|
"Basic smoke test - package loads and Elixir is available."
|
||||||
|
(should (featurep 'ob-elixir))
|
||||||
|
(should (fboundp 'org-babel-execute:elixir))
|
||||||
|
(should (boundp 'org-babel-default-header-args:elixir)))
|
||||||
|
|
||||||
|
(provide 'test-ob-elixir)
|
||||||
|
;;; test-ob-elixir.el ends here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Create core execution tests
|
||||||
|
|
||||||
|
`test/test-ob-elixir-core.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; test-ob-elixir-core.el --- Core execution tests -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'ob-elixir)
|
||||||
|
|
||||||
|
;;; Command Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-command-exists ()
|
||||||
|
"Test that the Elixir command is configured."
|
||||||
|
(should (stringp ob-elixir-command))
|
||||||
|
(should (not (string-empty-p ob-elixir-command))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-command-executable ()
|
||||||
|
"Test that the Elixir command is executable."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(should (executable-find ob-elixir-command)))
|
||||||
|
|
||||||
|
;;; Basic Execution Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-execute-simple-value ()
|
||||||
|
"Test simple value evaluation."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(should (equal "2" (ob-elixir--execute "1 + 1" 'value))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-execute-simple-output ()
|
||||||
|
"Test simple output evaluation."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(should (equal "hello" (ob-elixir--execute "IO.puts(\"hello\")" 'output))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-execute-multiline ()
|
||||||
|
"Test multiline code execution."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((code "x = 10\ny = 20\nx + y"))
|
||||||
|
(should (equal "30" (ob-elixir--execute code 'value)))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-execute-function-def ()
|
||||||
|
"Test function definition and call."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((code "
|
||||||
|
defmodule Test do
|
||||||
|
def double(x), do: x * 2
|
||||||
|
end
|
||||||
|
Test.double(21)"))
|
||||||
|
(should (equal "42" (ob-elixir--execute code 'value)))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-execute-enum ()
|
||||||
|
"Test Enum module usage."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(should (equal "15"
|
||||||
|
(ob-elixir--execute "Enum.sum([1, 2, 3, 4, 5])" 'value))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-execute-pipe ()
|
||||||
|
"Test pipe operator."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((code "[1, 2, 3] |> Enum.map(&(&1 * 2)) |> Enum.sum()"))
|
||||||
|
(should (equal "12" (ob-elixir--execute code 'value)))))
|
||||||
|
|
||||||
|
;;; Data Type Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-execute-list ()
|
||||||
|
"Test list result."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(should (equal "[1, 2, 3]" (ob-elixir--execute "[1, 2, 3]" 'value))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-execute-tuple ()
|
||||||
|
"Test tuple result."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(should (equal "{:ok, 42}" (ob-elixir--execute "{:ok, 42}" 'value))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-execute-map ()
|
||||||
|
"Test map result."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((result (ob-elixir--execute "%{a: 1, b: 2}" 'value)))
|
||||||
|
(should (string-match-p "%{" result))
|
||||||
|
(should (string-match-p "a:" result))
|
||||||
|
(should (string-match-p "b:" result))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-execute-string ()
|
||||||
|
"Test string result."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(should (equal "\"hello world\""
|
||||||
|
(ob-elixir--execute "\"hello world\"" 'value))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-execute-atom ()
|
||||||
|
"Test atom result."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(should (equal ":ok" (ob-elixir--execute ":ok" 'value))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-execute-boolean ()
|
||||||
|
"Test boolean result."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(should (equal "true" (ob-elixir--execute "true" 'value)))
|
||||||
|
(should (equal "false" (ob-elixir--execute "false" 'value))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-execute-nil ()
|
||||||
|
"Test nil result."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(should (equal "nil" (ob-elixir--execute "nil" 'value))))
|
||||||
|
|
||||||
|
;;; Wrapper Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-wrap-for-value ()
|
||||||
|
"Test value wrapper generation."
|
||||||
|
(let ((wrapped (ob-elixir--wrap-for-value "1 + 1")))
|
||||||
|
(should (string-match-p "result = " wrapped))
|
||||||
|
(should (string-match-p "IO\\.puts" wrapped))
|
||||||
|
(should (string-match-p "inspect" wrapped))))
|
||||||
|
|
||||||
|
(provide 'test-ob-elixir-core)
|
||||||
|
;;; test-ob-elixir-core.el ends here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Create variable tests
|
||||||
|
|
||||||
|
`test/test-ob-elixir-vars.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; test-ob-elixir-vars.el --- Variable handling tests -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'ob-elixir)
|
||||||
|
|
||||||
|
;;; Type Conversion Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-nil ()
|
||||||
|
(should (equal "nil" (ob-elixir--elisp-to-elixir nil))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-true ()
|
||||||
|
(should (equal "true" (ob-elixir--elisp-to-elixir t))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-integer ()
|
||||||
|
(should (equal "42" (ob-elixir--elisp-to-elixir 42)))
|
||||||
|
(should (equal "-10" (ob-elixir--elisp-to-elixir -10)))
|
||||||
|
(should (equal "0" (ob-elixir--elisp-to-elixir 0))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-float ()
|
||||||
|
(should (equal "3.14" (ob-elixir--elisp-to-elixir 3.14)))
|
||||||
|
(should (equal "-2.5" (ob-elixir--elisp-to-elixir -2.5))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-string ()
|
||||||
|
(should (equal "\"hello\"" (ob-elixir--elisp-to-elixir "hello")))
|
||||||
|
(should (equal "\"\"" (ob-elixir--elisp-to-elixir ""))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-string-escaping ()
|
||||||
|
(should (equal "\"say \\\"hi\\\"\""
|
||||||
|
(ob-elixir--elisp-to-elixir "say \"hi\"")))
|
||||||
|
(should (equal "\"line1\\nline2\""
|
||||||
|
(ob-elixir--elisp-to-elixir "line1\nline2")))
|
||||||
|
(should (equal "\"tab\\there\""
|
||||||
|
(ob-elixir--elisp-to-elixir "tab\there"))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-symbol ()
|
||||||
|
(should (equal ":foo" (ob-elixir--elisp-to-elixir 'foo)))
|
||||||
|
(should (equal ":ok" (ob-elixir--elisp-to-elixir 'ok)))
|
||||||
|
(should (equal ":error" (ob-elixir--elisp-to-elixir 'error))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-list ()
|
||||||
|
(should (equal "[1, 2, 3]" (ob-elixir--elisp-to-elixir '(1 2 3))))
|
||||||
|
(should (equal "[]" (ob-elixir--elisp-to-elixir '())))
|
||||||
|
(should (equal "[\"a\", \"b\"]" (ob-elixir--elisp-to-elixir '("a" "b")))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-nested-list ()
|
||||||
|
(should (equal "[[1, 2], [3, 4]]"
|
||||||
|
(ob-elixir--elisp-to-elixir '((1 2) (3 4))))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-vector ()
|
||||||
|
(should (equal "{1, 2, 3}" (ob-elixir--elisp-to-elixir [1 2 3]))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-convert-mixed ()
|
||||||
|
(should (equal "[1, \"two\", :three]"
|
||||||
|
(ob-elixir--elisp-to-elixir '(1 "two" three)))))
|
||||||
|
|
||||||
|
;;; Variable Assignment Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-var-assignments-single ()
|
||||||
|
(let ((params '((:var . ("x" . 5)))))
|
||||||
|
(should (equal '("x = 5")
|
||||||
|
(org-babel-variable-assignments:elixir params)))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-var-assignments-multiple ()
|
||||||
|
(let ((params '((:var . ("x" . 5))
|
||||||
|
(:var . ("y" . 10)))))
|
||||||
|
(let ((assignments (org-babel-variable-assignments:elixir params)))
|
||||||
|
(should (= 2 (length assignments)))
|
||||||
|
(should (member "x = 5" assignments))
|
||||||
|
(should (member "y = 10" assignments)))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-var-assignments-string ()
|
||||||
|
(let ((params '((:var . ("name" . "Alice")))))
|
||||||
|
(should (equal '("name = \"Alice\"")
|
||||||
|
(org-babel-variable-assignments:elixir params)))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-var-assignments-list ()
|
||||||
|
(let ((params '((:var . ("data" . (1 2 3))))))
|
||||||
|
(should (equal '("data = [1, 2, 3]")
|
||||||
|
(org-babel-variable-assignments:elixir params)))))
|
||||||
|
|
||||||
|
;;; Execution with Variables
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-execute-with-var ()
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let* ((params '((:var . ("x" . 10))))
|
||||||
|
(var-lines (org-babel-variable-assignments:elixir params))
|
||||||
|
(full-body (concat (mapconcat #'identity var-lines "\n")
|
||||||
|
"\nx * 2")))
|
||||||
|
(should (equal "20" (ob-elixir--execute full-body 'value)))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-execute-with-list-var ()
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let* ((params '((:var . ("nums" . (1 2 3 4 5)))))
|
||||||
|
(var-lines (org-babel-variable-assignments:elixir params))
|
||||||
|
(full-body (concat (mapconcat #'identity var-lines "\n")
|
||||||
|
"\nEnum.sum(nums)")))
|
||||||
|
(should (equal "15" (ob-elixir--execute full-body 'value)))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-execute-with-string-var ()
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let* ((params '((:var . ("name" . "World"))))
|
||||||
|
(var-lines (org-babel-variable-assignments:elixir params))
|
||||||
|
(full-body (concat (mapconcat #'identity var-lines "\n")
|
||||||
|
"\n\"Hello, #{name}!\"")))
|
||||||
|
(should (equal "\"Hello, World!\""
|
||||||
|
(ob-elixir--execute full-body 'value)))))
|
||||||
|
|
||||||
|
(provide 'test-ob-elixir-vars)
|
||||||
|
;;; test-ob-elixir-vars.el ends here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Create result formatting tests
|
||||||
|
|
||||||
|
`test/test-ob-elixir-results.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; test-ob-elixir-results.el --- Result formatting tests -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'ob-elixir)
|
||||||
|
|
||||||
|
;;; Parsing Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-parse-simple-list ()
|
||||||
|
(should (equal '(1 2 3) (ob-elixir--table-or-string "[1, 2, 3]"))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-parse-nested-list ()
|
||||||
|
(should (equal '((1 2) (3 4))
|
||||||
|
(ob-elixir--table-or-string "[[1, 2], [3, 4]]"))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-parse-empty-list ()
|
||||||
|
(should (equal '() (ob-elixir--table-or-string "[]"))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-parse-tuple ()
|
||||||
|
(should (equal '(1 2 3) (ob-elixir--table-or-string "{1, 2, 3}"))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-parse-scalar-number ()
|
||||||
|
(should (equal "42" (ob-elixir--table-or-string "42"))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-parse-scalar-atom ()
|
||||||
|
(should (equal ":ok" (ob-elixir--table-or-string ":ok"))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-parse-scalar-string ()
|
||||||
|
(should (equal "\"hello\"" (ob-elixir--table-or-string "\"hello\""))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-parse-empty ()
|
||||||
|
(should (null (ob-elixir--table-or-string "")))
|
||||||
|
(should (null (ob-elixir--table-or-string " "))))
|
||||||
|
|
||||||
|
;;; Table Sanitization Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-sanitize-nil-values ()
|
||||||
|
(let ((ob-elixir-nil-to 'hline))
|
||||||
|
(should (equal '((1 hline) (hline 2))
|
||||||
|
(ob-elixir--sanitize-table '((1 nil) (nil 2)))))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-sanitize-nested ()
|
||||||
|
(let ((ob-elixir-nil-to 'hline))
|
||||||
|
(should (equal '((1 2) (3 4))
|
||||||
|
(ob-elixir--sanitize-table '((1 2) (3 4)))))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-sanitize-simple ()
|
||||||
|
(should (equal '(1 2 3)
|
||||||
|
(ob-elixir--sanitize-table '(1 2 3)))))
|
||||||
|
|
||||||
|
;;; Integration Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-full-result-list ()
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((result (ob-elixir--table-or-string
|
||||||
|
(ob-elixir--execute "[1, 2, 3]" 'value))))
|
||||||
|
(should (equal '(1 2 3) result))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-full-result-table ()
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((result (ob-elixir--table-or-string
|
||||||
|
(ob-elixir--execute "[[1, 2], [3, 4]]" 'value))))
|
||||||
|
(should (equal '((1 2) (3 4)) result))))
|
||||||
|
|
||||||
|
(provide 'test-ob-elixir-results)
|
||||||
|
;;; test-ob-elixir-results.el ends here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Create error handling tests
|
||||||
|
|
||||||
|
`test/test-ob-elixir-errors.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; test-ob-elixir-errors.el --- Error handling tests -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'ob-elixir)
|
||||||
|
|
||||||
|
;;; Error Detection Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-detect-runtime-error ()
|
||||||
|
(let ((output "** (RuntimeError) something went wrong"))
|
||||||
|
(should (ob-elixir--detect-error output))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-detect-compile-error ()
|
||||||
|
(let ((output "** (CompileError) test.exs:1: undefined function"))
|
||||||
|
(should (ob-elixir--detect-error output))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-detect-no-error ()
|
||||||
|
(should-not (ob-elixir--detect-error "42"))
|
||||||
|
(should-not (ob-elixir--detect-error "[1, 2, 3]"))
|
||||||
|
(should-not (ob-elixir--detect-error ":ok")))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-error-type-runtime ()
|
||||||
|
(let* ((output "** (RuntimeError) test error")
|
||||||
|
(info (ob-elixir--detect-error output)))
|
||||||
|
(should (eq 'runtime (plist-get info :type)))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-error-type-compile ()
|
||||||
|
(let* ((output "** (CompileError) syntax error")
|
||||||
|
(info (ob-elixir--detect-error output)))
|
||||||
|
(should (eq 'compile (plist-get info :type)))))
|
||||||
|
|
||||||
|
;;; Error Execution Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-runtime-error-no-signal ()
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((ob-elixir-signal-errors nil))
|
||||||
|
(let ((result (ob-elixir--execute "raise \"test\"" 'value)))
|
||||||
|
(should (string-match-p "RuntimeError" result)))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-runtime-error-signal ()
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((ob-elixir-signal-errors t))
|
||||||
|
(should-error (ob-elixir--execute "raise \"test\"" 'value)
|
||||||
|
:type 'ob-elixir-runtime-error)))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-compile-error ()
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((ob-elixir-signal-errors nil))
|
||||||
|
(let ((result (ob-elixir--execute "def incomplete(" 'value)))
|
||||||
|
(should (string-match-p "\\(SyntaxError\\|TokenMissingError\\)" result)))))
|
||||||
|
|
||||||
|
(provide 'test-ob-elixir-errors)
|
||||||
|
;;; test-ob-elixir-errors.el ends here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Create org integration tests
|
||||||
|
|
||||||
|
`test/test-ob-elixir-org.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; test-ob-elixir-org.el --- Org integration tests -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'org)
|
||||||
|
(require 'ob)
|
||||||
|
(require 'ob-elixir)
|
||||||
|
|
||||||
|
;;; Helper Functions
|
||||||
|
|
||||||
|
(defun ob-elixir-test--execute-src-block (code &optional header-args)
|
||||||
|
"Execute CODE as an Elixir src block with HEADER-ARGS."
|
||||||
|
(with-temp-buffer
|
||||||
|
(org-mode)
|
||||||
|
(insert (format "#+BEGIN_SRC elixir%s\n%s\n#+END_SRC"
|
||||||
|
(if header-args (concat " " header-args) "")
|
||||||
|
code))
|
||||||
|
(goto-char (point-min))
|
||||||
|
(forward-line 1)
|
||||||
|
(org-babel-execute-src-block)))
|
||||||
|
|
||||||
|
;;; Basic Org Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-org-simple ()
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(should (equal "2" (ob-elixir-test--execute-src-block "1 + 1"))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-org-with-var ()
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(should (equal "20" (ob-elixir-test--execute-src-block "x * 2" ":var x=10"))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-org-results-output ()
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(should (equal "hello"
|
||||||
|
(ob-elixir-test--execute-src-block
|
||||||
|
"IO.puts(\"hello\")"
|
||||||
|
":results output"))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-org-results-value ()
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(should (equal '(1 2 3)
|
||||||
|
(ob-elixir-test--execute-src-block
|
||||||
|
"[1, 2, 3]"
|
||||||
|
":results value"))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-org-results-verbatim ()
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(should (stringp (ob-elixir-test--execute-src-block
|
||||||
|
"[1, 2, 3]"
|
||||||
|
":results verbatim"))))
|
||||||
|
|
||||||
|
;;; Table Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-org-table-result ()
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((result (ob-elixir-test--execute-src-block "[[1, 2], [3, 4]]")))
|
||||||
|
(should (equal '((1 2) (3 4)) result))))
|
||||||
|
|
||||||
|
(provide 'test-ob-elixir-org)
|
||||||
|
;;; test-ob-elixir-org.el ends here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 8: Update Makefile
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
EMACS ?= emacs
|
||||||
|
BATCH = $(EMACS) -Q -batch -L . -L test
|
||||||
|
|
||||||
|
.PHONY: all compile test test-unit test-integration lint clean
|
||||||
|
|
||||||
|
all: compile test
|
||||||
|
|
||||||
|
compile:
|
||||||
|
$(BATCH) -f batch-byte-compile ob-elixir.el
|
||||||
|
|
||||||
|
test: test-unit
|
||||||
|
|
||||||
|
test-unit:
|
||||||
|
$(BATCH) -l ert \
|
||||||
|
-l test/test-ob-elixir.el \
|
||||||
|
-f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
|
test-integration:
|
||||||
|
$(BATCH) -l ert \
|
||||||
|
-l org -l ob \
|
||||||
|
-l test/test-ob-elixir.el \
|
||||||
|
-f ert-run-tests-batch-and-exit
|
||||||
|
|
||||||
|
lint:
|
||||||
|
$(BATCH) --eval "(require 'package)" \
|
||||||
|
--eval "(package-initialize)" \
|
||||||
|
--eval "(unless (package-installed-p 'package-lint) \
|
||||||
|
(package-refresh-contents) \
|
||||||
|
(package-install 'package-lint))" \
|
||||||
|
-l package-lint \
|
||||||
|
-f package-lint-batch-and-exit ob-elixir.el
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f *.elc test/*.elc
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] All test files created and organized
|
||||||
|
- [ ] `make test` runs all tests
|
||||||
|
- [ ] Tests cover core execution, variables, results, and errors
|
||||||
|
- [ ] Org integration tests work
|
||||||
|
- [ ] Tests can run in CI (batch mode)
|
||||||
|
- [ ] Test coverage is comprehensive (major code paths)
|
||||||
|
|
||||||
|
## Test Coverage Goals
|
||||||
|
|
||||||
|
| Component | Tests | Coverage |
|
||||||
|
|-----------|-------|----------|
|
||||||
|
| Type conversion | 12+ tests | All Elisp types |
|
||||||
|
| Execution | 10+ tests | Value/output, types |
|
||||||
|
| Variables | 8+ tests | All var scenarios |
|
||||||
|
| Results | 8+ tests | Parsing, tables |
|
||||||
|
| Errors | 6+ tests | Detection, signaling |
|
||||||
|
| Org integration | 6+ tests | Full workflow |
|
||||||
|
|
||||||
|
## Files Created
|
||||||
|
|
||||||
|
- `test/test-ob-elixir.el` - Main test file
|
||||||
|
- `test/test-ob-elixir-core.el` - Core tests
|
||||||
|
- `test/test-ob-elixir-vars.el` - Variable tests
|
||||||
|
- `test/test-ob-elixir-results.el` - Result tests
|
||||||
|
- `test/test-ob-elixir-errors.el` - Error tests
|
||||||
|
- `test/test-ob-elixir-org.el` - Org integration tests
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [docs/02-testing-emacs-elisp.md](../docs/02-testing-emacs-elisp.md)
|
||||||
|
- [ERT Manual](https://www.gnu.org/software/emacs/manual/html_node/ert/)
|
||||||
442
tasks/07-session-support.md
Normal file
442
tasks/07-session-support.md
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
# Task 07: IEx Session Support
|
||||||
|
|
||||||
|
**Phase**: 2 - Sessions
|
||||||
|
**Priority**: High
|
||||||
|
**Estimated Time**: 3-4 hours
|
||||||
|
**Dependencies**: Phase 1 Complete
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Implement IEx session support so code blocks can share state when using `:session` header argument.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Phase 1 complete
|
||||||
|
- Understanding of Emacs comint mode
|
||||||
|
- IEx (Elixir REPL) available
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Sessions allow multiple code blocks to share state:
|
||||||
|
|
||||||
|
```org
|
||||||
|
#+BEGIN_SRC elixir :session my-session
|
||||||
|
x = 42
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :session my-session
|
||||||
|
x * 2 # Can use x from previous block
|
||||||
|
#+END_SRC
|
||||||
|
```
|
||||||
|
|
||||||
|
This requires:
|
||||||
|
1. Starting an IEx process
|
||||||
|
2. Managing the process via comint
|
||||||
|
3. Sending code and capturing output
|
||||||
|
4. Proper prompt detection
|
||||||
|
5. Session cleanup
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### Step 1: Add session configuration
|
||||||
|
|
||||||
|
Add to `ob-elixir.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Session Configuration
|
||||||
|
|
||||||
|
(defcustom ob-elixir-iex-command "iex"
|
||||||
|
"Command to start IEx session."
|
||||||
|
:type 'string
|
||||||
|
:group 'ob-elixir
|
||||||
|
:safe #'stringp)
|
||||||
|
|
||||||
|
(defconst ob-elixir--prompt-regexp
|
||||||
|
"^\\(?:iex\\|\\.\\.\\)([0-9]+)> "
|
||||||
|
"Regexp matching IEx prompt.
|
||||||
|
Matches both regular prompt 'iex(N)> ' and continuation '...(N)> '.")
|
||||||
|
|
||||||
|
(defconst ob-elixir--eoe-marker
|
||||||
|
"__ob_elixir_eoe_marker__"
|
||||||
|
"End-of-evaluation marker for session output.")
|
||||||
|
|
||||||
|
(defvar ob-elixir--sessions (make-hash-table :test 'equal)
|
||||||
|
"Hash table mapping session names to buffer names.")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Implement session initialization
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Session Management
|
||||||
|
|
||||||
|
(require 'ob-comint)
|
||||||
|
|
||||||
|
(defun org-babel-elixir-initiate-session (&optional session params)
|
||||||
|
"Create or return an Elixir session buffer.
|
||||||
|
|
||||||
|
SESSION is the session name (string or nil).
|
||||||
|
PARAMS are the header arguments.
|
||||||
|
|
||||||
|
Returns the session buffer, or nil if SESSION is \"none\"."
|
||||||
|
(unless (or (not session) (string= session "none"))
|
||||||
|
(let* ((session-name (if (stringp session) session "default"))
|
||||||
|
(buffer (ob-elixir--get-or-create-session session-name params)))
|
||||||
|
(when buffer
|
||||||
|
(puthash session-name (buffer-name buffer) ob-elixir--sessions))
|
||||||
|
buffer)))
|
||||||
|
|
||||||
|
(defun ob-elixir--get-or-create-session (name params)
|
||||||
|
"Get or create an IEx session named NAME with PARAMS."
|
||||||
|
(let* ((buffer-name (format "*ob-elixir:%s*" name))
|
||||||
|
(existing (get-buffer buffer-name)))
|
||||||
|
(if (and existing (org-babel-comint-buffer-livep existing))
|
||||||
|
existing
|
||||||
|
(ob-elixir--start-session buffer-name name params))))
|
||||||
|
|
||||||
|
(defun ob-elixir--start-session (buffer-name session-name params)
|
||||||
|
"Start a new IEx session in BUFFER-NAME."
|
||||||
|
(let* ((buffer (get-buffer-create buffer-name))
|
||||||
|
(process-environment (cons "TERM=dumb" process-environment)))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
;; Start the IEx process
|
||||||
|
(make-comint-in-buffer
|
||||||
|
(format "ob-elixir-%s" session-name)
|
||||||
|
buffer
|
||||||
|
ob-elixir-iex-command
|
||||||
|
nil)
|
||||||
|
|
||||||
|
;; Wait for initial prompt
|
||||||
|
(ob-elixir--wait-for-prompt buffer 10)
|
||||||
|
|
||||||
|
;; Configure IEx for programmatic use
|
||||||
|
(ob-elixir--configure-session buffer)
|
||||||
|
|
||||||
|
buffer)))
|
||||||
|
|
||||||
|
(defun ob-elixir--configure-session (buffer)
|
||||||
|
"Configure IEx session in BUFFER for programmatic use."
|
||||||
|
(let ((config-commands
|
||||||
|
'("IEx.configure(colors: [enabled: false])"
|
||||||
|
"IEx.configure(inspect: [limit: :infinity, printable_limit: :infinity])")))
|
||||||
|
(dolist (cmd config-commands)
|
||||||
|
(ob-elixir--send-command buffer cmd)
|
||||||
|
(ob-elixir--wait-for-prompt buffer 5))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Implement prompt detection
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun ob-elixir--wait-for-prompt (buffer timeout)
|
||||||
|
"Wait for IEx prompt in BUFFER with TIMEOUT seconds."
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(let ((end-time (+ (float-time) timeout)))
|
||||||
|
(while (and (< (float-time) end-time)
|
||||||
|
(not (ob-elixir--at-prompt-p)))
|
||||||
|
(accept-process-output (get-buffer-process buffer) 0.1)
|
||||||
|
(goto-char (point-max)))
|
||||||
|
(ob-elixir--at-prompt-p))))
|
||||||
|
|
||||||
|
(defun ob-elixir--at-prompt-p ()
|
||||||
|
"Return t if the last line in buffer looks like an IEx prompt."
|
||||||
|
(save-excursion
|
||||||
|
(goto-char (point-max))
|
||||||
|
(forward-line 0)
|
||||||
|
(looking-at ob-elixir--prompt-regexp)))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Implement command sending
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun ob-elixir--send-command (buffer command)
|
||||||
|
"Send COMMAND to IEx process in BUFFER."
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(goto-char (point-max))
|
||||||
|
(insert command)
|
||||||
|
(comint-send-input nil t)))
|
||||||
|
|
||||||
|
(defun ob-elixir--evaluate-in-session (session body result-type)
|
||||||
|
"Evaluate BODY in SESSION, return result.
|
||||||
|
|
||||||
|
RESULT-TYPE is 'value or 'output."
|
||||||
|
(let* ((buffer (org-babel-elixir-initiate-session session nil))
|
||||||
|
(code (if (eq result-type 'value)
|
||||||
|
(ob-elixir--session-wrap-for-value body)
|
||||||
|
body))
|
||||||
|
(start-marker nil)
|
||||||
|
output)
|
||||||
|
(unless buffer
|
||||||
|
(error "Failed to create Elixir session: %s" session))
|
||||||
|
|
||||||
|
(with-current-buffer buffer
|
||||||
|
;; Mark position before output
|
||||||
|
(goto-char (point-max))
|
||||||
|
(setq start-marker (point-marker))
|
||||||
|
|
||||||
|
;; Send the code
|
||||||
|
(ob-elixir--send-command buffer code)
|
||||||
|
(ob-elixir--wait-for-prompt buffer 30)
|
||||||
|
|
||||||
|
;; Send EOE marker
|
||||||
|
(ob-elixir--send-command buffer
|
||||||
|
(format "\"%s\"" ob-elixir--eoe-marker))
|
||||||
|
(ob-elixir--wait-for-prompt buffer 5)
|
||||||
|
|
||||||
|
;; Extract output
|
||||||
|
(setq output (ob-elixir--extract-session-output
|
||||||
|
buffer start-marker)))
|
||||||
|
|
||||||
|
(ob-elixir--clean-session-output output)))
|
||||||
|
|
||||||
|
(defconst ob-elixir--session-value-wrapper
|
||||||
|
"_ob_result_ = (
|
||||||
|
%s
|
||||||
|
)
|
||||||
|
IO.puts(\"__ob_value_start__\")
|
||||||
|
IO.puts(inspect(_ob_result_, limit: :infinity, printable_limit: :infinity))
|
||||||
|
IO.puts(\"__ob_value_end__\")
|
||||||
|
:ok
|
||||||
|
"
|
||||||
|
"Wrapper for capturing value in session mode.")
|
||||||
|
|
||||||
|
(defun ob-elixir--session-wrap-for-value (body)
|
||||||
|
"Wrap BODY to capture its value in session mode."
|
||||||
|
(format ob-elixir--session-value-wrapper body))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Implement output extraction
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun ob-elixir--extract-session-output (buffer start-marker)
|
||||||
|
"Extract output from BUFFER since START-MARKER."
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(let ((end-pos (point-max)))
|
||||||
|
(buffer-substring-no-properties start-marker end-pos))))
|
||||||
|
|
||||||
|
(defun ob-elixir--clean-session-output (output)
|
||||||
|
"Clean OUTPUT from IEx session."
|
||||||
|
(let ((result output))
|
||||||
|
;; Remove ANSI escape codes
|
||||||
|
(setq result (ansi-color-filter-apply result))
|
||||||
|
|
||||||
|
;; Remove prompts
|
||||||
|
(setq result (replace-regexp-in-string
|
||||||
|
ob-elixir--prompt-regexp "" result))
|
||||||
|
|
||||||
|
;; Remove the input echo
|
||||||
|
(setq result (replace-regexp-in-string
|
||||||
|
"^.*\n" "" result nil nil nil 1))
|
||||||
|
|
||||||
|
;; Remove EOE marker
|
||||||
|
(setq result (replace-regexp-in-string
|
||||||
|
(format "\"%s\"" ob-elixir--eoe-marker) "" result))
|
||||||
|
|
||||||
|
;; Extract value if using value wrapper
|
||||||
|
(when (string-match "__ob_value_start__\n\\(.*\\)\n__ob_value_end__" result)
|
||||||
|
(setq result (match-string 1 result)))
|
||||||
|
|
||||||
|
;; Remove :ok from wrapper
|
||||||
|
(setq result (replace-regexp-in-string ":ok\n*$" "" result))
|
||||||
|
|
||||||
|
(string-trim result)))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Update execute function
|
||||||
|
|
||||||
|
Modify `org-babel-execute:elixir`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun org-babel-execute:elixir (body params)
|
||||||
|
"Execute a block of Elixir code with org-babel."
|
||||||
|
(let* ((session (cdr (assq :session params)))
|
||||||
|
(result-type (cdr (assq :result-type params)))
|
||||||
|
(result-params (cdr (assq :result-params params)))
|
||||||
|
(full-body (org-babel-expand-body:generic
|
||||||
|
body params
|
||||||
|
(org-babel-variable-assignments:elixir params)))
|
||||||
|
(result (if (and session (not (string= session "none")))
|
||||||
|
;; Session mode
|
||||||
|
(ob-elixir--evaluate-in-session session full-body result-type)
|
||||||
|
;; External process mode
|
||||||
|
(ob-elixir--execute full-body result-type))))
|
||||||
|
(org-babel-reassemble-table
|
||||||
|
(org-babel-result-cond result-params
|
||||||
|
result
|
||||||
|
(ob-elixir--table-or-string result))
|
||||||
|
(org-babel-pick-name (cdr (assq :colname-names params))
|
||||||
|
(cdr (assq :colnames params)))
|
||||||
|
(org-babel-pick-name (cdr (assq :rowname-names params))
|
||||||
|
(cdr (assq :rownames params))))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Implement prep-session
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun org-babel-prep-session:elixir (session params)
|
||||||
|
"Prepare SESSION according to PARAMS.
|
||||||
|
|
||||||
|
Sends variable assignments to the session."
|
||||||
|
(let ((buffer (org-babel-elixir-initiate-session session params))
|
||||||
|
(var-lines (org-babel-variable-assignments:elixir params)))
|
||||||
|
(when (and buffer var-lines)
|
||||||
|
(dolist (var-line var-lines)
|
||||||
|
(ob-elixir--send-command buffer var-line)
|
||||||
|
(ob-elixir--wait-for-prompt buffer 5)))
|
||||||
|
buffer))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 8: Implement session cleanup
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun ob-elixir-kill-session (session)
|
||||||
|
"Kill the Elixir session named SESSION."
|
||||||
|
(interactive
|
||||||
|
(list (completing-read "Kill session: "
|
||||||
|
(hash-table-keys ob-elixir--sessions))))
|
||||||
|
(let ((buffer-name (gethash session ob-elixir--sessions)))
|
||||||
|
(when buffer-name
|
||||||
|
(let ((buffer (get-buffer buffer-name)))
|
||||||
|
(when buffer
|
||||||
|
(let ((process (get-buffer-process buffer)))
|
||||||
|
(when process
|
||||||
|
(delete-process process)))
|
||||||
|
(kill-buffer buffer)))
|
||||||
|
(remhash session ob-elixir--sessions))))
|
||||||
|
|
||||||
|
(defun ob-elixir-kill-all-sessions ()
|
||||||
|
"Kill all Elixir sessions."
|
||||||
|
(interactive)
|
||||||
|
(maphash (lambda (name _buffer)
|
||||||
|
(ob-elixir-kill-session name))
|
||||||
|
ob-elixir--sessions))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 9: Add tests
|
||||||
|
|
||||||
|
Add to `test/test-ob-elixir-sessions.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; test-ob-elixir-sessions.el --- Session tests -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'ob-elixir)
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-session-creation ()
|
||||||
|
"Test session creation."
|
||||||
|
(skip-unless (executable-find ob-elixir-iex-command))
|
||||||
|
(unwind-protect
|
||||||
|
(let ((buffer (org-babel-elixir-initiate-session "test-create" nil)))
|
||||||
|
(should buffer)
|
||||||
|
(should (org-babel-comint-buffer-livep buffer)))
|
||||||
|
(ob-elixir-kill-session "test-create")))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-session-persistence ()
|
||||||
|
"Test that sessions persist state."
|
||||||
|
(skip-unless (executable-find ob-elixir-iex-command))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
;; First evaluation - define variable
|
||||||
|
(ob-elixir--evaluate-in-session "test-persist" "x = 42" 'value)
|
||||||
|
;; Second evaluation - use variable
|
||||||
|
(let ((result (ob-elixir--evaluate-in-session
|
||||||
|
"test-persist" "x * 2" 'value)))
|
||||||
|
(should (equal "84" result))))
|
||||||
|
(ob-elixir-kill-session "test-persist")))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-session-none ()
|
||||||
|
"Test that :session none uses external process."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(should (null (org-babel-elixir-initiate-session "none" nil))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-session-module-def ()
|
||||||
|
"Test defining module in session."
|
||||||
|
(skip-unless (executable-find ob-elixir-iex-command))
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(ob-elixir--evaluate-in-session
|
||||||
|
"test-module"
|
||||||
|
"defmodule TestMod do\n def double(x), do: x * 2\nend"
|
||||||
|
'value)
|
||||||
|
(let ((result (ob-elixir--evaluate-in-session
|
||||||
|
"test-module" "TestMod.double(21)" 'value)))
|
||||||
|
(should (equal "42" result))))
|
||||||
|
(ob-elixir-kill-session "test-module")))
|
||||||
|
|
||||||
|
(provide 'test-ob-elixir-sessions)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 10: Test in org buffer
|
||||||
|
|
||||||
|
Create session tests in `test.org`:
|
||||||
|
|
||||||
|
```org
|
||||||
|
* Session Tests
|
||||||
|
|
||||||
|
** Define variable in session
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :session my-session
|
||||||
|
x = 42
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** Use variable from session
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :session my-session
|
||||||
|
x * 2
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS:
|
||||||
|
: 84
|
||||||
|
|
||||||
|
** Define module in session
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :session my-session
|
||||||
|
defmodule Helper do
|
||||||
|
def greet(name), do: "Hello, #{name}!"
|
||||||
|
end
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** Use module from session
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :session my-session
|
||||||
|
Helper.greet("World")
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
#+RESULTS:
|
||||||
|
: "Hello, World!"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] `:session name` creates persistent IEx session
|
||||||
|
- [ ] Variables persist across blocks in same session
|
||||||
|
- [ ] Module definitions persist
|
||||||
|
- [ ] `:session none` uses external process (default)
|
||||||
|
- [ ] Multiple named sessions work independently
|
||||||
|
- [ ] Sessions can be killed with `ob-elixir-kill-session`
|
||||||
|
- [ ] Proper prompt detection
|
||||||
|
- [ ] Output is clean (no prompts, ANSI codes)
|
||||||
|
- [ ] All tests pass
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Session hangs
|
||||||
|
|
||||||
|
Check for proper prompt detection. IEx prompts can vary.
|
||||||
|
|
||||||
|
### ANSI codes in output
|
||||||
|
|
||||||
|
The `ansi-color-filter-apply` should remove them. Check TERM environment variable.
|
||||||
|
|
||||||
|
### Process dies unexpectedly
|
||||||
|
|
||||||
|
Check for Elixir errors. May need to handle compilation errors in session context.
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
- `ob-elixir.el` - Add session support
|
||||||
|
- `test/test-ob-elixir-sessions.el` - Add session tests
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [docs/03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md) - Session Management
|
||||||
|
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - IEx Session Management
|
||||||
|
- [Emacs Comint Mode](https://www.gnu.org/software/emacs/manual/html_node/emacs/Shell-Mode.html)
|
||||||
421
tasks/08-mix-project-support.md
Normal file
421
tasks/08-mix-project-support.md
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
# Task 08: Mix Project Support
|
||||||
|
|
||||||
|
**Phase**: 3 - Mix Integration
|
||||||
|
**Priority**: High
|
||||||
|
**Estimated Time**: 2-3 hours
|
||||||
|
**Dependencies**: Task 07 (Session Support) or Phase 1 complete
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Implement Mix project support so Elixir code can be executed within the context of a Mix project, with access to project dependencies and modules.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Phase 1 complete (or Phase 2 for session+mix)
|
||||||
|
- Understanding of Mix build tool
|
||||||
|
- A test Mix project
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Mix projects have:
|
||||||
|
- Dependencies in `mix.exs`
|
||||||
|
- Compiled modules in `_build/`
|
||||||
|
- Configuration in `config/`
|
||||||
|
|
||||||
|
To execute code in project context, we need:
|
||||||
|
1. Run code from the project directory
|
||||||
|
2. Use `mix run` for one-shot execution
|
||||||
|
3. Use `iex -S mix` for sessions
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### Step 1: Add Mix configuration
|
||||||
|
|
||||||
|
Add to `ob-elixir.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Mix Configuration
|
||||||
|
|
||||||
|
(defcustom ob-elixir-mix-command "mix"
|
||||||
|
"Command to run Mix."
|
||||||
|
:type 'string
|
||||||
|
:group 'ob-elixir
|
||||||
|
:safe #'stringp)
|
||||||
|
|
||||||
|
(defcustom ob-elixir-auto-detect-mix t
|
||||||
|
"Whether to automatically detect Mix projects.
|
||||||
|
|
||||||
|
When non-nil and no :mix-project is specified, ob-elixir will
|
||||||
|
search upward from the org file for a mix.exs file."
|
||||||
|
:type 'boolean
|
||||||
|
:group 'ob-elixir)
|
||||||
|
|
||||||
|
(defconst org-babel-header-args:elixir
|
||||||
|
'((mix-project . :any) ; Path to Mix project root
|
||||||
|
(mix-env . :any) ; MIX_ENV (dev, test, prod)
|
||||||
|
(mix-target . :any)) ; MIX_TARGET for Nerves, etc.
|
||||||
|
"Elixir-specific header arguments.")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Implement Mix project detection
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Mix Project Detection
|
||||||
|
|
||||||
|
(defun ob-elixir--find-mix-project (&optional start-dir)
|
||||||
|
"Find Mix project root by searching for mix.exs.
|
||||||
|
|
||||||
|
Starts from START-DIR (default: current directory) and searches
|
||||||
|
upward. Returns the directory containing mix.exs, or nil."
|
||||||
|
(let* ((dir (or start-dir default-directory))
|
||||||
|
(found (locate-dominating-file dir "mix.exs")))
|
||||||
|
(when found
|
||||||
|
(file-name-directory found))))
|
||||||
|
|
||||||
|
(defun ob-elixir--resolve-mix-project (params)
|
||||||
|
"Resolve Mix project path from PARAMS or auto-detection.
|
||||||
|
|
||||||
|
Returns project path or nil."
|
||||||
|
(let ((explicit (cdr (assq :mix-project params))))
|
||||||
|
(cond
|
||||||
|
;; Explicit project path
|
||||||
|
((and explicit (not (eq explicit 'no)))
|
||||||
|
(expand-file-name explicit))
|
||||||
|
;; Explicitly disabled
|
||||||
|
((eq explicit 'no)
|
||||||
|
nil)
|
||||||
|
;; Auto-detect if enabled
|
||||||
|
(ob-elixir-auto-detect-mix
|
||||||
|
(ob-elixir--find-mix-project))
|
||||||
|
;; No project
|
||||||
|
(t nil))))
|
||||||
|
|
||||||
|
(defun ob-elixir--in-mix-project-p (params)
|
||||||
|
"Return t if execution should happen in Mix project context."
|
||||||
|
(not (null (ob-elixir--resolve-mix-project params))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Implement Mix execution
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Mix Execution
|
||||||
|
|
||||||
|
(defun ob-elixir--execute-with-mix (body result-type params)
|
||||||
|
"Execute BODY in Mix project context.
|
||||||
|
|
||||||
|
RESULT-TYPE is 'value or 'output.
|
||||||
|
PARAMS contains header arguments including :mix-project."
|
||||||
|
(let* ((project-dir (ob-elixir--resolve-mix-project params))
|
||||||
|
(mix-env (cdr (assq :mix-env params)))
|
||||||
|
(mix-target (cdr (assq :mix-target params)))
|
||||||
|
(default-directory project-dir)
|
||||||
|
(tmp-file (org-babel-temp-file "ob-elixir-mix-" ".exs"))
|
||||||
|
(code (if (eq result-type 'value)
|
||||||
|
(ob-elixir--wrap-for-value body)
|
||||||
|
body))
|
||||||
|
(env-vars (ob-elixir--build-mix-env mix-env mix-target)))
|
||||||
|
|
||||||
|
;; Write code to temp file
|
||||||
|
(with-temp-file tmp-file
|
||||||
|
(insert code))
|
||||||
|
|
||||||
|
;; Execute with mix run
|
||||||
|
(let ((command (format "%s%s run %s"
|
||||||
|
env-vars
|
||||||
|
ob-elixir-mix-command
|
||||||
|
(org-babel-process-file-name tmp-file))))
|
||||||
|
(ob-elixir--process-result
|
||||||
|
(shell-command-to-string command)))))
|
||||||
|
|
||||||
|
(defun ob-elixir--build-mix-env (mix-env mix-target)
|
||||||
|
"Build environment variable prefix for Mix execution."
|
||||||
|
(let ((vars '()))
|
||||||
|
(when mix-env
|
||||||
|
(push (format "MIX_ENV=%s" mix-env) vars))
|
||||||
|
(when mix-target
|
||||||
|
(push (format "MIX_TARGET=%s" mix-target) vars))
|
||||||
|
(if vars
|
||||||
|
(concat (mapconcat #'identity vars " ") " ")
|
||||||
|
"")))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Update execute function
|
||||||
|
|
||||||
|
Modify `org-babel-execute:elixir`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun org-babel-execute:elixir (body params)
|
||||||
|
"Execute a block of Elixir code with org-babel."
|
||||||
|
(let* ((session (cdr (assq :session params)))
|
||||||
|
(result-type (cdr (assq :result-type params)))
|
||||||
|
(result-params (cdr (assq :result-params params)))
|
||||||
|
(mix-project (ob-elixir--resolve-mix-project params))
|
||||||
|
(full-body (org-babel-expand-body:generic
|
||||||
|
body params
|
||||||
|
(org-babel-variable-assignments:elixir params)))
|
||||||
|
(result (cond
|
||||||
|
;; Session mode
|
||||||
|
((and session (not (string= session "none")))
|
||||||
|
(ob-elixir--evaluate-in-session
|
||||||
|
session full-body result-type params))
|
||||||
|
;; Mix project mode
|
||||||
|
(mix-project
|
||||||
|
(ob-elixir--execute-with-mix
|
||||||
|
full-body result-type params))
|
||||||
|
;; Plain execution
|
||||||
|
(t
|
||||||
|
(ob-elixir--execute full-body result-type)))))
|
||||||
|
(org-babel-reassemble-table
|
||||||
|
(org-babel-result-cond result-params
|
||||||
|
result
|
||||||
|
(ob-elixir--table-or-string result))
|
||||||
|
(org-babel-pick-name (cdr (assq :colname-names params))
|
||||||
|
(cdr (assq :colnames params)))
|
||||||
|
(org-babel-pick-name (cdr (assq :rowname-names params))
|
||||||
|
(cdr (assq :rownames params))))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Update session for Mix projects
|
||||||
|
|
||||||
|
Modify session creation to support Mix:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun ob-elixir--start-session (buffer-name session-name params)
|
||||||
|
"Start a new IEx session in BUFFER-NAME."
|
||||||
|
(let* ((mix-project (ob-elixir--resolve-mix-project params))
|
||||||
|
(mix-env (cdr (assq :mix-env params)))
|
||||||
|
(buffer (get-buffer-create buffer-name))
|
||||||
|
(default-directory (or mix-project default-directory))
|
||||||
|
(process-environment
|
||||||
|
(append
|
||||||
|
(list "TERM=dumb")
|
||||||
|
(when mix-env (list (format "MIX_ENV=%s" mix-env)))
|
||||||
|
process-environment)))
|
||||||
|
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(if mix-project
|
||||||
|
;; Start with mix
|
||||||
|
(make-comint-in-buffer
|
||||||
|
(format "ob-elixir-%s" session-name)
|
||||||
|
buffer
|
||||||
|
ob-elixir-iex-command
|
||||||
|
nil
|
||||||
|
"-S" "mix")
|
||||||
|
;; Start plain IEx
|
||||||
|
(make-comint-in-buffer
|
||||||
|
(format "ob-elixir-%s" session-name)
|
||||||
|
buffer
|
||||||
|
ob-elixir-iex-command
|
||||||
|
nil))
|
||||||
|
|
||||||
|
;; Wait for prompt
|
||||||
|
(ob-elixir--wait-for-prompt buffer 30)
|
||||||
|
|
||||||
|
;; Configure IEx
|
||||||
|
(ob-elixir--configure-session buffer)
|
||||||
|
|
||||||
|
buffer)))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Add compilation support
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defcustom ob-elixir-compile-before-run nil
|
||||||
|
"Whether to run mix compile before execution.
|
||||||
|
|
||||||
|
When non-nil, ensures project is compiled before running code.
|
||||||
|
This adds overhead but catches compilation errors early."
|
||||||
|
:type 'boolean
|
||||||
|
:group 'ob-elixir)
|
||||||
|
|
||||||
|
(defun ob-elixir--ensure-compiled (project-dir)
|
||||||
|
"Ensure Mix project at PROJECT-DIR is compiled."
|
||||||
|
(let ((default-directory project-dir))
|
||||||
|
(shell-command-to-string
|
||||||
|
(format "%s compile --force-check" ob-elixir-mix-command))))
|
||||||
|
|
||||||
|
(defun ob-elixir--execute-with-mix (body result-type params)
|
||||||
|
"Execute BODY in Mix project context."
|
||||||
|
(let* ((project-dir (ob-elixir--resolve-mix-project params))
|
||||||
|
(default-directory project-dir))
|
||||||
|
|
||||||
|
;; Optionally compile first
|
||||||
|
(when ob-elixir-compile-before-run
|
||||||
|
(ob-elixir--ensure-compiled project-dir))
|
||||||
|
|
||||||
|
;; ... rest of execution
|
||||||
|
))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Add tests
|
||||||
|
|
||||||
|
Create `test/test-ob-elixir-mix.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; test-ob-elixir-mix.el --- Mix project tests -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'ob-elixir)
|
||||||
|
|
||||||
|
(defvar ob-elixir-test--mix-project-dir nil
|
||||||
|
"Temporary Mix project directory for testing.")
|
||||||
|
|
||||||
|
(defun ob-elixir-test--setup-mix-project ()
|
||||||
|
"Create a temporary Mix project for testing."
|
||||||
|
(let ((dir (make-temp-file "ob-elixir-test-" t)))
|
||||||
|
(setq ob-elixir-test--mix-project-dir dir)
|
||||||
|
(let ((default-directory dir))
|
||||||
|
;; Create mix.exs
|
||||||
|
(with-temp-file (expand-file-name "mix.exs" dir)
|
||||||
|
(insert "defmodule TestProject.MixProject do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
|
def project do
|
||||||
|
[app: :test_project, version: \"0.1.0\", elixir: \"~> 1.14\"]
|
||||||
|
end
|
||||||
|
end"))
|
||||||
|
;; Create lib directory and module
|
||||||
|
(make-directory (expand-file-name "lib" dir))
|
||||||
|
(with-temp-file (expand-file-name "lib/test_project.ex" dir)
|
||||||
|
(insert "defmodule TestProject do
|
||||||
|
def hello, do: \"Hello from TestProject!\"
|
||||||
|
def add(a, b), do: a + b
|
||||||
|
end")))
|
||||||
|
dir))
|
||||||
|
|
||||||
|
(defun ob-elixir-test--cleanup-mix-project ()
|
||||||
|
"Clean up temporary Mix project."
|
||||||
|
(when ob-elixir-test--mix-project-dir
|
||||||
|
(delete-directory ob-elixir-test--mix-project-dir t)
|
||||||
|
(setq ob-elixir-test--mix-project-dir nil)))
|
||||||
|
|
||||||
|
;;; Tests
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-find-mix-project ()
|
||||||
|
"Test Mix project detection."
|
||||||
|
(skip-unless (executable-find ob-elixir-mix-command))
|
||||||
|
(unwind-protect
|
||||||
|
(let* ((project-dir (ob-elixir-test--setup-mix-project))
|
||||||
|
(sub-dir (expand-file-name "lib" project-dir))
|
||||||
|
(default-directory sub-dir))
|
||||||
|
(should (equal project-dir
|
||||||
|
(ob-elixir--find-mix-project))))
|
||||||
|
(ob-elixir-test--cleanup-mix-project)))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-mix-project-execution ()
|
||||||
|
"Test code execution in Mix project context."
|
||||||
|
(skip-unless (and (executable-find ob-elixir-mix-command)
|
||||||
|
(executable-find ob-elixir-command)))
|
||||||
|
(unwind-protect
|
||||||
|
(let* ((project-dir (ob-elixir-test--setup-mix-project))
|
||||||
|
(params `((:mix-project . ,project-dir))))
|
||||||
|
;; Compile first
|
||||||
|
(let ((default-directory project-dir))
|
||||||
|
(shell-command-to-string "mix compile"))
|
||||||
|
;; Test execution
|
||||||
|
(let ((result (ob-elixir--execute-with-mix
|
||||||
|
"TestProject.hello()" 'value params)))
|
||||||
|
(should (string-match-p "Hello from TestProject" result))))
|
||||||
|
(ob-elixir-test--cleanup-mix-project)))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-mix-env ()
|
||||||
|
"Test MIX_ENV handling."
|
||||||
|
(skip-unless (executable-find ob-elixir-mix-command))
|
||||||
|
(let ((env-str (ob-elixir--build-mix-env "test" nil)))
|
||||||
|
(should (string-match-p "MIX_ENV=test" env-str))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-explicit-no-mix ()
|
||||||
|
"Test disabling Mix with :mix-project no."
|
||||||
|
(let ((params '((:mix-project . no))))
|
||||||
|
(should (null (ob-elixir--resolve-mix-project params)))))
|
||||||
|
|
||||||
|
(provide 'test-ob-elixir-mix)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 8: Test in org buffer
|
||||||
|
|
||||||
|
Create Mix tests in `test.org`:
|
||||||
|
|
||||||
|
```org
|
||||||
|
* Mix Project Tests
|
||||||
|
|
||||||
|
** Using project module (explicit path)
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :mix-project ~/my_elixir_project
|
||||||
|
MyApp.hello()
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** Using project module (auto-detect)
|
||||||
|
|
||||||
|
When this org file is inside a Mix project:
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir
|
||||||
|
MyApp.some_function()
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** With specific MIX_ENV
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :mix-project ~/my_elixir_project :mix-env test
|
||||||
|
Application.get_env(:my_app, :some_config)
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** Session with Mix
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :session mix-session :mix-project ~/my_elixir_project
|
||||||
|
# Has access to project modules
|
||||||
|
alias MyApp.SomeModule
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** Disable auto-detect
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :mix-project no
|
||||||
|
# Plain Elixir, no project context
|
||||||
|
1 + 1
|
||||||
|
#+END_SRC
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] `:mix-project path` executes in specified project
|
||||||
|
- [ ] Auto-detection finds `mix.exs` in parent directories
|
||||||
|
- [ ] `:mix-project no` disables auto-detection
|
||||||
|
- [ ] `:mix-env` sets MIX_ENV correctly
|
||||||
|
- [ ] Project modules are accessible
|
||||||
|
- [ ] Sessions with `:mix-project` use `iex -S mix`
|
||||||
|
- [ ] Compilation errors are reported properly
|
||||||
|
- [ ] All tests pass
|
||||||
|
|
||||||
|
## Header Arguments Reference
|
||||||
|
|
||||||
|
| Argument | Values | Description |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| `:mix-project` | path, `no` | Project path or disable |
|
||||||
|
| `:mix-env` | dev, test, prod | MIX_ENV value |
|
||||||
|
| `:mix-target` | host, target | MIX_TARGET for Nerves |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Module not found
|
||||||
|
|
||||||
|
Ensure project is compiled:
|
||||||
|
```bash
|
||||||
|
cd /path/to/project && mix compile
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependencies not available
|
||||||
|
|
||||||
|
Check that `mix deps.get` has been run.
|
||||||
|
|
||||||
|
### Wrong MIX_ENV
|
||||||
|
|
||||||
|
Explicitly set `:mix-env` header argument.
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
- `ob-elixir.el` - Add Mix support
|
||||||
|
- `test/test-ob-elixir-mix.el` - Add Mix tests
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Mix Project Context
|
||||||
|
- [Mix Documentation](https://hexdocs.pm/mix/Mix.html)
|
||||||
401
tasks/09-remote-shell.md
Normal file
401
tasks/09-remote-shell.md
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
# Task 09: Remote Shell (remsh) Support
|
||||||
|
|
||||||
|
**Phase**: 4 - Advanced Features
|
||||||
|
**Priority**: Medium
|
||||||
|
**Estimated Time**: 2-3 hours
|
||||||
|
**Dependencies**: Task 07 (Session Support)
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Implement remote shell support to connect to running Elixir/Erlang nodes and execute code against them.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Session support implemented (Task 07)
|
||||||
|
- Understanding of Erlang distribution
|
||||||
|
- Running Elixir node for testing
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Elixir/Erlang nodes can connect to each other for distributed computing. The `--remsh` flag allows IEx to connect to a running node:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
iex --sname console --remsh my_app@hostname --cookie secret
|
||||||
|
```
|
||||||
|
|
||||||
|
This is useful for:
|
||||||
|
- Inspecting production systems
|
||||||
|
- Running code against a live application
|
||||||
|
- Debugging distributed systems
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### Step 1: Add remote shell configuration
|
||||||
|
|
||||||
|
Add to `ob-elixir.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Remote Shell Configuration
|
||||||
|
|
||||||
|
(defconst org-babel-header-args:elixir
|
||||||
|
'((mix-project . :any)
|
||||||
|
(mix-env . :any)
|
||||||
|
(remsh . :any) ; Remote node to connect to
|
||||||
|
(node-name . :any) ; --name for local node
|
||||||
|
(node-sname . :any) ; --sname for local node
|
||||||
|
(cookie . :any)) ; Erlang cookie
|
||||||
|
"Elixir-specific header arguments.")
|
||||||
|
|
||||||
|
(defcustom ob-elixir-default-cookie nil
|
||||||
|
"Default Erlang cookie for remote connections.
|
||||||
|
|
||||||
|
Set this if all your nodes use the same cookie.
|
||||||
|
Can be overridden with :cookie header argument."
|
||||||
|
:type '(choice (const nil) string)
|
||||||
|
:group 'ob-elixir)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Implement remote session creation
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Remote Shell Sessions
|
||||||
|
|
||||||
|
(defun ob-elixir--start-remote-session (buffer-name session-name params)
|
||||||
|
"Start a remote shell session in BUFFER-NAME.
|
||||||
|
|
||||||
|
Connects to the node specified in PARAMS."
|
||||||
|
(let* ((remsh (cdr (assq :remsh params)))
|
||||||
|
(node-name (cdr (assq :node-name params)))
|
||||||
|
(node-sname (cdr (assq :node-sname params)))
|
||||||
|
(cookie (or (cdr (assq :cookie params))
|
||||||
|
ob-elixir-default-cookie))
|
||||||
|
(buffer (get-buffer-create buffer-name))
|
||||||
|
(local-name (or node-sname
|
||||||
|
node-name
|
||||||
|
(format "ob_elixir_%d" (random 99999))))
|
||||||
|
(process-environment (cons "TERM=dumb" process-environment)))
|
||||||
|
|
||||||
|
(unless remsh
|
||||||
|
(error "No remote node specified. Use :remsh header argument"))
|
||||||
|
|
||||||
|
(with-current-buffer buffer
|
||||||
|
;; Build command arguments
|
||||||
|
(let ((args (append
|
||||||
|
;; Local node name
|
||||||
|
(if node-name
|
||||||
|
(list "--name" node-name)
|
||||||
|
(list "--sname" local-name))
|
||||||
|
;; Cookie
|
||||||
|
(when cookie
|
||||||
|
(list "--cookie" cookie))
|
||||||
|
;; Remote shell
|
||||||
|
(list "--remsh" remsh))))
|
||||||
|
|
||||||
|
(apply #'make-comint-in-buffer
|
||||||
|
(format "ob-elixir-remsh-%s" session-name)
|
||||||
|
buffer
|
||||||
|
ob-elixir-iex-command
|
||||||
|
nil
|
||||||
|
args))
|
||||||
|
|
||||||
|
;; Wait for connection
|
||||||
|
(unless (ob-elixir--wait-for-prompt buffer 30)
|
||||||
|
(error "Failed to connect to remote node: %s" remsh))
|
||||||
|
|
||||||
|
;; Configure session
|
||||||
|
(ob-elixir--configure-session buffer)
|
||||||
|
|
||||||
|
buffer)))
|
||||||
|
|
||||||
|
(defun ob-elixir--is-remote-session-p (params)
|
||||||
|
"Return t if PARAMS specify a remote shell connection."
|
||||||
|
(not (null (cdr (assq :remsh params)))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Update session creation dispatcher
|
||||||
|
|
||||||
|
Modify `ob-elixir--start-session`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun ob-elixir--start-session (buffer-name session-name params)
|
||||||
|
"Start a new IEx session in BUFFER-NAME."
|
||||||
|
(cond
|
||||||
|
;; Remote shell
|
||||||
|
((ob-elixir--is-remote-session-p params)
|
||||||
|
(ob-elixir--start-remote-session buffer-name session-name params))
|
||||||
|
|
||||||
|
;; Mix project
|
||||||
|
((ob-elixir--resolve-mix-project params)
|
||||||
|
(ob-elixir--start-mix-session buffer-name session-name params))
|
||||||
|
|
||||||
|
;; Plain IEx
|
||||||
|
(t
|
||||||
|
(ob-elixir--start-plain-session buffer-name session-name params))))
|
||||||
|
|
||||||
|
(defun ob-elixir--start-plain-session (buffer-name session-name params)
|
||||||
|
"Start a plain IEx session in BUFFER-NAME."
|
||||||
|
(let* ((buffer (get-buffer-create buffer-name))
|
||||||
|
(process-environment (cons "TERM=dumb" process-environment)))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(make-comint-in-buffer
|
||||||
|
(format "ob-elixir-%s" session-name)
|
||||||
|
buffer
|
||||||
|
ob-elixir-iex-command
|
||||||
|
nil)
|
||||||
|
|
||||||
|
(ob-elixir--wait-for-prompt buffer 10)
|
||||||
|
(ob-elixir--configure-session buffer)
|
||||||
|
buffer)))
|
||||||
|
|
||||||
|
(defun ob-elixir--start-mix-session (buffer-name session-name params)
|
||||||
|
"Start an IEx session with Mix in BUFFER-NAME."
|
||||||
|
(let* ((mix-project (ob-elixir--resolve-mix-project params))
|
||||||
|
(mix-env (cdr (assq :mix-env params)))
|
||||||
|
(buffer (get-buffer-create buffer-name))
|
||||||
|
(default-directory mix-project)
|
||||||
|
(process-environment
|
||||||
|
(append
|
||||||
|
(list "TERM=dumb")
|
||||||
|
(when mix-env (list (format "MIX_ENV=%s" mix-env)))
|
||||||
|
process-environment)))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(make-comint-in-buffer
|
||||||
|
(format "ob-elixir-%s" session-name)
|
||||||
|
buffer
|
||||||
|
ob-elixir-iex-command
|
||||||
|
nil
|
||||||
|
"-S" "mix")
|
||||||
|
|
||||||
|
(ob-elixir--wait-for-prompt buffer 30)
|
||||||
|
(ob-elixir--configure-session buffer)
|
||||||
|
buffer)))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Add connection verification
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun ob-elixir--verify-remote-connection (buffer remsh)
|
||||||
|
"Verify that BUFFER is connected to remote node REMSH."
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(let ((result (ob-elixir--send-and-receive buffer "Node.self()")))
|
||||||
|
(when (string-match-p (regexp-quote remsh) result)
|
||||||
|
t))))
|
||||||
|
|
||||||
|
(defun ob-elixir--send-and-receive (buffer command)
|
||||||
|
"Send COMMAND to BUFFER and return the response."
|
||||||
|
(let ((start-pos nil)
|
||||||
|
(result nil))
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(goto-char (point-max))
|
||||||
|
(setq start-pos (point))
|
||||||
|
(ob-elixir--send-command buffer command)
|
||||||
|
(ob-elixir--wait-for-prompt buffer 10)
|
||||||
|
(setq result (buffer-substring-no-properties start-pos (point))))
|
||||||
|
(ob-elixir--clean-session-output result)))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Add safety checks
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defcustom ob-elixir-remsh-confirm t
|
||||||
|
"Whether to confirm before connecting to remote nodes.
|
||||||
|
|
||||||
|
When non-nil, ask for confirmation before connecting.
|
||||||
|
This is a safety measure for production systems."
|
||||||
|
:type 'boolean
|
||||||
|
:group 'ob-elixir)
|
||||||
|
|
||||||
|
(defun ob-elixir--confirm-remsh (node)
|
||||||
|
"Confirm remote shell connection to NODE."
|
||||||
|
(or (not ob-elixir-remsh-confirm)
|
||||||
|
(yes-or-no-p
|
||||||
|
(format "Connect to remote Elixir node '%s'? " node))))
|
||||||
|
|
||||||
|
(defun ob-elixir--start-remote-session (buffer-name session-name params)
|
||||||
|
"Start a remote shell session in BUFFER-NAME."
|
||||||
|
(let ((remsh (cdr (assq :remsh params))))
|
||||||
|
(unless (ob-elixir--confirm-remsh remsh)
|
||||||
|
(user-error "Remote shell connection cancelled"))
|
||||||
|
;; ... rest of implementation
|
||||||
|
))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Add helper commands
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun ob-elixir-connect-to-node (node &optional cookie)
|
||||||
|
"Interactively connect to a remote Elixir NODE.
|
||||||
|
|
||||||
|
Optional COOKIE specifies the Erlang cookie."
|
||||||
|
(interactive
|
||||||
|
(list (read-string "Remote node: ")
|
||||||
|
(when current-prefix-arg
|
||||||
|
(read-string "Cookie: "))))
|
||||||
|
(let ((params `((:remsh . ,node)
|
||||||
|
,@(when cookie `((:cookie . ,cookie))))))
|
||||||
|
(org-babel-elixir-initiate-session "remote" params)))
|
||||||
|
|
||||||
|
(defun ob-elixir-list-sessions ()
|
||||||
|
"List all active ob-elixir sessions."
|
||||||
|
(interactive)
|
||||||
|
(if (= 0 (hash-table-count ob-elixir--sessions))
|
||||||
|
(message "No active sessions")
|
||||||
|
(with-output-to-temp-buffer "*ob-elixir sessions*"
|
||||||
|
(princ "Active ob-elixir sessions:\n\n")
|
||||||
|
(maphash (lambda (name buffer-name)
|
||||||
|
(let ((buffer (get-buffer buffer-name)))
|
||||||
|
(princ (format " %s -> %s (%s)\n"
|
||||||
|
name
|
||||||
|
buffer-name
|
||||||
|
(if (and buffer
|
||||||
|
(get-buffer-process buffer))
|
||||||
|
"running"
|
||||||
|
"dead")))))
|
||||||
|
ob-elixir--sessions))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Add tests
|
||||||
|
|
||||||
|
Create `test/test-ob-elixir-remsh.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; test-ob-elixir-remsh.el --- Remote shell tests -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'ob-elixir)
|
||||||
|
|
||||||
|
;; Note: These tests require a running Elixir node
|
||||||
|
;; Start one with: iex --sname testnode --cookie testcookie
|
||||||
|
|
||||||
|
(defvar ob-elixir-test-remote-node "testnode@localhost"
|
||||||
|
"Remote node for testing. Set to your test node.")
|
||||||
|
|
||||||
|
(defvar ob-elixir-test-remote-cookie "testcookie"
|
||||||
|
"Cookie for test node.")
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-is-remote-session ()
|
||||||
|
"Test remote session detection."
|
||||||
|
(should (ob-elixir--is-remote-session-p
|
||||||
|
'((:remsh . "node@host"))))
|
||||||
|
(should-not (ob-elixir--is-remote-session-p
|
||||||
|
'((:session . "test")))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-remote-session-creation ()
|
||||||
|
"Test remote session creation."
|
||||||
|
(skip-unless (executable-find ob-elixir-iex-command))
|
||||||
|
(skip-unless (getenv "OB_ELIXIR_TEST_REMSH")) ; Only run if explicitly enabled
|
||||||
|
(let ((ob-elixir-remsh-confirm nil))
|
||||||
|
(unwind-protect
|
||||||
|
(let* ((params `((:remsh . ,ob-elixir-test-remote-node)
|
||||||
|
(:cookie . ,ob-elixir-test-remote-cookie)))
|
||||||
|
(buffer (org-babel-elixir-initiate-session "test-remote" params)))
|
||||||
|
(should buffer)
|
||||||
|
(should (org-babel-comint-buffer-livep buffer)))
|
||||||
|
(ob-elixir-kill-session "test-remote"))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-remote-execution ()
|
||||||
|
"Test code execution on remote node."
|
||||||
|
(skip-unless (executable-find ob-elixir-iex-command))
|
||||||
|
(skip-unless (getenv "OB_ELIXIR_TEST_REMSH"))
|
||||||
|
(let ((ob-elixir-remsh-confirm nil))
|
||||||
|
(unwind-protect
|
||||||
|
(let* ((params `((:remsh . ,ob-elixir-test-remote-node)
|
||||||
|
(:cookie . ,ob-elixir-test-remote-cookie)))
|
||||||
|
(result (ob-elixir--evaluate-in-session
|
||||||
|
"test-remote-exec"
|
||||||
|
"Node.self() |> to_string()"
|
||||||
|
'value
|
||||||
|
params)))
|
||||||
|
(should (string-match-p ob-elixir-test-remote-node result)))
|
||||||
|
(ob-elixir-kill-session "test-remote-exec"))))
|
||||||
|
|
||||||
|
(provide 'test-ob-elixir-remsh)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 8: Document usage
|
||||||
|
|
||||||
|
Add to documentation:
|
||||||
|
|
||||||
|
```org
|
||||||
|
* Remote Shell Usage
|
||||||
|
|
||||||
|
** Connecting to a running node
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :session remote :remsh myapp@localhost :cookie secret
|
||||||
|
Node.self()
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** With explicit node name
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :session remote :remsh myapp@localhost :node-sname console :cookie secret
|
||||||
|
# Local node will be named 'console'
|
||||||
|
Node.list()
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** With distributed name
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :session remote :remsh myapp@myhost.example.com :node-name console@myhost.example.com :cookie secret
|
||||||
|
# Use full node names for cross-machine connections
|
||||||
|
Application.get_env(:my_app, :some_setting)
|
||||||
|
#+END_SRC
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] `:remsh node@host` connects to remote node
|
||||||
|
- [ ] `:cookie` sets the Erlang cookie
|
||||||
|
- [ ] `:node-sname` and `:node-name` set local node name
|
||||||
|
- [ ] Connection failures produce clear error messages
|
||||||
|
- [ ] Safety confirmation before connecting (configurable)
|
||||||
|
- [ ] `ob-elixir-connect-to-node` interactive command works
|
||||||
|
- [ ] Remote session appears in `ob-elixir-list-sessions`
|
||||||
|
- [ ] All tests pass
|
||||||
|
|
||||||
|
## Header Arguments Reference
|
||||||
|
|
||||||
|
| Argument | Values | Description |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| `:remsh` | node@host | Remote node to connect to |
|
||||||
|
| `:cookie` | string | Erlang cookie for authentication |
|
||||||
|
| `:node-sname` | name | Short name for local node |
|
||||||
|
| `:node-name` | name@host | Full name for local node |
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Cookies**: Never commit cookies to version control
|
||||||
|
2. **Confirmation**: Enable `ob-elixir-remsh-confirm` for production
|
||||||
|
3. **Network**: Ensure proper firewall rules for Erlang distribution port (4369 + dynamic)
|
||||||
|
4. **Scope**: Consider what code could be executed on production systems
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Cannot connect to node
|
||||||
|
|
||||||
|
1. Verify node is running: `epmd -names`
|
||||||
|
2. Check cookie matches
|
||||||
|
3. Verify network connectivity on port 4369
|
||||||
|
4. Check that node allows remote connections
|
||||||
|
|
||||||
|
### Connection times out
|
||||||
|
|
||||||
|
Increase timeout or check for network issues:
|
||||||
|
```elisp
|
||||||
|
(setq ob-elixir-session-timeout 60)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node not found
|
||||||
|
|
||||||
|
Ensure using correct node name format:
|
||||||
|
- Short names: `node@hostname` (same machine or subnet)
|
||||||
|
- Long names: `node@hostname.domain.com` (cross-network)
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
- `ob-elixir.el` - Add remote shell support
|
||||||
|
- `test/test-ob-elixir-remsh.el` - Add remote shell tests
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Remote Shell section
|
||||||
|
- [Erlang Distribution](https://www.erlang.org/doc/reference_manual/distributed.html)
|
||||||
|
- [IEx Remote Shell](https://hexdocs.pm/iex/IEx.html#module-remote-shells)
|
||||||
358
tasks/10-async-execution.md
Normal file
358
tasks/10-async-execution.md
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
# Task 10: Async Execution Support
|
||||||
|
|
||||||
|
**Phase**: 4 - Advanced Features
|
||||||
|
**Priority**: Low
|
||||||
|
**Estimated Time**: 3-4 hours
|
||||||
|
**Dependencies**: Phase 1 Complete
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Implement asynchronous execution so long-running Elixir code blocks don't freeze Emacs.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Phase 1 complete
|
||||||
|
- Understanding of Emacs async processes
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Some Elixir operations can take a long time:
|
||||||
|
- Database migrations
|
||||||
|
- Large data processing
|
||||||
|
- Network operations
|
||||||
|
- Build tasks
|
||||||
|
|
||||||
|
Async execution allows:
|
||||||
|
- Continue editing while code runs
|
||||||
|
- Visual indicator of running blocks
|
||||||
|
- Cancel long-running operations
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### Step 1: Add async configuration
|
||||||
|
|
||||||
|
Add to `ob-elixir.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Async Configuration
|
||||||
|
|
||||||
|
(defcustom ob-elixir-async-timeout 300
|
||||||
|
"Timeout in seconds for async execution.
|
||||||
|
|
||||||
|
After this time, async execution will be cancelled."
|
||||||
|
:type 'integer
|
||||||
|
:group 'ob-elixir)
|
||||||
|
|
||||||
|
(defvar ob-elixir--async-processes (make-hash-table :test 'equal)
|
||||||
|
"Hash table mapping buffer positions to async processes.")
|
||||||
|
|
||||||
|
(defconst org-babel-header-args:elixir
|
||||||
|
'((mix-project . :any)
|
||||||
|
(mix-env . :any)
|
||||||
|
(remsh . :any)
|
||||||
|
(node-name . :any)
|
||||||
|
(node-sname . :any)
|
||||||
|
(cookie . :any)
|
||||||
|
(async . ((yes no)))) ; NEW: async execution
|
||||||
|
"Elixir-specific header arguments.")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Implement async execution
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; Async Execution
|
||||||
|
|
||||||
|
(defun ob-elixir--execute-async (body result-type callback)
|
||||||
|
"Execute BODY asynchronously.
|
||||||
|
|
||||||
|
RESULT-TYPE is 'value or 'output.
|
||||||
|
CALLBACK is called with the result when complete."
|
||||||
|
(let* ((tmp-file (org-babel-temp-file "ob-elixir-async-" ".exs"))
|
||||||
|
(code (if (eq result-type 'value)
|
||||||
|
(ob-elixir--wrap-for-value body)
|
||||||
|
body))
|
||||||
|
(output-buffer (generate-new-buffer " *ob-elixir-async*"))
|
||||||
|
process)
|
||||||
|
|
||||||
|
;; Write code to temp file
|
||||||
|
(with-temp-file tmp-file
|
||||||
|
(insert code))
|
||||||
|
|
||||||
|
;; Start async process
|
||||||
|
(setq process
|
||||||
|
(start-process
|
||||||
|
"ob-elixir-async"
|
||||||
|
output-buffer
|
||||||
|
ob-elixir-command
|
||||||
|
tmp-file))
|
||||||
|
|
||||||
|
;; Set up process sentinel
|
||||||
|
(set-process-sentinel
|
||||||
|
process
|
||||||
|
(lambda (proc event)
|
||||||
|
(when (memq (process-status proc) '(exit signal))
|
||||||
|
(let ((result (with-current-buffer (process-buffer proc)
|
||||||
|
(buffer-string))))
|
||||||
|
;; Clean up
|
||||||
|
(kill-buffer (process-buffer proc))
|
||||||
|
(delete-file tmp-file)
|
||||||
|
;; Call callback with result
|
||||||
|
(funcall callback (ob-elixir--process-result
|
||||||
|
(string-trim result)))))))
|
||||||
|
|
||||||
|
;; Set up timeout
|
||||||
|
(run-at-time ob-elixir-async-timeout nil
|
||||||
|
(lambda ()
|
||||||
|
(when (process-live-p process)
|
||||||
|
(kill-process process)
|
||||||
|
(funcall callback "Error: Async execution timed out"))))
|
||||||
|
|
||||||
|
process))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Integrate with org-babel
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun ob-elixir--async-p (params)
|
||||||
|
"Return t if PARAMS specify async execution."
|
||||||
|
(string= "yes" (cdr (assq :async params))))
|
||||||
|
|
||||||
|
(defun org-babel-execute:elixir (body params)
|
||||||
|
"Execute a block of Elixir code with org-babel."
|
||||||
|
(let* ((session (cdr (assq :session params)))
|
||||||
|
(result-type (cdr (assq :result-type params)))
|
||||||
|
(result-params (cdr (assq :result-params params)))
|
||||||
|
(async (ob-elixir--async-p params))
|
||||||
|
(full-body (org-babel-expand-body:generic
|
||||||
|
body params
|
||||||
|
(org-babel-variable-assignments:elixir params))))
|
||||||
|
|
||||||
|
(if async
|
||||||
|
;; Async execution
|
||||||
|
(ob-elixir--execute-async-block full-body result-type params)
|
||||||
|
;; Sync execution (existing code)
|
||||||
|
(let ((result (ob-elixir--execute-sync full-body result-type params)))
|
||||||
|
(org-babel-reassemble-table
|
||||||
|
(org-babel-result-cond result-params
|
||||||
|
result
|
||||||
|
(ob-elixir--table-or-string result))
|
||||||
|
(org-babel-pick-name (cdr (assq :colname-names params))
|
||||||
|
(cdr (assq :colnames params)))
|
||||||
|
(org-babel-pick-name (cdr (assq :rowname-names params))
|
||||||
|
(cdr (assq :rownames params))))))))
|
||||||
|
|
||||||
|
(defun ob-elixir--execute-sync (body result-type params)
|
||||||
|
"Execute BODY synchronously."
|
||||||
|
(cond
|
||||||
|
((and (cdr (assq :session params))
|
||||||
|
(not (string= (cdr (assq :session params)) "none")))
|
||||||
|
(ob-elixir--evaluate-in-session
|
||||||
|
(cdr (assq :session params)) body result-type params))
|
||||||
|
((ob-elixir--resolve-mix-project params)
|
||||||
|
(ob-elixir--execute-with-mix body result-type params))
|
||||||
|
(t
|
||||||
|
(ob-elixir--execute body result-type))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Implement async block handling
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun ob-elixir--execute-async-block (body result-type params)
|
||||||
|
"Execute BODY asynchronously and insert results when done."
|
||||||
|
(let ((buffer (current-buffer))
|
||||||
|
(point (point))
|
||||||
|
(result-params (cdr (assq :result-params params)))
|
||||||
|
(marker (copy-marker (point))))
|
||||||
|
|
||||||
|
;; Show placeholder
|
||||||
|
(ob-elixir--insert-async-placeholder marker)
|
||||||
|
|
||||||
|
;; Execute async
|
||||||
|
(ob-elixir--execute-async
|
||||||
|
body
|
||||||
|
result-type
|
||||||
|
(lambda (result)
|
||||||
|
(ob-elixir--insert-async-result
|
||||||
|
buffer marker result result-params params)))
|
||||||
|
|
||||||
|
;; Return placeholder message
|
||||||
|
"Executing asynchronously..."))
|
||||||
|
|
||||||
|
(defun ob-elixir--insert-async-placeholder (marker)
|
||||||
|
"Insert a placeholder at MARKER indicating async execution."
|
||||||
|
(save-excursion
|
||||||
|
(goto-char marker)
|
||||||
|
(end-of-line)
|
||||||
|
(insert "\n")
|
||||||
|
(insert "#+RESULTS:\n")
|
||||||
|
(insert ": [Executing...]\n")))
|
||||||
|
|
||||||
|
(defun ob-elixir--insert-async-result (buffer marker result result-params params)
|
||||||
|
"Insert RESULT at MARKER in BUFFER."
|
||||||
|
(when (buffer-live-p buffer)
|
||||||
|
(with-current-buffer buffer
|
||||||
|
(save-excursion
|
||||||
|
(goto-char marker)
|
||||||
|
;; Find and remove placeholder
|
||||||
|
(when (search-forward ": [Executing...]" nil t)
|
||||||
|
(beginning-of-line)
|
||||||
|
(let ((start (point)))
|
||||||
|
(forward-line 1)
|
||||||
|
(delete-region start (point))))
|
||||||
|
;; Insert real result
|
||||||
|
(let ((formatted (org-babel-result-cond result-params
|
||||||
|
result
|
||||||
|
(ob-elixir--table-or-string result))))
|
||||||
|
(org-babel-insert-result formatted result-params))))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Add cancellation support
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun ob-elixir-cancel-async ()
|
||||||
|
"Cancel the async execution at point."
|
||||||
|
(interactive)
|
||||||
|
(let* ((pos (point))
|
||||||
|
(process (gethash pos ob-elixir--async-processes)))
|
||||||
|
(if (and process (process-live-p process))
|
||||||
|
(progn
|
||||||
|
(kill-process process)
|
||||||
|
(remhash pos ob-elixir--async-processes)
|
||||||
|
(message "Async execution cancelled"))
|
||||||
|
(message "No async execution at point"))))
|
||||||
|
|
||||||
|
(defun ob-elixir-cancel-all-async ()
|
||||||
|
"Cancel all running async executions."
|
||||||
|
(interactive)
|
||||||
|
(maphash (lambda (_pos process)
|
||||||
|
(when (process-live-p process)
|
||||||
|
(kill-process process)))
|
||||||
|
ob-elixir--async-processes)
|
||||||
|
(clrhash ob-elixir--async-processes)
|
||||||
|
(message "All async executions cancelled"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Add visual indicators
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defface ob-elixir-async-running
|
||||||
|
'((t :background "yellow" :foreground "black"))
|
||||||
|
"Face for source blocks with running async execution."
|
||||||
|
:group 'ob-elixir)
|
||||||
|
|
||||||
|
(defun ob-elixir--highlight-async-block (start end)
|
||||||
|
"Highlight the region from START to END as running."
|
||||||
|
(let ((overlay (make-overlay start end)))
|
||||||
|
(overlay-put overlay 'face 'ob-elixir-async-running)
|
||||||
|
(overlay-put overlay 'ob-elixir-async t)
|
||||||
|
overlay))
|
||||||
|
|
||||||
|
(defun ob-elixir--remove-async-highlight ()
|
||||||
|
"Remove async highlighting from current block."
|
||||||
|
(dolist (ov (overlays-in (point-min) (point-max)))
|
||||||
|
(when (overlay-get ov 'ob-elixir-async)
|
||||||
|
(delete-overlay ov))))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Add tests
|
||||||
|
|
||||||
|
Create `test/test-ob-elixir-async.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; test-ob-elixir-async.el --- Async execution tests -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'ob-elixir)
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-async-detection ()
|
||||||
|
"Test async header argument detection."
|
||||||
|
(should (ob-elixir--async-p '((:async . "yes"))))
|
||||||
|
(should-not (ob-elixir--async-p '((:async . "no"))))
|
||||||
|
(should-not (ob-elixir--async-p '())))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-async-execution ()
|
||||||
|
"Test async execution completion."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((result nil)
|
||||||
|
(done nil))
|
||||||
|
(ob-elixir--execute-async
|
||||||
|
"1 + 1"
|
||||||
|
'value
|
||||||
|
(lambda (r)
|
||||||
|
(setq result r)
|
||||||
|
(setq done t)))
|
||||||
|
;; Wait for completion
|
||||||
|
(with-timeout (10 (error "Async test timed out"))
|
||||||
|
(while (not done)
|
||||||
|
(accept-process-output nil 0.1)))
|
||||||
|
(should (equal "2" result))))
|
||||||
|
|
||||||
|
(ert-deftest ob-elixir-test-async-timeout ()
|
||||||
|
"Test async timeout handling."
|
||||||
|
(skip-unless (executable-find ob-elixir-command))
|
||||||
|
(let ((ob-elixir-async-timeout 1)
|
||||||
|
(result nil)
|
||||||
|
(done nil))
|
||||||
|
(ob-elixir--execute-async
|
||||||
|
":timer.sleep(5000)" ; Sleep for 5 seconds
|
||||||
|
'value
|
||||||
|
(lambda (r)
|
||||||
|
(setq result r)
|
||||||
|
(setq done t)))
|
||||||
|
;; Wait for timeout
|
||||||
|
(with-timeout (3 (error "Test timed out"))
|
||||||
|
(while (not done)
|
||||||
|
(accept-process-output nil 0.1)))
|
||||||
|
(should (string-match-p "timed out" result))))
|
||||||
|
|
||||||
|
(provide 'test-ob-elixir-async)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 8: Document usage
|
||||||
|
|
||||||
|
Add to documentation:
|
||||||
|
|
||||||
|
```org
|
||||||
|
* Async Execution
|
||||||
|
|
||||||
|
** Long-running computation
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :async yes
|
||||||
|
# This won't block Emacs
|
||||||
|
Enum.reduce(1..1000000, 0, &+/2)
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** Async with Mix project
|
||||||
|
|
||||||
|
#+BEGIN_SRC elixir :async yes :mix-project ~/my_app
|
||||||
|
MyApp.expensive_operation()
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** Cancel with M-x ob-elixir-cancel-async
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] `:async yes` executes code asynchronously
|
||||||
|
- [ ] Placeholder shown while executing
|
||||||
|
- [ ] Results inserted when complete
|
||||||
|
- [ ] Timeout handled gracefully
|
||||||
|
- [ ] `ob-elixir-cancel-async` cancels execution
|
||||||
|
- [ ] Visual indicator for running blocks
|
||||||
|
- [ ] All tests pass
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Sessions cannot be async (they're inherently stateful)
|
||||||
|
- Multiple async blocks may have ordering issues
|
||||||
|
- Async results may not integrate perfectly with noweb
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
- `ob-elixir.el` - Add async support
|
||||||
|
- `test/test-ob-elixir-async.el` - Add async tests
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Emacs Async Processes](https://www.gnu.org/software/emacs/manual/html_node/elisp/Asynchronous-Processes.html)
|
||||||
|
- [Process Sentinels](https://www.gnu.org/software/emacs/manual/html_node/elisp/Sentinels.html)
|
||||||
Reference in New Issue
Block a user