From: Stefan Monnier via "Bug reports for GNU Emacs, the Swiss army knife of text editors" <bug-gnu-emacs@gnu.org>
To: Eli Zaretskii <eliz@gnu.org>
Cc: casouri@gmail.com, 70077@debbugs.gnu.org, yantar92@posteo.net,
qhong@alum.mit.edu, frederic.bour@lakaban.net,
joaotavora@gmail.com, mail@nicolasgoaziou.fr, acm@muc.de,
stephen_leake@stephe-leake.org, alan.zimm@gmail.com,
phillip.lord@russet.org.uk
Subject: bug#70077: An easier way to track buffer changes
Date: Sat, 30 Mar 2024 01:09:41 -0400 [thread overview]
Message-ID: <jwvy1a0uxd7.fsf-monnier+emacs@gnu.org> (raw)
In-Reply-To: <jwv4jcowguh.fsf-monnier+emacs@gnu.org> (Stefan Monnier's message of "Fri, 29 Mar 2024 23:17:09 -0400")
[-- Attachment #1: Type: text/plain, Size: 833 bytes --]
> Here's my first attempt at a real-life use.
And here's a second attempt, which is a tentative patch for `eglot.el`.
This one does make use of the `before` argument, so it exercises more
the API.
The `eglot--virtual-pos-to-lsp-position` is not completely satisfactory,
since to compute the LSP position of the end of the chunk before it was
modified, I end up creating a temp buffer to insert the part of the text
that changed (to count its line+column, which is much easier in a buffer
than in a string). That kinda sucks performancewise, but we do it at
most once per command rather than once per buffer-modification, so it
should be lost in the noise.
The upside is that we're insulated from the quirks of the
after/before-change-functions evidenced by the copious comments
referring to various bug reports.
Stefan
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: eglot.patch --]
[-- Type: text/x-diff, Size: 7993 bytes --]
diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index 7d2f1a55165..d2268cea940 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -110,6 +110,7 @@
(require 'text-property-search nil t)
(require 'diff-mode)
(require 'diff)
+(require 'track-changes)
;; These dependencies are also GNU ELPA core packages. Because of
;; bug#62576, since there is a risk that M-x package-install, despite
@@ -1732,6 +1733,9 @@ eglot-utf-16-linepos
"Calculate number of UTF-16 code units from position given by LBP.
LBP defaults to `eglot--bol'."
(/ (- (length (encode-coding-region (or lbp (eglot--bol))
+ ;; FIXME: How could `point' ever be
+ ;; larger than `point-max' (sounds like
+ ;; a bug in Emacs).
;; Fix github#860
(min (point) (point-max)) 'utf-16 t))
2)
@@ -1749,6 +1753,24 @@ eglot--pos-to-lsp-position
:character (progn (when pos (goto-char pos))
(funcall eglot-current-linepos-function)))))
+(defun eglot--virtual-pos-to-lsp-position (pos string)
+ "Return the LSP position at the end of STRING if it were inserted at POS."
+ (eglot--widening
+ (goto-char pos)
+ (forward-char 0)
+ ;; LSP line is zero-origin; Emacs is one-origin.
+ (let ((posline (1- (line-number-at-pos nil t)))
+ (linebeg (buffer-substring (point) pos))
+ (colfun eglot-current-linepos-function))
+ ;; Use a temp buffer because:
+ ;; - I don't know of a fast way to count newlines in a string.
+ ;; - We currently don't have `eglot-current-linepos-function' for strings.
+ (with-temp-buffer
+ (insert linebeg string)
+ (goto-char (point-max))
+ (list :line (+ posline (1- (line-number-at-pos nil t)))
+ :character (funcall colfun))))))
+
(defvar eglot-move-to-linepos-function #'eglot-move-to-utf-16-linepos
"Function to move to a position within a line reported by the LSP server.
@@ -1946,6 +1968,8 @@ eglot-managed-mode-hook
"A hook run by Eglot after it started/stopped managing a buffer.
Use `eglot-managed-p' to determine if current buffer is managed.")
+(defvar-local eglot--track-changes nil)
+
(define-minor-mode eglot--managed-mode
"Mode for source buffers managed by some Eglot project."
:init-value nil :lighter nil :keymap eglot-mode-map
@@ -1959,8 +1983,9 @@ eglot--managed-mode
("utf-8"
(eglot--setq-saving eglot-current-linepos-function #'eglot-utf-8-linepos)
(eglot--setq-saving eglot-move-to-linepos-function #'eglot-move-to-utf-8-linepos)))
- (add-hook 'after-change-functions #'eglot--after-change nil t)
- (add-hook 'before-change-functions #'eglot--before-change nil t)
+ (unless eglot--track-changes
+ (setq eglot--track-changes
+ (track-changes-register #'eglot--track-changes-signal)))
(add-hook 'kill-buffer-hook #'eglot--managed-mode-off nil t)
;; Prepend "didClose" to the hook after the "nonoff", so it will run first
(add-hook 'kill-buffer-hook #'eglot--signal-textDocument/didClose nil t)
@@ -1994,8 +2019,8 @@ eglot--managed-mode
(eldoc-mode 1))
(cl-pushnew (current-buffer) (eglot--managed-buffers (eglot-current-server))))
(t
- (remove-hook 'after-change-functions #'eglot--after-change t)
- (remove-hook 'before-change-functions #'eglot--before-change t)
+ (when eglot--track-changes
+ (track-changes-unregister eglot--track-changes))
(remove-hook 'kill-buffer-hook #'eglot--managed-mode-off t)
(remove-hook 'kill-buffer-hook #'eglot--signal-textDocument/didClose t)
(remove-hook 'before-revert-hook #'eglot--signal-textDocument/didClose t)
@@ -2564,54 +2589,20 @@ jsonrpc-connection-ready-p
(defvar-local eglot--change-idle-timer nil "Idle timer for didChange signals.")
-(defun eglot--before-change (beg end)
- "Hook onto `before-change-functions' with BEG and END."
- (when (listp eglot--recent-changes)
- ;; 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 nil))
- (,end . ,(copy-marker end t)))
- eglot--recent-changes)))
-
(defvar eglot--document-changed-hook '(eglot--signal-textDocument/didChange)
"Internal hook for doing things when the document changes.")
-(defun eglot--after-change (beg end pre-change-length)
- "Hook onto `after-change-functions'.
-Records BEG, END and PRE-CHANGE-LENGTH locally."
+(defun eglot--track-changes-signal (id)
(cl-incf eglot--versioned-identifier)
- (pcase (car-safe eglot--recent-changes)
- (`(,lsp-beg ,lsp-end
- (,b-beg . ,b-beg-marker)
- (,b-end . ,b-end-marker))
- ;; github#259 and github#367: with `capitalize-word' & friends,
- ;; `before-change-functions' records the whole word's `b-beg' and
- ;; `b-end'. Similarly, when `fill-paragraph' coalesces two
- ;; lines, `b-beg' and `b-end' mark end of first line and end of
- ;; second line, resp. In both situations, `beg' and `end'
- ;; received here seemingly contradict that: they will differ by 1
- ;; and encompass the capitalized character or, in the coalescing
- ;; case, the replacement of the newline with a space. We keep
- ;; both markers and positions to detect and correct this. In
- ;; this specific case, we ignore `beg', `len' and
- ;; `pre-change-len' and send richer information about the region
- ;; from the markers. I've also experimented with doing this
- ;; unconditionally but it seems to break when newlines are added.
- (if (and (= b-end b-end-marker) (= b-beg b-beg-marker)
- (or (/= beg b-beg) (/= end b-end)))
- (setcar eglot--recent-changes
- `(,lsp-beg ,lsp-end ,(- b-end-marker b-beg-marker)
- ,(buffer-substring-no-properties b-beg-marker
- b-end-marker)))
- (setcar eglot--recent-changes
- `(,lsp-beg ,lsp-end ,pre-change-length
- ,(buffer-substring-no-properties beg end)))))
- (_ (setf eglot--recent-changes :emacs-messup)))
+ (track-changes-fetch
+ id (lambda (beg end before)
+ (if (stringp before)
+ (push `(,(eglot--pos-to-lsp-position beg)
+ ,(eglot--virtual-pos-to-lsp-position beg before)
+ ,(length before)
+ ,(buffer-substring-no-properties beg end))
+ eglot--recent-changes)
+ (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
@@ -2741,12 +2732,6 @@ eglot--signal-textDocument/didChange
(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)
next prev parent reply other threads:[~2024-03-30 5:09 UTC|newest]
Thread overview: 50+ messages / expand[flat|nested] mbox.gz Atom feed top
2024-03-29 16:15 bug#70077: An easier way to track buffer changes Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-03-29 18:12 ` Eli Zaretskii
2024-03-29 18:53 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-03-30 6:34 ` Eli Zaretskii
2024-03-30 14:58 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-03-30 16:45 ` Eli Zaretskii
2024-03-31 2:57 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-01 11:53 ` Ihor Radchenko
2024-04-01 14:51 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-01 17:49 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-02 14:22 ` Ihor Radchenko
2024-04-02 15:17 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-02 16:21 ` Ihor Radchenko
2024-04-02 17:51 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-03 12:34 ` Ihor Radchenko
2024-04-03 12:45 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-04 17:58 ` Ihor Radchenko
2024-03-30 3:17 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-03-30 5:09 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors [this message]
2024-03-29 22:20 ` phillip.lord
2024-03-29 22:59 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-03-30 6:46 ` Eli Zaretskii
2024-03-30 12:06 ` phillip.lord
2024-03-30 13:39 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-03-30 9:51 ` Ihor Radchenko
2024-03-30 12:49 ` Eli Zaretskii
2024-03-30 13:19 ` Ihor Radchenko
2024-03-30 13:31 ` Eli Zaretskii
2024-03-30 14:09 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-05 22:12 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-06 8:43 ` Eli Zaretskii
2024-04-08 15:24 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-08 15:53 ` Eli Zaretskii
2024-04-08 17:17 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-08 17:27 ` Andrea Corallo
2024-04-08 18:36 ` Eli Zaretskii
2024-04-08 20:57 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-09 4:10 ` Eli Zaretskii
2024-04-08 20:45 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-09 3:56 ` Eli Zaretskii
2024-04-09 23:30 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-13 13:44 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-06 17:37 ` Dmitry Gutov
2024-04-06 19:44 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-07 14:40 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-07 15:47 ` Dmitry Gutov
2024-04-07 14:07 ` Ihor Radchenko
2024-04-08 16:06 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-04-09 17:35 ` Ihor Radchenko
2024-04-10 2:02 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
List information: https://www.gnu.org/software/emacs/
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=jwvy1a0uxd7.fsf-monnier+emacs@gnu.org \
--to=bug-gnu-emacs@gnu.org \
--cc=70077@debbugs.gnu.org \
--cc=acm@muc.de \
--cc=alan.zimm@gmail.com \
--cc=casouri@gmail.com \
--cc=eliz@gnu.org \
--cc=frederic.bour@lakaban.net \
--cc=joaotavora@gmail.com \
--cc=mail@nicolasgoaziou.fr \
--cc=monnier@iro.umontreal.ca \
--cc=phillip.lord@russet.org.uk \
--cc=qhong@alum.mit.edu \
--cc=stephen_leake@stephe-leake.org \
--cc=yantar92@posteo.net \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
Code repositories for project(s) associated with this public inbox
https://git.savannah.gnu.org/cgit/emacs.git
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).