diff options
author | Björn Linse <bjorn.linse@gmail.com> | 2018-03-24 14:26:20 +0100 |
---|---|---|
committer | Björn Linse <bjorn.linse@gmail.com> | 2018-09-17 10:41:29 +0200 |
commit | 45f53b370b11626468c35b58b939dfd2f0aa9de0 (patch) | |
tree | a1487b7201dc53b0658f8df9a7534f685f48a39c | |
parent | b2d7b70f4ddd043a7f0f905c74f7d7e2e2a81ebb (diff) | |
download | rneovim-45f53b370b11626468c35b58b939dfd2f0aa9de0.tar.gz rneovim-45f53b370b11626468c35b58b939dfd2f0aa9de0.tar.bz2 rneovim-45f53b370b11626468c35b58b939dfd2f0aa9de0.zip |
buffer: add support for virtual text annotations
-rw-r--r-- | src/nvim/api/buffer.c | 79 | ||||
-rw-r--r-- | src/nvim/buffer.c | 63 | ||||
-rw-r--r-- | src/nvim/bufhl_defs.h | 15 | ||||
-rw-r--r-- | src/nvim/screen.c | 207 | ||||
-rw-r--r-- | test/functional/ui/bufhl_spec.lua | 143 |
5 files changed, 429 insertions, 78 deletions
diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c index 67a4b70c9e..16196ec910 100644 --- a/src/nvim/api/buffer.c +++ b/src/nvim/api/buffer.c @@ -967,6 +967,85 @@ void nvim_buf_clear_highlight(Buffer buffer, bufhl_clear_line_range(buf, (int)src_id, (int)line_start+1, (int)line_end); } + +/// Set the virtual text (annotation) for a buffer line. +/// +/// By default (and currently the only option) the text will be placed after +/// the buffer text. Virtual text will never cause reflow, rather virtual +/// text will be truncated at the end of the screen line. The virtual text will +/// begin after one cell to the right of the ordinary text, this will contain +/// the |lcs-eol| char if set, otherwise just be a space. +/// +/// @param buffer Buffer handle +/// @param src_id Source group to use or 0 to use a new group, +/// or -1 for a ungrouped annotation +/// @param line Line to annotate with virtual text (zero-indexed) +/// @param chunks A list of [text, hl_group] arrays, each representing a +/// text chunk with specified highlight. `hl_group` element +/// can be omitted for no highlight. +/// @param opts Optional parameters. Currently not used. +/// @param[out] err Error details, if any +/// @return The src_id that was used +Integer nvim_buf_set_virtual_text(Buffer buffer, + Integer src_id, + Integer line, + Array chunks, + Dictionary opts, + Error *err) + FUNC_API_SINCE(5) +{ + buf_T *buf = find_buffer_by_handle(buffer, err); + if (!buf) { + return 0; + } + + if (line < 0 || line >= MAXLNUM) { + api_set_error(err, kErrorTypeValidation, "Line number outside range"); + return 0; + } + + if (opts.size > 0) { + api_set_error(err, kErrorTypeValidation, "opts dict isn't empty"); + return 0; + } + + VirtText virt_text = KV_INITIAL_VALUE; + for (size_t i = 0; i < chunks.size; i++) { + if (chunks.items[i].type != kObjectTypeArray) { + api_set_error(err, kErrorTypeValidation, "Chunk is not an array"); + goto free_exit; + } + Array chunk = chunks.items[i].data.array; + if (chunk.size == 0 || chunk.size > 2 + || chunk.items[0].type != kObjectTypeString + || (chunk.size == 2 && chunk.items[1].type != kObjectTypeString)) { + api_set_error(err, kErrorTypeValidation, + "Chunk is not an array with one or two strings"); + goto free_exit; + } + + String str = chunk.items[0].data.string; + char *text = xstrdup(str.size > 0 ? str.data : ""); + + int hl_id = 0; + if (chunk.size == 2) { + String hl = chunk.items[1].data.string; + if (hl.size > 0) { + hl_id = syn_check_group((char_u *)hl.data, (int)hl.size); + } + } + kv_push(virt_text, ((VirtTextChunk){ .text = text, .hl_id = hl_id })); + } + + src_id = bufhl_add_virt_text(buf, (int)src_id, (linenr_T)line+1, + virt_text); + return src_id; + +free_exit: + kv_destroy(virt_text); + return 0; +} + // Check if deleting lines made the cursor position invalid. // Changed the lines from "lo" to "hi" and added "extra" lines (negative if // deleted). diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 384c7f77b2..a0ac8d3201 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -5375,6 +5375,45 @@ void bufhl_add_hl_pos_offset(buf_T *buf, } } +int bufhl_add_virt_text(buf_T *buf, + int src_id, + linenr_T lnum, + VirtText virt_text) +{ + static int next_src_id = 1; + if (src_id == 0) { + src_id = next_src_id++; + } + + BufhlLine *lineinfo = bufhl_tree_ref(&buf->b_bufhl_info, lnum, true); + + bufhl_clear_virttext(&lineinfo->virt_text); + if (kv_size(virt_text) > 0) { + lineinfo->virt_text_src = src_id; + lineinfo->virt_text = virt_text; + } else { + lineinfo->virt_text_src = 0; + // currently not needed, but allow a future caller with + // 0 size and non-zero capacity + kv_destroy(virt_text); + } + + if (0 < lnum && lnum <= buf->b_ml.ml_line_count) { + changed_lines_buf(buf, lnum, lnum+1, 0); + redraw_buf_later(buf, VALID); + } + return src_id; +} + +static void bufhl_clear_virttext(VirtText *text) +{ + for (size_t i = 0; i < kv_size(*text); i++) { + xfree(kv_A(*text, i).text); + } + kv_destroy(*text); + *text = (VirtText)KV_INITIAL_VALUE; +} + /// Clear bufhl highlights from a given source group and range of lines. /// /// @param buf The buffer to remove highlights from @@ -5430,6 +5469,7 @@ void bufhl_clear_line_range(buf_T *buf, static BufhlLineStatus bufhl_clear_line(BufhlLine *lineinfo, int src_id, linenr_T lnum) { + BufhlLineStatus changed = kBLSUnchanged; size_t oldsize = kv_size(lineinfo->items); if (src_id < 0) { kv_size(lineinfo->items) = 0; @@ -5445,14 +5485,25 @@ static BufhlLineStatus bufhl_clear_line(BufhlLine *lineinfo, int src_id, } kv_size(lineinfo->items) = newidx; } + if (kv_size(lineinfo->items) != oldsize) { + changed = kBLSChanged; + } + + if (kv_size(lineinfo->virt_text) != 0 + && (src_id < 0 || src_id == lineinfo->virt_text_src)) { + bufhl_clear_virttext(&lineinfo->virt_text); + lineinfo->virt_text_src = 0; + changed = kBLSChanged; + } - if (kv_size(lineinfo->items) == 0) { + if (kv_size(lineinfo->items) == 0 && kv_size(lineinfo->virt_text) == 0) { kv_destroy(lineinfo->items); return kBLSDeleted; } - return kv_size(lineinfo->items) != oldsize ? kBLSChanged : kBLSUnchanged; + return changed; } + /// Remove all highlights and free the highlight data void bufhl_clear_all(buf_T *buf) { @@ -5527,8 +5578,8 @@ bool bufhl_start_line(buf_T *buf, linenr_T lnum, BufhlLineInfo *info) return false; } info->valid_to = -1; - info->entries = lineinfo->items; - return kv_size(info->entries) > 0; + info->line = lineinfo; + return true; } /// get highlighting at column col @@ -5548,8 +5599,8 @@ int bufhl_get_attr(BufhlLineInfo *info, colnr_T col) } int attr = 0; info->valid_to = MAXCOL; - for (size_t i = 0; i < kv_size(info->entries); i++) { - BufhlItem entry = kv_A(info->entries, i); + for (size_t i = 0; i < kv_size(info->line->items); i++) { + BufhlItem entry = kv_A(info->line->items, i); if (entry.start <= col && col <= entry.stop) { int entry_attr = syn_id2attr(entry.hl_id); attr = hl_combine_attr(attr, entry_attr); diff --git a/src/nvim/bufhl_defs.h b/src/nvim/bufhl_defs.h index 14b1afa7d9..d0fb40ab88 100644 --- a/src/nvim/bufhl_defs.h +++ b/src/nvim/bufhl_defs.h @@ -14,16 +14,23 @@ typedef struct { colnr_T stop; // last column to highlight } BufhlItem; -typedef kvec_t(BufhlItem) BufhlItemVec; +typedef struct { + char *text; + int hl_id; +} VirtTextChunk; + +typedef kvec_t(VirtTextChunk) VirtText; typedef struct { linenr_T line; - BufhlItemVec items; + kvec_t(BufhlItem) items; + int virt_text_src; + VirtText virt_text; } BufhlLine; -#define BUFHLLINE_INIT(l) { l, KV_INITIAL_VALUE } +#define BUFHLLINE_INIT(l) { l, KV_INITIAL_VALUE, 0, KV_INITIAL_VALUE } typedef struct { - BufhlItemVec entries; + BufhlLine *line; int current; colnr_T valid_to; } BufhlLineInfo; diff --git a/src/nvim/screen.c b/src/nvim/screen.c index 2e27bef3de..fe6a15c5fc 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -132,6 +132,15 @@ static schar_T *current_ScreenLine; StlClickDefinition *tab_page_click_defs = NULL; long tab_page_click_defs_size = 0; +// for line_putchar. Contains the state that needs to be remembered from +// putting one character to the next. +typedef struct { + const char_u *p; + int prev_c; // previous Arabic character + int prev_c1; // first composing char for prev_c +} LineState; +#define LINE_STATE(p) { p, 0, 0 } + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "screen.c.generated.h" #endif @@ -1731,6 +1740,56 @@ static int compute_foldcolumn(win_T *wp, int col) return fdc; } +/// Put a single char from an UTF-8 buffer into a line buffer. +/// +/// Handles composing chars and arabic shaping state. +static int line_putchar(LineState *s, schar_T *dest, int maxcells, bool rl) +{ + const char_u *p = s->p; + int cells = utf_ptr2cells(p); + int c_len = utfc_ptr2len(p); + int u8c, u8cc[MAX_MCO]; + if (cells > maxcells) { + return -1; + } + u8c = utfc_ptr2char(p, u8cc); + if (*p < 0x80 && u8cc[0] == 0) { + schar_from_ascii(dest[0], *p); + s->prev_c = u8c; + } else { + if (p_arshape && !p_tbidi && arabic_char(u8c)) { + // Do Arabic shaping. + int pc, pc1, nc; + int pcc[MAX_MCO]; + int firstbyte = *p; + + // The idea of what is the previous and next + // character depends on 'rightleft'. + if (rl) { + pc = s->prev_c; + pc1 = s->prev_c1; + nc = utf_ptr2char(p + c_len); + s->prev_c1 = u8cc[0]; + } else { + pc = utfc_ptr2char(p + c_len, pcc); + nc = s->prev_c; + pc1 = pcc[0]; + } + s->prev_c = u8c; + + u8c = arabic_shape(u8c, &firstbyte, &u8cc[0], pc, pc1, nc); + } else { + s->prev_c = u8c; + } + schar_from_cc(dest[0], u8c, u8cc); + } + if (cells > 1) { + dest[1][0] = 0; + } + s->p += c_len; + return cells; +} + /* * Display one folded line. */ @@ -1863,13 +1922,7 @@ static void fold_line(win_T *wp, long fold_count, foldinfo_T *foldinfo, linenr_T * Right-left text is put in columns 0 - number-col, normal text is put * in columns number-col - window-width. */ - int cells; - int u8c, u8cc[MAX_MCO]; int idx; - int c_len; - char_u *p; - int prev_c = 0; // previous Arabic character - int prev_c1 = 0; // first composing char for prev_c if (wp->w_p_rl) { idx = off; @@ -1877,50 +1930,20 @@ static void fold_line(win_T *wp, long fold_count, foldinfo_T *foldinfo, linenr_T idx = off + col; } - // Store multibyte characters in ScreenLines[] et al. correctly. - for (p = text; *p != NUL; ) { - cells = utf_ptr2cells(p); - c_len = utfc_ptr2len(p); - if (col + cells > wp->w_width - (wp->w_p_rl ? col : 0)) { - break; - } - u8c = utfc_ptr2char(p, u8cc); - if (*p < 0x80 && u8cc[0] == 0) { - schar_from_ascii(ScreenLines[idx], *p); - prev_c = u8c; - } else { - if (p_arshape && !p_tbidi && arabic_char(u8c)) { - // Do Arabic shaping. - int pc, pc1, nc; - int pcc[MAX_MCO]; - int firstbyte = *p; - - // The idea of what is the previous and next - // character depends on 'rightleft'. - if (wp->w_p_rl) { - pc = prev_c; - pc1 = prev_c1; - nc = utf_ptr2char(p + c_len); - prev_c1 = u8cc[0]; - } else { - pc = utfc_ptr2char(p + c_len, pcc); - nc = prev_c; - pc1 = pcc[0]; - } - prev_c = u8c; + LineState s = LINE_STATE(text); - u8c = arabic_shape(u8c, &firstbyte, &u8cc[0], pc, pc1, nc); - } else { - prev_c = u8c; - } - schar_from_cc(ScreenLines[idx], u8c, u8cc); - } - if (cells > 1) { - ScreenLines[idx + 1][0] = 0; + while (*s.p != NUL) { + // TODO(bfredl): cargo-culted from the old Vim code: + // if(col + cells > wp->w_width - (wp->w_p_rl ? col : 0)) { break; } + // This is obvious wrong. If Vim ever fixes this, solve for "cells" again + // in the correct condition. + int maxcells = wp->w_width - col - (wp->w_p_rl ? col : 0); + int cells = line_putchar(&s, &ScreenLines[idx], maxcells, wp->w_p_rl); + if (cells == -1) { + break; } col += cells; idx += cells; - p += c_len; } /* Fill the rest of the line with the fold filler */ @@ -2215,9 +2238,9 @@ win_line ( int did_line_attr = 0; bool search_attr_from_match = false; // if search_attr is from :match - bool has_bufhl = false; // this buffer has highlight matches - int bufhl_attr = 0; // attributes desired by bufhl BufhlLineInfo bufhl_info; // bufhl data for this line + bool has_bufhl = false; // this buffer has highlight matches + bool do_virttext = false; // draw virtual text for this line /* draw_state: items that are drawn in sequence: */ #define WL_START 0 /* nothing done yet */ @@ -2279,8 +2302,13 @@ win_line ( } if (bufhl_start_line(wp->w_buffer, lnum, &bufhl_info)) { - has_bufhl = true; - extra_check = true; + if (kv_size(bufhl_info.line->items)) { + has_bufhl = true; + extra_check = true; + } + if (kv_size(bufhl_info.line->virt_text)) { + do_virttext = true; + } } // Check for columns to display for 'colorcolumn'. @@ -3429,7 +3457,7 @@ win_line ( } if (has_bufhl && v > 0) { - bufhl_attr = bufhl_get_attr(&bufhl_info, (colnr_T)v); + int bufhl_attr = bufhl_get_attr(&bufhl_info, (colnr_T)v); if (bufhl_attr != 0) { if (!attr_pri) { char_attr = hl_combine_attr(char_attr, bufhl_attr); @@ -3949,40 +3977,87 @@ win_line ( && (int)wp->w_virtcol < wp->w_width * (row - startrow + 1) + v && lnum != wp->w_cursor.lnum) - || draw_color_col) - && !wp->w_p_rl - ) { + || draw_color_col || do_virttext) + && !wp->w_p_rl) { int rightmost_vcol = 0; int i; - if (wp->w_p_cuc) + VirtText virt_text = do_virttext ? bufhl_info.line->virt_text + : (VirtText)KV_INITIAL_VALUE; + size_t virt_pos = 0; + LineState s = LINE_STATE((char_u *)""); + int virt_attr = 0; + + // Make sure alignment is the same regardless + // if listchars=eol:X is used or not. + bool delay_virttext = lcs_eol <= 0; + + if (wp->w_p_cuc) { rightmost_vcol = wp->w_virtcol; - if (draw_color_col) - /* determine rightmost colorcolumn to possibly draw */ - for (i = 0; color_cols[i] >= 0; ++i) - if (rightmost_vcol < color_cols[i]) + } + + if (draw_color_col) { + // determine rightmost colorcolumn to possibly draw + for (i = 0; color_cols[i] >= 0; i++) { + if (rightmost_vcol < color_cols[i]) { rightmost_vcol = color_cols[i]; + } + } + } int cuc_attr = win_hl_attr(wp, HLF_CUC); int mc_attr = win_hl_attr(wp, HLF_MC); while (col < wp->w_width) { - schar_from_ascii(ScreenLines[off], ' '); - col++; + int cells = -1; + if (do_virttext && !delay_virttext) { + if (*s.p == NUL) { + if (virt_pos < virt_text.size) { + s.p = (char_u *)kv_A(virt_text, virt_pos).text; + int hl_id = kv_A(virt_text, virt_pos).hl_id; + virt_attr = hl_id > 0 ? syn_id2attr(hl_id) : 0; + virt_pos++; + } else { + do_virttext = false; + } + } + if (*s.p != NUL) { + cells = line_putchar(&s, &ScreenLines[off], wp->w_width - col, + false); + } + } + delay_virttext = false; + + if (cells == -1) { + schar_from_ascii(ScreenLines[off], ' '); + cells = 1; + } + col += cells; if (draw_color_col) { draw_color_col = advance_color_col(VCOL_HLC, &color_cols); } + int attr = 0; if (wp->w_p_cuc && VCOL_HLC == (long)wp->w_virtcol) { - ScreenAttrs[off++] = cuc_attr; + attr = cuc_attr; } else if (draw_color_col && VCOL_HLC == *color_cols) { - ScreenAttrs[off++] = mc_attr; - } else { - ScreenAttrs[off++] = wp->w_hl_attr_normal; + attr = mc_attr; + } + + if (do_virttext) { + attr = hl_combine_attr(attr, virt_attr); } - if (VCOL_HLC >= rightmost_vcol) + ScreenAttrs[off] = attr; + if (cells == 2) { + ScreenAttrs[off+1] = attr; + } + off += cells; + + if (VCOL_HLC >= rightmost_vcol && *s.p == NUL + && virt_pos >= virt_text.size) { break; + } ++vcol; } diff --git a/test/functional/ui/bufhl_spec.lua b/test/functional/ui/bufhl_spec.lua index 5b38921e50..ba3e44b677 100644 --- a/test/functional/ui/bufhl_spec.lua +++ b/test/functional/ui/bufhl_spec.lua @@ -23,7 +23,10 @@ describe('Buffer highlighting', function() [7] = {bold = true}, [8] = {underline = true, bold = true, foreground = Screen.colors.SlateBlue}, [9] = {foreground = Screen.colors.SlateBlue, underline = true}, - [10] = {foreground = Screen.colors.Red} + [10] = {foreground = Screen.colors.Red}, + [11] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red}, + [12] = {foreground = Screen.colors.Blue1}, + [13] = {background = Screen.colors.LightGrey}, }) end) @@ -77,7 +80,7 @@ describe('Buffer highlighting', function() | ]]) - clear_hl(-1, 0 , -1) + clear_hl(-1, 0, -1) screen:expect([[ these are some lines | ^ | @@ -275,4 +278,140 @@ describe('Buffer highlighting', function() | ]]) end) + + it('supports virtual text annotations', function() + local set_virtual_text = curbufmeths.set_virtual_text + insert([[ + 1 + 2 + 3 + + x = 4]]) + feed('O<esc>20A5, <esc>gg') + screen:expect([[ + ^1 + 2 | + 3 + | + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5| + , 5, 5, 5, 5, 5, 5, | + x = 4 | + {1:~ }| + {1:~ }| + | + ]]) + + local id1 = set_virtual_text(0, 0, {{"=", "Statement"}, {" 3", "Number"}}, {}) + set_virtual_text(id1, 1, {{"ERROR:", "ErrorMsg"}, {" invalid syntax"}}, {}) + local id2 = set_virtual_text(0, 2, {{"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."}}, {}) + neq(id2, id1) + + screen:expect([[ + ^1 + 2 {3:=}{2: 3} | + 3 + {11:ERROR:} invalid syntax | + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5| + , 5, 5, 5, 5, 5, 5, Lorem ipsum dolor s| + x = 4 | + {1:~ }| + {1:~ }| + | + ]]) + + clear_hl(id1, 0, -1) + screen:expect([[ + ^1 + 2 | + 3 + | + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5| + , 5, 5, 5, 5, 5, 5, Lorem ipsum dolor s| + x = 4 | + {1:~ }| + {1:~ }| + | + ]]) + + -- Handles doublewidth chars, leaving a space if truncating + -- in the middle of a char + set_virtual_text(id1, 1, {{"暗x事zz速野谷質結育副住新覚丸活解終事", "Comment"}}, {}) + screen:expect([[ + ^1 + 2 | + 3 + {12:暗x事zz速野谷質結育副住新覚丸活解終 }| + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5| + , 5, 5, 5, 5, 5, 5, Lorem ipsum dolor s| + x = 4 | + {1:~ }| + {1:~ }| + | + ]]) + + feed("2Gx") + screen:expect([[ + 1 + 2 | + ^ + {12:暗x事zz速野谷質結育副住新覚丸活解終事}| + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5| + , 5, 5, 5, 5, 5, 5, Lorem ipsum dolor s| + x = 4 | + {1:~ }| + {1:~ }| + | + ]]) + + -- visual selection doesn't highlight virtual text + feed("ggVG") + screen:expect([[ + {13:1 + 2} | + {13: +} {12:暗x事zz速野谷質結育副住新覚丸活解終事}| + {13:5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}| + {13:, 5, 5, 5, 5, 5, 5, } Lorem ipsum dolor s| + ^x{13: = 4} | + {1:~ }| + {1:~ }| + {7:-- VISUAL LINE --} | + ]]) + + feed("<esc>") + screen:expect([[ + 1 + 2 | + + {12:暗x事zz速野谷質結育副住新覚丸活解終事}| + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5| + , 5, 5, 5, 5, 5, 5, Lorem ipsum dolor s| + ^x = 4 | + {1:~ }| + {1:~ }| + | + ]]) + + feed("2Gdd") + screen:expect([[ + 1 + 2 | + ^5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5| + , 5, 5, 5, 5, 5, 5, Lorem ipsum dolor s| + x = 4 | + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]) + + -- listchars=eol:- works, and doesn't shift virtual text + command("set list") + screen:expect([[ + 1 + 2 | + ^5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5| + , 5, 5, 5, 5, 5, 5,{1:-} Lorem ipsum dolor s| + x = 4 | + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]) + + clear_hl(-1, 0, -1) + screen:expect([[ + 1 + 2 | + ^5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5| + , 5, 5, 5, 5, 5, 5,{1:-} | + x = 4 | + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]) + + end) end) |