From 6186783d28ac68716b5415bb76b815e6ced1ced4 Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Sat, 23 May 2020 22:05:49 +0100 Subject: [PATCH] feat: Perform a less invasive buffer content replacement operation Borrowed from go-mode's gofmt and releated functions: https://github.com/dominikh/go-mode.el/blob/734d5232455ffde088021ea5908849ac570e890f/go-mode.el#L1850-L1956 Hence I'm not the original author of most this code, but I have used a number of borrowed variations of it over the years for hand-rolled formatting solutions. It works by passing the current buffer content to `diff` via STDIN to compare it against the target file on disk producing a RCS formatted diff that is easy to parse. It then iterates through the said diff only modifying relevant lines to make the buffer be identical to the target file. In my experience it does a much better job of maintaining cursor position compared to `insert-file-contents`, as it only modifies lines that got corrected, and leaves all other lines in the buffer untouched. Performance is also excellent and near instant. On my late-2016 13-inch MacBook Pro with a Dual-Core i7 at 3.3Ghz, I can't really tell the difference between this solution and `insert-file-contents`. I did also test it against the Haskell example from: https://github.com/purcell/reformatter.el/issues/19 Replacing buffer content of `Req.hs` with that of `Req-reformatted.hs` is most of the time just as fast as `insert-file-contents` (around 50ms), and only sometimes a little slower of around 100-150ms based on my totally unscientific measurement technique of guesstimating it. I find that rather impressive as the RCS-formatted diff itself is 17KB. I also tested the old `replace-buffer-contents`-based variant to see how it performs on my machine. It took around 15 seconds. --- reformatter.el | 97 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 7 deletions(-) diff --git a/reformatter.el b/reformatter.el index 8c1df62..e690124 100644 --- a/reformatter.el +++ b/reformatter.el @@ -221,13 +221,96 @@ DISPLAY-ERRORS, shows a buffer if the formatting fails." ,minor-mode-form))) (defun reformatter-replace-buffer-contents-from-file (file) - "Replace the accessible portion of the current buffer with the contents of FILE." - ;; While the function `replace-buffer-contents' exists in recent - ;; Emacs versions, it exhibits pathologically slow behaviour in many - ;; cases, and the simple replacement approach we use instead is well - ;; proven and typically preserves point and markers to a reasonable - ;; degree. - (insert-file-contents file nil nil nil t)) + "Replace the accessible portion of the current buffer with +the contents of FILE." + (let* ((coding-system-for-read 'utf-8) + (coding-system-for-write 'utf-8) + (patchbuf (get-buffer-create "*Reformatter Patch*"))) + + (unwind-protect + (save-restriction + (widen) + (with-current-buffer patchbuf (erase-buffer)) + (call-process-region (point-min) (point-max) + "diff" nil patchbuf nil "-n" "-" file) + (if (> (buffer-size patchbuf) 0) + (reformatter--apply-rcs-patch patchbuf))) + + (kill-buffer patchbuf)))) + +(defun reformatter--apply-rcs-patch (patch-buffer) + "Apply an RCS-formatted diff from PATCH-BUFFER to the current buffer." + (let ((target-buffer (current-buffer)) + ;; Relative offset between buffer line numbers and line numbers + ;; in patch. + ;; + ;; Line numbers in the patch are based on the source file, so + ;; we have to keep an offset when making changes to the + ;; buffer. + ;; + ;; Appending lines decrements the offset (possibly making it + ;; negative), deleting lines increments it. This order + ;; simplifies the forward-line invocations. + (line-offset 0) + (column (current-column))) + (save-excursion + (with-current-buffer patch-buffer + (goto-char (point-min)) + (while (not (eobp)) + (unless (looking-at "^\\([ad]\\)\\([0-9]+\\) \\([0-9]+\\)") + (error "Invalid rcs patch or internal error in reformatter--apply-rcs-patch")) + (forward-line) + (let ((action (match-string 1)) + (from (string-to-number (match-string 2))) + (len (string-to-number (match-string 3)))) + (cond + ((equal action "a") + (let ((start (point))) + (forward-line len) + (let ((text (buffer-substring start (point)))) + (with-current-buffer target-buffer + (cl-decf line-offset len) + (goto-char (point-min)) + (forward-line (- from len line-offset)) + (insert text))))) + ((equal action "d") + (with-current-buffer target-buffer + (reformatter--goto-line (- from line-offset)) + (cl-incf line-offset len) + (reformatter--delete-whole-line len))) + (t + (error "Invalid rcs patch or internal error in reformatter--apply-rcs-patch"))))))) + (move-to-column column))) + +(defun reformatter--goto-line (line) + (goto-char (point-min)) + (forward-line (1- line))) + +(defun reformatter--delete-whole-line (&optional arg) + "Delete the current line without putting it in the `kill-ring'. +Derived from function `kill-whole-line'. ARG is defined as for that +function." + (setq arg (or arg 1)) + (if (and (> arg 0) + (eobp) + (save-excursion (forward-visible-line 0) (eobp))) + (signal 'end-of-buffer nil)) + (if (and (< arg 0) + (bobp) + (save-excursion (end-of-visible-line) (bobp))) + (signal 'beginning-of-buffer nil)) + (cond ((zerop arg) + (delete-region (progn (forward-visible-line 0) (point)) + (progn (end-of-visible-line) (point)))) + ((< arg 0) + (delete-region (progn (end-of-visible-line) (point)) + (progn (forward-visible-line (1+ arg)) + (unless (bobp) + (backward-char)) + (point)))) + (t + (delete-region (progn (forward-visible-line 0) (point)) + (progn (forward-visible-line arg) (point)))))) (provide 'reformatter)