function definitions outside modules
This commit is contained in:
253
ob-elixir.el
253
ob-elixir.el
@@ -451,6 +451,55 @@ Returns the imports content as a string, or nil if no imports block found."
|
||||
(setq found (match-string-no-properties 1))))
|
||||
found)))
|
||||
|
||||
;;; Module Definition Block Parsing
|
||||
|
||||
(defun ob-elixir--find-all-module-blocks (pos)
|
||||
"Find all module definition blocks before POS.
|
||||
|
||||
Scans the buffer for Elixir source blocks with a :module header argument.
|
||||
Returns an alist of (MODULE-NAME . BODY-STRING) with merged bodies
|
||||
for blocks sharing the same module name.
|
||||
|
||||
Blocks are processed in document order, so later blocks with the same
|
||||
module name have their content appended to earlier blocks."
|
||||
(save-excursion
|
||||
(save-restriction
|
||||
(widen)
|
||||
(goto-char (point-min))
|
||||
(let ((modules (make-hash-table :test 'equal)))
|
||||
(org-element-map (org-element-parse-buffer) 'src-block
|
||||
(lambda (src-block)
|
||||
(when (and (< (org-element-property :begin src-block) pos)
|
||||
(string= (org-element-property :language src-block) "elixir"))
|
||||
(let* ((params (org-babel-parse-header-arguments
|
||||
(or (org-element-property :parameters src-block) "")))
|
||||
(module-name (cdr (assq :module params))))
|
||||
(when module-name
|
||||
(let* ((body (org-element-property :value src-block))
|
||||
(existing (gethash module-name modules "")))
|
||||
(puthash module-name
|
||||
(if (string-empty-p existing)
|
||||
(string-trim body)
|
||||
(concat existing "\n\n" (string-trim body)))
|
||||
modules)))))))
|
||||
;; Convert hash table to alist
|
||||
(let (result)
|
||||
(maphash (lambda (k v) (push (cons k v) result)) modules)
|
||||
(nreverse result))))))
|
||||
|
||||
(defun ob-elixir--generate-module-definitions (modules-alist)
|
||||
"Generate Elixir module definitions from MODULES-ALIST.
|
||||
|
||||
Each entry in MODULES-ALIST is (MODULE-NAME . BODY-STRING).
|
||||
Returns a string with all defmodule definitions separated by blank lines,
|
||||
or nil if MODULES-ALIST is empty."
|
||||
(when modules-alist
|
||||
(mapconcat
|
||||
(lambda (entry)
|
||||
(format "defmodule %s do\n%s\nend" (car entry) (cdr entry)))
|
||||
modules-alist
|
||||
"\n\n")))
|
||||
|
||||
(defun ob-elixir--normalize-deps (deps-string)
|
||||
"Normalize DEPS-STRING for consistent hashing.
|
||||
|
||||
@@ -558,20 +607,36 @@ Returns the project directory path."
|
||||
|
||||
;;; Execution with Dependencies
|
||||
|
||||
(defun ob-elixir--execute-with-deps (body result-type deps-string &optional imports-string)
|
||||
(defun ob-elixir--execute-with-deps (body result-type deps-string &optional imports-string modules-string)
|
||||
"Execute BODY with dependencies from DEPS-STRING.
|
||||
|
||||
RESULT-TYPE is `value' or `output'.
|
||||
IMPORTS-STRING, if provided, is prepended to the code before execution."
|
||||
IMPORTS-STRING, if provided, is prepended after module definitions.
|
||||
MODULES-STRING, if provided, contains module definitions to prepend first.
|
||||
|
||||
When modules are defined, imports and user code are wrapped in Code.eval_string.
|
||||
This is required because Elixir cannot import a module defined in the same file
|
||||
at the top level - Code.eval_string defers evaluation until runtime."
|
||||
(let* ((project-dir (ob-elixir--get-deps-project deps-string))
|
||||
(default-directory project-dir)
|
||||
(tmp-file (org-babel-temp-file "ob-elixir-" ".exs"))
|
||||
(wrapped (if (eq result-type 'value)
|
||||
(ob-elixir--wrap-for-value body)
|
||||
body))
|
||||
(code (if imports-string
|
||||
(concat (string-trim imports-string) "\n\n" wrapped)
|
||||
wrapped)))
|
||||
(code (if modules-string
|
||||
;; When modules are defined, wrap imports + code in Code.eval_string
|
||||
(let ((eval-body (concat
|
||||
(when imports-string (concat (string-trim imports-string) "\n"))
|
||||
wrapped)))
|
||||
(concat
|
||||
(string-trim modules-string) "\n\n"
|
||||
"Code.eval_string(\"\"\"\n"
|
||||
(ob-elixir--escape-for-eval-string eval-body)
|
||||
"\n\"\"\")"))
|
||||
;; Normal path without modules
|
||||
(concat
|
||||
(when imports-string (concat (string-trim imports-string) "\n\n"))
|
||||
wrapped))))
|
||||
|
||||
;; Write code to temp file
|
||||
(with-temp-file tmp-file
|
||||
@@ -682,18 +747,34 @@ IO.puts(\"__ob_value_end__\")
|
||||
"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 &optional imports-string)
|
||||
(defun ob-elixir--evaluate-in-session (session body result-type &optional imports-string modules-string)
|
||||
"Evaluate BODY in SESSION, return result.
|
||||
|
||||
RESULT-TYPE is `value' or `output'.
|
||||
IMPORTS-STRING, if provided, is prepended to the code before execution."
|
||||
IMPORTS-STRING, if provided, is prepended after module definitions.
|
||||
MODULES-STRING, if provided, contains module definitions to prepend first.
|
||||
|
||||
When modules are defined, imports and user code are wrapped in Code.eval_string.
|
||||
This is required because Elixir cannot import a module defined in the same file
|
||||
at the top level - Code.eval_string defers evaluation until runtime."
|
||||
(let* ((buffer (org-babel-elixir-initiate-session session nil))
|
||||
(wrapped (if (eq result-type 'value)
|
||||
(ob-elixir--session-wrap-for-value body)
|
||||
body))
|
||||
(code (if imports-string
|
||||
(concat (string-trim imports-string) "\n\n" wrapped)
|
||||
wrapped))
|
||||
(code (if modules-string
|
||||
;; When modules are defined, wrap imports + code in Code.eval_string
|
||||
(let ((eval-body (concat
|
||||
(when imports-string (concat (string-trim imports-string) "\n"))
|
||||
wrapped)))
|
||||
(concat
|
||||
(string-trim modules-string) "\n\n"
|
||||
"Code.eval_string(\"\"\"\n"
|
||||
(ob-elixir--escape-for-eval-string eval-body)
|
||||
"\n\"\"\")"))
|
||||
;; Normal path without modules
|
||||
(concat
|
||||
(when imports-string (concat (string-trim imports-string) "\n\n"))
|
||||
wrapped)))
|
||||
(eoe-indicator ob-elixir--eoe-marker)
|
||||
(full-body (concat code "\nIO.puts(\"" eoe-indicator "\")"))
|
||||
output)
|
||||
@@ -836,18 +917,34 @@ DEPS-STRING contains the dependencies specification."
|
||||
(puthash name deps-hash ob-elixir--session-deps)
|
||||
buffer))))
|
||||
|
||||
(defun ob-elixir--evaluate-in-session-with-deps (session body result-type deps-string &optional imports-string)
|
||||
(defun ob-elixir--evaluate-in-session-with-deps (session body result-type deps-string &optional imports-string modules-string)
|
||||
"Evaluate BODY in SESSION with DEPS-STRING context.
|
||||
|
||||
RESULT-TYPE is `value' or `output'.
|
||||
IMPORTS-STRING, if provided, is prepended to the code before execution."
|
||||
IMPORTS-STRING, if provided, is prepended after module definitions.
|
||||
MODULES-STRING, if provided, contains module definitions to prepend first.
|
||||
|
||||
When modules are defined, imports and user code are wrapped in Code.eval_string.
|
||||
This is required because Elixir cannot import a module defined in the same file
|
||||
at the top level - Code.eval_string defers evaluation until runtime."
|
||||
(let* ((buffer (ob-elixir--get-or-create-session-with-deps session deps-string))
|
||||
(wrapped (if (eq result-type 'value)
|
||||
(ob-elixir--session-wrap-for-value body)
|
||||
body))
|
||||
(code (if imports-string
|
||||
(concat (string-trim imports-string) "\n\n" wrapped)
|
||||
wrapped))
|
||||
(code (if modules-string
|
||||
;; When modules are defined, wrap imports + code in Code.eval_string
|
||||
(let ((eval-body (concat
|
||||
(when imports-string (concat (string-trim imports-string) "\n"))
|
||||
wrapped)))
|
||||
(concat
|
||||
(string-trim modules-string) "\n\n"
|
||||
"Code.eval_string(\"\"\"\n"
|
||||
(ob-elixir--escape-for-eval-string eval-body)
|
||||
"\n\"\"\")"))
|
||||
;; Normal path without modules
|
||||
(concat
|
||||
(when imports-string (concat (string-trim imports-string) "\n\n"))
|
||||
wrapped)))
|
||||
(eoe-indicator ob-elixir--eoe-marker)
|
||||
(full-body (concat code "\nIO.puts(\"" eoe-indicator "\")"))
|
||||
output)
|
||||
@@ -916,13 +1013,32 @@ The wrapper evaluates BODY, then prints the result using
|
||||
`inspect/2` with infinite limits to avoid truncation."
|
||||
(format ob-elixir--value-wrapper body))
|
||||
|
||||
(defun ob-elixir--execute (body result-type &optional imports-string)
|
||||
(defun ob-elixir--escape-for-eval-string (str)
|
||||
"Escape STR for use inside Elixir Code.eval_string heredoc.
|
||||
|
||||
Escapes backslashes and double quotes."
|
||||
(let ((result str))
|
||||
(setq result (replace-regexp-in-string "\\\\" "\\\\\\\\" result))
|
||||
(setq result (replace-regexp-in-string "\"" "\\\\\"" result))
|
||||
result))
|
||||
|
||||
(defun ob-elixir--execute (body result-type &optional imports-string modules-string)
|
||||
"Execute BODY as Elixir code.
|
||||
|
||||
RESULT-TYPE is either `value' or `output'.
|
||||
For `value', wraps code to capture return value.
|
||||
For `output', captures stdout directly.
|
||||
IMPORTS-STRING, if provided, is prepended to the code before execution.
|
||||
IMPORTS-STRING, if provided, contains import/alias/require statements.
|
||||
MODULES-STRING, if provided, contains module definitions to prepend.
|
||||
|
||||
Code is assembled in this order:
|
||||
1. Module definitions (MODULES-STRING)
|
||||
2. Imports (IMPORTS-STRING) - wrapped in Code.eval_string if modules present
|
||||
3. User code (BODY, wrapped for value if needed)
|
||||
|
||||
When modules are defined, imports and user code are wrapped in Code.eval_string.
|
||||
This is required because Elixir cannot import a module defined in the same file
|
||||
at the top level - Code.eval_string defers evaluation until runtime.
|
||||
|
||||
Returns the result as a string.
|
||||
May signal `ob-elixir-error' if execution fails and
|
||||
@@ -931,9 +1047,20 @@ May signal `ob-elixir-error' if execution fails and
|
||||
(wrapped (if (eq result-type 'value)
|
||||
(ob-elixir--wrap-for-value body)
|
||||
body))
|
||||
(code (if imports-string
|
||||
(concat (string-trim imports-string) "\n\n" wrapped)
|
||||
wrapped)))
|
||||
(code (if modules-string
|
||||
;; When modules are defined, wrap imports + code in Code.eval_string
|
||||
(let ((eval-body (concat
|
||||
(when imports-string (concat (string-trim imports-string) "\n"))
|
||||
wrapped)))
|
||||
(concat
|
||||
(string-trim modules-string) "\n\n"
|
||||
"Code.eval_string(\"\"\"\n"
|
||||
(ob-elixir--escape-for-eval-string eval-body)
|
||||
"\n\"\"\")"))
|
||||
;; Normal path without modules
|
||||
(concat
|
||||
(when imports-string (concat (string-trim imports-string) "\n\n"))
|
||||
wrapped))))
|
||||
(with-temp-file tmp-file
|
||||
(insert code))
|
||||
(let ((result (with-temp-buffer
|
||||
@@ -950,45 +1077,53 @@ 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* ((session (cdr (assq :session params)))
|
||||
(result-type (cdr (assq :result-type params)))
|
||||
(result-params (cdr (assq :result-params params)))
|
||||
;; Find deps for this block's position
|
||||
(deps-string (ob-elixir--find-deps-for-position (point)))
|
||||
;; Find imports for this block's position
|
||||
(imports-string (ob-elixir--find-imports-for-position (point)))
|
||||
;; Expand body with variable assignments
|
||||
(full-body (org-babel-expand-body:generic
|
||||
body params
|
||||
(org-babel-variable-assignments:elixir params)))
|
||||
(result (condition-case err
|
||||
(cond
|
||||
;; Session mode with deps
|
||||
((and session (not (string= session "none")) deps-string)
|
||||
(ob-elixir--evaluate-in-session-with-deps
|
||||
session full-body result-type deps-string imports-string))
|
||||
;; Session mode without deps
|
||||
((and session (not (string= session "none")))
|
||||
(ob-elixir--evaluate-in-session session full-body result-type imports-string))
|
||||
;; Non-session with deps
|
||||
(deps-string
|
||||
(ob-elixir--execute-with-deps full-body result-type deps-string imports-string))
|
||||
;; Plain execution
|
||||
(t
|
||||
(ob-elixir--execute full-body result-type imports-string)))
|
||||
(ob-elixir-error
|
||||
;; Return error message so it appears in buffer
|
||||
(cadr err)))))
|
||||
(org-babel-reassemble-table
|
||||
(org-babel-result-cond result-params
|
||||
;; For output/scalar/verbatim - return as-is
|
||||
result
|
||||
;; For value - parse into Elisp data
|
||||
(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))))))
|
||||
(let ((module-name (cdr (assq :module params))))
|
||||
(if module-name
|
||||
;; This is a module definition block - don't execute
|
||||
(format "Module %s: functions defined" module-name)
|
||||
;; Normal execution path
|
||||
(let* ((session (cdr (assq :session params)))
|
||||
(result-type (cdr (assq :result-type params)))
|
||||
(result-params (cdr (assq :result-params params)))
|
||||
;; Find deps for this block's position
|
||||
(deps-string (ob-elixir--find-deps-for-position (point)))
|
||||
;; Find imports for this block's position
|
||||
(imports-string (ob-elixir--find-imports-for-position (point)))
|
||||
;; Find module definitions for this block's position
|
||||
(modules-alist (ob-elixir--find-all-module-blocks (point)))
|
||||
(modules-string (ob-elixir--generate-module-definitions modules-alist))
|
||||
;; Expand body with variable assignments
|
||||
(full-body (org-babel-expand-body:generic
|
||||
body params
|
||||
(org-babel-variable-assignments:elixir params)))
|
||||
(result (condition-case err
|
||||
(cond
|
||||
;; Session mode with deps
|
||||
((and session (not (string= session "none")) deps-string)
|
||||
(ob-elixir--evaluate-in-session-with-deps
|
||||
session full-body result-type deps-string imports-string modules-string))
|
||||
;; Session mode without deps
|
||||
((and session (not (string= session "none")))
|
||||
(ob-elixir--evaluate-in-session session full-body result-type imports-string modules-string))
|
||||
;; Non-session with deps
|
||||
(deps-string
|
||||
(ob-elixir--execute-with-deps full-body result-type deps-string imports-string modules-string))
|
||||
;; Plain execution
|
||||
(t
|
||||
(ob-elixir--execute full-body result-type imports-string modules-string)))
|
||||
(ob-elixir-error
|
||||
;; Return error message so it appears in buffer
|
||||
(cadr err)))))
|
||||
(org-babel-reassemble-table
|
||||
(org-babel-result-cond result-params
|
||||
;; For output/scalar/verbatim - return as-is
|
||||
result
|
||||
;; For value - parse into Elisp data
|
||||
(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))))))))
|
||||
|
||||
(provide 'ob-elixir)
|
||||
;;; ob-elixir.el ends here
|
||||
|
||||
Reference in New Issue
Block a user