xref: /vim-8.2.3635/src/textprop.c (revision fc643e60)
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 property.
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     changed_lines_buf(buf, start_lnum, end_lnum + 1, 0);
349     redraw_buf_later(buf, VALID);
350 }
351 
352 /*
353  * Fetch the text properties for line "lnum" in buffer "buf".
354  * Returns the number of text properties and, when non-zero, a pointer to the
355  * first one in "props" (note that it is not aligned, therefore the char_u
356  * pointer).
357  */
358     int
359 get_text_props(buf_T *buf, linenr_T lnum, char_u **props, int will_change)
360 {
361     char_u *text;
362     size_t textlen;
363     size_t proplen;
364 
365     // Be quick when no text property types have been defined or the buffer,
366     // unless we are adding one.
367     if ((!buf->b_has_textprop && !will_change) || buf->b_ml.ml_mfp == NULL)
368 	return 0;
369 
370     // Fetch the line to get the ml_line_len field updated.
371     text = ml_get_buf(buf, lnum, will_change);
372     textlen = STRLEN(text) + 1;
373     proplen = buf->b_ml.ml_line_len - textlen;
374     if (proplen % sizeof(textprop_T) != 0)
375     {
376 	iemsg(_("E967: text property info corrupted"));
377 	return 0;
378     }
379     if (proplen > 0)
380 	*props = text + textlen;
381     return (int)(proplen / sizeof(textprop_T));
382 }
383 
384 /**
385  * Return the number of text properties on line "lnum" in the current buffer.
386  * When "only_starting" is true only text properties starting in this line will
387  * be considered.
388  */
389     int
390 count_props(linenr_T lnum, int only_starting)
391 {
392     char_u	*props;
393     int		proplen = get_text_props(curbuf, lnum, &props, 0);
394     int		result = proplen;
395     int		i;
396     textprop_T	prop;
397 
398     if (only_starting)
399 	for (i = 0; i < proplen; ++i)
400 	{
401 	    mch_memmove(&prop, props + i * sizeof(prop), sizeof(prop));
402 	    if (prop.tp_flags & TP_FLAG_CONT_PREV)
403 		--result;
404 	}
405     return result;
406 }
407 
408 /*
409  * Find text property "type_id" in the visible lines of window "wp".
410  * Match "id" when it is > 0.
411  * Returns FAIL when not found.
412  */
413     int
414 find_visible_prop(win_T *wp, int type_id, int id, textprop_T *prop,
415 							  linenr_T *found_lnum)
416 {
417     linenr_T		lnum;
418     char_u		*props;
419     int			count;
420     int			i;
421 
422     // w_botline may not have been updated yet.
423     validate_botline_win(wp);
424     for (lnum = wp->w_topline; lnum < wp->w_botline; ++lnum)
425     {
426 	count = get_text_props(wp->w_buffer, lnum, &props, FALSE);
427 	for (i = 0; i < count; ++i)
428 	{
429 	    mch_memmove(prop, props + i * sizeof(textprop_T),
430 							   sizeof(textprop_T));
431 	    if (prop->tp_type == type_id && (id <= 0 || prop->tp_id == id))
432 	    {
433 		*found_lnum = lnum;
434 		return OK;
435 	    }
436 	}
437     }
438     return FAIL;
439 }
440 
441 /*
442  * Set the text properties for line "lnum" to "props" with length "len".
443  * If "len" is zero text properties are removed, "props" is not used.
444  * Any existing text properties are dropped.
445  * Only works for the current buffer.
446  */
447     static void
448 set_text_props(linenr_T lnum, char_u *props, int len)
449 {
450     char_u  *text;
451     char_u  *newtext;
452     int	    textlen;
453 
454     text = ml_get(lnum);
455     textlen = (int)STRLEN(text) + 1;
456     newtext = alloc(textlen + len);
457     if (newtext == NULL)
458 	return;
459     mch_memmove(newtext, text, textlen);
460     if (len > 0)
461 	mch_memmove(newtext + textlen, props, len);
462     if (curbuf->b_ml.ml_flags & ML_LINE_DIRTY)
463 	vim_free(curbuf->b_ml.ml_line_ptr);
464     curbuf->b_ml.ml_line_ptr = newtext;
465     curbuf->b_ml.ml_line_len = textlen + len;
466     curbuf->b_ml.ml_flags |= ML_LINE_DIRTY;
467 }
468 
469     static proptype_T *
470 find_type_by_id(hashtab_T *ht, int id)
471 {
472     long	todo;
473     hashitem_T	*hi;
474 
475     if (ht == NULL)
476 	return NULL;
477 
478     // TODO: Make this faster by keeping a list of types sorted on ID and use
479     // a binary search.
480 
481     todo = (long)ht->ht_used;
482     for (hi = ht->ht_array; todo > 0; ++hi)
483     {
484 	if (!HASHITEM_EMPTY(hi))
485 	{
486 	    proptype_T *prop = HI2PT(hi);
487 
488 	    if (prop->pt_id == id)
489 		return prop;
490 	    --todo;
491 	}
492     }
493     return NULL;
494 }
495 
496 /*
497  * Fill 'dict' with text properties in 'prop'.
498  */
499     static void
500 prop_fill_dict(dict_T *dict, textprop_T *prop, buf_T *buf)
501 {
502     proptype_T *pt;
503 
504     dict_add_number(dict, "col", prop->tp_col);
505     dict_add_number(dict, "length", prop->tp_len);
506     dict_add_number(dict, "id", prop->tp_id);
507     dict_add_number(dict, "start", !(prop->tp_flags & TP_FLAG_CONT_PREV));
508     dict_add_number(dict, "end", !(prop->tp_flags & TP_FLAG_CONT_NEXT));
509     pt = text_prop_type_by_id(buf, prop->tp_type);
510     if (pt != NULL)
511 	dict_add_string(dict, "type", pt->pt_name);
512 }
513 
514 /*
515  * Find a property type by ID in "buf" or globally.
516  * Returns NULL if not found.
517  */
518     proptype_T *
519 text_prop_type_by_id(buf_T *buf, int id)
520 {
521     proptype_T *type;
522 
523     type = find_type_by_id(buf->b_proptypes, id);
524     if (type == NULL)
525 	type = find_type_by_id(global_proptypes, id);
526     return type;
527 }
528 
529 /*
530  * prop_clear({lnum} [, {lnum_end} [, {bufnr}]])
531  */
532     void
533 f_prop_clear(typval_T *argvars, typval_T *rettv UNUSED)
534 {
535     linenr_T start = tv_get_number(&argvars[0]);
536     linenr_T end = start;
537     linenr_T lnum;
538     buf_T    *buf = curbuf;
539     int	    did_clear = FALSE;
540 
541     if (argvars[1].v_type != VAR_UNKNOWN)
542     {
543 	end = tv_get_number(&argvars[1]);
544 	if (argvars[2].v_type != VAR_UNKNOWN)
545 	{
546 	    if (get_bufnr_from_arg(&argvars[2], &buf) == FAIL)
547 		return;
548 	}
549     }
550     if (start < 1 || end < 1)
551     {
552 	emsg(_(e_invrange));
553 	return;
554     }
555 
556     for (lnum = start; lnum <= end; ++lnum)
557     {
558 	char_u *text;
559 	size_t len;
560 
561 	if (lnum > buf->b_ml.ml_line_count)
562 	    break;
563 	text = ml_get_buf(buf, lnum, FALSE);
564 	len = STRLEN(text) + 1;
565 	if ((size_t)buf->b_ml.ml_line_len > len)
566 	{
567 	    did_clear = TRUE;
568 	    if (!(buf->b_ml.ml_flags & ML_LINE_DIRTY))
569 	    {
570 		char_u *newtext = vim_strsave(text);
571 
572 		// need to allocate the line now
573 		if (newtext == NULL)
574 		    return;
575 		buf->b_ml.ml_line_ptr = newtext;
576 		buf->b_ml.ml_flags |= ML_LINE_DIRTY;
577 	    }
578 	    buf->b_ml.ml_line_len = (int)len;
579 	}
580     }
581     if (did_clear)
582 	redraw_buf_later(buf, NOT_VALID);
583 }
584 
585 /*
586  * prop_find({props} [, {direction}])
587  */
588     void
589 f_prop_find(typval_T *argvars, typval_T *rettv)
590 {
591     pos_T       *cursor = &curwin->w_cursor;
592     dict_T      *dict;
593     buf_T       *buf = curbuf;
594     dictitem_T  *di;
595     int		lnum_start;
596     int		start_pos_has_prop = 0;
597     int		seen_end = 0;
598     int		id = -1;
599     int		type_id = -1;
600     int		skipstart = 0;
601     int		lnum = -1;
602     int		col = -1;
603     int		dir = 1;    // 1 = forward, -1 = backward
604     int		both;
605 
606     if (argvars[0].v_type != VAR_DICT || argvars[0].vval.v_dict == NULL)
607     {
608 	emsg(_(e_invarg));
609 	return;
610     }
611     dict = argvars[0].vval.v_dict;
612 
613     if (get_bufnr_from_arg(&argvars[0], &buf) == FAIL)
614 	return;
615     if (buf->b_ml.ml_mfp == NULL)
616 	return;
617 
618     if (argvars[1].v_type != VAR_UNKNOWN)
619     {
620 	char_u      *dir_s = tv_get_string(&argvars[1]);
621 
622 	if (*dir_s == 'b')
623 	    dir = -1;
624 	else if (*dir_s != 'f')
625 	{
626 	    emsg(_(e_invarg));
627 	    return;
628 	}
629     }
630 
631     di = dict_find(dict, (char_u *)"lnum", -1);
632     if (di != NULL)
633 	lnum = tv_get_number(&di->di_tv);
634 
635     di = dict_find(dict, (char_u *)"col", -1);
636     if (di != NULL)
637 	col = tv_get_number(&di->di_tv);
638 
639     if (lnum == -1)
640     {
641 	lnum = cursor->lnum;
642 	col = cursor->col + 1;
643     }
644     else if (col == -1)
645 	col = 1;
646 
647     if (lnum < 1 || lnum > buf->b_ml.ml_line_count)
648     {
649 	emsg(_(e_invrange));
650 	return;
651     }
652 
653     skipstart = dict_get_bool(dict, (char_u *)"skipstart", 0);
654 
655     if (dict_find(dict, (char_u *)"id", -1) != NULL)
656 	id = dict_get_number(dict, (char_u *)"id");
657     if (dict_find(dict, (char_u *)"type", -1))
658     {
659 	char_u	    *name = dict_get_string(dict, (char_u *)"type", FALSE);
660 	proptype_T  *type = lookup_prop_type(name, buf);
661 
662 	if (type == NULL)
663 	    return;
664 	type_id = type->pt_id;
665     }
666     both = dict_get_bool(dict, (char_u *)"both", FALSE);
667     if (id == -1 && type_id == -1)
668     {
669 	emsg(_("E968: Need at least one of 'id' or 'type'"));
670 	return;
671     }
672     if (both && (id == -1 || type_id == -1))
673     {
674 	emsg(_("E860: Need 'id' and 'type' with 'both'"));
675 	return;
676     }
677 
678     lnum_start = lnum;
679 
680     if (rettv_dict_alloc(rettv) == FAIL)
681 	return;
682 
683     while (1)
684     {
685 	char_u	*text = ml_get_buf(buf, lnum, FALSE);
686 	size_t	textlen = STRLEN(text) + 1;
687 	int	count = (int)((buf->b_ml.ml_line_len - textlen)
688 							 / sizeof(textprop_T));
689 	int	    i;
690 	textprop_T  prop;
691 	int	    prop_start;
692 	int	    prop_end;
693 
694 	for (i = 0; i < count; ++i)
695 	{
696 	    mch_memmove(&prop, text + textlen + i * sizeof(textprop_T),
697 			    sizeof(textprop_T));
698 
699 	    if (lnum == lnum_start)
700 	    {
701 		if (dir < 0)
702 		{
703 		    if (col < prop.tp_col)
704 			break;
705 		}
706 		else if (prop.tp_col + prop.tp_len - (prop.tp_len != 0) < col)
707 		    continue;
708 	    }
709 	    if (both ? prop.tp_id == id && prop.tp_type == type_id
710 		     : prop.tp_id == id || prop.tp_type == type_id)
711 	    {
712 		// Check if the starting position has text props.
713 		if (lnum_start == lnum
714 			&& col >= prop.tp_col
715 			&& (col <= prop.tp_col + prop.tp_len
716 							 - (prop.tp_len != 0)))
717 		    start_pos_has_prop = 1;
718 
719 		prop_start = !(prop.tp_flags & TP_FLAG_CONT_PREV);
720 		prop_end = !(prop.tp_flags & TP_FLAG_CONT_NEXT);
721 		if (!prop_start && prop_end && dir > 0)
722 		    seen_end = 1;
723 
724 		// Skip lines without the start flag.
725 		if (!prop_start)
726 		{
727 		    // Always search backwards for start when search started
728 		    // on a prop and we're not skipping.
729 		    if (start_pos_has_prop && !skipstart)
730 			dir = -1;
731 		    continue;
732 		}
733 
734 		// If skipstart is true, skip the prop at start pos (even if
735 		// continued from another line).
736 		if (start_pos_has_prop && skipstart && !seen_end)
737 		{
738 		    start_pos_has_prop = 0;
739 		    continue;
740 		}
741 
742 		prop_fill_dict(rettv->vval.v_dict, &prop, buf);
743 		dict_add_number(rettv->vval.v_dict, "lnum", lnum);
744 
745 		return;
746 	    }
747 	}
748 
749 	if (dir > 0)
750 	{
751 	    if (lnum >= buf->b_ml.ml_line_count)
752 		break;
753 	    lnum++;
754 	}
755 	else
756 	{
757 	    if (lnum <= 1)
758 		break;
759 	    lnum--;
760 	}
761 	// Adjust col to indicate that we're continuing from prev/next line.
762 	col = dir < 0 ? buf->b_ml.ml_line_len : 1;
763     }
764 }
765 
766 /*
767  * prop_list({lnum} [, {bufnr}])
768  */
769     void
770 f_prop_list(typval_T *argvars, typval_T *rettv)
771 {
772     linenr_T lnum = tv_get_number(&argvars[0]);
773     buf_T    *buf = curbuf;
774 
775     if (argvars[1].v_type != VAR_UNKNOWN)
776     {
777 	if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL)
778 	    return;
779     }
780     if (lnum < 1 || lnum > buf->b_ml.ml_line_count)
781     {
782 	emsg(_(e_invrange));
783 	return;
784     }
785 
786     if (rettv_list_alloc(rettv) == OK)
787     {
788 	char_u	    *text = ml_get_buf(buf, lnum, FALSE);
789 	size_t	    textlen = STRLEN(text) + 1;
790 	int	    count = (int)((buf->b_ml.ml_line_len - textlen)
791 							 / sizeof(textprop_T));
792 	int	    i;
793 	textprop_T  prop;
794 
795 	for (i = 0; i < count; ++i)
796 	{
797 	    dict_T *d = dict_alloc();
798 
799 	    if (d == NULL)
800 		break;
801 	    mch_memmove(&prop, text + textlen + i * sizeof(textprop_T),
802 							   sizeof(textprop_T));
803 	    prop_fill_dict(d, &prop, buf);
804 	    list_append_dict(rettv->vval.v_list, d);
805 	}
806     }
807 }
808 
809 /*
810  * prop_remove({props} [, {lnum} [, {lnum_end}]])
811  */
812     void
813 f_prop_remove(typval_T *argvars, typval_T *rettv)
814 {
815     linenr_T	start = 1;
816     linenr_T	end = 0;
817     linenr_T	lnum;
818     dict_T	*dict;
819     buf_T	*buf = curbuf;
820     int		do_all;
821     int		id = -1;
822     int		type_id = -1;
823     int		both;
824 
825     rettv->vval.v_number = 0;
826     if (argvars[0].v_type != VAR_DICT || argvars[0].vval.v_dict == NULL)
827     {
828 	emsg(_(e_invarg));
829 	return;
830     }
831 
832     if (argvars[1].v_type != VAR_UNKNOWN)
833     {
834 	start = tv_get_number(&argvars[1]);
835 	end = start;
836 	if (argvars[2].v_type != VAR_UNKNOWN)
837 	    end = tv_get_number(&argvars[2]);
838 	if (start < 1 || end < 1)
839 	{
840 	    emsg(_(e_invrange));
841 	    return;
842 	}
843     }
844 
845     dict = argvars[0].vval.v_dict;
846     if (get_bufnr_from_arg(&argvars[0], &buf) == FAIL)
847 	return;
848     if (buf->b_ml.ml_mfp == NULL)
849 	return;
850 
851     do_all = dict_get_bool(dict, (char_u *)"all", FALSE);
852 
853     if (dict_find(dict, (char_u *)"id", -1) != NULL)
854 	id = dict_get_number(dict, (char_u *)"id");
855     if (dict_find(dict, (char_u *)"type", -1))
856     {
857 	char_u	    *name = dict_get_string(dict, (char_u *)"type", FALSE);
858 	proptype_T  *type = lookup_prop_type(name, buf);
859 
860 	if (type == NULL)
861 	    return;
862 	type_id = type->pt_id;
863     }
864     both = dict_get_bool(dict, (char_u *)"both", FALSE);
865 
866     if (id == -1 && type_id == -1)
867     {
868 	emsg(_("E968: Need at least one of 'id' or 'type'"));
869 	return;
870     }
871     if (both && (id == -1 || type_id == -1))
872     {
873 	emsg(_("E860: Need 'id' and 'type' with 'both'"));
874 	return;
875     }
876 
877     if (end == 0)
878 	end = buf->b_ml.ml_line_count;
879     for (lnum = start; lnum <= end; ++lnum)
880     {
881 	char_u *text;
882 	size_t len;
883 
884 	if (lnum > buf->b_ml.ml_line_count)
885 	    break;
886 	text = ml_get_buf(buf, lnum, FALSE);
887 	len = STRLEN(text) + 1;
888 	if ((size_t)buf->b_ml.ml_line_len > len)
889 	{
890 	    static textprop_T	textprop;  // static because of alignment
891 	    unsigned		idx;
892 
893 	    for (idx = 0; idx < (buf->b_ml.ml_line_len - len)
894 						   / sizeof(textprop_T); ++idx)
895 	    {
896 		char_u *cur_prop = buf->b_ml.ml_line_ptr + len
897 						    + idx * sizeof(textprop_T);
898 		size_t	taillen;
899 
900 		mch_memmove(&textprop, cur_prop, sizeof(textprop_T));
901 		if (both ? textprop.tp_id == id && textprop.tp_type == type_id
902 			 : textprop.tp_id == id || textprop.tp_type == type_id)
903 		{
904 		    if (!(buf->b_ml.ml_flags & ML_LINE_DIRTY))
905 		    {
906 			char_u *newptr = alloc(buf->b_ml.ml_line_len);
907 
908 			// need to allocate the line to be able to change it
909 			if (newptr == NULL)
910 			    return;
911 			mch_memmove(newptr, buf->b_ml.ml_line_ptr,
912 							buf->b_ml.ml_line_len);
913 			buf->b_ml.ml_line_ptr = newptr;
914 			buf->b_ml.ml_flags |= ML_LINE_DIRTY;
915 
916 			cur_prop = buf->b_ml.ml_line_ptr + len
917 						    + idx * sizeof(textprop_T);
918 		    }
919 
920 		    taillen = buf->b_ml.ml_line_len - len
921 					      - (idx + 1) * sizeof(textprop_T);
922 		    if (taillen > 0)
923 			mch_memmove(cur_prop, cur_prop + sizeof(textprop_T),
924 								      taillen);
925 		    buf->b_ml.ml_line_len -= sizeof(textprop_T);
926 		    --idx;
927 
928 		    ++rettv->vval.v_number;
929 		    if (!do_all)
930 			break;
931 		}
932 	    }
933 	}
934     }
935     if (rettv->vval.v_number > 0)
936     {
937 	if (start == 1 && end == buf->b_ml.ml_line_count)
938 	    redraw_buf_later(buf, NOT_VALID);
939 	else
940 	{
941 	    changed_lines_buf(buf, start, end + 1, 0);
942 	    redraw_buf_later(buf, VALID);
943 	}
944     }
945 }
946 
947 /*
948  * Common for f_prop_type_add() and f_prop_type_change().
949  */
950     static void
951 prop_type_set(typval_T *argvars, int add)
952 {
953     char_u	*name;
954     buf_T	*buf = NULL;
955     dict_T	*dict;
956     dictitem_T  *di;
957     proptype_T	*prop;
958 
959     name = tv_get_string(&argvars[0]);
960     if (*name == NUL)
961     {
962 	emsg(_(e_invarg));
963 	return;
964     }
965 
966     if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL)
967 	return;
968     dict = argvars[1].vval.v_dict;
969 
970     prop = find_prop(name, buf);
971     if (add)
972     {
973 	hashtab_T **htp;
974 
975 	if (prop != NULL)
976 	{
977 	    semsg(_("E969: Property type %s already defined"), name);
978 	    return;
979 	}
980 	prop = alloc_clear(offsetof(proptype_T, pt_name) + STRLEN(name) + 1);
981 	if (prop == NULL)
982 	    return;
983 	STRCPY(prop->pt_name, name);
984 	prop->pt_id = ++proptype_id;
985 	prop->pt_flags = PT_FLAG_COMBINE;
986 	htp = buf == NULL ? &global_proptypes : &buf->b_proptypes;
987 	if (*htp == NULL)
988 	{
989 	    *htp = ALLOC_ONE(hashtab_T);
990 	    if (*htp == NULL)
991 	    {
992 		vim_free(prop);
993 		return;
994 	    }
995 	    hash_init(*htp);
996 	}
997 	hash_add(*htp, PT2HIKEY(prop));
998     }
999     else
1000     {
1001 	if (prop == NULL)
1002 	{
1003 	    semsg(_(e_type_not_exist), name);
1004 	    return;
1005 	}
1006     }
1007 
1008     if (dict != NULL)
1009     {
1010 	di = dict_find(dict, (char_u *)"highlight", -1);
1011 	if (di != NULL)
1012 	{
1013 	    char_u	*highlight;
1014 	    int		hl_id = 0;
1015 
1016 	    highlight = dict_get_string(dict, (char_u *)"highlight", FALSE);
1017 	    if (highlight != NULL && *highlight != NUL)
1018 		hl_id = syn_name2id(highlight);
1019 	    if (hl_id <= 0)
1020 	    {
1021 		semsg(_("E970: Unknown highlight group name: '%s'"),
1022 			highlight == NULL ? (char_u *)"" : highlight);
1023 		return;
1024 	    }
1025 	    prop->pt_hl_id = hl_id;
1026 	}
1027 
1028 	di = dict_find(dict, (char_u *)"combine", -1);
1029 	if (di != NULL)
1030 	{
1031 	    if (tv_get_bool(&di->di_tv))
1032 		prop->pt_flags |= PT_FLAG_COMBINE;
1033 	    else
1034 		prop->pt_flags &= ~PT_FLAG_COMBINE;
1035 	}
1036 
1037 	di = dict_find(dict, (char_u *)"priority", -1);
1038 	if (di != NULL)
1039 	    prop->pt_priority = tv_get_number(&di->di_tv);
1040 
1041 	di = dict_find(dict, (char_u *)"start_incl", -1);
1042 	if (di != NULL)
1043 	{
1044 	    if (tv_get_bool(&di->di_tv))
1045 		prop->pt_flags |= PT_FLAG_INS_START_INCL;
1046 	    else
1047 		prop->pt_flags &= ~PT_FLAG_INS_START_INCL;
1048 	}
1049 
1050 	di = dict_find(dict, (char_u *)"end_incl", -1);
1051 	if (di != NULL)
1052 	{
1053 	    if (tv_get_bool(&di->di_tv))
1054 		prop->pt_flags |= PT_FLAG_INS_END_INCL;
1055 	    else
1056 		prop->pt_flags &= ~PT_FLAG_INS_END_INCL;
1057 	}
1058     }
1059 }
1060 
1061 /*
1062  * prop_type_add({name}, {props})
1063  */
1064     void
1065 f_prop_type_add(typval_T *argvars, typval_T *rettv UNUSED)
1066 {
1067     prop_type_set(argvars, TRUE);
1068 }
1069 
1070 /*
1071  * prop_type_change({name}, {props})
1072  */
1073     void
1074 f_prop_type_change(typval_T *argvars, typval_T *rettv UNUSED)
1075 {
1076     prop_type_set(argvars, FALSE);
1077 }
1078 
1079 /*
1080  * prop_type_delete({name} [, {bufnr}])
1081  */
1082     void
1083 f_prop_type_delete(typval_T *argvars, typval_T *rettv UNUSED)
1084 {
1085     char_u	*name;
1086     buf_T	*buf = NULL;
1087     hashitem_T	*hi;
1088 
1089     name = tv_get_string(&argvars[0]);
1090     if (*name == NUL)
1091     {
1092 	emsg(_(e_invarg));
1093 	return;
1094     }
1095 
1096     if (argvars[1].v_type != VAR_UNKNOWN)
1097     {
1098 	if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL)
1099 	    return;
1100     }
1101 
1102     hi = find_prop_hi(name, buf);
1103     if (hi != NULL)
1104     {
1105 	hashtab_T	*ht;
1106 	proptype_T	*prop = HI2PT(hi);
1107 
1108 	if (buf == NULL)
1109 	    ht = global_proptypes;
1110 	else
1111 	    ht = buf->b_proptypes;
1112 	hash_remove(ht, hi);
1113 	vim_free(prop);
1114     }
1115 }
1116 
1117 /*
1118  * prop_type_get({name} [, {bufnr}])
1119  */
1120     void
1121 f_prop_type_get(typval_T *argvars, typval_T *rettv)
1122 {
1123     char_u *name = tv_get_string(&argvars[0]);
1124 
1125     if (*name == NUL)
1126     {
1127 	emsg(_(e_invarg));
1128 	return;
1129     }
1130     if (rettv_dict_alloc(rettv) == OK)
1131     {
1132 	proptype_T  *prop = NULL;
1133 	buf_T	    *buf = NULL;
1134 
1135 	if (argvars[1].v_type != VAR_UNKNOWN)
1136 	{
1137 	    if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL)
1138 		return;
1139 	}
1140 
1141 	prop = find_prop(name, buf);
1142 	if (prop != NULL)
1143 	{
1144 	    dict_T *d = rettv->vval.v_dict;
1145 
1146 	    if (prop->pt_hl_id > 0)
1147 		dict_add_string(d, "highlight", syn_id2name(prop->pt_hl_id));
1148 	    dict_add_number(d, "priority", prop->pt_priority);
1149 	    dict_add_number(d, "combine",
1150 				   (prop->pt_flags & PT_FLAG_COMBINE) ? 1 : 0);
1151 	    dict_add_number(d, "start_incl",
1152 			    (prop->pt_flags & PT_FLAG_INS_START_INCL) ? 1 : 0);
1153 	    dict_add_number(d, "end_incl",
1154 			      (prop->pt_flags & PT_FLAG_INS_END_INCL) ? 1 : 0);
1155 	    if (buf != NULL)
1156 		dict_add_number(d, "bufnr", buf->b_fnum);
1157 	}
1158     }
1159 }
1160 
1161     static void
1162 list_types(hashtab_T *ht, list_T *l)
1163 {
1164     long	todo;
1165     hashitem_T	*hi;
1166 
1167     todo = (long)ht->ht_used;
1168     for (hi = ht->ht_array; todo > 0; ++hi)
1169     {
1170 	if (!HASHITEM_EMPTY(hi))
1171 	{
1172 	    proptype_T *prop = HI2PT(hi);
1173 
1174 	    list_append_string(l, prop->pt_name, -1);
1175 	    --todo;
1176 	}
1177     }
1178 }
1179 
1180 /*
1181  * prop_type_list([{bufnr}])
1182  */
1183     void
1184 f_prop_type_list(typval_T *argvars, typval_T *rettv UNUSED)
1185 {
1186     buf_T *buf = NULL;
1187 
1188     if (rettv_list_alloc(rettv) == OK)
1189     {
1190 	if (argvars[0].v_type != VAR_UNKNOWN)
1191 	{
1192 	    if (get_bufnr_from_arg(&argvars[0], &buf) == FAIL)
1193 		return;
1194 	}
1195 	if (buf == NULL)
1196 	{
1197 	    if (global_proptypes != NULL)
1198 		list_types(global_proptypes, rettv->vval.v_list);
1199 	}
1200 	else if (buf->b_proptypes != NULL)
1201 	    list_types(buf->b_proptypes, rettv->vval.v_list);
1202     }
1203 }
1204 
1205 /*
1206  * Free all property types in "ht".
1207  */
1208     static void
1209 clear_ht_prop_types(hashtab_T *ht)
1210 {
1211     long	todo;
1212     hashitem_T	*hi;
1213 
1214     if (ht == NULL)
1215 	return;
1216 
1217     todo = (long)ht->ht_used;
1218     for (hi = ht->ht_array; todo > 0; ++hi)
1219     {
1220 	if (!HASHITEM_EMPTY(hi))
1221 	{
1222 	    proptype_T *prop = HI2PT(hi);
1223 
1224 	    vim_free(prop);
1225 	    --todo;
1226 	}
1227     }
1228 
1229     hash_clear(ht);
1230     vim_free(ht);
1231 }
1232 
1233 #if defined(EXITFREE) || defined(PROTO)
1234 /*
1235  * Free all global property types.
1236  */
1237     void
1238 clear_global_prop_types(void)
1239 {
1240     clear_ht_prop_types(global_proptypes);
1241     global_proptypes = NULL;
1242 }
1243 #endif
1244 
1245 /*
1246  * Free all property types for "buf".
1247  */
1248     void
1249 clear_buf_prop_types(buf_T *buf)
1250 {
1251     clear_ht_prop_types(buf->b_proptypes);
1252     buf->b_proptypes = NULL;
1253 }
1254 
1255 // Struct used to return two values from adjust_prop().
1256 typedef struct
1257 {
1258     int dirty;	    // if the property was changed
1259     int can_drop;   // whether after this change, the prop may be removed
1260 } adjustres_T;
1261 
1262 /*
1263  * Adjust the property for "added" bytes (can be negative) inserted at "col".
1264  *
1265  * Note that "col" is zero-based, while tp_col is one-based.
1266  * Only for the current buffer.
1267  * "flags" can have:
1268  * APC_SUBSTITUTE:	Text is replaced, not inserted.
1269  */
1270     static adjustres_T
1271 adjust_prop(
1272 	textprop_T *prop,
1273 	colnr_T col,
1274 	int added,
1275 	int flags)
1276 {
1277     proptype_T	*pt = text_prop_type_by_id(curbuf, prop->tp_type);
1278     int		start_incl = (pt != NULL
1279 				    && (pt->pt_flags & PT_FLAG_INS_START_INCL))
1280 						   || (flags & APC_SUBSTITUTE);
1281     int		end_incl = (pt != NULL
1282 				     && (pt->pt_flags & PT_FLAG_INS_END_INCL));
1283 		// Do not drop zero-width props if they later can increase in
1284 		// size.
1285     int		droppable = !(start_incl || end_incl);
1286     adjustres_T res = {TRUE, FALSE};
1287 
1288     if (added > 0)
1289     {
1290 	if (col + 1 <= prop->tp_col
1291 		- (start_incl || (prop->tp_len == 0 && end_incl)))
1292 	    // Change is entirely before the text property: Only shift
1293 	    prop->tp_col += added;
1294 	else if (col + 1 < prop->tp_col + prop->tp_len + end_incl)
1295 	    // Insertion was inside text property
1296 	    prop->tp_len += added;
1297     }
1298     else if (prop->tp_col > col + 1)
1299     {
1300 	if (prop->tp_col + added < col + 1)
1301 	{
1302 	    prop->tp_len += (prop->tp_col - 1 - col) + added;
1303 	    prop->tp_col = col + 1;
1304 	    if (prop->tp_len <= 0)
1305 	    {
1306 		prop->tp_len = 0;
1307 		res.can_drop = droppable;
1308 	    }
1309 	}
1310 	else
1311 	    prop->tp_col += added;
1312     }
1313     else if (prop->tp_len > 0 && prop->tp_col + prop->tp_len > col)
1314     {
1315 	int after = col - added - (prop->tp_col - 1 + prop->tp_len);
1316 
1317 	prop->tp_len += after > 0 ? added + after : added;
1318 	res.can_drop = prop->tp_len <= 0 && droppable;
1319     }
1320     else
1321 	res.dirty = FALSE;
1322 
1323     return res;
1324 }
1325 
1326 /*
1327  * Adjust the columns of text properties in line "lnum" after position "col" to
1328  * shift by "bytes_added" (can be negative).
1329  * Note that "col" is zero-based, while tp_col is one-based.
1330  * Only for the current buffer.
1331  * "flags" can have:
1332  * APC_SAVE_FOR_UNDO:	Call u_savesub() before making changes to the line.
1333  * APC_SUBSTITUTE:	Text is replaced, not inserted.
1334  * Caller is expected to check b_has_textprop and "bytes_added" being non-zero.
1335  * Returns TRUE when props were changed.
1336  */
1337     int
1338 adjust_prop_columns(
1339 	linenr_T    lnum,
1340 	colnr_T	    col,
1341 	int	    bytes_added,
1342 	int	    flags)
1343 {
1344     int		proplen;
1345     char_u	*props;
1346     int		dirty = FALSE;
1347     int		ri, wi;
1348     size_t	textlen;
1349 
1350     if (text_prop_frozen > 0)
1351 	return FALSE;
1352 
1353     proplen = get_text_props(curbuf, lnum, &props, TRUE);
1354     if (proplen == 0)
1355 	return FALSE;
1356     textlen = curbuf->b_ml.ml_line_len - proplen * sizeof(textprop_T);
1357 
1358     wi = 0; // write index
1359     for (ri = 0; ri < proplen; ++ri)
1360     {
1361 	textprop_T	prop;
1362 	adjustres_T	res;
1363 
1364 	mch_memmove(&prop, props + ri * sizeof(prop), sizeof(prop));
1365 	res = adjust_prop(&prop, col, bytes_added, flags);
1366 	if (res.dirty)
1367 	{
1368 	    // Save for undo if requested and not done yet.
1369 	    if ((flags & APC_SAVE_FOR_UNDO) && !dirty
1370 						    && u_savesub(lnum) == FAIL)
1371 		return FALSE;
1372 	    dirty = TRUE;
1373 
1374 	    // u_savesub() may have updated curbuf->b_ml, fetch it again
1375 	    if (curbuf->b_ml.ml_line_lnum != lnum)
1376 		proplen = get_text_props(curbuf, lnum, &props, TRUE);
1377 	}
1378 	if (res.can_drop)
1379 	    continue; // Drop this text property
1380 	mch_memmove(props + wi * sizeof(textprop_T), &prop, sizeof(textprop_T));
1381 	++wi;
1382     }
1383     if (dirty)
1384     {
1385 	colnr_T newlen = (int)textlen + wi * (colnr_T)sizeof(textprop_T);
1386 
1387 	if ((curbuf->b_ml.ml_flags & ML_LINE_DIRTY) == 0)
1388 	    curbuf->b_ml.ml_line_ptr =
1389 				 vim_memsave(curbuf->b_ml.ml_line_ptr, newlen);
1390 	curbuf->b_ml.ml_flags |= ML_LINE_DIRTY;
1391 	curbuf->b_ml.ml_line_len = newlen;
1392     }
1393     return dirty;
1394 }
1395 
1396 /*
1397  * Adjust text properties for a line that was split in two.
1398  * "lnum_props" is the line that has the properties from before the split.
1399  * "lnum_top" is the top line.
1400  * "kept" is the number of bytes kept in the first line, while
1401  * "deleted" is the number of bytes deleted.
1402  */
1403     void
1404 adjust_props_for_split(
1405 	linenr_T lnum_props,
1406 	linenr_T lnum_top,
1407 	int kept,
1408 	int deleted)
1409 {
1410     char_u	*props;
1411     int		count;
1412     garray_T    prevprop;
1413     garray_T    nextprop;
1414     int		i;
1415     int		skipped = kept + deleted;
1416 
1417     if (!curbuf->b_has_textprop)
1418 	return;
1419 
1420     // Get the text properties from "lnum_props".
1421     count = get_text_props(curbuf, lnum_props, &props, FALSE);
1422     ga_init2(&prevprop, sizeof(textprop_T), 10);
1423     ga_init2(&nextprop, sizeof(textprop_T), 10);
1424 
1425     // Keep the relevant ones in the first line, reducing the length if needed.
1426     // Copy the ones that include the split to the second line.
1427     // Move the ones after the split to the second line.
1428     for (i = 0; i < count; ++i)
1429     {
1430 	textprop_T  prop;
1431 	proptype_T *pt;
1432 	int	    start_incl, end_incl;
1433 	int	    cont_prev, cont_next;
1434 
1435 	// copy the prop to an aligned structure
1436 	mch_memmove(&prop, props + i * sizeof(textprop_T), sizeof(textprop_T));
1437 
1438 	pt = text_prop_type_by_id(curbuf, prop.tp_type);
1439 	start_incl = (pt != NULL && (pt->pt_flags & PT_FLAG_INS_START_INCL));
1440 	end_incl = (pt != NULL && (pt->pt_flags & PT_FLAG_INS_END_INCL));
1441 	cont_prev = prop.tp_col + !start_incl <= kept;
1442 	cont_next = skipped <= prop.tp_col + prop.tp_len - !end_incl;
1443 
1444 	if (cont_prev && ga_grow(&prevprop, 1) == OK)
1445 	{
1446 	    textprop_T *p = ((textprop_T *)prevprop.ga_data) + prevprop.ga_len;
1447 
1448 	    *p = prop;
1449 	    ++prevprop.ga_len;
1450 	    if (p->tp_col + p->tp_len >= kept)
1451 		p->tp_len = kept - p->tp_col;
1452 	    if (cont_next)
1453 		p->tp_flags |= TP_FLAG_CONT_NEXT;
1454 	}
1455 
1456 	// Only add the property to the next line if the length is bigger than
1457 	// zero.
1458 	if (cont_next && ga_grow(&nextprop, 1) == OK)
1459 	{
1460 	    textprop_T *p = ((textprop_T *)nextprop.ga_data) + nextprop.ga_len;
1461 	    *p = prop;
1462 	    ++nextprop.ga_len;
1463 	    if (p->tp_col > skipped)
1464 		p->tp_col -= skipped - 1;
1465 	    else
1466 	    {
1467 		p->tp_len -= skipped - p->tp_col;
1468 		p->tp_col = 1;
1469 	    }
1470 	    if (cont_prev)
1471 		p->tp_flags |= TP_FLAG_CONT_PREV;
1472 	}
1473     }
1474 
1475     set_text_props(lnum_top, prevprop.ga_data,
1476 					 prevprop.ga_len * sizeof(textprop_T));
1477     ga_clear(&prevprop);
1478     set_text_props(lnum_top + 1, nextprop.ga_data,
1479 					 nextprop.ga_len * sizeof(textprop_T));
1480     ga_clear(&nextprop);
1481 }
1482 
1483 /*
1484  * Prepend properties of joined line "lnum" to "new_props".
1485  */
1486     void
1487 prepend_joined_props(
1488 	char_u	    *new_props,
1489 	int	    propcount,
1490 	int	    *props_remaining,
1491 	linenr_T    lnum,
1492 	int	    add_all,
1493 	long	    col,
1494 	int	    removed)
1495 {
1496     char_u *props;
1497     int	    proplen = get_text_props(curbuf, lnum, &props, FALSE);
1498     int	    i;
1499 
1500     for (i = proplen; i-- > 0; )
1501     {
1502 	textprop_T  prop;
1503 	int	    end;
1504 
1505 	mch_memmove(&prop, props + i * sizeof(prop), sizeof(prop));
1506 	end = !(prop.tp_flags & TP_FLAG_CONT_NEXT);
1507 
1508 	adjust_prop(&prop, 0, -removed, 0); // Remove leading spaces
1509 	adjust_prop(&prop, -1, col, 0); // Make line start at its final column
1510 
1511 	if (add_all || end)
1512 	    mch_memmove(new_props + --(*props_remaining) * sizeof(prop),
1513 							  &prop, sizeof(prop));
1514 	else
1515 	{
1516 	    int j;
1517 	    int	found = FALSE;
1518 
1519 	    // Search for continuing prop.
1520 	    for (j = *props_remaining; j < propcount; ++j)
1521 	    {
1522 		textprop_T op;
1523 
1524 		mch_memmove(&op, new_props + j * sizeof(op), sizeof(op));
1525 		if ((op.tp_flags & TP_FLAG_CONT_PREV)
1526 			&& op.tp_id == prop.tp_id && op.tp_type == prop.tp_type)
1527 		{
1528 		    found = TRUE;
1529 		    op.tp_len += op.tp_col - prop.tp_col;
1530 		    op.tp_col = prop.tp_col;
1531 		    // Start/end is taken care of when deleting joined lines
1532 		    op.tp_flags = prop.tp_flags;
1533 		    mch_memmove(new_props + j * sizeof(op), &op, sizeof(op));
1534 		    break;
1535 		}
1536 	    }
1537 	    if (!found)
1538 		internal_error("text property above joined line not found");
1539 	}
1540     }
1541 }
1542 
1543 #endif // FEAT_PROP_POPUP
1544