unofficial mirror of emacs-devel@gnu.org 
 help / color / mirror / code / Atom feed
* region-based face-remapping
@ 2024-01-02  0:22 JD Smith
  2024-01-02 13:00 ` Eli Zaretskii
  0 siblings, 1 reply; 37+ messages in thread
From: JD Smith @ 2024-01-02  0:22 UTC (permalink / raw)
  To: emacs-devel


Has there been any discussion of implementing region-based face remapping, perhaps by adding 'face-remap as a property to overlays?  This would be very useful for modes in which local syntax highlighting depends on point, now broadly possible thanks to treesitter (think “brighten surrounding semantic unit” or similar).


^ permalink raw reply	[flat|nested] 37+ messages in thread
* Re: region-based face-remapping
@ 2024-01-08 21:49 JD Smith
  2024-01-09 13:03 ` Eli Zaretskii
  0 siblings, 1 reply; 37+ messages in thread
From: JD Smith @ 2024-01-08 21:49 UTC (permalink / raw)
  To: Eli Zaretskii; +Cc: emacs-devel

[-- Attachment #1: Type: text/plain, Size: 7950 bytes --]



> 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.

[-- Attachment #2: Type: text/html, Size: 13556 bytes --]

^ permalink raw reply	[flat|nested] 37+ messages in thread

end of thread, other threads:[~2024-01-15 20:36 UTC | newest]

Thread overview: 37+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2024-01-02  0:22 region-based face-remapping JD Smith
2024-01-02 13:00 ` Eli Zaretskii
2024-01-02 15:49   ` JD Smith
2024-01-03 12:31     ` Eli Zaretskii
2024-01-03 12:40       ` Dmitry Gutov
2024-01-03 13:42         ` Eli Zaretskii
2024-01-04  0:07           ` Dmitry Gutov
2024-01-04  7:05             ` Eli Zaretskii
2024-01-05  3:49               ` Dmitry Gutov
2024-01-05  8:50                 ` Eli Zaretskii
2024-01-05 14:18                   ` Dmitry Gutov
2024-01-05 14:34                     ` Eli Zaretskii
2024-01-05 16:25                       ` Dmitry Gutov
2024-01-03 23:15       ` JD Smith
2024-01-04  6:58         ` Eli Zaretskii
2024-01-05  0:51           ` JD Smith
2024-01-05  8:19             ` Eli Zaretskii
2024-01-05 16:35               ` Dmitry Gutov
2024-01-06 14:04                 ` JD Smith
2024-01-06 13:53               ` JD Smith
2024-01-06 14:27                 ` Eli Zaretskii
2024-01-06 14:56                   ` JD Smith
2024-01-08 17:28                     ` Eli Zaretskii
2024-01-07  3:41                 ` Dmitry Gutov
2024-01-15 19:55     ` Stefan Monnier via Emacs development discussions.
2024-01-15 20:19       ` Eli Zaretskii
2024-01-15 20:25         ` Eli Zaretskii
2024-01-15 20:36         ` Stefan Monnier
  -- strict thread matches above, loose matches on Subject: below --
2024-01-08 21:49 JD Smith
2024-01-09 13:03 ` Eli Zaretskii
2024-01-09 14:15   ` Stefan Monnier
2024-01-09 20:20     ` JD Smith
2024-01-10 12:36       ` Eli Zaretskii
2024-01-09 20:20     ` JD Smith
2024-01-15 20:17       ` Stefan Monnier via Emacs development discussions.
2024-01-09 21:31   ` JD Smith
2024-01-10 12:44     ` Eli Zaretskii

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).