diff options
| author | Yuan Fu | 2022-11-02 16:31:25 -0700 |
|---|---|---|
| committer | Yuan Fu | 2022-11-02 16:47:53 -0700 |
| commit | f331be1f074d68e7e5cdbac324419e07c186492a (patch) | |
| tree | eb2d16d70a9aa23ab3e5b2a083db4b59fefa780e | |
| parent | 040991a4697b50ebcb54e498e7de54b8d0885101 (diff) | |
| download | emacs-f331be1f074d68e7e5cdbac324419e07c186492a.tar.gz emacs-f331be1f074d68e7e5cdbac324419e07c186492a.zip | |
Add handling of contextual entities in tree-sitter font-lock
* lisp/progmodes/python.el: Remove function.
(python--treesit-settings): Capture contextual node.
* lisp/treesit.el (treesit--set-nonsticky):
(treesit-font-lock-contextual-post-process): New functions.
(treesit-font-lock-fontify-region): Change local variable START and
END to NODE-START and NODE-END, handle special capture name
"contextual".
* doc/lispref/modes.texi (Parser-based Font Lock): Update manual.
| -rw-r--r-- | doc/lispref/modes.texi | 20 | ||||
| -rw-r--r-- | lisp/progmodes/python.el | 10 | ||||
| -rw-r--r-- | lisp/treesit.el | 88 |
3 files changed, 107 insertions, 11 deletions
diff --git a/doc/lispref/modes.texi b/doc/lispref/modes.texi index 3c91893bbfd..c6f848ffb23 100644 --- a/doc/lispref/modes.texi +++ b/doc/lispref/modes.texi | |||
| @@ -3992,6 +3992,26 @@ priority. If a capture name is neither a face nor a function, it is | |||
| 3992 | ignored. | 3992 | ignored. |
| 3993 | @end defun | 3993 | @end defun |
| 3994 | 3994 | ||
| 3995 | Contextual entities, like multi-line strings, or @code{/* */} style | ||
| 3996 | comments, need special care, because change in these entities might | ||
| 3997 | cause change in a large portion of the buffer. For example, inserting | ||
| 3998 | the closing comment delimiter @code{*/} will change all the text | ||
| 3999 | between it and the opening delimiter to comment face. Such entities | ||
| 4000 | should be captured in a special name @code{contextual}, so Emacs can | ||
| 4001 | correctly update their fontification. Here is an example for | ||
| 4002 | comments: | ||
| 4003 | |||
| 4004 | @example | ||
| 4005 | @group | ||
| 4006 | (treesit-font-lock-rules | ||
| 4007 | :language 'javascript | ||
| 4008 | :feature 'comment | ||
| 4009 | :override t | ||
| 4010 | '((comment) @@font-lock-comment-face) | ||
| 4011 | (comment) @@contextual)) | ||
| 4012 | @end group | ||
| 4013 | @end example | ||
| 4014 | |||
| 3995 | @defvar treesit-font-lock-feature-list | 4015 | @defvar treesit-font-lock-feature-list |
| 3996 | This is a list of lists of feature symbols. Each element of the list | 4016 | This is a list of lists of feature symbols. Each element of the list |
| 3997 | is a list that represents a decoration level. | 4017 | is a list that represents a decoration level. |
diff --git a/lisp/progmodes/python.el b/lisp/progmodes/python.el index 46559db2cd1..603cdb14e15 100644 --- a/lisp/progmodes/python.el +++ b/lisp/progmodes/python.el | |||
| @@ -1035,12 +1035,6 @@ f for f-strings. OVERRIDE is the override flag described in | |||
| 1035 | (cl-incf string-beg)) | 1035 | (cl-incf string-beg)) |
| 1036 | (treesit-fontify-with-override string-beg string-end face override))) | 1036 | (treesit-fontify-with-override string-beg string-end face override))) |
| 1037 | 1037 | ||
| 1038 | (defun python--treesit-fontify-string-end (node &rest _) | ||
| 1039 | "Mark the whole string as to-be-fontified. | ||
| 1040 | NODE is the ending quote of a string." | ||
| 1041 | (let ((string (treesit-node-parent node))) | ||
| 1042 | (setq jit-lock-context-unfontify-pos (treesit-node-start string)))) | ||
| 1043 | |||
| 1044 | (defvar python--treesit-settings | 1038 | (defvar python--treesit-settings |
| 1045 | (treesit-font-lock-rules | 1039 | (treesit-font-lock-rules |
| 1046 | :feature 'comment | 1040 | :feature 'comment |
| @@ -1051,8 +1045,8 @@ NODE is the ending quote of a string." | |||
| 1051 | :language 'python | 1045 | :language 'python |
| 1052 | :override t | 1046 | :override t |
| 1053 | ;; TODO Document on why we do this. | 1047 | ;; TODO Document on why we do this. |
| 1054 | '((string "\"" @python--treesit-fontify-string-end :anchor) | 1048 | '((string :anchor "\"" @python--treesit-fontify-string) |
| 1055 | (string :anchor "\"" @python--treesit-fontify-string :anchor)) | 1049 | (string) @contextual) |
| 1056 | 1050 | ||
| 1057 | :feature 'string-interpolation | 1051 | :feature 'string-interpolation |
| 1058 | :language 'python | 1052 | :language 'python |
diff --git a/lisp/treesit.el b/lisp/treesit.el index 248c23bf888..6a7ba87e836 100644 --- a/lisp/treesit.el +++ b/lisp/treesit.el | |||
| @@ -636,6 +636,82 @@ See `treesit-font-lock-rules' for their semantic." | |||
| 636 | "Unrecognized value of :override option" | 636 | "Unrecognized value of :override option" |
| 637 | override))))) | 637 | override))))) |
| 638 | 638 | ||
| 639 | (defun treesit--set-nonsticky (start end sym &optional remove) | ||
| 640 | "Set `rear-nonsticky' property between START and END. | ||
| 641 | Set the proeprty to a list containing SYM. If there is already a | ||
| 642 | list, add SYM to that list. If REMOVE is non-nil, remove SYM | ||
| 643 | instead." | ||
| 644 | (let* ((prop (get-text-property start 'rear-nonsticky)) | ||
| 645 | (new-prop | ||
| 646 | (pcase prop | ||
| 647 | ((pred listp) ; PROP is a list or nil. | ||
| 648 | (if remove | ||
| 649 | (remove sym prop) | ||
| 650 | ;; We should make sure PORP doesn't contain SYM, but | ||
| 651 | ;; whatever. | ||
| 652 | (cons sym prop))) | ||
| 653 | ;; PROP is t. | ||
| 654 | (_ (if remove | ||
| 655 | nil | ||
| 656 | (list sym)))))) | ||
| 657 | (if (null new-prop) | ||
| 658 | (remove-text-properties start end '(rear-nonsticky nil)) | ||
| 659 | (put-text-property start end 'rear-nonsticky new-prop)))) | ||
| 660 | |||
| 661 | ;; This post-processing tries to deal with the following scenario: | ||
| 662 | ;; User inserts "/*", then go down the buffer and inserts "*/". | ||
| 663 | ;; Before the user inserts "*/", tree-sitter cannot construct a | ||
| 664 | ;; comment node and the parse tree is incomplete, and we can't fontify | ||
| 665 | ;; the comment. But once the user inserts the "*/", the parse-tree is | ||
| 666 | ;; complete and we want to refontify the whole comment, and possibly | ||
| 667 | ;; text after comment (the "/*" could damage the parse tree enough | ||
| 668 | ;; that makes tree-sitter unable to produce reasonable information for | ||
| 669 | ;; text after it). | ||
| 670 | ;; | ||
| 671 | ;; So we set jit-lock-context-unfontify-pos to comment start, and | ||
| 672 | ;; jit-lock-context will refontify text after that position in a | ||
| 673 | ;; timer. Refontifying those text will end up calling this function | ||
| 674 | ;; again, and we don't want to fall into infinite recursion. So we | ||
| 675 | ;; mark the end of the comment with a text property, so we can | ||
| 676 | ;; distinguish between initial and follow up invocation of this | ||
| 677 | ;; function. | ||
| 678 | (defun treesit-font-lock-contextual-post-process | ||
| 679 | (node start end &optional verbose) | ||
| 680 | "Post-processing for contextual syntax nodes. | ||
| 681 | NODE is a comment or string node, START and END are the region | ||
| 682 | being fontified. | ||
| 683 | |||
| 684 | If VERBOSE is non-nil, print debugging information." | ||
| 685 | (let* ((node-start (treesit-node-start node)) | ||
| 686 | (node-end (treesit-node-end node)) | ||
| 687 | (node-end-1 (max (point-min) (1- node-end))) | ||
| 688 | (prop-sym 'treesit-context-refontify-in-progress)) | ||
| 689 | (when verbose | ||
| 690 | (message "Contextual: region: %s-%s, node: %s-%s" | ||
| 691 | start end node-start node-end)) | ||
| 692 | (when (<= node-end end) | ||
| 693 | (if (get-text-property node-end-1 prop-sym) | ||
| 694 | ;; We are called from a refontification by jit-lock-context, | ||
| 695 | ;; caused by a previous call to this function. | ||
| 696 | (progn (when verbose | ||
| 697 | (message "Contextual: in progress")) | ||
| 698 | (remove-text-properties | ||
| 699 | node-end-1 node-end `(,prop-sym nil)) | ||
| 700 | (treesit--set-nonsticky node-end-1 node-end prop-sym t)) | ||
| 701 | ;; We are called from a normal fontification. | ||
| 702 | (when verbose | ||
| 703 | (message "Contextual: initial")) | ||
| 704 | (setq jit-lock-context-unfontify-pos node-start) | ||
| 705 | (put-text-property node-end-1 node-end prop-sym t) | ||
| 706 | (treesit--set-nonsticky node-end-1 node-end prop-sym))))) | ||
| 707 | |||
| 708 | ;; Some details worth explaining: | ||
| 709 | ;; | ||
| 710 | ;; 1. When we apply face to a node, we clip the face into the | ||
| 711 | ;; currently fontifying region, this way we don't overwrite faces | ||
| 712 | ;; applied by regexp-based font-lock. The clipped part will be | ||
| 713 | ;; fontified fine when Emacs fontifies the region containing it. | ||
| 714 | ;; | ||
| 639 | (defun treesit-font-lock-fontify-region | 715 | (defun treesit-font-lock-fontify-region |
| 640 | (start end &optional loudly) | 716 | (start end &optional loudly) |
| 641 | "Fontify the region between START and END. | 717 | "Fontify the region between START and END. |
| @@ -666,11 +742,17 @@ If LOUDLY is non-nil, display some debugging information." | |||
| 666 | (dolist (capture captures) | 742 | (dolist (capture captures) |
| 667 | (let* ((face (car capture)) | 743 | (let* ((face (car capture)) |
| 668 | (node (cdr capture)) | 744 | (node (cdr capture)) |
| 669 | (start (treesit-node-start node)) | 745 | (node-start (treesit-node-start node)) |
| 670 | (end (treesit-node-end node))) | 746 | (node-end (treesit-node-end node))) |
| 671 | (cond | 747 | (cond |
| 748 | ((eq face 'contextual) | ||
| 749 | (treesit-font-lock-contextual-post-process | ||
| 750 | node start end | ||
| 751 | (or loudly treesit--font-lock-verbose))) | ||
| 672 | ((facep face) | 752 | ((facep face) |
| 673 | (treesit-fontify-with-override start end face override)) | 753 | (treesit-fontify-with-override |
| 754 | (max node-start start) (min node-end end) | ||
| 755 | face override)) | ||
| 674 | ((functionp face) | 756 | ((functionp face) |
| 675 | (funcall face node override))) | 757 | (funcall face node override))) |
| 676 | ;; Don't raise an error if FACE is neither a face nor | 758 | ;; Don't raise an error if FACE is neither a face nor |