Subject: bug#77217: move_it_to moves point to line after to_charpos when to_charpos is an image.

From: Yifan Zhu <[email-masked]>
Date: Wed, 26 Mar 2025 09:39:18 -0700

Hi,

Thank you for your prompt response!

On 3/26/25 7:47 AM, Eli Zaretskii wrote:
> I think the problem is not in vertical-motion at all.  The problem is
> that the "normal" display of embedded images allows us to get into a
> situation with layout which vertical-motion doesn't expect to happen,
> when these two minor modes are turned on.  Please try the patch below
> and see if it gives good results.
>
> diff --git a/src/xdisp.c b/src/xdisp.c
> index 4e8bb7d..7afa64d 100644
> --- a/src/xdisp.c
> +++ b/src/xdisp.c
> @@ -31956,12 +31956,13 @@ produce_image_glyph (struct it *it)
>        word-wrap, unless the image starts at column zero, because
>        wrapping correctly needs the real pixel width of the image.  */
>     if ((it->line_wrap != WORD_WRAP
> -       || it->hpos == 0
> +       || it->hpos == 0 + (it->lnum_width ? it->lnum_width + 2 : 0)
>          /* Always crop images larger than the window-width, minus 1 space.  */
>          || it->pixel_width > it->last_visible_x - FRAME_COLUMN_WIDTH (it->f))
>         && (crop = it->pixel_width - (it->last_visible_x - it->current_x),
>   	  crop > 0)
> -      && (it->hpos == 0 || it->pixel_width > it->last_visible_x / 4))
> +      && (it->hpos == 0 + (it->lnum_width ? it->lnum_width + 2 : 0)
> +	  || it->pixel_width > it->last_visible_x / 4))
>       {
>         it->pixel_width -= crop;
>         slice.width -= crop;

Unfortunately with this patch, I could still get the cursor stuck. See 
stuck.png and following gdb logs when it gets stuck:

+bt
#0  produce_image_glyph (it=0x7fff258031d0) at xdisp.c:31958
#1  0x000057ba672d79e1 in gui_produce_glyphs (it=0x7fff258031d0) at 
xdisp.c:33672
#2  0x000057ba672bcb18 in display_line (it=0x7fff258031d0, 
cursor_vpos=6) at xdisp.c:25720
#3  0x000057ba672aeb0a in try_window (window=0x57baa27b9c2d, pos=..., 
flags=1) at xdisp.c:21413
#4  0x000057ba672abaa3 in redisplay_window (window=0x57baa27b9c2d, 
just_this_one_p=true) at xdisp.c:20784
#5  0x000057ba672a2fa9 in redisplay_window_1 (window=0x57baa27b9c2d) at 
xdisp.c:18268
#6  0x000057ba67460bf1 in internal_condition_case_1 (bfun=0x57ba672a2f67 
<redisplay_window_1>, arg=0x57baa27b9c2d, handlers=0x710b5b9a7d33, 
hfun=0x57ba672a2df7 <redisplay_window_error>) at eval.c:1644
#7  0x000057ba672a207d in redisplay_internal () at xdisp.c:17774
#8  0x000057ba6729fb21 in redisplay () at xdisp.c:16802
#9  0x000057ba6739a0cb in read_char (commandflag=1, map=0x57baa32452f3, 
prev_event=0x0, used_mouse_menu=0x7fff2580871a, end_time=0x0) at 
keyboard.c:2672
#10 0x000057ba673ad0fb in read_key_sequence (keybuf=0x7fff25808990, 
prompt=0x0, dont_downcase_last=false, can_return_switch_frame=true, 
fix_current_buffer=true, prevent_redisplay=false, 
disable_text_conversion_p=false) at keyboard.c:10848
#11 0x000057ba67396751 in command_loop_1 () at keyboard.c:1424
#12 0x000057ba67460b4a in internal_condition_case (bfun=0x57ba67396343 
<command_loop_1>, handlers=0x90, hfun=0x57ba67395874 <cmd_error>) at 
eval.c:1620
#13 0x000057ba67395f90 in command_loop_2 (handlers=0x90) at keyboard.c:1163
#14 0x000057ba6746009f in internal_catch (tag=0x11f40, 
func=0x57ba67395f66 <command_loop_2>, arg=0x90) at eval.c:1300
#15 0x000057ba67395f22 in command_loop () at keyboard.c:1141
#16 0x000057ba67395416 in recursive_edit_1 () at keyboard.c:749
#17 0x000057ba673955c2 in Frecursive_edit () at keyboard.c:832
#18 0x000057ba6739167f in main (argc=3, argv=0x7fff25808e78) at emacs.c:2560
+p *it
$11 = {window = 0x57baa27b9c2d, w = 0x57baa27b9c28, f = 0x57baa27b99d0, 
method = GET_FROM_IMAGE, stop_charpos = 127, prev_stop = 126, 
base_level_stop = 126, end_charpos = 302, medium_narrowing_begv = 0, 
medium_narrowing_zv = 0, large_narrowing_begv = 0, large_narrowing_zv = 
0, s = 0x0, string_nchars = 0, multibyte_p = true, tab_line_p = false, 
header_line_p = false, string_from_display_prop_p = false, 
string_from_prefix_prop_p = false, from_disp_prop_p = true, ellipsis_p = 
false, avoid_cursor_p = false, dp = 0x0, dpvec = 0x0, dpend = 0x0, 
dpvec_char_len = 0, dpvec_face_id = 0, saved_face_id = 34, ctl_chars = 
{0x0 <repeats 16 times>}, start = {pos = {charpos = 121, bytepos = 121}, 
overlay_string_index = -1, string_pos = {charpos = -1, bytepos = -1}, 
dpvec_index = -1}, current = {pos = {charpos = 126, bytepos = 126}, 
overlay_string_index = -1, string_pos = {charpos = -1, bytepos = -1}, 
dpvec_index = -1}, n_overlay_strings = 0, overlay_strings_charpos = 126, 
overlay_strings = {0x0 <repeats 16 times>}, string_overlays = {0x0 
<repeats 16 times>}, string = 0x0, from_overlay = 0x0, stack = {{string 
= 0x0, string_nchars = 0, end_charpos = 302, stop_charpos = 127, 
prev_stop = 126, base_level_stop = 126, cmp_it = {stop_pos = 129, id = 
-1, ch = -2, rule_idx = 0, lookback = 0, nglyphs = 0, reversed_p = 
false, parent_it = 0x7fff258031d0, charpos = 0, nchars = 0, nbytes = 0, 
from = 0, to = 0, width = 0}, face_id = 34, u = {image = {object = 0x0, 
slice = {x = 0x0, y = 0x0, width = 0x0, height = 0x0}, image_id = 0}, 
stretch = {object = 0x0}, xwidget = {object = 0x0}}, position = {charpos 
= 127, bytepos = 127}, current = {pos = {charpos = 127, bytepos = 127}, 
overlay_string_index = -1, string_pos = {charpos = -1, bytepos = -1}, 
dpvec_index = -1}, from_overlay = 0x0, area = TEXT_AREA, method = 
GET_FROM_BUFFER, paragraph_embedding = NEUTRAL_DIR, multibyte_p = true, 
string_from_display_prop_p = false, string_from_prefix_prop_p = false, 
display_ellipsis_p = false, avoid_cursor_p = false, bidi_p = true, 
from_disp_prop_p = false, line_wrap = WORD_WRAP, voffset = 0, 
space_width = 0x0, font_height = 0x0}, {string = 0x0, string_nchars = 0, 
end_charpos = 0, stop_charpos = 0, prev_stop = 0, base_level_stop = 0, 
cmp_it = {stop_pos = 0, id = 0, ch = 0, rule_idx = 0, lookback = 0, 
nglyphs = 0, reversed_p = false, parent_it = 0x0, charpos = 0, nchars = 
0, nbytes = 0, from = 0, to = 0, width = 0}, face_id = 0, u = {image = 
{object = 0x0, slice = {x = 0x0, y = 0x0, width = 0x0, height = 0x0}, 
image_id = 0}, stretch = {object = 0x0}, xwidget = {object = 0x0}}, 
position = {charpos = 0, bytepos = 0}, current = {pos = {charpos = 0, 
bytepos = 0}, overlay_string_index = 0, string_pos = {charpos = 0, 
bytepos = 0}, dpvec_index = 0}, from_overlay = 0x0, area = 
LEFT_MARGIN_AREA, method = GET_FROM_BUFFER, paragraph_embedding = 
NEUTRAL_DIR, multibyte_p = false, string_from_display_prop_p = false, 
string_from_prefix_prop_p = false, display_ellipsis_p = false, 
avoid_cursor_p = false, bidi_p = false, from_disp_prop_p = false, 
line_wrap = TRUNCATE, voffset = 0, space_width = 0x0, font_height = 
0x0}, {string = 0x0, string_nchars = 0, end_charpos = 0, stop_charpos = 
0, prev_stop = 0, base_level_stop = 0, cmp_it = {stop_pos = 0, id = 0, 
ch = 0, rule_idx = 0, lookback = 0, nglyphs = 0, reversed_p = false, 
parent_it = 0x0, charpos = 0, nchars = 0, nbytes = 0, from = 0, to = 0, 
width = 0}, face_id = 0, u = {image = {object = 0x0, slice = {x = 0x0, y 
= 0x0, width = 0x0, height = 0x0}, image_id = 0}, stretch = {object = 
0x0}, xwidget = {object = 0x0}}, position = {charpos = 0, bytepos = 0}, 
current = {pos = {charpos = 0, bytepos = 0}, overlay_string_index = 0, 
string_pos = {charpos = 0, bytepos = 0}, dpvec_index = 0}, from_overlay 
= 0x0, area = LEFT_MARGIN_AREA, method = GET_FROM_BUFFER, 
paragraph_embedding = NEUTRAL_DIR, multibyte_p = false, 
string_from_display_prop_p = false, string_from_prefix_prop_p = false, 
display_ellipsis_p = false, avoid_cursor_p = false, bidi_p = false, 
from_disp_prop_p = false, line_wrap = TRUNCATE, voffset = 0, space_width 
= 0x0, font_height = 0x0}, {string = 0x0, string_nchars = 0, end_charpos 
= 0, stop_charpos = 0, prev_stop = 0, base_level_stop = 0, cmp_it = 
{stop_pos = 0, id = 0, ch = 0, rule_idx = 0, lookback = 0, nglyphs = 0, 
reversed_p = false, parent_it = 0x0, charpos = 0, nchars = 0, nbytes = 
0, from = 0, to = 0, width = 0}, face_id = 0, u = {image = {object = 
0x0, slice = {x = 0x0, y = 0x0, width = 0x0, height = 0x0}, image_id = 
0}, stretch = {object = 0x0}, xwidget = {object = 0x0}}, position = 
{charpos = 0, bytepos = 0}, current = {pos = {charpos = 0, bytepos = 0}, 
overlay_string_index = 0, string_pos = {charpos = 0, bytepos = 0}, 
dpvec_index = 0}, from_overlay = 0x0, area = LEFT_MARGIN_AREA, method = 
GET_FROM_BUFFER, paragraph_embedding = NEUTRAL_DIR, multibyte_p = false, 
string_from_display_prop_p = false, string_from_prefix_prop_p = false, 
display_ellipsis_p = false, avoid_cursor_p = false, bidi_p = false, 
from_disp_prop_p = false, line_wrap = TRUNCATE, voffset = 0, space_width 
= 0x0, font_height = 0x0}, {string = 0x0, string_nchars = 0, end_charpos 
= 0, stop_charpos = 0, prev_stop = 0, base_level_stop = 0, cmp_it = 
{stop_pos = 0, id = 0, ch = 0, rule_idx = 0, lookback = 0, nglyphs = 0, 
reversed_p = false, parent_it = 0x0, charpos = 0, nchars = 0, nbytes = 
0, from = 0, to = 0, width = 0}, face_id = 0, u = {image = {object = 
0x0, slice = {x = 0x0, y = 0x0, width = 0x0, height = 0x0}, image_id = 
0}, stretch = {object = 0x0}, xwidget = {object = 0x0}}, position = 
{charpos = 0, bytepos = 0}, current = {pos = {charpos = 0, bytepos = 0}, 
overlay_string_index = 0, string_pos = {charpos = 0, bytepos = 0}, 
dpvec_index = 0}, from_overlay = 0x0, area = LEFT_MARGIN_AREA, method = 
GET_FROM_BUFFER, paragraph_embedding = NEUTRAL_DIR, multibyte_p = false, 
string_from_display_prop_p = false, string_from_prefix_prop_p = false, 
display_ellipsis_p = false, avoid_cursor_p = false, bidi_p = false, 
from_disp_prop_p = false, line_wrap = TRUNCATE, voffset = 0, space_width 
= 0x0, font_height = 0x0}}, sp = 1, selective = 0, what = IT_IMAGE, 
face_id = 34, selective_display_ellipsis_p = true, ctl_arrow_p = true, 
face_box_p = false, start_of_box_run_p = false, end_of_box_run_p = 
false, overlay_strings_at_end_processed_p = false, 
ignore_overlay_strings_at_pos_p = false, glyph_not_available_p = false, 
starts_in_middle_of_char_p = false, face_before_selective_p = false, 
constrain_row_ascent_descent_p = false, line_number_produced_p = true, 
align_visually_p = false, line_wrap = WORD_WRAP, base_face_id = 34, c = 
101, len = 1, cmp_it = {stop_pos = 129, id = -1, ch = -2, rule_idx = 0, 
lookback = 0, nglyphs = 0, reversed_p = false, parent_it = 
0x7fff258031d0, charpos = 0, nchars = 0, nbytes = 0, from = 0, to = 0, 
width = 0}, char_to_display = 101, glyphless_method = 
GLYPHLESS_DISPLAY_THIN_SPACE, image_id = 13, xwidget = 0x0, slice = {x = 
0x0, y = 0x0, width = 0x0, height = 0x0}, space_width = 0x0, voffset = 
0, tab_width = 8, font_height = 0x0, object = 0x57baa2a18f9d, position = 
{charpos = 126, bytepos = 126}, truncation_pixel_width = 0, 
continuation_pixel_width = 20, first_visible_x = 0, last_visible_x = 
1012, last_visible_y = 918, extra_line_spacing = 0, 
max_extra_line_spacing = 0, override_ascent = -1, override_descent = 0, 
override_boff = 0, glyph_row = 0x57baa2af4120, area = TEXT_AREA, nglyphs 
= 1, pixel_width = 948, ascent = 20, descent = 20, max_ascent = 36, 
max_descent = 10, phys_ascent = 20, phys_descent = 20, max_phys_ascent = 
25, max_phys_descent = 8, current_x = 180, wrap_prefix_width = 0, 
continuation_lines_width = 0, eol_pos = {charpos = 0, bytepos = 0}, 
current_y = 230, first_vpos = 0, vpos = 5, hpos = 9, lnum = 3, 
lnum_bytepos = 121, lnum_width = 2, lnum_pixel_width = 80, pt_lnum = 0, 
stretch_adjust = 0, left_user_fringe_bitmap = 0, 
right_user_fringe_bitmap = 0, left_user_fringe_face_id = 0, 
right_user_fringe_face_id = 0, bidi_p = true, bidi_it = {bytepos = 126, 
charpos = 126, ch = 65532, nchars = 1, ch_len = 1, type = STRONG_L, 
type_after_wn = NEUTRAL_ON, orig_type = NEUTRAL_ON, resolved_level = 0 
'\000', isolate_level = 0 '\000', invalid_levels = 0, invalid_isolates = 
0, prev = {charpos = 125, type = STRONG_L, orig_type = STRONG_L}, 
last_strong = {charpos = 125, type = STRONG_L, orig_type = STRONG_L}, 
next_for_neutral = {charpos = -1, type = UNKNOWN_BT, orig_type = 
UNKNOWN_BT}, prev_for_neutral = {charpos = 125, type = STRONG_L, 
orig_type = STRONG_L}, next_for_ws = {charpos = -1, type = UNKNOWN_BT, 
orig_type = UNKNOWN_BT}, bracket_pairing_pos = -1, bracket_enclosed_type 
= UNKNOWN_BT, next_en_pos = 0, next_en_type = UNKNOWN_BT, sos = L2R, 
scan_dir = 1, disp_pos = 302, disp_prop = 0, stack_idx = 0, level_stack 
= {{next_for_neutral_pos = 0, next_for_neutral_type = 0, 
last_strong_type = 0, prev_for_neutral_type = 0, level = 0 '\000', flags 
= 0 '\000'} <repeats 128 times>}, string = {lstring = 0x0, s = 0x0, 
schars = 0, bufpos = 0, from_disp_str = false, unibyte = false}, w = 
0x57baa27b9c28, paragraph_dir = L2R, separator_limit = -1, first_elt = 
false, new_paragraph = false, frame_window_p = true}, 
paragraph_embedding = NEUTRAL_DIR, min_width_property = 0x0, 
min_width_start = 0}

An updated patch resolves the issue (also attached):

diff --git a/src/xdisp.c b/src/xdisp.c
index 4e8bb7d9b97..c6b87b08ae9 100644
--- a/src/xdisp.c
+++ b/src/xdisp.c
@@ -31956,12 +31956,14 @@ produce_image_glyph (struct it *it)
       word-wrap, unless the image starts at column zero, because
       wrapping correctly needs the real pixel width of the image. */
    if ((it->line_wrap != WORD_WRAP
-       || it->hpos == 0
+       || it->hpos == (it->lnum_width ? it->lnum_width + 2 : 0)
         /* Always crop images larger than the window-width, minus 1 
space.  */
-       || it->pixel_width > it->last_visible_x - FRAME_COLUMN_WIDTH 
(it->f))
+       || it->pixel_width > it->last_visible_x - FRAME_COLUMN_WIDTH (it->f)
+                                - it->lnum_pixel_width)
        && (crop = it->pixel_width - (it->last_visible_x - it->current_x),
-         crop > 0)
-      && (it->hpos == 0 || it->pixel_width > it->last_visible_x / 4))
+          crop > 0)
+      && (it->hpos == (it->lnum_width ? it->lnum_width + 2 : 0)
+          || it->pixel_width > it->last_visible_x / 4))
      {
        it->pixel_width -= crop;
        slice.width -= crop;

However, I think that images wider than the line should always start on 
a new line, instead of being almost clipped on the current line. E.g., 
see bad_clipping.png. Do you think I should file a separate bug report 
for this?
From 63b1718ab7b1c3f8b5265ae33c273fd016c9c657 Mon Sep 17 00:00:00 2001
From: Yifan Zhu <[email-masked]>
Date: Wed, 26 Mar 2025 09:28:39 -0700
Subject: [PATCH] correctly handle image cropping under
 'display-line-numbers-mode'

* src/xdisp.c (produce_image_glyph): Take into consideration
consideration the glyphs produced for line-number display
when deciding whether to crop images
(Bug#77217)
---
 src/xdisp.c | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/src/xdisp.c b/src/xdisp.c
index 4e8bb7d9b97..c6b87b08ae9 100644
--- a/src/xdisp.c
+++ b/src/xdisp.c
@@ -31956,12 +31956,14 @@ produce_image_glyph (struct it *it)
      word-wrap, unless the image starts at column zero, because
      wrapping correctly needs the real pixel width of the image.  */
   if ((it->line_wrap != WORD_WRAP
-       || it->hpos == 0
+       || it->hpos == (it->lnum_width ? it->lnum_width + 2 : 0)
        /* Always crop images larger than the window-width, minus 1 space.  */
-       || it->pixel_width > it->last_visible_x - FRAME_COLUMN_WIDTH (it->f))
+       || it->pixel_width > it->last_visible_x - FRAME_COLUMN_WIDTH (it->f)
+                                - it->lnum_pixel_width)
       && (crop = it->pixel_width - (it->last_visible_x - it->current_x),
-	  crop > 0)
-      && (it->hpos == 0 || it->pixel_width > it->last_visible_x / 4))
+          crop > 0)
+      && (it->hpos == (it->lnum_width ? it->lnum_width + 2 : 0)
+          || it->pixel_width > it->last_visible_x / 4))
     {
       it->pixel_width -= crop;
       slice.width -= crop;
-- 
2.49.0