diff options
| author | João Távora | 2025-01-22 15:44:41 +0000 |
|---|---|---|
| committer | João Távora | 2025-01-22 23:02:35 +0000 |
| commit | ac902ddadcd236dfd1d610768569e26ea8fc5b7f (patch) | |
| tree | a77a5e63f4b6d791699020c8206277d056abdebb | |
| parent | d3ada49a37e19d25170bf6322fee70d527055958 (diff) | |
| download | emacs-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.el | 142 |
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. |
| 2013 | Use `eglot-managed-p' to determine if current buffer is managed.") | 1994 | Use `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 | 2654 | Records 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) |