Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:47:18 +08:00
commit 47ea710e8d
15 changed files with 4925 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"name": "emacs-libraries",
"description": "Skills for various Emacs libraries and packages",
"version": "1.0.0",
"author": {
"name": "Hugo Duncan",
"email": "hugo@hugoduncan.org"
},
"skills": [
"./skills"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# emacs-libraries
Skills for various Emacs libraries and packages

89
plugin.lock.json Normal file
View File

@@ -0,0 +1,89 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:hugoduncan/library-skills:plugins/emacs-libraries",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "008da69fec532b6ae90eeb92d97be681a0af8c73",
"treeHash": "06cec28ca7e8f888d4ea09c147998ad68c14db4a8b6490b1c7e1f61bc9fa244d",
"generatedAt": "2025-11-28T10:17:36.453672Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "emacs-libraries",
"description": "Skills for various Emacs libraries and packages",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "31a4fcb29e9ee05f77f41d89b832bc1c7a0788d9beaf910505758ec8e859476d"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "fd39bd083e143c949cfd3187cf4febd5316549f83816d5f3b402925555cbecc7"
},
{
"path": "skills/package-conventions/SKILL.md",
"sha256": "10deb934ceb79fd046dd32a5cc5ff6dcc9459fd8bf25986ba61dc211de2b48e0"
},
{
"path": "skills/widget/metadata.edn",
"sha256": "1535bc89faebf21ec54189be82cc31f85162304fb687f044863017b0ba3f6256"
},
{
"path": "skills/widget/README.md",
"sha256": "935c09c861c3cce049b00f445905fee0553407ba621d238b313d5477fdd82d4c"
},
{
"path": "skills/widget/SKILL.md",
"sha256": "4af47372576b4cbf014830c053d3b271fec2302590bbff88c4665f6fbb8026fb"
},
{
"path": "skills/ert/metadata.edn",
"sha256": "8f403b4b940dd363bebb55dea2b99b4c5648651a57f1629eff8abadb6a2b6442"
},
{
"path": "skills/ert/README.md",
"sha256": "6af2707c8fa2bc14ea1b01bb9c505ca2c3fa4bba96e5039dcea1cd2c37461c5f"
},
{
"path": "skills/ert/SUMMARY.txt",
"sha256": "ed21138798de501d2f3dcaf0aaf1d7f4cf0cda3fbe699c36f1d1d8a177d0a85c"
},
{
"path": "skills/ert/SKILL.md",
"sha256": "532618d8ff957d6ff836a7241873ca08238355ba13f1f64940d5fd59fee669d3"
},
{
"path": "skills/ert/QUICK_REFERENCE.md",
"sha256": "c485f4436a3cd0beef8fe4c1197b02b748a2e86fab57efbdad10b915f2134224"
},
{
"path": "skills/magit-section/metadata.edn",
"sha256": "79f2c862c8c01c1497a04c8aa206e4b976c2bc59303f08e983c3308ae6068326"
},
{
"path": "skills/magit-section/README.md",
"sha256": "9e800e46557ab5a272e08074f42aa8c494feab72f2e720739f079e19c2bb3c59"
},
{
"path": "skills/magit-section/SKILL.md",
"sha256": "eeab118c2e685d288422952b20bbe28349ba3fa1bad87f55e08d058fe56d1868"
}
],
"dirSha256": "06cec28ca7e8f888d4ea09c147998ad68c14db4a8b6490b1c7e1f61bc9fa244d"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,270 @@
# ERT Quick Reference
## Test Definition
```elisp
(ert-deftest NAME ()
[DOCSTRING]
[:tags (TAG...)]
BODY...)
```
**Example:**
```elisp
(ert-deftest my-pkg-test-feature ()
"Test feature implementation."
:tags '(unit quick)
(should (my-pkg-feature-works)))
```
## Assertions
| Macro | Purpose | Example |
|-------|---------|---------|
| `(should FORM)` | Assert true | `(should (= 4 (+ 2 2)))` |
| `(should-not FORM)` | Assert false | `(should-not (zerop 5))` |
| `(should-error FORM [:type TYPE])` | Assert error | `(should-error (/ 1 0) :type 'arith-error)` |
## Running Tests
### Interactive
```elisp
M-x ert RET SELECTOR RET
```
**Common selectors:**
- `t` - All tests
- `"^my-pkg-"` - Tests matching regex
- `:tag quick` - Tests with tag
- `:failed` - Failed tests from last run
- `(not :tag slow)` - Exclude slow tests
### Batch Mode
```bash
# Run all tests
emacs -batch -l ert -l tests.el -f ert-run-tests-batch-and-exit
# Run specific tests
emacs -batch -l ert -l tests.el \
--eval '(ert-run-tests-batch-and-exit "^test-feature-")'
# Quiet mode
emacs -batch -l ert -l tests.el \
--eval '(let ((ert-quiet t)) (ert-run-tests-batch-and-exit))'
```
## Interactive Debugging Keys
In `*ert*` results buffer:
| Key | Command | Description |
|-----|---------|-------------|
| `.` | Jump to definition | Open test source code |
| `d` | Debug test | Re-run with debugger |
| `b` | Show backtrace | Display failure backtrace |
| `r` | Re-run test | Re-run test at point |
| `R` | Re-run all | Re-run all tests |
| `l` | Show assertions | Display executed `should` forms |
| `m` | Show messages | Display test messages |
| `TAB` | Expand/collapse | Toggle test details |
## Test Environment Patterns
### Temporary Buffer
```elisp
(with-temp-buffer
(insert "content")
(my-function)
(should (string= (buffer-string) "expected")))
```
### Cleanup
```elisp
(let ((resource (create-resource)))
(unwind-protect
(should (use-resource resource))
(cleanup-resource resource)))
```
### Local Variables
```elisp
(let ((my-var 'test-value))
(should (eq 'test-value (get-my-var))))
```
## Mocking
### With cl-letf
```elisp
(cl-letf (((symbol-function 'external-fn)
(lambda () "mocked")))
(should (string= "mocked" (calls-external-fn))))
```
### With flet (older style)
```elisp
(require 'cl)
(flet ((external-fn () "mocked"))
(should (string= "mocked" (calls-external-fn))))
```
## Skip Tests
```elisp
;; Skip if condition false
(skip-unless (featurep 'some-feature))
;; Skip if condition true
(skip-when (eq system-type 'windows-nt))
```
## Test Organization
### Naming Convention
```elisp
;; Format: package-test-feature
(ert-deftest my-package-test-parsing () ...)
(ert-deftest my-package-test-validation () ...)
```
### Tags
```elisp
(ert-deftest test-quick ()
:tags '(quick unit)
...)
(ert-deftest test-slow ()
:tags '(slow integration)
...)
;; Run: M-x ert RET :tag quick RET
;; Run: M-x ert RET (not :tag slow) RET
```
## Fixture Pattern
```elisp
(defun with-test-fixture (body)
"Execute BODY within test fixture."
(let ((setup (do-setup)))
(unwind-protect
(funcall body setup)
(do-teardown setup))))
(ert-deftest my-test ()
(with-test-fixture
(lambda (fixture)
(should (test-with fixture)))))
```
## Common Test Patterns
### Testing Errors
```elisp
;; Any error
(should-error (/ 1 0))
;; Specific error type
(should-error (error "msg") :type 'error)
(should-error (/ 1 0) :type 'arith-error)
```
### Testing Buffer State
```elisp
(with-temp-buffer
(my-mode)
(should (eq major-mode 'my-mode))
(should (local-variable-p 'my-mode-var)))
```
### Testing Interactive Commands
```elisp
(with-temp-buffer
(insert "text")
(goto-char (point-min))
(call-interactively 'my-command)
(should (= (point) 5)))
```
### Testing Messages
```elisp
(let ((messages))
(cl-letf (((symbol-function 'message)
(lambda (fmt &rest args)
(push (apply #'format fmt args) messages))))
(my-function)
(should (member "Expected message" messages))))
```
## Test File Template
```elisp
;;; my-package-test.el --- Tests for my-package -*- lexical-binding: t -*-
;;; Commentary:
;; Test suite for my-package.
;;; Code:
(require 'ert)
(require 'my-package)
(ert-deftest my-package-test-basic ()
"Test basic functionality."
(should (my-package-function)))
(provide 'my-package-test)
;;; my-package-test.el ends here
```
## Makefile Integration
```makefile
.PHONY: test
test:
emacs -batch -l ert \
-l my-package.el \
-l test/my-package-test.el \
-f ert-run-tests-batch-and-exit
```
## CI/CD (GitHub Actions)
```yaml
- name: Run tests
run: |
emacs -batch -l ert \
-l my-package.el \
-l test/my-package-test.el \
-f ert-run-tests-batch-and-exit
```
## Key Best Practices
1. **Name descriptively:** `package-test-feature` format
2. **Use tags:** Organize by speed/type
3. **Isolate environment:** `let`, temp buffers
4. **Always cleanup:** `unwind-protect`
5. **Mock I/O:** Avoid filesystem/network
6. **Test errors:** Use `should-error`
7. **One focus per test:** Clear failures
8. **Add docstrings:** Document test purpose
## Resources
- **Manual:** `C-h i m ert RET`
- **Online:** https://www.gnu.org/software/emacs/manual/html_mono/ert.html
- **Source:** `lisp/emacs-lisp/ert.el`

142
skills/ert/README.md Normal file
View File

@@ -0,0 +1,142 @@
# ERT (Emacs Lisp Regression Testing)
Built-in testing framework for Emacs Lisp, providing comprehensive test definition, execution, and debugging capabilities.
## Quick Start
### Basic Test
```elisp
(require 'ert)
(ert-deftest test-addition ()
"Test that addition works correctly."
(should (= 4 (+ 2 2)))
(should (= 0 (+ -5 5))))
```
### Run Tests
**Interactively:**
```elisp
M-x ert RET t RET
```
**From Command Line:**
```bash
emacs -batch -l ert -l my-tests.el -f ert-run-tests-batch-and-exit
```
### Assertions
```elisp
;; Assert true
(should (= 2 (+ 1 1)))
;; Assert false
(should-not (= 3 (+ 1 1)))
;; Assert error
(should-error (/ 1 0) :type 'arith-error)
```
## Key Features
- **Built-in:** No installation required (Emacs 24+)
- **Interactive debugging:** Backtrace inspection, test reruns
- **Flexible selectors:** Run specific tests by name, tag, or status
- **Batch mode:** CI/CD integration with exit codes
- **Mocking support:** Easy mocking via dynamic binding
- **Rich assertions:** Detailed failure reporting with subexpression values
## Common Patterns
### Temporary Buffers
```elisp
(ert-deftest test-buffer-operation ()
(with-temp-buffer
(insert "test content")
(my-function)
(should (string= (buffer-string) "expected"))))
```
### Cleanup with unwind-protect
```elisp
(ert-deftest test-with-cleanup ()
(let ((temp-file (make-temp-file "test-")))
(unwind-protect
(progn
(write-region "data" nil temp-file)
(should (file-exists-p temp-file)))
(delete-file temp-file))))
```
### Mocking Functions
```elisp
(ert-deftest test-with-mock ()
(cl-letf (((symbol-function 'external-call)
(lambda () "mocked result")))
(should (string= "mocked result" (my-function)))))
```
### Test Organization with Tags
```elisp
(ert-deftest test-quick-operation ()
:tags '(quick unit)
(should (fast-function)))
;; Run only quick tests
M-x ert RET :tag quick RET
```
## Interactive Debugging Commands
When viewing test results (after `M-x ert`):
- `.` - Jump to test definition
- `d` - Re-run test with debugger
- `b` - Show backtrace
- `r` - Re-run test
- `R` - Re-run all tests
- `l` - Show executed assertions
- `m` - Show messages
## Best Practices
1. **Name tests descriptively:** `package-test-feature` format
2. **One assertion focus per test:** Makes failures clear
3. **Isolate test environment:** Use `let`, temporary buffers
4. **Always cleanup:** Use `unwind-protect` for resources
5. **Tag tests:** Organize by speed and type
6. **Test error cases:** Use `should-error` for edge cases
7. **Mock external dependencies:** Avoid filesystem/network I/O
## Using the Skill
This skill provides:
- Complete API reference for all ERT functions
- Comprehensive best practices and patterns
- Detailed examples for common testing scenarios
- Debugging techniques and workflow integration
- CI/CD integration examples
Consult `SKILL.md` for in-depth documentation.
## Learning Path
1. **Basics:** `ert-deftest`, `should`, running tests interactively
2. **Assertions:** `should-not`, `should-error` with error types
3. **Environment:** `with-temp-buffer`, `let`, `unwind-protect`
4. **Organization:** Tags, test naming, selectors
5. **Advanced:** Mocking, fixtures, custom assertions
6. **Integration:** Batch mode, CI/CD, Makefiles
## External Resources
- [Official ERT Manual](https://www.gnu.org/software/emacs/manual/html_mono/ert.html)
- [ERT in Emacs Info](info:ert) or `C-h i m ert RET`
- [ERT Reference Card](https://github.com/fniessen/refcard-ERT)

935
skills/ert/SKILL.md Normal file
View File

@@ -0,0 +1,935 @@
---
name: ert
description: A guide to using ERT (Emacs Lisp Regression Testing) for testing Emacs Lisp code.
---
# ERT: Emacs Lisp Regression Testing
ERT is Emacs's built-in testing framework for automated testing of Emacs Lisp code. It provides facilities for defining tests, running them interactively or in batch mode, and debugging failures with integrated tooling.
## Overview
ERT (Emacs Lisp Regression Testing) is included with Emacs and requires no additional installation. It leverages Emacs's dynamic and interactive nature to provide powerful testing capabilities for unit tests, integration tests, and regression prevention.
**Key Characteristics:**
- Built into Emacs (available in Emacs 24+)
- Interactive debugging with backtrace inspection
- Flexible test selection and organization
- Batch mode for CI/CD integration
- Dynamic binding for easy mocking
- No external dependencies
## Core Concepts
### Test Definition
Tests are defined using `ert-deftest`, which creates a named test function:
```elisp
(ert-deftest test-name ()
"Docstring describing what the test verifies."
(should (= 2 (+ 1 1))))
```
### Assertions (Should Forms)
ERT provides three assertion macros:
- `should` - Assert that a form evaluates to non-nil
- `should-not` - Assert that a form evaluates to nil
- `should-error` - Assert that a form signals an error
Unlike `cl-assert`, these macros provide detailed error reporting including the form, evaluated subexpressions, and resulting values.
### Test Selectors
Selectors specify which tests to run:
- `t` - All tests
- `"regex"` - Tests matching regular expression
- `:tag symbol` - Tests tagged with symbol
- `:failed` - Tests that failed in last run
- `:passed` - Tests that passed in last run
- Combinations using `(and ...)`, `(or ...)`, `(not ...)`
## API Reference
### Defining Tests
#### `ert-deftest`
```elisp
(ert-deftest NAME () [DOCSTRING] [:tags (TAG...)] BODY...)
```
Define a test named NAME.
**Parameters:**
- `NAME` - Symbol naming the test
- `DOCSTRING` - Optional description of what the test verifies
- `:tags` - Optional list of tags for test organization
- `BODY` - Test code containing assertions
**Example:**
```elisp
(ert-deftest test-addition ()
"Test basic arithmetic addition."
:tags '(arithmetic quick)
(should (= 4 (+ 2 2)))
(should (= 0 (+ -1 1))))
```
### Assertion Macros
#### `should`
```elisp
(should FORM)
```
Assert that FORM evaluates to non-nil. On failure, displays the form and all evaluated subexpressions.
**Example:**
```elisp
(should (string-match "foo" "foobar"))
(should (< 1 2))
(should (member 'x '(x y z)))
```
#### `should-not`
```elisp
(should-not FORM)
```
Assert that FORM evaluates to nil.
**Example:**
```elisp
(should-not (string-match "baz" "foobar"))
(should-not (> 1 2))
```
#### `should-error`
```elisp
(should-error FORM [:type TYPE])
```
Assert that FORM signals an error. Optional `:type` specifies the expected error type.
**Example:**
```elisp
;; Any error accepted
(should-error (/ 1 0))
;; Specific error type required
(should-error (/ 1 0) :type 'arith-error)
;; Wrong error type would fail
(should-error (error "message") :type 'arith-error) ; fails
```
### Running Tests Interactively
#### `ert` / `ert-run-tests-interactively`
```elisp
M-x ert RET SELECTOR RET
```
Run tests matching SELECTOR and display results in interactive buffer.
**Common selectors:**
- `t` - Run all tests
- `"^my-package-"` - Tests matching regex
- `:failed` - Re-run failed tests
**Interactive debugging commands:**
- `.` - Jump to test definition
- `d` - Re-run test with debugger enabled
- `b` - Show backtrace of failed test
- `r` - Re-run test at point
- `R` - Re-run all tests
- `l` - Show executed `should` forms
- `m` - Show messages from test
- `TAB` - Expand/collapse test details
### Running Tests in Batch Mode
#### `ert-run-tests-batch-and-exit`
```elisp
(ert-run-tests-batch-and-exit [SELECTOR])
```
Run tests in batch mode and exit with status code (0 for success, non-zero for failure).
**Command line usage:**
```bash
# Run all tests
emacs -batch -l ert -l my-tests.el -f ert-run-tests-batch-and-exit
# Run specific tests
emacs -batch -l ert -l my-tests.el \
--eval '(ert-run-tests-batch-and-exit "^test-feature-")'
# Quiet mode (only unexpected results)
emacs -batch -l ert -l my-tests.el \
--eval '(let ((ert-quiet t)) (ert-run-tests-batch-and-exit))'
```
#### `ert-run-tests-batch`
```elisp
(ert-run-tests-batch [SELECTOR])
```
Run tests in batch mode but do not exit. Useful when running tests is part of a larger batch script.
### Test Organization
#### Tags
Use `:tags` in `ert-deftest` to organize tests:
```elisp
(ert-deftest test-fast-operation ()
:tags '(quick unit)
(should (fast-function)))
(ert-deftest test-slow-integration ()
:tags '(slow integration)
(should (slow-integration-test)))
```
Run tagged tests:
```elisp
;; Run only quick tests
M-x ert RET :tag quick RET
;; Run integration tests
M-x ert RET :tag integration RET
```
#### Test Naming Conventions
Prefix test names with the package name:
```elisp
(ert-deftest my-package-test-feature ()
"Test feature implementation."
...)
(ert-deftest my-package-test-edge-case ()
"Test handling of edge case."
...)
```
This enables:
- Running all package tests: `M-x ert RET "^my-package-" RET`
- Clear test ownership and organization
- Avoiding name collisions
### Skipping Tests
#### `skip-unless`
```elisp
(skip-unless CONDITION)
```
Skip test if CONDITION is nil.
**Example:**
```elisp
(ert-deftest test-graphical-feature ()
"Test feature requiring graphical display."
(skip-unless (display-graphic-p))
(should (graphical-operation)))
```
#### `skip-when`
```elisp
(skip-when CONDITION)
```
Skip test if CONDITION is non-nil.
**Example:**
```elisp
(ert-deftest test-unix-specific ()
"Test Unix-specific functionality."
(skip-when (eq system-type 'windows-nt))
(should (unix-specific-function)))
```
### Programmatic Test Execution
#### `ert-run-tests`
```elisp
(ert-run-tests SELECTOR LISTENER)
```
Run tests matching SELECTOR, reporting results to LISTENER.
**Example:**
```elisp
;; Run all tests silently
(ert-run-tests t #'ert-quiet-listener)
;; Custom listener
(defun my-listener (event-type &rest data)
(pcase event-type
('test-started ...)
('test-passed ...)
('test-failed ...)
('run-ended ...)))
(ert-run-tests "^my-" #'my-listener)
```
## Best Practices
### Test Structure
**1. Use descriptive test names:**
```elisp
;; Good
(ert-deftest my-package-parse-valid-json ()
...)
;; Poor
(ert-deftest test1 ()
...)
```
**2. Include clear docstrings:**
```elisp
(ert-deftest my-package-handle-empty-input ()
"Verify that empty input returns nil without error."
(should-not (my-package-process "")))
```
**3. One logical assertion per test:**
```elisp
;; Good - focused test
(ert-deftest my-package-parse-returns-alist ()
"Parser returns result as alist."
(should (listp (my-package-parse "data")))
(should (eq 'cons (type-of (car (my-package-parse "data"))))))
;; Better - even more focused
(ert-deftest my-package-parse-returns-list ()
"Parser returns a list."
(should (listp (my-package-parse "data"))))
(ert-deftest my-package-parse-list-contains-alist-entries ()
"Parser list contains alist entries."
(should (eq 'cons (type-of (car (my-package-parse "data"))))))
```
### Test Environment
**1. Isolate tests from environment:**
```elisp
(ert-deftest my-package-test-configuration ()
"Test respects custom configuration."
;; Save and restore configuration
(let ((my-package-option 'custom-value))
(should (eq 'custom-value (my-package-get-option)))))
```
**2. Use temporary buffers:**
```elisp
(ert-deftest my-package-buffer-manipulation ()
"Test buffer manipulation functions."
(with-temp-buffer
(insert "test content")
(my-package-process-buffer)
(should (string= (buffer-string) "processed content"))))
```
**3. Clean up with `unwind-protect`:**
```elisp
(ert-deftest my-package-file-operation ()
"Test file operations with cleanup."
(let ((temp-file (make-temp-file "my-package-test-")))
(unwind-protect
(progn
(my-package-write-file temp-file "data")
(should (file-exists-p temp-file))
(should (string= "data" (my-package-read-file temp-file))))
;; Cleanup always runs
(when (file-exists-p temp-file)
(delete-file temp-file)))))
```
### Fixtures with Higher-Order Functions
Instead of traditional fixture systems, use Lisp functions:
```elisp
(defun my-package-with-test-environment (body)
"Execute BODY within test environment."
(let ((my-package-test-mode t)
(original-config (my-package-get-config)))
(unwind-protect
(progn
(my-package-set-config 'test-config)
(funcall body))
(my-package-set-config original-config))))
(ert-deftest my-package-test-feature ()
"Test feature in test environment."
(my-package-with-test-environment
(lambda ()
(should (my-package-feature-works)))))
```
### Mocking with Dynamic Binding
Use `cl-letf` to override functions temporarily:
```elisp
(ert-deftest my-package-test-without-side-effects ()
"Test function without filesystem access."
(cl-letf (((symbol-function 'file-exists-p)
(lambda (file) t))
((symbol-function 'insert-file-contents)
(lambda (file) (insert "mock content"))))
(should (my-package-load-config "config.el"))))
```
Traditional `flet` can also be used:
```elisp
(require 'cl)
(ert-deftest my-package-test-mocked ()
"Test with mocked dependencies."
(flet ((external-api-call (arg) "mocked response"))
(should (string= "mocked response"
(my-package-use-api "data")))))
```
### Handling Preconditions
**Skip tests when preconditions aren't met:**
```elisp
(ert-deftest my-package-test-requires-feature ()
"Test functionality requiring optional feature."
(skip-unless (featurep 'some-feature))
(should (my-package-use-feature)))
(ert-deftest my-package-test-requires-external-tool ()
"Test requiring external program."
(skip-unless (executable-find "tool"))
(should (my-package-call-tool)))
```
### Testing for Side Effects
**1. Test buffer modifications:**
```elisp
(ert-deftest my-package-insert-text ()
"Verify text insertion."
(with-temp-buffer
(my-package-insert-greeting)
(should (string= (buffer-string) "Hello, World!\n"))
(should (= (point) (point-max)))))
```
**2. Test variable changes:**
```elisp
(ert-deftest my-package-increment-counter ()
"Counter increments correctly."
(let ((my-package-counter 0))
(my-package-increment)
(should (= my-package-counter 1))
(my-package-increment)
(should (= my-package-counter 2))))
```
**3. Test message output:**
```elisp
(ert-deftest my-package-logs-message ()
"Function logs expected message."
(let ((logged-messages))
(cl-letf (((symbol-function 'message)
(lambda (fmt &rest args)
(push (apply #'format fmt args) logged-messages))))
(my-package-operation)
(should (member "Operation completed" logged-messages)))))
```
### Error Handling
**1. Test error conditions:**
```elisp
(ert-deftest my-package-invalid-input-signals-error ()
"Invalid input signals appropriate error."
(should-error (my-package-parse nil) :type 'wrong-type-argument)
(should-error (my-package-parse "") :type 'user-error))
```
**2. Test error recovery:**
```elisp
(ert-deftest my-package-recovers-from-error ()
"Function recovers gracefully from error condition."
(let ((result (my-package-safe-operation 'invalid-input)))
(should (eq result 'fallback-value))
(should (my-package-error-logged-p))))
```
### Testing Asynchronous Code
**Use timers and `accept-process-output`:**
```elisp
(ert-deftest my-package-async-operation ()
"Test asynchronous operation completion."
(let ((callback-called nil)
(callback-result nil))
(my-package-async-call
(lambda (result)
(setq callback-called t
callback-result result)))
;; Wait for async operation
(with-timeout (5 (error "Async operation timeout"))
(while (not callback-called)
(accept-process-output nil 0.1)))
(should callback-called)
(should (string= "expected" callback-result))))
```
### Debugging Failed Tests
**1. Use detailed assertions:**
```elisp
;; Less helpful
(should (my-package-valid-p data))
;; More helpful
(should (listp data))
(should (= 3 (length data)))
(should (stringp (car data)))
```
**2. Add messages for context:**
```elisp
(ert-deftest my-package-complex-test ()
"Test complex operation."
(let ((data (my-package-prepare-data)))
(message "Prepared data: %S" data)
(should (my-package-valid-p data))
(let ((result (my-package-process data)))
(message "Processing result: %S" result)
(should (my-package-expected-result-p result)))))
```
**3. Break complex tests into steps:**
```elisp
(ert-deftest my-package-pipeline ()
"Test processing pipeline."
(let* ((input "raw data")
(parsed (progn
(should (stringp input))
(my-package-parse input)))
(validated (progn
(should (listp parsed))
(my-package-validate parsed)))
(processed (progn
(should validated)
(my-package-process validated))))
(should (my-package-expected-output-p processed))))
```
## Common Patterns
### Testing Mode Definitions
```elisp
(ert-deftest my-mode-initialization ()
"Major mode initializes correctly."
(with-temp-buffer
(my-mode)
(should (eq major-mode 'my-mode))
(should (local-variable-p 'my-mode-variable))
(should (keymapp my-mode-map))))
(ert-deftest my-mode-font-lock ()
"Font lock keywords defined correctly."
(with-temp-buffer
(my-mode)
(insert "keyword other-keyword")
(font-lock-ensure)
(should (eq (get-text-property 1 'face) 'my-mode-keyword-face))))
```
### Testing Interactive Commands
```elisp
(ert-deftest my-package-interactive-command ()
"Interactive command behaves correctly."
(with-temp-buffer
(insert "initial text")
(goto-char (point-min))
;; Simulate command execution
(call-interactively 'my-package-command)
(should (string= (buffer-string) "modified text"))
(should (= (point) 14))))
```
### Testing with Mock Data
```elisp
(ert-deftest my-package-parse-json ()
"JSON parsing produces expected structure."
(let ((json-data "{\"name\": \"test\", \"value\": 42}"))
(cl-letf (((symbol-function 'url-retrieve-synchronously)
(lambda (url)
(with-temp-buffer
(insert json-data)
(current-buffer)))))
(let ((result (my-package-fetch-data "https://api.example.com")))
(should (string= "test" (alist-get 'name result)))
(should (= 42 (alist-get 'value result)))))))
```
### Testing Regular Expressions
```elisp
(ert-deftest my-package-regex-matches ()
"Regular expression matches expected patterns."
(let ((re (my-package-build-regex)))
;; Positive cases
(should (string-match re "valid-input-123"))
(should (string-match re "another_valid_case"))
;; Negative cases
(should-not (string-match re "invalid input"))
(should-not (string-match re "123-invalid"))))
```
### Testing Hooks
```elisp
(ert-deftest my-package-hook-executes ()
"Hook functions execute in correct order."
(let ((execution-order nil))
(unwind-protect
(progn
(add-hook 'my-package-hook
(lambda () (push 'first execution-order)))
(add-hook 'my-package-hook
(lambda () (push 'second execution-order)))
(run-hooks 'my-package-hook)
(should (equal '(second first) execution-order)))
(setq my-package-hook nil))))
```
## Performance Considerations
### Keep Tests Fast
```elisp
;; Tag slow tests
(ert-deftest my-package-slow-integration-test ()
:tags '(slow integration)
...)
;; Run only fast tests during development
M-x ert RET (not :tag slow) RET
;; Mock slow operations
(ert-deftest my-package-test-with-mock ()
(cl-letf (((symbol-function 'slow-network-call)
(lambda () "instant mock result")))
(should (my-package-feature))))
```
### Avoid Redundant Setup
```elisp
;; Inefficient - recreates data in each test
(ert-deftest test-1 ()
(let ((data (expensive-data-creation)))
(should (test-aspect-1 data))))
(ert-deftest test-2 ()
(let ((data (expensive-data-creation)))
(should (test-aspect-2 data))))
;; Better - use fixture function
(defun with-test-data (body)
(let ((data (expensive-data-creation)))
(funcall body data)))
(ert-deftest test-1 ()
(with-test-data
(lambda (data)
(should (test-aspect-1 data)))))
(ert-deftest test-2 ()
(with-test-data
(lambda (data)
(should (test-aspect-2 data)))))
```
## Integration with Development Workflow
### Key Bindings for Quick Testing
```elisp
;; In your init.el
(global-set-key (kbd "C-c t") #'ert)
;; Run tests for current package
(defun my/ert-run-package-tests ()
"Run all tests for current package."
(interactive)
(let ((prefix (file-name-base (buffer-file-name))))
(ert (concat "^" prefix "-"))))
(global-set-key (kbd "C-c C-t") #'my/ert-run-package-tests)
```
### File Organization
```
my-package/
├── my-package.el ; Main package code
├── my-package-utils.el ; Utility functions
└── test/
├── my-package-test.el ; Tests for main package
└── my-package-utils-test.el ; Tests for utilities
```
### Test File Template
```elisp
;;; my-package-test.el --- Tests for my-package -*- lexical-binding: t -*-
;;; Commentary:
;; Test suite for my-package functionality.
;;; Code:
(require 'ert)
(require 'my-package)
(ert-deftest my-package-test-basic ()
"Test basic functionality."
(should (my-package-function)))
(provide 'my-package-test)
;;; my-package-test.el ends here
```
### Makefile Integration
```makefile
.PHONY: test
test:
emacs -batch -l ert \
-l my-package.el \
-l test/my-package-test.el \
-f ert-run-tests-batch-and-exit
.PHONY: test-interactive
test-interactive:
emacs -l my-package.el \
-l test/my-package-test.el \
--eval "(ert t)"
```
### CI/CD Integration
```yaml
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
emacs-version: ['27.2', '28.2', '29.1']
steps:
- uses: actions/checkout@v2
- uses: purcell/setup-emacs@master
with:
version: ${{ matrix.emacs-version }}
- name: Run tests
run: |
emacs -batch -l ert \
-l my-package.el \
-l test/my-package-test.el \
-f ert-run-tests-batch-and-exit
```
## Common Pitfalls
### 1. Tests Depend on External State
```elisp
;; Bad - depends on file system
(ert-deftest bad-test ()
(should (file-exists-p "~/.emacs")))
;; Good - controls environment
(ert-deftest good-test ()
(let ((temp-file (make-temp-file "test-")))
(unwind-protect
(should (file-exists-p temp-file))
(delete-file temp-file))))
```
### 2. Tests Interfere with Each Other
```elisp
;; Bad - modifies global state
(defvar my-package-state nil)
(ert-deftest bad-test-1 ()
(setq my-package-state 'value1)
(should (eq my-package-state 'value1)))
(ert-deftest bad-test-2 ()
;; May fail if bad-test-1 ran first
(should-not my-package-state))
;; Good - isolates state
(ert-deftest good-test-1 ()
(let ((my-package-state 'value1))
(should (eq my-package-state 'value1))))
(ert-deftest good-test-2 ()
(let ((my-package-state nil))
(should-not my-package-state)))
```
### 3. Overly Broad Assertions
```elisp
;; Bad - unclear what failed
(ert-deftest bad-test ()
(should (and (condition-1)
(condition-2)
(condition-3))))
;; Good - each assertion is explicit
(ert-deftest good-test ()
(should (condition-1))
(should (condition-2))
(should (condition-3)))
```
### 4. Missing Cleanup
```elisp
;; Bad - leaves processes running
(ert-deftest bad-test ()
(let ((proc (start-process "test" nil "sleep" "10")))
(should (process-live-p proc))))
;; Process keeps running after test
;; Good - ensures cleanup
(ert-deftest good-test ()
(let ((proc (start-process "test" nil "sleep" "10")))
(unwind-protect
(should (process-live-p proc))
(when (process-live-p proc)
(kill-process proc)))))
```
### 5. Not Testing Error Cases
```elisp
;; Incomplete - only tests success path
(ert-deftest incomplete-test ()
(should (= 5 (my-package-divide 10 2))))
;; Complete - tests both success and failure
(ert-deftest complete-test ()
(should (= 5 (my-package-divide 10 2)))
(should-error (my-package-divide 10 0) :type 'arith-error))
```
## Advanced Topics
### Custom Should Forms
Create domain-specific assertions:
```elisp
(defun should-match-regex (string regex)
"Assert that STRING matches REGEX."
(declare (indent 1))
(should (string-match regex string)))
(ert-deftest test-with-custom-should ()
(should-match-regex "foobar" "^foo"))
```
### Test Statistics
```elisp
(defun my-package-test-stats ()
"Display statistics about test suite."
(interactive)
(let* ((all-tests (ert-select-tests t t))
(total (length all-tests))
(tagged (length (ert-select-tests '(tag slow) t)))
(quick (- total tagged)))
(message "Total: %d, Quick: %d, Slow: %d" total quick tagged)))
```
### Running Specific Test Programmatically
```elisp
;; Run single test
(ert-run-tests 'my-package-specific-test #'ert-quiet-listener)
;; Run tests matching pattern
(ert-run-tests "^my-package-feature-" #'ert-batch-listener)
;; Run tests with custom listener
(defvar my-test-results nil)
(defun my-test-listener (event-type &rest args)
(pcase event-type
('test-started (push (car args) my-test-results))
('test-ended (message "Finished: %s" (car args)))))
(ert-run-tests t #'my-test-listener)
```
## Resources
- **Official Manual:** https://www.gnu.org/software/emacs/manual/html_mono/ert.html
- **Info in Emacs:** `C-h i m ert RET`
- **Source Code:** `lisp/emacs-lisp/ert.el` in Emacs repository
- **ERT Reference Card:** https://github.com/fniessen/refcard-ERT
## Summary
ERT provides a comprehensive testing framework fully integrated with Emacs:
1. Define tests with `ert-deftest` and assertions with `should` forms
2. Run tests interactively for rapid development feedback
3. Use batch mode for automated testing in CI/CD
4. Leverage dynamic binding for easy mocking
5. Debug failures interactively with integrated tooling
6. Organize tests with tags and selectors
7. Follow best practices: isolation, cleanup, clear assertions
8. Keep tests fast and focused for quick feedback cycles
ERT's integration with Emacs's interactive environment makes it uniquely powerful for developing and debugging Emacs Lisp code.

127
skills/ert/SUMMARY.txt Normal file
View File

@@ -0,0 +1,127 @@
ERT (Emacs Lisp Regression Testing) - Built-in Testing Framework
CORE CONCEPTS
- Test definition: (ert-deftest name () [:tags (...)] body)
- Assertions: should, should-not, should-error
- Test selectors: t, "regex", :tag symbol, :failed, :passed
- Interactive debugging with integrated backtrace inspection
- Batch mode for CI/CD with exit codes
KEY FUNCTIONS
Test Definition:
(ert-deftest NAME () [:tags (TAG...)] BODY)
- Define test with optional tags for organization
Assertions:
(should FORM) - Assert FORM is non-nil
(should-not FORM) - Assert FORM is nil
(should-error FORM [:type TYPE]) - Assert FORM signals error
Running Tests:
M-x ert RET SELECTOR - Run tests interactively
(ert-run-tests-batch-and-exit) - Batch mode, exit with status
(ert-run-tests-batch) - Batch mode, no exit
Skip Tests:
(skip-unless CONDITION) - Skip if CONDITION nil
(skip-when CONDITION) - Skip if CONDITION non-nil
INTERACTIVE DEBUGGING (in *ert* buffer)
. - Jump to test definition
d - Re-run with debugger enabled
b - Show backtrace
r - Re-run test at point
R - Re-run all tests
l - Show executed should forms
m - Show test messages
BEST PRACTICES
Environment Isolation:
(with-temp-buffer ...) - Isolated buffer operations
(let ((var value)) ...) - Local variable binding
(unwind-protect (test-code) (cleanup)) - Ensure cleanup
Mocking:
(cl-letf (((symbol-function 'fn) (lambda () "mock"))) ...)
(flet ((fn () "mock")) ...)
Test Organization:
- Name: package-test-feature format
- Tags: :tags '(quick unit) or '(slow integration)
- Selectors: Run by tag, regex, or status
Fixtures:
(defun with-fixture (body)
(let ((setup (do-setup)))
(unwind-protect (funcall body setup)
(do-teardown setup))))
COMMON PATTERNS
Test Errors:
(should-error (/ 1 0) :type 'arith-error)
Test Buffer State:
(with-temp-buffer
(my-mode)
(should (eq major-mode 'my-mode)))
Test Interactive Commands:
(with-temp-buffer
(insert "text")
(call-interactively 'my-cmd)
(should (= (point) expected)))
BATCH MODE
emacs -batch -l ert -l tests.el -f ert-run-tests-batch-and-exit
emacs -batch -l ert -l tests.el \
--eval '(ert-run-tests-batch-and-exit "^test-prefix-")'
emacs -batch -l ert -l tests.el \
--eval '(let ((ert-quiet t)) (ert-run-tests-batch-and-exit))'
CI/CD INTEGRATION
- Exit code 0 for success, non-zero for failure
- Use ert-run-tests-batch-and-exit for automated testing
- Set EMACS_TEST_VERBOSE for detailed failure output
- Test across multiple Emacs versions in matrix builds
TEST FILE TEMPLATE
;;; package-test.el --- Tests for package -*- lexical-binding: t -*-
(require 'ert)
(require 'package)
(ert-deftest package-test-feature ()
"Test feature implementation."
(should (package-function)))
(provide 'package-test)
;;; package-test.el ends here
KEY FEATURES
- No external dependencies (built into Emacs 24+)
- Interactive debugging with Emacs integration
- Detailed failure reporting with subexpression values
- Flexible test selection and organization
- Dynamic binding enables easy mocking
- Support for both unit and integration tests
- Batch mode for automated testing
- Test skipping based on conditions
TYPICAL WORKFLOW
1. Write test with ert-deftest
2. Run interactively: M-x ert RET t RET
3. Debug failures: press 'd' or 'b' on failed test
4. Iterate until green
5. Tag appropriately: :tags '(quick) or '(slow)
6. Integrate into CI/CD with batch mode
PITFALLS TO AVOID
- Tests depending on global state
- Missing cleanup (use unwind-protect)
- Overly broad assertions (split into multiple should forms)
- Not testing error cases
- Tests interfering with each other
- Relying on filesystem/network without mocking

98
skills/ert/metadata.edn Normal file
View File

@@ -0,0 +1,98 @@
{:skill
{:name "ert"
:version "1.0.0"
:description "Emacs Lisp Regression Testing (ERT) - Built-in testing framework for Emacs Lisp"
:tags [:testing :emacs-lisp :unit-testing :debugging :tdd :bdd]
:library
{:name "ert"
:version "Built-in (Emacs 24+)"
:url "https://www.gnu.org/software/emacs/manual/html_mono/ert.html"
:license "GPL-3.0-or-later"
:built-in true
:package-manager :none}
:features
["Test definition with ert-deftest"
"Assertions: should, should-not, should-error"
"Interactive test execution and debugging"
"Batch mode for CI/CD integration"
"Test selectors (regex, tags, status)"
"Detailed failure reporting with subexpressions"
"Dynamic binding for easy mocking"
"Test organization with tags"
"Skip tests based on conditions"
"Backtrace inspection and debugging"
"No external dependencies"]
:use-cases
["Unit testing Emacs Lisp packages"
"Regression testing for bug fixes"
"TDD/BDD development workflows"
"Integration testing for Emacs modes"
"CI/CD automated testing"
"Interactive debugging of test failures"
"Testing buffer manipulation functions"
"Testing interactive commands"
"Mocking external dependencies"
"Performance regression detection"]
:file-structure
{:main-docs "SKILL.md"
:quick-start "README.md"
:quick-reference "QUICK_REFERENCE.md"
:metadata "metadata.edn"}
:learning-path
{:beginner
["Understanding ert-deftest syntax"
"Writing basic assertions with should"
"Running tests interactively with M-x ert"
"Reading test results"]
:intermediate
["Using should-not and should-error"
"Organizing tests with tags"
"Working with temporary buffers"
"Test environment isolation with let"
"Cleanup with unwind-protect"
"Using test selectors"]
:advanced
["Mocking functions with cl-letf and flet"
"Creating fixture functions with higher-order functions"
"Running tests in batch mode"
"Custom test listeners"
"Debugging test failures interactively"
"CI/CD integration"
"Testing asynchronous code"
"Custom assertion macros"]}
:platform
{:os [:unix :windows :macos]
:emacs-version "24.1+"}
:api-coverage
{:core-macros
["ert-deftest"
"should"
"should-not"
"should-error"
"skip-unless"
"skip-when"]
:test-execution
["ert"
"ert-run-tests-interactively"
"ert-run-tests-batch"
"ert-run-tests-batch-and-exit"
"ert-run-tests"]
:test-selection
["Test selectors: t, regex, :tag, :failed, :passed"
"Selector combinators: and, or, not"]
:debugging
["Interactive commands: . d b r R l m"
"ert-results buffer"
"Backtrace inspection"]}}}

View File

@@ -0,0 +1,228 @@
# magit-section
Library for creating collapsible, hierarchical buffer UIs with interactive sections - the foundation of Magit's interface.
## Quick Start
### Basic Section Buffer
```elisp
(require 'magit-section)
(defun my-simple-browser ()
"Create a buffer with collapsible file sections."
(interactive)
(let ((buf (get-buffer-create "*File Browser*")))
(with-current-buffer buf
(magit-section-mode)
(let ((inhibit-read-only t))
(erase-buffer)
(magit-insert-section (root)
(magit-insert-heading "Files")
(dolist (file (directory-files default-directory))
(magit-insert-section (file file)
(magit-insert-heading file)
(insert (format " Size: %d bytes\n"
(file-attribute-size
(file-attributes file)))))))))
(pop-to-buffer buf)))
```
### Navigation
**Keybindings:**
- `n` / `p` - Next/previous section
- `^` - Parent section
- `TAB` - Toggle current section
- `M-n` / `M-p` - Next/previous sibling
- `1-4` - Show levels around current section
### Section Creation
```elisp
;; Basic section
(magit-insert-section (type-name value)
(magit-insert-heading "Section Title")
(insert "Content here\n"))
;; Nested sections
(magit-insert-section (parent "parent-value")
(magit-insert-heading "Parent")
(magit-insert-section (child "child-value")
(magit-insert-heading "Child")
(insert "Nested content\n")))
;; Initially hidden section
(magit-insert-section (data value t) ; t = hidden
(magit-insert-heading "Hidden by Default")
(insert "This content is initially collapsed\n"))
```
## Key Features
- **Hierarchical structure:** Nest sections to any depth
- **Interactive navigation:** Built-in keybindings for movement
- **Visibility control:** Collapse/expand sections individually or globally
- **Section-specific keymaps:** Custom actions per section type
- **Data association:** Store arbitrary values in sections
- **Visibility caching:** Preserve collapse state across refreshes
- **Performance:** Deferred content loading for large datasets
- **Mouse support:** Click margins to toggle sections
## Common Patterns
### Refreshable Buffer
```elisp
(defvar-local my-refresh-fn nil)
(defun my-refresh ()
"Refresh current buffer."
(interactive)
(when my-refresh-fn
(let ((inhibit-read-only t))
(erase-buffer)
(funcall my-refresh-fn))))
(defun my-create-buffer ()
(let ((buf (get-buffer-create "*My Data*")))
(with-current-buffer buf
(magit-section-mode)
(setq my-refresh-fn #'my-insert-data)
(local-set-key (kbd "g") #'my-refresh)
(my-refresh))
buf))
```
### Section-Specific Actions
```elisp
(defclass file-section (magit-section)
((keymap :initform 'file-section-map)))
(defvar file-section-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "RET") #'my-open-file)
(define-key map (kbd "d") #'my-delete-file)
map))
(defun my-open-file ()
"Open file in current section."
(interactive)
(when-let ((section (magit-current-section)))
(when (eq (oref section type) 'file-section)
(find-file (oref section value)))))
;; Use the custom section type
(magit-insert-section (file-section "/path/to/file")
(magit-insert-heading "README.md")
(insert "Content\n"))
```
### Querying Sections
```elisp
;; Get current section
(let ((section (magit-current-section)))
(message "Type: %s, Value: %s"
(oref section type)
(oref section value)))
;; Check section type
(when (magit-section-match 'file)
(message "On a file section"))
;; Match hierarchy (hunk within file)
(when (magit-section-match [file hunk])
(message "On a hunk in a file"))
;; Get selected sections in region
(let ((files (magit-region-sections 'file t)))
(message "Selected %d files" (length files)))
```
### Deferred Content Loading
```elisp
;; Only compute when section is first expanded
(magit-insert-section (expensive-data)
(magit-insert-heading "Large Dataset")
(magit-insert-section-body
(insert (compute-expensive-data))))
```
## Configuration
```elisp
;; Highlight current section
(setq magit-section-highlight-current t)
;; Show child count in headings
(setq magit-section-show-child-count t)
;; Cache visibility across refreshes
(setq magit-section-cache-visibility t)
;; Custom visibility indicators
(setq magit-section-visibility-indicators
'((expanded . "")
(collapsed . "")))
;; Initial visibility by type
(setq magit-section-initial-visibility-alist
'((details . hide)
(metadata . hide)))
```
## Use Cases
- **File browsers** with hierarchical directory trees
- **Log viewers** with collapsible entries
- **Process monitors** with expandable details
- **Configuration inspectors** organizing settings
- **Documentation browsers** with section navigation
- **Data explorers** for structured content
- **Custom Git UIs** (like Magit itself)
- **API response viewers** with nested data
## Using the Skill
This skill provides:
- Complete API reference for all magit-section functions
- Comprehensive usage patterns and best practices
- Detailed examples for common scenarios
- Performance optimization techniques
- Integration patterns with Emacs modes
Consult `SKILL.md` for in-depth documentation.
## Learning Path
1. **Basics:** Create simple sections, basic navigation
2. **Structure:** Nested hierarchies, headings, content
3. **Interaction:** Section keymaps, actions, queries
4. **Visibility:** Toggle, cycle, caching strategies
5. **Advanced:** Custom section types, washing buffers, performance
6. **Integration:** Major modes, refreshing, context menus
## External Resources
- [Magit Repository](https://github.com/magit/magit)
- [Magit-Section Tutorial](https://github.com/magit/magit/wiki/Magit-Section-Tutorial)
- [Magit Documentation](https://magit.vc/manual/magit.html)
- Package: Available on NonGNU ELPA
## Requirements
- Emacs 28.1+
- Dependencies: compat 30.1+, cond-let 0.1+, llama 1.0+, seq 2.24+
## Installation
```elisp
;; Via package.el
(package-install 'magit-section)
;; In your init file
(require 'magit-section)
```

View File

@@ -0,0 +1,792 @@
---
name: magit-section
description: A guide to using magit-section for building collapsible, hierarchical buffer UIs in Emacs.
---
# magit-section: Collapsible Section-Based UIs
magit-section is Emacs's premier library for creating interactive, hierarchical buffer interfaces with collapsible sections. Originally extracted from Magit, it provides the foundation for building information-dense UIs that users can navigate and explore efficiently.
## Overview
magit-section enables creation of buffers with tree-like, collapsible content organized into sections. Each section can contain nested child sections, custom keybindings, associated data, and responsive highlighting.
**Key Characteristics:**
- Hierarchical collapsible sections with visibility control
- Section-specific keymaps and actions
- Built-in navigation commands
- Visibility caching across buffer refreshes
- Mouse and keyboard interaction
- Integrated with Emacs region selection
- Requires Emacs 28.1+
**Version:** 4.2.0+ (January 2025)
**Repository:** https://github.com/magit/magit
**License:** GPL-3.0+
## Core Concepts
### Section Object
Sections are EIEIO objects with these slots:
- `type` - Symbol identifying section kind (e.g., `file`, `commit`, `hunk`)
- `value` - Associated data (filename, commit SHA, etc.)
- `start` - Buffer position where section begins (includes heading)
- `content` - Buffer position where body content starts
- `end` - Buffer position where section ends
- `hidden` - Visibility state (nil=visible, non-nil=hidden)
- `children` - List of child sections
- `parent` - Parent section reference
- `keymap` - Section-specific key bindings
- `washer` - Function for deferred content generation
### Buffer Structure
Every magit-section buffer requires a single root section that spans the entire buffer. Sections form a tree hierarchy with proper nesting.
### Visibility States
Sections can be:
- **Fully visible** - Heading and all content shown
- **Hidden** - Only heading visible, content collapsed
- **Heading-only** - Nested sections show only headings
## API Reference
### Creating Sections
#### magit-insert-section
Primary macro for section creation:
```elisp
(magit-insert-section (type value &optional hide)
[HEADING-FORM]
BODY...)
```
**Arguments:**
- `type` - Section type (symbol or `(eval FORM)`)
- `value` - Data to store in section's value slot
- `hide` - Initial visibility (nil=visible, t=hidden)
**Example:**
```elisp
(magit-insert-section (file "README.md")
(magit-insert-heading "README.md")
(insert "File contents here\n"))
```
**Advanced Usage:**
```elisp
(magit-insert-section (commit commit-sha nil)
(magit-insert-heading
(format "%s %s"
(substring commit-sha 0 7)
(magit-commit-message commit-sha)))
;; Insert commit details
(magit-insert-section (diffstat commit-sha)
(insert (magit-format-diffstat commit-sha))))
```
#### magit-insert-heading
Insert section heading with optional child count:
```elisp
(magit-insert-heading &rest ARGS)
```
**Example:**
```elisp
(magit-insert-heading
(format "Changes (%d)" (length file-list)))
```
#### magit-insert-section-body
Defer section body evaluation until first expansion:
```elisp
(magit-insert-section-body
;; Expensive operations here
(insert (expensive-computation)))
```
Use for performance when sections are initially hidden.
#### magit-cancel-section
Abort partial section creation:
```elisp
(when (null items)
(magit-cancel-section))
```
Removes partial section from buffer and section tree.
### Navigation Commands
#### Movement
```elisp
(magit-section-forward) ; Next sibling or parent's next
(magit-section-backward) ; Previous sibling or parent
(magit-section-up) ; Parent section
(magit-section-forward-sibling) ; Next sibling
(magit-section-backward-sibling) ; Previous sibling
```
**Keybindings:**
- `n` - Forward
- `p` - Backward
- `^` - Up to parent
- `M-n` - Forward sibling
- `M-p` - Backward sibling
**Example Navigation Hook:**
```elisp
(add-hook 'magit-section-movement-hook
(lambda ()
(when (eq (oref (magit-current-section) type) 'commit)
(message "On commit: %s"
(oref (magit-current-section) value)))))
```
### Visibility Control
#### Basic Toggle
```elisp
(magit-section-toggle &optional SECTION) ; Toggle current/specified
(magit-section-show SECTION) ; Expand
(magit-section-hide SECTION) ; Collapse
```
**Keybindings:**
- `TAB` - Toggle current section
- `C-c TAB` / `C-<tab>` - Cycle visibility states
- `<backtab>` - Cycle all top-level sections
#### Recursive Operations
```elisp
(magit-section-show-children SECTION &optional DEPTH)
(magit-section-hide-children SECTION)
(magit-section-show-headings SECTION)
```
**Example:**
```elisp
;; Expand section and first level of children
(magit-section-show-children (magit-current-section) 1)
```
#### Level-Based Visibility
```elisp
(magit-section-show-level-1) ; Show only top level
(magit-section-show-level-2) ; Show two levels
(magit-section-show-level-3) ; Show three levels
(magit-section-show-level-4) ; Show four levels
```
**Keybindings:**
- `1` / `2` / `3` / `4` - Show levels around current section
- `M-1` / `M-2` / `M-3` / `M-4` - Show levels for entire buffer
#### Cycling
```elisp
(magit-section-cycle &optional SECTION) ; Current section
(magit-section-cycle-global) ; All sections
```
Cycles through: collapsed → expanded → children-collapsed → collapsed
### Querying Sections
#### Current Section
```elisp
(magit-current-section) ; Section at point
(magit-section-at &optional POS) ; Section at position
```
**Example:**
```elisp
(let ((section (magit-current-section)))
(message "Type: %s, Value: %s"
(oref section type)
(oref section value)))
```
#### Section Properties
```elisp
(magit-section-ident SECTION) ; Unique identifier
(magit-section-lineage SECTION) ; List of ancestor sections
(magit-section-hidden SECTION) ; Hidden or ancestor hidden?
(magit-section-content-p SECTION) ; Has body content?
```
**Example:**
```elisp
(when (magit-section-content-p section)
(magit-section-show section))
```
#### Section Matching
```elisp
(magit-section-match CONDITIONS &optional SECTION)
```
**Conditions:**
- Symbol matches type
- List `(TYPE VALUE)` matches type and value
- List of symbols matches any type in list
- `[TYPE...]` matches type hierarchy
**Example:**
```elisp
;; Check if section is a file
(when (magit-section-match 'file)
(message "Current section is a file"))
;; Match specific file
(when (magit-section-match '(file "README.md"))
(message "On README.md"))
;; Match hierarchy: hunk within file
(when (magit-section-match [file hunk])
(message "In a hunk within a file section"))
```
#### Region Selection
```elisp
(magit-region-sections &optional CONDITION MULTIPLE)
```
Get sibling sections in active region.
**Example:**
```elisp
(let ((selected-files (magit-region-sections 'file t)))
(dolist (section selected-files)
(message "Selected: %s" (oref section value))))
```
### Section Inspection
```elisp
(magit-describe-section &optional SECTION INTERACTIVE)
(magit-describe-section-briefly &optional SECTION INTERACTIVE)
```
Display detailed section information for debugging.
**Keybinding:** `C-h .` (in magit-section buffers)
### Section-Specific Keymaps
Define per-type keybindings using the `:keymap` class slot:
```elisp
(defclass my-file-section (magit-section)
((keymap :initform 'my-file-section-map)))
(defvar my-file-section-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "RET") 'my-open-file)
(define-key map (kbd "d") 'my-delete-file)
map))
```
**Example:**
```elisp
(magit-insert-section (my-file-section filename)
(magit-insert-heading filename)
(insert "Content..."))
;; RET and d keys work only on this section type
```
## Configuration
### Highlighting
```elisp
;; Highlight current section
(setq magit-section-highlight-current t)
;; Highlight region selection
(setq magit-section-highlight-selection t)
```
### Child Count Display
```elisp
;; Show number of children in headings
(setq magit-section-show-child-count t)
```
### Visibility Indicators
```elisp
;; Customize expansion/collapse indicators
(setq magit-section-visibility-indicators
'((expanded . "")
(collapsed . "")))
```
### Visibility Caching
```elisp
;; Cache visibility across refreshes
(setq magit-section-cache-visibility t)
;; Cache only specific section types
(setq magit-section-cache-visibility '(file commit))
;; Set initial visibility by type
(setq magit-section-initial-visibility-alist
'((stash . hide)
(untracked . hide)))
```
### Line Numbers
```elisp
;; Disable line numbers in section buffers (default)
(setq magit-section-disable-line-numbers t)
```
## Common Patterns
### Basic Section Buffer
```elisp
(defun my-section-buffer ()
"Create a buffer with collapsible sections."
(interactive)
(let ((buf (get-buffer-create "*My Sections*")))
(with-current-buffer buf
(magit-section-mode)
(let ((inhibit-read-only t))
(erase-buffer)
(magit-insert-section (root)
(magit-insert-heading "My Data")
(magit-insert-section (files)
(magit-insert-heading "Files")
(dolist (file (directory-files default-directory))
(magit-insert-section (file file)
(insert file "\n"))))
(magit-insert-section (buffers)
(magit-insert-heading "Buffers")
(dolist (buf (buffer-list))
(magit-insert-section (buffer buf)
(insert (buffer-name buf) "\n")))))))
(pop-to-buffer buf)))
```
### Refreshable Buffer
```elisp
(defvar-local my-refresh-function nil)
(defun my-section-refresh ()
"Refresh current section buffer."
(interactive)
(when my-refresh-function
(let ((inhibit-read-only t)
(line (line-number-at-pos))
(col (current-column)))
(erase-buffer)
(funcall my-refresh-function)
(goto-char (point-min))
(forward-line (1- line))
(move-to-column col))))
(defun my-create-buffer ()
(let ((buf (get-buffer-create "*My Data*")))
(with-current-buffer buf
(magit-section-mode)
(setq my-refresh-function #'my-insert-content)
(local-set-key (kbd "g") #'my-section-refresh)
(my-section-refresh))
buf))
```
### Washing External Command Output
```elisp
(defun my-insert-git-status ()
"Insert git status output as sections."
(magit-insert-section (status)
(magit-insert-heading "Git Status")
(let ((start (point)))
(call-process "git" nil t nil "status" "--short")
(save-restriction
(narrow-to-region start (point))
(goto-char start)
(while (not (eobp))
(let ((line-start (point))
(file (buffer-substring (+ (point) 3)
(line-end-position))))
(magit-insert-section (file file)
(forward-line 1))))))))
```
### Section Actions
```elisp
(defclass file-section (magit-section)
((keymap :initform 'file-section-map)))
(defvar file-section-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "RET") 'my-visit-file)
(define-key map (kbd "d") 'my-delete-file)
(define-key map (kbd "r") 'my-rename-file)
map))
(defun my-visit-file ()
"Visit file in current section."
(interactive)
(when-let ((section (magit-current-section)))
(when (eq (oref section type) 'file-section)
(find-file (oref section value)))))
(defun my-delete-file ()
"Delete file in current section."
(interactive)
(when-let ((section (magit-current-section)))
(when (and (eq (oref section type) 'file-section)
(yes-or-no-p (format "Delete %s? "
(oref section value))))
(delete-file (oref section value))
(my-section-refresh))))
```
### Deferred Content Loading
```elisp
(magit-insert-section (expensive-data)
(magit-insert-heading "Large Dataset")
(magit-insert-section-body
;; Only computed when section first expanded
(insert (format-large-dataset (compute-expensive-data)))))
```
### Visibility Hooks
```elisp
(add-hook 'magit-section-set-visibility-hook
(lambda (section)
;; Keep commit details hidden by default
(when (eq (oref section type) 'commit-details)
'hide)))
```
### Context Menu Integration
```elisp
(defun my-section-context-menu (menu section)
"Add items to context menu based on section."
(when (eq (oref section type) 'file)
(define-key menu [my-open]
'("Open File" . my-visit-file))
(define-key menu [my-delete]
'("Delete File" . my-delete-file)))
menu)
(add-hook 'magit-menu-alternative-section-hook
#'my-section-context-menu)
```
## Use Cases
### File Browser
```elisp
(defun my-file-browser (dir)
"Browse directory with collapsible sections."
(interactive "DDirectory: ")
(let ((buf (get-buffer-create (format "*Files: %s*" dir))))
(with-current-buffer buf
(magit-section-mode)
(let ((inhibit-read-only t)
(default-directory dir))
(erase-buffer)
(magit-insert-section (root)
(magit-insert-heading (format "Directory: %s" dir))
(my-insert-directory-tree "."))))
(pop-to-buffer buf)))
(defun my-insert-directory-tree (path)
"Recursively insert directory structure."
(dolist (file (directory-files path))
(unless (member file '("." ".."))
(let ((full-path (expand-file-name file path)))
(if (file-directory-p full-path)
(magit-insert-section (directory full-path)
(magit-insert-heading (concat file "/"))
(my-insert-directory-tree full-path))
(magit-insert-section (file full-path)
(insert file "\n")))))))
```
### Log Viewer
```elisp
(defun my-log-viewer (log-file)
"View log file with collapsible sections per entry."
(interactive "fLog file: ")
(let ((buf (get-buffer-create "*Log Viewer*")))
(with-current-buffer buf
(magit-section-mode)
(let ((inhibit-read-only t))
(erase-buffer)
(magit-insert-section (root)
(magit-insert-heading (format "Log: %s" log-file))
(with-temp-buffer
(insert-file-contents log-file)
(goto-char (point-min))
(my-parse-log-entries)))))
(pop-to-buffer buf)))
```
### Process Monitor
```elisp
(defun my-process-monitor ()
"Display running processes in sections."
(interactive)
(let ((buf (get-buffer-create "*Processes*")))
(with-current-buffer buf
(magit-section-mode)
(setq my-refresh-function #'my-insert-processes)
(local-set-key (kbd "g") #'my-section-refresh)
(local-set-key (kbd "k") #'my-kill-process)
(my-section-refresh))
(pop-to-buffer buf)))
(defun my-insert-processes ()
"Insert process list as sections."
(magit-insert-section (root)
(magit-insert-heading "Running Processes")
(dolist (proc (process-list))
(magit-insert-section (process proc)
(magit-insert-heading
(format "%s [%s]"
(process-name proc)
(process-status proc)))
(insert (format " Command: %s\n"
(mapconcat #'identity
(process-command proc)
" ")))))))
```
### Configuration Inspector
```elisp
(defun my-config-inspector ()
"Inspect Emacs configuration in sections."
(interactive)
(let ((buf (get-buffer-create "*Config*")))
(with-current-buffer buf
(magit-section-mode)
(let ((inhibit-read-only t))
(erase-buffer)
(magit-insert-section (root)
(magit-insert-heading "Emacs Configuration")
(magit-insert-section (variables)
(magit-insert-heading "Custom Variables")
(dolist (var (sort (my-get-custom-vars) #'string<))
(magit-insert-section (variable var)
(magit-insert-heading (symbol-name var))
(insert (format " %S\n" (symbol-value var))))))
(magit-insert-section (features)
(magit-insert-heading "Loaded Features")
(dolist (feature features)
(magit-insert-section (feature feature)
(insert (format "%s\n" feature))))))))
(pop-to-buffer buf)))
```
## Error Handling
### Validation
```elisp
;; Ensure root section exists
(defun my-ensure-root-section ()
(unless magit-root-section
(error "Buffer does not have a root section. Enable magit-section-mode.")))
;; Validate section type
(defun my-require-file-section ()
(let ((section (magit-current-section)))
(unless (eq (oref section type) 'file)
(user-error "Not on a file section"))))
```
### Graceful Degradation
```elisp
(defun my-safe-section-value ()
"Get section value safely."
(when-let ((section (magit-current-section)))
(ignore-errors
(oref section value))))
```
## Performance Considerations
### Lazy Loading
Use `magit-insert-section-body` for expensive operations:
```elisp
(magit-insert-section (large-data)
(magit-insert-heading "Large Dataset")
(magit-insert-section-body
;; Only executed when expanded
(my-compute-and-insert-large-data)))
```
### Visibility Caching
Cache section visibility to preserve state across refreshes:
```elisp
(setq magit-section-cache-visibility t)
;; Preserve visibility during refresh
(let ((magit-section-preserve-visibility t))
(my-refresh-buffer))
```
### Efficient Updates
Minimize full buffer refreshes:
```elisp
;; Update single section instead of full refresh
(defun my-update-section (section)
(save-excursion
(goto-char (oref section start))
(let ((inhibit-read-only t)
(hidden (oref section hidden)))
(delete-region (oref section start) (oref section end))
(my-insert-section-content section)
(when hidden
(magit-section-hide section)))))
```
## Integration with Emacs
### magit-section-mode
Enable in buffers using sections:
```elisp
(define-derived-mode my-viewer-mode magit-section-mode "MyViewer"
"Major mode for viewing data with sections."
(setq-local my-refresh-function #'my-insert-data))
```
### Faces
Customize appearance:
```elisp
(custom-set-faces
'(magit-section-heading ((t (:weight bold :foreground "blue"))))
'(magit-section-highlight ((t (:background "gray95")))))
```
### Mouse Support
Mouse clicks on section margins toggle visibility automatically.
## Debugging
### Inspection Commands
```elisp
;; Describe current section
(call-interactively #'magit-describe-section)
;; Brief description
(magit-describe-section-briefly)
```
### Debug Helpers
```elisp
(defun my-debug-section ()
"Debug current section structure."
(interactive)
(let ((section (magit-current-section)))
(message "Type: %S, Value: %S, Hidden: %S, Children: %d"
(oref section type)
(oref section value)
(oref section hidden)
(length (oref section children)))))
```
## Migration from Other UIs
### From outline-mode
magit-section offers superior interactivity and data association:
```elisp
;; outline-mode
(outline-hide-subtree)
;; magit-section equivalent
(magit-section-hide (magit-current-section))
```
### From org-mode
Use magit-section for custom data not suited to org's document structure:
```elisp
;; org-mode cycling
(org-cycle)
;; magit-section cycling
(magit-section-cycle)
```
## Resources
- **Repository:** https://github.com/magit/magit
- **Tutorial:** https://github.com/magit/magit/wiki/Magit-Section-Tutorial
- **Magit Documentation:** https://magit.vc/manual/magit.html
- **ELPA:** Available on NonGNU ELPA
## See Also
- **Magit:** Git interface built on magit-section
- **taxy-magit-section:** Integrate taxy hierarchies with magit-section
- **outline-mode:** Built-in outline collapsing
- **org-mode:** Document structure with folding

View File

@@ -0,0 +1,148 @@
{:skill
{:name "magit-section"
:version "1.0.0"
:description "Build collapsible, hierarchical buffer UIs with sections - the foundation of Magit's interface"
:tags [:ui :navigation :buffers :interactive :hierarchy :magit :emacs-lisp]
:library
{:name "magit-section"
:version "4.2.0+"
:url "https://github.com/magit/magit"
:license "GPL-3.0-or-later"
:built-in false
:package-manager :nongnu-elpa}
:features
["Hierarchical collapsible sections"
"Section-specific keymaps and actions"
"Built-in navigation commands"
"Visibility state caching across refreshes"
"Mouse and keyboard interaction"
"Integrated region selection"
"Deferred content loading for performance"
"Section matching and querying"
"Customizable visibility indicators"
"Context menu integration"
"Highlighting and visual feedback"
"EIEIO-based section objects"]
:use-cases
["Building custom file browsers"
"Creating log viewers with collapsible entries"
"Process monitoring interfaces"
"Configuration inspectors"
"Package management UIs"
"Documentation browsers"
"Data explorers and viewers"
"Interactive command output"
"Project management interfaces"
"Custom Git interfaces (like Magit)"
"Database query result viewers"
"API response inspectors"]
:file-structure
{:main-docs "SKILL.md"
:quick-start "README.md"
:metadata "metadata.edn"}
:learning-path
{:beginner
["Understanding section objects and hierarchy"
"Creating basic sections with magit-insert-section"
"Using magit-insert-heading"
"Basic navigation with n/p/^"
"Toggling visibility with TAB"
"Querying sections with magit-current-section"]
:intermediate
["Section type matching with magit-section-match"
"Creating refreshable buffers"
"Using section-specific keymaps"
"Working with section values and data"
"Visibility control and cycling"
"Region selection of sections"
"Configuring visibility caching"]
:advanced
["Washing external command output"
"Deferred content loading with magit-insert-section-body"
"Custom section classes with EIEIO"
"Visibility hooks and customization"
"Context menu integration"
"Performance optimization techniques"
"Complex hierarchical data structures"
"Dynamic section updates"]}
:platform
{:os [:unix :windows :macos]
:emacs-version "28.1+"}
:api-coverage
{:section-creation
["magit-insert-section"
"magit-insert-heading"
"magit-insert-section-body"
"magit-cancel-section"
"magit-insert-child-count"]
:navigation
["magit-section-forward"
"magit-section-backward"
"magit-section-up"
"magit-section-forward-sibling"
"magit-section-backward-sibling"
"magit-section-goto"]
:visibility
["magit-section-toggle"
"magit-section-show"
"magit-section-hide"
"magit-section-cycle"
"magit-section-cycle-global"
"magit-section-show-children"
"magit-section-hide-children"
"magit-section-show-headings"
"magit-section-show-level-1 through 4"]
:querying
["magit-current-section"
"magit-section-at"
"magit-section-ident"
"magit-section-match"
"magit-section-value-if"
"magit-section-lineage"
"magit-section-hidden"
"magit-section-content-p"
"magit-region-sections"]
:inspection
["magit-describe-section"
"magit-describe-section-briefly"]
:section-object-slots
["type"
"value"
"start"
"content"
"end"
"hidden"
"children"
"parent"
"keymap"
"washer"]
:configuration
["magit-section-highlight-current"
"magit-section-highlight-selection"
"magit-section-show-child-count"
"magit-section-cache-visibility"
"magit-section-initial-visibility-alist"
"magit-section-visibility-indicators"
"magit-section-disable-line-numbers"]
:hooks
["magit-section-movement-hook"
"magit-section-set-visibility-hook"
"magit-insert-section-hook"
"magit-menu-alternative-section-hook"
"magit-region-highlight-hook"]}}}

View File

@@ -0,0 +1,918 @@
---
name: package-conventions
description: Emacs Lisp package development standards and conventions
---
# Emacs Package Conventions
Comprehensive guide to Emacs Lisp package development standards, covering naming, structure, metadata, and distribution requirements.
## Overview
Emacs packages follow strict conventions to ensure compatibility, discoverability, and quality. These conventions cover file structure, naming, metadata, documentation, and distribution through package archives like MELPA and GNU ELPA.
## Package Types
### Simple Package
Single `.el` file with header metadata.
```elisp
;;; mypackage.el --- Brief description -*- lexical-binding: t; -*-
;; Copyright (C) 2025 Your Name
;; Author: Your Name <you@example.com>
;; Version: 1.0.0
;; Package-Requires: ((emacs "25.1"))
;; Keywords: convenience, tools
;; URL: https://github.com/user/mypackage
;;; Commentary:
;; Longer description of what the package does.
;;; Code:
(defun mypackage-hello ()
"Say hello."
(interactive)
(message "Hello from mypackage!"))
(provide 'mypackage)
;;; mypackage.el ends here
```
### Multi-File Package
Directory with `-pkg.el` descriptor file.
Structure:
```
mypackage/
├── mypackage.el
├── mypackage-utils.el
├── mypackage-pkg.el
└── README.md
```
`mypackage-pkg.el`:
```elisp
(define-package "mypackage" "1.0.0"
"Brief description"
'((emacs "25.1")
(dash "2.19.1"))
:keywords '("convenience" "tools")
:url "https://github.com/user/mypackage")
```
## File Header Conventions
### Required Headers
**Simple package** (single `.el`):
- `;;; filename.el --- description`
- `;; Author:`
- `;; Version:` or `;; Package-Version:`
- `;;; Commentary:`
- `;;; Code:`
- `(provide 'feature-name)`
- `;;; filename.el ends here`
### Standard Headers
```elisp
;;; mypackage.el --- Brief one-line description -*- lexical-binding: t; -*-
;; Copyright (C) 2025 Author Name
;; Author: Author Name <email@example.com>
;; Maintainer: Maintainer Name <email@example.com>
;; Version: 1.0.0
;; Package-Requires: ((emacs "25.1") (dash "2.19.1"))
;; Keywords: convenience tools
;; URL: https://github.com/user/mypackage
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; License:
;; 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.
;;; Commentary:
;; Detailed description spanning multiple lines.
;; Explain what the package does, how to use it.
;;; Code:
```
### Lexical Binding
Always enable lexical binding:
```elisp
;;; mypackage.el --- Description -*- lexical-binding: t; -*-
```
Required for modern Emacs development and MELPA acceptance.
## Naming Conventions
### Package Prefix
Choose short, unique prefix. All public symbols must use this prefix.
```elisp
;; Package: super-mode
;; Prefix: super-
(defun super-activate () ; ✓ Public function
...)
(defvar super-default-value nil) ; ✓ Public variable
(defun super--internal-helper () ; ✓ Private function (double dash)
...)
(defvar super--state nil) ; ✓ Private variable
```
### Symbol Naming Rules
**Public vs Private:**
- Public: `prefix-name`
- Private: `prefix--name` (double dash)
**Variable types:**
- Function variable: `prefix-hook-function`
- Hook: `prefix-mode-hook`
- Option: `prefix-enable-feature`
- Local variable: `prefix--internal-state`
**Special cases:**
- Commands can omit prefix if memorable: `list-frobs` in `frob` package
- Major modes: `prefix-mode`
- Minor modes: `prefix-minor-mode`
### Case Convention
Use lowercase with hyphens (lisp-case):
```elisp
(defun my-package-do-something () ; ✓
...)
(defun myPackageDoSomething () ; ✗ Wrong
...)
```
## Package Metadata
### Version Format
Semantic versioning: `MAJOR.MINOR.PATCH`
```elisp
;; Version: 1.2.3
```
For snapshot builds:
```elisp
;; Package-Version: 1.2.3-snapshot
;; Version: 1.2.3
```
### Dependencies
Specify minimum Emacs version and package dependencies:
```elisp
;; Package-Requires: ((emacs "26.1") (dash "2.19.1") (s "1.12.0"))
```
Each dependency: `(package-name "version")`
### Keywords
Use standard keywords from `finder-known-keywords`:
```elisp
;; Keywords: convenience tools matching
```
Common keywords:
- `convenience` - Convenience features
- `tools` - Programming tools
- `extensions` - Emacs extensions
- `languages` - Language support
- `comm` - Communication
- `files` - File handling
- `data` - Data structures
Check available: `M-x describe-variable RET finder-known-keywords`
## Code Organization
### Feature Provision
Always end with `provide`:
```elisp
(provide 'mypackage)
;;; mypackage.el ends here
```
Feature name must match file name (without `.el`).
### Loading Behavior
**Don't modify Emacs on load:**
```elisp
;; ✗ Bad - changes behavior on load
(global-set-key (kbd "C-c m") #'my-command)
;; ✓ Good - user explicitly enables
(defun my-mode-setup ()
"Set up keybindings for my-mode."
(local-set-key (kbd "C-c m") #'my-command))
```
### Autoload Cookies
Mark interactive commands for autoloading:
```elisp
;;;###autoload
(defun my-package-start ()
"Start my-package."
(interactive)
...)
;;;###autoload
(define-minor-mode my-mode
"Toggle My Mode."
...)
```
### Group and Custom Variables
Define customization group:
```elisp
(defgroup my-package nil
"Settings for my-package."
:group 'applications
:prefix "my-package-")
(defcustom my-package-option t
"Description of option."
:type 'boolean
:group 'my-package)
```
## Documentation Standards
### Docstrings
**Functions:**
```elisp
(defun my-package-process (input &optional format)
"Process INPUT according to FORMAT.
INPUT should be a string or buffer.
FORMAT, if non-nil, specifies output format (symbol).
Return processed result as string."
...)
```
First line: brief description ending with period.
Following lines: detailed explanation.
Document arguments in CAPS.
Document return value.
**Variables:**
```elisp
(defvar my-package-cache nil
"Cache for processed results.
Each entry is (KEY . VALUE) where KEY is input and VALUE is result.")
```
**User options:**
```elisp
(defcustom my-package-auto-save t
"Non-nil means automatically save results.
When enabled, results are saved to `my-package-save-file'."
:type 'boolean
:group 'my-package)
```
### Checkdoc Compliance
Verify documentation:
```elisp
M-x checkdoc RET
```
Requirements:
- First line ends with period
- First line fits in 80 columns
- Argument names in CAPS
- References to symbols quoted with `symbol'
- No spelling errors
## Code Quality
### Required Tools
**package-lint:**
```elisp
M-x package-lint-current-buffer
```
Checks:
- Header format
- Dependency declarations
- Symbol naming
- Autoload cookies
**flycheck-package:**
```elisp
(require 'flycheck-package)
(flycheck-package-setup)
```
Real-time package.el validation.
### Common Issues
**Missing lexical binding:**
```elisp
;;; package.el --- Description -*- lexical-binding: t; -*-
```
**Wrong provide:**
```elisp
;; File: my-utils.el
(provide 'my-utils) ; ✓ Matches filename
;; File: my-package.el
(provide 'my-pkg) ; ✗ Doesn't match filename
```
**Namespace pollution:**
```elisp
;; ✗ Bad
(defun format-string (s) ; Collides with other packages
...)
;; ✓ Good
(defun my-package-format-string (s)
...)
```
**Global state on load:**
```elisp
;; ✗ Bad
(setq some-global-var t) ; Changes Emacs on load
;; ✓ Good
(defcustom my-package-feature-enabled nil
"Enable my-package feature."
:type 'boolean
:set (lambda (sym val)
(set-default sym val)
(when val (my-package-activate))))
```
## MELPA Submission
### Repository Requirements
**Source control:**
- Git or Mercurial only
- Official repository (no forks)
- Contains LICENSE file
**Structure:**
```
mypackage/
├── mypackage.el
├── LICENSE
└── README.md
```
### Recipe Format
Create `recipes/mypackage` in MELPA repository:
```elisp
(mypackage :fetcher github
:repo "user/mypackage"
:files (:defaults "icons/*.png"))
```
**Fetchers:**
- `:fetcher github` - GitHub repository
- `:fetcher gitlab` - GitLab repository
- `:fetcher codeberg` - Codeberg repository
**Files:**
- `:defaults` - Standard `.el` files
- Custom patterns: `"subdir/*.el"`
### Quality Checklist
Before submission:
- [ ] Lexical binding enabled
- [ ] All public symbols prefixed
- [ ] Private symbols use `--` separator
- [ ] Docstrings complete and accurate
- [ ] `package-lint` passes
- [ ] `checkdoc` clean
- [ ] Dependencies declared in `Package-Requires`
- [ ] Autoloads on interactive commands
- [ ] `provide` matches filename
- [ ] LICENSE file present
- [ ] No loading side effects
- [ ] Keywords from standard list
### Testing Locally
Build recipe locally:
```bash
make recipes/mypackage
```
Install from file:
```elisp
M-x package-install-file RET /path/to/mypackage.el
```
Test in clean Emacs:
```bash
emacs -Q -l package -f package-initialize -f package-install-file mypackage.el
```
## Major Mode Conventions
### Mode Definition
```elisp
(defvar my-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-c") #'my-mode-command)
map)
"Keymap for `my-mode'.")
(define-derived-mode my-mode fundamental-mode "My"
"Major mode for editing My files.
\\{my-mode-map}"
(setq-local comment-start "#")
(setq-local comment-end ""))
```
### Mode Hooks
Provide hook for customization:
```elisp
(defvar my-mode-hook nil
"Hook run when entering `my-mode'.")
(define-derived-mode my-mode fundamental-mode "My"
...
(run-hooks 'my-mode-hook))
```
### Auto-mode Association
Use autoload for file associations:
```elisp
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.my\\'" . my-mode))
```
## Minor Mode Conventions
### Global Minor Mode
```elisp
;;;###autoload
(define-minor-mode my-minor-mode
"Toggle My Minor Mode.
When enabled, provides X functionality."
:global t
:lighter " My"
:group 'my-package
(if my-minor-mode
(my-minor-mode--enable)
(my-minor-mode--disable)))
```
### Buffer-local Minor Mode
```elisp
;;;###autoload
(define-minor-mode my-local-mode
"Toggle My Local Mode in current buffer."
:lighter " MyL"
:keymap my-local-mode-map
(if my-local-mode
(add-hook 'post-command-hook #'my-local-mode--update nil t)
(remove-hook 'post-command-hook #'my-local-mode--update t)))
```
## Library vs Package
### Library
Collection of functions for other packages to use:
```elisp
;;; mylib.el --- Utility functions -*- lexical-binding: t; -*-
;; Author: Name
;; Version: 1.0.0
;;; Commentary:
;; Library of utility functions. Not a standalone package.
;;; Code:
(defun mylib-helper (x)
"Help with X."
...)
(provide 'mylib)
;;; mylib.el ends here
```
### Package
End-user feature with commands:
```elisp
;;; mypackage.el --- User feature -*- lexical-binding: t; -*-
;; Package-Requires: ((emacs "25.1"))
;; Keywords: convenience
;;; Code:
;;;###autoload
(defun mypackage-start ()
"Start mypackage."
(interactive)
...)
(provide 'mypackage)
;;; mypackage.el ends here
```
## Common Patterns
### Configuration Option
```elisp
(defcustom my-package-backend 'default
"Backend to use for processing.
Possible values:
`default' - Use built-in backend
`external' - Use external program
`auto' - Detect automatically"
:type '(choice (const :tag "Default" default)
(const :tag "External" external)
(const :tag "Auto-detect" auto))
:group 'my-package)
```
### Hook Variable
```elisp
(defvar my-package-before-save-hook nil
"Hook run before saving with my-package.
Functions receive no arguments.")
(defun my-package-save ()
"Save current state."
(run-hooks 'my-package-before-save-hook)
...)
```
### Function Variable
```elisp
(defcustom my-package-format-function #'my-package-default-format
"Function to format output.
Called with one argument (the data to format).
Should return formatted string."
:type 'function
:group 'my-package)
```
### Feature Check
```elisp
(when (featurep 'some-package)
;; Integration with some-package
...)
```
## Error Handling
### User Errors
```elisp
(defun my-package-process (input)
"Process INPUT."
(unless input
(user-error "No input provided"))
(unless (stringp input)
(user-error "Input must be string, got %s" (type-of input)))
...)
```
### Regular Errors
```elisp
(defun my-package--internal ()
"Internal function."
(unless (my-package--valid-state-p)
(error "Invalid state: %s" my-package--state))
...)
```
### Condition Handling
```elisp
(defun my-package-try-operation ()
"Attempt operation, return nil on failure."
(condition-case err
(my-package--do-operation)
(file-error
(message "File error: %s" (error-message-string err))
nil)
(error
(message "Operation failed: %s" (error-message-string err))
nil)))
```
## Performance Considerations
### Deferred Loading
Use autoload to defer loading:
```elisp
;;;###autoload
(defun my-package-start ()
"Start my-package."
(interactive)
(require 'my-package-core)
(my-package-core-start))
```
### Compilation
Byte-compile packages for performance:
```bash
emacs -batch -f batch-byte-compile mypackage.el
```
Check warnings:
```elisp
M-x byte-compile-file RET mypackage.el
```
### Lazy Evaluation
```elisp
(defvar my-package--cache nil
"Cached data.")
(defun my-package-get-data ()
"Get data, using cache if available."
(or my-package--cache
(setq my-package--cache (my-package--compute-data))))
```
## Testing Packages
### ERT Tests
```elisp
;;; mypackage-tests.el --- Tests -*- lexical-binding: t; -*-
(require 'ert)
(require 'mypackage)
(ert-deftest mypackage-test-basic ()
(should (equal (mypackage-process "input") "expected")))
(ert-deftest mypackage-test-error ()
(should-error (mypackage-process nil) :type 'user-error))
```
Run tests:
```elisp
M-x ert RET t RET
```
### Buttercup Tests
```elisp
(require 'buttercup)
(require 'mypackage)
(describe "mypackage-process"
(it "handles valid input"
(expect (mypackage-process "input") :to-equal "expected"))
(it "signals error for nil"
(expect (mypackage-process nil) :to-throw 'user-error)))
```
## Distribution
### GNU ELPA
Requirements:
- GPL-compatible license
- Copyright assignment to FSF (for core packages)
- High quality standards
Submit to `emacs-devel@gnu.org`
### MELPA
Requirements:
- Git/Mercurial repository
- Standard package format
- Quality checks pass
Submit PR to https://github.com/melpa/melpa
### MELPA Stable
Requires Git tags for versions:
```bash
git tag -a v1.0.0 -m "Release 1.0.0"
git push origin v1.0.0
```
Recipe includes `:branch`:
```elisp
(mypackage :fetcher github
:repo "user/mypackage"
:branch "stable")
```
## License Conventions
### GPL Boilerplate
Include above Commentary section:
```elisp
;;; mypackage.el --- Description -*- lexical-binding: t; -*-
;; Copyright (C) 2025 Author Name
;; SPDX-License-Identifier: GPL-3.0-or-later
;; 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.
;;; Commentary:
```
### LICENSE File
Include full license text in repository root.
Common choices:
- GPL-3.0-or-later
- GPL-2.0-or-later
- MIT (for non-GNU repositories)
## Multi-File Packages
### Package Descriptor
`mypackage-pkg.el`:
```elisp
(define-package "mypackage" "1.0.0"
"Brief description of package"
'((emacs "26.1")
(dash "2.19.1"))
:keywords '("convenience" "tools")
:authors '(("Author Name" . "email@example.com"))
:maintainer '("Maintainer" . "email@example.com")
:url "https://github.com/user/mypackage")
```
### File Organization
```
mypackage/
├── mypackage.el ; Main entry point with autoloads
├── mypackage-core.el ; Core functionality
├── mypackage-ui.el ; UI components
├── mypackage-utils.el ; Utilities
├── mypackage-pkg.el ; Package descriptor
└── mypackage-tests.el ; Tests (not packaged)
```
### Feature Naming
Each file provides named feature:
```elisp
;; mypackage.el
(provide 'mypackage)
;; mypackage-core.el
(provide 'mypackage-core)
;; mypackage-ui.el
(provide 'mypackage-ui)
```
Main file requires subfeatures:
```elisp
;;; mypackage.el --- Main file
(require 'mypackage-core)
(require 'mypackage-ui)
(provide 'mypackage)
```
## Migration and Compatibility
### Obsolete Functions
```elisp
(defun mypackage-new-name ()
"New function name."
...)
(define-obsolete-function-alias
'mypackage-old-name
'mypackage-new-name
"1.5.0"
"Use `mypackage-new-name' instead.")
```
### Obsolete Variables
```elisp
(defvar mypackage-new-option t
"New option.")
(define-obsolete-variable-alias
'mypackage-old-option
'mypackage-new-option
"1.5.0"
"Use `mypackage-new-option' instead.")
```
### Version Checking
```elisp
(when (version< emacs-version "26.1")
(error "Mypackage requires Emacs 26.1 or later"))
;; Feature-based check preferred
(unless (fboundp 'some-function)
(error "Mypackage requires some-function"))
```
## Summary
Emacs package conventions ensure quality, compatibility, and discoverability:
1. Use standard file header format with required metadata
2. Enable lexical binding in all files
3. Prefix all public symbols with package name
4. Use double-dash for private symbols
5. Write comprehensive docstrings
6. Mark interactive commands with autoload cookies
7. Provide customization group and options
8. Don't modify Emacs behavior on load
9. Include LICENSE file
10. Test with package-lint and checkdoc
11. Follow MELPA guidelines for distribution
12. Use semantic versioning
13. Declare all dependencies
14. End files with provide statement
Following these conventions enables smooth integration with package.el, acceptance into package archives, and positive user experience.

138
skills/widget/README.md Normal file
View File

@@ -0,0 +1,138 @@
# Emacs Widget Library
Build interactive forms and UI elements in Emacs buffers.
## Quick Start
```elisp
(require 'widget)
(eval-when-compile
(require 'wid-edit))
(defun my-simple-form ()
(interactive)
(switch-to-buffer "*My Form*")
(kill-all-local-variables)
(erase-buffer)
(remove-overlays)
;; Create widgets
(widget-insert "Contact Form\n\n")
(widget-create 'editable-field
:format "Name: %v\n"
:size 30)
(widget-insert "\n")
(widget-create 'editable-field
:format "Email: %v\n"
:size 40)
(widget-insert "\n\n")
(widget-create 'push-button
:notify (lambda (&rest ignore)
(message "Form submitted!"))
"Submit")
;; Enable interaction
(use-local-map widget-keymap)
(widget-setup))
```
## Key Widget Types
**Text Input:**
- `editable-field` - Single-line text
- `text` - Multi-line text area
**Buttons:**
- `push-button` - Clickable action button
- `link` - Hyperlink with action
**Selection:**
- `checkbox` - Boolean toggle
- `radio-button-choice` - Single choice from options
- `menu-choice` - Dropdown selection
- `checklist` - Multiple selections
**Lists:**
- `editable-list` - Dynamic list with add/remove
## Essential Functions
```elisp
;; Create widget
(widget-create TYPE &rest ARGS)
;; Enable widgets (required after creation)
(widget-setup)
;; Get/set values
(widget-value WIDGET)
(widget-value-set WIDGET VALUE)
;; Navigate
(widget-forward) ; TAB
(widget-backward) ; S-TAB
;; Properties
(widget-get WIDGET PROPERTY)
(widget-put WIDGET PROPERTY VALUE)
```
## Common Pattern
```elisp
(defun my-widget-interface ()
(interactive)
(let (field1 field2)
;; Setup buffer
(switch-to-buffer "*Interface*")
(kill-all-local-variables)
(erase-buffer)
(remove-overlays)
;; Build UI
(widget-insert "Title\n\n")
(setq field1 (widget-create 'editable-field "default"))
(widget-insert "\n")
(setq field2 (widget-create 'checkbox t))
;; Submit button
(widget-create 'push-button
:notify (lambda (&rest _)
(message "Values: %S %S"
(widget-value field1)
(widget-value field2)))
"Submit")
;; Activate
(use-local-map widget-keymap)
(widget-setup)))
```
## Using the Skill
Invoke this skill when building:
- Custom configuration interfaces
- Interactive forms
- Settings editors
- Data entry dialogs
The skill provides comprehensive coverage of widget types, API functions, patterns, and real-world examples.
## Learning Path
1. **Basics** - Simple forms with editable-field and push-button
2. **Selection** - checkbox, radio-button-choice, menu-choice
3. **Callbacks** - :notify handlers and value retrieval
4. **Validation** - Input validation and error handling
5. **Advanced** - Custom widgets and complex interfaces
## Resources
- Emacs Info: `C-h i m Widget RET`
- Source: `wid-edit.el` in Emacs distribution
- Built-in demo: `widget-example` (if available)

952
skills/widget/SKILL.md Normal file
View File

@@ -0,0 +1,952 @@
---
name: widget
description: A guide to using the Emacs Widget Library for creating interactive UI elements in buffers.
---
# Emacs Widget Library
Build interactive forms and UI elements in Emacs buffers using the built-in widget library (wid-edit.el).
## Overview
The Emacs Widget Library provides a framework for creating interactive UI elements within text buffers. Widgets enable building forms, configuration interfaces, and custom UI components without requiring external dependencies.
**Core libraries:**
- `widget.el` - Top-level widget interface
- `wid-edit.el` - Widget implementation and editing
**Primary use cases:**
- Custom configuration interfaces
- Interactive forms and dialogs
- Settings and preference editors
- Data entry and validation
## Setup
```elisp
(require 'widget)
(eval-when-compile
(require 'wid-edit))
```
## Core Concepts
### Widget Lifecycle
1. **Create** - `widget-create` returns a widget object
2. **Setup** - `widget-setup` enables interaction after creation
3. **Interact** - User edits/activates widgets
4. **Query** - Retrieve values with `widget-value`
5. **Delete** - Clean up with `widget-delete`
### Widget Buffer Setup
```elisp
(defun my-widget-example ()
(interactive)
(switch-to-buffer "*Widget Example*")
(kill-all-local-variables)
(erase-buffer)
(remove-overlays)
;; Create widgets
(widget-insert "Example Form\n\n")
(widget-create 'editable-field
:format "Name: %v"
:size 30
"Enter name")
(widget-insert "\n\n")
(widget-create 'push-button
:notify (lambda (&rest ignore)
(message "Submitted!"))
"Submit")
(widget-insert "\n")
;; Enable widgets
(use-local-map widget-keymap)
(widget-setup))
```
### Widget Properties
Widgets are configured via keyword arguments:
- `:value` - Initial/current value
- `:format` - Display format string
- `:tag` - Label text
- `:notify` - Callback function on change
- `:help-echo` - Tooltip text
## Widget Types
### Text Input
#### editable-field
Basic text input field.
```elisp
;; Simple field
(widget-create 'editable-field
:format "Label: %v\n"
"default text")
;; Sized field
(widget-create 'editable-field
:size 20
:format "Username: %v\n")
;; Password field
(widget-create 'editable-field
:secret ?*
:format "Password: %v\n")
;; With change notification
(widget-create 'editable-field
:notify (lambda (widget &rest ignore)
(message "Value: %s" (widget-value widget)))
:format "Email: %v\n")
```
#### text
Multi-line text area.
```elisp
(widget-create 'text
:format "Comments:\n%v"
:value "Line 1\nLine 2\nLine 3")
```
### Buttons
#### push-button
Clickable button that triggers action.
```elisp
;; Basic button
(widget-create 'push-button
:notify (lambda (&rest ignore)
(message "Clicked!"))
"Click Me")
;; Styled button
(widget-create 'push-button
:button-face 'custom-button
:format "%[%t%]\n"
:tag "Submit Form"
:notify (lambda (&rest ignore)
(my-submit-form)))
```
#### link
Hyperlink that executes action.
```elisp
;; Function link
(widget-create 'link
:button-face 'info-xref
:help-echo "View documentation"
:notify (lambda (&rest ignore)
(describe-function 'widget-create))
"Documentation")
;; URL link
(widget-create 'url-link
:format "%[%t%]"
:tag "Emacs Manual"
"https://www.gnu.org/software/emacs/manual/")
```
### Selection
#### checkbox
Boolean toggle (checked/unchecked).
```elisp
(widget-create 'checkbox
:format "%[%v%] Enable feature\n"
:notify (lambda (widget &rest ignore)
(message "Feature %s"
(if (widget-value widget)
"enabled"
"disabled")))
t) ; Initial state
```
#### toggle
Text-based on/off switch.
```elisp
(widget-create 'toggle
:on "Enabled"
:off "Disabled"
:notify (lambda (widget &rest ignore)
(my-update-setting (widget-value widget)))
nil) ; Initially off
```
#### radio-button-choice
Single selection from multiple options.
```elisp
(widget-create 'radio-button-choice
:value "medium"
:notify (lambda (widget &rest ignore)
(message "Selected: %s" (widget-value widget)))
'(item "small")
'(item "medium")
'(item "large"))
```
#### menu-choice
Dropdown menu selection.
```elisp
(widget-create 'menu-choice
:tag "Output Format"
:value 'json
'(const :tag "JSON" json)
'(const :tag "XML" xml)
'(const :tag "Plain Text" text)
'(editable-field :menu-tag "Custom" "custom-format"))
```
#### checklist
Multiple selections (subset of options).
```elisp
(widget-create 'checklist
:notify (lambda (widget &rest ignore)
(message "Selected: %S" (widget-value widget)))
'(const :tag "Option A" option-a)
'(const :tag "Option B" option-b)
'(const :tag "Option C" option-c))
```
### Lists
#### editable-list
Dynamic list with add/remove buttons.
```elisp
(widget-create 'editable-list
:entry-format "%i %d %v"
:value '("item1" "item2")
'(editable-field :value ""))
```
**Format specifiers:**
- `%i` - Insert button (INS)
- `%d` - Delete button (DEL)
- `%v` - The value widget
## Widget API
### Creation and Setup
#### widget-create
Create widget and return widget object.
```elisp
(widget-create TYPE [KEYWORD ARGUMENT]...)
;; Example
(setq my-widget
(widget-create 'editable-field
:size 25
:value "initial"))
```
#### widget-setup
Enable widgets after creation. Must be called before user interaction.
```elisp
(widget-setup)
```
Required after:
- Initial widget creation
- Calling `widget-value-set`
- Modifying widget structure
#### widget-insert
Insert text at point (not a widget).
```elisp
(widget-insert "Header Text\n\n")
```
### Value Access
#### widget-value
Get current widget value.
```elisp
(widget-value WIDGET)
;; Example
(let ((name (widget-value name-widget))
(enabled (widget-value checkbox-widget)))
(message "Name: %s, Enabled: %s" name enabled))
```
#### widget-value-set
Set widget value programmatically.
```elisp
(widget-value-set WIDGET VALUE)
(widget-setup) ; Required after value change
;; Example
(widget-value-set email-widget "user@example.com")
(widget-setup)
```
### Property Access
#### widget-get
Retrieve widget property.
```elisp
(widget-get WIDGET PROPERTY)
;; Example
(widget-get my-widget :tag)
(widget-get my-widget :size)
```
#### widget-put
Set widget property.
```elisp
(widget-put WIDGET PROPERTY VALUE)
;; Example
(widget-put my-widget :help-echo "Enter your email address")
```
#### widget-apply
Call widget method with arguments.
```elisp
(widget-apply WIDGET PROPERTY &rest ARGS)
;; Example
(widget-apply my-widget :notify)
```
### Navigation
#### widget-forward
Move to next widget (bound to TAB).
```elisp
(widget-forward &optional COUNT)
```
#### widget-backward
Move to previous widget (bound to S-TAB/M-TAB).
```elisp
(widget-backward &optional COUNT)
```
### Cleanup
#### widget-delete
Delete widget and clean up.
```elisp
(widget-delete WIDGET)
```
## Common Patterns
### Form with Validation
```elisp
(defun my-registration-form ()
(interactive)
(let (username-widget email-widget)
(switch-to-buffer "*Registration*")
(kill-all-local-variables)
(erase-buffer)
(remove-overlays)
(widget-insert "User Registration\n\n")
(setq username-widget
(widget-create 'editable-field
:format "Username: %v\n"
:size 25))
(widget-insert "\n")
(setq email-widget
(widget-create 'editable-field
:format "Email: %v\n"
:size 40))
(widget-insert "\n\n")
(widget-create 'push-button
:notify (lambda (&rest ignore)
(let ((user (widget-value username-widget))
(email (widget-value email-widget)))
(if (and (> (length user) 0)
(string-match-p "@" email))
(message "Registered: %s <%s>" user email)
(message "Invalid input"))))
"Register")
(use-local-map widget-keymap)
(widget-setup)))
```
### Settings Interface
```elisp
(defun my-settings ()
(interactive)
(let (theme-widget auto-save-widget)
(switch-to-buffer "*Settings*")
(kill-all-local-variables)
(erase-buffer)
(remove-overlays)
(widget-insert "Application Settings\n\n")
;; Theme selection
(widget-insert "Theme:\n")
(setq theme-widget
(widget-create 'radio-button-choice
:value my-current-theme
'(const :tag "Light" light)
'(const :tag "Dark" dark)
'(const :tag "Auto" auto)))
(widget-insert "\n")
;; Auto-save toggle
(setq auto-save-widget
(widget-create 'checkbox
:format "%[%v%] Enable auto-save\n"
my-auto-save-enabled))
(widget-insert "\n")
;; Save button
(widget-create 'push-button
:notify (lambda (&rest ignore)
(setq my-current-theme
(widget-value theme-widget))
(setq my-auto-save-enabled
(widget-value auto-save-widget))
(message "Settings saved"))
"Save Settings")
(use-local-map widget-keymap)
(widget-setup)))
```
### Dynamic Form
```elisp
(defun my-dynamic-list ()
(interactive)
(let (items-widget)
(switch-to-buffer "*Dynamic List*")
(kill-all-local-variables)
(erase-buffer)
(remove-overlays)
(widget-insert "Todo List\n\n")
(setq items-widget
(widget-create 'editable-list
:format "%v%i\n"
:entry-format "%i %d %v\n"
:value '("Buy groceries" "Write code")
'(editable-field :size 40)))
(widget-insert "\n")
(widget-create 'push-button
:notify (lambda (&rest ignore)
(let ((items (widget-value items-widget)))
(message "Todo items: %S" items)))
"Show Items")
(use-local-map widget-keymap)
(widget-setup)))
```
### Collecting Multiple Values
```elisp
(defun my-collect-values (widgets)
"Collect values from multiple widgets into alist."
(mapcar (lambda (pair)
(cons (car pair)
(widget-value (cdr pair))))
widgets))
(defun my-form-with-collection ()
(interactive)
(let (name-w email-w age-w widgets)
(switch-to-buffer "*Form*")
(kill-all-local-variables)
(erase-buffer)
(remove-overlays)
(widget-insert "User Information\n\n")
(setq name-w (widget-create 'editable-field
:format "Name: %v\n"))
(setq email-w (widget-create 'editable-field
:format "Email: %v\n"))
(setq age-w (widget-create 'editable-field
:format "Age: %v\n"))
(setq widgets `((name . ,name-w)
(email . ,email-w)
(age . ,age-w)))
(widget-insert "\n")
(widget-create 'push-button
:notify (lambda (&rest ignore)
(let ((data (my-collect-values widgets)))
(message "Data: %S" data)))
"Submit")
(use-local-map widget-keymap)
(widget-setup)))
```
## Format Strings
Widget `:format` property controls display using escape sequences:
- `%v` - Widget value
- `%t` - Tag
- `%d` - Documentation string
- `%h` - Help-echo
- `%[` - Button prefix
- `%]` - Button suffix
- `%%` - Literal %
```elisp
;; Custom formats
(widget-create 'push-button
:format "Click %[here%] to continue\n"
:notify #'my-callback
"Continue")
(widget-create 'editable-field
:format "%t: %v (%h)\n"
:tag "Email"
:help-echo "user@domain.com")
```
## Notifications and Callbacks
The `:notify` property specifies callback on widget change/activation.
**Callback signature:**
```elisp
(lambda (widget &optional changed-widget &rest event)
;; widget - the widget with :notify property
;; changed-widget - widget that actually changed (for composite widgets)
;; event - interaction event
...)
```
**Examples:**
```elisp
;; Simple notification
(widget-create 'checkbox
:notify (lambda (w &rest _)
(message "Checked: %s" (widget-value w)))
nil)
;; Access parent widget
(widget-create 'radio-button-choice
:notify (lambda (parent child &rest _)
(message "Parent value: %s" (widget-value parent))
(message "Child value: %s" (widget-value child)))
'(item "A")
'(item "B"))
;; Update other widgets
(let (field1 field2)
(setq field1
(widget-create 'editable-field
:notify (lambda (w &rest _)
(widget-value-set field2
(upcase (widget-value w)))
(widget-setup))))
(setq field2 (widget-create 'editable-field)))
```
## Keybindings
`widget-keymap` provides default bindings:
- `TAB` - `widget-forward` - Next widget
- `S-TAB` / `M-TAB` - `widget-backward` - Previous widget
- `RET` - `widget-button-press` - Activate button
- `C-k` - `widget-kill-line` - Kill to end of field
- `M-TAB` - `widget-complete` - Complete field (if supported)
**Custom keymap:**
```elisp
(defvar my-widget-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map widget-keymap)
(define-key map (kbd "C-c C-c") 'my-submit)
(define-key map (kbd "C-c C-k") 'my-cancel)
map))
(use-local-map my-widget-mode-map)
```
## Error Handling
### Validation
```elisp
(defun my-validate-email (widget)
(let ((value (widget-value widget)))
(unless (string-match-p "^[^@]+@[^@]+\\.[^@]+$" value)
(error "Invalid email format"))))
(widget-create 'editable-field
:format "Email: %v\n"
:notify (lambda (w &rest _)
(condition-case err
(my-validate-email w)
(error (message "Error: %s" (error-message-string err))))))
```
### Safe Value Retrieval
```elisp
(defun my-safe-widget-value (widget &optional default)
"Get widget value with fallback."
(condition-case nil
(widget-value widget)
(error default)))
```
## Buffer Cleanup
```elisp
(defun my-widget-cleanup ()
"Clean up widget buffer."
(interactive)
(when (eq major-mode 'my-widget-mode)
(remove-overlays)
(kill-buffer)))
;; With kill-buffer-hook
(defvar my-widget-mode-hook nil)
(add-hook 'my-widget-mode-hook
(lambda ()
(add-hook 'kill-buffer-hook
(lambda ()
(remove-overlays))
nil t)))
```
## Defining Custom Widgets
```elisp
(define-widget 'my-email-field 'editable-field
"Email input field with validation."
:format "Email: %v\n"
:size 40
:valid-regexp "^[^@]+@[^@]+\\.[^@]+$"
:notify (lambda (widget &rest ignore)
(let ((value (widget-value widget))
(regexp (widget-get widget :valid-regexp)))
(if (string-match-p regexp value)
(message "Valid email")
(message "Invalid email format")))))
;; Usage
(widget-create 'my-email-field)
```
**Inheritance:**
```elisp
(define-widget 'my-readonly-field 'editable-field
"Read-only text field."
:keymap (let ((map (copy-keymap widget-field-keymap)))
(suppress-keymap map)
map))
```
## Performance Considerations
**Minimize redraws:**
```elisp
;; Bad: Multiple setups
(widget-value-set widget1 val1)
(widget-setup)
(widget-value-set widget2 val2)
(widget-setup)
;; Good: Batch updates
(widget-value-set widget1 val1)
(widget-value-set widget2 val2)
(widget-setup)
```
**Use buffer-local variables:**
```elisp
(defvar-local my-widget-data nil
"Buffer-local widget storage.")
```
**Avoid redundant notifications:**
```elisp
(defvar my-updating nil)
(widget-create 'editable-field
:notify (lambda (w &rest _)
(unless my-updating
(setq my-updating t)
(my-expensive-update)
(setq my-updating nil))))
```
## Integration with Major Modes
```elisp
(define-derived-mode my-widget-mode special-mode "MyWidget"
"Major mode for widget-based interface."
:group 'my-widget
(setq truncate-lines t)
(setq buffer-read-only nil))
(defun my-widget-interface ()
(interactive)
(switch-to-buffer "*My Interface*")
(my-widget-mode)
(erase-buffer)
;; Build interface
(widget-insert "My Application\n\n")
;; ... create widgets ...
(use-local-map widget-keymap)
(widget-setup)
(goto-char (point-min))
(widget-forward 1))
```
## Debugging
```elisp
;; Inspect widget properties
(pp-eval-expression '(widget-get my-widget :value))
;; Check widget type
(widget-type my-widget)
;; View all properties
(let ((widget my-widget))
(while widget
(prin1 (car widget))
(terpri)
(setq widget (cdr widget))))
;; Trace notifications
(widget-create 'checkbox
:notify (lambda (&rest args)
(message "Notify called with: %S" args)
(backtrace))
t)
```
## Use Cases
### Configuration Editor
Customize package using widgets instead of Custom interface:
```elisp
(defun my-config-editor ()
(interactive)
(let (theme-w size-w)
(switch-to-buffer "*Config*")
(kill-all-local-variables)
(erase-buffer)
(widget-insert "Configuration\n\n")
(setq theme-w
(widget-create 'menu-choice
:tag "Theme"
:value my-theme
'(const light)
'(const dark)))
(setq size-w
(widget-create 'editable-field
:format "\nFont size: %v\n"
:value (number-to-string my-font-size)))
(widget-insert "\n")
(widget-create 'push-button
:notify (lambda (&rest _)
(setq my-theme (widget-value theme-w))
(setq my-font-size
(string-to-number (widget-value size-w)))
(my-apply-config))
"Apply")
(use-local-map widget-keymap)
(widget-setup)))
```
### Search/Filter Interface
```elisp
(defun my-search-interface ()
(interactive)
(let (query-w type-w)
(switch-to-buffer "*Search*")
(kill-all-local-variables)
(erase-buffer)
(setq query-w
(widget-create 'editable-field
:format "Query: %v\n"
:size 50))
(setq type-w
(widget-create 'checklist
:format "\nTypes:\n%v\n"
'(const :tag "Functions" function)
'(const :tag "Variables" variable)
'(const :tag "Faces" face)))
(widget-create 'push-button
:notify (lambda (&rest _)
(my-perform-search
(widget-value query-w)
(widget-value type-w)))
"Search")
(use-local-map widget-keymap)
(widget-setup)))
```
### Wizard/Multi-step Form
```elisp
(defvar my-wizard-step 1)
(defvar my-wizard-data nil)
(defun my-wizard-next ()
(setq my-wizard-data
(plist-put my-wizard-data
(intern (format "step%d" my-wizard-step))
(my-collect-current-step)))
(setq my-wizard-step (1+ my-wizard-step))
(my-wizard-show))
(defun my-wizard-show ()
(erase-buffer)
(cond
((= my-wizard-step 1)
(my-wizard-step-1))
((= my-wizard-step 2)
(my-wizard-step-2))
(t
(my-wizard-finish)))
(widget-setup))
```
## Common Pitfalls
**Forgetting widget-setup:**
```elisp
;; Wrong
(widget-create 'editable-field)
;; User can't interact yet!
;; Correct
(widget-create 'editable-field)
(widget-setup)
```
**Not calling widget-setup after value-set:**
```elisp
;; Wrong
(widget-value-set widget "new value")
;; Widget not updated!
;; Correct
(widget-value-set widget "new value")
(widget-setup)
```
**Incorrect buffer setup:**
```elisp
;; Missing keymap
(erase-buffer)
(widget-create 'editable-field)
(widget-setup)
;; TAB won't navigate!
;; Correct
(erase-buffer)
(widget-create 'editable-field)
(use-local-map widget-keymap)
(widget-setup)
```
**Not cleaning overlays:**
```elisp
;; Memory leak
(erase-buffer)
(widget-create ...)
;; Proper cleanup
(erase-buffer)
(remove-overlays)
(widget-create ...)
```
## Resources
- Info manual: `C-h i m Widget RET`
- Source: `wid-edit.el`, `widget.el` in Emacs distribution
- Demo: `M-x widget-example` (if available)
- Tutorial: Official Emacs Widget Library manual

View File

@@ -0,0 +1,73 @@
{:name "widget"
:version "1.0.0"
:description "A guide to using the Emacs Widget Library for creating interactive UI elements in buffers."
:library {:name "wid-edit.el / widget.el"
:version "Built-in"
:url "https://www.gnu.org/software/emacs/manual/html_mono/widget.html"
:license "GPL-3.0-or-later"
:repository "https://github.com/emacs-mirror/emacs/blob/master/lisp/wid-edit.el"}
:tags [:emacs :ui :forms :widgets :interactive :buffer :interface]
:use-cases ["Custom configuration interfaces"
"Interactive forms and dialogs"
"Settings and preference editors"
"Data entry and validation"
"Multi-step wizards"
"Search and filter interfaces"]
:features ["Text input fields (single and multi-line)"
"Buttons and links"
"Checkboxes and radio buttons"
"Dropdown menus and checklists"
"Dynamic editable lists"
"Custom widget definitions"
"Value validation"
"Callback notifications"
"Keyboard navigation"]
:files {:skill "SKILL.md"
:readme "README.md"
:examples nil}
:learning-path [{:level "beginner"
:topics ["Basic widget creation"
"Buffer setup"
"editable-field and push-button"
"widget-setup requirement"]}
{:level "intermediate"
:topics ["Selection widgets"
"Callbacks and notifications"
"Value retrieval and setting"
"Form validation"]}
{:level "advanced"
:topics ["Custom widget definitions"
"Complex forms"
"Major mode integration"
"Performance optimization"]}]
:platform {:emacs "Built-in"
:dependencies ["widget.el" "wid-edit.el"]}
:api-coverage {:core ["widget-create"
"widget-setup"
"widget-value"
"widget-value-set"
"widget-insert"]
:navigation ["widget-forward"
"widget-backward"]
:properties ["widget-get"
"widget-put"
"widget-apply"]
:lifecycle ["widget-delete"]
:types ["editable-field"
"text"
"push-button"
"link"
"checkbox"
"toggle"
"radio-button-choice"
"menu-choice"
"checklist"
"editable-list"]}}