7.3 KiB
Task 12: Add Imports Block Support
Problem
When using common imports (import, alias, use, require) across multiple Elixir code blocks in an org file, users must manually add these lines to each block, leading to repetition and maintenance burden.
Desired Behavior
Users can define an imports block that automatically prepends its content to all subsequent Elixir code blocks:
#+BEGIN_IMPORTS elixir
import Enum
alias MyApp.Helpers, as: H
require Logger
#+END_IMPORTS
#+begin_src elixir
# This block will have the imports prepended automatically
map([1, 2, 3], &(&1 * 2))
#+end_src
Scope
- Imports block applies to all elixir blocks after it until the next imports block (or end of file)
- Works with both session and non-session modes
- Works with deps blocks (imports prepended after deps are loaded)
Implementation Plan
Step 1: Define the imports block regexp
File: ob-elixir.el (near line 418, after ob-elixir--deps-block-regexp)
Add: A new constant for matching imports blocks:
(defconst ob-elixir--imports-block-regexp
"^[ \t]*#\\+BEGIN_IMPORTS[ \t]+elixir[ \t]*\n\\(\\(?:.*\n\\)*?\\)[ \t]*#\\+END_IMPORTS"
"Regexp matching an imports block.
Group 1 captures the imports content.")
Step 2: Add function to find imports for a position
File: ob-elixir.el (after ob-elixir--find-deps-for-position)
Add: A function to find the most recent imports block before a given position:
(defun ob-elixir--find-imports-for-position (pos)
"Find the most recent imports block before POS.
Returns the imports content as a string, or nil if no imports block found."
(save-excursion
(goto-char pos)
(let ((found nil))
(while (and (not found)
(re-search-backward ob-elixir--imports-block-regexp nil t))
(when (< (match-end 0) pos)
(setq found (match-string-no-properties 1))))
found)))
Step 3: Modify org-babel-execute:elixir to prepend imports
File: ob-elixir.el (in org-babel-execute:elixir function, around line 923)
Current code:
(deps-string (ob-elixir--find-deps-for-position (point)))
;; Expand body with variable assignments
(full-body (org-babel-expand-body:generic
body params
(org-babel-variable-assignments:elixir params)))
Proposed change:
(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 (let ((expanded (org-babel-expand-body:generic
body params
(org-babel-variable-assignments:elixir params))))
;; Prepend imports if present
(if imports-string
(concat (string-trim imports-string) "\n\n" expanded)
expanded)))
Rationale:
- Imports are found before body expansion
- Imports are prepended to the fully expanded body (after variable assignments)
- The
string-trimensures no extra whitespace issues - A blank line separates imports from the user's code for readability
Step 4: Add test file for imports functionality
File: test/test-ob-elixir-imports.el (new file)
;;; test-ob-elixir-imports.el --- Imports block tests -*- lexical-binding: t; -*-
;;; Commentary:
;; Tests for the imports block functionality.
;;; Code:
(require 'ert)
(require 'ob-elixir)
;;; Imports Block Parsing Tests
(ert-deftest ob-elixir-test-imports-block-parsing ()
"Test that imports blocks are correctly parsed."
(with-temp-buffer
(insert "#+BEGIN_IMPORTS elixir\nimport Enum\nalias Foo\n#+END_IMPORTS\n")
(goto-char (point-max))
(let ((imports (ob-elixir--find-imports-for-position (point))))
(should imports)
(should (string-match-p "import Enum" imports))
(should (string-match-p "alias Foo" imports)))))
(ert-deftest ob-elixir-test-no-imports-block ()
"Test that nil is returned when no imports block exists."
(with-temp-buffer
(insert "#+begin_src elixir\n1 + 1\n#+end_src\n")
(should (null (ob-elixir--find-imports-for-position (point))))))
(ert-deftest ob-elixir-test-imports-block-before-position ()
"Test that imports block must be before position."
(with-temp-buffer
(insert "#+begin_src elixir\n1 + 1\n#+end_src\n")
(let ((pos (point)))
(insert "#+BEGIN_IMPORTS elixir\nimport Enum\n#+END_IMPORTS\n")
(should (null (ob-elixir--find-imports-for-position pos))))))
(ert-deftest ob-elixir-test-imports-block-override ()
"Test that later imports blocks override earlier ones."
(with-temp-buffer
(insert "#+BEGIN_IMPORTS elixir\nimport Enum\n#+END_IMPORTS\n")
(insert "#+BEGIN_IMPORTS elixir\nimport String\n#+END_IMPORTS\n")
(goto-char (point-max))
(let ((imports (ob-elixir--find-imports-for-position (point))))
(should imports)
(should (string-match-p "import String" imports))
(should-not (string-match-p "import Enum" imports)))))
;;; Imports Execution Tests
(ert-deftest ob-elixir-test-imports-execution ()
"Test that imports are applied during execution."
(skip-unless (executable-find ob-elixir-command))
(with-temp-buffer
(org-mode)
(insert "#+BEGIN_IMPORTS elixir\nimport Enum\n#+END_IMPORTS\n\n")
(insert "#+begin_src elixir :results value\nmap([1,2,3], &(&1 * 2))\n#+end_src\n")
(goto-char (point-min))
(search-forward "#+begin_src")
(let ((result (org-babel-execute-src-block)))
;; Without import, this would fail because map/2 requires Enum prefix
(should result)
(should (equal result '(2 4 6))))))
(ert-deftest ob-elixir-test-imports-with-alias ()
"Test that alias works in imports block."
(skip-unless (executable-find ob-elixir-command))
(with-temp-buffer
(org-mode)
(insert "#+BEGIN_IMPORTS elixir\nalias String, as: S\n#+END_IMPORTS\n\n")
(insert "#+begin_src elixir :results value\nS.upcase(\"hello\")\n#+end_src\n")
(goto-char (point-min))
(search-forward "#+begin_src")
(let ((result (org-babel-execute-src-block)))
(should (equal result "HELLO")))))
(provide 'test-ob-elixir-imports)
;;; test-ob-elixir-imports.el ends here
Step 5: Update test loader
File: test/test-ob-elixir.el
Add: Require statement for the new test file (with other requires):
(require 'test-ob-elixir-imports)
Summary of Changes
| File | Change |
|---|---|
ob-elixir.el |
Add ob-elixir--imports-block-regexp constant |
ob-elixir.el |
Add ob-elixir--find-imports-for-position function |
ob-elixir.el |
Modify org-babel-execute:elixir to prepend imports |
test/test-ob-elixir-imports.el |
New file with imports tests |
test/test-ob-elixir.el |
Require new test file |
Files NOT Modified
- Session-related code (imports will work automatically since they're prepended to body)
- Deps handling (imports are independent, applied after variable expansion)
Verification
After implementation, test with:
#+BEGIN_IMPORTS elixir
import Enum
alias String, as: S
#+END_IMPORTS
#+begin_src elixir
# Both of these should work without prefixes
result = map([1, 2, 3], &(&1 * 2))
S.upcase("hello")
#+end_src
Expected: The code block executes successfully using the imported map/2 function and the S alias.