;;; lsp-semantic-tokens.el --- Semantic tokens -*- lexical-binding: t; -*- ;; ;; Copyright (C) 2020 emacs-lsp maintainers ;; ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;; ;;; Commentary: ;; ;; Semantic tokens ;; https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens ;; ;;; Code: (require 'lsp-mode) (require 'dash) (defgroup lsp-semantic-tokens nil "LSP support for semantic-tokens." :prefix "lsp-semantic-tokens-" :group 'lsp-mode :tag "LSP Semantic tokens") (define-obsolete-variable-alias 'lsp-semantic-highlighting-warn-on-missing-face 'lsp-semantic-tokens-warn-on-missing-face "lsp-mode 8.0.0") (defcustom lsp-semantic-tokens-warn-on-missing-face nil "Warning on missing face for token type/modifier. When non-nil, this option will emit a warning any time a token or modifier type returned by a language server has no face associated with it." :group 'lsp-semantic-tokens :type 'boolean) (defcustom lsp-semantic-tokens-apply-modifiers t "Whether semantic tokens should take token modifiers into account." :group 'lsp-semantic-tokens :type 'boolean) (defcustom lsp-semantic-tokens-allow-ranged-requests t "Whether to use ranged semantic token requests when available. Note that even when this is set to t, delta requests will be preferred whenever possible, unless `lsp-semantic-tokens-allow-delta-requests' is false." :group 'lsp-semantic-tokens :type 'boolean) (defcustom lsp-semantic-tokens-allow-delta-requests t "Whether to use semantic token delta requests when available. When supported by the language server, delta requests are always preferred over both full and ranged token requests." :group 'lsp-semantic-tokens :type 'boolean) (defcustom lsp-semantic-tokens-honor-refresh-requests nil "Whether to honor semanticTokens/refresh requests. When set to nil, refresh requests will be silently discarded. When set to t, semantic tokens will be re-requested for all buffers associated with the requesting language server." :group 'lsp-semantic-tokens :type 'boolean) (defcustom lsp-semantic-tokens-enable-multiline-token-support t "When set to nil, tokens will be truncated after end-of-line." :group 'lsp-semantic-tokens :type 'boolean) (defface lsp-face-semhl-constant '((t :inherit font-lock-constant-face)) "Face used for semantic highlighting scopes matching constant scopes." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-variable '((t :inherit font-lock-variable-name-face)) "Face used for semantic highlighting scopes matching variable.*. Unless overridden by a more specific face association." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-function '((t :inherit font-lock-function-name-face)) "Face used for semantic highlighting scopes matching entity.name.function.*. Unless overridden by a more specific face association." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-method '((t :inherit lsp-face-semhl-function)) "Face used for semantic highlighting scopes matching entity.name.method.*. Unless overridden by a more specific face association." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-namespace '((t :inherit font-lock-type-face :weight bold)) "Face used for semantic highlighting scopes matching entity.name.namespace.*. Unless overridden by a more specific face association." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-comment '((t (:inherit font-lock-comment-face))) "Face used for comments." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-keyword '((t (:inherit font-lock-keyword-face))) "Face used for keywords." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-string '((t (:inherit font-lock-string-face))) "Face used for keywords." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-number '((t (:inherit font-lock-constant-face))) "Face used for numbers." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-regexp '((t (:inherit font-lock-string-face :slant italic))) "Face used for regexps." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-operator '((t (:inherit font-lock-function-name-face))) "Face used for operators." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-namespace '((t (:inherit font-lock-keyword-face))) "Face used for namespaces." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-type '((t (:inherit font-lock-type-face))) "Face used for types." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-struct '((t (:inherit font-lock-type-face))) "Face used for structs." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-class '((t (:inherit font-lock-type-face))) "Face used for classes." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-interface '((t (:inherit font-lock-type-face))) "Face used for interfaces." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-enum '((t (:inherit font-lock-type-face))) "Face used for enums." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-type-parameter '((t (:inherit font-lock-type-face))) "Face used for type parameters." :group 'lsp-semantic-tokens) ;; function face already defined, move here when support ;; for theia highlighting gets removed (defface lsp-face-semhl-member '((t (:inherit font-lock-variable-name-face))) "Face used for members." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-property '((t (:inherit font-lock-variable-name-face))) "Face used for properties." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-event '((t (:inherit font-lock-variable-name-face))) "Face used for event properties." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-macro '((t (:inherit font-lock-preprocessor-face))) "Face used for macros." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-variable '((t (:inherit font-lock-variable-name-face))) "Face used for variables." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-parameter '((t (:inherit font-lock-variable-name-face))) "Face used for parameters." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-label '((t (:inherit font-lock-comment-face))) "Face used for labels." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-deprecated '((t :strike-through t)) "Face used for semantic highlighting scopes matching constant scopes." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-definition '((t :inherit font-lock-function-name-face :weight bold)) "Face used for definition modifier." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-implementation '((t :inherit font-lock-function-name-face :weight bold)) "Face used for implementation modifier." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-default-library '((t :inherit font-lock-builtin-face)) "Face used for defaultLibrary modifier." :group 'lsp-semantic-tokens) (defface lsp-face-semhl-static '((t :inherit font-lock-keyword-face)) "Face used for static modifier." :group 'lsp-semantic-tokens) (defvar-local lsp-semantic-token-faces '(("comment" . lsp-face-semhl-comment) ("keyword" . lsp-face-semhl-keyword) ("string" . lsp-face-semhl-string) ("number" . lsp-face-semhl-number) ("regexp" . lsp-face-semhl-regexp) ("operator" . lsp-face-semhl-operator) ("namespace" . lsp-face-semhl-namespace) ("type" . lsp-face-semhl-type) ("struct" . lsp-face-semhl-struct) ("class" . lsp-face-semhl-class) ("interface" . lsp-face-semhl-interface) ("enum" . lsp-face-semhl-enum) ("typeParameter" . lsp-face-semhl-type-parameter) ("function" . lsp-face-semhl-function) ("method" . lsp-face-semhl-method) ("member" . lsp-face-semhl-member) ("property" . lsp-face-semhl-property) ("event" . lsp-face-semhl-event) ("macro" . lsp-face-semhl-macro) ("variable" . lsp-face-semhl-variable) ("parameter" . lsp-face-semhl-parameter) ("label" . lsp-face-semhl-label) ("enumConstant" . lsp-face-semhl-constant) ("enumMember" . lsp-face-semhl-constant) ("dependent" . lsp-face-semhl-type) ("concept" . lsp-face-semhl-interface)) "Faces to use for semantic tokens.") (defvar-local lsp-semantic-token-modifier-faces '(("declaration" . lsp-face-semhl-interface) ("definition" . lsp-face-semhl-definition) ("implementation" . lsp-face-semhl-implementation) ("readonly" . lsp-face-semhl-constant) ("static" . lsp-face-semhl-static) ("deprecated" . lsp-face-semhl-deprecated) ("abstract" . lsp-face-semhl-keyword) ("async" . lsp-face-semhl-macro) ("modification" . lsp-face-semhl-operator) ("documentation" . lsp-face-semhl-comment) ("defaultLibrary" . lsp-face-semhl-default-library)) "Semantic tokens modifier faces. Faces to use for semantic token modifiers if `lsp-semantic-tokens-apply-modifiers' is non-nil.") (defun lsp--semantic-tokens-capabilities () `((semanticTokens . ((dynamicRegistration . t) (requests . ((range . t) (full . t))) (tokenModifiers . ,(if lsp-semantic-tokens-apply-modifiers (apply 'vector (mapcar #'car (lsp-semantic-tokens--modifier-faces-for (lsp--workspace-client lsp--cur-workspace)))) [])) (overlappingTokenSupport . t) (multilineTokenSupport . ,(if lsp-semantic-tokens-enable-multiline-token-support t json-false)) (tokenTypes . ,(apply 'vector (mapcar #'car (lsp-semantic-tokens--type-faces-for (lsp--workspace-client lsp--cur-workspace))))) (formats . ["relative"]))))) (defvar lsp--semantic-tokens-pending-full-token-requests '() "Buffers which should have their semantic tokens refreshed on idle. This is an alist of the form ((buffer_i . fontify_immediately_i) ...); entries with fontify_immediately set to t will immediately refontify once their token request is answered.") ;; NOTE: doesn't keep track of outstanding requests, so might still produce large latency outliers ;; if the language server doesn't process all outstanding token requests within one lsp-idle-delay (defcustom lsp-semantic-tokens-max-concurrent-idle-requests 1 "Maximum number of on-idle token requests to be dispatched simultaneously." :group 'lsp-semantic-tokens :type 'integer) (defvar lsp--semantic-tokens-idle-timer nil) (defun lsp--semantic-tokens-process-pending-requests () (let ((fuel lsp-semantic-tokens-max-concurrent-idle-requests)) (while (and lsp--semantic-tokens-pending-full-token-requests (> fuel 0)) (-let (((buffer . fontify-immediately) (pop lsp--semantic-tokens-pending-full-token-requests))) (when (buffer-live-p buffer) (setq fuel (1- fuel)) (with-current-buffer buffer (lsp--semantic-tokens-request nil fontify-immediately)))))) (unless lsp--semantic-tokens-pending-full-token-requests (cancel-timer lsp--semantic-tokens-idle-timer) (setq lsp--semantic-tokens-idle-timer nil))) (defun lsp--semantic-tokens-sort-pending-requests (pending-requests) ;; service currently visible buffers first, otherwise prefer immediate-fontification requests (-sort (lambda (entry-a entry-b) (let ((a-hidden (eq nil (get-buffer-window (car entry-a)))) (b-hidden (eq nil (get-buffer-window (car entry-b))))) (cond ((and b-hidden (not a-hidden)) t) ; sort a before b ((and a-hidden (not b-hidden)) nil) ; sort b before a ((and (not (cdr entry-a)) (cdr entry-b)) nil) ; otherwise sort b before a only if b is immediate and a is not (t t)))) (--filter (buffer-live-p (car it)) pending-requests))) (defun lsp--semantic-tokens-request-full-token-set-when-idle (buffer fontify-immediately) "Request full token set after an idle timeout of `lsp-idle-delay'. If FONTIFY-IMMEDIATELY is non-nil, fontification will be performed immediately once the corresponding response is received." (let ((do-fontify-immediately (or fontify-immediately (cdr (assoc buffer lsp--semantic-tokens-pending-full-token-requests))))) (setq lsp--semantic-tokens-pending-full-token-requests (lsp--semantic-tokens-sort-pending-requests (cons (cons buffer do-fontify-immediately) (--remove (eq buffer (car it)) lsp--semantic-tokens-pending-full-token-requests))))) (unless lsp--semantic-tokens-idle-timer (setq lsp--semantic-tokens-idle-timer (run-with-idle-timer lsp-idle-delay t #'lsp--semantic-tokens-process-pending-requests)))) (defun lsp--semantic-tokens-refresh-if-enabled (buffer) (when (buffer-local-value 'lsp-semantic-tokens-mode buffer) (lsp--semantic-tokens-request-full-token-set-when-idle buffer t))) (defvar-local lsp--semantic-tokens-cache nil "Previously returned token set. When non-nil, `lsp--semantic-tokens-cache' should adhere to the following lsp-interface: `(_SemanticTokensCache (:_documentVersion) (:response :_region :_truncated))'.") (defsubst lsp--semantic-tokens-putcache (k v) "Set key K of `lsp--semantic-tokens-cache' to V." (setq lsp--semantic-tokens-cache (plist-put lsp--semantic-tokens-cache k v))) (defvar-local lsp--semantic-tokens-teardown nil) (defun lsp--semantic-tokens-ingest-range-response (response) "Handle RESPONSE to semanticTokens/range request." (lsp--semantic-tokens-putcache :response response) (cl-assert (plist-get lsp--semantic-tokens-cache :_region)) (lsp--semantic-tokens-request-full-token-set-when-idle (current-buffer) nil)) (defun lsp--semantic-tokens-ingest-full-response (response) "Handle RESPONSE to semanticTokens/full request." (lsp--semantic-tokens-putcache :response response) (cl-assert (not (plist-get lsp--semantic-tokens-cache :_region)))) (defsubst lsp--semantic-tokens-apply-delta-edits (old-data edits) "Apply EDITS obtained from full/delta request to OLD-DATA." (let* ((old-token-count (length old-data)) (old-token-index 0) (substrings)) (cl-loop for edit across edits do (when (< old-token-index (lsp-get edit :start)) (push (substring old-data old-token-index (lsp-get edit :start)) substrings)) (push (lsp-get edit :data) substrings) (setq old-token-index (+ (lsp-get edit :start) (lsp-get edit :deleteCount))) finally do (push (substring old-data old-token-index old-token-count) substrings)) (apply #'vconcat (nreverse substrings)))) (defun lsp--semantic-tokens-ingest-full/delta-response (response) "Handle RESPONSE to semanticTokens/full/delta request." (if (lsp-get response :edits) (let ((old-data (--> lsp--semantic-tokens-cache (plist-get it :response) (lsp-get it :data)))) (cl-assert (not (plist-get lsp--semantic-tokens-cache :_region))) (when old-data (lsp--semantic-tokens-putcache :response (lsp-put response :data (lsp--semantic-tokens-apply-delta-edits old-data (lsp-get response :edits)))))) ;; server decided to send full response instead (lsp--semantic-tokens-ingest-full-response response))) (defun lsp--semantic-tokens-request (region fontify-immediately) "Send semantic tokens request to the language server. A full/delta request will be sent if delta requests are supported by the language server, allowed via `lsp-semantic-tokens-allow-delta-requests', and if a full set of tokens had previously been received. Otherwise, a ranged request will be dispatched if REGION is non-nil, ranged requests are supported by the language server, and allowed via `lsp-semantic-tokens-allow-delta-requests'. In all other cases, a full tokens request will be dispatched. If FONTIFY-IMMEDIATELY is non-nil, fontification will be performed immediately upon receiving the response." (let ((request-type "textDocument/semanticTokens/full") (request `(:textDocument ,(lsp--text-document-identifier))) (response-handler nil) (final-region nil)) (cond ((and lsp-semantic-tokens-allow-delta-requests (lsp-feature? "textDocument/semanticTokensFull/Delta") (--> lsp--semantic-tokens-cache (plist-get it :response) (and (lsp-get it :resultId) (lsp-get it :data) (not (plist-get lsp--semantic-tokens-cache :_region))))) (setq request-type "textDocument/semanticTokens/full/delta") (setq response-handler #'lsp--semantic-tokens-ingest-full/delta-response) (setq request (plist-put request :previousResultId (lsp-get (plist-get lsp--semantic-tokens-cache :response) :resultId)))) ((and lsp-semantic-tokens-allow-ranged-requests region (lsp-feature? "textDocument/semanticTokensRangeProvider")) (setq request-type "textDocument/semanticTokens/range") (setq final-region region) (setq request (plist-put request :range (lsp--region-to-range (car final-region) (cdr final-region)))) (setq response-handler #'lsp--semantic-tokens-ingest-range-response)) (t (setq response-handler #'lsp--semantic-tokens-ingest-full-response))) (lsp-request-async request-type request (lambda (response) (lsp--semantic-tokens-putcache :_documentVersion lsp--cur-version) (lsp--semantic-tokens-putcache :_region final-region) (funcall response-handler response) (when (or fontify-immediately (plist-get lsp--semantic-tokens-cache :_truncated)) (font-lock-flush))) :error-handler ;; buffer is not captured in `error-handler', it is in `callback' (let ((buf (current-buffer))) (lambda (&rest _) (when (buffer-live-p buf) (lsp--semantic-tokens-request-full-token-set-when-idle buf t)))) :mode 'tick :cancel-token (format "semantic-tokens-%s" (lsp--buffer-uri))))) ;;;###autoload (defvar-local semantic-token-modifier-cache (make-hash-table) "A cache of modifier values to the selected fonts. This allows whole-bitmap lookup instead of checking each bit. The expectation is that usage of modifiers will tend to cluster, so we will not have the full range of possible usages, hence a tractable hash map. This is set as buffer-local. It should probably be shared in a given workspace/language-server combination. This cache should be flushed every time any modifier configuration changes.") (defun lsp-semantic-tokens--fontify (old-fontify-region beg-orig end-orig &optional loudly) "Apply fonts to retrieved semantic tokens. OLD-FONTIFY-REGION is the underlying region fontification function, e.g., `font-lock-fontify-region'. BEG-ORIG and END-ORIG deliminate the requested fontification region and maybe modified by OLD-FONTIFY-REGION. LOUDLY will be forwarded to OLD-FONTIFY-REGION as-is." ;; TODO: support multiple language servers per buffer? (let ((faces (seq-some #'lsp--workspace-semantic-tokens-faces lsp--buffer-workspaces)) (modifier-faces (when lsp-semantic-tokens-apply-modifiers (seq-some #'lsp--workspace-semantic-tokens-modifier-faces lsp--buffer-workspaces))) old-bounds beg end) (cond ((or (eq nil faces) (eq nil lsp--semantic-tokens-cache) (eq nil (plist-get lsp--semantic-tokens-cache :response))) ;; default to non-semantic highlighting until first response has arrived (funcall old-fontify-region beg-orig end-orig loudly)) ((not (= lsp--cur-version (plist-get lsp--semantic-tokens-cache :_documentVersion))) ;; delay fontification until we have fresh tokens '(jit-lock-bounds 0 . 0)) (t (setq old-bounds (funcall old-fontify-region beg-orig end-orig loudly)) ;; this is to prevent flickering when semantic token highlighting ;; is layered on top of, e.g., tree-sitter-hl, or clojure-mode's syntax highlighting. (setq beg (min beg-orig (cadr old-bounds)) end (max end-orig (cddr old-bounds))) ;; if we're using the response to a ranged request, we'll only be able to fontify within ;; that range (and hence shouldn't clear any highlights outside of that range) (let ((token-region (plist-get lsp--semantic-tokens-cache :_region))) (if token-region (progn (lsp--semantic-tokens-putcache :_truncated (or (< beg (car token-region)) (> end (cdr token-region)))) (setq beg (max beg (car token-region))) (setq end (min end (cdr token-region)))) (lsp--semantic-tokens-putcache :_truncated nil))) (-let* ((inhibit-field-text-motion t) (data (lsp-get (plist-get lsp--semantic-tokens-cache :response) :data)) (i0 0) (i-max (1- (length data))) (current-line 1) (line-delta) (column 0) (face) (line-start-pos) (line-min) (line-max-inclusive) (text-property-beg) (text-property-end)) (save-mark-and-excursion (save-restriction (widen) (goto-char beg) (goto-char (line-beginning-position)) (setq line-min (line-number-at-pos)) (with-silent-modifications (goto-char end) (goto-char (line-end-position)) (setq line-max-inclusive (line-number-at-pos)) (forward-line (- line-min line-max-inclusive)) (let ((skip-lines (- line-min current-line))) (while (and (<= i0 i-max) (< (aref data i0) skip-lines)) (setq skip-lines (- skip-lines (aref data i0))) (setq i0 (+ i0 5))) (setq current-line (- line-min skip-lines))) (forward-line (- current-line line-min)) (setq line-start-pos (point)) (cl-loop for i from i0 to i-max by 5 do (setq line-delta (aref data i)) (unless (= line-delta 0) (forward-line line-delta) (setq line-start-pos (point)) (setq column 0) (setq current-line (+ current-line line-delta))) (setq column (+ column (aref data (1+ i)))) (setq face (aref faces (aref data (+ i 3)))) (setq text-property-beg (+ line-start-pos column)) (setq text-property-end (min (if lsp-semantic-tokens-enable-multiline-token-support (point-max) (line-end-position)) (+ text-property-beg (aref data (+ i 2))))) (when face (put-text-property text-property-beg text-property-end 'face face)) ;; Deal with modifiers. We cache common combinations of ;; modifiers, storing the faces they resolve to. (let* ((modifier-code (aref data (+ i 4))) (faces-to-apply (gethash modifier-code semantic-token-modifier-cache 'not-found))) (when (eq 'not-found faces-to-apply) (setq faces-to-apply nil) (cl-loop for j from 0 to (1- (length modifier-faces)) do (when (and (aref modifier-faces j) (> (logand modifier-code (ash 1 j)) 0)) (push (aref modifier-faces j) faces-to-apply))) (puthash modifier-code faces-to-apply semantic-token-modifier-cache)) (dolist (face faces-to-apply) (add-face-text-property text-property-beg text-property-end face))) when (> current-line line-max-inclusive) return nil))))) `(jit-lock-bounds ,beg . ,end))))) (defun lsp-semantic-tokens--request-update () "Request semantic-tokens update." ;; when dispatching ranged requests, we'll over-request by several chunks in both directions, ;; which should minimize those occasions where font-lock region extension extends beyond the ;; region covered by our freshly requested tokens (see lsp-mode issue #3154), while still limiting ;; requests to fairly small regions even if the underlying buffer is large (when (lsp-feature? "textDocument/semanticTokensFull") (lsp--semantic-tokens-request (cons (max (point-min) (- (window-start) (* 5 jit-lock-chunk-size))) (min (point-max) (+ (window-end) (* 5 jit-lock-chunk-size)))) t))) (defun lsp--semantic-tokens-as-defined-by-workspace (workspace) "Return plist of token-types and token-modifiers defined by WORKSPACE, or nil if none are defined." (when-let ((token-capabilities (or (-some-> (lsp--registered-capability "textDocument/semanticTokens") (lsp--registered-capability-options)) (lsp:server-capabilities-semantic-tokens-provider? (lsp--workspace-server-capabilities workspace))))) (-let* (((&SemanticTokensOptions :legend) token-capabilities)) `(:token-types ,(lsp:semantic-tokens-legend-token-types legend) :token-modifiers ,(lsp:semantic-tokens-legend-token-modifiers legend))))) (defun lsp-semantic-tokens-suggest-overrides () "Suggest face overrides that best match the faces chosen by `font-lock-fontify-region'." (interactive) (-when-let* ((token-info (-some #'lsp--semantic-tokens-as-defined-by-workspace lsp--buffer-workspaces)) ((&plist :token-types token-types :token-modifiers token-modifiers) token-info)) (let* ((tokens (lsp-request "textDocument/semanticTokens/full" `(:textDocument, (lsp--text-document-identifier)))) (inhibit-field-text-motion t) (data (lsp-get tokens :data)) (associated-faces '()) (line-delta) ;; KLUDGE: clear cache so our font-lock advice won't apply semantic-token faces (old-cache lsp--semantic-tokens-cache) (face-or-faces)) (setq lsp--semantic-tokens-cache nil) (save-restriction (save-excursion (widen) (font-lock-fontify-region (point-min) (point-max) t) (save-mark-and-excursion (save-restriction (widen) (goto-char (point-min)) (cl-loop for i from 0 to (1- (length data)) by 5 do (setq line-delta (aref data i)) (unless (= line-delta 0) (forward-line line-delta)) (forward-char (aref data (+ i 1))) (setq face-or-faces (get-text-property (point) 'face)) ;; TODO: consider modifiers? (when face-or-faces (--each (if (listp face-or-faces) face-or-faces (list face-or-faces)) (cl-pushnew `(,(aref data (+ i 3)) . ,it) associated-faces :test #'equal)))) (setq lsp--semantic-tokens-cache old-cache) (font-lock-flush))))) (switch-to-buffer (get-buffer-create "*Suggested Overrides*")) (insert "(") ;; TODO: sort alternatives by frequency (--each-indexed (-group-by #'car associated-faces) (insert (if (= it-index 0) "(" "\n (")) (insert (format "%s . " (aref token-types (car it)))) (--each-indexed (mapcar #'cdr (cdr it)) (insert (if (= it-index 0) (format "%s)" (prin1-to-string it)) (format " ; Alternative: %s" (prin1-to-string it)))))) (insert ")")))) (declare-function tree-sitter-hl-mode "ext:tree-sitter-hl") (with-eval-after-load 'tree-sitter-hl (add-hook 'tree-sitter-hl-mode-hook (lambda () (when (and lsp-mode lsp--semantic-tokens-teardown (boundp 'tree-sitter-hl-mode) tree-sitter-hl-mode) (lsp-warn "It seems you have configured tree-sitter-hl to activate after lsp-mode. To prevent tree-sitter-hl from overriding lsp-mode's semantic token highlighting, lsp-mode will now disable both semantic highlighting and tree-sitter-hl mode and subsequently re-enable both, starting with tree-sitter-hl-mode. Please adapt your config to prevent unnecessary mode reinitialization in the future.") (tree-sitter-hl-mode -1) (funcall lsp--semantic-tokens-teardown) (setq lsp--semantic-tokens-teardown nil) (tree-sitter-hl-mode t) (lsp--semantic-tokens-initialize-buffer))))) ;;;###autoload (defun lsp--semantic-tokens-initialize-buffer () "Initialize the buffer for semantic tokens. IS-RANGE-PROVIDER is non-nil when server supports range requests." (let* ((old-extend-region-functions font-lock-extend-region-functions) ;; make sure font-lock always fontifies entire lines (TODO: do we also have ;; to change some jit-lock-...-region functions/variables?) (new-extend-region-functions (if (memq 'font-lock-extend-region-wholelines old-extend-region-functions) old-extend-region-functions (cons 'font-lock-extend-region-wholelines old-extend-region-functions))) (buffer (current-buffer))) (setq lsp--semantic-tokens-cache nil) (setq font-lock-extend-region-functions new-extend-region-functions) (add-function :around (local 'font-lock-fontify-region-function) #'lsp-semantic-tokens--fontify) (add-hook 'lsp-on-change-hook #'lsp-semantic-tokens--request-update nil t) (lsp-semantic-tokens--request-update) (setq lsp--semantic-tokens-teardown (lambda () (setq lsp--semantic-tokens-pending-full-token-requests (--remove (eq buffer (car it)) lsp--semantic-tokens-pending-full-token-requests)) (setq font-lock-extend-region-functions old-extend-region-functions) (setq lsp--semantic-tokens-cache nil) (remove-function (local 'font-lock-fontify-region-function) #'lsp-semantic-tokens--fontify) (remove-hook 'lsp-on-change-hook #'lsp-semantic-tokens--request-update t))))) (defun lsp--semantic-tokens-build-face-map (identifiers faces category varname) "Build map of FACES for IDENTIFIERS using CATEGORY and VARNAME." (apply 'vector (mapcar (lambda (id) (let ((maybe-face (cdr (assoc id faces)))) (when (and lsp-semantic-tokens-warn-on-missing-face (not maybe-face)) (lsp-warn "No face has been associated to the %s '%s': consider adding a corresponding definition to %s" category id varname)) maybe-face)) identifiers))) (defun lsp-semantic-tokens--apply-alist-overrides (base overrides discard-defaults) "Merge or replace BASE with OVERRIDES, depending on DISCARD-DEFAULTS. For keys present in both alists, the assignments made by OVERRIDES will take precedence." (if discard-defaults overrides (let* ((copy-base (copy-alist base))) (mapc (-lambda ((key . value)) (setf (alist-get key copy-base nil nil #'string=) value)) overrides) copy-base))) (defun lsp-semantic-tokens--type-faces-for (client) "Return the semantic token type faces for CLIENT." (lsp-semantic-tokens--apply-alist-overrides lsp-semantic-token-faces (plist-get (lsp--client-semantic-tokens-faces-overrides client) :types) (plist-get (lsp--client-semantic-tokens-faces-overrides client) :discard-default-types))) (defun lsp-semantic-tokens--modifier-faces-for (client) "Return the semantic token type faces for CLIENT." (lsp-semantic-tokens--apply-alist-overrides lsp-semantic-token-modifier-faces (plist-get (lsp--client-semantic-tokens-faces-overrides client) :modifiers) (plist-get (lsp--client-semantic-tokens-faces-overrides client) :discard-default-modifiers))) (defun lsp--semantic-tokens-on-refresh (workspace) "Clear semantic tokens within all buffers of WORKSPACE, refresh in currently active buffer." (cl-assert (not (eq nil workspace))) (when lsp-semantic-tokens-honor-refresh-requests (cl-loop for ws-buffer in (lsp--workspace-buffers workspace) do (let ((fontify-immediately (equal (current-buffer) ws-buffer))) (with-current-buffer ws-buffer (lsp--semantic-tokens-request nil fontify-immediately)))))) ;;;###autoload (defun lsp--semantic-tokens-initialize-workspace (workspace) "Initialize semantic tokens for WORKSPACE." (cl-assert workspace) (-let (((&plist :token-types types :token-modifiers modifiers) (lsp--semantic-tokens-as-defined-by-workspace workspace)) (client (lsp--workspace-client workspace))) (setf (lsp--workspace-semantic-tokens-faces workspace) (lsp--semantic-tokens-build-face-map types (lsp-semantic-tokens--type-faces-for client) "semantic token" "lsp-semantic-token-faces")) (setf (lsp--workspace-semantic-tokens-modifier-faces workspace) (lsp--semantic-tokens-build-face-map modifiers (lsp-semantic-tokens--modifier-faces-for client) "semantic token modifier" "lsp-semantic-token-modifier-faces")))) ;;;###autoload (defun lsp-semantic-tokens--warn-about-deprecated-setting () "Warn about deprecated semantic highlighting variable." (when (boundp 'lsp-semantic-highlighting) (pcase lsp-semantic-highlighting (:semantic-tokens (lsp-warn "It seems you wish to use semanticTokens-based highlighting. To do so, please remove any references to the deprecated variable `lsp-semantic-highlighting' from your configuration and set `lsp-semantic-tokens-enable' to `t' instead.") (setq lsp-semantic-tokens-enable t)) ((or :immediate :deferred) (lsp-warn "It seems you wish to use Theia-based semantic highlighting. This protocol has been superseded by the semanticTokens protocol specified by LSP v3.16 and is no longer supported by lsp-mode. If your language server provides semanticToken support, please set `lsp-semantic-tokens-enable' to `t' to use it."))))) ;;;###autoload (defun lsp-semantic-tokens--enable () "Enable semantic tokens mode." (when (and lsp-semantic-tokens-enable (lsp-feature? "textDocument/semanticTokensFull")) (lsp-semantic-tokens--warn-about-deprecated-setting) (lsp-semantic-tokens-mode 1))) (defun lsp-semantic-tokens--disable () "Disable semantic tokens mode." (lsp-semantic-tokens-mode -1)) ;;;###autoload (define-minor-mode lsp-semantic-tokens-mode "Toggle semantic-tokens support." :group 'lsp-semantic-tokens :global nil (cond ((and lsp-semantic-tokens-mode (lsp-feature? "textDocument/semanticTokensFull")) (add-hook 'lsp-configure-hook #'lsp-semantic-tokens--enable nil t) (add-hook 'lsp-unconfigure-hook #'lsp-semantic-tokens--disable nil t) (mapc #'lsp--semantic-tokens-initialize-workspace (lsp--find-workspaces-for "textDocument/semanticTokensFull")) (lsp--semantic-tokens-initialize-buffer)) (t (remove-hook 'lsp-configure-hook #'lsp-semantic-tokens--enable t) (remove-hook 'lsp-unconfigure-hook #'lsp-semantic-tokens--disable t) (when lsp--semantic-tokens-teardown (funcall lsp--semantic-tokens-teardown)) (lsp-semantic-tokens--request-update) (setq lsp--semantic-tokens-cache nil lsp--semantic-tokens-teardown nil)))) ;; debugging helpers (defun lsp--semantic-tokens-verify () "Store current token set and compare with the response to a full token request." (interactive) (let ((old-tokens (--> lsp--semantic-tokens-cache (plist-get it :response) (lsp-get it :data))) (old-version (--> lsp--semantic-tokens-cache (plist-get it :_documentVersion)))) (if (not (equal lsp--cur-version old-version)) (message "Stored documentVersion %d differs from current version %d" old-version lsp--cur-version) (lsp-request-async "textDocument/semanticTokens/full" `(:textDocument ,(lsp--text-document-identifier)) (lambda (response) (let ((new-tokens (lsp-get response :data))) (if (equal old-tokens new-tokens) (message "New tokens (total count %d) are identical to previously held token set" (length new-tokens)) (message "Newly returned tokens differ from old token set") (print old-tokens) (print new-tokens)))) :mode 'tick :cancel-token (format "semantic-tokens-%s" (lsp--buffer-uri)))))) (defvar-local lsp-semantic-tokens--log '()) (defvar-local lsp-semantic-tokens--prev-response nil) (defun lsp-semantic-tokens--log-buffer-contents (tag) "Log buffer contents for TAG." (save-restriction (save-excursion (widen) (push `(:tag ,tag :buffer-contents ,(buffer-substring (point-min) (point-max)) :prev-response ,lsp-semantic-tokens--prev-response) lsp-semantic-tokens--log)))) (defun lsp-semantic-tokens-enable-log () "Enable logging of intermediate fontification states. This is a debugging tool, and may incur significant performance penalties." (setq lsp-semantic-tokens--log '()) (defun lsp-advice-tokens-fontify (orig-func old-fontify-region beg-orig end-orig &optional loudly) (lsp-semantic-tokens--log-buffer-contents 'before) (let ((result (funcall orig-func old-fontify-region beg-orig end-orig loudly))) (lsp-semantic-tokens--log-buffer-contents 'after) result)) (advice-add 'lsp-semantic-tokens--fontify :around 'lsp-advice-tokens-fontify) (defun lsp-log-delta-response (response) (setq lsp-semantic-tokens--prev-response `(:request-type "delta" :response ,response :version ,lsp--cur-version))) (advice-add 'lsp--semantic-tokens-ingest-full/delta-response :before 'lsp-log-delta-response) (defun lsp-log-full-response (response) (setq lsp-semantic-tokens--prev-response `(:request-type "full" :response ,response :version ,lsp--cur-version))) (advice-add 'lsp--semantic-tokens-ingest-full-response :before 'lsp-log-full-response) (defun lsp-log-range-response (response) (setq lsp-semantic-tokens--prev-response `(:request-type "range" :response ,response :version ,lsp--cur-version))) (advice-add 'lsp--semantic-tokens-ingest-range-response :before 'lsp-log-range-response)) (defun lsp-semantic-tokens-disable-log () "Disable logging of intermediate fontification states." (advice-remove 'lsp-semantic-tokens--fontify 'lsp-advice-tokens-fontify) (advice-remove 'lsp--semantic-tokens-ingest-full/delta-response 'lsp-log-delta-response) (advice-remove 'lsp--semantic-tokens-ingest-full-response 'lsp-log-full-response) (advice-remove 'lsp--semantic-tokens-ingest-range-response 'lsp-log-range-response)) (declare-function htmlize-buffer "ext:htmlize") (defun lsp-semantic-tokens-export-log () "Write HTML-formatted snapshots of previous fontification results to /tmp." (require 'htmlize) (let* ((outdir (f-join "/tmp" "semantic-token-snapshots")) (progress-reporter (make-progress-reporter (format "Writing buffer snapshots to %s..." outdir) 0 (length lsp-semantic-tokens--log)))) (f-mkdir outdir) (--each-indexed (reverse lsp-semantic-tokens--log) (-let* (((&plist :tag tag :buffer-contents buffer-contents :prev-response prev-response) it) (html-buffer)) ;; FIXME: doesn't update properly; sit-for helps... somewhat, ;; but unreliably (when (= (% it-index 5) 0) (progress-reporter-update progress-reporter it-index) (sit-for 0.01)) ;; we're emitting 2 snapshots (before & after) per update, so request ;; parameters should only change on every 2nd invocation (when (cl-evenp it-index) (with-temp-buffer (insert (prin1-to-string prev-response)) (write-file (f-join outdir (format "parameters_%d.el" (/ it-index 2)))))) (with-temp-buffer (insert buffer-contents) (setq html-buffer (htmlize-buffer)) (with-current-buffer html-buffer ;; some configs such as emacs-doom may autoformat on save; switch to ;; fundamental-mode to avoid this (fundamental-mode) (write-file (f-join outdir (format "buffer_%d_%s.html" (/ it-index 2) tag))))) (kill-buffer html-buffer))) (progress-reporter-done progress-reporter))) (lsp-consistency-check lsp-semantic-tokens) (provide 'lsp-semantic-tokens) ;;; lsp-semantic-tokens.el ends here