781 lines
28 KiB
Markdown
781 lines
28 KiB
Markdown
# Task 13: Add Module Definition Blocks Support
|
|
|
|
## Problem
|
|
|
|
When working with Elixir in org-mode, users often want to define reusable functions that can be called from multiple code blocks. Currently, users must either:
|
|
|
|
1. Define full `defmodule` wrappers in each block
|
|
2. Use sessions and manually manage module definitions
|
|
3. Duplicate function code across blocks
|
|
|
|
This is cumbersome and doesn't align with the literate programming paradigm where functions should be defined once and used throughout the document.
|
|
|
|
## Desired Behavior
|
|
|
|
Users can define Elixir functions in code blocks with a `:module` header argument. These blocks:
|
|
|
|
1. **Define functions** but don't execute any code
|
|
2. **Merge** when multiple blocks share the same module name
|
|
3. Are **automatically wrapped** in `defmodule` when a regular block is executed
|
|
4. Require **explicit imports** via the `#+BEGIN_IMPORTS elixir` block to use
|
|
|
|
### Example
|
|
|
|
```org
|
|
#+BEGIN_SRC elixir :module Helpers
|
|
def greet(name), do: "Hello, #{name}!"
|
|
def double(x), do: x * 2
|
|
#+END_SRC
|
|
|
|
#+BEGIN_SRC elixir :module Helpers
|
|
# This merges with the above block
|
|
def triple(x), do: x * 3
|
|
#+END_SRC
|
|
|
|
#+BEGIN_SRC elixir :module Math
|
|
def square(x), do: x * x
|
|
@moduledoc "Math utilities"
|
|
#+END_SRC
|
|
|
|
#+BEGIN_IMPORTS elixir
|
|
import Helpers
|
|
import Math
|
|
#+END_IMPORTS
|
|
|
|
#+BEGIN_SRC elixir
|
|
greet("World") <> " - " <> to_string(square(5))
|
|
#+END_SRC
|
|
```
|
|
|
|
When the last block executes, the generated code is:
|
|
|
|
```elixir
|
|
defmodule Helpers do
|
|
def greet(name), do: "Hello, #{name}!"
|
|
def double(x), do: x * 2
|
|
def triple(x), do: x * 3
|
|
end
|
|
|
|
defmodule Math do
|
|
def square(x), do: x * x
|
|
@moduledoc "Math utilities"
|
|
end
|
|
|
|
import Helpers
|
|
import Math
|
|
|
|
result = (
|
|
greet("World") <> " - " <> to_string(square(5))
|
|
)
|
|
IO.puts(inspect(result, limit: :infinity, printable_limit: :infinity, charlists: :as_lists))
|
|
```
|
|
|
|
## Scope
|
|
|
|
- `:module ModuleName` header marks a block as a function definition block
|
|
- Definition blocks don't execute - they return a confirmation message
|
|
- Multiple blocks with the same `:module` value have their bodies merged
|
|
- Only blocks **before** the current position are gathered (consistent with imports/deps)
|
|
- Works with both session and non-session modes
|
|
- Works alongside deps blocks and imports blocks
|
|
- Supports all Elixir module constructs (`def`, `defp`, `@moduledoc`, `use`, etc.)
|
|
|
|
## Implementation Plan
|
|
|
|
### Step 1: Add function to find all module definition blocks
|
|
|
|
**File:** `ob-elixir.el` (after `ob-elixir--find-imports-for-position`, around line 453)
|
|
|
|
**Add:** A function to scan the buffer for all elixir src blocks with `:module` header:
|
|
|
|
```elisp
|
|
(defun ob-elixir--find-all-module-blocks (pos)
|
|
"Find all module definition blocks before POS.
|
|
|
|
Scans the buffer for Elixir source blocks with a :module header argument.
|
|
Returns an alist of (MODULE-NAME . BODY-STRING) with merged bodies
|
|
for blocks sharing the same module name.
|
|
|
|
Blocks are processed in document order, so later blocks with the same
|
|
module name have their content appended to earlier blocks."
|
|
(save-excursion
|
|
(save-restriction
|
|
(widen)
|
|
(goto-char (point-min))
|
|
(let ((modules (make-hash-table :test 'equal)))
|
|
(org-element-map (org-element-parse-buffer) 'src-block
|
|
(lambda (src-block)
|
|
(when (and (< (org-element-property :begin src-block) pos)
|
|
(string= (org-element-property :language src-block) "elixir"))
|
|
(let* ((params (org-babel-parse-header-arguments
|
|
(or (org-element-property :parameters src-block) "")))
|
|
(module-name (cdr (assq :module params))))
|
|
(when module-name
|
|
(let* ((body (org-element-property :value src-block))
|
|
(existing (gethash module-name modules "")))
|
|
(puthash module-name
|
|
(if (string-empty-p existing)
|
|
(string-trim body)
|
|
(concat existing "\n\n" (string-trim body)))
|
|
modules)))))))
|
|
;; Convert hash table to alist
|
|
(let (result)
|
|
(maphash (lambda (k v) (push (cons k v) result)) modules)
|
|
(nreverse result))))))
|
|
```
|
|
|
|
### Step 2: Add function to generate module definitions
|
|
|
|
**File:** `ob-elixir.el` (after `ob-elixir--find-all-module-blocks`)
|
|
|
|
**Add:** A function to convert the modules alist to Elixir `defmodule` code:
|
|
|
|
```elisp
|
|
(defun ob-elixir--generate-module-definitions (modules-alist)
|
|
"Generate Elixir module definitions from MODULES-ALIST.
|
|
|
|
Each entry in MODULES-ALIST is (MODULE-NAME . BODY-STRING).
|
|
Returns a string with all defmodule definitions separated by blank lines,
|
|
or nil if MODULES-ALIST is empty."
|
|
(when modules-alist
|
|
(mapconcat
|
|
(lambda (entry)
|
|
(format "defmodule %s do\n%s\nend" (car entry) (cdr entry)))
|
|
modules-alist
|
|
"\n\n")))
|
|
```
|
|
|
|
### Step 3: Modify `org-babel-execute:elixir` to detect `:module` blocks
|
|
|
|
**File:** `ob-elixir.el` (in `org-babel-execute:elixir` function, around line 946)
|
|
|
|
**Change:** Add early return when `:module` header is present:
|
|
|
|
**Current code:**
|
|
```elisp
|
|
(defun org-babel-execute:elixir (body params)
|
|
"Execute a block of Elixir code with org-babel.
|
|
..."
|
|
(let* ((session (cdr (assq :session params)))
|
|
...)
|
|
```
|
|
|
|
**Proposed change:**
|
|
```elisp
|
|
(defun org-babel-execute:elixir (body params)
|
|
"Execute a block of Elixir code with org-babel.
|
|
..."
|
|
(let ((module-name (cdr (assq :module params))))
|
|
(if module-name
|
|
;; This is a module definition block - don't execute
|
|
(format "Module %s: functions defined" module-name)
|
|
;; Normal execution path
|
|
(let* ((session (cdr (assq :session params)))
|
|
...))))
|
|
```
|
|
|
|
### Step 4: Modify `ob-elixir--execute` to accept modules-string
|
|
|
|
**File:** `ob-elixir.el` (around line 919)
|
|
|
|
**Current signature:**
|
|
```elisp
|
|
(defun ob-elixir--execute (body result-type &optional imports-string)
|
|
```
|
|
|
|
**Proposed signature:**
|
|
```elisp
|
|
(defun ob-elixir--execute (body result-type &optional imports-string modules-string)
|
|
```
|
|
|
|
**Changes to function body:**
|
|
|
|
```elisp
|
|
(defun ob-elixir--execute (body result-type &optional imports-string modules-string)
|
|
"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.
|
|
IMPORTS-STRING, if provided, contains import/alias/require statements.
|
|
MODULES-STRING, if provided, contains module definitions to prepend.
|
|
|
|
Code is assembled in this order:
|
|
1. Module definitions (MODULES-STRING)
|
|
2. Imports (IMPORTS-STRING)
|
|
3. User code (BODY, wrapped for value if needed)
|
|
|
|
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"))
|
|
(wrapped (if (eq result-type 'value)
|
|
(ob-elixir--wrap-for-value body)
|
|
body))
|
|
(code (concat
|
|
(when modules-string (concat (string-trim modules-string) "\n\n"))
|
|
(when imports-string (concat (string-trim imports-string) "\n\n"))
|
|
wrapped)))
|
|
(with-temp-file tmp-file
|
|
(insert code))
|
|
(let ((result (with-temp-buffer
|
|
(call-process ob-elixir-command nil t nil
|
|
(org-babel-process-file-name tmp-file))
|
|
(buffer-string))))
|
|
(ob-elixir--process-result result))))
|
|
```
|
|
|
|
### Step 5: Modify `ob-elixir--execute-with-deps` to accept modules-string
|
|
|
|
**File:** `ob-elixir.el` (around line 561)
|
|
|
|
**Current signature:**
|
|
```elisp
|
|
(defun ob-elixir--execute-with-deps (body result-type deps-string &optional imports-string)
|
|
```
|
|
|
|
**Proposed signature:**
|
|
```elisp
|
|
(defun ob-elixir--execute-with-deps (body result-type deps-string &optional imports-string modules-string)
|
|
```
|
|
|
|
**Changes to function body:**
|
|
|
|
```elisp
|
|
(defun ob-elixir--execute-with-deps (body result-type deps-string &optional imports-string modules-string)
|
|
"Execute BODY with dependencies from DEPS-STRING.
|
|
|
|
RESULT-TYPE is `value' or `output'.
|
|
IMPORTS-STRING, if provided, is prepended after module definitions.
|
|
MODULES-STRING, if provided, contains module definitions to prepend first."
|
|
(let* ((project-dir (ob-elixir--get-deps-project deps-string))
|
|
(default-directory project-dir)
|
|
(tmp-file (org-babel-temp-file "ob-elixir-" ".exs"))
|
|
(wrapped (if (eq result-type 'value)
|
|
(ob-elixir--wrap-for-value body)
|
|
body))
|
|
(code (concat
|
|
(when modules-string (concat (string-trim modules-string) "\n\n"))
|
|
(when imports-string (concat (string-trim imports-string) "\n\n"))
|
|
wrapped)))
|
|
|
|
;; Write code to temp file
|
|
(with-temp-file tmp-file
|
|
(insert code))
|
|
|
|
;; Execute with mix run
|
|
(let ((command (format "%s run --no-compile %s 2>&1"
|
|
ob-elixir-mix-command
|
|
(shell-quote-argument tmp-file))))
|
|
(ob-elixir--process-result
|
|
(shell-command-to-string command)))))
|
|
```
|
|
|
|
### Step 6: Modify `ob-elixir--evaluate-in-session` to accept modules-string
|
|
|
|
**File:** `ob-elixir.el` (around line 685)
|
|
|
|
**Current signature:**
|
|
```elisp
|
|
(defun ob-elixir--evaluate-in-session (session body result-type &optional imports-string)
|
|
```
|
|
|
|
**Proposed signature:**
|
|
```elisp
|
|
(defun ob-elixir--evaluate-in-session (session body result-type &optional imports-string modules-string)
|
|
```
|
|
|
|
**Changes to function body:**
|
|
|
|
```elisp
|
|
(defun ob-elixir--evaluate-in-session (session body result-type &optional imports-string modules-string)
|
|
"Evaluate BODY in SESSION, return result.
|
|
|
|
RESULT-TYPE is `value' or `output'.
|
|
IMPORTS-STRING, if provided, is prepended after module definitions.
|
|
MODULES-STRING, if provided, contains module definitions to prepend first."
|
|
(let* ((buffer (org-babel-elixir-initiate-session session nil))
|
|
(wrapped (if (eq result-type 'value)
|
|
(ob-elixir--session-wrap-for-value body)
|
|
body))
|
|
(code (concat
|
|
(when modules-string (concat (string-trim modules-string) "\n\n"))
|
|
(when imports-string (concat (string-trim imports-string) "\n\n"))
|
|
wrapped))
|
|
(eoe-indicator ob-elixir--eoe-marker)
|
|
(full-body (concat code "\nIO.puts(\"" eoe-indicator "\")"))
|
|
output)
|
|
(unless buffer
|
|
(error "Failed to create Elixir session: %s" session))
|
|
|
|
(setq output
|
|
(org-babel-comint-with-output
|
|
(buffer eoe-indicator t full-body)
|
|
(ob-elixir--send-command buffer full-body)))
|
|
|
|
(ob-elixir--clean-session-output output result-type)))
|
|
```
|
|
|
|
### Step 7: Modify `ob-elixir--evaluate-in-session-with-deps` to accept modules-string
|
|
|
|
**File:** `ob-elixir.el` (around line 839)
|
|
|
|
**Current signature:**
|
|
```elisp
|
|
(defun ob-elixir--evaluate-in-session-with-deps (session body result-type deps-string &optional imports-string)
|
|
```
|
|
|
|
**Proposed signature:**
|
|
```elisp
|
|
(defun ob-elixir--evaluate-in-session-with-deps (session body result-type deps-string &optional imports-string modules-string)
|
|
```
|
|
|
|
**Changes to function body:**
|
|
|
|
```elisp
|
|
(defun ob-elixir--evaluate-in-session-with-deps (session body result-type deps-string &optional imports-string modules-string)
|
|
"Evaluate BODY in SESSION with DEPS-STRING context.
|
|
|
|
RESULT-TYPE is `value' or `output'.
|
|
IMPORTS-STRING, if provided, is prepended after module definitions.
|
|
MODULES-STRING, if provided, contains module definitions to prepend first."
|
|
(let* ((buffer (ob-elixir--get-or-create-session-with-deps session deps-string))
|
|
(wrapped (if (eq result-type 'value)
|
|
(ob-elixir--session-wrap-for-value body)
|
|
body))
|
|
(code (concat
|
|
(when modules-string (concat (string-trim modules-string) "\n\n"))
|
|
(when imports-string (concat (string-trim imports-string) "\n\n"))
|
|
wrapped))
|
|
(eoe-indicator ob-elixir--eoe-marker)
|
|
(full-body (concat code "\nIO.puts(\"" eoe-indicator "\")"))
|
|
output)
|
|
|
|
(unless buffer
|
|
(error "Failed to create Elixir session with deps: %s" session))
|
|
|
|
(setq output
|
|
(org-babel-comint-with-output
|
|
(buffer eoe-indicator t full-body)
|
|
(ob-elixir--send-command buffer full-body)))
|
|
|
|
(ob-elixir--clean-session-output output result-type)))
|
|
```
|
|
|
|
### Step 8: Update `org-babel-execute:elixir` to gather and pass modules
|
|
|
|
**File:** `ob-elixir.el` (in `org-babel-execute:elixir`, the normal execution path)
|
|
|
|
**Changes:** Add module gathering and pass to all execution functions:
|
|
|
|
```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 ((module-name (cdr (assq :module params))))
|
|
(if module-name
|
|
;; This is a module definition block - don't execute
|
|
(format "Module %s: functions defined" module-name)
|
|
;; Normal execution path
|
|
(let* ((session (cdr (assq :session params)))
|
|
(result-type (cdr (assq :result-type params)))
|
|
(result-params (cdr (assq :result-params params)))
|
|
;; Find deps for this block's position
|
|
(deps-string (ob-elixir--find-deps-for-position (point)))
|
|
;; Find imports for this block's position
|
|
(imports-string (ob-elixir--find-imports-for-position (point)))
|
|
;; Find module definitions for this block's position
|
|
(modules-alist (ob-elixir--find-all-module-blocks (point)))
|
|
(modules-string (ob-elixir--generate-module-definitions modules-alist))
|
|
;; Expand body with variable assignments
|
|
(full-body (org-babel-expand-body:generic
|
|
body params
|
|
(org-babel-variable-assignments:elixir params)))
|
|
(result (condition-case err
|
|
(cond
|
|
;; Session mode with deps
|
|
((and session (not (string= session "none")) deps-string)
|
|
(ob-elixir--evaluate-in-session-with-deps
|
|
session full-body result-type deps-string imports-string modules-string))
|
|
;; Session mode without deps
|
|
((and session (not (string= session "none")))
|
|
(ob-elixir--evaluate-in-session session full-body result-type imports-string modules-string))
|
|
;; Non-session with deps
|
|
(deps-string
|
|
(ob-elixir--execute-with-deps full-body result-type deps-string imports-string modules-string))
|
|
;; Plain execution
|
|
(t
|
|
(ob-elixir--execute full-body result-type imports-string modules-string)))
|
|
(ob-elixir-error
|
|
;; Return error message so it appears in buffer
|
|
(cadr err)))))
|
|
(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 9: Create test file for module functionality
|
|
|
|
**File:** `test/test-ob-elixir-modules.el` (new file)
|
|
|
|
```elisp
|
|
;;; test-ob-elixir-modules.el --- Module definition block tests -*- lexical-binding: t; -*-
|
|
|
|
;;; Commentary:
|
|
|
|
;; Tests for the module definition block functionality.
|
|
;; Tests the :module header argument and related functions.
|
|
|
|
;;; Code:
|
|
|
|
(require 'ert)
|
|
(require 'ob-elixir)
|
|
(require 'org)
|
|
(require 'org-element)
|
|
|
|
;;; Module Block Detection Tests
|
|
|
|
(ert-deftest ob-elixir-test-module-block-detection ()
|
|
"Test that :module header argument is detected in params."
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert "#+BEGIN_SRC elixir :module MyModule\ndef foo, do: :bar\n#+END_SRC\n")
|
|
(goto-char (point-min))
|
|
(search-forward "#+BEGIN_SRC")
|
|
(let* ((info (org-babel-get-src-block-info))
|
|
(params (nth 2 info))
|
|
(module-name (cdr (assq :module params))))
|
|
(should module-name)
|
|
(should (string= module-name "MyModule")))))
|
|
|
|
(ert-deftest ob-elixir-test-module-block-no-execute ()
|
|
"Test that blocks with :module don't execute Elixir code."
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert "#+BEGIN_SRC elixir :module TestMod\ndef foo, do: raise \"should not run\"\n#+END_SRC\n")
|
|
(goto-char (point-min))
|
|
(search-forward "#+BEGIN_SRC")
|
|
(let ((result (org-babel-execute-src-block)))
|
|
;; Should return a message, not execute the code
|
|
(should (stringp result))
|
|
(should (string-match-p "Module.*TestMod.*defined" result)))))
|
|
|
|
;;; Module Block Gathering Tests
|
|
|
|
(ert-deftest ob-elixir-test-find-module-blocks-single ()
|
|
"Test finding a single module block."
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert "#+BEGIN_SRC elixir :module Helpers\ndef greet(name), do: name\n#+END_SRC\n")
|
|
(insert "#+BEGIN_SRC elixir\n:ok\n#+END_SRC\n")
|
|
(goto-char (point-max))
|
|
(let ((modules (ob-elixir--find-all-module-blocks (point))))
|
|
(should modules)
|
|
(should (= 1 (length modules)))
|
|
(should (string= "Helpers" (caar modules)))
|
|
(should (string-match-p "def greet" (cdar modules))))))
|
|
|
|
(ert-deftest ob-elixir-test-find-module-blocks-multiple-same-name ()
|
|
"Test that multiple blocks with same module name are merged."
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert "#+BEGIN_SRC elixir :module Helpers\ndef foo, do: 1\n#+END_SRC\n")
|
|
(insert "#+BEGIN_SRC elixir :module Helpers\ndef bar, do: 2\n#+END_SRC\n")
|
|
(insert "#+BEGIN_SRC elixir\n:ok\n#+END_SRC\n")
|
|
(goto-char (point-max))
|
|
(let ((modules (ob-elixir--find-all-module-blocks (point))))
|
|
(should modules)
|
|
(should (= 1 (length modules)))
|
|
(should (string= "Helpers" (caar modules)))
|
|
(let ((body (cdar modules)))
|
|
(should (string-match-p "def foo" body))
|
|
(should (string-match-p "def bar" body))))))
|
|
|
|
(ert-deftest ob-elixir-test-find-module-blocks-different-names ()
|
|
"Test finding multiple modules with different names."
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert "#+BEGIN_SRC elixir :module ModA\ndef a, do: 1\n#+END_SRC\n")
|
|
(insert "#+BEGIN_SRC elixir :module ModB\ndef b, do: 2\n#+END_SRC\n")
|
|
(insert "#+BEGIN_SRC elixir\n:ok\n#+END_SRC\n")
|
|
(goto-char (point-max))
|
|
(let ((modules (ob-elixir--find-all-module-blocks (point))))
|
|
(should modules)
|
|
(should (= 2 (length modules)))
|
|
(should (assoc "ModA" modules))
|
|
(should (assoc "ModB" modules)))))
|
|
|
|
(ert-deftest ob-elixir-test-find-module-blocks-position-scoped ()
|
|
"Test that only blocks before current position are found."
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert "#+BEGIN_SRC elixir :module Before\ndef before, do: 1\n#+END_SRC\n")
|
|
(let ((middle-pos (point)))
|
|
(insert "#+BEGIN_SRC elixir :module After\ndef after, do: 2\n#+END_SRC\n")
|
|
(let ((modules (ob-elixir--find-all-module-blocks middle-pos)))
|
|
(should modules)
|
|
(should (= 1 (length modules)))
|
|
(should (assoc "Before" modules))
|
|
(should-not (assoc "After" modules))))))
|
|
|
|
(ert-deftest ob-elixir-test-find-module-blocks-no-modules ()
|
|
"Test that nil/empty is returned when no module blocks exist."
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert "#+BEGIN_SRC elixir\n1 + 1\n#+END_SRC\n")
|
|
(goto-char (point-max))
|
|
(let ((modules (ob-elixir--find-all-module-blocks (point))))
|
|
(should (null modules)))))
|
|
|
|
;;; Module Definition Generation Tests
|
|
|
|
(ert-deftest ob-elixir-test-generate-module-definitions-single ()
|
|
"Test generating a single module definition."
|
|
(let* ((modules '(("MyMod" . "def foo, do: :bar")))
|
|
(result (ob-elixir--generate-module-definitions modules)))
|
|
(should result)
|
|
(should (string-match-p "defmodule MyMod do" result))
|
|
(should (string-match-p "def foo, do: :bar" result))
|
|
(should (string-match-p "end" result))))
|
|
|
|
(ert-deftest ob-elixir-test-generate-module-definitions-multiple ()
|
|
"Test generating multiple module definitions."
|
|
(let* ((modules '(("ModA" . "def a, do: 1") ("ModB" . "def b, do: 2")))
|
|
(result (ob-elixir--generate-module-definitions modules)))
|
|
(should result)
|
|
(should (string-match-p "defmodule ModA do" result))
|
|
(should (string-match-p "defmodule ModB do" result))))
|
|
|
|
(ert-deftest ob-elixir-test-generate-module-definitions-empty ()
|
|
"Test that nil is returned for empty modules list."
|
|
(should (null (ob-elixir--generate-module-definitions nil)))
|
|
(should (null (ob-elixir--generate-module-definitions '()))))
|
|
|
|
;;; Integration Tests
|
|
|
|
(ert-deftest ob-elixir-test-module-integration-basic ()
|
|
"Test full integration: define module, import, call function."
|
|
(skip-unless (executable-find ob-elixir-command))
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert "#+BEGIN_SRC elixir :module Helpers\n")
|
|
(insert "def double(x), do: x * 2\n")
|
|
(insert "#+END_SRC\n\n")
|
|
(insert "#+BEGIN_IMPORTS elixir\n")
|
|
(insert "import Helpers\n")
|
|
(insert "#+END_IMPORTS\n\n")
|
|
(insert "#+BEGIN_SRC elixir :results value\n")
|
|
(insert "double(21)\n")
|
|
(insert "#+END_SRC\n")
|
|
(goto-char (point-min))
|
|
(search-forward "double(21)")
|
|
(beginning-of-line)
|
|
(search-backward "#+BEGIN_SRC")
|
|
(let ((result (org-babel-execute-src-block)))
|
|
(should result)
|
|
(should (equal result 42)))))
|
|
|
|
(ert-deftest ob-elixir-test-module-integration-merged ()
|
|
"Test that merged module functions are all available."
|
|
(skip-unless (executable-find ob-elixir-command))
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert "#+BEGIN_SRC elixir :module Math\n")
|
|
(insert "def double(x), do: x * 2\n")
|
|
(insert "#+END_SRC\n\n")
|
|
(insert "#+BEGIN_SRC elixir :module Math\n")
|
|
(insert "def triple(x), do: x * 3\n")
|
|
(insert "#+END_SRC\n\n")
|
|
(insert "#+BEGIN_IMPORTS elixir\n")
|
|
(insert "import Math\n")
|
|
(insert "#+END_IMPORTS\n\n")
|
|
(insert "#+BEGIN_SRC elixir :results value\n")
|
|
(insert "double(10) + triple(10)\n")
|
|
(insert "#+END_SRC\n")
|
|
(goto-char (point-min))
|
|
(search-forward "double(10)")
|
|
(beginning-of-line)
|
|
(search-backward "#+BEGIN_SRC")
|
|
(let ((result (org-babel-execute-src-block)))
|
|
(should result)
|
|
(should (equal result 50)))))
|
|
|
|
(ert-deftest ob-elixir-test-module-integration-multiple-modules ()
|
|
"Test multiple different modules."
|
|
(skip-unless (executable-find ob-elixir-command))
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert "#+BEGIN_SRC elixir :module ModA\n")
|
|
(insert "def value, do: 10\n")
|
|
(insert "#+END_SRC\n\n")
|
|
(insert "#+BEGIN_SRC elixir :module ModB\n")
|
|
(insert "def value, do: 20\n")
|
|
(insert "#+END_SRC\n\n")
|
|
(insert "#+BEGIN_IMPORTS elixir\n")
|
|
(insert "alias ModA\nalias ModB\n")
|
|
(insert "#+END_IMPORTS\n\n")
|
|
(insert "#+BEGIN_SRC elixir :results value\n")
|
|
(insert "ModA.value() + ModB.value()\n")
|
|
(insert "#+END_SRC\n")
|
|
(goto-char (point-min))
|
|
(search-forward "ModA.value()")
|
|
(beginning-of-line)
|
|
(search-backward "#+BEGIN_SRC")
|
|
(let ((result (org-babel-execute-src-block)))
|
|
(should result)
|
|
(should (equal result 30)))))
|
|
|
|
(ert-deftest ob-elixir-test-module-with-private-functions ()
|
|
"Test that defp (private functions) work in modules."
|
|
(skip-unless (executable-find ob-elixir-command))
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert "#+BEGIN_SRC elixir :module Helpers\n")
|
|
(insert "def public(x), do: private_helper(x)\n")
|
|
(insert "defp private_helper(x), do: x * 2\n")
|
|
(insert "#+END_SRC\n\n")
|
|
(insert "#+BEGIN_IMPORTS elixir\n")
|
|
(insert "import Helpers\n")
|
|
(insert "#+END_IMPORTS\n\n")
|
|
(insert "#+BEGIN_SRC elixir :results value\n")
|
|
(insert "public(21)\n")
|
|
(insert "#+END_SRC\n")
|
|
(goto-char (point-min))
|
|
(search-forward "public(21)")
|
|
(beginning-of-line)
|
|
(search-backward "#+BEGIN_SRC")
|
|
(let ((result (org-babel-execute-src-block)))
|
|
(should result)
|
|
(should (equal result 42)))))
|
|
|
|
(ert-deftest ob-elixir-test-module-with-module-attributes ()
|
|
"Test that module attributes work in modules."
|
|
(skip-unless (executable-find ob-elixir-command))
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert "#+BEGIN_SRC elixir :module Config\n")
|
|
(insert "@default_value 42\n")
|
|
(insert "def get_default, do: @default_value\n")
|
|
(insert "#+END_SRC\n\n")
|
|
(insert "#+BEGIN_IMPORTS elixir\n")
|
|
(insert "import Config\n")
|
|
(insert "#+END_IMPORTS\n\n")
|
|
(insert "#+BEGIN_SRC elixir :results value\n")
|
|
(insert "get_default()\n")
|
|
(insert "#+END_SRC\n")
|
|
(goto-char (point-min))
|
|
(search-forward "get_default()")
|
|
(beginning-of-line)
|
|
(search-backward "#+BEGIN_SRC")
|
|
(let ((result (org-babel-execute-src-block)))
|
|
(should result)
|
|
(should (equal result 42)))))
|
|
|
|
(ert-deftest ob-elixir-test-module-without-import ()
|
|
"Test that modules are defined but require import to use directly."
|
|
(skip-unless (executable-find ob-elixir-command))
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert "#+BEGIN_SRC elixir :module Helpers\n")
|
|
(insert "def greet, do: \"hello\"\n")
|
|
(insert "#+END_SRC\n\n")
|
|
;; No imports block - must use full module name
|
|
(insert "#+BEGIN_SRC elixir :results value\n")
|
|
(insert "Helpers.greet()\n")
|
|
(insert "#+END_SRC\n")
|
|
(goto-char (point-min))
|
|
(search-forward "Helpers.greet()")
|
|
(beginning-of-line)
|
|
(search-backward "#+BEGIN_SRC")
|
|
(let ((result (org-babel-execute-src-block)))
|
|
(should result)
|
|
(should (equal result "hello")))))
|
|
|
|
(provide 'test-ob-elixir-modules)
|
|
;;; test-ob-elixir-modules.el ends here
|
|
```
|
|
|
|
### Step 10: Update test loader
|
|
|
|
**File:** `test/test-ob-elixir.el`
|
|
|
|
**Add:** Require statement for the new test file (with other requires):
|
|
|
|
```elisp
|
|
(require 'test-ob-elixir-modules)
|
|
```
|
|
|
|
## Summary of Changes
|
|
|
|
| File | Change |
|
|
|------|--------|
|
|
| `ob-elixir.el` | Add `ob-elixir--find-all-module-blocks` function |
|
|
| `ob-elixir.el` | Add `ob-elixir--generate-module-definitions` function |
|
|
| `ob-elixir.el` | Modify `org-babel-execute:elixir` - early return for `:module` blocks |
|
|
| `ob-elixir.el` | Modify `ob-elixir--execute` - add `modules-string` parameter |
|
|
| `ob-elixir.el` | Modify `ob-elixir--execute-with-deps` - add `modules-string` parameter |
|
|
| `ob-elixir.el` | Modify `ob-elixir--evaluate-in-session` - add `modules-string` parameter |
|
|
| `ob-elixir.el` | Modify `ob-elixir--evaluate-in-session-with-deps` - add `modules-string` parameter |
|
|
| `ob-elixir.el` | Modify `org-babel-execute:elixir` - gather modules and pass to execution |
|
|
| `test/test-ob-elixir-modules.el` | New file with module definition tests |
|
|
| `test/test-ob-elixir.el` | Require new test file |
|
|
|
|
## Code Assembly Order
|
|
|
|
When a regular (non-module) block is executed, code is assembled in this order:
|
|
|
|
1. **Module definitions** (`defmodule ... end` for each gathered module)
|
|
2. **Imports** (from `#+BEGIN_IMPORTS elixir` block)
|
|
3. **Variable assignments** (from `:var` headers)
|
|
4. **User code** (the block body, wrapped for value capture if needed)
|
|
|
|
## Verification
|
|
|
|
After implementation, test with this org file:
|
|
|
|
```org
|
|
#+BEGIN_SRC elixir :module Helpers
|
|
def greet(name), do: "Hello, #{name}!"
|
|
def double(x), do: x * 2
|
|
#+END_SRC
|
|
|
|
#+BEGIN_SRC elixir :module Helpers
|
|
def triple(x), do: x * 3
|
|
#+END_SRC
|
|
|
|
#+BEGIN_SRC elixir :module Math
|
|
def square(x), do: x * x
|
|
@doc "Adds two numbers"
|
|
def add(a, b), do: a + b
|
|
#+END_SRC
|
|
|
|
#+BEGIN_IMPORTS elixir
|
|
import Helpers
|
|
import Math
|
|
#+END_IMPORTS
|
|
|
|
#+BEGIN_SRC elixir :results value
|
|
{greet("World"), double(5), triple(5), square(4), add(10, 20)}
|
|
#+END_SRC
|
|
```
|
|
|
|
Expected result: `{"Hello, World!", 10, 15, 16, 30}`
|
|
|
|
## Notes
|
|
|
|
- Module redefinition warnings may appear in sessions (this is expected Elixir behavior)
|
|
- Only blocks **before** the current position are gathered (consistent with imports/deps behavior)
|
|
- The `:module` header value should be a valid Elixir module name (e.g., `MyModule`, `My.Nested.Module`)
|
|
- All standard Elixir module constructs are supported (`def`, `defp`, `@moduledoc`, `@doc`, `use`, `require`, `import`, `alias`, etc.)
|