446 lines
11 KiB
Markdown
446 lines
11 KiB
Markdown
# Emacs Lisp Best Practices for Package Development
|
|
|
|
This document covers best practices for writing Emacs Lisp packages, specifically targeting org-babel language implementations.
|
|
|
|
## Table of Contents
|
|
|
|
- [File Structure and Headers](#file-structure-and-headers)
|
|
- [Naming Conventions](#naming-conventions)
|
|
- [Variables and Customization](#variables-and-customization)
|
|
- [Function Definitions](#function-definitions)
|
|
- [Error Handling](#error-handling)
|
|
- [Documentation](#documentation)
|
|
- [Dependencies and Loading](#dependencies-and-loading)
|
|
- [References](#references)
|
|
|
|
---
|
|
|
|
## File Structure and Headers
|
|
|
|
Every Emacs Lisp package file should follow a standard structure:
|
|
|
|
```elisp
|
|
;;; ob-elixir.el --- Org Babel functions for Elixir evaluation -*- lexical-binding: t; -*-
|
|
|
|
;; Copyright (C) 2024 Your Name
|
|
|
|
;; Author: Your Name <your.email@example.com>
|
|
;; URL: https://github.com/username/ob-elixir
|
|
;; Keywords: literate programming, reproducible research, elixir
|
|
;; Version: 1.0.0
|
|
;; Package-Requires: ((emacs "27.1") (org "9.4"))
|
|
|
|
;; This file is not part of GNU Emacs.
|
|
|
|
;; This program is free software; you can redistribute it and/or modify
|
|
;; it under the terms of the GNU General Public License as published by
|
|
;; the Free Software Foundation, either version 3 of the License, or
|
|
;; (at your option) any later version.
|
|
|
|
;; This program is distributed in the hope that it will be useful,
|
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
;; GNU General Public License for more details.
|
|
|
|
;; You should have received a copy of the GNU General Public License
|
|
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
;;; Commentary:
|
|
|
|
;; This package provides Org Babel support for evaluating Elixir code blocks.
|
|
;;
|
|
;; Features:
|
|
;; - Execute Elixir code in org-mode source blocks
|
|
;; - Support for Mix project context
|
|
;; - Session support via IEx
|
|
;; - Variable passing between code blocks
|
|
;;
|
|
;; Usage:
|
|
;; Add (elixir . t) to `org-babel-load-languages':
|
|
;;
|
|
;; (org-babel-do-load-languages
|
|
;; 'org-babel-load-languages
|
|
;; '((elixir . t)))
|
|
|
|
;;; Code:
|
|
|
|
(require 'ob)
|
|
(require 'ob-ref)
|
|
(require 'ob-comint)
|
|
(require 'ob-eval)
|
|
|
|
;; ... your code here ...
|
|
|
|
(provide 'ob-elixir)
|
|
;;; ob-elixir.el ends here
|
|
```
|
|
|
|
### Key Header Elements
|
|
|
|
| Element | Purpose | Example |
|
|
|----------------------|--------------------------------------------|-----------------------|
|
|
| `lexical-binding: t` | Enable lexical scoping (always use this) | First line comment |
|
|
| `Package-Requires` | Declare dependencies with minimum versions | `((emacs "27.1"))` |
|
|
| `Keywords` | Help with package discovery | Standard keyword list |
|
|
| `Version` | Semantic versioning | `1.0.0` |
|
|
|
|
---
|
|
|
|
## Naming Conventions
|
|
|
|
### Package Prefix
|
|
|
|
All symbols (functions, variables, constants) must be prefixed with the package name to avoid namespace collisions:
|
|
|
|
```elisp
|
|
;; GOOD - properly prefixed
|
|
(defvar ob-elixir-command "elixir")
|
|
(defun ob-elixir-evaluate (body params) ...)
|
|
(defconst ob-elixir-eoe-indicator "---ob-elixir-eoe---")
|
|
|
|
;; BAD - no prefix, will collide
|
|
(defvar elixir-command "elixir")
|
|
(defun evaluate-elixir (body) ...)
|
|
```
|
|
|
|
### Naming Patterns for org-babel
|
|
|
|
Org-babel uses specific naming conventions that must be followed:
|
|
|
|
```elisp
|
|
;; Required function - note the colon syntax
|
|
(defun org-babel-execute:elixir (body params)
|
|
"Execute a block of Elixir code with Babel.")
|
|
|
|
;; Optional session function
|
|
(defun org-babel-elixir-initiate-session (&optional session params)
|
|
"Create or return an Elixir session buffer.")
|
|
|
|
;; Variable assignments function
|
|
(defun org-babel-variable-assignments:elixir (params)
|
|
"Return Elixir code to assign variables.")
|
|
|
|
;; Default header arguments variable
|
|
(defvar org-babel-default-header-args:elixir '()
|
|
"Default header arguments for Elixir code blocks.")
|
|
|
|
;; Prep session function
|
|
(defun org-babel-prep-session:elixir (session params)
|
|
"Prepare SESSION with PARAMS.")
|
|
```
|
|
|
|
### Private Functions
|
|
|
|
Use double hyphens for internal/private functions:
|
|
|
|
```elisp
|
|
;; Public API
|
|
(defun ob-elixir-evaluate (body params)
|
|
"Evaluate BODY with PARAMS.")
|
|
|
|
;; Internal helper - note the double hyphen
|
|
(defun ob-elixir--format-value (value)
|
|
"Internal: Format VALUE for Elixir.")
|
|
|
|
(defun ob-elixir--clean-output (output)
|
|
"Internal: Remove ANSI codes from OUTPUT.")
|
|
```
|
|
|
|
---
|
|
|
|
## Variables and Customization
|
|
|
|
### User-Customizable Variables
|
|
|
|
Use `defcustom` for variables users should configure:
|
|
|
|
```elisp
|
|
(defgroup ob-elixir nil
|
|
"Org Babel support for Elixir."
|
|
:group 'org-babel
|
|
:prefix "ob-elixir-")
|
|
|
|
(defcustom ob-elixir-command "elixir"
|
|
"Command to invoke Elixir.
|
|
Can be a full path or just the command name if in PATH."
|
|
:type 'string
|
|
:group 'ob-elixir)
|
|
|
|
(defcustom ob-elixir-iex-command "iex"
|
|
"Command to invoke IEx for session mode."
|
|
:type 'string
|
|
:group 'ob-elixir)
|
|
|
|
(defcustom ob-elixir-mix-command "mix"
|
|
"Command to invoke Mix."
|
|
:type 'string
|
|
:group 'ob-elixir)
|
|
|
|
(defcustom ob-elixir-default-session-timeout 30
|
|
"Default timeout in seconds for session operations."
|
|
:type 'integer
|
|
:group 'ob-elixir)
|
|
|
|
;; For options with specific choices
|
|
(defcustom ob-elixir-output-format 'value
|
|
"Default output format for results."
|
|
:type '(choice (const :tag "Return value" value)
|
|
(const :tag "Standard output" output))
|
|
:group 'ob-elixir)
|
|
```
|
|
|
|
### Internal Variables
|
|
|
|
Use `defvar` for internal state:
|
|
|
|
```elisp
|
|
(defvar ob-elixir--session-buffer nil
|
|
"Current active session buffer.
|
|
This is internal state and should not be modified directly.")
|
|
|
|
(defvar ob-elixir--pending-output ""
|
|
"Accumulated output waiting to be processed.")
|
|
```
|
|
|
|
### Constants
|
|
|
|
Use `defconst` for values that never change:
|
|
|
|
```elisp
|
|
(defconst ob-elixir-eoe-indicator "__ob_elixir_eoe__"
|
|
"String used to indicate end of evaluation output.")
|
|
|
|
(defconst ob-elixir-error-regexp "\\*\\* (\\w+Error)"
|
|
"Regexp to match Elixir error messages.")
|
|
```
|
|
|
|
---
|
|
|
|
## Function Definitions
|
|
|
|
### Function Documentation
|
|
|
|
Always include comprehensive docstrings:
|
|
|
|
```elisp
|
|
(defun ob-elixir-evaluate (body params)
|
|
"Evaluate BODY as Elixir code according to PARAMS.
|
|
|
|
BODY is the source code string to execute.
|
|
PARAMS is an alist of header arguments.
|
|
|
|
Supported header arguments:
|
|
:session - Name of IEx session to use, or \"none\"
|
|
:results - How to handle results (value, output)
|
|
:var - Variables to inject into the code
|
|
|
|
Returns the result as a string, or nil if execution failed.
|
|
|
|
Example:
|
|
(ob-elixir-evaluate \"1 + 1\" \\='((:results . \"value\")))"
|
|
...)
|
|
```
|
|
|
|
### Interactive Commands
|
|
|
|
Mark user-facing commands as interactive:
|
|
|
|
```elisp
|
|
(defun ob-elixir-check-installation ()
|
|
"Check if Elixir is properly installed and accessible.
|
|
Display version information in the minibuffer."
|
|
(interactive)
|
|
(let ((version (shell-command-to-string
|
|
(format "%s --version" ob-elixir-command))))
|
|
(message "Elixir: %s" (string-trim version))))
|
|
```
|
|
|
|
### Using `cl-lib` Properly
|
|
|
|
Prefer `cl-lib` functions over deprecated `cl` package:
|
|
|
|
```elisp
|
|
(require 'cl-lib)
|
|
|
|
;; Use cl- prefixed versions
|
|
(cl-defun ob-elixir-parse-result (output &key (format 'value))
|
|
"Parse OUTPUT according to FORMAT."
|
|
...)
|
|
|
|
(cl-loop for pair in params
|
|
when (eq (car pair) :var)
|
|
collect (cdr pair))
|
|
|
|
(cl-destructuring-bind (name . value) variable
|
|
...)
|
|
```
|
|
|
|
---
|
|
|
|
## Error Handling
|
|
|
|
### Signaling Errors
|
|
|
|
Use appropriate error functions:
|
|
|
|
```elisp
|
|
;; For user errors (incorrect usage)
|
|
(user-error "No Elixir session found for %s" session-name)
|
|
|
|
;; For programming errors
|
|
(error "Invalid parameter: %S" param)
|
|
|
|
;; Custom error types
|
|
(define-error 'ob-elixir-error "Ob-elixir error")
|
|
(define-error 'ob-elixir-execution-error "Elixir execution error" 'ob-elixir-error)
|
|
(define-error 'ob-elixir-session-error "Elixir session error" 'ob-elixir-error)
|
|
|
|
;; Signaling custom errors
|
|
(signal 'ob-elixir-execution-error (list "Compilation failed" output))
|
|
```
|
|
|
|
### Handling Errors Gracefully
|
|
|
|
```elisp
|
|
(defun ob-elixir-safe-evaluate (body params)
|
|
"Safely evaluate BODY, handling errors gracefully."
|
|
(condition-case err
|
|
(ob-elixir-evaluate body params)
|
|
(ob-elixir-execution-error
|
|
(message "Elixir error: %s" (cadr err))
|
|
nil)
|
|
(file-error
|
|
(user-error "Cannot access Elixir command: %s" (error-message-string err)))
|
|
(error
|
|
(message "Unexpected error: %s" (error-message-string err))
|
|
nil)))
|
|
```
|
|
|
|
### Input Validation
|
|
|
|
```elisp
|
|
(defun ob-elixir-evaluate (body params)
|
|
"Evaluate BODY with PARAMS."
|
|
;; Validate inputs early
|
|
(unless (stringp body)
|
|
(error "BODY must be a string, got %S" (type-of body)))
|
|
(unless (listp params)
|
|
(error "PARAMS must be an alist"))
|
|
(when (string-empty-p body)
|
|
(user-error "Cannot evaluate empty code block"))
|
|
...)
|
|
```
|
|
|
|
---
|
|
|
|
## Documentation
|
|
|
|
### Commentary Section
|
|
|
|
Write helpful commentary:
|
|
|
|
```elisp
|
|
;;; Commentary:
|
|
|
|
;; ob-elixir provides Org Babel support for the Elixir programming language.
|
|
;;
|
|
;; Installation:
|
|
;;
|
|
;; 1. Ensure Elixir is installed and in your PATH
|
|
;; 2. Add to your init.el:
|
|
;;
|
|
;; (require 'ob-elixir)
|
|
;; (org-babel-do-load-languages
|
|
;; 'org-babel-load-languages
|
|
;; '((elixir . t)))
|
|
;;
|
|
;; Usage:
|
|
;;
|
|
;; In an org file, create a source block:
|
|
;;
|
|
;; #+BEGIN_SRC elixir
|
|
;; IO.puts("Hello from Elixir!")
|
|
;; 1 + 1
|
|
;; #+END_SRC
|
|
;;
|
|
;; Press C-c C-c to evaluate.
|
|
;;
|
|
;; Header Arguments:
|
|
;;
|
|
;; - :session NAME Use a persistent IEx session
|
|
;; - :results value Return the last expression's value
|
|
;; - :results output Capture stdout
|
|
;; - :mix PROJECT Execute in Mix project context
|
|
;;
|
|
;; See the README for complete documentation.
|
|
```
|
|
|
|
### Info Manual Integration (Optional)
|
|
|
|
For larger packages, consider an info manual:
|
|
|
|
```elisp
|
|
(defun ob-elixir-info ()
|
|
"Open the ob-elixir info manual."
|
|
(interactive)
|
|
(info "(ob-elixir)"))
|
|
```
|
|
|
|
---
|
|
|
|
## Dependencies and Loading
|
|
|
|
### Requiring Dependencies
|
|
|
|
```elisp
|
|
;;; Code:
|
|
|
|
;; Required dependencies
|
|
(require 'ob)
|
|
(require 'ob-ref)
|
|
(require 'ob-comint)
|
|
(require 'ob-eval)
|
|
|
|
;; Soft dependencies (optional features)
|
|
(require 'elixir-mode nil t) ; t = noerror
|
|
|
|
;; Check for optional dependency
|
|
(defvar ob-elixir-has-elixir-mode (featurep 'elixir-mode)
|
|
"Non-nil if elixir-mode is available.")
|
|
```
|
|
|
|
### Autoloads
|
|
|
|
For functions that should be available before the package loads:
|
|
|
|
```elisp
|
|
;;;###autoload
|
|
(defun org-babel-execute:elixir (body params)
|
|
"Execute a block of Elixir code with Babel."
|
|
...)
|
|
|
|
;;;###autoload
|
|
(eval-after-load 'org
|
|
'(add-to-list 'org-src-lang-modes '("elixir" . elixir)))
|
|
```
|
|
|
|
### Provide Statement
|
|
|
|
Always end with provide:
|
|
|
|
```elisp
|
|
(provide 'ob-elixir)
|
|
;;; ob-elixir.el ends here
|
|
```
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
- [GNU Emacs Lisp Reference Manual](https://www.gnu.org/software/emacs/manual/elisp.html)
|
|
- [Emacs Lisp Style Guide](https://github.com/bbatsov/emacs-lisp-style-guide)
|
|
- [Packaging Guidelines (MELPA)](https://github.com/melpa/melpa/blob/master/CONTRIBUTING.org)
|
|
- [Writing GNU Emacs Extensions](https://www.gnu.org/software/emacs/manual/html_node/eintr/)
|
|
- [Org Mode Source Code](https://git.savannah.gnu.org/cgit/emacs/org-mode.git/)
|
|
- [Elisp Conventions (Emacs Wiki)](https://www.emacswiki.org/emacs/ElispConventions)
|