359 lines
10 KiB
Markdown
359 lines
10 KiB
Markdown
# Task 10: Async Execution Support
|
|
|
|
**Phase**: 4 - Advanced Features
|
|
**Priority**: Low
|
|
**Estimated Time**: 3-4 hours
|
|
**Dependencies**: Phase 1 Complete
|
|
|
|
## Objective
|
|
|
|
Implement asynchronous execution so long-running Elixir code blocks don't freeze Emacs.
|
|
|
|
## Prerequisites
|
|
|
|
- Phase 1 complete
|
|
- Understanding of Emacs async processes
|
|
|
|
## Background
|
|
|
|
Some Elixir operations can take a long time:
|
|
- Database migrations
|
|
- Large data processing
|
|
- Network operations
|
|
- Build tasks
|
|
|
|
Async execution allows:
|
|
- Continue editing while code runs
|
|
- Visual indicator of running blocks
|
|
- Cancel long-running operations
|
|
|
|
## Steps
|
|
|
|
### Step 1: Add async configuration
|
|
|
|
Add to `ob-elixir.el`:
|
|
|
|
```elisp
|
|
;;; Async Configuration
|
|
|
|
(defcustom ob-elixir-async-timeout 300
|
|
"Timeout in seconds for async execution.
|
|
|
|
After this time, async execution will be cancelled."
|
|
:type 'integer
|
|
:group 'ob-elixir)
|
|
|
|
(defvar ob-elixir--async-processes (make-hash-table :test 'equal)
|
|
"Hash table mapping buffer positions to async processes.")
|
|
|
|
(defconst org-babel-header-args:elixir
|
|
'((mix-project . :any)
|
|
(mix-env . :any)
|
|
(remsh . :any)
|
|
(node-name . :any)
|
|
(node-sname . :any)
|
|
(cookie . :any)
|
|
(async . ((yes no)))) ; NEW: async execution
|
|
"Elixir-specific header arguments.")
|
|
```
|
|
|
|
### Step 2: Implement async execution
|
|
|
|
```elisp
|
|
;;; Async Execution
|
|
|
|
(defun ob-elixir--execute-async (body result-type callback)
|
|
"Execute BODY asynchronously.
|
|
|
|
RESULT-TYPE is 'value or 'output.
|
|
CALLBACK is called with the result when complete."
|
|
(let* ((tmp-file (org-babel-temp-file "ob-elixir-async-" ".exs"))
|
|
(code (if (eq result-type 'value)
|
|
(ob-elixir--wrap-for-value body)
|
|
body))
|
|
(output-buffer (generate-new-buffer " *ob-elixir-async*"))
|
|
process)
|
|
|
|
;; Write code to temp file
|
|
(with-temp-file tmp-file
|
|
(insert code))
|
|
|
|
;; Start async process
|
|
(setq process
|
|
(start-process
|
|
"ob-elixir-async"
|
|
output-buffer
|
|
ob-elixir-command
|
|
tmp-file))
|
|
|
|
;; Set up process sentinel
|
|
(set-process-sentinel
|
|
process
|
|
(lambda (proc event)
|
|
(when (memq (process-status proc) '(exit signal))
|
|
(let ((result (with-current-buffer (process-buffer proc)
|
|
(buffer-string))))
|
|
;; Clean up
|
|
(kill-buffer (process-buffer proc))
|
|
(delete-file tmp-file)
|
|
;; Call callback with result
|
|
(funcall callback (ob-elixir--process-result
|
|
(string-trim result)))))))
|
|
|
|
;; Set up timeout
|
|
(run-at-time ob-elixir-async-timeout nil
|
|
(lambda ()
|
|
(when (process-live-p process)
|
|
(kill-process process)
|
|
(funcall callback "Error: Async execution timed out"))))
|
|
|
|
process))
|
|
```
|
|
|
|
### Step 3: Integrate with org-babel
|
|
|
|
```elisp
|
|
(defun ob-elixir--async-p (params)
|
|
"Return t if PARAMS specify async execution."
|
|
(string= "yes" (cdr (assq :async params))))
|
|
|
|
(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)))
|
|
(async (ob-elixir--async-p params))
|
|
(full-body (org-babel-expand-body:generic
|
|
body params
|
|
(org-babel-variable-assignments:elixir params))))
|
|
|
|
(if async
|
|
;; Async execution
|
|
(ob-elixir--execute-async-block full-body result-type params)
|
|
;; Sync execution (existing code)
|
|
(let ((result (ob-elixir--execute-sync full-body result-type params)))
|
|
(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))))))))
|
|
|
|
(defun ob-elixir--execute-sync (body result-type params)
|
|
"Execute BODY synchronously."
|
|
(cond
|
|
((and (cdr (assq :session params))
|
|
(not (string= (cdr (assq :session params)) "none")))
|
|
(ob-elixir--evaluate-in-session
|
|
(cdr (assq :session params)) body result-type params))
|
|
((ob-elixir--resolve-mix-project params)
|
|
(ob-elixir--execute-with-mix body result-type params))
|
|
(t
|
|
(ob-elixir--execute body result-type))))
|
|
```
|
|
|
|
### Step 4: Implement async block handling
|
|
|
|
```elisp
|
|
(defun ob-elixir--execute-async-block (body result-type params)
|
|
"Execute BODY asynchronously and insert results when done."
|
|
(let ((buffer (current-buffer))
|
|
(point (point))
|
|
(result-params (cdr (assq :result-params params)))
|
|
(marker (copy-marker (point))))
|
|
|
|
;; Show placeholder
|
|
(ob-elixir--insert-async-placeholder marker)
|
|
|
|
;; Execute async
|
|
(ob-elixir--execute-async
|
|
body
|
|
result-type
|
|
(lambda (result)
|
|
(ob-elixir--insert-async-result
|
|
buffer marker result result-params params)))
|
|
|
|
;; Return placeholder message
|
|
"Executing asynchronously..."))
|
|
|
|
(defun ob-elixir--insert-async-placeholder (marker)
|
|
"Insert a placeholder at MARKER indicating async execution."
|
|
(save-excursion
|
|
(goto-char marker)
|
|
(end-of-line)
|
|
(insert "\n")
|
|
(insert "#+RESULTS:\n")
|
|
(insert ": [Executing...]\n")))
|
|
|
|
(defun ob-elixir--insert-async-result (buffer marker result result-params params)
|
|
"Insert RESULT at MARKER in BUFFER."
|
|
(when (buffer-live-p buffer)
|
|
(with-current-buffer buffer
|
|
(save-excursion
|
|
(goto-char marker)
|
|
;; Find and remove placeholder
|
|
(when (search-forward ": [Executing...]" nil t)
|
|
(beginning-of-line)
|
|
(let ((start (point)))
|
|
(forward-line 1)
|
|
(delete-region start (point))))
|
|
;; Insert real result
|
|
(let ((formatted (org-babel-result-cond result-params
|
|
result
|
|
(ob-elixir--table-or-string result))))
|
|
(org-babel-insert-result formatted result-params))))))
|
|
```
|
|
|
|
### Step 5: Add cancellation support
|
|
|
|
```elisp
|
|
(defun ob-elixir-cancel-async ()
|
|
"Cancel the async execution at point."
|
|
(interactive)
|
|
(let* ((pos (point))
|
|
(process (gethash pos ob-elixir--async-processes)))
|
|
(if (and process (process-live-p process))
|
|
(progn
|
|
(kill-process process)
|
|
(remhash pos ob-elixir--async-processes)
|
|
(message "Async execution cancelled"))
|
|
(message "No async execution at point"))))
|
|
|
|
(defun ob-elixir-cancel-all-async ()
|
|
"Cancel all running async executions."
|
|
(interactive)
|
|
(maphash (lambda (_pos process)
|
|
(when (process-live-p process)
|
|
(kill-process process)))
|
|
ob-elixir--async-processes)
|
|
(clrhash ob-elixir--async-processes)
|
|
(message "All async executions cancelled"))
|
|
```
|
|
|
|
### Step 6: Add visual indicators
|
|
|
|
```elisp
|
|
(defface ob-elixir-async-running
|
|
'((t :background "yellow" :foreground "black"))
|
|
"Face for source blocks with running async execution."
|
|
:group 'ob-elixir)
|
|
|
|
(defun ob-elixir--highlight-async-block (start end)
|
|
"Highlight the region from START to END as running."
|
|
(let ((overlay (make-overlay start end)))
|
|
(overlay-put overlay 'face 'ob-elixir-async-running)
|
|
(overlay-put overlay 'ob-elixir-async t)
|
|
overlay))
|
|
|
|
(defun ob-elixir--remove-async-highlight ()
|
|
"Remove async highlighting from current block."
|
|
(dolist (ov (overlays-in (point-min) (point-max)))
|
|
(when (overlay-get ov 'ob-elixir-async)
|
|
(delete-overlay ov))))
|
|
```
|
|
|
|
### Step 7: Add tests
|
|
|
|
Create `test/test-ob-elixir-async.el`:
|
|
|
|
```elisp
|
|
;;; test-ob-elixir-async.el --- Async execution tests -*- lexical-binding: t; -*-
|
|
|
|
(require 'ert)
|
|
(require 'ob-elixir)
|
|
|
|
(ert-deftest ob-elixir-test-async-detection ()
|
|
"Test async header argument detection."
|
|
(should (ob-elixir--async-p '((:async . "yes"))))
|
|
(should-not (ob-elixir--async-p '((:async . "no"))))
|
|
(should-not (ob-elixir--async-p '())))
|
|
|
|
(ert-deftest ob-elixir-test-async-execution ()
|
|
"Test async execution completion."
|
|
(skip-unless (executable-find ob-elixir-command))
|
|
(let ((result nil)
|
|
(done nil))
|
|
(ob-elixir--execute-async
|
|
"1 + 1"
|
|
'value
|
|
(lambda (r)
|
|
(setq result r)
|
|
(setq done t)))
|
|
;; Wait for completion
|
|
(with-timeout (10 (error "Async test timed out"))
|
|
(while (not done)
|
|
(accept-process-output nil 0.1)))
|
|
(should (equal "2" result))))
|
|
|
|
(ert-deftest ob-elixir-test-async-timeout ()
|
|
"Test async timeout handling."
|
|
(skip-unless (executable-find ob-elixir-command))
|
|
(let ((ob-elixir-async-timeout 1)
|
|
(result nil)
|
|
(done nil))
|
|
(ob-elixir--execute-async
|
|
":timer.sleep(5000)" ; Sleep for 5 seconds
|
|
'value
|
|
(lambda (r)
|
|
(setq result r)
|
|
(setq done t)))
|
|
;; Wait for timeout
|
|
(with-timeout (3 (error "Test timed out"))
|
|
(while (not done)
|
|
(accept-process-output nil 0.1)))
|
|
(should (string-match-p "timed out" result))))
|
|
|
|
(provide 'test-ob-elixir-async)
|
|
```
|
|
|
|
### Step 8: Document usage
|
|
|
|
Add to documentation:
|
|
|
|
```org
|
|
* Async Execution
|
|
|
|
** Long-running computation
|
|
|
|
#+BEGIN_SRC elixir :async yes
|
|
# This won't block Emacs
|
|
Enum.reduce(1..1000000, 0, &+/2)
|
|
#+END_SRC
|
|
|
|
** Async with Mix project
|
|
|
|
#+BEGIN_SRC elixir :async yes :mix-project ~/my_app
|
|
MyApp.expensive_operation()
|
|
#+END_SRC
|
|
|
|
** Cancel with M-x ob-elixir-cancel-async
|
|
```
|
|
|
|
## Acceptance Criteria
|
|
|
|
- [ ] `:async yes` executes code asynchronously
|
|
- [ ] Placeholder shown while executing
|
|
- [ ] Results inserted when complete
|
|
- [ ] Timeout handled gracefully
|
|
- [ ] `ob-elixir-cancel-async` cancels execution
|
|
- [ ] Visual indicator for running blocks
|
|
- [ ] All tests pass
|
|
|
|
## Limitations
|
|
|
|
- Sessions cannot be async (they're inherently stateful)
|
|
- Multiple async blocks may have ordering issues
|
|
- Async results may not integrate perfectly with noweb
|
|
|
|
## Files Modified
|
|
|
|
- `ob-elixir.el` - Add async support
|
|
- `test/test-ob-elixir-async.el` - Add async tests
|
|
|
|
## References
|
|
|
|
- [Emacs Async Processes](https://www.gnu.org/software/emacs/manual/html_node/elisp/Asynchronous-Processes.html)
|
|
- [Process Sentinels](https://www.gnu.org/software/emacs/manual/html_node/elisp/Sentinels.html)
|