Skip to content

Commit ff41e9d

Browse files
h-eastpaul-ollisclaude
authored andcommitted
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>
1 parent c79edc0 commit ff41e9d

20 files changed

Lines changed: 1692 additions & 379 deletions

runtime/doc/textprop.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*textprop.txt* For Vim version 9.2. Last change: 2026 Apr 06
1+
*textprop.txt* For Vim version 9.2. Last change: 2026 Apr 07
22

33

44
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -511,7 +511,9 @@ will move accordingly.
511511

512512
When text is deleted and a text property no longer includes any text, it is
513513
deleted. However, a text property that was defined as zero-width will remain,
514-
unless the whole line is deleted.
514+
unless the whole line is deleted. When lines are joined by a multi-line
515+
substitute command, virtual text properties on the deleted lines are moved to
516+
the resulting joined line.
515517
*E275*
516518
When a buffer is unloaded, all the text properties are gone. There is no way
517519
to store the properties in a file. You can only re-create them. When a

runtime/doc/version9.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52625,6 +52625,8 @@ Changed~
5262552625
- |json_decode()| is stricter: keywords must be lowercase, lone surrogates are
5262652626
now invalid
5262752627
- |js_decode()| rejects lone surrogates
52628+
- virtual text properties on lines deleted by a multi-line substitute
52629+
are moved to the resulting joined line instead of being dropped.
5262852630

5262952631
*added-9.3*
5263052632
Added ~

src/buffer.c

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,9 +1115,6 @@ free_buffer_stuff(
11151115
#endif
11161116
#ifdef FEAT_NETBEANS_INTG
11171117
netbeans_file_killed(buf);
1118-
#endif
1119-
#ifdef FEAT_PROP_POPUP
1120-
ga_clear_strings(&buf->b_textprop_text);
11211118
#endif
11221119
map_clear_mode(buf, MAP_ALL_MODES, TRUE, FALSE); // clear local mappings
11231120
map_clear_mode(buf, MAP_ALL_MODES, TRUE, TRUE); // clear local abbrevs

src/change.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2570,7 +2570,7 @@ truncate_line(int fixpos)
25702570
* Saves the lines for undo first if "undo" is TRUE.
25712571
*/
25722572
void
2573-
del_lines(long nlines, int undo)
2573+
del_lines(long nlines, int undo)
25742574
{
25752575
long n;
25762576
linenr_T first = curwin->w_cursor.lnum;

src/charset.c

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,22 @@ init_chartabsize_arg(
11401140
cts->cts_text_props[text_prop_idxs[i]];
11411141
vim_free(text_prop_idxs);
11421142
}
1143+
1144+
// Convert tp_text_offset to tp_text pointer.
1145+
char_u *count_ptr = prop_start - PROP_COUNT_SIZE;
1146+
1147+
for (i = 0; i < count; ++i)
1148+
{
1149+
textprop_T *tp = &cts->cts_text_props[i];
1150+
1151+
if (tp->tp_id < 0 && tp->u.tp_text_offset > 0)
1152+
{
1153+
tp->u.tp_text = count_ptr + tp->u.tp_text_offset;
1154+
tp->tp_flags |= TP_FLAG_VTEXT_PTR;
1155+
}
1156+
else
1157+
tp->u.tp_text = NULL;
1158+
}
11431159
}
11441160
}
11451161
}
@@ -1294,7 +1310,7 @@ win_lbr_chartabsize(
12941310
int charlen = *s == NUL ? 1 : mb_ptr2len(s);
12951311
int i;
12961312
int col = (int)(s - line);
1297-
garray_T *gap = &wp->w_buffer->b_textprop_text;
1313+
12981314

12991315
// The "$" for 'list' mode will go between the EOL and
13001316
// the text prop, account for that.
@@ -1318,9 +1334,9 @@ win_lbr_chartabsize(
13181334
&& ((tp->tp_flags & TP_FLAG_ALIGN_ABOVE)
13191335
? col == 0
13201336
: s[0] == NUL && cts->cts_with_trailing)))
1321-
&& -tp->tp_id - 1 < gap->ga_len)
1337+
&& tp->u.tp_text != NULL)
13221338
{
1323-
char_u *p = ((char_u **)gap->ga_data)[-tp->tp_id - 1];
1339+
char_u *p = tp->u.tp_text;
13241340

13251341
if (p != NULL)
13261342
{

src/drawline.c

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -685,8 +685,7 @@ text_prop_position(
685685
int above = (tp->tp_flags & TP_FLAG_ALIGN_ABOVE);
686686
int below = (tp->tp_flags & TP_FLAG_ALIGN_BELOW);
687687
int wrap = tp->tp_col < MAXCOL || (tp->tp_flags & TP_FLAG_WRAP);
688-
int padding = tp->tp_col == MAXCOL && tp->tp_len > 1
689-
? tp->tp_len - 1 : 0;
688+
int padding = tp->tp_col == MAXCOL ? tp->tp_padleft : 0;
690689
int col_with_padding = scr_col + (below ? 0 : padding);
691690
int room = wp->w_width - col_with_padding;
692691
int before = room; // spaces before the text
@@ -1705,9 +1704,29 @@ win_line(
17051704
else
17061705
text_props = ALLOC_MULT(textprop_T, text_prop_count);
17071706
if (text_props != NULL)
1707+
{
17081708
mch_memmove(text_props, prop_start,
17091709
text_prop_count * sizeof(textprop_T));
17101710

1711+
// Convert tp_text_offset to tp_text pointer for virtual
1712+
// text properties. prop_start points into the memline
1713+
// after the prop_count field.
1714+
char_u *count_ptr = prop_start - PROP_COUNT_SIZE;
1715+
1716+
for (int i = 0; i < text_prop_count; ++i)
1717+
{
1718+
if (text_props[i].tp_id < 0
1719+
&& text_props[i].u.tp_text_offset > 0)
1720+
{
1721+
text_props[i].u.tp_text =
1722+
count_ptr + text_props[i].u.tp_text_offset;
1723+
text_props[i].tp_flags |= TP_FLAG_VTEXT_PTR;
1724+
}
1725+
else
1726+
text_props[i].u.tp_text = NULL;
1727+
}
1728+
}
1729+
17111730
// Allocate an array for the indexes.
17121731
if (text_prop_count <= WIN_LINE_TEXT_PROP_STACK_LEN)
17131732
text_prop_idxs = text_prop_idxs_buf;
@@ -2301,13 +2320,10 @@ win_line(
23012320
}
23022321
}
23032322
if (text_prop_id < 0 && used_tpi >= 0
2304-
&& -text_prop_id
2305-
<= wp->w_buffer->b_textprop_text.ga_len)
2323+
&& text_props[used_tpi].u.tp_text != NULL)
23062324
{
23072325
textprop_T *tp = &text_props[used_tpi];
2308-
char_u *p = ((char_u **)wp->w_buffer
2309-
->b_textprop_text.ga_data)[
2310-
-text_prop_id - 1];
2326+
char_u *p = tp->u.tp_text;
23112327
int above = (tp->tp_flags
23122328
& TP_FLAG_ALIGN_ABOVE);
23132329
int bail_out = FALSE;
@@ -2325,8 +2341,7 @@ win_line(
23252341
int wrap = tp->tp_col < MAXCOL
23262342
|| (tp->tp_flags & TP_FLAG_WRAP);
23272343
int padding = tp->tp_col == MAXCOL
2328-
&& tp->tp_len > 1
2329-
? tp->tp_len - 1 : 0;
2344+
? tp->tp_padleft : 0;
23302345

23312346
// Insert virtual text before the current
23322347
// character, or add after the end of the line.

src/errors.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3431,9 +3431,8 @@ EXTERN char e_internal_error_shortmess_too_long[]
34313431
#ifdef FEAT_EVAL
34323432
EXTERN char e_class_variable_str_not_found_in_class_str[]
34333433
INIT(= N_("E1337: Class variable \"%s\" not found in class \"%s\""));
3434-
// E1338 unused
34353434
#endif
3436-
// E1339 unused
3435+
// E1338 and E1339 unused
34373436
#ifdef FEAT_EVAL
34383437
EXTERN char e_argument_already_declared_in_class_str[]
34393438
INIT(= N_("E1340: Argument already declared in the class: %s"));

src/ex_cmds.c

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4895,15 +4895,27 @@ ex_substitute(exarg_T *eap)
48954895
text_prop_count);
48964896
if (text_props != NULL)
48974897
{
4898-
int pi;
4899-
49004898
mch_memmove(text_props, prop_start,
49014899
text_prop_count * sizeof(textprop_T));
4902-
// After joining the text prop columns will
4903-
// increase.
4904-
for (pi = 0; pi < text_prop_count; ++pi)
4905-
text_props[pi].tp_col +=
4906-
regmatch.startpos[0].col + sublen - 1;
4900+
// Filter out virtual text and continuation
4901+
// properties from deleted lines, convert
4902+
// offsets to pointers, and adjust columns.
4903+
int wi = 0;
4904+
for (int pi = 0; pi < text_prop_count; ++pi)
4905+
{
4906+
// Skip virtual text and continuation
4907+
// properties from the deleted line.
4908+
if (text_props[pi].tp_id < 0
4909+
|| (text_props[pi].tp_flags
4910+
& TP_FLAG_CONT_PREV))
4911+
continue;
4912+
text_props[wi] = text_props[pi];
4913+
text_props[wi].tp_col +=
4914+
regmatch.startpos[0].col + sublen - 1;
4915+
text_props[wi].u.tp_text = NULL;
4916+
++wi;
4917+
}
4918+
text_prop_count = wi;
49074919
}
49084920
}
49094921
}
@@ -5142,7 +5154,14 @@ ex_substitute(exarg_T *eap)
51425154
break;
51435155
for (i = 0; i < nmatch_tl; ++i)
51445156
ml_delete(lnum);
5145-
mark_adjust(lnum, lnum + nmatch_tl - 1,
5157+
if (copycol > 0)
5158+
mark_adjust(lnum, lnum + nmatch_tl - 1,
5159+
(long)MAXLNUM, -nmatch_tl);
5160+
else
5161+
// The entire last matched line was consumed,
5162+
// so the first line was effectively replaced
5163+
// by lines below.
5164+
mark_adjust(lnum - 1, lnum - 1,
51465165
(long)MAXLNUM, -nmatch_tl);
51475166
if (subflags.do_ask)
51485167
deleted_lines(lnum, nmatch_tl);

src/memline.c

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2930,13 +2930,19 @@ add_text_props_for_append(
29302930
{
29312931
if (round == 2)
29322932
{
2933+
uint16_t pc;
2934+
29332935
if (new_prop_count == 0)
29342936
return; // nothing to do
2935-
new_len = *len + new_prop_count * sizeof(textprop_T);
2937+
new_len = *len + (int)PROP_COUNT_SIZE
2938+
+ new_prop_count * (int)sizeof(textprop_T);
29362939
new_line = alloc(new_len);
29372940
if (new_line == NULL)
29382941
return;
29392942
mch_memmove(new_line, *line, *len);
2943+
// Write prop_count header.
2944+
pc = (uint16_t)new_prop_count;
2945+
mch_memmove(new_line + *len, &pc, PROP_COUNT_SIZE);
29402946
new_prop_count = 0;
29412947
}
29422948

@@ -2954,8 +2960,10 @@ add_text_props_for_append(
29542960
prop.tp_flags |= TP_FLAG_CONT_PREV;
29552961
prop.tp_col = 1;
29562962
prop.tp_len = *len; // not exactly the right length
2957-
mch_memmove(new_line + *len + new_prop_count
2958-
* sizeof(textprop_T), &prop, sizeof(textprop_T));
2963+
prop.u.tp_text_offset = 0;
2964+
mch_memmove(new_line + *len + (int)PROP_COUNT_SIZE
2965+
+ new_prop_count * sizeof(textprop_T),
2966+
&prop, sizeof(textprop_T));
29592967
}
29602968
++new_prop_count;
29612969
}
@@ -3772,34 +3780,48 @@ adjust_text_props_for_delete(
37723780
textlen = STRLEN(text) + 1;
37733781
if ((long)textlen >= line_size)
37743782
{
3783+
// No properties on this line.
37753784
if (above)
37763785
internal_error("no text property above deleted line");
37773786
else
37783787
internal_error("no text property below deleted line");
37793788
return;
37803789
}
3781-
this_props_len = line_size - (int)textlen;
3790+
if ((long)textlen + (long)PROP_COUNT_SIZE > line_size)
3791+
{
3792+
internal_error("text property data too short");
3793+
return;
3794+
}
3795+
3796+
uint16_t pc;
3797+
3798+
mch_memmove(&pc, text + textlen, PROP_COUNT_SIZE);
3799+
this_props_len = pc * (int)sizeof(textprop_T);
37823800
}
37833801

37843802
found = FALSE;
3785-
for (done_this = 0; done_this < this_props_len;
3786-
done_this += sizeof(textprop_T))
37873803
{
3788-
int flag = above ? TP_FLAG_CONT_NEXT
3804+
char_u *props_start = text + textlen + PROP_COUNT_SIZE;
3805+
3806+
for (done_this = 0; done_this < this_props_len;
3807+
done_this += sizeof(textprop_T))
3808+
{
3809+
int flag = above ? TP_FLAG_CONT_NEXT
37893810
: TP_FLAG_CONT_PREV;
3790-
textprop_T prop_this;
3811+
textprop_T prop_this;
37913812

3792-
mch_memmove(&prop_this, text + textlen + done_this,
3813+
mch_memmove(&prop_this, props_start + done_this,
37933814
sizeof(textprop_T));
3794-
if ((prop_this.tp_flags & flag)
3795-
&& prop_del.tp_id == prop_this.tp_id
3796-
&& prop_del.tp_type == prop_this.tp_type)
3797-
{
3798-
found = TRUE;
3799-
prop_this.tp_flags &= ~flag;
3800-
mch_memmove(text + textlen + done_this, &prop_this,
3815+
if ((prop_this.tp_flags & flag)
3816+
&& prop_del.tp_id == prop_this.tp_id
3817+
&& prop_del.tp_type == prop_this.tp_type)
3818+
{
3819+
found = TRUE;
3820+
prop_this.tp_flags &= ~flag;
3821+
mch_memmove(props_start + done_this, &prop_this,
38013822
sizeof(textprop_T));
3802-
break;
3823+
break;
3824+
}
38033825
}
38043826
}
38053827
if (!found)
@@ -4003,13 +4025,23 @@ ml_delete_int(buf_T *buf, linenr_T lnum, int flags)
40034025
#ifdef FEAT_PROP_POPUP
40044026
if (textprop_save != NULL)
40054027
{
4028+
// textprop_save is [prop_count][textprop_T...][vtext...].
4029+
// Skip prop_count header and pass only the textprop_T part.
4030+
uint16_t pc;
4031+
char_u *props_data;
4032+
int props_bytes;
4033+
4034+
mch_memmove(&pc, textprop_save, PROP_COUNT_SIZE);
4035+
props_data = textprop_save + PROP_COUNT_SIZE;
4036+
props_bytes = pc * (int)sizeof(textprop_T);
4037+
40064038
// Adjust text properties in the line above and below.
40074039
if (lnum > 1)
4008-
adjust_text_props_for_delete(buf, lnum - 1, textprop_save,
4009-
(int)textprop_len, TRUE);
4040+
adjust_text_props_for_delete(buf, lnum - 1,
4041+
props_data, props_bytes, TRUE);
40104042
if (lnum <= buf->b_ml.ml_line_count)
4011-
adjust_text_props_for_delete(buf, lnum, textprop_save,
4012-
(int)textprop_len, FALSE);
4043+
adjust_text_props_for_delete(buf, lnum,
4044+
props_data, props_bytes, FALSE);
40134045
}
40144046
vim_free(textprop_save);
40154047
#endif

0 commit comments

Comments
 (0)