changeset 29560:14b139cbec49 v9.0.0121

patch 9.0.0121: cannot put virtual text after or below a line Commit: https://github.com/vim/vim/commit/b7963df98f9dbbb824713acad2f47c9989fcf8f3 Author: Bram Moolenaar <Bram@vim.org> Date: Sun Jul 31 17:12:43 2022 +0100 patch 9.0.0121: cannot put virtual text after or below a line Problem: Cannot put virtual text after or below a line. Solution: Add "text_align" and "text_wrap" arguments.
author Bram Moolenaar <Bram@vim.org>
date Sun, 31 Jul 2022 18:15:03 +0200
parents 69ceb540c619
children 0634e31bf2f6
files runtime/doc/textprop.txt src/charset.c src/drawline.c src/structs.h src/testdir/dumps/Test_prop_with_text_after_1.dump src/testdir/test_textprop.vim src/textprop.c src/version.c
diffstat 8 files changed, 246 insertions(+), 48 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/textprop.txt
+++ b/runtime/doc/textprop.txt
@@ -141,7 +141,20 @@ prop_add({lnum}, {col}, {props})
 				then "id" must not be present and will be set
 				automatically to a negative number; otherwise
 				zero is used
-		   text		text to be displayed at {col}
+		   text		text to be displayed before {col}, or after the
+				line if {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
+				   below   in the next screen line
+				When omitted "after" is used.
+		   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.
 
@@ -162,17 +175,26 @@ prop_add({lnum}, {col}, {props})
 		added to. When not found, the global property types are used.
 		If not found an error is given.
 							*virtual-text*
-		When "text" is used this text will be displayed at the start
-		location of the text property.  The text of the buffer line
-		will be shifted to make room.  This is called "virtual text".
+		When "text" is used and the column is non-zero then this text
+		will be displayed at the start location of the text property
+		after the text.  The text of the buffer line will be shifted
+		to make room.  This is called "virtual text".
+		When the column is zero the virtual text will appear after the
+		buffer text.  The "text_align" and "text_wrap" arguments
+		determine how it is displayed.
 		The text will be displayed but it is not part of the actual
 		buffer line, the cursor cannot be placed on it.  A mouse click
 		in the text will move the cursor to the first character after
-		the text.
+		the text, or the last character of the line.
 		A negative "id" will be chosen and is returned.  Once a
 		property with "text" has been added for a buffer then using a
 		negative "id" for any other property will give an error:
 		*E1293*
+		Make sure to use a highlight that makes clear to the user that
+		this is virtual text, otherwise it will be very confusing that
+		the text cannot be edited.
+		To separate the virtual text from the buffer text prepend
+		and/or append spaces to the "text" field.
 
 		Can also be used as a |method|: >
 			GetLnum()->prop_add(col, props)
--- a/src/charset.c
+++ b/src/charset.c
@@ -771,6 +771,7 @@ win_linetabsize(win_T *wp, linenr_T lnum
     chartabsize_T cts;
 
     init_chartabsize_arg(&cts, wp, lnum, 0, line, line);
+    cts.cts_with_trailing = len = MAXCOL;
     for ( ; *cts.cts_ptr != NUL && (len == MAXCOL || cts.cts_ptr < line + len);
 						      MB_PTR_ADV(cts.cts_ptr))
 	cts.cts_vcol += win_lbr_chartabsize(&cts, NULL);
@@ -1089,15 +1090,24 @@ win_lbr_chartabsize(
 	    textprop_T *tp = cts->cts_text_props + i;
 
 	    if (tp->tp_id < 0
-		     && tp->tp_col - 1 >= col && tp->tp_col - 1 < col + size
-		     && -tp->tp_id <= wp->w_buffer->b_textprop_text.ga_len)
+		    && ((tp->tp_col - 1 >= col && tp->tp_col - 1 < col + size
+			&& -tp->tp_id <= wp->w_buffer->b_textprop_text.ga_len)
+		    || (tp->tp_col == MAXCOL && (s[0] == NUL || s[1] == NUL)
+						   && cts->cts_with_trailing)))
 	    {
 		char_u *p = ((char_u **)wp->w_buffer->b_textprop_text.ga_data)[
 							       -tp->tp_id - 1];
+		int len = (int)STRLEN(p);
+
 		// TODO: count screen cells
-		cts->cts_cur_text_width = (int)STRLEN(p);
-		size += cts->cts_cur_text_width;
-		break;
+		if (tp->tp_col == MAXCOL)
+		{
+		    // TODO: truncating
+		    if (tp->tp_flags & TP_FLAG_ALIGN_BELOW)
+			len += wp->w_width - (vcol + size) % wp->w_width;
+		}
+		cts->cts_cur_text_width += len;
+		size += len;
 	    }
 	    if (tp->tp_col - 1 > col)
 		break;
--- a/src/drawline.c
+++ b/src/drawline.c
@@ -208,13 +208,16 @@ static buf_T		*current_buf = NULL;
 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;
-    pt1 = text_prop_type_by_id(current_buf, current_text_props[idx1].tp_type);
-    pt2 = text_prop_type_by_id(current_buf, current_text_props[idx2].tp_type);
+    tp1 = &current_text_props[idx1];
+    tp2 = &current_text_props[idx2];
+    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)
 	return 0;
     if (pt1 == NULL)
@@ -223,8 +226,25 @@ text_prop_compare(const void *s1, const 
 	return 1;
     if (pt1->pt_priority != pt2->pt_priority)
 	return pt1->pt_priority > pt2->pt_priority ? 1 : -1;
-    col1 = current_text_props[idx1].tp_col;
-    col2 = current_text_props[idx2].tp_col;
+    col1 = tp1->tp_col;
+    col2 = tp2->tp_col;
+    if (col1 == MAXCOL && col2 == MAXCOL)
+    {
+	int flags1 = 0;
+	int flags2 = 0;
+
+	// order on 0: after, 1: right, 2: below
+	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;
+    }
     return col1 == col2 ? 0 : col1 > col2 ? 1 : -1;
 }
 #endif
@@ -281,10 +301,11 @@ win_line(
     int		saved_c_final = 0;
     int		saved_char_attr = 0;
 
-    int		n_attr = 0;		// chars with special attr
-    int		saved_attr2 = 0;	// char_attr saved for n_attr
-    int		n_attr3 = 0;		// chars with overruling special attr
-    int		saved_attr3 = 0;	// char_attr saved for n_attr3
+    int		n_attr = 0;	    // chars with special attr
+    int		n_attr_skip = 0;    // chars to skip before using extra_attr
+    int		saved_attr2 = 0;    // char_attr saved for n_attr
+    int		n_attr3 = 0;	    // chars with overruling special attr
+    int		saved_attr3 = 0;    // char_attr saved for n_attr3
 
     int		n_skip = 0;		// nr of chars to skip for 'nowrap'
 
@@ -328,6 +349,7 @@ win_line(
     int		text_prop_attr = 0;
     int		text_prop_id = 0;	// active property ID
     int		text_prop_combine = FALSE;
+    int		text_prop_follows = FALSE;  // another text prop to display
 #endif
 #ifdef FEAT_SPELL
     int		has_spell = FALSE;	// this buffer has spell checking
@@ -1472,7 +1494,9 @@ win_line(
 # endif
 		// Add any text property that starts in this column.
 		while (text_prop_next < text_prop_count
-			   && bcol >= text_props[text_prop_next].tp_col - 1)
+			   && (text_props[text_prop_next].tp_col == MAXCOL
+			      ? *ptr == NUL
+			      : bcol >= text_props[text_prop_next].tp_col - 1))
 		{
 		    if (bcol <= text_props[text_prop_next].tp_col - 1
 					   + text_props[text_prop_next].tp_len)
@@ -1484,13 +1508,15 @@ win_line(
 		text_prop_combine = FALSE;
 		text_prop_type = NULL;
 		text_prop_id = 0;
-		if (text_props_active > 0)
+		if (text_props_active > 0 && n_extra == 0)
 		{
 		    int used_tpi = -1;
 		    int used_attr = 0;
+		    int other_tpi = -1;
 
 		    // 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,
@@ -1511,10 +1537,11 @@ win_line(
 				   hl_combine_attr(text_prop_attr, used_attr);
 			    text_prop_combine = pt->pt_flags & PT_FLAG_COMBINE;
 			    text_prop_id = text_props[tpi].tp_id;
+			    other_tpi = used_tpi;
 			    used_tpi = tpi;
 			}
 		    }
-		    if (n_extra == 0 && text_prop_id < 0 && used_tpi >= 0
+		    if (text_prop_id < 0 && used_tpi >= 0
 			    && -text_prop_id
 				      <= wp->w_buffer->b_textprop_text.ga_len)
 		    {
@@ -1523,6 +1550,11 @@ win_line(
 							   -text_prop_id - 1];
 			if (p != NULL)
 			{
+			    int	    right = (text_props[used_tpi].tp_flags
+							& TP_FLAG_ALIGN_RIGHT);
+			    int	    below = (text_props[used_tpi].tp_flags
+							& TP_FLAG_ALIGN_BELOW);
+
 			    p_extra = p;
 			    c_extra = NUL;
 			    c_final = NUL;
@@ -1530,6 +1562,33 @@ win_line(
 			    extra_attr = used_attr;
 			    n_attr = n_extra;
 			    text_prop_attr = 0;
+			    if (*ptr == NUL)
+				// don't combine char attr after EOL
+				text_prop_combine = FALSE;
+
+			    // TODO: truncation if it doesn't fit
+			    if (right || below)
+			    {
+				int	added = wp->w_width - col;
+				char_u	*l;
+
+				// Right-align: fill with spaces
+				// TODO: count screen columns
+				if (right)
+				    added -= n_extra;
+				if (added < 0 || (below && col == 0))
+				    added = 0;
+				l = alloc(n_extra + added + 1);
+				if (l != NULL)
+				{
+				    vim_memset(l, ' ', added);
+				    STRCPY(l + added, p);
+				    vim_free(p_extra_free);
+				    p_extra = p_extra_free = l;
+				    n_extra += added;
+				    n_attr_skip = added;
+				}
+			    }
 
 			    // If the cursor is on or after this position,
 			    // move it forward.
@@ -1541,6 +1600,10 @@ win_line(
 			// reset the ID in the copy to avoid it being used
 			// again
 			text_props[used_tpi].tp_id = -MAXCOL;
+
+			// If another text prop follows the condition below at
+			// the last window column must know.
+			text_prop_follows = other_tpi != -1;
 		    }
 		}
 	    }
@@ -2641,8 +2704,9 @@ win_line(
 	}
 #endif
 
-	// Don't override visual selection highlighting.
-	if (n_attr > 0
+	// Use "extra_attr", but don't override visual selection highlighting.
+	// Don't use "extra_attr" until n_attr_skip is zero.
+	if (n_attr_skip == 0 && n_attr > 0
 		&& draw_state == WL_LINE
 		&& !attr_pri)
 	{
@@ -3188,8 +3252,11 @@ win_line(
 	    char_attr = saved_attr3;
 
 	// restore attributes after last 'listchars' or 'number' char
-	if (n_attr > 0 && draw_state == WL_LINE && --n_attr == 0)
+	if (n_attr > 0 && draw_state == WL_LINE
+					  && n_attr_skip == 0 && --n_attr == 0)
 	    char_attr = saved_attr2;
+	if (n_attr_skip > 0)
+	    --n_attr_skip;
 
 	// At end of screen line and there is more to come: Display the line
 	// so far.  If there is no more to display it is caught above.
@@ -3203,6 +3270,9 @@ win_line(
 #ifdef FEAT_DIFF
 		    || filler_todo > 0
 #endif
+#ifdef FEAT_PROP_POPUP
+		    || text_prop_follows
+#endif
 		    || (wp->w_p_list && wp->w_lcs_chars.eol != NUL
 						&& p_extra != at_end_str)
 		    || (n_extra != 0 && (c_extra != NUL || *p_extra != NUL)))
@@ -3223,7 +3293,10 @@ win_line(
 	    // '$' and highlighting until last column, break here.
 	    if ((!wp->w_p_wrap
 #ifdef FEAT_DIFF
-		    && filler_todo <= 0
+			&& filler_todo <= 0
+#endif
+#ifdef FEAT_PROP_POPUP
+			&& !text_prop_follows
 #endif
 		    ) || lcs_eol_one == -1)
 		break;
@@ -3251,6 +3324,9 @@ win_line(
 #ifdef FEAT_DIFF
 		     && filler_todo <= 0
 #endif
+#ifdef FEAT_PROP_POPUP
+		     && !text_prop_follows
+#endif
 		     && wp->w_width == Columns)
 	    {
 		// Remember that the line wraps, used for modeless copy.
--- a/src/structs.h
+++ b/src/structs.h
@@ -808,7 +808,13 @@ typedef struct textprop_S
 
 #define TP_FLAG_CONT_NEXT	0x1	// property continues in next line
 #define TP_FLAG_CONT_PREV	0x2	// property was continued from prev line
-#define TP_VIRTUAL		0x4	// virtual text, uses tp_id
+
+// without these text is placed after the end of the line
+#define TP_FLAG_ALIGN_RIGHT	0x10	// virtual text is right-aligned
+#define TP_FLAG_ALIGN_BELOW	0x20	// virtual text on next screen line
+
+#define TP_FLAG_WRAP		0x40	// virtual text wraps - when missing
+					// text is truncated
 
 /*
  * Structure defining a property type.
@@ -4575,6 +4581,8 @@ typedef struct {
     textprop_T	*cts_text_props;	// text props (allocated)
     char	cts_has_prop_with_text; // TRUE if if a property inserts text
     int         cts_cur_text_width;     // width of current inserted text
+    int		cts_with_trailing;	// include size of trailing props with
+					// last character
 #endif
     int		cts_vcol;	    // virtual column at current position
 } chartabsize_T;
new file mode 100644
--- /dev/null
+++ b/src/testdir/dumps/Test_prop_with_text_after_1.dump
@@ -0,0 +1,6 @@
+>s+0&#ffffff0|o|m|e| |t|e|x|t| |h|e|r|e| |a|n|d| |o|t|h|e|r| |t|e|x|t| |t|h|e|r|e| +0&#ffff4012|A|F|T|E|R| | +0&#ffffff0@10| +0#ffffff16#e000002|R|I|G|H|T| 
+| +0#0000000#5fd7ff255|B|E|L|O|W| | +0&#ffffff0@52
+|~+0#4040ff13&| @58
+|~| @58
+|~| @58
+| +0#0000000&@41|1|,|1| @10|A|l@1| 
--- a/src/testdir/test_textprop.vim
+++ b/src/testdir/test_textprop.vim
@@ -2213,6 +2213,26 @@ func Test_prop_inserts_text()
   call delete('XscriptPropsWithText')
 endfunc
 
+func Test_props_with_text_after()
+  CheckRunVimInTerminal
+
+  let lines =<< trim END
+      call setline(1, 'some text here and other text there')
+      call prop_type_add('rightprop', #{highlight: 'ErrorMsg'})
+      call prop_type_add('afterprop', #{highlight: 'Search'})
+      call prop_type_add('belowprop', #{highlight: 'DiffAdd'})
+      call prop_add(1, 0, #{type: 'rightprop', text: ' RIGHT ', text_align: 'right'})
+      call prop_add(1, 0, #{type: 'afterprop', text: ' AFTER ', text_align: 'after'})
+      call prop_add(1, 0, #{type: 'belowprop', text: ' BELOW ', text_align: 'below'})
+  END
+  call writefile(lines, 'XscriptPropsWithTextAfter')
+  let buf = RunVimInTerminal('-S XscriptPropsWithTextAfter', #{rows: 6, cols: 60})
+  call VerifyScreenDump(buf, 'Test_prop_with_text_after_1', {})
+
+  call StopVimInTerminal(buf)
+  call delete('XscriptPropsWithTextAfter')
+endfunc
+
 func Test_removed_prop_with_text_cleans_up_array()
   new
   call setline(1, 'some text here')
--- a/src/textprop.c
+++ b/src/textprop.c
@@ -163,11 +163,6 @@ f_prop_add(typval_T *argvars, typval_T *
 
     start_lnum = tv_get_number(&argvars[0]);
     start_col = tv_get_number(&argvars[1]);
-    if (start_col < 1)
-    {
-	semsg(_(e_invalid_column_number_nr), (long)start_col);
-	return;
-    }
     if (argvars[2].v_type != VAR_DICT)
     {
 	emsg(_(e_dictionary_required));
@@ -190,6 +185,7 @@ prop_add_one(
 	char_u		*type_name,
 	int		id,
 	char_u		*text_arg,
+	int		text_flags,
 	linenr_T	start_lnum,
 	linenr_T	end_lnum,
 	colnr_T		start_col,
@@ -256,7 +252,7 @@ prop_add_one(
 	    col = start_col;
 	else
 	    col = 1;
-	if (col - 1 > (colnr_T)textlen)
+	if (col - 1 > (colnr_T)textlen && !(col == 0 && text_arg != NULL))
 	{
 	    semsg(_(e_invalid_column_number_nr), (long)start_col);
 	    goto theend;
@@ -271,6 +267,13 @@ prop_add_one(
 	if (length < 0)
 	    length = 0;		// zero-width property
 
+	if (text_arg != NULL)
+	{
+	    length = 1;		// text is placed on one character
+	    if (col == 0)
+		col = MAXCOL;	// after the line
+	}
+
 	// Allocate the new line with space for the new property.
 	newtext = alloc(buf->b_ml.ml_line_len + sizeof(textprop_T));
 	if (newtext == NULL)
@@ -296,8 +299,9 @@ prop_add_one(
 	tmp_prop.tp_len = length;
 	tmp_prop.tp_id = id;
 	tmp_prop.tp_type = type->pt_id;
-	tmp_prop.tp_flags = (lnum > start_lnum ? TP_FLAG_CONT_PREV : 0)
-			  | (lnum < end_lnum ? TP_FLAG_CONT_NEXT : 0);
+	tmp_prop.tp_flags = text_flags
+			    | (lnum > start_lnum ? TP_FLAG_CONT_PREV : 0)
+			    | (lnum < end_lnum ? TP_FLAG_CONT_NEXT : 0);
 	mch_memmove(newprops + i * sizeof(textprop_T), &tmp_prop,
 							   sizeof(textprop_T));
 
@@ -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, start_lnum, end_lnum,
+	if (prop_add_one(buf, type_name, id, NULL, 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		flags = 0;
 
     if (dict == NULL || !dict_has_key(dict, "type"))
     {
@@ -483,6 +488,45 @@ prop_add_common(
 	    goto theend;
 	// use a default length of 1 to make multiple props show up
 	end_col = start_col + 1;
+
+	if (dict_has_key(dict, "text_align"))
+	{
+	    char_u *p = dict_get_string(dict, "text_align", FALSE);
+
+	    if (p == NULL)
+		goto theend;
+	    if (STRCMP(p, "right") == 0)
+		flags |= TP_FLAG_ALIGN_RIGHT;
+	    else if (STRCMP(p, "below") == 0)
+		flags |= TP_FLAG_ALIGN_BELOW;
+	    else if (STRCMP(p, "after") != 0)
+	    {
+		semsg(_(e_invalid_value_for_argument_str_str), "text_align", p);
+		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)
+		flags |= TP_FLAG_WRAP;
+	    else if (STRCMP(p, "truncate") != 0)
+	    {
+		semsg(_(e_invalid_value_for_argument_str_str), "text_wrap", p);
+		goto theend;
+	    }
+	}
+    }
+
+    // Column must be 1 or more for a normal text property; when "text" is
+    // present zero means it goes after the line.
+    if (start_col < (text == NULL ? 1 : 0))
+    {
+	semsg(_(e_invalid_column_number_nr), (long)start_col);
+	goto theend;
     }
 
     if (dict_arg != NULL && get_bufnr_from_arg(dict_arg, &buf) == FAIL)
@@ -501,7 +545,7 @@ prop_add_common(
     // correctly set.
     buf->b_has_textprop = TRUE;  // this is never reset
 
-    prop_add_one(buf, type_name, id, text,
+    prop_add_one(buf, type_name, id, text, flags,
 				    start_lnum, end_lnum, start_col, end_col);
     text = NULL;
 
@@ -1738,22 +1782,32 @@ typedef struct
  */
     static adjustres_T
 adjust_prop(
-	textprop_T *prop,
-	colnr_T col,
-	int added,
-	int flags)
+	textprop_T  *prop,
+	colnr_T	    col,
+	int	    added,
+	int	    flags)
 {
-    proptype_T	*pt = text_prop_type_by_id(curbuf, prop->tp_type);
-    int		start_incl = (pt != NULL
-				    && (pt->pt_flags & PT_FLAG_INS_START_INCL))
+    proptype_T	*pt;
+    int		start_incl;
+    int		end_incl;
+    int		droppable;
+    adjustres_T res = {TRUE, FALSE};
+
+    // prop after end of the line doesn't move
+    if (prop->tp_col == MAXCOL)
+    {
+	res.dirty = FALSE;
+	return res;
+    }
+
+    pt = text_prop_type_by_id(curbuf, prop->tp_type);
+    start_incl = (pt != NULL && (pt->pt_flags & PT_FLAG_INS_START_INCL))
 				|| (flags & APC_SUBSTITUTE)
 				|| (prop->tp_flags & TP_FLAG_CONT_PREV);
-    int		end_incl = (pt != NULL
-				      && (pt->pt_flags & PT_FLAG_INS_END_INCL))
+    end_incl = (pt != NULL && (pt->pt_flags & PT_FLAG_INS_END_INCL))
 				|| (prop->tp_flags & TP_FLAG_CONT_NEXT);
-    // Do not drop zero-width props if they later can increase in size.
-    int		droppable = !(start_incl || end_incl);
-    adjustres_T res = {TRUE, FALSE};
+    // do not drop zero-width props if they later can increase in size
+    droppable = !(start_incl || end_incl);
 
     if (added > 0)
     {
--- a/src/version.c
+++ b/src/version.c
@@ -736,6 +736,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    121,
+/**/
     120,
 /**/
     119,