From: Hirohito Higashi Date: Tue, 7 Apr 2026 20:46:10 +0000 (+0000) Subject: patch 9.2.0320: several bugs with text properties X-Git-Tag: v9.2.0320^0 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=ff41e9d853ef3e366575e375d8c40cf11d5e331b;p=thirdparty%2Fvim.git patch 9.2.0320: several bugs with text properties Problem: several bugs with text properties Solution: Fix the bugs, rework the text properties work related: #19685 fixes: #19680 fixes: #19681 fixes: #12568 fixes: #19256 closes: #19869 Co-Authored-By: Paul Ollis Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Hirohito Higashi Signed-off-by: Christian Brabandt --- diff --git a/runtime/doc/textprop.txt b/runtime/doc/textprop.txt index f57a238e29..9fd5ff6686 100644 --- a/runtime/doc/textprop.txt +++ b/runtime/doc/textprop.txt @@ -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 diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt index 31d467e0ab..8adff066f5 100644 --- a/runtime/doc/version9.txt +++ b/runtime/doc/version9.txt @@ -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 ~ diff --git a/src/buffer.c b/src/buffer.c index 0f119e7622..6c99acd0a1 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -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 diff --git a/src/change.c b/src/change.c index ecb27d72d7..1fdee65445 100644 --- a/src/change.c +++ b/src/change.c @@ -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; diff --git a/src/charset.c b/src/charset.c index 1fbbf98f76..8ad768d354 100644 --- a/src/charset.c +++ b/src/charset.c @@ -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) { diff --git a/src/drawline.c b/src/drawline.c index f3a3b16710..ee93e07837 100644 --- a/src/drawline.c +++ b/src/drawline.c @@ -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. diff --git a/src/errors.h b/src/errors.h index 081402e274..53e5b1dda6 100644 --- a/src/errors.h +++ b/src/errors.h @@ -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")); diff --git a/src/ex_cmds.c b/src/ex_cmds.c index 4382ea5b70..efb0c66dc0 100644 --- a/src/ex_cmds.c +++ b/src/ex_cmds.c @@ -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); diff --git a/src/memline.c b/src/memline.c index 886f08f973..c15946a6eb 100644 --- a/src/memline.c +++ b/src/memline.c @@ -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 diff --git a/src/ops.c b/src/ops.c index 6ea50c912e..715897331e 100644 --- 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) diff --git a/src/popupwin.c b/src/popupwin.c index 4b69b61893..d5fdbb8d26 100644 --- a/src/popupwin.c +++ b/src/popupwin.c @@ -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 diff --git a/src/proto/textprop.pro b/src/proto/textprop.pro index 4b9a7a4491..d3ecf6d14c 100644 --- a/src/proto/textprop.pro +++ b/src/proto/textprop.pro @@ -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 : */ diff --git a/src/structs.h b/src/structs.h index 0e61aedef4..5bb51dd549 100644 --- a/src/structs.h +++ b/src/structs.h @@ -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) diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak index c0e4e68860..f8c7f8bb46 100644 --- a/src/testdir/Make_all.mak +++ b/src/testdir/Make_all.mak @@ -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 \ diff --git a/src/testdir/dumps/Test_prop_with_text_after_nowrap_2.dump b/src/testdir/dumps/Test_prop_with_text_after_nowrap_2.dump index b981380606..1d5c534834 100644 --- a/src/testdir/dumps/Test_prop_with_text_after_nowrap_2.dump +++ b/src/testdir/dumps/Test_prop_with_text_after_nowrap_2.dump @@ -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| diff --git a/src/testdir/dumps/Test_prop_with_text_after_nowrap_3.dump b/src/testdir/dumps/Test_prop_with_text_after_nowrap_3.dump index 5342f5ba16..ca849123a5 100644 --- a/src/testdir/dumps/Test_prop_with_text_after_nowrap_3.dump +++ b/src/testdir/dumps/Test_prop_with_text_after_nowrap_3.dump @@ -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| diff --git a/src/testdir/test_textprop.vim b/src/testdir/test_textprop.vim index 09b6e87f47..f94acfc97c 100644 --- a/src/testdir/test_textprop.vim +++ b/src/testdir/test_textprop.vim @@ -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\") + call term_sendkeys(buf, ":set signcolumn=yes foldcolumn=3 cursorline\\") 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 index 0000000000..193a808415 --- /dev/null +++ b/src/testdir/test_textprop2.vim @@ -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 diff --git a/src/textprop.c b/src/textprop.c index f0d418eee2..33165a8e43 100644 --- a/src/textprop.c +++ b/src/textprop.c @@ -15,6 +15,522 @@ #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 diff --git a/src/version.c b/src/version.c index 8e20e639c0..81519ea17b 100644 --- a/src/version.c +++ b/src/version.c @@ -734,6 +734,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 320, /**/ 319, /**/