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