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