patch 9.1.1296: completion: incorrect truncation logic

Problem:  completion: incorrect truncation logic (after: v9.1.1284)
Solution: replace string allocation with direct screen rendering and
          fixe RTL/LTR truncation calculations (glepnir)

closes: #17081

Signed-off-by: glepnir <glephunter@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index 9fab3f4..089df14 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -3621,6 +3621,7 @@
 	  lastline	'@'		'display' contains lastline/truncate
 	  trunc		'>'		truncated text in the
 					|ins-completion-menu|.
+	  truncrl	'<'		same as "trunc' in 'rightleft' mode
 
 	Any one that is omitted will fall back to the default.
 
@@ -3645,6 +3646,7 @@
 	  lastline	NonText			|hl-NonText|
 	  trunc		one of the many Popup menu highlighting groups like
 			|hl-PmenuSel|
+	  truncrl	same as "trunc"
 
 						*'findfunc'* *'ffu'* *E1514*
 'findfunc' 'ffu'	string	(default empty)
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index 8a7f49e..befd92f 100644
--- a/runtime/doc/version9.txt
+++ b/runtime/doc/version9.txt
@@ -1,4 +1,4 @@
-*version9.txt*  For Vim version 9.1.  Last change: 2025 Apr 08
+*version9.txt*  For Vim version 9.1.  Last change: 2025 Apr 12
 
 
 		  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -41631,6 +41631,7 @@
   and CTRL-D / CTRL-U for half-pagewise scrolling
 - New option value for 'fillchars':
   	"trunc"		- configure truncation indicator, 'pummaxwidth'
+  	"truncrl"	- like "trunc" but in 'rl' mode, 'pummaxwidth'
 
 Ex commands: ~
 - allow to specify a priority when defining a new sign |:sign-define|
diff --git a/src/popupmenu.c b/src/popupmenu.c
index 556c2c3..07c99f0 100644
--- a/src/popupmenu.c
+++ b/src/popupmenu.c
@@ -604,10 +604,15 @@
     int		last_isabbr = FALSE;
     int		orig_attr = -1;
     int		scroll_range = pum_size - pum_height;
-    char_u	*new_str = NULL;
-    char_u	*ptr = NULL;
     int		remaining = 0;
-    int		fcs_trunc = curwin->w_fill_chars.trunc;
+    int		fcs_trunc;
+
+#ifdef  FEAT_RIGHTLEFT
+    if (pum_rl)
+	fcs_trunc = curwin->w_fill_chars.truncrl;
+    else
+#endif
+	fcs_trunc = curwin->w_fill_chars.trunc;
 
     hlf_T	hlfsNorm[3];
     hlf_T	hlfsSel[3];
@@ -722,10 +727,19 @@
 
 			    if (rt != NULL)
 			    {
-				char_u		*rt_start = rt;
-				int		cells;
+				char_u	    *rt_start = rt;
+				int	    cells;
+				int	    over_cell = 0;
+				int	    truncated = FALSE;
 
 				cells = mb_string2cells(rt , -1);
+				truncated = pum_width == p_pmw
+						&& pum_width - totwidth < cells;
+
+				if (pum_width == p_pmw && !truncated
+					&& (j + 1 < 3 && pum_get_item(idx, order[j + 1]) != NULL))
+				    truncated = TRUE;
+
 				if (cells > pum_width)
 				{
 				    do
@@ -746,13 +760,9 @@
 				    }
 				}
 
-				// truncated
-				if (pum_width == p_pmw
-					&& totwidth + 1 + cells >= pum_width)
+				if (truncated)
 				{
 				    char_u  *orig_rt = rt;
-				    char_u  *old_rt = NULL;
-				    int	    over_cell = 0;
 				    int	    size = 0;
 
 				    remaining = pum_width - totwidth - 1;
@@ -768,26 +778,19 @@
 				    size = (int)STRLEN(orig_rt);
 				    if (cells < remaining)
 					over_cell =  remaining - cells;
-				    new_str = alloc(size + over_cell + 1 + utf_char2len(fcs_trunc));
-				    if (!new_str)
-					return;
-				    ptr = new_str;
-				    if (fcs_trunc != NUL && fcs_trunc != '>')
-					ptr += (*mb_char2bytes)(fcs_trunc, ptr);
+
+				    cells = mb_string2cells(orig_rt, size);
+				    width = cells + over_cell + 1;
+				    rt = orig_rt;
+
+				    if (fcs_trunc != NUL)
+					screen_putchar(fcs_trunc, row, col - width + 1, attr);
 				    else
-					*ptr++ = '<';
-				    if (over_cell)
-				    {
-					vim_memset(ptr, ' ', over_cell);
-					ptr += over_cell;
-				    }
-				    memcpy(ptr, orig_rt, size);
-				    ptr[size] = NUL;
-				    old_rt = rt_start;
-				    rt = rt_start = new_str;
-				    vim_free(old_rt);
-				    cells = mb_string2cells(rt, -1);
-				    width = cells;
+					screen_putchar('<', row, col - width + 1, attr);
+
+				    if (over_cell > 0)
+					screen_fill(row, row + 1, col - width + 2,
+						    col - width + 2 + over_cell, ' ', ' ', attr);
 				}
 
 				if (attrs == NULL)
@@ -809,10 +812,16 @@
 		    {
 			if (st != NULL)
 			{
-			    int size = (int)STRLEN(st);
-			    int cells = (*mb_string2cells)(st, size);
-			    char_u *st_end = NULL;
-			    int over_cell = 0;
+			    int		size = (int)STRLEN(st);
+			    int		cells = (*mb_string2cells)(st, size);
+			    char_u	*st_end = NULL;
+			    int		over_cell = 0;
+			    int		truncated = pum_width == p_pmw
+						&& pum_width - totwidth < cells;
+
+			    if (pum_width == p_pmw && !truncated
+				    && (j + 1 < 3 && pum_get_item(idx, order[j + 1]) != NULL))
+				truncated = TRUE;
 
 			    // only draw the text that fits
 			    while (size > 0
@@ -829,8 +838,7 @@
 			    }
 
 			    // truncated
-			    if (pum_width == p_pmw
-				    && totwidth + 1 + cells >= pum_width)
+			    if (truncated)
 			    {
 				remaining = pum_width - totwidth - 1;
 				if (cells > remaining)
@@ -846,28 +854,8 @@
 
 				if (cells < remaining)
 				    over_cell =  remaining - cells;
-				new_str = alloc(size + over_cell + 1 + utf_char2len(fcs_trunc));
-				if (!new_str)
-				    return;
-				memcpy(new_str, st, size);
-				ptr = new_str + size;
-				if (over_cell > 0)
-				{
-				    vim_memset(ptr, ' ', over_cell);
-				    ptr += over_cell;
-				}
-
-				if (fcs_trunc != NUL)
-				    ptr += (*mb_char2bytes)(fcs_trunc, ptr);
-				else
-				    *ptr++ = '>';
-
-				*ptr = NUL;
-				vim_free(st);
-				st = new_str;
-				cells = mb_string2cells(st, -1);
-				size = (int)STRLEN(st);
-				width = cells;
+				cells = mb_string2cells(st, size);
+				width = cells + over_cell + 1;
 			    }
 
 			    if (attrs == NULL)
@@ -875,6 +863,18 @@
 			    else
 				pum_screen_puts_with_attrs(row, col, cells,
 							      st, size, attrs);
+			    if (truncated)
+			    {
+				if (over_cell > 0)
+				    screen_fill(row, row + 1, col + cells,
+					    col + cells + over_cell, ' ', ' ', attr);
+				if (fcs_trunc != NUL)
+				    screen_putchar(fcs_trunc, row,
+					    col + cells + over_cell, attr);
+				else
+				    screen_putchar('>', row,
+					    col + cells + over_cell, attr);
+			    }
 
 			    vim_free(st);
 			}
diff --git a/src/screen.c b/src/screen.c
index 9a5927a..ab37e1d 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -4714,6 +4714,7 @@
     CHARSTAB_ENTRY(&fill_chars.eob,	    "eob"),
     CHARSTAB_ENTRY(&fill_chars.lastline,    "lastline"),
     CHARSTAB_ENTRY(&fill_chars.trunc,	    "trunc"),
+    CHARSTAB_ENTRY(&fill_chars.truncrl,	    "truncrl"),
 };
 static lcs_chars_T lcs_chars;
 static struct charstab lcstab[] =
@@ -4828,6 +4829,7 @@
 		fill_chars.eob = '~';
 		fill_chars.lastline = '@';
 		fill_chars.trunc = '>';
+		fill_chars.truncrl = '<';
 	    }
 	}
 	p = value;
diff --git a/src/structs.h b/src/structs.h
index 9b44598..b5c898d 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -3851,6 +3851,7 @@
     int	eob;
     int	lastline;
     int trunc;
+    int truncrl;
 } fill_chars_T;
 
 /*
diff --git a/src/testdir/dumps/Test_pum_maxwidth_07.dump b/src/testdir/dumps/Test_pum_maxwidth_07.dump
index ada8acb..112e1f5 100644
--- a/src/testdir/dumps/Test_pum_maxwidth_07.dump
+++ b/src/testdir/dumps/Test_pum_maxwidth_07.dump
@@ -1,7 +1,7 @@
 |1+0&#ffffff0|2|3|4|5|6|7|8|9|_|1|2|3|4|5|6|7|8|9|_|1|2|3|4|5|6|7|8|9|_> @44
 |1+0#0000001#e0e0e08|2|3|4|5|6|7|8|9|>| +0#4040ff13#ffffff0@64
 |一*0#0000001#ffd7ff255|二|三|四| +&|>| +0#4040ff13#ffffff0@64
-|a+0#0000001#ffd7ff255|b|c|d|e|f|g|h|i|>| +0#4040ff13#ffffff0@64
+|a+0#0000001#ffd7ff255|b|c|d|e|f|g|h|i|j| +0#4040ff13#ffffff0@64
 |上*0#0000001#ffd7ff255|下|左|右| +&@1| +0#4040ff13#ffffff0@64
 |~| @73
 |~| @73
diff --git a/src/testdir/dumps/Test_pum_maxwidth_08.dump b/src/testdir/dumps/Test_pum_maxwidth_08.dump
index aa41b76..9f92ae7 100644
--- a/src/testdir/dumps/Test_pum_maxwidth_08.dump
+++ b/src/testdir/dumps/Test_pum_maxwidth_08.dump
@@ -1,7 +1,7 @@
 | +0&#ffffff0@43> |_|9|8|7|6|5|4|3|2|1|_|9|8|7|6|5|4|3|2|1|_|9|8|7|6|5|4|3|2|1
 | +0#4040ff13&@64|<+0#0000001#e0e0e08|9|8|7|6|5|4|3|2|1
 | +0#4040ff13#ffffff0@64|<+0#0000001#ffd7ff255| |四*&|三|二|一
-| +0#4040ff13#ffffff0@64|<+0#0000001#ffd7ff255|i|h|g|f|e|d|c|b|a
+| +0#4040ff13#ffffff0@64|j+0#0000001#ffd7ff255|i|h|g|f|e|d|c|b|a
 | +0#4040ff13#ffffff0@64| +0#0000001#ffd7ff255@1|右*&|左|下|上
 | +0#4040ff13#ffffff0@73|~
 | @73|~
diff --git a/src/testdir/test_popup.vim b/src/testdir/test_popup.vim
index d282f91..b2952fd 100644
--- a/src/testdir/test_popup.vim
+++ b/src/testdir/test_popup.vim
@@ -2126,7 +2126,7 @@
     call VerifyScreenDump(buf, 'Test_pum_maxwidth_16', {'rows': 8})
     call term_sendkeys(buf, "\<ESC>")
 
-    call term_sendkeys(buf, ":set fcs+=trunc:…\<CR>")
+    call term_sendkeys(buf, ":set fcs+=truncrl:…\<CR>")
     call term_sendkeys(buf, "S\<C-X>\<C-O>")
     call VerifyScreenDump(buf, 'Test_pum_maxwidth_17', {'rows': 8})
     call term_sendkeys(buf, "\<ESC>")
diff --git a/src/version.c b/src/version.c
index 53b7941..6f3f241 100644
--- a/src/version.c
+++ b/src/version.c
@@ -705,6 +705,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1296,
+/**/
     1295,
 /**/
     1294,