Subject: bug#72392: 30.0.50; Wrong `next-line` behavior

From: Stefan Monnier <[email-masked]>
Date: Wed, 31 Jul 2024 03:43:20 -0400

Package: Emacs
Version: 30.0.50

Here's a funny corner case I just bumped into:

- Apply the `smerge-mode.patch` below to Emacs's` master` (and
  recompile `smerge-mode.el`).
- Open the short diff file below with:

    emacs -Q --eval '(setq-default word-wrap t diff-font-lock-prettify t)' \
          .../bug-weird-next-line.diff

- In my case, when this opens there's a "word wrap" just before "This"
  and just before "\id{seg}" (on the 4th and 5th lines, resp).
  If that's not the case for you, resize your frame so that this is
  the case.

- Do `C-n` a few times.

What I see is that `C-n` jumps down to the "T" of the word-wrapped
"This" as it should, but that the next `C-n` doesn't jump to the next
(visual) line but jumps to the "s" of "This" instead.
The same kind of misbehavior occurs on the next (logical) line with
"\id{seg}".

        Stefan
diff --git a/lisp/vc/smerge-mode.el b/lisp/vc/smerge-mode.el
index a16c7871ff9..05ab757f09a 100644
--- a/lisp/vc/smerge-mode.el
+++ b/lisp/vc/smerge-mode.el
@@ -1034,26 +1034,68 @@ smerge--refine-highlight-change
                                   smerge-refine-forward-function)
                                 startline)
                        (point)))
-           (end (progn (funcall (if smerge-refine-weight-hack
-                                    #'forward-char
-                                  smerge-refine-forward-function)
-                          (if match-num2
-                              (- (string-to-number match-num2)
-                                 startline)
-                            1))
-                       (point))))
-      (when smerge-refine-ignore-whitespace
-        (skip-chars-backward " \t\n" beg) (setq end (point))
-        (goto-char beg)
-        (skip-chars-forward " \t\n" end)  (setq beg (point)))
-      (when (> end beg)
+           (end (if (eq t match-num2) beg
+                  (funcall (if smerge-refine-weight-hack
+                               #'forward-char
+                             smerge-refine-forward-function)
+                           (if match-num2
+                               (- (string-to-number match-num2)
+                                  startline)
+                             1))
+                  (point))))
+      (when (eq t match-num2)
+        ;; FIXME: No idea where this off-by-one comes from!
+        (setq beg (1+ beg))
+        (setq end (1+ end))
+        (goto-char end))
+      (when (and smerge-refine-ignore-whitespace
+                 (< beg end))
+        (let* ((newend (progn (skip-chars-backward " \t\n" beg) (point)))
+               (newbeg (progn (goto-char beg)
+                              (skip-chars-forward " \t\n" newend) (point))))
+          (unless (eq newend newbeg)
+            (setq end newend)
+            (setq beg newbeg))))
+      (cond
+       ((> end beg)
         (let ((ol (make-overlay
                    beg end nil
                    ;; Make them tend to shrink rather than spread when editing.
                    'front-advance nil)))
+          (overlay-put ol 'smerge--debug (list match-num1 match-num2 startline))
           (overlay-put ol 'evaporate t)
           (dolist (x props) (overlay-put ol (car x) (cdr x)))
-          ol)))))
+          ol))
+       ((= end beg)
+        (setq end (if (< beg (point-max))
+                      (1+ beg)
+                    (cl-assert (< (point-min) (point-max)))
+                    (setq beg (1- (point-max)))
+                    (point-max)))
+        (let ((ol (make-overlay
+                   beg end nil
+                   ;; Make them tend to shrink rather than spread when editing.
+                   'front-advance nil)))
+          (overlay-put ol 'smerge--debug (list match-num1 match-num2 startline))
+          (overlay-put ol 'evaporate t)
+          ;; Some of the properties need to go to `ol' and others
+          ;; need to go to the string :-(
+          ;; FIXME: I've seen lines in `diff-mode' where the half-space ends up
+          ;; placed at the beginning of the next line rather than the
+          ;; end of the current line, as in:
+          ;;
+          ;;     -foo bar
+          ;;     + foo bar b
+          ;;      ^
+          (overlay-put ol 'before-string
+                       (propertize
+                        " " 'face (cdr (assq 'face props))
+                        'display '(space :width 0.5)))
+          (dolist (x props)
+            (unless (eq (car-safe x) 'face)
+              (overlay-put ol (car x) (cdr x))))
+          ol))
+       (t (error "Smerge WOW! %S" (list beg end match-num2)))))))
 
 ;;;###autoload
 (defun smerge-refine-regions (beg1 end1 beg2 end2 props-c &optional preproc props-r props-a)
@@ -1114,20 +1156,18 @@ smerge-refine-regions
                     (m2 (match-string 2))
                     (m4 (match-string 4))
                     (m5 (match-string 5)))
-                (when (memq op '(?d ?c))
-                  (setq last1
-                        (smerge--refine-highlight-change
-			 beg1 m1 m2
-			 ;; Try to use props-c only for changed chars,
-			 ;; fallback to props-r for changed/removed chars,
-			 ;; but if props-r is nil then fallback to props-c.
-			 (or (and (eq op '?c) props-c) props-r props-c))))
-                (when (memq op '(?a ?c))
-                  (setq last2
-                        (smerge--refine-highlight-change
-			 beg2 m4 m5
-			 ;; Same logic as for removed chars above.
-			 (or (and (eq op '?c) props-c) props-a props-c)))))
+                (setq last1
+                      (smerge--refine-highlight-change
+		       beg1 m1 (if (eq op ?a) t m2)
+		       ;; Try to use props-c only for changed chars,
+		       ;; fallback to props-r for changed/removed chars,
+		       ;; but if props-r is nil then fallback to props-c.
+		       (or (and (eq op '?c) props-c) props-r props-c)))
+                (setq last2
+                      (smerge--refine-highlight-change
+		       beg2 m4 (if (eq op ?d) t m5)
+		       ;; Same logic as for removed chars above.
+		       (or (and (eq op '?c) props-c) props-a props-c))))
               (forward-line 1)                            ;Skip hunk header.
               (and (re-search-forward "^[0-9]" nil 'move) ;Skip hunk body.
                    (goto-char (match-beginning 0))))
@@ -2004,7 +2538,8 @@
 
 equivalent to a function out of the interval. Its defining characteristic is
-that the output of such a function has to agree at both endpoints of $\bI$. This
-is enforced by the elimination principle of the $\bI$ that ensures that \id{seg}
+that the output of such a function has to agree at both endpoints of $\bI$.
+This is enforced by the elimination principle of the $\bI$ that ensures that
+\id{seg} is respected.
 
 \begin{minted}[escapeinside=@@,mathescape=true]{agda}
     type I