Files
gh-hugoduncan-library-skill…/skills/magit-section/SKILL.md
2025-11-29 18:47:18 +08:00

793 lines
20 KiB
Markdown

---
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