xref: /vim-8.2.3635/src/textprop.c (revision 94688b8a)
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.
12  *
13  * Text properties are attached to the text.  They move with the text when
14  * text is inserted/deleted.
15  *
16  * Text properties have a user specified ID number, which can be unique.
17  * Text properties have a type, which can be used to specify highlighting.
18  *
19  * TODO:
20  * - Adjust text property column and length when text is inserted/deleted.
21  *   -> a :substitute with a multi-line match
22  *   -> search for changed_bytes() from misc1.c
23  * - Perhaps we only need TP_FLAG_CONT_NEXT and can drop TP_FLAG_CONT_PREV?
24  * - Add an arrray for global_proptypes, to quickly lookup a prop type by ID
25  * - Add an arrray for b_proptypes, to quickly lookup a prop type by ID
26  * - Checking the text length to detect text properties is slow.  Use a flag in
27  *   the index, like DB_MARKED?
28  * - Also test line2byte() with many lines, so that ml_updatechunk() is taken
29  *   into account.
30  * - add mechanism to keep track of changed lines.
31  */
32 
33 #include "vim.h"
34 
35 #if defined(FEAT_TEXT_PROP) || defined(PROTO)
36 
37 /*
38  * In a hashtable item "hi_key" points to "pt_name" in a proptype_T.
39  * This avoids adding a pointer to the hashtable item.
40  * PT2HIKEY() converts a proptype pointer to a hashitem key pointer.
41  * HIKEY2PT() converts a hashitem key pointer to a proptype pointer.
42  * HI2PT() converts a hashitem pointer to a proptype pointer.
43  */
44 #define PT2HIKEY(p)  ((p)->pt_name)
45 #define HIKEY2PT(p)   ((proptype_T *)((p) - offsetof(proptype_T, pt_name)))
46 #define HI2PT(hi)      HIKEY2PT((hi)->hi_key)
47 
48 // The global text property types.
49 static hashtab_T *global_proptypes = NULL;
50 
51 // The last used text property type ID.
52 static int proptype_id = 0;
53 
54 static char_u e_type_not_exist[] = N_("E971: Property type %s does not exist");
55 static char_u e_invalid_col[] = N_("E964: Invalid column number: %ld");
56 static char_u e_invalid_lnum[] = N_("E966: Invalid line number: %ld");
57 
58 /*
59  * Find a property type by name, return the hashitem.
60  * Returns NULL if the item can't be found.
61  */
62     static hashitem_T *
63 find_prop_hi(char_u *name, buf_T *buf)
64 {
65     hashtab_T	*ht;
66     hashitem_T	*hi;
67 
68     if (*name == NUL)
69 	return NULL;
70     if (buf == NULL)
71 	ht = global_proptypes;
72     else
73 	ht = buf->b_proptypes;
74 
75     if (ht == NULL)
76 	return NULL;
77     hi = hash_find(ht, name);
78     if (HASHITEM_EMPTY(hi))
79 	return NULL;
80     return hi;
81 }
82 
83 /*
84  * Like find_prop_hi() but return the property type.
85  */
86     static proptype_T *
87 find_prop(char_u *name, buf_T *buf)
88 {
89     hashitem_T	*hi = find_prop_hi(name, buf);
90 
91     if (hi == NULL)
92 	return NULL;
93     return HI2PT(hi);
94 }
95 
96 /*
97  * Lookup a property type by name.  First in "buf" and when not found in the
98  * global types.
99  * When not found gives an error message and returns NULL.
100  */
101     static proptype_T *
102 lookup_prop_type(char_u *name, buf_T *buf)
103 {
104     proptype_T *type = find_prop(name, buf);
105 
106     if (type == NULL)
107 	type = find_prop(name, NULL);
108     if (type == NULL)
109 	semsg(_(e_type_not_exist), name);
110     return type;
111 }
112 
113 /*
114  * Get an optional "bufnr" item from the dict in "arg".
115  * When the argument is not used or "bufnr" is not present then "buf" is
116  * unchanged.
117  * If "bufnr" is valid or not present return OK.
118  * When "arg" is not a dict or "bufnr" is invalide return FAIL.
119  */
120     static int
121 get_bufnr_from_arg(typval_T *arg, buf_T **buf)
122 {
123     dictitem_T	*di;
124 
125     if (arg->v_type != VAR_DICT)
126     {
127 	emsg(_(e_dictreq));
128 	return FAIL;
129     }
130     if (arg->vval.v_dict == NULL)
131 	return OK;  // NULL dict is like an empty dict
132     di = dict_find(arg->vval.v_dict, (char_u *)"bufnr", -1);
133     if (di != NULL)
134     {
135 	*buf = tv_get_buf(&di->di_tv, FALSE);
136 	if (*buf == NULL)
137 	    return FAIL;
138     }
139     return OK;
140 }
141 
142 /*
143  * prop_add({lnum}, {col}, {props})
144  */
145     void
146 f_prop_add(typval_T *argvars, typval_T *rettv UNUSED)
147 {
148     linenr_T	lnum;
149     linenr_T	start_lnum;
150     linenr_T	end_lnum;
151     colnr_T	start_col;
152     colnr_T	end_col;
153     dict_T	*dict;
154     char_u	*type_name;
155     proptype_T	*type;
156     buf_T	*buf = curbuf;
157     int		id = 0;
158     char_u	*newtext;
159     int		proplen;
160     size_t	textlen;
161     char_u	*props;
162     char_u	*newprops;
163     textprop_T	tmp_prop;
164     int		i;
165 
166     start_lnum = tv_get_number(&argvars[0]);
167     start_col = tv_get_number(&argvars[1]);
168     if (start_col < 1)
169     {
170 	semsg(_(e_invalid_col), (long)start_col);
171 	return;
172     }
173     if (argvars[2].v_type != VAR_DICT)
174     {
175 	emsg(_(e_dictreq));
176 	return;
177     }
178     dict = argvars[2].vval.v_dict;
179 
180     if (dict == NULL || dict_find(dict, (char_u *)"type", -1) == NULL)
181     {
182 	emsg(_("E965: missing property type name"));
183 	return;
184     }
185     type_name = dict_get_string(dict, (char_u *)"type", FALSE);
186 
187     if (dict_find(dict, (char_u *)"end_lnum", -1) != NULL)
188     {
189 	end_lnum = dict_get_number(dict, (char_u *)"end_lnum");
190 	if (end_lnum < start_lnum)
191 	{
192 	    semsg(_(e_invargval), "end_lnum");
193 	    return;
194 	}
195     }
196     else
197 	end_lnum = start_lnum;
198 
199     if (dict_find(dict, (char_u *)"length", -1) != NULL)
200     {
201 	long length = dict_get_number(dict, (char_u *)"length");
202 
203 	if (length < 0 || end_lnum > start_lnum)
204 	{
205 	    semsg(_(e_invargval), "length");
206 	    return;
207 	}
208 	end_col = start_col + length;
209     }
210     else if (dict_find(dict, (char_u *)"end_col", -1) != NULL)
211     {
212 	end_col = dict_get_number(dict, (char_u *)"end_col");
213 	if (end_col <= 0)
214 	{
215 	    semsg(_(e_invargval), "end_col");
216 	    return;
217 	}
218     }
219     else if (start_lnum == end_lnum)
220 	end_col = start_col;
221     else
222 	end_col = 1;
223 
224     if (dict_find(dict, (char_u *)"id", -1) != NULL)
225 	id = dict_get_number(dict, (char_u *)"id");
226 
227     if (get_bufnr_from_arg(&argvars[2], &buf) == FAIL)
228 	return;
229 
230     type = lookup_prop_type(type_name, buf);
231     if (type == NULL)
232 	return;
233 
234     if (start_lnum < 1 || start_lnum > buf->b_ml.ml_line_count)
235     {
236 	semsg(_(e_invalid_lnum), (long)start_lnum);
237 	return;
238     }
239     if (end_lnum < start_lnum || end_lnum > buf->b_ml.ml_line_count)
240     {
241 	semsg(_(e_invalid_lnum), (long)end_lnum);
242 	return;
243     }
244 
245     for (lnum = start_lnum; lnum <= end_lnum; ++lnum)
246     {
247 	colnr_T col;	// start column
248 	long	length;	// in bytes
249 
250 	// Fetch the line to get the ml_line_len field updated.
251 	proplen = get_text_props(buf, lnum, &props, TRUE);
252 	textlen = buf->b_ml.ml_line_len - proplen * sizeof(textprop_T);
253 
254 	if (lnum == start_lnum)
255 	    col = start_col;
256 	else
257 	    col = 1;
258 	if (col - 1 > (colnr_T)textlen)
259 	{
260 	    semsg(_(e_invalid_col), (long)start_col);
261 	    return;
262 	}
263 
264 	if (lnum == end_lnum)
265 	    length = end_col - col;
266 	else
267 	    length = (int)textlen - col + 1;
268 	if (length > (long)textlen)
269 	    length = (int)textlen;	// can include the end-of-line
270 	if (length < 0)
271 	    length = 0;		// zero-width property
272 
273 	// Allocate the new line with space for the new proprety.
274 	newtext = alloc(buf->b_ml.ml_line_len + sizeof(textprop_T));
275 	if (newtext == NULL)
276 	    return;
277 	// Copy the text, including terminating NUL.
278 	mch_memmove(newtext, buf->b_ml.ml_line_ptr, textlen);
279 
280 	// Find the index where to insert the new property.
281 	// Since the text properties are not aligned properly when stored with the
282 	// text, we need to copy them as bytes before using it as a struct.
283 	for (i = 0; i < proplen; ++i)
284 	{
285 	    mch_memmove(&tmp_prop, props + i * sizeof(textprop_T),
286 							       sizeof(textprop_T));
287 	    if (tmp_prop.tp_col >= col)
288 		break;
289 	}
290 	newprops = newtext + textlen;
291 	if (i > 0)
292 	    mch_memmove(newprops, props, sizeof(textprop_T) * i);
293 
294 	tmp_prop.tp_col = col;
295 	tmp_prop.tp_len = length;
296 	tmp_prop.tp_id = id;
297 	tmp_prop.tp_type = type->pt_id;
298 	tmp_prop.tp_flags = (lnum > start_lnum ? TP_FLAG_CONT_PREV : 0)
299 			  | (lnum < end_lnum ? TP_FLAG_CONT_NEXT : 0);
300 	mch_memmove(newprops + i * sizeof(textprop_T), &tmp_prop,
301 							       sizeof(textprop_T));
302 
303 	if (i < proplen)
304 	    mch_memmove(newprops + (i + 1) * sizeof(textprop_T),
305 					    props + i * sizeof(textprop_T),
306 					    sizeof(textprop_T) * (proplen - i));
307 
308 	if (buf->b_ml.ml_flags & ML_LINE_DIRTY)
309 	    vim_free(buf->b_ml.ml_line_ptr);
310 	buf->b_ml.ml_line_ptr = newtext;
311 	buf->b_ml.ml_line_len += sizeof(textprop_T);
312 	buf->b_ml.ml_flags |= ML_LINE_DIRTY;
313     }
314 
315     buf->b_has_textprop = TRUE;  // this is never reset
316     redraw_buf_later(buf, NOT_VALID);
317 }
318 
319 /*
320  * Fetch the text properties for line "lnum" in buffer "buf".
321  * Returns the number of text properties and, when non-zero, a pointer to the
322  * first one in "props" (note that it is not aligned, therefore the char_u
323  * pointer).
324  */
325     int
326 get_text_props(buf_T *buf, linenr_T lnum, char_u **props, int will_change)
327 {
328     char_u *text;
329     size_t textlen;
330     size_t proplen;
331 
332     // Be quick when no text property types have been defined or the buffer,
333     // unless we are adding one.
334     if (!buf->b_has_textprop && !will_change)
335 	return 0;
336 
337     // Fetch the line to get the ml_line_len field updated.
338     text = ml_get_buf(buf, lnum, will_change);
339     textlen = STRLEN(text) + 1;
340     proplen = buf->b_ml.ml_line_len - textlen;
341     if (proplen % sizeof(textprop_T) != 0)
342     {
343 	iemsg(_("E967: text property info corrupted"));
344 	return 0;
345     }
346     if (proplen > 0)
347 	*props = text + textlen;
348     return (int)(proplen / sizeof(textprop_T));
349 }
350 
351 /*
352  * Set the text properties for line "lnum" to "props" with length "len".
353  * If "len" is zero text properties are removed, "props" is not used.
354  * Any existing text properties are dropped.
355  * Only works for the current buffer.
356  */
357     static void
358 set_text_props(linenr_T lnum, char_u *props, int len)
359 {
360     char_u  *text;
361     char_u  *newtext;
362     int	    textlen;
363 
364     text = ml_get(lnum);
365     textlen = (int)STRLEN(text) + 1;
366     newtext = alloc(textlen + len);
367     if (newtext == NULL)
368 	return;
369     mch_memmove(newtext, text, textlen);
370     if (len > 0)
371 	mch_memmove(newtext + textlen, props, len);
372     if (curbuf->b_ml.ml_flags & ML_LINE_DIRTY)
373 	vim_free(curbuf->b_ml.ml_line_ptr);
374     curbuf->b_ml.ml_line_ptr = newtext;
375     curbuf->b_ml.ml_line_len = textlen + len;
376     curbuf->b_ml.ml_flags |= ML_LINE_DIRTY;
377 }
378 
379     static proptype_T *
380 find_type_by_id(hashtab_T *ht, int id)
381 {
382     long	todo;
383     hashitem_T	*hi;
384 
385     if (ht == NULL)
386 	return NULL;
387 
388     // TODO: Make this faster by keeping a list of types sorted on ID and use
389     // a binary search.
390 
391     todo = (long)ht->ht_used;
392     for (hi = ht->ht_array; todo > 0; ++hi)
393     {
394 	if (!HASHITEM_EMPTY(hi))
395 	{
396 	    proptype_T *prop = HI2PT(hi);
397 
398 	    if (prop->pt_id == id)
399 		return prop;
400 	    --todo;
401 	}
402     }
403     return NULL;
404 }
405 
406 /*
407  * Find a property type by ID in "buf" or globally.
408  * Returns NULL if not found.
409  */
410     proptype_T *
411 text_prop_type_by_id(buf_T *buf, int id)
412 {
413     proptype_T *type;
414 
415     type = find_type_by_id(buf->b_proptypes, id);
416     if (type == NULL)
417 	type = find_type_by_id(global_proptypes, id);
418     return type;
419 }
420 
421 /*
422  * prop_clear({lnum} [, {lnum_end} [, {bufnr}]])
423  */
424     void
425 f_prop_clear(typval_T *argvars, typval_T *rettv UNUSED)
426 {
427     linenr_T start = tv_get_number(&argvars[0]);
428     linenr_T end = start;
429     linenr_T lnum;
430     buf_T    *buf = curbuf;
431 
432     if (argvars[1].v_type != VAR_UNKNOWN)
433     {
434 	end = tv_get_number(&argvars[1]);
435 	if (argvars[2].v_type != VAR_UNKNOWN)
436 	{
437 	    if (get_bufnr_from_arg(&argvars[2], &buf) == FAIL)
438 		return;
439 	}
440     }
441     if (start < 1 || end < 1)
442     {
443 	emsg(_(e_invrange));
444 	return;
445     }
446 
447     for (lnum = start; lnum <= end; ++lnum)
448     {
449 	char_u *text;
450 	size_t len;
451 
452 	if (lnum > buf->b_ml.ml_line_count)
453 	    break;
454 	text = ml_get_buf(buf, lnum, FALSE);
455 	len = STRLEN(text) + 1;
456 	if ((size_t)buf->b_ml.ml_line_len > len)
457 	{
458 	    if (!(buf->b_ml.ml_flags & ML_LINE_DIRTY))
459 	    {
460 		char_u *newtext = vim_strsave(text);
461 
462 		// need to allocate the line now
463 		if (newtext == NULL)
464 		    return;
465 		buf->b_ml.ml_line_ptr = newtext;
466 		buf->b_ml.ml_flags |= ML_LINE_DIRTY;
467 	    }
468 	    buf->b_ml.ml_line_len = (int)len;
469 	}
470     }
471     redraw_buf_later(buf, NOT_VALID);
472 }
473 
474 /*
475  * prop_list({lnum} [, {bufnr}])
476  */
477     void
478 f_prop_list(typval_T *argvars, typval_T *rettv)
479 {
480     linenr_T lnum = tv_get_number(&argvars[0]);
481     buf_T    *buf = curbuf;
482 
483     if (argvars[1].v_type != VAR_UNKNOWN)
484     {
485 	if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL)
486 	    return;
487     }
488     if (lnum < 1 || lnum > buf->b_ml.ml_line_count)
489     {
490 	emsg(_(e_invrange));
491 	return;
492     }
493 
494     if (rettv_list_alloc(rettv) == OK)
495     {
496 	char_u	    *text = ml_get_buf(buf, lnum, FALSE);
497 	size_t	    textlen = STRLEN(text) + 1;
498 	int	    count = (int)((buf->b_ml.ml_line_len - textlen)
499 							 / sizeof(textprop_T));
500 	int	    i;
501 	textprop_T  prop;
502 	proptype_T  *pt;
503 
504 	for (i = 0; i < count; ++i)
505 	{
506 	    dict_T *d = dict_alloc();
507 
508 	    if (d == NULL)
509 		break;
510 	    mch_memmove(&prop, text + textlen + i * sizeof(textprop_T),
511 							   sizeof(textprop_T));
512 	    dict_add_number(d, "col", prop.tp_col);
513 	    dict_add_number(d, "length", prop.tp_len);
514 	    dict_add_number(d, "id", prop.tp_id);
515 	    dict_add_number(d, "start", !(prop.tp_flags & TP_FLAG_CONT_PREV));
516 	    dict_add_number(d, "end", !(prop.tp_flags & TP_FLAG_CONT_NEXT));
517 	    pt = text_prop_type_by_id(buf, prop.tp_type);
518 	    if (pt != NULL)
519 		dict_add_string(d, "type", pt->pt_name);
520 
521 	    list_append_dict(rettv->vval.v_list, d);
522 	}
523     }
524 }
525 
526 /*
527  * prop_remove({props} [, {lnum} [, {lnum_end}]])
528  */
529     void
530 f_prop_remove(typval_T *argvars, typval_T *rettv)
531 {
532     linenr_T	start = 1;
533     linenr_T	end = 0;
534     linenr_T	lnum;
535     dict_T	*dict;
536     buf_T	*buf = curbuf;
537     dictitem_T	*di;
538     int		do_all = FALSE;
539     int		id = -1;
540     int		type_id = -1;
541 
542     rettv->vval.v_number = 0;
543     if (argvars[0].v_type != VAR_DICT || argvars[0].vval.v_dict == NULL)
544     {
545 	emsg(_(e_invarg));
546 	return;
547     }
548 
549     if (argvars[1].v_type != VAR_UNKNOWN)
550     {
551 	start = tv_get_number(&argvars[1]);
552 	end = start;
553 	if (argvars[2].v_type != VAR_UNKNOWN)
554 	    end = tv_get_number(&argvars[2]);
555 	if (start < 1 || end < 1)
556 	{
557 	    emsg(_(e_invrange));
558 	    return;
559 	}
560     }
561 
562     dict = argvars[0].vval.v_dict;
563     di = dict_find(dict, (char_u *)"bufnr", -1);
564     if (di != NULL)
565     {
566 	buf = tv_get_buf(&di->di_tv, FALSE);
567 	if (buf == NULL)
568 	    return;
569     }
570 
571     di = dict_find(dict, (char_u*)"all", -1);
572     if (di != NULL)
573 	do_all = dict_get_number(dict, (char_u *)"all");
574 
575     if (dict_find(dict, (char_u *)"id", -1) != NULL)
576 	id = dict_get_number(dict, (char_u *)"id");
577     if (dict_find(dict, (char_u *)"type", -1))
578     {
579 	char_u	    *name = dict_get_string(dict, (char_u *)"type", FALSE);
580 	proptype_T  *type = lookup_prop_type(name, buf);
581 
582 	if (type == NULL)
583 	    return;
584 	type_id = type->pt_id;
585     }
586     if (id == -1 && type_id == -1)
587     {
588 	emsg(_("E968: Need at least one of 'id' or 'type'"));
589 	return;
590     }
591 
592     if (end == 0)
593 	end = buf->b_ml.ml_line_count;
594     for (lnum = start; lnum <= end; ++lnum)
595     {
596 	char_u *text;
597 	size_t len;
598 
599 	if (lnum > buf->b_ml.ml_line_count)
600 	    break;
601 	text = ml_get_buf(buf, lnum, FALSE);
602 	len = STRLEN(text) + 1;
603 	if ((size_t)buf->b_ml.ml_line_len > len)
604 	{
605 	    static textprop_T textprop;  // static because of alignment
606 	    unsigned          idx;
607 
608 	    for (idx = 0; idx < (buf->b_ml.ml_line_len - len)
609 						   / sizeof(textprop_T); ++idx)
610 	    {
611 		char_u *cur_prop = buf->b_ml.ml_line_ptr + len
612 						    + idx * sizeof(textprop_T);
613 		size_t	taillen;
614 
615 		mch_memmove(&textprop, cur_prop, sizeof(textprop_T));
616 		if (textprop.tp_id == id || textprop.tp_type == type_id)
617 		{
618 		    if (!(buf->b_ml.ml_flags & ML_LINE_DIRTY))
619 		    {
620 			char_u *newptr = alloc(buf->b_ml.ml_line_len);
621 
622 			// need to allocate the line to be able to change it
623 			if (newptr == NULL)
624 			    return;
625 			mch_memmove(newptr, buf->b_ml.ml_line_ptr,
626 							buf->b_ml.ml_line_len);
627 			buf->b_ml.ml_line_ptr = newptr;
628 			curbuf->b_ml.ml_flags |= ML_LINE_DIRTY;
629 		    }
630 
631 		    taillen = buf->b_ml.ml_line_len - len
632 					      - (idx + 1) * sizeof(textprop_T);
633 		    if (taillen > 0)
634 			mch_memmove(cur_prop, cur_prop + sizeof(textprop_T),
635 								      taillen);
636 		    buf->b_ml.ml_line_len -= sizeof(textprop_T);
637 		    --idx;
638 
639 		    ++rettv->vval.v_number;
640 		    if (!do_all)
641 			break;
642 		}
643 	    }
644 	}
645     }
646     redraw_buf_later(buf, NOT_VALID);
647 }
648 
649 /*
650  * Common for f_prop_type_add() and f_prop_type_change().
651  */
652     void
653 prop_type_set(typval_T *argvars, int add)
654 {
655     char_u	*name;
656     buf_T	*buf = NULL;
657     dict_T	*dict;
658     dictitem_T  *di;
659     proptype_T	*prop;
660 
661     name = tv_get_string(&argvars[0]);
662     if (*name == NUL)
663     {
664 	emsg(_(e_invarg));
665 	return;
666     }
667 
668     if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL)
669 	return;
670     dict = argvars[1].vval.v_dict;
671 
672     prop = find_prop(name, buf);
673     if (add)
674     {
675 	hashtab_T **htp;
676 
677 	if (prop != NULL)
678 	{
679 	    semsg(_("E969: Property type %s already defined"), name);
680 	    return;
681 	}
682 	prop = (proptype_T *)alloc_clear((int)(sizeof(proptype_T) + STRLEN(name)));
683 	if (prop == NULL)
684 	    return;
685 	STRCPY(prop->pt_name, name);
686 	prop->pt_id = ++proptype_id;
687 	htp = buf == NULL ? &global_proptypes : &buf->b_proptypes;
688 	if (*htp == NULL)
689 	{
690 	    *htp = (hashtab_T *)alloc(sizeof(hashtab_T));
691 	    if (*htp == NULL)
692 	    {
693 		vim_free(prop);
694 		return;
695 	    }
696 	    hash_init(*htp);
697 	}
698 	hash_add(*htp, PT2HIKEY(prop));
699     }
700     else
701     {
702 	if (prop == NULL)
703 	{
704 	    semsg(_(e_type_not_exist), name);
705 	    return;
706 	}
707     }
708 
709     if (dict != NULL)
710     {
711 	di = dict_find(dict, (char_u *)"highlight", -1);
712 	if (di != NULL)
713 	{
714 	    char_u	*highlight;
715 	    int		hl_id = 0;
716 
717 	    highlight = dict_get_string(dict, (char_u *)"highlight", FALSE);
718 	    if (highlight != NULL && *highlight != NUL)
719 		hl_id = syn_name2id(highlight);
720 	    if (hl_id <= 0)
721 	    {
722 		semsg(_("E970: Unknown highlight group name: '%s'"),
723 			highlight == NULL ? (char_u *)"" : highlight);
724 		return;
725 	    }
726 	    prop->pt_hl_id = hl_id;
727 	}
728 
729 	di = dict_find(dict, (char_u *)"priority", -1);
730 	if (di != NULL)
731 	    prop->pt_priority = tv_get_number(&di->di_tv);
732 
733 	di = dict_find(dict, (char_u *)"start_incl", -1);
734 	if (di != NULL)
735 	{
736 	    if (tv_get_number(&di->di_tv))
737 		prop->pt_flags |= PT_FLAG_INS_START_INCL;
738 	    else
739 		prop->pt_flags &= ~PT_FLAG_INS_START_INCL;
740 	}
741 
742 	di = dict_find(dict, (char_u *)"end_incl", -1);
743 	if (di != NULL)
744 	{
745 	    if (tv_get_number(&di->di_tv))
746 		prop->pt_flags |= PT_FLAG_INS_END_INCL;
747 	    else
748 		prop->pt_flags &= ~PT_FLAG_INS_END_INCL;
749 	}
750     }
751 }
752 
753 /*
754  * prop_type_add({name}, {props})
755  */
756     void
757 f_prop_type_add(typval_T *argvars, typval_T *rettv UNUSED)
758 {
759     prop_type_set(argvars, TRUE);
760 }
761 
762 /*
763  * prop_type_change({name}, {props})
764  */
765     void
766 f_prop_type_change(typval_T *argvars, typval_T *rettv UNUSED)
767 {
768     prop_type_set(argvars, FALSE);
769 }
770 
771 /*
772  * prop_type_delete({name} [, {bufnr}])
773  */
774     void
775 f_prop_type_delete(typval_T *argvars, typval_T *rettv UNUSED)
776 {
777     char_u	*name;
778     buf_T	*buf = NULL;
779     hashitem_T	*hi;
780 
781     name = tv_get_string(&argvars[0]);
782     if (*name == NUL)
783     {
784 	emsg(_(e_invarg));
785 	return;
786     }
787 
788     if (argvars[1].v_type != VAR_UNKNOWN)
789     {
790 	if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL)
791 	    return;
792     }
793 
794     hi = find_prop_hi(name, buf);
795     if (hi != NULL)
796     {
797 	hashtab_T	*ht;
798 	proptype_T	*prop = HI2PT(hi);
799 
800 	if (buf == NULL)
801 	    ht = global_proptypes;
802 	else
803 	    ht = buf->b_proptypes;
804 	hash_remove(ht, hi);
805 	vim_free(prop);
806     }
807 }
808 
809 /*
810  * prop_type_get({name} [, {bufnr}])
811  */
812     void
813 f_prop_type_get(typval_T *argvars, typval_T *rettv UNUSED)
814 {
815     char_u *name = tv_get_string(&argvars[0]);
816 
817     if (*name == NUL)
818     {
819 	emsg(_(e_invarg));
820 	return;
821     }
822     if (rettv_dict_alloc(rettv) == OK)
823     {
824 	proptype_T  *prop = NULL;
825 	buf_T	    *buf = NULL;
826 
827 	if (argvars[1].v_type != VAR_UNKNOWN)
828 	{
829 	    if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL)
830 		return;
831 	}
832 
833 	prop = find_prop(name, buf);
834 	if (prop != NULL)
835 	{
836 	    dict_T *d = rettv->vval.v_dict;
837 
838 	    if (prop->pt_hl_id > 0)
839 		dict_add_string(d, "highlight", syn_id2name(prop->pt_hl_id));
840 	    dict_add_number(d, "priority", prop->pt_priority);
841 	    dict_add_number(d, "start_incl",
842 			    (prop->pt_flags & PT_FLAG_INS_START_INCL) ? 1 : 0);
843 	    dict_add_number(d, "end_incl",
844 			      (prop->pt_flags & PT_FLAG_INS_END_INCL) ? 1 : 0);
845 	    if (buf != NULL)
846 		dict_add_number(d, "bufnr", buf->b_fnum);
847 	}
848     }
849 }
850 
851     static void
852 list_types(hashtab_T *ht, list_T *l)
853 {
854     long	todo;
855     hashitem_T	*hi;
856 
857     todo = (long)ht->ht_used;
858     for (hi = ht->ht_array; todo > 0; ++hi)
859     {
860 	if (!HASHITEM_EMPTY(hi))
861 	{
862 	    proptype_T *prop = HI2PT(hi);
863 
864 	    list_append_string(l, prop->pt_name, -1);
865 	    --todo;
866 	}
867     }
868 }
869 
870 /*
871  * prop_type_list([{bufnr}])
872  */
873     void
874 f_prop_type_list(typval_T *argvars, typval_T *rettv UNUSED)
875 {
876     buf_T *buf = NULL;
877 
878     if (rettv_list_alloc(rettv) == OK)
879     {
880 	if (argvars[0].v_type != VAR_UNKNOWN)
881 	{
882 	    if (get_bufnr_from_arg(&argvars[0], &buf) == FAIL)
883 		return;
884 	}
885 	if (buf == NULL)
886 	{
887 	    if (global_proptypes != NULL)
888 		list_types(global_proptypes, rettv->vval.v_list);
889 	}
890 	else if (buf->b_proptypes != NULL)
891 	    list_types(buf->b_proptypes, rettv->vval.v_list);
892     }
893 }
894 
895 /*
896  * Free all property types in "ht".
897  */
898     static void
899 clear_ht_prop_types(hashtab_T *ht)
900 {
901     long	todo;
902     hashitem_T	*hi;
903 
904     if (ht == NULL)
905 	return;
906 
907     todo = (long)ht->ht_used;
908     for (hi = ht->ht_array; todo > 0; ++hi)
909     {
910 	if (!HASHITEM_EMPTY(hi))
911 	{
912 	    proptype_T *prop = HI2PT(hi);
913 
914 	    vim_free(prop);
915 	    --todo;
916 	}
917     }
918 
919     hash_clear(ht);
920     vim_free(ht);
921 }
922 
923 #if defined(EXITFREE) || defined(PROTO)
924 /*
925  * Free all global property types.
926  */
927     void
928 clear_global_prop_types(void)
929 {
930     clear_ht_prop_types(global_proptypes);
931     global_proptypes = NULL;
932 }
933 #endif
934 
935 /*
936  * Free all property types for "buf".
937  */
938     void
939 clear_buf_prop_types(buf_T *buf)
940 {
941     clear_ht_prop_types(buf->b_proptypes);
942     buf->b_proptypes = NULL;
943 }
944 
945 /*
946  * Adjust the columns of text properties in line "lnum" after position "col" to
947  * shift by "bytes_added" (can be negative).
948  * Note that "col" is zero-based, while tp_col is one-based.
949  * Only for the current buffer.
950  * Called is expected to check b_has_textprop and "bytes_added" being non-zero.
951  */
952     void
953 adjust_prop_columns(
954 	linenr_T    lnum,
955 	colnr_T	    col,
956 	int	    bytes_added)
957 {
958     int		proplen;
959     char_u	*props;
960     textprop_T	tmp_prop;
961     proptype_T  *pt;
962     int		dirty = FALSE;
963     int		ri, wi;
964     size_t	textlen;
965 
966     if (text_prop_frozen > 0)
967 	return;
968 
969     proplen = get_text_props(curbuf, lnum, &props, TRUE);
970     if (proplen == 0)
971 	return;
972     textlen = curbuf->b_ml.ml_line_len - proplen * sizeof(textprop_T);
973 
974     wi = 0; // write index
975     for (ri = 0; ri < proplen; ++ri)
976     {
977 	mch_memmove(&tmp_prop, props + ri * sizeof(textprop_T),
978 							   sizeof(textprop_T));
979 	pt = text_prop_type_by_id(curbuf, tmp_prop.tp_type);
980 
981 	if (bytes_added > 0
982 		? (tmp_prop.tp_col >= col
983 		       + (pt != NULL && (pt->pt_flags & PT_FLAG_INS_START_INCL)
984 								      ? 2 : 1))
985 		: (tmp_prop.tp_col > col + 1))
986 	{
987 	    tmp_prop.tp_col += bytes_added;
988 	    dirty = TRUE;
989 	}
990 	else if (tmp_prop.tp_len > 0
991 		&& tmp_prop.tp_col + tmp_prop.tp_len > col
992 		       + ((pt != NULL && (pt->pt_flags & PT_FLAG_INS_END_INCL))
993 								      ? 0 : 1))
994 	{
995 	    tmp_prop.tp_len += bytes_added;
996 	    dirty = TRUE;
997 	    if (tmp_prop.tp_len <= 0)
998 		continue;  // drop this text property
999 	}
1000 	mch_memmove(props + wi * sizeof(textprop_T), &tmp_prop,
1001 							   sizeof(textprop_T));
1002 	++wi;
1003     }
1004     if (dirty)
1005     {
1006 	colnr_T newlen = (int)textlen + wi * (colnr_T)sizeof(textprop_T);
1007 
1008 	if ((curbuf->b_ml.ml_flags & ML_LINE_DIRTY) == 0)
1009 	    curbuf->b_ml.ml_line_ptr =
1010 				 vim_memsave(curbuf->b_ml.ml_line_ptr, newlen);
1011 	curbuf->b_ml.ml_flags |= ML_LINE_DIRTY;
1012 	curbuf->b_ml.ml_line_len = newlen;
1013     }
1014 }
1015 
1016 /*
1017  * Adjust text properties for a line that was split in two.
1018  * "lnum" is the newly inserted line.  The text properties are now on the line
1019  * below it.  "kept" is the number of bytes kept in the first line, while
1020  * "deleted" is the number of bytes deleted.
1021  */
1022     void
1023 adjust_props_for_split(linenr_T lnum, int kept, int deleted)
1024 {
1025     char_u	*props;
1026     int		count;
1027     garray_T    prevprop;
1028     garray_T    nextprop;
1029     int		i;
1030     int		skipped = kept + deleted;
1031 
1032     if (!curbuf->b_has_textprop)
1033 	return;
1034     count = get_text_props(curbuf, lnum + 1, &props, FALSE);
1035     ga_init2(&prevprop, sizeof(textprop_T), 10);
1036     ga_init2(&nextprop, sizeof(textprop_T), 10);
1037 
1038     // Get the text properties, which are at "lnum + 1".
1039     // Keep the relevant ones in the first line, reducing the length if needed.
1040     // Copy the ones that include the split to the second line.
1041     // Move the ones after the split to the second line.
1042     for (i = 0; i < count; ++i)
1043     {
1044 	textprop_T  prop;
1045 	textprop_T *p;
1046 
1047 	// copy the prop to an aligned structure
1048 	mch_memmove(&prop, props + i * sizeof(textprop_T), sizeof(textprop_T));
1049 
1050 	if (prop.tp_col < kept && ga_grow(&prevprop, 1) == OK)
1051 	{
1052 	    p = ((textprop_T *)prevprop.ga_data) + prevprop.ga_len;
1053 	    *p = prop;
1054 	    if (p->tp_col + p->tp_len >= kept)
1055 		p->tp_len = kept - p->tp_col;
1056 	    ++prevprop.ga_len;
1057 	}
1058 
1059 	if (prop.tp_col + prop.tp_len >= skipped && ga_grow(&nextprop, 1) == OK)
1060 	{
1061 	    p = ((textprop_T *)nextprop.ga_data) + nextprop.ga_len;
1062 	    *p = prop;
1063 	    if (p->tp_col > skipped)
1064 		p->tp_col -= skipped - 1;
1065 	    else
1066 	    {
1067 		p->tp_len -= skipped - p->tp_col;
1068 		p->tp_col = 1;
1069 	    }
1070 	    ++nextprop.ga_len;
1071 	}
1072     }
1073 
1074     set_text_props(lnum, prevprop.ga_data, prevprop.ga_len * sizeof(textprop_T));
1075     ga_clear(&prevprop);
1076 
1077     set_text_props(lnum + 1, nextprop.ga_data, nextprop.ga_len * sizeof(textprop_T));
1078     ga_clear(&nextprop);
1079 }
1080 
1081 #endif // FEAT_TEXT_PROP
1082