xref: /vim-8.2.3635/src/textprop.c (revision ea2d8d25)
1 /* vi:set ts=8 sts=4 sw=4 noet:
2  *
3  * VIM - Vi IMproved	by Bram Moolenaar
4  *
5  * Do ":help uganda"  in Vim to read copying and usage conditions.
6  * Do ":help credits" in Vim to see a list of people who contributed.
7  * See README.txt for an overview of the Vim source code.
8  */
9 
10 /*
11  * Text properties implementation.  See ":help text-properties".
12  *
13  * TODO:
14  * - Adjust text property column and length when text is inserted/deleted.
15  *   -> a :substitute with a multi-line match
16  *   -> search for changed_bytes() from misc1.c
17  *   -> search for mark_col_adjust()
18  * - Perhaps we only need TP_FLAG_CONT_NEXT and can drop TP_FLAG_CONT_PREV?
19  * - Add an array for global_proptypes, to quickly lookup a prop type by ID
20  * - Add an array for b_proptypes, to quickly lookup a prop type by ID
21  * - Checking the text length to detect text properties is slow.  Use a flag in
22  *   the index, like DB_MARKED?
23  * - Also test line2byte() with many lines, so that ml_updatechunk() is taken
24  *   into account.
25  * - Perhaps have a window-local option to disable highlighting from text
26  *   properties?
27  */
28 
29 #include "vim.h"
30 
31 #if defined(FEAT_PROP_POPUP) || defined(PROTO)
32 
33 /*
34  * In a hashtable item "hi_key" points to "pt_name" in a proptype_T.
35  * This avoids adding a pointer to the hashtable item.
36  * PT2HIKEY() converts a proptype pointer to a hashitem key pointer.
37  * HIKEY2PT() converts a hashitem key pointer to a proptype pointer.
38  * HI2PT() converts a hashitem pointer to a proptype pointer.
39  */
40 #define PT2HIKEY(p)  ((p)->pt_name)
41 #define HIKEY2PT(p)   ((proptype_T *)((p) - offsetof(proptype_T, pt_name)))
42 #define HI2PT(hi)      HIKEY2PT((hi)->hi_key)
43 
44 // The global text property types.
45 static hashtab_T *global_proptypes = NULL;
46 
47 // The last used text property type ID.
48 static int proptype_id = 0;
49 
50 static char_u e_type_not_exist[] = N_("E971: Property type %s does not exist");
51 static char_u e_invalid_col[] = N_("E964: Invalid column number: %ld");
52 static char_u e_invalid_lnum[] = N_("E966: Invalid line number: %ld");
53 
54 /*
55  * Find a property type by name, return the hashitem.
56  * Returns NULL if the item can't be found.
57  */
58     static hashitem_T *
59 find_prop_hi(char_u *name, buf_T *buf)
60 {
61     hashtab_T	*ht;
62     hashitem_T	*hi;
63 
64     if (*name == NUL)
65 	return NULL;
66     if (buf == NULL)
67 	ht = global_proptypes;
68     else
69 	ht = buf->b_proptypes;
70 
71     if (ht == NULL)
72 	return NULL;
73     hi = hash_find(ht, name);
74     if (HASHITEM_EMPTY(hi))
75 	return NULL;
76     return hi;
77 }
78 
79 /*
80  * Like find_prop_hi() but return the property type.
81  */
82     static proptype_T *
83 find_prop(char_u *name, buf_T *buf)
84 {
85     hashitem_T	*hi = find_prop_hi(name, buf);
86 
87     if (hi == NULL)
88 	return NULL;
89     return HI2PT(hi);
90 }
91 
92 /*
93  * Get the prop type ID of "name".
94  * When not found return zero.
95  */
96     int
97 find_prop_type_id(char_u *name, buf_T *buf)
98 {
99     proptype_T *pt = find_prop(name, buf);
100 
101     if (pt == NULL)
102 	return 0;
103     return pt->pt_id;
104 }
105 
106 /*
107  * Lookup a property type by name.  First in "buf" and when not found in the
108  * global types.
109  * When not found gives an error message and returns NULL.
110  */
111     static proptype_T *
112 lookup_prop_type(char_u *name, buf_T *buf)
113 {
114     proptype_T *type = find_prop(name, buf);
115 
116     if (type == NULL)
117 	type = find_prop(name, NULL);
118     if (type == NULL)
119 	semsg(_(e_type_not_exist), name);
120     return type;
121 }
122 
123 /*
124  * Get an optional "bufnr" item from the dict in "arg".
125  * When the argument is not used or "bufnr" is not present then "buf" is
126  * unchanged.
127  * If "bufnr" is valid or not present return OK.
128  * When "arg" is not a dict or "bufnr" is invalid return FAIL.
129  */
130     static int
131 get_bufnr_from_arg(typval_T *arg, buf_T **buf)
132 {
133     dictitem_T	*di;
134 
135     if (arg->v_type != VAR_DICT)
136     {
137 	emsg(_(e_dictreq));
138 	return FAIL;
139     }
140     if (arg->vval.v_dict == NULL)
141 	return OK;  // NULL dict is like an empty dict
142     di = dict_find(arg->vval.v_dict, (char_u *)"bufnr", -1);
143     if (di != NULL)
144     {
145 	*buf = get_buf_arg(&di->di_tv);
146 	if (*buf == NULL)
147 	    return FAIL;
148     }
149     return OK;
150 }
151 
152 /*
153  * prop_add({lnum}, {col}, {props})
154  */
155     void
156 f_prop_add(typval_T *argvars, typval_T *rettv UNUSED)
157 {
158     linenr_T	start_lnum;
159     colnr_T	start_col;
160 
161     start_lnum = tv_get_number(&argvars[0]);
162     start_col = tv_get_number(&argvars[1]);
163     if (start_col < 1)
164     {
165 	semsg(_(e_invalid_col), (long)start_col);
166 	return;
167     }
168     if (argvars[2].v_type != VAR_DICT)
169     {
170 	emsg(_(e_dictreq));
171 	return;
172     }
173 
174     prop_add_common(start_lnum, start_col, argvars[2].vval.v_dict,
175 							  curbuf, &argvars[2]);
176 }
177 
178 /*
179  * Shared between prop_add() and popup_create().
180  * "dict_arg" is the function argument of a dict containing "bufnr".
181  * it is NULL for popup_create().
182  */
183     void
184 prop_add_common(
185 	linenr_T    start_lnum,
186 	colnr_T	    start_col,
187 	dict_T	    *dict,
188 	buf_T	    *default_buf,
189 	typval_T    *dict_arg)
190 {
191     linenr_T	lnum;
192     linenr_T	end_lnum;
193     colnr_T	end_col;
194     char_u	*type_name;
195     proptype_T	*type;
196     buf_T	*buf = default_buf;
197     int		id = 0;
198     char_u	*newtext;
199     int		proplen;
200     size_t	textlen;
201     char_u	*props = NULL;
202     char_u	*newprops;
203     textprop_T	tmp_prop;
204     int		i;
205 
206     if (dict == NULL || dict_find(dict, (char_u *)"type", -1) == NULL)
207     {
208 	emsg(_("E965: missing property type name"));
209 	return;
210     }
211     type_name = dict_get_string(dict, (char_u *)"type", FALSE);
212 
213     if (dict_find(dict, (char_u *)"end_lnum", -1) != NULL)
214     {
215 	end_lnum = dict_get_number(dict, (char_u *)"end_lnum");
216 	if (end_lnum < start_lnum)
217 	{
218 	    semsg(_(e_invargval), "end_lnum");
219 	    return;
220 	}
221     }
222     else
223 	end_lnum = start_lnum;
224 
225     if (dict_find(dict, (char_u *)"length", -1) != NULL)
226     {
227 	long length = dict_get_number(dict, (char_u *)"length");
228 
229 	if (length < 0 || end_lnum > start_lnum)
230 	{
231 	    semsg(_(e_invargval), "length");
232 	    return;
233 	}
234 	end_col = start_col + length;
235     }
236     else if (dict_find(dict, (char_u *)"end_col", -1) != NULL)
237     {
238 	end_col = dict_get_number(dict, (char_u *)"end_col");
239 	if (end_col <= 0)
240 	{
241 	    semsg(_(e_invargval), "end_col");
242 	    return;
243 	}
244     }
245     else if (start_lnum == end_lnum)
246 	end_col = start_col;
247     else
248 	end_col = 1;
249 
250     if (dict_find(dict, (char_u *)"id", -1) != NULL)
251 	id = dict_get_number(dict, (char_u *)"id");
252 
253     if (dict_arg != NULL && get_bufnr_from_arg(dict_arg, &buf) == FAIL)
254 	return;
255 
256     type = lookup_prop_type(type_name, buf);
257     if (type == NULL)
258 	return;
259 
260     if (start_lnum < 1 || start_lnum > buf->b_ml.ml_line_count)
261     {
262 	semsg(_(e_invalid_lnum), (long)start_lnum);
263 	return;
264     }
265     if (end_lnum < start_lnum || end_lnum > buf->b_ml.ml_line_count)
266     {
267 	semsg(_(e_invalid_lnum), (long)end_lnum);
268 	return;
269     }
270 
271     if (buf->b_ml.ml_mfp == NULL)
272     {
273 	emsg(_("E275: Cannot add text property to unloaded buffer"));
274 	return;
275     }
276 
277     for (lnum = start_lnum; lnum <= end_lnum; ++lnum)
278     {
279 	colnr_T col;	// start column
280 	long	length;	// in bytes
281 
282 	// Fetch the line to get the ml_line_len field updated.
283 	proplen = get_text_props(buf, lnum, &props, TRUE);
284 	textlen = buf->b_ml.ml_line_len - proplen * sizeof(textprop_T);
285 
286 	if (lnum == start_lnum)
287 	    col = start_col;
288 	else
289 	    col = 1;
290 	if (col - 1 > (colnr_T)textlen)
291 	{
292 	    semsg(_(e_invalid_col), (long)start_col);
293 	    return;
294 	}
295 
296 	if (lnum == end_lnum)
297 	    length = end_col - col;
298 	else
299 	    length = (int)textlen - col + 1;
300 	if (length > (long)textlen)
301 	    length = (int)textlen;	// can include the end-of-line
302 	if (length < 0)
303 	    length = 0;		// zero-width property
304 
305 	// Allocate the new line with space for the new proprety.
306 	newtext = alloc(buf->b_ml.ml_line_len + sizeof(textprop_T));
307 	if (newtext == NULL)
308 	    return;
309 	// Copy the text, including terminating NUL.
310 	mch_memmove(newtext, buf->b_ml.ml_line_ptr, textlen);
311 
312 	// Find the index where to insert the new property.
313 	// Since the text properties are not aligned properly when stored with
314 	// the text, we need to copy them as bytes before using it as a struct.
315 	for (i = 0; i < proplen; ++i)
316 	{
317 	    mch_memmove(&tmp_prop, props + i * sizeof(textprop_T),
318 							   sizeof(textprop_T));
319 	    if (tmp_prop.tp_col >= col)
320 		break;
321 	}
322 	newprops = newtext + textlen;
323 	if (i > 0)
324 	    mch_memmove(newprops, props, sizeof(textprop_T) * i);
325 
326 	tmp_prop.tp_col = col;
327 	tmp_prop.tp_len = length;
328 	tmp_prop.tp_id = id;
329 	tmp_prop.tp_type = type->pt_id;
330 	tmp_prop.tp_flags = (lnum > start_lnum ? TP_FLAG_CONT_PREV : 0)
331 			  | (lnum < end_lnum ? TP_FLAG_CONT_NEXT : 0);
332 	mch_memmove(newprops + i * sizeof(textprop_T), &tmp_prop,
333 							   sizeof(textprop_T));
334 
335 	if (i < proplen)
336 	    mch_memmove(newprops + (i + 1) * sizeof(textprop_T),
337 					    props + i * sizeof(textprop_T),
338 					    sizeof(textprop_T) * (proplen - i));
339 
340 	if (buf->b_ml.ml_flags & ML_LINE_DIRTY)
341 	    vim_free(buf->b_ml.ml_line_ptr);
342 	buf->b_ml.ml_line_ptr = newtext;
343 	buf->b_ml.ml_line_len += sizeof(textprop_T);
344 	buf->b_ml.ml_flags |= ML_LINE_DIRTY;
345     }
346 
347     buf->b_has_textprop = TRUE;  // this is never reset
348     redraw_buf_later(buf, NOT_VALID);
349 }
350 
351 /*
352  * Fetch the text properties for line "lnum" in buffer "buf".
353  * Returns the number of text properties and, when non-zero, a pointer to the
354  * first one in "props" (note that it is not aligned, therefore the char_u
355  * pointer).
356  */
357     int
358 get_text_props(buf_T *buf, linenr_T lnum, char_u **props, int will_change)
359 {
360     char_u *text;
361     size_t textlen;
362     size_t proplen;
363 
364     // Be quick when no text property types have been defined or the buffer,
365     // unless we are adding one.
366     if ((!buf->b_has_textprop && !will_change) || buf->b_ml.ml_mfp == NULL)
367 	return 0;
368 
369     // Fetch the line to get the ml_line_len field updated.
370     text = ml_get_buf(buf, lnum, will_change);
371     textlen = STRLEN(text) + 1;
372     proplen = buf->b_ml.ml_line_len - textlen;
373     if (proplen % sizeof(textprop_T) != 0)
374     {
375 	iemsg(_("E967: text property info corrupted"));
376 	return 0;
377     }
378     if (proplen > 0)
379 	*props = text + textlen;
380     return (int)(proplen / sizeof(textprop_T));
381 }
382 
383 /**
384  * Return the number of text properties on line "lnum" in the current buffer.
385  * When "only_starting" is true only text properties starting in this line will
386  * be considered.
387  */
388     int
389 count_props(linenr_T lnum, int only_starting)
390 {
391     char_u	*props;
392     int		proplen = get_text_props(curbuf, lnum, &props, 0);
393     int		result = proplen;
394     int		i;
395     textprop_T	prop;
396 
397     if (only_starting)
398 	for (i = 0; i < proplen; ++i)
399 	{
400 	    mch_memmove(&prop, props + i * sizeof(prop), sizeof(prop));
401 	    if (prop.tp_flags & TP_FLAG_CONT_PREV)
402 		--result;
403 	}
404     return result;
405 }
406 
407 /*
408  * Find text property "type_id" in the visible lines of window "wp".
409  * Match "id" when it is > 0.
410  * Returns FAIL when not found.
411  */
412     int
413 find_visible_prop(win_T *wp, int type_id, int id, textprop_T *prop,
414 							  linenr_T *found_lnum)
415 {
416     linenr_T		lnum;
417     char_u		*props;
418     int			count;
419     int			i;
420 
421     // w_botline may not have been updated yet.
422     validate_botline();
423     for (lnum = wp->w_topline; lnum < wp->w_botline; ++lnum)
424     {
425 	count = get_text_props(wp->w_buffer, lnum, &props, FALSE);
426 	for (i = 0; i < count; ++i)
427 	{
428 	    mch_memmove(prop, props + i * sizeof(textprop_T),
429 							   sizeof(textprop_T));
430 	    if (prop->tp_type == type_id && (id <= 0 || prop->tp_id == id))
431 	    {
432 		*found_lnum = lnum;
433 		return OK;
434 	    }
435 	}
436     }
437     return FAIL;
438 }
439 
440 /*
441  * Set the text properties for line "lnum" to "props" with length "len".
442  * If "len" is zero text properties are removed, "props" is not used.
443  * Any existing text properties are dropped.
444  * Only works for the current buffer.
445  */
446     static void
447 set_text_props(linenr_T lnum, char_u *props, int len)
448 {
449     char_u  *text;
450     char_u  *newtext;
451     int	    textlen;
452 
453     text = ml_get(lnum);
454     textlen = (int)STRLEN(text) + 1;
455     newtext = alloc(textlen + len);
456     if (newtext == NULL)
457 	return;
458     mch_memmove(newtext, text, textlen);
459     if (len > 0)
460 	mch_memmove(newtext + textlen, props, len);
461     if (curbuf->b_ml.ml_flags & ML_LINE_DIRTY)
462 	vim_free(curbuf->b_ml.ml_line_ptr);
463     curbuf->b_ml.ml_line_ptr = newtext;
464     curbuf->b_ml.ml_line_len = textlen + len;
465     curbuf->b_ml.ml_flags |= ML_LINE_DIRTY;
466 }
467 
468     static proptype_T *
469 find_type_by_id(hashtab_T *ht, int id)
470 {
471     long	todo;
472     hashitem_T	*hi;
473 
474     if (ht == NULL)
475 	return NULL;
476 
477     // TODO: Make this faster by keeping a list of types sorted on ID and use
478     // a binary search.
479 
480     todo = (long)ht->ht_used;
481     for (hi = ht->ht_array; todo > 0; ++hi)
482     {
483 	if (!HASHITEM_EMPTY(hi))
484 	{
485 	    proptype_T *prop = HI2PT(hi);
486 
487 	    if (prop->pt_id == id)
488 		return prop;
489 	    --todo;
490 	}
491     }
492     return NULL;
493 }
494 
495 /*
496  * Fill 'dict' with text properties in 'prop'.
497  */
498     static void
499 prop_fill_dict(dict_T *dict, textprop_T *prop, buf_T *buf)
500 {
501     proptype_T *pt;
502 
503     dict_add_number(dict, "col", prop->tp_col);
504     dict_add_number(dict, "length", prop->tp_len);
505     dict_add_number(dict, "id", prop->tp_id);
506     dict_add_number(dict, "start", !(prop->tp_flags & TP_FLAG_CONT_PREV));
507     dict_add_number(dict, "end", !(prop->tp_flags & TP_FLAG_CONT_NEXT));
508     pt = text_prop_type_by_id(buf, prop->tp_type);
509     if (pt != NULL)
510 	dict_add_string(dict, "type", pt->pt_name);
511 }
512 
513 /*
514  * Find a property type by ID in "buf" or globally.
515  * Returns NULL if not found.
516  */
517     proptype_T *
518 text_prop_type_by_id(buf_T *buf, int id)
519 {
520     proptype_T *type;
521 
522     type = find_type_by_id(buf->b_proptypes, id);
523     if (type == NULL)
524 	type = find_type_by_id(global_proptypes, id);
525     return type;
526 }
527 
528 /*
529  * prop_clear({lnum} [, {lnum_end} [, {bufnr}]])
530  */
531     void
532 f_prop_clear(typval_T *argvars, typval_T *rettv UNUSED)
533 {
534     linenr_T start = tv_get_number(&argvars[0]);
535     linenr_T end = start;
536     linenr_T lnum;
537     buf_T    *buf = curbuf;
538 
539     if (argvars[1].v_type != VAR_UNKNOWN)
540     {
541 	end = tv_get_number(&argvars[1]);
542 	if (argvars[2].v_type != VAR_UNKNOWN)
543 	{
544 	    if (get_bufnr_from_arg(&argvars[2], &buf) == FAIL)
545 		return;
546 	}
547     }
548     if (start < 1 || end < 1)
549     {
550 	emsg(_(e_invrange));
551 	return;
552     }
553 
554     for (lnum = start; lnum <= end; ++lnum)
555     {
556 	char_u *text;
557 	size_t len;
558 
559 	if (lnum > buf->b_ml.ml_line_count)
560 	    break;
561 	text = ml_get_buf(buf, lnum, FALSE);
562 	len = STRLEN(text) + 1;
563 	if ((size_t)buf->b_ml.ml_line_len > len)
564 	{
565 	    if (!(buf->b_ml.ml_flags & ML_LINE_DIRTY))
566 	    {
567 		char_u *newtext = vim_strsave(text);
568 
569 		// need to allocate the line now
570 		if (newtext == NULL)
571 		    return;
572 		buf->b_ml.ml_line_ptr = newtext;
573 		buf->b_ml.ml_flags |= ML_LINE_DIRTY;
574 	    }
575 	    buf->b_ml.ml_line_len = (int)len;
576 	}
577     }
578     redraw_buf_later(buf, NOT_VALID);
579 }
580 
581 /*
582  * prop_find({props} [, {direction}])
583  */
584     void
585 f_prop_find(typval_T *argvars, typval_T *rettv)
586 {
587     pos_T       *cursor = &curwin->w_cursor;
588     dict_T      *dict;
589     buf_T       *buf = curbuf;
590     dictitem_T  *di;
591     int		lnum_start;
592     int		start_pos_has_prop = 0;
593     int		seen_end = 0;
594     int		id = -1;
595     int		type_id = -1;
596     int		skipstart = 0;
597     int		lnum = -1;
598     int		col = -1;
599     int		dir = 1;    // 1 = forward, -1 = backward
600 
601     if (argvars[0].v_type != VAR_DICT || argvars[0].vval.v_dict == NULL)
602     {
603 	emsg(_(e_invarg));
604 	return;
605     }
606     dict = argvars[0].vval.v_dict;
607 
608     if (get_bufnr_from_arg(&argvars[0], &buf) == FAIL)
609 	return;
610     if (buf->b_ml.ml_mfp == NULL)
611 	return;
612 
613     if (argvars[1].v_type != VAR_UNKNOWN)
614     {
615 	char_u      *dir_s = tv_get_string(&argvars[1]);
616 
617 	if (*dir_s == 'b')
618 	    dir = -1;
619 	else if (*dir_s != 'f')
620 	{
621 	    emsg(_(e_invarg));
622 	    return;
623 	}
624     }
625 
626     di = dict_find(dict, (char_u *)"lnum", -1);
627     if (di != NULL)
628 	lnum = tv_get_number(&di->di_tv);
629 
630     di = dict_find(dict, (char_u *)"col", -1);
631     if (di != NULL)
632 	col = tv_get_number(&di->di_tv);
633 
634     if (lnum == -1)
635     {
636 	lnum = cursor->lnum;
637 	col = cursor->col + 1;
638     }
639     else if (col == -1)
640 	col = 1;
641 
642     if (lnum < 1 || lnum > buf->b_ml.ml_line_count)
643     {
644 	emsg(_(e_invrange));
645 	return;
646     }
647 
648     di = dict_find(dict, (char_u *)"skipstart", -1);
649     if (di != NULL)
650 	skipstart = tv_get_number(&di->di_tv);
651 
652     if (dict_find(dict, (char_u *)"id", -1) != NULL)
653 	id = dict_get_number(dict, (char_u *)"id");
654     if (dict_find(dict, (char_u *)"type", -1))
655     {
656 	char_u	    *name = dict_get_string(dict, (char_u *)"type", FALSE);
657 	proptype_T  *type = lookup_prop_type(name, buf);
658 
659 	if (type == NULL)
660 	    return;
661 	type_id = type->pt_id;
662     }
663     if (id == -1 && type_id == -1)
664     {
665 	emsg(_("E968: Need at least one of 'id' or 'type'"));
666 	return;
667     }
668 
669     lnum_start = lnum;
670 
671     if (rettv_dict_alloc(rettv) == FAIL)
672 	return;
673 
674     while (1)
675     {
676 	char_u	*text = ml_get_buf(buf, lnum, FALSE);
677 	size_t	textlen = STRLEN(text) + 1;
678 	int	count = (int)((buf->b_ml.ml_line_len - textlen)
679 							 / sizeof(textprop_T));
680 	int	    i;
681 	textprop_T  prop;
682 	int	    prop_start;
683 	int	    prop_end;
684 
685 	for (i = 0; i < count; ++i)
686 	{
687 	    mch_memmove(&prop, text + textlen + i * sizeof(textprop_T),
688 			    sizeof(textprop_T));
689 
690 	    if (lnum == lnum_start)
691 	    {
692 		if (dir < 0)
693 		{
694 		    if (col < prop.tp_col)
695 			break;
696 		}
697 		else if (prop.tp_col + prop.tp_len - (prop.tp_len != 0) < col)
698 		    continue;
699 	    }
700 	    if (prop.tp_id == id || prop.tp_type == type_id)
701 	    {
702 		// Check if the starting position has text props.
703 		if (lnum_start == lnum
704 			&& col >= prop.tp_col
705 			&& (col <= prop.tp_col + prop.tp_len
706 							 - (prop.tp_len != 0)))
707 		    start_pos_has_prop = 1;
708 
709 		prop_start = !(prop.tp_flags & TP_FLAG_CONT_PREV);
710 		prop_end = !(prop.tp_flags & TP_FLAG_CONT_NEXT);
711 		if (!prop_start && prop_end && dir > 0)
712 		    seen_end = 1;
713 
714 		// Skip lines without the start flag.
715 		if (!prop_start)
716 		{
717 		    // Always search backwards for start when search started
718 		    // on a prop and we're not skipping.
719 		    if (start_pos_has_prop && !skipstart)
720 			dir = -1;
721 		    break;
722 		}
723 
724 		// If skipstart is true, skip the prop at start pos (even if
725 		// continued from another line).
726 		if (start_pos_has_prop && skipstart && !seen_end)
727 		{
728 		    start_pos_has_prop = 0;
729 		    break;
730 		}
731 
732 		prop_fill_dict(rettv->vval.v_dict, &prop, buf);
733 		dict_add_number(rettv->vval.v_dict, "lnum", lnum);
734 
735 		return;
736 	    }
737 	}
738 
739 	if (dir > 0)
740 	{
741 	    if (lnum >= buf->b_ml.ml_line_count)
742 		break;
743 	    lnum++;
744 	}
745 	else
746 	{
747 	    if (lnum <= 1)
748 		break;
749 	    lnum--;
750 	}
751 	// Adjust col to indicate that we're continuing from prev/next line.
752 	col = dir < 0 ? buf->b_ml.ml_line_len : 1;
753     }
754 }
755 
756 /*
757  * prop_list({lnum} [, {bufnr}])
758  */
759     void
760 f_prop_list(typval_T *argvars, typval_T *rettv)
761 {
762     linenr_T lnum = tv_get_number(&argvars[0]);
763     buf_T    *buf = curbuf;
764 
765     if (argvars[1].v_type != VAR_UNKNOWN)
766     {
767 	if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL)
768 	    return;
769     }
770     if (lnum < 1 || lnum > buf->b_ml.ml_line_count)
771     {
772 	emsg(_(e_invrange));
773 	return;
774     }
775 
776     if (rettv_list_alloc(rettv) == OK)
777     {
778 	char_u	    *text = ml_get_buf(buf, lnum, FALSE);
779 	size_t	    textlen = STRLEN(text) + 1;
780 	int	    count = (int)((buf->b_ml.ml_line_len - textlen)
781 							 / sizeof(textprop_T));
782 	int	    i;
783 	textprop_T  prop;
784 
785 	for (i = 0; i < count; ++i)
786 	{
787 	    dict_T *d = dict_alloc();
788 
789 	    if (d == NULL)
790 		break;
791 	    mch_memmove(&prop, text + textlen + i * sizeof(textprop_T),
792 							   sizeof(textprop_T));
793 	    prop_fill_dict(d, &prop, buf);
794 	    list_append_dict(rettv->vval.v_list, d);
795 	}
796     }
797 }
798 
799 /*
800  * prop_remove({props} [, {lnum} [, {lnum_end}]])
801  */
802     void
803 f_prop_remove(typval_T *argvars, typval_T *rettv)
804 {
805     linenr_T	start = 1;
806     linenr_T	end = 0;
807     linenr_T	lnum;
808     dict_T	*dict;
809     buf_T	*buf = curbuf;
810     dictitem_T	*di;
811     int		do_all = FALSE;
812     int		id = -1;
813     int		type_id = -1;
814     int		both = FALSE;
815 
816     rettv->vval.v_number = 0;
817     if (argvars[0].v_type != VAR_DICT || argvars[0].vval.v_dict == NULL)
818     {
819 	emsg(_(e_invarg));
820 	return;
821     }
822 
823     if (argvars[1].v_type != VAR_UNKNOWN)
824     {
825 	start = tv_get_number(&argvars[1]);
826 	end = start;
827 	if (argvars[2].v_type != VAR_UNKNOWN)
828 	    end = tv_get_number(&argvars[2]);
829 	if (start < 1 || end < 1)
830 	{
831 	    emsg(_(e_invrange));
832 	    return;
833 	}
834     }
835 
836     dict = argvars[0].vval.v_dict;
837     if (get_bufnr_from_arg(&argvars[0], &buf) == FAIL)
838 	return;
839     if (buf->b_ml.ml_mfp == NULL)
840 	return;
841 
842     di = dict_find(dict, (char_u*)"all", -1);
843     if (di != NULL)
844 	do_all = dict_get_number(dict, (char_u *)"all");
845 
846     if (dict_find(dict, (char_u *)"id", -1) != NULL)
847 	id = dict_get_number(dict, (char_u *)"id");
848     if (dict_find(dict, (char_u *)"type", -1))
849     {
850 	char_u	    *name = dict_get_string(dict, (char_u *)"type", FALSE);
851 	proptype_T  *type = lookup_prop_type(name, buf);
852 
853 	if (type == NULL)
854 	    return;
855 	type_id = type->pt_id;
856     }
857     if (dict_find(dict, (char_u *)"both", -1) != NULL)
858 	both = dict_get_number(dict, (char_u *)"both");
859     if (id == -1 && type_id == -1)
860     {
861 	emsg(_("E968: Need at least one of 'id' or 'type'"));
862 	return;
863     }
864     if (both && (id == -1 || type_id == -1))
865     {
866 	emsg(_("E860: Need 'id' and 'type' with 'both'"));
867 	return;
868     }
869 
870     if (end == 0)
871 	end = buf->b_ml.ml_line_count;
872     for (lnum = start; lnum <= end; ++lnum)
873     {
874 	char_u *text;
875 	size_t len;
876 
877 	if (lnum > buf->b_ml.ml_line_count)
878 	    break;
879 	text = ml_get_buf(buf, lnum, FALSE);
880 	len = STRLEN(text) + 1;
881 	if ((size_t)buf->b_ml.ml_line_len > len)
882 	{
883 	    static textprop_T	textprop;  // static because of alignment
884 	    unsigned		idx;
885 
886 	    for (idx = 0; idx < (buf->b_ml.ml_line_len - len)
887 						   / sizeof(textprop_T); ++idx)
888 	    {
889 		char_u *cur_prop = buf->b_ml.ml_line_ptr + len
890 						    + idx * sizeof(textprop_T);
891 		size_t	taillen;
892 
893 		mch_memmove(&textprop, cur_prop, sizeof(textprop_T));
894 		if (both ? textprop.tp_id == id && textprop.tp_type == type_id
895 			 : textprop.tp_id == id || textprop.tp_type == type_id)
896 		{
897 		    if (!(buf->b_ml.ml_flags & ML_LINE_DIRTY))
898 		    {
899 			char_u *newptr = alloc(buf->b_ml.ml_line_len);
900 
901 			// need to allocate the line to be able to change it
902 			if (newptr == NULL)
903 			    return;
904 			mch_memmove(newptr, buf->b_ml.ml_line_ptr,
905 							buf->b_ml.ml_line_len);
906 			buf->b_ml.ml_line_ptr = newptr;
907 			buf->b_ml.ml_flags |= ML_LINE_DIRTY;
908 
909 			cur_prop = buf->b_ml.ml_line_ptr + len
910 						    + idx * sizeof(textprop_T);
911 		    }
912 
913 		    taillen = buf->b_ml.ml_line_len - len
914 					      - (idx + 1) * sizeof(textprop_T);
915 		    if (taillen > 0)
916 			mch_memmove(cur_prop, cur_prop + sizeof(textprop_T),
917 								      taillen);
918 		    buf->b_ml.ml_line_len -= sizeof(textprop_T);
919 		    --idx;
920 
921 		    ++rettv->vval.v_number;
922 		    if (!do_all)
923 			break;
924 		}
925 	    }
926 	}
927     }
928     redraw_buf_later(buf, NOT_VALID);
929 }
930 
931 /*
932  * Common for f_prop_type_add() and f_prop_type_change().
933  */
934     static void
935 prop_type_set(typval_T *argvars, int add)
936 {
937     char_u	*name;
938     buf_T	*buf = NULL;
939     dict_T	*dict;
940     dictitem_T  *di;
941     proptype_T	*prop;
942 
943     name = tv_get_string(&argvars[0]);
944     if (*name == NUL)
945     {
946 	emsg(_(e_invarg));
947 	return;
948     }
949 
950     if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL)
951 	return;
952     dict = argvars[1].vval.v_dict;
953 
954     prop = find_prop(name, buf);
955     if (add)
956     {
957 	hashtab_T **htp;
958 
959 	if (prop != NULL)
960 	{
961 	    semsg(_("E969: Property type %s already defined"), name);
962 	    return;
963 	}
964 	prop = alloc_clear(offsetof(proptype_T, pt_name) + STRLEN(name) + 1);
965 	if (prop == NULL)
966 	    return;
967 	STRCPY(prop->pt_name, name);
968 	prop->pt_id = ++proptype_id;
969 	prop->pt_flags = PT_FLAG_COMBINE;
970 	htp = buf == NULL ? &global_proptypes : &buf->b_proptypes;
971 	if (*htp == NULL)
972 	{
973 	    *htp = ALLOC_ONE(hashtab_T);
974 	    if (*htp == NULL)
975 	    {
976 		vim_free(prop);
977 		return;
978 	    }
979 	    hash_init(*htp);
980 	}
981 	hash_add(*htp, PT2HIKEY(prop));
982     }
983     else
984     {
985 	if (prop == NULL)
986 	{
987 	    semsg(_(e_type_not_exist), name);
988 	    return;
989 	}
990     }
991 
992     if (dict != NULL)
993     {
994 	di = dict_find(dict, (char_u *)"highlight", -1);
995 	if (di != NULL)
996 	{
997 	    char_u	*highlight;
998 	    int		hl_id = 0;
999 
1000 	    highlight = dict_get_string(dict, (char_u *)"highlight", FALSE);
1001 	    if (highlight != NULL && *highlight != NUL)
1002 		hl_id = syn_name2id(highlight);
1003 	    if (hl_id <= 0)
1004 	    {
1005 		semsg(_("E970: Unknown highlight group name: '%s'"),
1006 			highlight == NULL ? (char_u *)"" : highlight);
1007 		return;
1008 	    }
1009 	    prop->pt_hl_id = hl_id;
1010 	}
1011 
1012 	di = dict_find(dict, (char_u *)"combine", -1);
1013 	if (di != NULL)
1014 	{
1015 	    if (tv_get_number(&di->di_tv))
1016 		prop->pt_flags |= PT_FLAG_COMBINE;
1017 	    else
1018 		prop->pt_flags &= ~PT_FLAG_COMBINE;
1019 	}
1020 
1021 	di = dict_find(dict, (char_u *)"priority", -1);
1022 	if (di != NULL)
1023 	    prop->pt_priority = tv_get_number(&di->di_tv);
1024 
1025 	di = dict_find(dict, (char_u *)"start_incl", -1);
1026 	if (di != NULL)
1027 	{
1028 	    if (tv_get_number(&di->di_tv))
1029 		prop->pt_flags |= PT_FLAG_INS_START_INCL;
1030 	    else
1031 		prop->pt_flags &= ~PT_FLAG_INS_START_INCL;
1032 	}
1033 
1034 	di = dict_find(dict, (char_u *)"end_incl", -1);
1035 	if (di != NULL)
1036 	{
1037 	    if (tv_get_number(&di->di_tv))
1038 		prop->pt_flags |= PT_FLAG_INS_END_INCL;
1039 	    else
1040 		prop->pt_flags &= ~PT_FLAG_INS_END_INCL;
1041 	}
1042     }
1043 }
1044 
1045 /*
1046  * prop_type_add({name}, {props})
1047  */
1048     void
1049 f_prop_type_add(typval_T *argvars, typval_T *rettv UNUSED)
1050 {
1051     prop_type_set(argvars, TRUE);
1052 }
1053 
1054 /*
1055  * prop_type_change({name}, {props})
1056  */
1057     void
1058 f_prop_type_change(typval_T *argvars, typval_T *rettv UNUSED)
1059 {
1060     prop_type_set(argvars, FALSE);
1061 }
1062 
1063 /*
1064  * prop_type_delete({name} [, {bufnr}])
1065  */
1066     void
1067 f_prop_type_delete(typval_T *argvars, typval_T *rettv UNUSED)
1068 {
1069     char_u	*name;
1070     buf_T	*buf = NULL;
1071     hashitem_T	*hi;
1072 
1073     name = tv_get_string(&argvars[0]);
1074     if (*name == NUL)
1075     {
1076 	emsg(_(e_invarg));
1077 	return;
1078     }
1079 
1080     if (argvars[1].v_type != VAR_UNKNOWN)
1081     {
1082 	if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL)
1083 	    return;
1084     }
1085 
1086     hi = find_prop_hi(name, buf);
1087     if (hi != NULL)
1088     {
1089 	hashtab_T	*ht;
1090 	proptype_T	*prop = HI2PT(hi);
1091 
1092 	if (buf == NULL)
1093 	    ht = global_proptypes;
1094 	else
1095 	    ht = buf->b_proptypes;
1096 	hash_remove(ht, hi);
1097 	vim_free(prop);
1098     }
1099 }
1100 
1101 /*
1102  * prop_type_get({name} [, {bufnr}])
1103  */
1104     void
1105 f_prop_type_get(typval_T *argvars, typval_T *rettv)
1106 {
1107     char_u *name = tv_get_string(&argvars[0]);
1108 
1109     if (*name == NUL)
1110     {
1111 	emsg(_(e_invarg));
1112 	return;
1113     }
1114     if (rettv_dict_alloc(rettv) == OK)
1115     {
1116 	proptype_T  *prop = NULL;
1117 	buf_T	    *buf = NULL;
1118 
1119 	if (argvars[1].v_type != VAR_UNKNOWN)
1120 	{
1121 	    if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL)
1122 		return;
1123 	}
1124 
1125 	prop = find_prop(name, buf);
1126 	if (prop != NULL)
1127 	{
1128 	    dict_T *d = rettv->vval.v_dict;
1129 
1130 	    if (prop->pt_hl_id > 0)
1131 		dict_add_string(d, "highlight", syn_id2name(prop->pt_hl_id));
1132 	    dict_add_number(d, "priority", prop->pt_priority);
1133 	    dict_add_number(d, "combine",
1134 				   (prop->pt_flags & PT_FLAG_COMBINE) ? 1 : 0);
1135 	    dict_add_number(d, "start_incl",
1136 			    (prop->pt_flags & PT_FLAG_INS_START_INCL) ? 1 : 0);
1137 	    dict_add_number(d, "end_incl",
1138 			      (prop->pt_flags & PT_FLAG_INS_END_INCL) ? 1 : 0);
1139 	    if (buf != NULL)
1140 		dict_add_number(d, "bufnr", buf->b_fnum);
1141 	}
1142     }
1143 }
1144 
1145     static void
1146 list_types(hashtab_T *ht, list_T *l)
1147 {
1148     long	todo;
1149     hashitem_T	*hi;
1150 
1151     todo = (long)ht->ht_used;
1152     for (hi = ht->ht_array; todo > 0; ++hi)
1153     {
1154 	if (!HASHITEM_EMPTY(hi))
1155 	{
1156 	    proptype_T *prop = HI2PT(hi);
1157 
1158 	    list_append_string(l, prop->pt_name, -1);
1159 	    --todo;
1160 	}
1161     }
1162 }
1163 
1164 /*
1165  * prop_type_list([{bufnr}])
1166  */
1167     void
1168 f_prop_type_list(typval_T *argvars, typval_T *rettv UNUSED)
1169 {
1170     buf_T *buf = NULL;
1171 
1172     if (rettv_list_alloc(rettv) == OK)
1173     {
1174 	if (argvars[0].v_type != VAR_UNKNOWN)
1175 	{
1176 	    if (get_bufnr_from_arg(&argvars[0], &buf) == FAIL)
1177 		return;
1178 	}
1179 	if (buf == NULL)
1180 	{
1181 	    if (global_proptypes != NULL)
1182 		list_types(global_proptypes, rettv->vval.v_list);
1183 	}
1184 	else if (buf->b_proptypes != NULL)
1185 	    list_types(buf->b_proptypes, rettv->vval.v_list);
1186     }
1187 }
1188 
1189 /*
1190  * Free all property types in "ht".
1191  */
1192     static void
1193 clear_ht_prop_types(hashtab_T *ht)
1194 {
1195     long	todo;
1196     hashitem_T	*hi;
1197 
1198     if (ht == NULL)
1199 	return;
1200 
1201     todo = (long)ht->ht_used;
1202     for (hi = ht->ht_array; todo > 0; ++hi)
1203     {
1204 	if (!HASHITEM_EMPTY(hi))
1205 	{
1206 	    proptype_T *prop = HI2PT(hi);
1207 
1208 	    vim_free(prop);
1209 	    --todo;
1210 	}
1211     }
1212 
1213     hash_clear(ht);
1214     vim_free(ht);
1215 }
1216 
1217 #if defined(EXITFREE) || defined(PROTO)
1218 /*
1219  * Free all global property types.
1220  */
1221     void
1222 clear_global_prop_types(void)
1223 {
1224     clear_ht_prop_types(global_proptypes);
1225     global_proptypes = NULL;
1226 }
1227 #endif
1228 
1229 /*
1230  * Free all property types for "buf".
1231  */
1232     void
1233 clear_buf_prop_types(buf_T *buf)
1234 {
1235     clear_ht_prop_types(buf->b_proptypes);
1236     buf->b_proptypes = NULL;
1237 }
1238 
1239 // Struct used to return two values from adjust_prop().
1240 typedef struct
1241 {
1242     int dirty;	    // if the property was changed
1243     int can_drop;   // whether after this change, the prop may be removed
1244 } adjustres_T;
1245 
1246 /*
1247  * Adjust the property for "added" bytes (can be negative) inserted at "col".
1248  *
1249  * Note that "col" is zero-based, while tp_col is one-based.
1250  * Only for the current buffer.
1251  * "flags" can have:
1252  * APC_SUBSTITUTE:	Text is replaced, not inserted.
1253  */
1254     static adjustres_T
1255 adjust_prop(
1256 	textprop_T *prop,
1257 	colnr_T col,
1258 	int added,
1259 	int flags)
1260 {
1261     proptype_T	*pt = text_prop_type_by_id(curbuf, prop->tp_type);
1262     int		start_incl = (pt != NULL
1263 				    && (pt->pt_flags & PT_FLAG_INS_START_INCL))
1264 						   || (flags & APC_SUBSTITUTE);
1265     int		end_incl = (pt != NULL
1266 				     && (pt->pt_flags & PT_FLAG_INS_END_INCL));
1267 		// Do not drop zero-width props if they later can increase in
1268 		// size.
1269     int		droppable = !(start_incl || end_incl);
1270     adjustres_T res = {TRUE, FALSE};
1271 
1272     if (added > 0)
1273     {
1274 	if (col + 1 <= prop->tp_col
1275 		- (start_incl || (prop->tp_len == 0 && end_incl)))
1276 	    // Change is entirely before the text property: Only shift
1277 	    prop->tp_col += added;
1278 	else if (col + 1 < prop->tp_col + prop->tp_len + end_incl)
1279 	    // Insertion was inside text property
1280 	    prop->tp_len += added;
1281     }
1282     else if (prop->tp_col > col + 1)
1283     {
1284 	if (prop->tp_col + added < col + 1)
1285 	{
1286 	    prop->tp_len += (prop->tp_col - 1 - col) + added;
1287 	    prop->tp_col = col + 1;
1288 	    if (prop->tp_len <= 0)
1289 	    {
1290 		prop->tp_len = 0;
1291 		res.can_drop = droppable;
1292 	    }
1293 	}
1294 	else
1295 	    prop->tp_col += added;
1296     }
1297     else if (prop->tp_len > 0 && prop->tp_col + prop->tp_len > col)
1298     {
1299 	int after = col - added - (prop->tp_col - 1 + prop->tp_len);
1300 
1301 	prop->tp_len += after > 0 ? added + after : added;
1302 	res.can_drop = prop->tp_len <= 0 && droppable;
1303     }
1304     else
1305 	res.dirty = FALSE;
1306 
1307     return res;
1308 }
1309 
1310 /*
1311  * Adjust the columns of text properties in line "lnum" after position "col" to
1312  * shift by "bytes_added" (can be negative).
1313  * Note that "col" is zero-based, while tp_col is one-based.
1314  * Only for the current buffer.
1315  * "flags" can have:
1316  * APC_SAVE_FOR_UNDO:	Call u_savesub() before making changes to the line.
1317  * APC_SUBSTITUTE:	Text is replaced, not inserted.
1318  * Caller is expected to check b_has_textprop and "bytes_added" being non-zero.
1319  * Returns TRUE when props were changed.
1320  */
1321     int
1322 adjust_prop_columns(
1323 	linenr_T    lnum,
1324 	colnr_T	    col,
1325 	int	    bytes_added,
1326 	int	    flags)
1327 {
1328     int		proplen;
1329     char_u	*props;
1330     int		dirty = FALSE;
1331     int		ri, wi;
1332     size_t	textlen;
1333 
1334     if (text_prop_frozen > 0)
1335 	return FALSE;
1336 
1337     proplen = get_text_props(curbuf, lnum, &props, TRUE);
1338     if (proplen == 0)
1339 	return FALSE;
1340     textlen = curbuf->b_ml.ml_line_len - proplen * sizeof(textprop_T);
1341 
1342     wi = 0; // write index
1343     for (ri = 0; ri < proplen; ++ri)
1344     {
1345 	textprop_T	prop;
1346 	adjustres_T	res;
1347 
1348 	mch_memmove(&prop, props + ri * sizeof(prop), sizeof(prop));
1349 	res = adjust_prop(&prop, col, bytes_added, flags);
1350 	if (res.dirty)
1351 	{
1352 	    // Save for undo if requested and not done yet.
1353 	    if ((flags & APC_SAVE_FOR_UNDO) && !dirty
1354 						    && u_savesub(lnum) == FAIL)
1355 		return FALSE;
1356 	    dirty = TRUE;
1357 	}
1358 	if (res.can_drop)
1359 	    continue; // Drop this text property
1360 	mch_memmove(props + wi * sizeof(textprop_T), &prop, sizeof(textprop_T));
1361 	++wi;
1362     }
1363     if (dirty)
1364     {
1365 	colnr_T newlen = (int)textlen + wi * (colnr_T)sizeof(textprop_T);
1366 
1367 	if ((curbuf->b_ml.ml_flags & ML_LINE_DIRTY) == 0)
1368 	    curbuf->b_ml.ml_line_ptr =
1369 				 vim_memsave(curbuf->b_ml.ml_line_ptr, newlen);
1370 	curbuf->b_ml.ml_flags |= ML_LINE_DIRTY;
1371 	curbuf->b_ml.ml_line_len = newlen;
1372     }
1373     return dirty;
1374 }
1375 
1376 /*
1377  * Adjust text properties for a line that was split in two.
1378  * "lnum_props" is the line that has the properties from before the split.
1379  * "lnum_top" is the top line.
1380  * "kept" is the number of bytes kept in the first line, while
1381  * "deleted" is the number of bytes deleted.
1382  */
1383     void
1384 adjust_props_for_split(
1385 	linenr_T lnum_props,
1386 	linenr_T lnum_top,
1387 	int kept,
1388 	int deleted)
1389 {
1390     char_u	*props;
1391     int		count;
1392     garray_T    prevprop;
1393     garray_T    nextprop;
1394     int		i;
1395     int		skipped = kept + deleted;
1396 
1397     if (!curbuf->b_has_textprop)
1398 	return;
1399 
1400     // Get the text properties from "lnum_props".
1401     count = get_text_props(curbuf, lnum_props, &props, FALSE);
1402     ga_init2(&prevprop, sizeof(textprop_T), 10);
1403     ga_init2(&nextprop, sizeof(textprop_T), 10);
1404 
1405     // Keep the relevant ones in the first line, reducing the length if needed.
1406     // Copy the ones that include the split to the second line.
1407     // Move the ones after the split to the second line.
1408     for (i = 0; i < count; ++i)
1409     {
1410 	textprop_T  prop;
1411 	proptype_T *pt;
1412 	int	    start_incl, end_incl;
1413 	int	    cont_prev, cont_next;
1414 
1415 	// copy the prop to an aligned structure
1416 	mch_memmove(&prop, props + i * sizeof(textprop_T), sizeof(textprop_T));
1417 
1418 	pt = text_prop_type_by_id(curbuf, prop.tp_type);
1419 	start_incl = (pt != NULL && (pt->pt_flags & PT_FLAG_INS_START_INCL));
1420 	end_incl = (pt != NULL && (pt->pt_flags & PT_FLAG_INS_END_INCL));
1421 	cont_prev = prop.tp_col + !start_incl <= kept;
1422 	cont_next = skipped <= prop.tp_col + prop.tp_len - !end_incl;
1423 
1424 	if (cont_prev && ga_grow(&prevprop, 1) == OK)
1425 	{
1426 	    textprop_T *p = ((textprop_T *)prevprop.ga_data) + prevprop.ga_len;
1427 
1428 	    *p = prop;
1429 	    ++prevprop.ga_len;
1430 	    if (p->tp_col + p->tp_len >= kept)
1431 		p->tp_len = kept - p->tp_col;
1432 	    if (cont_next)
1433 		p->tp_flags |= TP_FLAG_CONT_NEXT;
1434 	}
1435 
1436 	// Only add the property to the next line if the length is bigger than
1437 	// zero.
1438 	if (cont_next && ga_grow(&nextprop, 1) == OK)
1439 	{
1440 	    textprop_T *p = ((textprop_T *)nextprop.ga_data) + nextprop.ga_len;
1441 	    *p = prop;
1442 	    ++nextprop.ga_len;
1443 	    if (p->tp_col > skipped)
1444 		p->tp_col -= skipped - 1;
1445 	    else
1446 	    {
1447 		p->tp_len -= skipped - p->tp_col;
1448 		p->tp_col = 1;
1449 	    }
1450 	    if (cont_prev)
1451 		p->tp_flags |= TP_FLAG_CONT_PREV;
1452 	}
1453     }
1454 
1455     set_text_props(lnum_top, prevprop.ga_data,
1456 					 prevprop.ga_len * sizeof(textprop_T));
1457     ga_clear(&prevprop);
1458     set_text_props(lnum_top + 1, nextprop.ga_data,
1459 					 nextprop.ga_len * sizeof(textprop_T));
1460     ga_clear(&nextprop);
1461 }
1462 
1463 /*
1464  * Prepend properties of joined line "lnum" to "new_props".
1465  */
1466     void
1467 prepend_joined_props(
1468 	char_u	    *new_props,
1469 	int	    propcount,
1470 	int	    *props_remaining,
1471 	linenr_T    lnum,
1472 	int	    add_all,
1473 	long	    col,
1474 	int	    removed)
1475 {
1476     char_u *props;
1477     int	    proplen = get_text_props(curbuf, lnum, &props, FALSE);
1478     int	    i;
1479 
1480     for (i = proplen; i-- > 0; )
1481     {
1482 	textprop_T  prop;
1483 	int	    end;
1484 
1485 	mch_memmove(&prop, props + i * sizeof(prop), sizeof(prop));
1486 	end = !(prop.tp_flags & TP_FLAG_CONT_NEXT);
1487 
1488 	adjust_prop(&prop, 0, -removed, 0); // Remove leading spaces
1489 	adjust_prop(&prop, -1, col, 0); // Make line start at its final colum
1490 
1491 	if (add_all || end)
1492 	    mch_memmove(new_props + --(*props_remaining) * sizeof(prop),
1493 							  &prop, sizeof(prop));
1494 	else
1495 	{
1496 	    int j;
1497 	    int	found = FALSE;
1498 
1499 	    // Search for continuing prop.
1500 	    for (j = *props_remaining; j < propcount; ++j)
1501 	    {
1502 		textprop_T op;
1503 
1504 		mch_memmove(&op, new_props + j * sizeof(op), sizeof(op));
1505 		if ((op.tp_flags & TP_FLAG_CONT_PREV)
1506 			&& op.tp_id == prop.tp_id && op.tp_type == prop.tp_type)
1507 		{
1508 		    found = TRUE;
1509 		    op.tp_len += op.tp_col - prop.tp_col;
1510 		    op.tp_col = prop.tp_col;
1511 		    // Start/end is taken care of when deleting joined lines
1512 		    op.tp_flags = prop.tp_flags;
1513 		    mch_memmove(new_props + j * sizeof(op), &op, sizeof(op));
1514 		    break;
1515 		}
1516 	    }
1517 	    if (!found)
1518 		internal_error("text property above joined line not found");
1519 	}
1520     }
1521 }
1522 
1523 #endif // FEAT_PROP_POPUP
1524