# HG changeset patch # User Bram Moolenaar # Date 1659284103 -7200 # Node ID 14b139cbec492e3fce4ed20b6f37216ea89ca602 # Parent 69ceb540c61959f2e697aac4e601601d95368192 patch 9.0.0121: cannot put virtual text after or below a line Commit: https://github.com/vim/vim/commit/b7963df98f9dbbb824713acad2f47c9989fcf8f3 Author: Bram Moolenaar 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. diff --git a/runtime/doc/textprop.txt b/runtime/doc/textprop.txt --- 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) diff --git a/src/charset.c b/src/charset.c --- 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; diff --git a/src/drawline.c b/src/drawline.c --- 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 = ¤t_text_props[idx1]; + tp2 = ¤t_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. diff --git a/src/structs.h b/src/structs.h --- 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; diff --git a/src/testdir/dumps/Test_prop_with_text_after_1.dump b/src/testdir/dumps/Test_prop_with_text_after_1.dump 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| diff --git a/src/testdir/test_textprop.vim b/src/testdir/test_textprop.vim --- 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') diff --git a/src/textprop.c b/src/textprop.c --- 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) { diff --git a/src/version.c b/src/version.c --- 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,