hideshow: New minor mode 'hs-indentation-mode'. (Bug#80179)

This minor mode configures hs-minor-mode to use
indentation-based folding.

* lisp/progmodes/hideshow.el (hs-hideable-block-p): New
function.
(hs-indentation-respect-end-block): New option.
(hs-indentation--store-vars): New variable.
(hs-cycle-filter, hs-get-first-block-on-line, hs-get-near-block)
(hs-find-block-beg-fn--default): Adapt code to use
'hs-hideable-block-p'.
(hs-block-positions): Update.
(hs-indentation-mode): New minor mode.

* doc/emacs/programs.texi (Hideshow): Update documentation.

* etc/NEWS: Announce changes

* test/lisp/progmodes/hideshow-tests.el: Add 'require'.
(hideshow-check-indentation-folding): New test.
This commit is contained in:
Elías Gabriel Pérez
2026-02-22 21:30:43 -06:00
committed by Eli Zaretskii
parent 1f04208898
commit c911495fb1
4 changed files with 149 additions and 35 deletions

View File

@@ -1688,6 +1688,13 @@ row). Just what constitutes a block depends on the major mode. In C
mode and related modes, blocks are delimited by braces, while in Lisp mode and related modes, blocks are delimited by braces, while in Lisp
mode they are delimited by parentheses. Multi-line comments also mode they are delimited by parentheses. Multi-line comments also
count as blocks. count as blocks.
Additionally, Hideshow mode supports optional indentation-based
hiding/showing. By default this is disabled; to enable it, turn on the
buffer-local minor mode @code{hs-indentation-mode}. Enabling
@code{hs-indentation-mode} does not require that @code{hs-minor-mode} is
already enabled.
@vindex hs-prefix-map @vindex hs-prefix-map
Hideshow mode provides the following commands (defined in @code{hs-prefix-map}): Hideshow mode provides the following commands (defined in @code{hs-prefix-map}):
@@ -1743,6 +1750,7 @@ Either hide or show all the blocks in the current buffer. (@code{hs-toggle-all})
@vindex hs-isearch-open @vindex hs-isearch-open
@vindex hs-hide-block-behavior @vindex hs-hide-block-behavior
@vindex hs-cycle-filter @vindex hs-cycle-filter
@vindex hs-indentation-respect-end-block
These variables can be used to customize Hideshow mode: These variables can be used to customize Hideshow mode:
@table @code @table @code
@@ -1795,6 +1803,11 @@ block. Its value should be either @code{code} (unhide only code
blocks), @code{comment} (unhide only comments), @code{t} (unhide both blocks), @code{comment} (unhide only comments), @code{t} (unhide both
code blocks and comments), or @code{nil} (unhide neither code blocks code blocks and comments), or @code{nil} (unhide neither code blocks
nor comments). The default value is @code{code}. nor comments). The default value is @code{code}.
@item hs-indentation-respect-end-block
This variable controls whether the end of the block should be hidden
together with the hidden region. This only has effect if
@code{hs-indentation-mode} is enabled.
@end table @end table
@node Symbol Completion @node Symbol Completion

View File

@@ -1374,6 +1374,14 @@ buffer-local variables 'hs-block-start-regexp', 'hs-c-start-regexp',
*** 'hs-hide-level' can now hide comments too. *** 'hs-hide-level' can now hide comments too.
This is controlled by 'hs-hide-comments-when-hiding-all'. This is controlled by 'hs-hide-comments-when-hiding-all'.
+++
*** New minor mode 'hs-indentation-mode'.
This buffer-local minor mode configures 'hs-indentation-mode' to detect
blocks based on indentation.
The new user option 'hs-indentation-respect-end-block' can be used to
adjust the hiding range for this minor mode.
** C-ts mode ** C-ts mode
+++ +++

View File

@@ -63,6 +63,8 @@
;; hideshow minor mode by typing `M-x hs-minor-mode'. After hideshow is ;; hideshow minor mode by typing `M-x hs-minor-mode'. After hideshow is
;; activated or deactivated, `hs-minor-mode-hook' is run with `run-hooks'. ;; activated or deactivated, `hs-minor-mode-hook' is run with `run-hooks'.
;; ;;
;; To enable indentation-based hiding/showing turn on `hs-indentation-mode'.
;;
;; Additionally, Joseph Eydelnant writes: ;; Additionally, Joseph Eydelnant writes:
;; I enjoy your package hideshow.el Version 5.24 2001/02/13 ;; I enjoy your package hideshow.el Version 5.24 2001/02/13
;; a lot and I've been looking for the following functionality: ;; a lot and I've been looking for the following functionality:
@@ -83,28 +85,16 @@
;; Hideshow provides the following user options: ;; Hideshow provides the following user options:
;; ;;
;; - `hs-hide-comments-when-hiding-all' ;; - `hs-hide-comments-when-hiding-all'
;; If non-nil, `hs-hide-all', `hs-cycle' and `hs-hide-level' will hide
;; comments too.
;; - `hs-hide-all-non-comment-function' ;; - `hs-hide-all-non-comment-function'
;; If non-nil, after calling `hs-hide-all', this function is called
;; with no arguments.
;; - `hs-isearch-open' ;; - `hs-isearch-open'
;; What kind of hidden blocks to open when doing isearch.
;; - `hs-set-up-overlay' ;; - `hs-set-up-overlay'
;; Function called with one arg (an overlay), intended to customize
;; the block hiding appearance.
;; - `hs-display-lines-hidden' ;; - `hs-display-lines-hidden'
;; Displays the number of hidden lines next to the ellipsis.
;; - `hs-show-indicators' ;; - `hs-show-indicators'
;; Display indicators to show and toggle the block hiding.
;; - `hs-indicator-type' ;; - `hs-indicator-type'
;; Which indicator type should be used for the block indicators.
;; - `hs-indicator-maximum-buffer-size' ;; - `hs-indicator-maximum-buffer-size'
;; Max buffer size in bytes where the indicators should be enabled.
;; - `hs-allow-nesting' ;; - `hs-allow-nesting'
;; If non-nil, hiding remembers internal blocks.
;; - `hs-cycle-filter' ;; - `hs-cycle-filter'
;; Control where typing a `TAB' cycles the visibility. ;; - `hs-indentation-respect-end-block'
;; ;;
;; The variable `hs-hide-all-non-comment-function' may be useful if you ;; The variable `hs-hide-all-non-comment-function' may be useful if you
;; only want to hide some N levels blocks for some languages/files or ;; only want to hide some N levels blocks for some languages/files or
@@ -458,10 +448,7 @@ Currently it affects only the command `hs-toggle-hiding' by default,
but it can be easily replaced with the command `hs-cycle'." but it can be easily replaced with the command `hs-cycle'."
:type `(choice (const :tag "Nowhere" nil) :type `(choice (const :tag "Nowhere" nil)
(const :tag "Everywhere on the headline" t) (const :tag "Everywhere on the headline" t)
(const :tag "At block beginning" (const :tag "At block beginning" hs-hideable-block-p)
,(lambda ()
(pcase-let ((`(,beg ,end) (hs-block-positions)))
(and beg (hs-hideable-region-p beg end)))))
(const :tag "At line beginning" bolp) (const :tag "At line beginning" bolp)
(const :tag "Not at line beginning" (const :tag "Not at line beginning"
,(lambda () (not (bolp)))) ,(lambda () (not (bolp))))
@@ -469,6 +456,18 @@ but it can be easily replaced with the command `hs-cycle'."
(function :tag "Custom filter function")) (function :tag "Custom filter function"))
:version "31.1") :version "31.1")
;; Used in `hs-indentation-mode'
(defcustom hs-indentation-respect-end-block nil
"If non-nil, the end of the block will not be hidden.
This only has effect if `hs-indentation-mode' is enabled.
NOTE: For some modes, enabling this may result in hiding wrong parts of
the buffer. If this happens, enable this only for some modes (usually
using `add-hook')."
:type 'boolean
:local t
:version "31.1")
;;;; Icons ;;;; Icons
(define-icon hs-indicator-hide nil (define-icon hs-indicator-hide nil
@@ -616,6 +615,9 @@ Note that `mode-line-format' is buffer-local.")
;; Used in `hs-toggle-all' ;; Used in `hs-toggle-all'
(defvar-local hs--toggle-all-state) (defvar-local hs--toggle-all-state)
;; Used in `hs-indentation-mode'
(defvar-local hs-indentation--store-vars nil)
;;;; API variables ;;;; API variables
@@ -788,6 +790,17 @@ Skip \"internal\" overlays if `hs-allow-nesting' is non-nil."
(and beg end (and beg end
(< beg (save-excursion (goto-char end) (pos-bol))))) (< beg (save-excursion (goto-char end) (pos-bol)))))
(defun hs-hideable-block-p (&optional include-comment)
"Return t if block at point is hideable.
If INCLUDE-COMMENT is non-nil, include comments first.
If there is no block at point, return nil."
(pcase-let ((`(,beg ,end)
(or (and include-comment
(funcall hs-inside-comment-predicate))
(hs-block-positions))))
(hs-hideable-region-p beg end)))
(defun hs-already-hidden-p () (defun hs-already-hidden-p ()
"Return non-nil if point is in an already-hidden block, otherwise nil." "Return non-nil if point is in an already-hidden block, otherwise nil."
(save-excursion (save-excursion
@@ -820,14 +833,13 @@ This is for code block positions only, for comments use
(save-match-data (save-match-data
(save-excursion (save-excursion
(when (funcall hs-looking-at-block-start-predicate) (when (funcall hs-looking-at-block-start-predicate)
(let* ((beg (match-end 0)) end) (let ((beg (match-end 0)) end)
;; `beg' is the point at the block beginning, which may need ;; `beg' is the point at the block beginning, which may need
;; to be adjusted ;; to be adjusted
(when adjust-beg (when adjust-beg
(setq beg (pos-eol)) (setq beg (if hs-adjust-block-beginning-function
(save-excursion (funcall hs-adjust-block-beginning-function beg)
(when hs-adjust-block-beginning-function (pos-eol))))
(goto-char (funcall hs-adjust-block-beginning-function beg)))))
(goto-char (match-beginning hs-block-start-mdata-select)) (goto-char (match-beginning hs-block-start-mdata-select))
(condition-case _ (condition-case _
@@ -897,13 +909,9 @@ If INCLUDE-COMMENTS is non-nil, also search for a comment block."
(funcall hs-find-next-block-function regexp (pos-eol) include-comments) (funcall hs-find-next-block-function regexp (pos-eol) include-comments)
(save-excursion (save-excursion
(goto-char (match-beginning 0)) (goto-char (match-beginning 0))
(pcase-let ((`(,beg ,end) (if (hs-hideable-block-p include-comments)
(or (and include-comments (setq exit (point))
(funcall hs-inside-comment-predicate)) t))))
(hs-block-positions))))
(if (and beg (hs-hideable-region-p beg end))
(setq exit (point))
t)))))
(unless exit (goto-char bk-point)) (unless exit (goto-char bk-point))
exit)) exit))
@@ -930,10 +938,10 @@ Intended to be used in commands."
(goto-char pos) (goto-char pos)
t) t)
((and (or (funcall hs-looking-at-block-start-predicate) ((and (or (hs-hideable-block-p)
(and (forward-line 0) (and (forward-line 0)
(funcall hs-find-block-beginning-function))) (funcall hs-find-block-beginning-function)
(apply #'hs-hideable-region-p (hs-block-positions))) (hs-hideable-block-p))))
t)))) t))))
(defun hs-hide-level-recursive (arg beg end &optional include-comments func progress) (defun hs-hide-level-recursive (arg beg end &optional include-comments func progress)
@@ -1268,7 +1276,7 @@ region (point BOUND)."
Return point, or nil if original point was not in a block." Return point, or nil if original point was not in a block."
(let ((here (point)) done) (let ((here (point)) done)
;; look if current line is block start ;; look if current line is block start
(if (funcall hs-looking-at-block-start-predicate) (if (hs-hideable-block-p)
here here
;; look backward for the start of a block that contains the cursor ;; look backward for the start of a block that contains the cursor
(save-excursion (save-excursion
@@ -1276,8 +1284,8 @@ Return point, or nil if original point was not in a block."
(goto-char (match-beginning 0)) (goto-char (match-beginning 0))
;; go again if in a comment or a string ;; go again if in a comment or a string
(or (save-match-data (nth 8 (syntax-ppss))) (or (save-match-data (nth 8 (syntax-ppss)))
(not (setq done (and (<= here (cadr (hs-block-positions))) (not (setq done (pcase-let ((`(_ ,end) (hs-block-positions)))
(point)))))))) (and end (<= here end) (point)))))))))
(when done (goto-char done))))) (when done (goto-char done)))))
;; This function is not used anymore (Bug#700). ;; This function is not used anymore (Bug#700).
@@ -1478,6 +1486,61 @@ only blocks which are that many levels below the level of point."
(hs-hide-all)) (hs-hide-all))
(setq-local hs--toggle-all-state (not hs--toggle-all-state))) (setq-local hs--toggle-all-state (not hs--toggle-all-state)))
;;;###autoload
(define-minor-mode hs-indentation-mode
"Toggle indentation-based hiding/showing."
:group 'hideshow
(if hs-indentation-mode
(progn
(setq hs-indentation--store-vars
(buffer-local-set-state
hs-forward-sexp-function
(lambda (_)
(let ((size (current-indentation)) end)
(save-match-data
(save-excursion
(forward-line 1) ; Start from next line
(while (and (not (eobp))
(re-search-forward hs-block-start-regexp nil t)
(> (current-indentation) size))
(setq end (point))
(forward-line 1))))
(when end (goto-char end) (end-of-line))))
hs-block-start-regexp (rx (0+ blank) (1+ nonl))
hs-block-end-regexp nil
hs-adjust-block-end-function
;; Adjust line to the "end of the block" (Usually this is
;; the next line after the position by
;; `hs-forward-sexp-function' with the same indentation
;; level as the block start)
(if hs-indentation-respect-end-block
(lambda (beg)
(save-excursion
(when (and (not (eobp))
(forward-line 1)
(not (looking-at-p (rx (0+ blank) eol)))
(= (current-indentation)
(save-excursion
(goto-char beg)
(current-indentation)))
(progn (back-to-indentation)
(not (hs-hideable-block-p))))
(point))))
hs-adjust-block-end-function)
;; Set the other variables to their default values
hs-looking-at-block-start-predicate #'hs-looking-at-block-start-p--default
hs-find-next-block-function #'hs-find-next-block-fn--default
hs-find-block-beginning-function #'hs-find-block-beg-fn--default
hs-c-start-regexp (string-trim-right (regexp-quote comment-start))))
;; Refresh indicators (if needed)
(when (and hs-show-indicators hs-minor-mode)
(hs-minor-mode -1)
(hs-minor-mode +1)))
(buffer-local-restore-state hs-indentation--store-vars)
(when (and hs-show-indicators hs-minor-mode)
(hs-minor-mode -1)
(hs-minor-mode +1))))
;;;###autoload ;;;###autoload
(define-minor-mode hs-minor-mode (define-minor-mode hs-minor-mode
"Minor mode to selectively hide/show code and comment blocks. "Minor mode to selectively hide/show code and comment blocks.

View File

@@ -26,6 +26,7 @@
;; Dependencies for testing: ;; Dependencies for testing:
(require 'cc-mode) (require 'cc-mode)
(require 'sh-script)
(defmacro hideshow-tests-with-temp-buffer (mode contents &rest body) (defmacro hideshow-tests-with-temp-buffer (mode contents &rest body)
@@ -475,6 +476,35 @@ def test1 ():
(beginning-of-line) (beginning-of-line)
(should-not (hs-block-positions))))) (should-not (hs-block-positions)))))
(ert-deftest hideshow-check-indentation-folding ()
"Check indentation-based folding with and without end of the block respected."
(let ((contents "
if [1]
then 2
fi"))
(hideshow-tests-with-temp-buffer
sh-mode
contents
(hs-indentation-mode t)
(hideshow-tests-look-at "if")
(beginning-of-line)
(hs-hide-block)
(should (string=
(hideshow-tests-visible-string)
"
if [1]
fi"))
(hs-show-all)
;; End of the block respected
(hs-indentation-mode nil) ; Reset variables
(setq-local hs-indentation-respect-end-block t)
(hs-indentation-mode t)
(hs-hide-block)
(should (string=
(hideshow-tests-visible-string)
"
if [1]fi")))))
(provide 'hideshow-tests) (provide 'hideshow-tests)
;;; hideshow-tests.el ends here ;;; hideshow-tests.el ends here