aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoão Távora2026-01-09 15:45:08 +0000
committerJoão Távora2026-01-11 03:42:01 +0000
commit236647ab58e6d3dd0b092e753c54317ad9004f39 (patch)
tree8be4bbb7855e7dba090adf88501dc2631c874b65
parentfa5a65629262d47d95c70e5e1404b225ce7fb2f8 (diff)
downloademacs-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-NEWS7
-rw-r--r--lisp/progmodes/eglot.el91
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
25Diagnostic conversion between LSP and Flymake versions is now much
26faster. Previously, editing, e.g. a Python file with thousands of
27diagnostics was next to impossible to to periodic interruptions of
28diagnostic 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
25Eglot can now leverage LSP server multiplexer programs like Rassumfrassum 32Eglot 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.
1322For each element in OBJECTS, bind OBJECT-SYM to the element and
1323REGION-SYM to its computed Emacs region (a cons of buffer positions).
1324Evaluate BODY and collect the result into a list. Return that list.
1325
1326KEY, if non-nil, should be a function to extract the LSP range from each
1327element. If nil, elements are assumed to be plists with `:range' keys.
1328
1329This macro uses optimized incremental navigation instead of repeatedly
1330calling `eglot-range-region', providing significant performance benefits
1331when 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)))