Library-skills magit-section
A guide to using magit-section for building collapsible, hierarchical buffer UIs in Emacs.
git clone https://github.com/hugoduncan/library-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/hugoduncan/library-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/emacs-libraries/skills/magit-section" ~/.claude/skills/hugoduncan-library-skills-magit-section && rm -rf "$T"
plugins/emacs-libraries/skills/magit-section/SKILL.mdmagit-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:
- Symbol identifying section kind (e.g.,type
,file
,commit
)hunk
- Associated data (filename, commit SHA, etc.)value
- Buffer position where section begins (includes heading)start
- Buffer position where body content startscontent
- Buffer position where section endsend
- Visibility state (nil=visible, non-nil=hidden)hidden
- List of child sectionschildren
- Parent section referenceparent
- Section-specific key bindingskeymap
- Function for deferred content generationwasher
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:
(magit-insert-section (type value &optional hide) [HEADING-FORM] BODY...)
Arguments:
- Section type (symbol ortype
)(eval FORM)
- Data to store in section's value slotvalue
- Initial visibility (nil=visible, t=hidden)hide
Example:
(magit-insert-section (file "README.md") (magit-insert-heading "README.md") (insert "File contents here\n"))
Advanced Usage:
(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:
(magit-insert-heading &rest ARGS)
Example:
(magit-insert-heading (format "Changes (%d)" (length file-list)))
magit-insert-section-body
Defer section body evaluation until first expansion:
(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:
(when (null items) (magit-cancel-section))
Removes partial section from buffer and section tree.
Navigation Commands
Movement
(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:
- Forwardn
- Backwardp
- Up to parent^
- Forward siblingM-n
- Backward siblingM-p
Example Navigation Hook:
(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
(magit-section-toggle &optional SECTION) ; Toggle current/specified (magit-section-show SECTION) ; Expand (magit-section-hide SECTION) ; Collapse
Keybindings:
- Toggle current sectionTAB
/C-c TAB
- Cycle visibility statesC-<tab>
- Cycle all top-level sections<backtab>
Recursive Operations
(magit-section-show-children SECTION &optional DEPTH) (magit-section-hide-children SECTION) (magit-section-show-headings SECTION)
Example:
;; Expand section and first level of children (magit-section-show-children (magit-current-section) 1)
Level-Based Visibility
(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
- Show levels around current section4
/M-1
/M-2
/M-3
- Show levels for entire bufferM-4
Cycling
(magit-section-cycle &optional SECTION) ; Current section (magit-section-cycle-global) ; All sections
Cycles through: collapsed → expanded → children-collapsed → collapsed
Querying Sections
Current Section
(magit-current-section) ; Section at point (magit-section-at &optional POS) ; Section at position
Example:
(let ((section (magit-current-section))) (message "Type: %s, Value: %s" (oref section type) (oref section value)))
Section Properties
(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:
(when (magit-section-content-p section) (magit-section-show section))
Section Matching
(magit-section-match CONDITIONS &optional SECTION)
Conditions:
- Symbol matches type
- List
matches type and value(TYPE VALUE) - List of symbols matches any type in list
matches type hierarchy[TYPE...]
Example:
;; 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
(magit-region-sections &optional CONDITION MULTIPLE)
Get sibling sections in active region.
Example:
(let ((selected-files (magit-region-sections 'file t))) (dolist (section selected-files) (message "Selected: %s" (oref section value))))
Section Inspection
(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:
(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:
(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
;; Highlight current section (setq magit-section-highlight-current t) ;; Highlight region selection (setq magit-section-highlight-selection t)
Child Count Display
;; Show number of children in headings (setq magit-section-show-child-count t)
Visibility Indicators
;; Customize expansion/collapse indicators (setq magit-section-visibility-indicators '((expanded . "▼") (collapsed . "▶")))
Visibility Caching
;; 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
;; Disable line numbers in section buffers (default) (setq magit-section-disable-line-numbers t)
Common Patterns
Basic Section Buffer
(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
(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
(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
(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
(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
(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
(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
(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
(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
(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
(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
;; 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
(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:
(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:
(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:
;; 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:
(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:
(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
;; Describe current section (call-interactively #'magit-describe-section) ;; Brief description (magit-describe-section-briefly)
Debug Helpers
(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:
;; 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:
;; 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