xref: /vim-8.2.3635/src/textprop.c (revision 809fcecd)
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     skipstart = dict_get_bool(dict, (char_u *)"skipstart", 0);
649 
650     if (dict_find(dict, (char_u *)"id", -1) != NULL)
651 	id = dict_get_number(dict, (char_u *)"id");
652     if (dict_find(dict, (char_u *)"type", -1))
653     {
654 	char_u	    *name = dict_get_string(dict, (char_u *)"type", FALSE);
655 	proptype_T  *type = lookup_prop_type(name, buf);
656 
657 	if (type == NULL)
658 	    return;
659 	type_id = type->pt_id;
660     }
661     if (id == -1 && type_id == -1)
662     {
663 	emsg(_("E968: Need at least one of 'id' or 'type'"));
664 	return;
665     }
666 
667     lnum_start = lnum;
668 
669     if (rettv_dict_alloc(rettv) == FAIL)
670 	return;
671 
672     while (1)
673     {
674 	char_u	*text = ml_get_buf(buf, lnum, FALSE);
675 	size_t	textlen = STRLEN(text) + 1;
676 	int	count = (int)((buf->b_ml.ml_line_len - textlen)
677 							 / sizeof(textprop_T));
678 	int	    i;
679 	textprop_T  prop;
680 	int	    prop_start;
681 	int	    prop_end;
682 
683 	for (i = 0; i < count; ++i)
684 	{
685 	    mch_memmove(&prop, text + textlen + i * sizeof(textprop_T),
686 			    sizeof(textprop_T));
687 
688 	    if (lnum == lnum_start)
689 	    {
690 		if (dir < 0)
691 		{
692 		    if (col < prop.tp_col)
693 			break;
694 		}
695 		else if (prop.tp_col + prop.tp_len - (prop.tp_len != 0) < col)
696 		    continue;
697 	    }
698 	    if (prop.tp_id == id || prop.tp_type == type_id)
699 	    {
700 		// Check if the starting position has text props.
701 		if (lnum_start == lnum
702 			&& col >= prop.tp_col
703 			&& (col <= prop.tp_col + prop.tp_len
704 							 - (prop.tp_len != 0)))
705 		    start_pos_has_prop = 1;
706 
707 		prop_start = !(prop.tp_flags & TP_FLAG_CONT_PREV);
708 		prop_end = !(prop.tp_flags & TP_FLAG_CONT_NEXT);
709 		if (!prop_start && prop_end && dir > 0)
710 		    seen_end = 1;
711 
712 		// Skip lines without the start flag.
713 		if (!prop_start)
714 		{
715 		    // Always search backwards for start when search started
716 		    // on a prop and we're not skipping.
717 		    if (start_pos_has_prop && !skipstart)
718 			dir = -1;
719 		    continue;
720 		}
721 
722 		// If skipstart is true, skip the prop at start pos (even if
723 		// continued from another line).
724 		if (start_pos_has_prop && skipstart && !seen_end)
725 		{
726 		    start_pos_has_prop = 0;
727 		    continue;
728 		}
729 
730 		prop_fill_dict(rettv->vval.v_dict, &prop, buf);
731 		dict_add_number(rettv->vval.v_dict, "lnum", lnum);
732 
733 		return;
734 	    }
735 	}
736 
737 	if (dir > 0)
738 	{
739 	    if (lnum >= buf->b_ml.ml_line_count)
740 		break;
741 	    lnum++;
742 	}
743 	else
744 	{
745 	    if (lnum <= 1)
746 		break;
747 	    lnum--;
748 	}
749 	// Adjust col to indicate that we're continuing from prev/next line.
750 	col = dir < 0 ? buf->b_ml.ml_line_len : 1;
751     }
752 }
753 
754 /*
755  * prop_list({lnum} [, {bufnr}])
756  */
757     void
758 f_prop_list(typval_T *argvars, typval_T *rettv)
759 {
760     linenr_T lnum = tv_get_number(&argvars[0]);
761     buf_T    *buf = curbuf;
762 
763     if (argvars[1].v_type != VAR_UNKNOWN)
764     {
765 	if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL)
766 	    return;
767     }
768     if (lnum < 1 || lnum > buf->b_ml.ml_line_count)
769     {
770 	emsg(_(e_invrange));
771 	return;
772     }
773 
774     if (rettv_list_alloc(rettv) == OK)
775     {
776 	char_u	    *text = ml_get_buf(buf, lnum, FALSE);
777 	size_t	    textlen = STRLEN(text) + 1;
778 	int	    count = (int)((buf->b_ml.ml_line_len - textlen)
779 							 / sizeof(textprop_T));
780 	int	    i;
781 	textprop_T  prop;
782 
783 	for (i = 0; i < count; ++i)
784 	{
785 	    dict_T *d = dict_alloc();
786 
787 	    if (d == NULL)
788 		break;
789 	    mch_memmove(&prop, text + textlen + i * sizeof(textprop_T),
790 							   sizeof(textprop_T));
791 	    prop_fill_dict(d, &prop, buf);
792 	    list_append_dict(rettv->vval.v_list, d);
793 	}
794     }
795 }
796 
797 /*
798  * prop_remove({props} [, {lnum} [, {lnum_end}]])
799  */
800     void
801 f_prop_remove(typval_T *argvars, typval_T *rettv)
802 {
803     linenr_T	start = 1;
804     linenr_T	end = 0;
805     linenr_T	lnum;
806     dict_T	*dict;
807     buf_T	*buf = curbuf;
808     int		do_all;
809     int		id = -1;
810     int		type_id = -1;
811     int		both;
812 
813     rettv->vval.v_number = 0;
814     if (argvars[0].v_type != VAR_DICT || argvars[0].vval.v_dict == NULL)
815     {
816 	emsg(_(e_invarg));
817 	return;
818     }
819 
820     if (argvars[1].v_type != VAR_UNKNOWN)
821     {
822 	start = tv_get_number(&argvars[1]);
823 	end = start;
824 	if (argvars[2].v_type != VAR_UNKNOWN)
825 	    end = tv_get_number(&argvars[2]);
826 	if (start < 1 || end < 1)
827 	{
828 	    emsg(_(e_invrange));
829 	    return;
830 	}
831     }
832 
833     dict = argvars[0].vval.v_dict;
834     if (get_bufnr_from_arg(&argvars[0], &buf) == FAIL)
835 	return;
836     if (buf->b_ml.ml_mfp == NULL)
837 	return;
838 
839     do_all = dict_get_bool(dict, (char_u *)"all", FALSE);
840 
841     if (dict_find(dict, (char_u *)"id", -1) != NULL)
842 	id = dict_get_number(dict, (char_u *)"id");
843     if (dict_find(dict, (char_u *)"type", -1))
844     {
845 	char_u	    *name = dict_get_string(dict, (char_u *)"type", FALSE);
846 	proptype_T  *type = lookup_prop_type(name, buf);
847 
848 	if (type == NULL)
849 	    return;
850 	type_id = type->pt_id;
851     }
852     both = dict_get_bool(dict, (char_u *)"both", FALSE);
853 
854     if (id == -1 && type_id == -1)
855     {
856 	emsg(_("E968: Need at least one of 'id' or 'type'"));
857 	return;
858     }
859     if (both && (id == -1 || type_id == -1))
860     {
861 	emsg(_("E860: Need 'id' and 'type' with 'both'"));
862 	return;
863     }
864 
865     if (end == 0)
866 	end = buf->b_ml.ml_line_count;
867     for (lnum = start; lnum <= end; ++lnum)
868     {
869 	char_u *text;
870 	size_t len;
871 
872 	if (lnum > buf->b_ml.ml_line_count)
873 	    break;
874 	text = ml_get_buf(buf, lnum, FALSE);
875 	len = STRLEN(text) + 1;
876 	if ((size_t)buf->b_ml.ml_line_len > len)
877 	{
878 	    static textprop_T	textprop;  // static because of alignment
879 	    unsigned		idx;
880 
881 	    for (idx = 0; idx < (buf->b_ml.ml_line_len - len)
882 						   / sizeof(textprop_T); ++idx)
883 	    {
884 		char_u *cur_prop = buf->b_ml.ml_line_ptr + len
885 						    + idx * sizeof(textprop_T);
886 		size_t	taillen;
887 
888 		mch_memmove(&textprop, cur_prop, sizeof(textprop_T));
889 		if (both ? textprop.tp_id == id && textprop.tp_type == type_id
890 			 : textprop.tp_id == id || textprop.tp_type == type_id)
891 		{
892 		    if (!(buf->b_ml.ml_flags & ML_LINE_DIRTY))
893 		    {
894 			char_u *newptr = alloc(buf->b_ml.ml_line_len);
895 
896 			// need to allocate the line to be able to change it
897 			if (newptr == NULL)
898 			    return;
899 			mch_memmove(newptr, buf->b_ml.ml_line_ptr,
900 							buf->b_ml.ml_line_len);
901 			buf->b_ml.ml_line_ptr = newptr;
902 			buf->b_ml.ml_flags |= ML_LINE_DIRTY;
903 
904 			cur_prop = buf->b_ml.ml_line_ptr + len
905 						    + idx * sizeof(textprop_T);
906 		    }
907 
908 		    taillen = buf->b_ml.ml_line_len - len
909 					      - (idx + 1) * sizeof(textprop_T);
910 		    if (taillen > 0)
911 			mch_memmove(cur_prop, cur_prop + sizeof(textprop_T),
912 								      taillen);
913 		    buf->b_ml.ml_line_len -= sizeof(textprop_T);
914 		    --idx;
915 
916 		    ++rettv->vval.v_number;
917 		    if (!do_all)
918 			break;
919 		}
920 	    }
921 	}
922     }
923     redraw_buf_later(buf, NOT_VALID);
924 }
925 
926 /*
927  * Common for f_prop_type_add() and f_prop_type_change().
928  */
929     static void
930 prop_type_set(typval_T *argvars, int add)
931 {
932     char_u	*name;
933     buf_T	*buf = NULL;
934     dict_T	*dict;
935     dictitem_T  *di;
936     proptype_T	*prop;
937 
938     name = tv_get_string(&argvars[0]);
939     if (*name == NUL)
940     {
941 	emsg(_(e_invarg));
942 	return;
943     }
944 
945     if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL)
946 	return;
947     dict = argvars[1].vval.v_dict;
948 
949     prop = find_prop(name, buf);
950     if (add)
951     {
952 	hashtab_T **htp;
953 
954 	if (prop != NULL)
955 	{
956 	    semsg(_("E969: Property type %s already defined"), name);
957 	    return;
958 	}
959 	prop = alloc_clear(offsetof(proptype_T, pt_name) + STRLEN(name) + 1);
960 	if (prop == NULL)
961 	    return;
962 	STRCPY(prop->pt_name, name);
963 	prop->pt_id = ++proptype_id;
964 	prop->pt_flags = PT_FLAG_COMBINE;
965 	htp = buf == NULL ? &global_proptypes : &buf->b_proptypes;
966 	if (*htp == NULL)
967 	{
968 	    *htp = ALLOC_ONE(hashtab_T);
969 	    if (*htp == NULL)
970 	    {
971 		vim_free(prop);
972 		return;
973 	    }
974 	    hash_init(*htp);
975 	}
976 	hash_add(*htp, PT2HIKEY(prop));
977     }
978     else
979     {
980 	if (prop == NULL)
981 	{
982 	    semsg(_(e_type_not_exist), name);
983 	    return;
984 	}
985     }
986 
987     if (dict != NULL)
988     {
989 	di = dict_find(dict, (char_u *)"highlight", -1);
990 	if (di != NULL)
991 	{
992 	    char_u	*highlight;
993 	    int		hl_id = 0;
994 
995 	    highlight = dict_get_string(dict, (char_u *)"highlight", FALSE);
996 	    if (highlight != NULL && *highlight != NUL)
997 		hl_id = syn_name2id(highlight);
998 	    if (hl_id <= 0)
999 	    {
1000 		semsg(_("E970: Unknown highlight group name: '%s'"),
1001 			highlight == NULL ? (char_u *)"" : highlight);
1002 		return;
1003 	    }
1004 	    prop->pt_hl_id = hl_id;
1005 	}
1006 
1007 	di = dict_find(dict, (char_u *)"combine", -1);
1008 	if (di != NULL)
1009 	{
1010 	    if (tv_get_bool(&di->di_tv))
1011 		prop->pt_flags |= PT_FLAG_COMBINE;
1012 	    else
1013 		prop->pt_flags &= ~PT_FLAG_COMBINE;
1014 	}
1015 
1016 	di = dict_find(dict, (char_u *)"priority", -1);
1017 	if (di != NULL)
1018 	    prop->pt_priority = tv_get_number(&di->di_tv);
1019 
1020 	di = dict_find(dict, (char_u *)"start_incl", -1);
1021 	if (di != NULL)
1022 	{
1023 	    if (tv_get_bool(&di->di_tv))
1024 		prop->pt_flags |= PT_FLAG_INS_START_INCL;
1025 	    else
1026 		prop->pt_flags &= ~PT_FLAG_INS_START_INCL;
1027 	}
1028 
1029 	di = dict_find(dict, (char_u *)"end_incl", -1);
1030 	if (di != NULL)
1031 	{
1032 	    if (tv_get_bool(&di->di_tv))
1033 		prop->pt_flags |= PT_FLAG_INS_END_INCL;
1034 	    else
1035 		prop->pt_flags &= ~PT_FLAG_INS_END_INCL;
1036 	}
1037     }
1038 }
1039 
1040 /*
1041  * prop_type_add({name}, {props})
1042  */
1043     void
1044 f_prop_type_add(typval_T *argvars, typval_T *rettv UNUSED)
1045 {
1046     prop_type_set(argvars, TRUE);
1047 }
1048 
1049 /*
1050  * prop_type_change({name}, {props})
1051  */
1052     void
1053 f_prop_type_change(typval_T *argvars, typval_T *rettv UNUSED)
1054 {
1055     prop_type_set(argvars, FALSE);
1056 }
1057 
1058 /*
1059  * prop_type_delete({name} [, {bufnr}])
1060  */
1061     void
1062 f_prop_type_delete(typval_T *argvars, typval_T *rettv UNUSED)
1063 {
1064     char_u	*name;
1065     buf_T	*buf = NULL;
1066     hashitem_T	*hi;
1067 
1068     name = tv_get_string(&argvars[0]);
1069     if (*name == NUL)
1070     {
1071 	emsg(_(e_invarg));
1072 	return;
1073     }
1074 
1075     if (argvars[1].v_type != VAR_UNKNOWN)
1076     {
1077 	if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL)
1078 	    return;
1079     }
1080 
1081     hi = find_prop_hi(name, buf);
1082     if (hi != NULL)
1083     {
1084 	hashtab_T	*ht;
1085 	proptype_T	*prop = HI2PT(hi);
1086 
1087 	if (buf == NULL)
1088 	    ht = global_proptypes;
1089 	else
1090 	    ht = buf->b_proptypes;
1091 	hash_remove(ht, hi);
1092 	vim_free(prop);
1093     }
1094 }
1095 
1096 /*
1097  * prop_type_get({name} [, {bufnr}])
1098  */
1099     void
1100 f_prop_type_get(typval_T *argvars, typval_T *rettv)
1101 {
1102     char_u *name = tv_get_string(&argvars[0]);
1103 
1104     if (*name == NUL)
1105     {
1106 	emsg(_(e_invarg));
1107 	return;
1108     }
1109     if (rettv_dict_alloc(rettv) == OK)
1110     {
1111 	proptype_T  *prop = NULL;
1112 	buf_T	    *buf = NULL;
1113 
1114 	if (argvars[1].v_type != VAR_UNKNOWN)
1115 	{
1116 	    if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL)
1117 		return;
1118 	}
1119 
1120 	prop = find_prop(name, buf);
1121 	if (prop != NULL)
1122 	{
1123 	    dict_T *d = rettv->vval.v_dict;
1124 
1125 	    if (prop->pt_hl_id > 0)
1126 		dict_add_string(d, "highlight", syn_id2name(prop->pt_hl_id));
1127 	    dict_add_number(d, "priority", prop->pt_priority);
1128 	    dict_add_number(d, "combine",
1129 				   (prop->pt_flags & PT_FLAG_COMBINE) ? 1 : 0);
1130 	    dict_add_number(d, "start_incl",
1131 			    (prop->pt_flags & PT_FLAG_INS_START_INCL) ? 1 : 0);
1132 	    dict_add_number(d, "end_incl",
1133 			      (prop->pt_flags & PT_FLAG_INS_END_INCL) ? 1 : 0);
1134 	    if (buf != NULL)
1135 		dict_add_number(d, "bufnr", buf->b_fnum);
1136 	}
1137     }
1138 }
1139 
1140     static void
1141 list_types(hashtab_T *ht, list_T *l)
1142 {
1143     long	todo;
1144     hashitem_T	*hi;
1145 
1146     todo = (long)ht->ht_used;
1147     for (hi = ht->ht_array; todo > 0; ++hi)
1148     {
1149 	if (!HASHITEM_EMPTY(hi))
1150 	{
1151 	    proptype_T *prop = HI2PT(hi);
1152 
1153 	    list_append_string(l, prop->pt_name, -1);
1154 	    --todo;
1155 	}
1156     }
1157 }
1158 
1159 /*
1160  * prop_type_list([{bufnr}])
1161  */
1162     void
1163 f_prop_type_list(typval_T *argvars, typval_T *rettv UNUSED)
1164 {
1165     buf_T *buf = NULL;
1166 
1167     if (rettv_list_alloc(rettv) == OK)
1168     {
1169 	if (argvars[0].v_type != VAR_UNKNOWN)
1170 	{
1171 	    if (get_bufnr_from_arg(&argvars[0], &buf) == FAIL)
1172 		return;
1173 	}
1174 	if (buf == NULL)
1175 	{
1176 	    if (global_proptypes != NULL)
1177 		list_types(global_proptypes, rettv->vval.v_list);
1178 	}
1179 	else if (buf->b_proptypes != NULL)
1180 	    list_types(buf->b_proptypes, rettv->vval.v_list);
1181     }
1182 }
1183 
1184 /*
1185  * Free all property types in "ht".
1186  */
1187     static void
1188 clear_ht_prop_types(hashtab_T *ht)
1189 {
1190     long	todo;
1191     hashitem_T	*hi;
1192 
1193     if (ht == NULL)
1194 	return;
1195 
1196     todo = (long)ht->ht_used;
1197     for (hi = ht->ht_array; todo > 0; ++hi)
1198     {
1199 	if (!HASHITEM_EMPTY(hi))
1200 	{
1201 	    proptype_T *prop = HI2PT(hi);
1202 
1203 	    vim_free(prop);
1204 	    --todo;
1205 	}
1206     }
1207 
1208     hash_clear(ht);
1209     vim_free(ht);
1210 }
1211 
1212 #if defined(EXITFREE) || defined(PROTO)
1213 /*
1214  * Free all global property types.
1215  */
1216     void
1217 clear_global_prop_types(void)
1218 {
1219     clear_ht_prop_types(global_proptypes);
1220     global_proptypes = NULL;
1221 }
1222 #endif
1223 
1224 /*
1225  * Free all property types for "buf".
1226  */
1227     void
1228 clear_buf_prop_types(buf_T *buf)
1229 {
1230     clear_ht_prop_types(buf->b_proptypes);
1231     buf->b_proptypes = NULL;
1232 }
1233 
1234 // Struct used to return two values from adjust_prop().
1235 typedef struct
1236 {
1237     int dirty;	    // if the property was changed
1238     int can_drop;   // whether after this change, the prop may be removed
1239 } adjustres_T;
1240 
1241 /*
1242  * Adjust the property for "added" bytes (can be negative) inserted at "col".
1243  *
1244  * Note that "col" is zero-based, while tp_col is one-based.
1245  * Only for the current buffer.
1246  * "flags" can have:
1247  * APC_SUBSTITUTE:	Text is replaced, not inserted.
1248  */
1249     static adjustres_T
1250 adjust_prop(
1251 	textprop_T *prop,
1252 	colnr_T col,
1253 	int added,
1254 	int flags)
1255 {
1256     proptype_T	*pt = text_prop_type_by_id(curbuf, prop->tp_type);
1257     int		start_incl = (pt != NULL
1258 				    && (pt->pt_flags & PT_FLAG_INS_START_INCL))
1259 						   || (flags & APC_SUBSTITUTE);
1260     int		end_incl = (pt != NULL
1261 				     && (pt->pt_flags & PT_FLAG_INS_END_INCL));
1262 		// Do not drop zero-width props if they later can increase in
1263 		// size.
1264     int		droppable = !(start_incl || end_incl);
1265     adjustres_T res = {TRUE, FALSE};
1266 
1267     if (added > 0)
1268     {
1269 	if (col + 1 <= prop->tp_col
1270 		- (start_incl || (prop->tp_len == 0 && end_incl)))
1271 	    // Change is entirely before the text property: Only shift
1272 	    prop->tp_col += added;
1273 	else if (col + 1 < prop->tp_col + prop->tp_len + end_incl)
1274 	    // Insertion was inside text property
1275 	    prop->tp_len += added;
1276     }
1277     else if (prop->tp_col > col + 1)
1278     {
1279 	if (prop->tp_col + added < col + 1)
1280 	{
1281 	    prop->tp_len += (prop->tp_col - 1 - col) + added;
1282 	    prop->tp_col = col + 1;
1283 	    if (prop->tp_len <= 0)
1284 	    {
1285 		prop->tp_len = 0;
1286 		res.can_drop = droppable;
1287 	    }
1288 	}
1289 	else
1290 	    prop->tp_col += added;
1291     }
1292     else if (prop->tp_len > 0 && prop->tp_col + prop->tp_len > col)
1293     {
1294 	int after = col - added - (prop->tp_col - 1 + prop->tp_len);
1295 
1296 	prop->tp_len += after > 0 ? added + after : added;
1297 	res.can_drop = prop->tp_len <= 0 && droppable;
1298     }
1299     else
1300 	res.dirty = FALSE;
1301 
1302     return res;
1303 }
1304 
1305 /*
1306  * Adjust the columns of text properties in line "lnum" after position "col" to
1307  * shift by "bytes_added" (can be negative).
1308  * Note that "col" is zero-based, while tp_col is one-based.
1309  * Only for the current buffer.
1310  * "flags" can have:
1311  * APC_SAVE_FOR_UNDO:	Call u_savesub() before making changes to the line.
1312  * APC_SUBSTITUTE:	Text is replaced, not inserted.
1313  * Caller is expected to check b_has_textprop and "bytes_added" being non-zero.
1314  * Returns TRUE when props were changed.
1315  */
1316     int
1317 adjust_prop_columns(
1318 	linenr_T    lnum,
1319 	colnr_T	    col,
1320 	int	    bytes_added,
1321 	int	    flags)
1322 {
1323     int		proplen;
1324     char_u	*props;
1325     int		dirty = FALSE;
1326     int		ri, wi;
1327     size_t	textlen;
1328 
1329     if (text_prop_frozen > 0)
1330 	return FALSE;
1331 
1332     proplen = get_text_props(curbuf, lnum, &props, TRUE);
1333     if (proplen == 0)
1334 	return FALSE;
1335     textlen = curbuf->b_ml.ml_line_len - proplen * sizeof(textprop_T);
1336 
1337     wi = 0; // write index
1338     for (ri = 0; ri < proplen; ++ri)
1339     {
1340 	textprop_T	prop;
1341 	adjustres_T	res;
1342 
1343 	mch_memmove(&prop, props + ri * sizeof(prop), sizeof(prop));
1344 	res = adjust_prop(&prop, col, bytes_added, flags);
1345 	if (res.dirty)
1346 	{
1347 	    // Save for undo if requested and not done yet.
1348 	    if ((flags & APC_SAVE_FOR_UNDO) && !dirty
1349 						    && u_savesub(lnum) == FAIL)
1350 		return FALSE;
1351 	    dirty = TRUE;
1352 
1353 	    // u_savesub() may have updated curbuf->b_ml, fetch it again
1354 	    if (curbuf->b_ml.ml_line_lnum != lnum)
1355 		proplen = get_text_props(curbuf, lnum, &props, TRUE);
1356 	}
1357 	if (res.can_drop)
1358 	    continue; // Drop this text property
1359 	mch_memmove(props + wi * sizeof(textprop_T), &prop, sizeof(textprop_T));
1360 	++wi;
1361     }
1362     if (dirty)
1363     {
1364 	colnr_T newlen = (int)textlen + wi * (colnr_T)sizeof(textprop_T);
1365 
1366 	if ((curbuf->b_ml.ml_flags & ML_LINE_DIRTY) == 0)
1367 	    curbuf->b_ml.ml_line_ptr =
1368 				 vim_memsave(curbuf->b_ml.ml_line_ptr, newlen);
1369 	curbuf->b_ml.ml_flags |= ML_LINE_DIRTY;
1370 	curbuf->b_ml.ml_line_len = newlen;
1371     }
1372     return dirty;
1373 }
1374 
1375 /*
1376  * Adjust text properties for a line that was split in two.
1377  * "lnum_props" is the line that has the properties from before the split.
1378  * "lnum_top" is the top line.
1379  * "kept" is the number of bytes kept in the first line, while
1380  * "deleted" is the number of bytes deleted.
1381  */
1382     void
1383 adjust_props_for_split(
1384 	linenr_T lnum_props,
1385 	linenr_T lnum_top,
1386 	int kept,
1387 	int deleted)
1388 {
1389     char_u	*props;
1390     int		count;
1391     garray_T    prevprop;
1392     garray_T    nextprop;
1393     int		i;
1394     int		skipped = kept + deleted;
1395 
1396     if (!curbuf->b_has_textprop)
1397 	return;
1398 
1399     // Get the text properties from "lnum_props".
1400     count = get_text_props(curbuf, lnum_props, &props, FALSE);
1401     ga_init2(&prevprop, sizeof(textprop_T), 10);
1402     ga_init2(&nextprop, sizeof(textprop_T), 10);
1403 
1404     // Keep the relevant ones in the first line, reducing the length if needed.
1405     // Copy the ones that include the split to the second line.
1406     // Move the ones after the split to the second line.
1407     for (i = 0; i < count; ++i)
1408     {
1409 	textprop_T  prop;
1410 	proptype_T *pt;
1411 	int	    start_incl, end_incl;
1412 	int	    cont_prev, cont_next;
1413 
1414 	// copy the prop to an aligned structure
1415 	mch_memmove(&prop, props + i * sizeof(textprop_T), sizeof(textprop_T));
1416 
1417 	pt = text_prop_type_by_id(curbuf, prop.tp_type);
1418 	start_incl = (pt != NULL && (pt->pt_flags & PT_FLAG_INS_START_INCL));
1419 	end_incl = (pt != NULL && (pt->pt_flags & PT_FLAG_INS_END_INCL));
1420 	cont_prev = prop.tp_col + !start_incl <= kept;
1421 	cont_next = skipped <= prop.tp_col + prop.tp_len - !end_incl;
1422 
1423 	if (cont_prev && ga_grow(&prevprop, 1) == OK)
1424 	{
1425 	    textprop_T *p = ((textprop_T *)prevprop.ga_data) + prevprop.ga_len;
1426 
1427 	    *p = prop;
1428 	    ++prevprop.ga_len;
1429 	    if (p->tp_col + p->tp_len >= kept)
1430 		p->tp_len = kept - p->tp_col;
1431 	    if (cont_next)
1432 		p->tp_flags |= TP_FLAG_CONT_NEXT;
1433 	}
1434 
1435 	// Only add the property to the next line if the length is bigger than
1436 	// zero.
1437 	if (cont_next && ga_grow(&nextprop, 1) == OK)
1438 	{
1439 	    textprop_T *p = ((textprop_T *)nextprop.ga_data) + nextprop.ga_len;
1440 	    *p = prop;
1441 	    ++nextprop.ga_len;
1442 	    if (p->tp_col > skipped)
1443 		p->tp_col -= skipped - 1;
1444 	    else
1445 	    {
1446 		p->tp_len -= skipped - p->tp_col;
1447 		p->tp_col = 1;
1448 	    }
1449 	    if (cont_prev)
1450 		p->tp_flags |= TP_FLAG_CONT_PREV;
1451 	}
1452     }
1453 
1454     set_text_props(lnum_top, prevprop.ga_data,
1455 					 prevprop.ga_len * sizeof(textprop_T));
1456     ga_clear(&prevprop);
1457     set_text_props(lnum_top + 1, nextprop.ga_data,
1458 					 nextprop.ga_len * sizeof(textprop_T));
1459     ga_clear(&nextprop);
1460 }
1461 
1462 /*
1463  * Prepend properties of joined line "lnum" to "new_props".
1464  */
1465     void
1466 prepend_joined_props(
1467 	char_u	    *new_props,
1468 	int	    propcount,
1469 	int	    *props_remaining,
1470 	linenr_T    lnum,
1471 	int	    add_all,
1472 	long	    col,
1473 	int	    removed)
1474 {
1475     char_u *props;
1476     int	    proplen = get_text_props(curbuf, lnum, &props, FALSE);
1477     int	    i;
1478 
1479     for (i = proplen; i-- > 0; )
1480     {
1481 	textprop_T  prop;
1482 	int	    end;
1483 
1484 	mch_memmove(&prop, props + i * sizeof(prop), sizeof(prop));
1485 	end = !(prop.tp_flags & TP_FLAG_CONT_NEXT);
1486 
1487 	adjust_prop(&prop, 0, -removed, 0); // Remove leading spaces
1488 	adjust_prop(&prop, -1, col, 0); // Make line start at its final colum
1489 
1490 	if (add_all || end)
1491 	    mch_memmove(new_props + --(*props_remaining) * sizeof(prop),
1492 							  &prop, sizeof(prop));
1493 	else
1494 	{
1495 	    int j;
1496 	    int	found = FALSE;
1497 
1498 	    // Search for continuing prop.
1499 	    for (j = *props_remaining; j < propcount; ++j)
1500 	    {
1501 		textprop_T op;
1502 
1503 		mch_memmove(&op, new_props + j * sizeof(op), sizeof(op));
1504 		if ((op.tp_flags & TP_FLAG_CONT_PREV)
1505 			&& op.tp_id == prop.tp_id && op.tp_type == prop.tp_type)
1506 		{
1507 		    found = TRUE;
1508 		    op.tp_len += op.tp_col - prop.tp_col;
1509 		    op.tp_col = prop.tp_col;
1510 		    // Start/end is taken care of when deleting joined lines
1511 		    op.tp_flags = prop.tp_flags;
1512 		    mch_memmove(new_props + j * sizeof(op), &op, sizeof(op));
1513 		    break;
1514 		}
1515 	    }
1516 	    if (!found)
1517 		internal_error("text property above joined line not found");
1518 	}
1519     }
1520 }
1521 
1522 #endif // FEAT_PROP_POPUP
1523