commit 5ccd4d7406847b79c483d4fffba42b301171c4fd (HEAD, refs/remotes/origin/master) Author: João Távora Date: Wed Nov 12 21:29:57 2025 +0000 ; Eglot: tweak manual's wording of "language server" * doc/misc/eglot.texi (Eglot Features) (Eglot and Buffers) (Eglot Commands): "language-server" -> "language server" diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index ae04c3f7b10..12379f890c9 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -422,21 +422,21 @@ enhances diagnostics with interactive server-suggested fixes (so-called @item Finding definitions and uses of identifiers, via Xref (@pxref{Xref,,, emacs, GNU Emacs Manual}). Eglot provides a backend for the Xref -capabilities which uses the language-server understanding of the program -source. In particular, it eliminates the need to generate tags tables -(@pxref{Tags tables,,, emacs, GNU Emacs Manual}) for languages which are -only supported by the @code{etags} backend. +capabilities which uses the language server's understanding of the +program source. In particular, it eliminates the need to generate tags +tables (@pxref{Tags tables,,, emacs, GNU Emacs Manual}) for languages +which are only supported by the @code{etags} backend. @item Buffer navigation by name of function, class, method, etc., via Imenu (@pxref{Imenu,,, emacs, GNU Emacs Manual}). Eglot provides its own variant of @code{imenu-create-index-function}, which generates the index -for the buffer based on language-server program source analysis. +for the buffer based on the language server's program source analysis. @item Enhanced completion of symbol at point by the @code{completion-at-point} command (@pxref{Symbol Completion,,, emacs, GNU Emacs Manual}). This -uses the language-server's parser data for the completion candidates. +uses the language server's parser data for the completion candidates. @item Server-suggested code refactorings. The ElDoc package is also leveraged @@ -473,7 +473,7 @@ supported and is activated automatically as you type. If a completion package such as the Company package (a popular third-party completion package providing @code{company-mode}), is installed, Eglot enhances it by providing completion candidates based on -the language-server analysis of the source code. (Company can be +the language server's analysis of the source code. (Company can be installed from GNU ELPA.) @item @@ -523,14 +523,14 @@ One of the main strong points of using a language server is that a language server has a broad view of the program: it considers more than just the single source file you are editing. Ideally, the language server should know about all the source files of your program which are -written in the language supported by the server. In the language-server -parlance, the set of the source files of a program is known as a -@dfn{workspace}. The Emacs equivalent of a workspace is a @dfn{project} -(@pxref{Projects,,, emacs, GNU Emacs Manual}). Eglot fully supports -Emacs projects, and considers the file in whose buffer Eglot is turned -on as belonging to a project. In the simplest case, that file is the -entire project, i.e.@: your project consists of a single file. But -there are other more complex projects: +written in the language supported by the server. In LSP parlance, the +set of the source files of a program is known as a @dfn{workspace}. The +Emacs equivalent of a workspace is a @dfn{project} (@pxref{Projects,,, +emacs, GNU Emacs Manual}). Eglot fully supports Emacs projects, and +considers the file in whose buffer Eglot is turned on as belonging to a +project. In the simplest case, that file is the entire project, i.e.@: +your project consists of a single file. But there are other more +complex projects: @itemize @bullet @item @@ -580,7 +580,7 @@ specially in several ways: @vindex eglot-menu @item All of the project's file-visiting buffers under the same major-mode are -served by a single language-server connection. (If the project uses +served by a single language server connection. (If the project uses several programming languages, there will usually be a separate server connection for each group of files written in the same language and using the same Emacs major-mode.) Eglot adds the @@ -674,7 +674,7 @@ major mode to use and for the server program to start. If invoked with If the language server is successfully started and contacted, this command arranges for any other buffers belonging to the same project and -using the same major mode to use the same language-server session. That +using the same major mode to use the same language server session. That includes any buffers created by visiting files after this command succeeds to connect to a language server. @@ -717,7 +717,7 @@ language server of the current buffer to implement the renaming. @item M-x eglot-format This command reformats the active region according to the -language-server rules. If no region is active, it reformats the entire +language server rules. If no region is active, it reformats the entire current buffer. @item M-x eglot-format-buffer commit 6b2a7b84793da8c917deb159c34e9a7efa5267ec Author: João Távora Date: Wed Nov 12 13:11:49 2025 +0000 Eglot: document semantic tokens (semtok) feature (bug#79374) Also clarify how to turn off semantic tokens and inlay hints, since I suspect that will be a theme. * doc/misc/eglot.texi (Eglot Features): Add semantic tokens. (Eglot Commands): Document eglot-semantic-tokens-mode. (Eglot and Buffers): Provide example on how to turn off semtok and inlay hints. * etc/EGLOT-NEWS (Changes to upcoming Eglot): Announce semantic tokens support diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index 93268ce7c15..ae04c3f7b10 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -452,6 +452,13 @@ be it the type of a variable, or the name of a formal parameter in a function call. @xref{Eglot Commands}, and the @code{eglot-inlay-hints-mode} minor mode. +@item +Enhanced source code fontification via LSP @dfn{semantic tokens}. Eglot +can leverage the language server's understanding of the role and meaning +of identifiers in the code to provide more accurate syntax highlighting. +@xref{Eglot Commands}, and the @code{eglot-semantic-tokens-mode} minor +mode. + @item Display of function call and type hierarchies via the @code{eglot-show-call-hierarchy} and @code{eglot-show-type-hierarchy} @@ -501,7 +508,8 @@ capabilities to Emacs users. However, @xref{Extending Eglot}. Finally, it's worth noting that, by default, Eglot generally turns on all features that it @emph{can} turn on. It's possible to opt out of -some features via user options (@pxref{Customizing Eglot}) and a hook +features via the @code{eglot-ignored-server-capabilities} user option +(@pxref{Customizing Eglot}) and the @code{eglot-managed-mode-hook} hook that runs after Eglot starts managing a buffer (@pxref{Eglot and Buffers}). @@ -631,6 +639,17 @@ minor mode is turned on and when it's turned off; use the function or not. @end itemize +For example, to turn off @dfn{inlay hints} and @dfn{semantic tokens} by +default but still retain the ability to turn them on again +interactively use this: + +@lisp + (add-hook 'eglot-managed-mode-hook + (lambda () + (eglot-inlay-hints-mode -1) + (eglot-semantic-tokens-mode -1))) +@end lisp + @node Eglot Commands @section Eglot Commands @cindex commands, Eglot @@ -744,6 +763,29 @@ serve hints about positional parameter names in function calls and a variable's automatically deduced type. Inlay hints help the user not have to remember these things by heart. +@cindex semantic tokens +@item M-x eglot-semantic-tokens-mode +This command toggles LSP @dfn{semantic tokens} fontification on and off +for the current buffer. Semantic tokens provide enhanced syntax +highlighting based on the language server's semantic analysis of the +code, going beyond traditional regular-expression-based fontification. +For example, the server can distinguish between different kinds of +variables (parameters, local variables, fields) or indicate whether a +function is being declared or called. + +@vindex eglot-semantic-token-types +@vindex eglot-semantic-token-modifiers +The @code{eglot-semantic-faces} customization group contains the user +options @code{eglot-semantic-token-types} and +@code{eglot-semantic-token-modifiers}, which control which semantic +token types and modifiers reported by the language server Eglot should +consider when fontifying. For performance reasons, the server +connection has to be restarted before changes take effect. + +@cindex semantic token faces +The @code{eglot-semantic-faces} customization group also contains the +face definitions for controlling the appearance of each token. + @cindex type hierarchy @item M-x eglot-show-type-hierarchy Pop up a special buffer showing a interactive tree which represents a diff --git a/etc/EGLOT-NEWS b/etc/EGLOT-NEWS index 628fb741571..d9999248a31 100644 --- a/etc/EGLOT-NEWS +++ b/etc/EGLOT-NEWS @@ -17,6 +17,19 @@ This refers to https://github.com/joaotavora/eglot/issues/. That is, to look up issue github#1234, go to https://github.com/joaotavora/eglot/issues/1234. + +* Changes to upcoming Eglot + +** Support for semantic tokens (bug#79374) + +The new minor mode 'eglot-semantic-tokens-mode' provides enhanced syntax +highlighting based on the language server's analysis, going beyond +traditional regular-expression-based fontification. The +'eglot-semantic-faces' customization group contains options for +controlling which token types and modifiers to consider, as well as +faces for customizing their appearance. The minor mode is on by +default: consult the manual on how to turn it off. + * Changes in Eglot 1.19 (23/10/2025) commit f466c0c43dee332e40d7261fabf14d44d9f52767 Author: João Távora Date: Wed Nov 12 13:30:27 2025 +0000 Eglot: don't use eglot--semtok-idle-timer (bug#79374) Don't have evidence for the usefulness of this technique yet. * lisp/progmodes/eglot.el (eglot--semtok-idle-timer): Delete. (eglot--semtok-request): Simplify. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 33a09fea4ca..ceebcc80ce2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -4606,9 +4606,6 @@ If NOERROR, return predicate, else erroring function." semtok-cache) probe)))) -(defvar-local eglot--semtok-idle-timer nil - "Idle timer to request full semantic tokens.") - (cl-defmethod eglot-handle-request (server (_method (eql workspace/semanticTokens/refresh))) "Handle a semanticTokens/refresh request from SERVER." @@ -4712,16 +4709,7 @@ If NOERROR, return predicate, else erroring function." (list :textDocument (eglot--TextDocumentIdentifier) :range (list :start (eglot--pos-to-lsp-position beg) :end (eglot--pos-to-lsp-position end))) - (lambda (response) - (when eglot--semtok-idle-timer - (cancel-timer eglot--semtok-idle-timer)) - (setq eglot--semtok-idle-timer - (run-with-idle-timer (* 3 eglot-send-changes-idle-time) nil - (lambda () - (eglot--when-live-buffer buf - (eglot--semtok-request - (point-min) (point-max)))))) - response))) + #'identity)) (t (req :textDocument/semanticTokens/full (point-min) (point-max) (list :textDocument (eglot--TextDocumentIdentifier)) commit 6337ff8214ec89deb896f8e2b571e580f217774d Author: João Távora Date: Wed Nov 12 12:38:28 2025 +0000 Eglot: rework semtok defcustoms and face calculation (bug#79374) * lisp/progmodes/eglot.el (eglot-semantic-tokens-faces) (eglot-semantic-tokens-modifier-faces): Delete. (eglot--semtok-types, eglot--semtok-modifiers): Rename from eglot-semantic-tokens-faces and eglot-semantic-tokens-modifier-faces. (eglot-client-capabilities): Tweak. (eglot--lsp-interface-alist): Add SemanticTokensLegend. (eglot--connect): Don't initialize a server. (eglot--semtok-define-things): New helper. (eglot-lsp-server): Just one slot needed. (eglot--semtok-token-faces): Rework. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 69826481a1f..33a09fea4ca 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -629,71 +629,6 @@ Note additionally: (string :tag "Specify your own"))) :package-version '(Eglot . "1.19")) -(defcustom eglot-semantic-tokens-faces - '(("namespace" . font-lock-keyword-face) - ("type" . font-lock-type-face) - ("class" . font-lock-type-face) - ("enum" . font-lock-type-face) - ("interface" . font-lock-type-face) - ("struct" . font-lock-type-face) - ("typeParameter" . font-lock-type-face) - ("parameter" . font-lock-variable-name-face) - ("variable" . font-lock-variable-name-face) - ("property" . font-lock-property-use-face) - ("enumMember" . font-lock-constant-face) - ("event" . font-lock-variable-name-face) - ("function" . font-lock-function-name-face) - ("method" . font-lock-function-name-face) - ("macro" . font-lock-preprocessor-face) - ("keyword" . font-lock-keyword-face) - ("modifier" . font-lock-function-name-face) - ("comment" . font-lock-comment-face) - ("string" . font-lock-string-face) - ("number" . font-lock-constant-face) - ("regexp" . font-lock-string-face) - ("operator" . font-lock-function-name-face) - ("decorator" . font-lock-type-face)) - "Alist of faces to use to highlight semantic tokens. -Each element is a cons cell whose car is a token type name and cdr is -the face to use." - :type `(alist :key-type (string :tag "Token name") - :value-type (choice (face :tag "Face") - (plist :tag "Face Attributes" - :key-type - (choice - ,@(mapcar - (lambda (cell) - `(const :tag ,(capitalize - (cdr cell)) - ,(car cell))) - face-attribute-name-alist)))))) - -(defcustom eglot-semantic-tokens-modifier-faces - '(("declaration" . font-lock-function-name-face) - ("definition" . font-lock-function-name-face) - ("readonly" . font-lock-constant-face) - ("static" . font-lock-keyword-face) - ("deprecated" . eglot-diagnostic-tag-deprecated-face) - ("abstract" . font-lock-keyword-face) - ("async" . font-lock-preprocessor-face) - ("modification" . font-lock-function-name-face) - ("documentation" . font-lock-doc-face) - ("defaultLibrary" . font-lock-builtin-face)) - "List of face to use to highlight tokens with modifiers. -Each element is a cons cell whose car is a modifier name and cdr is -the face to use." - :type `(alist :key-type (string :tag "Token name") - :value-type (choice (face :tag "Face") - (plist :tag "Face Attributes" - :key-type - (choice - ,@(mapcar - (lambda (cell) - `(const :tag ,(capitalize - (cdr cell)) - ,(car cell))) - face-attribute-name-alist)))))) - (defvar eglot-withhold-process-id nil "If non-nil, Eglot will not send the Emacs process id to the language server. This can be useful when using docker to run a language server.") @@ -726,6 +661,47 @@ This can be useful when using docker to run a language server.") `((1 . eglot-diagnostic-tag-unnecessary-face) (2 . eglot-diagnostic-tag-deprecated-face))) +(eval-when-compile + (defconst eglot--semtok-types + '(("namespace" . font-lock-keyword-face) + ("type" . font-lock-type-face) + ("class" . font-lock-type-face) + ("enum" . font-lock-type-face) + ("interface" . font-lock-type-face) + ("struct" . font-lock-type-face) + ("typeParameter" . font-lock-type-face) + ("parameter" . font-lock-variable-name-face) + ("variable" . font-lock-variable-name-face) + ("property" . font-lock-property-use-face) + ("enumMember" . font-lock-constant-face) + ("event" . font-lock-variable-name-face) + ("function" . font-lock-function-name-face) + ("method" . font-lock-function-name-face) + ("macro" . font-lock-preprocessor-face) + ("keyword" . font-lock-keyword-face) + ("modifier" . font-lock-function-name-face) + ("comment" . font-lock-comment-face) + ("string" . font-lock-string-face) + ("number" . font-lock-constant-face) + ("regexp" . font-lock-string-face) + ("operator" . font-lock-function-name-face) + ("decorator" . font-lock-type-face))) + + (defconst eglot--semtok-modifiers + '(("declaration" . font-lock-function-name-face) + ("definition" . font-lock-function-name-face) + ("readonly" . font-lock-constant-face) + ("static" . font-lock-keyword-face) + ("deprecated" . eglot-diagnostic-tag-deprecated-face) + ("abstract" . font-lock-keyword-face) + ("async" . font-lock-preprocessor-face) + ("modification" . font-lock-function-name-face) + ("documentation" . font-lock-doc-face) + ("defaultLibrary" . font-lock-builtin-face)))) + +(defvar eglot-semantic-token-types) ;; forward-declare +(defvar eglot-semantic-token-modifiers) ;; forward-declare + (defvaralias 'eglot-{} 'eglot--{}) (defconst eglot--{} (make-hash-table :size 0) "The empty JSON object.") @@ -777,6 +753,7 @@ This can be useful when using docker to run a language server.") (ResponseError (:code :message) (:data)) (ShowMessageParams (:type :message)) (ShowMessageRequestParams (:type :message) (:actions)) + (SemanticTokensLegend (:tokenTypes :tokenModifiers)) (SignatureHelp (:signatures) (:activeSignature :activeParameter)) (SignatureInformation (:label) (:documentation :parameters :activeParameter)) (SymbolInformation (:name :kind :location) @@ -1149,10 +1126,10 @@ object." :rename `(:dynamicRegistration :json-false) :semanticTokens `(:dynamicRegistration :json-false :requests '(:range t :full (:delta t)) - :tokenModifiers [,@(mapcar #'car eglot-semantic-tokens-modifier-faces)] :overlappingTokenSupport t :multilineTokenSupport t - :tokenTypes [,@(mapcar #'car eglot-semantic-tokens-faces)] + :tokenTypes [,@eglot-semantic-token-types] + :tokenModifiers [,@eglot-semantic-token-modifiers] :formats ["relative"]) :inlayHint `(:dynamicRegistration :json-false) :callHierarchy `(:dynamicRegistration :json-false) @@ -1225,15 +1202,9 @@ object." (saved-initargs :documentation "Saved initargs for reconnection purposes." :accessor eglot--saved-initargs) - (semtok-faces - :initform nil - :documentation "Semantic tokens faces.") - (semtok-modifier-faces - :initform nil - :documentation "Semantic tokens modifier faces.") - (semtok-modifier-cache - :initform (make-hash-table) - :documentation "Map LSP modifier values to the selected faces.")) + (semtok-cache + :initform (make-hash-table :test #'equal) + :documentation "Map LSP token conses to face names.")) :documentation "Represents a server. Wraps a process for LSP communication.") @@ -1815,7 +1786,6 @@ This docstring appeases checkdoc, that's all." (gethash project eglot--servers-by-project)) (setf (eglot--capabilities server) capabilities) (setf (eglot--server-info server) serverInfo) - (eglot--semtok-initialize server) (jsonrpc-notify server :initialized eglot--{}) (dolist (buffer (buffer-list)) (with-current-buffer buffer @@ -4589,22 +4559,52 @@ If NOERROR, return predicate, else erroring function." ;;; Semantic tokens +(defmacro eglot--semtok-define-things () + (cl-flet ((def-it (name def) + `(defface ,(intern (format "eglot-semantic-%s-face" name)) + '((t (:inherit ,def))) + ,(format "Face for painting a `%s' LSP semantic token" name) + :group 'eglot-semantic-fontification))) + (let ((types (mapcar #'car eglot--semtok-types)) + (modifiers (mapcar #'car eglot--semtok-modifiers))) + `(progn + (defgroup eglot-semantic-faces nil + "Faces and options for LSP semantic fontification." :group 'eglot) + ,@(cl-loop for (n . d) in eglot--semtok-types collect (def-it n d)) + ,@(cl-loop for (n . d) in eglot--semtok-modifiers collect (def-it n d)) + (defcustom eglot-semantic-token-types + ',types "LSP-supplied semantic types Eglot should consider." + :type '(set ,@(mapcar (lambda (o) `(const ,o)) types)) + :group 'eglot-semantic-fontification) + (defcustom eglot-semantic-token-modifiers + ',modifiers "LSP-supplied semantic modifiers Eglot should consider." + :type '(set ,@(mapcar (lambda (o) `(const ,o)) modifiers)) + :group 'eglot-semantic-fontification))))) + +(eglot--semtok-define-things) + (defun eglot--semtok-token-faces (tok) - (with-slots (semtok-faces - (modifier-faces semtok-modifier-faces) - (modifier-cache semtok-modifier-cache)) - (eglot-current-server) - (let* ((code (cdr tok)) - (mods (gethash code modifier-cache 'not-found))) - (when (eq mods 'not-found) - (setq mods (cl-loop for j from 0 below (length modifier-faces) - if (> (logand code (ash 1 j)) 0) - if (aref modifier-faces j) - collect (aref modifier-faces j))) - (puthash code mods modifier-cache)) - (if-let* ((main (aref semtok-faces (car tok)))) - (cons main mods) - mods)))) + (with-slots (semtok-cache capabilities) + (eglot--current-server-or-lose) + (let ((probe (gethash tok semtok-cache :missing)) + tname) + (if (eq probe :missing) + (puthash + tok + (eglot--dbind ((SemanticTokensLegend) tokenTypes tokenModifiers) + (plist-get (plist-get capabilities :semanticTokensProvider) :legend) + (setq tname (aref tokenTypes (car tok))) + (when (member tname eglot-semantic-token-types) + (cl-loop + for j from 0 for m across tokenModifiers + unless (or (zerop (logand (cdr tok) (ash 1 j))) + (not (member m eglot-semantic-token-modifiers))) + collect (intern (format "eglot-semantic-%s-face" m)) into mfaces + finally (cl-return + (cons (intern (format "eglot-semantic-%s-face" tname)) + mfaces))))) + semtok-cache) + probe)))) (defvar-local eglot--semtok-idle-timer nil "Idle timer to request full semantic tokens.") @@ -4616,32 +4616,6 @@ If NOERROR, return predicate, else erroring function." (eglot--when-live-buffer buffer (unless (zerop eglot--versioned-identifier) (font-lock-flush))))) -(defun eglot--semtok-build-face-map (identifiers faces category varname) - "Build map of FACES for IDENTIFIERS using CATEGORY and VARNAME." - (vconcat - (mapcar (lambda (id) - (let ((maybe-face (cdr (assoc id faces)))) - (when (not maybe-face) - (eglot--warn "No face has been associated to the %s `%s': consider adding a corresponding definition to %s" - category id varname)) - maybe-face)) - identifiers))) - -(defun eglot--semtok-initialize (server) - "Initialize SERVER for semantic tokens." - (with-slots (semtok-faces semtok-modifier-faces capabilities) server - ;; FIXME: eglot-dbind - (cl-destructuring-bind (&key tokenTypes tokenModifiers &allow-other-keys) - (plist-get (plist-get capabilities :semanticTokensProvider) :legend) - (setq semtok-faces - (eglot--semtok-build-face-map - tokenTypes eglot-semantic-tokens-faces - "semantic token" 'eglot-semantic-tokens-faces) - semtok-modifier-faces - (eglot--semtok-build-face-map - tokenModifiers eglot-semantic-tokens-modifier-faces - "semantic token modifier" 'eglot-semantic-tokens-modifier-faces))))) - (define-minor-mode eglot-semantic-tokens-mode "Minor mode for fontifying buffer with LSP server's semantic tokens." :global nil commit 583493cb2166e11fe2b765c0ab48f36d242eda7b Author: João Távora Date: Wed Nov 12 00:10:49 2025 +0000 Eglot: debounce consecutive semtok full/delta requests (bug#79374) Many back-to-back calls for 'eglot--semtok-request' and small regions occur even on trivial fast edits. Even though it's cheap and harmless to send many delta rquests, it's nicer to just send just one. The debouncing from jsonrpc-async-request isn't enough. When a response to a semtok request is received, it's important to remember which regions to flush. Font-lock logic will then do its magic and coalesce those regions into one before invoking eglot--semtok-font-lock, and eglot--semtok-font-lock-1 will have up-to-date data to work with. * lisp/progmodes/eglot.el (eglot--semtok-inflight): New variable. (eglot--semtok-request): Use it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 82d3e27f3b3..69826481a1f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -4657,6 +4657,9 @@ If NOERROR, return predicate, else erroring function." (defvar-local eglot--semtok-cache nil "Cache of the last response from the server.") +(defvar-local eglot--semtok-inflight nil + "List of (BEG . END) regions of inflight semtok requests.") + (defsubst eglot--semtok-apply-delta-edits (old-data edits) "Apply EDITS obtained from full/delta request to OLD-DATA." (cl-loop @@ -4668,54 +4671,66 @@ If NOERROR, return predicate, else erroring function." finally (cl-return (vconcat new (substring old-data old-token-index (length old-data)))))) -(defun eglot--semtok-request (beg end) +(cl-defun eglot--semtok-request (beg end) "Ask server for tokens. Font-lock flush from BEG to END." (let* ((buf (current-buffer)) (id eglot--versioned-identifier)) - (cl-flet ((req (method from to params cont) - ;; (trace-values - ;; "Requesting: " from to method params) - (eglot--async-request - (eglot--current-server-or-lose) method params - :success-fn - (lambda (response) - (eglot--when-live-buffer buf + (cl-labels ((req (method from to params cont) + ;; (trace-values + ;; "Requesting: " from to method params) + (eglot--async-request + (eglot--current-server-or-lose) method params + :success-fn + (lambda (response) ;; (trace-values "Response: " eglot--versioned-identifier id ;; "edits: " ;; (length (cl-getf response :edits)) ;; "data: " ;; (length (cl-getf response :data))) - (when (eq id eglot--versioned-identifier) - ;; FIXME: If `id' is not `eglot--versioned-identifier' - ;; should we re-send the reqquest? - ;; - ;; JT@2025-11-11: Good question. Probably, and I - ;; would hope `font-lock-flush' eventually does - ;; that for us, so I moved it out of the `when'. - (setq eglot--semtok-cache - (list :documentVersion id - :method method - :response (funcall cont response) - :from from :to to))) - (font-lock-flush beg end))) - :hint method)) - (cache-get (&rest path) - (let ((x eglot--semtok-cache)) - (dolist (op path x) (setq x (if (natnump op) (aref x op) - (plist-get x op))))))) + (eglot--when-live-buffer buf + ;; A user edit may have come in while the request + ;; was inflight, changing the state of the buffer... + (when (eq id eglot--versioned-identifier) + (setq eglot--semtok-cache + (list :documentVersion id + :method method + :response (funcall cont response) + :from from :to to))) + ;; ... but we should flush unconditionally. If + ;; this response is out-of-date, + ;; `eglot--semtok-font-lock' should just trigger + ;; another request. + (cl-loop for (b . e) in eglot--semtok-inflight + do (font-lock-flush b e)) + ;; (trace-values "Flushed" (length eglot--semtok-inflight) + ;; "regions") + (setq eglot--semtok-inflight nil))) + :hint method)) + (cache-get (&rest path) + (let ((x eglot--semtok-cache)) + (dolist (op path x) (setq x (if (natnump op) (aref x op) + (plist-get x op))))))) + (push (cons beg end) eglot--semtok-inflight) (cond ((and (eglot-server-capable :semanticTokensProvider :full :delta) (cache-get :response :data) (not (eq :textDocument/semanticTokens/range (cache-get :method)))) + ;; JT@2025-11-12: many back-to-back calls for + ;; `eglot--semtok-request' and small regions occur even on + ;; trivial/fast edits. Even though it's fairly cheap to send + ;; multiple delta requests, it's nicer to just send just one. + (when (cdr eglot--semtok-inflight) + (cl-return-from eglot--semtok-request)) (req :textDocument/semanticTokens/full/delta (point-min) (point-max) (list :textDocument (eglot--TextDocumentIdentifier) :previousResultId (cache-get :response :resultId)) (lambda (response) (if-let* ((edits (plist-get response :edits))) (progn - (plist-put response :data (eglot--semtok-apply-delta-edits - (cache-get :response :data) - edits))) + (plist-put response :data + (eglot--semtok-apply-delta-edits + (cache-get :response :data) + edits))) ;; server sent full response instead, so just record that. response)))) ((eglot-server-capable :semanticTokensProvider :range) commit 2d153ab44adcac36ab5da5c393f1c8e856afdb53 Author: João Távora Date: Tue Nov 11 22:44:41 2025 +0000 Eglot: rework semtok again to use only font-lock (bug#79374) Stefan's idea to use ONLY font-lock instead of jit-lock seems more promising and less buggy. I did have to add this slightly odd eglot--semtok-font-lock-2 kludge. Other than that, it seems fine. * lisp/progmodes/eglot.el (eglot-semantic-tokens-mode): Now use ONLY font-lock. (eglot--semtok-request): Rework, don't call painting function here. (eglot--semtok-font-lock): Rename from eglot--semtok-jit-lock and use a single arg. Maybe call eglot--semtok-font-lock-2. Return nil or risk crashing Emacs. (eglot--semtok-font-lock-1): Rework slightly. (eglot--semtok-font-lock-2): New helper. (eglot-region-range): Delete. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 91e19d3b319..82d3e27f3b3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1295,11 +1295,6 @@ If optional MARKERS, make markers instead." (end (eglot--lsp-position-to-point (plist-get range :end) markers))) (cons beg end))) -(defun eglot-region-range (beg end) - "Return a LSP range representing region BEG to END." - (list :start (eglot--pos-to-lsp-position beg) - :end (eglot--pos-to-lsp-position end))) - (defun eglot-server-capable (&rest feats) "Determine if current server is capable of FEATS." (unless (cl-some (lambda (feat) @@ -4653,10 +4648,11 @@ If NOERROR, return predicate, else erroring function." (cond (eglot-semantic-tokens-mode (if (not (eglot-server-capable :semanticTokensProvider)) (eglot-semantic-tokens-mode -1) - (jit-lock-register #'eglot--semtok-jit-lock 'contextual))) + (font-lock-add-keywords nil '((eglot--semtok-font-lock)) 'append) + (font-lock-flush))) (t - (font-lock-flush) - (jit-lock-unregister #'eglot--semtok-jit-lock)))) + (font-lock-remove-keywords nil '((eglot--semtok-font-lock))) + (font-lock-flush)))) (defvar-local eglot--semtok-cache nil "Cache of the last response from the server.") @@ -4690,12 +4686,18 @@ If NOERROR, return predicate, else erroring function." ;; "data: " ;; (length (cl-getf response :data))) (when (eq id eglot--versioned-identifier) + ;; FIXME: If `id' is not `eglot--versioned-identifier' + ;; should we re-send the reqquest? + ;; + ;; JT@2025-11-11: Good question. Probably, and I + ;; would hope `font-lock-flush' eventually does + ;; that for us, so I moved it out of the `when'. (setq eglot--semtok-cache (list :documentVersion id :method method :response (funcall cont response) - :from from :to to)) - (eglot--semtok-jit-lock-1 beg end)))) + :from from :to to))) + (font-lock-flush beg end))) :hint method)) (cache-get (&rest path) (let ((x eglot--semtok-cache)) @@ -4719,7 +4721,8 @@ If NOERROR, return predicate, else erroring function." ((eglot-server-capable :semanticTokensProvider :range) (req :textDocument/semanticTokens/range beg end (list :textDocument (eglot--TextDocumentIdentifier) - :range (eglot-region-range beg end)) + :range (list :start (eglot--pos-to-lsp-position beg) + :end (eglot--pos-to-lsp-position end))) (lambda (response) (when eglot--semtok-idle-timer (cancel-timer eglot--semtok-idle-timer)) @@ -4735,28 +4738,30 @@ If NOERROR, return predicate, else erroring function." (list :textDocument (eglot--TextDocumentIdentifier)) #'identity)))))) -(defun eglot--semtok-jit-lock (beg end) - "Endeavor to update semantic tokens properties from BEG to END. +(cl-defun eglot--semtok-font-lock (limit &aux (beg (point)) (end limit)) + "Endeavor to semantically font-lock from point until LIMIT. Either do it immediately if the information available is up-to-date or request new information from the server and return and hope the font lock machinery calls us again." (cond ((and (eq (plist-get eglot--semtok-cache :documentVersion) eglot--versioned-identifier) (and (<= (plist-get eglot--semtok-cache :from) beg) - (<= end (plist-get eglot--semtok-cache :to)))) - (eglot--semtok-jit-lock-1 beg end) - `(jit-lock-bounds ,beg . ,end)) + (<= beg (plist-get eglot--semtok-cache :to)))) + (eglot--semtok-font-lock-1 beg end)) (t - (eglot--semtok-request beg end) - `(jit-lock-bounds ,0 . ,0)))) + (eglot--semtok-font-lock-2 beg end) + (eglot--semtok-request beg end))) + nil) -(defun eglot--semtok-jit-lock-1 (beg end) +(defun eglot--semtok-font-lock-1 (beg end &optional data) + "Do the face-painting work for `eglot--semtok-font-lock'." (eglot--widening (with-silent-modifications - (remove-list-of-text-properties beg end '(font-lock-face)) + (remove-list-of-text-properties beg end '(eglot--semtok-token + eglot--semtok-faces)) (goto-char (point-min)) (cl-loop - with data = (plist-get (plist-get eglot--semtok-cache :response) :data) + with data = (or data (plist-get (plist-get eglot--semtok-cache :response) :data)) with column = 0 with p-beg = 0 with p-end = 0 for i from 0 below (length data) by 5 when (> (aref data i) 0) do @@ -4769,14 +4774,34 @@ lock machinery calls us again." (setq p-beg (point)) (funcall eglot-move-to-linepos-function (+ column (aref data (+ i 2)))) (setq p-end (point)) - (let ((tok (cons (aref data (+ i 3)) - (aref data (+ i 4))))) - (put-text-property p-beg p-end 'eglot-semantic-token tok) - (dolist (f (eglot--semtok-token-faces tok)) - (add-face-text-property p-beg p-end f ;; 'append - ))) + (let* ((tok (cons (aref data (+ i 3)) + (aref data (+ i 4)))) + (faces (eglot--semtok-token-faces tok))) + ;; The `eglot--semtok-token' prop doesn't serve much purpose: + ;; just for debug... + (put-text-property p-beg p-end 'eglot--semtok-token tok) + (put-text-property p-beg p-end 'eglot--semtok-faces faces) + (dolist (f faces) + (add-face-text-property p-beg p-end f))) count 1 into napplied)))) +(defun eglot--semtok-font-lock-2 (beg end) + ;; JT@2025-11-11: FIXME: I wish I didn't need this kludge but the + ;; faces applied earlier with `add-face-text-property' from + ;; `eglot--semtok-font-lock-1' disappear for a moment while the + ;; request is in flight. + "Repaint from stale-but-not-that-much local properties." + (eglot--widening + (with-silent-modifications + (save-excursion + (cl-loop + initially (goto-char beg) + for match = (text-property-search-forward 'eglot--semtok-faces) + while (and match (< (point) end)) + do (dolist (f (prop-match-value match)) + (add-face-text-property + (prop-match-beginning match) (prop-match-end match) f))))))) + ;;; Call and type hierarchies (require 'button) commit 7ead81da1d1f5130e6c23454900e3820cf3ed104 Author: João Távora Date: Tue Nov 11 00:11:00 2025 +0000 Eglot: rework semtok to use only jit-lock (bug#79374) Kept the general structure but clarified the code paths. Simplify many things. Instead of font-lock-add-keywords, use jit-lock-register exclusively. Works, but slightly problematic, since when editing the buffer the existing enriched face information is first removed, then re-added. Will try other approaches. * lisp/progmodes/eglot.el (eglot-lsp-server): Tweak doc. (eglot--semtok-request-full-on-idle): Delete. Integrated into caller. (eglot--semtok-jit-lock): Rename from eglot--semtok-propertize. Rework. (eglot--semtok-jit-lock-1): Extract from old eglot--semtok-propertize. (eglot-semantic-tokens-mode): Use new eglot--semtok-jit-lock name. (eglot--semtok-put-cache) (eglot--semtok-ingest-range-response) (eglot--semtok-ingest-delta-response) (eglot--semtok-ingest-full-response): Delete helpers. (eglot--semtok-apply-delta-edits): Rework with more cl-loopy idioms. (eglot--semtok-flush-region): Delete. (eglot--semtok-request): Now takes a region as argument. Rework extensively. (eglot-connect-hook): Don't put eglot--semtok-initialize here. (eglot--connect): Rather here. (eglot--semtok-token-faces): New helper. (eglot--semtok-font-lock): Delete. (eglot--semtok-initialize): Rework. (eglot-semantic-tokens-mode): Simplify. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e789dd93a21..91e19d3b319 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1233,7 +1233,7 @@ object." :documentation "Semantic tokens modifier faces.") (semtok-modifier-cache :initform (make-hash-table) - :documentation "A hashmap of modifier values to the selected faces.")) + :documentation "Map LSP modifier values to the selected faces.")) :documentation "Represents a server. Wraps a process for LSP communication.") @@ -1665,8 +1665,7 @@ Use current server's or first available Eglot events buffer." (jsonrpc-forget-pending-continuations server)) (defvar eglot-connect-hook - '(eglot-signal-didChangeConfiguration - eglot--semtok-initialize) + '(eglot-signal-didChangeConfiguration) "Hook run after connecting to a server. Each function is passed an `eglot-lsp-server' instance as argument.") @@ -1821,6 +1820,7 @@ This docstring appeases checkdoc, that's all." (gethash project eglot--servers-by-project)) (setf (eglot--capabilities server) capabilities) (setf (eglot--server-info server) serverInfo) + (eglot--semtok-initialize server) (jsonrpc-notify server :initialized eglot--{}) (dolist (buffer (buffer-list)) (with-current-buffer buffer @@ -4594,42 +4594,26 @@ If NOERROR, return predicate, else erroring function." ;;; Semantic tokens -(defun eglot--semtok-font-lock (limit) - "Apply face property for tokens from point until LIMIT. -Intended for `font-lock-add-keywords'." - (with-slots ((faces semtok-faces) +(defun eglot--semtok-token-faces (tok) + (with-slots (semtok-faces (modifier-faces semtok-modifier-faces) (modifier-cache semtok-modifier-cache)) (eglot-current-server) - (let (beg (end (point)) tok) - (while (and (< end limit) - (setq beg (text-property-not-all end limit 'eglot-semantic-token nil)) - (setq end (next-single-property-change beg 'eglot-semantic-token nil limit)) - (setq tok (get-text-property beg 'eglot-semantic-token))) - (when-let* ((face (aref faces (car tok)))) - (add-face-text-property beg end face)) - (let* ((code (cdr tok)) - (faces (gethash code modifier-cache 'not-found))) - (when (eq faces 'not-found) - (setq faces (cl-loop for j from 0 below (length modifier-faces) - if (> (logand code (ash 1 j)) 0) - if (aref modifier-faces j) - collect (aref modifier-faces j))) - (puthash code faces modifier-cache)) - (dolist (face faces) (add-face-text-property beg end face))))) - nil)) + (let* ((code (cdr tok)) + (mods (gethash code modifier-cache 'not-found))) + (when (eq mods 'not-found) + (setq mods (cl-loop for j from 0 below (length modifier-faces) + if (> (logand code (ash 1 j)) 0) + if (aref modifier-faces j) + collect (aref modifier-faces j))) + (puthash code mods modifier-cache)) + (if-let* ((main (aref semtok-faces (car tok)))) + (cons main mods) + mods)))) (defvar-local eglot--semtok-idle-timer nil "Idle timer to request full semantic tokens.") -(defun eglot--semtok-request-full-on-idle () - "Make a full semantic tokens request after an idle timer." - (let* ((buf (current-buffer)) - (fun (lambda () - (eglot--when-live-buffer buf (eglot--semtok-request))))) - (when eglot--semtok-idle-timer (cancel-timer eglot--semtok-idle-timer)) - (setq eglot--semtok-idle-timer (run-with-idle-timer (* 3 eglot-send-changes-idle-time) nil fun)))) - (cl-defmethod eglot-handle-request (server (_method (eql workspace/semanticTokens/refresh))) "Handle a semanticTokens/refresh request from SERVER." @@ -4650,182 +4634,148 @@ Intended for `font-lock-add-keywords'." (defun eglot--semtok-initialize (server) "Initialize SERVER for semantic tokens." - (cl-destructuring-bind (&key tokenTypes tokenModifiers &allow-other-keys) - (plist-get (plist-get (eglot--capabilities server) - :semanticTokensProvider) - :legend) - (oset server semtok-faces - (eglot--semtok-build-face-map - tokenTypes eglot-semantic-tokens-faces - "semantic token" "eglot-semantic-tokens-faces")) - (oset server semtok-modifier-faces - (eglot--semtok-build-face-map - tokenModifiers eglot-semantic-tokens-modifier-faces - "semantic token modifier" "eglot-semantic-tokens-modifier-faces")))) + (with-slots (semtok-faces semtok-modifier-faces capabilities) server + ;; FIXME: eglot-dbind + (cl-destructuring-bind (&key tokenTypes tokenModifiers &allow-other-keys) + (plist-get (plist-get capabilities :semanticTokensProvider) :legend) + (setq semtok-faces + (eglot--semtok-build-face-map + tokenTypes eglot-semantic-tokens-faces + "semantic token" 'eglot-semantic-tokens-faces) + semtok-modifier-faces + (eglot--semtok-build-face-map + tokenModifiers eglot-semantic-tokens-modifier-faces + "semantic token modifier" 'eglot-semantic-tokens-modifier-faces))))) (define-minor-mode eglot-semantic-tokens-mode "Minor mode for fontifying buffer with LSP server's semantic tokens." :global nil - (when eglot-semantic-tokens-mode - (if (not (eglot-server-capable :semanticTokensProvider)) - (eglot-semantic-tokens-mode -1) - (with-silent-modifications - (save-restriction - (widen) - (remove-list-of-text-properties - (point-min) (point-max) '(eglot--semtok-propertized)))) - (jit-lock-register #'eglot--semtok-propertize) - (font-lock-add-keywords nil '((eglot--semtok-font-lock)) 'append) - (font-lock-flush))) - (unless eglot-semantic-tokens-mode - (jit-lock-unregister #'eglot--semtok-propertize) - (font-lock-remove-keywords nil '((eglot--semtok-font-lock))) - (font-lock-flush))) + (cond (eglot-semantic-tokens-mode + (if (not (eglot-server-capable :semanticTokensProvider)) + (eglot-semantic-tokens-mode -1) + (jit-lock-register #'eglot--semtok-jit-lock 'contextual))) + (t + (font-lock-flush) + (jit-lock-unregister #'eglot--semtok-jit-lock)))) (defvar-local eglot--semtok-cache nil "Cache of the last response from the server.") -(defsubst eglot--semtok-put-cache (k v) - "Set key K of `eglot-semantic-tokens--cache' to V." - (setq eglot--semtok-cache - (plist-put eglot--semtok-cache k v))) - -(defun eglot--semtok-ingest-range-response (response) - "Handle RESPONSE to semanticTokens/range request." - (eglot--semtok-put-cache :response response) - (cl-assert (plist-get eglot--semtok-cache :region))) - -(defun eglot--semtok-ingest-full-response (response) - "Handle RESPONSE to semanticTokens/full request." - (eglot--semtok-put-cache :response response) - (cl-assert (not (plist-get eglot--semtok-cache :region)))) - (defsubst eglot--semtok-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 (plist-get edit :start)) - (push (substring old-data old-token-index (plist-get edit :start)) substrings)) - (push (plist-get edit :data) substrings) - (setq old-token-index (+ (plist-get edit :start) (plist-get edit :deleteCount))) - finally do (push (substring old-data old-token-index old-token-count) substrings)) - (apply #'vconcat (nreverse substrings)))) - -(defun eglot--semtok-ingest-delta-response (response) - "Handle RESPONSE to semanticTokens/full/delta request." - (if-let* ((edits (plist-get response :edits))) - (progn - (cl-assert (not (plist-get eglot--semtok-cache :region))) - (when-let* ((old-data (plist-get (plist-get eglot--semtok-cache :response) :data))) - (eglot--semtok-put-cache - :response - (plist-put response :data (eglot--semtok-apply-delta-edits old-data edits))))) - ;; server decided to send full response instead - (eglot--semtok-ingest-full-response response))) - -(defvar-local eglot--semtok-flush-region nil - "Region whose fontification is pending to be flushed.") - -(defun eglot--semtok-expand-flush-region (beg end) - "Expand the flush region to contain the lines from BEG to END." - (setq beg (save-excursion (goto-char beg) (eglot--bol))) - (cl-symbol-macrolet ((r eglot--semtok-flush-region)) - (setq r (if r (cons (min beg (car r)) (max end (cdr r))) - (cons beg end))))) - -(defun eglot--semtok-request () - "Send semantic tokens request to the language server." - (let* ((region eglot--semtok-flush-region) - (method :textDocument/semanticTokens/full) - (params (list :textDocument (eglot--TextDocumentIdentifier))) - (response-handler #'eglot--semtok-ingest-full-response) - (buf (current-buffer)) - (id eglot--versioned-identifier) - (final-region)) - (cond - ((and (eglot-server-capable :semanticTokensProvider :full :delta) - (let ((response (plist-get eglot--semtok-cache :response))) - (and (plist-get response :resultId) (plist-get response :data) - (not (plist-get eglot--semtok-cache :region))))) - (setq method :textDocument/semanticTokens/full/delta) - (setq response-handler #'eglot--semtok-ingest-delta-response) - (setq params - (plist-put params :previousResultId - (plist-get (plist-get eglot--semtok-cache :response) :resultId)))) - ((and region (eglot-server-capable :semanticTokensProvider :range)) - (setq method :textDocument/semanticTokens/range) - (setq final-region region) - (setq params - (plist-put params :range - (eglot-region-range (car region) (cdr region)))) - (setq response-handler #'eglot--semtok-ingest-range-response))) - (eglot--async-request - (eglot--current-server-or-lose) method params - :success-fn - (lambda (response) - (eglot--when-live-buffer buf - ;; this is to avoid requesting again, when the following sequence of events happen: - ;; Request tokens (1) ---> - ;; DocumentChanged ---> - ;; Request tokens (deferred, 2) ---> - ;; <--- (1) Tokens ! outdated, but should not trigger another request - ;; <--- (2) Tokens ! ok - (when (eq id eglot--versioned-identifier) - (eglot--semtok-put-cache :documentVersion id) - (eglot--semtok-put-cache :region final-region) - (setq eglot--semtok-flush-region nil) - (funcall response-handler response) - (when final-region (eglot--semtok-request-full-on-idle)) - (when region (font-lock-flush (car region) (cdr region)))))) - :hint #'eglot--semtok-request))) - -(defun eglot--semtok-propertize (beg end) - "Update the semantic tokens text properties from BEG to END. -Also request new tokens from the server, if necessary." - (if (not (and eglot--semtok-cache - (plist-get eglot--semtok-cache :response) - (eq (plist-get eglot--semtok-cache :documentVersion) - eglot--versioned-identifier) - (if-let* ((token-region (plist-get eglot--semtok-cache :region))) - (and (<= (car token-region) beg) (<= end (cdr token-region))) - t))) - (progn (eglot--semtok-expand-flush-region beg end) - (eglot--semtok-request)) - (eglot--widening - (with-silent-modifications - ;; when full tokens are available, add some margins for performance - (unless (plist-get eglot--semtok-cache :region) - (setq beg (max (point-min) (- beg (* 5 jit-lock-chunk-size)))) - (setq end (min (point-max) (+ end (* 5 jit-lock-chunk-size))))) - (when-let* ((beg (text-property-not-all beg end 'eglot--semtok-propertized - eglot--versioned-identifier))) - (setq beg (prog2 (goto-char beg) (eglot--bol))) - (when (eq (get-text-property end 'eglot--semtok-propertized) - eglot--versioned-identifier) - (setq end (previous-single-property-change end 'eglot--semtok-propertized nil beg))) - (let* ((data (plist-get (plist-get eglot--semtok-cache :response) :data)) - (i-max (length data)) - (property-beg) - (property-end)) - (remove-list-of-text-properties beg end '(eglot-semantic-token)) - (goto-char (point-min)) - (cl-do ((i 0 (+ i 5)) (column 0)) ((>= i i-max)) - (when (> (aref data i) 0) - (setq column 0) - (forward-line (aref data i))) - (unless (< (point) beg) - (setq column (+ column (aref data (+ i 1)))) - (funcall eglot-move-to-linepos-function column) - (when (> (point) end) (cl-return)) - (setq property-beg (point)) - (funcall eglot-move-to-linepos-function (+ column (aref data (+ i 2)))) - (setq property-end (point)) - (put-text-property property-beg property-end 'eglot-semantic-token - (cons (aref data (+ i 3)) - (aref data (+ i 4)))))) - (put-text-property beg end 'eglot--semtok-propertized eglot--versioned-identifier))))))) + (cl-loop + for old-token-index = 0 then (+ (plist-get edit :start) (plist-get edit :deleteCount)) + for edit across edits + when (< old-token-index (plist-get edit :start)) + vconcat (substring old-data old-token-index (plist-get edit :start)) into new + vconcat (plist-get edit :data) into new + finally + (cl-return (vconcat new (substring old-data old-token-index (length old-data)))))) + +(defun eglot--semtok-request (beg end) + "Ask server for tokens. Font-lock flush from BEG to END." + (let* ((buf (current-buffer)) + (id eglot--versioned-identifier)) + (cl-flet ((req (method from to params cont) + ;; (trace-values + ;; "Requesting: " from to method params) + (eglot--async-request + (eglot--current-server-or-lose) method params + :success-fn + (lambda (response) + (eglot--when-live-buffer buf + ;; (trace-values "Response: " eglot--versioned-identifier id + ;; "edits: " + ;; (length (cl-getf response :edits)) + ;; "data: " + ;; (length (cl-getf response :data))) + (when (eq id eglot--versioned-identifier) + (setq eglot--semtok-cache + (list :documentVersion id + :method method + :response (funcall cont response) + :from from :to to)) + (eglot--semtok-jit-lock-1 beg end)))) + :hint method)) + (cache-get (&rest path) + (let ((x eglot--semtok-cache)) + (dolist (op path x) (setq x (if (natnump op) (aref x op) + (plist-get x op))))))) + (cond + ((and (eglot-server-capable :semanticTokensProvider :full :delta) + (cache-get :response :data) + (not (eq :textDocument/semanticTokens/range (cache-get :method)))) + (req :textDocument/semanticTokens/full/delta (point-min) (point-max) + (list :textDocument (eglot--TextDocumentIdentifier) + :previousResultId (cache-get :response :resultId)) + (lambda (response) + (if-let* ((edits (plist-get response :edits))) + (progn + (plist-put response :data (eglot--semtok-apply-delta-edits + (cache-get :response :data) + edits))) + ;; server sent full response instead, so just record that. + response)))) + ((eglot-server-capable :semanticTokensProvider :range) + (req :textDocument/semanticTokens/range beg end + (list :textDocument (eglot--TextDocumentIdentifier) + :range (eglot-region-range beg end)) + (lambda (response) + (when eglot--semtok-idle-timer + (cancel-timer eglot--semtok-idle-timer)) + (setq eglot--semtok-idle-timer + (run-with-idle-timer (* 3 eglot-send-changes-idle-time) nil + (lambda () + (eglot--when-live-buffer buf + (eglot--semtok-request + (point-min) (point-max)))))) + response))) + (t + (req :textDocument/semanticTokens/full (point-min) (point-max) + (list :textDocument (eglot--TextDocumentIdentifier)) + #'identity)))))) + +(defun eglot--semtok-jit-lock (beg end) + "Endeavor to update semantic tokens properties from BEG to END. +Either do it immediately if the information available is up-to-date or +request new information from the server and return and hope the font +lock machinery calls us again." + (cond ((and (eq (plist-get eglot--semtok-cache :documentVersion) + eglot--versioned-identifier) + (and (<= (plist-get eglot--semtok-cache :from) beg) + (<= end (plist-get eglot--semtok-cache :to)))) + (eglot--semtok-jit-lock-1 beg end) + `(jit-lock-bounds ,beg . ,end)) + (t + (eglot--semtok-request beg end) + `(jit-lock-bounds ,0 . ,0)))) + +(defun eglot--semtok-jit-lock-1 (beg end) + (eglot--widening + (with-silent-modifications + (remove-list-of-text-properties beg end '(font-lock-face)) + (goto-char (point-min)) + (cl-loop + with data = (plist-get (plist-get eglot--semtok-cache :response) :data) + with column = 0 with p-beg = 0 with p-end = 0 + for i from 0 below (length data) by 5 + when (> (aref data i) 0) do + (setq column 0) + (forward-line (aref data i)) + unless (< (point) beg) do + (setq column (+ column (aref data (+ i 1)))) + (funcall eglot-move-to-linepos-function column) + (when (> (point) end) (cl-return napplied)) + (setq p-beg (point)) + (funcall eglot-move-to-linepos-function (+ column (aref data (+ i 2)))) + (setq p-end (point)) + (let ((tok (cons (aref data (+ i 3)) + (aref data (+ i 4))))) + (put-text-property p-beg p-end 'eglot-semantic-token tok) + (dolist (f (eglot--semtok-token-faces tok)) + (add-face-text-property p-beg p-end f ;; 'append + ))) + count 1 into napplied)))) ;;; Call and type hierarchies commit c65a556829c11c475aa1c1bb2e3502e9cc89bd86 Author: João Távora Date: Tue Nov 11 12:01:33 2025 +0000 Eglot: fix bug on server-intiated semtok refresh (bug#79374) src/emacs -Q src/cm.c \ --eval '(trace-function (quote eglot--semtok-propertize))' \ --eval '(trace-function (quote eglot--semtok-request))' \ --eval '(trace-function (quote font-lock-flush))' -f eglot More trace-values will show that too many full requests are being sent. * lisp/progmodes/eglot.el (eglot-handle-request): Don't bump eglot--versioned-identifier, since that confuses semtok bootstrap in a new file with clangd. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0cf5a36ffc5..e789dd93a21 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -4635,8 +4635,7 @@ Intended for `font-lock-add-keywords'." "Handle a semanticTokens/refresh request from SERVER." (dolist (buffer (eglot--managed-buffers server)) (eglot--when-live-buffer buffer - (cl-incf eglot--versioned-identifier) - (font-lock-flush)))) + (unless (zerop eglot--versioned-identifier) (font-lock-flush))))) (defun eglot--semtok-build-face-map (identifiers faces category varname) "Build map of FACES for IDENTIFIERS using CATEGORY and VARNAME." commit cfd77501d7f32c27ce1e6f7818a5732582663ef7 Author: João Távora Date: Mon Nov 10 23:37:47 2025 +0000 Eglot: rework semtok code (bug#79374) * lisp/progmodes/eglot.el (eglot-semantic-tokens-faces) (eglot-semantic-tokens-modifier-faces): Move up to defcustom section. (eglot--semtok-request-full-on-idle, eglot-handle-request) (eglot--semtok-build-face-map, eglot--semtok-initialize) (eglot-semantic-tokens-mode): Move into semantic tokens section. (eglot--semtok-font-lock): Rename from eglot--semtok-fontify-tokens. (eglot-semantic-tokens-mode): Use eglot--semtok-font-lock. (eglot--semtok-ingest-delta-response): Rename from eglot--semtok-ingest-full/delta-response (eglot--semtok-request): Use new function. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 89b305b808a..0cf5a36ffc5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -141,8 +141,6 @@ (defvar company-tooltip-align-annotations) (defvar tramp-ssh-controlmaster-options) (defvar tramp-use-ssh-controlmaster-options) -(defvar eglot-semantic-tokens-faces) -(defvar eglot-semantic-tokens-modifier-faces) ;;; Obsolete aliases @@ -631,6 +629,71 @@ Note additionally: (string :tag "Specify your own"))) :package-version '(Eglot . "1.19")) +(defcustom eglot-semantic-tokens-faces + '(("namespace" . font-lock-keyword-face) + ("type" . font-lock-type-face) + ("class" . font-lock-type-face) + ("enum" . font-lock-type-face) + ("interface" . font-lock-type-face) + ("struct" . font-lock-type-face) + ("typeParameter" . font-lock-type-face) + ("parameter" . font-lock-variable-name-face) + ("variable" . font-lock-variable-name-face) + ("property" . font-lock-property-use-face) + ("enumMember" . font-lock-constant-face) + ("event" . font-lock-variable-name-face) + ("function" . font-lock-function-name-face) + ("method" . font-lock-function-name-face) + ("macro" . font-lock-preprocessor-face) + ("keyword" . font-lock-keyword-face) + ("modifier" . font-lock-function-name-face) + ("comment" . font-lock-comment-face) + ("string" . font-lock-string-face) + ("number" . font-lock-constant-face) + ("regexp" . font-lock-string-face) + ("operator" . font-lock-function-name-face) + ("decorator" . font-lock-type-face)) + "Alist of faces to use to highlight semantic tokens. +Each element is a cons cell whose car is a token type name and cdr is +the face to use." + :type `(alist :key-type (string :tag "Token name") + :value-type (choice (face :tag "Face") + (plist :tag "Face Attributes" + :key-type + (choice + ,@(mapcar + (lambda (cell) + `(const :tag ,(capitalize + (cdr cell)) + ,(car cell))) + face-attribute-name-alist)))))) + +(defcustom eglot-semantic-tokens-modifier-faces + '(("declaration" . font-lock-function-name-face) + ("definition" . font-lock-function-name-face) + ("readonly" . font-lock-constant-face) + ("static" . font-lock-keyword-face) + ("deprecated" . eglot-diagnostic-tag-deprecated-face) + ("abstract" . font-lock-keyword-face) + ("async" . font-lock-preprocessor-face) + ("modification" . font-lock-function-name-face) + ("documentation" . font-lock-doc-face) + ("defaultLibrary" . font-lock-builtin-face)) + "List of face to use to highlight tokens with modifiers. +Each element is a cons cell whose car is a modifier name and cdr is +the face to use." + :type `(alist :key-type (string :tag "Token name") + :value-type (choice (face :tag "Face") + (plist :tag "Face Attributes" + :key-type + (choice + ,@(mapcar + (lambda (cell) + `(const :tag ,(capitalize + (cdr cell)) + ,(car cell))) + face-attribute-name-alist)))))) + (defvar eglot-withhold-process-id nil "If non-nil, Eglot will not send the Emacs process id to the language server. This can be useful when using docker to run a language server.") @@ -4531,75 +4594,95 @@ If NOERROR, return predicate, else erroring function." ;;; Semantic tokens - -(defcustom eglot-semantic-tokens-faces - '(("namespace" . font-lock-keyword-face) - ("type" . font-lock-type-face) - ("class" . font-lock-type-face) - ("enum" . font-lock-type-face) - ("interface" . font-lock-type-face) - ("struct" . font-lock-type-face) - ("typeParameter" . font-lock-type-face) - ("parameter" . font-lock-variable-name-face) - ("variable" . font-lock-variable-name-face) - ("property" . font-lock-property-use-face) - ("enumMember" . font-lock-constant-face) - ("event" . font-lock-variable-name-face) - ("function" . font-lock-function-name-face) - ("method" . font-lock-function-name-face) - ("macro" . font-lock-preprocessor-face) - ("keyword" . font-lock-keyword-face) - ("modifier" . font-lock-function-name-face) - ("comment" . font-lock-comment-face) - ("string" . font-lock-string-face) - ("number" . font-lock-constant-face) - ("regexp" . font-lock-string-face) - ("operator" . font-lock-function-name-face) - ("decorator" . font-lock-type-face)) - "Alist of faces to use to highlight semantic tokens. -Each element is a cons cell whose car is a token type name and cdr is -the face to use." - :type `(alist :key-type (string :tag "Token name") - :value-type (choice (face :tag "Face") - (plist :tag "Face Attributes" - :key-type - (choice - ,@(mapcar - (lambda (cell) - `(const :tag ,(capitalize - (cdr cell)) - ,(car cell))) - face-attribute-name-alist)))))) - -(defcustom eglot-semantic-tokens-modifier-faces - '(("declaration" . font-lock-function-name-face) - ("definition" . font-lock-function-name-face) - ("readonly" . font-lock-constant-face) - ("static" . font-lock-keyword-face) - ("deprecated" . eglot-diagnostic-tag-deprecated-face) - ("abstract" . font-lock-keyword-face) - ("async" . font-lock-preprocessor-face) - ("modification" . font-lock-function-name-face) - ("documentation" . font-lock-doc-face) - ("defaultLibrary" . font-lock-builtin-face)) - "List of face to use to highlight tokens with modifiers. -Each element is a cons cell whose car is a modifier name and cdr is -the face to use." - :type `(alist :key-type (string :tag "Token name") - :value-type (choice (face :tag "Face") - (plist :tag "Face Attributes" - :key-type - (choice - ,@(mapcar - (lambda (cell) - `(const :tag ,(capitalize - (cdr cell)) - ,(car cell))) - face-attribute-name-alist)))))) +(defun eglot--semtok-font-lock (limit) + "Apply face property for tokens from point until LIMIT. +Intended for `font-lock-add-keywords'." + (with-slots ((faces semtok-faces) + (modifier-faces semtok-modifier-faces) + (modifier-cache semtok-modifier-cache)) + (eglot-current-server) + (let (beg (end (point)) tok) + (while (and (< end limit) + (setq beg (text-property-not-all end limit 'eglot-semantic-token nil)) + (setq end (next-single-property-change beg 'eglot-semantic-token nil limit)) + (setq tok (get-text-property beg 'eglot-semantic-token))) + (when-let* ((face (aref faces (car tok)))) + (add-face-text-property beg end face)) + (let* ((code (cdr tok)) + (faces (gethash code modifier-cache 'not-found))) + (when (eq faces 'not-found) + (setq faces (cl-loop for j from 0 below (length modifier-faces) + if (> (logand code (ash 1 j)) 0) + if (aref modifier-faces j) + collect (aref modifier-faces j))) + (puthash code faces modifier-cache)) + (dolist (face faces) (add-face-text-property beg end face))))) + nil)) (defvar-local eglot--semtok-idle-timer nil "Idle timer to request full semantic tokens.") +(defun eglot--semtok-request-full-on-idle () + "Make a full semantic tokens request after an idle timer." + (let* ((buf (current-buffer)) + (fun (lambda () + (eglot--when-live-buffer buf (eglot--semtok-request))))) + (when eglot--semtok-idle-timer (cancel-timer eglot--semtok-idle-timer)) + (setq eglot--semtok-idle-timer (run-with-idle-timer (* 3 eglot-send-changes-idle-time) nil fun)))) + +(cl-defmethod eglot-handle-request + (server (_method (eql workspace/semanticTokens/refresh))) + "Handle a semanticTokens/refresh request from SERVER." + (dolist (buffer (eglot--managed-buffers server)) + (eglot--when-live-buffer buffer + (cl-incf eglot--versioned-identifier) + (font-lock-flush)))) + +(defun eglot--semtok-build-face-map (identifiers faces category varname) + "Build map of FACES for IDENTIFIERS using CATEGORY and VARNAME." + (vconcat + (mapcar (lambda (id) + (let ((maybe-face (cdr (assoc id faces)))) + (when (not maybe-face) + (eglot--warn "No face has been associated to the %s `%s': consider adding a corresponding definition to %s" + category id varname)) + maybe-face)) + identifiers))) + +(defun eglot--semtok-initialize (server) + "Initialize SERVER for semantic tokens." + (cl-destructuring-bind (&key tokenTypes tokenModifiers &allow-other-keys) + (plist-get (plist-get (eglot--capabilities server) + :semanticTokensProvider) + :legend) + (oset server semtok-faces + (eglot--semtok-build-face-map + tokenTypes eglot-semantic-tokens-faces + "semantic token" "eglot-semantic-tokens-faces")) + (oset server semtok-modifier-faces + (eglot--semtok-build-face-map + tokenModifiers eglot-semantic-tokens-modifier-faces + "semantic token modifier" "eglot-semantic-tokens-modifier-faces")))) + +(define-minor-mode eglot-semantic-tokens-mode + "Minor mode for fontifying buffer with LSP server's semantic tokens." + :global nil + (when eglot-semantic-tokens-mode + (if (not (eglot-server-capable :semanticTokensProvider)) + (eglot-semantic-tokens-mode -1) + (with-silent-modifications + (save-restriction + (widen) + (remove-list-of-text-properties + (point-min) (point-max) '(eglot--semtok-propertized)))) + (jit-lock-register #'eglot--semtok-propertize) + (font-lock-add-keywords nil '((eglot--semtok-font-lock)) 'append) + (font-lock-flush))) + (unless eglot-semantic-tokens-mode + (jit-lock-unregister #'eglot--semtok-propertize) + (font-lock-remove-keywords nil '((eglot--semtok-font-lock))) + (font-lock-flush))) + (defvar-local eglot--semtok-cache nil "Cache of the last response from the server.") @@ -4631,7 +4714,7 @@ the face to use." finally do (push (substring old-data old-token-index old-token-count) substrings)) (apply #'vconcat (nreverse substrings)))) -(defun eglot--semtok-ingest-full/delta-response (response) +(defun eglot--semtok-ingest-delta-response (response) "Handle RESPONSE to semanticTokens/full/delta request." (if-let* ((edits (plist-get response :edits))) (progn @@ -4668,7 +4751,7 @@ the face to use." (and (plist-get response :resultId) (plist-get response :data) (not (plist-get eglot--semtok-cache :region))))) (setq method :textDocument/semanticTokens/full/delta) - (setq response-handler #'eglot--semtok-ingest-full/delta-response) + (setq response-handler #'eglot--semtok-ingest-delta-response) (setq params (plist-put params :previousResultId (plist-get (plist-get eglot--semtok-cache :response) :resultId)))) @@ -4745,91 +4828,6 @@ Also request new tokens from the server, if necessary." (aref data (+ i 4)))))) (put-text-property beg end 'eglot--semtok-propertized eglot--versioned-identifier))))))) -(defun eglot--semtok-fontify-tokens (limit) - "Apply face property for tokens from point until LIMIT." - (with-slots ((faces semtok-faces) - (modifier-faces semtok-modifier-faces) - (modifier-cache semtok-modifier-cache)) - (eglot-current-server) - (let (beg (end (point)) tok) - (while (and (< end limit) - (setq beg (text-property-not-all end limit 'eglot-semantic-token nil)) - (setq end (next-single-property-change beg 'eglot-semantic-token nil limit)) - (setq tok (get-text-property beg 'eglot-semantic-token))) - (when-let* ((face (aref faces (car tok)))) - (add-face-text-property beg end face)) - (let* ((code (cdr tok)) - (faces (gethash code modifier-cache 'not-found))) - (when (eq faces 'not-found) - (setq faces (cl-loop for j from 0 below (length modifier-faces) - if (> (logand code (ash 1 j)) 0) - if (aref modifier-faces j) - collect (aref modifier-faces j))) - (puthash code faces modifier-cache)) - (dolist (face faces) (add-face-text-property beg end face))))) - nil)) - -(defun eglot--semtok-request-full-on-idle () - "Make a full semantic tokens request after an idle timer." - (let* ((buf (current-buffer)) - (fun (lambda () - (eglot--when-live-buffer buf (eglot--semtok-request))))) - (when eglot--semtok-idle-timer (cancel-timer eglot--semtok-idle-timer)) - (setq eglot--semtok-idle-timer (run-with-idle-timer (* 3 eglot-send-changes-idle-time) nil fun)))) - -(cl-defmethod eglot-handle-request - (server (_method (eql workspace/semanticTokens/refresh))) - "Handle a semanticTokens/refresh request from SERVER." - (dolist (buffer (eglot--managed-buffers server)) - (eglot--when-live-buffer buffer - (cl-incf eglot--versioned-identifier) - (font-lock-flush)))) - -(defun eglot--semtok-build-face-map (identifiers faces category varname) - "Build map of FACES for IDENTIFIERS using CATEGORY and VARNAME." - (vconcat - (mapcar (lambda (id) - (let ((maybe-face (cdr (assoc id faces)))) - (when (not maybe-face) - (eglot--warn "No face has been associated to the %s `%s': consider adding a corresponding definition to %s" - category id varname)) - maybe-face)) - identifiers))) - -(defun eglot--semtok-initialize (server) - "Initialize SERVER for semantic tokens." - (cl-destructuring-bind (&key tokenTypes tokenModifiers &allow-other-keys) - (plist-get (plist-get (eglot--capabilities server) - :semanticTokensProvider) - :legend) - (oset server semtok-faces - (eglot--semtok-build-face-map - tokenTypes eglot-semantic-tokens-faces - "semantic token" "eglot-semantic-tokens-faces")) - (oset server semtok-modifier-faces - (eglot--semtok-build-face-map - tokenModifiers eglot-semantic-tokens-modifier-faces - "semantic token modifier" "eglot-semantic-tokens-modifier-faces")))) - -(define-minor-mode eglot-semantic-tokens-mode - "Minor mode for fontifying buffer with LSP server's semantic tokens." - :global nil - (when eglot-semantic-tokens-mode - (if (not (eglot-server-capable :semanticTokensProvider)) - (eglot-semantic-tokens-mode -1) - (with-silent-modifications - (save-restriction - (widen) - (remove-list-of-text-properties - (point-min) (point-max) '(eglot--semtok-propertized)))) - (jit-lock-register #'eglot--semtok-propertize) - (font-lock-add-keywords nil '((eglot--semtok-fontify-tokens)) 'append) - (font-lock-flush))) - (unless eglot-semantic-tokens-mode - (jit-lock-unregister #'eglot--semtok-propertize) - (font-lock-remove-keywords nil '((eglot--semtok-fontify-tokens))) - (font-lock-flush))) - ;;; Call and type hierarchies (require 'button) commit 51d0b3ef9899de85b52214d8621ad40f25648d99 Author: Lua Viana Reis Date: Mon Nov 10 13:25:26 2025 +0000 Eglot: add semantic token (semtok) support (bug#79374) * lisp/progmodes/eglot.el (eglot-semantic-tokens-faces) (eglot-semantic-tokens-modifier-faces): New defcustom.. (eglot-ignored-server-capabilities): Tweak. (eglot-client-capabilities): Advertise semtok support. (eglot-lsp-server): Tweak. (eglot-region-range): New helper. (eglot-connect-hook): Add eglot--semtok-initialize. (eglot--maybe-activate-editing-mode): Activate eglot-semantic-tokens-mode. (eglot--semtok-idle-timer, eglot--semtok-cache) (eglot--semtok-put-cache, eglot--semtok-ingest-range-response) (eglot--semtok-ingest-full-response) (eglot--semtok-apply-delta-edits) (eglot--semtok-ingest-full/delta-response) (eglot--semtok-flush-region, eglot--semtok-request) (eglot--semtok-propertize, eglot--semtok-fontify-tokens) (eglot--semtok-request-full-on-idle): New helpers. (eglot-handle-request workspace/semanticTokens/refresh): New request handler. (eglot--semtok-build-face-map, eglot--semtok-initialize): New helpers. (eglot-semantic-tokens-mode): New minor mode. (desktop): Mention eglot-semantic-tokens-mode. (command-modes tweak): Add eglot-semantic-tokens-mode. Co-authored-by: João Távora diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9b84c38349a..89b305b808a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -141,6 +141,8 @@ (defvar company-tooltip-align-annotations) (defvar tramp-ssh-controlmaster-options) (defvar tramp-use-ssh-controlmaster-options) +(defvar eglot-semantic-tokens-faces) +(defvar eglot-semantic-tokens-modifier-faces) ;;; Obsolete aliases @@ -573,6 +575,7 @@ under cursor." (const :tag "Fold regions of buffer" :foldingRangeProvider) (const :tag "Execute custom commands" :executeCommandProvider) (const :tag "Inlay hints" :inlayHintProvider) + (const :tag "Semantic tokens" :semanticTokensProvider) (const :tag "Type hierarchies" :typeHierarchyProvider) (const :tag "Call hierarchies" :callHierarchyProvider))) @@ -1018,6 +1021,7 @@ object." `(:dynamicRegistration ,(if (eglot--trampish-p s) :json-false t)) :symbol `(:dynamicRegistration :json-false) + :semanticTokens '(:refreshSupport t) :configuration t :workspaceFolders t) :textDocument @@ -1080,6 +1084,13 @@ object." :formatting `(:dynamicRegistration :json-false) :rangeFormatting `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) + :semanticTokens `(:dynamicRegistration :json-false + :requests '(:range t :full (:delta t)) + :tokenModifiers [,@(mapcar #'car eglot-semantic-tokens-modifier-faces)] + :overlappingTokenSupport t + :multilineTokenSupport t + :tokenTypes [,@(mapcar #'car eglot-semantic-tokens-faces)] + :formats ["relative"]) :inlayHint `(:dynamicRegistration :json-false) :callHierarchy `(:dynamicRegistration :json-false) :typeHierarchy `(:dynamicRegistration :json-false) @@ -1150,7 +1161,16 @@ object." :accessor eglot--managed-buffers) (saved-initargs :documentation "Saved initargs for reconnection purposes." - :accessor eglot--saved-initargs)) + :accessor eglot--saved-initargs) + (semtok-faces + :initform nil + :documentation "Semantic tokens faces.") + (semtok-modifier-faces + :initform nil + :documentation "Semantic tokens modifier faces.") + (semtok-modifier-cache + :initform (make-hash-table) + :documentation "A hashmap of modifier values to the selected faces.")) :documentation "Represents a server. Wraps a process for LSP communication.") @@ -1212,6 +1232,11 @@ If optional MARKERS, make markers instead." (end (eglot--lsp-position-to-point (plist-get range :end) markers))) (cons beg end))) +(defun eglot-region-range (beg end) + "Return a LSP range representing region BEG to END." + (list :start (eglot--pos-to-lsp-position beg) + :end (eglot--pos-to-lsp-position end))) + (defun eglot-server-capable (&rest feats) "Determine if current server is capable of FEATS." (unless (cl-some (lambda (feat) @@ -1577,7 +1602,8 @@ Use current server's or first available Eglot events buffer." (jsonrpc-forget-pending-continuations server)) (defvar eglot-connect-hook - '(eglot-signal-didChangeConfiguration) + '(eglot-signal-didChangeConfiguration + eglot--semtok-initialize) "Hook run after connecting to a server. Each function is passed an `eglot-lsp-server' instance as argument.") @@ -2297,6 +2323,7 @@ If it is activated, also signal textDocument/didOpen." ;; Run user hook after 'textDocument/didOpen' so server knows ;; about the buffer. (eglot-inlay-hints-mode 1) + (eglot-semantic-tokens-mode 1) (run-hooks 'eglot-managed-mode-hook)))) (add-hook 'after-change-major-mode-hook #'eglot--maybe-activate-editing-mode) @@ -4502,6 +4529,307 @@ If NOERROR, return predicate, else erroring function." (jit-lock-unregister #'eglot--update-hints) (remove-overlays nil nil 'eglot--inlay-hint t)))) + +;;; Semantic tokens + +(defcustom eglot-semantic-tokens-faces + '(("namespace" . font-lock-keyword-face) + ("type" . font-lock-type-face) + ("class" . font-lock-type-face) + ("enum" . font-lock-type-face) + ("interface" . font-lock-type-face) + ("struct" . font-lock-type-face) + ("typeParameter" . font-lock-type-face) + ("parameter" . font-lock-variable-name-face) + ("variable" . font-lock-variable-name-face) + ("property" . font-lock-property-use-face) + ("enumMember" . font-lock-constant-face) + ("event" . font-lock-variable-name-face) + ("function" . font-lock-function-name-face) + ("method" . font-lock-function-name-face) + ("macro" . font-lock-preprocessor-face) + ("keyword" . font-lock-keyword-face) + ("modifier" . font-lock-function-name-face) + ("comment" . font-lock-comment-face) + ("string" . font-lock-string-face) + ("number" . font-lock-constant-face) + ("regexp" . font-lock-string-face) + ("operator" . font-lock-function-name-face) + ("decorator" . font-lock-type-face)) + "Alist of faces to use to highlight semantic tokens. +Each element is a cons cell whose car is a token type name and cdr is +the face to use." + :type `(alist :key-type (string :tag "Token name") + :value-type (choice (face :tag "Face") + (plist :tag "Face Attributes" + :key-type + (choice + ,@(mapcar + (lambda (cell) + `(const :tag ,(capitalize + (cdr cell)) + ,(car cell))) + face-attribute-name-alist)))))) + +(defcustom eglot-semantic-tokens-modifier-faces + '(("declaration" . font-lock-function-name-face) + ("definition" . font-lock-function-name-face) + ("readonly" . font-lock-constant-face) + ("static" . font-lock-keyword-face) + ("deprecated" . eglot-diagnostic-tag-deprecated-face) + ("abstract" . font-lock-keyword-face) + ("async" . font-lock-preprocessor-face) + ("modification" . font-lock-function-name-face) + ("documentation" . font-lock-doc-face) + ("defaultLibrary" . font-lock-builtin-face)) + "List of face to use to highlight tokens with modifiers. +Each element is a cons cell whose car is a modifier name and cdr is +the face to use." + :type `(alist :key-type (string :tag "Token name") + :value-type (choice (face :tag "Face") + (plist :tag "Face Attributes" + :key-type + (choice + ,@(mapcar + (lambda (cell) + `(const :tag ,(capitalize + (cdr cell)) + ,(car cell))) + face-attribute-name-alist)))))) + +(defvar-local eglot--semtok-idle-timer nil + "Idle timer to request full semantic tokens.") + +(defvar-local eglot--semtok-cache nil + "Cache of the last response from the server.") + +(defsubst eglot--semtok-put-cache (k v) + "Set key K of `eglot-semantic-tokens--cache' to V." + (setq eglot--semtok-cache + (plist-put eglot--semtok-cache k v))) + +(defun eglot--semtok-ingest-range-response (response) + "Handle RESPONSE to semanticTokens/range request." + (eglot--semtok-put-cache :response response) + (cl-assert (plist-get eglot--semtok-cache :region))) + +(defun eglot--semtok-ingest-full-response (response) + "Handle RESPONSE to semanticTokens/full request." + (eglot--semtok-put-cache :response response) + (cl-assert (not (plist-get eglot--semtok-cache :region)))) + +(defsubst eglot--semtok-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 (plist-get edit :start)) + (push (substring old-data old-token-index (plist-get edit :start)) substrings)) + (push (plist-get edit :data) substrings) + (setq old-token-index (+ (plist-get edit :start) (plist-get edit :deleteCount))) + finally do (push (substring old-data old-token-index old-token-count) substrings)) + (apply #'vconcat (nreverse substrings)))) + +(defun eglot--semtok-ingest-full/delta-response (response) + "Handle RESPONSE to semanticTokens/full/delta request." + (if-let* ((edits (plist-get response :edits))) + (progn + (cl-assert (not (plist-get eglot--semtok-cache :region))) + (when-let* ((old-data (plist-get (plist-get eglot--semtok-cache :response) :data))) + (eglot--semtok-put-cache + :response + (plist-put response :data (eglot--semtok-apply-delta-edits old-data edits))))) + ;; server decided to send full response instead + (eglot--semtok-ingest-full-response response))) + +(defvar-local eglot--semtok-flush-region nil + "Region whose fontification is pending to be flushed.") + +(defun eglot--semtok-expand-flush-region (beg end) + "Expand the flush region to contain the lines from BEG to END." + (setq beg (save-excursion (goto-char beg) (eglot--bol))) + (cl-symbol-macrolet ((r eglot--semtok-flush-region)) + (setq r (if r (cons (min beg (car r)) (max end (cdr r))) + (cons beg end))))) + +(defun eglot--semtok-request () + "Send semantic tokens request to the language server." + (let* ((region eglot--semtok-flush-region) + (method :textDocument/semanticTokens/full) + (params (list :textDocument (eglot--TextDocumentIdentifier))) + (response-handler #'eglot--semtok-ingest-full-response) + (buf (current-buffer)) + (id eglot--versioned-identifier) + (final-region)) + (cond + ((and (eglot-server-capable :semanticTokensProvider :full :delta) + (let ((response (plist-get eglot--semtok-cache :response))) + (and (plist-get response :resultId) (plist-get response :data) + (not (plist-get eglot--semtok-cache :region))))) + (setq method :textDocument/semanticTokens/full/delta) + (setq response-handler #'eglot--semtok-ingest-full/delta-response) + (setq params + (plist-put params :previousResultId + (plist-get (plist-get eglot--semtok-cache :response) :resultId)))) + ((and region (eglot-server-capable :semanticTokensProvider :range)) + (setq method :textDocument/semanticTokens/range) + (setq final-region region) + (setq params + (plist-put params :range + (eglot-region-range (car region) (cdr region)))) + (setq response-handler #'eglot--semtok-ingest-range-response))) + (eglot--async-request + (eglot--current-server-or-lose) method params + :success-fn + (lambda (response) + (eglot--when-live-buffer buf + ;; this is to avoid requesting again, when the following sequence of events happen: + ;; Request tokens (1) ---> + ;; DocumentChanged ---> + ;; Request tokens (deferred, 2) ---> + ;; <--- (1) Tokens ! outdated, but should not trigger another request + ;; <--- (2) Tokens ! ok + (when (eq id eglot--versioned-identifier) + (eglot--semtok-put-cache :documentVersion id) + (eglot--semtok-put-cache :region final-region) + (setq eglot--semtok-flush-region nil) + (funcall response-handler response) + (when final-region (eglot--semtok-request-full-on-idle)) + (when region (font-lock-flush (car region) (cdr region)))))) + :hint #'eglot--semtok-request))) + +(defun eglot--semtok-propertize (beg end) + "Update the semantic tokens text properties from BEG to END. +Also request new tokens from the server, if necessary." + (if (not (and eglot--semtok-cache + (plist-get eglot--semtok-cache :response) + (eq (plist-get eglot--semtok-cache :documentVersion) + eglot--versioned-identifier) + (if-let* ((token-region (plist-get eglot--semtok-cache :region))) + (and (<= (car token-region) beg) (<= end (cdr token-region))) + t))) + (progn (eglot--semtok-expand-flush-region beg end) + (eglot--semtok-request)) + (eglot--widening + (with-silent-modifications + ;; when full tokens are available, add some margins for performance + (unless (plist-get eglot--semtok-cache :region) + (setq beg (max (point-min) (- beg (* 5 jit-lock-chunk-size)))) + (setq end (min (point-max) (+ end (* 5 jit-lock-chunk-size))))) + (when-let* ((beg (text-property-not-all beg end 'eglot--semtok-propertized + eglot--versioned-identifier))) + (setq beg (prog2 (goto-char beg) (eglot--bol))) + (when (eq (get-text-property end 'eglot--semtok-propertized) + eglot--versioned-identifier) + (setq end (previous-single-property-change end 'eglot--semtok-propertized nil beg))) + (let* ((data (plist-get (plist-get eglot--semtok-cache :response) :data)) + (i-max (length data)) + (property-beg) + (property-end)) + (remove-list-of-text-properties beg end '(eglot-semantic-token)) + (goto-char (point-min)) + (cl-do ((i 0 (+ i 5)) (column 0)) ((>= i i-max)) + (when (> (aref data i) 0) + (setq column 0) + (forward-line (aref data i))) + (unless (< (point) beg) + (setq column (+ column (aref data (+ i 1)))) + (funcall eglot-move-to-linepos-function column) + (when (> (point) end) (cl-return)) + (setq property-beg (point)) + (funcall eglot-move-to-linepos-function (+ column (aref data (+ i 2)))) + (setq property-end (point)) + (put-text-property property-beg property-end 'eglot-semantic-token + (cons (aref data (+ i 3)) + (aref data (+ i 4)))))) + (put-text-property beg end 'eglot--semtok-propertized eglot--versioned-identifier))))))) + +(defun eglot--semtok-fontify-tokens (limit) + "Apply face property for tokens from point until LIMIT." + (with-slots ((faces semtok-faces) + (modifier-faces semtok-modifier-faces) + (modifier-cache semtok-modifier-cache)) + (eglot-current-server) + (let (beg (end (point)) tok) + (while (and (< end limit) + (setq beg (text-property-not-all end limit 'eglot-semantic-token nil)) + (setq end (next-single-property-change beg 'eglot-semantic-token nil limit)) + (setq tok (get-text-property beg 'eglot-semantic-token))) + (when-let* ((face (aref faces (car tok)))) + (add-face-text-property beg end face)) + (let* ((code (cdr tok)) + (faces (gethash code modifier-cache 'not-found))) + (when (eq faces 'not-found) + (setq faces (cl-loop for j from 0 below (length modifier-faces) + if (> (logand code (ash 1 j)) 0) + if (aref modifier-faces j) + collect (aref modifier-faces j))) + (puthash code faces modifier-cache)) + (dolist (face faces) (add-face-text-property beg end face))))) + nil)) + +(defun eglot--semtok-request-full-on-idle () + "Make a full semantic tokens request after an idle timer." + (let* ((buf (current-buffer)) + (fun (lambda () + (eglot--when-live-buffer buf (eglot--semtok-request))))) + (when eglot--semtok-idle-timer (cancel-timer eglot--semtok-idle-timer)) + (setq eglot--semtok-idle-timer (run-with-idle-timer (* 3 eglot-send-changes-idle-time) nil fun)))) + +(cl-defmethod eglot-handle-request + (server (_method (eql workspace/semanticTokens/refresh))) + "Handle a semanticTokens/refresh request from SERVER." + (dolist (buffer (eglot--managed-buffers server)) + (eglot--when-live-buffer buffer + (cl-incf eglot--versioned-identifier) + (font-lock-flush)))) + +(defun eglot--semtok-build-face-map (identifiers faces category varname) + "Build map of FACES for IDENTIFIERS using CATEGORY and VARNAME." + (vconcat + (mapcar (lambda (id) + (let ((maybe-face (cdr (assoc id faces)))) + (when (not maybe-face) + (eglot--warn "No face has been associated to the %s `%s': consider adding a corresponding definition to %s" + category id varname)) + maybe-face)) + identifiers))) + +(defun eglot--semtok-initialize (server) + "Initialize SERVER for semantic tokens." + (cl-destructuring-bind (&key tokenTypes tokenModifiers &allow-other-keys) + (plist-get (plist-get (eglot--capabilities server) + :semanticTokensProvider) + :legend) + (oset server semtok-faces + (eglot--semtok-build-face-map + tokenTypes eglot-semantic-tokens-faces + "semantic token" "eglot-semantic-tokens-faces")) + (oset server semtok-modifier-faces + (eglot--semtok-build-face-map + tokenModifiers eglot-semantic-tokens-modifier-faces + "semantic token modifier" "eglot-semantic-tokens-modifier-faces")))) + +(define-minor-mode eglot-semantic-tokens-mode + "Minor mode for fontifying buffer with LSP server's semantic tokens." + :global nil + (when eglot-semantic-tokens-mode + (if (not (eglot-server-capable :semanticTokensProvider)) + (eglot-semantic-tokens-mode -1) + (with-silent-modifications + (save-restriction + (widen) + (remove-list-of-text-properties + (point-min) (point-max) '(eglot--semtok-propertized)))) + (jit-lock-register #'eglot--semtok-propertize) + (font-lock-add-keywords nil '((eglot--semtok-fontify-tokens)) 'append) + (font-lock-flush))) + (unless eglot-semantic-tokens-mode + (jit-lock-unregister #'eglot--semtok-propertize) + (font-lock-remove-keywords nil '((eglot--semtok-fontify-tokens))) + (font-lock-flush))) + ;;; Call and type hierarchies (require 'button) @@ -4736,7 +5064,8 @@ If NOERROR, return predicate, else erroring function." ;; harder. For now, use `with-eval-after-load'. See also github#1183. (with-eval-after-load 'desktop (add-to-list 'desktop-minor-mode-handlers '(eglot--managed-mode . ignore)) - (add-to-list 'desktop-minor-mode-handlers '(eglot-inlay-hints-mode . ignore))) + (add-to-list 'desktop-minor-mode-handlers '(eglot-inlay-hints-mode . ignore)) + (add-to-list 'desktop-minor-mode-handlers '(eglot-semantic-tokens-mode . ignore))) ;;; Misc @@ -4765,6 +5094,7 @@ If NOERROR, return predicate, else erroring function." eglot-format eglot-format-buffer eglot-inlay-hints-mode + eglot-semantic-tokens-mode eglot-reconnect eglot-rename eglot-signal-didChangeConfiguration commit f9c94e05f5b6102c8665d22c1bbf8c8262ee28cb Author: Sean Whitton Date: Wed Nov 12 15:00:00 2025 +0000 vc-do-async-command: Print how long command took to run * lisp/vc/vc-dispatcher.el (vc-do-async-command): Print how long the command took to run. diff --git a/lisp/vc/vc-dispatcher.el b/lisp/vc/vc-dispatcher.el index 4bc0d9887f5..bce23529b47 100644 --- a/lisp/vc/vc-dispatcher.el +++ b/lisp/vc/vc-dispatcher.el @@ -544,9 +544,26 @@ of a buffer, which is created. ROOT should be the directory in which the command should be run. The process object is returned. Display the buffer in some window, but don't select it." - (let ((dir default-directory) - (inhibit-read-only t) - proc) + (letrec ((dir default-directory) + (start-time) (proc) + (finished-fun + (lambda (proc _msg) + (cond ((not (buffer-live-p buffer)) + (remove-function (process-sentinel proc) + finished-fun)) + ((not (eq (process-status proc) 'run)) + (remove-function (process-sentinel proc) + finished-fun) + (with-current-buffer buffer + (save-excursion + (goto-char (process-mark proc)) + (let ((inhibit-read-only t)) + (insert + (format "Finished in %f seconds\n" + (time-to-seconds + (time-since start-time)))) + (set-marker (process-mark proc) + (point)))))))))) (setq buffer (get-buffer-create buffer)) (if (get-buffer-process buffer) (error "Another VC action on %s is running" root)) @@ -555,6 +572,7 @@ Display the buffer in some window, but don't select it." (let* (;; Run in the original working directory. (default-directory dir) (orig-fun vc-filter-command-function) + (inhibit-read-only t) (vc-filter-command-function (lambda (&rest args) (cl-destructuring-bind (&whole args cmd _ flags) @@ -579,7 +597,9 @@ Display the buffer in some window, but don't select it." (insert flag)))) (insert "'...\n") args)))) - (setq proc (apply #'vc-do-command t 'async command nil args)))) + (setq start-time (current-time) + proc (apply #'vc-do-command t 'async command nil args)))) + (add-function :after (process-sentinel proc) finished-fun) (vc--display-async-command-buffer buffer) proc)) commit e7696c64a9272b7f26433e39df7a0dad9c15f7bb Author: Sean Whitton Date: Wed Nov 12 14:21:15 2025 +0000 ; * lisp/vc/vc-dispatcher.el (vc-do-command): Improve docstring. diff --git a/lisp/vc/vc-dispatcher.el b/lisp/vc/vc-dispatcher.el index 1642a168bef..4bc0d9887f5 100644 --- a/lisp/vc/vc-dispatcher.el +++ b/lisp/vc/vc-dispatcher.el @@ -380,6 +380,7 @@ Intended to be used as the value of `vc-filter-command-function'." ;;;###autoload (defun vc-do-command (destination okstatus command file-or-list &rest flags) "Execute an inferior command, notifying user and checking for errors. + DESTINATION specifies what to do with COMMAND's output. It can be a buffer or the name of a buffer to insert output there, t to mean the current buffer, or nil to discard output. @@ -390,17 +391,19 @@ STDERR-FILE may only be nil which means to discard standard error output or t which means to mix it with standard output. If the destination for standard output is a buffer that is not the current buffer, set up the buffer properly and erase it. -The command is considered successful if its exit status does not exceed -OKSTATUS (if OKSTATUS is nil, that means to ignore error status, if it -is `async', that means not to wait for termination of the subprocess; if -it is t it means to ignore all execution errors). + +OKSTATUS `async' means not to wait for termination of the subprocess and +return the process object. Otherwise, OKSTATUS determines when to +signal an error instead of returning a numeric exit status or signal +description string. OKSTATUS an integer means to signal an error if the +command's exit status exceeds that value or the command is killed by a +signal, nil means to signal an error only if the command is killed by a +signal, and t means never to signal an error. + FILE-OR-LIST is the name of a working file; it may be a list of files or be nil (to execute commands that don't expect a file name or set of files). If an optional list of FLAGS is present, -that is inserted into the command line before the filename. - -Return the return value of the inferior command in the synchronous case, -and the process object in the asynchronous case." +that is inserted into the command line before the filename." ;; STDERR-FILE is limited to nil or t, instead of also supporting ;; putting stderr output into a buffer or file, because of how we ;; support both synchronous and asynchronous execution. commit 1484e3108a980729d0b3613ced7f1e905935f9c8 Author: Sean Whitton Date: Wed Nov 12 13:08:17 2025 +0000 vc-do-command: Restore using a pipe in the async case * lisp/vc/vc-dispatcher.el (vc-do-command): Restore using a pipe in the async case. This was unintentionally changed when adding support for discarding output. diff --git a/lisp/vc/vc-dispatcher.el b/lisp/vc/vc-dispatcher.el index 75f3e75e0cb..1642a168bef 100644 --- a/lisp/vc/vc-dispatcher.el +++ b/lisp/vc/vc-dispatcher.el @@ -472,7 +472,7 @@ and the process object in the asynchronous case." (make-process :name command :buffer (and stdout (current-buffer)) :command (cons command squeezed) - :connection-type nil + :connection-type 'pipe :filter #'vc-process-filter :sentinel #'ignore :stderr stderr-buf commit 6969b530287a3733332c7295a3d4c26bb60b82d7 Author: Eli Zaretskii Date: Tue Nov 11 21:36:33 2025 +0200 ; * lisp/progmodes/hideshow.el (hs-hideable-region-p): Doc fix. diff --git a/lisp/progmodes/hideshow.el b/lisp/progmodes/hideshow.el index 68be0d59abd..57188fe6996 100644 --- a/lisp/progmodes/hideshow.el +++ b/lisp/progmodes/hideshow.el @@ -683,8 +683,8 @@ Skip \"internal\" overlays if `hs-allow-nesting' is non-nil." (hs--refresh-indicators from to)) (defun hs-hideable-region-p (&optional beg end) - "Return t if region in BEG and END can be hidden. -If BEG and END are not specified, it will try to check at the current + "Return t if region between BEG and END can be hidden. +If BEG and END are not specified, try to check the current block at point." ;; Check if BEG and END are not in the same line number, ;; since using `count-lines' is slow. commit 45a82437a3ea651db9efa3215588cd828e06c79c Merge: b398a6c6293 ca4af1768d6 Author: Po Lu Date: Wed Nov 12 01:14:37 2025 +0800 Merge from savannah/emacs-30 ca4af1768d6 Fix crash on Android 2.2 commit ca4af1768d663b41f13b49455242fda59ba1e74f Author: Po Lu Date: Wed Nov 12 01:13:46 2025 +0800 Fix crash on Android 2.2 * src/android-asset.h (AAssetManager_open): Initialize desc and asset to NULL, lest `desc' be accessed uninitialized if C_NAME does not exist in the directory tree. diff --git a/src/android-asset.h b/src/android-asset.h index a0206ec9459..b65875faa13 100644 --- a/src/android-asset.h +++ b/src/android-asset.h @@ -186,15 +186,13 @@ static AAsset * AAssetManager_open (AAssetManager *manager, const char *c_name, int mode) { - jobject desc; + jobject desc = NULL; jstring name; - AAsset *asset; + AAsset *asset = NULL; const char *asset_dir; jlong st_size = -1; /* Push a local frame. */ - asset = NULL; - (*(manager->env))->PushLocalFrame (manager->env, 3); if ((*(manager->env))->ExceptionCheck (manager->env)) @@ -226,8 +224,6 @@ AAssetManager_open (AAssetManager *manager, const char *c_name, stream directly upon the first attempt to read from the asset, sidestepping the intermediate AssetFileDescriptor. */ - desc = NULL; - if (st_size < 0) /* Otherwise attempt to open an ``AssetFileDescriptor''. */ desc = (*(manager->env))->CallObjectMethod (manager->env, commit b398a6c6293fd96f45cfdcc0b65c734156ed1258 Author: Michael Albinus Date: Tue Nov 11 16:40:33 2025 +0100 Extend Tramp direct async processes for further methods * lisp/net/tramp-container.el (tramp-methods) : * lisp/net/tramp-sh.el (tramp-methods) : Add `tramp-direct-async'. (tramp-expand-script): Fix typo in script name. (tramp-maybe-send-script): Adapt docstring. diff --git a/lisp/net/tramp-container.el b/lisp/net/tramp-container.el index 6e82bc67be1..5ac2e46819a 100644 --- a/lisp/net/tramp-container.el +++ b/lisp/net/tramp-container.el @@ -701,8 +701,8 @@ see its function help for a description of the format." (tramp-login-program ,tramp-distrobox-program) (tramp-login-args (("enter") ("-n" "%h") - ("--" "%l"))) - ;(tramp-direct-async (,tramp-default-remote-shell "-c")) + ("--") ("%l"))) + (tramp-direct-async (,tramp-default-remote-shell "-c")) (tramp-remote-shell ,tramp-default-remote-shell) (tramp-remote-shell-login ("-l")) (tramp-remote-shell-args ("-c")) @@ -761,7 +761,14 @@ see its function help for a description of the format." ("--env" ,(format "TERM=%s" tramp-terminal-type)) ("instance://%h") - ("%h"))) ; Needed for multi-hop check. + ;; Needed for multi-hop check, + ;; ignored by the "shell" command. + ("%h"))) + ;; `tramp-direct-async' must be used *instead* of `tramp-login-args'. + ;; (tramp-direct-async (("exec") + ;; ("--env" + ;; ,(format "TERM=%s" tramp-terminal-type)) + ;; ("instance://%h")) (tramp-remote-shell ,tramp-default-remote-shell) (tramp-remote-shell-login ("-l")) (tramp-remote-shell-args ("-c")) diff --git a/lisp/net/tramp-message.el b/lisp/net/tramp-message.el index d349179d0e6..aafceaf3335 100644 --- a/lisp/net/tramp-message.el +++ b/lisp/net/tramp-message.el @@ -172,6 +172,7 @@ They are completed by `M-x TAB' only in Tramp debug buffers." (use-local-map special-mode-map) (set-buffer-modified-p nil) ;; For debugging purposes. + ;(add-hook 'kill-buffer-hook #'debug nil 'local) (local-set-key "\M-n" 'clone-buffer) (add-hook 'clone-buffer-hook #'tramp-setup-debug-buffer nil 'local)) diff --git a/lisp/net/tramp-sh.el b/lisp/net/tramp-sh.el index fb5f8a21ceb..e55af1f9659 100644 --- a/lisp/net/tramp-sh.el +++ b/lisp/net/tramp-sh.el @@ -303,6 +303,7 @@ The string is used in `tramp-methods'.") `("sg" (tramp-login-program "sg") (tramp-login-args (("-") ("%u"))) + (tramp-direct-async ("-c")) (tramp-remote-shell ,tramp-default-remote-shell) (tramp-remote-shell-args ("-c")) (tramp-connection-timeout 10))) @@ -4073,8 +4074,8 @@ If VEC is nil, the respective local commands are used." (bundle (when (string-match-p (rx (| bol (not "%")) "%b") script) (tramp-maybe-send-script vec tramp-bundle-read-file-names - "tramp_bundle_read_file-names") - "tramp_bundle_read_file-names")) + "tramp_bundle_read_file_names") + "tramp_bundle_read_file_names")) (hdmp (when (string-match-p (rx (| bol (not "%")) "%h") script) (or (if vec (tramp-get-remote-hexdump vec) @@ -4142,7 +4143,7 @@ If VEC is nil, the respective local commands are used." ?p perl ?q test-e ?r readlink ?s stat ?t tmp ?y python))))) (defun tramp-maybe-send-script (vec script name) - "Define in remote shell function NAME implemented as SCRIPT. + "Define a remote shell function NAME implemented as SCRIPT. Only send the definition if it has not already been done." ;; We cannot let-bind (tramp-get-connection-process vec) because it ;; might be nil. commit 4bb1b938dfe2a9daffcd9b046e8de292de13b71b Author: Sean Whitton Date: Tue Nov 11 12:47:31 2025 +0000 ; * lisp/progmodes/hideshow.el (hs-minor-mode): Style fixes. diff --git a/lisp/progmodes/hideshow.el b/lisp/progmodes/hideshow.el index 9e93916661a..68be0d59abd 100644 --- a/lisp/progmodes/hideshow.el +++ b/lisp/progmodes/hideshow.el @@ -27,7 +27,7 @@ ;; * Commands provided ;; -;; This file provides Hideshow Minor Mode. When active, nine commands +;; This file provides the Hideshow minor mode. When active, nine commands ;; are available, implementing block hiding and showing. They (and their ;; keybindings) are: ;; @@ -1395,7 +1395,8 @@ Key bindings: (progn (unless (and comment-start comment-end) (setq hs-minor-mode nil) - (user-error "%S doesn't support Hideshow Minor Mode" major-mode)) + (user-error "%S doesn't support the Hideshow minor mode" + major-mode)) ;; Set the variables (hs-grok-mode-type) commit 3d844e49f7aeac4f439984a8cfc832f6ebb2a407 Author: Sean Whitton Date: Tue Nov 11 12:45:16 2025 +0000 ; * lisp/vc/vc-hooks.el: Note vc-after-revert in after-revert-hook. diff --git a/lisp/vc/vc-hooks.el b/lisp/vc/vc-hooks.el index 32586f57804..d3c98311358 100644 --- a/lisp/vc/vc-hooks.el +++ b/lisp/vc/vc-hooks.el @@ -30,7 +30,8 @@ ;; The noninteractive hooks into the rest of Emacs are: ;; - `vc-refresh-state' in `find-file-hook' ;; - `vc-kill-buffer-hook' in `kill-buffer-hook' -;; - `vc-after-save' which is called by `basic-save-buffer'. +;; - `vc-after-save' which is called by `basic-save-buffer' +;; - `vc-after-revert' in `after-revert-hook'. ;;; Code: commit fd16fcce5225ff5163b962628796bb515dcc9f27 Author: Sean Whitton Date: Tue Nov 11 12:38:38 2025 +0000 ; minibuffer-default-add-shell-commands: Use 'and' not 'unless'. diff --git a/lisp/simple.el b/lisp/simple.el index fdf8b79444d..2ee04177892 100644 --- a/lisp/simple.el +++ b/lisp/simple.el @@ -4317,8 +4317,8 @@ stdout will be intermixed in the output stream.") This function is used to add all related commands retrieved by `shell-command-guess' to the end of the list of defaults just after the default value." - (let* ((filename (unless (consp minibuffer-default) - minibuffer-default)) + (let* ((filename (and (atom minibuffer-default) + minibuffer-default)) (commands (and filename (require 'dired-aux) (shell-command-guess (list filename))))) (setq commands (mapcar (lambda (command)