diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/nvim/buffer_defs.h | 52 | ||||
-rw-r--r-- | src/nvim/change.c | 1 | ||||
-rw-r--r-- | src/nvim/diff.c | 719 | ||||
-rw-r--r-- | src/nvim/drawline.c | 71 | ||||
-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/option_vars.h | 10 | ||||
-rw-r--r-- | src/nvim/options.lua | 18 | ||||
-rw-r--r-- | src/nvim/optionstr.c | 10 |
10 files changed, 788 insertions, 96 deletions
diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index c4241eed45..be28e08675 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 paste 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 4c5b86adc4..585a937558 100644 --- a/src/nvim/diff.c +++ b/src/nvim/diff.c @@ -87,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; @@ -137,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 @@ -523,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; @@ -533,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; @@ -764,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; } @@ -944,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); @@ -1640,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 @@ -1662,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; } @@ -1683,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 { @@ -1717,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; @@ -1735,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) { @@ -1789,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; } @@ -2532,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); @@ -2604,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; } +} - if ((dp == NULL) || (diff_check_sanity(curtab, dp) == FAIL)) { - xfree(line_org); +/// used for simple inline diff algorithm +static diffline_change_T simple_diffline_change; + +/// 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; @@ -2660,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 @@ -2738,6 +2840,470 @@ 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; + } + + if (lnum - dp->df_lnum[idx] > INT_MAX) { + // Integer overflow protection + 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. + idx = diff_buf_idx(wp->w_buffer, curtab); + 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. /// @@ -3499,20 +4065,29 @@ void f_diff_filler(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) /// "diff_hlID()" function 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 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 (lnum != prev_lnum + if (!cache_results + || lnum != prev_lnum || changedtick != buf_get_changedtick(curbuf) - || fnum != curbuf->b_fnum) { + || 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); @@ -3520,10 +4095,14 @@ void f_diff_hlID(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) if (filler_lines == -1 || linestatus == -1) { change_start = MAXCOL; change_end = -1; - if (diff_find_change(curwin, lnum, &change_start, &change_end)) { + 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 @@ -3531,17 +4110,37 @@ void f_diff_hlID(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) } else { hlID = (hlf_T)0; } - prev_lnum = lnum; - changedtick = buf_get_changedtick(curbuf); - fnum = curbuf->b_fnum; + + 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 (col >= change_start && col <= change_end) { - hlID = HLF_TXD; // Changed text. + 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; // Changed line. + 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/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/option_vars.h b/src/nvim/option_vars.h index 2e5698870f..0b5d0a45b4 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," \ diff --git a/src/nvim/options.lua b/src/nvim/options.lua index f566582c0c..4a11078b80 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 nothing + 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 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; } |