changeset 29816:bbe62ea78aac v9.0.0247

patch 9.0.0247: cannot add padding to virtual text without highlight Commit: https://github.com/vim/vim/commit/f396ce83eebf6c61596184231d39ce4d41eeac04 Author: Bram Moolenaar <Bram@vim.org> Date: Tue Aug 23 18:39:37 2022 +0100 patch 9.0.0247: cannot add padding to virtual text without highlight Problem: Cannot add padding to virtual text without highlight. Solution: Add the "text_padding_left" argument. (issue https://github.com/vim/vim/issues/10906)
author Bram Moolenaar <Bram@vim.org>
date Tue, 23 Aug 2022 19:45:05 +0200
parents 9941dc321348
children 1fa9e8491729
files runtime/doc/textprop.txt src/charset.c src/drawline.c src/errors.h src/proto/drawline.pro src/proto/textprop.pro src/structs.h src/testdir/dumps/Test_prop_right_align_twice_2.dump src/testdir/dumps/Test_prop_text_with_padding_1.dump src/testdir/dumps/Test_prop_text_with_padding_2.dump src/testdir/dumps/Test_prop_text_with_padding_3.dump src/testdir/dumps/Test_prop_with_text_after_wraps_1.dump src/testdir/test_textprop.vim src/textprop.c src/version.c
diffstat 15 files changed, 402 insertions(+), 188 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/textprop.txt
+++ b/runtime/doc/textprop.txt
@@ -126,6 +126,7 @@ prop_add({lnum}, {col}, {props})
 		If {col} is invalid an error is given. *E964*
 
 		{props} is a dictionary with these fields:
+		   type		name of the text property type
 		   length	length of text in bytes, can only be used
 				for a property that does not continue in
 				another line; can be zero
@@ -142,9 +143,10 @@ prop_add({lnum}, {col}, {props})
 				automatically to a negative number; otherwise
 				zero is used
 		   text		text to be displayed before {col}, or after the
-				line if {col} is zero
+				line if {col} is zero; prepend and/or append
+				spaces for padding with highlighting
 		   					*E1294*
-		   text_align	when "text" is present and {col} is zero
+		   text_align	when "text" is present and {col} is zero;
 				specifies where to display the text:
 				   after   after the end of the line
 				   right   right aligned in the window (unless
@@ -152,14 +154,20 @@ prop_add({lnum}, {col}, {props})
 					   line)
 				   below   in the next screen line
 				When omitted "after" is used.  Only one
-				"right" property can fit in earch line.
+				"right" property can fit in each line, if
+				there are two ore more these will go in a
+				separate line (still right aligned).
+		   text_padding_left				*E1296*
+				used when "text" is present and {col} is zero;
+				padding between the end of the text line
+				(leftmost column for "below") and the virtual
+				text, not highlighted
 		   text_wrap	when "text" is present and {col} is zero,
 				specifies what happens if the text doesn't
 				fit:
 				   wrap      wrap the text to the next line
 				   truncate  truncate the text to make it fit
 		   		When omitted "truncate" is used.
-		   type		name of the text property type
 		All fields except "type" are optional.
 
 		It is an error when both "length" and "end_lnum" or "end_col"
--- a/src/charset.c
+++ b/src/charset.c
@@ -957,26 +957,26 @@ init_chartabsize_arg(
 #ifdef FEAT_PROP_POPUP
     if (lnum > 0)
     {
-	char_u *prop_start;
+	char_u	*prop_start;
+	int	count;
 
-	cts->cts_text_prop_count = get_text_props(wp->w_buffer, lnum,
-							  &prop_start, FALSE);
-	if (cts->cts_text_prop_count > 0)
+	count = get_text_props(wp->w_buffer, lnum, &prop_start, FALSE);
+	cts->cts_text_prop_count = count;
+	if (count > 0)
 	{
 	    // Make a copy of the properties, so that they are properly
-	    // aligned.
-	    cts->cts_text_props = ALLOC_MULT(textprop_T,
-						    cts->cts_text_prop_count);
+	    // aligned.  Make it twice as long for the sorting below.
+	    cts->cts_text_props = ALLOC_MULT(textprop_T, count * 2);
 	    if (cts->cts_text_props == NULL)
 		cts->cts_text_prop_count = 0;
 	    else
 	    {
-		int i;
+		int	i;
 
-		mch_memmove(cts->cts_text_props, prop_start,
-			       cts->cts_text_prop_count * sizeof(textprop_T));
-		for (i = 0; i < cts->cts_text_prop_count; ++i)
-		    if (cts->cts_text_props[i].tp_id < 0)
+		mch_memmove(cts->cts_text_props + count, prop_start,
+						   count * sizeof(textprop_T));
+		for (i = 0; i < count; ++i)
+		    if (cts->cts_text_props[i + count].tp_id < 0)
 		    {
 			cts->cts_has_prop_with_text = TRUE;
 			break;
@@ -987,6 +987,27 @@ init_chartabsize_arg(
 		    VIM_CLEAR(cts->cts_text_props);
 		    cts->cts_text_prop_count = 0;
 		}
+		else
+		{
+		    int	    *text_prop_idxs;
+
+		    // Need to sort the array to get any truncation right.
+		    // Do the sorting in the second part of the array, then
+		    // move the sorted props to the first part of the array.
+		    text_prop_idxs = ALLOC_MULT(int, count);
+		    if (text_prop_idxs != NULL)
+		    {
+			for (i = 0; i < count; ++i)
+			    text_prop_idxs[i] = i + count;
+			sort_text_props(curbuf, cts->cts_text_props,
+							text_prop_idxs, count);
+			// Here we want the reverse order.
+			for (i = 0; i < count; ++i)
+			    cts->cts_text_props[count - i - 1] =
+					cts->cts_text_props[text_prop_idxs[i]];
+			vim_free(text_prop_idxs);
+		    }
+		}
 	    }
 	}
     }
@@ -1159,6 +1180,11 @@ win_lbr_chartabsize(
 	int	    col = (int)(s - line);
 	garray_T    *gap = &wp->w_buffer->b_textprop_text;
 
+	// The "$" for 'list' mode will go between the EOL and
+	// the text prop, account for that.
+	if (wp->w_p_list && wp->w_lcs_chars.eol != NUL)
+	    ++vcol;
+
 	for (i = 0; i < cts->cts_text_prop_count; ++i)
 	{
 	    textprop_T *tp = cts->cts_text_props + i;
@@ -1176,46 +1202,21 @@ win_lbr_chartabsize(
 
 		if (p != NULL)
 		{
-		    int	cells = vim_strsize(p);
+		    int	cells;
 
 		    if (tp->tp_col == MAXCOL)
 		    {
-			int below = (tp->tp_flags & TP_FLAG_ALIGN_BELOW);
-			int right = (tp->tp_flags & TP_FLAG_ALIGN_RIGHT);
-			int wrap = (tp->tp_flags & TP_FLAG_WRAP);
-			int len = (int)STRLEN(p);
-			int n_used = len;
-
-			// The "$" for 'list' mode will go between the EOL and
-			// the text prop, account for that.
-			if (wp->w_p_list && wp->w_lcs_chars.eol != NUL)
-			    ++vcol;
+			int n_extra = (int)STRLEN(p);
 
-			// Keep in sync with where textprop_size_after_trunc()
-			// is called in win_line().
-			if (!wrap)
-			{
-			    added = wp->w_width - (vcol + size) % wp->w_width;
-			    cells = textprop_size_after_trunc(wp,
-						     below, added, p, &n_used);
-			}
-			if (below)
-			    cells += wp->w_width - (vcol + size) % wp->w_width;
-			else if (right)
-			{
-			    len = wp->w_width - vcol % wp->w_width;
-			    if (len > cells + size)
-				// add the padding for right-alignment
-				cells = len - size;
-			    else if (len == 0)
-				// padding to right-align in the next line
-				cells += cells > wp->w_width ? 0
-							  :wp->w_width - cells;
-			}
+			cells = text_prop_position(wp, tp,
+					    (vcol + size) % wp->w_width,
+						     &n_extra, &p, NULL, NULL);
 #ifdef FEAT_LINEBREAK
 			no_sbr = TRUE;  // don't use 'showbreak' now
 #endif
 		    }
+		    else
+			cells = vim_strsize(p);
 		    cts->cts_cur_text_width += cells;
 		    cts->cts_start_incl = tp->tp_flags & TP_FLAG_START_INCL;
 		    size += cells;
@@ -1231,6 +1232,8 @@ win_lbr_chartabsize(
 	    if (tp->tp_col != MAXCOL && tp->tp_col - 1 > col)
 		break;
 	}
+	if (wp->w_p_list && wp->w_lcs_chars.eol != NUL)
+	    --vcol;
     }
 # endif
 
--- a/src/drawline.c
+++ b/src/drawline.c
@@ -277,74 +277,123 @@ get_sign_display_info(
 }
 #endif
 
-#ifdef FEAT_PROP_POPUP
-static textprop_T	*current_text_props = NULL;
-static buf_T		*current_buf = NULL;
-
+#if defined(FEAT_PROP_POPUP) || defined(PROTO)
 /*
- * Function passed to qsort() to sort text properties.
- * Return 1 if "s1" has priority over "s2", -1 if the other way around, zero if
- * both have the same priority.
+ * Take care of padding, right-align and truncation of virtual text after a
+ * line.
+ * if "n_attr" is not NULL then "n_extra" and "p_extra" are adjusted for any
+ * padding, right-align and truncation.  Otherwise only the size is computed.
+ * When "n_attr" is NULL returns the number of screen cells used.
+ * Otherwise returns TRUE when drawing continues on the next line.
  */
-    static int
-text_prop_compare(const void *s1, const void *s2)
+    int
+text_prop_position(
+	win_T	    *wp,
+	textprop_T  *tp,
+	int	    vcol,	    // current screen column
+	int	    *n_extra,	    // nr of bytes for virtual text
+	char_u	    **p_extra,	    // virtual text
+	int	    *n_attr,	    // attribute cells, NULL if not used
+	int	    *n_attr_skip)   // cells to skip attr, NULL if not used
 {
-    int  idx1, idx2;
-    textprop_T	*tp1, *tp2;
-    proptype_T  *pt1, *pt2;
-    colnr_T col1, col2;
+    int	    right = (tp->tp_flags & TP_FLAG_ALIGN_RIGHT);
+    int	    below = (tp->tp_flags & TP_FLAG_ALIGN_BELOW);
+    int	    wrap = (tp->tp_flags & TP_FLAG_WRAP);
+    int	    padding = tp->tp_col == MAXCOL && tp->tp_len > 1
+				  ? tp->tp_len - 1 : 0;
+    int	    col_with_padding = vcol + (below ? 0 : padding);
+    int	    room = wp->w_width - col_with_padding;
+    int	    added = room;
+    int	    n_used = *n_extra;
+    char_u  *l = NULL;
+    int	    strsize = vim_strsize(*p_extra);
+    int	    cells = wrap ? strsize
+	      : textprop_size_after_trunc(wp, below, added, *p_extra, &n_used);
 
-    idx1 = *(int *)s1;
-    idx2 = *(int *)s2;
-    tp1 = &current_text_props[idx1];
-    tp2 = &current_text_props[idx2];
-    col1 = tp1->tp_col;
-    col2 = tp2->tp_col;
-    if (col1 == MAXCOL && col2 == MAXCOL)
+    if (wrap || right || below || padding > 0 || n_used < *n_extra)
     {
-	int flags1 = 0;
-	int flags2 = 0;
+	// Right-align: fill with spaces
+	if (right)
+	    added -= cells;
+	if (added < 0
+		|| !(right || below)
+		|| (below
+		    ? (col_with_padding == 0 || !wp->w_p_wrap)
+		    : (n_used < *n_extra)))
+	{
+	    if (right && (wrap || room < PROP_TEXT_MIN_CELLS))
+	    {
+		// right-align on next line instead of wrapping if possible
+		added = wp->w_width - strsize + room;
+		if (added < 0)
+		    added = 0;
+		else
+		    n_used = *n_extra;
+	    }
+	    else
+		added = 0;
+	}
+
+	// With 'nowrap' add one to show the "extends" character if needed (it
+	// doesn't show if the text just fits).
+	if (!wp->w_p_wrap
+		&& n_used < *n_extra
+		&& wp->w_lcs_chars.ext != NUL
+		&& wp->w_p_list)
+	    ++n_used;
+
+	// add 1 for NUL, 2 for when '…' is used
+	if (n_attr != NULL)
+	    l = alloc(n_used + added + padding + 3);
+	if (n_attr == NULL || l != NULL)
+	{
+	    int off = 0;
 
-	// both props add text are after the line, order on 0: after (default),
-	// 1: right, 2: below (comes last)
-	if (tp1->tp_flags & TP_FLAG_ALIGN_RIGHT)
-	    flags1 = 1;
-	if (tp1->tp_flags & TP_FLAG_ALIGN_BELOW)
-	    flags1 = 2;
-	if (tp2->tp_flags & TP_FLAG_ALIGN_RIGHT)
-	    flags2 = 1;
-	if (tp2->tp_flags & TP_FLAG_ALIGN_BELOW)
-	    flags2 = 2;
-	if (flags1 != flags2)
-	    return flags1 < flags2 ? 1 : -1;
+	    if (n_attr != NULL)
+	    {
+		vim_memset(l, ' ', added);
+		off += added;
+		if (padding > 0)
+		{
+		    vim_memset(l + off, ' ', padding);
+		    off += padding;
+		}
+		vim_strncpy(l + off, *p_extra, n_used);
+		off += n_used;
+	    }
+	    else
+	    {
+		off = added + padding + n_used;
+		cells += added + padding;
+	    }
+	    if (n_attr != NULL)
+	    {
+		if (n_used < *n_extra && wp->w_p_wrap)
+		{
+		    char_u *lp = l + off - 1;
+
+		    if (has_mbyte)
+		    {
+			// change last character to '…'
+			lp -= (*mb_head_off)(l, lp);
+			STRCPY(lp, "…");
+			n_used = lp - l + 3 - padding;
+		    }
+		    else
+			// change last character to '>'
+			*lp = '>';
+		}
+		*p_extra = l;
+		*n_extra = n_used + added + padding;
+		*n_attr = mb_charlen(*p_extra);
+		*n_attr_skip = added + padding;
+	    }
+	}
     }
 
-    // property that inserts text has priority over one that doesn't
-    if ((tp1->tp_id < 0) != (tp2->tp_id < 0))
-	return tp1->tp_id < 0 ? 1 : -1;
-
-    // check highest priority, defined by the type
-    pt1 = text_prop_type_by_id(current_buf, tp1->tp_type);
-    pt2 = text_prop_type_by_id(current_buf, tp2->tp_type);
-    if (pt1 != pt2)
-    {
-	if (pt1 == NULL)
-	    return -1;
-	if (pt2 == NULL)
-	    return 1;
-	if (pt1->pt_priority != pt2->pt_priority)
-	    return pt1->pt_priority > pt2->pt_priority ? 1 : -1;
-    }
-
-    // same priority, one that starts first wins
-    if (col1 != col2)
-	return col1 < col2 ? 1 : -1;
-
-    // for a property with text the id can be used as tie breaker
-    if (tp1->tp_id < 0)
-	return tp1->tp_id > tp2->tp_id ? 1 : -1;
-
-    return 0;
+    if (n_attr == NULL)
+	return cells;
+    return (below && col_with_padding > win_col_off(wp) && !wp->w_p_wrap);
 }
 #endif
 
@@ -1219,6 +1268,9 @@ win_line(
 
 	    // Allocate an array for the indexes.
 	    text_prop_idxs = ALLOC_MULT(int, text_prop_count);
+	    if (text_prop_idxs == NULL)
+		VIM_CLEAR(text_props);
+
 	    area_highlighting = TRUE;
 	    extra_check = TRUE;
 	}
@@ -1609,8 +1661,9 @@ win_line(
 		{
 		    int tpi = text_prop_idxs[pi];
 
-		    if (bcol >= text_props[tpi].tp_col - 1
-						  + text_props[tpi].tp_len)
+		    if (text_props[tpi].tp_col != MAXCOL
+			    && bcol >= text_props[tpi].tp_col - 1
+						      + text_props[tpi].tp_len)
 		    {
 			if (pi + 1 < text_props_active)
 			    mch_memmove(text_prop_idxs + pi,
@@ -1674,10 +1727,8 @@ win_line(
 		    // Sort the properties on priority and/or starting last.
 		    // Then combine the attributes, highest priority last.
 		    text_prop_follows = FALSE;
-		    current_text_props = text_props;
-		    current_buf = wp->w_buffer;
-		    qsort((void *)text_prop_idxs, (size_t)text_props_active,
-					       sizeof(int), text_prop_compare);
+		    sort_text_props(wp->w_buffer, text_props,
+					    text_prop_idxs, text_props_active);
 
 		    for (pi = 0; pi < text_props_active; ++pi)
 		    {
@@ -1704,23 +1755,28 @@ win_line(
 			    && -text_prop_id
 				      <= wp->w_buffer->b_textprop_text.ga_len)
 		    {
-			char_u *p = ((char_u **)wp->w_buffer
+			textprop_T  *tp = &text_props[used_tpi];
+			char_u	    *p = ((char_u **)wp->w_buffer
 						   ->b_textprop_text.ga_data)[
 							   -text_prop_id - 1];
 
 			// reset the ID in the copy to avoid it being used
 			// again
-			text_props[used_tpi].tp_id = -MAXCOL;
+			tp->tp_id = -MAXCOL;
 
 			if (p != NULL)
 			{
-			    int	    right = (text_props[used_tpi].tp_flags
+			    int	    right = (tp->tp_flags
 							& TP_FLAG_ALIGN_RIGHT);
-			    int	    below = (text_props[used_tpi].tp_flags
+			    int	    below = (tp->tp_flags
 							& TP_FLAG_ALIGN_BELOW);
-			    int	    wrap = (text_props[used_tpi].tp_flags
-							& TP_FLAG_WRAP);
+			    int	    wrap = (tp->tp_flags & TP_FLAG_WRAP);
+			    int	    padding = tp->tp_col == MAXCOL
+						 && tp->tp_len > 1
+							  ? tp->tp_len - 1 : 0;
 
+			    // Insert virtual text before the current
+			    // character, or add after the end of the line.
 			    wlv.p_extra = p;
 			    wlv.c_extra = NUL;
 			    wlv.c_final = NUL;
@@ -1746,72 +1802,30 @@ win_line(
 			    // Keep in sync with where
 			    // textprop_size_after_trunc() is called in
 			    // win_lbr_chartabsize().
-			    if ((right || below || !wrap) && wp->w_width > 2)
+			    if ((right || below || !wrap || padding > 0)
+							    && wp->w_width > 2)
 			    {
-				int	added = wp->w_width - wlv.col;
-				int	n_used = wlv.n_extra;
-				char_u	*l;
-				int	strsize = wrap
-					  ? vim_strsize(wlv.p_extra)
-					  : textprop_size_after_trunc(wp,
-					   below, added, wlv.p_extra, &n_used);
-
-				if (wrap || right || below
-						       || n_used < wlv.n_extra)
-				{
-				    // Right-align: fill with spaces
-				    if (right)
-					added -= strsize;
-				    if (added < 0
-					    || (below
-						? wlv.col == 0 || !wp->w_p_wrap
-						: n_used < wlv.n_extra))
-					added = 0;
+				char_u	*prev_p_extra = wlv.p_extra;
+				int	start_line;
 
-				    // With 'nowrap' add one to show the
-				    // "extends" character if needed (it
-				    // doesn't show it the text just fits).
-				    if (!wp->w_p_wrap
-					    && n_used < wlv.n_extra
-					    && wp->w_lcs_chars.ext != NUL
-					    && wp->w_p_list)
-					++n_used;
-
-				    // add 1 for NUL, 2 for when '…' is used
-				    l = alloc(n_used + added + 3);
-				    if (l != NULL)
-				    {
-					vim_memset(l, ' ', added);
-					vim_strncpy(l + added, wlv.p_extra,
-								       n_used);
-					if (n_used < wlv.n_extra
-							       && wp->w_p_wrap)
-					{
-					    char_u *lp = l + added + n_used - 1;
-
-					    if (has_mbyte)
-					    {
-						// change last character to '…'
-						lp -= (*mb_head_off)(l, lp);
-						STRCPY(lp, "…");
-						n_used = lp - l + 3;
-					    }
-					    else
-						// change last character to '>'
-						*lp = '>';
-					}
-					vim_free(p_extra_free2);
-					wlv.p_extra = p_extra_free2 = l;
-					wlv.n_extra = n_used + added;
-					n_attr_skip = added;
-					n_attr = mb_charlen(wlv.p_extra);
-				    }
+				// Take care of padding, right-align and
+				// truncation.
+				// Shared with win_lbr_chartabsize(), must do
+				// exactly the same.
+				start_line = text_prop_position(wp, tp,
+						    wlv.col,
+						    &wlv.n_extra, &wlv.p_extra,
+						    &n_attr, &n_attr_skip);
+				if (wlv.p_extra != prev_p_extra)
+				{
+				    // wlv.p_extra was allocated
+				    vim_free(p_extra_free2);
+				    p_extra_free2 = wlv.p_extra;
 				}
 
 				// When 'wrap' is off then for "below" we need
 				// to start a new line explictly.
-				if (below && wlv.col > win_col_off(wp)
-							      && !wp->w_p_wrap)
+				if (start_line)
 				{
 				    draw_screen_line(wp, &wlv);
 
--- a/src/errors.h
+++ b/src/errors.h
@@ -1218,6 +1218,8 @@ EXTERN char e_pattern_not_found_str[]
 	INIT(= N_("E486: Pattern not found: %s"));
 EXTERN char e_argument_must_be_positive[]
 	INIT(= N_("E487: Argument must be positive"));
+EXTERN char e_argument_must_be_positive_str[]
+	INIT(= N_("E487: Argument must be positive: %s"));
 EXTERN char e_trailing_characters[]
 	INIT(= N_("E488: Trailing characters"));
 EXTERN char e_trailing_characters_str[]
@@ -3319,4 +3321,6 @@ EXTERN char e_can_only_use_text_align_wh
 #ifdef FEAT_PROP_POPUP
 EXTERN char e_cannot_specify_both_type_and_types[]
 	INIT(= N_("E1295: Cannot specify both 'type' and 'types'"));
+EXTERN char e_can_only_use_left_padding_when_column_is_zero[]
+	INIT(= N_("E1296: Can only use left padding when column is zero"));
 #endif
--- a/src/proto/drawline.pro
+++ b/src/proto/drawline.pro
@@ -1,3 +1,4 @@
 /* drawline.c */
+int text_prop_position(win_T *wp, textprop_T *tp, int vcol, int *n_extra, char_u **p_extra, int *n_attr, int *n_attr_skip);
 int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int nochange, int number_only);
 /* vim: set ft=c : */
--- a/src/proto/textprop.pro
+++ b/src/proto/textprop.pro
@@ -6,6 +6,7 @@ int prop_add_common(linenr_T start_lnum,
 int get_text_props(buf_T *buf, linenr_T lnum, char_u **props, int will_change);
 int prop_count_below(buf_T *buf, linenr_T lnum);
 int count_props(linenr_T lnum, int only_starting, int last_line);
+void sort_text_props(buf_T *buf, textprop_T *props, int *idxs, int count);
 int find_visible_prop(win_T *wp, int type_id, int id, textprop_T *prop, linenr_T *found_lnum);
 void add_text_props(linenr_T lnum, textprop_T *text_props, int text_prop_count);
 proptype_T *text_prop_type_by_id(buf_T *buf, int id);
--- a/src/structs.h
+++ b/src/structs.h
@@ -800,7 +800,8 @@ typedef struct memline
 typedef struct textprop_S
 {
     colnr_T	tp_col;		// start column (one based, in bytes)
-    colnr_T	tp_len;		// length in bytes
+    colnr_T	tp_len;		// length in bytes, when tp_id is negative used
+				// for left padding plus one
     int		tp_id;		// identifier
     int		tp_type;	// property type
     int		tp_flags;	// TP_FLAG_ values
--- a/src/testdir/dumps/Test_prop_right_align_twice_2.dump
+++ b/src/testdir/dumps/Test_prop_right_align_twice_2.dump
@@ -1,8 +1,8 @@
-|s+0&#ffffff0|o|m|e| |m|o|r|e| |t|e|x|t|s|o|m|e| |t|e|x|t| |s|o|m|e| |t|e|x|t| |s|o|m|e| |t|e|x|t| |s|o|m|e| |t|e|x|t| +0&#ffd7ff255|n|o|t|h|i|n|g| |h|e|r|e|S+0#ffffff16#e000002|o|m|e| |e|r@1|o
-|r| +0#0000000#ffffff0@60|A+0#ffffff16#e000002|n|o|t|h|e|r| |e|r@1|o|r
+|s+0&#ffffff0|o|m|e| |m|o|r|e| |t|e|x|t|s|o|m|e| |t|e|x|t| |s|o|m|e| |t|e|x|t| |s|o|m|e| |t|e|x|t| |s|o|m|e| |t|e|x|t| +0&#ffd7ff255|n|o|t|h|i|n|g| |h|e|r|e| +0&#ffffff0@8
+@65|S+0#ffffff16#e000002|o|m|e| |e|r@1|o|r
+| +0#0000000#ffffff0@61|A+0#ffffff16#e000002|n|o|t|h|e|r| |e|r@1|o|r
 |l+0#0000000#ffffff0|i|n|e| |t|w>o| @66
 |~+0#4040ff13&| @73
 |~| @73
 |~| @73
-|~| @73
 | +0#0000000&@56|2|,|8| @10|A|l@1| 
new file mode 100644
--- /dev/null
+++ b/src/testdir/dumps/Test_prop_text_with_padding_1.dump
@@ -0,0 +1,8 @@
+>S+0&#ffffff0|o|m|e| |t|e|x|t| |t|o| |a|d@1| |v|i|r|t|u|a|l| |t|e|x|t| |t|o|.| @2|a+0&#ffd7ff255|f|t|e|r| +0&#ffffff0@5|r+0&#ffd7ff255|i|g|h|t| |a|l|i|g|n|e|d
+| +0&#ffffff0@3|b+0&#ffd7ff255|e|l|o|w| |t|h|e| |l|i|n|e| +0&#ffffff0@41
+|s|e|c|o|n|d| |l|i|n|e| @48
+|A|n|o|t|h|e|r| |l|i|n|e| |w|i|t|h| |s|o|m|e| |t|e|x|t| |t|o| |m|a|k|e| |t|h|e| |w|r|a|p|.| @5|r+0&#ffd7ff255|i|g|h|t|m|o|s|t
+|~+0#4040ff13#ffffff0| @58
+|~| @58
+|~| @58
+| +0#0000000&@41|1|,|1| @10|A|l@1| 
new file mode 100644
--- /dev/null
+++ b/src/testdir/dumps/Test_prop_text_with_padding_2.dump
@@ -0,0 +1,8 @@
+|x+0&#ffffff0@9|S|o|m|e| |t|e|x|t| |t|o| |a|d@1| |v|i|r|t|u|a|l| |t|e|x|t| |t|o|.| @2|a+0&#ffd7ff255|f|t|e|r| +0&#ffffff0@4|r+0&#ffd7ff255|i|g|…
+| +0&#ffffff0@3|b+0&#ffd7ff255|e|l|o|w| |t|h|e| |l|i|n|e| +0&#ffffff0@41
+|s|e|c|o|n|d| |l|i|n|e| @48
+>x|A|n|o|t|h|e|r| |l|i|n|e| |w|i|t|h| |s|o|m|e| |t|e|x|t| |t|o| |m|a|k|e| |t|h|e| |w|r|a|p|.| @13
+@51|r+0&#ffd7ff255|i|g|h|t|m|o|s|t
+|~+0#4040ff13#ffffff0| @58
+|~| @58
+| +0#0000000&@41|3|,|1| @10|A|l@1| 
new file mode 100644
--- /dev/null
+++ b/src/testdir/dumps/Test_prop_text_with_padding_3.dump
@@ -0,0 +1,8 @@
+>x+0&#ffffff0@10|S|o|m|e| |t|e|x|t| |t|o| |a|d@1| |v|i|r|t|u|a|l| |t|e|x|t| |t|o|.| @2|a+0&#ffd7ff255|f|t|e|r| +0&#ffffff0@7
+@47|r+0&#ffd7ff255|i|g|h|t| |a|l|i|g|n|e|d
+| +0&#ffffff0@3|b+0&#ffd7ff255|e|l|o|w| |t|h|e| |l|i|n|e| +0&#ffffff0@41
+|s|e|c|o|n|d| |l|i|n|e| @48
+|x|A|n|o|t|h|e|r| |l|i|n|e| |w|i|t|h| |s|o|m|e| |t|e|x|t| |t|o| |m|a|k|e| |t|h|e| |w|r|a|p|.| @13
+@51|r+0&#ffd7ff255|i|g|h|t|m|o|s|t
+|~+0#4040ff13#ffffff0| @58
+| +0#0000000&@41|1|,|1| @10|A|l@1| 
--- a/src/testdir/dumps/Test_prop_with_text_after_wraps_1.dump
+++ b/src/testdir/dumps/Test_prop_with_text_after_wraps_1.dump
@@ -1,8 +1,8 @@
 |o+0&#ffffff0|n|e| |t|w|o| |t|h|r|e@1| |f|o|u|r| |f|i|v|e| |s|i|x| |s|e|v|e|n| +0&#ffff4012|O|N|E| |a|n|d| |T|W|O| |a|n|d| |T|H|R|E@1| |a|n|d| 
 |F|O|U|R| |a|n|d| |F|I|V|E| +0&#ffffff0@46
-|o|n|e| |t|w|o| |t|h|r|e@1| |f|o|u|r| |f|i|v|e| |s|i|x| |s|e|v|e|n| +0&#ffff4012|o|n|e| |A|N|D| |t|w|o| |A|N|D| |t|h|r|e@1| |A|N|D| 
-|f|o|u|r| |A|N|D| |f|i|v|e| +0&#ffffff0@46
 |o|n|e| |t|w|o| |t|h|r|e@1| |f|o|u|r| |f|i|v|e| |s|i|x| |s|e|v|e|n| @26
+@20| +0&#ffff4012|o|n|e| |A|N|D| |t|w|o| |A|N|D| |t|h|r|e@1| |A|N|D| |f|o|u|r| |A|N|D| |f|i|v|e
+|o+0&#ffffff0|n|e| |t|w|o| |t|h|r|e@1| |f|o|u|r| |f|i|v|e| |s|i|x| |s|e|v|e|n| @26
 | +0&#ffff4012|o|n|e| |A|N|D| |t|w|o| |A|N|D| |t|h|r|e@1| |A|N|D| |f|o|u|r| |A|N|D| |f|i|v|e| |l|e|t|s| |w|r|a|p| |a|f|t|e|r| |s|o|m
 |e| |m|o|r|e| |t|e|x|t| +0&#ffffff0@48
 |c|u|r|s|o|r| >h|e|r|e| @48
--- a/src/testdir/test_textprop.vim
+++ b/src/testdir/test_textprop.vim
@@ -3041,4 +3041,54 @@ func Test_insert_text_list_mode()
   call delete('XscriptPropsListMode')
 endfunc
 
+func Test_insert_text_with_padding()
+  CheckRunVimInTerminal
+
+  let lines =<< trim END
+      vim9script
+      setline(1, ['Some text to add virtual text to.',
+                  'second line',
+                  'Another line with some text to make the wrap.'])
+      prop_type_add('theprop', {highlight: 'DiffChange'})
+      prop_add(1, 0, {
+          type: 'theprop',
+          text: 'after',
+          text_align: 'after',
+          text_padding_left: 3,
+      })
+      prop_add(1, 0, {
+          type: 'theprop',
+          text: 'right aligned',
+          text_align: 'right',
+          text_padding_left: 5,
+      })
+      prop_add(1, 0, {
+          type: 'theprop',
+          text: 'below the line',
+          text_align: 'below',
+          text_padding_left: 4,
+      })
+      prop_add(3, 0, {
+          type: 'theprop',
+          text: 'rightmost',
+          text_align: 'right',
+          text_padding_left: 6,
+          text_wrap: 'wrap',
+      })
+  END
+  call writefile(lines, 'XscriptPropsPadded')
+  let buf = RunVimInTerminal('-S XscriptPropsPadded', #{rows: 8, cols: 60})
+  call VerifyScreenDump(buf, 'Test_prop_text_with_padding_1', {})
+
+  call term_sendkeys(buf, "ggixxxxxxxxxx\<Esc>")
+  call term_sendkeys(buf, "3Gix\<Esc>")
+  call VerifyScreenDump(buf, 'Test_prop_text_with_padding_2', {})
+
+  call term_sendkeys(buf, "ggix\<Esc>")
+  call VerifyScreenDump(buf, 'Test_prop_text_with_padding_3', {})
+
+  call StopVimInTerminal(buf)
+  call delete('XscriptPropsPadded')
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab
--- a/src/textprop.c
+++ b/src/textprop.c
@@ -171,6 +171,7 @@ prop_add_one(
 	char_u		*type_name,
 	int		id,
 	char_u		*text_arg,
+	int		text_padding_left,
 	int		text_flags,
 	linenr_T	start_lnum,
 	linenr_T	end_lnum,
@@ -264,7 +265,10 @@ prop_add_one(
 	{
 	    length = 1;		// text is placed on one character
 	    if (col == 0)
+	    {
 		col = MAXCOL;	// after the line
+		length += text_padding_left;
+	    }
 	}
 
 	// Allocate the new line with space for the new property.
@@ -390,7 +394,7 @@ f_prop_add_list(typval_T *argvars, typva
 	    emsg(_(e_invalid_argument));
 	    return;
 	}
-	if (prop_add_one(buf, type_name, id, NULL, 0, start_lnum, end_lnum,
+	if (prop_add_one(buf, type_name, id, NULL, 0, 0, start_lnum, end_lnum,
 						start_col, end_col) == FAIL)
 	    return;
     }
@@ -428,6 +432,7 @@ prop_add_common(
     buf_T	*buf = default_buf;
     int		id = 0;
     char_u	*text = NULL;
+    int		text_padding_left = 0;
     int		flags = 0;
 
     if (dict == NULL || !dict_has_key(dict, "type"))
@@ -507,9 +512,20 @@ prop_add_common(
 	    }
 	}
 
+	if (dict_has_key(dict, "text_padding_left"))
+	{
+	    text_padding_left = dict_get_number(dict, "text_padding_left");
+	    if (text_padding_left < 0)
+	    {
+		semsg(_(e_argument_must_be_positive_str), "text_padding_left");
+		goto theend;
+	    }
+	}
+
 	if (dict_has_key(dict, "text_wrap"))
 	{
 	    char_u *p = dict_get_string(dict, "text_wrap", FALSE);
+
 	    if (p == NULL)
 		goto theend;
 	    if (STRCMP(p, "wrap") == 0)
@@ -529,6 +545,11 @@ prop_add_common(
 	semsg(_(e_invalid_column_number_nr), (long)start_col);
 	goto theend;
     }
+    if (start_col > 0 && text_padding_left > 0)
+    {
+	emsg(_(e_can_only_use_left_padding_when_column_is_zero));
+	goto theend;
+    }
 
     if (dict_arg != NULL && get_bufnr_from_arg(dict_arg, &buf) == FAIL)
 	goto theend;
@@ -546,7 +567,7 @@ prop_add_common(
     // correctly set.
     buf->b_has_textprop = TRUE;  // this is never reset
 
-    prop_add_one(buf, type_name, id, text, flags,
+    prop_add_one(buf, type_name, id, text, text_padding_left, flags,
 				    start_lnum, end_lnum, start_col, end_col);
     text = NULL;
 
@@ -655,6 +676,91 @@ count_props(linenr_T lnum, int only_star
     return result;
 }
 
+static textprop_T	*text_prop_compare_props;
+static buf_T		*text_prop_compare_buf;
+
+/*
+ * Function passed to qsort() to sort text properties.
+ * Return 1 if "s1" has priority over "s2", -1 if the other way around, zero if
+ * both have the same priority.
+ */
+    static int
+text_prop_compare(const void *s1, const void *s2)
+{
+    int  idx1, idx2;
+    textprop_T	*tp1, *tp2;
+    proptype_T  *pt1, *pt2;
+    colnr_T col1, col2;
+
+    idx1 = *(int *)s1;
+    idx2 = *(int *)s2;
+    tp1 = &text_prop_compare_props[idx1];
+    tp2 = &text_prop_compare_props[idx2];
+    col1 = tp1->tp_col;
+    col2 = tp2->tp_col;
+    if (col1 == MAXCOL && col2 == MAXCOL)
+    {
+	int flags1 = 0;
+	int flags2 = 0;
+
+	// both props add text are after the line, order on 0: after (default),
+	// 1: right, 2: below (comes last)
+	if (tp1->tp_flags & TP_FLAG_ALIGN_RIGHT)
+	    flags1 = 1;
+	if (tp1->tp_flags & TP_FLAG_ALIGN_BELOW)
+	    flags1 = 2;
+	if (tp2->tp_flags & TP_FLAG_ALIGN_RIGHT)
+	    flags2 = 1;
+	if (tp2->tp_flags & TP_FLAG_ALIGN_BELOW)
+	    flags2 = 2;
+	if (flags1 != flags2)
+	    return flags1 < flags2 ? 1 : -1;
+    }
+
+    // property that inserts text has priority over one that doesn't
+    if ((tp1->tp_id < 0) != (tp2->tp_id < 0))
+	return tp1->tp_id < 0 ? 1 : -1;
+
+    // check highest priority, defined by the type
+    pt1 = text_prop_type_by_id(text_prop_compare_buf, tp1->tp_type);
+    pt2 = text_prop_type_by_id(text_prop_compare_buf, tp2->tp_type);
+    if (pt1 != pt2)
+    {
+	if (pt1 == NULL)
+	    return -1;
+	if (pt2 == NULL)
+	    return 1;
+	if (pt1->pt_priority != pt2->pt_priority)
+	    return pt1->pt_priority > pt2->pt_priority ? 1 : -1;
+    }
+
+    // same priority, one that starts first wins
+    if (col1 != col2)
+	return col1 < col2 ? 1 : -1;
+
+    // for a property with text the id can be used as tie breaker
+    if (tp1->tp_id < 0)
+	return tp1->tp_id > tp2->tp_id ? 1 : -1;
+
+    return 0;
+}
+
+/*
+ * Sort "count" text properties using an array if indexes "idxs" into the list
+ * of text props "props" for buffer "buf".
+ */
+    void
+sort_text_props(
+	buf_T	    *buf,
+	textprop_T  *props,
+	int	    *idxs,
+	int	    count)
+{
+    text_prop_compare_buf = buf;
+    text_prop_compare_props = props;
+    qsort((void *)idxs, (size_t)count, sizeof(int), text_prop_compare);
+}
+
 /*
  * Find text property "type_id" in the visible lines of window "wp".
  * Match "id" when it is > 0.
--- a/src/version.c
+++ b/src/version.c
@@ -732,6 +732,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    247,
+/**/
     246,
 /**/
     245,