Subject: Re: bug#79294: [PATCH] Add hideable indicators for hideshow.

From: Eli Zaretskii <[email-masked]>
Date: Sat, 23 Aug 2025 11:00:37 +0300

> From: Elijah Gabe Pérez <[email-masked]>
> Date: Fri, 22 Aug 2025 22:56:33 -0600
> 
> This patch adds indicators to toggle source block visibility (as shown
> in the emacs-devel thread).
> 
> Currently the implementation makes scrolling a bit slow, due to the
> computation required to get the hideable blocks.

How slow?  Can you post some measurements, comparing the scroll times
with and without the indicators?

> +@vindex hs-indicators-type

I think this variable should be named "hs-indicator-type", in
singular.

> +@item hs-show-indicators
> +This variable specifies if hideshow should display indicators to toggle
> +the block hiding.

This sentence reads awkward ("indicators to toggle") because you
conflated two separate features together.  I would leave them
separate:

  This variable controls whether Hideshow mode should display
  indicators of hidden and shown blocks.  The indicators also allow
  toggling the hide/show state of each block.

>                     If this is set to non-@code{nil}, the indicators are
> +enabled except in comments, otherwise, if set to
> +@code{include-comments}, the indicators will be displayed also in
> +comments.

It is better to design this the other way around:

  If the value is @code{t}, the indicators are enabled.  The special
  value @code{exclude-comments} enables the indicators in all blocks
  except comment blocks.  The default is @code{nil}, which disables
  the indicators.

Note that this also documents the default value.

> +@item hs-indicactors-type
> +This variable specifies which indicator type to use.  Its value should
> +be either @code{left-fringe} (display the indicators in the left
> +fringe), @code{right-fringe} (display the indicators in the right
> +fringe), @code{left-margin} (display the indicators in the left margin),
> +@code{right-margin} (display the indicators in the right margin), or
> +@code{nil} (display a string as indicator after the block beginning).
> +If @code{left-fringe} or @code{right-fringe} are selected and current
> +Emacs session does not support fringes, margins will be used instead.

This is too detailed for such a minor feature.  I would simply say

  This variable controls where to show the indicators, if they are
  enabled.  You can show them on the fringe (@pxref{Windows Fringes})
  or in the window's margin.  The default is to use the fringe if it's
  available, otherwise to use the margin.

The other details are clear from the variable's doc string, and need
not be repeated here.

Note that I deliberately omitted the left/right part, because I think
your current design is wrong in this area, see below.

> ++++
>  *** New user option 'hs-display-lines-hidden'.
>  If this option is non-nil, Hideshow displays the number of hidden
>  lines next to the ellipsis.

Please tell the default value.

> ++++
> +*** New user option 'hs-show-indicators'.
> +This user option determines if hideshow should display indicators to
> +toggle the block hiding. If non-nil, the indicators are enabled except
> +in comments.           ^^

Two spaces between sentences.  In addition "in comments" is
problematic here, as the indicators are not part of text.  Did you
mean "for comment blocks" or something?

> +The new icons 'hs-indicator-show' and 'hs-indicator-hide', can be used
> +for customize the indicators appearance, only if 'hs-indicators-type' is
   ^^^^^^^^^^^^^
Either "to customize" or "for customizing".

> +set to 'left-margin', 'right-margin' or nil.
> +
>  ** C-ts mode
>  
>  +++
> diff --git a/lisp/progmodes/hideshow.el b/lisp/progmodes/hideshow.el
> index 445bdeeb7a7..4407cb78b94 100644
> --- a/lisp/progmodes/hideshow.el
> +++ b/lisp/progmodes/hideshow.el
> @@ -220,6 +220,9 @@
>  
>  ;;; Code:
>  (require 'mule-util) ; For `truncate-string-ellipsis'
> +;; For indicators
> +(require 'icons)
> +(require 'fringe)
>  
>  ;;---------------------------------------------------------------------------
>  ;; user-configurable variables
> @@ -236,6 +239,16 @@ hs-ellipsis
>  use that face for the ellipsis instead."
>    :version "31.1")
>  
> +(defface hs-indicator-hide
> +  '((t :inherit (shadow default)))
> +  "Face used in hideshow indicator to hide current block."

Not "to show", but "to indicate a shown block".

> +  :version "31.1")
> +
> +(defface hs-indicator-show
> +  '((t :inherit hs-indicator-hide :weight bold))
> +  "Face used in hideshow indicator to show current block."

Same here.

> +(defcustom hs-show-indicators nil
> +  "Whether hideshow should display block indicators.

I'd say

  Whether hideshow should display block hide/show indicators.

> +If non-nil, hideshow will display indicators for toggle the
> +visibility of a block.                       ^^^^^^^^^^

"for toggling"

> +If set to `include-comments', the indicators are displayed also in
> +comments.

See the comment above about "in comments".  Also, I would design the
values the other way around: the value t shows indicators everywhere,
the value exclude-comments excludes comment blocks.

> +(defcustom hs-indicators-type 'left-fringe
> +  "Indicate which indicator type to use for the block indicators.
> +
> +The possible values can be:
> +
> + - `left-fringe', display the indicators in the left fringe.
> + - `right-fringe', display the indicators in the right fringe.
> +
> + - `left-margin', display the indicators in the left margin.
> + - `right-margin', display the indicators in the right margin.
> +
> + - nil, display a string as indicator after the block beginning.
> +
> +If `left-fringe' or `right-fringe' are selected and fringes are not
> +supported, margins will be used instead.
> +
> +This only have effect if `hs-show-indicators' is non-nil."
> +  :type '(choice
> +          (const :tag "Left fringe" left-fringe)
> +          (const :tag "Right fringe" right-fringe)
> +          (const :tag "Left margin" left-margin)
> +          (const :tag "Right margin" right-margin)
> +          (const :tag "String after block beginning." nil))
> +  :version "31.1")

I think this is sub-optimal, if not fundamentally wrong.  In a
left-to-right text (which is an overwhelmingly the usual case), I
don't expect users to want to show the indicators on the right.  OTOH,
when the text is right-to-left, the indicators should be on the right.
Also, when showing the "show" indicator on the right, the direction of
the arrow should be reversed: it should point to the left, not to the
right.

So I think we should allow just two values: 'fringe' and 'margin', and
determine the left/right part from the actual text directionality.  In
the remote possibility that we would like to allow the indicators "on
the opposite part" of the display (but why? it sounds like unnecessary
feature to me), we could have values like 'opposite-fringe' etc.  The
main point is to avoid "left" and "right" when we choose which one to
use dynamically.

> +(define-icon hs-indicator-hide nil
> +  `((image "outline-open.svg" "outline-open.pbm"
> +           :face hs-indicator-hide
> +           :height (0.6 . em)
> +           :ascent center)
> +    (symbol "🞃" "▶")
> +    (text "-"))
> +  "Icon used for hide block at point.
> +This is only used if `hs-indicators-type' is set to `left-margin',
> +`right-margin' or nil."
> +  :version "31.1")
> +
> +(define-icon hs-indicator-show nil
> +  `((image "outline-close.svg" "outline-close.pbm"
> +           :face hs-indicator-show
> +           :height (0.6 . em)
> +           :ascent center)
> +    (symbol "▸" "▶")
> +    (text "+"))
> +  "Icon used for show block at point.

The image and symbol parts of the "show" indicators should support the
right margin with an arrow pointing to the left, so we'd need
additional images for that.

Also, the "▶" symbol is the same for both hide and show indicators?
Is this intentional?

> +(defvar hs--indicators-rx
> +  `((hs-discard-indicator-overlays)
> +    (,(lambda (bound)
> +        (funcall hs-find-next-block-func
> +                 hs-block-start-regexp
> +                 bound
> +                 (unless (eq hs-show-indicators 'exclude-comments)
> +                   t)))
> +     0 (hs--add-indicators)))
> +  "Hideshow indicator regexp for `font-lock-keywords'.")

Why do we need the value in a defvar, instead of invoking the function
directly?

> +              p q)
> +          ;; `p' is the point at the end of the block beginning, which
> +          ;; may need to be adjusted
> +          (save-excursion
> +            (goto-char (funcall (or hs-adjust-block-beginning #'identity)
> +                                header-end))
> +            (setq p (line-end-position)))
> +          ;; `q' is the point at the end of the block

Can you name these variables by more meaningful names, like
pt-at-block-beg and pt-at-block-end?

> +        (when hs-show-indicators
> +          (when-let* ((type
> +                       (if (not (display-graphic-p))
> +                           'left-margin hs-indicators-type)))
> +            (setq-local
> +             hs-indicators-type
> +             (cond
> +              ((not (eq (current-bidi-paragraph-direction) 'right-to-left)) type)
> +              ((eq hs-indicators-type 'left-fringe) 'right-fringe)
> +              ((eq hs-indicators-type 'left-margin) 'right-margin)
> +              (t hs-indicators-type))))
> +
> +          (font-lock-add-keywords nil hs--indicators-rx)

I think the fact that you modify overlays inside font-lock is the main
reason for this being slow: it requires an immediate second redisplay
cycle after a new window-full was displayed.  It's the same problem as
the one which made linenum-mode so slow.  Did you try using
pre-redisplay-function(s) for this purpose instead?