# HG changeset patch # User Bram Moolenaar # Date 1544736608 -3600 # Node ID 9df130fd5e0ddbe251cd6fd7263aada7aa394041 # Parent 44f47a35a3f454c9273deb0452313411842d0faa patch 8.1.0579: cannot attach properties to text commit https://github.com/vim/vim/commit/98aefe7c3250bb5d4153b994f878594d1745424e Author: Bram Moolenaar Date: Thu Dec 13 22:20:09 2018 +0100 patch 8.1.0579: cannot attach properties to text Problem: Cannot attach properties to text. Solution: First part of adding text properties. diff --git a/Filelist b/Filelist --- a/Filelist +++ b/Filelist @@ -91,6 +91,7 @@ SRC_ALL = \ src/terminal.c \ src/term.h \ src/termlib.c \ + src/textprop.c \ src/ui.c \ src/undo.c \ src/userfunc.c \ @@ -198,6 +199,7 @@ SRC_ALL = \ src/proto/term.pro \ src/proto/terminal.pro \ src/proto/termlib.pro \ + src/proto/textprop.pro \ src/proto/ui.pro \ src/proto/undo.pro \ src/proto/userfunc.pro \ diff --git a/runtime/doc/Makefile b/runtime/doc/Makefile --- a/runtime/doc/Makefile +++ b/runtime/doc/Makefile @@ -102,6 +102,7 @@ DOCS = \ tagsrch.txt \ term.txt \ terminal.txt \ + textprop.txt \ tips.txt \ todo.txt \ uganda.txt \ @@ -238,6 +239,7 @@ HTMLS = \ tagsrch.html \ term.html \ terminal.html \ + textprop.html \ tips.html \ todo.html \ uganda.html \ diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -1,4 +1,4 @@ -*eval.txt* For Vim version 8.1. Last change: 2018 Dec 09 +*eval.txt* For Vim version 8.1. Last change: 2018 Dec 13 VIM REFERENCE MANUAL by Bram Moolenaar @@ -1104,7 +1104,7 @@ Or, if you don't want to write them in a also use functions, like the following: > :let pi = acos(-1.0) :let e = exp(1.0) - +< *floating-point-precision* The precision and range of floating points numbers depends on what "double" means in the library Vim was compiled with. There is no way to change this at @@ -2317,6 +2317,22 @@ printf({fmt}, {expr1}...) String format prompt_setcallback({buf}, {expr}) none set prompt callback function prompt_setinterrupt({buf}, {text}) none set prompt interrupt function prompt_setprompt({buf}, {text}) none set prompt text +prop_add({lnum}, {col}, {props}) none add a text property +prop_clear({lnum} [, {lnum_end} [, {bufnr}]]) + none remove all text properties +prop_find({props} [, {direction}]) + Dict search for a text property +prop_list({lnum} [, {props}) List text properties in {lnum} +prop_remove({props} [, {lnum} [, {lnum_end}]]) + Number remove a text property +prop_type_add({name}, {props}) none define a new property type +prop_type_change({name}, {props}) + none change an existing property type +prop_type_delete({name} [, {props}]) + none delete a property type +prop_type_get([{name} [, {props}]) + Dict get property type values +prop_type_list([{props}]) List get list of property types pumvisible() Number whether popup menu is visible pyeval({expr}) any evaluate |Python| expression py3eval({expr}) any evaluate |python3| expression @@ -3142,8 +3158,9 @@ ch_logfile({fname} [, {mode}]) *ch_l When {mode} is omitted or "a" append to the file. When {mode} is "w" start with an empty file. - The file is flushed after every message, on Unix you can use - "tail -f" to see what is going on in real time. + Use |ch_log()| to write log messages. The file is flushed + after every message, on Unix you can use "tail -f" to see what + is going on in real time. This function is not available in the |sandbox|. NOTE: the channel communication is stored in the file, be @@ -6656,6 +6673,191 @@ prompt_setprompt({buf}, {text}) *prom The result is only visible if {buf} has 'buftype' set to "prompt". Example: > call prompt_setprompt(bufnr(''), 'command: ') +< + *prop_add()* *E965* +prop_add({lnum}, {col}, {props}) + Attach a text property at position {lnum}, {col}. Use one for + the first column. + If {lnum} is invalid an error is given. *E966* + If {col} is invalid an error is given. *E964* + + {props} is a dictionary with these fields: + "length" - length of text in characters, can only be + used for a property that does not + continue in another line + "end_lnum" - line number for end of text + "end_col" - column for end of text; not used when + "length" is present + "bufnr - buffer to add the property to; when + omitted the current buffer is used + "id" - user defined ID for the property; when + omitted zero is used + "type" - name of the text property type + All fields except "type" are optional. + + It is an error when both "length" and "end_lnum" or "end_col" + are passed. Either use "length" or "end_col" for a property + within one line, or use "end_lnum" and "end_col" for a + property that spans more than one line. + When neither "length" or "end_col" are passed the property + will apply to one character. + + "type" will first be looked up in the buffer the property is + added to. When not found, the global property types are used. + If not found an error is given. + + See |text-properties| for information about text properties. + + +prop_clear({lnum} [, {lnum_end} [, {props}]]) *prop_clear()* + Remove all text properties from line {lnum}. + When {lnum_end} is given, remove all text properties from line + {lnum} to {lnum_end} (inclusive). + + When {props} contains a "bufnr" item use this buffer, + otherwise use the current buffer. + + See |text-properties| for information about text properties. + + *prop_find()* +prop_find({props} [, {direction}]) + NOT IMPLEMENTED YET + Search for a text property as specified with {props}: + "id" property with this ID + "type" property with this type name + "bufnr buffer to search in; when present a + start position with "lnum" and "col" + must be given; when omitted the + current buffer is used + "lnum" start in this line (when omitted start + at the cursor) + "col" start at this column (when omitted + and "lnum" is given: use column 1, + otherwise start at the cursor) + "skipstart" do not look for a match at the start + position + + {direction} can be "f" for forward and "b" for backward. When + omitted forward search is performed. + + If a match is found then a Dict is returned with the entries + as with prop_list(), and additionally an "lnum" entry. + If no match is found then an empty Dict is returned. + + See |text-properties| for information about text properties. + + +prop_list({lnum} [, {props}]) *prop_list()* + Return a List with all text properties in line {lnum}. + + When {props} contains a "bufnr" item, use this buffer instead + of the current buffer. + + The properties are ordered by starting column and priority. + Each property is a Dict with these entries: + "col" starting column + "length" length in bytes + "id" property ID + "type" name of the property type, omitted if + the type was deleted + "start" when TRUE property starts in this line + "end" when TRUE property ends in this line + + When "start" is zero the property started in a previous line, + the current one is a continuation. + When "end" is zero the property continues in the next line. + The line break after this line is included. + + See |text-properties| for information about text properties. + + + *prop_remove()* *E968* +prop_remove({props} [, {lnum} [, {lnum_end}]]) + Remove a matching text property from line {lnum}. When + {lnum_end} is given, remove matching text properties from line + {lnum} to {lnum_end} (inclusive). + When {lnum} is omitted remove matching text properties from + all lines. + + {props} is a dictionary with these fields: + "id" - remove text properties with this ID + "type" - remove text properties with this type name + "bufnr" - use this buffer instead of the current one + "all" - when TRUE remove all matching text + properties, not just the first one + A property matches when either "id" or "type" matches. + + Returns the number of properties that were removed. + + See |text-properties| for information about text properties. + + +prop_type_add({name}, {props}) *prop_type_add()* *E969* *E970* + Add a text property type {name}. If a property type with this + name already exists an error is given. + {props} is a dictionary with these optional fields: + "bufnr" - define the property only for this + buffer; this avoids name collisions and + automatically clears the property types + when the buffer is deleted. + "highlight" - name of highlight group to use + "priority" - when a character has multiple text + properties the one with the highest + priority will be used; negative values + can be used, the default priority is + zero + "start_incl" - when TRUE inserts at the start + position will be included in the text + property + "end_incl" - when TRUE inserts at the end + position will be included in the text + property + + See |text-properties| for information about text properties. + + +prop_type_change({name}, {props}) *prop_type_change()* + Change properties of an existing text property type. If a + property with this name does not exist an error is given. + The {props} argument is just like |prop_type_add()|. + + See |text-properties| for information about text properties. + + +prop_type_delete({name} [, {props}]) *prop_type_delete()* + Remove the text property type {name}. When text properties + using the type {name} are still in place, they will not have + an effect and can no longer be removed by name. + + {props} can contain a "bufnr" item. When it is given, delete + a property type from this buffer instead of from the global + property types. + + When text property type {name} is not found there is no error. + + See |text-properties| for information about text properties. + + +prop_type_get([{name} [, {props}]) *prop_type_get()* + Returns the properties of property type {name}. This is a + dictionary with the same fields as was given to + prop_type_add(). + When the property type {name} does not exist, an empty + dictionary is returned. + + {props} can contain a "bufnr" item. When it is given, use + this buffer instead of the global property types. + + See |text-properties| for information about text properties. + + +prop_type_list([{props}]) *prop_type_list()* + Returns a list with all property type names. + + {props} can contain a "bufnr" item. When it is given, use + this buffer instead of the global property types. + + See |text-properties| for information about text properties. pumvisible() *pumvisible()* @@ -9609,6 +9811,7 @@ terminal Compiled with |terminal| suppo terminfo Compiled with terminfo instead of termcap. termresponse Compiled with support for |t_RV| and |v:termresponse|. textobjects Compiled with support for |text-objects|. +textprop Compiled with support for |text-properties|. tgetent Compiled with tgetent support, able to use a termcap or terminfo file. timers Compiled with |timer_start()| support. diff --git a/runtime/doc/textprop.txt b/runtime/doc/textprop.txt new file mode 100644 --- /dev/null +++ b/runtime/doc/textprop.txt @@ -0,0 +1,114 @@ +*textprop.txt* For Vim version 8.1. Last change: 2018 Dec 13 + + + VIM REFERENCE MANUAL by Bram Moolenaar + + +Displaying text with properties attached. *text-properties* + +THIS IS UNDER DEVELOPMENT - ANYTHING MAY STILL CHANGE *E967* + +What is not working yet: +- Adjusting column/length when inserting text +- Text properties spanning more than one line +- prop_find() +- callbacks when text properties are outdated + + +1. Introduction |text-prop-intro| +2. Functions |text-prop-functions| + + +{Vi does not have text properties} +{not able to use text properties when the |+textprop| feature was +disabled at compile time} + +============================================================================== +1. Introduction *text-prop-intro* + +Text properties can be attached to text in a buffer. They will move with the +text: If lines are deleted or inserted the properties move with the text they +are attached to. Also when inserting/deleting text in the line before the +text property. And when inserting/deleting text inside the text property, it +will increase/decrease in size. + +The main use for text properties is to highlight text. This can be seen as a +replacement for syntax highlighting. Instead of defining patterns to match +the text, the highlighting is set by a script, possibly using the output of an +external parser. This only needs to be done once, not every time when +redrawing the screen, thus can be much faster, after the initial cost of +attaching the text properties. + +Text properties can also be used for other purposes to identify text. For +example, add a text property on a function name, so that a search can be +defined to jump to the next/previous function. + +A text property is attached at a specific line and column, and has a specified +length. The property can span multiple lines. + +A text property has these fields: + "id" a number to be used as desired + "type" the name of a property type + + +Property Types ~ + *E971* +A text property normally has the name of a property type, which defines +how to highlight the text. The property type can have these entries: + "highlight" name of the highlight group to use + "priority" when properties overlap, the one with the highest + priority will be used. + "start_incl" when TRUE inserts at the start position will be + included in the text property + "end_incl" when TRUE inserts at the end position will be + included in the text property + + +Example ~ + +Suppose line 11 in a buffer has this text (excluding the indent): + + The number 123 is smaller than 4567. + +To highlight the numbers: > + call prop_type_add('number', {'highlight': 'Constant'}) + call prop_add(11, 12, {'length': 3, 'type': 'number}) + call prop_add(11, 32, {'length': 4, 'type': 'number}) + +Setting "start_incl" and "end_incl" is useful when white space surrounds the +text, e.g. for a function name. Using false is useful when the text starts +and/or ends with a specific character, such as the quote surrounding a string. + + func FuncName(arg) ~ + ^^^^^^^^ property with start_incl and end_incl set + + var = "text"; ~ + ^^^^^^ property with start_incl and end_incl not set + +Nevertheless, when text is inserted or deleted the text may need to be parsed +and the text properties updated. But this can be done asynchrnously. + +============================================================================== +2. Functions *text-prop-functions* + +Manipulating text property types: + +prop_type_add({name}, {props}) define a new property type +prop_type_change({name}, {props}) change an existing property type +prop_type_delete({name} [, {props}]) delete a property type +prop_type_get([{name} [, {props}]) get property type values +prop_type_list([{props}]) get list of property types + + +Manipulating text properties: + +prop_add({lnum}, {col}, {props}) add a text property +prop_clear({lnum} [, {lnum_end} [, {bufnr}]]) + remove all text properties +prop_find({props} [, {direction}]) search for a text property +prop_list({lnum} [, {props}) text properties in {lnum} +prop_remove({props} [, {lnum} [, {lnum_end}]]) + remove a text property + + + vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/src/Make_all.mak b/src/Make_all.mak --- a/src/Make_all.mak +++ b/src/Make_all.mak @@ -186,6 +186,7 @@ NEW_TESTS = \ test_terminal_fail \ test_textformat \ test_textobjects \ + test_textprop \ test_timers \ test_true_false \ test_undo \ diff --git a/src/Make_cyg_ming.mak b/src/Make_cyg_ming.mak --- a/src/Make_cyg_ming.mak +++ b/src/Make_cyg_ming.mak @@ -751,6 +751,7 @@ OBJ = \ $(OUTDIR)/syntax.o \ $(OUTDIR)/tag.o \ $(OUTDIR)/term.o \ + $(OUTDIR)/textprop.o \ $(OUTDIR)/ui.o \ $(OUTDIR)/undo.o \ $(OUTDIR)/userfunc.o \ @@ -1090,6 +1091,9 @@ endif $(OUTDIR)/terminal.o: terminal.c $(INCL) $(TERM_DEPS) $(CC) -c $(CFLAGS) terminal.c -o $(OUTDIR)/terminal.o +$(OUTDIR)/textprop.o: textprop.c $(INCL) + $(CC) -c $(CFLAGS) textprop.c -o $(OUTDIR)/textprop.o + CCCTERM = $(CC) -c $(CFLAGS) -Ilibvterm/include -DINLINE="" \ -DVSNPRINTF=vim_vsnprintf \ diff --git a/src/Make_mvc.mak b/src/Make_mvc.mak --- a/src/Make_mvc.mak +++ b/src/Make_mvc.mak @@ -754,6 +754,7 @@ OBJ = \ $(OUTDIR)\syntax.obj \ $(OUTDIR)\tag.obj \ $(OUTDIR)\term.obj \ + $(OUTDIR)\textprop.obj \ $(OUTDIR)\ui.obj \ $(OUTDIR)\undo.obj \ $(OUTDIR)\userfunc.obj \ @@ -1529,6 +1530,8 @@ lib$(MZSCHEME_MAIN_LIB)$(MZSCHEME_VER).l $(OUTDIR)/term.obj: $(OUTDIR) term.c $(INCL) +$(OUTDIR)/textprop.obj: $(OUTDIR) textprop.c $(INCL) + $(OUTDIR)/ui.obj: $(OUTDIR) ui.c $(INCL) $(OUTDIR)/undo.obj: $(OUTDIR) undo.c $(INCL) @@ -1667,6 +1670,7 @@ proto.h: \ proto/syntax.pro \ proto/tag.pro \ proto/term.pro \ + proto/textprop.pro \ proto/ui.pro \ proto/undo.pro \ proto/userfunc.pro \ diff --git a/src/Makefile b/src/Makefile --- a/src/Makefile +++ b/src/Makefile @@ -1577,8 +1577,6 @@ include Make_all.mak # TAGS_INCL: include files used for make tags # ALL_SRC: source files used for make depend and make lint -TAGS_INCL = *.h - BASIC_SRC = \ arabic.c \ beval.c \ @@ -1636,6 +1634,7 @@ BASIC_SRC = \ tag.c \ term.c \ terminal.c \ + textprop.c \ ui.c \ undo.c \ userfunc.c \ @@ -1657,7 +1656,8 @@ SRC = $(BASIC_SRC) \ $(WORKSHOP_SRC) \ $(WSDEBUG_SRC) -TAGS_SRC = *.c *.cpp if_perl.xs +TAGS_SRC = *.c *.cpp $(PERL_SRC) $(TERM_SRC) $(XDIFF_SRC) +TAGS_INCL = *.h $(TERM_DEPS) $(XDIFF_INCL) EXTRA_SRC = hangulin.c if_lua.c if_mzsch.c auto/if_perl.c if_perlsfio.c \ if_python.c if_python3.c if_tcl.c if_ruby.c \ @@ -1747,6 +1747,7 @@ OBJ_COMMON = \ objects/tag.o \ objects/term.o \ objects/terminal.o \ + objects/textprop.o \ objects/ui.o \ objects/undo.o \ objects/userfunc.o \ @@ -1881,6 +1882,7 @@ PRO_AUTO = \ term.pro \ terminal.pro \ termlib.pro \ + textprop.pro \ ui.pro \ undo.pro \ userfunc.pro \ @@ -2092,7 +2094,7 @@ notags: # Motif and Athena GUI # You can ignore error messages for missing files. tags TAGS: notags - $(TAGPRG) $(TAGS_SRC) $(TAGS_INCL) $(TERM_SRC) $(TERM_DEPS) + $(TAGPRG) $(TAGS_SRC) $(TAGS_INCL) # Make a highlight file for types. Requires Exuberant ctags and awk types: types.vim @@ -3211,6 +3213,9 @@ objects/term.o: term.c objects/terminal.o: terminal.c $(TERM_DEPS) $(CCC) -o $@ terminal.c +objects/textprop.o: textprop.c + $(CCC) -o $@ textprop.c + objects/ui.o: ui.c $(CCC) -o $@ ui.c @@ -3602,6 +3607,10 @@ objects/terminal.o: terminal.c vim.h pro proto/gui_beval.pro structs.h regexp.h gui.h alloc.h ex_cmds.h spell.h \ proto.h globals.h farsi.h arabic.h libvterm/include/vterm.h \ libvterm/include/vterm_keycodes.h +objects/textprop.o: textprop.c vim.h protodef.h auto/config.h feature.h os_unix.h \ + auto/osdef.h ascii.h keymap.h term.h macros.h option.h beval.h \ + proto/gui_beval.pro structs.h regexp.h gui.h alloc.h ex_cmds.h spell.h \ + proto.h globals.h farsi.h arabic.h objects/ui.o: ui.c vim.h protodef.h auto/config.h feature.h os_unix.h \ auto/osdef.h ascii.h keymap.h term.h macros.h option.h beval.h \ proto/gui_beval.pro structs.h regexp.h gui.h alloc.h ex_cmds.h spell.h \ diff --git a/src/buffer.c b/src/buffer.c --- a/src/buffer.c +++ b/src/buffer.c @@ -823,6 +823,9 @@ buf_freeall(buf_T *buf, int flags) #ifdef FEAT_SYN_HL syntax_clear(&buf->b_s); /* reset syntax info */ #endif +#ifdef FEAT_TEXT_PROP + clear_buf_prop_types(buf); +#endif buf->b_flags &= ~BF_READERR; /* a read error is no longer relevant */ } diff --git a/src/edit.c b/src/edit.c --- a/src/edit.c +++ b/src/edit.c @@ -10302,6 +10302,9 @@ ins_tab(void) if ((State & REPLACE_FLAG) && !(State & VREPLACE_FLAG)) for (temp = i; --temp >= 0; ) replace_join(repl_off); +#ifdef FEAT_TEXT_PROP + curbuf->b_ml.ml_line_len -= i; +#endif } #ifdef FEAT_NETBEANS_INTG if (netbeans_active()) diff --git a/src/evalfunc.c b/src/evalfunc.c --- a/src/evalfunc.c +++ b/src/evalfunc.c @@ -772,6 +772,17 @@ static struct fst {"prompt_setinterrupt", 2, 2, f_prompt_setinterrupt}, {"prompt_setprompt", 2, 2, f_prompt_setprompt}, #endif +#ifdef FEAT_TEXT_PROP + {"prop_add", 3, 3, f_prop_add}, + {"prop_clear", 1, 3, f_prop_clear}, + {"prop_list", 1, 2, f_prop_list}, + {"prop_remove", 2, 3, f_prop_remove}, + {"prop_type_add", 2, 2, f_prop_type_add}, + {"prop_type_change", 2, 2, f_prop_type_change}, + {"prop_type_delete", 1, 2, f_prop_type_delete}, + {"prop_type_get", 1, 2, f_prop_type_get}, + {"prop_type_list", 0, 1, f_prop_type_list}, +#endif {"pumvisible", 0, 0, f_pumvisible}, #ifdef FEAT_PYTHON3 {"py3eval", 1, 1, f_py3eval}, @@ -6478,6 +6489,9 @@ f_has(typval_T *argvars, typval_T *rettv #ifdef FEAT_TEXTOBJ "textobjects", #endif +#ifdef FEAT_TEXT_PROP + "textprop", +#endif #ifdef HAVE_TGETENT "tgetent", #endif diff --git a/src/ex_getln.c b/src/ex_getln.c --- a/src/ex_getln.c +++ b/src/ex_getln.c @@ -769,6 +769,21 @@ may_add_char_to_search(int firstc, int * stuffcharReadbuff(*c); *c = '\\'; } +#ifdef FEAT_MBYTE + // add any composing characters + if (mb_char2len(*c) != mb_ptr2len(ml_get_cursor())) + { + int save_c = *c; + + while (mb_char2len(*c) != mb_ptr2len(ml_get_cursor())) + { + curwin->w_cursor.col += mb_char2len(*c); + *c = gchar_cursor(); + stuffcharReadbuff(*c); + } + *c = save_c; + } +#endif return FAIL; } } diff --git a/src/feature.h b/src/feature.h --- a/src/feature.h +++ b/src/feature.h @@ -502,6 +502,13 @@ #endif /* + * +textprop Text properties + */ +#if defined(FEAT_EVAL) && defined(FEAT_SYN_HL) +# define FEAT_TEXT_PROP +#endif + +/* * +spell spell checking * * Disabled for EBCDIC: * Doesn't work (SIGSEGV). diff --git a/src/memline.c b/src/memline.c --- a/src/memline.c +++ b/src/memline.c @@ -2487,7 +2487,6 @@ ml_get_buf( { bhdr_T *hp; DATA_BL *dp; - char_u *ptr; static int recursive = 0; if (lnum > buf->b_ml.ml_line_count) /* invalid line number */ @@ -2518,6 +2517,10 @@ errorret: */ if (buf->b_ml.ml_line_lnum != lnum || mf_dont_release) { + unsigned start, end; + colnr_T len; + int idx; + ml_flush_line(buf); /* @@ -2540,8 +2543,18 @@ errorret: dp = (DATA_BL *)(hp->bh_data); - ptr = (char_u *)dp + ((dp->db_index[lnum - buf->b_ml.ml_locked_low]) & DB_INDEX_MASK); - buf->b_ml.ml_line_ptr = ptr; + idx = lnum - buf->b_ml.ml_locked_low; + start = ((dp->db_index[idx]) & DB_INDEX_MASK); + // The text ends where the previous line starts. The first line ends + // at the end of the block. + if (idx == 0) + end = dp->db_txt_end; + else + end = ((dp->db_index[idx - 1]) & DB_INDEX_MASK); + len = end - start; + + buf->b_ml.ml_line_ptr = (char_u *)dp + start; + buf->b_ml.ml_line_len = len; buf->b_ml.ml_line_lnum = lnum; buf->b_ml.ml_flags &= ~ML_LINE_DIRTY; } @@ -2614,20 +2627,21 @@ ml_append_buf( static int ml_append_int( buf_T *buf, - linenr_T lnum, /* append after this line (can be 0) */ - char_u *line, /* text of the new line */ - colnr_T len, /* length of line, including NUL, or 0 */ - int newfile, /* flag, see above */ - int mark) /* mark the new line */ + linenr_T lnum, // append after this line (can be 0) + char_u *line, // text of the new line + colnr_T len_arg, // length of line, including NUL, or 0 + int newfile, // flag, see above + int mark) // mark the new line { + colnr_T len = len_arg; // length of line, including NUL, or 0 int i; - int line_count; /* number of indexes in current block */ + int line_count; // number of indexes in current block int offset; int from, to; - int space_needed; /* space needed for new line */ + int space_needed; // space needed for new line int page_size; int page_count; - int db_idx; /* index for lnum in data block */ + int db_idx; // index for lnum in data block bhdr_T *hp; memfile_T *mfp; DATA_BL *dp; @@ -2642,8 +2656,8 @@ ml_append_int( lowest_marked = lnum + 1; if (len == 0) - len = (colnr_T)STRLEN(line) + 1; /* space needed for the text */ - space_needed = len + INDEX_SIZE; /* space needed for text + index */ + len = (colnr_T)STRLEN(line) + 1; // space needed for the text + space_needed = len + INDEX_SIZE; // space needed for text + index mfp = buf->b_ml.ml_mfp; page_size = mfp->mf_page_size; @@ -2728,7 +2742,8 @@ ml_append_int( dp->db_index[i + 1] = dp->db_index[i] - len; dp->db_index[db_idx + 1] = offset - len; } - else /* add line at the end */ + else + // add line at the end (which is the start of the text) dp->db_index[db_idx + 1] = dp->db_txt_start; /* @@ -3128,6 +3143,19 @@ ml_append_int( int ml_replace(linenr_T lnum, char_u *line, int copy) { + colnr_T len = -1; + + if (line != NULL) + len = STRLEN(line); + return ml_replace_len(lnum, line, len, copy); +} + + int +ml_replace_len(linenr_T lnum, char_u *line_arg, colnr_T len_arg, int copy) +{ + char_u *line = line_arg; + colnr_T len = len_arg; + if (line == NULL) /* just checking... */ return FAIL; @@ -3135,7 +3163,7 @@ ml_replace(linenr_T lnum, char_u *line, if (curbuf->b_ml.ml_mfp == NULL && open_buffer(FALSE, NULL, 0) == FAIL) return FAIL; - if (copy && (line = vim_strsave(line)) == NULL) /* allocate memory */ + if (copy && (line = vim_strnsave(line, len)) == NULL) /* allocate memory */ return FAIL; #ifdef FEAT_NETBEANS_INTG if (netbeans_active()) @@ -3144,11 +3172,48 @@ ml_replace(linenr_T lnum, char_u *line, netbeans_inserted(curbuf, lnum, 0, line, (int)STRLEN(line)); } #endif - if (curbuf->b_ml.ml_line_lnum != lnum) /* other line buffered */ - ml_flush_line(curbuf); /* flush it */ - else if (curbuf->b_ml.ml_flags & ML_LINE_DIRTY) /* same line allocated */ + if (curbuf->b_ml.ml_line_lnum != lnum) + { + // another line is buffered, flush it + ml_flush_line(curbuf); + +#ifdef FEAT_TEXT_PROP + curbuf->b_ml.ml_flags &= ~ML_LINE_DIRTY; + if (has_any_text_properties(curbuf)) + // Need to fetch the old line to copy over any text properties. + ml_get_buf(curbuf, lnum, TRUE); +#endif + } + +#ifdef FEAT_TEXT_PROP + if (has_any_text_properties(curbuf)) + { + size_t oldtextlen = STRLEN(curbuf->b_ml.ml_line_ptr) + 1; + + if (oldtextlen < (size_t)curbuf->b_ml.ml_line_len) + { + char_u *newline; + size_t textproplen = curbuf->b_ml.ml_line_len - oldtextlen; + + // Need to copy over text properties, stored after the text. + newline = alloc(len + 1 + textproplen); + if (newline != NULL) + { + mch_memmove(newline, line, len + 1); + mch_memmove(newline + len + 1, curbuf->b_ml.ml_line_ptr + oldtextlen, textproplen); + vim_free(line); + line = newline; + len += textproplen; + } + } + } +#endif + + if (curbuf->b_ml.ml_flags & ML_LINE_DIRTY) /* same line allocated */ vim_free(curbuf->b_ml.ml_line_ptr); /* free it */ + curbuf->b_ml.ml_line_ptr = line; + curbuf->b_ml.ml_line_len = len + 1; curbuf->b_ml.ml_line_lnum = lnum; curbuf->b_ml.ml_flags = (curbuf->b_ml.ml_flags | ML_LINE_DIRTY) & ~ML_EMPTY; @@ -3490,7 +3555,7 @@ ml_flush_line(buf_T *buf) old_len = dp->db_txt_end - start; else /* text of previous line follows */ old_len = (dp->db_index[idx - 1] & DB_INDEX_MASK) - start; - new_len = (colnr_T)STRLEN(new_line) + 1; + new_len = buf->b_ml.ml_line_len; extra = new_len - old_len; /* negative if lines gets smaller */ /* @@ -5009,8 +5074,7 @@ ml_updatechunk( */ buf->b_ml.ml_usedchunks = 1; buf->b_ml.ml_chunksize[0].mlcs_numlines = 1; - buf->b_ml.ml_chunksize[0].mlcs_totalsize = - (long)STRLEN(buf->b_ml.ml_line_ptr) + 1; + buf->b_ml.ml_chunksize[0].mlcs_totalsize = (long)buf->b_ml.ml_line_len; return; } diff --git a/src/misc1.c b/src/misc1.c --- a/src/misc1.c +++ b/src/misc1.c @@ -2631,9 +2631,10 @@ del_bytes( { char_u *oldp, *newp; colnr_T oldlen; + colnr_T newlen; linenr_T lnum = curwin->w_cursor.lnum; colnr_T col = curwin->w_cursor.col; - int was_alloced; + int alloc_newp; long movelen; int fixpos = fixpos_arg; @@ -2710,6 +2711,7 @@ del_bytes( count = oldlen - col; movelen = 1; } + newlen = oldlen - count; /* * If the old line has been allocated the deletion can be done in the @@ -2720,24 +2722,34 @@ del_bytes( */ #ifdef FEAT_NETBEANS_INTG if (netbeans_active()) - was_alloced = FALSE; + alloc_newp = TRUE; else #endif - was_alloced = ml_line_alloced(); /* check if oldp was allocated */ - if (was_alloced) - newp = oldp; /* use same allocated memory */ + alloc_newp = !ml_line_alloced(); // check if oldp was allocated + if (!alloc_newp) + newp = oldp; // use same allocated memory else - { /* need to allocate a new line */ - newp = alloc((unsigned)(oldlen + 1 - count)); + { // need to allocate a new line + newp = alloc((unsigned)(newlen + 1)); if (newp == NULL) return FAIL; mch_memmove(newp, oldp, (size_t)col); } mch_memmove(newp + col, oldp + col + count, (size_t)movelen); - if (!was_alloced) + if (alloc_newp) ml_replace(lnum, newp, FALSE); - - /* mark the buffer as changed and prepare for displaying */ +#ifdef FEAT_TEXT_PROP + else + { + // Also move any following text properties. + if (oldlen + 1 < curbuf->b_ml.ml_line_len) + mch_memmove(newp + newlen + 1, oldp + oldlen + 1, + (size_t)curbuf->b_ml.ml_line_len - oldlen - 1); + curbuf->b_ml.ml_line_len -= count; + } +#endif + + // mark the buffer as changed and prepare for displaying changed_bytes(lnum, curwin->w_cursor.col); return OK; diff --git a/src/misc2.c b/src/misc2.c --- a/src/misc2.c +++ b/src/misc2.c @@ -1191,6 +1191,9 @@ free_all_mem(void) # ifdef FEAT_CMDHIST init_history(); # endif +#ifdef FEAT_TEXT_PROP + clear_global_prop_types(); +#endif #ifdef FEAT_QUICKFIX { diff --git a/src/proto.h b/src/proto.h --- a/src/proto.h +++ b/src/proto.h @@ -183,6 +183,9 @@ void qsort(void *base, size_t elm_count, # if defined(HAVE_TGETENT) && (defined(AMIGA) || defined(VMS)) # include "termlib.pro" # endif +# ifdef FEAT_TEXT_PROP +# include "textprop.pro" +# endif # include "ui.pro" # include "undo.pro" # include "userfunc.pro" diff --git a/src/proto/memline.pro b/src/proto/memline.pro --- a/src/proto/memline.pro +++ b/src/proto/memline.pro @@ -24,6 +24,7 @@ int ml_line_alloced(void); int ml_append(linenr_T lnum, char_u *line, colnr_T len, int newfile); int ml_append_buf(buf_T *buf, linenr_T lnum, char_u *line, colnr_T len, int newfile); int ml_replace(linenr_T lnum, char_u *line, int copy); +int ml_replace_len(linenr_T lnum, char_u *line_arg, colnr_T len_arg, int copy); int ml_delete(linenr_T lnum, int message); void ml_setmarked(linenr_T lnum); linenr_T ml_firstmarked(void); diff --git a/src/proto/textprop.pro b/src/proto/textprop.pro new file mode 100644 --- /dev/null +++ b/src/proto/textprop.pro @@ -0,0 +1,17 @@ +/* textprop.c */ +void f_prop_add(typval_T *argvars, typval_T *rettv); +int has_any_text_properties(buf_T *buf); +int get_text_props(buf_T *buf, linenr_T lnum, char_u **props, int will_change); +proptype_T *text_prop_type_by_id(buf_T *buf, int id); +void f_prop_clear(typval_T *argvars, typval_T *rettv); +void f_prop_list(typval_T *argvars, typval_T *rettv); +void f_prop_remove(typval_T *argvars, typval_T *rettv); +void prop_type_set(typval_T *argvars, int add); +void f_prop_type_add(typval_T *argvars, typval_T *rettv); +void f_prop_type_change(typval_T *argvars, typval_T *rettv); +void f_prop_type_delete(typval_T *argvars, typval_T *rettv); +void f_prop_type_get(typval_T *argvars, typval_T *rettv); +void f_prop_type_list(typval_T *argvars, typval_T *rettv); +void clear_global_prop_types(void); +void clear_buf_prop_types(buf_T *buf); +/* vim: set ft=c : */ diff --git a/src/screen.c b/src/screen.c --- a/src/screen.c +++ b/src/screen.c @@ -3128,6 +3128,15 @@ win_line( int draw_color_col = FALSE; /* highlight colorcolumn */ int *color_cols = NULL; /* pointer to according columns array */ #endif +#ifdef FEAT_TEXT_PROP + int text_prop_count; + int text_prop_next = 0; // next text property to use + textprop_T *text_props = NULL; + int *text_prop_idxs = NULL; + int text_props_active = 0; + proptype_T *text_prop_type = NULL; + int text_prop_attr = 0; +#endif #ifdef FEAT_SPELL int has_spell = FALSE; /* this buffer has spell checking */ # define SPWORDLEN 150 @@ -3144,7 +3153,7 @@ win_line( static linenr_T capcol_lnum = 0; /* line number where "cap_col" used */ int cur_checked_col = 0; /* checked column for current line */ #endif - int extra_check = 0; // has syntax or linebreak + int extra_check = 0; // has extra highlighting #ifdef FEAT_MBYTE int multi_attr = 0; /* attributes desired by multibyte */ int mb_l = 1; /* multi-byte byte length */ @@ -3784,6 +3793,30 @@ win_line( } #endif +#ifdef FEAT_TEXT_PROP + { + char_u *prop_start; + + text_prop_count = get_text_props(wp->w_buffer, lnum, + &prop_start, FALSE); + if (text_prop_count > 0) + { + // Make a copy of the properties, so that they are properly + // aligned. + text_props = (textprop_T *)alloc( + text_prop_count * sizeof(textprop_T)); + if (text_props != NULL) + mch_memmove(text_props, prop_start, + text_prop_count * sizeof(textprop_T)); + + // Allocate an array for the indexes. + text_prop_idxs = (int *)alloc(text_prop_count * sizeof(int)); + area_highlighting = TRUE; + extra_check = TRUE; + } + } +#endif + off = (unsigned)(current_ScreenLine - ScreenLines); col = 0; #ifdef FEAT_RIGHTLEFT @@ -4283,6 +4316,11 @@ win_line( else { attr_pri = FALSE; +#ifdef FEAT_TEXT_PROP + if (text_prop_type != NULL) + char_attr = text_prop_attr; + else +#endif #ifdef FEAT_SYN_HL if (has_syntax) char_attr = syntax_attr; @@ -4663,6 +4701,66 @@ win_line( } #endif +#ifdef FEAT_TEXT_PROP + if (text_props != NULL) + { + int pi; + + // Check if any active property ends. + for (pi = 0; pi < text_props_active; ++pi) + { + int tpi = text_prop_idxs[pi]; + + if (col >= text_props[tpi].tp_col - 1 + + text_props[tpi].tp_len) + { + if (pi + 1 < text_props_active) + mch_memmove(text_prop_idxs + pi, + text_prop_idxs + pi + 1, + sizeof(int) + * (text_props_active - (pi + 1))); + --text_props_active; + --pi; + } + } + + // Add any text property that starts in this column. + while (text_prop_next < text_prop_count + && col >= text_props[text_prop_next].tp_col - 1) + text_prop_idxs[text_props_active++] = text_prop_next++; + + text_prop_type = NULL; + if (text_props_active > 0) + { + int max_priority = INT_MIN; + int max_col = 0; + + // Get the property type with the highest priority + // and/or starting last. + for (pi = 0; pi < text_props_active; ++pi) + { + int tpi = text_prop_idxs[pi]; + proptype_T *pt; + + pt = text_prop_type_by_id( + curwin->w_buffer, text_props[tpi].tp_type); + if (pt != NULL + && (pt->pt_priority > max_priority + || (pt->pt_priority == max_priority + && text_props[tpi].tp_col >= max_col))) + { + text_prop_type = pt; + max_priority = pt->pt_priority; + max_col = text_props[tpi].tp_col; + } + } + if (text_prop_type != NULL) + text_prop_attr = + syn_id2attr(text_prop_type->pt_hl_id); + } + } +#endif + #ifdef FEAT_SPELL /* Check spelling (unless at the end of the line). * Only do this when there is no syntax highlighting, the @@ -6025,6 +6123,10 @@ win_line( cap_col = 0; } #endif +#ifdef FEAT_TEXT_PROP + vim_free(text_props); + vim_free(text_prop_idxs); +#endif vim_free(p_extra_free); return row; diff --git a/src/structs.h b/src/structs.h --- a/src/structs.h +++ b/src/structs.h @@ -684,6 +684,7 @@ typedef struct memline linenr_T ml_line_lnum; /* line number of cached line, 0 if not valid */ char_u *ml_line_ptr; /* pointer to cached line */ + colnr_T ml_line_len; /* length of the cached line, including NUL */ bhdr_T *ml_locked; /* block used by last ml_get */ linenr_T ml_locked_low; /* first line in ml_locked */ @@ -696,6 +697,41 @@ typedef struct memline #endif } memline_T; + +/* + * Structure defining text properties. These stick with the text. + * When stored in memline they are after the text, ml_line_len is larger than + * STRLEN(ml_line_ptr) + 1. + */ +typedef struct textprop_S +{ + colnr_T tp_col; // start column + colnr_T tp_len; // length in bytes + int tp_id; // identifier + int tp_type; // property type + int tp_flags; // TP_FLAG_ values +} textprop_T; + +#define TP_FLAG_CONT_NEXT 1 // property continues in next line +#define TP_FLAG_CONT_PREV 2 // property was continued from prev line + +/* + * Structure defining a property type. + */ +typedef struct proptype_S +{ + int pt_id; // value used for tp_id + int pt_type; // number used for tp_type + int pt_hl_id; // highlighting + int pt_priority; // priority + int pt_flags; // PT_FLAG_ values + char_u pt_name[1]; // property type name, actually longer +} proptype_T; + +#define PT_FLAG_INS_START_INCL 1 // insert at start included in property +#define PT_FLAG_INS_END_INCL 2 // insert at end included in property + + #if defined(FEAT_SIGNS) || defined(PROTO) typedef struct signlist signlist_T; @@ -2358,6 +2394,9 @@ struct file_buffer dictitem_T b_bufvar; /* variable for "b:" Dictionary */ dict_T *b_vars; /* internal variables, local to buffer */ #endif +#ifdef FEAT_TEXT_PROP + hashtab_T *b_proptypes; /* text property types local to buffer */ +#endif #if defined(FEAT_BEVAL) && defined(FEAT_EVAL) char_u *b_p_bexpr; /* 'balloonexpr' local value */ diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak --- a/src/testdir/Make_all.mak +++ b/src/testdir/Make_all.mak @@ -177,6 +177,7 @@ NEW_TESTS = test_arabic.res \ test_terminal_fail.res \ test_textformat.res \ test_textobjects.res \ + test_textprop.res \ test_undo.res \ test_user_func.res \ test_usercommands.res \ diff --git a/src/testdir/test_textprop.vim b/src/testdir/test_textprop.vim new file mode 100644 --- /dev/null +++ b/src/testdir/test_textprop.vim @@ -0,0 +1,200 @@ +" Tests for defining text property types and adding text properties to the +" buffer. + +if !has('textprop') + finish +endif + +func Test_proptype_global() + call prop_type_add('comment', {'highlight': 'Directory', 'priority': 123, 'start_incl': 1, 'end_incl': 1}) + let proptypes = prop_type_list() + call assert_equal(1, len(proptypes)) + call assert_equal('comment', proptypes[0]) + + let proptype = prop_type_get('comment') + call assert_equal('Directory', proptype['highlight']) + call assert_equal(123, proptype['priority']) + call assert_equal(1, proptype['start_incl']) + call assert_equal(1, proptype['end_incl']) + + call prop_type_delete('comment') + call assert_equal(0, len(prop_type_list())) + + call prop_type_add('one', {}) + call assert_equal(1, len(prop_type_list())) + let proptype = prop_type_get('one') + call assert_false(has_key(proptype, 'highlight')) + call assert_equal(0, proptype['priority']) + call assert_equal(0, proptype['start_incl']) + call assert_equal(0, proptype['end_incl']) + + call prop_type_add('two', {}) + call assert_equal(2, len(prop_type_list())) + call prop_type_delete('one') + call assert_equal(1, len(prop_type_list())) + call prop_type_delete('two') + call assert_equal(0, len(prop_type_list())) +endfunc + +func Test_proptype_buf() + let bufnr = bufnr('') + call prop_type_add('comment', {'bufnr': bufnr, 'highlight': 'Directory', 'priority': 123, 'start_incl': 1, 'end_incl': 1}) + let proptypes = prop_type_list({'bufnr': bufnr}) + call assert_equal(1, len(proptypes)) + call assert_equal('comment', proptypes[0]) + + let proptype = prop_type_get('comment', {'bufnr': bufnr}) + call assert_equal('Directory', proptype['highlight']) + call assert_equal(123, proptype['priority']) + call assert_equal(1, proptype['start_incl']) + call assert_equal(1, proptype['end_incl']) + + call prop_type_delete('comment', {'bufnr': bufnr}) + call assert_equal(0, len(prop_type_list({'bufnr': bufnr}))) + + call prop_type_add('one', {'bufnr': bufnr}) + let proptype = prop_type_get('one', {'bufnr': bufnr}) + call assert_false(has_key(proptype, 'highlight')) + call assert_equal(0, proptype['priority']) + call assert_equal(0, proptype['start_incl']) + call assert_equal(0, proptype['end_incl']) + + call prop_type_add('two', {'bufnr': bufnr}) + call assert_equal(2, len(prop_type_list({'bufnr': bufnr}))) + call prop_type_delete('one', {'bufnr': bufnr}) + call assert_equal(1, len(prop_type_list({'bufnr': bufnr}))) + call prop_type_delete('two', {'bufnr': bufnr}) + call assert_equal(0, len(prop_type_list({'bufnr': bufnr}))) +endfunc + +func AddPropTypes() + call prop_type_add('one', {}) + call prop_type_add('two', {}) + call prop_type_add('three', {}) + call prop_type_add('whole', {}) +endfunc + +func DeletePropTypes() + call prop_type_delete('one') + call prop_type_delete('two') + call prop_type_delete('three') + call prop_type_delete('whole') +endfunc + +func SetupPropsInFirstLine() + call setline(1, 'one two three') + call prop_add(1, 1, {'length': 3, 'id': 11, 'type': 'one'}) + call prop_add(1, 5, {'length': 3, 'id': 12, 'type': 'two'}) + call prop_add(1, 8, {'length': 5, 'id': 13, 'type': 'three'}) + call prop_add(1, 1, {'length': 13, 'id': 14, 'type': 'whole'}) +endfunc + +let s:expected_props = [{'col': 1, 'length': 13, 'id': 14, 'type': 'whole', 'start': 1, 'end': 1}, + \ {'col': 1, 'length': 3, 'id': 11, 'type': 'one', 'start': 1, 'end': 1}, + \ {'col': 5, 'length': 3, 'id': 12, 'type': 'two', 'start': 1, 'end': 1}, + \ {'col': 8, 'length': 5, 'id': 13, 'type': 'three', 'start': 1, 'end': 1}, + \ ] + +func Test_prop_add() + new + call AddPropTypes() + call SetupPropsInFirstLine() + call assert_equal(s:expected_props, prop_list(1)) + call assert_fails("call prop_add(10, 1, {'length': 1, 'id': 14, 'type': 'whole'})", 'E966:') + call assert_fails("call prop_add(1, 22, {'length': 1, 'id': 14, 'type': 'whole'})", 'E964:') + + " Insert a line above, text props must still be there. + call append(0, 'empty') + call assert_equal(s:expected_props, prop_list(2)) + " Delete a line above, text props must still be there. + 1del + call assert_equal(s:expected_props, prop_list(1)) + + call DeletePropTypes() + bwipe! +endfunc + +func Test_prop_remove() + new + call AddPropTypes() + call SetupPropsInFirstLine() + let props = deepcopy(s:expected_props) + call assert_equal(props, prop_list(1)) + + " remove by id + call prop_remove({'id': 12}, 1) + unlet props[2] + call assert_equal(props, prop_list(1)) + + " remove by type + call prop_remove({'type': 'one'}, 1) + unlet props[1] + call assert_equal(props, prop_list(1)) + + call DeletePropTypes() + bwipe! +endfunc + +func Test_prop_add_remove_buf() + new + let bufnr = bufnr('') + call AddPropTypes() + call setline(1, 'one two three') + wincmd w + call prop_add(1, 1, {'length': 3, 'id': 11, 'type': 'one', 'bufnr': bufnr}) + call prop_add(1, 5, {'length': 3, 'id': 12, 'type': 'two', 'bufnr': bufnr}) + call prop_add(1, 11, {'length': 3, 'id': 13, 'type': 'three', 'bufnr': bufnr}) + + let props = [ + \ {'col': 1, 'length': 3, 'id': 11, 'type': 'one', 'start': 1, 'end': 1}, + \ {'col': 5, 'length': 3, 'id': 12, 'type': 'two', 'start': 1, 'end': 1}, + \ {'col': 11, 'length': 3, 'id': 13, 'type': 'three', 'start': 1, 'end': 1}, + \] + call assert_equal(props, prop_list(1, {'bufnr': bufnr})) + + " remove by id + call prop_remove({'id': 12, 'bufnr': bufnr}, 1) + unlet props[1] + call assert_equal(props, prop_list(1, {'bufnr': bufnr})) + + " remove by type + call prop_remove({'type': 'one', 'bufnr': bufnr}, 1) + unlet props[0] + call assert_equal(props, prop_list(1, {'bufnr': bufnr})) + + call DeletePropTypes() + wincmd w + bwipe! +endfunc + + +func Test_prop_clear() + new + call AddPropTypes() + call SetupPropsInFirstLine() + call assert_equal(s:expected_props, prop_list(1)) + + call prop_clear(1) + call assert_equal([], prop_list(1)) + + call DeletePropTypes() + bwipe! +endfunc + +func Test_prop_clear_buf() + new + call AddPropTypes() + call SetupPropsInFirstLine() + let bufnr = bufnr('') + wincmd w + call assert_equal(s:expected_props, prop_list(1, {'bufnr': bufnr})) + + call prop_clear(1, 1, {'bufnr': bufnr}) + call assert_equal([], prop_list(1, {'bufnr': bufnr})) + + wincmd w + call DeletePropTypes() + bwipe! +endfunc + +" TODO: screenshot test with highlighting diff --git a/src/textprop.c b/src/textprop.c new file mode 100644 --- /dev/null +++ b/src/textprop.c @@ -0,0 +1,869 @@ +/* vi:set ts=8 sts=4 sw=4 noet: + * + * VIM - Vi IMproved by Bram Moolenaar + * + * Do ":help uganda" in Vim to read copying and usage conditions. + * Do ":help credits" in Vim to see a list of people who contributed. + * See README.txt for an overview of the Vim source code. + */ + +/* + * Text properties implementation. + * + * Text properties are attached to the text. They move with the text when + * text is inserted/deleted. + * + * Text properties have a user specified ID number, which can be unique. + * Text properties have a type, which can be used to specify highlighting. + * + * TODO: + * - Add an arrray for global_proptypes, to quickly lookup a proptype by ID + * - Add an arrray for b_proptypes, to quickly lookup a proptype by ID + * - adjust property column when text is inserted/deleted + * - support properties that continue over a line break + * - add mechanism to keep track of changed lines. + */ + +#include "vim.h" + +#if defined(FEAT_TEXT_PROP) || defined(PROTO) + +/* + * In a hashtable item "hi_key" points to "pt_name" in a proptype_T. + * This avoids adding a pointer to the hashtable item. + * PT2HIKEY() converts a proptype pointer to a hashitem key pointer. + * HIKEY2PT() converts a hashitem key pointer to a proptype pointer. + * HI2PT() converts a hashitem pointer to a proptype pointer. + */ +#define PT2HIKEY(p) ((p)->pt_name) +#define HIKEY2PT(p) ((proptype_T *)((p) - offsetof(proptype_T, pt_name))) +#define HI2PT(hi) HIKEY2PT((hi)->hi_key) + +// The global text property types. +static hashtab_T *global_proptypes = NULL; + +// The last used text property type ID. +static int proptype_id = 0; + +static char_u e_type_not_exist[] = N_("E971: Property type %s does not exist"); +static char_u e_invalid_col[] = N_("E964: Invalid column number: %ld"); + +/* + * Find a property type by name, return the hashitem. + * Returns NULL if the item can't be found. + */ + static hashitem_T * +find_prop_hi(char_u *name, buf_T *buf) +{ + hashtab_T *ht; + hashitem_T *hi; + + if (*name == NUL) + return NULL; + if (buf == NULL) + ht = global_proptypes; + else + ht = buf->b_proptypes; + + if (ht == NULL) + return NULL; + hi = hash_find(ht, name); + if (HASHITEM_EMPTY(hi)) + return NULL; + return hi; +} + +/* + * Like find_prop_hi() but return the property type. + */ + static proptype_T * +find_prop(char_u *name, buf_T *buf) +{ + hashitem_T *hi = find_prop_hi(name, buf); + + if (hi == NULL) + return NULL; + return HI2PT(hi); +} + +/* + * Lookup a property type by name. First in "buf" and when not found in the + * global types. + * When not found gives an error message and returns NULL. + */ + static proptype_T * +lookup_prop_type(char_u *name, buf_T *buf) +{ + proptype_T *type = find_prop(name, buf); + + if (type == NULL) + type = find_prop(name, NULL); + if (type == NULL) + EMSG2(_(e_type_not_exist), name); + return type; +} + +/* + * Get an optional "bufnr" item from the dict in "arg". + * When the argument is not used or "bufnr" is not present then "buf" is + * unchanged. + * If "bufnr" is valid or not present return OK. + * When "arg" is not a dict or "bufnr" is invalide return FAIL. + */ + static int +get_bufnr_from_arg(typval_T *arg, buf_T **buf) +{ + dictitem_T *di; + + if (arg->v_type != VAR_DICT) + { + EMSG(_(e_dictreq)); + return FAIL; + } + if (arg->vval.v_dict == NULL) + return OK; // NULL dict is like an empty dict + di = dict_find(arg->vval.v_dict, (char_u *)"bufnr", -1); + if (di != NULL) + { + *buf = get_buf_tv(&di->di_tv, FALSE); + if (*buf == NULL) + return FAIL; + } + return OK; +} + +/* + * prop_add({lnum}, {col}, {props}) + */ + void +f_prop_add(typval_T *argvars, typval_T *rettv UNUSED) +{ + linenr_T lnum; + colnr_T col; + dict_T *dict; + colnr_T length = 1; + char_u *type_name; + proptype_T *type; + buf_T *buf = curbuf; + int id = 0; + char_u *newtext; + int proplen; + size_t textlen; + char_u *props; + char_u *newprops; + static textprop_T tmp_prop; // static to get it aligned. + int i; + + lnum = get_tv_number(&argvars[0]); + col = get_tv_number(&argvars[1]); + if (col < 1) + { + EMSGN(_(e_invalid_col), (long)col); + return; + } + if (argvars[2].v_type != VAR_DICT) + { + EMSG(_(e_dictreq)); + return; + } + dict = argvars[2].vval.v_dict; + + if (dict == NULL || dict_find(dict, (char_u *)"type", -1) == NULL) + { + EMSG(_("E965: missing property type name")); + return; + } + type_name = get_dict_string(dict, (char_u *)"type", FALSE); + + if (dict_find(dict, (char_u *)"end_lnum", -1) != NULL) + { + // TODO: handle end_lnum + EMSG("Sorry, end_lnum not supported yet"); + return; + } + + if (dict_find(dict, (char_u *)"length", -1) != NULL) + length = get_dict_number(dict, (char_u *)"length"); + else if (dict_find(dict, (char_u *)"end_col", -1) != NULL) + { + length = get_dict_number(dict, (char_u *)"end_col") - col; + if (length <= 0) + { + EMSG2(_(e_invargval), "end_col"); + return; + } + } + + if (dict_find(dict, (char_u *)"id", -1) != NULL) + id = get_dict_number(dict, (char_u *)"id"); + + if (get_bufnr_from_arg(&argvars[2], &buf) == FAIL) + return; + + type = lookup_prop_type(type_name, buf); + if (type == NULL) + return; + + if (lnum < 1 || lnum > buf->b_ml.ml_line_count) + { + EMSGN(_("E966: Invalid line number: %ld"), (long)lnum); + return; + } + + // Fetch the line to get the ml_line_len field updated. + proplen = get_text_props(buf, lnum, &props, TRUE); + + if (col >= (colnr_T)STRLEN(buf->b_ml.ml_line_ptr)) + { + EMSGN(_(e_invalid_col), (long)col); + return; + } + + // Allocate the new line with space for the new proprety. + newtext = alloc(buf->b_ml.ml_line_len + sizeof(textprop_T)); + if (newtext == NULL) + return; + // Copy the text, including terminating NUL. + textlen = buf->b_ml.ml_line_len - proplen * sizeof(textprop_T); + mch_memmove(newtext, buf->b_ml.ml_line_ptr, textlen); + + // Find the index where to insert the new property. + // Since the text properties are not aligned properly when stored with the + // text, we need to copy them as bytes before using it as a struct. + for (i = 0; i < proplen; ++i) + { + mch_memmove(&tmp_prop, props + i * sizeof(proptype_T), + sizeof(proptype_T)); + if (tmp_prop.tp_col >= col) + break; + } + newprops = newtext + textlen; + if (i > 0) + mch_memmove(newprops, props, sizeof(textprop_T) * i); + + tmp_prop.tp_col = col; + tmp_prop.tp_len = length; + tmp_prop.tp_id = id; + tmp_prop.tp_type = type->pt_id; + tmp_prop.tp_flags = 0; + mch_memmove(newprops + i * sizeof(textprop_T), &tmp_prop, + sizeof(textprop_T)); + + if (i < proplen) + mch_memmove(newprops + (i + 1) * sizeof(textprop_T), + props + i * sizeof(textprop_T), + sizeof(textprop_T) * (proplen - i)); + + if (buf->b_ml.ml_flags & ML_LINE_DIRTY) + vim_free(buf->b_ml.ml_line_ptr); + buf->b_ml.ml_line_ptr = newtext; + buf->b_ml.ml_line_len += sizeof(textprop_T); + buf->b_ml.ml_flags |= ML_LINE_DIRTY; + + redraw_buf_later(buf, NOT_VALID); +} + +/* + * Return TRUE if any text properties are defined globally or for buffer + * 'buf". + */ + int +has_any_text_properties(buf_T *buf) +{ + return buf->b_proptypes != NULL || global_proptypes != NULL; +} + +/* + * Fetch the text properties for line "lnum" in buffer 'buf". + * Returns the number of text properties and, when non-zero, a pointer to the + * first one in "props" (note that it is not aligned, therefore the char_u + * pointer). + */ + int +get_text_props(buf_T *buf, linenr_T lnum, char_u **props, int will_change) +{ + char_u *text; + size_t textlen; + size_t proplen; + + // Be quick when no text property types are defined. + if (!has_any_text_properties(buf)) + return 0; + + // Fetch the line to get the ml_line_len field updated. + text = ml_get_buf(buf, lnum, will_change); + textlen = STRLEN(text) + 1; + proplen = buf->b_ml.ml_line_len - textlen; + if (proplen % sizeof(textprop_T) != 0) + { + IEMSG(_("E967: text property info corrupted")); + return 0; + } + if (proplen > 0) + *props = text + textlen; + return proplen / sizeof(textprop_T); +} + + static proptype_T * +find_type_by_id(hashtab_T *ht, int id) +{ + long todo; + hashitem_T *hi; + + if (ht == NULL) + return NULL; + + // TODO: Make this faster by keeping a list of types sorted on ID and use + // a binary search. + + todo = (long)ht->ht_used; + for (hi = ht->ht_array; todo > 0; ++hi) + { + if (!HASHITEM_EMPTY(hi)) + { + proptype_T *prop = HI2PT(hi); + + if (prop->pt_id == id) + return prop; + --todo; + } + } + return NULL; +} + +/* + * Find a property type by ID in "buf" or globally. + * Returns NULL if not found. + */ + proptype_T * +text_prop_type_by_id(buf_T *buf, int id) +{ + proptype_T *type; + + type = find_type_by_id(buf->b_proptypes, id); + if (type == NULL) + type = find_type_by_id(global_proptypes, id); + return type; +} + +/* + * prop_clear({lnum} [, {lnum_end} [, {bufnr}]]) + */ + void +f_prop_clear(typval_T *argvars, typval_T *rettv UNUSED) +{ + linenr_T start = get_tv_number(&argvars[0]); + linenr_T end = start; + linenr_T lnum; + buf_T *buf = curbuf; + + if (argvars[1].v_type != VAR_UNKNOWN) + { + end = get_tv_number(&argvars[1]); + if (argvars[2].v_type != VAR_UNKNOWN) + { + if (get_bufnr_from_arg(&argvars[2], &buf) == FAIL) + return; + } + } + if (start < 1 || end < 1) + { + EMSG(_(e_invrange)); + return; + } + + for (lnum = start; lnum <= end; ++lnum) + { + char_u *text; + size_t len; + + if (lnum > buf->b_ml.ml_line_count) + break; + text = ml_get_buf(buf, lnum, FALSE); + len = STRLEN(text) + 1; + if ((size_t)buf->b_ml.ml_line_len > len) + { + if (!(buf->b_ml.ml_flags & ML_LINE_DIRTY)) + { + char_u *newtext = vim_strsave(text); + + // need to allocate the line now + if (newtext == NULL) + return; + buf->b_ml.ml_line_ptr = newtext; + buf->b_ml.ml_flags |= ML_LINE_DIRTY; + } + buf->b_ml.ml_line_len = len; + } + } + redraw_buf_later(buf, NOT_VALID); +} + +/* + * prop_list({lnum} [, {bufnr}]) + */ + void +f_prop_list(typval_T *argvars, typval_T *rettv) +{ + linenr_T lnum = get_tv_number(&argvars[0]); + buf_T *buf = curbuf; + + if (argvars[1].v_type != VAR_UNKNOWN) + { + if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL) + return; + } + if (lnum < 1 || lnum > buf->b_ml.ml_line_count) + { + EMSG(_(e_invrange)); + return; + } + + if (rettv_list_alloc(rettv) == OK) + { + char_u *text = ml_get_buf(buf, lnum, FALSE); + size_t textlen = STRLEN(text) + 1; + int count = (buf->b_ml.ml_line_len - textlen) + / sizeof(textprop_T); + int i; + textprop_T prop; + proptype_T *pt; + + for (i = 0; i < count; ++i) + { + dict_T *d = dict_alloc(); + + if (d == NULL) + break; + mch_memmove(&prop, text + textlen + i * sizeof(textprop_T), + sizeof(textprop_T)); + dict_add_number(d, "col", prop.tp_col); + dict_add_number(d, "length", prop.tp_len); + dict_add_number(d, "id", prop.tp_id); + dict_add_number(d, "start", !(prop.tp_flags & TP_FLAG_CONT_PREV)); + dict_add_number(d, "end", !(prop.tp_flags & TP_FLAG_CONT_NEXT)); + pt = text_prop_type_by_id(buf, prop.tp_type); + if (pt != NULL) + dict_add_string(d, "type", pt->pt_name); + + list_append_dict(rettv->vval.v_list, d); + } + } +} + +/* + * prop_remove({props} [, {lnum} [, {lnum_end}]]) + */ + void +f_prop_remove(typval_T *argvars, typval_T *rettv) +{ + linenr_T start = 1; + linenr_T end = 0; + linenr_T lnum; + dict_T *dict; + buf_T *buf = curbuf; + dictitem_T *di; + int do_all = FALSE; + int id = -1; + int type_id = -1; + + rettv->vval.v_number = 0; + if (argvars[0].v_type != VAR_DICT || argvars[0].vval.v_dict == NULL) + { + EMSG(_(e_invarg)); + return; + } + + if (argvars[1].v_type != VAR_UNKNOWN) + { + start = get_tv_number(&argvars[1]); + end = start; + if (argvars[2].v_type != VAR_UNKNOWN) + end = get_tv_number(&argvars[2]); + if (start < 1 || end < 1) + { + EMSG(_(e_invrange)); + return; + } + } + + dict = argvars[0].vval.v_dict; + di = dict_find(dict, (char_u *)"bufnr", -1); + if (di != NULL) + { + buf = get_buf_tv(&di->di_tv, FALSE); + if (buf == NULL) + return; + } + + di = dict_find(dict, (char_u*)"all", -1); + if (di != NULL) + do_all = get_dict_number(dict, (char_u *)"all"); + + if (dict_find(dict, (char_u *)"id", -1) != NULL) + id = get_dict_number(dict, (char_u *)"id"); + if (dict_find(dict, (char_u *)"type", -1)) + { + char_u *name = get_dict_string(dict, (char_u *)"type", FALSE); + proptype_T *type = lookup_prop_type(name, buf); + + if (type == NULL) + return; + type_id = type->pt_id; + } + if (id == -1 && type_id == -1) + { + EMSG(_("E968: Need at least one of 'id' or 'type'")); + return; + } + + if (end == 0) + end = buf->b_ml.ml_line_count; + for (lnum = start; lnum <= end; ++lnum) + { + char_u *text; + size_t len; + + if (lnum > buf->b_ml.ml_line_count) + break; + text = ml_get_buf(buf, lnum, FALSE); + len = STRLEN(text) + 1; + if ((size_t)buf->b_ml.ml_line_len > len) + { + static textprop_T textprop; // static because of alignment + unsigned idx; + + for (idx = 0; idx < (buf->b_ml.ml_line_len - len) + / sizeof(textprop_T); ++idx) + { + char_u *cur_prop = buf->b_ml.ml_line_ptr + len + + idx * sizeof(textprop_T); + size_t taillen; + + mch_memmove(&textprop, cur_prop, sizeof(textprop_T)); + if (textprop.tp_id == id || textprop.tp_type == type_id) + { + if (!(buf->b_ml.ml_flags & ML_LINE_DIRTY)) + { + char_u *newptr = alloc(buf->b_ml.ml_line_len); + + // need to allocate the line to be able to change it + if (newptr == NULL) + return; + mch_memmove(newptr, buf->b_ml.ml_line_ptr, + buf->b_ml.ml_line_len); + buf->b_ml.ml_line_ptr = newptr; + curbuf->b_ml.ml_flags |= ML_LINE_DIRTY; + } + + taillen = buf->b_ml.ml_line_len - len + - (idx + 1) * sizeof(textprop_T); + if (taillen > 0) + mch_memmove(cur_prop, cur_prop + sizeof(textprop_T), + taillen); + buf->b_ml.ml_line_len -= sizeof(textprop_T); + --idx; + + ++rettv->vval.v_number; + if (!do_all) + break; + } + } + } + } + redraw_buf_later(buf, NOT_VALID); +} + +/* + * Common for f_prop_type_add() and f_prop_type_change(). + */ + void +prop_type_set(typval_T *argvars, int add) +{ + char_u *name; + buf_T *buf = NULL; + dict_T *dict; + dictitem_T *di; + proptype_T *prop; + + name = get_tv_string(&argvars[0]); + if (*name == NUL) + { + EMSG(_(e_invarg)); + return; + } + + if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL) + return; + dict = argvars[1].vval.v_dict; + + prop = find_prop(name, buf); + if (add) + { + hashtab_T **htp; + + if (prop != NULL) + { + EMSG2(_("E969: Property type %s already defined"), name); + return; + } + prop = (proptype_T *)alloc_clear(sizeof(proptype_T) + STRLEN(name)); + if (prop == NULL) + return; + STRCPY(prop->pt_name, name); + prop->pt_id = ++proptype_id; + htp = buf == NULL ? &global_proptypes : &buf->b_proptypes; + if (*htp == NULL) + { + *htp = (hashtab_T *)alloc(sizeof(hashtab_T)); + if (*htp == NULL) + return; + hash_init(*htp); + } + hash_add(buf == NULL ? global_proptypes : buf->b_proptypes, + PT2HIKEY(prop)); + } + else + { + if (prop == NULL) + { + EMSG2(_(e_type_not_exist), name); + return; + } + } + + if (dict != NULL) + { + di = dict_find(dict, (char_u *)"highlight", -1); + if (di != NULL) + { + char_u *highlight; + int hl_id = 0; + + highlight = get_dict_string(dict, (char_u *)"highlight", TRUE); + if (highlight != NULL && *highlight != NUL) + hl_id = syn_name2id(highlight); + if (hl_id <= 0) + { + EMSG2(_("E970: Unknown highlight group name: '%s'"), + highlight == NULL ? (char_u *)"" : highlight); + return; + } + prop->pt_hl_id = hl_id; + } + + di = dict_find(dict, (char_u *)"priority", -1); + if (di != NULL) + prop->pt_priority = get_tv_number(&di->di_tv); + + di = dict_find(dict, (char_u *)"start_incl", -1); + if (di != NULL) + { + if (get_tv_number(&di->di_tv)) + prop->pt_flags |= PT_FLAG_INS_START_INCL; + else + prop->pt_flags &= ~PT_FLAG_INS_START_INCL; + } + + di = dict_find(dict, (char_u *)"end_incl", -1); + if (di != NULL) + { + if (get_tv_number(&di->di_tv)) + prop->pt_flags |= PT_FLAG_INS_END_INCL; + else + prop->pt_flags &= ~PT_FLAG_INS_END_INCL; + } + } +} + +/* + * prop_type_add({name}, {props}) + */ + void +f_prop_type_add(typval_T *argvars, typval_T *rettv UNUSED) +{ + prop_type_set(argvars, TRUE); +} + +/* + * prop_type_change({name}, {props}) + */ + void +f_prop_type_change(typval_T *argvars, typval_T *rettv UNUSED) +{ + prop_type_set(argvars, FALSE); +} + +/* + * prop_type_delete({name} [, {bufnr}]) + */ + void +f_prop_type_delete(typval_T *argvars, typval_T *rettv UNUSED) +{ + char_u *name; + buf_T *buf = NULL; + hashitem_T *hi; + + name = get_tv_string(&argvars[0]); + if (*name == NUL) + { + EMSG(_(e_invarg)); + return; + } + + if (argvars[1].v_type != VAR_UNKNOWN) + { + if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL) + return; + } + + hi = find_prop_hi(name, buf); + if (hi != NULL) + { + hashtab_T *ht; + + if (buf == NULL) + ht = global_proptypes; + else + ht = buf->b_proptypes; + hash_remove(ht, hi); + } +} + +/* + * prop_type_get({name} [, {bufnr}]) + */ + void +f_prop_type_get(typval_T *argvars, typval_T *rettv UNUSED) +{ + char_u *name = get_tv_string(&argvars[0]); + + if (*name == NUL) + { + EMSG(_(e_invarg)); + return; + } + if (rettv_dict_alloc(rettv) == OK) + { + proptype_T *prop = NULL; + buf_T *buf = NULL; + + if (argvars[1].v_type != VAR_UNKNOWN) + { + if (get_bufnr_from_arg(&argvars[1], &buf) == FAIL) + return; + } + + prop = find_prop(name, buf); + if (prop != NULL) + { + dict_T *d = rettv->vval.v_dict; + + if (prop->pt_hl_id > 0) + dict_add_string(d, "highlight", syn_id2name(prop->pt_hl_id)); + dict_add_number(d, "priority", prop->pt_priority); + dict_add_number(d, "start_incl", + (prop->pt_flags & PT_FLAG_INS_START_INCL) ? 1 : 0); + dict_add_number(d, "end_incl", + (prop->pt_flags & PT_FLAG_INS_END_INCL) ? 1 : 0); + if (buf != NULL) + dict_add_number(d, "bufnr", buf->b_fnum); + } + } +} + + static void +list_types(hashtab_T *ht, list_T *l) +{ + long todo; + hashitem_T *hi; + + todo = (long)ht->ht_used; + for (hi = ht->ht_array; todo > 0; ++hi) + { + if (!HASHITEM_EMPTY(hi)) + { + proptype_T *prop = HI2PT(hi); + + list_append_string(l, prop->pt_name, -1); + --todo; + } + } +} + +/* + * prop_type_list([{bufnr}]) + */ + void +f_prop_type_list(typval_T *argvars, typval_T *rettv UNUSED) +{ + buf_T *buf = NULL; + + if (rettv_list_alloc(rettv) == OK) + { + if (argvars[0].v_type != VAR_UNKNOWN) + { + if (get_bufnr_from_arg(&argvars[0], &buf) == FAIL) + return; + } + if (buf == NULL) + { + if (global_proptypes != NULL) + list_types(global_proptypes, rettv->vval.v_list); + } + else if (buf->b_proptypes != NULL) + list_types(buf->b_proptypes, rettv->vval.v_list); + } +} + +/* + * Free all property types in "ht". + */ + static void +clear_ht_prop_types(hashtab_T *ht) +{ + long todo; + hashitem_T *hi; + + if (ht == NULL) + return; + + todo = (long)ht->ht_used; + for (hi = ht->ht_array; todo > 0; ++hi) + { + if (!HASHITEM_EMPTY(hi)) + { + proptype_T *prop = HI2PT(hi); + + vim_free(prop); + --todo; + } + } + + hash_clear(ht); + vim_free(ht); +} + +#if defined(EXITFREE) || defined(PROTO) +/* + * Free all property types for "buf". + */ + void +clear_global_prop_types(void) +{ + clear_ht_prop_types(global_proptypes); + global_proptypes = NULL; +} +#endif + +/* + * Free all property types for "buf". + */ + void +clear_buf_prop_types(buf_T *buf) +{ + clear_ht_prop_types(buf->b_proptypes); + buf->b_proptypes = NULL; +} + +#endif // FEAT_TEXT_PROP diff --git a/src/userfunc.c b/src/userfunc.c --- a/src/userfunc.c +++ b/src/userfunc.c @@ -25,7 +25,7 @@ /* From user function to hashitem and back. */ #define UF2HIKEY(fp) ((fp)->uf_name) -#define HIKEY2UF(p) ((ufunc_T *)(p - offsetof(ufunc_T, uf_name))) +#define HIKEY2UF(p) ((ufunc_T *)((p) - offsetof(ufunc_T, uf_name))) #define HI2UF(hi) HIKEY2UF((hi)->hi_key) #define FUNCARG(fp, j) ((char_u **)(fp->uf_args.ga_data))[j] diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -653,12 +653,6 @@ static char *(features[]) = # else "-terminfo", # endif -#else /* unix always includes termcap support */ -# ifdef HAVE_TGETENT - "+tgetent", -# else - "-tgetent", -# endif #endif #ifdef FEAT_TERMRESPONSE "+termresponse", @@ -670,6 +664,19 @@ static char *(features[]) = #else "-textobjects", #endif +#ifdef FEAT_TEXT_PROP + "+textprop", +#else + "-textprop", +#endif +#if !defined(UNIX) +/* unix always includes termcap support */ +# ifdef HAVE_TGETENT + "+tgetent", +# else + "-tgetent", +# endif +#endif #ifdef FEAT_TIMERS "+timers", #else @@ -793,6 +800,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 579, +/**/ 578, /**/ 577,