Files
ob-elixir/tasks/07-session-support.md

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)