228 lines
7.3 KiB
Markdown
228 lines
7.3 KiB
Markdown
# 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:
|
|
|
|
```org
|
|
#+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:
|
|
|
|
```elisp
|
|
(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:
|
|
|
|
```elisp
|
|
(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:**
|
|
```elisp
|
|
(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:**
|
|
```elisp
|
|
(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-trim` ensures 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)
|
|
|
|
```elisp
|
|
;;; 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):
|
|
|
|
```elisp
|
|
(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:
|
|
|
|
```org
|
|
#+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.
|