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
|
||||
|
||||
Reference in New Issue
Block a user