emacs-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: region-based face-remapping


From: JD Smith
Subject: Re: region-based face-remapping
Date: Mon, 8 Jan 2024 16:49:21 -0500



On Jan 8, 2024, at 12:28 PM, Eli Zaretskii <eliz@gnu.org> wrote:

To make it more concrete, what I had in mind is an update to indent-bars which would changes the
appearance of the set of bars in a “scope” region via treesitter queries in a post-command hook.  As
point changes, the TS “enclosing scope” is calculated, and if it has changed, all the existing indent
bars in that region would be updated with “alternate” styling (and formerly highlighted text would be
returned to normal styling).  See [1] for some images to give you the idea of how the normal styling
can look.  Important to note are that:

Why cannot this be done by modifying faces or overlays in the affected
region?

It can, but the region where such face modification is needed could be arbitrarily large (including the entire buffer), which makes this more like a font-lock-problem in a PCH or via a timer (with the bonus that it could occur either on modification or just point change).  So at worst it would be like re-font-locking the entire buffer (many thousands of locations, most just 1 character wide) on every key press (or rapidly with a timer).  See below for an attempted “better” approach.

Btw, my advice is to use an idle timer, not post-command-hook, if that
is possible.  A timer-based implementation will not slow down Emacs
when the user moves point quickly or scrolls through a portion of the
buffer by leaning on an arrow key.  Also, the timer will always run
with point corresponding to what is on the screen, whereas
post-command-hook runs before point adjustment, so could use
inaccurate value of point

For sure.  I already by default use a delay timer via a PCH to avoid updating the current-depth (single bar) highlight too quickly (though not an idle timer: instead I schedule the update say 75ms in the future, and push it forward another 75ms when new commands come in before the timer fires — what I call a “kick the can” timer [1, for aside on timers]).  As I briefly mentioned before, face-remapping-alist on my system is actually performant enough to do this as fast as users can fire commands (e.g. ~10ms for smooth scrolling).  That speed and my subsequent inference about what the display engine might be able to do in this space is what motivated my notion of a ‘face-remap property.

What I’m struggling with is how to do something “like font lock” — i.e. refontify some potentially substantial fraction of all the faces in a buffer, not (just) on modifications in an after-change-hook, but also on point-dependent “region of interest” changes, with a priority given to the displayed region in the window.  IMO, tree-sitter will make this kind of idea more common.

I’ve thought of simply performing the highlight update operation on the intersection between the treesitter region-of-interest and the window-start/end region, maybe with 20% space padding on either side of the window.  And then, when new regions are indicated, highlight the (new-old) difference, and unhighlight the (old-new) difference (leaving their union unchanged).  Something like this (in pseudo-code):

(defvar-local old-roi nil)
(let* ((ts-roi (some-treesitter-command)) ; a (start . end) "region of interest"
       (ws (window-start)) (we (window-end))
       (space (round (/ (- we ws) 5)))
       regs)
  ;; Add 20% padding to window region
  (setq ws (max (point-min) (- ws space))
we (min (point-max) (+ we space)))
  (setq ts-roi (region-intersection ts-roi (cons ws we))) ;find intersection range, possibly nil
  (if (null ts-roi)
      (do-unhighlight old-roi)
    (if old-roi
(progn
 ;; newly highlighted
 (setq regs (region-differences ts-roi old-roi))
 (while regs
   (do-highlight (car regs))
   (setq regs (cdr regs)))
 ;; newly unhighlighted
 (setq regs (region-differences old-roi ts-roi))
 (while regs
   (do-unhighlight (car regs))
   (setq regs (cdr regs))))
      (do-highlight ts-roi)))
  (setq old-roi ts-roi))

A bit fiddly, but not terrible.  Would be better to see if point has changed, and if not, use a cached ts-roi.

For the highlighting/unhighlighting operation, I think I also mentioned that the faces of interest can and will live in display properties, so you’d need to check all ‘display strings within the (un-)highlit region too, and rebuild those with the updated faces.

But suppose the change of region was precipitated by the removal or addition of text in the buffer, not just point movement?  Now your old region (old-roi, above) is outdated, likely wrong, and possibly had holes put in it by the edits.

So instead of saving old-roi in a variable, reach for a marker text property, call it 'indent-bars-ts-highlighted (rear-nonsticky of course).  Then, when the region of interest has changed, find (via `next-single-property-changes’) all changes to this marker property across the entire buffer (hopefully fast enough?).   Intersect this (possibly disjoint set of) old region(s) against the new (window-region + ts-roi) intersection, and (de-)highlight as before.

OK, getting there, but what about invisible text?  What if the window includes a huge range within a large buffer, most of it hidden, and your ts-roi is large (like the whole buffer)?  There’s no point doing the (un-)highlighting operation on invisible text.  But you can’t just skip over invisible text, it better have its marker property removed, and the act of hiding/unhiding anything is now cause for recalculating everything.

You can see how tangled it can get, compared to the simplicity (for the elisp programmer) of setting a ‘face-remap overlay and moving it around (i.e. similar to updating the class of a div in HTML), and letting the display engine sort it out.

So I think it’s possible, but it’s also painful, and I’d hazard to guess most package authors wouldn’t go to such lengths.

It could be a buffer-local variable, which defines the size of the
region around point where the faces should change their appearance,
and how to change the appearance.  The display engine then could take
that into consideration when processing buffer positions around point.

Whether this makes sense depends on the applications you have in mind.

Since there are many small stretches of text (single character stretches) that would be impacted over
a larger region, I’m afraid such a simple approach wouldn’t work.

If all you need is change the faces, I think it will work.

Maybe here you mean something like “within the window region, updating as that changes”, similar to what I outlined above?

I understand.  The question is whether it would be desirable, tractable, performant, and maintainable
to add any such infrastructure.

I don't know.  I do know it will not be simple.

OK, fair enough.  Possibly it’s worth pressing on this to see what I can do with current capabilities then.  Please let me know if I’m missing any obvious strategies that would simplify the mess above.

Thanks for your thoughts.

[1]  Short aside on timers: do you think an idle timer that repeatedly runs every 75ms of idle time and asks “did point change?” then, if so, “did TS region of interest change?” would be preferable to a post command hook that kicks a timer to do the same?  I already use `timer-set-time' to avoid rapidly reallocating a timer.  I’d guess these two approaches are ~equivalent performance-wise, but PCH’s can be buffer-local and idle-timers can’t so they are always running.

Aside within aside: it would be great if `timer-activate' included an optional no-error argument so you don’t have to check if it is on `timer-list’ twice.  I.e. if a timer is already on timer-list and `timer-activate’ (with no-error) is called on it, do nothing.

reply via email to

[Prev in Thread] Current Thread [Next in Thread]