443 lines
13 KiB
Markdown
443 lines
13 KiB
Markdown
# 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)
|