Subject: Re: bug#67604: Motion problems with inline images

From: JD Smith <[email-masked]>
Date: Sat, 17 May 2025 23:13:45 -0400

I have some progress to report.  To keep things simple, I compared two cases, both moving vertically down by one line, starting with point on the just-wrapped green image:

1. The "fine-tuned" case, which skips a line.
2. A "nearby" case with the same window width that doesn't exhibit the bug (no skip).

Recall that this bug/no-bug tuning proceeds by making small changes to the width of the red image on the first screen line, effectively "pushing" the green image further along, pixel by pixel.  You can move between cases 1 and 2 by changing the red image width by as little as one pixel.

By comparing these two cases, I've tracked things back to where I started: `move_it_in_display_line_to'.  I investigated various backtraces, all of which land via vertical-motion in this function:

* thread #5, name = 'org.gnu.Emacs.lisp-main', stop reason = step over
  * frame #0: 0x0000000100038374 Emacs`move_it_in_display_line_to(it=0x0000000170793950, to_charpos=89, to_x=-1, op=MOVE_TO_POS) at xdisp.c:10594:10
    frame #1: 0x0000000100031bd0 Emacs`move_it_to(it=0x0000000170793950, to_charpos=89, to_x=-1, to_y=-1, to_vpos=-1, op=8) at xdisp.c:10841:9
    frame #2: 0x000000010002d600 Emacs`start_display(it=0x0000000170793950, w=0x000000012a808cd0, pos=(charpos = 89, bytepos = 89)) at xdisp.c:3763:4
    frame #3: 0x00000001001d72d4 Emacs`Fvertical_motion(lines=(EMACS_INT) $259 = 1, window=(struct Lisp_Symbol *) $277 = 0x0000000100a73070, cur_col=(struct Lisp_Symbol *) $298 = 0x0000000100a73070) at indent.c:2260:7

There are many paths from vertical-motion which lead here, but I suspect this is the critical one.  It occurs on `start_display', which does:

	  reseat_at_previous_visible_line_start (it);
	  move_it_to (it, CHARPOS (pos), -1, -1, -1, MOVE_TO_POS);

That move_it_to(MOVE_TO_POS) is what initially sets up the bug.  I believe the idea here is to correct initialize the position.

By setting breakpoints when the iterator is on the green image (charpos=89), I discovered the cause of the pixel-precise "fine-tuning" behavior.  It results from this code in xdisp.c:move_it_in_display_line_to:

	      if (/* Lines are continued.  */
		  it->line_wrap != TRUNCATE
		...
		  if (/* IT->hpos == 0 means the very first glyph
			 doesn't fit on the line, e.g. a wide
			 image.  */
		      it->hpos == 0  
		      || (new_x == it->last_visible_x  //**!!! FINE-TUNING
			  && FRAME_WINDOW_P (it->f)))
		    {
		      ++it->hpos;  //!!! The fine-tuned case lands here: new_x==last_visible_x
		      it->current_x = new_x;
		      /* The character's last glyph just barely fits
			 in this row.  */
			....

			  prev_method = it->method;
			  if (it->method == GET_FROM_BUFFER)
			    prev_pos = IT_CHARPOS (*it);
			  set_iterator_to_next (it, true);
			...
		  else if (wrap_it.sp >= 0)  ///**!!! THIS IS THE WRAP RESTORATION WE MISS
		    {
		      RESTORE_IT (it, &wrap_it, wrap_data);
		      atpos_it.sp = -1;
		      atx_it.sp = -1;
		    }

I.e. if the position of the green image is tuned so that it "barely" fits on a line, we take another code path.  For this special fine-tuned case, this prevents wrap_it from being restored, which leaves the it->current_x position after start_display at 0 instead of the correct value of 108 (the width of the green image).  The rest of the bug proceeds along the lines of my previous conjecture about point being pushed ahead past the wrapped image.

In terms of the special case path for "last glyph just fits":  this is appropriate for (non-word) window wrapping, but not, I believe word-wrapping.  Why? I noticed that wrapped words carry _all_ their trailing whitespace with them when they wrap (presumably to keep WS from wrapping to the start of a display line).  In other words, why care if the green image itself can "barely fit" on a line, when in fact, it will _already have wrapped_ at a larger window width, due to the trailing whitespace (of which there must be at least one).

BTW, the "barely fits" test I zeroed in on is similar in xdisp.c on master, only adding a check for line-number pixel width:

		  if (/* IT->hpos == 0 (modulo line-number width) means
			 the very first glyph doesn't fit on the line,
			 e.g., a wide image.  */
		      it->hpos == 0 + (it->lnum_width ? it->lnum_width + 2 : 0)
		      || (new_x == it->last_visible_x
			  && FRAME_WINDOW_P (it->f)))

Here is a proposed patch, which skips the fine-tuned branch when word-wrapping, if the next position can't wrap.  `char_can_wrap_before' is actually called before this, so it's possible the result could be saved and reused.   

Definitely poke around and be sure this makes sense.  That was a _lot_ of hunting!