Use parser notifier to set parser ranges

This is a continuation from an earlier commit where I added
treesit-parser-changed-ranges and friends.  Dmitry raised an concern
about the edge case where the parser re-parses multiple times before
treesit--pre-redisplay has a chance to run and process changed ranges.

Instead of making treesit-parser-changed-ranges DTRT and become more
complicated, it's agreed that using parser notifier is a better
solution (and treesit-parser-changed-ranges can probably be removed).

So, I took out the code that does the work from treesit--pre-redisplay
and put them into treesit--font-lock-mark-ranges-to-fontify.  This
function will be called on each parser re-parse.  And in
treesit--pre-redisplay, to ensure that treesit--f-l-m-r-to-f gets
called, we force the primary parser to re-parse.

I also added a new variable that major modes need to set,
treesit-primary-parser.  I also added code that makes Emacs guess the
primary parser if that variable isn't set.

Documentation fot treesit-primary-parser will come later.

For futher reference, the message id for the message that prompted
this change is <dc94733b-df75-446c-980e-1c8ea65826cf@gutov.dev>

* lisp/treesit.el (treesit-primary-parser): New variable.
(treesit--font-lock-mark-ranges-to-fontify): New function.
(treesit--guess-primary-parser): New function.
(treesit--pre-redisplay): Extract out.
(treesit--pre-syntax-ppss): Add comments.
(treesit-major-mode-setup): Guess and set treesit-primary-parser; add
treesit--font-lock-mark-ranges-to-fontify as a notifier to the primary
parser.
This commit is contained in:
Yuan Fu 2024-06-02 21:52:48 -07:00
parent b44d511102
commit 760b54de08
No known key found for this signature in database
GPG key ID: 56E19BC57664A442

View file

@ -793,6 +793,18 @@ omitted, default END to BEG."
"Generic tree-sitter font-lock error"
'treesit-error)
;; The primary parser will be access frequently (after each re-parse,
;; before redisplay, etc, see
;; `treesit--font-lock-mark-ranges-to-fontify'), so we don't want to
;; allow it to be a callback function which returns the primary parser
;; (it might be slow). It's not something that needs to be dynamic
;; anyway.
(defvar-local treesit-primary-parser nil
"The primary parser for this buffer.
The primary parser should be a parser that parses the entire buffer, as
opposed to embedded parsers which parses only part of the buffer.")
(defvar-local treesit-font-lock-settings nil
"A list of SETTINGs for treesit-based fontification.
@ -1391,13 +1403,15 @@ Because `pre-redisplay-functions' could be called multiple times
during a single command loop, we use this variable to debounce
calls to `treesit--pre-redisplay'.")
(defun treesit--pre-redisplay (&rest _)
"Force a reparse on the primary parser and do some work.
(defun treesit--font-lock-mark-ranges-to-fontify (ranges _parser)
"A notifier that marks ranges that needs refontification.
For RANGES and PARSER see `treesit-parser-add-notifier'.
After the parser reparses, we get the changed ranges, and
1) update non-primary parsers' ranges in the changed ranges
2) mark these ranges as to-be-fontified,
3) tell syntax-ppss to start reparsing from the min point of the ranges
3) tell syntax-ppss to start reparsing from the min point of the ranges.
We need to mark to-be-fontified ranges before redisplay starts working,
because sometimes the range edited by the user is not the only range
@ -1405,33 +1419,48 @@ that needs to be refontified. For example, when the user types the
final slash of a C block comment /* xxx */, not only do we need to
fontify the slash, but also the whole block comment, which previously
wasn't fontified as comment due to incomplete parse tree."
(dolist (range ranges)
;; 1. Update ranges.
(treesit-update-ranges (car range) (cdr range))
;; 2. Mark the changed ranges to be fontified.
(when treesit--font-lock-verbose
(message "Notifier received range: %s-%s"
(car range) (cdr range)))
(with-silent-modifications
(put-text-property (car range) (cdr range) 'fontified nil))
;; 3. Set `treesit--syntax-propertize-start'.
(if (null treesit--syntax-propertize-start)
(setq treesit--syntax-propertize-start (car range))
(setq treesit--syntax-propertize-start
(min treesit--syntax-propertize-start (car range))))))
(defun treesit--guess-primary-parser ()
"Guess the primary parser of the current buffer and return it.
Normally in a tree-sitter major mode, there is a primary parser that
parses the entire buffer (as opposed to embedded parsers which only
parses part of the buffer). This function tries to find and return that
parser."
(if treesit-range-settings
(let ((query (car (car treesit-range-settings))))
(if (treesit-query-p query)
(treesit-parser-create
(treesit-query-language query))
(car (treesit-parser-list))))
(car (treesit-parser-list))))
(defun treesit--pre-redisplay (&rest _)
"Force a reparse on the primary parser and mark regions to be fontified.
The actual work is carried out by
`treesit--font-lock-mark-ranges-to-fontify', which see."
(unless (eq treesit--pre-redisplay-tick (buffer-chars-modified-tick))
(let ((primary-parser
;; TODO: We need something less ugly than this for getting
;; the primary parser/language.
(if treesit-range-settings
(let ((query (car (car treesit-range-settings))))
(if (treesit-query-p query)
(treesit-parser-create
(treesit-query-language query))
(car (treesit-parser-list))))
(car (treesit-parser-list)))))
;; Force a reparse on the primary parser.
(treesit-parser-root-node primary-parser)
(dolist (range (treesit-parser-changed-ranges primary-parser))
;; 1. Update ranges.
(treesit-update-ranges (car range) (cdr range))
;; 2. Mark the changed ranges to be fontified.
(when treesit--font-lock-verbose
(message "Notifier received range: %s-%s"
(car range) (cdr range)))
(with-silent-modifications
(put-text-property (car range) (cdr range) 'fontified nil))
;; 3. Set `treesit--syntax-propertize-start'.
(if (null treesit--syntax-propertize-start)
(setq treesit--syntax-propertize-start (car range))
(setq treesit--syntax-propertize-start
(min treesit--syntax-propertize-start (car range))))))
(when treesit-primary-parser
;; Force a reparse on the primary parser, if everything is setup
;; correctly, the parser should call
;; `treesit--font-lock-mark-ranges-to-fontify' (which should be a
;; notifier function of the primary parser).
(treesit-parser-root-node treesit-primary-parser))
(setq treesit--pre-redisplay-tick (buffer-chars-modified-tick))))
@ -1445,6 +1474,10 @@ whole region affected by the last reparse.
START and END mark the current to-be-propertized region."
(treesit--pre-redisplay)
;; `treesit--syntax-propertize-start' is set by
;; `treesit--font-lock-mark-ranges-to-fontify', which is called after
;; each re-parser on the primary parser and in
;; `treesit--pre-redisplay'.
(let ((new-start treesit--syntax-propertize-start))
(if (and new-start (< new-start start))
(progn
@ -2991,6 +3024,8 @@ enable tree-sitter navigation commands for them.
Make sure necessary parsers are created for the current buffer
before calling this function."
(unless treesit-primary-parser
(setq treesit-primary-parser (treesit--guess-primary-parser)))
;; Font-lock.
(when treesit-font-lock-settings
;; `font-lock-mode' wouldn't set up properly if
@ -3000,7 +3035,10 @@ before calling this function."
(font-lock-fontify-syntactically-function
. treesit-font-lock-fontify-region)))
(treesit-font-lock-recompute-features)
(add-hook 'pre-redisplay-functions #'treesit--pre-redisplay 0 t))
(add-hook 'pre-redisplay-functions #'treesit--pre-redisplay 0 t)
(when treesit-primary-parser
(treesit-parser-add-notifier
treesit-primary-parser #'treesit--font-lock-mark-ranges-to-fontify)))
;; Syntax
(add-hook 'syntax-propertize-extend-region-functions
#'treesit--pre-syntax-ppss 0 t)