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)
|
||||||
(require 'ob-eval)
|
(require 'ob-eval)
|
||||||
|
(require 'ob-comint)
|
||||||
(require 'cl-lib)
|
(require 'cl-lib)
|
||||||
|
(require 'ansi-color)
|
||||||
|
|
||||||
;;; Customization
|
;;; Customization
|
||||||
|
|
||||||
@@ -68,6 +70,26 @@ When nil, warnings are stripped from the output."
|
|||||||
:type 'boolean
|
:type 'boolean
|
||||||
:group 'ob-elixir)
|
: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
|
;;; Header Arguments
|
||||||
|
|
||||||
(defvar org-babel-default-header-args:elixir
|
(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))))
|
(ob-elixir--elisp-to-elixir value))))
|
||||||
(org-babel--get-vars params)))
|
(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
|
;;; Execution
|
||||||
|
|
||||||
(defconst ob-elixir--value-wrapper
|
(defconst ob-elixir--value-wrapper
|
||||||
@@ -411,13 +631,18 @@ BODY is the Elixir code to execute.
|
|||||||
PARAMS is an alist of header arguments.
|
PARAMS is an alist of header arguments.
|
||||||
|
|
||||||
This function is called by `org-babel-execute-src-block'."
|
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)))
|
(result-params (cdr (assq :result-params params)))
|
||||||
;; Expand body with variable assignments
|
;; Expand body with variable assignments
|
||||||
(full-body (org-babel-expand-body:generic
|
(full-body (org-babel-expand-body:generic
|
||||||
body params
|
body params
|
||||||
(org-babel-variable-assignments:elixir 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-reassemble-table
|
||||||
(org-babel-result-cond result-params
|
(org-babel-result-cond result-params
|
||||||
;; For output/scalar/verbatim - return as-is
|
;; 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