]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0320: several bugs with text properties v9.2.0320
authorHirohito Higashi <h.east.727@gmail.com>
Tue, 7 Apr 2026 20:46:10 +0000 (20:46 +0000)
committerChristian Brabandt <cb@256bit.org>
Tue, 7 Apr 2026 20:46:10 +0000 (20:46 +0000)
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>
20 files changed:
runtime/doc/textprop.txt
runtime/doc/version9.txt
src/buffer.c
src/change.c
src/charset.c
src/drawline.c
src/errors.h
src/ex_cmds.c
src/memline.c
src/ops.c
src/popupwin.c
src/proto/textprop.pro
src/structs.h
src/testdir/Make_all.mak
src/testdir/dumps/Test_prop_with_text_after_nowrap_2.dump
src/testdir/dumps/Test_prop_with_text_after_nowrap_3.dump
src/testdir/test_textprop.vim
src/testdir/test_textprop2.vim [new file with mode: 0644]
src/textprop.c
src/version.c

index f57a238e29f6666000da2aaf64f55b0b1a3d352b..9fd5ff6686a5446d43e650c1cc6a65b9c8d20fc2 100644 (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
index 31d467e0abc49fe647217abe425df74d1a5fbf6a..8adff066f5ef9f7ee3030698ded45797ef27e3c4 100644 (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 ~
index 0f119e76220dc6c760a6cef2f0ef0b73904450ac..6c99acd0a18cb914335bfd8f86cc5d392ce409ea 100644 (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
index ecb27d72d75f0fc9b9341471da8f3bd35486c38c..1fdee65445e28f1b967a6b6eb6de6d0ad6487508 100644 (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;
index 1fbbf98f76d47413ad419483dfd2ff54e54f6cdf..8ad768d35490d40bdd944ba12ef433fe3d64641e 100644 (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)
                {
index f3a3b1671077eaaac946dc9a34040389eec791b0..ee93e07837f4c53ac150efc14059703330489aad 100644 (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.
index 081402e274045f175a77418951f701fa5cea60c7..53e5b1dda62d782df5829bbdc1f8fdb9505592e1 100644 (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"));
index 4382ea5b70659f7060b2c1036a7e8afb4f1a4361..efb0c66dc01f5f81a09a10931d613ac13decce58 100644 (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);
index 886f08f973c7057332e5ced07bad94e4b7c20283..c15946a6eba80a049ec195fc774f5ef5dc8acd0f 100644 (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
+               char_u *props_start = text + textlen + PROP_COUNT_SIZE;
+
+               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;
+                   textprop_T  prop_this;
 
-               mch_memmove(&prop_this, text + textlen + done_this,
+                   mch_memmove(&prop_this, props_start + 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)
-               {
-                   found = TRUE;
-                   prop_this.tp_flags &= ~flag;
-                   mch_memmove(text + textlen + done_this, &prop_this,
+                   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;
+                       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
index 6ea50c912e3767c2c6554400a06b5c722d35214f..715897331e092b0ce2731c76896d750ee7623b3f 100644 (file)
--- a/src/ops.c
+++ b/src/ops.c
@@ -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)
index 4b69b61893a8fe33acbe82e49bcbe9b578ac8d20..d5fdbb8d26604ffd4a0931bb88e5d1c2b37a9dc0 100644 (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
index 4b9a7a44914c4485ecdb3d7a9b45690b94e3d398..d3ecf6d14c8e573d8dcc47ddfc6a0171637d37fe 100644 (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 : */
index 0e61aedef4bc47c04d553365edcd292558a84c42..5bb51dd5496942f0c081705dbf8edeb9c6d20f98 100644 (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)
index c0e4e68860ae217b1884059b80176868678c0347..f8c7f8bb462ae5ff1642462a49e2c09317642ee1 100644 (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 \
index b981380606c6eed8ddbd437a4ca0d865139a9cdc..1d5c534834d9c799308803773754f5a479efd4f6 100644 (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| 
index 5342f5ba1625f90d699362c8bc3a5bcfb6ebcb37..ca849123a5b96b6f2beedc5f8f0c276fa842bd92 100644 (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| 
index 09b6e87f4761fb04c49dedf7400c972cb3527875..f94acfc97cb82a9af6592c026006b45b23cfa0a6 100644 (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))
 
diff --git a/src/testdir/test_textprop2.vim b/src/testdir/test_textprop2.vim
new file mode 100644 (file)
index 0000000..193a808
--- /dev/null
@@ -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
index f0d418eee2f3f2c349a5457b6eca2540376d63c8..33165a8e43a4a6f363ff4d05577916ce74e1077e 100644 (file)
 
 #if defined(FEAT_PROP_POPUP)
 
+static void um_store_changes(unpacked_memline_T *um);
+
+/*
+ * Free virtual text strings in a detached unpacked memline's props.
+ */
+    static void
+um_free_vtext(textprop_T *props, int count)
+{
+    for (int i = 0; i < count; ++i)
+       if (props[i].tp_flags & TP_FLAG_VTEXT_PTR)
+           VIM_CLEAR(props[i].u.tp_text);
+}
+
+/*
+ * Free memory used by an unpacked memline.
+ */
+    static void
+um_free(unpacked_memline_T *um)
+{
+    if (um->detached)
+    {
+       VIM_CLEAR(um->text);
+       um_free_vtext(um->props, um->prop_count);
+    }
+    VIM_CLEAR(um->props);
+    um->prop_size = 0;
+    um->prop_count = 0;
+    um->lnum = 0;
+    um->detached = false;
+}
+
+/*
+ * Initialize an unpacked memline for the given buffer.
+ * No line is loaded yet; use um_goto_line() to load one.
+ */
+    unpacked_memline_T
+um_open(buf_T *buf)
+{
+    unpacked_memline_T um;
+
+    CLEAR_FIELD(um);
+    um.buf = buf;
+    return um;
+}
+
+/*
+ * Load line "lnum" into an unpacked memline.  If a previous line was
+ * loaded and modified, it is stored first.
+ * "extra_props" is the number of extra prop slots to pre-allocate.
+ * Returns true on success, false on error (um becomes closed).
+ */
+    bool
+um_goto_line(unpacked_memline_T *um, linenr_T lnum, int extra_props)
+{
+    char_u     *line;
+    size_t     textlen;
+    size_t     propdata_len;
+    int                proplen;
+
+    if (um->buf == NULL)
+       return false;
+
+    // Store changes to the current line if any.
+    if (um->lnum > 0 && um->detached)
+       um_store_changes(um);
+    um_free(um);
+
+    if (lnum == 0)
+       return true;    // just unload
+
+    line = ml_get_buf(um->buf, lnum, FALSE);
+    textlen = ml_get_buf_len(um->buf, lnum) + 1;
+    propdata_len = um->buf->b_ml.ml_line_len - textlen;
+
+    um->lnum = lnum;
+    um->text_size = (colnr_T)textlen;
+    um->text = line;
+
+    if (propdata_len == 0)
+       return true;
+
+    // New format: [prop_count (uint16)][textprop_T...][vtext...]
+    if (propdata_len < PROP_COUNT_SIZE + sizeof(textprop_T))
+    {
+       iemsg(e_text_property_info_corrupted);
+       um->buf = NULL;
+       return false;
+    }
+
+    uint16_t    prop_count;
+    char_u         *count_ptr = line + textlen;
+    char_u         *props_start;
+
+    mch_memmove(&prop_count, count_ptr, PROP_COUNT_SIZE);
+    proplen = (int)prop_count;
+    props_start = count_ptr + PROP_COUNT_SIZE;
+
+    um->props = ALLOC_MULT(textprop_T, proplen + extra_props);
+    if (um->props == NULL)
+    {
+       um->buf = NULL;
+       return false;
+    }
+    um->prop_size = proplen + extra_props;
+    um->prop_count = proplen;
+    mch_memmove(um->props, props_start, proplen * sizeof(textprop_T));
+
+    // Convert tp_text_offset to tp_text pointer for virtual text
+    // props.
+    for (int i = 0; i < proplen; ++i)
+    {
+       if (um->props[i].tp_id < 0 && um->props[i].u.tp_text_offset > 0)
+       {
+           um->props[i].u.tp_text = count_ptr + um->props[i].u.tp_text_offset;
+           um->props[i].tp_flags |= TP_FLAG_VTEXT_PTR;
+       }
+       else
+           um->props[i].u.tp_text = NULL;
+    }
+
+    return true;
+}
+
+/*
+ * Open an unpacked memline at a specific line.
+ * Check um.buf != NULL for success.
+ */
+    unpacked_memline_T
+um_open_at(buf_T *buf, linenr_T lnum, int extra_props)
+{
+    unpacked_memline_T um = um_open(buf);
+
+    if (!um_goto_line(&um, lnum, extra_props))
+       um.buf = NULL;
+    return um;
+}
+
+/*
+ * Make the unpacked memline writable by copying text and virtual text
+ * strings to allocated memory (LOADED -> DETACHED).
+ * Returns true on success.
+ */
+    static bool
+um_detach(unpacked_memline_T *um)
+{
+    char_u  *newtext;
+
+    if (um->detached || um->buf == NULL)
+       return um->detached;
+
+    newtext = vim_strnsave(um->text, um->text_size - 1);
+    if (newtext == NULL)
+    {
+       um->buf = NULL;
+       return false;
+    }
+    um->text = newtext;
+
+    for (int i = 0; i < um->prop_count; ++i)
+    {
+       if (um->props[i].tp_flags & TP_FLAG_VTEXT_PTR)
+       {
+           char_u *copy = vim_strsave(um->props[i].u.tp_text);
+
+           if (copy == NULL)
+           {
+               for (int j = 0; j < i; ++j)
+                   if (um->props[j].tp_id < 0)
+                       VIM_CLEAR(um->props[j].u.tp_text);
+               VIM_CLEAR(um->text);
+               um->buf = NULL;
+               return false;
+           }
+           um->props[i].u.tp_text = copy;
+       }
+    }
+
+    um->detached = true;
+    return true;
+}
+
+/*
+ * Set the text of an unpacked memline.  "text" must be allocated memory;
+ * ownership is transferred to the unpacked memline.
+ */
+    bool
+um_set_text(unpacked_memline_T *um, char_u *text)
+{
+    if (um->buf == NULL)
+       return false;
+    if (um->detached)
+       vim_free(um->text);
+    um->text = text;
+    um->text_size = (colnr_T)STRLEN(text) + 1;
+    um->text_changed = true;
+    um->detached = true;
+    return true;
+}
+
+/*
+ * Ensure there is space for at least "needed" more properties.
+ * Returns true on success.
+ */
+    static bool
+um_grow_props(unpacked_memline_T *um, int needed)
+{
+    if (um->prop_count + needed <= um->prop_size)
+       return true;
+
+    int            new_size = um->prop_count + needed;
+    textprop_T  *newprops = vim_realloc(um->props,
+                                           new_size * sizeof(textprop_T));
+    if (newprops == NULL)
+       return false;
+    um->props = newprops;
+    um->prop_size = new_size;
+
+    return true;
+}
+
+/*
+ * Add a property to the end of the props array without sorting.
+ * For virtual text props, u.tp_text ownership is transferred.
+ */
+    static void
+um_add_prop(unpacked_memline_T *um, textprop_T *prop)
+{
+    if (um->buf == NULL)
+       return;
+    if (!um->detached)
+       um_detach(um);
+    if (!um_grow_props(um, 1))
+    {
+       um->buf = NULL;
+       return;
+    }
+    um->props[um->prop_count++] = *prop;
+}
+
+/*
+ * Reverse the order of all properties.
+ */
+    void
+um_reverse_props(unpacked_memline_T *um)
+{
+    for (int i = 0, j = um->prop_count - 1; i < j; ++i, --j)
+    {
+       textprop_T tmp = um->props[i];
+       um->props[i] = um->props[j];
+       um->props[j] = tmp;
+    }
+}
+
+/*
+ * Open an unpacked memline at "lnum" with no properties copied,
+ * but with space for "prop_count" properties.  The umemline is
+ * immediately in the DETACHED state.
+ */
+    unpacked_memline_T
+um_open_at_no_props(buf_T *buf, linenr_T lnum, int prop_count)
+{
+    unpacked_memline_T um;
+
+    CLEAR_FIELD(um);
+    um.buf = buf;
+
+    if (lnum < 1 || lnum > buf->b_ml.ml_line_count)
+    {
+       um.buf = NULL;
+       return um;
+    }
+
+    char_u *line = ml_get_buf(buf, lnum, FALSE);
+    char_u *text_copy = vim_strsave(line);
+
+    if (text_copy == NULL)
+    {
+       um.buf = NULL;
+       return um;
+    }
+    um.lnum = lnum;
+    um.text = text_copy;
+    um.text_size = (colnr_T)STRLEN(text_copy) + 1;
+    um.detached = true;
+
+    if (prop_count > 0)
+    {
+       um.props = ALLOC_MULT(textprop_T, prop_count);
+       if (um.props == NULL)
+       {
+           vim_free(um.text);
+           um.buf = NULL;
+           return um;
+       }
+       um.prop_size = prop_count;
+    }
+
+    return um;
+}
+
+/*
+ * Clear a continuation flag on a neighboring line's text property.
+ * Find a property on "lnum" in "buf" that matches "tp_id" and "tp_type",
+ * and clear "flag_to_clear" (TP_FLAG_CONT_NEXT or TP_FLAG_CONT_PREV).
+ */
+    static void
+clear_cont_flag_on_neighbor(buf_T *buf, linenr_T lnum,
+                                       int tp_id, int tp_type, int flag)
+{
+    unpacked_memline_T neighbor;
+
+    if (lnum < 1 || lnum > buf->b_ml.ml_line_count)
+       return;
+
+    neighbor = um_open_at(buf, lnum, 0);
+    if (neighbor.buf == NULL)
+       return;
+
+    for (int i = 0; i < neighbor.prop_count; ++i)
+    {
+       textprop_T *p = &neighbor.props[i];
+
+       if (p->tp_id == tp_id && p->tp_type == tp_type && (p->tp_flags & flag))
+       {
+           if (!neighbor.detached && !um_detach(&neighbor))
+               break;
+           // Re-get pointer after detach.
+           p = &neighbor.props[i];
+           p->tp_flags &= ~flag;
+           break;
+       }
+    }
+    um_close(&neighbor);
+}
+
+/*
+ * Mark a property for deletion and free its virtual text if any.
+ * Automatically detaches if needed.
+ * Also adjusts continuation flags on neighboring lines.
+ */
+    void
+um_delete_prop(unpacked_memline_T *um, int index)
+{
+    textprop_T *prop;
+
+    if (um->buf == NULL || index < 0 || index >= um->prop_count)
+       return;
+    if (!um->detached && !um_detach(um))
+       return;
+
+    prop = &um->props[index];
+
+    // Adjust continuation flags on neighboring lines before deleting.
+    if (prop->tp_flags & TP_FLAG_CONT_PREV)
+       clear_cont_flag_on_neighbor(um->buf, um->lnum - 1,
+                              prop->tp_id, prop->tp_type, TP_FLAG_CONT_NEXT);
+    if (prop->tp_flags & TP_FLAG_CONT_NEXT)
+       clear_cont_flag_on_neighbor(um->buf, um->lnum + 1,
+                              prop->tp_id, prop->tp_type, TP_FLAG_CONT_PREV);
+
+    if (prop->tp_flags & TP_FLAG_VTEXT_PTR)
+       VIM_CLEAR(prop->u.tp_text);
+    prop->tp_flags |= TP_FLAG_DELETED;
+}
+
+/*
+ * Pack an unpacked memline into a newly allocated buffer.
+ * Packed format: [text][NUL][textprop_T...][vtext strings...]
+ * Returns the packed buffer, or NULL on failure.
+ * "packed_len" is set to the total length.
+ *
+ * NOTE: Currently packs in the OLD format (no inline vtext) because
+ * the read side has not been converted yet.
+ */
+    static char_u *
+um_pack(unpacked_memline_T *um, int *packed_len)
+{
+    uint16_t   live_count = 0;
+    int                vtext_size = 0;
+    int                total_len;
+    char_u     *buf;
+    char_u     *count_dest;
+    char_u     *prop_dest;
+    char_u     *vtext_dest;
+
+    for (int i = 0; i < um->prop_count; ++i)
+    {
+       textprop_T *prop = &um->props[i];
+
+       if (prop->tp_flags & TP_FLAG_DELETED)
+           continue;
+       ++live_count;
+       if (prop->tp_flags & TP_FLAG_VTEXT_PTR)
+           vtext_size += prop->tp_len + 1;
+    }
+
+    if (live_count == 0)
+    {
+       // No properties: just text, no prop_count header.
+       total_len = um->text_size;
+       buf = alloc(total_len);
+       if (buf == NULL)
+           return NULL;
+       *packed_len = total_len;
+       mch_memmove(buf, um->text, um->text_size);
+       return buf;
+    }
+
+    // Format: [text][NUL][prop_count][textprop_T...][vtext...]
+    total_len = um->text_size + (int)PROP_COUNT_SIZE
+               + live_count * (int)sizeof(textprop_T) + vtext_size;
+    buf = alloc(total_len);
+    if (buf == NULL)
+       return NULL;
+    *packed_len = total_len;
+
+    mch_memmove(buf, um->text, um->text_size);
+
+    count_dest = buf + um->text_size;
+    mch_memmove(count_dest, &live_count, PROP_COUNT_SIZE);
+
+    prop_dest = count_dest + PROP_COUNT_SIZE;
+    vtext_dest = prop_dest + live_count * sizeof(textprop_T);
+
+    for (int i = 0; i < um->prop_count; ++i)
+    {
+       textprop_T prop = um->props[i];
+
+       if (prop.tp_flags & TP_FLAG_DELETED)
+           continue;
+
+       if (prop.tp_id < 0 && (prop.tp_flags & TP_FLAG_VTEXT_PTR))
+       {
+           int copysize = prop.tp_len + 1;
+
+           mch_memmove(vtext_dest, prop.u.tp_text, copysize);
+           prop.u.tp_text_offset = (colnr_T)(vtext_dest - count_dest);
+           vtext_dest += copysize;
+       }
+       else
+           prop.u.tp_text_offset = 0;
+       prop.tp_flags &= ~(TP_FLAG_DELETED | TP_FLAG_VTEXT_PTR);
+
+       mch_memmove(prop_dest, &prop, sizeof(textprop_T));
+       prop_dest += sizeof(textprop_T);
+    }
+
+    return buf;
+}
+
+/*
+ * Store changes to the current line back into the buffer's memline.
+ */
+    static void
+um_store_changes(unpacked_memline_T *um)
+{
+    char_u  *packed;
+    int            packed_len;
+
+    if (!um->detached || um->buf == NULL || um->lnum == 0)
+       return;
+
+    packed = um_pack(um, &packed_len);
+    if (packed == NULL)
+    {
+       um->buf = NULL;
+       return;
+    }
+
+    // Flush any other dirty line before setting ours.  ml_get_buf()
+    // with will_change=TRUE will flush the current dirty line and set
+    // up for our line.
+    if (um->buf->b_ml.ml_line_lnum != um->lnum)
+       ml_get_buf(um->buf, um->lnum, TRUE);
+    else if (!(um->buf->b_ml.ml_flags & ML_LINE_DIRTY))
+       (void)ml_get_buf(um->buf, um->lnum, TRUE);
+
+    // Free the detached text and vtext before switching to packed.
+    vim_free(um->text);
+    um_free_vtext(um->props, um->prop_count);
+
+    if (um->buf->b_ml.ml_flags & (ML_LINE_DIRTY | ML_ALLOCATED))
+       vim_free(um->buf->b_ml.ml_line_ptr);
+    um->buf->b_ml.ml_line_ptr = packed;
+    um->buf->b_ml.ml_line_len = packed_len;
+    um->buf->b_ml.ml_flags |= ML_LINE_DIRTY;
+
+    um->detached = false;
+    um->text = packed;
+    um->text_size = (colnr_T)STRLEN(packed) + 1;
+}
+
+/*
+ * Close an unpacked memline, storing any changes.
+ */
+    void
+um_close(unpacked_memline_T *um)
+{
+    if (um->buf == NULL)
+       return;
+    if (um->detached && um->lnum > 0)
+       um_store_changes(um);
+    um_free(um);
+    um->buf = NULL;
+}
+
+/*
+ * Abort an unpacked memline, discarding any changes.
+ */
+    void
+um_abort(unpacked_memline_T *um)
+{
+    um_free(um);
+    um->buf = NULL;
+}
+
 /*
  * In a hashtable item "hi_key" points to "pt_name" in a proptype_T.
  * This avoids adding a pointer to the hashtable item.
@@ -160,7 +676,7 @@ f_prop_add(typval_T *argvars, typval_T *rettv)
  * Attach a text property 'type_name' to the text starting
  * at [start_lnum, start_col] and ending at [end_lnum, end_col] in
  * the buffer "buf" and assign identifier "id".
- * When "text" is not NULL add it to buf->b_textprop_text[-id - 1].
+ * When "text" is not NULL store it as virtual text in the memline.
  */
     static int
 prop_add_one(
@@ -210,24 +726,13 @@ prop_add_one(
 
     if (text != NULL)
     {
-       garray_T    *gap = &buf->b_textprop_text;
        char_u      *p;
 
-       // double check we got the right ID
-       if (-id - 1 != gap->ga_len)
-           iemsg("text prop ID mismatch");
-       if (gap->ga_growsize == 0)
-           ga_init2(gap, sizeof(char *), 50);
-       if (ga_grow(gap, 1) == FAIL)
-           goto theend;
-       ((char_u **)gap->ga_data)[gap->ga_len++] = text;
-
        // change any control character (Tab, Newline, etc.) to a Space to make
        // it simpler to compute the size
        for (p = text; *p != NUL; MB_PTR_ADV(p))
            if (*p < ' ')
                *p = ' ';
-       text = NULL;
     }
 
     for (lnum = start_lnum; lnum <= end_lnum; ++lnum)
@@ -238,7 +743,7 @@ prop_add_one(
 
        // Fetch the line to get the ml_line_len field updated.
        proplen = get_text_props(buf, lnum, &props, TRUE);
-       textlen = buf->b_ml.ml_line_len - proplen * sizeof(textprop_T);
+       textlen = ml_get_buf_len(buf, lnum) + 1;
 
        if (lnum == start_lnum)
            col = start_col;
@@ -262,43 +767,17 @@ prop_add_one(
 
        if (text_arg != NULL)
        {
-           length = 1;         // text is placed on one character
+           length = (long)STRLEN(text_arg);
            if (col == 0)
            {
                col = MAXCOL;   // before or after the line
                if ((text_flags & TP_FLAG_ALIGN_ABOVE) == 0)
                    sort_col = MAXCOL;
-               length += text_padding_left;
            }
        }
 
-       // Allocate the new line with space for the new property.
-       newtext = alloc(buf->b_ml.ml_line_len + sizeof(textprop_T));
-       if (newtext == NULL)
-           goto theend;
-       // Copy the text, including terminating NUL.
-       mch_memmove(newtext, buf->b_ml.ml_line_ptr, textlen);
-
-       // Find the index where to insert the new property.
-       // Since the text properties are not aligned properly when stored with
-       // the text, we need to copy them as bytes before using it as a struct.
-       for (i = 0; i < proplen; ++i)
-       {
-           colnr_T prop_col;
-
-           mch_memmove(&tmp_prop, props + i * sizeof(textprop_T),
-                                                          sizeof(textprop_T));
-           // col is MAXCOL when the text goes above or after the line, when
-           // above we should use column zero for sorting
-           prop_col = (tmp_prop.tp_flags & TP_FLAG_ALIGN_ABOVE)
-                               ? 0 : tmp_prop.tp_col;
-           if (prop_col >= sort_col)
-               break;
-       }
-       newprops = newtext + textlen;
-       if (i > 0)
-           mch_memmove(newprops, props, sizeof(textprop_T) * i);
-
+       // Build the new property.
+       CLEAR_FIELD(tmp_prop);
        tmp_prop.tp_col = col;
        tmp_prop.tp_len = length;
        tmp_prop.tp_id = id;
@@ -309,18 +788,115 @@ prop_add_one(
                            | ((type->pt_flags & PT_FLAG_INS_START_INCL)
                                                     ? TP_FLAG_START_INCL : 0);
        tmp_prop.tp_padleft = text_padding_left;
-       mch_memmove(newprops + i * sizeof(textprop_T), &tmp_prop,
+       if (text_arg != NULL)
+           tmp_prop.u.tp_text = text_arg;
+
+       // Find the index where to insert the new property.
+       for (i = 0; i < proplen; ++i)
+       {
+           textprop_T  existing;
+           colnr_T     prop_col;
+
+           mch_memmove(&existing, props + i * sizeof(textprop_T),
                                                           sizeof(textprop_T));
+           prop_col = (existing.tp_flags & TP_FLAG_ALIGN_ABOVE)
+                               ? 0 : existing.tp_col;
+           if (prop_col >= sort_col)
+               break;
+       }
+
+       // Compute the sizes for the new memline format:
+       //   [text][NUL][prop_count][textprop_T...][vtext...]
+       uint16_t        new_propcount = (uint16_t)(proplen + 1);
+       int             vtext_total = 0;
+       int             new_line_len;
+       char_u  *count_dest;
+       char_u  *vtext_dest;
+       int             j;
+
+       // Compute total vtext size from existing props.
+       for (j = 0; j < proplen; ++j)
+       {
+           textprop_T ep;
+
+           mch_memmove(&ep, props + j * sizeof(textprop_T),
+                                                       sizeof(textprop_T));
+           if (ep.tp_id < 0 && ep.tp_len > 0)
+               vtext_total += ep.tp_len + 1;
+       }
+       // Add new vtext if this is a virtual text prop.
+       if (text_arg != NULL)
+           vtext_total += length + 1;
+
+       new_line_len = (int)textlen + (int)PROP_COUNT_SIZE
+                       + new_propcount * (int)sizeof(textprop_T)
+                       + vtext_total;
+       newtext = alloc(new_line_len);
+       if (newtext == NULL)
+           goto theend;
 
+       // Copy line text.
+       mch_memmove(newtext, buf->b_ml.ml_line_ptr, textlen);
+
+       // Write prop_count.
+       count_dest = newtext + textlen;
+       mch_memmove(count_dest, &new_propcount, PROP_COUNT_SIZE);
+
+       // Write properties: [0..i-1] existing, [i] new, [i..proplen-1]
+       // existing.
+       newprops = count_dest + PROP_COUNT_SIZE;
+       if (i > 0)
+           mch_memmove(newprops, props, sizeof(textprop_T) * i);
+       // new prop is written after vtext offsets are computed
        if (i < proplen)
            mch_memmove(newprops + (i + 1) * sizeof(textprop_T),
-                                           props + i * sizeof(textprop_T),
-                                           sizeof(textprop_T) * (proplen - i));
+                                       props + i * sizeof(textprop_T),
+                                       sizeof(textprop_T) * (proplen - i));
+
+       // Write vtext strings and set offsets.
+       vtext_dest = newprops + new_propcount * sizeof(textprop_T);
+       for (j = 0; j < new_propcount; ++j)
+       {
+           textprop_T ep;
+
+           if (j == i)
+               continue;  // handle new prop separately below
+           mch_memmove(&ep, newprops + j * sizeof(textprop_T),
+                   sizeof(textprop_T));
+           if (ep.tp_id < 0 && ep.tp_len > 0)
+           {
+               // Copy existing vtext from old memline data.
+               char_u *old_count = (char_u *)props - PROP_COUNT_SIZE;
+               char_u *old_vtext = old_count + ep.u.tp_text_offset;
+
+               mch_memmove(vtext_dest, old_vtext, ep.tp_len + 1);
+               ep.u.tp_text_offset = (colnr_T)(vtext_dest - count_dest);
+               mch_memmove(newprops + j * sizeof(textprop_T), &ep,
+                       sizeof(textprop_T));
+               vtext_dest += ep.tp_len + 1;
+           }
+       }
+
+       // Write new prop with vtext.
+       if (text_arg != NULL)
+       {
+           int copysize = tmp_prop.tp_len + 1;
+
+           mch_memmove(vtext_dest, text_arg, copysize);
+           tmp_prop.u.tp_text_offset =
+                                   (colnr_T)(vtext_dest - count_dest);
+           vtext_dest += copysize;
+       }
+       else
+           tmp_prop.u.tp_text_offset = 0;
+
+       mch_memmove(newprops + i * sizeof(textprop_T), &tmp_prop,
+                                                       sizeof(textprop_T));
 
        if (buf->b_ml.ml_flags & (ML_LINE_DIRTY | ML_ALLOCATED))
            vim_free(buf->b_ml.ml_line_ptr);
        buf->b_ml.ml_line_ptr = newtext;
-       buf->b_ml.ml_line_len += sizeof(textprop_T);
+       buf->b_ml.ml_line_len = new_line_len;
        buf->b_ml.ml_flags |= ML_LINE_DIRTY;
     }
 
@@ -424,14 +1000,18 @@ f_prop_add_list(typval_T *argvars, typval_T *rettv UNUSED)
     redraw_buf_later(buf, UPD_VALID);
 }
 
+// Counter for virtual text property IDs (decremented for each new one).
+static int vt_id_counter = 0;
+
 /*
- * Get the next ID to use for a textprop with text in buffer "buf".
+ * Get the next ID to use for a textprop.
  */
     static int
-get_textprop_id(buf_T *buf)
+get_textprop_id(void)
 {
-    // TODO: recycle deleted entries
-    return -(buf->b_textprop_text.ga_len + 1);
+    if (vt_id_counter > 0)
+       vt_id_counter = 0;
+    return --vt_id_counter;
 }
 
 /*
@@ -597,7 +1177,7 @@ prop_add_common(
 
     if (text != NULL)
        // Always assign an internal negative id; ignore any user-provided id.
-       id = get_textprop_id(buf);
+       id = get_textprop_id();
     else if (id < 0)
     {
        emsg(_(e_cannot_use_negative_id));
@@ -629,9 +1209,10 @@ theend:
     int
 get_text_props(buf_T *buf, linenr_T lnum, char_u **props, int will_change)
 {
-    char_u *text;
-    size_t textlen;
-    size_t proplen;
+    char_u     *text;
+    size_t     textlen;
+    size_t     propdata_len;
+    uint16_t   prop_count;
 
     // Be quick when no text property types have been defined for the buffer,
     // unless we are adding one.
@@ -641,16 +1222,21 @@ get_text_props(buf_T *buf, linenr_T lnum, char_u **props, int will_change)
     // Fetch the line to get the ml_line_len field updated.
     text = ml_get_buf(buf, lnum, will_change);
     textlen = ml_get_buf_len(buf, lnum) + 1;
-    proplen = buf->b_ml.ml_line_len - textlen;
-    if (proplen == 0)
+    propdata_len = buf->b_ml.ml_line_len - textlen;
+    if (propdata_len == 0)
        return 0;
-    if (proplen % sizeof(textprop_T) != 0)
+
+    // Format: [prop_count (uint16)][textprop_T...][vtext...]
+    // Lines without properties have no prop data at all.
+    // prop_count is never zero.
+    if (propdata_len < PROP_COUNT_SIZE + sizeof(textprop_T))
     {
        iemsg(e_text_property_info_corrupted);
        return 0;
     }
-    *props = text + textlen;
-    return (int)(proplen / sizeof(textprop_T));
+    mch_memmove(&prop_count, text + textlen, PROP_COUNT_SIZE);
+    *props = text + textlen + PROP_COUNT_SIZE;
+    return (int)prop_count;
 }
 
 /*
@@ -701,10 +1287,9 @@ count_props(linenr_T lnum, int only_starting, int last_line)
     char_u     *props;
     int                proplen = get_text_props(curbuf, lnum, &props, 0);
     int                result = proplen;
-    int                i;
     textprop_T prop;
 
-    for (i = 0; i < proplen; ++i)
+    for (int i = 0; i < proplen; ++i)
     {
        mch_memmove(&prop, props + i * sizeof(prop), sizeof(prop));
        // A prop is dropped when in the first line and it continues from the
@@ -813,9 +1398,9 @@ sort_text_props(
 /*
  * Find text property "type_id" in the visible lines of window "wp".
  * Match "id" when it is > 0.
- * Returns FAIL when not found.
+ * Returns false when not found.
  */
-    int
+    bool
 find_visible_prop(
        win_T       *wp,
        int         type_id,
@@ -825,7 +1410,7 @@ find_visible_prop(
 {
     // return when "type_id" no longer exists
     if (text_prop_type_by_id(wp->w_buffer, type_id) == NULL)
-       return FAIL;
+       return false;
 
     // w_botline may not have been updated yet.
     validate_botline_win(wp);
@@ -840,21 +1425,22 @@ find_visible_prop(
            if (prop->tp_type == type_id && (id <= 0 || prop->tp_id == id))
            {
                *found_lnum = lnum;
-               return OK;
+               return true;
            }
        }
     }
-    return FAIL;
+    return false;
 }
 
 /*
- * Set the text properties for line "lnum" to "props" with length "len".
- * If "len" is zero text properties are removed, "props" is not used.
+ * Set the text properties for line "lnum" to "tps" array with "count" entries.
+ * If "count" is zero text properties are removed.
  * Any existing text properties are dropped.
+ * For virtual text props, u.tp_text must point to the vtext string.
  * Only works for the current buffer.
  */
     static void
-set_text_props(linenr_T lnum, char_u *props, int len)
+set_text_props(linenr_T lnum, textprop_T *tps, int count)
 {
     char_u  *text;
     char_u  *newtext;
@@ -862,17 +1448,150 @@ set_text_props(linenr_T lnum, char_u *props, int len)
 
     text = ml_get(lnum);
     textlen = ml_get_len(lnum) + 1;
-    newtext = alloc(textlen + len);
-    if (newtext == NULL)
-       return;
-    mch_memmove(newtext, text, textlen);
-    if (len > 0)
-       mch_memmove(newtext + textlen, props, len);
-    if (curbuf->b_ml.ml_flags & (ML_LINE_DIRTY | ML_ALLOCATED))
-       vim_free(curbuf->b_ml.ml_line_ptr);
-    curbuf->b_ml.ml_line_ptr = newtext;
-    curbuf->b_ml.ml_line_len = textlen + len;
-    curbuf->b_ml.ml_flags |= ML_LINE_DIRTY;
+
+    if (count == 0)
+    {
+       // No properties: just text.
+       newtext = alloc(textlen);
+       if (newtext == NULL)
+           return;
+       mch_memmove(newtext, text, textlen);
+       if (curbuf->b_ml.ml_flags & (ML_LINE_DIRTY | ML_ALLOCATED))
+           vim_free(curbuf->b_ml.ml_line_ptr);
+       curbuf->b_ml.ml_line_ptr = newtext;
+       curbuf->b_ml.ml_line_len = textlen;
+       curbuf->b_ml.ml_flags |= ML_LINE_DIRTY;
+    }
+    else
+    {
+       // Build new format: [text][NUL][prop_count][props...][vtext...]
+       uint16_t    prop_count = (uint16_t)count;
+       int         vtext_size = 0;
+       int         total_len;
+       char_u      *count_dest;
+       char_u      *prop_dest;
+       char_u      *vtext_dest;
+       int         i;
+
+       for (i = 0; i < count; ++i)
+           if (tps[i].tp_flags & TP_FLAG_VTEXT_PTR)
+               vtext_size += tps[i].tp_len + 1;
+
+       total_len = textlen + (int)PROP_COUNT_SIZE
+                   + count * (int)sizeof(textprop_T) + vtext_size;
+       newtext = alloc(total_len);
+       if (newtext == NULL)
+           return;
+       mch_memmove(newtext, text, textlen);
+
+       count_dest = newtext + textlen;
+       mch_memmove(count_dest, &prop_count, PROP_COUNT_SIZE);
+
+       prop_dest = count_dest + PROP_COUNT_SIZE;
+       vtext_dest = prop_dest + count * sizeof(textprop_T);
+
+       for (i = 0; i < count; ++i)
+       {
+           textprop_T prop = tps[i];
+
+           if (prop.tp_flags & TP_FLAG_VTEXT_PTR)
+           {
+               int copysize = prop.tp_len + 1;
+
+               mch_memmove(vtext_dest, prop.u.tp_text, copysize);
+               vim_free(tps[i].u.tp_text);
+               tps[i].u.tp_text = NULL;
+               prop.u.tp_text_offset = (colnr_T)(vtext_dest - count_dest);
+               vtext_dest += copysize;
+           }
+           else
+               prop.u.tp_text_offset = 0;
+           mch_memmove(prop_dest, &prop, sizeof(textprop_T));
+           prop_dest += sizeof(textprop_T);
+       }
+
+       if (curbuf->b_ml.ml_flags & (ML_LINE_DIRTY | ML_ALLOCATED))
+           vim_free(curbuf->b_ml.ml_line_ptr);
+       curbuf->b_ml.ml_line_ptr = newtext;
+       curbuf->b_ml.ml_line_len = total_len;
+       curbuf->b_ml.ml_flags |= ML_LINE_DIRTY;
+    }
+}
+
+/*
+ * Convert a line buffer with text properties in the old format
+ * [text][NUL][textprop_T...] to the new format
+ * [text][NUL][prop_count][textprop_T...][vtext...].
+ * For virtual text props, u.tp_text is expected to be a pointer to the
+ * vtext string (allocated).  These strings are freed after copying.
+ * Returns a newly allocated buffer, or NULL on failure.
+ * "line_len" is the total length (text + props).  "textlen" is the length
+ * of the text including NUL.  "*new_len" is set to the new total length.
+ */
+    char_u *
+props_add_count_header(char_u *line, int line_len, int textlen, int *new_len)
+{
+    int                prop_bytes = line_len - textlen;
+    uint16_t   prop_count;
+    int                vtext_size = 0;
+    int                total;
+    char_u     *newline;
+    char_u     *count_dest;
+    char_u     *prop_dest;
+    char_u     *vtext_dest;
+    int                i;
+
+    if (prop_bytes <= 0 || prop_bytes % sizeof(textprop_T) != 0)
+    {
+       *new_len = line_len;
+       return NULL;
+    }
+    prop_count = (uint16_t)(prop_bytes / sizeof(textprop_T));
+
+    // Calculate total vtext size.
+    for (i = 0; i < (int)prop_count; ++i)
+    {
+       textprop_T prop;
+
+       mch_memmove(&prop, line + textlen + i * sizeof(textprop_T),
+                                                       sizeof(textprop_T));
+       if (prop.tp_flags & TP_FLAG_VTEXT_PTR)
+           vtext_size += prop.tp_len + 1;
+    }
+
+    total = textlen + (int)PROP_COUNT_SIZE + prop_bytes + vtext_size;
+    newline = alloc(total);
+    if (newline == NULL)
+       return NULL;
+    *new_len = total;
+
+    mch_memmove(newline, line, textlen);
+    count_dest = newline + textlen;
+    mch_memmove(count_dest, &prop_count, PROP_COUNT_SIZE);
+    prop_dest = count_dest + PROP_COUNT_SIZE;
+    vtext_dest = prop_dest + prop_bytes;
+
+    for (i = 0; i < (int)prop_count; ++i)
+    {
+       textprop_T prop;
+
+       mch_memmove(&prop, line + textlen + i * sizeof(textprop_T),
+                                                       sizeof(textprop_T));
+       if (prop.tp_flags & TP_FLAG_VTEXT_PTR)
+       {
+           int copysize = prop.tp_len + 1;
+
+           mch_memmove(vtext_dest, prop.u.tp_text, copysize);
+           vim_free(prop.u.tp_text);
+           prop.u.tp_text_offset = (colnr_T)(vtext_dest - count_dest);
+           vtext_dest += copysize;
+       }
+       else
+           prop.u.tp_text_offset = 0;
+       mch_memmove(prop_dest + i * sizeof(textprop_T), &prop,
+                                                       sizeof(textprop_T));
+    }
+    return newline;
 }
 
 /*
@@ -881,21 +1600,36 @@ set_text_props(linenr_T lnum, char_u *props, int len)
     void
 add_text_props(linenr_T lnum, textprop_T *text_props, int text_prop_count)
 {
-    char_u  *text;
-    char_u  *newtext;
-    int            proplen = text_prop_count * (int)sizeof(textprop_T);
+    unpacked_memline_T um;
+    int                        i;
 
-    text = ml_get(lnum);
-    newtext = alloc(curbuf->b_ml.ml_line_len + proplen);
-    if (newtext == NULL)
+    um = um_open_at(curbuf, lnum, text_prop_count);
+    if (um.buf == NULL)
        return;
-    mch_memmove(newtext, text, curbuf->b_ml.ml_line_len);
-    mch_memmove(newtext + curbuf->b_ml.ml_line_len, text_props, proplen);
-    if (curbuf->b_ml.ml_flags & (ML_LINE_DIRTY | ML_ALLOCATED))
-       vim_free(curbuf->b_ml.ml_line_ptr);
-    curbuf->b_ml.ml_line_ptr = newtext;
-    curbuf->b_ml.ml_line_len += proplen;
-    curbuf->b_ml.ml_flags |= ML_LINE_DIRTY;
+    if (!um_detach(&um))
+    {
+       um_abort(&um);
+       return;
+    }
+
+    // Grow props array if needed.
+    if (um.prop_count + text_prop_count > um.prop_size)
+    {
+       textprop_T *newprops = vim_realloc(um.props,
+               (um.prop_count + text_prop_count) * sizeof(textprop_T));
+       if (newprops == NULL)
+       {
+           um_abort(&um);
+           return;
+       }
+       um.props = newprops;
+       um.prop_size = um.prop_count + text_prop_count;
+    }
+
+    for (i = 0; i < text_prop_count; ++i)
+       um.props[um.prop_count++] = text_props[i];
+
+    um_close(&um);
 }
 
 /*
@@ -966,11 +1700,7 @@ prop_fill_dict(dict_T *dict, textprop_T *prop, buf_T *buf)
 {
     proptype_T *pt;
     int buflocal = TRUE;
-    // A negative tp_id normally means a virtual text property, but a user
-    // may set a negative id for a regular property when no virtual text
-    // properties exist.  Guard against that by checking the index is valid.
-    int virtualtext_prop = prop->tp_id < 0
-                          && -prop->tp_id - 1 < buf->b_textprop_text.ga_len;
+    int virtualtext_prop = prop->tp_id < 0;
 
     dict_add_number(dict, "col", (prop->tp_col == MAXCOL) ? 0 : prop->tp_col);
     if (!virtualtext_prop)
@@ -997,13 +1727,8 @@ prop_fill_dict(dict_T *dict, textprop_T *prop, buf_T *buf)
        dict_add_number(dict, "type_bufnr", 0);
     if (virtualtext_prop)
     {
-       // virtual text property
-       garray_T    *gap = &buf->b_textprop_text;
-       char_u      *text;
-
-       // negate the property id to get the string index
-       text = ((char_u **)gap->ga_data)[-prop->tp_id - 1];
-       dict_add_string(dict, "text", text);
+       // virtual text property - u.tp_text must be set by caller
+       dict_add_string(dict, "text", prop->u.tp_text);
 
        // text_align
        char_u      *text_align = NULL;
@@ -1040,9 +1765,9 @@ text_prop_type_by_id(buf_T *buf, int id)
 }
 
 /*
- * Return TRUE if "prop" is a valid text property type.
+ * Return true if "prop" is a valid text property type.
  */
-    int
+    bool
 text_prop_type_valid(buf_T *buf, textprop_T *prop)
 {
     return text_prop_type_by_id(buf, prop->tp_type) != NULL;
@@ -1084,32 +1809,25 @@ f_prop_clear(typval_T *argvars, typval_T *rettv UNUSED)
        return;
     }
 
-    for (lnum = start; lnum <= end; ++lnum)
     {
-       char_u *text;
-       size_t len;
+       unpacked_memline_T um = um_open(buf);
 
-       if (lnum > buf->b_ml.ml_line_count)
-           break;
-       text = ml_get_buf(buf, lnum, FALSE);
-       len = ml_get_buf_len(buf, lnum) + 1;
-       if ((size_t)buf->b_ml.ml_line_len > len)
+       for (lnum = start; lnum <= end; ++lnum)
        {
-           did_clear = TRUE;
-           if (!(buf->b_ml.ml_flags & ML_LINE_DIRTY))
-           {
-               char_u *newtext = vim_strsave(text);
+           int idx;
 
-               // need to allocate the line now
-               if (newtext == NULL)
-                   return;
-               if (buf->b_ml.ml_flags & ML_ALLOCATED)
-                   vim_free(buf->b_ml.ml_line_ptr);
-               buf->b_ml.ml_line_ptr = newtext;
-               buf->b_ml.ml_flags |= ML_LINE_DIRTY;
+           if (lnum > buf->b_ml.ml_line_count)
+               break;
+           if (!um_goto_line(&um, lnum, 0))
+               break;
+           if (um.prop_count > 0)
+           {
+               did_clear = TRUE;
+               for (idx = um.prop_count - 1; idx >= 0; --idx)
+                   um_delete_prop(&um, idx);
            }
-           buf->b_ml.ml_line_len = (int)len;
        }
+       um_close(&um);
     }
     if (did_clear)
        redraw_buf_later(buf, UPD_NOT_VALID);
@@ -1221,10 +1939,8 @@ f_prop_find(typval_T *argvars, typval_T *rettv)
 
     while (1)
     {
-       char_u  *text = ml_get_buf(buf, lnum, FALSE);
-       size_t  textlen = ml_get_buf_len(buf, lnum) + 1;
-       int     count = (int)((buf->b_ml.ml_line_len - textlen)
-                                                        / sizeof(textprop_T));
+       char_u  *prop_start_ptr;
+       int     count = get_text_props(buf, lnum, &prop_start_ptr, FALSE);
        int         i;
        textprop_T  prop;
        int         prop_start;
@@ -1232,8 +1948,17 @@ f_prop_find(typval_T *argvars, typval_T *rettv)
 
        for (i = dir == BACKWARD ? count - 1 : 0; i >= 0 && i < count; i += dir)
        {
-           mch_memmove(&prop, text + textlen + i * sizeof(textprop_T),
+           mch_memmove(&prop, prop_start_ptr + i * sizeof(textprop_T),
                                                           sizeof(textprop_T));
+           // Convert offset to pointer for virtual text props.
+           if (prop.tp_id < 0 && prop.u.tp_text_offset > 0)
+           {
+               prop.u.tp_text = (prop_start_ptr - PROP_COUNT_SIZE)
+                                                   + prop.u.tp_text_offset;
+               prop.tp_flags |= TP_FLAG_VTEXT_PTR;
+           }
+           else
+               prop.u.tp_text = NULL;
 
            // For the very first line try to find the first property before or
            // after `col`, depending on the search direction.
@@ -1342,17 +2067,25 @@ get_props_in_line(
        list_T          *retlist,
        int             add_lnum)
 {
-    char_u     *text = ml_get_buf(buf, lnum, FALSE);
-    size_t     textlen = ml_get_buf_len(buf, lnum) + 1;
+    char_u     *props;
     int                count;
     int                i;
     textprop_T prop;
 
-    count = (int)((buf->b_ml.ml_line_len - textlen) / sizeof(textprop_T));
+    count = get_text_props(buf, lnum, &props, FALSE);
     for (i = 0; i < count; ++i)
     {
-       mch_memmove(&prop, text + textlen + i * sizeof(textprop_T),
+       mch_memmove(&prop, props + i * sizeof(textprop_T),
                sizeof(textprop_T));
+       // Convert offset to pointer for virtual text props.
+       if (prop.tp_id < 0 && prop.u.tp_text_offset > 0)
+       {
+           prop.u.tp_text = (props - PROP_COUNT_SIZE)
+                                               + prop.u.tp_text_offset;
+           prop.tp_flags |= TP_FLAG_VTEXT_PTR;
+       }
+       else
+           prop.u.tp_text = NULL;
        if ((prop_types == NULL
                    || prop_type_or_id_in_list(prop_types, prop_types_len,
                        prop.tp_type))
@@ -1579,7 +2312,6 @@ f_prop_remove(typval_T *argvars, typval_T *rettv)
     int                *type_ids = NULL;   // array, for a list of "types", allocated
     int                num_type_ids = 0;   // number of elements in "type_ids"
     int                both;
-    int                did_remove_text = FALSE;
 
     rettv->vval.v_number = 0;
 
@@ -1674,84 +2406,44 @@ f_prop_remove(typval_T *argvars, typval_T *rettv)
 
     if (end == 0)
        end = buf->b_ml.ml_line_count;
-    for (lnum = start; lnum <= end; ++lnum)
+
     {
-       size_t len;
+       unpacked_memline_T um = um_open(buf);
 
-       if (lnum > buf->b_ml.ml_line_count)
-           break;
-       len = ml_get_buf_len(buf, lnum) + 1;
-       if ((size_t)buf->b_ml.ml_line_len > len)
+       for (lnum = start; lnum <= end; ++lnum)
        {
-           static textprop_T   textprop;  // static because of alignment
-           unsigned            idx;
+           int idx;
 
-           for (idx = 0; idx < (buf->b_ml.ml_line_len - len)
-                                                  / sizeof(textprop_T); ++idx)
+           if (lnum > buf->b_ml.ml_line_count)
+               break;
+           if (!um_goto_line(&um, lnum, 0))
+               break;
+
+           for (idx = 0; idx < um.prop_count; ++idx)
            {
-               char_u *cur_prop = buf->b_ml.ml_line_ptr + len
-                                                   + idx * sizeof(textprop_T);
-               size_t  taillen;
-               int matches_id = 0;
-               int matches_type = 0;
+               textprop_T  *prop = &um.props[idx];
+               int         matches_id = 0;
+               int         matches_type = 0;
 
-               mch_memmove(&textprop, cur_prop, sizeof(textprop_T));
+               if (prop->tp_flags & TP_FLAG_DELETED)
+                   continue;
 
-               matches_id = textprop.tp_id == id;
+               matches_id = prop->tp_id == id;
                if (num_type_ids > 0)
                {
                    int idx2;
 
-                   for (idx2 = 0; !matches_type && idx2 < num_type_ids; ++idx2)
-                       matches_type = textprop.tp_type == type_ids[idx2];
+                   for (idx2 = 0; !matches_type && idx2 < num_type_ids;
+                                                                     ++idx2)
+                       matches_type = prop->tp_type == type_ids[idx2];
                }
                else
-               {
-                   matches_type = textprop.tp_type == type_id;
-               }
+                   matches_type = prop->tp_type == type_id;
 
                if (both ? matches_id && matches_type
                         : matches_id || matches_type)
                {
-                   if (!(buf->b_ml.ml_flags & ML_LINE_DIRTY))
-                   {
-                       char_u *newptr = alloc(buf->b_ml.ml_line_len);
-
-                       // need to allocate the line to be able to change it
-                       if (newptr == NULL)
-                           goto cleanup_prop_remove;
-                       mch_memmove(newptr, buf->b_ml.ml_line_ptr,
-                                                       buf->b_ml.ml_line_len);
-                       if (buf->b_ml.ml_flags & ML_ALLOCATED)
-                           vim_free(buf->b_ml.ml_line_ptr);
-                       buf->b_ml.ml_line_ptr = newptr;
-                       buf->b_ml.ml_flags |= ML_LINE_DIRTY;
-
-                       cur_prop = buf->b_ml.ml_line_ptr + len
-                                                   + idx * sizeof(textprop_T);
-                   }
-
-                   taillen = buf->b_ml.ml_line_len - len
-                                             - (idx + 1) * sizeof(textprop_T);
-                   if (taillen > 0)
-                       mch_memmove(cur_prop, cur_prop + sizeof(textprop_T),
-                                                                     taillen);
-                   buf->b_ml.ml_line_len -= sizeof(textprop_T);
-                   --idx;
-
-                   if (textprop.tp_id < 0)
-                   {
-                       garray_T    *gap = &buf->b_textprop_text;
-                       int         ii = -textprop.tp_id - 1;
-
-                       // negative ID: property with text - free the text
-                       if (ii < gap->ga_len)
-                       {
-                           char_u **p = ((char_u **)gap->ga_data) + ii;
-                           VIM_CLEAR(*p);
-                           did_remove_text = TRUE;
-                       }
-                   }
+                   um_delete_prop(&um, idx);
 
                    if (first_changed == 0)
                        first_changed = lnum;
@@ -1762,6 +2454,7 @@ f_prop_remove(typval_T *argvars, typval_T *rettv)
                }
            }
        }
+       um_close(&um);
     }
 
     if (first_changed > 0)
@@ -1771,16 +2464,6 @@ f_prop_remove(typval_T *argvars, typval_T *rettv)
        redraw_buf_later(buf, UPD_VALID);
     }
 
-    if (did_remove_text)
-    {
-       garray_T    *gap = &buf->b_textprop_text;
-
-       // Reduce the growarray size for NULL pointers at the end.
-       while (gap->ga_len > 0
-                        && ((char_u **)gap->ga_data)[gap->ga_len - 1] == NULL)
-           --gap->ga_len;
-    }
-
 cleanup_prop_remove:
     vim_free(type_ids);
 }
@@ -2198,7 +2881,8 @@ adjust_prop(
                - (start_incl || (prop->tp_len == 0 && end_incl)))
            // Change is entirely before the text property: Only shift
            prop->tp_col += added;
-       else if (col + 1 < prop->tp_col + prop->tp_len + end_incl)
+       else if (col + 1 < prop->tp_col + prop->tp_len + end_incl
+               && prop->tp_id >= 0)  // don't change length for virtual text
            // Insertion was inside text property
            prop->tp_len += added;
     }
@@ -2206,12 +2890,24 @@ adjust_prop(
     {
        if (prop->tp_col + added < col + 1)
        {
-           prop->tp_len += (prop->tp_col - 1 - col) + added;
-           prop->tp_col = col + 1;
-           if (prop->tp_len <= 0)
+           if (prop->tp_id < 0)
            {
-               prop->tp_len = 0;
-               res.can_drop = droppable;
+               // Inline virtual text swallowed by the deletion.
+               res.can_drop = TRUE;
+           }
+           else
+           {
+               prop->tp_len += (prop->tp_col - 1 - col) + added;
+               prop->tp_col = col + 1;
+               if (prop->tp_len <= 0)
+               {
+                   prop->tp_len = 0;
+                   // Multiline properties with no text left should
+                   // also be dropped.
+                   res.can_drop = droppable
+                       || (prop->tp_flags
+                               & (TP_FLAG_CONT_PREV | TP_FLAG_CONT_NEXT));
+               }
            }
        }
        else
@@ -2220,10 +2916,22 @@ adjust_prop(
     else if (prop->tp_len > 0 && prop->tp_col + prop->tp_len > col
            && prop->tp_id >= 0)  // don't change length for virtual text
     {
-       int after = col - added - (prop->tp_col - 1 + prop->tp_len);
+       if (added <= -MAXCOL)
+           // Delete everything from col to end of line.
+           prop->tp_len = col - (prop->tp_col - 1);
+       else
+       {
+           int after = col - added - (prop->tp_col - 1 + prop->tp_len);
 
-       prop->tp_len += after > 0 ? added + after : added;
-       res.can_drop = prop->tp_len <= 0 && droppable;
+           prop->tp_len += after > 0 ? added + after : added;
+       }
+       // A multiline property with no text left on this line
+       // should also be dropped.  When TP_FLAG_CONT_NEXT is set,
+       // tp_len == 1 means only the newline remains.
+       if (prop->tp_len <= 0 || ((prop->tp_flags & TP_FLAG_CONT_NEXT)
+                   && prop->tp_len <= 1))
+           res.can_drop = droppable || (prop->tp_flags
+                       & (TP_FLAG_CONT_PREV | TP_FLAG_CONT_NEXT));
     }
     else
        res.dirty = FALSE;
@@ -2250,60 +2958,60 @@ adjust_prop_columns(
        int         bytes_added,
        int         flags)
 {
-    int                proplen;
-    char_u     *props;
+    unpacked_memline_T um;
     int                dirty = FALSE;
-    int                ri, wi;
-    size_t     textlen;
+    int                ri;
 
     if (text_prop_frozen > 0)
        return FALSE;
 
-    proplen = get_text_props(curbuf, lnum, &props, TRUE);
-    if (proplen == 0)
+    um = um_open_at(curbuf, lnum, 0);
+    if (um.buf == NULL || um.prop_count == 0)
+    {
+       um_abort(&um);
        return FALSE;
-    textlen = curbuf->b_ml.ml_line_len - proplen * sizeof(textprop_T);
+    }
 
-    wi = 0; // write index
-    for (ri = 0; ri < proplen; ++ri)
+    for (ri = 0; ri < um.prop_count; ++ri)
     {
-       textprop_T      prop;
+       textprop_T      *prop = &um.props[ri];
        adjustres_T     res;
 
-       mch_memmove(&prop, props + ri * sizeof(prop), sizeof(prop));
-       res = adjust_prop(&prop, col, bytes_added, flags);
+       res = adjust_prop(prop, col, bytes_added, flags);
        if (res.dirty)
        {
-           // Save for undo if requested and not done yet.
-           if ((flags & APC_SAVE_FOR_UNDO) && !dirty
-                                                   && u_savesub(lnum) == FAIL)
-               return FALSE;
-           dirty = TRUE;
+           if (!dirty)
+           {
+               // Detach before u_savesub() so that all text and vtext
+               // pointers are allocated copies, immune to memline
+               // changes caused by u_savesub().
+               if (!um.detached && !um_detach(&um))
+               {
+                   um_abort(&um);
+                   return FALSE;
+               }
+               // Re-get prop pointer after detach (array may move).
+               prop = &um.props[ri];
 
-           // u_savesub() may have updated curbuf->b_ml, fetch it again
-           if (curbuf->b_ml.ml_line_lnum != lnum)
-               proplen = get_text_props(curbuf, lnum, &props, TRUE);
+               if ((flags & APC_SAVE_FOR_UNDO)
+                                              && u_savesub(lnum) == FAIL)
+               {
+                   um_abort(&um);
+                   return FALSE;
+               }
+           }
+           dirty = TRUE;
        }
        if (res.can_drop)
-           continue; // Drop this text property
-       mch_memmove(props + wi * sizeof(textprop_T), &prop, sizeof(textprop_T));
-       ++wi;
-    }
-    if (dirty)
-    {
-       colnr_T newlen = (int)textlen + wi * (colnr_T)sizeof(textprop_T);
-
-       if ((curbuf->b_ml.ml_flags & ML_LINE_DIRTY) == 0)
        {
-           char_u *p = vim_memsave(curbuf->b_ml.ml_line_ptr, newlen);
-
-           if (curbuf->b_ml.ml_flags & ML_ALLOCATED)
-               vim_free(curbuf->b_ml.ml_line_ptr);
-           curbuf->b_ml.ml_line_ptr = p;
+           um_delete_prop(&um, ri);
+           continue;
        }
-       curbuf->b_ml.ml_flags |= ML_LINE_DIRTY;
-       curbuf->b_ml.ml_line_len = newlen;
     }
+    if (dirty)
+       um_close(&um);
+    else
+       um_abort(&um);
     return dirty;
 }
 
@@ -2327,7 +3035,6 @@ adjust_props_for_split(
     int                count;
     garray_T    prevprop;
     garray_T    nextprop;
-    int                i;
     int                skipped = kept + deleted;
 
     if (!curbuf->b_has_textprop)
@@ -2338,10 +3045,13 @@ adjust_props_for_split(
     ga_init2(&prevprop, sizeof(textprop_T), 10);
     ga_init2(&nextprop, sizeof(textprop_T), 10);
 
+    // count_ptr points to the prop_count field in the memline.
+    char_u *count_ptr = props - PROP_COUNT_SIZE;
+
     // Keep the relevant ones in the first line, reducing the length if needed.
     // Copy the ones that include the split to the second line.
     // Move the ones after the split to the second line.
-    for (i = 0; i < count; ++i)
+    for (int i = 0; i < count; ++i)
     {
        textprop_T  prop;
        proptype_T *pt;
@@ -2352,6 +3062,16 @@ adjust_props_for_split(
        // copy the prop to an aligned structure
        mch_memmove(&prop, props + i * sizeof(textprop_T), sizeof(textprop_T));
 
+       // Convert offset to pointer and copy for virtual text props.
+       // Must copy because set_text_props() may invalidate memline data.
+       if (prop.tp_id < 0 && prop.u.tp_text_offset > 0)
+       {
+           prop.u.tp_text = vim_strsave(count_ptr + prop.u.tp_text_offset);
+           prop.tp_flags |= TP_FLAG_VTEXT_PTR;
+       }
+       else
+           prop.u.tp_text = NULL;
+
        pt = text_prop_type_by_id(curbuf, prop.tp_type);
        start_incl = (pt != NULL && (pt->pt_flags & PT_FLAG_INS_START_INCL));
        end_incl = (pt != NULL && (pt->pt_flags & PT_FLAG_INS_END_INCL));
@@ -2366,8 +3086,17 @@ adjust_props_for_split(
        }
        else
        {
+           // Floating virtual text (tp_col == MAXCOL) should not use
+           // tp_len for continuation since tp_len holds the vtext
+           // string length.
+           int is_floating_vtext = (prop.tp_id < 0
+                                               && prop.tp_col == MAXCOL);
+
            cont_prev = prop_col + !start_incl <= kept;
-           cont_next = skipped <= prop_col + prop.tp_len - !end_incl;
+           if (is_floating_vtext)
+               cont_next = FALSE;
+           else
+               cont_next = skipped <= prop_col + prop.tp_len - !end_incl;
        }
        // when a prop has text it is never copied
        if (prop.tp_id < 0 && cont_next)
@@ -2376,10 +3105,11 @@ adjust_props_for_split(
        if (cont_prev && ga_grow(&prevprop, 1) == OK)
        {
            textprop_T *p = ((textprop_T *)prevprop.ga_data) + prevprop.ga_len;
+           int is_vtext = (prop.tp_id < 0);
 
            *p = prop;
            ++prevprop.ga_len;
-           if (p->tp_col != MAXCOL && p->tp_col + p->tp_len >= kept)
+           if (!is_vtext && p->tp_col + p->tp_len >= kept)
                p->tp_len = kept - p->tp_col;
            if (cont_next)
                p->tp_flags |= TP_FLAG_CONT_NEXT;
@@ -2390,6 +3120,7 @@ adjust_props_for_split(
        if (cont_next && ga_grow(&nextprop, 1) == OK)
        {
            textprop_T *p = ((textprop_T *)nextprop.ga_data) + nextprop.ga_len;
+           int is_vtext = (prop.tp_id < 0);
 
            *p = prop;
            ++nextprop.ga_len;
@@ -2399,7 +3130,8 @@ adjust_props_for_split(
                    p->tp_col -= skipped - 1;
                else
                {
-                   p->tp_len -= skipped - p->tp_col;
+                   if (!is_vtext)
+                       p->tp_len -= skipped - p->tp_col;
                    p->tp_col = 1;
                }
            }
@@ -2408,67 +3140,75 @@ adjust_props_for_split(
        }
     }
 
-    set_text_props(lnum_top, prevprop.ga_data,
-                                        prevprop.ga_len * sizeof(textprop_T));
+    set_text_props(lnum_top, (textprop_T *)prevprop.ga_data,
+                                                           prevprop.ga_len);
     ga_clear(&prevprop);
-    set_text_props(lnum_top + 1, nextprop.ga_data,
-                                        nextprop.ga_len * sizeof(textprop_T));
+    set_text_props(lnum_top + 1, (textprop_T *)nextprop.ga_data,
+                                                           nextprop.ga_len);
     ga_clear(&nextprop);
 }
 
 /*
- * Prepend properties of joined line "lnum" to "new_props".
+ * Prepend properties of joined line "lnum" to the unpacked memline "um".
+ * Properties are added in reverse order; caller should call
+ * um_reverse_props() after all lines are processed.
  */
     void
 prepend_joined_props(
-       char_u      *new_props,
-       int         propcount,
-       int         *props_remaining,
-       linenr_T    lnum,
-       int         last_line,
-       long        col,
-       int         removed)
+       unpacked_memline_T  *um,
+       linenr_T            lnum,
+       int                 last_line,
+       long                col,
+       int                 removed)
 {
-    char_u *props;
-    int            proplen = get_text_props(curbuf, lnum, &props, FALSE);
-    int            i;
+    unpacked_memline_T r_um;
+
+    if (um->buf == NULL)
+       return;
 
-    for (i = proplen; i-- > 0; )
+    r_um = um_open_at(um->buf, lnum, 0);
+    if (r_um.buf == NULL)
+       return;
+    if (!um_detach(&r_um))
     {
-       textprop_T  prop;
+       um_abort(&r_um);
+       return;
+    }
+
+    for (int i = r_um.prop_count - 1; i >= 0; --i)
+    {
+       textprop_T  *prop = &r_um.props[i];
        int         end;
 
-       mch_memmove(&prop, props + i * sizeof(prop), sizeof(prop));
-       if (prop.tp_col == MAXCOL && !last_line)
-           continue;  // drop property with text after the line
-       end = !(prop.tp_flags & TP_FLAG_CONT_NEXT);
+       if (prop->tp_col == MAXCOL && !last_line)
+           continue;  // drop floating text for non-last lines
+       end = !(prop->tp_flags & TP_FLAG_CONT_NEXT);
 
-       adjust_prop(&prop, 0, -removed, 0); // Remove leading spaces
-       adjust_prop(&prop, -1, col, 0); // Make line start at its final column
+       adjust_prop(prop, 0, -removed, 0);
+       adjust_prop(prop, -1, col, 0);
 
        if (last_line || end)
-           mch_memmove(new_props + --(*props_remaining) * sizeof(prop),
-                                                         &prop, sizeof(prop));
+       {
+           um_add_prop(um, prop);
+           prop->u.tp_text = NULL;  // ownership transferred
+       }
        else
        {
-           int j;
-           int found = FALSE;
+           // Search for continuing prop in um.
+           bool    found = false;
 
-           // Search for continuing prop.
-           for (j = *props_remaining; j < propcount; ++j)
+           for (int j = 0; j < um->prop_count; ++j)
            {
-               textprop_T op;
+               textprop_T *op = &um->props[j];
 
-               mch_memmove(&op, new_props + j * sizeof(op), sizeof(op));
-               if ((op.tp_flags & TP_FLAG_CONT_PREV)
-                       && op.tp_id == prop.tp_id && op.tp_type == prop.tp_type)
+               if ((op->tp_flags & TP_FLAG_CONT_PREV)
+                       && op->tp_id == prop->tp_id
+                       && op->tp_type == prop->tp_type)
                {
-                   found = TRUE;
-                   op.tp_len += op.tp_col - prop.tp_col;
-                   op.tp_col = prop.tp_col;
-                   // Start/end is taken care of when deleting joined lines
-                   op.tp_flags = prop.tp_flags;
-                   mch_memmove(new_props + j * sizeof(op), &op, sizeof(op));
+                   found = true;
+                   op->tp_len += op->tp_col - prop->tp_col;
+                   op->tp_col = prop->tp_col;
+                   op->tp_flags = prop->tp_flags;
                    break;
                }
            }
@@ -2476,6 +3216,7 @@ prepend_joined_props(
                internal_error("text property above joined line not found");
        }
     }
+    um_abort(&r_um);
 }
 
 #endif // FEAT_PROP_POPUP
index 8e20e639c05ded9fa7e1ab1b4234c05bbf3f5f54..81519ea17bc6e2472472d2c0595d37df69eb7c64 100644 (file)
@@ -734,6 +734,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    320,
 /**/
     319,
 /**/