]> git.eshelyaron.com Git - emacs.git/commitdiff
Work around a bug in emacs's change detection
authorJoão Távora <joaotavora@gmail.com>
Thu, 9 May 2019 10:38:58 +0000 (11:38 +0100)
committerJoão Távora <joaotavora@gmail.com>
Fri, 10 May 2019 21:28:36 +0000 (22:28 +0100)
When using capitalize-word, or any case-fiddling function,
before-change-functions will record e.g. the whole word's start and
end, even though only the first character has changed.  Not only is
this longer than needed but also conflicts with what we get in
after-change-functions, which records just the one-char-long change.

Also, if the word didn't need any fiddling at all then
before-change-function will run but after-change-functions won't: an
"orphan" before-change will erroneously be sent to the server.

* eglot.el (eglot--after-change): Detect problematic case and fix
change description.
(eglot--before-change): Store markers of changed region.
(eglot--signal-textDocument/didChange): Weed out orphan changes.

GitHub-reference: fix https://github.com/joaotavora/eglot/issues/259

lisp/progmodes/eglot.el

index b9fc25c01e387ad89c729f1bbae7865570dfed3c..0c2f4e97bdd71ee6813fc52d60e8c13f0f66af08 100644 (file)
@@ -1530,26 +1530,42 @@ THINGS are either registrations or unregisterations (sic)."
 
 (defvar-local eglot--change-idle-timer nil "Idle timer for didChange signals.")
 
-(defun eglot--before-change (start end)
-  "Hook onto `before-change-functions' with START and END."
-  ;; Records START and END, crucially convert them into LSP
-  ;; (line/char) positions before that information is lost (because
-  ;; the after-change thingy doesn't know if newlines were
-  ;; deleted/added)
+(defun eglot--before-change (beg end)
+  "Hook onto `before-change-functions' with BEG and END."
   (when (listp eglot--recent-changes)
-    (push `(,(eglot--pos-to-lsp-position start)
-            ,(eglot--pos-to-lsp-position end))
+    ;; Records BEG and END, crucially convert them into LSP
+    ;; (line/char) positions before that information is lost (because
+    ;; the after-change thingy doesn't know if newlines were
+    ;; deleted/added).  Also record markers of BEG and END
+    ;; (github#259)
+    (push `(,(eglot--pos-to-lsp-position beg)
+            ,(eglot--pos-to-lsp-position end)
+            (,beg . ,(copy-marker beg))
+            (,end . ,(copy-marker end)))
           eglot--recent-changes)))
 
-(defun eglot--after-change (start end pre-change-length)
+(defun eglot--after-change (beg end pre-change-length)
   "Hook onto `after-change-functions'.
-Records START, END and PRE-CHANGE-LENGTH locally."
+Records BEG, END and PRE-CHANGE-LENGTH locally."
   (cl-incf eglot--versioned-identifier)
-  (if (and (listp eglot--recent-changes)
-           (null (cddr (car eglot--recent-changes))))
-      (setf (cddr (car eglot--recent-changes))
-            `(,pre-change-length ,(buffer-substring-no-properties start end)))
-    (setf eglot--recent-changes :emacs-messup))
+  (pcase (and (listp eglot--recent-changes)
+              (car eglot--recent-changes))
+    (`(,lsp-beg ,lsp-end
+                (,b-beg . ,b-beg-marker)
+                (,b-end . ,b-end-marker))
+     ;; github#259: With `upcase-word' or somesuch,
+     ;; `before-change-functions' always records the whole word's
+     ;; `beg' and `end'.  Not only is this longer than needed but
+     ;; conflicts with the args received here.  Detect this using
+     ;; markers recorded earlier and `pre-change-len', then fix it.
+     (when (and (= b-end b-end-marker) (= b-beg b-beg-marker)
+                (not (zerop pre-change-length)))
+       (setq lsp-end (eglot--pos-to-lsp-position end)
+             lsp-beg (eglot--pos-to-lsp-position beg)))
+     (setcar eglot--recent-changes
+             `(,lsp-beg ,lsp-end ,pre-change-length
+                        ,(buffer-substring-no-properties beg end))))
+    (_ (setf eglot--recent-changes :emacs-messup)))
   (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer))
   (let ((buf (current-buffer)))
     (setq eglot--change-idle-timer
@@ -1609,6 +1625,12 @@ When called interactively, use the currently active server"
                               (buffer-substring-no-properties (point-min)
                                                               (point-max)))))
           (cl-loop for (beg end len text) in (reverse eglot--recent-changes)
+                   ;; github#259: `capitalize-word' and commands based
+                   ;; on `casify_region' will cause multiple duplicate
+                   ;; empty entries in `eglot--before-change' calls
+                   ;; without an `eglot--after-change' reciprocal.
+                   ;; Weed them out here.
+                   when (numberp len)
                    vconcat `[,(list :range `(:start ,beg :end ,end)
                                     :rangeLength len :text text)]))))
       (setq eglot--recent-changes nil)