api: multiple decoration providers at once

This commit is contained in:
Björn Linse
2020-09-21 10:37:28 +02:00
parent 11ec53e558
commit 0b615dae07
16 changed files with 502 additions and 271 deletions

View File

@@ -6,6 +6,8 @@ TSHighlighter.__index = TSHighlighter
TSHighlighter.active = TSHighlighter.active or {} TSHighlighter.active = TSHighlighter.active or {}
local ns = a.nvim_create_namespace("treesitter/highlighter")
-- These are conventions defined by tree-sitter, though it -- These are conventions defined by tree-sitter, though it
-- needs to be user extensible also. -- needs to be user extensible also.
TSHighlighter.hl_map = { TSHighlighter.hl_map = {
@@ -158,7 +160,11 @@ function TSHighlighter._on_line(_, _win, buf, line)
local start_row, start_col, end_row, end_col = node:range() local start_row, start_col, end_row, end_col = node:range()
local hl = self.hl_cache[capture] local hl = self.hl_cache[capture]
if hl and end_row >= line then if hl and end_row >= line then
a.nvim__put_attr(start_row, start_col, { end_line = end_row, end_col = end_col, hl_group = hl }) a.nvim_buf_set_extmark(buf, ns, start_row, start_col,
{ end_line = end_row, end_col = end_col,
hl_group = hl,
ephemeral = true
})
end end
if start_row > line then if start_row > line then
self.nextrow = start_row self.nextrow = start_row
@@ -166,7 +172,7 @@ function TSHighlighter._on_line(_, _win, buf, line)
end end
end end
function TSHighlighter._on_start(_, buf, _tick) function TSHighlighter._on_buf(_, buf)
local self = TSHighlighter.active[buf] local self = TSHighlighter.active[buf]
if self then if self then
local tree = self.parser:parse() local tree = self.parser:parse()
@@ -187,10 +193,10 @@ function TSHighlighter._on_win(_, _win, buf, _topline, botline)
return true return true
end end
a.nvim__set_luahl { a.nvim_set_decoration_provider(ns, {
on_start = TSHighlighter._on_start; on_buf = TSHighlighter._on_buf;
on_win = TSHighlighter._on_win; on_win = TSHighlighter._on_win;
on_line = TSHighlighter._on_line; on_line = TSHighlighter._on_line;
} })
return TSHighlighter return TSHighlighter

View File

@@ -7,6 +7,7 @@
#include <stdint.h> #include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
#include <limits.h> #include <limits.h>
#include <lauxlib.h> #include <lauxlib.h>
#include "nvim/api/buffer.h" #include "nvim/api/buffer.h"
@@ -1285,6 +1286,10 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id,
/// - hl_group : name of the highlight group used to highlight /// - hl_group : name of the highlight group used to highlight
/// this mark. /// this mark.
/// - virt_text : virtual text to link to this mark. /// - virt_text : virtual text to link to this mark.
/// - ephemeral : for use with |nvim_set_decoration_provider|
/// callbacks. The mark will only be used for the current
/// redraw cycle, and not be permantently stored in the
/// buffer.
/// @param[out] err Error details, if any /// @param[out] err Error details, if any
/// @return Id of the created/updated extmark /// @return Id of the created/updated extmark
Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id,
@@ -1318,6 +1323,8 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id,
return 0; return 0;
} }
bool ephemeral = false;
uint64_t id = 0; uint64_t id = 0;
int line2 = -1, hl_id = 0; int line2 = -1, hl_id = 0;
colnr_T col2 = 0; colnr_T col2 = 0;
@@ -1386,6 +1393,11 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id,
if (ERROR_SET(err)) { if (ERROR_SET(err)) {
goto error; goto error;
} }
} else if (strequal("ephemeral", k.data)) {
ephemeral = api_is_truthy(*v, "ephemeral", false, err);
if (ERROR_SET(err)) {
goto error;
}
} else { } else {
api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data); api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data);
goto error; goto error;
@@ -1410,17 +1422,33 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id,
col2 = 0; col2 = 0;
} }
Decoration *decor = NULL; // TODO(bfredl): synergize these two branches even more
if (kv_size(virt_text)) { if (ephemeral && redrawn_win && redrawn_win->w_buffer == buf) {
decor = xcalloc(1, sizeof(*decor)); int attr_id = hl_id > 0 ? syn_id2attr(hl_id) : 0;
decor->hl_id = hl_id; VirtText *vt_allocated = NULL;
decor->virt_text = virt_text; if (kv_size(virt_text)) {
} else if (hl_id) { vt_allocated = xmalloc(sizeof *vt_allocated);
decor = decoration_hl(hl_id); *vt_allocated = virt_text;
} }
decorations_add_ephemeral(attr_id, (int)line, (colnr_T)col,
(int)line2, (colnr_T)col2, vt_allocated);
} else {
if (ephemeral) {
api_set_error(err, kErrorTypeException, "not yet implemented");
goto error;
}
Decoration *decor = NULL;
if (kv_size(virt_text)) {
decor = xcalloc(1, sizeof(*decor));
decor->hl_id = hl_id;
decor->virt_text = virt_text;
} else if (hl_id) {
decor = decoration_hl(hl_id);
}
id = extmark_set(buf, (uint64_t)ns_id, id, id = extmark_set(buf, (uint64_t)ns_id, id, (int)line, (colnr_T)col,
(int)line, (colnr_T)col, line2, col2, decor, kExtmarkNoUndo); line2, col2, decor, kExtmarkNoUndo);
}
return (Integer)id; return (Integer)id;

View File

@@ -1619,14 +1619,28 @@ free_exit:
return virt_text; return virt_text;
} }
bool api_is_truthy(Object obj, const char *what, Error *err) bool api_is_truthy(Object obj, const char *what, bool nil_truthy, Error *err)
{ {
if (obj.type == kObjectTypeBoolean) { if (obj.type == kObjectTypeBoolean) {
return obj.data.boolean; return obj.data.boolean;
} else if (obj.type == kObjectTypeInteger) { } else if (obj.type == kObjectTypeInteger) {
return obj.data.integer; // C semantics: non-zery int is true return obj.data.integer; // C semantics: non-zero int is true
} else if (obj.type == kObjectTypeNil) {
return nil_truthy; // caller decides what NIL (missing retval in lua) means
} else { } else {
api_set_error(err, kErrorTypeValidation, "%s is not an boolean", what); api_set_error(err, kErrorTypeValidation, "%s is not an boolean", what);
return false; return false;
} }
} }
const char *describe_ns(NS ns_id)
{
String name;
handle_T id;
map_foreach(namespace_ids, name, id, {
if ((NS)id == ns_id && name.size) {
return name.data;
}
})
return "(UNKNOWN PLUGIN)";
}

View File

@@ -2604,166 +2604,134 @@ Array nvim__inspect_cell(Integer grid, Integer row, Integer col, Error *err)
return ret; return ret;
} }
/// Set attrs in nvim__buf_set_lua_hl callbacks
///
/// TODO(bfredl): This is rather pedestrian. The final
/// interface should probably be derived from a reformed
/// bufhl/virttext interface with full support for multi-line
/// ranges etc
void nvim__put_attr(Integer line, Integer col, Dictionary opts, Error *err)
FUNC_API_LUA_ONLY
{
if (!lua_attr_active) {
return;
}
int line2 = -1, hl_id = 0;
colnr_T col2 = 0;
VirtText virt_text = KV_INITIAL_VALUE;
for (size_t i = 0; i < opts.size; i++) {
String k = opts.items[i].key;
Object *v = &opts.items[i].value;
if (strequal("end_line", k.data)) {
if (v->type != kObjectTypeInteger) {
api_set_error(err, kErrorTypeValidation,
"end_line is not an integer");
goto error;
}
if (v->data.integer < 0) {
api_set_error(err, kErrorTypeValidation,
"end_line value outside range");
goto error;
}
line2 = (int)v->data.integer;
} else if (strequal("end_col", k.data)) {
if (v->type != kObjectTypeInteger) {
api_set_error(err, kErrorTypeValidation,
"end_col is not an integer");
goto error;
}
if (v->data.integer < 0 || v->data.integer > MAXCOL) {
api_set_error(err, kErrorTypeValidation,
"end_col value outside range");
goto error;
}
col2 = (colnr_T)v->data.integer;
} else if (strequal("hl_group", k.data)) {
String hl_group;
switch (v->type) {
case kObjectTypeString:
hl_group = v->data.string;
hl_id = syn_check_group(
(char_u *)(hl_group.data),
(int)hl_group.size);
break;
case kObjectTypeInteger:
hl_id = (int)v->data.integer;
break;
default:
api_set_error(err, kErrorTypeValidation,
"hl_group is not valid.");
goto error;
}
} else if (strequal("virt_text", k.data)) {
if (v->type != kObjectTypeArray) {
api_set_error(err, kErrorTypeValidation,
"virt_text is not an Array");
goto error;
}
virt_text = parse_virt_text(v->data.array, err);
if (ERROR_SET(err)) {
goto error;
}
} else {
api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data);
goto error;
}
}
if (col2 && line2 < 0) {
line2 = (int)line;
}
int attr = hl_id ? syn_id2attr((int)hl_id) : 0;
if (attr == 0 && !kv_size(virt_text)) {
return;
}
VirtText *v = xmalloc(sizeof(*v));
*v = virt_text; // LeakSanitizer be sad
decorations_add_luahl_attr(attr, (int)line, (colnr_T)col,
(int)line2, (colnr_T)col2, v);
error:
return;
}
void nvim__screenshot(String path) void nvim__screenshot(String path)
FUNC_API_FAST FUNC_API_FAST
{ {
ui_call_screenshot(path); ui_call_screenshot(path);
} }
static void clear_luahl(bool force) static DecorationProvider *get_provider(NS ns_id, bool force)
{ {
if (luahl_active || force) { ssize_t i;
api_free_luaref(luahl_start); for (i = 0; i < (ssize_t)kv_size(decoration_providers); i++) {
api_free_luaref(luahl_win); DecorationProvider *item = &kv_A(decoration_providers, i);
api_free_luaref(luahl_line); if (item->ns_id == ns_id) {
api_free_luaref(luahl_end); return item;
} else if (item->ns_id > ns_id) {
break;
}
} }
luahl_start = LUA_NOREF;
luahl_win = LUA_NOREF; if (!force) {
luahl_line = LUA_NOREF; return NULL;
luahl_end = LUA_NOREF; }
luahl_active = false;
for (ssize_t j = (ssize_t)kv_size(decoration_providers)-1; j >= i; j++) {
// allocates if needed:
kv_a(decoration_providers, (size_t)j+1) = kv_A(decoration_providers, j);
}
DecorationProvider *item = &kv_a(decoration_providers, (size_t)i);
*item = DECORATION_PROVIDER_INIT(ns_id);
return item;
} }
/// Unstabilized interface for defining syntax hl in lua. static void clear_provider(DecorationProvider *p)
///
/// This is not yet safe for general use, lua callbacks will need to
/// be restricted, like textlock and probably other stuff.
///
/// The API on_line/nvim__put_attr is quite raw and not intended to be the
/// final shape. Ideally this should operate on chunks larger than a single
/// line to reduce interpreter overhead, and generate annotation objects
/// (bufhl/virttext) on the fly but using the same representation.
void nvim__set_luahl(DictionaryOf(LuaRef) opts, Error *err)
FUNC_API_LUA_ONLY
{ {
redraw_later(NOT_VALID); NLUA_CLEAR_REF(p->redraw_start);
clear_luahl(false); NLUA_CLEAR_REF(p->redraw_buf);
NLUA_CLEAR_REF(p->redraw_win);
NLUA_CLEAR_REF(p->redraw_line);
NLUA_CLEAR_REF(p->redraw_end);
p->active = false;
}
/// Set or change decoration provider for a namespace
///
/// This is a very general purpose interface for having lua callbacks
/// being triggered during the redraw code.
///
/// The expected usage is to set extmarks for the currently
/// redrawn buffer. |nvim_buf_set_extmark| can be called to add marks
/// on a per-window or per-lines basis. Use the `ephemeral` key to only
/// use the mark for the current screen redraw (the callback will be called
/// again for the next redraw ).
///
/// Note: this function should not be called often. Rather, the callbacks
/// themselves can be used to throttle unneeded callbacks. the `on_start`
/// callback can return `false` to disable the provider until the next redraw.
/// Similarily, return `false` in `on_win` will skip the `on_lines` calls
/// for that window (but any extmarks set in `on_win` will still be used).
/// A plugin managing multiple sources of decorations should ideally only set
/// one provider, and merge the sources internally. You can use multiple `ns_id`
/// for the extmarks set/modified inside the callback anyway.
///
/// Note: doing anything other than setting extmarks is considered experimental.
/// Doing things like changing options are not expliticly forbidden, but is
/// likely to have unexpected consequences (such as 100% CPU consumption).
/// doing `vim.rpcnotify` should be OK, but `vim.rpcrequest` is quite dubious
/// for the moment.
///
/// @param ns_id Namespace id from |nvim_create_namespace()|
/// @param opts Callbacks invoked during redraw:
/// - on_start: called first on each screen redraw
/// ["start", tick]
/// - on_buf: called for each buffer being redrawn (before window
/// callbacks)
/// ["buf", bufnr, tick]
/// - on_win: called when starting to redraw a specific window.
/// ["win", winid, bufnr, topline, botline_guess]
/// - on_line: called for each buffer line being redrawn. (The
/// interation with fold lines is subject to change)
/// ["win", winid, bufnr, row]
/// - on_end: called at the end of a redraw cycle
/// ["end", tick]
void nvim_set_decoration_provider(Integer ns_id, DictionaryOf(LuaRef) opts,
Error *err)
FUNC_API_SINCE(7) FUNC_API_LUA_ONLY
{
DecorationProvider *p = get_provider((NS)ns_id, true);
clear_provider(p);
// regardless of what happens, it seems good idea to redraw
redraw_later(NOT_VALID); // TODO(bfredl): too soon?
struct {
const char *name;
LuaRef *dest;
} cbs[] = {
{ "on_start", &p->redraw_start },
{ "on_buf", &p->redraw_buf },
{ "on_win", &p->redraw_win },
{ "on_line", &p->redraw_line },
{ "on_end", &p->redraw_end },
{ NULL, NULL },
};
for (size_t i = 0; i < opts.size; i++) { for (size_t i = 0; i < opts.size; i++) {
String k = opts.items[i].key; String k = opts.items[i].key;
Object *v = &opts.items[i].value; Object *v = &opts.items[i].value;
if (strequal("on_start", k.data)) { size_t j;
if (v->type != kObjectTypeLuaRef) { for (j = 0; cbs[j].name; j++) {
api_set_error(err, kErrorTypeValidation, "callback is not a function"); if (strequal(cbs[j].name, k.data)) {
goto error; if (v->type != kObjectTypeLuaRef) {
api_set_error(err, kErrorTypeValidation,
"%s is not a function", cbs[j].name);
goto error;
}
*(cbs[j].dest) = v->data.luaref;
v->data.luaref = LUA_NOREF;
break;
} }
luahl_start = v->data.luaref; }
v->data.luaref = LUA_NOREF; if (!cbs[j].name) {
} else if (strequal("on_win", k.data)) {
if (v->type != kObjectTypeLuaRef) {
api_set_error(err, kErrorTypeValidation, "callback is not a function");
goto error;
}
luahl_win = v->data.luaref;
v->data.luaref = LUA_NOREF;
} else if (strequal("on_line", k.data)) {
if (v->type != kObjectTypeLuaRef) {
api_set_error(err, kErrorTypeValidation, "callback is not a function");
goto error;
}
luahl_line = v->data.luaref;
v->data.luaref = LUA_NOREF;
} else {
api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data); api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data);
goto error; goto error;
} }
} }
luahl_active = true;
p->active = true;
return; return;
error: error:
clear_luahl(true); clear_provider(p);
} }

View File

@@ -544,6 +544,9 @@ struct file_buffer {
long b_mod_xlines; // number of extra buffer lines inserted; long b_mod_xlines; // number of extra buffer lines inserted;
// negative when lines were deleted // negative when lines were deleted
wininfo_T *b_wininfo; // list of last used info for each window wininfo_T *b_wininfo; // list of last used info for each window
int b_mod_tick_syn; // last display tick syntax was updated
int b_mod_tick_deco; // last display tick decoration providers
// where invoked
long b_mtime; // last change time of original file long b_mtime; // last change time of original file
long b_mtime_read; // last change time when reading long b_mtime_read; // last change time when reading

View File

@@ -1,6 +1,7 @@
#ifndef NVIM_EXTMARK_H #ifndef NVIM_EXTMARK_H
#define NVIM_EXTMARK_H #define NVIM_EXTMARK_H
#include "nvim/pos.h"
#include "nvim/buffer_defs.h" #include "nvim/buffer_defs.h"
#include "nvim/extmark_defs.h" #include "nvim/extmark_defs.h"
#include "nvim/marktree.h" #include "nvim/marktree.h"
@@ -98,6 +99,8 @@ typedef struct {
VirtText *virt_text; VirtText *virt_text;
} DecorationRedrawState; } DecorationRedrawState;
EXTERN kvec_t(DecorationProvider) decoration_providers INIT(= KV_INITIAL_VALUE);
EXTERN win_T *redrawn_win INIT(= NULL); // used for ephemeral extmarks
#ifdef INCLUDE_GENERATED_DECLARATIONS #ifdef INCLUDE_GENERATED_DECLARATIONS
# include "extmark.h.generated.h" # include "extmark.h.generated.h"

View File

@@ -1,7 +1,7 @@
#ifndef NVIM_EXTMARK_DEFS_H #ifndef NVIM_EXTMARK_DEFS_H
#define NVIM_EXTMARK_DEFS_H #define NVIM_EXTMARK_DEFS_H
#include "nvim/pos.h" // for colnr_T #include "nvim/types.h"
#include "nvim/lib/kvec.h" #include "nvim/lib/kvec.h"
typedef struct { typedef struct {
@@ -42,4 +42,18 @@ typedef enum {
kExtmarkUndoNoRedo, // Operation should be undoable, but not redoable kExtmarkUndoNoRedo, // Operation should be undoable, but not redoable
} ExtmarkOp; } ExtmarkOp;
typedef struct {
NS ns_id;
bool active;
LuaRef redraw_start;
LuaRef redraw_buf;
LuaRef redraw_win;
LuaRef redraw_line;
LuaRef redraw_end;
} DecorationProvider;
#define DECORATION_PROVIDER_INIT(ns_id) (DecorationProvider) \
{ ns_id, false, LUA_NOREF, LUA_NOREF, \
LUA_NOREF, LUA_NOREF, LUA_NOREF }
#endif // NVIM_EXTMARK_DEFS_H #endif // NVIM_EXTMARK_DEFS_H

View File

@@ -125,8 +125,6 @@ typedef off_t off_T;
EXTERN int mod_mask INIT(= 0x0); // current key modifiers EXTERN int mod_mask INIT(= 0x0); // current key modifiers
EXTERN bool lua_attr_active INIT(= false);
// Cmdline_row is the row where the command line starts, just below the // Cmdline_row is the row where the command line starts, just below the
// last window. // last window.
// When the cmdline gets longer than the available space the screen gets // When the cmdline gets longer than the available space the screen gets
@@ -405,12 +403,6 @@ EXTERN int sys_menu INIT(= false);
// ('lines' and 'rows') must not be changed. // ('lines' and 'rows') must not be changed.
EXTERN int updating_screen INIT(= 0); EXTERN int updating_screen INIT(= 0);
EXTERN bool luahl_active INIT(= false);
EXTERN LuaRef luahl_start INIT(= LUA_NOREF);
EXTERN LuaRef luahl_win INIT(= LUA_NOREF);
EXTERN LuaRef luahl_line INIT(= LUA_NOREF);
EXTERN LuaRef luahl_end INIT(= LUA_NOREF);
// All windows are linked in a list. firstwin points to the first entry, // All windows are linked in a list. firstwin points to the first entry,
// lastwin to the last entry (can be the same as firstwin) and curwin to the // lastwin to the last entry (can be the same as firstwin) and curwin to the
// currently active window. // currently active window.

View File

@@ -24,6 +24,15 @@ EXTERN LuaRef nlua_empty_dict_ref INIT(= LUA_NOREF);
memcpy(&err_->msg[0], s, sizeof(s)); \ memcpy(&err_->msg[0], s, sizeof(s)); \
} while (0) } while (0)
#define NLUA_CLEAR_REF(x) \
do { \
/* Take the address to avoid double evaluation. #1375 */ \
if ((x) != LUA_NOREF) { \
api_free_luaref(x); \
(x) = LUA_NOREF; \
} \
} while (0)
#ifdef INCLUDE_GENERATED_DECLARATIONS #ifdef INCLUDE_GENERATED_DECLARATIONS
# include "lua/executor.h.generated.h" # include "lua/executor.h.generated.h"
#endif #endif

View File

@@ -183,7 +183,6 @@ MAP_IMPL(uint64_t, ssize_t, SSIZE_INITIALIZER)
MAP_IMPL(uint64_t, uint64_t, DEFAULT_INITIALIZER) MAP_IMPL(uint64_t, uint64_t, DEFAULT_INITIALIZER)
#define EXTMARK_NS_INITIALIZER { 0, 0 } #define EXTMARK_NS_INITIALIZER { 0, 0 }
MAP_IMPL(uint64_t, ExtmarkNs, EXTMARK_NS_INITIALIZER) MAP_IMPL(uint64_t, ExtmarkNs, EXTMARK_NS_INITIALIZER)
#define KVEC_INITIALIZER { .size = 0, .capacity = 0, .items = NULL }
#define EXTMARK_ITEM_INITIALIZER { 0, 0, NULL } #define EXTMARK_ITEM_INITIALIZER { 0, 0, NULL }
MAP_IMPL(uint64_t, ExtmarkItem, EXTMARK_ITEM_INITIALIZER) MAP_IMPL(uint64_t, ExtmarkItem, EXTMARK_ITEM_INITIALIZER)
MAP_IMPL(handle_T, ptr_t, DEFAULT_INITIALIZER) MAP_IMPL(handle_T, ptr_t, DEFAULT_INITIALIZER)

View File

@@ -2,6 +2,7 @@
#define NVIM_MARKTREE_H #define NVIM_MARKTREE_H
#include <stdint.h> #include <stdint.h>
#include "nvim/pos.h"
#include "nvim/map.h" #include "nvim/map.h"
#include "nvim/garray.h" #include "nvim/garray.h"

View File

@@ -119,10 +119,12 @@
#include "nvim/api/private/helpers.h" #include "nvim/api/private/helpers.h"
#include "nvim/api/vim.h" #include "nvim/api/vim.h"
#include "nvim/lua/executor.h" #include "nvim/lua/executor.h"
#include "nvim/lib/kvec.h"
#define MB_FILLER_CHAR '<' /* character used when a double-width character #define MB_FILLER_CHAR '<' /* character used when a double-width character
* doesn't fit. */ * doesn't fit. */
typedef kvec_withinit_t(DecorationProvider *, 4) Providers;
// temporary buffer for rendering a single screenline, so it can be // temporary buffer for rendering a single screenline, so it can be
// compared with previous contents to calculate smallest delta. // compared with previous contents to calculate smallest delta.
@@ -156,13 +158,43 @@ static bool msg_grid_invalid = false;
static bool resizing = false; static bool resizing = false;
static bool do_luahl_line = false;
#ifdef INCLUDE_GENERATED_DECLARATIONS #ifdef INCLUDE_GENERATED_DECLARATIONS
# include "screen.c.generated.h" # include "screen.c.generated.h"
#endif #endif
#define SEARCH_HL_PRIORITY 0 #define SEARCH_HL_PRIORITY 0
static char * provider_first_error = NULL;
static bool provider_invoke(NS ns_id, const char *name, LuaRef ref,
Array args, bool default_true)
{
Error err = ERROR_INIT;
textlock++;
Object ret = nlua_call_ref(ref, name, args, true, &err);
textlock--;
if (!ERROR_SET(&err)
&& api_is_truthy(ret, "provider %s retval", default_true, &err)) {
return true;
}
if (ERROR_SET(&err)) {
const char *ns_name = describe_ns(ns_id);
ELOG("error in provider %s:%s: %s", ns_name, name, err.msg);
bool verbose_errs = true; // TODO(bfredl):
if (verbose_errs && provider_first_error == NULL) {
static char errbuf[IOSIZE];
snprintf(errbuf, sizeof errbuf, "%s: %s", ns_name, err.msg);
provider_first_error = xstrdup(errbuf);
}
}
api_free_object(ret);
return false;
}
/* /*
* Redraw the current window later, with update_screen(type). * Redraw the current window later, with update_screen(type).
* Set must_redraw only if not already set to a higher value. * Set must_redraw only if not already set to a higher value.
@@ -446,6 +478,29 @@ int update_screen(int type)
ui_comp_set_screen_valid(true); ui_comp_set_screen_valid(true);
Providers providers;
kvi_init(providers);
for (size_t i = 0; i < kv_size(decoration_providers); i++) {
DecorationProvider *p = &kv_A(decoration_providers, i);
if (!p->active) {
continue;
}
bool active;
if (p->redraw_start != LUA_NOREF) {
FIXED_TEMP_ARRAY(args, 2);
args.items[0] = INTEGER_OBJ(display_tick);
args.items[1] = INTEGER_OBJ(type);
active = provider_invoke(p->ns_id, "start", p->redraw_start, args, true);
} else {
active = true;
}
if (active) {
kvi_push(providers, p);
}
}
if (clear_cmdline) /* going to clear cmdline (done below) */ if (clear_cmdline) /* going to clear cmdline (done below) */
check_for_delay(FALSE); check_for_delay(FALSE);
@@ -494,30 +549,24 @@ int update_screen(int type)
FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
update_window_hl(wp, type >= NOT_VALID); update_window_hl(wp, type >= NOT_VALID);
if (wp->w_buffer->b_mod_set) { buf_T *buf = wp->w_buffer;
win_T *wwp; if (buf->b_mod_set) {
if (buf->b_mod_tick_syn < display_tick
// Check if we already did this buffer. && syntax_present(wp)) {
for (wwp = firstwin; wwp != wp; wwp = wwp->w_next) { syn_stack_apply_changes(buf);
if (wwp->w_buffer == wp->w_buffer) { buf->b_mod_tick_syn = display_tick;
break;
}
}
if (wwp == wp && syntax_present(wp)) {
syn_stack_apply_changes(wp->w_buffer);
} }
buf_T *buf = wp->w_buffer; if (buf->b_mod_tick_deco < display_tick) {
if (luahl_active && luahl_start != LUA_NOREF) { for (size_t i = 0; i < kv_size(providers); i++) {
Error err = ERROR_INIT; DecorationProvider *p = kv_A(providers, i);
FIXED_TEMP_ARRAY(args, 2); if (p && p->redraw_buf != LUA_NOREF) {
args.items[0] = BUFFER_OBJ(buf->handle); FIXED_TEMP_ARRAY(args, 1);
args.items[1] = INTEGER_OBJ(display_tick); args.items[0] = BUFFER_OBJ(buf->handle);
nlua_call_ref(luahl_start, "start", args, false, &err); provider_invoke(p->ns_id, "buf", p->redraw_buf, args, true);
if (ERROR_SET(&err)) { }
ELOG("error in luahl start: %s", err.msg);
api_clear_error(&err);
} }
buf->b_mod_tick_deco = display_tick;
} }
} }
} }
@@ -531,6 +580,8 @@ int update_screen(int type)
FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
redrawn_win = wp;
if (wp->w_redr_type == CLEAR && wp->w_floating && wp->w_grid.chars) { if (wp->w_redr_type == CLEAR && wp->w_floating && wp->w_grid.chars) {
grid_invalidate(&wp->w_grid); grid_invalidate(&wp->w_grid);
wp->w_redr_type = NOT_VALID; wp->w_redr_type = NOT_VALID;
@@ -541,13 +592,15 @@ int update_screen(int type)
did_one = TRUE; did_one = TRUE;
start_search_hl(); start_search_hl();
} }
win_update(wp); win_update(wp, &providers);
} }
/* redraw status line after the window to minimize cursor movement */ /* redraw status line after the window to minimize cursor movement */
if (wp->w_redr_status) { if (wp->w_redr_status) {
win_redr_status(wp); win_redr_status(wp);
} }
redrawn_win = NULL;
} }
end_search_hl(); end_search_hl();
@@ -578,6 +631,24 @@ int update_screen(int type)
maybe_intro_message(); maybe_intro_message();
did_intro = TRUE; did_intro = TRUE;
for (size_t i = 0; i < kv_size(providers); i++) {
DecorationProvider *p = kv_A(providers, i);
if (!p->active) {
continue;
}
bool active;
if (p->redraw_end != LUA_NOREF) {
FIXED_TEMP_ARRAY(args, 1);
args.items[0] = INTEGER_OBJ(display_tick);
active = provider_invoke(p->ns_id, "end", p->redraw_end, args, true);
} else {
active = true;
}
}
kvi_destroy(providers);
// either cmdline is cleared, not drawn or mode is last drawn // either cmdline is cleared, not drawn or mode is last drawn
cmdline_was_last_drawn = false; cmdline_was_last_drawn = false;
return OK; return OK;
@@ -637,13 +708,14 @@ bool win_cursorline_standout(const win_T *wp)
static DecorationRedrawState decorations; static DecorationRedrawState decorations;
bool decorations_active = false; bool decorations_active = false;
void decorations_add_luahl_attr(int attr_id, void decorations_add_ephemeral(int attr_id,
int start_row, int start_col, int start_row, int start_col,
int end_row, int end_col, VirtText *virt_text) int end_row, int end_col, VirtText *virt_text)
{ {
kv_push(decorations.active, kv_push(decorations.active,
((HlRange){ start_row, start_col, ((HlRange){ start_row, start_col,
end_row, end_col, attr_id, virt_text, true })); end_row, end_col,
attr_id, virt_text, virt_text != NULL }));
} }
/* /*
@@ -673,7 +745,7 @@ void decorations_add_luahl_attr(int attr_id,
* mid: from mid_start to mid_end (update inversion or changed text) * mid: from mid_start to mid_end (update inversion or changed text)
* bot: from bot_start to last row (when scrolled up) * bot: from bot_start to last row (when scrolled up)
*/ */
static void win_update(win_T *wp) static void win_update(win_T *wp, Providers *providers)
{ {
buf_T *buf = wp->w_buffer; buf_T *buf = wp->w_buffer;
int type; int type;
@@ -1241,31 +1313,26 @@ static void win_update(win_T *wp)
decorations_active = decorations_redraw_reset(buf, &decorations); decorations_active = decorations_redraw_reset(buf, &decorations);
do_luahl_line = false; Providers line_providers;
kvi_init(line_providers);
if (luahl_win != LUA_NOREF) { linenr_T knownmax = ((wp->w_valid & VALID_BOTLINE)
Error err = ERROR_INIT; ? wp->w_botline
FIXED_TEMP_ARRAY(args, 4); : (wp->w_topline + wp->w_height_inner));
linenr_T knownmax = ((wp->w_valid & VALID_BOTLINE)
? wp->w_botline
: (wp->w_topline + wp->w_height_inner));
args.items[0] = WINDOW_OBJ(wp->handle);
args.items[1] = BUFFER_OBJ(buf->handle);
// TODO(bfredl): we are not using this, but should be first drawn line?
args.items[2] = INTEGER_OBJ(wp->w_topline-1);
args.items[3] = INTEGER_OBJ(knownmax);
// TODO(bfredl): we could allow this callback to change mod_top, mod_bot.
// For now the "start" callback is expected to use nvim__buf_redraw_range.
Object ret = nlua_call_ref(luahl_win, "win", args, true, &err);
if (!ERROR_SET(&err) && api_is_truthy(ret, "luahl_window retval", &err)) { for (size_t k = 0; k < kv_size(*providers); k++) {
do_luahl_line = true; DecorationProvider *p = kv_A(*providers, k);
decorations_active = true; if (p && p->redraw_win != LUA_NOREF) {
} FIXED_TEMP_ARRAY(args, 4);
args.items[0] = WINDOW_OBJ(wp->handle);
if (ERROR_SET(&err)) { args.items[1] = BUFFER_OBJ(buf->handle);
ELOG("error in luahl window: %s", err.msg); // TODO(bfredl): we are not using this, but should be first drawn line?
api_clear_error(&err); args.items[2] = INTEGER_OBJ(wp->w_topline-1);
args.items[3] = INTEGER_OBJ(knownmax);
if (provider_invoke(p->ns_id, "win", p->redraw_win, args, true)) {
kvi_push(line_providers, p);
decorations_active = true;
}
} }
} }
@@ -1483,7 +1550,7 @@ static void win_update(win_T *wp)
// Display one line // Display one line
row = win_line(wp, lnum, srow, row = win_line(wp, lnum, srow,
foldinfo.fi_lines ? srow : wp->w_grid.Rows, foldinfo.fi_lines ? srow : wp->w_grid.Rows,
mod_top == 0, false, foldinfo); mod_top == 0, false, foldinfo, &line_providers);
wp->w_lines[idx].wl_folded = foldinfo.fi_lines != 0; wp->w_lines[idx].wl_folded = foldinfo.fi_lines != 0;
wp->w_lines[idx].wl_lastlnum = lnum; wp->w_lines[idx].wl_lastlnum = lnum;
@@ -1519,7 +1586,8 @@ static void win_update(win_T *wp)
// 'relativenumber' set: The text doesn't need to be drawn, but // 'relativenumber' set: The text doesn't need to be drawn, but
// the number column nearly always does. // the number column nearly always does.
foldinfo_T info = fold_info(wp, lnum); foldinfo_T info = fold_info(wp, lnum);
(void)win_line(wp, lnum, srow, wp->w_grid.Rows, true, true, info); (void)win_line(wp, lnum, srow, wp->w_grid.Rows, true, true,
info, &line_providers);
} }
// This line does not need to be drawn, advance to the next one. // This line does not need to be drawn, advance to the next one.
@@ -1615,6 +1683,8 @@ static void win_update(win_T *wp)
HLF_EOB); HLF_EOB);
} }
kvi_destroy(line_providers);
if (wp->w_redr_type >= REDRAW_TOP) { if (wp->w_redr_type >= REDRAW_TOP) {
draw_vsep_win(wp, 0); draw_vsep_win(wp, 0);
} }
@@ -1674,7 +1744,7 @@ static void win_update(win_T *wp)
curbuf->b_mod_set = false; curbuf->b_mod_set = false;
j = curbuf->b_mod_xlines; j = curbuf->b_mod_xlines;
curbuf->b_mod_xlines = 0; curbuf->b_mod_xlines = 0;
win_update(curwin); win_update(curwin, providers);
curbuf->b_mod_set = i; curbuf->b_mod_set = i;
curbuf->b_mod_xlines = j; curbuf->b_mod_xlines = j;
} }
@@ -1919,28 +1989,23 @@ fill_foldcolumn(
return MAX(char_counter + (fdc-i), (size_t)fdc); return MAX(char_counter + (fdc-i), (size_t)fdc);
} }
/// Display line "lnum" of window 'wp' on the screen. /// Display line "lnum" of window 'wp' on the screen.
/// Start at row "startrow", stop when "endrow" is reached.
/// wp->w_virtcol needs to be valid. /// wp->w_virtcol needs to be valid.
/// ///
/// @param lnum line to display /// @param lnum line to display
/// @param endrow stop drawing once reaching this row /// @param startrow first row relative to window grid
/// @param nochange not updating for changed text /// @param endrow last grid row to be redrawn
/// @param number_only only update the number column /// @param nochange not updating for changed text
/// @param foldinfo fold info for this line /// @param number_only only update the number column
/// @param foldinfo fold info for this line
/// @param[in, out] providers decoration providers active this line
/// items will be disables if they cause errors
/// or explicitly return `false`.
/// ///
/// @return the number of last row the line occupies. /// @return the number of last row the line occupies.
static int static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow,
win_line ( bool nochange, bool number_only, foldinfo_T foldinfo,
win_T *wp, Providers *providers)
linenr_T lnum,
int startrow,
int endrow,
bool nochange,
bool number_only,
foldinfo_T foldinfo
)
{ {
int c = 0; // init for GCC int c = 0; // init for GCC
long vcol = 0; // virtual column (for tabs) long vcol = 0; // virtual column (for tabs)
@@ -2084,7 +2149,7 @@ win_line (
row = startrow; row = startrow;
char *luatext = NULL; char *err_text = NULL;
buf_T *buf = wp->w_buffer; buf_T *buf = wp->w_buffer;
@@ -2109,22 +2174,18 @@ win_line (
} }
} }
if (decorations_active) { for (size_t k = 0; k < kv_size(*providers); k++) {
if (do_luahl_line && luahl_line != LUA_NOREF) { DecorationProvider *p = kv_A(*providers, k);
Error err = ERROR_INIT; if (p && p->redraw_line != LUA_NOREF) {
FIXED_TEMP_ARRAY(args, 3); FIXED_TEMP_ARRAY(args, 3);
args.items[0] = WINDOW_OBJ(wp->handle); args.items[0] = WINDOW_OBJ(wp->handle);
args.items[1] = BUFFER_OBJ(buf->handle); args.items[1] = BUFFER_OBJ(buf->handle);
args.items[2] = INTEGER_OBJ(lnum-1); args.items[2] = INTEGER_OBJ(lnum-1);
lua_attr_active = true; if (provider_invoke(p->ns_id, "line", p->redraw_line, args, true)) {
extra_check = true; decorations_active = true;
nlua_call_ref(luahl_line, "line", args, false, &err); } else {
lua_attr_active = false; // return 'false' or error: skip rest of this window
kv_A(*providers, k) = NULL;
if (ERROR_SET(&err)) {
ELOG("error in luahl line: %s", err.msg);
luatext = err.msg;
do_virttext = true;
} }
} }
@@ -2135,6 +2196,12 @@ win_line (
} }
} }
if (provider_first_error) {
err_text = provider_first_error;
provider_first_error = NULL;
do_virttext = true;
}
// Check for columns to display for 'colorcolumn'. // Check for columns to display for 'colorcolumn'.
color_cols = wp->w_buffer->terminal ? NULL : wp->w_p_cc_cols; color_cols = wp->w_buffer->terminal ? NULL : wp->w_p_cc_cols;
if (color_cols != NULL) { if (color_cols != NULL) {
@@ -3835,8 +3902,10 @@ win_line (
draw_color_col = advance_color_col(VCOL_HLC, &color_cols); draw_color_col = advance_color_col(VCOL_HLC, &color_cols);
VirtText virt_text = KV_INITIAL_VALUE; VirtText virt_text = KV_INITIAL_VALUE;
if (luatext) { if (err_text) {
kv_push(virt_text, ((VirtTextChunk){ .text = luatext, .hl_id = 0 })); int hl_err = syn_check_group((char_u *)S_LEN("ErrorMsg"));
kv_push(virt_text, ((VirtTextChunk){ .text = err_text,
.hl_id = hl_err }));
do_virttext = true; do_virttext = true;
} else if (has_decorations) { } else if (has_decorations) {
VirtText *vp = decorations_redraw_virt_text(wp->w_buffer, &decorations); VirtText *vp = decorations_redraw_virt_text(wp->w_buffer, &decorations);
@@ -4256,7 +4325,7 @@ win_line (
} }
xfree(p_extra_free); xfree(p_extra_free);
xfree(luatext); xfree(err_text);
return row; return row;
} }

View File

@@ -21,6 +21,8 @@ typedef int handle_T;
// absent callback etc. // absent callback etc.
typedef int LuaRef; typedef int LuaRef;
typedef uint64_t NS;
typedef struct expand expand_T; typedef struct expand expand_T;
typedef enum { typedef enum {

View File

@@ -22,6 +22,7 @@ local ok = global_helpers.ok
local sleep = global_helpers.sleep local sleep = global_helpers.sleep
local tbl_contains = global_helpers.tbl_contains local tbl_contains = global_helpers.tbl_contains
local write_file = global_helpers.write_file local write_file = global_helpers.write_file
local fail = global_helpers.fail
local module = { local module = {
NIL = mpack.NIL, NIL = mpack.NIL,
@@ -592,6 +593,24 @@ function module.expect_any(contents)
return ok(nil ~= string.find(module.curbuf_contents(), contents, 1, true)) return ok(nil ~= string.find(module.curbuf_contents(), contents, 1, true))
end end
function module.expect_events(expected, received, kind)
local inspect = require'vim.inspect'
if not pcall(eq, expected, received) then
local msg = 'unexpected '..kind..' received.\n\n'
msg = msg .. 'received events:\n'
for _, e in ipairs(received) do
msg = msg .. ' ' .. inspect(e) .. ';\n'
end
msg = msg .. '\nexpected events:\n'
for _, e in ipairs(expected) do
msg = msg .. ' ' .. inspect(e) .. ';\n'
end
fail(msg)
end
return received
end
-- Checks that the Nvim session did not terminate. -- Checks that the Nvim session did not terminate.
function module.assert_alive() function module.assert_alive()
assert(2 == module.eval('1+1'), 'crash? request failed') assert(2 == module.eval('1+1'), 'crash? request failed')

View File

@@ -1,8 +1,6 @@
-- Test suite for testing interactions with API bindings -- Test suite for testing interactions with API bindings
local helpers = require('test.functional.helpers')(after_each) local helpers = require('test.functional.helpers')(after_each)
local inspect = require'vim.inspect'
local command = helpers.command local command = helpers.command
local meths = helpers.meths local meths = helpers.meths
local funcs = helpers.funcs local funcs = helpers.funcs
@@ -12,6 +10,7 @@ local fail = helpers.fail
local exec_lua = helpers.exec_lua local exec_lua = helpers.exec_lua
local feed = helpers.feed local feed = helpers.feed
local deepcopy = helpers.deepcopy local deepcopy = helpers.deepcopy
local expect_events = helpers.expect_events
local origlines = {"original line 1", local origlines = {"original line 1",
"original line 2", "original line 2",
@@ -297,20 +296,7 @@ describe('lua: nvim_buf_attach on_bytes', function()
local verify_name = "test1" local verify_name = "test1"
local function check_events(expected) local function check_events(expected)
local events = exec_lua("return get_events(...)" ) local events = exec_lua("return get_events(...)" )
expect_events(expected, events, "byte updates")
if not pcall(eq, expected, events) then
local msg = 'unexpected byte updates received.\n\n'
msg = msg .. 'received events:\n'
for _, e in ipairs(events) do
msg = msg .. ' ' .. inspect(e) .. ';\n'
end
msg = msg .. '\nexpected events:\n'
for _, e in ipairs(expected) do
msg = msg .. ' ' .. inspect(e) .. ';\n'
end
fail(msg)
end
if not verify then if not verify then
return return

View File

@@ -0,0 +1,118 @@
local helpers = require('test.functional.helpers')(after_each)
local Screen = require('test.functional.ui.screen')
local clear = helpers.clear
local feed = helpers.feed
local insert = helpers.insert
local exec_lua = helpers.exec_lua
local expect_events = helpers.expect_events
describe('decorations provider', function()
local screen
before_each(function()
clear()
screen = Screen.new(40, 8)
screen:attach()
screen:set_default_attr_ids({
[1] = {bold=true, foreground=Screen.colors.Blue},
})
end)
local mudholland = [[
// just to see if there was an accident
// on Mulholland Drive
try_start();
bufref_T save_buf;
switch_buffer(&save_buf, buf);
posp = getmark(mark, false);
restore_buffer(&save_buf); ]]
local function setup_provider(code)
exec_lua ([[
local a = vim.api
test1 = a.nvim_create_namespace "test1"
]] .. (code or [[
beamtrace = {}
function on_do(kind, ...)
table.insert(beamtrace, {kind, ...})
end
]]) .. [[
a.nvim_set_decoration_provider(
test1, {
on_start = on_do; on_buf = on_do;
on_win = on_do; on_line = on_do;
on_end = on_do;
})
]])
end
local function check_trace(expected)
local actual = exec_lua [[ local b = beamtrace beamtrace = {} return b ]]
expect_events(expected, actual, "beam trace")
end
it('leaves a trace', function()
insert(mudholland)
setup_provider()
screen:expect{grid=[[
// just to see if there was an accident |
// on Mulholland Drive |
try_start(); |
bufref_T save_buf; |
switch_buffer(&save_buf, buf); |
posp = getmark(mark, false); |
restore_buffer(&save_buf);^ |
|
]]}
check_trace {
{ "start", 4, 40 };
{ "win", 1000, 1, 0, 8 };
{ "line", 1000, 1, 0 };
{ "line", 1000, 1, 1 };
{ "line", 1000, 1, 2 };
{ "line", 1000, 1, 3 };
{ "line", 1000, 1, 4 };
{ "line", 1000, 1, 5 };
{ "line", 1000, 1, 6 };
{ "end", 4 };
}
feed "iü<esc>"
screen:expect{grid=[[
// just to see if there was an accident |
// on Mulholland Drive |
try_start(); |
bufref_T save_buf; |
switch_buffer(&save_buf, buf); |
posp = getmark(mark, false); |
restore_buffer(&save_buf);^ü |
|
]]}
check_trace {
{ "start", 5, 10 };
{ "buf", 1 };
{ "win", 1000, 1, 0, 8 };
{ "line", 1000, 1, 6 };
{ "end", 5 };
}
end)
it('single provider', function()
insert(mudholland)
setup_provider [[
local hl = a.nvim_get_hl_id_by_name "ErrorMsg"
function do_it(event, ...)
if event == "line" then
local win, buf, line = ...
a.nvim_buf_set_extmark(buf, test_ns, line, line,
{ end_line = line, end_col = line+1,
hl_group = hl,
ephemeral = true
})
end
end
]]
end)
end)