aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoão Távora2025-01-22 15:44:41 +0000
committerJoão Távora2025-01-22 23:02:35 +0000
commitac902ddadcd236dfd1d610768569e26ea8fc5b7f (patch)
treea77a5e63f4b6d791699020c8206277d056abdebb
parentd3ada49a37e19d25170bf6322fee70d527055958 (diff)
downloademacs-ac902ddadcd236dfd1d610768569e26ea8fc5b7f.tar.gz
emacs-ac902ddadcd236dfd1d610768569e26ea8fc5b7f.zip
Eglot: abandon track-changes.el
After a ~10 month period of using track-changes.el as a support library for tracking buffer changes, I've decided to go back to manually using after-change-functions and before-change-functions. track-changes.el showed promise: - One of the selling points was to turn complicated a-c-functions and b-c-functions into something easier, but that objectively didn't pan out, with "virtual" positions, one-shot hooks, and tracker registrations being abstractions and complications mastered by very few. - The other selling point was the ability to log and detect those parts of Emacs that cheat the modification hooks and correct them. As far as I can tell, only one such cheater -- quail.el -- was identified. But with little consequence, only an ugly workaround in eglot.el (now removed). - After using Eglot daily for all this time, I didn't notice any decrease in desynchronization events. - I did notice an increase in track-changes.el related bugs, some of which still baffle me and and hard to reproduce. A common occurence is the '(cl-assertion-failed (memq id track-changes--trackers))' which is hard to track down. - The library makes it more complicated to run Eglot on older Emacsen. I might yet revisit this matter for the next version but this experience has shown that it didn't bring the advantages I thought it would, so I'm abandoning it until at least 1.19 is out. * lisp/progmodes/eglot.el (track-changes): No longer require. (eglot--virtual-pos-to-lsp-position): Delete. (eglot--managed-mode): Simplify. (eglot--track-changes): Delete this variable. (eglot--recent-changes): Reword doc. (eglot--before-change, eglot--after-change): Bring back. (eglot--track-changes-fetch): Delete. (eglot--add-one-shot-hook): Delete. (eglot--track-changes-signal): Delete.
-rw-r--r--lisp/progmodes/eglot.el142
1 files changed, 60 insertions, 82 deletions
diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index 181f4a9c7a0..85a06a95b77 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -7,7 +7,7 @@
7;; Maintainer: João Távora <joaotavora@gmail.com> 7;; Maintainer: João Távora <joaotavora@gmail.com>
8;; URL: https://github.com/joaotavora/eglot 8;; URL: https://github.com/joaotavora/eglot
9;; Keywords: convenience, languages 9;; Keywords: convenience, languages
10;; Package-Requires: ((emacs "26.3") (compat "27.1") (eldoc "1.14.0") (external-completion "0.1") (flymake "1.2.1") (jsonrpc "1.0.24") (project "0.9.8") (seq "2.23") (track-changes "1.2") (xref "1.6.2")) 10;; Package-Requires: ((emacs "26.3") (compat "27.1") (eldoc "1.14.0") (external-completion "0.1") (flymake "1.2.1") (jsonrpc "1.0.24") (project "0.9.8") (seq "2.23") (xref "1.6.2"))
11 11
12;; This is a GNU ELPA :core package. Avoid adding functionality 12;; This is a GNU ELPA :core package. Avoid adding functionality
13;; that is not available in the version of Emacs recorded above or any 13;; that is not available in the version of Emacs recorded above or any
@@ -108,7 +108,6 @@
108(require 'text-property-search nil t) 108(require 'text-property-search nil t)
109(require 'diff-mode) 109(require 'diff-mode)
110(require 'diff) 110(require 'diff)
111(require 'track-changes)
112(require 'compat) 111(require 'compat)
113 112
114;; These dependencies are also GNU ELPA core packages. Because of 113;; These dependencies are also GNU ELPA core packages. Because of
@@ -1788,24 +1787,6 @@ LBP defaults to `eglot--bol'."
1788 :character (progn (when pos (goto-char pos)) 1787 :character (progn (when pos (goto-char pos))
1789 (funcall eglot-current-linepos-function))))) 1788 (funcall eglot-current-linepos-function)))))
1790 1789
1791(defun eglot--virtual-pos-to-lsp-position (pos string)
1792 "Return the LSP position at the end of STRING if it were inserted at POS."
1793 (eglot--widening
1794 (goto-char pos)
1795 (forward-line 0)
1796 ;; LSP line is zero-origin; Emacs is one-origin.
1797 (let ((posline (1- (line-number-at-pos nil t)))
1798 (linebeg (buffer-substring (point) pos))
1799 (colfun eglot-current-linepos-function))
1800 ;; Use a temp buffer because:
1801 ;; - I don't know of a fast way to count newlines in a string.
1802 ;; - We currently don't have `eglot-current-linepos-function' for strings.
1803 (with-temp-buffer
1804 (insert linebeg string)
1805 (goto-char (point-max))
1806 (list :line (+ posline (1- (line-number-at-pos nil t)))
1807 :character (funcall colfun))))))
1808
1809(defvar eglot-move-to-linepos-function #'eglot-move-to-utf-16-linepos 1790(defvar eglot-move-to-linepos-function #'eglot-move-to-utf-16-linepos
1810 "Function to move to a position within a line reported by the LSP server. 1791 "Function to move to a position within a line reported by the LSP server.
1811 1792
@@ -2012,8 +1993,6 @@ For example, to keep your Company customization, add the symbol
2012 "A hook run by Eglot after it started/stopped managing a buffer. 1993 "A hook run by Eglot after it started/stopped managing a buffer.
2013Use `eglot-managed-p' to determine if current buffer is managed.") 1994Use `eglot-managed-p' to determine if current buffer is managed.")
2014 1995
2015(defvar-local eglot--track-changes nil)
2016
2017(define-minor-mode eglot--managed-mode 1996(define-minor-mode eglot--managed-mode
2018 "Mode for source buffers managed by some Eglot project." 1997 "Mode for source buffers managed by some Eglot project."
2019 :init-value nil :lighter nil :keymap eglot-mode-map :interactive nil 1998 :init-value nil :lighter nil :keymap eglot-mode-map :interactive nil
@@ -2027,10 +2006,8 @@ Use `eglot-managed-p' to determine if current buffer is managed.")
2027 ("utf-8" 2006 ("utf-8"
2028 (eglot--setq-saving eglot-current-linepos-function #'eglot-utf-8-linepos) 2007 (eglot--setq-saving eglot-current-linepos-function #'eglot-utf-8-linepos)
2029 (eglot--setq-saving eglot-move-to-linepos-function #'eglot-move-to-utf-8-linepos))) 2008 (eglot--setq-saving eglot-move-to-linepos-function #'eglot-move-to-utf-8-linepos)))
2030 (unless eglot--track-changes 2009 (add-hook 'after-change-functions #'eglot--after-change nil t)
2031 (setq eglot--track-changes 2010 (add-hook 'before-change-functions #'eglot--before-change nil t)
2032 (track-changes-register
2033 #'eglot--track-changes-signal :disjoint t)))
2034 (add-hook 'kill-buffer-hook #'eglot--managed-mode-off nil t) 2011 (add-hook 'kill-buffer-hook #'eglot--managed-mode-off nil t)
2035 ;; Prepend "didClose" to the hook after the "nonoff", so it will run first 2012 ;; Prepend "didClose" to the hook after the "nonoff", so it will run first
2036 (add-hook 'kill-buffer-hook #'eglot--signal-textDocument/didClose nil t) 2013 (add-hook 'kill-buffer-hook #'eglot--signal-textDocument/didClose nil t)
@@ -2064,6 +2041,8 @@ Use `eglot-managed-p' to determine if current buffer is managed.")
2064 (eldoc-mode 1)) 2041 (eldoc-mode 1))
2065 (cl-pushnew (current-buffer) (eglot--managed-buffers (eglot-current-server)))) 2042 (cl-pushnew (current-buffer) (eglot--managed-buffers (eglot-current-server))))
2066 (t 2043 (t
2044 (remove-hook 'after-change-functions #'eglot--after-change t)
2045 (remove-hook 'before-change-functions #'eglot--before-change t)
2067 (remove-hook 'kill-buffer-hook #'eglot--managed-mode-off t) 2046 (remove-hook 'kill-buffer-hook #'eglot--managed-mode-off t)
2068 (remove-hook 'kill-buffer-hook #'eglot--signal-textDocument/didClose t) 2047 (remove-hook 'kill-buffer-hook #'eglot--signal-textDocument/didClose t)
2069 (remove-hook 'before-revert-hook #'eglot--signal-textDocument/didClose t) 2048 (remove-hook 'before-revert-hook #'eglot--signal-textDocument/didClose t)
@@ -2093,10 +2072,7 @@ Use `eglot-managed-p' to determine if current buffer is managed.")
2093 (delq (current-buffer) (eglot--managed-buffers server))) 2072 (delq (current-buffer) (eglot--managed-buffers server)))
2094 (when (and eglot-autoshutdown 2073 (when (and eglot-autoshutdown
2095 (null (eglot--managed-buffers server))) 2074 (null (eglot--managed-buffers server)))
2096 (eglot-shutdown server)))) 2075 (eglot-shutdown server)))))))
2097 (when eglot--track-changes
2098 (track-changes-unregister eglot--track-changes)
2099 (setq eglot--track-changes nil)))))
2100 2076
2101(defun eglot--managed-mode-off () 2077(defun eglot--managed-mode-off ()
2102 "Turn off `eglot--managed-mode' unconditionally." 2078 "Turn off `eglot--managed-mode' unconditionally."
@@ -2648,7 +2624,7 @@ buffer."
2648 `(:triggerKind 2 :triggerCharacter ,trigger) `(:triggerKind 1))))) 2624 `(:triggerKind 2 :triggerCharacter ,trigger) `(:triggerKind 1)))))
2649 2625
2650(defvar-local eglot--recent-changes nil 2626(defvar-local eglot--recent-changes nil
2651 "Recent buffer changes as collected by `eglot--track-changes-fetch'.") 2627 "Recent buffer changes as collected by `eglot--before-change'.")
2652 2628
2653(cl-defmethod jsonrpc-connection-ready-p ((_server eglot-lsp-server) _what) 2629(cl-defmethod jsonrpc-connection-ready-p ((_server eglot-lsp-server) _what)
2654 "Tell if SERVER is ready for WHAT in current buffer." 2630 "Tell if SERVER is ready for WHAT in current buffer."
@@ -2656,59 +2632,63 @@ buffer."
2656 2632
2657(defvar-local eglot--change-idle-timer nil "Idle timer for didChange signals.") 2633(defvar-local eglot--change-idle-timer nil "Idle timer for didChange signals.")
2658 2634
2635(defun eglot--before-change (beg end)
2636 "Hook onto `before-change-functions' with BEG and END."
2637 (when (listp eglot--recent-changes)
2638 ;; Records BEG and END, crucially convert them into LSP
2639 ;; (line/char) positions before that information is lost (because
2640 ;; the after-change thingy doesn't know if newlines were
2641 ;; deleted/added). Also record markers of BEG and END
2642 ;; (github#259)
2643 (push `(,(eglot--pos-to-lsp-position beg)
2644 ,(eglot--pos-to-lsp-position end)
2645 (,beg . ,(copy-marker beg nil))
2646 (,end . ,(copy-marker end t)))
2647 eglot--recent-changes)))
2648
2659(defvar eglot--document-changed-hook '(eglot--signal-textDocument/didChange) 2649(defvar eglot--document-changed-hook '(eglot--signal-textDocument/didChange)
2660 "Internal hook for doing things when the document changes.") 2650 "Internal hook for doing things when the document changes.")
2661 2651
2662(defun eglot--track-changes-fetch (id) 2652(defun eglot--after-change (beg end pre-change-length)
2663 (if (eq eglot--recent-changes :pending) (setq eglot--recent-changes nil)) 2653 "Hook onto `after-change-functions'.
2664 (track-changes-fetch 2654Records BEG, END and PRE-CHANGE-LENGTH locally."
2665 id (lambda (beg end before) 2655 (cl-incf eglot--versioned-identifier)
2666 (cl-incf eglot--versioned-identifier) 2656 (pcase (car-safe eglot--recent-changes)
2667 (cond 2657 (`(,lsp-beg ,lsp-end
2668 ((eq eglot--recent-changes :emacs-messup) nil) 2658 (,b-beg . ,b-beg-marker)
2669 ((eq before 'error) (setf eglot--recent-changes :emacs-messup)) 2659 (,b-end . ,b-end-marker))
2670 (t (push `(,(eglot--pos-to-lsp-position beg) 2660 ;; github#259 and github#367: with `capitalize-word' & friends,
2671 ,(eglot--virtual-pos-to-lsp-position beg before) 2661 ;; `before-change-functions' records the whole word's `b-beg' and
2672 ,(length before) 2662 ;; `b-end'. Similarly, when `fill-paragraph' coalesces two
2673 ,(buffer-substring-no-properties beg end)) 2663 ;; lines, `b-beg' and `b-end' mark end of first line and end of
2674 eglot--recent-changes)))))) 2664 ;; second line, resp. In both situations, `beg' and `end'
2675 2665 ;; received here seemingly contradict that: they will differ by 1
2676(defun eglot--add-one-shot-hook (hook function &optional append local) 2666 ;; and encompass the capitalized character or, in the coalescing
2677 "Like `add-hook' but calls FUNCTION only once." 2667 ;; case, the replacement of the newline with a space. We keep
2678 (let* ((fname (make-symbol (format "eglot--%s-once" function))) 2668 ;; both markers and positions to detect and correct this. In
2679 (fun (lambda (&rest args) 2669 ;; this specific case, we ignore `beg', `len' and
2680 (remove-hook hook fname local) 2670 ;; `pre-change-len' and send richer information about the region
2681 (apply function args)))) 2671 ;; from the markers. I've also experimented with doing this
2682 (fset fname fun) 2672 ;; unconditionally but it seems to break when newlines are added.
2683 (add-hook hook fname append local))) 2673 (if (and (= b-end b-end-marker) (= b-beg b-beg-marker)
2684 2674 (or (/= beg b-beg) (/= end b-end)))
2685(defun eglot--track-changes-signal (id &optional distance) 2675 (setcar eglot--recent-changes
2686 (cond 2676 `(,lsp-beg ,lsp-end ,(- b-end-marker b-beg-marker)
2687 (distance 2677 ,(buffer-substring-no-properties b-beg-marker
2688 ;; When distance is <100, we may as well coalesce the changes. 2678 b-end-marker)))
2689 (when (> distance 100) (eglot--track-changes-fetch id))) 2679 (setcar eglot--recent-changes
2690 (eglot--recent-changes nil) 2680 `(,lsp-beg ,lsp-end ,pre-change-length
2691 ;; Note that there are pending changes, for the benefit of those 2681 ,(buffer-substring-no-properties beg end)))))
2692 ;; who check it as a boolean. 2682 (_ (setf eglot--recent-changes :emacs-messup)))
2693 (t (setq eglot--recent-changes :pending)))
2694 (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer)) 2683 (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer))
2695 (setq eglot--change-idle-timer 2684 (let ((buf (current-buffer)))
2696 (run-with-idle-timer 2685 (setq eglot--change-idle-timer
2697 eglot-send-changes-idle-time nil 2686 (run-with-idle-timer
2698 (lambda (buf) 2687 eglot-send-changes-idle-time
2699 (eglot--when-live-buffer buf 2688 nil (lambda () (eglot--when-live-buffer buf
2700 (when eglot--managed-mode 2689 (when eglot--managed-mode
2701 (if (track-changes-inconsistent-state-p) 2690 (run-hooks 'eglot--document-changed-hook)
2702 ;; Not a good time (e.g. in the middle of Quail thingy, 2691 (setq eglot--change-idle-timer nil))))))))
2703 ;; bug#70541): reschedule for the next idle period.
2704 (eglot--add-one-shot-hook
2705 'post-command-hook
2706 (lambda ()
2707 (eglot--when-live-buffer buf
2708 (eglot--track-changes-signal id))))
2709 (run-hooks 'eglot--document-changed-hook)
2710 (setq eglot--change-idle-timer nil)))))
2711 (current-buffer))))
2712 2692
2713(defvar-local eglot-workspace-configuration () 2693(defvar-local eglot-workspace-configuration ()
2714 "Configure LSP servers specifically for a given project. 2694 "Configure LSP servers specifically for a given project.
@@ -2812,7 +2792,6 @@ When called interactively, use the currently active server"
2812 2792
2813(defun eglot--signal-textDocument/didChange () 2793(defun eglot--signal-textDocument/didChange ()
2814 "Send textDocument/didChange to server." 2794 "Send textDocument/didChange to server."
2815 (eglot--track-changes-fetch eglot--track-changes)
2816 (when eglot--recent-changes 2795 (when eglot--recent-changes
2817 (let* ((server (eglot--current-server-or-lose)) 2796 (let* ((server (eglot--current-server-or-lose))
2818 (sync-capability (eglot-server-capable :textDocumentSync)) 2797 (sync-capability (eglot-server-capable :textDocumentSync))
@@ -2838,7 +2817,6 @@ When called interactively, use the currently active server"
2838(defun eglot--signal-textDocument/didOpen () 2817(defun eglot--signal-textDocument/didOpen ()
2839 "Send textDocument/didOpen to server." 2818 "Send textDocument/didOpen to server."
2840 ;; Flush any potential pending change. 2819 ;; Flush any potential pending change.
2841 (eglot--track-changes-fetch eglot--track-changes)
2842 (setq eglot--recent-changes nil 2820 (setq eglot--recent-changes nil
2843 eglot--versioned-identifier 0 2821 eglot--versioned-identifier 0
2844 eglot--TextDocumentIdentifier-cache nil) 2822 eglot--TextDocumentIdentifier-cache nil)