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