tasks/07-session-support.md done
This commit is contained in:
229
ob-elixir.el
229
ob-elixir.el
@@ -36,7 +36,9 @@
|
||||
|
||||
(require 'ob)
|
||||
(require 'ob-eval)
|
||||
(require 'ob-comint)
|
||||
(require 'cl-lib)
|
||||
(require 'ansi-color)
|
||||
|
||||
;;; Customization
|
||||
|
||||
@@ -68,6 +70,26 @@ When nil, warnings are stripped from the output."
|
||||
:type 'boolean
|
||||
:group 'ob-elixir)
|
||||
|
||||
;;; 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.")
|
||||
|
||||
;;; Header Arguments
|
||||
|
||||
(defvar org-babel-default-header-args:elixir
|
||||
@@ -363,6 +385,204 @@ Each statement has the form: var_name = value"
|
||||
(ob-elixir--elisp-to-elixir value))))
|
||||
(org-babel--get-vars params)))
|
||||
|
||||
;;; Session Management
|
||||
|
||||
(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.
|
||||
SESSION-NAME is used for the process name.
|
||||
_PARAMS is reserved for future use."
|
||||
(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))))
|
||||
|
||||
;;; Session Prompt Detection
|
||||
|
||||
(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)))
|
||||
|
||||
;;; Session Command Sending
|
||||
|
||||
(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)))
|
||||
|
||||
(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))
|
||||
|
||||
(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))
|
||||
(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)))
|
||||
|
||||
;;; Session Output Extraction
|
||||
|
||||
(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 result-type)
|
||||
"Clean OUTPUT from IEx session.
|
||||
RESULT-TYPE is `value' or `output'."
|
||||
(let ((result (if (listp output)
|
||||
(mapconcat #'identity output "\n")
|
||||
output)))
|
||||
;; Remove ANSI escape codes
|
||||
(setq result (ansi-color-filter-apply result))
|
||||
|
||||
;; Remove prompts (including continuation prompts)
|
||||
(setq result (replace-regexp-in-string
|
||||
"^\\(?:iex\\|\\.\\.\\)([0-9]+)> *" "" result))
|
||||
|
||||
;; Remove EOE marker output
|
||||
(setq result (replace-regexp-in-string
|
||||
(regexp-quote ob-elixir--eoe-marker) "" result))
|
||||
|
||||
;; For value results, extract the value between markers
|
||||
(when (eq result-type 'value)
|
||||
(if (string-match "__ob_value_start__\n\\(\\(?:.\\|\n\\)*?\\)\n__ob_value_end__" result)
|
||||
(setq result (match-string 1 result))
|
||||
;; Fallback: remove wrapper artifacts
|
||||
(setq result (replace-regexp-in-string
|
||||
"^_ob_result_ = (\n?" "" result))
|
||||
(setq result (replace-regexp-in-string
|
||||
"\n?)$" "" result))
|
||||
(setq result (replace-regexp-in-string
|
||||
"^IO\\.puts.*\n?" "" result))
|
||||
(setq result (replace-regexp-in-string
|
||||
":ok$" "" result))))
|
||||
|
||||
(string-trim result)))
|
||||
|
||||
;;; Session Prep
|
||||
|
||||
(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))
|
||||
|
||||
;;; Session Cleanup
|
||||
|
||||
(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))
|
||||
|
||||
;;; Execution
|
||||
|
||||
(defconst ob-elixir--value-wrapper
|
||||
@@ -411,13 +631,18 @@ 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)))
|
||||
(let* ((session (cdr (assq :session params)))
|
||||
(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)))
|
||||
(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
|
||||
;; For output/scalar/verbatim - return as-is
|
||||
|
||||
114
test/test-ob-elixir-sessions.el
Normal file
114
test/test-ob-elixir-sessions.el
Normal file
@@ -0,0 +1,114 @@
|
||||
;;; test-ob-elixir-sessions.el --- Session tests for ob-elixir -*- lexical-binding: t; -*-
|
||||
|
||||
;; Copyright (C) 2024
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Tests for IEx session support in ob-elixir.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(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")))
|
||||
|
||||
(ert-deftest ob-elixir-test-session-reuse ()
|
||||
"Test that same session name returns same buffer."
|
||||
(skip-unless (executable-find ob-elixir-iex-command))
|
||||
(unwind-protect
|
||||
(let ((buffer1 (org-babel-elixir-initiate-session "test-reuse" nil))
|
||||
(buffer2 (org-babel-elixir-initiate-session "test-reuse" nil)))
|
||||
(should (eq buffer1 buffer2)))
|
||||
(ob-elixir-kill-session "test-reuse")))
|
||||
|
||||
(ert-deftest ob-elixir-test-multiple-sessions ()
|
||||
"Test that multiple named sessions work independently."
|
||||
(skip-unless (executable-find ob-elixir-iex-command))
|
||||
(unwind-protect
|
||||
(progn
|
||||
;; Set x in session-a
|
||||
(ob-elixir--evaluate-in-session "session-a" "x = 1" 'value)
|
||||
;; Set x in session-b to different value
|
||||
(ob-elixir--evaluate-in-session "session-b" "x = 100" 'value)
|
||||
;; Verify each session has its own value
|
||||
(let ((result-a (ob-elixir--evaluate-in-session "session-a" "x" 'value))
|
||||
(result-b (ob-elixir--evaluate-in-session "session-b" "x" 'value)))
|
||||
(should (equal "1" result-a))
|
||||
(should (equal "100" result-b))))
|
||||
(ob-elixir-kill-session "session-a")
|
||||
(ob-elixir-kill-session "session-b")))
|
||||
|
||||
(ert-deftest ob-elixir-test-session-output-mode ()
|
||||
"Test session output mode (capturing stdout)."
|
||||
(skip-unless (executable-find ob-elixir-iex-command))
|
||||
(unwind-protect
|
||||
(let ((result (ob-elixir--evaluate-in-session
|
||||
"test-output"
|
||||
"IO.puts(\"Hello from session\")"
|
||||
'output)))
|
||||
(should (string-match-p "Hello from session" result)))
|
||||
(ob-elixir-kill-session "test-output")))
|
||||
|
||||
(ert-deftest ob-elixir-test-kill-session ()
|
||||
"Test killing a session."
|
||||
(skip-unless (executable-find ob-elixir-iex-command))
|
||||
(let ((buffer (org-babel-elixir-initiate-session "test-kill" nil)))
|
||||
(should buffer)
|
||||
(ob-elixir-kill-session "test-kill")
|
||||
(should-not (buffer-live-p buffer))
|
||||
(should-not (gethash "test-kill" ob-elixir--sessions))))
|
||||
|
||||
(ert-deftest ob-elixir-test-kill-all-sessions ()
|
||||
"Test killing all sessions."
|
||||
(skip-unless (executable-find ob-elixir-iex-command))
|
||||
(let ((buffer1 (org-babel-elixir-initiate-session "test-all-1" nil))
|
||||
(buffer2 (org-babel-elixir-initiate-session "test-all-2" nil)))
|
||||
(should buffer1)
|
||||
(should buffer2)
|
||||
(ob-elixir-kill-all-sessions)
|
||||
(should-not (buffer-live-p buffer1))
|
||||
(should-not (buffer-live-p buffer2))
|
||||
(should (= 0 (hash-table-count ob-elixir--sessions)))))
|
||||
|
||||
(provide 'test-ob-elixir-sessions)
|
||||
;;; test-ob-elixir-sessions.el ends here
|
||||
Reference in New Issue
Block a user