patch 9.2.0320: several bugs with text properties

Problem:  several bugs with text properties
Solution: Fix the bugs, rework the text properties work

related: #19685
fixes:   #19680
fixes:   #19681
fixes:   #12568
fixes:   #19256
closes:  #19869

Co-Authored-By: Paul Ollis <paul@cleversheep.org>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Hirohito Higashi <h.east.727@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Hirohito Higashi
2026-04-07 20:46:10 +00:00
committed by Christian Brabandt
parent c79edc0df9
commit ff41e9d853
20 changed files with 1697 additions and 384 deletions
+4 -2
View File
@@ -1,4 +1,4 @@
*textprop.txt* For Vim version 9.2. Last change: 2026 Apr 06
*textprop.txt* For Vim version 9.2. Last change: 2026 Apr 07
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -511,7 +511,9 @@ will move accordingly.
When text is deleted and a text property no longer includes any text, it is
deleted. However, a text property that was defined as zero-width will remain,
unless the whole line is deleted.
unless the whole line is deleted. When lines are joined by a multi-line
substitute command, virtual text properties on the deleted lines are moved to
the resulting joined line.
*E275*
When a buffer is unloaded, all the text properties are gone. There is no way
to store the properties in a file. You can only re-create them. When a
+2
View File
@@ -52625,6 +52625,8 @@ Changed~
- |json_decode()| is stricter: keywords must be lowercase, lone surrogates are
now invalid
- |js_decode()| rejects lone surrogates
- virtual text properties on lines deleted by a multi-line substitute
are moved to the resulting joined line instead of being dropped.
*added-9.3*
Added ~
-3
View File
@@ -1115,9 +1115,6 @@ free_buffer_stuff(
#endif
#ifdef FEAT_NETBEANS_INTG
netbeans_file_killed(buf);
#endif
#ifdef FEAT_PROP_POPUP
ga_clear_strings(&buf->b_textprop_text);
#endif
map_clear_mode(buf, MAP_ALL_MODES, TRUE, FALSE); // clear local mappings
map_clear_mode(buf, MAP_ALL_MODES, TRUE, TRUE); // clear local abbrevs
+1 -1
View File
@@ -2570,7 +2570,7 @@ truncate_line(int fixpos)
* Saves the lines for undo first if "undo" is TRUE.
*/
void
del_lines(long nlines, int undo)
del_lines(long nlines, int undo)
{
long n;
linenr_T first = curwin->w_cursor.lnum;
+19 -3
View File
@@ -1140,6 +1140,22 @@ init_chartabsize_arg(
cts->cts_text_props[text_prop_idxs[i]];
vim_free(text_prop_idxs);
}
// Convert tp_text_offset to tp_text pointer.
char_u *count_ptr = prop_start - PROP_COUNT_SIZE;
for (i = 0; i < count; ++i)
{
textprop_T *tp = &cts->cts_text_props[i];
if (tp->tp_id < 0 && tp->u.tp_text_offset > 0)
{
tp->u.tp_text = count_ptr + tp->u.tp_text_offset;
tp->tp_flags |= TP_FLAG_VTEXT_PTR;
}
else
tp->u.tp_text = NULL;
}
}
}
}
@@ -1294,7 +1310,7 @@ win_lbr_chartabsize(
int charlen = *s == NUL ? 1 : mb_ptr2len(s);
int i;
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.
@@ -1318,9 +1334,9 @@ win_lbr_chartabsize(
&& ((tp->tp_flags & TP_FLAG_ALIGN_ABOVE)
? col == 0
: s[0] == NUL && cts->cts_with_trailing)))
&& -tp->tp_id - 1 < gap->ga_len)
&& tp->u.tp_text != NULL)
{
char_u *p = ((char_u **)gap->ga_data)[-tp->tp_id - 1];
char_u *p = tp->u.tp_text;
if (p != NULL)
{
+24 -9
View File
@@ -685,8 +685,7 @@ text_prop_position(
int above = (tp->tp_flags & TP_FLAG_ALIGN_ABOVE);
int below = (tp->tp_flags & TP_FLAG_ALIGN_BELOW);
int wrap = tp->tp_col < MAXCOL || (tp->tp_flags & TP_FLAG_WRAP);
int padding = tp->tp_col == MAXCOL && tp->tp_len > 1
? tp->tp_len - 1 : 0;
int padding = tp->tp_col == MAXCOL ? tp->tp_padleft : 0;
int col_with_padding = scr_col + (below ? 0 : padding);
int room = wp->w_width - col_with_padding;
int before = room; // spaces before the text
@@ -1705,9 +1704,29 @@ win_line(
else
text_props = ALLOC_MULT(textprop_T, text_prop_count);
if (text_props != NULL)
{
mch_memmove(text_props, prop_start,
text_prop_count * sizeof(textprop_T));
// Convert tp_text_offset to tp_text pointer for virtual
// text properties. prop_start points into the memline
// after the prop_count field.
char_u *count_ptr = prop_start - PROP_COUNT_SIZE;
for (int i = 0; i < text_prop_count; ++i)
{
if (text_props[i].tp_id < 0
&& text_props[i].u.tp_text_offset > 0)
{
text_props[i].u.tp_text =
count_ptr + text_props[i].u.tp_text_offset;
text_props[i].tp_flags |= TP_FLAG_VTEXT_PTR;
}
else
text_props[i].u.tp_text = NULL;
}
}
// Allocate an array for the indexes.
if (text_prop_count <= WIN_LINE_TEXT_PROP_STACK_LEN)
text_prop_idxs = text_prop_idxs_buf;
@@ -2301,13 +2320,10 @@ win_line(
}
}
if (text_prop_id < 0 && used_tpi >= 0
&& -text_prop_id
<= wp->w_buffer->b_textprop_text.ga_len)
&& text_props[used_tpi].u.tp_text != NULL)
{
textprop_T *tp = &text_props[used_tpi];
char_u *p = ((char_u **)wp->w_buffer
->b_textprop_text.ga_data)[
-text_prop_id - 1];
char_u *p = tp->u.tp_text;
int above = (tp->tp_flags
& TP_FLAG_ALIGN_ABOVE);
int bail_out = FALSE;
@@ -2325,8 +2341,7 @@ win_line(
int wrap = tp->tp_col < MAXCOL
|| (tp->tp_flags & TP_FLAG_WRAP);
int padding = tp->tp_col == MAXCOL
&& tp->tp_len > 1
? tp->tp_len - 1 : 0;
? tp->tp_padleft : 0;
// Insert virtual text before the current
// character, or add after the end of the line.
+1 -2
View File
@@ -3431,9 +3431,8 @@ EXTERN char e_internal_error_shortmess_too_long[]
#ifdef FEAT_EVAL
EXTERN char e_class_variable_str_not_found_in_class_str[]
INIT(= N_("E1337: Class variable \"%s\" not found in class \"%s\""));
// E1338 unused
#endif
// E1339 unused
// E1338 and E1339 unused
#ifdef FEAT_EVAL
EXTERN char e_argument_already_declared_in_class_str[]
INIT(= N_("E1340: Argument already declared in the class: %s"));
+27 -8
View File
@@ -4895,15 +4895,27 @@ ex_substitute(exarg_T *eap)
text_prop_count);
if (text_props != NULL)
{
int pi;
mch_memmove(text_props, prop_start,
text_prop_count * sizeof(textprop_T));
// After joining the text prop columns will
// increase.
for (pi = 0; pi < text_prop_count; ++pi)
text_props[pi].tp_col +=
regmatch.startpos[0].col + sublen - 1;
// Filter out virtual text and continuation
// properties from deleted lines, convert
// offsets to pointers, and adjust columns.
int wi = 0;
for (int pi = 0; pi < text_prop_count; ++pi)
{
// Skip virtual text and continuation
// properties from the deleted line.
if (text_props[pi].tp_id < 0
|| (text_props[pi].tp_flags
& TP_FLAG_CONT_PREV))
continue;
text_props[wi] = text_props[pi];
text_props[wi].tp_col +=
regmatch.startpos[0].col + sublen - 1;
text_props[wi].u.tp_text = NULL;
++wi;
}
text_prop_count = wi;
}
}
}
@@ -5142,7 +5154,14 @@ skip:
break;
for (i = 0; i < nmatch_tl; ++i)
ml_delete(lnum);
mark_adjust(lnum, lnum + nmatch_tl - 1,
if (copycol > 0)
mark_adjust(lnum, lnum + nmatch_tl - 1,
(long)MAXLNUM, -nmatch_tl);
else
// The entire last matched line was consumed,
// so the first line was effectively replaced
// by lines below.
mark_adjust(lnum - 1, lnum - 1,
(long)MAXLNUM, -nmatch_tl);
if (subflags.do_ask)
deleted_lines(lnum, nmatch_tl);
+54 -22
View File
@@ -2930,13 +2930,19 @@ add_text_props_for_append(
{
if (round == 2)
{
uint16_t pc;
if (new_prop_count == 0)
return; // nothing to do
new_len = *len + new_prop_count * sizeof(textprop_T);
new_len = *len + (int)PROP_COUNT_SIZE
+ new_prop_count * (int)sizeof(textprop_T);
new_line = alloc(new_len);
if (new_line == NULL)
return;
mch_memmove(new_line, *line, *len);
// Write prop_count header.
pc = (uint16_t)new_prop_count;
mch_memmove(new_line + *len, &pc, PROP_COUNT_SIZE);
new_prop_count = 0;
}
@@ -2954,8 +2960,10 @@ add_text_props_for_append(
prop.tp_flags |= TP_FLAG_CONT_PREV;
prop.tp_col = 1;
prop.tp_len = *len; // not exactly the right length
mch_memmove(new_line + *len + new_prop_count
* sizeof(textprop_T), &prop, sizeof(textprop_T));
prop.u.tp_text_offset = 0;
mch_memmove(new_line + *len + (int)PROP_COUNT_SIZE
+ new_prop_count * sizeof(textprop_T),
&prop, sizeof(textprop_T));
}
++new_prop_count;
}
@@ -3772,34 +3780,48 @@ adjust_text_props_for_delete(
textlen = STRLEN(text) + 1;
if ((long)textlen >= line_size)
{
// No properties on this line.
if (above)
internal_error("no text property above deleted line");
else
internal_error("no text property below deleted line");
return;
}
this_props_len = line_size - (int)textlen;
if ((long)textlen + (long)PROP_COUNT_SIZE > line_size)
{
internal_error("text property data too short");
return;
}
uint16_t pc;
mch_memmove(&pc, text + textlen, PROP_COUNT_SIZE);
this_props_len = pc * (int)sizeof(textprop_T);
}
found = FALSE;
for (done_this = 0; done_this < this_props_len;
done_this += sizeof(textprop_T))
{
int flag = above ? TP_FLAG_CONT_NEXT
: TP_FLAG_CONT_PREV;
textprop_T prop_this;
char_u *props_start = text + textlen + PROP_COUNT_SIZE;
mch_memmove(&prop_this, text + textlen + done_this,
sizeof(textprop_T));
if ((prop_this.tp_flags & flag)
&& prop_del.tp_id == prop_this.tp_id
&& prop_del.tp_type == prop_this.tp_type)
for (done_this = 0; done_this < this_props_len;
done_this += sizeof(textprop_T))
{
found = TRUE;
prop_this.tp_flags &= ~flag;
mch_memmove(text + textlen + done_this, &prop_this,
int flag = above ? TP_FLAG_CONT_NEXT
: TP_FLAG_CONT_PREV;
textprop_T prop_this;
mch_memmove(&prop_this, props_start + done_this,
sizeof(textprop_T));
break;
if ((prop_this.tp_flags & flag)
&& prop_del.tp_id == prop_this.tp_id
&& prop_del.tp_type == prop_this.tp_type)
{
found = TRUE;
prop_this.tp_flags &= ~flag;
mch_memmove(props_start + done_this, &prop_this,
sizeof(textprop_T));
break;
}
}
}
if (!found)
@@ -4003,13 +4025,23 @@ theend:
#ifdef FEAT_PROP_POPUP
if (textprop_save != NULL)
{
// textprop_save is [prop_count][textprop_T...][vtext...].
// Skip prop_count header and pass only the textprop_T part.
uint16_t pc;
char_u *props_data;
int props_bytes;
mch_memmove(&pc, textprop_save, PROP_COUNT_SIZE);
props_data = textprop_save + PROP_COUNT_SIZE;
props_bytes = pc * (int)sizeof(textprop_T);
// Adjust text properties in the line above and below.
if (lnum > 1)
adjust_text_props_for_delete(buf, lnum - 1, textprop_save,
(int)textprop_len, TRUE);
adjust_text_props_for_delete(buf, lnum - 1,
props_data, props_bytes, TRUE);
if (lnum <= buf->b_ml.ml_line_count)
adjust_text_props_for_delete(buf, lnum, textprop_save,
(int)textprop_len, FALSE);
adjust_text_props_for_delete(buf, lnum,
props_data, props_bytes, FALSE);
}
vim_free(textprop_save);
#endif
+20 -15
View File
@@ -2172,10 +2172,6 @@ do_join(
int remove_comments = (use_formatoptions == TRUE)
&& has_format_option(FO_REMOVE_COMS);
int prev_was_comment;
#ifdef FEAT_PROP_POPUP
int propcount = 0; // number of props over all joined lines
int props_remaining;
#endif
if (save_undo && u_save((linenr_T)(curwin->w_cursor.lnum - 1),
(linenr_T)(curwin->w_cursor.lnum + count)) == FAIL)
@@ -2205,10 +2201,6 @@ do_join(
for (t = 0; t < count; ++t)
{
curr = curr_start = ml_get((linenr_T)(curwin->w_cursor.lnum + t));
#ifdef FEAT_PROP_POPUP
propcount += count_props((linenr_T) (curwin->w_cursor.lnum + t),
t > 0, t + 1 == count);
#endif
if (t == 0 && setmark && (cmdmod.cmod_flags & CMOD_LOCKMARKS) == 0)
{
// Set the '[ mark.
@@ -2295,9 +2287,6 @@ do_join(
// allocate the space for the new line
newp_len = sumsize + 1;
#ifdef FEAT_PROP_POPUP
newp_len += propcount * sizeof(textprop_T);
#endif
newp = alloc(newp_len);
if (newp == NULL)
{
@@ -2316,8 +2305,15 @@ do_join(
* should not really be a problem.
*/
#ifdef FEAT_PROP_POPUP
props_remaining = propcount;
unpacked_memline_T um = um_open_at_no_props(
curwin->w_buffer, curwin->w_cursor.lnum, 0);
// um_open_at_no_props may have invalidated "curr".
int curr_off = (int)(curr - curr_start);
curr = curr_start = ml_get((linenr_T)(curwin->w_cursor.lnum + count - 1));
curr += curr_off;
#endif
for (t = count - 1; ; --t)
{
int spaces_removed;
@@ -2338,9 +2334,8 @@ do_join(
mark_col_adjust(curwin->w_cursor.lnum + t, (colnr_T)0, -t,
(long)(cend - newp - spaces_removed), spaces_removed);
#ifdef FEAT_PROP_POPUP
prepend_joined_props(newp + sumsize + 1, propcount, &props_remaining,
curwin->w_cursor.lnum + t, t == count - 1,
(long)(cend - newp), spaces_removed);
prepend_joined_props(&um, curwin->w_cursor.lnum + t,
t == count - 1, (long)(cend - newp), spaces_removed);
#endif
if (t == 0)
break;
@@ -2352,6 +2347,16 @@ do_join(
currsize = (int)STRLEN(curr);
}
#ifdef FEAT_PROP_POPUP
if (um.buf != NULL)
{
um_set_text(&um, newp);
um_reverse_props(&um);
um_close(&um);
newp = NULL; // um_set_text took ownership
}
else
#endif
ml_replace_len(curwin->w_cursor.lnum, newp, (colnr_T)newp_len, TRUE, FALSE);
if (setmark && (cmdmod.cmod_flags & CMOD_LOCKMARKS) == 0)
+3 -3
View File
@@ -1318,9 +1318,9 @@ popup_adjust_position(win_T *wp)
int screen_ecol;
// Popup window is positioned relative to a text property.
if (find_visible_prop(prop_win,
if (!find_visible_prop(prop_win,
wp->w_popup_prop_type, wp->w_popup_prop_id,
&prop, &prop_lnum) == FAIL)
&prop, &prop_lnum))
{
// Text property is no longer visible, hide the popup.
// Unhiding the popup is done in check_popup_unhidden().
@@ -4307,7 +4307,7 @@ check_popup_unhidden(win_T *wp)
if ((wp->w_popup_flags & POPF_HIDDEN_FORCE) == 0
&& find_visible_prop(wp->w_popup_prop_win,
wp->w_popup_prop_type, wp->w_popup_prop_id,
&prop, &lnum) == OK)
&prop, &lnum))
{
wp->w_popup_flags &= ~POPF_HIDDEN;
wp->w_popup_prop_topline = 0; // force repositioning
+13 -3
View File
@@ -1,4 +1,13 @@
/* textprop.c */
unpacked_memline_T um_open(buf_T *buf);
bool um_goto_line(unpacked_memline_T *um, linenr_T lnum, int extra_props);
unpacked_memline_T um_open_at(buf_T *buf, linenr_T lnum, int extra_props);
bool um_set_text(unpacked_memline_T *um, char_u *text);
void um_reverse_props(unpacked_memline_T *um);
unpacked_memline_T um_open_at_no_props(buf_T *buf, linenr_T lnum, int prop_count);
void um_delete_prop(unpacked_memline_T *um, int index);
void um_close(unpacked_memline_T *um);
void um_abort(unpacked_memline_T *um);
int find_prop_type_id(char_u *name, buf_T *buf);
void f_prop_add(typval_T *argvars, typval_T *rettv);
void f_prop_add_list(typval_T *argvars, typval_T *rettv);
@@ -7,10 +16,11 @@ int get_text_props(buf_T *buf, linenr_T lnum, char_u **props, int will_change);
int prop_count_above_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);
bool find_visible_prop(win_T *wp, int type_id, int id, textprop_T *prop, linenr_T *found_lnum);
char_u *props_add_count_header(char_u *line, int line_len, int textlen, int *new_len);
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);
int text_prop_type_valid(buf_T *buf, textprop_T *prop);
bool text_prop_type_valid(buf_T *buf, textprop_T *prop);
void f_prop_clear(typval_T *argvars, typval_T *rettv);
void f_prop_find(typval_T *argvars, typval_T *rettv);
void f_prop_list(typval_T *argvars, typval_T *rettv);
@@ -24,5 +34,5 @@ void clear_global_prop_types(void);
void clear_buf_prop_types(buf_T *buf);
int adjust_prop_columns(linenr_T lnum, colnr_T col, int bytes_added, int flags);
void adjust_props_for_split(linenr_T lnum_props, linenr_T lnum_top, int kept, int deleted, int at_eol);
void prepend_joined_props(char_u *new_props, int propcount, int *props_remaining, linenr_T lnum, int last_line, long col, int removed);
void prepend_joined_props(unpacked_memline_T *um, linenr_T lnum, int last_line, long col, int removed);
/* vim: set ft=c : */
+43 -3
View File
@@ -893,13 +893,22 @@ typedef struct memline
typedef struct textprop_S
{
colnr_T tp_col; // start column (one based, in bytes)
colnr_T tp_len; // length in bytes, when tp_id is negative used
// for left padding plus one
colnr_T tp_len; // length in bytes; for virtual text props
// this is STRLEN(vtext) (not including NUL)
int tp_id; // identifier
int tp_type; // property type
int tp_flags; // TP_FLAG_ values
int tp_padleft; // left padding between text line and virtual
// text
union // For virtual text props (tp_id < 0):
{ // check TP_FLAG_VTEXT_PTR in tp_flags to
// determine which member is active.
colnr_T tp_text_offset; // offset to vtext string from the
// prop_count position in the memline
// (when TP_FLAG_VTEXT_PTR is NOT set)
char_u *tp_text; // pointer to virtual text string
// (when TP_FLAG_VTEXT_PTR IS set)
} u;
} textprop_T;
#define TP_FLAG_CONT_NEXT 0x1 // property continues in next line
@@ -913,10 +922,42 @@ typedef struct textprop_S
#define TP_FLAG_WRAP 0x080 // virtual text wraps - when missing
// text is truncated
#define TP_FLAG_START_INCL 0x100 // "start_incl" copied from proptype
#define TP_FLAG_DELETED 0x200 // marked for deletion in
// unpacked_memline_T
#define TP_FLAG_VTEXT_PTR 0x400 // u.tp_text access is valid
#define PROP_COUNT_SIZE sizeof(uint16_t) // size of prop_count in memline
#define PROP_TEXT_MIN_CELLS 4 // minimum number of cells to use for
// the text, even when truncating
/*
* An unpacked form of a single memline with text properties.
*
* When packed in a memline, the format is:
* [line text] [NUL] [textprop_T...] [vtext strings...]
* Virtual text props use u.tp_text_offset (relative to the start of props
* area). When unpacked, u.tp_text is a pointer: either into the memline
* data (LOADED) or to separately allocated memory (DETACHED).
*
* States:
* - LOADED: text and u.tp_text point into the memline. Read-only.
* - DETACHED: text and u.tp_text are separately allocated. Writable.
* - CLOSED: buf == NULL. Unusable (error recovery).
*/
typedef struct unpacked_memline_S
{
buf_T *buf; // the line's buffer (NULL = closed)
linenr_T lnum; // line number (0 = no line loaded)
bool detached; // true when text/vtext are allocated
colnr_T text_size; // size of text including NUL
char_u *text; // NUL-terminated text
int prop_size; // number of allocated prop slots
int prop_count; // number of properties
textprop_T *props; // property array
bool text_changed; // true if text was modified
} unpacked_memline_T;
/*
* Structure defining a property type.
*/
@@ -3550,7 +3591,6 @@ struct file_buffer
int b_has_textprop; // TRUE when text props were added
hashtab_T *b_proptypes; // text property types local to buffer
proptype_T **b_proparray; // entries of b_proptypes sorted on tp_id
garray_T b_textprop_text; // stores text for props, index by (-id - 1)
#endif
#if defined(FEAT_BEVAL) && defined(FEAT_EVAL)
+2
View File
@@ -337,6 +337,7 @@ NEW_TESTS = \
test_textformat \
test_textobjects \
test_textprop \
test_textprop2 \
test_timers \
test_true_false \
test_trycatch \
@@ -601,6 +602,7 @@ NEW_TESTS_RES = \
test_textformat.res \
test_textobjects.res \
test_textprop.res \
test_textprop2.res \
test_timers.res \
test_true_false.res \
test_trycatch.res \
+1 -1
View File
@@ -9,4 +9,4 @@
|~+0#4040ff13&| @58
|~| @58
|~| @58
|:+0#0000000&|s|e|t| |s|i|g|n|c|o|l|u|m|n|=|y|e|s| |f|o|l|d|c|o|l|u|m|n|=|3| |c|u|r|s|o|r|l|i|n|3|,|5| @10|A|l@1|
| +0#0000000&@41|3|,|5| @10|A|l@1|
+1 -1
View File
@@ -9,4 +9,4 @@
|~+0#4040ff13#ffffff0| @58
|~| @58
|~| @58
|:+0#0000000&|s|e|t| |s|i|g|n|c|o|l|u|m|n|=|y|e|s| |f|o|l|d|c|o|l|u|m|n|=|3| @9|4|,|4| @10|A|l@1|
| +0#0000000&@41|4|,|4| @10|A|l@1|
+7 -7
View File
@@ -3565,7 +3565,7 @@ func Test_props_with_text_after_nowrap()
let buf = RunVimInTerminal('-S XscriptPropsAfterNowrap', #{rows: 12, cols: 60})
call VerifyScreenDump(buf, 'Test_prop_with_text_after_nowrap_1', {})
call term_sendkeys(buf, ":set signcolumn=yes foldcolumn=3 cursorline\<CR>")
call term_sendkeys(buf, ":set signcolumn=yes foldcolumn=3 cursorline\<CR>\<C-L>")
call VerifyScreenDump(buf, 'Test_prop_with_text_after_nowrap_2', {})
call term_sendkeys(buf, "j")
@@ -3975,15 +3975,15 @@ func Test_removed_prop_with_text_cleans_up_array()
call setline(1, 'some text here')
call prop_type_add('some', #{highlight: 'ErrorMsg'})
let id1 = prop_add(1, 5, #{type: 'some', text: "SOME"})
call assert_equal(-1, id1)
call assert_true(id1 < 0)
let id2 = prop_add(1, 10, #{type: 'some', text: "HERE"})
call assert_equal(-2, id2)
call assert_true(id2 < id1)
" removing the props resets the index
" IDs are not recycled after removal; new IDs keep decreasing.
call prop_remove(#{id: id1})
call prop_remove(#{id: id2})
let id1 = prop_add(1, 5, #{type: 'some', text: "SOME"})
call assert_equal(-1, id1)
let id3 = prop_add(1, 5, #{type: 'some', text: "SOME"})
call assert_true(id3 < id2)
call prop_type_delete('some')
bwipe!
@@ -4672,7 +4672,7 @@ func Test_error_when_using_negative_id()
" Negative id is always rejected. Before the fix, prop_add() with a negative
" id succeeded when no virtual text existed, then prop_list() would dereference
" a NULL pointer (b_textprop_text.ga_data) and crash.
" a NULL pointer and crash.
call assert_fails("call prop_add(1, 1, #{type: 'test1', length: 1, id: -1})", 'E1293:')
call assert_equal([], prop_list(1))
+431
View File
@@ -0,0 +1,431 @@
" Additional tests for defining text property types and adding text properties
" to the buffer.
CheckFeature textprop
source util/screendump.vim
" Find a property of a given type on a given line.
func s:PropForType(lnum, type_name)
for p in prop_list(a:lnum)
if p['type'] == a:type_name
return p
endif
endfor
return {}
endfunc
" Clean up property types and wipe buffer.
func s:CleanupPropTypes(types)
for name in a:types
call prop_type_delete(name)
endfor
bwipe!
endfunc
" Set up buffer content and properties used by multiple tests.
"
" Properties:
" type '1': line 2 col 2 -> line 4 col 9 (multiline highlight)
" type '2': line 2 col 3 -> line 2 col 7 (single line highlight)
" type '2': line 3 col 3 -> line 3 col 8 (single line highlight)
" type '2': line 4 col 3 -> line 4 col 9 (single line highlight)
" type '3': line 2 col 5 -> line 4 col 9 (multiline highlight)
func s:Setup_multiline_props_1()
new
call setline(1, ['Line1', 'Line.2', 'Line..3', 'Line...4'])
silent! call prop_type_delete('1')
silent! call prop_type_delete('2')
silent! call prop_type_delete('3')
call prop_type_add('1', {'highlight': 'DiffAdd'})
call prop_type_add('2', {'highlight': 'DiffChange'})
call prop_type_add('3', {'highlight': 'DiffDelete'})
call prop_add(2, 2, {'type': '1', 'id': 42, 'end_lnum': 4, 'end_col': 9})
call prop_add(2, 3, {'type': '2', 'id': 42, 'end_lnum': 2, 'end_col': 7})
call prop_add(3, 3, {'type': '2', 'id': 42, 'end_lnum': 3, 'end_col': 8})
call prop_add(4, 3, {'type': '2', 'id': 42, 'end_lnum': 4, 'end_col': 9})
call prop_add(2, 5, {'type': '3', 'id': 42, 'end_lnum': 4, 'end_col': 9})
" Sanity check.
call assert_equal(4, line('$'))
call assert_equal(0, len(prop_list(1)))
call assert_equal(3, len(prop_list(2)))
call assert_equal(3, len(prop_list(3)))
call assert_equal(3, len(prop_list(4)))
endfunc
" Set up buffer with a multiline property spanning line 1 col 4 -> line 3 col 4.
func s:Setup_start_end_prop()
new
call setline(1, ['Line.1', 'Line..2', 'Line...3', 'Line....4'])
silent! call prop_type_delete('1')
call prop_type_add('1', {'highlight': 'DiffAdd'})
call prop_add(1, 4, {'type': '1', 'id': 42, 'end_lnum': 3, 'end_col': 4})
endfunc
" The substitute command should adjust marks when one or more whole lines are
" deleted.
func Test_subst_adjusts_marks()
" Buffer: 4 lines with a single multiline property spanning all lines.
" type '1': line 1 col 1 -> line 4 col 10
func DoEditAndCheck(edit, expected_marks, expected_nlines) closure
new
call setline(1, ['Line.1', 'Line..2', 'Line...3', 'Line....4'])
silent! call prop_type_delete('1')
call prop_type_add('1', {'highlight': 'DiffAdd'})
call prop_add(1, 1, {'type': '1', 'id': 42, 'end_lnum': 4, 'end_col': 10})
call setpos("'a", [0, 1, 1])
call setpos("'b", [0, 2, 1])
call setpos("'c", [0, 3, 1])
call setpos("'d", [0, 4, 1])
set undolevels&
let msg = printf('Edit command = "%s"', a:edit)
execute a:edit
call assert_equal(a:expected_nlines, line('$'), msg)
call assert_equal(a:expected_marks[0], getpos("'a"), msg .. ', mark a')
call assert_equal(a:expected_marks[1], getpos("'b"), msg .. ', mark b')
call assert_equal(a:expected_marks[2], getpos("'c"), msg .. ', mark c')
call assert_equal(a:expected_marks[3], getpos("'d"), msg .. ', mark d')
" Undo and verify original state is restored.
:undo
call assert_equal(4, line('$'), msg .. ', post-undo')
call assert_equal('Line.1', getline(1), msg .. ', post-undo line 1')
call assert_equal([0, 1, 1, 0], getpos("'a"), msg .. ', post-undo mark a')
call assert_equal([0, 2, 1, 0], getpos("'b"), msg .. ', post-undo mark b')
call assert_equal([0, 3, 1, 0], getpos("'c"), msg .. ', post-undo mark c')
call assert_equal([0, 4, 1, 0], getpos("'d"), msg .. ', post-undo mark d')
call prop_type_delete('1')
bwipe!
endfunc
" Delete line 1.
let expected = [[0, 0, 0, 0], [0, 1, 1, 0], [0, 2, 1, 0], [0, 3, 1, 0]]
for edit in [':1 substitute/Line.1\n//', ':1 delete', 'normal 1GVx']
call DoEditAndCheck(edit, expected, 3)
endfor
return
" NOTE: The tests below are disabled in the original too (after 'return').
" Delete line 2.
let expected = [[0, 1, 1, 0], [0, 0, 0, 0], [0, 2, 1, 0], [0, 3, 1, 0]]
for edit in [':2 substitute/Line..2\n//', ':1 substitute/\nLine..2//',
\ '2: delete', 'normal 2GVx']
call DoEditAndCheck(edit, expected, 3)
endfor
" Delete line 4.
let expected = [[0, 1, 1, 0], [0, 2, 1, 0], [0, 3, 1, 0], [0, 0, 0, 0]]
for edit in [':3 substitute/\nLine....4//', '4: delete', 'normal 4GVx']
call DoEditAndCheck(edit, expected, 3)
endfor
" Delete lines 2-3.
let expected = [[0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 2, 1, 0]]
for edit in [':2,3 substitute/Line.*[23]\n//',
\ ':2,3 substitute/\%(Line[.]*[23]\n\)*',
\ '2,3: delete', 'normal 2GVjx']
call DoEditAndCheck(edit, expected, 2)
endfor
" Delete lines 1-3.
let expected = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 1, 1, 0]]
for edit in [':1,$ substitute/Line.*[123]\n//',
\ ':1,$ substitute/\%(Line[.]*[123]\n\)*',
\ '1,3: delete', 'normal 1GVjjx']
call DoEditAndCheck(edit, expected, 1)
endfor
" Delete all lines.
let expected = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
for edit in [':1,$ substitute/Line.*[1234]\n//',
\ ':1,$ substitute/\%(Line[.]*[1234]\n\)*//',
\ '1,4: delete', 'normal 1GVjjjx']
call DoEditAndCheck(edit, expected, 1)
endfor
" Delete lines 3-4.
let expected = [[0, 1, 1, 0], [0, 2, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
for edit in [':2,$ substitute/\n\%(Line.*[34]\n\?\)*//',
\ '3,4: delete', 'normal 3GVjx']
call DoEditAndCheck(edit, expected, 2)
endfor
" Delete lines 2-4.
let expected = [[0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
for edit in [':1,$ substitute/\n\%(Line.*[234]\n\?\)*//',
\ '2,4: delete', 'normal 2GVjjx']
call DoEditAndCheck(edit, expected, 1)
endfor
endfunc
" The substitute command should correctly drop floating, virtual
" properties when lines are deleted.
func Test_multiline_substitute_del_lines_drops_virt_text_props()
" Helper to set up the buffer with virtual text properties.
" When a:virt_k_col is 1, 'virt-k' is at line 1 col 1 (floating);
" when 4, it is at line 1 col 4 (inline).
func SetupVirtProps(virt_k_col)
new
call setline(1, ['Line.1', 'Line..2', 'Line...3', 'Line....4'])
for s:t in ['1', '2', '3', '4', '7', '8']
silent! call prop_type_delete(s:t)
endfor
call prop_type_add('1', {'highlight': 'DiffAdd'})
call prop_type_add('2', {'highlight': 'DiffChange', 'end_incl': 1})
call prop_type_add('3', {'highlight': 'DiffDelete'})
call prop_type_add('4', {'highlight': 'DiffText'})
call prop_type_add('7', {'highlight': 'WarningMsg'})
call prop_type_add('8', {'highlight': 'Directory'})
" Floating virtual text.
call prop_add(1, 0, {'type': '1', 'text': 'virt-a', 'text_align': 'right'})
call prop_add(1, 0, {'type': '2', 'text': 'virt-b', 'text_align': 'right'})
call prop_add(2, 0, {'type': '3', 'text': 'virt-c', 'text_align': 'right'})
call prop_add(2, 0, {'type': '4', 'text': 'virt-d', 'text_align': 'right'})
call prop_add(3, 0, {'type': '4', 'text': 'virt-e', 'text_align': 'right'})
call prop_add(4, 0, {'type': '3', 'text': 'virt-g', 'text_align': 'right'})
call prop_add(4, 0, {'type': '7', 'text': 'virt-h', 'text_align': 'right'})
" Inline virtual text.
call prop_add(1, a:virt_k_col, {'type': '8', 'text': 'virt-k'})
" Highlight property spanning lines 1-4.
call prop_add(1, 1, {'type': '2', 'id': 42, 'end_lnum': 4, 'end_col': 4})
call prop_add(4, 4, {'type': '3', 'id': 42, 'end_lnum': 4, 'end_col': 7})
endfunc
" Join lines 1-2.
call SetupVirtProps(1)
1,2 substitute /e.1\nL/e.1 L/
call assert_equal(3, line('$'))
call assert_equal('Line.1 Line..2', getline(1))
call assert_equal(4, len(prop_list(1)))
call s:CleanupPropTypes(['1', '2', '3', '4', '7', '8'])
" Join lines 1-3.
call SetupVirtProps(1)
1,3 substitute /e.1\nLine..2\nL/e.1 L/
call assert_equal(2, line('$'))
call assert_equal('Line.1 Line...3', getline(1))
" NOTE: Original PR expected value is 3
call assert_equal(4, len(prop_list(1)))
call s:CleanupPropTypes(['1', '2', '3', '4', '7', '8'])
" Join lines 1-4.
call SetupVirtProps(1)
1,4 substitute /e.1\nLine..2\nLine...3\nL/e.1 L/
call assert_equal(1, line('$'))
call assert_equal('Line.1 Line....4', getline(1))
call assert_equal(5, len(prop_list(1)))
call s:CleanupPropTypes(['1', '2', '3', '4', '7', '8'])
" Second variant: inline virtual text at col 4.
call SetupVirtProps(4)
1,2 substitute /e.1\nL/e.1 L/
call assert_equal(3, line('$'))
call assert_equal(4, len(prop_list(1)))
call s:CleanupPropTypes(['1', '2', '3', '4', '7', '8'])
endfunc
" Deletion of text starting a multiline property should adjust next line.
func Test_text_deletion_of_start_to_eol_adjusts_multiline_property()
" Partial delete: property is shortened but not removed.
call s:Setup_start_end_prop()
normal 1G03l2x
call assert_equal('Lin1', getline(1))
call assert_equal(1, len(prop_list(1)))
call assert_equal(2, prop_list(1)[0]['length'])
call prop_type_delete('1')
bwipe!
" Full delete of start: property should be removed from line 1.
for edit in ['normal 1G03ld$', 'normal 1G03l3x',
\ 'normal 1G03lv x', '1 substitute /e.1//']
call s:Setup_start_end_prop()
execute edit
let msg = printf('op="%s"', edit)
call assert_equal([], prop_list(1), msg)
call prop_type_delete('1')
bwipe!
endfor
endfunc
" Deletion of text ending a multiline property should adjust previous line.
func Test_text_deletion_of_end_to_sol_adjusts_multiline_property()
" Partial delete: property end is adjusted but not removed.
call s:Setup_start_end_prop()
normal 3G02x
call assert_equal('ne...3', getline(3))
call assert_equal(1, len(prop_list(3)))
call assert_equal(0, prop_list(3)[0]['start'])
call prop_type_delete('1')
bwipe!
" Full delete of ending portion: property should be removed from line 3.
for edit in ['normal 3G03x', 'normal 3G0v x', '3 substitute /Lin//']
call s:Setup_start_end_prop()
execute edit
let msg = printf('op="%s"', edit)
call assert_equal([], prop_list(3), msg)
call prop_type_delete('1')
bwipe!
endfor
endfunc
" Inline text properties should be removed when surrounding text is removed.
func Test_text_deletion_removes_inline_virtual_text()
func SetupVirtText(start_incl, end_incl)
new
call setline(1, ['The line with properties....'])
let opts = {'highlight': 'DiffChange'}
if a:start_incl
let opts['start_incl'] = 1
endif
if a:end_incl
let opts['end_incl'] = 1
endif
silent! call prop_type_delete('2')
call prop_type_add('2', opts)
call prop_add(1, 7, {'type': '2', 'text': 'xxx'})
endfunc
" Test all combinations of start_incl/end_incl.
for [si, ei] in [[0, 0], [1, 0], [0, 1], [1, 1]]
" Deletion of one char before virtual text: property stays.
for edit in ['normal 1G05lx', '1 substitute /i//', 'normal 1G05lvx']
call SetupVirtText(si, ei)
execute edit
let msg = printf('si=%d ei=%d op="%s"', si, ei, edit)
call assert_equal(1, len(prop_list(1)), msg)
call assert_equal(6, prop_list(1)[0]['col'], msg)
call prop_type_delete('2')
bwipe!
endfor
" Deletion of one char after virtual text: property stays.
for edit in ['normal 1G06lx', '1 substitute /n//', 'normal 1G06lvx']
call SetupVirtText(si, ei)
execute edit
let msg = printf('si=%d ei=%d op="%s"', si, ei, edit)
call assert_equal(1, len(prop_list(1)), msg)
call assert_equal(7, prop_list(1)[0]['col'], msg)
call prop_type_delete('2')
bwipe!
endfor
" Deletion of both chars around virtual text: property is removed.
for edit in ['normal 1G05l2x', '1 substitute /in//', 'normal 1G05lv x']
call SetupVirtText(si, ei)
execute edit
let msg = printf('si=%d ei=%d op="%s"', si, ei, edit)
call assert_equal([], prop_list(1), msg)
call prop_type_delete('2')
bwipe!
endfor
endfor
endfunc
" Removing a multiline property from the last line should fix the property
" on the penultimate line.
func Test_multiline_prop_partial_remove_last_using_remove()
call s:Setup_multiline_props_1()
call prop_remove({'type': '3'}, 4)
call assert_equal(1, s:PropForType(3, '3')['end'])
call s:CleanupPropTypes(['1', '2', '3'])
endfunc
" Removing a multiline property from the penultimate line should fix the
" properties on the previous and last lines.
func Test_multiline_prop_partial_remove_penultimate_using_remove()
call s:Setup_multiline_props_1()
call prop_remove({'type': '3'}, 3)
call assert_equal(1, s:PropForType(2, '3')['end'])
call assert_equal(1, s:PropForType(4, '3')['start'])
call s:CleanupPropTypes(['1', '2', '3'])
endfunc
" Removing all properties from the first line should fix the properties
" on the second line.
func Test_multiline_prop_partial_remove_first_using_clear()
call s:Setup_multiline_props_1()
call prop_clear(2)
call assert_equal(1, s:PropForType(3, '3')['start'])
call assert_equal(1, s:PropForType(3, '1')['start'])
call s:CleanupPropTypes(['1', '2', '3'])
endfunc
" Removing all multiline properties from the last line should fix the
" properties on the penultimate line.
func Test_multiline_prop_partial_remove_last_using_clear()
call s:Setup_multiline_props_1()
call prop_clear(4)
call assert_equal(1, s:PropForType(3, '3')['end'])
call assert_equal(1, s:PropForType(3, '1')['end'])
call s:CleanupPropTypes(['1', '2', '3'])
endfunc
" Removing all multiline properties from the penultimate line should fix the
" properties on the previous and last lines.
func Test_multiline_prop_partial_remove_penultimate_using_clear()
call s:Setup_multiline_props_1()
call prop_clear(3)
call assert_equal(1, s:PropForType(2, '3')['end'])
call assert_equal(1, s:PropForType(4, '3')['start'])
call assert_equal(1, s:PropForType(2, '1')['end'])
call assert_equal(1, s:PropForType(4, '1')['start'])
call s:CleanupPropTypes(['1', '2', '3'])
endfunc
" Deleting the first line with multiline properties should fix the properties
" on the second line.
func Test_multiline_prop_delete_first_line()
call s:Setup_multiline_props_1()
:2 delete
call assert_equal(3, line('$'))
call assert_equal(1, s:PropForType(2, '1')['start'])
call assert_equal(1, s:PropForType(2, '3')['start'])
call s:CleanupPropTypes(['1', '2', '3'])
endfunc
" Deleting the last line with multiline properties should fix the properties
" on the penultimate line.
func Test_multiline_prop_delete_last_line()
call s:Setup_multiline_props_1()
:4 delete
call assert_equal(3, line('$'))
call assert_equal(1, s:PropForType(3, '1')['end'])
call assert_equal(1, s:PropForType(3, '3')['end'])
call s:CleanupPropTypes(['1', '2', '3'])
endfunc
" Deleting the penultimate line with multiline properties should keep
" the properties spanning lines.
func Test_multiline_prop_delete_penultimate_line()
call s:Setup_multiline_props_1()
:3 delete
call assert_equal(3, line('$'))
call assert_equal(0, s:PropForType(2, '1')['end'])
call assert_equal(0, s:PropForType(2, '3')['end'])
call assert_equal(0, s:PropForType(3, '1')['start'])
call assert_equal(0, s:PropForType(3, '3')['start'])
call s:CleanupPropTypes(['1', '2', '3'])
endfunc
" vim: shiftwidth=2 sts=2 expandtab
+1042 -301
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -734,6 +734,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
320,
/**/
319,
/**/