docs and tasks
This commit is contained in:
445
docs/01-emacs-elisp-best-practices.md
Normal file
445
docs/01-emacs-elisp-best-practices.md
Normal file
@@ -0,0 +1,445 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user