diff options
Diffstat (limited to 'src')
41 files changed, 1404 insertions, 586 deletions
diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c index 8e61976c4b..cc9dd51cf6 100644 --- a/src/nvim/api/buffer.c +++ b/src/nvim/api/buffer.c @@ -1108,15 +1108,67 @@ ArrayOf(Integer, 2) nvim_buf_get_mark(Buffer buffer, String name, Error *err) return rv; } +static Array extmark_to_array(ExtmarkInfo extmark, bool id, bool add_dict) +{ + Array rv = ARRAY_DICT_INIT; + if (id) { + ADD(rv, INTEGER_OBJ((Integer)extmark.mark_id)); + } + ADD(rv, INTEGER_OBJ(extmark.row)); + ADD(rv, INTEGER_OBJ(extmark.col)); + + if (add_dict) { + Dictionary dict = ARRAY_DICT_INIT; + + if (extmark.end_row >= 0) { + PUT(dict, "end_row", INTEGER_OBJ(extmark.end_row)); + PUT(dict, "end_col", INTEGER_OBJ(extmark.end_col)); + } + + if (extmark.decor) { + Decoration *decor = extmark.decor; + if (decor->hl_id) { + String name = cstr_to_string((const char *)syn_id2name(decor->hl_id)); + PUT(dict, "hl_group", STRING_OBJ(name)); + } + if (kv_size(decor->virt_text)) { + Array chunks = ARRAY_DICT_INIT; + for (size_t i = 0; i < decor->virt_text.size; i++) { + Array chunk = ARRAY_DICT_INIT; + VirtTextChunk *vtc = &decor->virt_text.items[i]; + ADD(chunk, STRING_OBJ(cstr_to_string(vtc->text))); + if (vtc->hl_id > 0) { + ADD(chunk, + STRING_OBJ(cstr_to_string( + (const char *)syn_id2name(vtc->hl_id)))); + } + ADD(chunks, ARRAY_OBJ(chunk)); + } + PUT(dict, "virt_text", ARRAY_OBJ(chunks)); + } + } + + if (dict.size) { + ADD(rv, DICTIONARY_OBJ(dict)); + } + } + + return rv; +} + /// Returns position for a given extmark id /// /// @param buffer Buffer handle, or 0 for current buffer /// @param ns_id Namespace id from |nvim_create_namespace()| /// @param id Extmark id +/// @param opts Optional parameters. Keys: +/// - limit: Maximum number of marks to return +/// - details Whether to include the details dict /// @param[out] err Error details, if any /// @return (row, col) tuple or empty list () if extmark id was absent ArrayOf(Integer) nvim_buf_get_extmark_by_id(Buffer buffer, Integer ns_id, - Integer id, Error *err) + Integer id, Dictionary opts, + Error *err) FUNC_API_SINCE(7) { Array rv = ARRAY_DICT_INIT; @@ -1132,13 +1184,31 @@ ArrayOf(Integer) nvim_buf_get_extmark_by_id(Buffer buffer, Integer ns_id, return rv; } + bool details = false; + for (size_t i = 0; i < opts.size; i++) { + String k = opts.items[i].key; + Object *v = &opts.items[i].value; + if (strequal("details", k.data)) { + if (v->type == kObjectTypeBoolean) { + details = v->data.boolean; + } else if (v->type == kObjectTypeInteger) { + details = v->data.integer; + } else { + api_set_error(err, kErrorTypeValidation, "details is not an boolean"); + return rv; + } + } else { + api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data); + return rv; + } + } + + ExtmarkInfo extmark = extmark_from_id(buf, (uint64_t)ns_id, (uint64_t)id); if (extmark.row < 0) { return rv; } - ADD(rv, INTEGER_OBJ((Integer)extmark.row)); - ADD(rv, INTEGER_OBJ((Integer)extmark.col)); - return rv; + return extmark_to_array(extmark, false, (bool)details); } /// Gets extmarks in "traversal order" from a |charwise| region defined by @@ -1181,10 +1251,12 @@ ArrayOf(Integer) nvim_buf_get_extmark_by_id(Buffer buffer, Integer ns_id, /// (whose position defines the bound) /// @param opts Optional parameters. Keys: /// - limit: Maximum number of marks to return +/// - details Whether to include the details dict /// @param[out] err Error details, if any /// @return List of [extmark_id, row, col] tuples in "traversal order". -Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, - Object end, Dictionary opts, Error *err) +Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, + Object start, Object end, + Dictionary opts, Error *err) FUNC_API_SINCE(7) { Array rv = ARRAY_DICT_INIT; @@ -1198,7 +1270,9 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, api_set_error(err, kErrorTypeValidation, "Invalid ns_id"); return rv; } + Integer limit = -1; + bool details = false; for (size_t i = 0; i < opts.size; i++) { String k = opts.items[i].key; @@ -1209,6 +1283,15 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, return rv; } limit = v->data.integer; + } else if (strequal("details", k.data)) { + if (v->type == kObjectTypeBoolean) { + details = v->data.boolean; + } else if (v->type == kObjectTypeInteger) { + details = v->data.integer; + } else { + api_set_error(err, kErrorTypeValidation, "details is not an boolean"); + return rv; + } } else { api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data); return rv; @@ -1241,16 +1324,11 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, } - ExtmarkArray marks = extmark_get(buf, (uint64_t)ns_id, l_row, l_col, u_row, - u_col, (int64_t)limit, reverse); + ExtmarkInfoArray marks = extmark_get(buf, (uint64_t)ns_id, l_row, l_col, + u_row, u_col, (int64_t)limit, reverse); for (size_t i = 0; i < kv_size(marks); i++) { - Array mark = ARRAY_DICT_INIT; - ExtmarkInfo extmark = kv_A(marks, i); - ADD(mark, INTEGER_OBJ((Integer)extmark.mark_id)); - ADD(mark, INTEGER_OBJ(extmark.row)); - ADD(mark, INTEGER_OBJ(extmark.col)); - ADD(rv, ARRAY_OBJ(mark)); + ADD(rv, ARRAY_OBJ(extmark_to_array(kv_A(marks, i), true, (bool)details))); } kv_destroy(marks); @@ -1260,27 +1338,36 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, /// Creates or updates an extmark. /// /// To create a new extmark, pass id=0. The extmark id will be returned. -// To move an existing mark, pass its id. +/// To move an existing mark, pass its id. /// /// It is also allowed to create a new mark by passing in a previously unused /// id, but the caller must then keep track of existing and unused ids itself. /// (Useful over RPC, to avoid waiting for the return value.) /// +/// Using the optional arguments, it is possible to use this to highlight +/// a range of text, and also to associate virtual text to the mark. +/// /// @param buffer Buffer handle, or 0 for current buffer /// @param ns_id Namespace id from |nvim_create_namespace()| -/// @param id Extmark id, or 0 to create new /// @param line Line number where to place the mark /// @param col Column where to place the mark -/// @param opts Optional parameters. Currently not used. +/// @param opts Optional parameters. +/// - id : id of the extmark to edit. +/// - end_line : ending line of the mark, 0-based inclusive. +/// - end_col : ending col of the mark, 0-based inclusive. +/// - hl_group : name of the highlight group used to highlight +/// this mark. +/// - virt_text : virtual text to link to this mark. /// @param[out] err Error details, if any /// @return Id of the created/updated extmark -Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer id, +Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer col, Dictionary opts, Error *err) FUNC_API_SINCE(7) { buf_T *buf = find_buffer_by_handle(buffer, err); if (!buf) { + api_set_error(err, kErrorTypeValidation, "Invalid buffer id"); return 0; } @@ -1289,11 +1376,6 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer id, return 0; } - if (opts.size > 0) { - api_set_error(err, kErrorTypeValidation, "opts dict isn't empty"); - return 0; - } - size_t len = 0; if (line < 0 || line > buf->b_ml.ml_line_count) { api_set_error(err, kErrorTypeValidation, "line value outside range"); @@ -1309,18 +1391,113 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer id, return 0; } - uint64_t id_num; - if (id >= 0) { - id_num = (uint64_t)id; - } else { - api_set_error(err, kErrorTypeValidation, "Invalid mark id"); - return 0; + uint64_t id = 0; + int line2 = -1, hl_id = 0; + colnr_T col2 = 0; + VirtText virt_text = KV_INITIAL_VALUE; + for (size_t i = 0; i < opts.size; i++) { + String k = opts.items[i].key; + Object *v = &opts.items[i].value; + if (strequal("id", k.data)) { + if (v->type != kObjectTypeInteger || v->data.integer <= 0) { + api_set_error(err, kErrorTypeValidation, + "id is not a positive integer"); + goto error; + } + + id = (uint64_t)v->data.integer; + } else if (strequal("end_line", k.data)) { + if (v->type != kObjectTypeInteger) { + api_set_error(err, kErrorTypeValidation, + "end_line is not an integer"); + goto error; + } + if (v->data.integer < 0 || v->data.integer > buf->b_ml.ml_line_count) { + api_set_error(err, kErrorTypeValidation, + "end_line value outside range"); + goto error; + } + + line2 = (int)v->data.integer; + } else if (strequal("end_col", k.data)) { + if (v->type != kObjectTypeInteger) { + api_set_error(err, kErrorTypeValidation, + "end_col is not an integer"); + goto error; + } + if (v->data.integer < 0 || v->data.integer > MAXCOL) { + api_set_error(err, kErrorTypeValidation, + "end_col value outside range"); + goto error; + } + + col2 = (colnr_T)v->data.integer; + } else if (strequal("hl_group", k.data)) { + String hl_group; + switch (v->type) { + case kObjectTypeString: + hl_group = v->data.string; + hl_id = syn_check_group( + (char_u *)(hl_group.data), + (int)hl_group.size); + break; + case kObjectTypeInteger: + hl_id = (int)v->data.integer; + break; + default: + api_set_error(err, kErrorTypeValidation, + "hl_group is not valid."); + goto error; + } + } else if (strequal("virt_text", k.data)) { + if (v->type != kObjectTypeArray) { + api_set_error(err, kErrorTypeValidation, + "virt_text is not an Array"); + goto error; + } + virt_text = parse_virt_text(v->data.array, err); + if (ERROR_SET(err)) { + goto error; + } + } else { + api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data); + goto error; + } + } + + if (col2 >= 0) { + if (line2 >= 0) { + len = STRLEN(ml_get_buf(buf, (linenr_T)line2+1, false)); + } else { + // reuse len from before + line2 = (int)line; + } + if (col2 > (Integer)len) { + api_set_error(err, kErrorTypeValidation, + "end_col value outside range"); + goto error; + } + } else if (line2 >= 0) { + col2 = 0; } - id_num = extmark_set(buf, (uint64_t)ns_id, id_num, - (int)line, (colnr_T)col, kExtmarkUndo); + Decoration *decor = NULL; + if (kv_size(virt_text)) { + decor = xcalloc(1, sizeof(*decor)); + decor->hl_id = hl_id; + decor->virt_text = virt_text; + } else if (hl_id) { + decor = decoration_hl(hl_id); + } - return (Integer)id_num; + id = extmark_set(buf, (uint64_t)ns_id, id, + (int)line, (colnr_T)col, line2, col2, decor, kExtmarkUndo); + + return (Integer)id; + +error: + clear_virttext(&virt_text); + return 0; } /// Removes an extmark. @@ -1412,9 +1589,9 @@ Integer nvim_buf_add_highlight(Buffer buffer, return src_id; } - int hlg_id = 0; + int hl_id = 0; if (hl_group.size > 0) { - hlg_id = syn_check_group((char_u *)hl_group.data, (int)hl_group.size); + hl_id = syn_check_group((char_u *)hl_group.data, (int)hl_group.size); } else { return src_id; } @@ -1425,10 +1602,10 @@ Integer nvim_buf_add_highlight(Buffer buffer, end_line++; } - extmark_add_decoration(buf, ns_id, hlg_id, - (int)line, (colnr_T)col_start, - end_line, (colnr_T)col_end, - VIRTTEXT_EMPTY); + ns_id = extmark_set(buf, ns_id, 0, + (int)line, (colnr_T)col_start, + end_line, (colnr_T)col_end, + decoration_hl(hl_id), kExtmarkUndo); return src_id; } @@ -1592,113 +1769,11 @@ Integer nvim_buf_set_virtual_text(Buffer buffer, return src_id; } - extmark_add_decoration(buf, ns_id, 0, - (int)line, 0, -1, -1, - virt_text); - return src_id; -} - -/// Get the virtual text (annotation) for a buffer line. -/// -/// The virtual text is returned as list of lists, whereas the inner lists have -/// either one or two elements. The first element is the actual text, the -/// optional second element is the highlight group. -/// -/// The format is exactly the same as given to nvim_buf_set_virtual_text(). -/// -/// If there is no virtual text associated with the given line, an empty list -/// is returned. -/// -/// @param buffer Buffer handle, or 0 for current buffer -/// @param line Line to get the virtual text from (zero-indexed) -/// @param[out] err Error details, if any -/// @return List of virtual text chunks -Array nvim_buf_get_virtual_text(Buffer buffer, Integer line, Error *err) - FUNC_API_SINCE(7) -{ - Array chunks = ARRAY_DICT_INIT; - - buf_T *buf = find_buffer_by_handle(buffer, err); - if (!buf) { - return chunks; - } - - if (line < 0 || line >= MAXLNUM) { - api_set_error(err, kErrorTypeValidation, "Line number outside range"); - return chunks; - } - - VirtText *virt_text = extmark_find_virttext(buf, (int)line, 0); - - if (!virt_text) { - return chunks; - } - - for (size_t i = 0; i < virt_text->size; i++) { - Array chunk = ARRAY_DICT_INIT; - VirtTextChunk *vtc = &virt_text->items[i]; - ADD(chunk, STRING_OBJ(cstr_to_string(vtc->text))); - if (vtc->hl_id > 0) { - ADD(chunk, STRING_OBJ(cstr_to_string( - (const char *)syn_id2name(vtc->hl_id)))); - } - ADD(chunks, ARRAY_OBJ(chunk)); - } - - return chunks; -} - -Integer nvim__buf_add_decoration(Buffer buffer, Integer ns_id, String hl_group, - Integer start_row, Integer start_col, - Integer end_row, Integer end_col, - Array virt_text, - Error *err) -{ - buf_T *buf = find_buffer_by_handle(buffer, err); - if (!buf) { - return 0; - } + Decoration *decor = xcalloc(1, sizeof(*decor)); + decor->virt_text = virt_text; - if (!ns_initialized((uint64_t)ns_id)) { - api_set_error(err, kErrorTypeValidation, "Invalid ns_id"); - return 0; - } - - - if (start_row < 0 || start_row >= MAXLNUM || end_row > MAXCOL) { - api_set_error(err, kErrorTypeValidation, "Line number outside range"); - return 0; - } - - if (start_col < 0 || start_col > MAXCOL || end_col > MAXCOL) { - api_set_error(err, kErrorTypeValidation, "Column value outside range"); - return 0; - } - if (end_row < 0 || end_col < 0) { - end_row = -1; - end_col = -1; - } - - if (start_row >= buf->b_ml.ml_line_count - || end_row >= buf->b_ml.ml_line_count) { - // safety check, we can't add marks outside the range - return 0; - } - - int hlg_id = 0; - if (hl_group.size > 0) { - hlg_id = syn_check_group((char_u *)hl_group.data, (int)hl_group.size); - } - - VirtText vt = parse_virt_text(virt_text, err); - if (ERROR_SET(err)) { - return 0; - } - - uint64_t mark_id = extmark_add_decoration(buf, (uint64_t)ns_id, hlg_id, - (int)start_row, (colnr_T)start_col, - (int)end_row, (colnr_T)end_col, vt); - return (Integer)mark_id; + extmark_set(buf, ns_id, 0, (int)line, 0, -1, -1, decor, kExtmarkUndo); + return src_id; } Dictionary nvim__buf_stats(Buffer buffer, Error *err) diff --git a/src/nvim/api/ui_events.in.h b/src/nvim/api/ui_events.in.h index ab31db39e9..ef5e90bf5c 100644 --- a/src/nvim/api/ui_events.in.h +++ b/src/nvim/api/ui_events.in.h @@ -36,6 +36,8 @@ void set_title(String title) FUNC_API_SINCE(3); void set_icon(String icon) FUNC_API_SINCE(3); +void screenshot(String path) + FUNC_API_SINCE(7) FUNC_API_REMOTE_IMPL; void option_set(String name, Object value) FUNC_API_SINCE(4) FUNC_API_BRIDGE_IMPL; // Stop event is not exported as such, represented by EOF in the msgpack stream. diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 305d5e6968..632f55f49a 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -204,9 +204,9 @@ Integer nvim_get_hl_id_by_name(String name) /// /// On execution error: does not fail, but updates v:errmsg. /// -/// If you need to input sequences like <C-o> use |nvim_replace_termcodes| -/// to replace the termcodes and then pass the resulting string to -/// nvim_feedkeys. You'll also want to enable escape_csi. +/// If you need to input sequences like <C-o> use |nvim_replace_termcodes| to +/// replace the termcodes and then pass the resulting string to nvim_feedkeys. +/// You'll also want to enable escape_csi. /// /// Example: /// <pre> @@ -2627,3 +2627,9 @@ void nvim__put_attr(Integer id, Integer start_row, Integer start_col, decorations_add_luahl_attr(attr, (int)start_row, (colnr_T)start_col, (int)end_row, (colnr_T)end_col); } + +void nvim__screenshot(String path) + FUNC_API_FAST +{ + ui_call_screenshot(path); +} diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 5aeb6fa746..21a5603b93 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -10383,10 +10383,13 @@ void script_host_eval(char *name, typval_T *argvars, typval_T *rettv) list_T *args = tv_list_alloc(1); tv_list_append_string(args, (const char *)argvars[0].vval.v_string, -1); - *rettv = eval_call_provider(name, "eval", args); + *rettv = eval_call_provider(name, "eval", args, false); } -typval_T eval_call_provider(char *provider, char *method, list_T *arguments) +/// @param discard Clears the value returned by the provider and returns +/// an empty typval_T. +typval_T eval_call_provider(char *provider, char *method, list_T *arguments, + bool discard) { if (!eval_has_provider(provider)) { emsgf("E319: No \"%s\" provider found. Run \":checkhealth provider\"", @@ -10445,6 +10448,10 @@ typval_T eval_call_provider(char *provider, char *method, list_T *arguments) provider_call_nesting--; assert(provider_call_nesting >= 0); + if (discard) { + tv_clear(&rettv); + } + return rettv; } diff --git a/src/nvim/eval/decode.c b/src/nvim/eval/decode.c index daba304f00..638fef331a 100644 --- a/src/nvim/eval/decode.c +++ b/src/nvim/eval/decode.c @@ -586,7 +586,7 @@ parse_json_number_check: if (p == ints) { emsgf(_("E474: Missing number after minus sign: %.*s"), LENP(s, e)); goto parse_json_number_fail; - } else if (p == fracs || exps_s == fracs + 1) { + } else if (p == fracs || (fracs != NULL && exps_s == fracs + 1)) { emsgf(_("E474: Missing number after decimal dot: %.*s"), LENP(s, e)); goto parse_json_number_fail; } else if (p == exps) { diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 519978f4fb..2bac6cba58 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -326,14 +326,19 @@ static int linelen(int *has_tab) int save; int len; - /* find the first non-blank character */ + // Get the line. If it's empty bail out early (could be the empty string + // for an unloaded buffer). line = get_cursor_line_ptr(); + if (*line == NUL) { + return 0; + } + // find the first non-blank character first = skipwhite(line); - /* find the character after the last non-blank character */ + // find the character after the last non-blank character for (last = first + STRLEN(first); - last > first && ascii_iswhite(last[-1]); --last) - ; + last > first && ascii_iswhite(last[-1]); last--) { + } save = *last; *last = NUL; // Get line length. diff --git a/src/nvim/ex_cmds2.c b/src/nvim/ex_cmds2.c index 0603bd4148..3e169f7a4e 100644 --- a/src/nvim/ex_cmds2.c +++ b/src/nvim/ex_cmds2.c @@ -4172,7 +4172,7 @@ static void script_host_execute(char *name, exarg_T *eap) tv_list_append_number(args, (int)eap->line1); tv_list_append_number(args, (int)eap->line2); - (void)eval_call_provider(name, "execute", args); + (void)eval_call_provider(name, "execute", args, true); } } @@ -4187,7 +4187,7 @@ static void script_host_execute_file(char *name, exarg_T *eap) // current range tv_list_append_number(args, (int)eap->line1); tv_list_append_number(args, (int)eap->line2); - (void)eval_call_provider(name, "execute_file", args); + (void)eval_call_provider(name, "execute_file", args, true); } static void script_host_do_range(char *name, exarg_T *eap) @@ -4196,7 +4196,7 @@ static void script_host_do_range(char *name, exarg_T *eap) tv_list_append_number(args, (int)eap->line1); tv_list_append_number(args, (int)eap->line2); tv_list_append_string(args, (const char *)eap->arg, -1); - (void)eval_call_provider(name, "do_range", args); + (void)eval_call_provider(name, "do_range", args, true); } /// ":drop" diff --git a/src/nvim/extmark.c b/src/nvim/extmark.c index 1457a1172d..c9b1c72828 100644 --- a/src/nvim/extmark.c +++ b/src/nvim/extmark.c @@ -1,29 +1,24 @@ // This is an open source non-commercial project. Dear PVS-Studio, please check // it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com -// Implements extended marks for plugins. Each mark exists in a btree of -// lines containing btrees of columns. +// Implements extended marks for plugins. Marks sit in a MarkTree +// datastructure which provides both efficient mark insertations/lookups +// and adjustment to text changes. See marktree.c for more details. // -// The btree provides efficient range lookups. // A map of pointers to the marks is used for fast lookup by mark id. // -// Marks are moved by calls to extmark_splice. Additionally mark_adjust -// might adjust extmarks to line inserts/deletes. +// Marks are moved by calls to extmark_splice. Some standard interfaces +// mark_adjust and inserted_bytes already adjust marks, check if these are +// being used before adding extmark_splice calls! // // Undo/Redo of marks is implemented by storing the call arguments to // extmark_splice. The list of arguments is applied in extmark_apply_undo. -// The only case where we have to copy extmarks is for the area being effected -// by a delete. +// We have to copy extmark positions when the extmarks are within a +// deleted/changed region. // // Marks live in namespaces that allow plugins/users to segregate marks // from other users. // -// For possible ideas for efficency improvements see: -// http://blog.atom.io/2015/06/16/optimizing-an-important-atom-primitive.html -// TODO(bfredl): These ideas could be used for an enhanced btree, which -// wouldn't need separate line and column layers. -// Other implementations exist in gtk and tk toolkits. -// // Deleting marks only happens when explicitly calling extmark_del, deleteing // over a range of marks will only move the marks. Deleting on a mark will // leave it in same position unless it is on the EOL of a line. @@ -48,6 +43,13 @@ # include "extmark.c.generated.h" #endif +static PMap(uint64_t) *hl_decors; + +void extmark_init(void) +{ + hl_decors = pmap_new(uint64_t)(); +} + static ExtmarkNs *buf_ns_ref(buf_T *buf, uint64_t ns_id, bool put) { if (!buf->b_extmark_ns) { if (!put) { @@ -71,7 +73,8 @@ static ExtmarkNs *buf_ns_ref(buf_T *buf, uint64_t ns_id, bool put) { /// must not be used during iteration! /// @returns the mark id uint64_t extmark_set(buf_T *buf, uint64_t ns_id, uint64_t id, - int row, colnr_T col, ExtmarkOp op) + int row, colnr_T col, int end_row, colnr_T end_col, + Decoration *decor, ExtmarkOp op) { ExtmarkNs *ns = buf_ns_ref(buf, ns_id, true); mtpos_t old_pos; @@ -82,7 +85,7 @@ uint64_t extmark_set(buf_T *buf, uint64_t ns_id, uint64_t id, } else { uint64_t old_mark = map_get(uint64_t, uint64_t)(ns->map, id); if (old_mark) { - if (old_mark & MARKTREE_PAIRED_FLAG) { + if (old_mark & MARKTREE_PAIRED_FLAG || end_row > -1) { extmark_del(buf, ns_id, id); } else { // TODO(bfredl): we need to do more if "revising" a decoration mark. @@ -90,7 +93,12 @@ uint64_t extmark_set(buf_T *buf, uint64_t ns_id, uint64_t id, old_pos = marktree_lookup(buf->b_marktree, old_mark, itr); assert(itr->node); if (old_pos.row == row && old_pos.col == col) { - map_del(uint64_t, ExtmarkItem)(buf->b_extmark_index, old_mark); + ExtmarkItem it = map_del(uint64_t, ExtmarkItem)(buf->b_extmark_index, + old_mark); + if (it.decor) { + decoration_redraw(buf, row, row, it.decor); + free_decoration(it.decor); + } mark = marktree_revise(buf->b_marktree, itr); goto revised; } @@ -101,11 +109,17 @@ uint64_t extmark_set(buf_T *buf, uint64_t ns_id, uint64_t id, } } - mark = marktree_put(buf->b_marktree, row, col, true); + if (end_row > -1) { + mark = marktree_put_pair(buf->b_marktree, + row, col, true, + end_row, end_col, false); + } else { + mark = marktree_put(buf->b_marktree, row, col, true); + } + revised: map_put(uint64_t, ExtmarkItem)(buf->b_extmark_index, mark, - (ExtmarkItem){ ns_id, id, 0, - KV_INITIAL_VALUE }); + (ExtmarkItem){ ns_id, id, decor }); map_put(uint64_t, uint64_t)(ns->map, id, mark); if (op != kExtmarkNoUndo) { @@ -114,6 +128,10 @@ revised: // adding new marks to old undo headers. u_extmark_set(buf, mark, row, col); } + + if (decor) { + decoration_redraw(buf, row, end_row > -1 ? end_row : row, decor); + } return id; } @@ -152,27 +170,23 @@ bool extmark_del(buf_T *buf, uint64_t ns_id, uint64_t id) assert(pos.row >= 0); marktree_del_itr(buf->b_marktree, itr, false); ExtmarkItem item = map_get(uint64_t, ExtmarkItem)(buf->b_extmark_index, mark); + mtpos_t pos2 = pos; if (mark & MARKTREE_PAIRED_FLAG) { - mtpos_t pos2 = marktree_lookup(buf->b_marktree, - mark|MARKTREE_END_FLAG, itr); + pos2 = marktree_lookup(buf->b_marktree, mark|MARKTREE_END_FLAG, itr); assert(pos2.row >= 0); marktree_del_itr(buf->b_marktree, itr, false); - if (item.hl_id && pos2.row >= pos.row) { - redraw_buf_range_later(buf, pos.row+1, pos2.row+1); - } } - if (kv_size(item.virt_text)) { - redraw_buf_line_later(buf, pos.row+1); + if (item.decor) { + decoration_redraw(buf, pos.row, pos2.row, item.decor); + free_decoration(item.decor); } - clear_virttext(&item.virt_text); map_del(uint64_t, uint64_t)(ns->map, id); map_del(uint64_t, ExtmarkItem)(buf->b_extmark_index, mark); // TODO(bfredl): delete it from current undo header, opportunistically? - return true; } @@ -202,9 +216,11 @@ bool extmark_clear(buf_T *buf, uint64_t ns_id, } // the value is either zero or the lnum (row+1) if highlight was present. - static Map(uint64_t, uint64_t) *delete_set = NULL; + static Map(uint64_t, ssize_t) *delete_set = NULL; + typedef struct { Decoration *decor; int row1; } DecorItem; + static kvec_t(DecorItem) decors; if (delete_set == NULL) { - delete_set = map_new(uint64_t, uint64_t)(); + delete_set = map_new(uint64_t, ssize_t)(); } MarkTreeIter itr[1] = { 0 }; @@ -216,14 +232,16 @@ bool extmark_clear(buf_T *buf, uint64_t ns_id, || (mark.row == u_row && mark.col > u_col)) { break; } - uint64_t *del_status = map_ref(uint64_t, uint64_t)(delete_set, mark.id, - false); + ssize_t *del_status = map_ref(uint64_t, ssize_t)(delete_set, mark.id, + false); if (del_status) { marktree_del_itr(buf->b_marktree, itr, false); - map_del(uint64_t, uint64_t)(delete_set, mark.id); - if (*del_status > 0) { - redraw_buf_range_later(buf, (linenr_T)(*del_status), mark.row+1); + if (*del_status >= 0) { // we had a decor_id + DecorItem it = kv_A(decors, *del_status); + decoration_redraw(buf, it.row1, mark.row, it.decor); + free_decoration(it.decor); } + map_del(uint64_t, ssize_t)(delete_set, mark.id); continue; } @@ -233,15 +251,21 @@ bool extmark_clear(buf_T *buf, uint64_t ns_id, assert(item.ns_id > 0 && item.mark_id > 0); if (item.mark_id > 0 && (item.ns_id == ns_id || all_ns)) { - if (kv_size(item.virt_text)) { - redraw_buf_line_later(buf, mark.row+1); - } - clear_virttext(&item.virt_text); marks_cleared = true; if (mark.id & MARKTREE_PAIRED_FLAG) { uint64_t other = mark.id ^ MARKTREE_END_FLAG; - uint64_t status = item.hl_id ? ((uint64_t)mark.row+1) : 0; - map_put(uint64_t, uint64_t)(delete_set, other, status); + ssize_t decor_id = -1; + if (item.decor) { + // Save the decoration and the first pos. Clear the decoration + // later when we know the full range. + decor_id = (ssize_t)kv_size(decors); + kv_push(decors, + ((DecorItem) { .decor = item.decor, .row1 = mark.row })); + } + map_put(uint64_t, ssize_t)(delete_set, other, decor_id); + } else if (item.decor) { + decoration_redraw(buf, mark.row, mark.row, item.decor); + free_decoration(item.decor); } ExtmarkNs *my_ns = all_ns ? buf_ns_ref(buf, item.ns_id, false) : ns; map_del(uint64_t, uint64_t)(my_ns->map, item.mark_id); @@ -251,16 +275,20 @@ bool extmark_clear(buf_T *buf, uint64_t ns_id, marktree_itr_next(buf->b_marktree, itr); } } - uint64_t id, status; - map_foreach(delete_set, id, status, { + uint64_t id; + ssize_t decor_id; + map_foreach(delete_set, id, decor_id, { mtpos_t pos = marktree_lookup(buf->b_marktree, id, itr); assert(itr->node); marktree_del_itr(buf->b_marktree, itr, false); - if (status > 0) { - redraw_buf_range_later(buf, (linenr_T)status, pos.row+1); + if (decor_id >= 0) { + DecorItem it = kv_A(decors, decor_id); + decoration_redraw(buf, it.row1, pos.row, it.decor); + free_decoration(it.decor); } }); - map_clear(uint64_t, uint64_t)(delete_set); + map_clear(uint64_t, ssize_t)(delete_set); + kv_size(decors) = 0; return marks_cleared; } @@ -270,31 +298,44 @@ bool extmark_clear(buf_T *buf, uint64_t ns_id, // will be searched to the start, or end // dir can be set to control the order of the array // amount = amount of marks to find or -1 for all -ExtmarkArray extmark_get(buf_T *buf, uint64_t ns_id, - int l_row, colnr_T l_col, - int u_row, colnr_T u_col, - int64_t amount, bool reverse) +ExtmarkInfoArray extmark_get(buf_T *buf, uint64_t ns_id, + int l_row, colnr_T l_col, + int u_row, colnr_T u_col, + int64_t amount, bool reverse) { - ExtmarkArray array = KV_INITIAL_VALUE; - MarkTreeIter itr[1] = { 0 }; + ExtmarkInfoArray array = KV_INITIAL_VALUE; + MarkTreeIter itr[1]; // Find all the marks marktree_itr_get_ext(buf->b_marktree, (mtpos_t){ l_row, l_col }, itr, reverse, false, NULL); int order = reverse ? -1 : 1; while ((int64_t)kv_size(array) < amount) { mtmark_t mark = marktree_itr_current(itr); + mtpos_t endpos = { -1, -1 }; if (mark.row < 0 || (mark.row - u_row) * order > 0 || (mark.row == u_row && (mark.col - u_col) * order > 0)) { break; } + if (mark.id & MARKTREE_END_FLAG) { + goto next_mark; + } else if (mark.id & MARKTREE_PAIRED_FLAG) { + endpos = marktree_lookup(buf->b_marktree, mark.id | MARKTREE_END_FLAG, + NULL); + } + + ExtmarkItem item = map_get(uint64_t, ExtmarkItem)(buf->b_extmark_index, mark.id); if (item.ns_id == ns_id) { kv_push(array, ((ExtmarkInfo) { .ns_id = item.ns_id, .mark_id = item.mark_id, - .row = mark.row, .col = mark.col })); + .row = mark.row, .col = mark.col, + .end_row = endpos.row, + .end_col = endpos.col, + .decor = item.decor })); } +next_mark: if (reverse) { marktree_itr_prev(buf->b_marktree, itr); } else { @@ -308,7 +349,7 @@ ExtmarkArray extmark_get(buf_T *buf, uint64_t ns_id, ExtmarkInfo extmark_from_id(buf_T *buf, uint64_t ns_id, uint64_t id) { ExtmarkNs *ns = buf_ns_ref(buf, ns_id, false); - ExtmarkInfo ret = { 0, 0, -1, -1 }; + ExtmarkInfo ret = { 0, 0, -1, -1, -1, -1, NULL }; if (!ns) { return ret; } @@ -319,12 +360,22 @@ ExtmarkInfo extmark_from_id(buf_T *buf, uint64_t ns_id, uint64_t id) } mtpos_t pos = marktree_lookup(buf->b_marktree, mark, NULL); + mtpos_t endpos = { -1, -1 }; + if (mark & MARKTREE_PAIRED_FLAG) { + endpos = marktree_lookup(buf->b_marktree, mark | MARKTREE_END_FLAG, NULL); + } assert(pos.row >= 0); + ExtmarkItem item = map_get(uint64_t, ExtmarkItem)(buf->b_extmark_index, + mark); + ret.ns_id = ns_id; ret.mark_id = id; ret.row = pos.row; ret.col = pos.col; + ret.end_row = endpos.row; + ret.end_col = endpos.col; + ret.decor = item.decor; return ret; } @@ -352,7 +403,7 @@ void extmark_free_all(buf_T *buf) map_foreach(buf->b_extmark_index, id, item, { (void)id; - clear_virttext(&item.virt_text); + free_decoration(item.decor); }); map_free(uint64_t, ExtmarkItem)(buf->b_extmark_index); buf->b_extmark_index = NULL; @@ -642,50 +693,6 @@ uint64_t src2ns(Integer *src_id) } } -/// Adds a decoration to a buffer. -/// -/// Unlike matchaddpos() highlights, these follow changes to the the buffer -/// texts. Decorations are represented internally and in the API as extmarks. -/// -/// @param buf The buffer to add decorations to -/// @param ns_id A valid namespace id. -/// @param hl_id Id of the highlight group to use (or zero) -/// @param start_row The line to highlight -/// @param start_col First column to highlight -/// @param end_row The line to highlight -/// @param end_col The last column to highlight -/// @param virt_text Virtual text (currently placed at the EOL of start_row) -/// @return The extmark id inside the namespace -uint64_t extmark_add_decoration(buf_T *buf, uint64_t ns_id, int hl_id, - int start_row, colnr_T start_col, - int end_row, colnr_T end_col, - VirtText virt_text) -{ - ExtmarkNs *ns = buf_ns_ref(buf, ns_id, true); - ExtmarkItem item; - item.ns_id = ns_id; - item.mark_id = ns->free_id++; - item.hl_id = hl_id; - item.virt_text = virt_text; - - uint64_t mark; - - if (end_row > -1) { - mark = marktree_put_pair(buf->b_marktree, - start_row, start_col, true, - end_row, end_col, false); - } else { - mark = marktree_put(buf->b_marktree, start_row, start_col, true); - } - - map_put(uint64_t, ExtmarkItem)(buf->b_extmark_index, mark, item); - map_put(uint64_t, uint64_t)(ns->map, item.mark_id, mark); - - redraw_buf_range_later(buf, start_row+1, - (end_row >= 0 ? end_row : start_row) + 1); - return item.mark_id; -} - /// Add highlighting to a buffer, bounded by two cursor positions, /// with an offset. /// @@ -705,6 +712,7 @@ void bufhl_add_hl_pos_offset(buf_T *buf, { colnr_T hl_start = 0; colnr_T hl_end = 0; + Decoration *decor = decoration_hl(hl_id); // TODO(bfredl): if decoration had blocky mode, we could avoid this loop for (linenr_T lnum = pos_start.lnum; lnum <= pos_end.lnum; lnum ++) { @@ -729,10 +737,44 @@ void bufhl_add_hl_pos_offset(buf_T *buf, hl_start = pos_start.col + offset; hl_end = pos_end.col + offset; } - (void)extmark_add_decoration(buf, (uint64_t)src_id, hl_id, - (int)lnum-1, hl_start, - (int)lnum-1+end_off, hl_end, - VIRTTEXT_EMPTY); + (void)extmark_set(buf, (uint64_t)src_id, 0, + (int)lnum-1, hl_start, (int)lnum-1+end_off, hl_end, + decor, kExtmarkUndo); + } +} + +Decoration *decoration_hl(int hl_id) +{ + assert(hl_id > 0); + Decoration **dp = (Decoration **)pmap_ref(uint64_t)(hl_decors, + (uint64_t)hl_id, true); + if (*dp) { + return *dp; + } + + Decoration *decor = xcalloc(1, sizeof(*decor)); + decor->hl_id = hl_id; + decor->shared = true; + *dp = decor; + return decor; +} + +void decoration_redraw(buf_T *buf, int row1, int row2, Decoration *decor) +{ + if (decor->hl_id && row2 >= row1) { + redraw_buf_range_later(buf, row1+1, row2+1); + } + + if (kv_size(decor->virt_text)) { + redraw_buf_line_later(buf, row1+1); + } +} + +void free_decoration(Decoration *decor) +{ + if (decor && !decor->shared) { + clear_virttext(&decor->virt_text); + xfree(decor); } } @@ -757,8 +799,8 @@ VirtText *extmark_find_virttext(buf_T *buf, int row, uint64_t ns_id) ExtmarkItem *item = map_ref(uint64_t, ExtmarkItem)(buf->b_extmark_index, mark.id, false); if (item && (ns_id == 0 || ns_id == item->ns_id) - && kv_size(item->virt_text)) { - return &item->virt_text; + && item->decor && kv_size(item->decor->virt_text)) { + return &item->decor->virt_text; } marktree_itr_next(buf->b_marktree, itr); } @@ -787,7 +829,6 @@ bool decorations_redraw_start(buf_T *buf, int top_row, if (mark.row < 0) { // || mark.row > end_row break; } - // TODO(bfredl): dedicated flag for being a decoration? if ((mark.row < top_row && mark.id&MARKTREE_END_FLAG)) { goto next_mark; } @@ -797,25 +838,30 @@ bool decorations_redraw_start(buf_T *buf, int top_row, uint64_t start_id = mark.id & ~MARKTREE_END_FLAG; ExtmarkItem *item = map_ref(uint64_t, ExtmarkItem)(buf->b_extmark_index, start_id, false); + if (!item || !item->decor) { + // TODO(bfredl): dedicated flag for being a decoration? + goto next_mark; + } + Decoration *decor = item->decor; + if ((!(mark.id&MARKTREE_END_FLAG) && altpos.row < top_row - && item && !kv_size(item->virt_text)) + && item && !kv_size(decor->virt_text)) || ((mark.id&MARKTREE_END_FLAG) && altpos.row >= top_row)) { goto next_mark; } - if (item && (item->hl_id > 0 || kv_size(item->virt_text))) { - int attr_id = item->hl_id > 0 ? syn_id2attr(item->hl_id) : 0; - VirtText *vt = kv_size(item->virt_text) ? &item->virt_text : NULL; - HlRange range; - if (mark.id&MARKTREE_END_FLAG) { - range = (HlRange){ altpos.row, altpos.col, mark.row, mark.col, - attr_id, vt }; - } else { - range = (HlRange){ mark.row, mark.col, altpos.row, - altpos.col, attr_id, vt }; - } - kv_push(state->active, range); + int attr_id = decor->hl_id > 0 ? syn_id2attr(decor->hl_id) : 0; + VirtText *vt = kv_size(decor->virt_text) ? &decor->virt_text : NULL; + HlRange range; + if (mark.id&MARKTREE_END_FLAG) { + range = (HlRange){ altpos.row, altpos.col, mark.row, mark.col, + attr_id, vt }; + } else { + range = (HlRange){ mark.row, mark.col, altpos.row, + altpos.col, attr_id, vt }; } + kv_push(state->active, range); + next_mark: if (marktree_itr_node_done(state->itr)) { break; @@ -860,21 +906,24 @@ int decorations_redraw_col(buf_T *buf, int col, DecorationRedrawState *state) ExtmarkItem *item = map_ref(uint64_t, ExtmarkItem)(buf->b_extmark_index, mark.id, false); + if (!item || !item->decor) { + // TODO(bfredl): dedicated flag for being a decoration? + goto next_mark; + } + Decoration *decor = item->decor; if (endpos.row < mark.row || (endpos.row == mark.row && endpos.col <= mark.col)) { - if (item && !kv_size(item->virt_text)) { + if (item && !kv_size(decor->virt_text)) { goto next_mark; } } - if (item && (item->hl_id > 0 || kv_size(item->virt_text))) { - int attr_id = item->hl_id > 0 ? syn_id2attr(item->hl_id) : 0; - VirtText *vt = kv_size(item->virt_text) ? &item->virt_text : NULL; - kv_push(state->active, ((HlRange){ mark.row, mark.col, - endpos.row, endpos.col, - attr_id, vt })); - } + int attr_id = decor->hl_id > 0 ? syn_id2attr(decor->hl_id) : 0; + VirtText *vt = kv_size(decor->virt_text) ? &decor->virt_text : NULL; + kv_push(state->active, ((HlRange){ mark.row, mark.col, + endpos.row, endpos.col, + attr_id, vt })); next_mark: marktree_itr_next(buf->b_marktree, state->itr); diff --git a/src/nvim/extmark.h b/src/nvim/extmark.h index b5eb0db3b6..c569e8b78d 100644 --- a/src/nvim/extmark.h +++ b/src/nvim/extmark.h @@ -13,9 +13,12 @@ typedef struct uint64_t mark_id; int row; colnr_T col; + int end_row; + colnr_T end_col; + Decoration *decor; } ExtmarkInfo; -typedef kvec_t(ExtmarkInfo) ExtmarkArray; +typedef kvec_t(ExtmarkInfo) ExtmarkInfoArray; // delete the columns between mincol and endcol diff --git a/src/nvim/extmark_defs.h b/src/nvim/extmark_defs.h index c927048981..76804db848 100644 --- a/src/nvim/extmark_defs.h +++ b/src/nvim/extmark_defs.h @@ -14,12 +14,20 @@ typedef kvec_t(VirtTextChunk) VirtText; typedef struct { - uint64_t ns_id; - uint64_t mark_id; int hl_id; // highlight group - // TODO(bfredl): virt_text is pretty larger than the rest, - // pointer indirection? VirtText virt_text; + // TODO(bfredl): style, signs, etc + bool shared; // shared decoration, don't free +} Decoration; + +typedef struct +{ + uint64_t ns_id; + uint64_t mark_id; + // TODO(bfredl): a lot of small allocations. Should probably use + // kvec_t(Decoration) as an arena. Alternatively, store ns_id/mark_id + // _inline_ in MarkTree and use the map only for decorations. + Decoration *decor; } ExtmarkItem; typedef struct undo_object ExtmarkUndoObject; diff --git a/src/nvim/fold.c b/src/nvim/fold.c index 61a85171e8..c29b878491 100644 --- a/src/nvim/fold.c +++ b/src/nvim/fold.c @@ -573,31 +573,36 @@ void foldCreate(win_T *wp, linenr_T start, linenr_T end) // Find the place to insert the new fold gap = &wp->w_folds; - for (;; ) { - if (!foldFind(gap, start_rel, &fp)) - break; - if (fp->fd_top + fp->fd_len > end_rel) { - /* New fold is completely inside this fold: Go one level deeper. */ - gap = &fp->fd_nested; - start_rel -= fp->fd_top; - end_rel -= fp->fd_top; - if (use_level || fp->fd_flags == FD_LEVEL) { - use_level = true; - if (level >= wp->w_p_fdl) { + if (gap->ga_len == 0) { + i = 0; + } else { + for (;;) { + if (!foldFind(gap, start_rel, &fp)) { + break; + } + if (fp->fd_top + fp->fd_len > end_rel) { + // New fold is completely inside this fold: Go one level deeper. + gap = &fp->fd_nested; + start_rel -= fp->fd_top; + end_rel -= fp->fd_top; + if (use_level || fp->fd_flags == FD_LEVEL) { + use_level = true; + if (level >= wp->w_p_fdl) { + closed = true; + } + } else if (fp->fd_flags == FD_CLOSED) { closed = true; } - } else if (fp->fd_flags == FD_CLOSED) { - closed = true; + level++; + } else { + // This fold and new fold overlap: Insert here and move some folds + // inside the new fold. + break; } - level++; - } else { - /* This fold and new fold overlap: Insert here and move some folds - * inside the new fold. */ - break; } + i = (int)(fp - (fold_T *)gap->ga_data); } - i = (int)(fp - (fold_T *)gap->ga_data); ga_grow(gap, 1); { fp = (fold_T *)gap->ga_data + i; @@ -788,13 +793,15 @@ void foldUpdate(win_T *wp, linenr_T top, linenr_T bot) return; } - // Mark all folds from top to bot as maybe-small. - fold_T *fp; - (void)foldFind(&wp->w_folds, top, &fp); - while (fp < (fold_T *)wp->w_folds.ga_data + wp->w_folds.ga_len - && fp->fd_top < bot) { - fp->fd_small = kNone; - fp++; + if (wp->w_folds.ga_len > 0) { + // Mark all folds from top to bot as maybe-small. + fold_T *fp; + (void)foldFind(&wp->w_folds, top, &fp); + while (fp < (fold_T *)wp->w_folds.ga_data + wp->w_folds.ga_len + && fp->fd_top < bot) { + fp->fd_small = kNone; + fp++; + } } if (foldmethodIsIndent(wp) @@ -1058,11 +1065,16 @@ void cloneFoldGrowArray(garray_T *from, garray_T *to) * the first fold below it (careful: it can be beyond the end of the array!). * Returns FALSE when there is no fold that contains "lnum". */ -static int foldFind(const garray_T *gap, linenr_T lnum, fold_T **fpp) +static bool foldFind(const garray_T *gap, linenr_T lnum, fold_T **fpp) { linenr_T low, high; fold_T *fp; + if (gap->ga_len == 0) { + *fpp = NULL; + return false; + } + /* * Perform a binary search. * "low" is lowest index of possible match. @@ -1086,7 +1098,7 @@ static int foldFind(const garray_T *gap, linenr_T lnum, fold_T **fpp) } } *fpp = fp + low; - return FALSE; + return false; } /* foldLevelWin() {{{2 */ @@ -1220,9 +1232,10 @@ setManualFoldWin( gap = &wp->w_folds; for (;; ) { if (!foldFind(gap, lnum, &fp)) { - /* If there is a following fold, continue there next time. */ - if (fp < (fold_T *)gap->ga_data + gap->ga_len) + // If there is a following fold, continue there next time. + if (fp != NULL && fp < (fold_T *)gap->ga_data + gap->ga_len) { next = fp->fd_top + off; + } break; } @@ -1389,6 +1402,10 @@ static void foldMarkAdjustRecurse( linenr_T last; linenr_T top; + if (gap->ga_len == 0) { + return; + } + /* In Insert mode an inserted line at the top of a fold is considered part * of the fold, otherwise it isn't. */ if ((State & INSERT) && amount == (linenr_T)1 && line2 == MAXLNUM) @@ -2265,14 +2282,15 @@ static linenr_T foldUpdateIEMSRecurse( /* Find an existing fold to re-use. Preferably one that * includes startlnum, otherwise one that ends just before * startlnum or starts after it. */ - if (foldFind(gap, startlnum, &fp) - || (fp < ((fold_T *)gap->ga_data) + gap->ga_len - && fp->fd_top <= firstlnum) - || foldFind(gap, firstlnum - concat, &fp) - || (fp < ((fold_T *)gap->ga_data) + gap->ga_len - && ((lvl < level && fp->fd_top < flp->lnum) - || (lvl >= level - && fp->fd_top <= flp->lnum_save)))) { + if (gap->ga_len > 0 + && (foldFind(gap, startlnum, &fp) + || (fp < ((fold_T *)gap->ga_data) + gap->ga_len + && fp->fd_top <= firstlnum) + || foldFind(gap, firstlnum - concat, &fp) + || (fp < ((fold_T *)gap->ga_data) + gap->ga_len + && ((lvl < level && fp->fd_top < flp->lnum) + || (lvl >= level + && fp->fd_top <= flp->lnum_save))))) { if (fp->fd_top + fp->fd_len + concat > firstlnum) { /* Use existing fold for the new fold. If it starts * before where we started looking, extend it. If it @@ -2363,7 +2381,11 @@ static linenr_T foldUpdateIEMSRecurse( } else { /* Insert new fold. Careful: ga_data may be NULL and it * may change! */ - i = (int)(fp - (fold_T *)gap->ga_data); + if (gap->ga_len == 0) { + i = 0; + } else { + i = (int)(fp - (fold_T *)gap->ga_data); + } foldInsert(gap, i); fp = (fold_T *)gap->ga_data + i; /* The new fold continues until bot, unless we find the @@ -2559,9 +2581,10 @@ static void foldInsert(garray_T *gap, int i) ga_grow(gap, 1); fp = (fold_T *)gap->ga_data + i; - if (i < gap->ga_len) + if (gap->ga_len > 0 && i < gap->ga_len) { memmove(fp + 1, fp, sizeof(fold_T) * (size_t)(gap->ga_len - i)); - ++gap->ga_len; + } + gap->ga_len++; ga_init(&fp->fd_nested, (int)sizeof(fold_T), 10); } @@ -2596,17 +2619,18 @@ static void foldSplit(buf_T *buf, garray_T *const gap, * any between top and bot, they have been removed by the caller. */ garray_T *const gap1 = &fp->fd_nested; garray_T *const gap2 = &fp[1].fd_nested; - (void)(foldFind(gap1, bot + 1 - fp->fd_top, &fp2)); - const int len = (int)((fold_T *)gap1->ga_data + gap1->ga_len - fp2); - if (len > 0) { - ga_grow(gap2, len); - for (int idx = 0; idx < len; idx++) { - ((fold_T *)gap2->ga_data)[idx] = fp2[idx]; - ((fold_T *)gap2->ga_data)[idx].fd_top - -= fp[1].fd_top - fp->fd_top; - } - gap2->ga_len = len; - gap1->ga_len -= len; + if (foldFind(gap1, bot + 1 - fp->fd_top, &fp2)) { + const int len = (int)((fold_T *)gap1->ga_data + gap1->ga_len - fp2); + if (len > 0) { + ga_grow(gap2, len); + for (int idx = 0; idx < len; idx++) { + ((fold_T *)gap2->ga_data)[idx] = fp2[idx]; + ((fold_T *)gap2->ga_data)[idx].fd_top + -= fp[1].fd_top - fp->fd_top; + } + gap2->ga_len = len; + gap1->ga_len -= len; + } } fp->fd_len = top - fp->fd_top; fold_changed = true; @@ -2641,7 +2665,7 @@ static void foldRemove( return; // nothing to do } - for (;; ) { + while (gap->ga_len > 0) { // Find fold that includes top or a following one. if (foldFind(gap, top, &fp) && fp->fd_top < top) { // 2: or 3: need to delete nested folds @@ -2657,7 +2681,8 @@ static void foldRemove( fold_changed = true; continue; } - if (fp >= (fold_T *)(gap->ga_data) + gap->ga_len + if (gap->ga_data == NULL + || fp >= (fold_T *)(gap->ga_data) + gap->ga_len || fp->fd_top > bot) { // 6: Found a fold below bot, can stop looking. break; @@ -2738,7 +2763,8 @@ static void truncate_fold(win_T *const wp, fold_T *fp, linenr_T end) } #define FOLD_END(fp) ((fp)->fd_top + (fp)->fd_len - 1) -#define VALID_FOLD(fp, gap) ((fp) < ((fold_T *)(gap)->ga_data + (gap)->ga_len)) +#define VALID_FOLD(fp, gap) \ + ((gap)->ga_len > 0 && (fp) < ((fold_T *)(gap)->ga_data + (gap)->ga_len)) #define FOLD_INDEX(fp, gap) ((size_t)(fp - ((fold_T *)(gap)->ga_data))) void foldMoveRange( win_T *const wp, garray_T *gap, diff --git a/src/nvim/lua/treesitter.c b/src/nvim/lua/treesitter.c index 138031237e..9a58823d64 100644 --- a/src/nvim/lua/treesitter.c +++ b/src/nvim/lua/treesitter.c @@ -62,6 +62,7 @@ static struct luaL_Reg node_meta[] = { { "end_", node_end }, { "type", node_type }, { "symbol", node_symbol }, + { "field", node_field }, { "named", node_named }, { "missing", node_missing }, { "has_error", node_has_error }, @@ -73,6 +74,7 @@ static struct luaL_Reg node_meta[] = { { "descendant_for_range", node_descendant_for_range }, { "named_descendant_for_range", node_named_descendant_for_range }, { "parent", node_parent }, + { "iter_children", node_iter_children }, { "_rawquery", node_rawquery }, { NULL, NULL } }; @@ -84,12 +86,17 @@ static struct luaL_Reg query_meta[] = { { NULL, NULL } }; -// cursor is not exposed, but still needs garbage collection +// cursors are not exposed, but still needs garbage collection static struct luaL_Reg querycursor_meta[] = { { "__gc", querycursor_gc }, { NULL, NULL } }; +static struct luaL_Reg treecursor_meta[] = { + { "__gc", treecursor_gc }, + { NULL, NULL } +}; + static PMap(cstr_t) *langs; static void build_meta(lua_State *L, const char *tname, const luaL_Reg *meta) @@ -116,6 +123,7 @@ void tslua_init(lua_State *L) build_meta(L, "treesitter_node", node_meta); build_meta(L, "treesitter_query", query_meta); build_meta(L, "treesitter_querycursor", querycursor_meta); + build_meta(L, "treesitter_treecursor", treecursor_meta); } int tslua_has_language(lua_State *L) @@ -646,6 +654,34 @@ static int node_symbol(lua_State *L) return 1; } +static int node_field(lua_State *L) +{ + TSNode node; + if (!node_check(L, 1, &node)) { + return 0; + } + + size_t name_len; + const char *field_name = luaL_checklstring(L, 2, &name_len); + + TSTreeCursor cursor = ts_tree_cursor_new(node); + + lua_newtable(L); // [table] + unsigned int curr_index = 0; + + if (ts_tree_cursor_goto_first_child(&cursor)) { + do { + if (!STRCMP(field_name, ts_tree_cursor_current_field_name(&cursor))) { + push_node(L, ts_tree_cursor_current_node(&cursor), 1); // [table, node] + lua_rawseti(L, -2, ++curr_index); + } + } while (ts_tree_cursor_goto_next_sibling(&cursor)); + } + + ts_tree_cursor_delete(&cursor); + return 1; +} + static int node_named(lua_State *L) { TSNode node; @@ -746,6 +782,74 @@ static int node_named_descendant_for_range(lua_State *L) return 1; } +static int node_next_child(lua_State *L) +{ + TSTreeCursor *ud = luaL_checkudata( + L, lua_upvalueindex(1), "treesitter_treecursor"); + if (!ud) { + return 0; + } + + TSNode source; + if (!node_check(L, lua_upvalueindex(2), &source)) { + return 0; + } + + // First call should return first child + if (ts_node_eq(source, ts_tree_cursor_current_node(ud))) { + if (ts_tree_cursor_goto_first_child(ud)) { + goto push; + } else { + goto end; + } + } + + if (ts_tree_cursor_goto_next_sibling(ud)) { +push: + push_node( + L, + ts_tree_cursor_current_node(ud), + lua_upvalueindex(2)); // [node] + + const char * field = ts_tree_cursor_current_field_name(ud); + + if (field != NULL) { + lua_pushstring(L, ts_tree_cursor_current_field_name(ud)); + } else { + lua_pushnil(L); + } // [node, field_name_or_nil] + return 2; + } + +end: + return 0; +} + +static int node_iter_children(lua_State *L) +{ + TSNode source; + if (!node_check(L, 1, &source)) { + return 0; + } + + TSTreeCursor *ud = lua_newuserdata(L, sizeof(TSTreeCursor)); // [udata] + *ud = ts_tree_cursor_new(source); + + lua_getfield(L, LUA_REGISTRYINDEX, "treesitter_treecursor"); // [udata, mt] + lua_setmetatable(L, -2); // [udata] + lua_pushvalue(L, 1); // [udata, source_node] + lua_pushcclosure(L, node_next_child, 2); + + return 1; +} + +static int treecursor_gc(lua_State *L) +{ + TSTreeCursor *ud = luaL_checkudata(L, 1, "treesitter_treecursor"); + ts_tree_cursor_delete(ud); + return 0; +} + static int node_parent(lua_State *L) { TSNode node; diff --git a/src/nvim/main.c b/src/nvim/main.c index f79fb57eae..1374c5eb5d 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -21,6 +21,7 @@ #include "nvim/ex_cmds.h" #include "nvim/ex_cmds2.h" #include "nvim/ex_docmd.h" +#include "nvim/extmark.h" #include "nvim/fileio.h" #include "nvim/fold.h" #include "nvim/getchar.h" @@ -160,6 +161,7 @@ void early_init(mparm_T *paramp) env_init(); fs_init(); handle_init(); + extmark_init(); eval_init(); // init global variables init_path(argv0 ? argv0 : "nvim"); init_normal_cmds(); // Init the table of Normal mode commands. diff --git a/src/nvim/map.c b/src/nvim/map.c index cba39f24b3..0c6bad7cb6 100644 --- a/src/nvim/map.c +++ b/src/nvim/map.c @@ -184,7 +184,7 @@ MAP_IMPL(uint64_t, uint64_t, DEFAULT_INITIALIZER) #define EXTMARK_NS_INITIALIZER { 0, 0 } MAP_IMPL(uint64_t, ExtmarkNs, EXTMARK_NS_INITIALIZER) #define KVEC_INITIALIZER { .size = 0, .capacity = 0, .items = NULL } -#define EXTMARK_ITEM_INITIALIZER { 0, 0, 0, KVEC_INITIALIZER } +#define EXTMARK_ITEM_INITIALIZER { 0, 0, NULL } MAP_IMPL(uint64_t, ExtmarkItem, EXTMARK_ITEM_INITIALIZER) MAP_IMPL(handle_T, ptr_t, DEFAULT_INITIALIZER) #define MSGPACK_HANDLER_INITIALIZER { .fn = NULL, .fast = false } diff --git a/src/nvim/map.h b/src/nvim/map.h index 0ad7865bf0..63a18f4129 100644 --- a/src/nvim/map.h +++ b/src/nvim/map.h @@ -73,6 +73,7 @@ MAP_DECLS(String, handle_T) #define pmap_has(T) map_has(T, ptr_t) #define pmap_key(T) map_key(T, ptr_t) #define pmap_put(T) map_put(T, ptr_t) +#define pmap_ref(T) map_ref(T, ptr_t) /// @see pmap_del2 #define pmap_del(T) map_del(T, ptr_t) #define pmap_clear(T) map_clear(T, ptr_t) diff --git a/src/nvim/ops.c b/src/nvim/ops.c index d31328219f..7e0cd737ca 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -5919,7 +5919,7 @@ static bool get_clipboard(int name, yankreg_T **target, bool quiet) const char regname = (char)name; tv_list_append_string(args, ®name, 1); - typval_T result = eval_call_provider("clipboard", "get", args); + typval_T result = eval_call_provider("clipboard", "get", args, false); if (result.v_type != VAR_LIST) { if (result.v_type == VAR_NUMBER && result.vval.v_number == 0) { @@ -6067,7 +6067,7 @@ static void set_clipboard(int name, yankreg_T *reg) tv_list_append_string(args, ®type, 1); // -V614 tv_list_append_string(args, ((char[]) { (char)name }), 1); - (void)eval_call_provider("clipboard", "set", args); + (void)eval_call_provider("clipboard", "set", args, true); } /// Avoid slow things (clipboard) during batch operations (while/for-loops). diff --git a/src/nvim/option_defs.h b/src/nvim/option_defs.h index ecaa941082..eb8f051b9c 100644 --- a/src/nvim/option_defs.h +++ b/src/nvim/option_defs.h @@ -453,7 +453,6 @@ EXTERN char_u *p_header; // 'printheader' EXTERN int p_prompt; // 'prompt' EXTERN char_u *p_guicursor; // 'guicursor' EXTERN char_u *p_guifont; // 'guifont' -EXTERN char_u *p_guifontset; // 'guifontset' EXTERN char_u *p_guifontwide; // 'guifontwide' EXTERN char_u *p_hf; // 'helpfile' EXTERN long p_hh; // 'helpheight' diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 60a38dc2e3..79fea1f628 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -1022,15 +1022,6 @@ return { defaults={if_true={vi=""}} }, { - full_name='guifontset', abbreviation='gfs', - type='string', list='onecomma', scope={'global'}, - deny_duplicates=true, - vi_def=true, - varname='p_guifontset', - redraw={'ui_option'}, - defaults={if_true={vi=""}} - }, - { full_name='guifontwide', abbreviation='gfw', type='string', list='onecomma', scope={'global'}, deny_duplicates=true, diff --git a/src/nvim/os/time.c b/src/nvim/os/time.c index 346e40e02e..4b6533cd0c 100644 --- a/src/nvim/os/time.c +++ b/src/nvim/os/time.c @@ -56,6 +56,8 @@ uint64_t os_now(void) /// Sleeps for `ms` milliseconds. /// +/// @see uv_sleep() (libuv v1.34.0) +/// /// @param ms Number of milliseconds to sleep /// @param ignoreinput If true, only SIGINT (CTRL-C) can interrupt. void os_delay(uint64_t ms, bool ignoreinput) @@ -72,6 +74,8 @@ void os_delay(uint64_t ms, bool ignoreinput) /// Sleeps for `us` microseconds. /// +/// @see uv_sleep() (libuv v1.34.0) +/// /// @param us Number of microseconds to sleep. /// @param ignoreinput If true, ignore all input (including SIGINT/CTRL-C). /// If false, waiting is aborted on any input. @@ -172,10 +176,11 @@ char *os_ctime_r(const time_t *restrict clock, char *restrict result, struct tm *clock_local_ptr = os_localtime_r(clock, &clock_local); // MSVC returns NULL for an invalid value of seconds. if (clock_local_ptr == NULL) { - snprintf(result, result_len, "%s\n", _("(Invalid)")); + xstrlcpy(result, _("(Invalid)"), result_len); } else { - strftime(result, result_len, "%a %b %d %H:%M:%S %Y\n", clock_local_ptr); + strftime(result, result_len, _("%a %b %d %H:%M:%S %Y"), clock_local_ptr); } + xstrlcat(result, "\n", result_len); return result; } diff --git a/src/nvim/po/check.vim b/src/nvim/po/check.vim index 650c6155e2..d55d4cfa4d 100644 --- a/src/nvim/po/check.vim +++ b/src/nvim/po/check.vim @@ -25,6 +25,7 @@ func! GetMline() " remove '%', not used for formatting. let idline = substitute(idline, "'%'", '', 'g') + let idline = substitute(idline, "%%", '', 'g') " remove '%' used for plural forms. let idline = substitute(idline, '\\nPlural-Forms: .\+;\\n', '', '') diff --git a/src/nvim/screen.c b/src/nvim/screen.c index 69de1de6b2..c68a90d6e3 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -2554,6 +2554,7 @@ win_line ( } // If this line has a sign with line highlighting set line_attr. + // TODO(bfredl, vigoux): this should not take priority over decorations! v = buf_getsigntype(wp->w_buffer, lnum, SIGN_LINEHL, 0, 1); if (v != 0) { line_attr = sign_get_attr((int)v, SIGN_LINEHL); diff --git a/src/nvim/spell.c b/src/nvim/spell.c index 95948dac78..05bb501fa2 100644 --- a/src/nvim/spell.c +++ b/src/nvim/spell.c @@ -5663,6 +5663,9 @@ check_suggestions ( int len; hlf_T attr; + if (gap->ga_len == 0) { + return; + } stp = &SUG(*gap, 0); for (int i = gap->ga_len - 1; i >= 0; --i) { // Need to append what follows to check for "the the". @@ -5765,14 +5768,14 @@ cleanup_suggestions ( ) FUNC_ATTR_NONNULL_ALL { - suggest_T *stp = &SUG(*gap, 0); - if (gap->ga_len > 0) { // Sort the list. qsort(gap->ga_data, (size_t)gap->ga_len, sizeof(suggest_T), sug_compare); // Truncate the list to the number of suggestions that will be displayed. if (gap->ga_len > keep) { + suggest_T *const stp = &SUG(*gap, 0); + for (int i = keep; i < gap->ga_len; i++) { xfree(stp[i].st_word); } diff --git a/src/nvim/spellfile.c b/src/nvim/spellfile.c index a7d26e2a8e..09d8646c6d 100644 --- a/src/nvim/spellfile.c +++ b/src/nvim/spellfile.c @@ -984,15 +984,17 @@ nextone: static char_u *read_cnt_string(FILE *fd, int cnt_bytes, int *cntp) { int cnt = 0; - int i; char_u *str; // read the length bytes, MSB first - for (i = 0; i < cnt_bytes; ++i) - cnt = (cnt << 8) + getc(fd); - if (cnt < 0) { - *cntp = SP_TRUNCERROR; - return NULL; + for (int i = 0; i < cnt_bytes; i++) { + const int c = getc(fd); + + if (c == EOF) { + *cntp = SP_TRUNCERROR; + return NULL; + } + cnt = (cnt << 8) + (unsigned)c; } *cntp = cnt; if (cnt == 0) @@ -3038,9 +3040,9 @@ static int spell_read_dic(spellinfo_T *spin, char_u *fname, afffile_T *affile) spin->si_msg_count = 999999; // Read and ignore the first line: word count. - (void)vim_fgets(line, MAXLINELEN, fd); - if (!ascii_isdigit(*skipwhite(line))) + if (vim_fgets(line, MAXLINELEN, fd) || !ascii_isdigit(*skipwhite(line))) { EMSG2(_("E760: No word count in %s"), fname); + } // Read all the lines in the file one by one. // The words are converted to 'encoding' here, before being added to diff --git a/src/nvim/testdir/check.vim b/src/nvim/testdir/check.vim index 57a8eb57b8..073873bcb0 100644 --- a/src/nvim/testdir/check.vim +++ b/src/nvim/testdir/check.vim @@ -1,6 +1,46 @@ source shared.vim source term_util.vim +" Command to check for the presence of a feature. +command -nargs=1 CheckFeature call CheckFeature(<f-args>) +func CheckFeature(name) + if !has(a:name) + throw 'Skipped: ' .. a:name .. ' feature missing' + endif +endfunc + +" Command to check for the presence of a working option. +command -nargs=1 CheckOption call CheckOption(<f-args>) +func CheckOption(name) + if !exists('+' .. a:name) + throw 'Skipped: ' .. a:name .. ' option not supported' + endif +endfunc + +" Command to check for the presence of a function. +command -nargs=1 CheckFunction call CheckFunction(<f-args>) +func CheckFunction(name) + if !exists('*' .. a:name) + throw 'Skipped: ' .. a:name .. ' function missing' + endif +endfunc + +" Command to check for running on MS-Windows +command CheckMSWindows call CheckMSWindows() +func CheckMSWindows() + if !has('win32') + throw 'Skipped: only works on MS-Windows' + endif +endfunc + +" Command to check for running on Unix +command CheckUnix call CheckUnix() +func CheckUnix() + if !has('unix') + throw 'Skipped: only works on Unix' + endif +endfunc + " Command to check that making screendumps is supported. " Caller must source screendump.vim command CheckScreendump call CheckScreendump() @@ -9,3 +49,19 @@ func CheckScreendump() throw 'Skipped: cannot make screendumps' endif endfunc + +" Command to check that we can Run Vim in a terminal window +command CheckRunVimInTerminal call CheckRunVimInTerminal() +func CheckRunVimInTerminal() + if !CanRunVimInTerminal() + throw 'Skipped: cannot run Vim in a terminal window' + endif +endfunc + +" Command to check that we can run the GUI +command CheckCanRunGui call CheckCanRunGui() +func CheckCanRunGui() + if !has('gui') || ($DISPLAY == "" && !has('gui_running')) + throw 'Skipped: cannot start the GUI' + endif +endfunc diff --git a/src/nvim/testdir/runtest.vim b/src/nvim/testdir/runtest.vim index 4f16aa807c..765ba2cbb6 100644 --- a/src/nvim/testdir/runtest.vim +++ b/src/nvim/testdir/runtest.vim @@ -101,6 +101,8 @@ let &runtimepath .= ','.expand($BUILD_DIR).'/runtime/' " Always use forward slashes. set shellslash +let s:t_bold = &t_md +let s:t_normal = &t_me if has('win32') " avoid prompt that is long or contains a line break let $PROMPT = '$P$G' @@ -209,7 +211,15 @@ func RunTheTest(test) let message = 'Executed ' . a:test if has('reltime') - let message ..= ' in ' .. reltimestr(reltime(func_start)) .. ' seconds' + let message ..= repeat(' ', 50 - len(message)) + let time = reltime(func_start) + if has('float') && reltimefloat(time) > 0.1 + let message = s:t_bold .. message + endif + let message ..= ' in ' .. reltimestr(time) .. ' seconds' + if has('float') && reltimefloat(time) > 0.1 + let message ..= s:t_normal + endif endif call add(s:messages, message) let s:done += 1 @@ -277,7 +287,9 @@ func FinishTesting() let message = 'Executed ' . s:done . (s:done > 1 ? ' tests' : ' test') endif if s:done > 0 && has('reltime') + let message = s:t_bold .. message .. repeat(' ', 40 - len(message)) let message ..= ' in ' .. reltimestr(reltime(s:start_time)) .. ' seconds' + let message ..= s:t_normal endif echo message call add(s:messages, message) diff --git a/src/nvim/testdir/shared.vim b/src/nvim/testdir/shared.vim index 41ff9b2bd6..6180d542ff 100644 --- a/src/nvim/testdir/shared.vim +++ b/src/nvim/testdir/shared.vim @@ -271,7 +271,7 @@ func GetVimCommand(...) let cmd = cmd . ' -u ' . name endif let cmd .= ' --headless -i NONE' - let cmd = substitute(cmd, 'VIMRUNTIME=.*VIMRUNTIME;', '', '') + let cmd = substitute(cmd, 'VIMRUNTIME=\S\+', '', '') " If using valgrind, make sure every run uses a different log file. if cmd =~ 'valgrind.*--log-file=' @@ -329,7 +329,3 @@ func RunVimPiped(before, after, arguments, pipecmd) endif return 1 endfunc - -func CanRunGui() - return has('gui') && ($DISPLAY != "" || has('gui_running')) -endfunc diff --git a/src/nvim/testdir/test_diffmode.vim b/src/nvim/testdir/test_diffmode.vim index a1f1dd3bab..f09a64c329 100644 --- a/src/nvim/testdir/test_diffmode.vim +++ b/src/nvim/testdir/test_diffmode.vim @@ -724,10 +724,140 @@ func Test_diff_lastline() bwipe! endfunc +func Test_diff_screen() + CheckScreendump + CheckFeature menu + + " clean up already existing swap files, just in case + call delete('.Xfile1.swp') + call delete('.Xfile2.swp') + + " Test 1: Add a line in beginning of file 2 + call WriteDiffFiles(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + let buf = RunVimInTerminal('-d Xfile1 Xfile2', {}) + " Set autoread mode, so that Vim won't complain once we re-write the test + " files + call term_sendkeys(buf, ":set autoread\<CR>\<c-w>w:set autoread\<CR>\<c-w>w") + + call VerifyBoth(buf, 'Test_diff_01', '') + + " Test 2: Add a line in beginning of file 1 + call WriteDiffFiles(buf, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + call VerifyBoth(buf, 'Test_diff_02', '') + + " Test 3: Add a line at the end of file 2 + call WriteDiffFiles(buf, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) + call VerifyBoth(buf, 'Test_diff_03', '') + + " Test 4: Add a line at the end of file 1 + call WriteDiffFiles(buf, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + call VerifyBoth(buf, 'Test_diff_04', '') + + " Test 5: Add a line in the middle of file 2, remove on at the end of file 1 + call WriteDiffFiles(buf, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], [1, 2, 3, 4, 4, 5, 6, 7, 8, 9, 10]) + call VerifyBoth(buf, 'Test_diff_05', '') + + " Test 6: Add a line in the middle of file 1, remove on at the end of file 2 + call WriteDiffFiles(buf, [1, 2, 3, 4, 4, 5, 6, 7, 8, 9, 10], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) + call VerifyBoth(buf, 'Test_diff_06', '') + + " Variants on test 6 with different context settings + call term_sendkeys(buf, ":set diffopt+=context:2\<cr>") + call VerifyScreenDump(buf, 'Test_diff_06.2', {}) + call term_sendkeys(buf, ":set diffopt-=context:2\<cr>") + call term_sendkeys(buf, ":set diffopt+=context:1\<cr>") + call VerifyScreenDump(buf, 'Test_diff_06.1', {}) + call term_sendkeys(buf, ":set diffopt-=context:1\<cr>") + call term_sendkeys(buf, ":set diffopt+=context:0\<cr>") + call VerifyScreenDump(buf, 'Test_diff_06.0', {}) + call term_sendkeys(buf, ":set diffopt-=context:0\<cr>") + + " Test 7 - 9: Test normal/patience/histogram diff algorithm + call WriteDiffFiles(buf, ['#include <stdio.h>', '', '// Frobs foo heartily', 'int frobnitz(int foo)', '{', + \ ' int i;', ' for(i = 0; i < 10; i++)', ' {', ' printf("Your answer is: ");', + \ ' printf("%d\n", foo);', ' }', '}', '', 'int fact(int n)', '{', ' if(n > 1)', ' {', + \ ' return fact(n-1) * n;', ' }', ' return 1;', '}', '', 'int main(int argc, char **argv)', + \ '{', ' frobnitz(fact(10));', '}'], + \ ['#include <stdio.h>', '', 'int fib(int n)', '{', ' if(n > 2)', ' {', + \ ' return fib(n-1) + fib(n-2);', ' }', ' return 1;', '}', '', '// Frobs foo heartily', + \ 'int frobnitz(int foo)', '{', ' int i;', ' for(i = 0; i < 10; i++)', ' {', + \ ' printf("%d\n", foo);', ' }', '}', '', + \ 'int main(int argc, char **argv)', '{', ' frobnitz(fib(10));', '}']) + call term_sendkeys(buf, ":diffupdate!\<cr>") + call term_sendkeys(buf, ":set diffopt+=internal\<cr>") + call VerifyScreenDump(buf, 'Test_diff_07', {}) + + call term_sendkeys(buf, ":set diffopt+=algorithm:patience\<cr>") + call VerifyScreenDump(buf, 'Test_diff_08', {}) + + call term_sendkeys(buf, ":set diffopt+=algorithm:histogram\<cr>") + call VerifyScreenDump(buf, 'Test_diff_09', {}) + + " Test 10-11: normal/indent-heuristic + call term_sendkeys(buf, ":set diffopt&vim\<cr>") + call WriteDiffFiles(buf, ['', ' def finalize(values)', '', ' values.each do |v|', ' v.finalize', ' end'], + \ ['', ' def finalize(values)', '', ' values.each do |v|', ' v.prepare', ' end', '', + \ ' values.each do |v|', ' v.finalize', ' end']) + call term_sendkeys(buf, ":diffupdate!\<cr>") + call term_sendkeys(buf, ":set diffopt+=internal\<cr>") + call VerifyScreenDump(buf, 'Test_diff_10', {}) + + " Leave trailing : at commandline! + call term_sendkeys(buf, ":set diffopt+=indent-heuristic\<cr>:\<cr>") + call VerifyScreenDump(buf, 'Test_diff_11', {}, 'one') + " shouldn't matter, if indent-algorithm comes before or after the algorithm + call term_sendkeys(buf, ":set diffopt&\<cr>") + call term_sendkeys(buf, ":set diffopt+=indent-heuristic,algorithm:patience\<cr>:\<cr>") + call VerifyScreenDump(buf, 'Test_diff_11', {}, 'two') + call term_sendkeys(buf, ":set diffopt&\<cr>") + call term_sendkeys(buf, ":set diffopt+=algorithm:patience,indent-heuristic\<cr>:\<cr>") + call VerifyScreenDump(buf, 'Test_diff_11', {}, 'three') + + " Test 12: diff the same file + call WriteDiffFiles(buf, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + call VerifyBoth(buf, 'Test_diff_12', '') + + " Test 13: diff an empty file + call WriteDiffFiles(buf, [], []) + call VerifyBoth(buf, 'Test_diff_13', '') + + " Test 14: test diffopt+=icase + call WriteDiffFiles(buf, ['a', 'b', 'cd'], ['A', 'b', 'cDe']) + call VerifyBoth(buf, 'Test_diff_14', " diffopt+=filler diffopt+=icase") + + " Test 15-16: test diffopt+=iwhite + call WriteDiffFiles(buf, ['int main()', '{', ' printf("Hello, World!");', ' return 0;', '}'], + \ ['int main()', '{', ' if (0)', ' {', ' printf("Hello, World!");', ' return 0;', ' }', '}']) + call term_sendkeys(buf, ":diffupdate!\<cr>") + call term_sendkeys(buf, ":set diffopt&vim diffopt+=filler diffopt+=iwhite\<cr>") + call VerifyScreenDump(buf, 'Test_diff_15', {}) + call term_sendkeys(buf, ":set diffopt+=internal\<cr>") + call VerifyScreenDump(buf, 'Test_diff_16', {}) + + " Test 17: test diffopt+=iblank + call WriteDiffFiles(buf, ['a', ' ', 'cd', 'ef', 'xxx'], ['a', 'cd', '', 'ef', 'yyy']) + call VerifyInternal(buf, 'Test_diff_17', " diffopt+=iblank") + + " Test 18: test diffopt+=iblank,iwhite / iwhiteall / iwhiteeol + call VerifyInternal(buf, 'Test_diff_18', " diffopt+=iblank,iwhite") + call VerifyInternal(buf, 'Test_diff_18', " diffopt+=iblank,iwhiteall") + call VerifyInternal(buf, 'Test_diff_18', " diffopt+=iblank,iwhiteeol") + + " Test 19: test diffopt+=iwhiteeol + call WriteDiffFiles(buf, ['a ', 'x', 'cd', 'ef', 'xx xx', 'foo', 'bar'], ['a', 'x', 'c d', ' ef', 'xx xx', 'foo', '', 'bar']) + call VerifyInternal(buf, 'Test_diff_19', " diffopt+=iwhiteeol") + + " Test 19: test diffopt+=iwhiteall + call VerifyInternal(buf, 'Test_diff_20', " diffopt+=iwhiteall") + + " clean up + call StopVimInTerminal(buf) + call delete('Xfile1') + call delete('Xfile2') +endfunc + func Test_diff_with_cursorline() - if !CanRunVimInTerminal() - throw 'Skipped: cannot run Vim in a terminal window' - endif + CheckScreendump call writefile([ \ 'hi CursorLine ctermbg=red ctermfg=white', @@ -751,13 +881,45 @@ func Test_diff_with_cursorline() call delete('Xtest_diff_cursorline') endfunc +func Test_diff_with_syntax() + CheckScreendump + + let lines =<< trim END + void doNothing() { + int x = 0; + char *s = "hello"; + return 5; + } + END + call writefile(lines, 'Xprogram1.c') + let lines =<< trim END + void doSomething() { + int x = 0; + char *s = "there"; + return 5; + } + END + call writefile(lines, 'Xprogram2.c') + + let lines =<< trim END + edit Xprogram1.c + diffsplit Xprogram2.c + END + call writefile(lines, 'Xtest_diff_syntax') + let buf = RunVimInTerminal('-S Xtest_diff_syntax', {}) + + call VerifyScreenDump(buf, 'Test_diff_syntax_1', {}) + + " clean up + call StopVimInTerminal(buf) + call delete('Xtest_diff_syntax') + call delete('Xprogram1.c') + call delete('Xprogram2.c') +endfunc + func Test_diff_of_diff() - if !CanRunVimInTerminal() - throw 'Skipped: cannot run Vim in a terminal window' - endif - if !has("rightleft") - throw 'Skipped: rightleft not supported' - endif + CheckScreendump + CheckFeature rightleft call writefile([ \ 'call setline(1, ["aa","bb","cc","@@ -3,2 +5,7 @@","dd","ee","ff"])', diff --git a/src/nvim/testdir/test_display.vim b/src/nvim/testdir/test_display.vim index 1c2f5a05ff..429253a863 100644 --- a/src/nvim/testdir/test_display.vim +++ b/src/nvim/testdir/test_display.vim @@ -6,11 +6,12 @@ " endif source view_util.vim +source check.vim +source screendump.vim + +func Test_display_foldcolumn() + CheckFeature folding -func! Test_display_foldcolumn() - if !has("folding") - return - endif new vnew vert resize 25 @@ -26,10 +27,10 @@ func! Test_display_foldcolumn() call cursor(2, 1) norm! zt - let lines=ScreenLines([1,2], winwidth(0)) + let lines = ScreenLines([1,2], winwidth(0)) call assert_equal(expect, lines) set fdc=2 - let lines=ScreenLines([1,2], winwidth(0)) + let lines = ScreenLines([1,2], winwidth(0)) let expect = [ \ " e more noise blah blah<", \ " 82> more stuff here " @@ -41,9 +42,8 @@ func! Test_display_foldcolumn() endfunc func! Test_display_foldtext_mbyte() - if !has("folding") - return - endif + CheckFeature folding + call NewWindow(10, 40) call append(0, range(1,20)) exe "set foldmethod=manual foldtext=foldtext() fillchars=fold:\u2500,vert:\u2502 fdc=2" @@ -70,6 +70,42 @@ func! Test_display_foldtext_mbyte() bw! endfunc +" check that win_ins_lines() and win_del_lines() work when t_cs is empty. +func Test_scroll_without_region() + CheckScreendump + + let lines =<< trim END + call setline(1, range(1, 20)) + set t_cs= + set laststatus=2 + END + call writefile(lines, 'Xtestscroll') + let buf = RunVimInTerminal('-S Xtestscroll', #{rows: 10}) + + call VerifyScreenDump(buf, 'Test_scroll_no_region_1', {}) + + call term_sendkeys(buf, ":3delete\<cr>") + call VerifyScreenDump(buf, 'Test_scroll_no_region_2', {}) + + call term_sendkeys(buf, ":4put\<cr>") + call VerifyScreenDump(buf, 'Test_scroll_no_region_3', {}) + + call term_sendkeys(buf, ":undo\<cr>") + call term_sendkeys(buf, ":undo\<cr>") + call term_sendkeys(buf, ":set laststatus=0\<cr>") + call VerifyScreenDump(buf, 'Test_scroll_no_region_4', {}) + + call term_sendkeys(buf, ":3delete\<cr>") + call VerifyScreenDump(buf, 'Test_scroll_no_region_5', {}) + + call term_sendkeys(buf, ":4put\<cr>") + call VerifyScreenDump(buf, 'Test_scroll_no_region_6', {}) + + " clean up + call StopVimInTerminal(buf) + call delete('Xtestscroll') +endfunc + func Test_display_listchars_precedes() set fillchars+=vert:\| call NewWindow(10, 10) @@ -125,3 +161,26 @@ func Test_display_listchars_precedes() set list& listchars& wrap& bw! endfunc + +" Check that win_lines() works correctly with the number_only parameter=TRUE +" should break early to optimize cost of drawing, but needs to make sure +" that the number column is correctly highlighted. +func Test_scroll_CursorLineNr_update() + CheckScreendump + + let lines =<< trim END + hi CursorLineNr ctermfg=73 ctermbg=236 + set nu rnu cursorline cursorlineopt=number + exe ":norm! o\<esc>110ia\<esc>" + END + let filename = 'Xdrawscreen' + call writefile(lines, filename) + let buf = RunVimInTerminal('-S '.filename, #{rows: 5, cols: 50}) + call term_sendkeys(buf, "k") + call term_wait(buf) + call VerifyScreenDump(buf, 'Test_winline_rnu', {}) + + " clean up + call StopVimInTerminal(buf) + call delete(filename) +endfunc diff --git a/src/nvim/testdir/test_filetype.vim b/src/nvim/testdir/test_filetype.vim index 529f237a97..617e3dfe41 100644 --- a/src/nvim/testdir/test_filetype.vim +++ b/src/nvim/testdir/test_filetype.vim @@ -75,6 +75,7 @@ let s:filename_checks = { \ 'ave': ['file.ave'], \ 'awk': ['file.awk', 'file.gawk'], \ 'b': ['file.mch', 'file.ref', 'file.imp'], + \ 'bzl': ['file.bazel', 'file.bzl', 'WORKSPACE'], \ 'bc': ['file.bc'], \ 'bdf': ['file.bdf'], \ 'bib': ['file.bib'], @@ -526,6 +527,7 @@ let s:filename_checks = { let s:filename_case_checks = { \ 'modula2': ['file.DEF', 'file.MOD'], + \ 'bzl': ['file.BUILD', 'BUILD'], \ } func CheckItems(checks) diff --git a/src/nvim/testdir/test_textformat.vim b/src/nvim/testdir/test_textformat.vim index 75673adf0a..2223be952c 100644 --- a/src/nvim/testdir/test_textformat.vim +++ b/src/nvim/testdir/test_textformat.vim @@ -1,4 +1,7 @@ " Tests for the various 'formatoptions' settings + +source check.vim + func Test_text_format() enew! @@ -490,6 +493,23 @@ func Test_format_list_auto() set fo& ai& bs& endfunc +func Test_crash_github_issue_5095() + CheckFeature autocmd + + " This used to segfault, see https://github.com/vim/vim/issues/5095 + augroup testing + au BufNew x center + augroup END + + next! x + + bw + augroup testing + au! + augroup END + augroup! testing +endfunc + " Test for formatting multi-byte text with 'fo=t' func Test_tw_2_fo_t() new diff --git a/src/nvim/testdir/test_window_cmd.vim b/src/nvim/testdir/test_window_cmd.vim index aaa291f87d..9f47ee2904 100644 --- a/src/nvim/testdir/test_window_cmd.vim +++ b/src/nvim/testdir/test_window_cmd.vim @@ -841,4 +841,46 @@ func Test_winnr() only | tabonly endfunc +func Test_window_resize() + " Vertical :resize (absolute, relative, min and max size). + vsplit + vert resize 8 + call assert_equal(8, winwidth(0)) + vert resize +2 + call assert_equal(10, winwidth(0)) + vert resize -2 + call assert_equal(8, winwidth(0)) + vert resize + call assert_equal(&columns - 2, winwidth(0)) + vert resize 0 + call assert_equal(1, winwidth(0)) + vert resize 99999 + call assert_equal(&columns - 2, winwidth(0)) + + %bwipe! + + " Horizontal :resize (with absolute, relative size, min and max size). + split + resize 8 + call assert_equal(8, winheight(0)) + resize +2 + call assert_equal(10, winheight(0)) + resize -2 + call assert_equal(8, winheight(0)) + resize + call assert_equal(&lines - 4, winheight(0)) + resize 0 + call assert_equal(1, winheight(0)) + resize 99999 + call assert_equal(&lines - 4, winheight(0)) + + " :resize with explicit window number. + let other_winnr = winnr('j') + exe other_winnr .. 'resize 10' + call assert_equal(10, winheight(other_winnr)) + call assert_equal(&lines - 10 - 3, winheight(0)) + + %bwipe! +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 3b71066094..dde17726fd 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -108,6 +108,7 @@ typedef struct { bool cork, overflow; bool cursor_color_changed; bool is_starting; + FILE *screenshot; cursorentry_T cursor_shapes[SHAPE_IDX_COUNT]; HlAttrs clear_attrs; kvec_t(HlAttrs) attrs; @@ -167,6 +168,7 @@ UI *tui_start(void) ui->suspend = tui_suspend; ui->set_title = tui_set_title; ui->set_icon = tui_set_icon; + ui->screenshot = tui_screenshot; ui->option_set= tui_option_set; ui->raw_line = tui_raw_line; @@ -412,6 +414,7 @@ static void tui_main(UIBridgeData *bridge, UI *ui) data->bridge = bridge; data->loop = &tui_loop; data->is_starting = true; + data->screenshot = NULL; kv_init(data->invalid_regions); signal_watcher_init(data->loop, &data->winch_handle, ui); signal_watcher_init(data->loop, &data->cont_handle, data); @@ -1317,6 +1320,31 @@ static void tui_set_icon(UI *ui, String icon) { } +static void tui_screenshot(UI *ui, String path) +{ + TUIData *data = ui->data; + UGrid *grid = &data->grid; + flush_buf(ui); + grid->row = 0; + grid->col = 0; + + FILE *f = fopen(path.data, "w"); + data->screenshot = f; + fprintf(f, "%d,%d\n", grid->height, grid->width); + unibi_out(ui, unibi_clear_screen); + for (int i = 0; i < grid->height; i++) { + cursor_goto(ui, i, 0); + for (int j = 0; j < grid->width; j++) { + print_cell(ui, &grid->cells[i][j]); + } + } + flush_buf(ui); + data->screenshot = NULL; + + fclose(f); +} + + static void tui_option_set(UI *ui, String name, Object value) { TUIData *data = ui->data; @@ -2054,9 +2082,15 @@ static void flush_buf(UI *ui) } } - uv_write(&req, STRUCT_CAST(uv_stream_t, &data->output_handle), - bufs, (unsigned)(bufp - bufs), NULL); - uv_run(&data->write_loop, UV_RUN_DEFAULT); + if (data->screenshot) { + for (size_t i = 0; i < (size_t)(bufp - bufs); i++) { + fwrite(bufs[i].base, bufs[i].len, 1, data->screenshot); + } + } else { + uv_write(&req, STRUCT_CAST(uv_stream_t, &data->output_handle), + bufs, (unsigned)(bufp - bufs), NULL); + uv_run(&data->write_loop, UV_RUN_DEFAULT); + } data->bufpos = 0; data->overflow = false; } diff --git a/src/nvim/ui_bridge.c b/src/nvim/ui_bridge.c index 9a1988739c..25f45b8fe6 100644 --- a/src/nvim/ui_bridge.c +++ b/src/nvim/ui_bridge.c @@ -61,6 +61,7 @@ UI *ui_bridge_attach(UI *ui, ui_main_fn ui_main, event_scheduler scheduler) rv->bridge.suspend = ui_bridge_suspend; rv->bridge.set_title = ui_bridge_set_title; rv->bridge.set_icon = ui_bridge_set_icon; + rv->bridge.screenshot = ui_bridge_screenshot; rv->bridge.option_set = ui_bridge_option_set; rv->bridge.raw_line = ui_bridge_raw_line; rv->bridge.inspect = ui_bridge_inspect; diff --git a/src/nvim/viml/parser/expressions.c b/src/nvim/viml/parser/expressions.c index b77b80a5f3..44b6ab5f5a 100644 --- a/src/nvim/viml/parser/expressions.c +++ b/src/nvim/viml/parser/expressions.c @@ -1431,7 +1431,7 @@ static inline void east_set_error(const ParserState *const pstate, const ParserLine pline = pstate->reader.lines.items[start.line]; ret_ast_err->msg = msg; ret_ast_err->arg_len = (int)(pline.size - start.col); - ret_ast_err->arg = pline.data + start.col; + ret_ast_err->arg = pline.data ? pline.data + start.col : NULL; } /// Set error from the given token and given message diff --git a/src/nvim/window.c b/src/nvim/window.c index 0fff93d984..cec0dfd67f 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -6986,7 +6986,7 @@ void win_findbuf(typval_T *argvars, list_T *list) int bufnr = tv_get_number(&argvars[0]); FOR_ALL_TAB_WINDOWS(tp, wp) { - if (wp->w_buffer->b_fnum == bufnr) { + if (!wp->w_closing && wp->w_buffer->b_fnum == bufnr) { tv_list_append_number(list, wp->handle); } } diff --git a/src/tree_sitter/alloc.h b/src/tree_sitter/alloc.h index d3c6b5eca8..32c90f23c8 100644 --- a/src/tree_sitter/alloc.h +++ b/src/tree_sitter/alloc.h @@ -58,7 +58,7 @@ static inline bool ts_toggle_allocation_recording(bool value) { static inline void *ts_malloc(size_t size) { void *result = malloc(size); if (size > 0 && !result) { - fprintf(stderr, "tree-sitter failed to allocate %lu bytes", size); + fprintf(stderr, "tree-sitter failed to allocate %zu bytes", size); exit(1); } return result; @@ -67,7 +67,7 @@ static inline void *ts_malloc(size_t size) { static inline void *ts_calloc(size_t count, size_t size) { void *result = calloc(count, size); if (count > 0 && !result) { - fprintf(stderr, "tree-sitter failed to allocate %lu bytes", count * size); + fprintf(stderr, "tree-sitter failed to allocate %zu bytes", count * size); exit(1); } return result; @@ -76,7 +76,7 @@ static inline void *ts_calloc(size_t count, size_t size) { static inline void *ts_realloc(void *buffer, size_t size) { void *result = realloc(buffer, size); if (size > 0 && !result) { - fprintf(stderr, "tree-sitter failed to reallocate %lu bytes", size); + fprintf(stderr, "tree-sitter failed to reallocate %zu bytes", size); exit(1); } return result; diff --git a/src/tree_sitter/lexer.c b/src/tree_sitter/lexer.c index 3f8a4c0ae8..a3c29544d3 100644 --- a/src/tree_sitter/lexer.c +++ b/src/tree_sitter/lexer.c @@ -73,7 +73,6 @@ static void ts_lexer__get_chunk(Lexer *self) { // code that spans the current position. static void ts_lexer__get_lookahead(Lexer *self) { uint32_t position_in_chunk = self->current_position.bytes - self->chunk_start; - const uint8_t *chunk = (const uint8_t *)self->chunk + position_in_chunk; uint32_t size = self->chunk_size - position_in_chunk; if (size == 0) { @@ -82,6 +81,7 @@ static void ts_lexer__get_lookahead(Lexer *self) { return; } + const uint8_t *chunk = (const uint8_t *)self->chunk + position_in_chunk; UnicodeDecodeFunction decode = self->input.encoding == TSInputEncodingUTF8 ? ts_decode_utf8 : ts_decode_utf16; diff --git a/src/tree_sitter/parser.c b/src/tree_sitter/parser.c index dd222cd3c4..79cad797a0 100644 --- a/src/tree_sitter/parser.c +++ b/src/tree_sitter/parser.c @@ -292,6 +292,7 @@ static bool ts_parser__better_version_exists( return true; case ErrorComparisonPreferRight: if (ts_stack_can_merge(self->stack, i, version)) return true; + break; default: break; } @@ -355,10 +356,14 @@ static Subtree ts_parser__lex( StackVersion version, TSStateId parse_state ) { + TSLexMode lex_mode = self->language->lex_modes[parse_state]; + if (lex_mode.lex_state == (uint16_t)-1) { + LOG("no_lookahead_after_non_terminal_extra"); + return NULL_SUBTREE; + } + Length start_position = ts_stack_position(self->stack, version); Subtree external_token = ts_stack_last_external_token(self->stack, version); - TSLexMode lex_mode = self->language->lex_modes[parse_state]; - if (lex_mode.lex_state == (uint16_t)-1) return NULL_SUBTREE; const bool *valid_external_tokens = ts_language_enabled_external_tokens( self->language, lex_mode.external_lex_state @@ -761,20 +766,26 @@ static StackVersion ts_parser__reduce( int dynamic_precedence, uint16_t production_id, bool is_fragile, - bool is_extra + bool end_of_non_terminal_extra ) { uint32_t initial_version_count = ts_stack_version_count(self->stack); - uint32_t removed_version_count = 0; - StackSliceArray pop = ts_stack_pop_count(self->stack, version, count); + // Pop the given number of nodes from the given version of the parse stack. + // If stack versions have previously merged, then there may be more than one + // path back through the stack. For each path, create a new parent node to + // contain the popped children, and push it onto the stack in place of the + // children. + StackSliceArray pop = ts_stack_pop_count(self->stack, version, count); + uint32_t removed_version_count = 0; for (uint32_t i = 0; i < pop.size; i++) { StackSlice slice = pop.contents[i]; StackVersion slice_version = slice.version - removed_version_count; - // Error recovery can sometimes cause lots of stack versions to merge, - // such that a single pop operation can produce a lots of slices. - // Avoid creating too many stack versions in that situation. - if (i > 0 && slice_version > MAX_VERSION_COUNT + MAX_VERSION_COUNT_OVERFLOW) { + // This is where new versions are added to the parse stack. The versions + // will all be sorted and truncated at the end of the outer parsing loop. + // Allow the maximum version count to be temporarily exceeded, but only + // by a limited threshold. + if (slice_version > MAX_VERSION_COUNT + MAX_VERSION_COUNT_OVERFLOW) { ts_stack_remove_version(self->stack, slice_version); ts_subtree_array_delete(&self->tree_pool, &slice.subtrees); removed_version_count++; @@ -826,7 +837,9 @@ static StackVersion ts_parser__reduce( TSStateId state = ts_stack_state(self->stack, slice_version); TSStateId next_state = ts_language_next_state(self->language, state, symbol); - if (is_extra) parent.ptr->extra = true; + if (end_of_non_terminal_extra && next_state == state) { + parent.ptr->extra = true; + } if (is_fragile || pop.size > 1 || initial_version_count > 1) { parent.ptr->fragile_left = true; parent.ptr->fragile_right = true; @@ -963,6 +976,7 @@ static bool ts_parser__do_all_potential_reductions( .dynamic_precedence = action.params.reduce.dynamic_precedence, .production_id = action.params.reduce.production_id, }); + break; default: break; } @@ -1339,23 +1353,26 @@ static bool ts_parser__advance( ); } - // Otherwise, re-run the lexer. - if (!lookahead.ptr) { - lookahead = ts_parser__lex(self, version, state); - if (lookahead.ptr) { - ts_parser__set_cached_token(self, position, last_external_token, lookahead); - ts_language_table_entry(self->language, state, ts_subtree_symbol(lookahead), &table_entry); - } + bool needs_lex = !lookahead.ptr; + for (;;) { + // Otherwise, re-run the lexer. + if (needs_lex) { + needs_lex = false; + lookahead = ts_parser__lex(self, version, state); + + if (lookahead.ptr) { + ts_parser__set_cached_token(self, position, last_external_token, lookahead); + ts_language_table_entry(self->language, state, ts_subtree_symbol(lookahead), &table_entry); + } - // When parsing a non-terminal extra, a null lookahead indicates the - // end of the rule. The reduction is stored in the EOF table entry. - // After the reduction, the lexer needs to be run again. - else { - ts_language_table_entry(self->language, state, ts_builtin_sym_end, &table_entry); + // When parsing a non-terminal extra, a null lookahead indicates the + // end of the rule. The reduction is stored in the EOF table entry. + // After the reduction, the lexer needs to be run again. + else { + ts_language_table_entry(self->language, state, ts_builtin_sym_end, &table_entry); + } } - } - for (;;) { // If a cancellation flag or a timeout was provided, then check every // time a fixed number of parse actions has been processed. if (++self->operation_count == OP_COUNT_PER_TIMEOUT_CHECK) { @@ -1407,12 +1424,12 @@ static bool ts_parser__advance( case TSParseActionTypeReduce: { bool is_fragile = table_entry.action_count > 1; - bool is_extra = lookahead.ptr == NULL; + bool end_of_non_terminal_extra = lookahead.ptr == NULL; LOG("reduce sym:%s, child_count:%u", SYM_NAME(action.params.reduce.symbol), action.params.reduce.child_count); StackVersion reduction_version = ts_parser__reduce( self, version, action.params.reduce.symbol, action.params.reduce.child_count, action.params.reduce.dynamic_precedence, action.params.reduce.production_id, - is_fragile, is_extra + is_fragile, end_of_non_terminal_extra ); if (reduction_version != STACK_VERSION_NONE) { last_reduction_version = reduction_version; @@ -1452,8 +1469,10 @@ static bool ts_parser__advance( // (and completing the non-terminal extra rule) run the lexer again based // on the current parse state. if (!lookahead.ptr) { - lookahead = ts_parser__lex(self, version, state); + needs_lex = true; + continue; } + ts_language_table_entry( self->language, state, @@ -1463,6 +1482,11 @@ static bool ts_parser__advance( continue; } + if (!lookahead.ptr) { + ts_stack_pause(self->stack, version, ts_builtin_sym_end); + return true; + } + // If there were no parse actions for the current lookahead token, then // it is not valid in this state. If the current lookahead token is a // keyword, then switch to treating it as the normal word token if that @@ -1500,6 +1524,9 @@ static bool ts_parser__advance( // push each of its children. Then try again to process the current // lookahead. if (ts_parser__breakdown_top_of_stack(self, version)) { + state = ts_stack_state(self->stack, version); + ts_subtree_release(&self->tree_pool, lookahead); + needs_lex = true; continue; } diff --git a/src/tree_sitter/query.c b/src/tree_sitter/query.c index 59902dee3b..b887b74ff6 100644 --- a/src/tree_sitter/query.c +++ b/src/tree_sitter/query.c @@ -11,7 +11,6 @@ // #define LOG(...) fprintf(stderr, __VA_ARGS__) #define LOG(...) -#define MAX_STATE_COUNT 256 #define MAX_CAPTURE_LIST_COUNT 32 #define MAX_STEP_CAPTURE_COUNT 3 @@ -49,7 +48,6 @@ typedef struct { uint16_t alternative_index; uint16_t depth; bool contains_captures: 1; - bool is_pattern_start: 1; bool is_immediate: 1; bool is_last_child: 1; bool is_pass_through: 1; @@ -119,9 +117,10 @@ typedef struct { uint16_t step_index; uint16_t pattern_index; uint16_t capture_list_id; - uint16_t consumed_capture_count: 14; + uint16_t consumed_capture_count: 12; bool seeking_immediate_match: 1; bool has_in_progress_alternatives: 1; + bool dead: 1; } QueryState; typedef Array(TSQueryCapture) CaptureList; @@ -172,6 +171,7 @@ struct TSQueryCursor { TSPoint start_point; TSPoint end_point; bool ascending; + bool halted; }; static const TSQueryError PARENT_DONE = -1; @@ -448,7 +448,6 @@ static QueryStep query_step__new( .alternative_index = NONE, .contains_captures = false, .is_last_child = false, - .is_pattern_start = false, .is_pass_through = false, .is_dead_end = false, .is_immediate = is_immediate, @@ -546,6 +545,23 @@ static inline void ts_query__pattern_map_insert( ) { uint32_t index; ts_query__pattern_map_search(self, symbol, &index); + + // Ensure that the entries are sorted not only by symbol, but also + // by pattern_index. This way, states for earlier patterns will be + // initiated first, which allows the ordering of the states array + // to be maintained more efficiently. + while (index < self->pattern_map.size) { + PatternEntry *entry = &self->pattern_map.contents[index]; + if ( + self->steps.contents[entry->step_index].symbol == symbol && + entry->pattern_index < pattern_index + ) { + index++; + } else { + break; + } + } + array_insert(&self->pattern_map, index, ((PatternEntry) { .step_index = start_step_index, .pattern_index = pattern_index, @@ -715,7 +731,7 @@ static TSQueryError ts_query__parse_pattern( uint32_t *capture_count, bool is_immediate ) { - uint32_t starting_step_index = self->steps.size; + const uint32_t starting_step_index = self->steps.size; if (stream->next == 0) return TSQueryErrorSyntax; @@ -804,8 +820,8 @@ static TSQueryError ts_query__parse_pattern( } } - // A pound character indicates the start of a predicate. - else if (stream->next == '#') { + // A dot/pound character indicates the start of a predicate. + else if (stream->next == '.' || stream->next == '#') { stream_advance(stream); return ts_query__parse_predicate(self, stream); } @@ -951,7 +967,6 @@ static TSQueryError ts_query__parse_pattern( stream_skip_whitespace(stream); // Parse the pattern - uint32_t step_index = self->steps.size; TSQueryError e = ts_query__parse_pattern( self, stream, @@ -972,7 +987,22 @@ static TSQueryError ts_query__parse_pattern( stream->input = field_name; return TSQueryErrorField; } - self->steps.contents[step_index].field = field_id; + + uint32_t step_index = starting_step_index; + QueryStep *step = &self->steps.contents[step_index]; + for (;;) { + step->field = field_id; + if ( + step->alternative_index != NONE && + step->alternative_index > step_index && + step->alternative_index < self->steps.size + ) { + step_index = step->alternative_index; + step = &self->steps.contents[step_index]; + } else { + break; + } + } } else { @@ -1041,15 +1071,16 @@ static TSQueryError ts_query__parse_pattern( length ); + uint32_t step_index = starting_step_index; for (;;) { query_step__add_capture(step, capture_id); if ( step->alternative_index != NONE && - step->alternative_index > starting_step_index && + step->alternative_index > step_index && step->alternative_index < self->steps.size ) { - starting_step_index = step->alternative_index; - step = &self->steps.contents[starting_step_index]; + step_index = step->alternative_index; + step = &self->steps.contents[step_index]; } else { break; } @@ -1152,7 +1183,6 @@ TSQuery *ts_query_new( // Maintain a map that can look up patterns for a given root symbol. for (;;) { QueryStep *step = &self->steps.contents[start_step_index]; - step->is_pattern_start = true; ts_query__pattern_map_insert(self, step->symbol, start_step_index, pattern_index); if (step->symbol == WILDCARD_SYMBOL) { self->wildcard_root_pattern_count++; @@ -1162,6 +1192,7 @@ TSQuery *ts_query_new( // then add multiple entries to the pattern map. if (step->alternative_index != NONE) { start_step_index = step->alternative_index; + step->alternative_index = NONE; } else { break; } @@ -1221,6 +1252,9 @@ const TSQueryPredicateStep *ts_query_predicates_for_pattern( ) { Slice slice = self->predicates_by_pattern.contents[pattern_index]; *step_count = slice.length; + if (self->predicate_steps.contents == NULL) { + return NULL; + } return &self->predicate_steps.contents[slice.offset]; } @@ -1271,6 +1305,7 @@ TSQueryCursor *ts_query_cursor_new(void) { TSQueryCursor *self = ts_malloc(sizeof(TSQueryCursor)); *self = (TSQueryCursor) { .ascending = false, + .halted = false, .states = array_new(), .finished_states = array_new(), .capture_list_pool = capture_list_pool_new(), @@ -1279,8 +1314,8 @@ TSQueryCursor *ts_query_cursor_new(void) { .start_point = {0, 0}, .end_point = POINT_MAX, }; - array_reserve(&self->states, MAX_STATE_COUNT); - array_reserve(&self->finished_states, MAX_CAPTURE_LIST_COUNT); + array_reserve(&self->states, 8); + array_reserve(&self->finished_states, 8); return self; } @@ -1304,6 +1339,7 @@ void ts_query_cursor_exec( self->next_state_id = 0; self->depth = 0; self->ascending = false; + self->halted = false; self->query = query; } @@ -1347,6 +1383,7 @@ static bool ts_query_cursor__first_in_progress_capture( *pattern_index = UINT32_MAX; for (unsigned i = 0; i < self->states.size; i++) { const QueryState *state = &self->states.contents[i]; + if (state->dead) continue; const CaptureList *captures = capture_list_pool_get( &self->capture_list_pool, state->capture_list_id @@ -1441,65 +1478,138 @@ void ts_query_cursor__compare_captures( } } -static bool ts_query_cursor__add_state( +static void ts_query_cursor__add_state( TSQueryCursor *self, const PatternEntry *pattern ) { - if (self->states.size >= MAX_STATE_COUNT) { - LOG(" too many states"); - return false; + QueryStep *step = &self->query->steps.contents[pattern->step_index]; + uint32_t start_depth = self->depth - step->depth; + + // Keep the states array in ascending order of start_depth and pattern_index, + // so that it can be processed more efficiently elsewhere. Usually, there is + // no work to do here because of two facts: + // * States with lower start_depth are naturally added first due to the + // order in which nodes are visited. + // * Earlier patterns are naturally added first because of the ordering of the + // pattern_map data structure that's used to initiate matches. + // + // This loop is only needed in cases where two conditions hold: + // * A pattern consists of more than one sibling node, so that its states + // remain in progress after exiting the node that started the match. + // * The first node in the pattern matches against multiple nodes at the + // same depth. + // + // An example of this is the pattern '((comment)* (function))'. If multiple + // `comment` nodes appear in a row, then we may initiate a new state for this + // pattern while another state for the same pattern is already in progress. + // If there are multiple patterns like this in a query, then this loop will + // need to execute in order to keep the states ordered by pattern_index. + uint32_t index = self->states.size; + while (index > 0) { + QueryState *prev_state = &self->states.contents[index - 1]; + if (prev_state->start_depth < start_depth) break; + if (prev_state->start_depth == start_depth) { + if (prev_state->pattern_index < pattern->pattern_index) break; + if (prev_state->pattern_index == pattern->pattern_index) { + // Avoid unnecessarily inserting an unnecessary duplicate state, + // which would be immediately pruned by the longest-match criteria. + if (prev_state->step_index == pattern->step_index) return; + } + } + index--; } + LOG( " start state. pattern:%u, step:%u\n", pattern->pattern_index, pattern->step_index ); - QueryStep *step = &self->query->steps.contents[pattern->step_index]; - array_push(&self->states, ((QueryState) { + array_insert(&self->states, index, ((QueryState) { .capture_list_id = NONE, .step_index = pattern->step_index, .pattern_index = pattern->pattern_index, - .start_depth = self->depth - step->depth, + .start_depth = start_depth, .consumed_capture_count = 0, - .seeking_immediate_match = false, + .seeking_immediate_match = true, + .has_in_progress_alternatives = false, + .dead = false, })); - return true; } -// Duplicate the given state and insert the newly-created state immediately after -// the given state in the `states` array. -static QueryState *ts_query__cursor_copy_state( +// Acquire a capture list for this state. If there are no capture lists left in the +// pool, this will steal the capture list from another existing state, and mark that +// other state as 'dead'. +static CaptureList *ts_query_cursor__prepare_to_capture( TSQueryCursor *self, - const QueryState *state + QueryState *state, + unsigned state_index_to_preserve ) { - if (self->states.size >= MAX_STATE_COUNT) { - LOG(" too many states"); - return NULL; + if (state->capture_list_id == NONE) { + state->capture_list_id = capture_list_pool_acquire(&self->capture_list_pool); + + // If there are no capture lists left in the pool, then terminate whichever + // state has captured the earliest node in the document, and steal its + // capture list. + if (state->capture_list_id == NONE) { + uint32_t state_index, byte_offset, pattern_index; + if ( + ts_query_cursor__first_in_progress_capture( + self, + &state_index, + &byte_offset, + &pattern_index + ) && + state_index != state_index_to_preserve + ) { + LOG( + " abandon state. index:%u, pattern:%u, offset:%u.\n", + state_index, pattern_index, byte_offset + ); + QueryState *other_state = &self->states.contents[state_index]; + state->capture_list_id = other_state->capture_list_id; + other_state->capture_list_id = NONE; + other_state->dead = true; + CaptureList *list = capture_list_pool_get_mut( + &self->capture_list_pool, + state->capture_list_id + ); + array_clear(list); + return list; + } else { + LOG(" ran out of capture lists"); + return NULL; + } + } } + return capture_list_pool_get_mut(&self->capture_list_pool, state->capture_list_id); +} - // If the state has captures, copy its capture list. +// Duplicate the given state and insert the newly-created state immediately after +// the given state in the `states` array. Ensures that the given state reference is +// still valid, even if the states array is reallocated. +static QueryState *ts_query_cursor__copy_state( + TSQueryCursor *self, + QueryState **state_ref +) { + const QueryState *state = *state_ref; + uint32_t state_index = state - self->states.contents; QueryState copy = *state; - copy.capture_list_id = state->capture_list_id; + copy.capture_list_id = NONE; + + // If the state has captures, copy its capture list. if (state->capture_list_id != NONE) { - copy.capture_list_id = capture_list_pool_acquire(&self->capture_list_pool); - if (copy.capture_list_id == NONE) { - LOG(" too many capture lists"); - return NULL; - } + CaptureList *new_captures = ts_query_cursor__prepare_to_capture(self, ©, state_index); + if (!new_captures) return NULL; const CaptureList *old_captures = capture_list_pool_get( &self->capture_list_pool, state->capture_list_id ); - CaptureList *new_captures = capture_list_pool_get_mut( - &self->capture_list_pool, - copy.capture_list_id - ); array_push_all(new_captures, old_captures); } - uint32_t index = (state - self->states.contents) + 1; - array_insert(&self->states, index, copy); - return &self->states.contents[index]; + array_insert(&self->states, state_index + 1, copy); + *state_ref = &self->states.contents[state_index]; + return &self->states.contents[state_index + 1]; } // Walk the tree, processing patterns until at least one pattern finishes, @@ -1507,18 +1617,30 @@ static QueryState *ts_query__cursor_copy_state( // `finished_states` array. Multiple patterns can finish on the same node. If // there are no more matches, return `false`. static inline bool ts_query_cursor__advance(TSQueryCursor *self) { - do { + bool did_match = false; + for (;;) { + if (self->halted) { + while (self->states.size > 0) { + QueryState state = array_pop(&self->states); + capture_list_pool_release( + &self->capture_list_pool, + state.capture_list_id + ); + } + } + + if (did_match || self->halted) return did_match; + if (self->ascending) { LOG("leave node. type:%s\n", ts_node_type(ts_tree_cursor_current_node(&self->cursor))); // Leave this node by stepping to its next sibling or to its parent. - bool did_move = true; if (ts_tree_cursor_goto_next_sibling(&self->cursor)) { self->ascending = false; } else if (ts_tree_cursor_goto_parent(&self->cursor)) { self->depth--; } else { - did_move = false; + self->halted = true; } // After leaving a node, remove any states that cannot make further progress. @@ -1530,10 +1652,11 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { // If a state completed its pattern inside of this node, but was deferred from finishing // in order to search for longer matches, mark it as finished. if (step->depth == PATTERN_DONE_MARKER) { - if (state->start_depth > self->depth || !did_move) { + if (state->start_depth > self->depth || self->halted) { LOG(" finish pattern %u\n", state->pattern_index); state->id = self->next_state_id++; array_push(&self->finished_states, *state); + did_match = true; deleted_count++; continue; } @@ -1560,10 +1683,6 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { } } self->states.size -= deleted_count; - - if (!did_move) { - return self->finished_states.size > 0; - } } else { // If this node is before the selected range, then avoid descending into it. TSNode node = ts_tree_cursor_current_node(&self->cursor); @@ -1581,7 +1700,10 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { if ( self->end_byte <= ts_node_start_byte(node) || point_lte(self->end_point, ts_node_start_point(node)) - ) return false; + ) { + self->halted = true; + continue; + } // Get the properties of the current node. TSSymbol symbol = ts_node_symbol(node); @@ -1613,7 +1735,7 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { // If this node matches the first step of the pattern, then add a new // state at the start of this pattern. if (step->field && field_id != step->field) continue; - if (!ts_query_cursor__add_state(self, pattern)) break; + ts_query_cursor__add_state(self, pattern); } // Add new states for any patterns whose root node matches this node. @@ -1625,7 +1747,7 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { // If this node matches the first step of the pattern, then add a new // state at the start of this pattern. if (step->field && field_id != step->field) continue; - if (!ts_query_cursor__add_state(self, pattern)) break; + ts_query_cursor__add_state(self, pattern); // Advance to the next pattern whose root node matches this node. i++; @@ -1693,12 +1815,8 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { // parent, then this query state cannot simply be updated in place. It must be // split into two states: one that matches this node, and one which skips over // this node, to preserve the possibility of matching later siblings. - if ( - later_sibling_can_match && - !step->is_pattern_start && - step->contains_captures - ) { - if (ts_query__cursor_copy_state(self, state)) { + if (later_sibling_can_match && step->contains_captures) { + if (ts_query_cursor__copy_state(self, &state)) { LOG( " split state for capture. pattern:%u, step:%u\n", state->pattern_index, @@ -1709,45 +1827,14 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { } // If the current node is captured in this pattern, add it to the capture list. - // For the first capture in a pattern, lazily acquire a capture list. if (step->capture_ids[0] != NONE) { - if (state->capture_list_id == NONE) { - state->capture_list_id = capture_list_pool_acquire(&self->capture_list_pool); - - // If there are no capture lists left in the pool, then terminate whichever - // state has captured the earliest node in the document, and steal its - // capture list. - if (state->capture_list_id == NONE) { - uint32_t state_index, byte_offset, pattern_index; - if (ts_query_cursor__first_in_progress_capture( - self, - &state_index, - &byte_offset, - &pattern_index - )) { - LOG( - " abandon state. index:%u, pattern:%u, offset:%u.\n", - state_index, pattern_index, byte_offset - ); - state->capture_list_id = self->states.contents[state_index].capture_list_id; - array_erase(&self->states, state_index); - if (state_index < i) { - i--; - state--; - } - } else { - LOG(" too many finished states.\n"); - array_erase(&self->states, i); - i--; - continue; - } - } + CaptureList *capture_list = ts_query_cursor__prepare_to_capture(self, state, UINT32_MAX); + if (!capture_list) { + array_erase(&self->states, i); + i--; + continue; } - CaptureList *capture_list = capture_list_pool_get_mut( - &self->capture_list_pool, - state->capture_list_id - ); for (unsigned j = 0; j < MAX_STEP_CAPTURE_COUNT; j++) { uint16_t capture_id = step->capture_ids[j]; if (step->capture_ids[j] == NONE) break; @@ -1770,10 +1857,9 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { state->step_index ); - // If this state's next step has an 'alternative' step (the step is either optional, - // or is the end of a repetition), then copy the state in order to pursue both - // alternatives. The alternative step itself may have an alternative, so this is - // an interative process. + // If this state's next step has an alternative step, then copy the state in order + // to pursue both alternatives. The alternative step itself may have an alternative, + // so this is an interative process. unsigned end_index = i + 1; for (unsigned j = i; j < end_index; j++) { QueryState *state = &self->states.contents[j]; @@ -1785,25 +1871,27 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { continue; } - QueryState *copy = ts_query__cursor_copy_state(self, state); if (next_step->is_pass_through) { state->step_index++; j--; } + + QueryState *copy = ts_query_cursor__copy_state(self, &state); if (copy) { - copy_count++; + LOG( + " split state for branch. pattern:%u, from_step:%u, to_step:%u, immediate:%d, capture_count: %u\n", + copy->pattern_index, + copy->step_index, + next_step->alternative_index, + next_step->alternative_is_immediate, + capture_list_pool_get(&self->capture_list_pool, copy->capture_list_id)->size + ); end_index++; + copy_count++; copy->step_index = next_step->alternative_index; if (next_step->alternative_is_immediate) { copy->seeking_immediate_match = true; } - LOG( - " split state for branch. pattern:%u, step:%u, step:%u, immediate:%d\n", - copy->pattern_index, - state->step_index, - copy->step_index, - copy->seeking_immediate_match - ); } } } @@ -1811,59 +1899,77 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { for (unsigned i = 0; i < self->states.size; i++) { QueryState *state = &self->states.contents[i]; - bool did_remove = false; + if (state->dead) { + array_erase(&self->states, i); + i--; + continue; + } // Enfore the longest-match criteria. When a query pattern contains optional or - // repeated nodes, this is necesssary to avoid multiple redundant states, where + // repeated nodes, this is necessary to avoid multiple redundant states, where // one state has a strict subset of another state's captures. + bool did_remove = false; for (unsigned j = i + 1; j < self->states.size; j++) { QueryState *other_state = &self->states.contents[j]; + + // Query states are kept in ascending order of start_depth and pattern_index. + // Since the longest-match criteria is only used for deduping matches of the same + // pattern and root node, we only need to perform pairwise comparisons within a + // small slice of the states array. if ( - state->pattern_index == other_state->pattern_index && - state->start_depth == other_state->start_depth - ) { - bool left_contains_right, right_contains_left; - ts_query_cursor__compare_captures( - self, - state, - other_state, - &left_contains_right, - &right_contains_left - ); - if (left_contains_right) { - if (state->step_index == other_state->step_index) { - LOG( - " drop shorter state. pattern: %u, step_index: %u\n", - state->pattern_index, - state->step_index - ); - capture_list_pool_release(&self->capture_list_pool, other_state->capture_list_id); - array_erase(&self->states, j); - j--; - continue; - } - other_state->has_in_progress_alternatives = true; + other_state->start_depth != state->start_depth || + other_state->pattern_index != state->pattern_index + ) break; + + bool left_contains_right, right_contains_left; + ts_query_cursor__compare_captures( + self, + state, + other_state, + &left_contains_right, + &right_contains_left + ); + if (left_contains_right) { + if (state->step_index == other_state->step_index) { + LOG( + " drop shorter state. pattern: %u, step_index: %u\n", + state->pattern_index, + state->step_index + ); + capture_list_pool_release(&self->capture_list_pool, other_state->capture_list_id); + array_erase(&self->states, j); + j--; + continue; } - if (right_contains_left) { - if (state->step_index == other_state->step_index) { - LOG( - " drop shorter state. pattern: %u, step_index: %u\n", - state->pattern_index, - state->step_index - ); - capture_list_pool_release(&self->capture_list_pool, state->capture_list_id); - array_erase(&self->states, i); - did_remove = true; - break; - } - state->has_in_progress_alternatives = true; + other_state->has_in_progress_alternatives = true; + } + if (right_contains_left) { + if (state->step_index == other_state->step_index) { + LOG( + " drop shorter state. pattern: %u, step_index: %u\n", + state->pattern_index, + state->step_index + ); + capture_list_pool_release(&self->capture_list_pool, state->capture_list_id); + array_erase(&self->states, i); + i--; + did_remove = true; + break; } + state->has_in_progress_alternatives = true; } } // If there the state is at the end of its pattern, remove it from the list // of in-progress states and add it to the list of finished states. if (!did_remove) { + LOG( + " keep state. pattern: %u, start_depth: %u, step_index: %u, capture_count: %u\n", + state->pattern_index, + state->start_depth, + state->step_index, + capture_list_pool_get(&self->capture_list_pool, state->capture_list_id)->size + ); QueryStep *next_step = &self->query->steps.contents[state->step_index]; if (next_step->depth == PATTERN_DONE_MARKER) { if (state->has_in_progress_alternatives) { @@ -1873,6 +1979,7 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { state->id = self->next_state_id++; array_push(&self->finished_states, *state); array_erase(&self->states, state - self->states.contents); + did_match = true; i--; } } @@ -1886,9 +1993,7 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { self->ascending = true; } } - } while (self->finished_states.size == 0); - - return true; + } } bool ts_query_cursor_next_match( @@ -2028,7 +2133,10 @@ bool ts_query_cursor_next_capture( // If there are no finished matches that are ready to be returned, then // continue finding more matches. - if (!ts_query_cursor__advance(self)) return false; + if ( + !ts_query_cursor__advance(self) && + self->finished_states.size == 0 + ) return false; } } diff --git a/src/tree_sitter/stack.c b/src/tree_sitter/stack.c index 6ceee2577f..6a8d897c37 100644 --- a/src/tree_sitter/stack.c +++ b/src/tree_sitter/stack.c @@ -571,7 +571,12 @@ void ts_stack_record_summary(Stack *self, StackVersion version, unsigned max_dep }; array_init(session.summary); stack__iter(self, version, summarize_stack_callback, &session, -1); - self->heads.contents[version].summary = session.summary; + StackHead *head = &self->heads.contents[version]; + if (head->summary) { + array_delete(head->summary); + ts_free(head->summary); + } + head->summary = session.summary; } StackSummary *ts_stack_get_summary(Stack *self, StackVersion version) { @@ -743,6 +748,10 @@ bool ts_stack_print_dot_graph(Stack *self, const TSLanguage *language, FILE *f) ts_stack_error_cost(self, i) ); + if (head->summary) { + fprintf(f, "\nsummary_size: %u", head->summary->size); + } + if (head->last_external_token.ptr) { const ExternalScannerState *state = &head->last_external_token.ptr->external_scanner_state; const char *data = ts_external_scanner_state_data(state); diff --git a/src/tree_sitter/treesitter_commit_hash.txt b/src/tree_sitter/treesitter_commit_hash.txt index bd7fcfbe76..322cdd24a6 100644 --- a/src/tree_sitter/treesitter_commit_hash.txt +++ b/src/tree_sitter/treesitter_commit_hash.txt @@ -1 +1 @@ -81d533d2d1b580fdb507accabc91ceddffb5b6f0 +87df53a99b51bce0d1e901cd6838f24e1c7a4073 |