| 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 startsend- Buffer position where section endshidden- Visibility state (nil=visible, non-nil=hidden)children- List of child sectionsparent- Parent section referencekeymap- Section-specific key bindingswasher- 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:
(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 slothide- Initial visibility (nil=visible, t=hidden)
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:
n- Forwardp- Backward^- Up to parentM-n- Forward siblingM-p- Backward sibling
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:
TAB- Toggle current sectionC-c TAB/C-<tab>- Cycle visibility states<backtab>- Cycle all top-level sections
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/4- Show levels around current sectionM-1/M-2/M-3/M-4- Show levels for entire buffer
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
(TYPE VALUE)matches type and value - List of symbols matches any type in list
[TYPE...]matches type hierarchy
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