diff options
Diffstat (limited to 'src')
73 files changed, 2276 insertions, 990 deletions
diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c index 15065760b3..2811a2da12 100644 --- a/src/nvim/api/buffer.c +++ b/src/nvim/api/buffer.c @@ -244,78 +244,6 @@ Boolean nvim_buf_detach(uint64_t channel_id, return true; } -static void buf_clear_luahl(buf_T *buf, bool force) -{ - if (buf->b_luahl || force) { - api_free_luaref(buf->b_luahl_start); - api_free_luaref(buf->b_luahl_window); - api_free_luaref(buf->b_luahl_line); - api_free_luaref(buf->b_luahl_end); - } - buf->b_luahl_start = LUA_NOREF; - buf->b_luahl_window = LUA_NOREF; - buf->b_luahl_line = LUA_NOREF; - buf->b_luahl_end = LUA_NOREF; -} - -/// Unstabilized interface for defining syntax hl in lua. -/// -/// This is not yet safe for general use, lua callbacks will need to -/// be restricted, like textlock and probably other stuff. -/// -/// The API on_line/nvim__put_attr is quite raw and not intended to be the -/// final shape. Ideally this should operate on chunks larger than a single -/// line to reduce interpreter overhead, and generate annotation objects -/// (bufhl/virttext) on the fly but using the same representation. -void nvim__buf_set_luahl(uint64_t channel_id, Buffer buffer, - DictionaryOf(LuaRef) opts, Error *err) - FUNC_API_LUA_ONLY -{ - buf_T *buf = find_buffer_by_handle(buffer, err); - - if (!buf) { - return; - } - - redraw_buf_later(buf, NOT_VALID); - buf_clear_luahl(buf, false); - - for (size_t i = 0; i < opts.size; i++) { - String k = opts.items[i].key; - Object *v = &opts.items[i].value; - if (strequal("on_start", k.data)) { - if (v->type != kObjectTypeLuaRef) { - api_set_error(err, kErrorTypeValidation, "callback is not a function"); - goto error; - } - buf->b_luahl_start = v->data.luaref; - v->data.luaref = LUA_NOREF; - } else if (strequal("on_window", k.data)) { - if (v->type != kObjectTypeLuaRef) { - api_set_error(err, kErrorTypeValidation, "callback is not a function"); - goto error; - } - buf->b_luahl_window = v->data.luaref; - v->data.luaref = LUA_NOREF; - } else if (strequal("on_line", k.data)) { - if (v->type != kObjectTypeLuaRef) { - api_set_error(err, kErrorTypeValidation, "callback is not a function"); - goto error; - } - buf->b_luahl_line = v->data.luaref; - v->data.luaref = LUA_NOREF; - } else { - api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data); - goto error; - } - } - buf->b_luahl = true; - return; -error: - buf_clear_luahl(buf, true); - buf->b_luahl = false; -} - void nvim__buf_redraw_range(Buffer buffer, Integer first, Integer last, Error *err) FUNC_API_LUA_ONLY @@ -1465,15 +1393,17 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, } if (col2 >= 0) { - if (line2 >= 0) { - len = STRLEN(ml_get_buf(buf, (linenr_T)line2+1, false)); + if (line2 >= 0 && line2 < buf->b_ml.ml_line_count) { + len = STRLEN(ml_get_buf(buf, (linenr_T)line2 + 1, false)); + } else if (line2 == buf->b_ml.ml_line_count) { + // We are trying to add an extmark past final newline + len = 0; } else { // reuse len from before line2 = (int)line; } if (col2 > (Integer)len) { - api_set_error(err, kErrorTypeValidation, - "end_col value outside range"); + api_set_error(err, kErrorTypeValidation, "end_col value outside range"); goto error; } } else if (line2 >= 0) { @@ -1490,7 +1420,7 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, } id = extmark_set(buf, (uint64_t)ns_id, id, - (int)line, (colnr_T)col, line2, col2, decor, kExtmarkUndo); + (int)line, (colnr_T)col, line2, col2, decor, kExtmarkNoUndo); return (Integer)id; @@ -1601,10 +1531,10 @@ Integer nvim_buf_add_highlight(Buffer buffer, end_line++; } - 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); + extmark_set(buf, ns_id, 0, + (int)line, (colnr_T)col_start, + end_line, (colnr_T)col_end, + decoration_hl(hl_id), kExtmarkNoUndo); return src_id; } @@ -1664,43 +1594,6 @@ void nvim_buf_clear_highlight(Buffer buffer, nvim_buf_clear_namespace(buffer, ns_id, line_start, line_end, err); } -static VirtText parse_virt_text(Array chunks, Error *err) -{ - VirtText virt_text = KV_INITIAL_VALUE; - for (size_t i = 0; i < chunks.size; i++) { - if (chunks.items[i].type != kObjectTypeArray) { - api_set_error(err, kErrorTypeValidation, "Chunk is not an array"); - goto free_exit; - } - Array chunk = chunks.items[i].data.array; - if (chunk.size == 0 || chunk.size > 2 - || chunk.items[0].type != kObjectTypeString - || (chunk.size == 2 && chunk.items[1].type != kObjectTypeString)) { - api_set_error(err, kErrorTypeValidation, - "Chunk is not an array with one or two strings"); - goto free_exit; - } - - String str = chunk.items[0].data.string; - char *text = transstr(str.size > 0 ? str.data : ""); // allocates - - int hl_id = 0; - if (chunk.size == 2) { - String hl = chunk.items[1].data.string; - if (hl.size > 0) { - hl_id = syn_check_group((char_u *)hl.data, (int)hl.size); - } - } - kv_push(virt_text, ((VirtTextChunk){ .text = text, .hl_id = hl_id })); - } - - return virt_text; - -free_exit: - clear_virttext(&virt_text); - return virt_text; -} - /// Set the virtual text (annotation) for a buffer line. /// @@ -1771,10 +1664,48 @@ Integer nvim_buf_set_virtual_text(Buffer buffer, Decoration *decor = xcalloc(1, sizeof(*decor)); decor->virt_text = virt_text; - extmark_set(buf, ns_id, 0, (int)line, 0, -1, -1, decor, kExtmarkUndo); + extmark_set(buf, ns_id, 0, (int)line, 0, -1, -1, decor, kExtmarkNoUndo); return src_id; } +/// call a function with buffer as temporary current buffer +/// +/// This temporarily switches current buffer to "buffer". +/// If the current window already shows "buffer", the window is not switched +/// If a window inside the current tabpage (including a float) already shows the +/// buffer One of these windows will be set as current window temporarily. +/// Otherwise a temporary scratch window (calleed the "autocmd window" for +/// historical reasons) will be used. +/// +/// This is useful e.g. to call vimL functions that only work with the current +/// buffer/window currently, like |termopen()|. +/// +/// @param buffer Buffer handle, or 0 for current buffer +/// @param fun Function to call inside the buffer (currently lua callable +/// only) +/// @param[out] err Error details, if any +/// @return Return value of function. NB: will deepcopy lua values +/// currently, use upvalues to send lua references in and out. +Object nvim_buf_call(Buffer buffer, LuaRef fun, Error *err) + FUNC_API_SINCE(7) + FUNC_API_LUA_ONLY +{ + buf_T *buf = find_buffer_by_handle(buffer, err); + if (!buf) { + return NIL; + } + try_start(); + aco_save_T aco; + aucmd_prepbuf(&aco, (buf_T *)buf); + + Array args = ARRAY_DICT_INIT; + Object res = nlua_call_ref(fun, NULL, args, true, err); + + aucmd_restbuf(&aco); + try_end(err); + return res; +} + Dictionary nvim__buf_stats(Buffer buffer, Error *err) { Dictionary rv = ARRAY_DICT_INIT; diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index 13f77d2d85..e0d5862e02 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -15,6 +15,8 @@ #include "nvim/lua/executor.h" #include "nvim/ascii.h" #include "nvim/assert.h" +#include "nvim/charset.h" +#include "nvim/syntax.h" #include "nvim/vim.h" #include "nvim/buffer.h" #include "nvim/window.h" @@ -1579,3 +1581,52 @@ bool extmark_get_index_from_obj(buf_T *buf, Integer ns_id, Object obj, int return false; } } + +VirtText parse_virt_text(Array chunks, Error *err) +{ + VirtText virt_text = KV_INITIAL_VALUE; + for (size_t i = 0; i < chunks.size; i++) { + if (chunks.items[i].type != kObjectTypeArray) { + api_set_error(err, kErrorTypeValidation, "Chunk is not an array"); + goto free_exit; + } + Array chunk = chunks.items[i].data.array; + if (chunk.size == 0 || chunk.size > 2 + || chunk.items[0].type != kObjectTypeString + || (chunk.size == 2 && chunk.items[1].type != kObjectTypeString)) { + api_set_error(err, kErrorTypeValidation, + "Chunk is not an array with one or two strings"); + goto free_exit; + } + + String str = chunk.items[0].data.string; + char *text = transstr(str.size > 0 ? str.data : ""); // allocates + + int hl_id = 0; + if (chunk.size == 2) { + String hl = chunk.items[1].data.string; + if (hl.size > 0) { + hl_id = syn_check_group((char_u *)hl.data, (int)hl.size); + } + } + kv_push(virt_text, ((VirtTextChunk){ .text = text, .hl_id = hl_id })); + } + + return virt_text; + +free_exit: + clear_virttext(&virt_text); + return virt_text; +} + +bool api_is_truthy(Object obj, const char *what, Error *err) +{ + if (obj.type == kObjectTypeBoolean) { + return obj.data.boolean; + } else if (obj.type == kObjectTypeInteger) { + return obj.data.integer; // C semantics: non-zery int is true + } else { + api_set_error(err, kErrorTypeValidation, "%s is not an boolean", what); + return false; + } +} diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 9155ffcfb8..1de1472fc2 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -2610,22 +2610,91 @@ Array nvim__inspect_cell(Integer grid, Integer row, Integer col, Error *err) /// interface should probably be derived from a reformed /// bufhl/virttext interface with full support for multi-line /// ranges etc -void nvim__put_attr(Integer id, Integer start_row, Integer start_col, - Integer end_row, Integer end_col) +void nvim__put_attr(Integer line, Integer col, Dictionary opts, Error *err) FUNC_API_LUA_ONLY { if (!lua_attr_active) { return; } - if (id == 0 || syn_get_final_id((int)id) == 0) { - return; + 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("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) { + 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 && line2 < 0) { + line2 = (int)line; } - int attr = syn_id2attr((int)id); - if (attr == 0) { + + int attr = hl_id ? syn_id2attr((int)hl_id) : 0; + if (attr == 0 && !kv_size(virt_text)) { return; } - decorations_add_luahl_attr(attr, (int)start_row, (colnr_T)start_col, - (int)end_row, (colnr_T)end_col); + + VirtText *v = xmalloc(sizeof(*v)); + *v = virt_text; // LeakSanitizer be sad + decorations_add_luahl_attr(attr, (int)line, (colnr_T)col, + (int)line2, (colnr_T)col2, v); +error: + return; } void nvim__screenshot(String path) @@ -2633,3 +2702,68 @@ void nvim__screenshot(String path) { ui_call_screenshot(path); } + +static void clear_luahl(bool force) +{ + if (luahl_active || force) { + api_free_luaref(luahl_start); + api_free_luaref(luahl_win); + api_free_luaref(luahl_line); + api_free_luaref(luahl_end); + } + luahl_start = LUA_NOREF; + luahl_win = LUA_NOREF; + luahl_line = LUA_NOREF; + luahl_end = LUA_NOREF; + luahl_active = false; +} + +/// Unstabilized interface for defining syntax hl in lua. +/// +/// This is not yet safe for general use, lua callbacks will need to +/// be restricted, like textlock and probably other stuff. +/// +/// The API on_line/nvim__put_attr is quite raw and not intended to be the +/// final shape. Ideally this should operate on chunks larger than a single +/// line to reduce interpreter overhead, and generate annotation objects +/// (bufhl/virttext) on the fly but using the same representation. +void nvim__set_luahl(DictionaryOf(LuaRef) opts, Error *err) + FUNC_API_LUA_ONLY +{ + redraw_later(NOT_VALID); + clear_luahl(false); + + for (size_t i = 0; i < opts.size; i++) { + String k = opts.items[i].key; + Object *v = &opts.items[i].value; + if (strequal("on_start", k.data)) { + if (v->type != kObjectTypeLuaRef) { + api_set_error(err, kErrorTypeValidation, "callback is not a function"); + goto error; + } + luahl_start = v->data.luaref; + v->data.luaref = LUA_NOREF; + } else if (strequal("on_win", k.data)) { + if (v->type != kObjectTypeLuaRef) { + api_set_error(err, kErrorTypeValidation, "callback is not a function"); + goto error; + } + luahl_win = v->data.luaref; + v->data.luaref = LUA_NOREF; + } else if (strequal("on_line", k.data)) { + if (v->type != kObjectTypeLuaRef) { + api_set_error(err, kErrorTypeValidation, "callback is not a function"); + goto error; + } + luahl_line = v->data.luaref; + v->data.luaref = LUA_NOREF; + } else { + api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data); + goto error; + } + } + luahl_active = true; + return; +error: + clear_luahl(true); +} diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 4648631ebe..ec633dcc26 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -837,7 +837,7 @@ static void clear_wininfo(buf_T *buf) buf->b_wininfo = wip->wi_next; if (wip->wi_optset) { clear_winopt(&wip->wi_opt); - deleteFoldRecurse(&wip->wi_folds); + deleteFoldRecurse(buf, &wip->wi_folds); } xfree(wip); } @@ -1941,6 +1941,7 @@ void free_buf_options(buf_T *buf, int free_p_ff) vim_regfree(buf->b_s.b_cap_prog); buf->b_s.b_cap_prog = NULL; clear_string_option(&buf->b_s.b_p_spl); + clear_string_option(&buf->b_s.b_p_spo); clear_string_option(&buf->b_p_sua); clear_string_option(&buf->b_p_ft); clear_string_option(&buf->b_p_cink); @@ -2502,7 +2503,7 @@ void buflist_setfpos(buf_T *const buf, win_T *const win, } if (copy_options && wip->wi_optset) { clear_winopt(&wip->wi_opt); - deleteFoldRecurse(&wip->wi_folds); + deleteFoldRecurse(buf, &wip->wi_folds); } } if (lnum != 0) { diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index b3c95f9362..ea968d9592 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -451,6 +451,7 @@ typedef struct { regprog_T *b_cap_prog; // program for 'spellcapcheck' char_u *b_p_spf; // 'spellfile' char_u *b_p_spl; // 'spelllang' + char_u *b_p_spo; // 'spelloptions' int b_cjk; // all CJK letters as OK char_u b_syn_chartab[32]; // syntax iskeyword option char_u *b_syn_isk; // iskeyword option @@ -842,12 +843,6 @@ struct file_buffer { // The number for times the current line has been flushed in the memline. int flush_count; - bool b_luahl; - LuaRef b_luahl_start; - LuaRef b_luahl_window; - LuaRef b_luahl_line; - LuaRef b_luahl_end; - int b_diff_failed; // internal diff failed for this buffer }; diff --git a/src/nvim/change.c b/src/nvim/change.c index b8bc08b747..71614363d2 100644 --- a/src/nvim/change.c +++ b/src/nvim/change.c @@ -362,8 +362,7 @@ void changed_bytes(linenr_T lnum, colnr_T col) /// insert/delete bytes at column /// /// Like changed_bytes() but also adjust extmark for "new" bytes. -/// When "new" is negative text was deleted. -static void inserted_bytes(linenr_T lnum, colnr_T col, int old, int new) +void inserted_bytes(linenr_T lnum, colnr_T col, int old, int new) { if (curbuf_splice_pending == 0) { extmark_splice_cols(curbuf, (int)lnum-1, col, old, new, kExtmarkUndo); @@ -1677,9 +1676,16 @@ int open_line( truncate_spaces(saved_line); } ml_replace(curwin->w_cursor.lnum, saved_line, false); - extmark_splice_cols( - curbuf, (int)curwin->w_cursor.lnum, - 0, curwin->w_cursor.col, (int)STRLEN(saved_line), kExtmarkUndo); + + int new_len = (int)STRLEN(saved_line); + + // TODO(vigoux): maybe there is issues there with expandtabs ? + if (new_len < curwin->w_cursor.col) { + extmark_splice_cols( + curbuf, (int)curwin->w_cursor.lnum, + new_len, curwin->w_cursor.col - new_len, 0, kExtmarkUndo); + } + saved_line = NULL; if (did_append) { changed_lines(curwin->w_cursor.lnum, curwin->w_cursor.col, diff --git a/src/nvim/cursor_shape.c b/src/nvim/cursor_shape.c index 3f06340611..0d21080aa5 100644 --- a/src/nvim/cursor_shape.c +++ b/src/nvim/cursor_shape.c @@ -13,6 +13,10 @@ #include "nvim/api/private/helpers.h" #include "nvim/ui.h" +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "cursor_shape.c.generated.h" +#endif + /// Handling of cursor and mouse pointer shapes in various modes. cursorentry_T shape_table[SHAPE_IDX_COUNT] = { @@ -77,7 +81,9 @@ Array mode_style_array(void) return all; } -/// Parse the 'guicursor' option +/// Parses the 'guicursor' option. +/// +/// Clears `shape_table` if 'guicursor' is empty. /// /// @param what SHAPE_CURSOR or SHAPE_MOUSE ('mouseshape') /// @@ -99,11 +105,17 @@ char_u *parse_shape_opt(int what) // First round: check for errors; second round: do it for real. for (round = 1; round <= 2; round++) { + if (round == 2 || *p_guicursor == NUL) { + // Set all entries to default (block, blinkon0, default color). + // This is the default for anything that is not set. + clear_shape_table(); + if (*p_guicursor == NUL) { + ui_mode_info_set(); + return NULL; + } + } // Repeat for all comma separated parts. modep = p_guicursor; - if (*p_guicursor == NUL) { - modep = (char_u *)"a:block-blinkon0"; - } while (modep != NULL && *modep != NUL) { colonp = vim_strchr(modep, ':'); commap = vim_strchr(modep, ','); @@ -144,14 +156,6 @@ char_u *parse_shape_opt(int what) if (all_idx >= 0) { idx = all_idx--; - } else if (round == 2) { - { - // Set the defaults, for the missing parts - shape_table[idx].shape = SHAPE_BLOCK; - shape_table[idx].blinkwait = 0L; - shape_table[idx].blinkon = 0L; - shape_table[idx].blinkoff = 0L; - } } /* Parse the part after the colon */ @@ -330,3 +334,16 @@ int cursor_get_mode_idx(void) return SHAPE_IDX_N; } } + +/// Clears all entries in shape_table to block, blinkon0, and default color. +static void clear_shape_table(void) +{ + for (int idx = 0; idx < SHAPE_IDX_COUNT; idx++) { + shape_table[idx].shape = SHAPE_BLOCK; + shape_table[idx].blinkwait = 0L; + shape_table[idx].blinkon = 0L; + shape_table[idx].blinkoff = 0L; + shape_table[idx].id = 0; + shape_table[idx].id_lm = 0; + } +} diff --git a/src/nvim/edit.c b/src/nvim/edit.c index 1e149da1dc..de2346a9d8 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -1254,14 +1254,6 @@ check_pum: normalchar: // Insert a normal character. - if (mod_mask == MOD_MASK_ALT || mod_mask == MOD_MASK_META) { - // Unmapped ALT/META chord behaves like ESC+c. #8213 - stuffcharReadbuff(ESC); - stuffcharReadbuff(s->c); - u_sync(false); - break; - } - if (!p_paste) { // Trigger InsertCharPre. char_u *str = do_insert_char_pre(s->c); diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 32830c5d7f..a2490be355 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -736,7 +736,7 @@ int eval_expr_typval(const typval_T *expr, typval_T *argv, if (s == NULL || *s == NUL) { return FAIL; } - if (call_func(s, (int)STRLEN(s), rettv, argc, argv, NULL, + if (call_func(s, -1, rettv, argc, argv, NULL, 0L, 0L, &dummy, true, NULL, NULL) == FAIL) { return FAIL; } @@ -746,7 +746,7 @@ int eval_expr_typval(const typval_T *expr, typval_T *argv, if (s == NULL || *s == NUL) { return FAIL; } - if (call_func(s, (int)STRLEN(s), rettv, argc, argv, NULL, + if (call_func(s, -1, rettv, argc, argv, NULL, 0L, 0L, &dummy, true, partial, NULL) == FAIL) { return FAIL; } @@ -3802,8 +3802,9 @@ static int eval6(char_u **arg, typval_T *rettv, int evaluate, int want_string) */ for (;; ) { op = **arg; - if (op != '*' && op != '/' && op != '%') + if (op != '*' && op != '/' && op != '%') { break; + } if (evaluate) { if (rettv->v_type == VAR_FLOAT) { @@ -3905,6 +3906,7 @@ static int eval6(char_u **arg, typval_T *rettv, int evaluate, int want_string) // (expression) nested expression // [expr, expr] List // {key: val, key: val} Dictionary +// #{key: val, key: val} Dictionary with literal keys // // Also handle: // ! in front logical NOT @@ -4012,11 +4014,21 @@ static int eval7( case '[': ret = get_list_tv(arg, rettv, evaluate); break; + // Dictionary: #{key: val, key: val} + case '#': + if ((*arg)[1] == '{') { + (*arg)++; + ret = dict_get_tv(arg, rettv, evaluate, true); + } else { + ret = NOTDONE; + } + break; + // Lambda: {arg, arg -> expr} - // Dictionary: {key: val, key: val} + // Dictionary: {'key': val, 'key': val} case '{': ret = get_lambda_tv(arg, rettv, evaluate); if (ret == NOTDONE) { - ret = dict_get_tv(arg, rettv, evaluate); + ret = dict_get_tv(arg, rettv, evaluate, false); } break; @@ -4518,7 +4530,6 @@ int get_option_tv(const char **const arg, typval_T *const rettv, static int get_string_tv(char_u **arg, typval_T *rettv, int evaluate) { char_u *p; - char_u *name; unsigned int extra = 0; /* @@ -4526,11 +4537,14 @@ static int get_string_tv(char_u **arg, typval_T *rettv, int evaluate) */ for (p = *arg + 1; *p != NUL && *p != '"'; MB_PTR_ADV(p)) { if (*p == '\\' && p[1] != NUL) { - ++p; - /* A "\<x>" form occupies at least 4 characters, and produces up - * to 6 characters: reserve space for 2 extra */ - if (*p == '<') - extra += 2; + p++; + // A "\<x>" form occupies at least 4 characters, and produces up + // to 21 characters (3 * 6 for the char and 3 for a modifier): + // reserve space for 18 extra. + // Each byte in the char could be encoded as K_SPECIAL K_EXTRA x. + if (*p == '<') { + extra += 18; + } } } @@ -4549,7 +4563,8 @@ static int get_string_tv(char_u **arg, typval_T *rettv, int evaluate) * Copy the string into allocated memory, handling backslashed * characters. */ - name = xmalloc(p - *arg + extra); + const int len = (int)(p - *arg + extra); + char_u *name = xmalloc(len); rettv->v_type = VAR_STRING; rettv->vval.v_string = name; @@ -4616,6 +4631,9 @@ static int get_string_tv(char_u **arg, typval_T *rettv, int evaluate) extra = trans_special((const char_u **)&p, STRLEN(p), name, true, true); if (extra != 0) { name += extra; + if (name >= rettv->vval.v_string + len) { + iemsg("get_string_tv() used more space than allocated"); + } break; } FALLTHROUGH; @@ -5341,11 +5359,31 @@ static inline bool set_ref_dict(dict_T *dict, int copyID) return false; } -/* - * Allocate a variable for a Dictionary and fill it from "*arg". - * Return OK or FAIL. Returns NOTDONE for {expr}. - */ -static int dict_get_tv(char_u **arg, typval_T *rettv, int evaluate) + +// Get the key for *{key: val} into "tv" and advance "arg". +// Return FAIL when there is no valid key. +static int get_literal_key(char_u **arg, typval_T *tv) + FUNC_ATTR_NONNULL_ALL +{ + char_u *p; + + if (!ASCII_ISALNUM(**arg) && **arg != '_' && **arg != '-') { + return FAIL; + } + for (p = *arg; ASCII_ISALNUM(*p) || *p == '_' || *p == '-'; p++) { + } + tv->v_type = VAR_STRING; + tv->vval.v_string = vim_strnsave(*arg, (int)(p - *arg)); + + *arg = skipwhite(p); + return OK; +} + +// Allocate a variable for a Dictionary and fill it from "*arg". +// "literal" is true for *{key: val} +// Return OK or FAIL. Returns NOTDONE for {expr}. +static int dict_get_tv(char_u **arg, typval_T *rettv, int evaluate, + bool literal) { dict_T *d = NULL; typval_T tvkey; @@ -5379,7 +5417,9 @@ static int dict_get_tv(char_u **arg, typval_T *rettv, int evaluate) *arg = skipwhite(*arg + 1); while (**arg != '}' && **arg != NUL) { - if (eval1(arg, &tvkey, evaluate) == FAIL) { // recursive! + if ((literal + ? get_literal_key(arg, &tvkey) + : eval1(arg, &tvkey, evaluate)) == FAIL) { // recursive! goto failret; } if (**arg != ':') { @@ -6962,9 +7002,10 @@ void set_buffer_lines(buf_T *buf, linenr_T lnum_arg, bool append, if (!append && lnum <= curbuf->b_ml.ml_line_count) { // Existing line, replace it. + int old_len = (int)STRLEN(ml_get(lnum)); if (u_savesub(lnum) == OK && ml_replace(lnum, (char_u *)line, true) == OK) { - changed_bytes(lnum, 0); + inserted_bytes(lnum, 0, old_len, STRLEN(line)); if (is_curbuf && lnum == curwin->w_cursor.lnum) { check_cursor_col(); } @@ -7263,7 +7304,7 @@ bool callback_call(Callback *const callback, const int argcount_in, } int dummy; - return call_func(name, (int)STRLEN(name), rettv, argcount_in, argvars_in, + return call_func(name, -1, rettv, argcount_in, argvars_in, NULL, curwin->w_cursor.lnum, curwin->w_cursor.lnum, &dummy, true, partial, NULL); } @@ -8485,7 +8526,7 @@ handle_subscript( } else { s = (char_u *)""; } - ret = get_func_tv(s, lua ? slen : (int)STRLEN(s), rettv, (char_u **)arg, + ret = get_func_tv(s, lua ? slen : -1, rettv, (char_u **)arg, curwin->w_cursor.lnum, curwin->w_cursor.lnum, &len, evaluate, pt, selfdict); diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index 3a4b4f2a50..83ad948a93 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -2584,8 +2584,6 @@ static void f_foldtextresult(typval_T *argvars, typval_T *rettv, FunPtr fptr) { char_u *text; char_u buf[FOLD_TEXT_LEN]; - foldinfo_T foldinfo; - int fold_count; static bool entered = false; rettv->v_type = VAR_STRING; @@ -2599,9 +2597,10 @@ static void f_foldtextresult(typval_T *argvars, typval_T *rettv, FunPtr fptr) if (lnum < 0) { lnum = 0; } - fold_count = foldedCount(curwin, lnum, &foldinfo); - if (fold_count > 0) { - text = get_foldtext(curwin, lnum, lnum + fold_count - 1, &foldinfo, buf); + + foldinfo_T info = fold_info(curwin, lnum); + if (info.fi_lines > 0) { + text = get_foldtext(curwin, lnum, lnum + info.fi_lines - 1, info, buf); if (text == buf) { text = vim_strsave(text); } @@ -9175,7 +9174,7 @@ static int item_compare2(const void *s1, const void *s2, bool keep_zero) rettv.v_type = VAR_UNKNOWN; // tv_clear() uses this res = call_func((const char_u *)func_name, - (int)STRLEN(func_name), + -1, &rettv, 2, argv, NULL, 0L, 0L, &dummy, true, partial, sortinfo->item_compare_selfdict); tv_clear(&argv[0]); diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c index 1b80b22213..e0361048bc 100644 --- a/src/nvim/eval/userfunc.c +++ b/src/nvim/eval/userfunc.c @@ -32,7 +32,11 @@ #define FC_DELETED 0x10 // :delfunction used while uf_refcount > 0 #define FC_REMOVED 0x20 // function redefined while uf_refcount > 0 #define FC_SANDBOX 0x40 // function defined in the sandbox -#define FC_CFUNC 0x80 // C function extension +#define FC_DEAD 0x80 // function kept only for reference to dfunc +#define FC_EXPORT 0x100 // "export def Func()" +#define FC_NOARGS 0x200 // no a: variables in lambda +#define FC_VIM9 0x400 // defined in vim9 script file +#define FC_CFUNC 0x800 // C function extension #ifdef INCLUDE_GENERATED_DECLARATIONS #include "eval/userfunc.c.generated.h" @@ -246,6 +250,10 @@ int get_lambda_tv(char_u **arg, typval_T *rettv, bool evaluate) ((char_u **)(newlines.ga_data))[newlines.ga_len++] = p; STRCPY(p, "return "); STRLCPY(p + 7, s, e - s + 1); + if (strstr((char *)p + 7, "a:") == NULL) { + // No a: variables are used for sure. + flags |= FC_NOARGS; + } fp->uf_refcount = 1; STRCPY(fp->uf_name, name); @@ -367,7 +375,7 @@ void emsg_funcname(char *ermsg, const char_u *name) int get_func_tv( const char_u *name, // name of the function - int len, // length of "name" + int len, // length of "name" or -1 to use strlen() typval_T *rettv, char_u **arg, // argument, pointing to the '(' linenr_T firstline, // first line of range @@ -813,17 +821,12 @@ void call_user_func(ufunc_T *fp, int argcount, typval_T *argvars, current_funccal = fc; fc->func = fp; fc->rettv = rettv; - rettv->vval.v_number = 0; - fc->linenr = 0; - fc->returned = FALSE; fc->level = ex_nesting_level; // Check if this function has a breakpoint. fc->breakpoint = dbg_find_breakpoint(false, fp->uf_name, (linenr_T)0); fc->dbg_tick = debug_tick; // Set up fields for closure. - fc->fc_refcount = 0; - fc->fc_copyID = 0; ga_init(&fc->fc_funcs, sizeof(ufunc_T *), 1); func_ptr_ref(fp); @@ -853,37 +856,42 @@ void call_user_func(ufunc_T *fp, int argcount, typval_T *argvars, ++selfdict->dv_refcount; } - /* - * Init a: variables. - * Set a:0 to "argcount". - * Set a:000 to a list with room for the "..." arguments. - */ + // Init a: variables, unless none found (in lambda). + // Set a:0 to "argcount". + // Set a:000 to a list with room for the "..." arguments. init_var_dict(&fc->l_avars, &fc->l_avars_var, VAR_SCOPE); - add_nr_var(&fc->l_avars, (dictitem_T *)&fc->fixvar[fixvar_idx++], "0", - (varnumber_T)(argcount - fp->uf_args.ga_len)); + if ((fp->uf_flags & FC_NOARGS) == 0) { + add_nr_var(&fc->l_avars, (dictitem_T *)&fc->fixvar[fixvar_idx++], "0", + (varnumber_T)(argcount - fp->uf_args.ga_len)); + } fc->l_avars.dv_lock = VAR_FIXED; - // Use "name" to avoid a warning from some compiler that checks the - // destination size. - v = (dictitem_T *)&fc->fixvar[fixvar_idx++]; + if ((fp->uf_flags & FC_NOARGS) == 0) { + // Use "name" to avoid a warning from some compiler that checks the + // destination size. + v = (dictitem_T *)&fc->fixvar[fixvar_idx++]; #ifndef __clang_analyzer__ - name = v->di_key; - STRCPY(name, "000"); + name = v->di_key; + STRCPY(name, "000"); #endif - v->di_flags = DI_FLAGS_RO | DI_FLAGS_FIX; - tv_dict_add(&fc->l_avars, v); - v->di_tv.v_type = VAR_LIST; - v->di_tv.v_lock = VAR_FIXED; - v->di_tv.vval.v_list = &fc->l_varlist; + v->di_flags = DI_FLAGS_RO | DI_FLAGS_FIX; + tv_dict_add(&fc->l_avars, v); + v->di_tv.v_type = VAR_LIST; + v->di_tv.v_lock = VAR_FIXED; + v->di_tv.vval.v_list = &fc->l_varlist; + } tv_list_init_static(&fc->l_varlist); tv_list_set_lock(&fc->l_varlist, VAR_FIXED); // Set a:firstline to "firstline" and a:lastline to "lastline". // Set a:name to named arguments. // Set a:N to the "..." arguments. - add_nr_var(&fc->l_avars, (dictitem_T *)&fc->fixvar[fixvar_idx++], - "firstline", (varnumber_T)firstline); - add_nr_var(&fc->l_avars, (dictitem_T *)&fc->fixvar[fixvar_idx++], - "lastline", (varnumber_T)lastline); + // Skipped when no a: variables used (in lambda). + if ((fp->uf_flags & FC_NOARGS) == 0) { + add_nr_var(&fc->l_avars, (dictitem_T *)&fc->fixvar[fixvar_idx++], + "firstline", (varnumber_T)firstline); + add_nr_var(&fc->l_avars, (dictitem_T *)&fc->fixvar[fixvar_idx++], + "lastline", (varnumber_T)lastline); + } for (int i = 0; i < argcount; i++) { bool addlocal = false; @@ -895,6 +903,10 @@ void call_user_func(ufunc_T *fp, int argcount, typval_T *argvars, addlocal = true; } } else { + if ((fp->uf_flags & FC_NOARGS) != 0) { + // Bail out if no a: arguments used (in lambda). + break; + } // "..." argument a:1, a:2, etc. snprintf((char *)numbuf, sizeof(numbuf), "%d", ai + 1); name = numbuf; @@ -1034,9 +1046,19 @@ void call_user_func(ufunc_T *fp, int argcount, typval_T *argvars, save_did_emsg = did_emsg; did_emsg = FALSE; - // call do_cmdline() to execute the lines - do_cmdline(NULL, get_func_line, (void *)fc, - DOCMD_NOWAIT|DOCMD_VERBOSE|DOCMD_REPEAT); + if (islambda) { + char_u *p = *(char_u **)fp->uf_lines.ga_data + 7; + + // A Lambda always has the command "return {expr}". It is much faster + // to evaluate {expr} directly. + ex_nesting_level++; + eval1(&p, rettv, true); + ex_nesting_level--; + } else { + // call do_cmdline() to execute the lines + do_cmdline(NULL, get_func_line, (void *)fc, + DOCMD_NOWAIT|DOCMD_VERBOSE|DOCMD_REPEAT); + } --RedrawingDisabled; @@ -1291,7 +1313,7 @@ int func_call(char_u *name, typval_T *args, partial_T *partial, tv_copy(TV_LIST_ITEM_TV(item), &argv[argc++]); }); - r = call_func(name, (int)STRLEN(name), rettv, argc, argv, NULL, + r = call_func(name, -1, rettv, argc, argv, NULL, curwin->w_cursor.lnum, curwin->w_cursor.lnum, &dummy, true, partial, selfdict); @@ -1304,6 +1326,36 @@ func_call_skip_call: return r; } +// Give an error message for the result of a function. +// Nothing if "error" is FCERR_NONE. +static void user_func_error(int error, const char_u *name) + FUNC_ATTR_NONNULL_ALL +{ + switch (error) { + case ERROR_UNKNOWN: + emsg_funcname(N_("E117: Unknown function: %s"), name); + break; + case ERROR_DELETED: + emsg_funcname(N_("E933: Function was deleted: %s"), name); + break; + case ERROR_TOOMANY: + emsg_funcname(_(e_toomanyarg), name); + break; + case ERROR_TOOFEW: + emsg_funcname(N_("E119: Not enough arguments for function: %s"), + name); + break; + case ERROR_SCRIPT: + emsg_funcname(N_("E120: Using <SID> not in a script context: %s"), + name); + break; + case ERROR_DICT: + emsg_funcname(N_("E725: Calling dict function without Dictionary: %s"), + name); + break; + } +} + /// Call a function with its resolved parameters /// /// "argv_func", when not NULL, can be used to fill in arguments only when the @@ -1316,7 +1368,7 @@ func_call_skip_call: int call_func( const char_u *funcname, // name of the function - int len, // length of "name" + int len, // length of "name" or -1 to use strlen() typval_T *rettv, // [out] value goes here int argcount_in, // number of "argvars" typval_T *argvars_in, // vars for arguments, must have "argcount" @@ -1333,11 +1385,11 @@ call_func( { int ret = FAIL; int error = ERROR_NONE; - ufunc_T *fp; + ufunc_T *fp = NULL; char_u fname_buf[FLEN_FIXED + 1]; char_u *tofree = NULL; - char_u *fname; - char_u *name; + char_u *fname = NULL; + char_u *name = NULL; int argcount = argcount_in; typval_T *argvars = argvars_in; dict_T *selfdict = selfdict_in; @@ -1348,11 +1400,18 @@ call_func( // even when call_func() returns FAIL. rettv->v_type = VAR_UNKNOWN; - // Make a copy of the name, if it comes from a funcref variable it could - // be changed or deleted in the called function. - name = vim_strnsave(funcname, len); - - fname = fname_trans_sid(name, fname_buf, &tofree, &error); + if (len <= 0) { + len = (int)STRLEN(funcname); + } + if (partial != NULL) { + fp = partial->pt_func; + } + if (fp == NULL) { + // Make a copy of the name, if it comes from a funcref variable it could + // be changed or deleted in the called function. + name = vim_strnsave(funcname, len); + fname = fname_trans_sid(name, fname_buf, &tofree, &error); + } *doesrange = false; @@ -1384,7 +1443,7 @@ call_func( char_u *rfname = fname; // Ignore "g:" before a function name. - if (fname[0] == 'g' && fname[1] == ':') { + if (fp == NULL && fname[0] == 'g' && fname[1] == ':') { rfname = fname + 2; } @@ -1397,11 +1456,9 @@ call_func( error = ERROR_NONE; nlua_typval_call((const char *)funcname, len, argvars, argcount, rettv); } - } else if (!builtin_function((const char *)rfname, -1)) { + } else if (fp != NULL || !builtin_function((const char *)rfname, -1)) { // User defined function. - if (partial != NULL && partial->pt_func != NULL) { - fp = partial->pt_func; - } else { + if (fp == NULL) { fp = find_func(rfname); } @@ -1480,29 +1537,7 @@ theend: // Report an error unless the argument evaluation or function call has been // cancelled due to an aborting error, an interrupt, or an exception. if (!aborting()) { - switch (error) { - case ERROR_UNKNOWN: - emsg_funcname(N_("E117: Unknown function: %s"), name); - break; - case ERROR_DELETED: - emsg_funcname(N_("E933: Function was deleted: %s"), name); - break; - case ERROR_TOOMANY: - emsg_funcname(_(e_toomanyarg), name); - break; - case ERROR_TOOFEW: - emsg_funcname(N_("E119: Not enough arguments for function: %s"), - name); - break; - case ERROR_SCRIPT: - emsg_funcname(N_("E120: Using <SID> not in a script context: %s"), - name); - break; - case ERROR_DICT: - emsg_funcname(N_("E725: Calling dict function without Dictionary: %s"), - name); - break; - } + user_func_error(error, (name != NULL) ? name : funcname); } while (argv_clear > 0) { @@ -2853,7 +2888,7 @@ void ex_call(exarg_T *eap) curwin->w_cursor.coladd = 0; } arg = startarg; - if (get_func_tv(name, (int)STRLEN(name), &rettv, &arg, + if (get_func_tv(name, -1, &rettv, &arg, eap->line1, eap->line2, &doesrange, true, partial, fudi.fd_dict) == FAIL) { failed = true; @@ -3347,9 +3382,13 @@ bool set_ref_in_previous_funccal(int copyID) { bool abort = false; - for (funccall_T *fc = previous_funccal; fc != NULL; fc = fc->caller) { - abort = abort || set_ref_in_ht(&fc->l_vars.dv_hashtab, copyID + 1, NULL); - abort = abort || set_ref_in_ht(&fc->l_avars.dv_hashtab, copyID + 1, NULL); + for (funccall_T *fc = previous_funccal; !abort && fc != NULL; + fc = fc->caller) { + fc->fc_copyID = copyID + 1; + abort = abort + || set_ref_in_ht(&fc->l_vars.dv_hashtab, copyID + 1, NULL) + || set_ref_in_ht(&fc->l_avars.dv_hashtab, copyID + 1, NULL) + || set_ref_in_list(&fc->l_varlist, copyID + 1, NULL); } return abort; } @@ -3360,9 +3399,11 @@ static bool set_ref_in_funccal(funccall_T *fc, int copyID) if (fc->fc_copyID != copyID) { fc->fc_copyID = copyID; - abort = abort || set_ref_in_ht(&fc->l_vars.dv_hashtab, copyID, NULL); - abort = abort || set_ref_in_ht(&fc->l_avars.dv_hashtab, copyID, NULL); - abort = abort || set_ref_in_func(NULL, fc->func, copyID); + abort = abort + || set_ref_in_ht(&fc->l_vars.dv_hashtab, copyID, NULL) + || set_ref_in_ht(&fc->l_avars.dv_hashtab, copyID, NULL) + || set_ref_in_list(&fc->l_varlist, copyID, NULL) + || set_ref_in_func(NULL, fc->func, copyID); } return abort; } @@ -3372,12 +3413,13 @@ bool set_ref_in_call_stack(int copyID) { bool abort = false; - for (funccall_T *fc = current_funccal; fc != NULL; fc = fc->caller) { + for (funccall_T *fc = current_funccal; !abort && fc != NULL; + fc = fc->caller) { abort = abort || set_ref_in_funccal(fc, copyID); } // Also go through the funccal_stack. - for (funccal_entry_T *entry = funccal_stack; entry != NULL; + for (funccal_entry_T *entry = funccal_stack; !abort && entry != NULL; entry = entry->next) { for (funccall_T *fc = entry->top_funccal; !abort && fc != NULL; fc = fc->caller) { diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 9be6adcd61..bb4e92efc0 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -2497,8 +2497,12 @@ int do_ecmd( new_name = NULL; } set_bufref(&bufref, buf); - if (p_ur < 0 || curbuf->b_ml.ml_line_count <= p_ur) { - // Save all the text, so that the reload can be undone. + + // If the buffer was used before, store the current contents so that + // the reload can be undone. Do not do this if the (empty) buffer is + // being re-used for another file. + if (!(curbuf->b_flags & BF_NEVERLOADED) + && (p_ur < 0 || curbuf->b_ml.ml_line_count <= p_ur)) { // Sync first so that this is a separate undo-able action. u_sync(false); if (u_savecommon(0, curbuf->b_ml.ml_line_count + 1, 0, true) diff --git a/src/nvim/ex_cmds.lua b/src/nvim/ex_cmds.lua index a01f92df27..d62b00fee1 100644 --- a/src/nvim/ex_cmds.lua +++ b/src/nvim/ex_cmds.lua @@ -337,7 +337,7 @@ return { }, { command='caddexpr', - flags=bit.bor(NEEDARG, WORD1, NOTRLCOM, TRLBAR), + flags=bit.bor(NEEDARG, WORD1, NOTRLCOM), addr_type=ADDR_LINES, func='ex_cexpr', }, @@ -409,7 +409,7 @@ return { }, { command='cexpr', - flags=bit.bor(NEEDARG, WORD1, NOTRLCOM, TRLBAR, BANG), + flags=bit.bor(NEEDARG, WORD1, NOTRLCOM, BANG), addr_type=ADDR_LINES, func='ex_cexpr', }, @@ -447,7 +447,7 @@ return { }, { command='cgetexpr', - flags=bit.bor(NEEDARG, WORD1, NOTRLCOM, TRLBAR), + flags=bit.bor(NEEDARG, WORD1, NOTRLCOM), addr_type=ADDR_LINES, func='ex_cexpr', }, @@ -1299,7 +1299,7 @@ return { }, { command='laddexpr', - flags=bit.bor(NEEDARG, WORD1, NOTRLCOM, TRLBAR), + flags=bit.bor(NEEDARG, WORD1, NOTRLCOM), addr_type=ADDR_LINES, func='ex_cexpr', }, @@ -1389,7 +1389,7 @@ return { }, { command='lexpr', - flags=bit.bor(NEEDARG, WORD1, NOTRLCOM, TRLBAR, BANG), + flags=bit.bor(NEEDARG, WORD1, NOTRLCOM, BANG), addr_type=ADDR_LINES, func='ex_cexpr', }, @@ -1427,7 +1427,7 @@ return { }, { command='lgetexpr', - flags=bit.bor(NEEDARG, WORD1, NOTRLCOM, TRLBAR), + flags=bit.bor(NEEDARG, WORD1, NOTRLCOM), addr_type=ADDR_LINES, func='ex_cexpr', }, diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index dfebd13868..ccaa0b0e52 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -258,6 +258,27 @@ void do_exmode(int improved) msg_scroll = save_msg_scroll; } +// Print the executed command for when 'verbose' is set. +// When "lnum" is 0 only print the command. +static void msg_verbose_cmd(linenr_T lnum, char_u *cmd) + FUNC_ATTR_NONNULL_ALL +{ + no_wait_return++; + verbose_enter_scroll(); + + if (lnum == 0) { + smsg(_("Executing: %s"), cmd); + } else { + smsg(_("line %" PRIdLINENR ": %s"), lnum, cmd); + } + if (msg_silent == 0) { + msg_puts("\n"); // don't overwrite this + } + + verbose_leave_scroll(); + no_wait_return--; +} + /* * Execute a simple command line. Used for translated commands like "*". */ @@ -567,17 +588,8 @@ int do_cmdline(char_u *cmdline, LineGetter fgetline, } } - if (p_verbose >= 15 && sourcing_name != NULL) { - ++no_wait_return; - verbose_enter_scroll(); - - smsg(_("line %" PRIdLINENR ": %s"), sourcing_lnum, cmdline_copy); - if (msg_silent == 0) { - msg_puts("\n"); // don't overwrite this either - } - - verbose_leave_scroll(); - --no_wait_return; + if ((p_verbose >= 15 && sourcing_name != NULL) || p_verbose >= 16) { + msg_verbose_cmd(sourcing_lnum, cmdline_copy); } /* @@ -2228,17 +2240,19 @@ int parse_command_modifiers(exarg_T *eap, char_u **errormsg, bool skip_only) continue; case 't': if (checkforcmd(&p, "tab", 3)) { - long tabnr = get_address( - eap, &eap->cmd, ADDR_TABS, eap->skip, skip_only, false, 1); + if (!skip_only) { + long tabnr = get_address( + eap, &eap->cmd, ADDR_TABS, eap->skip, skip_only, false, 1); - if (tabnr == MAXLNUM) { - cmdmod.tab = tabpage_index(curtab) + 1; - } else { - if (tabnr < 0 || tabnr > LAST_TAB_NR) { - *errormsg = (char_u *)_(e_invrange); - return false; + if (tabnr == MAXLNUM) { + cmdmod.tab = tabpage_index(curtab) + 1; + } else { + if (tabnr < 0 || tabnr > LAST_TAB_NR) { + *errormsg = (char_u *)_(e_invrange); + return false; + } + cmdmod.tab = tabnr + 1; } - cmdmod.tab = tabnr + 1; } eap->cmd = p; continue; @@ -9295,14 +9309,17 @@ static void ex_match(exarg_T *eap) static void ex_fold(exarg_T *eap) { if (foldManualAllowed(true)) { - foldCreate(curwin, eap->line1, eap->line2); + pos_T start = { eap->line1, 1, 0 }; + pos_T end = { eap->line2, 1, 0 }; + foldCreate(curwin, start, end); } } static void ex_foldopen(exarg_T *eap) { - opFoldRange(eap->line1, eap->line2, eap->cmdidx == CMD_foldopen, - eap->forceit, FALSE); + pos_T start = { eap->line1, 1, 0 }; + pos_T end = { eap->line2, 1, 0 }; + opFoldRange(start, end, eap->cmdidx == CMD_foldopen, eap->forceit, false); } static void ex_folddo(exarg_T *eap) diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index c966c780a0..f9ca7bfa42 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -276,6 +276,7 @@ static void init_incsearch_state(incsearch_state_T *s) // Sets search_first_line and search_last_line to the address range. static bool do_incsearch_highlighting(int firstc, incsearch_state_T *s, int *skiplen, int *patlen) + FUNC_ATTR_NONNULL_ALL { char_u *cmd; cmdmod_T save_cmdmod = cmdmod; @@ -287,6 +288,7 @@ static bool do_incsearch_highlighting(int firstc, incsearch_state_T *s, exarg_T ea; pos_T save_cursor; bool use_last_pat; + bool retval = false; *skiplen = 0; *patlen = ccline.cmdlen; @@ -306,6 +308,7 @@ static bool do_incsearch_highlighting(int firstc, incsearch_state_T *s, return false; } + emsg_off++; memset(&ea, 0, sizeof(ea)); ea.line1 = 1; ea.line2 = 1; @@ -317,13 +320,13 @@ static bool do_incsearch_highlighting(int firstc, incsearch_state_T *s, cmd = skip_range(ea.cmd, NULL); if (vim_strchr((char_u *)"sgvl", *cmd) == NULL) { - return false; + goto theend; } // Skip over "substitute" to find the pattern separator. for (p = cmd; ASCII_ISALPHA(*p); p++) {} if (*skipwhite(p) == NUL) { - return false; + goto theend; } if (STRNCMP(cmd, "substitute", p - cmd) == 0 @@ -336,12 +339,15 @@ static bool do_incsearch_highlighting(int firstc, incsearch_state_T *s, p_magic = false; } } else if (STRNCMP(cmd, "sort", MAX(p - cmd, 3)) == 0) { - // skip over flags. + // skip over ! and flags + if (*p == '!') { + p = skipwhite(p + 1); + } while (ASCII_ISALPHA(*(p = skipwhite(p)))) { p++; } if (*p == NUL) { - return false; + goto theend; } } else if (STRNCMP(cmd, "vimgrep", MAX(p - cmd, 3)) == 0 || STRNCMP(cmd, "vimgrepadd", MAX(p - cmd, 8)) == 0 @@ -352,14 +358,14 @@ static bool do_incsearch_highlighting(int firstc, incsearch_state_T *s, if (*p == '!') { p++; if (*skipwhite(p) == NUL) { - return false; + goto theend; } } if (*cmd != 'g') { delim_optional = true; } } else { - return false; + goto theend; } p = skipwhite(p); @@ -368,7 +374,7 @@ static bool do_incsearch_highlighting(int firstc, incsearch_state_T *s, use_last_pat = end == p && *end == delim; if (end == p && !use_last_pat) { - return false; + goto theend; } // Don't do 'hlsearch' highlighting if the pattern matches everything. @@ -380,7 +386,7 @@ static bool do_incsearch_highlighting(int firstc, incsearch_state_T *s, empty = empty_pattern(p); *end = c; if (empty) { - return false; + goto theend; } } @@ -408,7 +414,10 @@ static bool do_incsearch_highlighting(int firstc, incsearch_state_T *s, } curwin->w_cursor = save_cursor; - return true; + retval = true; +theend: + emsg_off--; + return retval; } // May do 'incsearch' highlighting if desired. @@ -443,6 +452,10 @@ static void may_do_incsearch_highlighting(int firstc, long count, if (search_first_line == 0) { // start at the original cursor position curwin->w_cursor = s->search_start; + } else if (search_first_line > curbuf->b_ml.ml_line_count) { + // start after the last line + curwin->w_cursor.lnum = curbuf->b_ml.ml_line_count; + curwin->w_cursor.col = MAXCOL; } else { // start at the first line in the range curwin->w_cursor.lnum = search_first_line; @@ -544,6 +557,7 @@ static void may_do_incsearch_highlighting(int firstc, long count, } update_screen(SOME_VALID); + highlight_match = false; restore_last_search_pattern(); // Leave it at the end to make CTRL-R CTRL-W work. But not when beyond the @@ -563,6 +577,7 @@ static void may_do_incsearch_highlighting(int firstc, long count, // May set "*c" to the added character. // Return OK when calling command_line_not_changed. static int may_add_char_to_search(int firstc, int *c, incsearch_state_T *s) + FUNC_ATTR_NONNULL_ALL { int skiplen, patlen; @@ -579,8 +594,8 @@ static int may_add_char_to_search(int firstc, int *c, incsearch_state_T *s) if (s->did_incsearch) { curwin->w_cursor = s->match_end; - if (!equalpos(curwin->w_cursor, s->search_start)) { - *c = gchar_cursor(); + *c = gchar_cursor(); + if (*c != NUL) { // If 'ignorecase' and 'smartcase' are set and the // command line has no uppercase characters, convert // the character to lowercase @@ -588,16 +603,14 @@ static int may_add_char_to_search(int firstc, int *c, incsearch_state_T *s) && !pat_has_uppercase(ccline.cmdbuff + skiplen)) { *c = mb_tolower(*c); } - if (*c != NUL) { - if (*c == firstc - || vim_strchr((char_u *)(p_magic ? "\\~^$.*[" : "\\^$"), *c) - != NULL) { - // put a backslash before special characters - stuffcharReadbuff(*c); - *c = '\\'; - } - return FAIL; + if (*c == firstc + || vim_strchr((char_u *)(p_magic ? "\\~^$.*[" : "\\^$"), *c) + != NULL) { + // put a backslash before special characters + stuffcharReadbuff(*c); + *c = '\\'; } + return FAIL; } } return OK; @@ -1444,6 +1457,7 @@ static int command_line_execute(VimState *state, int key) static int may_do_command_line_next_incsearch(int firstc, long count, incsearch_state_T *s, bool next_match) + FUNC_ATTR_NONNULL_ALL { int skiplen, patlen; @@ -1536,7 +1550,9 @@ static int may_do_command_line_next_incsearch(int firstc, long count, highlight_match = true; save_viewstate(&s->old_viewstate); update_screen(NOT_VALID); + highlight_match = false; redrawcmdline(); + curwin->w_cursor = s->match_end; } else { vim_beep(BO_ERROR); } @@ -6457,12 +6473,15 @@ static int open_cmdwin(void) // Save the command line info, can be used recursively. save_cmdline(&save_ccline); - /* No Ex mode here! */ + // No Ex mode here! exmode_active = 0; State = NORMAL; setmouse(); + // Reset here so it can be set by a CmdWinEnter autocommand. + cmdwin_result = 0; + // Trigger CmdwinEnter autocommands. typestr[0] = (char_u)cmdwin_type; typestr[1] = NUL; @@ -6478,7 +6497,6 @@ static int open_cmdwin(void) /* * Call the main loop until <CR> or CTRL-C is typed. */ - cmdwin_result = 0; normal_enter(true, false); RedrawingDisabled = i; diff --git a/src/nvim/extmark.c b/src/nvim/extmark.c index 3a04908ccb..0de396fd1f 100644 --- a/src/nvim/extmark.c +++ b/src/nvim/extmark.c @@ -568,8 +568,18 @@ void extmark_splice(buf_T *buf, int new_row, colnr_T new_col, bcount_t new_byte, ExtmarkOp undo) { - long offset = ml_find_line_or_offset(buf, start_row+1, NULL, true); - extmark_splice_impl(buf, start_row, start_col, offset+start_col, + long offset = ml_find_line_or_offset(buf, start_row + 1, NULL, true); + + // On empty buffers, when editing the first line, the line is buffered, + // causing offset to be < 0. While the buffer is not actually empty, the + // buffered line has not been flushed (and should not be) yet, so the call is + // valid but an edge case. + // + // TODO(vigoux): maybe the is a better way of testing that ? + if (offset < 0 && buf->b_ml.ml_chunksize == NULL) { + offset = 0; + } + extmark_splice_impl(buf, start_row, start_col, offset + start_col, old_row, old_col, old_byte, new_row, new_col, new_byte, undo); } @@ -773,7 +783,7 @@ void bufhl_add_hl_pos_offset(buf_T *buf, } (void)extmark_set(buf, (uint64_t)src_id, 0, (int)lnum-1, hl_start, (int)lnum-1+end_off, hl_end, - decor, kExtmarkUndo); + decor, kExtmarkNoUndo); } } @@ -844,8 +854,15 @@ VirtText *extmark_find_virttext(buf_T *buf, int row, uint64_t ns_id) bool decorations_redraw_reset(buf_T *buf, DecorationRedrawState *state) { state->row = -1; + for (size_t i = 0; i < kv_size(state->active); i++) { + HlRange item = kv_A(state->active, i); + if (item.virt_text_owned) { + clear_virttext(item.virt_text); + xfree(item.virt_text); + } + } kv_size(state->active) = 0; - return buf->b_extmark_index || buf->b_luahl; + return buf->b_extmark_index; } @@ -889,10 +906,10 @@ bool decorations_redraw_start(buf_T *buf, int top_row, HlRange range; if (mark.id&MARKTREE_END_FLAG) { range = (HlRange){ altpos.row, altpos.col, mark.row, mark.col, - attr_id, vt }; + attr_id, vt, false }; } else { range = (HlRange){ mark.row, mark.col, altpos.row, - altpos.col, attr_id, vt }; + altpos.col, attr_id, vt, false }; } kv_push(state->active, range); @@ -957,7 +974,7 @@ int decorations_redraw_col(buf_T *buf, int col, DecorationRedrawState *state) 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 })); + attr_id, vt, false })); next_mark: marktree_itr_next(buf->b_marktree, state->itr); @@ -991,6 +1008,9 @@ next_mark: } if (keep) { kv_A(state->active, j++) = kv_A(state->active, i); + } else if (item.virt_text_owned) { + clear_virttext(item.virt_text); + xfree(item.virt_text); } } kv_size(state->active) = j; diff --git a/src/nvim/extmark.h b/src/nvim/extmark.h index 534e97a7f4..d394d4d806 100644 --- a/src/nvim/extmark.h +++ b/src/nvim/extmark.h @@ -85,6 +85,7 @@ typedef struct { int end_col; int attr_id; VirtText *virt_text; + bool virt_text_owned; } HlRange; typedef struct { diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index 286f2b4fca..1b4a89fb6c 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -210,7 +210,8 @@ void filemess(buf_T *buf, char_u *name, char_u *s, int attr) if (msg_silent != 0) { return; } - add_quoted_fname((char *)IObuff, IOSIZE - 80, buf, (const char *)name); + add_quoted_fname((char *)IObuff, IOSIZE - 100, buf, (const char *)name); + // Avoid an over-long translation to cause trouble. xstrlcat((char *)IObuff, (const char *)s, IOSIZE); // For the first message may have to start a new line. // For further ones overwrite the previous one, reset msg_scroll before @@ -349,6 +350,7 @@ readfile( char_u *old_b_fname; int using_b_ffname; int using_b_fname; + static char *msg_is_a_directory = N_("is a directory"); au_did_filetype = false; // reset before triggering any autocommands @@ -443,21 +445,31 @@ readfile( else msg_scroll = TRUE; /* don't overwrite previous file message */ - /* - * If the name is too long we might crash further on, quit here. - */ + // If the name is too long we might crash further on, quit here. if (fname != NULL && *fname != NUL) { - if (STRLEN(fname) >= MAXPATHL) { + size_t namelen = STRLEN(fname); + + // If the name is too long we might crash further on, quit here. + if (namelen >= MAXPATHL) { filemess(curbuf, fname, (char_u *)_("Illegal file name"), 0); msg_end(); msg_scroll = msg_save; return FAIL; } + + // If the name ends in a path separator, we can't open it. Check here, + // because reading the file may actually work, but then creating the + // swap file may destroy it! Reported on MS-DOS and Win 95. + if (after_pathsep((const char *)fname, (const char *)(fname + namelen))) { + filemess(curbuf, fname, (char_u *)_(msg_is_a_directory), 0); + msg_end(); + msg_scroll = msg_save; + return FAIL; + } } if (!read_buffer && !read_stdin && !read_fifo) { perm = os_getperm((const char *)fname); -#ifdef UNIX // On Unix it is possible to read a directory, so we have to // check for it before os_open(). if (perm >= 0 && !S_ISREG(perm) // not a regular file ... @@ -473,7 +485,7 @@ readfile( # endif ) { if (S_ISDIR(perm)) { - filemess(curbuf, fname, (char_u *)_("is a directory"), 0); + filemess(curbuf, fname, (char_u *)_(msg_is_a_directory), 0); } else { filemess(curbuf, fname, (char_u *)_("is not a file"), 0); } @@ -481,7 +493,6 @@ readfile( msg_scroll = msg_save; return S_ISDIR(perm) ? NOTDONE : FAIL; } -#endif } /* Set default or forced 'fileformat' and 'binary'. */ @@ -540,13 +551,6 @@ readfile( if (fd < 0) { // cannot open at all msg_scroll = msg_save; -#ifndef UNIX - // On non-unix systems we can't open a directory, check here. - if (os_isdir(fname)) { - filemess(curbuf, sfname, (char_u *)_("is a directory"), 0); - curbuf->b_p_ro = true; // must use "w!" now - } else { -#endif if (!newfile) { return FAIL; } @@ -604,9 +608,6 @@ readfile( return FAIL; } -#ifndef UNIX - } -#endif /* * Only set the 'ro' flag for readonly files the first time they are diff --git a/src/nvim/fold.c b/src/nvim/fold.c index 9994ad3ea8..197aedabec 100644 --- a/src/nvim/fold.c +++ b/src/nvim/fold.c @@ -153,14 +153,22 @@ bool hasFolding(linenr_T lnum, linenr_T *firstp, linenr_T *lastp) return hasFoldingWin(curwin, lnum, firstp, lastp, true, NULL); } -/* hasFoldingWin() {{{2 */ +// hasFoldingWin() {{{2 +/// Search folds starting at lnum +/// @param lnum first line to search +/// @param[out] first first line of fold containing lnum +/// @param[out] lastp last line with a fold +/// @param cache when true: use cached values of window +/// @param[out] infop where to store fold info +/// +/// @return true if range contains folds bool hasFoldingWin( win_T *const win, const linenr_T lnum, linenr_T *const firstp, linenr_T *const lastp, - const bool cache, // when true: use cached values of window - foldinfo_T *const infop // where to store fold info + const bool cache, + foldinfo_T *const infop ) { bool had_folded = false; @@ -280,26 +288,31 @@ int foldLevel(linenr_T lnum) // Return false if line is not folded. bool lineFolded(win_T *const win, const linenr_T lnum) { - return foldedCount(win, lnum, NULL) != 0; + return fold_info(win, lnum).fi_lines != 0; } -/* foldedCount() {{{2 */ -/* - * Count the number of lines that are folded at line number "lnum". - * Normally "lnum" is the first line of a possible fold, and the returned - * number is the number of lines in the fold. - * Doesn't use caching from the displayed window. - * Returns number of folded lines from "lnum", or 0 if line is not folded. - * When "infop" is not NULL, fills *infop with the fold level info. - */ -long foldedCount(win_T *win, linenr_T lnum, foldinfo_T *infop) +/// fold_info() {{{2 +/// +/// Count the number of lines that are folded at line number "lnum". +/// Normally "lnum" is the first line of a possible fold, and the returned +/// number is the number of lines in the fold. +/// Doesn't use caching from the displayed window. +/// +/// @return with the fold level info. +/// fi_lines = number of folded lines from "lnum", +/// or 0 if line is not folded. +foldinfo_T fold_info(win_T *win, linenr_T lnum) { + foldinfo_T info; linenr_T last; - if (hasFoldingWin(win, lnum, NULL, &last, false, infop)) { - return (long)(last - lnum + 1); + if (hasFoldingWin(win, lnum, NULL, &last, false, &info)) { + info.fi_lines = (long)(last - lnum + 1); + } else { + info.fi_lines = 0; } - return 0; + + return info; } /* foldmethodIsManual() {{{2 */ @@ -356,23 +369,21 @@ int foldmethodIsDiff(win_T *wp) return wp->w_p_fdm[0] == 'd'; } -/* closeFold() {{{2 */ -/* - * Close fold for current window at line "lnum". - * Repeat "count" times. - */ -void closeFold(linenr_T lnum, long count) +// closeFold() {{{2 +/// Close fold for current window at line "lnum". +/// Repeat "count" times. +void closeFold(pos_T pos, long count) { - setFoldRepeat(lnum, count, FALSE); + setFoldRepeat(pos, count, false); } /* closeFoldRecurse() {{{2 */ /* * Close fold for current window at line "lnum" recursively. */ -void closeFoldRecurse(linenr_T lnum) +void closeFoldRecurse(pos_T pos) { - (void)setManualFold(lnum, FALSE, TRUE, NULL); + (void)setManualFold(pos, false, true, NULL); } /* opFoldRange() {{{2 */ @@ -382,28 +393,32 @@ void closeFoldRecurse(linenr_T lnum) */ void opFoldRange( - linenr_T first, - linenr_T last, + pos_T firstpos, + pos_T lastpos, int opening, // TRUE to open, FALSE to close int recurse, // TRUE to do it recursively int had_visual // TRUE when Visual selection used ) { - int done = DONE_NOTHING; /* avoid error messages */ + int done = DONE_NOTHING; // avoid error messages + linenr_T first = firstpos.lnum; + linenr_T last = lastpos.lnum; linenr_T lnum; linenr_T lnum_next; for (lnum = first; lnum <= last; lnum = lnum_next + 1) { + pos_T temp = { lnum, 0, 0 }; lnum_next = lnum; /* Opening one level only: next fold to open is after the one going to * be opened. */ if (opening && !recurse) (void)hasFolding(lnum, NULL, &lnum_next); - (void)setManualFold(lnum, opening, recurse, &done); - /* Closing one level only: next line to close a fold is after just - * closed fold. */ - if (!opening && !recurse) + (void)setManualFold(temp, opening, recurse, &done); + // Closing one level only: next line to close a fold is after just + // closed fold. + if (!opening && !recurse) { (void)hasFolding(lnum, NULL, &lnum_next); + } } if (done == DONE_NOTHING) EMSG(_(e_nofold)); @@ -417,18 +432,18 @@ opFoldRange( * Open fold for current window at line "lnum". * Repeat "count" times. */ -void openFold(linenr_T lnum, long count) +void openFold(pos_T pos, long count) { - setFoldRepeat(lnum, count, TRUE); + setFoldRepeat(pos, count, true); } /* openFoldRecurse() {{{2 */ /* * Open fold for current window at line "lnum" recursively. */ -void openFoldRecurse(linenr_T lnum) +void openFoldRecurse(pos_T pos) { - (void)setManualFold(lnum, TRUE, TRUE, NULL); + (void)setManualFold(pos, true, true, NULL); } /* foldOpenCursor() {{{2 */ @@ -443,9 +458,10 @@ void foldOpenCursor(void) if (hasAnyFolding(curwin)) for (;; ) { done = DONE_NOTHING; - (void)setManualFold(curwin->w_cursor.lnum, TRUE, FALSE, &done); - if (!(done & DONE_ACTION)) + (void)setManualFold(curwin->w_cursor, true, false, &done); + if (!(done & DONE_ACTION)) { break; + } } } @@ -542,21 +558,21 @@ int foldManualAllowed(int create) // foldCreate() {{{2 /// Create a fold from line "start" to line "end" (inclusive) in the current /// window. -void foldCreate(win_T *wp, linenr_T start, linenr_T end) +void foldCreate(win_T *wp, pos_T start, pos_T end) { fold_T *fp; garray_T *gap; garray_T fold_ga; - int i, j; + int i; int cont; int use_level = FALSE; int closed = FALSE; int level = 0; - linenr_T start_rel = start; - linenr_T end_rel = end; + pos_T start_rel = start; + pos_T end_rel = end; - if (start > end) { - /* reverse the range */ + if (start.lnum > end.lnum) { + // reverse the range end = start_rel; start = end_rel; start_rel = start; @@ -577,14 +593,14 @@ void foldCreate(win_T *wp, linenr_T start, linenr_T end) i = 0; } else { for (;;) { - if (!foldFind(gap, start_rel, &fp)) { + if (!foldFind(gap, start_rel.lnum, &fp)) { break; } - if (fp->fd_top + fp->fd_len > end_rel) { + if (fp->fd_top + fp->fd_len > end_rel.lnum) { // 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; + start_rel.lnum -= fp->fd_top; + end_rel.lnum -= fp->fd_top; if (use_level || fp->fd_flags == FD_LEVEL) { use_level = true; if (level >= wp->w_p_fdl) { @@ -608,30 +624,35 @@ void foldCreate(win_T *wp, linenr_T start, linenr_T end) fp = (fold_T *)gap->ga_data + i; ga_init(&fold_ga, (int)sizeof(fold_T), 10); - /* Count number of folds that will be contained in the new fold. */ - for (cont = 0; i + cont < gap->ga_len; ++cont) - if (fp[cont].fd_top > end_rel) + // Count number of folds that will be contained in the new fold. + for (cont = 0; i + cont < gap->ga_len; cont++) { + if (fp[cont].fd_top > end_rel.lnum) { break; + } + } if (cont > 0) { ga_grow(&fold_ga, cont); /* If the first fold starts before the new fold, let the new fold * start there. Otherwise the existing fold would change. */ - if (start_rel > fp->fd_top) - start_rel = fp->fd_top; - - /* When last contained fold isn't completely contained, adjust end - * of new fold. */ - if (end_rel < fp[cont - 1].fd_top + fp[cont - 1].fd_len - 1) - end_rel = fp[cont - 1].fd_top + fp[cont - 1].fd_len - 1; - /* Move contained folds to inside new fold. */ + if (start_rel.lnum > fp->fd_top) { + start_rel.lnum = fp->fd_top; + } + + // When last contained fold isn't completely contained, adjust end + // of new fold. + if (end_rel.lnum < fp[cont - 1].fd_top + fp[cont - 1].fd_len - 1) { + end_rel.lnum = fp[cont - 1].fd_top + fp[cont - 1].fd_len - 1; + } + // Move contained folds to inside new fold memmove(fold_ga.ga_data, fp, sizeof(fold_T) * (size_t)cont); fold_ga.ga_len += cont; i += cont; /* Adjust line numbers in contained folds to be relative to the * new fold. */ - for (j = 0; j < cont; ++j) - ((fold_T *)fold_ga.ga_data)[j].fd_top -= start_rel; + for (int j = 0; j < cont; j++) { + ((fold_T *)fold_ga.ga_data)[j].fd_top -= start_rel.lnum; + } } /* Move remaining entries to after the new fold. */ if (i < gap->ga_len) @@ -641,8 +662,8 @@ void foldCreate(win_T *wp, linenr_T start, linenr_T end) /* insert new fold */ fp->fd_nested = fold_ga; - fp->fd_top = start_rel; - fp->fd_len = end_rel - start_rel + 1; + fp->fd_top = start_rel.lnum; + fp->fd_len = end_rel.lnum - start_rel.lnum + 1; /* We want the new fold to be closed. If it would remain open because * of using 'foldlevel', need to adjust fd_flags of containing folds. @@ -771,7 +792,7 @@ void deleteFold( */ void clearFolding(win_T *win) { - deleteFoldRecurse(&win->w_folds); + deleteFoldRecurse(win->w_buffer, &win->w_folds); win->w_foldinvalid = false; } @@ -1143,14 +1164,14 @@ static void checkupdate(win_T *wp) * Open or close fold for current window at line "lnum". * Repeat "count" times. */ -static void setFoldRepeat(linenr_T lnum, long count, int do_open) +static void setFoldRepeat(pos_T pos, long count, int do_open) { int done; long n; for (n = 0; n < count; ++n) { done = DONE_NOTHING; - (void)setManualFold(lnum, do_open, FALSE, &done); + (void)setManualFold(pos, do_open, false, &done); if (!(done & DONE_ACTION)) { /* Only give an error message when no fold could be opened. */ if (n == 0 && !(done & DONE_FOLD)) @@ -1167,12 +1188,13 @@ static void setFoldRepeat(linenr_T lnum, long count, int do_open) */ static linenr_T setManualFold( - linenr_T lnum, + pos_T pos, int opening, // TRUE when opening, FALSE when closing int recurse, // TRUE when closing/opening recursive int *donep ) { + linenr_T lnum = pos.lnum; if (foldmethodIsDiff(curwin) && curwin->w_p_scb) { linenr_T dlnum; @@ -1326,7 +1348,7 @@ static void deleteFoldEntry(win_T *const wp, garray_T *const gap, const int idx, fold_T *fp = (fold_T *)gap->ga_data + idx; if (recursive || GA_EMPTY(&fp->fd_nested)) { // recursively delete the contained folds - deleteFoldRecurse(&fp->fd_nested); + deleteFoldRecurse(wp->w_buffer, &fp->fd_nested); gap->ga_len--; if (idx < gap->ga_len) { memmove(fp, fp + 1, sizeof(*fp) * (size_t)(gap->ga_len - idx)); @@ -1368,9 +1390,9 @@ static void deleteFoldEntry(win_T *const wp, garray_T *const gap, const int idx, /* * Delete nested folds in a fold. */ -void deleteFoldRecurse(garray_T *gap) +void deleteFoldRecurse(buf_T *bp, garray_T *gap) { -# define DELETE_FOLD_NESTED(fd) deleteFoldRecurse(&((fd)->fd_nested)) +# define DELETE_FOLD_NESTED(fd) deleteFoldRecurse(bp, &((fd)->fd_nested)) GA_DEEP_CLEAR(gap, fold_T, DELETE_FOLD_NESTED); } @@ -1610,7 +1632,7 @@ static void setSmallMaybe(garray_T *gap) * Create a fold from line "start" to line "end" (inclusive) in the current * window by adding markers. */ -static void foldCreateMarkers(win_T *wp, linenr_T start, linenr_T end) +static void foldCreateMarkers(win_T *wp, pos_T start, pos_T end) { buf_T *buf = wp->w_buffer; if (!MODIFIABLE(buf)) { @@ -1625,13 +1647,13 @@ static void foldCreateMarkers(win_T *wp, linenr_T start, linenr_T end) /* Update both changes here, to avoid all folds after the start are * changed when the start marker is inserted and the end isn't. */ // TODO(teto): pass the buffer - changed_lines(start, (colnr_T)0, end, 0L, false); + changed_lines(start.lnum, (colnr_T)0, end.lnum, 0L, false); // Note: foldAddMarker() may not actually change start and/or end if // u_save() is unable to save the buffer line, but we send the // nvim_buf_lines_event anyway since it won't do any harm. - int64_t num_changed = 1 + end - start; - buf_updates_send_changes(buf, start, num_changed, num_changed, true); + int64_t num_changed = 1 + end.lnum - start.lnum; + buf_updates_send_changes(buf, start.lnum, num_changed, num_changed, true); } /* foldAddMarker() {{{2 */ @@ -1639,13 +1661,14 @@ static void foldCreateMarkers(win_T *wp, linenr_T start, linenr_T end) * Add "marker[markerlen]" in 'commentstring' to line "lnum". */ static void foldAddMarker( - buf_T *buf, linenr_T lnum, const char_u *marker, size_t markerlen) + buf_T *buf, pos_T pos, const char_u *marker, size_t markerlen) { char_u *cms = buf->b_p_cms; char_u *line; char_u *newline; char_u *p = (char_u *)strstr((char *)buf->b_p_cms, "%s"); bool line_is_comment = false; + linenr_T lnum = pos.lnum; // Allocate a new line: old-line + 'cms'-start + marker + 'cms'-end line = ml_get_buf(buf, lnum, false); @@ -1751,11 +1774,16 @@ static void foldDelMarker( } // get_foldtext() {{{2 -/// Return the text for a closed fold at line "lnum", with last line "lnume". -/// When 'foldtext' isn't set puts the result in "buf[FOLD_TEXT_LEN]". +/// Generates text to display +/// +/// @param buf allocated memory of length FOLD_TEXT_LEN. Used when 'foldtext' +/// isn't set puts the result in "buf[FOLD_TEXT_LEN]". +/// @param at line "lnum", with last line "lnume". +/// @return the text for a closed fold +/// /// Otherwise the result is in allocated memory. char_u *get_foldtext(win_T *wp, linenr_T lnum, linenr_T lnume, - foldinfo_T *foldinfo, char_u *buf) + foldinfo_T foldinfo, char_u *buf) FUNC_ATTR_NONNULL_ARG(1) { char_u *text = NULL; @@ -1783,11 +1811,12 @@ char_u *get_foldtext(win_T *wp, linenr_T lnum, linenr_T lnume, set_vim_var_nr(VV_FOLDSTART, (varnumber_T) lnum); set_vim_var_nr(VV_FOLDEND, (varnumber_T) lnume); - /* Set "v:folddashes" to a string of "level" dashes. */ - /* Set "v:foldlevel" to "level". */ - level = foldinfo->fi_level; - if (level > (int)sizeof(dashes) - 1) + // Set "v:folddashes" to a string of "level" dashes. + // Set "v:foldlevel" to "level". + level = foldinfo.fi_level; + if (level > (int)sizeof(dashes) - 1) { level = (int)sizeof(dashes) - 1; + } memset(dashes, '-', (size_t)level); dashes[level] = NUL; set_vim_var_string(VV_FOLDDASHES, dashes, -1); diff --git a/src/nvim/fold.h b/src/nvim/fold.h index f35b328fb1..95c4b0c1dc 100644 --- a/src/nvim/fold.h +++ b/src/nvim/fold.h @@ -18,6 +18,7 @@ typedef struct foldinfo { other fields are invalid */ int fi_low_level; /* lowest fold level that starts in the same line */ + long fi_lines; } foldinfo_T; diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c index c35398cd8d..cbd9582f8b 100644 --- a/src/nvim/getchar.c +++ b/src/nvim/getchar.c @@ -1528,6 +1528,17 @@ int vgetc(void) c = utf_ptr2char(buf); } + // If mappings are enabled (i.e., not Ctrl-v) and the user directly typed + // something with a meta- or alt- modifier that was not mapped, interpret + // <M-x> as <Esc>x rather than as an unbound meta keypress. #8213 + if (!no_mapping && KeyTyped + && (mod_mask == MOD_MASK_ALT || mod_mask == MOD_MASK_META)) { + mod_mask = 0; + stuffcharReadbuff(c); + u_sync(false); + c = ESC; + } + break; } } @@ -2044,14 +2055,19 @@ static int vgetorpeek(bool advance) */ if (mp->m_expr) { int save_vgetc_busy = vgetc_busy; + const bool save_may_garbage_collect = may_garbage_collect; vgetc_busy = 0; + may_garbage_collect = false; + save_m_keys = vim_strsave(mp->m_keys); save_m_str = vim_strsave(mp->m_str); s = eval_map_expr(save_m_str, NUL); vgetc_busy = save_vgetc_busy; - } else + may_garbage_collect = save_may_garbage_collect; + } else { s = mp->m_str; + } /* * Insert the 'to' part in the typebuf.tb_buf. diff --git a/src/nvim/globals.h b/src/nvim/globals.h index 205be4b811..ddb69fc567 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -405,6 +405,12 @@ EXTERN int sys_menu INIT(= false); // ('lines' and 'rows') must not be changed. EXTERN int updating_screen INIT(= 0); +EXTERN bool luahl_active INIT(= false); +EXTERN LuaRef luahl_start INIT(= LUA_NOREF); +EXTERN LuaRef luahl_win INIT(= LUA_NOREF); +EXTERN LuaRef luahl_line INIT(= LUA_NOREF); +EXTERN LuaRef luahl_end INIT(= LUA_NOREF); + // All windows are linked in a list. firstwin points to the first entry, // lastwin to the last entry (can be the same as firstwin) and curwin to the // currently active window. diff --git a/src/nvim/log.h b/src/nvim/log.h index 17ff095473..f2e74df031 100644 --- a/src/nvim/log.h +++ b/src/nvim/log.h @@ -5,6 +5,17 @@ #include <stdbool.h> #include "auto/config.h" +#include "nvim/macros.h" + +// USDT probes. Example invokation: +// NVIM_PROBE(nvim_foo_bar, 1, string.data); +#if defined(HAVE_SYS_SDT_H) +#include <sys/sdt.h> // NOLINT +#define NVIM_PROBE(name, n, ...) STAP_PROBE##n(neovim, name, __VA_ARGS__) +#else +#define NVIM_PROBE(name, n, ...) +#endif + #define DEBUG_LOG_LEVEL 0 #define INFO_LOG_LEVEL 1 @@ -68,6 +79,10 @@ # define LOG_CALLSTACK_TO_FILE(fp) log_callstack_to_file(fp, __func__, __LINE__) #endif +#if NVIM_HAS_INCLUDE("sanitizer/asan_interface.h") +# include "sanitizer/asan_interface.h" +#endif + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "log.h.generated.h" #endif diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index 7722f9cdc3..5c665920b5 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -299,45 +299,66 @@ static int nlua_wait(lua_State *lstate) return luaL_error(lstate, "timeout must be > 0"); } - // Check if condition can be called. - bool is_function = (lua_type(lstate, 2) == LUA_TFUNCTION); + int lua_top = lua_gettop(lstate); - // Check if condition is callable table - if (!is_function && luaL_getmetafield(lstate, 2, "__call") != 0) { - is_function = (lua_type(lstate, -1) == LUA_TFUNCTION); - lua_pop(lstate, 1); - } + // Check if condition can be called. + bool is_function = false; + if (lua_top >= 2 && !lua_isnil(lstate, 2)) { + is_function = (lua_type(lstate, 2) == LUA_TFUNCTION); + + // Check if condition is callable table + if (!is_function && luaL_getmetafield(lstate, 2, "__call") != 0) { + is_function = (lua_type(lstate, -1) == LUA_TFUNCTION); + lua_pop(lstate, 1); + } - if (!is_function) { - lua_pushliteral(lstate, "vim.wait: condition must be a function"); - return lua_error(lstate); + if (!is_function) { + lua_pushliteral( + lstate, + "vim.wait: if passed, condition must be a function"); + return lua_error(lstate); + } } intptr_t interval = 200; - if (lua_gettop(lstate) >= 3) { + if (lua_top >= 3 && !lua_isnil(lstate, 3)) { interval = luaL_checkinteger(lstate, 3); if (interval < 0) { return luaL_error(lstate, "interval must be > 0"); } } + bool fast_only = false; + if (lua_top >= 4) { + fast_only = lua_toboolean(lstate, 4); + } + + MultiQueue *loop_events = fast_only || in_fast_callback > 0 + ? main_loop.fast_events : main_loop.events; + TimeWatcher *tw = xmalloc(sizeof(TimeWatcher)); // Start dummy timer. time_watcher_init(&main_loop, tw, NULL); - tw->events = main_loop.events; + tw->events = loop_events; tw->blockable = true; - time_watcher_start(tw, dummy_timer_due_cb, - (uint64_t)interval, (uint64_t)interval); + time_watcher_start( + tw, + dummy_timer_due_cb, + (uint64_t)interval, + (uint64_t)interval); int pcall_status = 0; bool callback_result = false; LOOP_PROCESS_EVENTS_UNTIL( &main_loop, - main_loop.events, + loop_events, (int)timeout, - nlua_wait_condition(lstate, &pcall_status, &callback_result) || got_int); + is_function ? nlua_wait_condition( + lstate, + &pcall_status, + &callback_result) : false || got_int); // Stop dummy timer time_watcher_stop(tw); diff --git a/src/nvim/lua/treesitter.c b/src/nvim/lua/treesitter.c index 8be4b6f376..33d9772bec 100644 --- a/src/nvim/lua/treesitter.c +++ b/src/nvim/lua/treesitter.c @@ -39,7 +39,7 @@ typedef struct { static struct luaL_Reg parser_meta[] = { { "__gc", parser_gc }, { "__tostring", parser_tostring }, - { "parse_buf", parser_parse_buf }, + { "parse", parser_parse }, { "edit", parser_edit }, { "tree", parser_tree }, { "set_included_ranges", parser_set_ranges }, @@ -174,6 +174,14 @@ int tslua_add_language(lua_State *L) return luaL_error(L, "Failed to load parser: internal error"); } + uint32_t lang_version = ts_language_version(lang); + if (lang_version < TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION) { + return luaL_error( + L, + "ABI version mismatch : expected %" PRIu32 ", found %" PRIu32, + TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION, lang_version); + } + pmap_put(cstr_t)(langs, xstrdup(lang_name), lang); lua_pushboolean(L, true); @@ -306,22 +314,44 @@ static const char *input_cb(void *payload, uint32_t byte_index, #undef BUFSIZE } -static int parser_parse_buf(lua_State *L) +static int parser_parse(lua_State *L) { TSLua_parser *p = parser_check(L); if (!p) { return 0; } - long bufnr = lua_tointeger(L, 2); - buf_T *buf = handle_get_buffer(bufnr); + TSTree *new_tree; + size_t len; + const char *str; + long bufnr; + buf_T *buf; + TSInput input; + + // This switch is necessary because of the behavior of lua_isstring, that + // consider numbers as strings... + switch (lua_type(L, 2)) { + case LUA_TSTRING: + str = lua_tolstring(L, 2, &len); + new_tree = ts_parser_parse_string(p->parser, p->tree, str, len); + break; + + case LUA_TNUMBER: + bufnr = lua_tointeger(L, 2); + buf = handle_get_buffer(bufnr); + + if (!buf) { + return luaL_error(L, "invalid buffer handle: %d", bufnr); + } - if (!buf) { - return luaL_error(L, "invalid buffer handle: %d", bufnr); - } + input = (TSInput){ (void *)buf, input_cb, TSInputEncodingUTF8 }; + new_tree = ts_parser_parse(p->parser, p->tree, input); - TSInput input = { (void *)buf, input_cb, TSInputEncodingUTF8 }; - TSTree *new_tree = ts_parser_parse(p->parser, p->tree, input); + break; + + default: + return luaL_error(L, "invalid argument to parser:parse()"); + } uint32_t n_ranges = 0; TSRange *changed = p->tree ? ts_tree_get_changed_ranges(p->tree, new_tree, diff --git a/src/nvim/macros.h b/src/nvim/macros.h index 0bbaa87aba..07dcb4a8e8 100644 --- a/src/nvim/macros.h +++ b/src/nvim/macros.h @@ -152,6 +152,12 @@ #define STR_(x) #x #define STR(x) STR_(x) +#ifndef __has_include +# define NVIM_HAS_INCLUDE(x) 0 +#else +# define NVIM_HAS_INCLUDE __has_include +#endif + #ifndef __has_attribute # define NVIM_HAS_ATTRIBUTE(x) 0 #elif defined(__clang__) && __clang__ == 1 \ diff --git a/src/nvim/main.c b/src/nvim/main.c index 1374c5eb5d..a22df9cc69 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -7,6 +7,8 @@ #include <string.h> #include <stdbool.h> +#include <lua.h> +#include <lauxlib.h> #include <msgpack.h> #include "nvim/ascii.h" diff --git a/src/nvim/memline.c b/src/nvim/memline.c index f9390bcb88..2a75f13cc2 100644 --- a/src/nvim/memline.c +++ b/src/nvim/memline.c @@ -1818,6 +1818,7 @@ ml_get_buf ( linenr_T lnum, bool will_change // line will be changed ) + FUNC_ATTR_NONNULL_ALL { bhdr_T *hp; DATA_BL *dp; diff --git a/src/nvim/move.c b/src/nvim/move.c index 8a8a639a52..e2a304efa5 100644 --- a/src/nvim/move.c +++ b/src/nvim/move.c @@ -641,7 +641,7 @@ void validate_virtcol_win(win_T *wp) /* * Validate curwin->w_cline_height only. */ -static void validate_cheight(void) +void validate_cheight(void) { check_cursor_moved(curwin); if (!(curwin->w_valid & VALID_CHEIGHT)) { @@ -943,6 +943,9 @@ void curs_columns( redraw_later(SOME_VALID); } + // now w_leftcol is valid, avoid check_cursor_moved() thinking otherwise + curwin->w_valid_leftcol = curwin->w_leftcol; + curwin->w_valid |= VALID_WCOL|VALID_WROW|VALID_VIRTCOL; } diff --git a/src/nvim/normal.c b/src/nvim/normal.c index 968cfde388..a51aa0dc07 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -1977,20 +1977,20 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank) case OP_FOLD: VIsual_reselect = false; // don't reselect now - foldCreate(curwin, oap->start.lnum, oap->end.lnum); + foldCreate(curwin, oap->start, oap->end); break; case OP_FOLDOPEN: case OP_FOLDOPENREC: case OP_FOLDCLOSE: case OP_FOLDCLOSEREC: - VIsual_reselect = false; /* don't reselect now */ - opFoldRange(oap->start.lnum, oap->end.lnum, - oap->op_type == OP_FOLDOPEN - || oap->op_type == OP_FOLDOPENREC, - oap->op_type == OP_FOLDOPENREC - || oap->op_type == OP_FOLDCLOSEREC, - oap->is_VIsual); + VIsual_reselect = false; // don't reselect now + opFoldRange(oap->start, oap->end, + oap->op_type == OP_FOLDOPEN + || oap->op_type == OP_FOLDOPENREC, + oap->op_type == OP_FOLDOPENREC + || oap->op_type == OP_FOLDCLOSEREC, + oap->is_VIsual); break; case OP_FOLDDEL: @@ -2483,7 +2483,7 @@ do_mouse ( typval_T rettv; int doesrange; (void)call_func((char_u *)tab_page_click_defs[mouse_col].func, - (int)strlen(tab_page_click_defs[mouse_col].func), + -1, &rettv, ARRAY_SIZE(argv), argv, NULL, curwin->w_cursor.lnum, curwin->w_cursor.lnum, &doesrange, true, NULL, NULL); @@ -2590,14 +2590,16 @@ do_mouse ( && !is_drag && (jump_flags & (MOUSE_FOLD_CLOSE | MOUSE_FOLD_OPEN)) && which_button == MOUSE_LEFT) { - /* open or close a fold at this line */ - if (jump_flags & MOUSE_FOLD_OPEN) - openFold(curwin->w_cursor.lnum, 1L); - else - closeFold(curwin->w_cursor.lnum, 1L); - /* don't move the cursor if still in the same window */ - if (curwin == old_curwin) + // open or close a fold at this line + if (jump_flags & MOUSE_FOLD_OPEN) { + openFold(curwin->w_cursor, 1L); + } else { + closeFold(curwin->w_cursor, 1L); + } + // don't move the cursor if still in the same window + if (curwin == old_curwin) { curwin->w_cursor = save_cursor; + } } @@ -4393,51 +4395,55 @@ dozet: case 'i': curwin->w_p_fen = !curwin->w_p_fen; break; - /* "za": open closed fold or close open fold at cursor */ - case 'a': if (hasFolding(curwin->w_cursor.lnum, NULL, NULL)) - openFold(curwin->w_cursor.lnum, cap->count1); - else { - closeFold(curwin->w_cursor.lnum, cap->count1); + // "za": open closed fold or close open fold at cursor + case 'a': if (hasFolding(curwin->w_cursor.lnum, NULL, NULL)) { + openFold(curwin->w_cursor, cap->count1); + } else { + closeFold(curwin->w_cursor, cap->count1); curwin->w_p_fen = true; } break; - /* "zA": open fold at cursor recursively */ - case 'A': if (hasFolding(curwin->w_cursor.lnum, NULL, NULL)) - openFoldRecurse(curwin->w_cursor.lnum); - else { - closeFoldRecurse(curwin->w_cursor.lnum); + // "zA": open fold at cursor recursively + case 'A': if (hasFolding(curwin->w_cursor.lnum, NULL, NULL)) { + openFoldRecurse(curwin->w_cursor); + } else { + closeFoldRecurse(curwin->w_cursor); curwin->w_p_fen = true; } break; - /* "zo": open fold at cursor or Visual area */ - case 'o': if (VIsual_active) + // "zo": open fold at cursor or Visual area + case 'o': if (VIsual_active) { nv_operator(cap); - else - openFold(curwin->w_cursor.lnum, cap->count1); + } else { + openFold(curwin->w_cursor, cap->count1); + } break; - /* "zO": open fold recursively */ - case 'O': if (VIsual_active) + // "zO": open fold recursively + case 'O': if (VIsual_active) { nv_operator(cap); - else - openFoldRecurse(curwin->w_cursor.lnum); + } else { + openFoldRecurse(curwin->w_cursor); + } break; - /* "zc": close fold at cursor or Visual area */ - case 'c': if (VIsual_active) + // "zc": close fold at cursor or Visual area + case 'c': if (VIsual_active) { nv_operator(cap); - else - closeFold(curwin->w_cursor.lnum, cap->count1); + } else { + closeFold(curwin->w_cursor, cap->count1); + } curwin->w_p_fen = true; break; - /* "zC": close fold recursively */ - case 'C': if (VIsual_active) + // "zC": close fold recursively + case 'C': if (VIsual_active) { nv_operator(cap); - else - closeFoldRecurse(curwin->w_cursor.lnum); + } else { + closeFoldRecurse(curwin->w_cursor); + } curwin->w_p_fen = true; break; diff --git a/src/nvim/ops.c b/src/nvim/ops.c index 1f55d2c315..8329daf5f1 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -119,7 +119,7 @@ static char opchars[][3] = { 'r', NUL, OPF_CHANGE }, // OP_REPLACE { 'I', NUL, OPF_CHANGE }, // OP_INSERT { 'A', NUL, OPF_CHANGE }, // OP_APPEND - { 'z', 'f', OPF_LINES }, // OP_FOLD + { 'z', 'f', 0 }, // OP_FOLD { 'z', 'o', OPF_LINES }, // OP_FOLDOPEN { 'z', 'O', OPF_LINES }, // OP_FOLDOPENREC { 'z', 'c', OPF_LINES }, // OP_FOLDCLOSE @@ -1544,10 +1544,10 @@ int op_delete(oparg_T *oap) oap->line_count = 0; // no lines deleted } else if (oap->motion_type == kMTLineWise) { if (oap->op_type == OP_CHANGE) { - /* Delete the lines except the first one. Temporarily move the - * cursor to the next line. Save the current line number, if the - * last line is deleted it may be changed. - */ + // Delete the lines except the first one. Temporarily move the + // cursor to the next line. Save the current line number, if the + // last line is deleted it may be changed. + if (oap->line_count > 1) { lnum = curwin->w_cursor.lnum; ++curwin->w_cursor.lnum; @@ -1560,12 +1560,21 @@ int op_delete(oparg_T *oap) beginline(BL_WHITE); // cursor on first non-white did_ai = true; // delete the indent when ESC hit ai_col = curwin->w_cursor.col; - } else - beginline(0); /* cursor in column 0 */ - truncate_line(FALSE); /* delete the rest of the line */ - /* leave cursor past last char in line */ - if (oap->line_count > 1) - u_clearline(); /* "U" command not possible after "2cc" */ + } else { + beginline(0); // cursor in column 0 + } + + int old_len = (int)STRLEN(ml_get(curwin->w_cursor.lnum)); + truncate_line(false); // delete the rest of the line + + extmark_splice_cols(curbuf, + (int)curwin->w_cursor.lnum-1, curwin->w_cursor.col, + old_len - curwin->w_cursor.col, 0, kExtmarkUndo); + + // leave cursor past last char in line + if (oap->line_count > 1) { + u_clearline(); // "U" command not possible after "2cc" + } } else { del_lines(oap->line_count, TRUE); beginline(BL_WHITE | BL_FIX); @@ -3100,6 +3109,9 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) for (i = 0; i < y_size; i++) { int spaces; char shortline; + // can just be 0 or 1, needed for blockwise paste beyond the current + // buffer end + int lines_appended = 0; bd.startspaces = 0; bd.endspaces = 0; @@ -3113,6 +3125,7 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) break; } nr_lines++; + lines_appended = 1; } /* get the old line and advance to the position to insert at */ oldp = get_cursor_line_ptr(); @@ -3185,14 +3198,15 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags) memmove(ptr, oldp + bd.textcol + delcount, (size_t)columns); ml_replace(curwin->w_cursor.lnum, newp, false); extmark_splice_cols(curbuf, (int)curwin->w_cursor.lnum-1, bd.textcol, - delcount, (int)totlen, kExtmarkUndo); + delcount, (int)totlen + lines_appended, kExtmarkUndo); ++curwin->w_cursor.lnum; if (i == 0) curwin->w_cursor.col += bd.startspaces; } - changed_lines(lnum, 0, curwin->w_cursor.lnum, nr_lines, true); + changed_lines(lnum, 0, curbuf->b_op_start.lnum + (linenr_T)y_size + - (linenr_T)nr_lines , nr_lines, true); /* Set '[ mark. */ curbuf->b_op_start = curwin->w_cursor; diff --git a/src/nvim/option.c b/src/nvim/option.c index 8d74cead8d..6ae9378236 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -174,6 +174,7 @@ static char_u *p_syn; static char_u *p_spc; static char_u *p_spf; static char_u *p_spl; +static char_u *p_spo; static long p_ts; static long p_tw; static int p_udf; @@ -2285,6 +2286,7 @@ void check_buf_options(buf_T *buf) check_string_option(&buf->b_s.b_p_spc); check_string_option(&buf->b_s.b_p_spf); check_string_option(&buf->b_s.b_p_spl); + check_string_option(&buf->b_s.b_p_spo); check_string_option(&buf->b_p_sua); check_string_option(&buf->b_p_cink); check_string_option(&buf->b_p_cino); @@ -3090,6 +3092,10 @@ ambw_end: } else if (varp == &(curwin->w_s->b_p_spc)) { // When 'spellcapcheck' is set compile the regexp program. errmsg = compile_cap_prog(curwin->w_s); + } else if (varp == &(curwin->w_s->b_p_spo)) { // 'spelloptions' + if (**varp != NUL && STRCMP("camel", *varp) != 0) { + errmsg = e_invarg; + } } else if (varp == &p_sps) { // 'spellsuggest' if (spell_check_sps() != OK) { errmsg = e_invarg; @@ -5896,6 +5902,7 @@ static char_u *get_varp(vimoption_T *p) case PV_SPC: return (char_u *)&(curwin->w_s->b_p_spc); case PV_SPF: return (char_u *)&(curwin->w_s->b_p_spf); case PV_SPL: return (char_u *)&(curwin->w_s->b_p_spl); + case PV_SPO: return (char_u *)&(curwin->w_s->b_p_spo); case PV_SW: return (char_u *)&(curbuf->b_p_sw); case PV_TFU: return (char_u *)&(curbuf->b_p_tfu); case PV_TS: return (char_u *)&(curbuf->b_p_ts); @@ -6175,6 +6182,7 @@ void buf_copy_options(buf_T *buf, int flags) (void)compile_cap_prog(&buf->b_s); buf->b_s.b_p_spf = vim_strsave(p_spf); buf->b_s.b_p_spl = vim_strsave(p_spl); + buf->b_s.b_p_spo = vim_strsave(p_spo); buf->b_p_inde = vim_strsave(p_inde); buf->b_p_indk = vim_strsave(p_indk); buf->b_p_fp = empty_option; diff --git a/src/nvim/option_defs.h b/src/nvim/option_defs.h index 02fa7ac216..a09811c8fb 100644 --- a/src/nvim/option_defs.h +++ b/src/nvim/option_defs.h @@ -787,6 +787,7 @@ enum { , BV_SPC , BV_SPF , BV_SPL + , BV_SPO , BV_STS , BV_SUA , BV_SW diff --git a/src/nvim/options.lua b/src/nvim/options.lua index f1221a52a2..02df0ab276 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -2320,6 +2320,16 @@ return { defaults={if_true={vi="best"}} }, { + full_name='spelloptions', abbreviation='spo', + type='string', list='onecomma', scope={'buffer'}, + deny_duplicates=true, + secure=true, + vi_def=true, + expand=true, + varname='p_spo', + defaults={if_true={vi="", vim=""}} + }, + { full_name='splitbelow', abbreviation='sb', type='bool', scope={'global'}, vi_def=true, diff --git a/src/nvim/os/lang.c b/src/nvim/os/lang.c index fe2d7986bf..603191a0ff 100644 --- a/src/nvim/os/lang.c +++ b/src/nvim/os/lang.c @@ -43,14 +43,20 @@ void lang_init(void) } } + char buf[50] = { 0 }; + bool set_lang; if (lang_region) { - os_setenv("LANG", lang_region, true); + set_lang = true; + xstrlcpy(buf, lang_region, sizeof(buf)); } else { - char buf[20] = { 0 }; - if (CFStringGetCString(cf_lang_region, buf, 20, - kCFStringEncodingUTF8)) { - os_setenv("LANG", buf, true); + set_lang = CFStringGetCString(cf_lang_region, buf, 40, + kCFStringEncodingUTF8); + } + if (set_lang) { + if (strcasestr(buf, "utf-8") == NULL) { + xstrlcat(buf, ".UTF-8", sizeof(buf)); } + os_setenv("LANG", buf, true); } CFRelease(cf_lang_region); # ifdef HAVE_LOCALE_H diff --git a/src/nvim/popupmnu.c b/src/nvim/popupmnu.c index c712762bdf..3beada5bc9 100644 --- a/src/nvim/popupmnu.c +++ b/src/nvim/popupmnu.c @@ -226,6 +226,7 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, pum_above = false; // Leave two lines of context if possible + validate_cheight(); if (curwin->w_cline_row + curwin->w_cline_height - curwin->w_wrow >= 3) { context_lines = 3; } else { diff --git a/src/nvim/quickfix.c b/src/nvim/quickfix.c index e897be5f6b..fc2e1a4295 100644 --- a/src/nvim/quickfix.c +++ b/src/nvim/quickfix.c @@ -811,7 +811,7 @@ retry: } break; } - if (STRLEN(IObuff) < IOSIZE - 1 || IObuff[IOSIZE - 1] == '\n') { + if (STRLEN(IObuff) < IOSIZE - 1 || IObuff[IOSIZE - 2] == '\n') { break; } } @@ -904,6 +904,7 @@ static bool qf_list_has_valid_entries(qf_list_T *qfl) /// Return a pointer to a list in the specified quickfix stack static qf_list_T * qf_get_list(qf_info_T *qi, int idx) + FUNC_ATTR_NONNULL_ALL { return &qi->qf_lists[idx]; } @@ -1233,6 +1234,7 @@ static char_u * qf_cmdtitle(char_u *cmd) /// Return a pointer to the current list in the specified quickfix stack static qf_list_T * qf_get_curlist(qf_info_T *qi) + FUNC_ATTR_NONNULL_ALL { return qf_get_list(qi, qi->qf_curlist); } @@ -3840,7 +3842,7 @@ static void qf_update_buffer(qf_info_T *qi, qfline_T *old_last) // Add an error line to the quickfix buffer. static int qf_buf_add_line(buf_T *buf, linenr_T lnum, const qfline_T *qfp, - char_u *dirname) + char_u *dirname, bool first_bufline) FUNC_ATTR_NONNULL_ALL { int len; @@ -3855,9 +3857,12 @@ static int qf_buf_add_line(buf_T *buf, linenr_T lnum, const qfline_T *qfp, if (qfp->qf_type == 1) { // :helpgrep STRLCPY(IObuff, path_tail(errbuf->b_fname), IOSIZE - 1); } else { - // shorten the file name if not done already - if (errbuf->b_sfname == NULL - || path_is_absolute(errbuf->b_sfname)) { + // Shorten the file name if not done already. + // For optimization, do this only for the first entry in a + // buffer. + if (first_bufline + && (errbuf->b_sfname == NULL + || path_is_absolute(errbuf->b_sfname))) { if (*dirname == NUL) { os_dirname(dirname, MAXPATHL); } @@ -3935,6 +3940,7 @@ static void qf_fill_buffer(qf_list_T *qfl, buf_T *buf, qfline_T *old_last) // Check if there is anything to display if (qfl != NULL) { char_u dirname[MAXPATHL]; + int prev_bufnr = -1; *dirname = NUL; @@ -3947,9 +3953,11 @@ static void qf_fill_buffer(qf_list_T *qfl, buf_T *buf, qfline_T *old_last) lnum = buf->b_ml.ml_line_count; } while (lnum < qfl->qf_count) { - if (qf_buf_add_line(buf, lnum, qfp, dirname) == FAIL) { + if (qf_buf_add_line(buf, lnum, qfp, dirname, + prev_bufnr != qfp->qf_fnum) == FAIL) { break; } + prev_bufnr = qfp->qf_fnum; lnum++; qfp = qfp->qf_next; if (qfp == NULL) { @@ -4915,12 +4923,13 @@ static bool vgr_qflist_valid(win_T *wp, qf_info_T *qi, unsigned qfid, /// Search for a pattern in all the lines in a buffer and add the matching lines /// to a quickfix list. static bool vgr_match_buflines(qf_info_T *qi, char_u *fname, buf_T *buf, - regmmatch_T *regmatch, long tomatch, + regmmatch_T *regmatch, long *tomatch, int duplicate_name, int flags) + FUNC_ATTR_NONNULL_ARG(1, 3, 4, 5) { bool found_match = false; - for (long lnum = 1; lnum <= buf->b_ml.ml_line_count && tomatch > 0; lnum++) { + for (long lnum = 1; lnum <= buf->b_ml.ml_line_count && *tomatch > 0; lnum++) { colnr_T col = 0; while (vim_regexec_multi(regmatch, curwin, buf, lnum, col, NULL, NULL) > 0) { @@ -4946,7 +4955,7 @@ static bool vgr_match_buflines(qf_info_T *qi, char_u *fname, buf_T *buf, break; } found_match = true; - if (--tomatch == 0) { + if (--*tomatch == 0) { break; } if ((flags & VGR_GLOBAL) == 0 || regmatch->endpos[0].lnum > 0) { @@ -5120,7 +5129,7 @@ void ex_vimgrep(exarg_T *eap) } else { // Try for a match in all lines of the buffer. // For ":1vimgrep" look for first match only. - found_match = vgr_match_buflines(qi, fname, buf, ®match, tomatch, + found_match = vgr_match_buflines(qi, fname, buf, ®match, &tomatch, duplicate_name, flags); if (using_dummy) { @@ -6458,7 +6467,7 @@ void ex_cexpr(exarg_T *eap) // Evaluate the expression. When the result is a string or a list we can // use it to fill the errorlist. typval_T tv; - if (eval0(eap->arg, &tv, NULL, true) != FAIL) { + if (eval0(eap->arg, &tv, &eap->nextcmd, true) != FAIL) { if ((tv.v_type == VAR_STRING && tv.vval.v_string != NULL) || tv.v_type == VAR_LIST) { incr_quickfix_busy(); diff --git a/src/nvim/regexp.c b/src/nvim/regexp.c index bcf02af4ef..6316129c6a 100644 --- a/src/nvim/regexp.c +++ b/src/nvim/regexp.c @@ -6708,14 +6708,14 @@ static int vim_regsub_both(char_u *source, typval_T *expr, char_u *dest, argv[0].vval.v_list = &matchList.sl_list; if (expr->v_type == VAR_FUNC) { s = expr->vval.v_string; - call_func(s, (int)STRLEN(s), &rettv, 1, argv, + call_func(s, -1, &rettv, 1, argv, fill_submatch_list, 0L, 0L, &dummy, true, NULL, NULL); } else if (expr->v_type == VAR_PARTIAL) { partial_T *partial = expr->vval.v_partial; s = partial_name(partial); - call_func(s, (int)STRLEN(s), &rettv, 1, argv, + call_func(s, -1, &rettv, 1, argv, fill_submatch_list, 0L, 0L, &dummy, true, partial, NULL); } @@ -7387,6 +7387,7 @@ long vim_regexec_multi( proftime_T *tm, // timeout limit or NULL int *timed_out // flag is set when timeout limit reached ) + FUNC_ATTR_NONNULL_ARG(1) { regexec_T rex_save; bool rex_in_use_save = rex_in_use; diff --git a/src/nvim/screen.c b/src/nvim/screen.c index 3c2e1ccaf5..a75b146024 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -133,8 +133,6 @@ static sattr_T *linebuf_attr = NULL; static match_T search_hl; /* used for 'hlsearch' highlight matching */ -static foldinfo_T win_foldinfo; /* info for 'foldcolumn' */ - StlClickDefinition *tab_page_click_defs = NULL; long tab_page_click_defs_size = 0; @@ -158,6 +156,8 @@ static bool msg_grid_invalid = false; static bool resizing = false; +static bool do_luahl_line = false; + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "screen.c.generated.h" #endif @@ -508,12 +508,12 @@ int update_screen(int type) } buf_T *buf = wp->w_buffer; - if (buf->b_luahl && buf->b_luahl_window != LUA_NOREF) { + if (luahl_active && luahl_start != LUA_NOREF) { Error err = ERROR_INIT; FIXED_TEMP_ARRAY(args, 2); args.items[0] = BUFFER_OBJ(buf->handle); args.items[1] = INTEGER_OBJ(display_tick); - nlua_call_ref(buf->b_luahl_start, "start", args, false, &err); + nlua_call_ref(luahl_start, "start", args, false, &err); if (ERROR_SET(&err)) { ELOG("error in luahl start: %s", err.msg); api_clear_error(&err); @@ -639,10 +639,11 @@ bool decorations_active = false; void decorations_add_luahl_attr(int attr_id, int start_row, int start_col, - int end_row, int end_col) + int end_row, int end_col, VirtText *virt_text) { kv_push(decorations.active, - ((HlRange){ start_row, start_col, end_row, end_col, attr_id, NULL })); + ((HlRange){ start_row, start_col, + end_row, end_col, attr_id, virt_text, true })); } /* @@ -697,9 +698,10 @@ static void win_update(win_T *wp) int didline = FALSE; /* if TRUE, we finished the last line */ int i; long j; - static int recursive = FALSE; /* being called recursively */ - int old_botline = wp->w_botline; - long fold_count; + static bool recursive = false; // being called recursively + const linenr_T old_botline = wp->w_botline; + const int old_wrow = wp->w_wrow; + const int old_wcol = wp->w_wcol; // Remember what happened to the previous line. #define DID_NONE 1 // didn't update a line #define DID_LINE 2 // updated a normal line @@ -710,6 +712,7 @@ static void win_update(win_T *wp) linenr_T mod_bot = 0; int save_got_int; + // If we can compute a change in the automatic sizing of the sign column // under 'signcolumn=auto:X' and signs currently placed in the buffer, better // figuring it out here so we can redraw the entire screen for it. @@ -898,11 +901,12 @@ static void win_update(win_T *wp) || type == INVERTED || type == INVERTED_ALL) && !wp->w_botfill && !wp->w_old_botfill ) { - if (mod_top != 0 && wp->w_topline == mod_top) { - /* - * w_topline is the first changed line, the scrolling will be done - * further down. - */ + if (mod_top != 0 + && wp->w_topline == mod_top + && (!wp->w_lines[0].wl_valid + || wp->w_topline <= wp->w_lines[0].wl_lnum)) { + // w_topline is the first changed line and window is not scrolled, + // the scrolling from changed lines will be done further down. } else if (wp->w_lines[0].wl_valid && (wp->w_topline < wp->w_lines[0].wl_lnum || (wp->w_topline == wp->w_lines[0].wl_lnum @@ -1226,7 +1230,6 @@ static void win_update(win_T *wp) // Set the time limit to 'redrawtime'. proftime_T syntax_tm = profile_setlimit(p_rdt); syn_set_timeout(&syntax_tm); - win_foldinfo.fi_level = 0; /* * Update all the window rows. @@ -1238,7 +1241,9 @@ static void win_update(win_T *wp) decorations_active = decorations_redraw_reset(buf, &decorations); - if (buf->b_luahl && buf->b_luahl_window != LUA_NOREF) { + do_luahl_line = false; + + if (luahl_win != LUA_NOREF) { Error err = ERROR_INIT; FIXED_TEMP_ARRAY(args, 4); linenr_T knownmax = ((wp->w_valid & VALID_BOTLINE) @@ -1251,7 +1256,13 @@ static void win_update(win_T *wp) args.items[3] = INTEGER_OBJ(knownmax); // TODO(bfredl): we could allow this callback to change mod_top, mod_bot. // For now the "start" callback is expected to use nvim__buf_redraw_range. - nlua_call_ref(buf->b_luahl_window, "window", args, false, &err); + Object ret = nlua_call_ref(luahl_win, "win", args, true, &err); + + if (!ERROR_SET(&err) && api_is_truthy(ret, "luahl_window retval", &err)) { + do_luahl_line = true; + decorations_active = true; + } + if (ERROR_SET(&err)) { ELOG("error in luahl window: %s", err.msg); api_clear_error(&err); @@ -1448,24 +1459,19 @@ static void win_update(win_T *wp) * Otherwise, display normally (can be several display lines when * 'wrap' is on). */ - fold_count = foldedCount(wp, lnum, &win_foldinfo); - if (fold_count != 0) { - fold_line(wp, fold_count, &win_foldinfo, lnum, row); - ++row; - --fold_count; - wp->w_lines[idx].wl_folded = TRUE; - wp->w_lines[idx].wl_lastlnum = lnum + fold_count; - did_update = DID_FOLD; - } else if (idx < wp->w_lines_valid - && wp->w_lines[idx].wl_valid - && wp->w_lines[idx].wl_lnum == lnum - && lnum > wp->w_topline - && !(dy_flags & (DY_LASTLINE | DY_TRUNCATE)) - && srow + wp->w_lines[idx].wl_size > wp->w_grid.Rows - && diff_check_fill(wp, lnum) == 0 - ) { - /* This line is not going to fit. Don't draw anything here, - * will draw "@ " lines below. */ + foldinfo_T foldinfo = fold_info(wp, lnum); + + if (foldinfo.fi_lines == 0 + && idx < wp->w_lines_valid + && wp->w_lines[idx].wl_valid + && wp->w_lines[idx].wl_lnum == lnum + && lnum > wp->w_topline + && !(dy_flags & (DY_LASTLINE | DY_TRUNCATE)) + && srow + wp->w_lines[idx].wl_size > wp->w_grid.Rows + && diff_check_fill(wp, lnum) == 0 + ) { + // This line is not going to fit. Don't draw anything here, + // will draw "@ " lines below. row = wp->w_grid.Rows + 1; } else { prepare_search_hl(wp, lnum); @@ -1474,14 +1480,21 @@ static void win_update(win_T *wp) && syntax_present(wp)) syntax_end_parsing(syntax_last_parsed + 1); - /* - * Display one line. - */ - row = win_line(wp, lnum, srow, wp->w_grid.Rows, mod_top == 0, false); + // Display one line + row = win_line(wp, lnum, srow, + foldinfo.fi_lines ? srow : wp->w_grid.Rows, + mod_top == 0, false, foldinfo); - wp->w_lines[idx].wl_folded = FALSE; + wp->w_lines[idx].wl_folded = foldinfo.fi_lines != 0; wp->w_lines[idx].wl_lastlnum = lnum; did_update = DID_LINE; + + if (foldinfo.fi_lines > 0) { + did_update = DID_FOLD; + foldinfo.fi_lines--; + wp->w_lines[idx].wl_lastlnum = lnum + foldinfo.fi_lines; + } + syntax_last_parsed = lnum; } @@ -1496,20 +1509,17 @@ static void win_update(win_T *wp) idx++; break; } - if (dollar_vcol == -1) + if (dollar_vcol == -1) { wp->w_lines[idx].wl_size = row - srow; - ++idx; - lnum += fold_count + 1; + } + idx++; + lnum += foldinfo.fi_lines + 1; } else { if (wp->w_p_rnu) { // 'relativenumber' set: The text doesn't need to be drawn, but // the number column nearly always does. - fold_count = foldedCount(wp, lnum, &win_foldinfo); - if (fold_count != 0) { - fold_line(wp, fold_count, &win_foldinfo, lnum, row); - } else { - (void)win_line(wp, lnum, srow, wp->w_grid.Rows, true, true); - } + foldinfo_T info = fold_info(wp, lnum); + (void)win_line(wp, lnum, srow, wp->w_grid.Rows, true, true, info); } // This line does not need to be drawn, advance to the next one. @@ -1631,18 +1641,51 @@ static void win_update(win_T *wp) wp->w_valid |= VALID_BOTLINE; wp->w_viewport_invalid = true; if (wp == curwin && wp->w_botline != old_botline && !recursive) { - recursive = TRUE; + const linenr_T old_topline = wp->w_topline; + const int new_wcol = wp->w_wcol; + recursive = true; curwin->w_valid &= ~VALID_TOPLINE; - update_topline(); /* may invalidate w_botline again */ - if (must_redraw != 0) { - /* Don't update for changes in buffer again. */ + update_topline(); // may invalidate w_botline again + + if (old_wcol != new_wcol + && (wp->w_valid & (VALID_WCOL|VALID_WROW)) + != (VALID_WCOL|VALID_WROW)) { + // A win_line() call applied a fix to screen cursor column to + // accomodate concealment of cursor line, but in this call to + // update_topline() the cursor's row or column got invalidated. + // If they are left invalid, setcursor() will recompute them + // but there won't be any further win_line() call to re-fix the + // column and the cursor will end up misplaced. So we call + // cursor validation now and reapply the fix again (or call + // win_line() to do it for us). + validate_cursor(); + if (wp->w_wcol == old_wcol + && wp->w_wrow == old_wrow + && old_topline == wp->w_topline) { + wp->w_wcol = new_wcol; + } else { + redrawWinline(wp, wp->w_cursor.lnum); + } + } + // New redraw either due to updated topline or due to wcol fix. + if (wp->w_redr_type != 0) { + // Don't update for changes in buffer again. i = curbuf->b_mod_set; curbuf->b_mod_set = false; + j = curbuf->b_mod_xlines; + curbuf->b_mod_xlines = 0; win_update(curwin); - must_redraw = 0; curbuf->b_mod_set = i; + curbuf->b_mod_xlines = j; + } + // Other windows might have w_redr_type raised in update_topline(). + must_redraw = 0; + FOR_ALL_WINDOWS_IN_TAB(wwp, curtab) { + if (wwp->w_redr_type > must_redraw) { + must_redraw = wwp->w_redr_type; + } } - recursive = FALSE; + recursive = false; } } @@ -1741,31 +1784,6 @@ static int advance_color_col(int vcol, int **color_cols) return **color_cols >= 0; } -// Returns the next grid column. -static int text_to_screenline(win_T *wp, char_u *text, int col, int off) - FUNC_ATTR_NONNULL_ALL -{ - int idx = wp->w_p_rl ? off : off + col; - LineState s = LINE_STATE(text); - - while (*s.p != NUL) { - // TODO(bfredl): cargo-culted from the old Vim code: - // if(col + cells > wp->w_width - (wp->w_p_rl ? col : 0)) { break; } - // This is obvious wrong. If Vim ever fixes this, solve for "cells" again - // in the correct condition. - const int maxcells = wp->w_grid.Columns - col - (wp->w_p_rl ? col : 0); - const int cells = line_putchar(&s, &linebuf_char[idx], maxcells, - wp->w_p_rl); - if (cells == -1) { - break; - } - col += cells; - idx += cells; - } - - return col; -} - // Compute the width of the foldcolumn. Based on 'foldcolumn' and how much // space is available for window "wp", minus "col". static int compute_foldcolumn(win_T *wp, int col) @@ -1830,271 +1848,6 @@ static int line_putchar(LineState *s, schar_T *dest, int maxcells, bool rl) return cells; } -/* - * Display one folded line. - */ -static void fold_line(win_T *wp, long fold_count, foldinfo_T *foldinfo, linenr_T lnum, int row) -{ - char_u buf[FOLD_TEXT_LEN]; - pos_T *top, *bot; - linenr_T lnume = lnum + fold_count - 1; - int len; - char_u *text; - int fdc; - int col; - int txtcol; - int off; - - /* Build the fold line: - * 1. Add the cmdwin_type for the command-line window - * 2. Add the 'foldcolumn' - * 3. Add the 'number' or 'relativenumber' column - * 4. Compose the text - * 5. Add the text - * 6. set highlighting for the Visual area an other text - */ - col = 0; - off = 0; - - /* - * 1. Add the cmdwin_type for the command-line window - * Ignores 'rightleft', this window is never right-left. - */ - if (cmdwin_type != 0 && wp == curwin) { - schar_from_ascii(linebuf_char[off], cmdwin_type); - linebuf_attr[off] = win_hl_attr(wp, HLF_AT); - col++; - } - -# define RL_MEMSET(p, v, l) \ - do { \ - if (wp->w_p_rl) { \ - for (int ri = 0; ri < l; ri++) { \ - linebuf_attr[off + (wp->w_grid.Columns - (p) - (l)) + ri] = v; \ - } \ - } else { \ - for (int ri = 0; ri < l; ri++) { \ - linebuf_attr[off + (p) + ri] = v; \ - } \ - } \ - } while (0) - - // 2. Add the 'foldcolumn' - // Reduce the width when there is not enough space. - fdc = compute_foldcolumn(wp, col); - if (fdc > 0) { - fill_foldcolumn(buf, wp, true, lnum); - const char_u *it = &buf[0]; - for (int i = 0; i < fdc; i++) { - int mb_c = mb_ptr2char_adv(&it); - if (wp->w_p_rl) { - schar_from_char(linebuf_char[off + wp->w_grid.Columns - i - 1 - col], - mb_c); - } else { - schar_from_char(linebuf_char[off + col + i], mb_c); - } - } - RL_MEMSET(col, win_hl_attr(wp, HLF_FC), fdc); - col += fdc; - } - - /* Set all attributes of the 'number' or 'relativenumber' column and the - * text */ - RL_MEMSET(col, win_hl_attr(wp, HLF_FL), wp->w_grid.Columns - col); - - // If signs are being displayed, add spaces. - if (win_signcol_count(wp) > 0) { - len = wp->w_grid.Columns - col; - if (len > 0) { - int len_max = win_signcol_width(wp) * win_signcol_count(wp); - if (len > len_max) { - len = len_max; - } - char_u space_buf[18] = " "; - assert((size_t)len_max <= sizeof(space_buf)); - copy_text_attr(off + col, space_buf, len, - win_hl_attr(wp, HLF_FL)); - col += len; - } - } - - /* - * 3. Add the 'number' or 'relativenumber' column - */ - if (wp->w_p_nu || wp->w_p_rnu) { - len = wp->w_grid.Columns - col; - if (len > 0) { - int w = number_width(wp); - long num; - char *fmt = "%*ld "; - - if (len > w + 1) - len = w + 1; - - if (wp->w_p_nu && !wp->w_p_rnu) - /* 'number' + 'norelativenumber' */ - num = (long)lnum; - else { - /* 'relativenumber', don't use negative numbers */ - num = labs((long)get_cursor_rel_lnum(wp, lnum)); - if (num == 0 && wp->w_p_nu && wp->w_p_rnu) { - /* 'number' + 'relativenumber': cursor line shows absolute - * line number */ - num = lnum; - fmt = "%-*ld "; - } - } - - snprintf((char *)buf, FOLD_TEXT_LEN, fmt, w, num); - if (wp->w_p_rl) { - // the line number isn't reversed - copy_text_attr(off + wp->w_grid.Columns - len - col, buf, len, - win_hl_attr(wp, HLF_FL)); - } else { - copy_text_attr(off + col, buf, len, win_hl_attr(wp, HLF_FL)); - } - col += len; - } - } - - /* - * 4. Compose the folded-line string with 'foldtext', if set. - */ - text = get_foldtext(wp, lnum, lnume, foldinfo, buf); - - txtcol = col; /* remember where text starts */ - - // 5. move the text to linebuf_char[off]. Fill up with "fold". - // Right-left text is put in columns 0 - number-col, normal text is put - // in columns number-col - window-width. - col = text_to_screenline(wp, text, col, off); - - /* Fill the rest of the line with the fold filler */ - if (wp->w_p_rl) - col -= txtcol; - - schar_T sc; - schar_from_char(sc, wp->w_p_fcs_chars.fold); - while (col < wp->w_grid.Columns - - (wp->w_p_rl ? txtcol : 0) - ) { - schar_copy(linebuf_char[off+col++], sc); - } - - if (text != buf) - xfree(text); - - /* - * 6. set highlighting for the Visual area an other text. - * If all folded lines are in the Visual area, highlight the line. - */ - if (VIsual_active && wp->w_buffer == curwin->w_buffer) { - if (ltoreq(curwin->w_cursor, VIsual)) { - /* Visual is after curwin->w_cursor */ - top = &curwin->w_cursor; - bot = &VIsual; - } else { - /* Visual is before curwin->w_cursor */ - top = &VIsual; - bot = &curwin->w_cursor; - } - if (lnum >= top->lnum - && lnume <= bot->lnum - && (VIsual_mode != 'v' - || ((lnum > top->lnum - || (lnum == top->lnum - && top->col == 0)) - && (lnume < bot->lnum - || (lnume == bot->lnum - && (bot->col - (*p_sel == 'e')) - >= (colnr_T)STRLEN(ml_get_buf(wp->w_buffer, lnume, - FALSE))))))) { - if (VIsual_mode == Ctrl_V) { - // Visual block mode: highlight the chars part of the block - if (wp->w_old_cursor_fcol + txtcol < (colnr_T)wp->w_grid.Columns) { - if (wp->w_old_cursor_lcol != MAXCOL - && wp->w_old_cursor_lcol + txtcol - < (colnr_T)wp->w_grid.Columns) { - len = wp->w_old_cursor_lcol; - } else { - len = wp->w_grid.Columns - txtcol; - } - RL_MEMSET(wp->w_old_cursor_fcol + txtcol, win_hl_attr(wp, HLF_V), - len - (int)wp->w_old_cursor_fcol); - } - } else { - // Set all attributes of the text - RL_MEMSET(txtcol, win_hl_attr(wp, HLF_V), wp->w_grid.Columns - txtcol); - } - } - } - - // Show colorcolumn in the fold line, but let cursorcolumn override it. - if (wp->w_p_cc_cols) { - int i = 0; - int j = wp->w_p_cc_cols[i]; - int old_txtcol = txtcol; - - while (j > -1) { - txtcol += j; - if (wp->w_p_wrap) { - txtcol -= wp->w_skipcol; - } else { - txtcol -= wp->w_leftcol; - } - if (txtcol >= 0 && txtcol < wp->w_grid.Columns) { - linebuf_attr[off + txtcol] = - hl_combine_attr(linebuf_attr[off + txtcol], win_hl_attr(wp, HLF_MC)); - } - txtcol = old_txtcol; - j = wp->w_p_cc_cols[++i]; - } - } - - /* Show 'cursorcolumn' in the fold line. */ - if (wp->w_p_cuc) { - txtcol += wp->w_virtcol; - if (wp->w_p_wrap) - txtcol -= wp->w_skipcol; - else - txtcol -= wp->w_leftcol; - if (txtcol >= 0 && txtcol < wp->w_grid.Columns) { - linebuf_attr[off + txtcol] = hl_combine_attr( - linebuf_attr[off + txtcol], win_hl_attr(wp, HLF_CUC)); - } - } - - grid_put_linebuf(&wp->w_grid, row, 0, wp->w_grid.Columns, wp->w_grid.Columns, - false, wp, wp->w_hl_attr_normal, false); - - /* - * Update w_cline_height and w_cline_folded if the cursor line was - * updated (saves a call to plines() later). - */ - if (wp == curwin - && lnum <= curwin->w_cursor.lnum - && lnume >= curwin->w_cursor.lnum) { - curwin->w_cline_row = row; - curwin->w_cline_height = 1; - curwin->w_cline_folded = true; - curwin->w_valid |= (VALID_CHEIGHT|VALID_CROW); - conceal_cursor_used = conceal_cursor_line(curwin); - } -} - - -/// Copy "buf[len]" to linebuf_char["off"] and set attributes to "attr". -/// -/// Only works for ASCII text! -static void copy_text_attr(int off, char_u *buf, int len, int attr) -{ - int i; - - for (i = 0; i < len; i++) { - schar_from_ascii(linebuf_char[off + i], buf[i]); - linebuf_attr[off + i] = attr; - } -} /// Fills the foldcolumn at "p" for window "wp". /// Only to be called when 'foldcolumn' > 0. @@ -2109,7 +1862,7 @@ static size_t fill_foldcolumn( char_u *p, win_T *wp, - int closed, + foldinfo_T foldinfo, linenr_T lnum ) { @@ -2120,10 +1873,11 @@ fill_foldcolumn( size_t char_counter = 0; int symbol = 0; int len = 0; + bool closed = foldinfo.fi_lines > 0; // Init to all spaces. memset(p, ' ', MAX_MCO * fdc + 1); - level = win_foldinfo.fi_level; + level = foldinfo.fi_level; // If the column is too narrow, we start at the lowest level that // fits and use numbers to indicated the depth. @@ -2133,8 +1887,8 @@ fill_foldcolumn( } for (i = 0; i < MIN(fdc, level); i++) { - if (win_foldinfo.fi_lnum == lnum - && first_level + i >= win_foldinfo.fi_low_level) { + if (foldinfo.fi_lnum == lnum + && first_level + i >= foldinfo.fi_low_level) { symbol = wp->w_p_fcs_chars.foldopen; } else if (first_level == 1) { symbol = wp->w_p_fcs_chars.foldsep; @@ -2165,21 +1919,27 @@ fill_foldcolumn( return MAX(char_counter + (fdc-i), (size_t)fdc); } -/* - * Display line "lnum" of window 'wp' on the screen. - * Start at row "startrow", stop when "endrow" is reached. - * wp->w_virtcol needs to be valid. - * - * Return the number of last row the line occupies. - */ + +/// Display line "lnum" of window 'wp' on the screen. +/// Start at row "startrow", stop when "endrow" is reached. +/// wp->w_virtcol needs to be valid. +/// +/// @param lnum line to display +/// @param endrow stop drawing once reaching this row +/// @param nochange not updating for changed text +/// @param number_only only update the number column +/// @param foldinfo fold info for this line +/// +/// @return the number of last row the line occupies. static int win_line ( win_T *wp, linenr_T lnum, int startrow, int endrow, - bool nochange, // not updating for changed text - bool number_only // only update the number column + bool nochange, + bool number_only, + foldinfo_T foldinfo ) { int c = 0; // init for GCC @@ -2284,6 +2044,8 @@ win_line ( bool has_decorations = false; // this buffer has decorations bool do_virttext = false; // draw virtual text for this line + char_u buf_fold[FOLD_TEXT_LEN + 1]; // Hold value returned by get_foldtext + /* draw_state: items that are drawn in sequence: */ #define WL_START 0 /* nothing done yet */ # define WL_CMDLINE WL_START + 1 /* cmdline window column */ @@ -2348,7 +2110,7 @@ win_line ( } if (decorations_active) { - if (buf->b_luahl && buf->b_luahl_line != LUA_NOREF) { + if (do_luahl_line && luahl_line != LUA_NOREF) { Error err = ERROR_INIT; FIXED_TEMP_ARRAY(args, 3); args.items[0] = WINDOW_OBJ(wp->handle); @@ -2356,14 +2118,10 @@ win_line ( args.items[2] = INTEGER_OBJ(lnum-1); lua_attr_active = true; extra_check = true; - Object o = nlua_call_ref(buf->b_luahl_line, "line", args, true, &err); + nlua_call_ref(luahl_line, "line", args, false, &err); lua_attr_active = false; - if (o.type == kObjectTypeString) { - // TODO(bfredl): this is a bit of a hack. A final API should use an - // "unified" interface where luahl can add both bufhl and virttext - luatext = o.data.string.data; - do_virttext = true; - } else if (ERROR_SET(&err)) { + + if (ERROR_SET(&err)) { ELOG("error in luahl line: %s", err.msg); luatext = err.msg; do_virttext = true; @@ -2839,7 +2597,7 @@ win_line ( // already be in use. xfree(p_extra_free); p_extra_free = xmalloc(MAX_MCO * fdc + 1); - n_extra = fill_foldcolumn(p_extra_free, wp, false, lnum); + n_extra = fill_foldcolumn(p_extra_free, wp, foldinfo, lnum); p_extra_free[n_extra] = NUL; p_extra = p_extra_free; c_extra = NUL; @@ -3065,6 +2823,51 @@ win_line ( break; } + if (draw_state == WL_LINE + && foldinfo.fi_level != 0 + && foldinfo.fi_lines > 0 + && vcol == 0 + && n_extra == 0 + && row == startrow) { + char_attr = win_hl_attr(wp, HLF_FL); + + linenr_T lnume = lnum + foldinfo.fi_lines - 1; + memset(buf_fold, ' ', FOLD_TEXT_LEN); + p_extra = get_foldtext(wp, lnum, lnume, foldinfo, buf_fold); + n_extra = STRLEN(p_extra); + + if (p_extra != buf_fold) { + xfree(p_extra_free); + p_extra_free = p_extra; + } + c_extra = NUL; + c_final = NUL; + p_extra[n_extra] = NUL; + } + + if (draw_state == WL_LINE + && foldinfo.fi_level != 0 + && foldinfo.fi_lines > 0 + && col < grid->Columns + && n_extra == 0 + && row == startrow) { + // fill rest of line with 'fold' + c_extra = wp->w_p_fcs_chars.fold; + c_final = NUL; + + n_extra = wp->w_p_rl ? (col + 1) : (grid->Columns - col); + } + + if (draw_state == WL_LINE + && foldinfo.fi_level != 0 + && foldinfo.fi_lines > 0 + && col >= grid->Columns + && n_extra != 0 + && row == startrow) { + // Truncate the folding. + n_extra = 0; + } + if (draw_state == WL_LINE && (area_highlighting || has_spell)) { // handle Visual or match highlighting in this line if (vcol == fromcol @@ -3293,6 +3096,10 @@ win_line ( p_extra++; } n_extra--; + } else if (foldinfo.fi_lines > 0) { + // skip writing the buffer line itself + c = NUL; + XFREE_CLEAR(p_extra_free); } else { int c0; @@ -3861,7 +3668,7 @@ win_line ( // not showing the '>', put pointer back to avoid getting stuck ptr++; } - } + } // end of printing from buffer content /* In the cursor line and we may be concealing characters: correct * the cursor column when we reach its position. */ @@ -3918,7 +3725,7 @@ win_line ( } // At end of the text line or just after the last character. - if (c == NUL) { + if (c == NUL && eol_hl_off == 0) { long prevcol = (long)(ptr - line) - 1; // we're not really at that column when skipping some text @@ -4171,11 +3978,10 @@ win_line ( if (wp == curwin && lnum == curwin->w_cursor.lnum) { curwin->w_cline_row = startrow; curwin->w_cline_height = row - startrow; - curwin->w_cline_folded = false; + curwin->w_cline_folded = foldinfo.fi_lines > 0; curwin->w_valid |= (VALID_CHEIGHT|VALID_CROW); conceal_cursor_used = conceal_cursor_line(curwin); } - break; } @@ -4364,6 +4170,7 @@ win_line ( * so far. If there is no more to display it is caught above. */ if ((wp->w_p_rl ? (col < 0) : (col >= grid->Columns)) + && foldinfo.fi_lines == 0 && (*ptr != NUL || filler_todo > 0 || (wp->w_p_list && wp->w_p_lcs_chars.eol != NUL diff --git a/src/nvim/search.c b/src/nvim/search.c index 23d84038d6..ea2107c5c7 100644 --- a/src/nvim/search.c +++ b/src/nvim/search.c @@ -96,12 +96,8 @@ static int lastc_bytelen = 1; // >1 for multi-byte char // copy of spats[], for keeping the search patterns while executing autocmds static struct spat saved_spats[2]; -// copy of spats[RE_SEARCH], for keeping the search patterns while incremental -// searching -static struct spat saved_last_search_spat; -static int did_save_last_search_spat = 0; -static int saved_last_idx = 0; -static bool saved_no_hlsearch = false; +static int saved_spats_last_idx = 0; +static bool saved_spats_no_hlsearch = false; static char_u *mr_pattern = NULL; // pattern used by search_regcomp() static int mr_pattern_alloced = false; // mr_pattern was allocated @@ -268,8 +264,8 @@ void save_search_patterns(void) saved_spats[1] = spats[1]; if (spats[1].pat != NULL) saved_spats[1].pat = vim_strsave(spats[1].pat); - saved_last_idx = last_idx; - saved_no_hlsearch = no_hlsearch; + saved_spats_last_idx = last_idx; + saved_spats_no_hlsearch = no_hlsearch; } } @@ -281,8 +277,8 @@ void restore_search_patterns(void) set_vv_searchforward(); free_spat(&spats[1]); spats[1] = saved_spats[1]; - last_idx = saved_last_idx; - set_no_hlsearch(saved_no_hlsearch); + last_idx = saved_spats_last_idx; + set_no_hlsearch(saved_spats_no_hlsearch); } } @@ -309,6 +305,13 @@ void free_search_patterns(void) #endif +// copy of spats[RE_SEARCH], for keeping the search patterns while incremental +// searching +static struct spat saved_last_search_spat; +static int did_save_last_search_spat = 0; +static int saved_last_idx = 0; +static bool saved_no_hlsearch = false; + /// Save and restore the search pattern for incremental highlight search /// feature. /// @@ -317,10 +320,9 @@ void free_search_patterns(void) /// cancelling incremental searching even if it's called inside user functions. void save_last_search_pattern(void) { - if (did_save_last_search_spat != 0) { - IEMSG("did_save_last_search_spat is not zero"); - } else { - did_save_last_search_spat++; + if (++did_save_last_search_spat != 1) { + // nested call, nothing to do + return; } saved_last_search_spat = spats[RE_SEARCH]; @@ -333,11 +335,15 @@ void save_last_search_pattern(void) void restore_last_search_pattern(void) { - if (did_save_last_search_spat != 1) { - IEMSG("did_save_last_search_spat is not one"); + if (--did_save_last_search_spat > 0) { + // nested call, nothing to do + return; + } + if (did_save_last_search_spat != 0) { + iemsg("restore_last_search_pattern() called more often than" + " save_last_search_pattern()"); return; } - did_save_last_search_spat--; xfree(spats[RE_SEARCH].pat); spats[RE_SEARCH] = saved_last_search_spat; @@ -488,7 +494,7 @@ void set_last_search_pat(const char_u *s, int idx, int magic, int setlast) saved_spats[idx].pat = NULL; else saved_spats[idx].pat = vim_strsave(spats[idx].pat); - saved_last_idx = last_idx; + saved_spats_last_idx = last_idx; } /* If 'hlsearch' set and search pat changed: need redraw. */ if (p_hls && idx == last_idx && !no_hlsearch) @@ -645,6 +651,10 @@ int searchit( colnr_T col = at_first_line && (options & SEARCH_COL) ? pos->col : 0; nmatched = vim_regexec_multi(®match, win, buf, lnum, col, tm, timed_out); + // vim_regexec_multi() may clear "regprog" + if (regmatch.regprog == NULL) { + break; + } // Abort searching on an error (e.g., out of stack). if (called_emsg || (timed_out != NULL && *timed_out)) { break; @@ -716,6 +726,10 @@ int searchit( match_ok = false; break; } + // vim_regexec_multi() may clear "regprog" + if (regmatch.regprog == NULL) { + break; + } matchpos = regmatch.startpos[0]; endpos = regmatch.endpos[0]; submatch = first_submatch(®match); @@ -805,10 +819,13 @@ int searchit( } break; } - - /* Need to get the line pointer again, a - * multi-line search may have made it invalid. */ - ptr = ml_get_buf(buf, lnum + matchpos.lnum, FALSE); + // vim_regexec_multi() may clear "regprog" + if (regmatch.regprog == NULL) { + break; + } + // Need to get the line pointer again, a + // multi-line search may have made it invalid. + ptr = ml_get_buf(buf, lnum + matchpos.lnum, false); } /* @@ -885,6 +902,11 @@ int searchit( } at_first_line = FALSE; + // vim_regexec_multi() may clear "regprog" + if (regmatch.regprog == NULL) { + break; + } + // Stop the search if wrapscan isn't set, "stop_lnum" is // specified, after an interrupt, after a match and after looping // twice. @@ -1149,8 +1171,8 @@ int do_search( pat = p; /* put pat after search command */ } - if ((options & SEARCH_ECHO) && messaging() - && !cmd_silent && msg_silent == 0) { + if ((options & SEARCH_ECHO) && messaging() && !msg_silent + && (!cmd_silent || !shortmess(SHM_SEARCHCOUNT))) { char_u *trunc; char_u off_buf[40]; size_t off_len = 0; @@ -1159,7 +1181,8 @@ int do_search( msg_start(); // Get the offset, so we know how long it is. - if (spats[0].off.line || spats[0].off.end || spats[0].off.off) { + if (!cmd_silent + && (spats[0].off.line || spats[0].off.end || spats[0].off.off)) { p = off_buf; // -V507 *p++ = dirc; if (spats[0].off.end) { @@ -1179,19 +1202,19 @@ int do_search( } if (*searchstr == NUL) { - p = spats[last_idx].pat; + p = spats[0].pat; } else { p = searchstr; } - if (!shortmess(SHM_SEARCHCOUNT)) { + if (!shortmess(SHM_SEARCHCOUNT) || cmd_silent) { // Reserve enough space for the search pattern + offset + // search stat. Use all the space available, so that the // search state is right aligned. If there is not enough space // msg_strtrunc() will shorten in the middle. if (ui_has(kUIMessages)) { len = 0; // adjusted below - } else if (msg_scrolled != 0) { + } else if (msg_scrolled != 0 && !cmd_silent) { // Use all the columns. len = (Rows - msg_row) * Columns - 1; } else { @@ -1208,11 +1231,13 @@ int do_search( xfree(msgbuf); msgbuf = xmalloc(len); - { - memset(msgbuf, ' ', len); - msgbuf[0] = dirc; - msgbuf[len - 1] = NUL; + memset(msgbuf, ' ', len); + msgbuf[len - 1] = NUL; + // do not fill the msgbuf buffer, if cmd_silent is set, leave it + // empty for the search_stat feature. + if (!cmd_silent) { + msgbuf[0] = dirc; if (utf_iscomposing(utf_ptr2char(p))) { // Use a space to draw the composing char on. msgbuf[1] = ' '; @@ -1356,12 +1381,15 @@ int do_search( // Show [1/15] if 'S' is not in 'shortmess'. if ((options & SEARCH_ECHO) && messaging() - && !(cmd_silent + msg_silent) + && !msg_silent && c != FAIL && !shortmess(SHM_SEARCHCOUNT) && msgbuf != NULL) { search_stat(dirc, &pos, show_top_bot_msg, msgbuf, - (count != 1 || has_offset)); + (count != 1 + || has_offset + || (!(fdo_flags & FDO_SEARCH) + && hasFolding(curwin->w_cursor.lnum, NULL, NULL)))); } // The search command can be followed by a ';' to do another search. @@ -4231,7 +4259,8 @@ is_zero_width(char_u *pattern, int move, pos_T *cur, Direction direction) if (nmatched != 0) { break; } - } while (direction == FORWARD + } while (regmatch.regprog != NULL + && direction == FORWARD ? regmatch.startpos[0].col < pos.col : regmatch.startpos[0].col > pos.col); @@ -4350,7 +4379,9 @@ static void search_stat(int dirc, pos_T *pos, len = STRLEN(t); if (show_top_bot_msg && len + 2 < SEARCH_STAT_BUF_LEN) { - STRCPY(t + len, " W"); + memmove(t + 2, t, len); + t[0] = 'W'; + t[1] = ' '; len += 2; } diff --git a/src/nvim/spell.c b/src/nvim/spell.c index dc1bfe25b4..f036d7fe04 100644 --- a/src/nvim/spell.c +++ b/src/nvim/spell.c @@ -362,6 +362,8 @@ size_t spell_check( size_t wrongcaplen = 0; int lpi; bool count_word = docount; + bool use_camel_case = *wp->w_s->b_p_spo != NUL; + bool camel_case = false; // A word never starts at a space or a control character. Return quickly // then, skipping over the character. @@ -394,9 +396,24 @@ size_t spell_check( mi.mi_word = ptr; mi.mi_fend = ptr; if (spell_iswordp(mi.mi_fend, wp)) { + int prev_upper; + int this_upper; + + if (use_camel_case) { + c = PTR2CHAR(mi.mi_fend); + this_upper = SPELL_ISUPPER(c); + } + do { MB_PTR_ADV(mi.mi_fend); - } while (*mi.mi_fend != NUL && spell_iswordp(mi.mi_fend, wp)); + if (use_camel_case) { + prev_upper = this_upper; + c = PTR2CHAR(mi.mi_fend); + this_upper = SPELL_ISUPPER(c); + camel_case = !prev_upper && this_upper; + } + } while (*mi.mi_fend != NUL && spell_iswordp(mi.mi_fend, wp) + && !camel_case); if (capcol != NULL && *capcol == 0 && wp->w_s->b_cap_prog != NULL) { // Check word starting with capital letter. @@ -428,6 +445,11 @@ size_t spell_check( (void)spell_casefold(ptr, (int)(mi.mi_fend - ptr), mi.mi_fword, MAXWLEN + 1); mi.mi_fwordlen = (int)STRLEN(mi.mi_fword); + if (camel_case) { + // introduce a fake word end space into the folded word. + mi.mi_fword[mi.mi_fwordlen - 1] = ' '; + } + // The word is bad unless we recognize it. mi.mi_result = SP_BAD; mi.mi_result2 = SP_BAD; diff --git a/src/nvim/syntax.c b/src/nvim/syntax.c index 4aa7c21ce4..9a9cc45c6b 100644 --- a/src/nvim/syntax.c +++ b/src/nvim/syntax.c @@ -5588,9 +5588,11 @@ void ex_ownsyntax(exarg_T *eap) hash_init(&curwin->w_s->b_keywtab_ic); // TODO: Keep the spell checking as it was. NOLINT(readability/todo) curwin->w_p_spell = false; // No spell checking + // make sure option values are "empty_option" instead of NULL clear_string_option(&curwin->w_s->b_p_spc); clear_string_option(&curwin->w_s->b_p_spf); clear_string_option(&curwin->w_s->b_p_spl); + clear_string_option(&curwin->w_s->b_p_spo); clear_string_option(&curwin->w_s->b_syn_isk); } diff --git a/src/nvim/testdir/check.vim b/src/nvim/testdir/check.vim index 073873bcb0..7f6b7dcfec 100644 --- a/src/nvim/testdir/check.vim +++ b/src/nvim/testdir/check.vim @@ -25,6 +25,14 @@ func CheckFunction(name) endif endfunc +" Command to check for the presence of python. Argument should have been +" obtained with PythonProg() +func CheckPython(name) + if a:name == '' + throw 'Skipped: python command not available' + endif +endfunc + " Command to check for running on MS-Windows command CheckMSWindows call CheckMSWindows() func CheckMSWindows() @@ -65,3 +73,27 @@ func CheckCanRunGui() throw 'Skipped: cannot start the GUI' endif endfunc + +" Command to check that not currently using the GUI +command CheckNotGui call CheckNotGui() +func CheckNotGui() + if has('gui_running') + throw 'Skipped: only works in the terminal' + endif +endfunc + +" Command to check that the current language is English +command CheckEnglish call CheckEnglish() +func CheckEnglish() + if v:lang != "C" && v:lang !~ '^[Ee]n' + throw 'Skipped: only works in English language environment' + endif +endfunc + +" Command to check for NOT running on MS-Windows +command CheckNotMSWindows call CheckNotMSWindows() +func CheckNotMSWindows() + if has('win32') + throw 'Skipped: does not work on MS-Windows' + endif +endfunc diff --git a/src/nvim/testdir/shared.vim b/src/nvim/testdir/shared.vim index 6180d542ff..9bc037a59f 100644 --- a/src/nvim/testdir/shared.vim +++ b/src/nvim/testdir/shared.vim @@ -294,6 +294,13 @@ func GetVimCommandClean() return cmd endfunc +" Get the command to run Vim, with --clean, and force to run in terminal so it +" won't start a new GUI. +func GetVimCommandCleanTerm() + " Add -v to have gvim run in the terminal (if possible) + return GetVimCommandClean() .. ' -v ' +endfunc + " Run Vim, using the "vimcmd" file and "-u NORC". " "before" is a list of Vim commands to be executed before loading plugins. " "after" is a list of Vim commands to be executed after loading plugins. @@ -329,3 +336,17 @@ func RunVimPiped(before, after, arguments, pipecmd) endif return 1 endfunc + +" Get all messages but drop the maintainer entry. +func GetMessages() + redir => result + redraw | messages + redir END + let msg_list = split(result, "\n") + " if msg_list->len() > 0 && msg_list[0] =~ 'Messages maintainer:' + " return msg_list[1:] + " endif + return msg_list +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_autocmd.vim b/src/nvim/testdir/test_autocmd.vim index 3dd68873d4..094bb3ebd1 100644 --- a/src/nvim/testdir/test_autocmd.vim +++ b/src/nvim/testdir/test_autocmd.vim @@ -1,6 +1,8 @@ " Tests for autocommands source shared.vim +source check.vim +source term_util.vim func! s:cleanup_buffers() abort for bnr in range(1, bufnr('$')) @@ -1735,6 +1737,35 @@ func Test_throw_in_BufWritePre() au! throwing endfunc +func Test_autocmd_CmdWinEnter() + CheckRunVimInTerminal + " There is not cmdwin switch, so + " test for cmdline_hist + " (both are available with small builds) + CheckFeature cmdline_hist + let lines =<< trim END + let b:dummy_var = 'This is a dummy' + autocmd CmdWinEnter * quit + let winnr = winnr('$') + END + let filename='XCmdWinEnter' + call writefile(lines, filename) + let buf = RunVimInTerminal('-S '.filename, #{rows: 6}) + + call term_sendkeys(buf, "q:") + call term_wait(buf) + call term_sendkeys(buf, ":echo b:dummy_var\<cr>") + call WaitForAssert({-> assert_match('^This is a dummy', term_getline(buf, 6))}, 1000) + call term_sendkeys(buf, ":echo &buftype\<cr>") + call WaitForAssert({-> assert_notmatch('^nofile', term_getline(buf, 6))}, 1000) + call term_sendkeys(buf, ":echo winnr\<cr>") + call WaitForAssert({-> assert_match('^1', term_getline(buf, 6))}, 1000) + + " clean up + call StopVimInTerminal(buf) + call delete(filename) +endfunc + func Test_FileChangedShell_reload() if !has('unix') return diff --git a/src/nvim/testdir/test_cmdline.vim b/src/nvim/testdir/test_cmdline.vim index f8d84f1a49..871143699a 100644 --- a/src/nvim/testdir/test_cmdline.vim +++ b/src/nvim/testdir/test_cmdline.vim @@ -1,5 +1,8 @@ " Tests for editing the command line. +source check.vim +source screendump.vim + func Test_complete_tab() call writefile(['testfile'], 'Xtestfile') call feedkeys(":e Xtestf\t\r", "tx") @@ -718,6 +721,27 @@ func Test_verbosefile() call delete('Xlog') endfunc +func Test_verbose_option() + " See test/functional/ui/cmdline_spec.lua + CheckScreendump + + let lines =<< trim [SCRIPT] + command DoSomething echo 'hello' |set ts=4 |let v = '123' |echo v + call feedkeys("\r", 't') " for the hit-enter prompt + set verbose=20 + [SCRIPT] + call writefile(lines, 'XTest_verbose') + + let buf = RunVimInTerminal('-S XTest_verbose', {'rows': 12}) + call term_wait(buf, 100) + call term_sendkeys(buf, ":DoSomething\<CR>") + call VerifyScreenDump(buf, 'Test_verbose_option_1', {}) + + " clean up + call StopVimInTerminal(buf) + call delete('XTest_verbose') +endfunc + func Test_setcmdpos() func InsertTextAtPos(text, pos) call assert_equal(0, setcmdpos(a:pos)) diff --git a/src/nvim/testdir/test_display.vim b/src/nvim/testdir/test_display.vim index 429253a863..e853b046dc 100644 --- a/src/nvim/testdir/test_display.vim +++ b/src/nvim/testdir/test_display.vim @@ -184,3 +184,40 @@ func Test_scroll_CursorLineNr_update() call StopVimInTerminal(buf) call delete(filename) endfunc + +" Test for scrolling that modifies buffer during visual block +func Test_visual_block_scroll() + " See test/functional/legacy/visual_mode_spec.lua + CheckScreendump + + let lines =<< trim END + source $VIMRUNTIME/plugin/matchparen.vim + set scrolloff=1 + call setline(1, ['a', 'b', 'c', 'd', 'e', '', '{', '}', '{', 'f', 'g', '}']) + call cursor(5, 1) + END + + let filename = 'Xvisualblockmodifiedscroll' + call writefile(lines, filename) + + let buf = RunVimInTerminal('-S '.filename, #{rows: 7}) + call term_sendkeys(buf, "V\<C-D>\<C-D>") + + call VerifyScreenDump(buf, 'Test_display_visual_block_scroll', {}) + + call StopVimInTerminal(buf) + call delete(filename) +endfunc + +func Test_display_scroll_at_topline() + " See test/functional/legacy/display_spec.lua + CheckScreendump + + let buf = RunVimInTerminal('', #{cols: 20}) + call term_sendkeys(buf, ":call setline(1, repeat('a', 21))\<CR>") + call term_wait(buf) + call term_sendkeys(buf, "O\<Esc>") + call VerifyScreenDump(buf, 'Test_display_scroll_at_topline', #{rows: 4}) + + call StopVimInTerminal(buf) +endfunc diff --git a/src/nvim/testdir/test_edit.vim b/src/nvim/testdir/test_edit.vim index 12d5d9790e..d4b1c63741 100644 --- a/src/nvim/testdir/test_edit.vim +++ b/src/nvim/testdir/test_edit.vim @@ -1,9 +1,11 @@ " Test for edit functions -" + if exists("+t_kD") let &t_kD="[3;*~" endif +source check.vim + " Needed for testing basic rightleft: Test_edit_rightleft source view_util.vim @@ -733,17 +735,16 @@ func! Test_edit_CTRL_O() endfunc func! Test_edit_CTRL_R() - throw 'skipped: Nvim does not support test_override()' " Insert Register new - call test_override("ALL", 1) + " call test_override("ALL", 1) set showcmd call feedkeys("AFOOBAR eins zwei\<esc>", 'tnix') call feedkeys("O\<c-r>.", 'tnix') call feedkeys("O\<c-r>=10*500\<cr>\<esc>", 'tnix') call feedkeys("O\<c-r>=getreg('=', 1)\<cr>\<esc>", 'tnix') call assert_equal(["getreg('=', 1)", '5000', "FOOBAR eins zwei", "FOOBAR eins zwei"], getline(1, '$')) - call test_override("ALL", 0) + " call test_override("ALL", 0) set noshowcmd bw! endfunc @@ -955,7 +956,6 @@ func! Test_edit_DROP() endfunc func! Test_edit_CTRL_V() - throw 'skipped: Nvim does not support test_override()' if has("ebcdic") return endif @@ -965,7 +965,7 @@ func! Test_edit_CTRL_V() " force some redraws set showmode showcmd "call test_override_char_avail(1) - call test_override('ALL', 1) + " call test_override('ALL', 1) call feedkeys("A\<c-v>\<c-n>\<c-v>\<c-l>\<c-v>\<c-b>\<esc>", 'tnix') call assert_equal(["abc\x0e\x0c\x02"], getline(1, '$')) @@ -978,7 +978,7 @@ func! Test_edit_CTRL_V() set norl endif - call test_override('ALL', 0) + " call test_override('ALL', 0) set noshowmode showcmd bw! endfunc @@ -1514,3 +1514,48 @@ func Test_edit_startinsert() set backspace& bwipe! endfunc + +func Test_edit_noesckeys() + CheckNotGui + new + + " <Left> moves cursor when 'esckeys' is set + exe "set t_kl=\<Esc>OD" + " set esckeys + call feedkeys("axyz\<Esc>ODX", "xt") + " call assert_equal("xyXz", getline(1)) + + " <Left> exits Insert mode when 'esckeys' is off + " set noesckeys + call setline(1, '') + call feedkeys("axyz\<Esc>ODX", "xt") + call assert_equal(["DX", "xyz"], getline(1, 2)) + + bwipe! + " set esckeys +endfunc + +" Test for editing a directory +func Test_edit_is_a_directory() + CheckEnglish + let dirname = getcwd() . "/Xdir" + call mkdir(dirname, 'p') + + new + redir => msg + exe 'edit' dirname + redir END + call assert_match("is a directory$", split(msg, "\n")[0]) + bwipe! + + let dirname .= '/' + + new + redir => msg + exe 'edit' dirname + redir END + call assert_match("is a directory$", split(msg, "\n")[0]) + bwipe! + + call delete(dirname, 'rf') +endfunc diff --git a/src/nvim/testdir/test_environ.vim b/src/nvim/testdir/test_environ.vim index 21bb09a690..a25d83753c 100644 --- a/src/nvim/testdir/test_environ.vim +++ b/src/nvim/testdir/test_environ.vim @@ -1,5 +1,9 @@ +" Test for environment variables. + scriptencoding utf-8 +source check.vim + func Test_environ() unlet! $TESTENV call assert_equal(0, has_key(environ(), 'TESTENV')) @@ -42,3 +46,24 @@ func Test_external_env() endif call assert_equal('', result) endfunc + +func Test_mac_locale() + CheckFeature osxdarwin + + " If $LANG is not set then the system locale will be used. + " Run Vim after unsetting all the locale environmental vars, and capture the + " output of :lang. + let lang_results = system("unset LANG; unset LC_MESSAGES; " .. + \ shellescape(v:progpath) .. + \ " --clean -esX -c 'redir @a' -c 'lang' -c 'put a' -c 'print' -c 'qa!' ") + + " Check that: + " 1. The locale is the form of <locale>.UTF-8. + " 2. Check that fourth item (LC_NUMERIC) is properly set to "C". + " Example match: "en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8" + call assert_match('"\([a-zA-Z_]\+\.UTF-8/\)\{3}C\(/[a-zA-Z_]\+\.UTF-8\)\{2}"', + \ lang_results, + \ "Default locale should have UTF-8 encoding set, and LC_NUMERIC set to 'C'") +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_filetype.vim b/src/nvim/testdir/test_filetype.vim index 617e3dfe41..c7ca682c8c 100644 --- a/src/nvim/testdir/test_filetype.vim +++ b/src/nvim/testdir/test_filetype.vim @@ -128,6 +128,7 @@ let s:filename_checks = { \ 'dart': ['file.dart', 'file.drt'], \ 'datascript': ['file.ds'], \ 'dcd': ['file.dcd'], + \ 'debchangelog': ['changelog.Debian', 'changelog.dch', 'NEWS.Debian', 'NEWS.dch', '/debian/changelog'], \ 'debcontrol': ['/debian/control'], \ 'debsources': ['/etc/apt/sources.list', '/etc/apt/sources.list.d/file.list'], \ 'def': ['file.def'], @@ -326,7 +327,7 @@ let s:filename_checks = { \ 'pamconf': ['/etc/pam.conf'], \ 'pamenv': ['/etc/security/pam_env.conf', '/home/user/.pam_environment'], \ 'papp': ['file.papp', 'file.pxml', 'file.pxsl'], - \ 'pascal': ['file.pas', 'file.dpr'], + \ 'pascal': ['file.pas', 'file.pp', 'file.dpr', 'file.lpr'], \ 'passwd': ['any/etc/passwd', 'any/etc/passwd-', 'any/etc/passwd.edit', 'any/etc/shadow', 'any/etc/shadow-', 'any/etc/shadow.edit', 'any/var/backups/passwd.bak', 'any/var/backups/shadow.bak'], \ 'pccts': ['file.g'], \ 'pdf': ['file.pdf'], @@ -455,7 +456,7 @@ let s:filename_checks = { \ 'texmf': ['texmf.cnf'], \ 'text': ['file.text', 'README'], \ 'tf': ['file.tf', '.tfrc', 'tfrc'], - \ 'tidy': ['.tidyrc', 'tidyrc'], + \ 'tidy': ['.tidyrc', 'tidyrc', 'tidy.conf'], \ 'tilde': ['file.t.html'], \ 'tli': ['file.tli'], \ 'tmux': ['tmuxfile.conf', '.tmuxfile.conf'], diff --git a/src/nvim/testdir/test_functions.vim b/src/nvim/testdir/test_functions.vim index 6b45ac61d1..8fa70a5313 100644 --- a/src/nvim/testdir/test_functions.vim +++ b/src/nvim/testdir/test_functions.vim @@ -1221,6 +1221,24 @@ func Test_reg_executing_and_recording() unlet s:reg_stat endfunc +func Test_getchar() + call feedkeys('a', '') + call assert_equal(char2nr('a'), getchar()) + + " call test_setmouse(1, 3) + " let v:mouse_win = 9 + " let v:mouse_winid = 9 + " let v:mouse_lnum = 9 + " let v:mouse_col = 9 + " call feedkeys("\<S-LeftMouse>", '') + call nvim_input_mouse('left', 'press', 'S', 0, 0, 2) + call assert_equal("\<S-LeftMouse>", getchar()) + call assert_equal(1, v:mouse_win) + call assert_equal(win_getid(1), v:mouse_winid) + call assert_equal(1, v:mouse_lnum) + call assert_equal(3, v:mouse_col) +endfunc + func Test_libcall_libcallnr() if !has('libcall') return @@ -1341,3 +1359,22 @@ func Test_readdir() call delete('Xdir', 'rf') endfunc + +" Test for the eval() function +func Test_eval() + call assert_fails("call eval('5 a')", 'E488:') +endfunc + +" Test for the nr2char() function +func Test_nr2char() + " set encoding=latin1 + call assert_equal('@', nr2char(64)) + set encoding=utf8 + call assert_equal('a', nr2char(97, 1)) + call assert_equal('a', nr2char(97, 0)) + + call assert_equal("\x80\xfc\b\xf4\x80\xfeX\x80\xfeX\x80\xfeX", eval('"\<M-' .. nr2char(0x100000) .. '>"')) + call assert_equal("\x80\xfc\b\xfd\x80\xfeX\x80\xfeX\x80\xfeX\x80\xfeX\x80\xfeX", eval('"\<M-' .. nr2char(0x40000000) .. '>"')) +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_gf.vim b/src/nvim/testdir/test_gf.vim index 4a4ffcefa1..ee548037ba 100644 --- a/src/nvim/testdir/test_gf.vim +++ b/src/nvim/testdir/test_gf.vim @@ -1,3 +1,4 @@ +" Test for the gf and gF (goto file) commands " This is a test if a URL is recognized by "gf", with the cursor before and " after the "://". Also test ":\\". @@ -109,7 +110,7 @@ func Test_gf() endfunc func Test_gf_visual() - call writefile([], "Xtest_gf_visual") + call writefile(['one', 'two', 'three', 'four'], "Xtest_gf_visual") new call setline(1, 'XXXtest_gf_visualXXX') set hidden @@ -118,6 +119,30 @@ func Test_gf_visual() norm! ttvtXgf call assert_equal('Xtest_gf_visual', bufname('%')) + " if multiple lines are selected, then gf should fail + call setline(1, ["one", "two"]) + normal VGgf + call assert_equal('Xtest_gf_visual', @%) + + " following line number is used for gF + bwipe! + new + call setline(1, 'XXXtest_gf_visual:3XXX') + norm! 0ttvt:gF + call assert_equal('Xtest_gf_visual', bufname('%')) + call assert_equal(3, getcurpos()[1]) + + " line number in visual area is used for file name + if has('unix') + bwipe! + call writefile([], "Xtest_gf_visual:3") + new + call setline(1, 'XXXtest_gf_visual:3XXX') + norm! 0ttvtXgF + call assert_equal('Xtest_gf_visual:3', bufname('%')) + call delete('Xtest_gf_visual:3') + endif + bwipe! call delete('Xtest_gf_visual') set hidden& diff --git a/src/nvim/testdir/test_highlight.vim b/src/nvim/testdir/test_highlight.vim index 6aa187b17e..00e42733a7 100644 --- a/src/nvim/testdir/test_highlight.vim +++ b/src/nvim/testdir/test_highlight.vim @@ -596,9 +596,17 @@ endfunc " This test must come before the Test_cursorline test, as it appears this " defines the Normal highlighting group anyway. func Test_1_highlight_Normalgroup_exists() - " MS-Windows GUI sets the font - if !has('win32') || !has('gui_running') - let hlNormal = HighlightArgs('Normal') + let hlNormal = HighlightArgs('Normal') + if !has('gui_running') call assert_match('hi Normal\s*clear', hlNormal) + elseif has('gui_gtk2') || has('gui_gnome') || has('gui_gtk3') + " expect is DEFAULT_FONT of gui_gtk_x11.c + call assert_match('hi Normal\s*font=Monospace 10', hlNormal) + elseif has('gui_motif') || has('gui_athena') + " expect is DEFAULT_FONT of gui_x11.c + call assert_match('hi Normal\s*font=7x13', hlNormal) + elseif has('win32') + " expect any font + call assert_match('hi Normal\s*font=.*', hlNormal) endif endfunc diff --git a/src/nvim/testdir/test_ins_complete.vim b/src/nvim/testdir/test_ins_complete.vim index 1c275d5bd1..1339a9f25d 100644 --- a/src/nvim/testdir/test_ins_complete.vim +++ b/src/nvim/testdir/test_ins_complete.vim @@ -1,3 +1,5 @@ +source screendump.vim +source check.vim " Test for insert expansion func Test_ins_complete() @@ -338,3 +340,27 @@ func Test_compl_in_cmdwin() delcom GetInput set wildmenu& wildchar& endfunc + +func Test_pum_with_folds_two_tabs() + CheckScreendump + + let lines =<< trim END + set fdm=marker + call setline(1, ['" x {{{1', '" a some text']) + call setline(3, range(&lines)->map({_, val -> '" a' .. val})) + norm! zm + tab sp + call feedkeys('2Gzv', 'xt') + call feedkeys("0fa", 'xt') + END + + call writefile(lines, 'Xpumscript') + let buf = RunVimInTerminal('-S Xpumscript', #{rows: 10}) + call term_wait(buf, 100) + call term_sendkeys(buf, "a\<C-N>") + call VerifyScreenDump(buf, 'Test_pum_with_folds_two_tabs', {}) + + call term_sendkeys(buf, "\<Esc>") + call StopVimInTerminal(buf) + call delete('Xpumscript') +endfunc diff --git a/src/nvim/testdir/test_lambda.vim b/src/nvim/testdir/test_lambda.vim index bfbb3e5c5b..f026c8a55f 100644 --- a/src/nvim/testdir/test_lambda.vim +++ b/src/nvim/testdir/test_lambda.vim @@ -181,7 +181,7 @@ function! Test_lambda_scope() let l:D = s:NewCounter2() call assert_equal(1, l:C()) - call assert_fails(':call l:D()', 'E15:') " E121: then E15: + call assert_fails(':call l:D()', 'E121:') call assert_equal(2, l:C()) endfunction diff --git a/src/nvim/testdir/test_listdict.vim b/src/nvim/testdir/test_listdict.vim index bea62cb0ad..31a8b48d37 100644 --- a/src/nvim/testdir/test_listdict.vim +++ b/src/nvim/testdir/test_listdict.vim @@ -280,6 +280,14 @@ func Test_dict_func_remove_in_use() call assert_equal(expected, d.func(string(remove(d, 'func')))) endfunc +func Test_dict_literal_keys() + call assert_equal({'one': 1, 'two2': 2, '3three': 3, '44': 4}, #{one: 1, two2: 2, 3three: 3, 44: 4},) + + " why *{} cannot be used + let blue = 'blue' + call assert_equal('6', trim(execute('echo 2 *{blue: 3}.blue'))) +endfunc + " Nasty: deepcopy() dict that refers to itself (fails when noref used) func Test_dict_deepcopy() let d = {1:1, 2:2} diff --git a/src/nvim/testdir/test_mapping.vim b/src/nvim/testdir/test_mapping.vim index 82562339f6..152afb4b9d 100644 --- a/src/nvim/testdir/test_mapping.vim +++ b/src/nvim/testdir/test_mapping.vim @@ -391,6 +391,42 @@ func Test_motionforce_omap() delfunc GetCommand endfunc +func Test_error_in_map_expr() + if !has('terminal') || (has('win32') && has('gui_running')) + throw 'Skipped: cannot run Vim in a terminal window' + endif + + let lines =<< trim [CODE] + func Func() + " fail to create list + let x = [ + endfunc + nmap <expr> ! Func() + set updatetime=50 + [CODE] + call writefile(lines, 'Xtest.vim') + + let buf = term_start(GetVimCommandCleanTerm() .. ' -S Xtest.vim', {'term_rows': 8}) + let job = term_getjob(buf) + call WaitForAssert({-> assert_notequal('', term_getline(buf, 8))}) + + " GC must not run during map-expr processing, which can make Vim crash. + call term_sendkeys(buf, '!') + call term_wait(buf, 100) + call term_sendkeys(buf, "\<CR>") + call term_wait(buf, 100) + call assert_equal('run', job_status(job)) + + call term_sendkeys(buf, ":qall!\<CR>") + call WaitFor({-> job_status(job) ==# 'dead'}) + if has('unix') + call assert_equal('', job_info(job).termsig) + endif + + call delete('Xtest.vim') + exe buf .. 'bwipe!' +endfunc + " Test for mapping errors func Test_map_error() call assert_fails('unmap', 'E474:') diff --git a/src/nvim/testdir/test_matchadd_conceal.vim b/src/nvim/testdir/test_matchadd_conceal.vim index b918525dbc..393e183ddb 100644 --- a/src/nvim/testdir/test_matchadd_conceal.vim +++ b/src/nvim/testdir/test_matchadd_conceal.vim @@ -1,9 +1,11 @@ " Test for matchadd() and conceal feature -if !has('conceal') - finish -endif + +source check.vim +CheckFeature conceal source shared.vim +source term_util.vim +source view_util.vim function! Test_simple_matchadd() new @@ -273,3 +275,70 @@ function! Test_matchadd_and_syn_conceal() call assert_notequal(screenattr(1, 11) , screenattr(1, 12)) call assert_equal(screenattr(1, 11) , screenattr(1, 32)) endfunction + +func Test_cursor_column_in_concealed_line_after_window_scroll() + CheckRunVimInTerminal + + " Test for issue #5012 fix. + " For a concealed line with cursor, there should be no window's cursor + " position invalidation during win_update() after scrolling attempt that is + " not successful and no real topline change happens. The invalidation would + " cause a window's cursor position recalc outside of win_line() where it's + " not possible to take conceal into account. + let lines =<< trim END + 3split + let m = matchadd('Conceal', '=') + setl conceallevel=2 concealcursor=nc + normal gg + "==expr== + END + call writefile(lines, 'Xcolesearch') + let buf = RunVimInTerminal('Xcolesearch', {}) + call term_wait(buf, 100) + + " Jump to something that is beyond the bottom of the window, + " so there's a scroll down. + call term_sendkeys(buf, ":so %\<CR>") + call term_wait(buf, 100) + call term_sendkeys(buf, "/expr\<CR>") + call term_wait(buf, 100) + + " Are the concealed parts of the current line really hidden? + let cursor_row = term_scrape(buf, '.')->map({_, e -> e.chars})->join('') + call assert_equal('"expr', cursor_row) + + " BugFix check: Is the window's cursor column properly updated for hidden + " parts of the current line? + call assert_equal(2, term_getcursor(buf)[1]) + + call StopVimInTerminal(buf) + call delete('Xcolesearch') +endfunc + +func Test_cursor_column_in_concealed_line_after_leftcol_change() + CheckRunVimInTerminal + + " Test for issue #5214 fix. + let lines =<< trim END + 0put = 'ab' .. repeat('-', &columns) .. 'c' + call matchadd('Conceal', '-') + set nowrap ss=0 cole=3 cocu=n + END + call writefile(lines, 'Xcurs-columns') + let buf = RunVimInTerminal('-S Xcurs-columns', {}) + + " Go to the end of the line (3 columns beyond the end of the screen). + " Horizontal scroll would center the cursor in the screen line, but conceal + " makes it go to screen column 1. + call term_sendkeys(buf, "$") + call term_wait(buf) + + " Are the concealed parts of the current line really hidden? + call WaitForAssert({-> assert_equal('c', term_getline(buf, '.'))}) + + " BugFix check: Is the window's cursor column properly updated for conceal? + call assert_equal(1, term_getcursor(buf)[1]) + + call StopVimInTerminal(buf) + call delete('Xcurs-columns') +endfunc diff --git a/src/nvim/testdir/test_popup.vim b/src/nvim/testdir/test_popup.vim index bb0ed6e00c..fb464d95ea 100644 --- a/src/nvim/testdir/test_popup.vim +++ b/src/nvim/testdir/test_popup.vim @@ -2,6 +2,7 @@ source shared.vim source screendump.vim +source check.vim let g:months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] let g:setting = '' @@ -755,6 +756,52 @@ func Test_popup_and_previewwindow_dump() call delete('Xscript') endfunc +func Test_balloon_split() + CheckFunction balloon_split + + call assert_equal([ + \ 'tempname: 0x555555e380a0 "/home/mool/.viminfz.tmp"', + \ ], balloon_split( + \ 'tempname: 0x555555e380a0 "/home/mool/.viminfz.tmp"')) + call assert_equal([ + \ 'one two three four one two three four one two thre', + \ 'e four', + \ ], balloon_split( + \ 'one two three four one two three four one two three four')) + + eval 'struct = {one = 1, two = 2, three = 3}' + \ ->balloon_split() + \ ->assert_equal([ + \ 'struct = {', + \ ' one = 1,', + \ ' two = 2,', + \ ' three = 3}', + \ ]) + + call assert_equal([ + \ 'struct = {', + \ ' one = 1,', + \ ' nested = {', + \ ' n1 = "yes",', + \ ' n2 = "no"}', + \ ' two = 2}', + \ ], balloon_split( + \ 'struct = {one = 1, nested = {n1 = "yes", n2 = "no"} two = 2}')) + call assert_equal([ + \ 'struct = 0x234 {', + \ ' long = 2343 "\\"some long string that will be wr', + \ 'apped in two\\"",', + \ ' next = 123}', + \ ], balloon_split( + \ 'struct = 0x234 {long = 2343 "\\"some long string that will be wrapped in two\\"", next = 123}')) + call assert_equal([ + \ 'Some comment', + \ '', + \ 'typedef this that;', + \ ], balloon_split( + \ "Some comment\n\ntypedef this that;")) +endfunc + func Test_popup_position() if !CanRunVimInTerminal() return diff --git a/src/nvim/testdir/test_quickfix.vim b/src/nvim/testdir/test_quickfix.vim index 0178413a8e..4090db2874 100644 --- a/src/nvim/testdir/test_quickfix.vim +++ b/src/nvim/testdir/test_quickfix.vim @@ -95,7 +95,7 @@ func XlistTests(cchar) " Populate the list and then try Xgetexpr ['non-error 1', 'Xtestfile1:1:3:Line1', \ 'non-error 2', 'Xtestfile2:2:2:Line2', - \ 'non-error 3', 'Xtestfile3:3:1:Line3'] + \ 'non-error| 3', 'Xtestfile3:3:1:Line3'] " List only valid entries let l = split(execute('Xlist', ''), "\n") @@ -107,7 +107,7 @@ func XlistTests(cchar) let l = split(execute('Xlist!', ''), "\n") call assert_equal([' 1: non-error 1', ' 2 Xtestfile1:1 col 3: Line1', \ ' 3: non-error 2', ' 4 Xtestfile2:2 col 2: Line2', - \ ' 5: non-error 3', ' 6 Xtestfile3:3 col 1: Line3'], l) + \ ' 5: non-error| 3', ' 6 Xtestfile3:3 col 1: Line3'], l) " List a range of errors let l = split(execute('Xlist 3,6', ''), "\n") @@ -507,7 +507,7 @@ func Xtest_browse(cchar) Xexpr "" call assert_equal(0, g:Xgetlist({'idx' : 0}).idx) call assert_equal(0, g:Xgetlist({'size' : 0}).size) - Xaddexpr ['foo', 'bar', 'baz', 'quux', 'shmoo'] + Xaddexpr ['foo', 'bar', 'baz', 'quux', 'sh|moo'] call assert_equal(5, g:Xgetlist({'size' : 0}).size) Xlast call assert_equal(5, g:Xgetlist({'idx' : 0}).idx) @@ -1621,6 +1621,24 @@ func Test_long_lines() call s:long_lines_tests('l') endfunc +func Test_cgetfile_on_long_lines() + " Problematic values if the line is longer than 4096 bytes. Then 1024 bytes + " are read at a time. + for len in [4078, 4079, 4080, 5102, 5103, 5104, 6126, 6127, 6128, 7150, 7151, 7152] + let lines = [ + \ '/tmp/file1:1:1:aaa', + \ '/tmp/file2:1:1:%s', + \ '/tmp/file3:1:1:bbb', + \ '/tmp/file4:1:1:ccc', + \ ] + let lines[1] = substitute(lines[1], '%s', repeat('x', len), '') + call writefile(lines, 'Xcqetfile.txt') + cgetfile Xcqetfile.txt + call assert_equal(4, getqflist(#{size: v:true}).size, 'with length ' .. len) + endfor + call delete('Xcqetfile.txt') +endfunc + func s:create_test_file(filename) let l = [] for i in range(1, 20) @@ -2474,6 +2492,22 @@ func Test_vimgrep() call XvimgrepTests('l') endfunc +" Test for incsearch highlighting of the :vimgrep pattern +" This test used to cause "E315: ml_get: invalid lnum" errors. +func Test_vimgrep_incsearch() + throw 'skipped: Nvim does not support test_override()' + enew + set incsearch + call test_override("char_avail", 1) + + call feedkeys(":2vimgrep assert test_quickfix.vim test_cdo.vim\<CR>", "ntx") + let l = getqflist() + call assert_equal(2, len(l)) + + call test_override("ALL", 0) + set noincsearch +endfunc + func XfreeTests(cchar) call s:setup_commands(a:cchar) @@ -3523,6 +3557,18 @@ func Test_shorten_fname() " Displaying the quickfix list should simplify the file path silent! clist call assert_equal('test_quickfix.vim', bufname('test_quickfix.vim')) + " Add a few entries for the same file with different paths and check whether + " the buffer name is shortened + %bwipe + call setqflist([], 'f') + call setqflist([{'filename' : 'test_quickfix.vim', 'lnum' : 10}, + \ {'filename' : '../testdir/test_quickfix.vim', 'lnum' : 20}, + \ {'filename' : fname, 'lnum' : 30}], ' ') + copen + call assert_equal(['test_quickfix.vim|10| ', + \ 'test_quickfix.vim|20| ', + \ 'test_quickfix.vim|30| '], getline(1, '$')) + cclose endfunc " Quickfix title tests @@ -4087,4 +4133,46 @@ func Test_qfcmd_abort() augroup END endfunc +" Test for using a file in one of the parent directories. +func Test_search_in_dirstack() + call mkdir('Xtestdir/a/b/c', 'p') + let save_cwd = getcwd() + call writefile(["X1_L1", "X1_L2"], 'Xtestdir/Xfile1') + call writefile(["X2_L1", "X2_L2"], 'Xtestdir/a/Xfile2') + call writefile(["X3_L1", "X3_L2"], 'Xtestdir/a/b/Xfile3') + call writefile(["X4_L1", "X4_L2"], 'Xtestdir/a/b/c/Xfile4') + + let lines = "Entering dir Xtestdir\n" . + \ "Entering dir a\n" . + \ "Entering dir b\n" . + \ "Xfile2:2:X2_L2\n" . + \ "Leaving dir a\n" . + \ "Xfile1:2:X1_L2\n" . + \ "Xfile3:1:X3_L1\n" . + \ "Entering dir c\n" . + \ "Xfile4:2:X4_L2\n" . + \ "Leaving dir c\n" + set efm=%DEntering\ dir\ %f,%XLeaving\ dir\ %f,%f:%l:%m + cexpr lines .. "Leaving dir Xtestdir|\n" | let next = 1 + call assert_equal(11, getqflist({'size' : 0}).size) + call assert_equal(4, getqflist({'idx' : 0}).idx) + call assert_equal('X2_L2', getline('.')) + call assert_equal(1, next) + cnext + call assert_equal(6, getqflist({'idx' : 0}).idx) + call assert_equal('X1_L2', getline('.')) + cnext + call assert_equal(7, getqflist({'idx' : 0}).idx) + call assert_equal(1, line('$')) + call assert_equal('', getline(1)) + cnext + call assert_equal(9, getqflist({'idx' : 0}).idx) + call assert_equal(1, line('$')) + call assert_equal('', getline(1)) + + set efm& + exe 'cd ' . save_cwd + call delete('Xtestdir', 'rf') +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_search.vim b/src/nvim/testdir/test_search.vim index 767cf99be3..5db23c22a8 100644 --- a/src/nvim/testdir/test_search.vim +++ b/src/nvim/testdir/test_search.vim @@ -244,6 +244,10 @@ func Test_search_cmdline2() " go to previous match (on line 2) call feedkeys("/the\<C-G>\<C-G>\<C-G>\<C-T>\<C-T>\<C-T>\<cr>", 'tx') call assert_equal(' 2 these', getline('.')) + 1 + " go to previous match (on line 2) + call feedkeys("/the\<C-G>\<C-R>\<C-W>\<cr>", 'tx') + call assert_equal('theother', @/) " Test 2: keep the view, " after deleting a character from the search cmd @@ -255,7 +259,7 @@ func Test_search_cmdline2() call assert_equal({'lnum': 10, 'leftcol': 0, 'col': 4, 'topfill': 0, 'topline': 6, 'coladd': 0, 'skipcol': 0, 'curswant': 4}, winsaveview()) " remove all history entries - for i in range(10) + for i in range(11) call histdel('/') endfor @@ -489,14 +493,14 @@ func Test_search_cmdline5() " Do not call test_override("char_avail", 1) so that <C-g> and <C-t> work " regardless char_avail. new - call setline(1, [' 1 the first', ' 2 the second', ' 3 the third']) + call setline(1, [' 1 the first', ' 2 the second', ' 3 the third', '']) set incsearch 1 call feedkeys("/the\<c-g>\<c-g>\<cr>", 'tx') call assert_equal(' 3 the third', getline('.')) $ call feedkeys("?the\<c-t>\<c-t>\<c-t>\<cr>", 'tx') - call assert_equal(' 2 the second', getline('.')) + call assert_equal(' 1 the first', getline('.')) " clean up set noincsearch bw! @@ -704,6 +708,19 @@ func Test_incsearch_substitute_dump() call VerifyScreenDump(buf, 'Test_incsearch_substitute_12', {}) call term_sendkeys(buf, "\<Esc>") call VerifyScreenDump(buf, 'Test_incsearch_substitute_13', {}) + call term_sendkeys(buf, ":%bwipe!\<CR>") + call term_sendkeys(buf, ":only!\<CR>") + + " get :'<,'>s command in history + call term_sendkeys(buf, ":set cmdheight=2\<CR>") + call term_sendkeys(buf, "aasdfasdf\<Esc>") + call term_sendkeys(buf, "V:s/a/b/g\<CR>") + " Using '<,'> does not give E20 + call term_sendkeys(buf, ":new\<CR>") + call term_sendkeys(buf, "aasdfasdf\<Esc>") + call term_sendkeys(buf, ":\<Up>\<Up>") + call VerifyScreenDump(buf, 'Test_incsearch_substitute_14', {}) + call term_sendkeys(buf, "<Esc>") call StopVimInTerminal(buf) call delete('Xis_subst_script') @@ -726,11 +743,14 @@ func Test_incsearch_sort_dump() " the 'ambiwidth' check. sleep 100m - " Need to send one key at a time to force a redraw. call term_sendkeys(buf, ':sort ni u /on') call VerifyScreenDump(buf, 'Test_incsearch_sort_01', {}) call term_sendkeys(buf, "\<Esc>") + call term_sendkeys(buf, ':sort! /on') + call VerifyScreenDump(buf, 'Test_incsearch_sort_02', {}) + call term_sendkeys(buf, "\<Esc>") + call StopVimInTerminal(buf) call delete('Xis_sort_script') endfunc @@ -861,6 +881,21 @@ func Test_incsearch_with_change() call delete('Xis_change_script') endfunc +func Test_incsearch_cmdline_modifier() + throw 'skipped: Nvim does not support test_override()' + if !exists('+incsearch') + return + endif + call test_override("char_avail", 1) + new + call setline(1, ['foo']) + set incsearch + " Test that error E14 does not occur in parsing command modifier. + call feedkeys("V:tab", 'tx') + + call Incsearch_cleanup() +endfunc + func Test_incsearch_scrolling() if !CanRunVimInTerminal() return @@ -946,6 +981,21 @@ func Test_incsearch_substitute() call Incsearch_cleanup() endfunc +func Test_incsearch_substitute_long_line() + throw 'skipped: Nvim does not support test_override()' + new + call test_override("char_avail", 1) + set incsearch + + call repeat('x', 100000)->setline(1) + call feedkeys(':s/\%c', 'xt') + redraw + call feedkeys("\<Esc>", 'xt') + + call Incsearch_cleanup() + bwipe! +endfunc + func Test_search_undefined_behaviour() if !has("terminal") return @@ -982,6 +1032,40 @@ func Test_search_sentence() / endfunc +" Test that there is no crash when there is a last search pattern but no last +" substitute pattern. +func Test_no_last_substitute_pat() + " Use viminfo to set the last search pattern to a string and make the last + " substitute pattern the most recent used and make it empty (NULL). + call writefile(['~MSle0/bar', '~MSle0~&'], 'Xviminfo') + rviminfo! Xviminfo + call assert_fails('normal n', 'E35:') + + call delete('Xviminfo') +endfunc + +func Test_search_Ctrl_L_combining() + " Make sure, that Ctrl-L works correctly with combining characters. + " It uses an artificial example of an 'a' with 4 combining chars: + " 'a' U+0061 Dec:97 LATIN SMALL LETTER A a /\%u61\Z "\u0061" + " ' ̀' U+0300 Dec:768 COMBINING GRAVE ACCENT ̀ /\%u300\Z "\u0300" + " ' ́' U+0301 Dec:769 COMBINING ACUTE ACCENT ́ /\%u301\Z "\u0301" + " ' ̇' U+0307 Dec:775 COMBINING DOT ABOVE ̇ /\%u307\Z "\u0307" + " ' ̣' U+0323 Dec:803 COMBINING DOT BELOW ̣ /\%u323 "\u0323" + " Those should also appear on the commandline + if !has('multi_byte') || !exists('+incsearch') + return + endif + call Cmdline3_prep() + 1 + let bufcontent = ['', 'Miạ̀́̇m'] + call append('$', bufcontent) + call feedkeys("/Mi\<c-l>\<c-l>\<cr>", 'tx') + call assert_equal(5, line('.')) + call assert_equal(bufcontent[1], @/) + call Incsearch_cleanup() +endfunc + func Test_large_hex_chars1() " This used to cause a crash, the character becomes an NFA state. try @@ -1019,6 +1103,24 @@ func Test_one_error_msg() call assert_fails('call search(" \\((\\v[[=P=]]){185}+ ")', 'E871:') endfunc +func Test_incsearch_add_char_under_cursor() + throw 'skipped: Nvim does not support test_override()' + if !exists('+incsearch') + return + endif + set incsearch + new + call setline(1, ['find match', 'anything']) + 1 + call test_override('char_avail', 1) + call feedkeys("fc/m\<C-L>\<C-L>\<C-L>\<C-L>\<C-L>\<CR>", 'tx') + call assert_equal('match', @/) + call test_override('char_avail', 0) + + set incsearch& + bwipe! +endfunc + " Test for the search() function with match at the cursor position func Test_search_match_at_curpos() new diff --git a/src/nvim/testdir/test_search_stat.vim b/src/nvim/testdir/test_search_stat.vim index cf36f3214a..11c6489ca2 100644 --- a/src/nvim/testdir/test_search_stat.vim +++ b/src/nvim/testdir/test_search_stat.vim @@ -1,11 +1,9 @@ " Tests for search_stats, when "S" is not in 'shortmess' -" -" This test is fragile, it might not work interactively, but it works when run -" as test! -source shared.vim +source screendump.vim +source check.vim -func! Test_search_stat() +func Test_search_stat() new set shortmess-=S " Append 50 lines with text to search for, "foobar" appears 20 times @@ -45,7 +43,7 @@ func! Test_search_stat() call assert_match(pat .. stat, g:a) call cursor(line('$'), 1) let g:a = execute(':unsilent :norm! n') - let stat = '\[1/>99\] W' + let stat = 'W \[1/>99\]' call assert_match(pat .. stat, g:a) " Many matches @@ -55,7 +53,7 @@ func! Test_search_stat() call assert_match(pat .. stat, g:a) call cursor(1, 1) let g:a = execute(':unsilent :norm! N') - let stat = '\[>99/>99\] W' + let stat = 'W \[>99/>99\]' call assert_match(pat .. stat, g:a) " right-left @@ -87,7 +85,7 @@ func! Test_search_stat() call cursor('$',1) let pat = 'raboof/\s\+' let g:a = execute(':unsilent :norm! n') - let stat = '\[20/1\]' + let stat = 'W \[20/1\]' call assert_match(pat .. stat, g:a) call assert_match('search hit BOTTOM, continuing at TOP', g:a) set norl @@ -98,10 +96,10 @@ func! Test_search_stat() let @/ = 'foobar' let pat = '?foobar\s\+' let g:a = execute(':unsilent :norm! N') - let stat = '\[20/20\]' + let stat = 'W \[20/20\]' call assert_match(pat .. stat, g:a) call assert_match('search hit TOP, continuing at BOTTOM', g:a) - call assert_match('\[20/20\] W', Screenline(&lines)) + call assert_match('W \[20/20\]', Screenline(&lines)) " normal, no match call cursor(1,1) @@ -160,7 +158,115 @@ func! Test_search_stat() let stat = '\[1/2\]' call assert_notmatch(pat .. stat, g:a) - " close the window + " normal, n comes from a silent mapping + " First test a normal mapping, then a silent mapping + call cursor(1,1) + nnoremap n n + let @/ = 'find this' + let pat = '/find this\s\+' + let g:a = execute(':unsilent :norm n') + let g:b = split(g:a, "\n")[-1] + let stat = '\[1/2\]' + call assert_match(pat .. stat, g:b) + nnoremap <silent> n n + call cursor(1,1) + let g:a = execute(':unsilent :norm n') + let g:b = split(g:a, "\n")[-1] + let stat = '\[1/2\]' + call assert_notmatch(pat .. stat, g:b) + call assert_match(stat, g:b) + " Test that the message is not truncated + " it would insert '...' into the output. + call assert_match('^\s\+' .. stat, g:b) + unmap n + + " Clean up set shortmess+=S + " close the window bwipe! endfunc + +func Test_search_stat_foldopen() + CheckScreendump + + let lines =<< trim END + set shortmess-=S + setl foldenable foldmethod=indent foldopen-=search + call append(0, ['if', "\tfoo", "\tfoo", 'endif']) + let @/ = 'foo' + call cursor(1,1) + norm n + END + call writefile(lines, 'Xsearchstat1') + + let buf = RunVimInTerminal('-S Xsearchstat1', #{rows: 10}) + call TermWait(buf) + call VerifyScreenDump(buf, 'Test_searchstat_3', {}) + + call term_sendkeys(buf, "n") + call TermWait(buf) + call VerifyScreenDump(buf, 'Test_searchstat_3', {}) + + call term_sendkeys(buf, "n") + call TermWait(buf) + call VerifyScreenDump(buf, 'Test_searchstat_3', {}) + + call StopVimInTerminal(buf) + call delete('Xsearchstat1') +endfunc + +func! Test_search_stat_screendump() + CheckScreendump + + let lines =<< trim END + set shortmess-=S + " Append 50 lines with text to search for, "foobar" appears 20 times + call append(0, repeat(['foobar', 'foo', 'fooooobar', 'foba', 'foobar'], 20)) + call setline(2, 'find this') + call setline(70, 'find this') + nnoremap n n + let @/ = 'find this' + call cursor(1,1) + norm n + END + call writefile(lines, 'Xsearchstat') + let buf = RunVimInTerminal('-S Xsearchstat', #{rows: 10}) + call term_wait(buf) + call VerifyScreenDump(buf, 'Test_searchstat_1', {}) + + call term_sendkeys(buf, ":nnoremap <silent> n n\<cr>") + call term_sendkeys(buf, "gg0n") + call term_wait(buf) + call VerifyScreenDump(buf, 'Test_searchstat_2', {}) + + call StopVimInTerminal(buf) + call delete('Xsearchstat') +endfunc + +func Test_searchcount_in_statusline() + CheckScreendump + + let lines =<< trim END + set shortmess-=S + call append(0, 'this is something') + function TestSearchCount() abort + let search_count = searchcount() + if !empty(search_count) + return '[' . search_count.current . '/' . search_count.total . ']' + else + return '' + endif + endfunction + set hlsearch + set laststatus=2 statusline+=%{TestSearchCount()} + END + call writefile(lines, 'Xsearchstatusline') + let buf = RunVimInTerminal('-S Xsearchstatusline', #{rows: 10}) + call TermWait(buf) + call term_sendkeys(buf, "/something") + call VerifyScreenDump(buf, 'Test_searchstat_4', {}) + + call term_sendkeys(buf, "\<Esc>") + call StopVimInTerminal(buf) + call delete('Xsearchstatusline') +endfunc diff --git a/src/nvim/testdir/test_spell.vim b/src/nvim/testdir/test_spell.vim index 414c7278eb..ab8a998bb8 100644 --- a/src/nvim/testdir/test_spell.vim +++ b/src/nvim/testdir/test_spell.vim @@ -79,6 +79,11 @@ func Test_spellbadword() call assert_equal(['bycycle', 'bad'], spellbadword('My bycycle.')) call assert_equal(['another', 'caps'], spellbadword('A sentence. another sentence')) + call assert_equal(['TheCamelWord', 'bad'], spellbadword('TheCamelWord asdf')) + set spelloptions=camel + call assert_equal(['asdf', 'bad'], spellbadword('TheCamelWord asdf')) + set spelloptions= + set spelllang=en call assert_equal(['', ''], spellbadword('centre')) call assert_equal(['', ''], spellbadword('center')) @@ -113,6 +118,43 @@ foobar/? set spell& endfunc +func Test_spelllang_inv_region() + set spell spelllang=en_xx + let messages = GetMessages() + call assert_equal('Warning: region xx not supported', messages[-1]) + set spell& spelllang& +endfunc + +func Test_compl_with_CTRL_X_CTRL_K_using_spell() + " When spell checking is enabled and 'dictionary' is empty, + " CTRL-X CTRL-K in insert mode completes using the spelling dictionary. + new + set spell spelllang=en dictionary= + + set ignorecase + call feedkeys("Senglis\<c-x>\<c-k>\<esc>", 'tnx') + call assert_equal(['English'], getline(1, '$')) + call feedkeys("SEnglis\<c-x>\<c-k>\<esc>", 'tnx') + call assert_equal(['English'], getline(1, '$')) + + set noignorecase + call feedkeys("Senglis\<c-x>\<c-k>\<esc>", 'tnx') + call assert_equal(['englis'], getline(1, '$')) + call feedkeys("SEnglis\<c-x>\<c-k>\<esc>", 'tnx') + call assert_equal(['English'], getline(1, '$')) + + set spelllang=en_us + call feedkeys("Stheat\<c-x>\<c-k>\<esc>", 'tnx') + call assert_equal(['theater'], getline(1, '$')) + set spelllang=en_gb + call feedkeys("Stheat\<c-x>\<c-k>\<esc>", 'tnx') + " FIXME: commented out, expected theatre bug got theater. See issue #7025. + " call assert_equal(['theatre'], getline(1, '$')) + + bwipe! + set spell& spelllang& dictionary& ignorecase& +endfunc + func Test_spellreall() new set spell diff --git a/src/nvim/testdir/test_startup.vim b/src/nvim/testdir/test_startup.vim index 9abaca5957..12bec745a8 100644 --- a/src/nvim/testdir/test_startup.vim +++ b/src/nvim/testdir/test_startup.vim @@ -369,12 +369,11 @@ func Test_invalid_args() endfor if has('clientserver') - " FIXME: need to add --servername to this list - " but it causes vim-8.1.1282 to crash! for opt in ['--remote', '--remote-send', '--remote-silent', '--remote-expr', \ '--remote-tab', '--remote-tab-wait', \ '--remote-tab-wait-silent', '--remote-tab-silent', \ '--remote-wait', '--remote-wait-silent', + \ '--servername', \ ] let out = split(system(GetVimCommand() .. ' ' .. opt), "\n") call assert_equal(1, v:shell_error) @@ -384,14 +383,21 @@ func Test_invalid_args() endfor endif - " FIXME: commented out as this causes vim-8.1.1282 to crash! - "if has('clipboard') - " let out = split(system(GetVimCommand() .. ' --display'), "\n") - " call assert_equal(1, v:shell_error) - " call assert_match('^VIM - Vi IMproved .* (.*)$', out[0]) - " call assert_equal('Argument missing after: "--display"', out[1]) - " call assert_equal('More info with: "vim -h"', out[2]) - "endif + if has('gui_gtk') + let out = split(system(GetVimCommand() .. ' --display'), "\n") + call assert_equal(1, v:shell_error) + call assert_match('^VIM - Vi IMproved .* (.*)$', out[0]) + call assert_equal('Argument missing after: "--display"', out[1]) + call assert_equal('More info with: "vim -h"', out[2]) + endif + + if has('xterm_clipboard') + let out = split(system(GetVimCommand() .. ' -display'), "\n") + call assert_equal(1, v:shell_error) + call assert_match('^VIM - Vi IMproved .* (.*)$', out[0]) + call assert_equal('Argument missing after: "-display"', out[1]) + call assert_equal('More info with: "vim -h"', out[2]) + endif let out = split(system(GetVimCommand() .. ' -ix'), "\n") call assert_equal(1, v:shell_error) diff --git a/src/nvim/testdir/test_statusline.vim b/src/nvim/testdir/test_statusline.vim index 8c81ec3431..7efd181d04 100644 --- a/src/nvim/testdir/test_statusline.vim +++ b/src/nvim/testdir/test_statusline.vim @@ -412,3 +412,22 @@ func Test_statusline_removed_group() call StopVimInTerminal(buf) call delete('XTest_statusline') endfunc + +func Test_statusline_after_split_vsplit() + only + + " Make the status line of each window show the window number. + set ls=2 stl=%{winnr()} + + split | redraw + vsplit | redraw + + " The status line of the third window should read '3' here. + call assert_equal('3', nr2char(screenchar(&lines - 1, 1))) + + only + set ls& stl& +endfunc + + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_syntax.vim b/src/nvim/testdir/test_syntax.vim index 85ee42420e..2404f113d9 100644 --- a/src/nvim/testdir/test_syntax.vim +++ b/src/nvim/testdir/test_syntax.vim @@ -369,7 +369,11 @@ func Test_ownsyntax() call setline(1, '#define FOO') syntax on set filetype=c + ownsyntax perl + " this should not crash + set + call assert_equal('perlComment', synIDattr(synID(line('.'), col('.'), 1), 'name')) call assert_equal('c', b:current_syntax) call assert_equal('perl', w:current_syntax) diff --git a/src/nvim/testdir/test_tagjump.vim b/src/nvim/testdir/test_tagjump.vim index 6abe5b7c89..7057cdefb2 100644 --- a/src/nvim/testdir/test_tagjump.vim +++ b/src/nvim/testdir/test_tagjump.vim @@ -1,5 +1,8 @@ " Tests for tagjump (tags and special searches) +source check.vim +source screendump.vim + " SEGV occurs in older versions. (At least 7.4.1748 or older) func Test_ptag_with_notagstack() set notagstack @@ -551,6 +554,37 @@ func Test_tag_line_toolong() let &verbose = old_vbs endfunc +" Check that using :tselect does not run into the hit-enter prompt. +" Requires a terminal to trigger that prompt. +func Test_tselect() + CheckScreendump + + call writefile([ + \ 'main Xtest.h /^void test();$/;" f', + \ 'main Xtest.c /^int main()$/;" f', + \ 'main Xtest.x /^void test()$/;" f', + \ ], 'Xtags') + cal writefile([ + \ 'int main()', + \ 'void test()', + \ ], 'Xtest.c') + + let lines =<< trim [SCRIPT] + set tags=Xtags + [SCRIPT] + call writefile(lines, 'XTest_tselect') + let buf = RunVimInTerminal('-S XTest_tselect', {'rows': 10, 'cols': 50}) + + call term_wait(buf, 100) + call term_sendkeys(buf, ":tselect main\<CR>2\<CR>") + call VerifyScreenDump(buf, 'Test_tselect_1', {}) + + call StopVimInTerminal(buf) + call delete('Xtags') + call delete('Xtest.c') + call delete('XTest_tselect') +endfunc + func Test_tagline() call writefile([ \ 'provision Xtest.py /^ def provision(self, **kwargs):$/;" m line:1 language:Python class:Foo', diff --git a/src/nvim/testdir/test_timers.vim b/src/nvim/testdir/test_timers.vim index cffd80ff4f..13971a918d 100644 --- a/src/nvim/testdir/test_timers.vim +++ b/src/nvim/testdir/test_timers.vim @@ -233,16 +233,17 @@ func Test_timer_catch_error() endfunc func FeedAndPeek(timer) - call test_feedinput('a') + " call test_feedinput('a') + call nvim_input('a') call getchar(1) endfunc func Interrupt(timer) - call test_feedinput("\<C-C>") + " call test_feedinput("\<C-C>") + call nvim_input("\<C-C>") endfunc func Test_peek_and_get_char() - throw 'skipped: Nvim does not support test_feedinput()' if !has('unix') && !has('gui_running') return endif @@ -339,6 +340,41 @@ func Test_nocatch_garbage_collect() delfunc FeedChar endfunc +func Test_error_in_timer_callback() + if !has('terminal') || (has('win32') && has('gui_running')) + throw 'Skipped: cannot run Vim in a terminal window' + endif + + let lines =<< trim [CODE] + func Func(timer) + " fail to create list + let x = [ + endfunc + set updatetime=50 + call timer_start(1, 'Func') + [CODE] + call writefile(lines, 'Xtest.vim') + + let buf = term_start(GetVimCommandCleanTerm() .. ' -S Xtest.vim', {'term_rows': 8}) + let job = term_getjob(buf) + call WaitForAssert({-> assert_notequal('', term_getline(buf, 8))}) + + " GC must not run during timer callback, which can make Vim crash. + call term_wait(buf, 100) + call term_sendkeys(buf, "\<CR>") + call term_wait(buf, 100) + call assert_equal('run', job_status(job)) + + call term_sendkeys(buf, ":qall!\<CR>") + call WaitFor({-> job_status(job) ==# 'dead'}) + if has('unix') + call assert_equal('', job_info(job).termsig) + endif + + call delete('Xtest.vim') + exe buf .. 'bwipe!' +endfunc + func Test_timer_invalid_callback() call assert_fails('call timer_start(0, "0")', 'E921') endfunc diff --git a/src/nvim/testdir/test_undo.vim b/src/nvim/testdir/test_undo.vim index adcdcb1cd9..3b66071d6d 100644 --- a/src/nvim/testdir/test_undo.vim +++ b/src/nvim/testdir/test_undo.vim @@ -364,6 +364,25 @@ func Test_wundo_errors() bwipe! endfunc +" Check that reading a truncted undo file doesn't hang. +func Test_undofile_truncated() + throw 'skipped: TODO: ' + new + call setline(1, 'hello') + set ul=100 + wundo Xundofile + let contents = readfile('Xundofile', 'B') + + " try several sizes + for size in range(20, 500, 33) + call writefile(contents[0:size], 'Xundofile') + call assert_fails('rundo Xundofile', 'E825:') + endfor + + bwipe! + call delete('Xundofile') +endfunc + func Test_rundo_errors() call assert_fails('rundo XfileDoesNotExist', 'E822:') @@ -373,6 +392,26 @@ func Test_rundo_errors() call delete('Xundofile') endfunc +func Test_undofile_next() + set undofile + new Xfoo.txt + execute "norm ix\<c-g>uy\<c-g>uz\<Esc>" + write + bwipe + + next Xfoo.txt + call assert_equal('xyz', getline(1)) + silent undo + call assert_equal('xy', getline(1)) + silent undo + call assert_equal('x', getline(1)) + bwipe! + + call delete('Xfoo.txt') + call delete('.Xfoo.txt.un~') + set undofile& +endfunc + " Test for undo working properly when executing commands from a register. " Also test this in an empty buffer. func Test_cmd_in_reg_undo() diff --git a/src/nvim/testdir/test_vimscript.vim b/src/nvim/testdir/test_vimscript.vim index d2f13ff072..cb81997d39 100644 --- a/src/nvim/testdir/test_vimscript.vim +++ b/src/nvim/testdir/test_vimscript.vim @@ -1409,6 +1409,17 @@ func Test_compound_assignment_operators() let @/ = '' endfunc +func Test_funccall_garbage_collect() + func Func(x, ...) + call add(a:x, a:000) + endfunc + call Func([], []) + " Must not crash cause by invalid freeing + call test_garbagecollect_now() + call assert_true(v:true) + delfunc Func +endfunc + func Test_function_defined_line() if has('gui_running') " Can't catch the output of gvim. diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index dde17726fd..2ef9bf5a2e 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -1083,6 +1083,7 @@ static void tui_set_mode(UI *ui, ModeShape mode) } } else if (c.id == 0) { // No cursor color for this mode; reset to default. + data->want_invisible = false; unibi_out_ext(ui, data->unibi_ext.reset_cursor_color); } diff --git a/src/nvim/undo.c b/src/nvim/undo.c index 97018f6c02..6c5a6cdb46 100644 --- a/src/nvim/undo.c +++ b/src/nvim/undo.c @@ -878,7 +878,12 @@ static u_header_T *unserialize_uhp(bufinfo_T *bi, for (;; ) { int len = undo_read_byte(bi); - if (len == 0 || len == EOF) { + if (len == EOF) { + corruption_error("truncated", file_name); + u_free_uhp(uhp); + return NULL; + } + if (len == 0) { break; } int what = undo_read_byte(bi); @@ -3029,8 +3034,6 @@ u_header_T *u_force_get_undo_header(buf_T *buf) curbuf = buf; // Args are tricky: this means replace empty range by empty range.. u_savecommon(0, 1, 1, true); - curbuf = save_curbuf; - uhp = buf->b_u_curhead; if (!uhp) { uhp = buf->b_u_newhead; @@ -3038,6 +3041,7 @@ u_header_T *u_force_get_undo_header(buf_T *buf) abort(); } } + curbuf = save_curbuf; } return uhp; } diff --git a/src/nvim/window.c b/src/nvim/window.c index cec0dfd67f..e53570edd8 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -1490,13 +1490,11 @@ int win_split_ins(int size, int flags, win_T *new_wp, int dir) if (flags & (WSP_TOP | WSP_BOT)) (void)win_comp_pos(); - /* - * Both windows need redrawing - */ + // Both windows need redrawing. Update all status lines, in case they + // show something related to the window count or position. redraw_win_later(wp, NOT_VALID); - wp->w_redr_status = TRUE; redraw_win_later(oldwin, NOT_VALID); - oldwin->w_redr_status = TRUE; + status_redraw_all(); if (need_status) { msg_row = Rows - 1; @@ -6019,6 +6017,12 @@ char_u *grab_file_name(long count, linenr_T *file_lnum) char_u *ptr; if (get_visual_text(NULL, &ptr, &len) == FAIL) return NULL; + // Only recognize ":123" here + if (file_lnum != NULL && ptr[len] == ':' && isdigit(ptr[len + 1])) { + char_u *p = ptr + len + 1; + + *file_lnum = getdigits_long(&p, false, 0); + } return find_file_name_in_path(ptr, len, options, count, curbuf->b_ffname); } return file_name_at_cursor(options | FNAME_HYP, count, file_lnum); |