diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/gen/gen_eval.lua | 1 | ||||
-rw-r--r-- | src/gen/gen_help_html.lua | 1 | ||||
-rw-r--r-- | src/gen/gen_keycodes.lua | 33 | ||||
-rw-r--r-- | src/nvim/api/autocmd.c | 4 | ||||
-rw-r--r-- | src/nvim/api/win_config.c | 51 | ||||
-rw-r--r-- | src/nvim/buffer_defs.h | 52 | ||||
-rw-r--r-- | src/nvim/change.c | 1 | ||||
-rw-r--r-- | src/nvim/diff.c | 752 | ||||
-rw-r--r-- | src/nvim/drawline.c | 71 | ||||
-rw-r--r-- | src/nvim/drawscreen.c | 16 | ||||
-rw-r--r-- | src/nvim/edit.c | 2 | ||||
-rw-r--r-- | src/nvim/eval.c | 2 | ||||
-rw-r--r-- | src/nvim/eval/funcs.c | 58 | ||||
-rw-r--r-- | src/nvim/ex_docmd.c | 8 | ||||
-rw-r--r-- | src/nvim/highlight.h | 1 | ||||
-rw-r--r-- | src/nvim/highlight_defs.h | 1 | ||||
-rw-r--r-- | src/nvim/highlight_group.c | 1 | ||||
-rw-r--r-- | src/nvim/keycodes.c | 7 | ||||
-rw-r--r-- | src/nvim/lua/treesitter.c | 57 | ||||
-rw-r--r-- | src/nvim/mapping.c | 3 | ||||
-rw-r--r-- | src/nvim/mouse.c | 9 | ||||
-rw-r--r-- | src/nvim/option_vars.h | 11 | ||||
-rw-r--r-- | src/nvim/options.lua | 33 | ||||
-rw-r--r-- | src/nvim/optionstr.c | 10 | ||||
-rw-r--r-- | src/nvim/os/shell.c | 5 | ||||
-rw-r--r-- | src/nvim/popupmenu.c | 65 | ||||
-rw-r--r-- | src/nvim/regexp.c | 17 | ||||
-rw-r--r-- | src/nvim/vterm/state.c | 18 | ||||
-rw-r--r-- | src/nvim/window.c | 10 |
29 files changed, 1033 insertions, 267 deletions
diff --git a/src/gen/gen_eval.lua b/src/gen/gen_eval.lua index 9d2f2f7523..2f2d09485b 100644 --- a/src/gen/gen_eval.lua +++ b/src/gen/gen_eval.lua @@ -16,6 +16,7 @@ hashpipe:write([[ #include "nvim/arglist.h" #include "nvim/cmdexpand.h" #include "nvim/cmdhist.h" +#include "nvim/diff.h" #include "nvim/digraph.h" #include "nvim/eval.h" #include "nvim/eval/buffer.h" diff --git a/src/gen/gen_help_html.lua b/src/gen/gen_help_html.lua index 0d98d9e1b1..817811e857 100644 --- a/src/gen/gen_help_html.lua +++ b/src/gen/gen_help_html.lua @@ -76,6 +76,7 @@ local new_layout = { ['news.txt'] = true, ['news-0.9.txt'] = true, ['news-0.10.txt'] = true, + ['news-0.11.txt'] = true, ['nvim.txt'] = true, ['provider.txt'] = true, ['tui.txt'] = true, diff --git a/src/gen/gen_keycodes.lua b/src/gen/gen_keycodes.lua index 6fbfb5190f..584f0a3d36 100644 --- a/src/gen/gen_keycodes.lua +++ b/src/gen/gen_keycodes.lua @@ -38,43 +38,26 @@ hashorder, hashfun = hashy.hashy_hash('get_special_key_code', hashorder, functio return 'key_names_table[' .. idx .. '].name.data' end, true) ---- @type table<string,integer> ---- Maps keys to the (after hash) indexes of the entries with preferred names. -local key_hash_idx = {} - -local name_orig_idx_ = vim.deepcopy(name_orig_idx) -for i, lower_name in ipairs(hashorder) do - local orig_idx = table.remove(name_orig_idx_[lower_name], 1) - local keycode = keycode_names[orig_idx] - local key = keycode[1] - if key_orig_idx[key] == orig_idx then - key_hash_idx[key] = i - end -end -assert(vim.iter(vim.tbl_values(name_orig_idx_)):all(vim.tbl_isempty)) - local names_tgt = assert(io.open(names_file, 'w')) names_tgt:write([[ static const struct key_name_entry { - int key; ///< Special key code or ascii value - String name; ///< Name of key - const String *pref_name; ///< Pointer to preferred key name - ///< (may be NULL or point to the name in another entry) + int key; ///< Special key code or ascii value + bool is_alt; ///< Is an alternative name + String name; ///< Name of key } key_names_table[] = {]]) -name_orig_idx_ = vim.deepcopy(name_orig_idx) -for i, lower_name in ipairs(hashorder) do +local name_orig_idx_ = vim.deepcopy(name_orig_idx) +for _, lower_name in ipairs(hashorder) do local orig_idx = table.remove(name_orig_idx_[lower_name], 1) local keycode = keycode_names[orig_idx] local key = keycode[1] local name = keycode[2] - local pref_idx = key_hash_idx[key] names_tgt:write( - ('\n {%s, {"%s", %u}, %s},'):format( + ('\n {%s, %s, {"%s", %u}},'):format( key, + key_orig_idx[key] == orig_idx and 'false' or 'true', name, - #name, - pref_idx == i and 'NULL' or ('&key_names_table[%u].name'):format(pref_idx - 1) + #name ) ) end diff --git a/src/nvim/api/autocmd.c b/src/nvim/api/autocmd.c index 1e33b0b62a..7c98736289 100644 --- a/src/nvim/api/autocmd.c +++ b/src/nvim/api/autocmd.c @@ -369,8 +369,8 @@ cleanup: /// - desc (string) optional: description (for documentation and troubleshooting). /// - callback (function|string) optional: Lua function (or Vimscript function name, if /// string) called when the event(s) is triggered. Lua callback can return a truthy -/// value (not `false` or `nil`) to delete the autocommand. Receives one argument, -/// a table with these keys: [event-args]() +/// value (not `false` or `nil`) to delete the autocommand, and receives one argument, a +/// table with these keys: [event-args]() /// - id: (number) autocommand id /// - event: (string) name of the triggered event |autocmd-events| /// - group: (number|nil) autocommand group id, if any diff --git a/src/nvim/api/win_config.c b/src/nvim/api/win_config.c index ee6571fd95..4f3ecf8efe 100644 --- a/src/nvim/api/win_config.c +++ b/src/nvim/api/win_config.c @@ -158,41 +158,40 @@ /// region is hidden by setting `eob` flag of /// 'fillchars' to a space char, and clearing the /// |hl-EndOfBuffer| region in 'winhighlight'. -/// - border: Style of (optional) window border. This can either be a string -/// or an array. The string values are the same as those described in 'winborder'. -/// If it is an array, it should have a length of eight or any divisor of -/// eight. The array will specify the eight chars building up the border -/// in a clockwise fashion starting with the top-left corner. As an -/// example, the double box style could be specified as: -/// ``` -/// [ "╔", "═" ,"╗", "║", "╝", "═", "╚", "║" ]. -/// ``` -/// If the number of chars are less than eight, they will be repeated. Thus -/// an ASCII border could be specified as -/// ``` -/// [ "/", "-", \"\\\\\", "|" ], -/// ``` -/// or all chars the same as -/// ``` -/// [ "x" ]. -/// ``` -/// An empty string can be used to turn off a specific border, for instance, +/// - border: (`string|string[]`) (defaults to 'winborder' option) Window border. The string form +/// accepts the same values as the 'winborder' option. The array form must have a length of +/// eight or any divisor of eight, specifying the chars that form the border in a clockwise +/// fashion starting from the top-left corner. For example, the double-box style can be +/// specified as: /// ``` -/// [ "", "", "", ">", "", "", "", "<" ] +/// [ "╔", "═" ,"╗", "║", "╝", "═", "╚", "║" ]. /// ``` -/// will only make vertical borders but not horizontal ones. -/// By default, `FloatBorder` highlight is used, which links to `WinSeparator` -/// when not defined. It could also be specified by character: +/// If fewer than eight chars are given, they will be repeated. An ASCII border could be +/// specified as: /// ``` -/// [ ["+", "MyCorner"], ["x", "MyBorder"] ]. +/// [ "/", "-", \"\\\\\", "|" ], /// ``` -/// - title: Title (optional) in window border, string or list. +/// Or one char for all sides: +/// ``` +/// [ "x" ]. +/// ``` +/// Empty string can be used to hide a specific border. This example will show only vertical +/// borders, not horizontal: +/// ``` +/// [ "", "", "", ">", "", "", "", "<" ] +/// ``` +/// By default, |hl-FloatBorder| highlight is used, which links to |hl-WinSeparator| when not +/// defined. Each border side can specify an optional highlight: +/// ``` +/// [ ["+", "MyCorner"], ["x", "MyBorder"] ]. +/// ``` +/// - title: (optional) Title in window border, string or list. /// List should consist of `[text, highlight]` tuples. /// If string, or a tuple lacks a highlight, the default highlight group is `FloatTitle`. /// - title_pos: Title position. Must be set with `title` option. /// Value can be one of "left", "center", or "right". /// Default is `"left"`. -/// - footer: Footer (optional) in window border, string or list. +/// - footer: (optional) Footer in window border, string or list. /// List should consist of `[text, highlight]` tuples. /// If string, or a tuple lacks a highlight, the default highlight group is `FloatFooter`. /// - footer_pos: Footer position. Must be set with `footer` option. diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index c4241eed45..f226ce9c42 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -743,18 +743,21 @@ struct file_buffer { // Stuff for diff mode. #define DB_COUNT 8 // up to four buffers can be diff'ed -// Each diffblock defines where a block of lines starts in each of the buffers -// and how many lines it occupies in that buffer. When the lines are missing -// in the buffer the df_count[] is zero. This is all counted in -// buffer lines. -// There is always at least one unchanged line in between the diffs. -// Otherwise it would have been included in the diff above or below it. -// df_lnum[] + df_count[] is the lnum below the change. When in one buffer -// lines have been inserted, in the other buffer df_lnum[] is the line below -// the insertion and df_count[] is zero. When appending lines at the end of -// the buffer, df_lnum[] is one beyond the end! -// This is using a linked list, because the number of differences is expected -// to be reasonable small. The list is sorted on lnum. +/// Each diffblock defines where a block of lines starts in each of the buffers +/// and how many lines it occupies in that buffer. When the lines are missing +/// in the buffer the df_count[] is zero. This is all counted in +/// buffer lines. +/// There is always at least one unchanged line in between the diffs (unless +/// linematch is used). Otherwise it would have been included in the diff above +/// or below it. +/// df_lnum[] + df_count[] is the lnum below the change. When in one buffer +/// lines have been inserted, in the other buffer df_lnum[] is the line below +/// the insertion and df_count[] is zero. When appending lines at the end of +/// the buffer, df_lnum[] is one beyond the end! +/// This is using a linked list, because the number of differences is expected +/// to be reasonable small. The list is sorted on lnum. +/// Each diffblock also contains a cached list of inline diff of changes within +/// the block, used for highlighting. typedef struct diffblock_S diff_T; struct diffblock_S { diff_T *df_next; @@ -762,6 +765,31 @@ struct diffblock_S { linenr_T df_count[DB_COUNT]; // nr of inserted/changed lines bool is_linematched; // has the linematch algorithm ran on this diff hunk to divide it into // smaller diff hunks? + + bool has_changes; ///< has cached list of inline changes + garray_T df_changes; ///< list of inline changes (diffline_change_T) +}; + +/// Each entry stores a single inline change within a diff block. Line numbers +/// are recorded as relative offsets, and columns are byte offsets, not +/// character counts. +/// Ranges are [start,end), with the end being exclusive. +typedef struct diffline_change_S diffline_change_T; +struct diffline_change_S { + colnr_T dc_start[DB_COUNT]; ///< byte offset of start of range in the line + colnr_T dc_end[DB_COUNT]; ///< 1 past byte offset of end of range in line + int dc_start_lnum_off[DB_COUNT]; ///< starting line offset + int dc_end_lnum_off[DB_COUNT]; ///< end line offset +}; + +/// Describes a single line's list of inline changes. Use diff_change_parse() to +/// parse this. +typedef struct diffline_S diffline_T; +struct diffline_S { + diffline_change_T *changes; + int num_changes; + int bufidx; + int lineoff; }; #define SNAP_HELP_IDX 0 diff --git a/src/nvim/change.c b/src/nvim/change.c index 192e0d9faa..cf7d5dfc4b 100644 --- a/src/nvim/change.c +++ b/src/nvim/change.c @@ -240,6 +240,7 @@ static void changed_common(buf_T *buf, linenr_T lnum, colnr_T col, linenr_T lnum FOR_ALL_WINDOWS_IN_TAB(win, curtab) { if (win->w_buffer == buf && win->w_p_diff && diff_internal()) { curtab->tp_diff_update = true; + diff_update_line(lnum); } } diff --git a/src/nvim/diff.c b/src/nvim/diff.c index c9ca58c816..6309aa6c5e 100644 --- a/src/nvim/diff.c +++ b/src/nvim/diff.c @@ -29,6 +29,7 @@ #include "nvim/drawscreen.h" #include "nvim/errors.h" #include "nvim/eval.h" +#include "nvim/eval/typval.h" #include "nvim/ex_cmds.h" #include "nvim/ex_cmds_defs.h" #include "nvim/ex_docmd.h" @@ -86,7 +87,13 @@ static bool diff_need_update = false; // ex_diffupdate needs to be called #define DIFF_CLOSE_OFF 0x400 // diffoff when closing window #define DIFF_FOLLOWWRAP 0x800 // follow the wrap option #define DIFF_LINEMATCH 0x1000 // match most similar lines within diff +#define DIFF_INLINE_NONE 0x2000 // no inline highlight +#define DIFF_INLINE_SIMPLE 0x4000 // inline highlight with simple algorithm +#define DIFF_INLINE_CHAR 0x8000 // inline highlight with character diff +#define DIFF_INLINE_WORD 0x10000 // inline highlight with word diff #define ALL_WHITE_DIFF (DIFF_IWHITE | DIFF_IWHITEALL | DIFF_IWHITEEOL) +#define ALL_INLINE (DIFF_INLINE_NONE | DIFF_INLINE_SIMPLE | DIFF_INLINE_CHAR | DIFF_INLINE_WORD) +#define ALL_INLINE_DIFF (DIFF_INLINE_CHAR | DIFF_INLINE_WORD) static int diff_flags = DIFF_INTERNAL | DIFF_FILLER | DIFF_CLOSE_OFF; static int diff_algorithm = 0; @@ -136,6 +143,15 @@ typedef enum { # include "diff.c.generated.h" #endif +#define FOR_ALL_DIFFBLOCKS_IN_TAB(tp, dp) \ + for ((dp) = (tp)->tp_first_diff; (dp) != NULL; (dp) = (dp)->df_next) + +static void clear_diffblock(diff_T *dp) +{ + ga_clear(&dp->df_changes); + xfree(dp); +} + /// Called when deleting or unloading a buffer: No longer make a diff with it. /// /// @param buf @@ -522,7 +538,7 @@ static void diff_mark_adjust_tp(tabpage_T *tp, int idx, linenr_T line1, linenr_T /// @return The new diff block. static diff_T *diff_alloc_new(tabpage_T *tp, diff_T *dprev, diff_T *dp) { - diff_T *dnew = xmalloc(sizeof(*dnew)); + diff_T *dnew = xcalloc(1, sizeof(*dnew)); dnew->is_linematched = false; dnew->df_next = dp; @@ -532,13 +548,15 @@ static diff_T *diff_alloc_new(tabpage_T *tp, diff_T *dprev, diff_T *dp) dprev->df_next = dnew; } + dnew->has_changes = false; + ga_init(&dnew->df_changes, sizeof(diffline_change_T), 20); return dnew; } static diff_T *diff_free(tabpage_T *tp, diff_T *dprev, diff_T *dp) { diff_T *ret = dp->df_next; - xfree(dp); + clear_diffblock(dp); if (dprev == NULL) { tp->tp_first_diff = ret; @@ -763,15 +781,32 @@ static int diff_write_buffer(buf_T *buf, mmfile_t *m, linenr_T start, linenr_T e char *s = ml_get_buf(buf, lnum); if (diff_flags & DIFF_ICASE) { while (*s != NUL) { + int c; + int c_len = 1; char cbuf[MB_MAXBYTES + 1]; - // xdiff doesn't support ignoring case, fold-case the text. - int c = *s == NL ? NUL : utf_fold(utf_ptr2char(s)); + if (*s == NL) { + c = NUL; + } else { + // xdiff doesn't support ignoring case, fold-case the text. + c = utf_ptr2char(s); + c_len = utf_char2len(c); + c = utf_fold(c); + } const int orig_len = utfc_ptr2len(s); - // TODO(Bram): handle byte length difference - char *s1 = (utf_char2bytes(c, cbuf) != orig_len) ? s : cbuf; - memmove(ptr + len, s1, (size_t)orig_len); + if (utf_char2bytes(c, cbuf) != c_len) { + // TODO(Bram): handle byte length difference + // One example is Å (3 bytes) and å (2 bytes). + memmove(ptr + len, s, (size_t)orig_len); + } else { + memmove(ptr + len, cbuf, (size_t)c_len); + if (orig_len > c_len) { + // Copy remaining composing characters + memmove(ptr + len + c_len, s + c_len, (size_t)(orig_len - c_len)); + } + } + s += orig_len; len += (size_t)orig_len; } @@ -943,8 +978,7 @@ void ex_diffupdate(exarg_T *eap) } // Only use the internal method if it did not fail for one of the buffers. - diffio_T diffio; - CLEAR_FIELD(diffio); + diffio_T diffio = { 0 }; diffio.dio_internal = diff_internal(); diff_try_update(&diffio, idx_orig, eap); @@ -1639,11 +1673,6 @@ static void process_hunk(diff_T **dpp, diff_T **dprevp, int idx_orig, int idx_ne if (off > 0) { dp->df_count[idx_new] += off; } - if ((dp->df_lnum[idx_new] + dp->df_count[idx_new] - 1) - > curtab->tp_diffbuf[idx_new]->b_ml.ml_line_count) { - dp->df_count[idx_new] = curtab->tp_diffbuf[idx_new]->b_ml.ml_line_count - - dp->df_lnum[idx_new] + 1; - } } // Adjust the size of the block to include all the lines to the @@ -1661,11 +1690,6 @@ static void process_hunk(diff_T **dpp, diff_T **dprevp, int idx_orig, int idx_ne // overlap later. dp->df_count[idx_new] += -off; } - if ((dp->df_lnum[idx_new] + dp->df_count[idx_new] - 1) - > curtab->tp_diffbuf[idx_new]->b_ml.ml_line_count) { - dp->df_count[idx_new] = curtab->tp_diffbuf[idx_new]->b_ml.ml_line_count - - dp->df_lnum[idx_new] + 1; - } off = 0; } @@ -1682,7 +1706,7 @@ static void process_hunk(diff_T **dpp, diff_T **dprevp, int idx_orig, int idx_ne while (dn != dp->df_next) { dpl = dn->df_next; - xfree(dn); + clear_diffblock(dn); dn = dpl; } } else { @@ -1716,7 +1740,7 @@ static void process_hunk(diff_T **dpp, diff_T **dprevp, int idx_orig, int idx_ne static void diff_read(int idx_orig, int idx_new, diffio_T *dio) { FILE *fd = NULL; - int line_idx = 0; + int line_hunk_idx = 0; // line or hunk index diff_T *dprev = NULL; diff_T *dp = curtab->tp_first_diff; diffout_T *dout = &dio->dio_diff; @@ -1734,7 +1758,7 @@ static void diff_read(int idx_orig, int idx_new, diffio_T *dio) while (true) { diffhunk_T hunk = { 0 }; bool eof = dio->dio_internal - ? extract_hunk_internal(dout, &hunk, &line_idx) + ? extract_hunk_internal(dout, &hunk, &line_hunk_idx) : extract_hunk(fd, &hunk, &diffstyle); if (eof) { @@ -1788,7 +1812,7 @@ void diff_clear(tabpage_T *tp) diff_T *next_p; for (diff_T *p = tp->tp_first_diff; p != NULL; p = next_p) { next_p = p->df_next; - xfree(p); + clear_diffblock(p); } tp->tp_first_diff = NULL; } @@ -2531,6 +2555,28 @@ int diffopt_changed(void) } else { return FAIL; } + } else if (strncmp(p, "inline:", 7) == 0) { + // Note: Keep this in sync with opt_dip_inline_values. + p += 7; + if (strncmp(p, "none", 4) == 0) { + p += 4; + diff_flags_new &= ~(ALL_INLINE); + diff_flags_new |= DIFF_INLINE_NONE; + } else if (strncmp(p, "simple", 6) == 0) { + p += 6; + diff_flags_new &= ~(ALL_INLINE); + diff_flags_new |= DIFF_INLINE_SIMPLE; + } else if (strncmp(p, "char", 4) == 0) { + p += 4; + diff_flags_new &= ~(ALL_INLINE); + diff_flags_new |= DIFF_INLINE_CHAR; + } else if (strncmp(p, "word", 4) == 0) { + p += 4; + diff_flags_new &= ~(ALL_INLINE); + diff_flags_new |= DIFF_INLINE_WORD; + } else { + return FAIL; + } } else if ((strncmp(p, "linematch:", 10) == 0) && ascii_isdigit(p[10])) { p += 10; linematch_lines_new = getdigits_int(&p, false, linematch_lines_new); @@ -2603,48 +2649,101 @@ bool diffopt_filler(void) return (diff_flags & DIFF_FILLER) != 0; } -/// Find the difference within a changed line. -/// -/// @param wp window whose current buffer to check -/// @param lnum line number to check within the buffer -/// @param startp first char of the change -/// @param endp last char of the change -/// -/// @return true if the line was added, no other buffer has it. -bool diff_find_change(win_T *wp, linenr_T lnum, int *startp, int *endp) - FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL +/// Called when a line has been updated. Used for updating inline diff in Insert +/// mode without waiting for global diff update later. +void diff_update_line(linenr_T lnum) { - // Make a copy of the line, the next ml_get() will invalidate it. - char *line_org = xstrdup(ml_get_buf(wp->w_buffer, lnum)); + if (!(diff_flags & ALL_INLINE_DIFF)) { + // We only care if we are doing inline-diff where we cache the diff results + return; + } - int idx = diff_buf_idx(wp->w_buffer, curtab); + int idx = diff_buf_idx(curbuf, curtab); if (idx == DB_COUNT) { - // cannot happen - xfree(line_org); - return false; + return; } - - // search for a change that includes "lnum" in the list of diffblocks. diff_T *dp; - for (dp = curtab->tp_first_diff; dp != NULL; dp = dp->df_next) { + FOR_ALL_DIFFBLOCKS_IN_TAB(curtab, dp) { if (lnum <= dp->df_lnum[idx] + dp->df_count[idx]) { break; } } - if (dp != NULL && dp->is_linematched) { - while (dp && dp->df_next - && lnum == dp->df_count[idx] + dp->df_lnum[idx] - && dp->df_next->df_lnum[idx] == lnum) { - dp = dp->df_next; - } + + // clear the inline change cache as it's invalid + if (dp != NULL) { + dp->has_changes = false; + dp->df_changes.ga_len = 0; } +} + +/// used for simple inline diff algorithm +static diffline_change_T simple_diffline_change; - if ((dp == NULL) || (diff_check_sanity(curtab, dp) == FAIL)) { - xfree(line_org); +/// Parse a diffline struct and returns the [start,end] byte offsets +/// +/// Returns true if this change was added, no other buffer has it. +bool diff_change_parse(diffline_T *diffline, diffline_change_T *change, int *change_start, + int *change_end) +{ + if (change->dc_start_lnum_off[diffline->bufidx] < diffline->lineoff) { + *change_start = 0; + } else { + *change_start = change->dc_start[diffline->bufidx]; + } + if (change->dc_end_lnum_off[diffline->bufidx] > diffline->lineoff) { + *change_end = INT_MAX; + } else { + *change_end = change->dc_end[diffline->bufidx]; + } + if (change == &simple_diffline_change) { + // This is what we returned from simple inline diff. We always consider + // the range to be changed, rather than added for now. return false; } + // Find out whether this is an addition. Note that for multi buffer diff, + // to tell whether lines are additions we check whether all the other diff + // lines are identical (in diff_check_with_linestatus). If so, we mark them + // as add. We don't do that for inline diff here for simplicity. + for (int i = 0; i < DB_COUNT; i++) { + if (i == diffline->bufidx) { + continue; + } + if (change->dc_start[i] != change->dc_end[i] + || change->dc_end_lnum_off[i] != change->dc_start_lnum_off[i]) { + return false; + } + } + return true; +} + +/// Find the difference within a changed line and returns [startp,endp] byte +/// positions. Performs a simple algorithm by finding a single range in the +/// middle. +/// +/// If diffopt has DIFF_INLINE_NONE set, then this will only calculate the return +/// value (added or changed), but startp/endp will not be calculated. +/// +/// @param wp window whose current buffer to check +/// @param lnum line number to check within the buffer +/// @param startp first char of the change +/// @param endp last char of the change +/// +/// @return true if the line was added, no other buffer has it. +static bool diff_find_change_simple(win_T *wp, linenr_T lnum, const diff_T *dp, int idx, + int *startp, int *endp) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL +{ + char *line_org; + if (diff_flags & DIFF_INLINE_NONE) { + // We only care about the return value, not the actual string comparisons. + line_org = NULL; + } else { + // Make a copy of the line, the next ml_get() will invalidate it. + line_org = xstrdup(ml_get_buf(wp->w_buffer, lnum)); + } + int si_org; int si_new; int ei_org; @@ -2659,6 +2758,10 @@ bool diff_find_change(win_T *wp, linenr_T lnum, int *startp, int *endp) continue; } added = false; + if (diff_flags & DIFF_INLINE_NONE) { + break; // early terminate as we only care about the return value + } + char *line_new = ml_get_buf(curtab->tp_diffbuf[i], dp->df_lnum[i] + off); // Search for start of difference @@ -2737,6 +2840,465 @@ bool diff_find_change(win_T *wp, linenr_T lnum, int *startp, int *endp) return added; } +/// Mapping used for mapping from temporary mmfile created for inline diff back +/// to original buffer's line/col. +typedef struct { + colnr_T byte_start; + colnr_T num_bytes; + int lineoff; +} linemap_entry_T; + +/// Refine inline character-wise diff blocks to create a more human readable +/// highlight. Otherwise a naive diff under existing algorithms tends to create +/// a messy output with lots of small gaps. +/// It does this by merging adjacent long diff blocks if they are only separated +/// by a couple characters. +/// These are done by heuristics and can be further tuned. +static void diff_refine_inline_char_highlight(diff_T *dp_orig, garray_T *linemap, int idx1) +{ + // Perform multiple passes so that newly merged blocks will now be long + // enough which may cause other previously unmerged gaps to be merged as + // well. + int pass = 1; + do { + bool has_unmerged_gaps = false; + bool has_merged_gaps = false; + diff_T *dp = dp_orig; + while (dp != NULL && dp->df_next != NULL) { + // Only use first buffer to calculate the gap because the gap is + // unchanged text, which would be the same in all buffers. + if (dp->df_lnum[idx1] + dp->df_count[idx1] - 1 >= linemap[idx1].ga_len + || dp->df_next->df_lnum[idx1] - 1 >= linemap[idx1].ga_len) { + dp = dp->df_next; + continue; + } + + // If the gap occurs over different lines, don't consider it + linemap_entry_T *entry1 = + &((linemap_entry_T *)linemap[idx1].ga_data)[dp->df_lnum[idx1] + + dp->df_count[idx1] - 1]; + linemap_entry_T *entry2 = + &((linemap_entry_T *)linemap[idx1].ga_data)[dp->df_next->df_lnum[idx1] - 1]; + if (entry1->lineoff != entry2->lineoff) { + dp = dp->df_next; + continue; + } + + linenr_T gap = dp->df_next->df_lnum[idx1] - (dp->df_lnum[idx1] + dp->df_count[idx1]); + if (gap <= 3) { + linenr_T max_df_count = 0; + for (int i = 0; i < DB_COUNT; i++) { + max_df_count = MAX(max_df_count, dp->df_count[i] + dp->df_next->df_count[i]); + } + + if (max_df_count >= gap * 4) { + // Merge current block with the next one. Don't advance the + // pointer so we try the same merged block against the next + // one. + for (int i = 0; i < DB_COUNT; i++) { + dp->df_count[i] = dp->df_next->df_lnum[i] + + dp->df_next->df_count[i] - dp->df_lnum[i]; + } + diff_T *dp_next = dp->df_next; + dp->df_next = dp_next->df_next; + clear_diffblock(dp_next); + has_merged_gaps = true; + continue; + } else { + has_unmerged_gaps = true; + } + } + dp = dp->df_next; + } + if (!has_unmerged_gaps || !has_merged_gaps) { + break; + } + } while (pass++ < 4); // use limited number of passes to avoid excessive looping +} + +/// Find the inline difference within a diff block among differnt buffers. Do +/// this by splitting each block's content into characters or words, and then +/// use internal xdiff to calculate the per-character/word diff. The result is +/// stored in dp instead of returned by the function. +static void diff_find_change_inline_diff(diff_T *dp) +{ + const int save_diff_algorithm = diff_algorithm; + + diffio_T dio = { 0 }; + ga_init(&dio.dio_diff.dout_ga, sizeof(char *), 1000); + + // inline diff only supports internal algo + dio.dio_internal = true; + + // always use indent-heuristics to slide diff splits along + // whitespace + diff_algorithm |= XDF_INDENT_HEURISTIC; + + // diff_read() has an implicit dependency on curtab->tp_first_diff + diff_T *orig_diff = curtab->tp_first_diff; + curtab->tp_first_diff = NULL; + + garray_T linemap[DB_COUNT]; + garray_T file1_str; + garray_T file2_str; + + // Buffers to populate mmfile 1/2 that would be passed to xdiff as memory + // files. Use a grow array as it is not obvious how much exact space we + // need. + ga_init(&file1_str, 1, 1024); + ga_init(&file2_str, 1, 1024); + + // Line map to map from generated mmfiles' line numbers back to original + // diff blocks' locations. Need this even for char diff because not all + // characters are 1-byte long / ASCII. + for (int i = 0; i < DB_COUNT; i++) { + ga_init(&linemap[i], sizeof(linemap_entry_T), 128); + } + + int file1_idx = -1; + for (int i = 0; i < DB_COUNT; i++) { + dio.dio_diff.dout_ga.ga_len = 0; + + buf_T *buf = curtab->tp_diffbuf[i]; + if (buf == NULL || buf->b_ml.ml_mfp == NULL) { + continue; // skip buffer that isn't loaded + } + if (dp->df_count[i] == 0) { + continue; // skip buffer that don't have any texts in this block + } + if (file1_idx == -1) { + file1_idx = i; + } + + garray_T *curstr = (file1_idx != i) ? &file2_str : &file1_str; + + linenr_T numlines = 0; + curstr->ga_len = 0; + + // Split each line into chars/words and populate fake file buffer as + // newline-delimited tokens as that's what xdiff requires. + for (int off = 0; off < dp->df_count[i]; off++) { + char *curline = ml_get_buf(curtab->tp_diffbuf[i], dp->df_lnum[i] + off); + + bool in_keyword = false; + + // iwhiteeol support vars + bool last_white = false; + int eol_ga_len = -1; + int eol_linemap_len = -1; + int eol_numlines = -1; + + char *s = curline; + while (*s != NUL) { + // Always use the first buffer's 'iskeyword' to have a consistent diff + bool new_in_keyword = false; + if (diff_flags & DIFF_INLINE_WORD) { + new_in_keyword = vim_iswordp_buf(s, curtab->tp_diffbuf[file1_idx]); + } + if (in_keyword && !new_in_keyword) { + ga_append(curstr, NL); + numlines++; + } + + if (ascii_iswhite(*s)) { + if (diff_flags & DIFF_IWHITEALL) { + in_keyword = false; + s = skipwhite(s); + continue; + } else if ((diff_flags & DIFF_IWHITEEOL) || (diff_flags & DIFF_IWHITE)) { + if (!last_white) { + eol_ga_len = curstr->ga_len; + eol_linemap_len = linemap[i].ga_len; + eol_numlines = numlines; + last_white = true; + } + } + } else { + if ((diff_flags & DIFF_IWHITEEOL) || (diff_flags & DIFF_IWHITE)) { + last_white = false; + eol_ga_len = -1; + eol_linemap_len = -1; + eol_numlines = -1; + } + } + + int char_len = 1; + if (*s == NL) { + // NL is internal substitute for NUL + ga_append(curstr, NUL); + } else { + char_len = utfc_ptr2len(s); + + if (ascii_iswhite(*s) && (diff_flags & DIFF_IWHITE)) { + // Treat the entire white space span as a single char. + char_len = (int)(skipwhite(s) - s); + } + + if (diff_flags & DIFF_ICASE) { + // xdiff doesn't support ignoring case, fold-case the text manually. + int c = utf_ptr2char(s); + int c_len = utf_char2len(c); + c = utf_fold(c); + char cbuf[MB_MAXBYTES + 1]; + int c_fold_len = utf_char2bytes(c, cbuf); + ga_concat_len(curstr, cbuf, (size_t)c_fold_len); + if (char_len > c_len) { + // There may be remaining composing characters. Write those back in. + // Composing characters don't need case folding. + ga_concat_len(curstr, s + c_len, (size_t)(char_len - c_len)); + } + } else { + ga_concat_len(curstr, s, (size_t)char_len); + } + } + + if (!new_in_keyword) { + ga_append(curstr, NL); + numlines++; + } + + if (!new_in_keyword || (new_in_keyword && !in_keyword)) { + // create a new mapping entry from the xdiff mmfile back to + // original line/col. + linemap_entry_T linemap_entry = { + .lineoff = off, + .byte_start = (colnr_T)(s - curline), + .num_bytes = char_len, + }; + GA_APPEND(linemap_entry_T, &linemap[i], linemap_entry); + } else { + // Still inside a keyword. Just increment byte count but + // don't make a new entry. + // linemap always has at least one entry here + ((linemap_entry_T *)linemap[i].ga_data)[linemap[i].ga_len - 1].num_bytes += char_len; + } + + in_keyword = new_in_keyword; + s += char_len; + } + if (in_keyword) { + ga_append(curstr, NL); + numlines++; + } + + if ((diff_flags & DIFF_IWHITEEOL) || (diff_flags & DIFF_IWHITE)) { + // Need to trim trailing whitespace. Do this simply by + // resetting arrays back to before we encountered them. + if (eol_ga_len != -1) { + curstr->ga_len = eol_ga_len; + linemap[i].ga_len = eol_linemap_len; + numlines = eol_numlines; + } + } + + if (!(diff_flags & DIFF_IWHITEALL)) { + // Add an empty line token mapped to the end-of-line in the + // original file. This helps diff newline differences among + // files, which will be visualized when using 'list' as the eol + // listchar will be highlighted. + ga_append(curstr, NL); + numlines++; + + linemap_entry_T linemap_entry = { + .lineoff = off, + .byte_start = (colnr_T)(s - curline), + .num_bytes = sizeof(NL), + }; + GA_APPEND(linemap_entry_T, &linemap[i], linemap_entry); + } + } + + if (file1_idx != i) { + dio.dio_new.din_mmfile.ptr = (char *)curstr->ga_data; + dio.dio_new.din_mmfile.size = curstr->ga_len; + } else { + dio.dio_orig.din_mmfile.ptr = (char *)curstr->ga_data; + dio.dio_orig.din_mmfile.size = curstr->ga_len; + } + if (file1_idx != i) { + // Perform diff with first file and read the results + int diff_status = diff_file_internal(&dio); + if (diff_status == FAIL) { + goto done; + } + + diff_read(0, i, &dio); + clear_diffout(&dio.dio_diff); + } + } + diff_T *new_diff = curtab->tp_first_diff; + + if (diff_flags & DIFF_INLINE_CHAR && file1_idx != -1) { + diff_refine_inline_char_highlight(new_diff, linemap, file1_idx); + } + + // After the diff, use the linemap to obtain the original line/col of the + // changes and cache them in dp. + dp->df_changes.ga_len = 0; // this should already be zero + for (; new_diff != NULL; new_diff = new_diff->df_next) { + diffline_change_T change = { 0 }; + for (int i = 0; i < DB_COUNT; i++) { + if (new_diff->df_lnum[i] == 0) { + continue; + } + linenr_T diff_lnum = new_diff->df_lnum[i] - 1; // use zero-index + linenr_T diff_lnum_end = diff_lnum + new_diff->df_count[i]; + + if (diff_lnum >= linemap[i].ga_len) { + change.dc_start[i] = MAXCOL; + change.dc_start_lnum_off[i] = INT_MAX; + } else { + change.dc_start[i] = ((linemap_entry_T *)linemap[i].ga_data)[diff_lnum].byte_start; + change.dc_start_lnum_off[i] = ((linemap_entry_T *)linemap[i].ga_data)[diff_lnum].lineoff; + } + + if (diff_lnum == diff_lnum_end) { + change.dc_end[i] = change.dc_start[i]; + change.dc_end_lnum_off[i] = change.dc_start_lnum_off[i]; + } else if (diff_lnum_end - 1 >= linemap[i].ga_len) { + change.dc_end[i] = MAXCOL; + change.dc_end_lnum_off[i] = INT_MAX; + } else { + change.dc_end[i] = ((linemap_entry_T *)linemap[i].ga_data)[diff_lnum_end - 1].byte_start + + ((linemap_entry_T *)linemap[i].ga_data)[diff_lnum_end - 1].num_bytes; + change.dc_end_lnum_off[i] = ((linemap_entry_T *)linemap[i].ga_data)[diff_lnum_end - + 1].lineoff; + } + } + GA_APPEND(diffline_change_T, &dp->df_changes, change); + } + +done: + diff_algorithm = save_diff_algorithm; + + dp->has_changes = true; + + diff_clear(curtab); + curtab->tp_first_diff = orig_diff; + + ga_clear(&file1_str); + ga_clear(&file2_str); + // No need to clear dio.dio_orig/dio_new because they were referencing + // strings that are now cleared. + clear_diffout(&dio.dio_diff); + for (int i = 0; i < DB_COUNT; i++) { + ga_clear(&linemap[i]); + } +} + +/// Find the difference within a changed line. +/// Returns true if the line was added, no other buffer has it. +bool diff_find_change(win_T *wp, linenr_T lnum, diffline_T *diffline) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL +{ + int idx = diff_buf_idx(wp->w_buffer, curtab); + if (idx == DB_COUNT) { // cannot happen + return false; + } + + // search for a change that includes "lnum" in the list of diffblocks. + diff_T *dp; + FOR_ALL_DIFFBLOCKS_IN_TAB(curtab, dp) { + if (lnum <= dp->df_lnum[idx] + dp->df_count[idx]) { + break; + } + } + if (dp && dp->is_linematched) { + while (dp && dp->df_next + && lnum == dp->df_count[idx] + dp->df_lnum[idx] + && dp->df_next->df_lnum[idx] == lnum) { + dp = dp->df_next; + } + } + if (dp == NULL || diff_check_sanity(curtab, dp) == FAIL) { + return false; + } + + int off = lnum - dp->df_lnum[idx]; + + if (!(diff_flags & ALL_INLINE_DIFF)) { + // Use simple algorithm + int change_start = MAXCOL; // first col of changed area + int change_end = -1; // last col of changed area + + int ret = diff_find_change_simple(wp, lnum, dp, idx, &change_start, &change_end); + + // convert from inclusive end to exclusive end per diffline's contract + change_end += 1; + + // Create a mock diffline struct. We always only have one so no need to + // allocate memory. + CLEAR_FIELD(simple_diffline_change); + diffline->changes = &simple_diffline_change; + diffline->num_changes = 1; + diffline->bufidx = idx; + diffline->lineoff = lnum - dp->df_lnum[idx]; + + simple_diffline_change.dc_start[idx] = change_start; + simple_diffline_change.dc_end[idx] = change_end; + simple_diffline_change.dc_start_lnum_off[idx] = off; + simple_diffline_change.dc_end_lnum_off[idx] = off; + return ret; + } + + // Use inline diff algorithm. + // The diff changes are usually cached so we check that first. + if (!dp->has_changes) { + diff_find_change_inline_diff(dp); + } + + garray_T *changes = &dp->df_changes; + + // Use linear search to find the first change for this line. We could + // optimize this to use binary search, but there should usually be a + // limited number of inline changes per diff block, and limited number of + // diff blocks shown on screen, so it is not necessary. + int num_changes = 0; + int change_idx = 0; + diffline->changes = NULL; + for (change_idx = 0; change_idx < changes->ga_len; change_idx++) { + diffline_change_T *change = + &((diffline_change_T *)dp->df_changes.ga_data)[change_idx]; + if (change->dc_end_lnum_off[idx] < off) { + continue; + } + if (change->dc_start_lnum_off[idx] > off) { + break; + } + if (diffline->changes == NULL) { + diffline->changes = change; + } + num_changes++; + } + diffline->num_changes = num_changes; + diffline->bufidx = idx; + diffline->lineoff = off; + + // Detect simple cases of added lines in the end within a diff block. This + // has to be the last change of this diff block, and all other buffers are + // considering this to be an addition past their last line. Other scenarios + // will be considered a changed line instead. + bool added = false; + if (num_changes == 1 && change_idx == dp->df_changes.ga_len) { + added = true; + for (int i = 0; i < DB_COUNT; i++) { + if (idx == i) { + continue; + } + if (curtab->tp_diffbuf[i] == NULL) { + continue; + } + diffline_change_T *change = + &((diffline_change_T *)dp->df_changes.ga_data)[dp->df_changes.ga_len - 1]; + if (change->dc_start_lnum_off[i] != INT_MAX) { + added = false; + break; + } + } + } + return added; +} + /// Check that line "lnum" is not close to a diff block, this line should /// be in a fold. /// @@ -3488,3 +4050,93 @@ static int xdiff_out(int start_a, int count_a, int start_b, int count_b, void *p })); return 0; } + +/// "diff_filler()" function +void f_diff_filler(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) +{ + rettv->vval.v_number = MAX(0, diff_check(curwin, tv_get_lnum(argvars))); +} + +/// "diff_hlID()" function +void f_diff_hlID(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) +{ + static linenr_T prev_lnum = 0; + static varnumber_T changedtick = 0; + static int fnum = 0; + static int prev_diff_flags = 0; + static int change_start = 0; + static int change_end = 0; + static hlf_T hlID = (hlf_T)0; + + diffline_T diffline = { 0 }; + // Remember the results if using simple since it's recalculated per + // call. Otherwise just call diff_find_change() every time since + // internally the result is cached interally. + const bool cache_results = !(diff_flags & ALL_INLINE_DIFF); + + linenr_T lnum = tv_get_lnum(argvars); + if (lnum < 0) { // ignore type error in {lnum} arg + lnum = 0; + } + if (!cache_results + || lnum != prev_lnum + || changedtick != buf_get_changedtick(curbuf) + || fnum != curbuf->b_fnum + || diff_flags != prev_diff_flags) { + // New line, buffer, change: need to get the values. + int linestatus = 0; + int filler_lines = diff_check_with_linestatus(curwin, lnum, &linestatus); + if (filler_lines < 0 || linestatus < 0) { + if (filler_lines == -1 || linestatus == -1) { + change_start = MAXCOL; + change_end = -1; + if (diff_find_change(curwin, lnum, &diffline)) { + hlID = HLF_ADD; // added line + } else { + hlID = HLF_CHD; // changed line + if (diffline.num_changes > 0 && cache_results) { + change_start = diffline.changes[0].dc_start[diffline.bufidx]; + change_end = diffline.changes[0].dc_end[diffline.bufidx]; + } + } + } else { + hlID = HLF_ADD; // added line + } + } else { + hlID = (hlf_T)0; + } + + if (cache_results) { + prev_lnum = lnum; + changedtick = buf_get_changedtick(curbuf); + fnum = curbuf->b_fnum; + prev_diff_flags = diff_flags; + } + } + + if (hlID == HLF_CHD || hlID == HLF_TXD) { + int col = (int)tv_get_number(&argvars[1]) - 1; // Ignore type error in {col}. + if (cache_results) { + if (col >= change_start && col < change_end) { + hlID = HLF_TXD; // Changed text. + } else { + hlID = HLF_CHD; // Changed line. + } + } else { + hlID = HLF_CHD; + for (int i = 0; i < diffline.num_changes; i++) { + bool added = diff_change_parse(&diffline, &diffline.changes[i], + &change_start, &change_end); + if (col >= change_start && col < change_end) { + hlID = added ? HLF_TXA : HLF_TXD; + break; + } + if (col < change_start) { + // the remaining changes are past this column and not relevant + break; + } + } + } + } + rettv->vval.v_number = hlID; +} diff --git a/src/nvim/drawline.c b/src/nvim/drawline.c index 16e3a95121..43d9f67b5c 100644 --- a/src/nvim/drawline.c +++ b/src/nvim/drawline.c @@ -844,6 +844,18 @@ static void apply_cursorline_highlight(win_T *wp, winlinevars_T *wlv) } } +static void set_line_attr_for_diff(win_T *wp, winlinevars_T *wlv) +{ + wlv->line_attr = win_hl_attr(wp, (int)wlv->diff_hlf); + // Overlay CursorLine onto diff-mode highlight. + if (wlv->cul_attr) { + wlv->line_attr = 0 != wlv->line_attr_lowprio // Low-priority CursorLine + ? hl_combine_attr(hl_combine_attr(wlv->cul_attr, wlv->line_attr), + hl_get_underline()) + : hl_combine_attr(wlv->line_attr, wlv->cul_attr); + } +} + /// Checks if there is more inline virtual text that need to be drawn. static bool has_more_inline_virt(winlinevars_T *wlv, ptrdiff_t v) { @@ -1259,14 +1271,28 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b int linestatus = 0; wlv.filler_lines = diff_check_with_linestatus(wp, lnum, &linestatus); + diffline_T line_changes = { 0 }; + int change_index = -1; if (wlv.filler_lines < 0 || linestatus < 0) { if (wlv.filler_lines == -1 || linestatus == -1) { - if (diff_find_change(wp, lnum, &change_start, &change_end)) { - wlv.diff_hlf = HLF_ADD; // added line - } else if (change_start == 0) { - wlv.diff_hlf = HLF_TXD; // changed text + if (diff_find_change(wp, lnum, &line_changes)) { + wlv.diff_hlf = HLF_ADD; // added line + } else if (line_changes.num_changes > 0) { + bool added = diff_change_parse(&line_changes, &line_changes.changes[0], + &change_start, &change_end); + if (change_start == 0) { + if (added) { + wlv.diff_hlf = HLF_TXA; // added text on changed line + } else { + wlv.diff_hlf = HLF_TXD; // changed text on changed line + } + } else { + wlv.diff_hlf = HLF_CHD; // unchanged text on changed line + } + change_index = 0; } else { - wlv.diff_hlf = HLF_CHD; // changed line + wlv.diff_hlf = HLF_CHD; // changed line + change_index = 0; } } else { wlv.diff_hlf = HLF_ADD; // added line @@ -1846,24 +1872,32 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b } if (wlv.diff_hlf != (hlf_T)0) { + if (line_changes.num_changes > 0 + && change_index >= 0 + && change_index < line_changes.num_changes - 1) { + if (ptr - line + >= line_changes.changes[change_index + 1].dc_start[line_changes.bufidx]) { + change_index += 1; + } + } + bool added = false; + if (line_changes.num_changes > 0 && change_index >= 0 + && change_index < line_changes.num_changes) { + added = diff_change_parse(&line_changes, &line_changes.changes[change_index], + &change_start, &change_end); + } // When there is extra text (eg: virtual text) it gets the // diff highlighting for the line, but not for changed text. if (wlv.diff_hlf == HLF_CHD && ptr - line >= change_start && wlv.n_extra == 0) { - wlv.diff_hlf = HLF_TXD; // changed text - } - if (wlv.diff_hlf == HLF_TXD && ((ptr - line > change_end && wlv.n_extra == 0) - || (wlv.n_extra > 0 && wlv.extra_for_extmark))) { - wlv.diff_hlf = HLF_CHD; // changed line + wlv.diff_hlf = added ? HLF_TXA : HLF_TXD; // added/changed text } - wlv.line_attr = win_hl_attr(wp, (int)wlv.diff_hlf); - // Overlay CursorLine onto diff-mode highlight. - if (wlv.cul_attr) { - wlv.line_attr = 0 != wlv.line_attr_lowprio // Low-priority CursorLine - ? hl_combine_attr(hl_combine_attr(wlv.cul_attr, wlv.line_attr), - hl_get_underline()) - : hl_combine_attr(wlv.line_attr, wlv.cul_attr); + if ((wlv.diff_hlf == HLF_TXD || wlv.diff_hlf == HLF_TXA) + && ((ptr - line >= change_end && wlv.n_extra == 0) + || (wlv.n_extra > 0 && wlv.extra_for_extmark))) { + wlv.diff_hlf = HLF_CHD; // changed line } + set_line_attr_for_diff(wp, &wlv); } // Decide which of the highlight attributes to use. @@ -2727,8 +2761,9 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b const int cuc_attr = win_hl_attr(wp, HLF_CUC); const int mc_attr = win_hl_attr(wp, HLF_MC); - if (wlv.diff_hlf == HLF_TXD) { + if (wlv.diff_hlf == HLF_TXD || wlv.diff_hlf == HLF_TXA) { wlv.diff_hlf = HLF_CHD; + set_line_attr_for_diff(wp, &wlv); } const int diff_attr = wlv.diff_hlf != 0 diff --git a/src/nvim/drawscreen.c b/src/nvim/drawscreen.c index 4c1b756ea1..2905d51657 100644 --- a/src/nvim/drawscreen.c +++ b/src/nvim/drawscreen.c @@ -1700,6 +1700,16 @@ static void win_update(win_T *wp) } } + // Below logic compares wp->w_topline against wp->w_lines[0].wl_lnum, + // which may point to a line below wp->w_topline if it is concealed; + // incurring scrolling even though wp->w_topline is still the same. + // Compare against an adjusted topline instead: + linenr_T topline_conceal = wp->w_topline; + while (decor_conceal_line(wp, topline_conceal - 1, false)) { + topline_conceal++; + hasFolding(wp, topline_conceal, NULL, &topline_conceal); + } + // If there are no changes on the screen that require a complete redraw, // handle three cases: // 1: we are off the top of the screen by a few lines: scroll down @@ -1712,12 +1722,12 @@ static void win_update(win_T *wp) if (mod_top != 0 && wp->w_topline == mod_top && (!wp->w_lines[0].wl_valid - || wp->w_topline == wp->w_lines[0].wl_lnum)) { + || topline_conceal == 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 + && (topline_conceal < wp->w_lines[0].wl_lnum + || (topline_conceal == wp->w_lines[0].wl_lnum && wp->w_topfill > wp->w_old_topfill))) { // New topline is above old topline: May scroll down. int j; diff --git a/src/nvim/edit.c b/src/nvim/edit.c index f2001b6f6f..4eeba5e38d 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -2774,7 +2774,7 @@ char *get_last_insert_save(void) char *s = xmemdupz(insert.data, insert.size); if (insert.size > 0 && s[insert.size - 1] == ESC) { // remain trailing ESC - s[insert.size - 1] = NUL; + s[--insert.size] = NUL; } return s; } diff --git a/src/nvim/eval.c b/src/nvim/eval.c index ee5dd25e93..3f73074333 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -8530,7 +8530,7 @@ void script_host_eval(char *name, typval_T *argvars, typval_T *rettv) typval_T eval_call_provider(char *provider, char *method, list_T *arguments, bool discard) { if (!eval_has_provider(provider, false)) { - semsg("E319: No \"%s\" provider found. Run \":checkhealth provider\"", + semsg("E319: No \"%s\" provider found. Run \":checkhealth vim.provider\"", provider); return (typval_T){ .v_type = VAR_NUMBER, diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index edb77778a3..25252cdfde 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -34,7 +34,6 @@ #include "nvim/cmdexpand_defs.h" #include "nvim/context.h" #include "nvim/cursor.h" -#include "nvim/diff.h" #include "nvim/edit.h" #include "nvim/errors.h" #include "nvim/eval.h" @@ -1300,63 +1299,6 @@ static void f_did_filetype(typval_T *argvars, typval_T *rettv, EvalFuncData fptr rettv->vval.v_number = curbuf->b_did_filetype; } -/// "diff_filler()" function -static void f_diff_filler(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) -{ - rettv->vval.v_number = MAX(0, diff_check(curwin, tv_get_lnum(argvars))); -} - -/// "diff_hlID()" function -static void f_diff_hlID(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) -{ - linenr_T lnum = tv_get_lnum(argvars); - static linenr_T prev_lnum = 0; - static varnumber_T changedtick = 0; - static int fnum = 0; - static int change_start = 0; - static int change_end = 0; - static hlf_T hlID = (hlf_T)0; - - if (lnum < 0) { // ignore type error in {lnum} arg - lnum = 0; - } - if (lnum != prev_lnum - || changedtick != buf_get_changedtick(curbuf) - || fnum != curbuf->b_fnum) { - // New line, buffer, change: need to get the values. - int linestatus = 0; - int filler_lines = diff_check_with_linestatus(curwin, lnum, &linestatus); - if (filler_lines < 0 || linestatus < 0) { - if (filler_lines == -1 || linestatus == -1) { - change_start = MAXCOL; - change_end = -1; - if (diff_find_change(curwin, lnum, &change_start, &change_end)) { - hlID = HLF_ADD; // added line - } else { - hlID = HLF_CHD; // changed line - } - } else { - hlID = HLF_ADD; // added line - } - } else { - hlID = (hlf_T)0; - } - prev_lnum = lnum; - changedtick = buf_get_changedtick(curbuf); - fnum = curbuf->b_fnum; - } - - if (hlID == HLF_CHD || hlID == HLF_TXD) { - int col = (int)tv_get_number(&argvars[1]) - 1; // Ignore type error in {col}. - if (col >= change_start && col <= change_end) { - hlID = HLF_TXD; // Changed text. - } else { - hlID = HLF_CHD; // Changed line. - } - } - rettv->vval.v_number = hlID; -} - /// "empty({expr})" function static void f_empty(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) { diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 6c5cca5d5c..746dc15c71 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -407,6 +407,7 @@ int do_cmdline(char *cmdline, LineGetter fgetline, void *cookie, int flags) int count = 0; // line number count bool did_inc = false; // incremented RedrawingDisabled int block_indent = -1; // indent for ext_cmdline block event + char *block_line = NULL; // block_line for ext_cmdline block event int retval = OK; cstack_T cstack = { // conditional stack .cs_idx = -1, @@ -577,7 +578,9 @@ int do_cmdline(char *cmdline, LineGetter fgetline, void *cookie, int flags) int indent = cstack.cs_idx < 0 ? 0 : (cstack.cs_idx + 1) * 2; if (count >= 1 && getline_equal(fgetline, cookie, getexline)) { if (ui_has(kUICmdline)) { - ui_ext_cmdline_block_append((size_t)MAX(0, block_indent), last_cmdline); + char *line = block_line == last_cmdline ? "" : last_cmdline; + ui_ext_cmdline_block_append((size_t)MAX(0, block_indent), line); + block_line = last_cmdline; block_indent = indent; } else if (count == 1) { // Need to set msg_didout for the first line after an ":if", @@ -683,8 +686,7 @@ int do_cmdline(char *cmdline, LineGetter fgetline, void *cookie, int flags) // If the command was typed, remember it for the ':' register. // Do this AFTER executing the command to make :@: work. - if (getline_equal(fgetline, cookie, getexline) - && new_last_cmdline != NULL) { + if (getline_equal(fgetline, cookie, getexline) && new_last_cmdline != NULL) { xfree(last_cmdline); last_cmdline = new_last_cmdline; new_last_cmdline = NULL; diff --git a/src/nvim/highlight.h b/src/nvim/highlight.h index a89d778474..03853c2ddb 100644 --- a/src/nvim/highlight.h +++ b/src/nvim/highlight.h @@ -45,6 +45,7 @@ EXTERN const char *hlf_names[] INIT( = { [HLF_CHD] = "DiffChange", [HLF_DED] = "DiffDelete", [HLF_TXD] = "DiffText", + [HLF_TXA] = "DiffTextAdd", [HLF_SC] = "SignColumn", [HLF_CONCEAL] = "Conceal", [HLF_SPB] = "SpellBad", diff --git a/src/nvim/highlight_defs.h b/src/nvim/highlight_defs.h index cbbc28311f..dba69fcf71 100644 --- a/src/nvim/highlight_defs.h +++ b/src/nvim/highlight_defs.h @@ -93,6 +93,7 @@ typedef enum { HLF_CHD, ///< Changed diff line HLF_DED, ///< Deleted diff line HLF_TXD, ///< Text Changed in diff line + HLF_TXA, ///< Text Added in changed diff line HLF_SC, ///< Sign column HLF_CONCEAL, ///< Concealed text HLF_SPB, ///< SpellBad diff --git a/src/nvim/highlight_group.c b/src/nvim/highlight_group.c index 901d2c84bc..2ecd3b9af7 100644 --- a/src/nvim/highlight_group.c +++ b/src/nvim/highlight_group.c @@ -159,6 +159,7 @@ static const char *highlight_init_both[] = { "default link CursorIM Cursor", "default link CursorLineFold FoldColumn", "default link CursorLineSign SignColumn", + "default link DiffTextAdd DiffText", "default link EndOfBuffer NonText", "default link FloatBorder NormalFloat", "default link FloatFooter FloatTitle", diff --git a/src/nvim/keycodes.c b/src/nvim/keycodes.c index 9cb2321eee..778682a881 100644 --- a/src/nvim/keycodes.c +++ b/src/nvim/keycodes.c @@ -338,10 +338,7 @@ char *get_special_key_name(int c, int modifiers) } } } else { // use name of special key - const String *s = key_names_table[table_idx].pref_name != NULL - ? key_names_table[table_idx].pref_name - : &key_names_table[table_idx].name; - + const String *s = &key_names_table[table_idx].name; if ((int)s->size + idx + 2 <= MAX_KEY_NAME_LEN) { STRCPY(string + idx, s->data); idx += (int)s->size; @@ -593,7 +590,7 @@ static int extract_modifiers(int key, int *modp, const bool simplify, bool *cons int find_special_key_in_table(int c) { for (int i = 0; i < (int)ARRAY_SIZE(key_names_table); i++) { - if (c == key_names_table[i].key) { + if (c == key_names_table[i].key && !key_names_table[i].is_alt) { return i; } } diff --git a/src/nvim/lua/treesitter.c b/src/nvim/lua/treesitter.c index a346bf5963..5d3599920e 100644 --- a/src/nvim/lua/treesitter.c +++ b/src/nvim/lua/treesitter.c @@ -15,6 +15,8 @@ #include <tree_sitter/api.h> #include <uv.h> +#include "nvim/os/time.h" + #ifdef HAVE_WASMTIME # include <wasm.h> @@ -52,6 +54,11 @@ typedef struct { TSTree *tree; } TSLuaTree; +typedef struct { + uint64_t parse_start_time; + uint64_t timeout_threshold_ns; +} TSLuaParserCallbackPayload; + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "lua/treesitter.c.generated.h" #endif @@ -362,8 +369,6 @@ static struct luaL_Reg parser_meta[] = { { "reset", parser_reset }, { "set_included_ranges", parser_set_ranges }, { "included_ranges", parser_get_ranges }, - { "set_timeout", parser_set_timeout }, - { "timeout", parser_get_timeout }, { "_set_logger", parser_set_logger }, { "_logger", parser_get_logger }, { NULL, NULL } @@ -487,6 +492,13 @@ static void push_ranges(lua_State *L, const TSRange *ranges, const size_t length } } +static bool on_parser_progress(TSParseState *state) +{ + TSLuaParserCallbackPayload *payload = state->payload; + uint64_t parse_time = os_hrtime() - payload->parse_start_time; + return parse_time >= payload->timeout_threshold_ns; +} + static int parser_parse(lua_State *L) { TSParser *p = parser_check(L, 1); @@ -524,7 +536,17 @@ static int parser_parse(lua_State *L) } input = (TSInput){ (void *)buf, input_cb, TSInputEncodingUTF8, NULL }; - new_tree = ts_parser_parse(p, old_tree, input); + if (!lua_isnil(L, 5)) { + uint64_t timeout_ns = (uint64_t)lua_tointeger(L, 5); + TSLuaParserCallbackPayload payload = + (TSLuaParserCallbackPayload){ .parse_start_time = os_hrtime(), + .timeout_threshold_ns = timeout_ns }; + TSParseOptions parse_options = { .payload = &payload, + .progress_callback = on_parser_progress }; + new_tree = ts_parser_parse_with_options(p, old_tree, input, parse_options); + } else { + new_tree = ts_parser_parse(p, old_tree, input); + } break; @@ -534,12 +556,11 @@ static int parser_parse(lua_State *L) bool include_bytes = (lua_gettop(L) >= 4) && lua_toboolean(L, 4); - // Sometimes parsing fails (timeout, or wrong parser ABI) - // In those case, just return an error. if (!new_tree) { - if (ts_parser_timeout_micros(p) == 0) { - // No timeout set, must have had an error - return luaL_error(L, "An error occurred when parsing."); + // Sometimes parsing fails (no language was set, or it was set to one with an incompatible ABI) + // In those cases, just return an error. + if (!ts_parser_language(p)) { + return luaL_error(L, "Language was unset, or has an incompatible ABI."); } return 0; } @@ -670,26 +691,6 @@ static int parser_get_ranges(lua_State *L) return 1; } -static int parser_set_timeout(lua_State *L) -{ - TSParser *p = parser_check(L, 1); - - if (lua_gettop(L) < 2) { - luaL_error(L, "integer expected"); - } - - uint32_t timeout = (uint32_t)luaL_checkinteger(L, 2); - ts_parser_set_timeout_micros(p, timeout); - return 0; -} - -static int parser_get_timeout(lua_State *L) -{ - TSParser *p = parser_check(L, 1); - lua_pushinteger(L, (lua_Integer)ts_parser_timeout_micros(p)); - return 1; -} - static void logger_cb(void *payload, TSLogType logtype, const char *s) { TSLuaLoggerOpts *opts = (TSLuaLoggerOpts *)payload; diff --git a/src/nvim/mapping.c b/src/nvim/mapping.c index ab9d68f911..3dc66f7dba 100644 --- a/src/nvim/mapping.c +++ b/src/nvim/mapping.c @@ -2805,7 +2805,8 @@ void modify_keymap(uint64_t channel_id, Buffer buffer, bool is_unmap, String mod goto fail_and_free; case 5: api_set_error(err, kErrorTypeException, - "E227: mapping already exists for %s", parsed_args.lhs); + is_abbrev ? e_abbreviation_already_exists_for_str + : e_mapping_already_exists_for_str, lhs.data); goto fail_and_free; default: assert(false && "Unrecognized return code!"); diff --git a/src/nvim/mouse.c b/src/nvim/mouse.c index b0ab235f6b..0003d88543 100644 --- a/src/nvim/mouse.c +++ b/src/nvim/mouse.c @@ -655,6 +655,7 @@ bool do_mouse(oparg_T *oap, int c, int dir, int count, bool fixindent) bool in_winbar = (jump_flags & MOUSE_WINBAR); bool in_statuscol = (jump_flags & MOUSE_STATUSCOL); bool in_status_line = (jump_flags & IN_STATUS_LINE); + bool in_global_statusline = in_status_line && global_stl_height() > 0; bool in_sep_line = (jump_flags & IN_SEP_LINE); if ((in_winbar || in_status_line || in_statuscol) && is_click) { @@ -671,7 +672,7 @@ bool do_mouse(oparg_T *oap, int c, int dir, int count, bool fixindent) : in_winbar ? wp->w_winbar_click_defs : wp->w_statuscol_click_defs; - if (in_status_line && global_stl_height() > 0) { + if (in_global_statusline) { // global statusline is displayed for the current window, // and spans the whole screen. click_defs = curwin->w_status_click_defs; @@ -681,7 +682,11 @@ bool do_mouse(oparg_T *oap, int c, int dir, int count, bool fixindent) if (in_statuscol && wp->w_p_rl) { click_col = wp->w_width_inner - click_col - 1; } - if (in_statuscol && click_col >= (int)wp->w_statuscol_click_defs_size) { + + if ((in_statuscol && click_col >= (int)wp->w_statuscol_click_defs_size) + || (in_status_line + && click_col >= + (int)(in_global_statusline ? curwin : wp)->w_status_click_defs_size)) { return false; } diff --git a/src/nvim/option_vars.h b/src/nvim/option_vars.h index 2e5698870f..e624ab80ef 100644 --- a/src/nvim/option_vars.h +++ b/src/nvim/option_vars.h @@ -13,11 +13,11 @@ // option_vars.h: definition of global variables for settable options #define HIGHLIGHT_INIT \ - "8:SpecialKey,~:EndOfBuffer,z:TermCursor,@:NonText,d:Directory,e:ErrorMsg," \ - "i:IncSearch,l:Search,y:CurSearch,m:MoreMsg,M:ModeMsg,n:LineNr,a:LineNrAbove,b:LineNrBelow," \ - "N:CursorLineNr,G:CursorLineSign,O:CursorLineFold,r:Question,s:StatusLine,S:StatusLineNC," \ - "c:VertSplit,t:Title,v:Visual,V:VisualNOS,w:WarningMsg,W:WildMenu,f:Folded,F:FoldColumn," \ - "A:DiffAdd,C:DiffChange,D:DiffDelete,T:DiffText,>:SignColumn,-:Conceal,B:SpellBad,P:SpellCap," \ + "8:SpecialKey,~:EndOfBuffer,z:TermCursor,@:NonText,d:Directory,e:ErrorMsg,i:IncSearch,l:Search," \ + "y:CurSearch,m:MoreMsg,M:ModeMsg,n:LineNr,a:LineNrAbove,b:LineNrBelow,N:CursorLineNr," \ + "G:CursorLineSign,O:CursorLineFold,r:Question,s:StatusLine,S:StatusLineNC,c:VertSplit,t:Title," \ + "v:Visual,V:VisualNOS,w:WarningMsg,W:WildMenu,f:Folded,F:FoldColumn,A:DiffAdd,C:DiffChange," \ + "D:DiffDelete,T:DiffText,E:DiffTextAdd,>:SignColumn,-:Conceal,B:SpellBad,P:SpellCap," \ "R:SpellRare,L:SpellLocal,+:Pmenu,=:PmenuSel,k:PmenuMatch,<:PmenuMatchSel,[:PmenuKind," \ "]:PmenuKindSel,{:PmenuExtra,}:PmenuExtraSel,x:PmenuSbar,X:PmenuThumb,*:TabLine,#:TabLineSel," \ "_:TabLineFill,!:CursorColumn,.:CursorLine,o:ColorColumn,q:QuickFixLine,z:StatusLineTerm," \ @@ -306,6 +306,7 @@ EXTERN char *p_csl; ///< 'completeslash' EXTERN OptInt p_pb; ///< 'pumblend' EXTERN OptInt p_ph; ///< 'pumheight' EXTERN OptInt p_pw; ///< 'pumwidth' +EXTERN OptInt p_pmw; ///< 'pummaxwidth' EXTERN char *p_com; ///< 'comments' EXTERN char *p_cpo; ///< 'cpoptions' EXTERN char *p_debug; ///< 'debug' diff --git a/src/nvim/options.lua b/src/nvim/options.lua index f566582c0c..f261abf30e 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -2188,7 +2188,7 @@ local options = { { abbreviation = 'dip', cb = 'did_set_diffopt', - defaults = 'internal,filler,closeoff,linematch:40', + defaults = 'internal,filler,closeoff,inline:simple,linematch:40', -- Keep this in sync with diffopt_changed(). values = { 'filler', @@ -2207,6 +2207,7 @@ local options = { 'internal', 'indent-heuristic', { 'algorithm:', { 'myers', 'minimal', 'patience', 'histogram' } }, + { 'inline:', { 'none', 'simple', 'char', 'word' } }, 'linematch:', }, deny_duplicates = true, @@ -2272,6 +2273,21 @@ local options = { Use the indent heuristic for the internal diff library. + inline:{text} Highlight inline differences within a change. + See |view-diffs|. Supported values are: + + none Do not perform inline highlighting. + simple Highlight from first different + character to the last one in each + line. This is the default if no + `inline:` value is set. + char Use internal diff to perform a + character-wise diff and highlight the + difference. + word Use internal diff to perform a + |word|-wise diff and highlight the + difference. + internal Use the internal diff library. This is ignored when 'diffexpr' is set. *E960* When running out of memory when writing a @@ -6410,6 +6426,21 @@ local options = { varname = 'p_ph', }, { + abbreviation = 'pmw', + defaults = 0, + desc = [=[ + Maximum width for the popup menu (|ins-completion-menu|). When zero, + there is no maximum width limit, otherwise the popup menu will never be + wider than this value. Truncated text will be indicated by "..." at the + end. Takes precedence over 'pumwidth'. + ]=], + full_name = 'pummaxwidth', + scope = { 'global' }, + short_desc = N_('maximum width of the popup menu'), + type = 'number', + varname = 'p_pmw', + }, + { abbreviation = 'pw', defaults = 15, desc = [=[ diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c index c6cc7af8cd..905656ccc9 100644 --- a/src/nvim/optionstr.c +++ b/src/nvim/optionstr.c @@ -1018,6 +1018,16 @@ int expand_set_diffopt(optexpand_T *args, int *numMatches, char ***matches) numMatches, matches); } + // Within "inline:", we have a subgroup of possible options. + const size_t inline_len = strlen("inline:"); + if (xp->xp_pattern - args->oe_set_arg >= (int)inline_len + && strncmp(xp->xp_pattern - inline_len, "inline:", inline_len) == 0) { + return expand_set_opt_string(args, + opt_dip_inline_values, + ARRAY_SIZE(opt_dip_inline_values) - 1, + numMatches, + matches); + } return FAIL; } diff --git a/src/nvim/os/shell.c b/src/nvim/os/shell.c index d60d0b3e55..e711611d67 100644 --- a/src/nvim/os/shell.c +++ b/src/nvim/os/shell.c @@ -1211,7 +1211,6 @@ static void read_input(StringBuilder *buf) size_t lplen = (size_t)ml_get_len(lnum); while (true) { - lplen -= written; if (lplen == 0) { len = 0; } else if (lp[written] == NL) { @@ -1220,11 +1219,11 @@ static void read_input(StringBuilder *buf) kv_push(*buf, NUL); } else { char *s = vim_strchr(lp + written, NL); - len = s == NULL ? lplen : (size_t)(s - (lp + written)); + len = s == NULL ? lplen - written : (size_t)(s - (lp + written)); kv_concat_len(*buf, lp + written, len); } - if (len == lplen) { + if (len == lplen - written) { // Finished a line, add a NL, unless this line should not have one. if (lnum != curbuf->b_op_end.lnum || (!curbuf->b_p_bin && curbuf->b_p_fixeol) diff --git a/src/nvim/popupmenu.c b/src/nvim/popupmenu.c index b1c6e02449..2e5d08e5ae 100644 --- a/src/nvim/popupmenu.c +++ b/src/nvim/popupmenu.c @@ -199,6 +199,9 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i } int def_width = (int)p_pw; + if (p_pmw > 0 && def_width > p_pmw) { + def_width = (int)p_pmw; + } win_T *pvwin = NULL; FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { @@ -307,6 +310,9 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i pum_compute_size(); int max_width = pum_base_width; + if (p_pmw > 0 && max_width > p_pmw) { + max_width = (int)p_pmw; + } // if there are more items than room we need a scrollbar if (pum_height < size) { @@ -339,6 +345,9 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i if (pum_width > content_width && pum_width > p_pw) { // Reduce width to fit item pum_width = MAX(content_width, (int)p_pw); + if (p_pmw > 0 && pum_width > p_pmw) { + pum_width = (int)p_pmw; + } } else if (((cursor_col - min_col > p_pw || cursor_col - min_col > max_width) && !pum_rl) || (pum_rl && (cursor_col < max_col - p_pw @@ -365,6 +374,9 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i if (pum_width < p_pw) { pum_width = (int)p_pw; + if (p_pmw > 0 && pum_width > p_pmw) { + pum_width = (int)p_pmw; + } if (pum_rl) { if (pum_width > pum_col - min_col) { pum_width = pum_col - min_col; @@ -376,6 +388,11 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i } } else if (pum_width > content_width && pum_width > p_pw) { pum_width = MAX(content_width, (int)p_pw); + if (p_pmw > 0 && pum_width > p_pmw) { + pum_width = (int)p_pmw; + } + } else if (p_pmw > 0 && pum_width > p_pmw) { + pum_width = (int)p_pmw; } } } else if (max_col - min_col < def_width) { @@ -386,11 +403,17 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i pum_col = min_col; } pum_width = max_col - min_col - 1; + if (p_pmw > 0 && pum_width > p_pmw) { + pum_width = (int)p_pmw; + } } else { if (max_width > p_pw) { // truncate max_width = (int)p_pw; } + if (p_pmw > 0 && max_width > p_pmw) { + max_width = (int)p_pmw; + } if (pum_rl) { pum_col = min_col + max_width - 1; } else { @@ -611,6 +634,8 @@ void pum_redraw(void) thumb_pos = (pum_first * (pum_height - thumb_height) + scroll_range / 2) / scroll_range; } + const int ellipsis_width = 3; + for (int i = 0; i < pum_height; i++) { int idx = i + pum_first; const hlf_T *const hlfs = (idx == pum_selected) ? hlfsSel : hlfsNorm; @@ -633,6 +658,7 @@ void pum_redraw(void) // Do this 3 times and order from p_cia int grid_col = col_off; int totwidth = 0; + bool need_ellipsis = false; int order[3]; int items_width_array[3] = { pum_base_width, pum_kind_width, pum_extra_width }; pum_align_order(order); @@ -684,15 +710,19 @@ void pum_redraw(void) if (pum_rl) { char *rt = reverse_text(st); char *rt_start = rt; - int cells = vim_strsize(rt); + int cells = (int)mb_string2cells(rt); + if (p_pmw > ellipsis_width && pum_width == p_pmw + && grid_col - cells < col_off - pum_width) { + need_ellipsis = true; + } - if (cells > pum_width) { + if (grid_col - cells < col_off - pum_width) { do { cells -= utf_ptr2cells(rt); MB_PTR_ADV(rt); - } while (cells > pum_width); + } while (grid_col - cells < col_off - pum_width); - if (cells < pum_width) { + if (grid_col - cells > col_off - pum_width) { // Most left character requires 2-cells but only 1 cell is available on // screen. Put a '<' on the left of the pum item. *(--rt) = '<'; @@ -710,10 +740,16 @@ void pum_redraw(void) xfree(st); grid_col -= width; } else { + int cells = (int)mb_string2cells(st); + if (p_pmw > ellipsis_width && pum_width == p_pmw + && grid_col + cells > col_off + pum_width) { + need_ellipsis = true; + } + if (attrs == NULL) { grid_line_puts(grid_col, st, -1, attr); } else { - pum_grid_puts_with_attrs(grid_col, vim_strsize(st), st, -1, attrs); + pum_grid_puts_with_attrs(grid_col, cells, st, -1, attrs); } xfree(st); @@ -772,9 +808,24 @@ void pum_redraw(void) } if (pum_rl) { - grid_line_fill(col_off - pum_width + 1, grid_col + 1, schar_from_ascii(' '), orig_attr); + const int lcol = col_off - pum_width + 1; + grid_line_fill(lcol, grid_col + 1, schar_from_ascii(' '), orig_attr); + if (need_ellipsis) { + bool over_wide = pum_width > ellipsis_width && linebuf_char[lcol + ellipsis_width] == NUL; + grid_line_fill(lcol, lcol + ellipsis_width, schar_from_ascii('.'), orig_attr); + if (over_wide) { + grid_line_put_schar(lcol + ellipsis_width, schar_from_ascii(' '), orig_attr); + } + } } else { - grid_line_fill(grid_col, col_off + pum_width, schar_from_ascii(' '), orig_attr); + const int rcol = col_off + pum_width; + grid_line_fill(grid_col, rcol, schar_from_ascii(' '), orig_attr); + if (need_ellipsis) { + if (pum_width > ellipsis_width && linebuf_char[rcol - ellipsis_width] == NUL) { + grid_line_put_schar(rcol - ellipsis_width - 1, schar_from_ascii(' '), orig_attr); + } + grid_line_fill(rcol - ellipsis_width, rcol, schar_from_ascii('.'), orig_attr); + } } if (pum_scrollbar > 0) { diff --git a/src/nvim/regexp.c b/src/nvim/regexp.c index de9a7e580f..7a8d963dee 100644 --- a/src/nvim/regexp.c +++ b/src/nvim/regexp.c @@ -367,6 +367,8 @@ static const char e_nfa_regexp_missing_value_in_chr[] static const char e_atom_engine_must_be_at_start_of_pattern[] = N_("E1281: Atom '\\%%#=%c' must be at the start of the pattern"); static const char e_substitute_nesting_too_deep[] = N_("E1290: substitute nesting too deep"); +static const char e_unicode_val_too_large[] + = N_("E1541: Value too large, max Unicode codepoint is U+10FFFF"); #define NOT_MULTI 0 #define MULTI_ONE 1 @@ -4796,6 +4798,11 @@ collection: || *regparse == 'u' || *regparse == 'U') { startc = coll_get_char(); + // max UTF-8 Codepoint is U+10FFFF, + // but allow values until INT_MAX + if (startc == INT_MAX) { + EMSG_RET_NULL(_(e_unicode_val_too_large)); + } if (startc == 0) { regc(0x0a); } else { @@ -5548,12 +5555,15 @@ static int coll_get_char(void) case 'U': nr = gethexchrs(8); break; } - if (nr < 0 || nr > INT_MAX) { + if (nr < 0) { // If getting the number fails be backwards compatible: the character // is a backslash. regparse--; nr = '\\'; } + if (nr > INT_MAX) { + nr = INT_MAX; + } return (int)nr; } @@ -10565,6 +10575,11 @@ collection: || *regparse == 'U') { // TODO(RE): This needs more testing startc = coll_get_char(); + // max UTF-8 Codepoint is U+10FFFF, + // but allow values until INT_MAX + if (startc == INT_MAX) { + EMSG_RET_FAIL(_(e_unicode_val_too_large)); + } got_coll_char = true; MB_PTR_BACK(old_regparse, regparse); } else { diff --git a/src/nvim/vterm/state.c b/src/nvim/vterm/state.c index 0e43107347..3b7b5e2997 100644 --- a/src/nvim/vterm/state.c +++ b/src/nvim/vterm/state.c @@ -1948,28 +1948,26 @@ static int on_osc(int command, VTermStringFragment frag, void *user) case 0: settermprop_string(state, VTERM_PROP_ICONNAME, frag); settermprop_string(state, VTERM_PROP_TITLE, frag); - return 1; + break; case 1: settermprop_string(state, VTERM_PROP_ICONNAME, frag); - return 1; + break; case 2: settermprop_string(state, VTERM_PROP_TITLE, frag); - return 1; + break; case 52: if (state->selection.callbacks) { osc_selection(state, frag); } + break; + } - return 1; - - default: - if (state->fallbacks && state->fallbacks->osc) { - if ((*state->fallbacks->osc)(command, frag, state->fbdata)) { - return 1; - } + if (state->fallbacks && state->fallbacks->osc) { + if ((*state->fallbacks->osc)(command, frag, state->fbdata)) { + return 1; } } diff --git a/src/nvim/window.c b/src/nvim/window.c index dd351a6af7..916c193469 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -196,6 +196,10 @@ win_T *swbuf_goto_win_with_buf(buf_T *buf) return wp; } +// 'cmdheight' value explicitly set by the user: window commands are allowed to +// resize the topframe to values higher than this minimum, but not lower. +static OptInt min_set_ch = 1; + /// all CTRL-W window commands are handled here, called from normal_cmd(). /// /// @param xchar extra char from ":wincmd gx" or NUL @@ -513,7 +517,7 @@ newwindow: // set current window height case Ctrl__: case '_': - win_setheight(Prenum ? Prenum : Rows - 1); + win_setheight(Prenum ? Prenum : Rows - (int)min_set_ch); break; // increase current window width @@ -3505,10 +3509,6 @@ static bool is_bottom_win(win_T *wp) return true; } -// 'cmdheight' value explicitly set by the user: window commands are allowed to -// resize the topframe to values higher than this minimum, but not lower. -static OptInt min_set_ch = 1; - /// Set a new height for a frame. Recursively sets the height for contained /// frames and windows. Caller must take care of positions. /// |