diff options
| author | João Távora | 2026-01-09 15:45:08 +0000 |
|---|---|---|
| committer | João Távora | 2026-01-11 03:42:01 +0000 |
| commit | 236647ab58e6d3dd0b092e753c54317ad9004f39 (patch) | |
| tree | 8be4bbb7855e7dba090adf88501dc2631c874b65 | |
| parent | fa5a65629262d47d95c70e5e1404b225ce7fb2f8 (diff) | |
| download | emacs-236647ab58e6d3dd0b092e753c54317ad9004f39.tar.gz emacs-236647ab58e6d3dd0b092e753c54317ad9004f39.zip | |
Eglot: spectacular optimization in files with many diagnostics
In a large (or simply long) file with many diagnostics, calling
eglot-range-region repeteadly constantly throws Emacs for a spin
around the buffer, since each diagnostics comes annotated with a
(line/col): LSP range spec that is reasonably expensive to
translate into Elisp point positions.
A much faster approach for such large lists is to first sort all the
objects containing ranges by their start lines and then do a
single pass of the buffer, moving lines by delta.
By much faster, I do mean spectacularly (100x) faster. A long python
with 7000 "ruff" diagnostics, before the change, typical editor
operations (add/delete words) are impossible.
14053 84% - jsonrpc-connection-receive
14052 84% - #<byte-code-function B94>
14052 84% - apply
14052 84% - eglot-handle-notification
14052 84% - applyn
14052 84% - #<byte-code-function 6DB>
14052 84% - eglot--flymake-handle-push
12295 74% - eglot--flymake-make-diag
12218 73% + eglot-range-region
50 0% + eglot--check-object
12 0% plist-member
3 0% flymake-make-diagnostic
After the change:
99 1% - jsonrpc-connection-receive
99 1% - #<byte-code-function 0EE>
99 1% - apply
99 1% - eglot-handle-notification
99 1% - apply
99 1% - #<byte-code-function E84>
99 1% - eglot--flymake-handle-push
99 1% - eglot--call-with-ranged
99 1% - #<byte-code-function 2C6>
99 1% - eglot-move-to-utf-16-linepos
99 1% line-end-position
* lisp/progmodes/eglot.el (eglot-move-to-linepos-function):
Forward declare.
(eglot--call-with-ranged, eglot--collecting-ranged): New helpers.
(eglot--flymake-report-1)
(eglot--imenu-SymbolInformation): Use eglot--collecting-ranged.
(eglot--imenu-DocumentSymbol): Could use eglot--collecting-ranged.
* etc/EGLOT-NEWS: Mention it
| -rw-r--r-- | etc/EGLOT-NEWS | 7 | ||||
| -rw-r--r-- | lisp/progmodes/eglot.el | 91 |
2 files changed, 79 insertions, 19 deletions
diff --git a/etc/EGLOT-NEWS b/etc/EGLOT-NEWS index 8735e966ee9..3fe87d77690 100644 --- a/etc/EGLOT-NEWS +++ b/etc/EGLOT-NEWS | |||
| @@ -20,6 +20,13 @@ https://github.com/joaotavora/eglot/issues/1234. | |||
| 20 | 20 | ||
| 21 | * Changes to upcoming Eglot | 21 | * Changes to upcoming Eglot |
| 22 | 22 | ||
| 23 | ** Dramatically faster handling of files with many diagnostics | ||
| 24 | |||
| 25 | Diagnostic conversion between LSP and Flymake versions is now much | ||
| 26 | faster. Previously, editing, e.g. a Python file with thousands of | ||
| 27 | diagnostics was next to impossible to to periodic interruptions of | ||
| 28 | diagnostic reports. Now it's practically unnoticeable. | ||
| 29 | |||
| 23 | ** Support for LSP server multiplexers via Rassumfrassum | 30 | ** Support for LSP server multiplexers via Rassumfrassum |
| 24 | 31 | ||
| 25 | Eglot can now leverage LSP server multiplexer programs like Rassumfrassum | 32 | Eglot can now leverage LSP server multiplexer programs like Rassumfrassum |
diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f7d38d8e417..82610d093ad 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el | |||
| @@ -1280,6 +1280,61 @@ If optional MARKERS, make markers instead." | |||
| 1280 | (list :start (eglot--pos-to-lsp-position from) | 1280 | (list :start (eglot--pos-to-lsp-position from) |
| 1281 | :end (eglot--pos-to-lsp-position to))) | 1281 | :end (eglot--pos-to-lsp-position to))) |
| 1282 | 1282 | ||
| 1283 | (defvar eglot-move-to-linepos-function) | ||
| 1284 | (cl-defun eglot--call-with-ranged (objs key fn &aux (curline 0)) | ||
| 1285 | (unless key | ||
| 1286 | (setq key (lambda (o) (plist-get o :range)))) | ||
| 1287 | (cl-flet ((moveit (line col) | ||
| 1288 | (forward-line (- line curline)) | ||
| 1289 | (setq curline line) | ||
| 1290 | (unless (eobp) | ||
| 1291 | (unless (wholenump col) (setq col 0)) | ||
| 1292 | (funcall eglot-move-to-linepos-function col)) | ||
| 1293 | (point))) | ||
| 1294 | (eglot--widening | ||
| 1295 | (goto-char (point-min)) | ||
| 1296 | (cl-loop | ||
| 1297 | with pairs = (if key | ||
| 1298 | (mapcar (lambda (obj) (cons obj (funcall key obj))) | ||
| 1299 | objs) | ||
| 1300 | (mapcar (lambda (range) (cons range range)) | ||
| 1301 | objs)) | ||
| 1302 | with sorted = | ||
| 1303 | (sort pairs | ||
| 1304 | (lambda (p1 p2) | ||
| 1305 | (< (plist-get (plist-get (cdr p1) :start) :line) | ||
| 1306 | (plist-get (plist-get (cdr p2) :start) :line)))) | ||
| 1307 | for (object . range) in sorted | ||
| 1308 | for spos = (plist-get range :start) | ||
| 1309 | for epos = (plist-get range :end) | ||
| 1310 | for sline = (plist-get spos :line) | ||
| 1311 | for scol = (plist-get spos :character) | ||
| 1312 | for eline = (plist-get epos :line) | ||
| 1313 | for ecol = (plist-get epos :character) | ||
| 1314 | collect (funcall fn object (cons (moveit sline scol) | ||
| 1315 | (moveit eline ecol))))))) | ||
| 1316 | |||
| 1317 | (cl-defmacro eglot--collecting-ranged ((object-sym region-sym | ||
| 1318 | objects | ||
| 1319 | &optional key) | ||
| 1320 | &rest body) | ||
| 1321 | "Iterate over OBJECTS, binding each element and its region. | ||
| 1322 | For each element in OBJECTS, bind OBJECT-SYM to the element and | ||
| 1323 | REGION-SYM to its computed Emacs region (a cons of buffer positions). | ||
| 1324 | Evaluate BODY and collect the result into a list. Return that list. | ||
| 1325 | |||
| 1326 | KEY, if non-nil, should be a function to extract the LSP range from each | ||
| 1327 | element. If nil, elements are assumed to be plists with `:range' keys. | ||
| 1328 | |||
| 1329 | This macro uses optimized incremental navigation instead of repeatedly | ||
| 1330 | calling `eglot-range-region', providing significant performance benefits | ||
| 1331 | when processing many ranges." | ||
| 1332 | (declare (indent 1) (debug t)) | ||
| 1333 | `(eglot--call-with-ranged | ||
| 1334 | ,objects | ||
| 1335 | ,key | ||
| 1336 | (lambda (,object-sym ,region-sym) ,@body))) | ||
| 1337 | |||
| 1283 | (defun eglot-server-capable (&rest feats) | 1338 | (defun eglot-server-capable (&rest feats) |
| 1284 | "Determine if current server is capable of FEATS." | 1339 | "Determine if current server is capable of FEATS." |
| 1285 | (unless (cl-some (lambda (feat) | 1340 | (unless (cl-some (lambda (feat) |
| @@ -3167,11 +3222,8 @@ version the diagnostics pertain to." | |||
| 3167 | eglot--flymake-report-fn) | 3222 | eglot--flymake-report-fn) |
| 3168 | (when (and ,diags (vectorp ,diags)) | 3223 | (when (and ,diags (vectorp ,diags)) |
| 3169 | (setf ,diags | 3224 | (setf ,diags |
| 3170 | (cl-loop | 3225 | (eglot--collecting-ranged (o r ,diags) |
| 3171 | for d across ,diags | 3226 | (eglot--flymake-make-diag o ,version r)))) |
| 3172 | collect (eglot--flymake-make-diag | ||
| 3173 | d | ||
| 3174 | ,version (eglot-range-region (plist-get d :range)))))) | ||
| 3175 | (eglot--flymake-report-2 ,diags ,mode))) | 3227 | (eglot--flymake-report-2 ,diags ,mode))) |
| 3176 | 3228 | ||
| 3177 | (cl-defmethod eglot-handle-notification | 3229 | (cl-defmethod eglot-handle-notification |
| @@ -4090,20 +4142,20 @@ for which LSP on-type-formatting should be requested." | |||
| 4090 | (alist-get kind eglot--symbol-kind-names "Unknown") | 4142 | (alist-get kind eglot--symbol-kind-names "Unknown") |
| 4091 | (mapcan | 4143 | (mapcan |
| 4092 | (pcase-lambda (`(,container . ,objs)) | 4144 | (pcase-lambda (`(,container . ,objs)) |
| 4093 | (let ((elems (mapcar | 4145 | (let ((elems |
| 4094 | (eglot--lambda ((SymbolInformation) kind name location) | 4146 | (eglot--collecting-ranged |
| 4095 | (let ((reg (eglot-range-region | 4147 | (s reg objs (lambda (o) |
| 4096 | (plist-get location :range))) | 4148 | (plist-get :range (plist-get o :location)))) |
| 4097 | (kind (alist-get kind eglot--symbol-kind-names))) | 4149 | (eglot--dbind ((SymbolInformation) kind name) s |
| 4098 | (cons (propertize name | 4150 | (let ((kind (alist-get kind eglot--symbol-kind-names))) |
| 4099 | 'imenu-region reg | 4151 | (cons (propertize name |
| 4100 | 'imenu-kind kind | 4152 | 'imenu-region reg |
| 4101 | ;; Backward-compatible props | 4153 | 'imenu-kind kind |
| 4102 | ;; to be removed later: | 4154 | ;; Backward-compatible props |
| 4103 | 'breadcrumb-region reg | 4155 | ;; to be removed later: |
| 4104 | 'breadcrumb-kind kind) | 4156 | 'breadcrumb-region reg |
| 4105 | (car reg)))) | 4157 | 'breadcrumb-kind kind) |
| 4106 | objs))) | 4158 | (car reg))))))) |
| 4107 | (if container (list (cons container elems)) elems))) | 4159 | (if container (list (cons container elems)) elems))) |
| 4108 | (seq-group-by | 4160 | (seq-group-by |
| 4109 | (eglot--lambda ((SymbolInformation) containerName) containerName) objs)))) | 4161 | (eglot--lambda ((SymbolInformation) containerName) containerName) objs)))) |
| @@ -4123,6 +4175,7 @@ for which LSP on-type-formatting should be requested." | |||
| 4123 | 'breadcrumb-kind kind))) | 4175 | 'breadcrumb-kind kind))) |
| 4124 | (if (seq-empty-p children) | 4176 | (if (seq-empty-p children) |
| 4125 | (cons name (car reg)) | 4177 | (cons name (car reg)) |
| 4178 | ;; FIXME: leverage eglot--collecting-ranged | ||
| 4126 | (cons name | 4179 | (cons name |
| 4127 | (mapcar (lambda (c) (apply #'dfs c)) children)))))) | 4180 | (mapcar (lambda (c) (apply #'dfs c)) children)))))) |
| 4128 | (mapcar (lambda (s) (apply #'dfs s)) res))) | 4181 | (mapcar (lambda (s) (apply #'dfs s)) res))) |