diff options
31 files changed, 645 insertions, 323 deletions
diff --git a/runtime/autoload/health/provider.vim b/runtime/autoload/health/provider.vim index a01cb9631c..5cda7cfd03 100644 --- a/runtime/autoload/health/provider.vim +++ b/runtime/autoload/health/provider.vim @@ -203,7 +203,7 @@ function! s:version_info(python) abort let nvim_path = s:trim(s:system([ \ a:python, '-c', \ 'import sys; ' . - \ 'sys.path = list(filter(lambda x: x != "", sys.path)); ' . + \ 'sys.path = [p for p in sys.path if p != ""]; ' . \ 'import neovim; print(neovim.__file__)'])) if s:shell_error || empty(nvim_path) return [python_version, 'unable to load neovim Python module', pypi_version, diff --git a/runtime/autoload/provider/pythonx.vim b/runtime/autoload/provider/pythonx.vim index 048f898e62..6211b457d6 100644 --- a/runtime/autoload/provider/pythonx.vim +++ b/runtime/autoload/provider/pythonx.vim @@ -8,7 +8,7 @@ let s:loaded_pythonx_provider = 1 function! provider#pythonx#Require(host) abort " Python host arguments let prog = provider#python3#Prog() - let args = [prog, '-c', 'import sys; sys.path = list(filter(lambda x: x != "", sys.path)); import neovim; neovim.start_host()'] + let args = [prog, '-c', 'import sys; sys.path = [p for p in sys.path if p != ""]; import neovim; neovim.start_host()'] " Collect registered Python plugins into args @@ -63,7 +63,7 @@ endfunction function! s:import_module(prog, module) abort let prog_version = system([a:prog, '-c' , printf( \ 'import sys; ' . - \ 'sys.path = list(filter(lambda x: x != "", sys.path)); ' . + \ 'sys.path = [p for p in sys.path if p != ""]; ' . \ 'sys.stdout.write(str(sys.version_info[0]) + "." + str(sys.version_info[1])); ' . \ 'import pkgutil; ' . \ 'exit(2*int(pkgutil.get_loader("%s") is None))', diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index 54eac87070..ac42b315a4 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -2364,7 +2364,7 @@ fnamemodify({fname}, {mods}) *fnamemodify()* Example: > :echo fnamemodify("main.c", ":p:h") < results in: > - /home/mool/vim/vim/src + /home/user/vim/vim/src < If {mods} is empty or an unsupported modifier is used then {fname} is returned. Note: Environment variables don't work in {fname}, use diff --git a/runtime/doc/map.txt b/runtime/doc/map.txt index 2206d13053..05cf30e078 100644 --- a/runtime/doc/map.txt +++ b/runtime/doc/map.txt @@ -345,15 +345,7 @@ Note: mapping is recursive. - In Visual mode you can use `line('v')` and `col('v')` to get one end of the Visual area, the cursor is at the other end. -- In Select mode, |:map| and |:vmap| command mappings are executed in - Visual mode. Use |:smap| to handle Select mode differently. One particular - edge case: > - :vnoremap <C-K> <Esc> -< This ends Visual mode when in Visual mode, but in Select mode it does not - work, because Select mode is restored after executing the mapped keys. You - need to use: > - :snoremap <C-K> <Esc> -< + *E5520* <Cmd> commands must terminate, that is, they must be followed by <CR> in the {rhs} of the mapping definition. |Command-line| mode is never entered. diff --git a/runtime/doc/visual.txt b/runtime/doc/visual.txt index 193c70e70a..905ae49887 100644 --- a/runtime/doc/visual.txt +++ b/runtime/doc/visual.txt @@ -499,6 +499,13 @@ work both in Visual mode and in Select mode. When these are used in Select mode Vim automatically switches to Visual mode, so that the same behavior as in Visual mode is effective. If you don't want this use |:xmap| or |:smap|. +One particular edge case: > + :vnoremap <C-K> <Esc> +This ends Visual mode when in Visual mode, but in Select mode it does not +work, because Select mode is restored after executing the mapped keys. You +need to use: > + :snoremap <C-K> <Esc> +< Users will expect printable characters to replace the selected area. Therefore avoid mapping printable characters in Select mode. Or use |:sunmap| after |:map| and |:vmap| to remove it for Select mode. diff --git a/runtime/indent/confini.vim b/runtime/indent/confini.vim deleted file mode 100644 index 50b3dd20c0..0000000000 --- a/runtime/indent/confini.vim +++ /dev/null @@ -1,10 +0,0 @@ -" Vim indent file -" Language: confini - -" Quit if an indent file was already loaded. -if exists("b:did_indent") - finish -endif - -" Use the cfg indenting, it's similar enough. -runtime! indent/cfg.vim diff --git a/runtime/indent/systemd.vim b/runtime/indent/systemd.vim deleted file mode 100644 index a05a87bb1c..0000000000 --- a/runtime/indent/systemd.vim +++ /dev/null @@ -1,10 +0,0 @@ -" Vim indent file -" Language: systemd.unit(5) - -" Only load this indent file when no other was loaded. -if exists("b:did_indent") - finish -endif - -" Looks a lot like dosini files. -runtime! indent/dosini.vim diff --git a/runtime/indent/yaml.vim b/runtime/indent/yaml.vim index 1f798416ed..d732c37c05 100644 --- a/runtime/indent/yaml.vim +++ b/runtime/indent/yaml.vim @@ -2,7 +2,7 @@ " Language: YAML " Maintainer: Nikolai Pavlov <zyx.vim@gmail.com> " Last Update: Lukas Reineke -" Last Change: 2022 May 02 +" Last Change: 2022 Jun 17 " Only load this indent file when no other was loaded. if exists('b:did_indent') @@ -44,30 +44,30 @@ function s:FindPrevLEIndentedLineMatchingRegex(lnum, regex) return plilnum endfunction -let s:mapkeyregex='\v^\s*\#@!\S@=%(\''%([^'']|\''\'')*\'''. - \ '|\"%([^"\\]|\\.)*\"'. +let s:mapkeyregex = '\v^\s*\#@!\S@=%(\''%([^'']|\''\'')*\''' .. + \ '|\"%([^"\\]|\\.)*\"' .. \ '|%(%(\:\ )@!.)*)\:%(\ |$)' -let s:liststartregex='\v^\s*%(\-%(\ |$))' +let s:liststartregex = '\v^\s*%(\-%(\ |$))' let s:c_ns_anchor_char = '\v%([\n\r\uFEFF \t,[\]{}]@!\p)' -let s:c_ns_anchor_name = s:c_ns_anchor_char.'+' -let s:c_ns_anchor_property = '\v\&'.s:c_ns_anchor_name +let s:c_ns_anchor_name = s:c_ns_anchor_char .. '+' +let s:c_ns_anchor_property = '\v\&' .. s:c_ns_anchor_name let s:ns_word_char = '\v[[:alnum:]_\-]' -let s:ns_tag_char = '\v%(%\x\x|'.s:ns_word_char.'|[#/;?:@&=+$.~*''()])' -let s:c_named_tag_handle = '\v\!'.s:ns_word_char.'+\!' +let s:ns_tag_char = '\v%(%\x\x|' .. s:ns_word_char .. '|[#/;?:@&=+$.~*''()])' +let s:c_named_tag_handle = '\v\!' .. s:ns_word_char .. '+\!' let s:c_secondary_tag_handle = '\v\!\!' let s:c_primary_tag_handle = '\v\!' -let s:c_tag_handle = '\v%('.s:c_named_tag_handle. - \ '|'.s:c_secondary_tag_handle. - \ '|'.s:c_primary_tag_handle.')' -let s:c_ns_shorthand_tag = '\v'.s:c_tag_handle . s:ns_tag_char.'+' +let s:c_tag_handle = '\v%(' .. s:c_named_tag_handle. + \ '|' .. s:c_secondary_tag_handle. + \ '|' .. s:c_primary_tag_handle .. ')' +let s:c_ns_shorthand_tag = '\v' .. s:c_tag_handle .. s:ns_tag_char .. '+' let s:c_non_specific_tag = '\v\!' -let s:ns_uri_char = '\v%(%\x\x|'.s:ns_word_char.'\v|[#/;?:@&=+$,.!~*''()[\]])' -let s:c_verbatim_tag = '\v\!\<'.s:ns_uri_char.'+\>' -let s:c_ns_tag_property = '\v'.s:c_verbatim_tag. - \ '\v|'.s:c_ns_shorthand_tag. - \ '\v|'.s:c_non_specific_tag +let s:ns_uri_char = '\v%(%\x\x|' .. s:ns_word_char .. '\v|[#/;?:@&=+$,.!~*''()[\]])' +let s:c_verbatim_tag = '\v\!\<' .. s:ns_uri_char.. '+\>' +let s:c_ns_tag_property = '\v' .. s:c_verbatim_tag. + \ '\v|' .. s:c_ns_shorthand_tag. + \ '\v|' .. s:c_non_specific_tag let s:block_scalar_header = '\v[|>]%([+-]?[1-9]|[1-9]?[+-])?' @@ -142,9 +142,9 @@ function GetYAMLIndent(lnum) " - List with " multiline scalar return previndent+2 - elseif prevline =~# s:mapkeyregex . '\v\s*%(%('.s:c_ns_tag_property. - \ '\v|'.s:c_ns_anchor_property. - \ '\v|'.s:block_scalar_header. + elseif prevline =~# s:mapkeyregex .. '\v\s*%(%(' .. s:c_ns_tag_property .. + \ '\v|' .. s:c_ns_anchor_property .. + \ '\v|' .. s:block_scalar_header .. \ '\v)%(\s+|\s*%(\#.*)?$))*' " Mapping with: value " that is multiline scalar diff --git a/runtime/syntax/vim.vim b/runtime/syntax/vim.vim index 9917a7f694..76ad1d523d 100644 --- a/runtime/syntax/vim.vim +++ b/runtime/syntax/vim.vim @@ -368,7 +368,7 @@ syn match vimSetMod contained "&vim\=\|[!&?<]\|all&" " Let: {{{2 " === syn keyword vimLet let unl[et] skipwhite nextgroup=vimVar,vimFuncVar,vimLetHereDoc -VimFoldh syn region vimLetHereDoc matchgroup=vimLetHereDocStart start='=<<\s\+\%(trim\>\)\=\s*\z(\L\S*\)' matchgroup=vimLetHereDocStop end='^\s*\z1\s*$' +VimFoldh syn region vimLetHereDoc matchgroup=vimLetHereDocStart start='=<<\s\+\%(trim\%(\s\+eval\)\=\|eval\%(\s\+trim\)\=\)\=\s*\z(\L\S*\)' matchgroup=vimLetHereDocStop end='^\s*\z1\s*$' " Abbreviations: {{{2 " ============= diff --git a/src/nvim/api/private/defs.h b/src/nvim/api/private/defs.h index 396fab721d..b1e0dd364c 100644 --- a/src/nvim/api/private/defs.h +++ b/src/nvim/api/private/defs.h @@ -6,9 +6,10 @@ #include <string.h> #include "nvim/func_attr.h" +#include "nvim/lib/kvec.h" #include "nvim/types.h" -#define ARRAY_DICT_INIT { .size = 0, .capacity = 0, .items = NULL } +#define ARRAY_DICT_INIT KV_INITIAL_VALUE #define STRING_INIT { .data = NULL, .size = 0 } #define OBJECT_INIT { .type = kObjectTypeNil } #define ERROR_INIT { .type = kErrorTypeNone, .msg = NULL } @@ -84,18 +85,10 @@ REMOTE_TYPE(Window); REMOTE_TYPE(Tabpage); typedef struct object Object; - -typedef struct { - Object *items; - size_t size, capacity; -} Array; +typedef kvec_t(Object) Array; typedef struct key_value_pair KeyValuePair; - -typedef struct { - KeyValuePair *items; - size_t size, capacity; -} Dictionary; +typedef kvec_t(KeyValuePair) Dictionary; typedef enum { kObjectTypeNil = 0, diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index ff6a4c37e8..693d946088 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -669,6 +669,32 @@ void api_free_string(String value) xfree(value.data); } +Array arena_array(Arena *arena, size_t max_size) +{ + Array arr = ARRAY_DICT_INIT; + kv_fixsize_arena(arena, arr, max_size); + return arr; +} + +Dictionary arena_dict(Arena *arena, size_t max_size) +{ + Dictionary dict = ARRAY_DICT_INIT; + kv_fixsize_arena(arena, dict, max_size); + return dict; +} + +String arena_string(Arena *arena, String str) +{ + if (str.size) { + char *mem = arena_alloc(arena, str.size + 1, false); + memcpy(mem, str.data, str.size); + mem[str.size] = NUL; + return cbuf_as_string(mem, str.size); + } else { + return (String)STRING_INIT; + } +} + void api_free_object(Object value) { switch (value.type) { diff --git a/src/nvim/api/private/helpers.h b/src/nvim/api/private/helpers.h index 8423db4970..a4348d8b44 100644 --- a/src/nvim/api/private/helpers.h +++ b/src/nvim/api/private/helpers.h @@ -63,17 +63,31 @@ #define PUT(dict, k, v) \ kv_push(dict, ((KeyValuePair) { .key = cstr_to_string(k), .value = v })) +#define PUT_C(dict, k, v) \ + kv_push_c(dict, ((KeyValuePair) { .key = cstr_as_string(k), .value = v })) + #define PUT_BOOL(dict, name, condition) PUT(dict, name, BOOLEAN_OBJ(condition)); #define ADD(array, item) \ kv_push(array, item) +#define ADD_C(array, item) \ + kv_push_c(array, item) + #define FIXED_TEMP_ARRAY(name, fixsize) \ Array name = ARRAY_DICT_INIT; \ Object name##__items[fixsize]; \ name.size = fixsize; \ name.items = name##__items; \ +#define MAXSIZE_TEMP_ARRAY(name, maxsize) \ + Array name = ARRAY_DICT_INIT; \ + Object name##__items[maxsize]; \ + name.capacity = maxsize; \ + name.items = name##__items; \ + +#define cbuf_as_string(d, s) ((String) { .data = d, .size = s }) + #define STATIC_CSTR_AS_STRING(s) ((String) { .data = s, .size = sizeof(s) - 1 }) /// Create a new String instance, putting data in allocated memory diff --git a/src/nvim/api/ui.c b/src/nvim/api/ui.c index dc04eedebc..54ce838b9b 100644 --- a/src/nvim/api/ui.c +++ b/src/nvim/api/ui.c @@ -9,11 +9,13 @@ #include "nvim/api/private/defs.h" #include "nvim/api/private/helpers.h" #include "nvim/api/ui.h" +#include "nvim/channel.h" #include "nvim/cursor_shape.h" #include "nvim/highlight.h" #include "nvim/map.h" #include "nvim/memory.h" #include "nvim/msgpack_rpc/channel.h" +#include "nvim/msgpack_rpc/helpers.h" #include "nvim/option.h" #include "nvim/popupmnu.h" #include "nvim/screen.h" @@ -21,14 +23,32 @@ #include "nvim/vim.h" #include "nvim/window.h" -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "api/ui.c.generated.h" -# include "ui_events_remote.generated.h" -#endif - typedef struct { uint64_t channel_id; - Array buffer; + +#define UI_BUF_SIZE 4096 ///< total buffer size for pending msgpack data. + /// guranteed size available for each new event (so packing of simple events + /// and the header of grid_line will never fail) +#define EVENT_BUF_SIZE 256 + char buf[UI_BUF_SIZE]; ///< buffer of packed but not yet sent msgpack data + char *buf_wptr; ///< write head of buffer + const char *cur_event; ///< name of current event (might get multiple arglists) + Array call_buf; ///< buffer for constructing a single arg list (max 16 elements!) + + // state for write_cb, while packing a single arglist to msgpack. This + // might fail due to buffer overflow. + size_t pack_totlen; + bool buf_overflow; + char *temp_buf; + + // We start packing the two outermost msgpack arrays before knowing the total + // number of elements. Thus track the location where array size will need + // to be written in the msgpack buffer, once the specifc array is finished. + char *nevents_pos; + char *ncalls_pos; + uint32_t nevents; ///< number of distinct events (top-level args to "redraw" + uint32_t ncalls; ///< number of calls made to the current event (plus one for the name!) + bool flushed_events; ///< events where sent to client without "flush" event int hl_id; // Current highlight for legacy put event. Integer cursor_row, cursor_col; // Intended visible cursor position. @@ -38,8 +58,76 @@ typedef struct { bool wildmenu_active; } UIData; +#define BUF_POS(data) ((size_t)((data)->buf_wptr - (data)->buf)) + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "api/ui.c.generated.h" +# include "ui_events_remote.generated.h" +#endif + static PMap(uint64_t) connected_uis = MAP_INIT; +#define mpack_w(b, byte) *(*b)++ = (char)(byte); +static void mpack_w2(char **b, uint32_t v) +{ + *(*b)++ = (char)((v >> 8) & 0xff); + *(*b)++ = (char)(v & 0xff); +} + +static void mpack_w4(char **b, uint32_t v) +{ + *(*b)++ = (char)((v >> 24) & 0xff); + *(*b)++ = (char)((v >> 16) & 0xff); + *(*b)++ = (char)((v >> 8) & 0xff); + *(*b)++ = (char)(v & 0xff); +} + +static void mpack_uint(char **buf, uint32_t val) +{ + if (val > 0xffff) { + mpack_w(buf, 0xce); + mpack_w4(buf, val); + } else if (val > 0xff) { + mpack_w(buf, 0xcd); + mpack_w2(buf, val); + } else if (val > 0x7f) { + mpack_w(buf, 0xcc); + mpack_w(buf, val); + } else { + mpack_w(buf, val); + } +} + +static void mpack_array(char **buf, uint32_t len) +{ + if (len < 0x10) { + mpack_w(buf, 0x90 | len); + } else if (len < 0x10000) { + mpack_w(buf, 0xdc); + mpack_w2(buf, len); + } else { + mpack_w(buf, 0xdd); + mpack_w4(buf, len); + } +} + +static char *mpack_array_dyn16(char **buf) +{ + mpack_w(buf, 0xdc); + char *pos = *buf; + mpack_w2(buf, 0xFFEF); + return pos; +} + +static void mpack_str(char **buf, const char *str) +{ + assert(sizeof(schar_T) - 1 < 0x20); + size_t len = STRLEN(str); + mpack_w(buf, 0xa0 | len); + memcpy(*buf, str, len); + *buf += len; +} + void remote_ui_disconnect(uint64_t channel_id) { UI *ui = pmap_get(uint64_t)(&connected_uis, channel_id); @@ -47,9 +135,9 @@ void remote_ui_disconnect(uint64_t channel_id) return; } UIData *data = ui->data; - api_free_array(data->buffer); // Destroy pending screen updates. + kv_destroy(data->call_buf); pmap_del(uint64_t)(&connected_uis, channel_id); - xfree(ui->data); + xfree(data); ui->data = NULL; // Flag UI as "stopped". ui_detach_impl(ui, channel_id); xfree(ui); @@ -159,10 +247,19 @@ void nvim_ui_attach(uint64_t channel_id, Integer width, Integer height, Dictiona UIData *data = xmalloc(sizeof(UIData)); data->channel_id = channel_id; - data->buffer = (Array)ARRAY_DICT_INIT; + data->cur_event = NULL; data->hl_id = 0; data->client_col = -1; + data->nevents_pos = NULL; + data->nevents = 0; + data->flushed_events = false; + data->ncalls_pos = NULL; + data->ncalls = 0; + data->buf_wptr = data->buf; + data->temp_buf = NULL; data->wildmenu_active = false; + data->call_buf = (Array)ARRAY_DICT_INIT; + kv_ensure_space(data->call_buf, 16); ui->data = data; pmap_put(uint64_t)(&connected_uis, channel_id, ui); @@ -433,34 +530,128 @@ void nvim_ui_pum_set_bounds(uint64_t channel_id, Float width, Float height, Floa ui->pum_pos = true; } -/// Pushes data into UI.UIData, to be consumed later by remote_ui_flush(). -static void push_call(UI *ui, const char *name, Array args) +static void flush_event(UIData *data) +{ + if (data->cur_event) { + mpack_w2(&data->ncalls_pos, data->ncalls); + data->cur_event = NULL; + } + if (!data->nevents_pos) { + assert(BUF_POS(data) == 0); + char **buf = &data->buf_wptr; + // [2, "redraw", [...]] + mpack_array(buf, 3); + mpack_uint(buf, 2); + mpack_str(buf, "redraw"); + data->nevents_pos = mpack_array_dyn16(buf); + } +} + +static inline int write_cb(void *vdata, const char *buf, size_t len) +{ + UIData *data = (UIData *)vdata; + if (!buf) { + return 0; + } + + data->pack_totlen += len; + if (!data->temp_buf && UI_BUF_SIZE - BUF_POS(data) < len) { + data->buf_overflow = true; + return 0; + } + + memcpy(data->buf_wptr, buf, len); + data->buf_wptr += len; + + return 0; +} + +static bool prepare_call(UI *ui, const char *name) { - Array call = ARRAY_DICT_INIT; UIData *data = ui->data; - // To optimize data transfer(especially for "put"), we bundle adjacent + if (BUF_POS(data) > UI_BUF_SIZE - EVENT_BUF_SIZE) { + remote_ui_flush_buf(ui); + } + + // To optimize data transfer(especially for "grid_line"), we bundle adjacent // calls to same method together, so only add a new call entry if the last // method call is different from "name" - if (kv_size(data->buffer)) { - call = kv_A(data->buffer, kv_size(data->buffer) - 1).data.array; - } - if (!kv_size(call) || strcmp(kv_A(call, 0).data.string.data, name)) { - call = (Array)ARRAY_DICT_INIT; - ADD(data->buffer, ARRAY_OBJ(call)); - ADD(call, STRING_OBJ(cstr_to_string(name))); + if (!data->cur_event || !strequal(data->cur_event, name)) { + flush_event(data); + data->cur_event = name; + char **buf = &data->buf_wptr; + data->ncalls_pos = mpack_array_dyn16(buf); + mpack_str(buf, name); + data->nevents++; + data->ncalls = 1; + return true; } - ADD(call, ARRAY_OBJ(args)); - kv_A(data->buffer, kv_size(data->buffer) - 1).data.array = call; + return false; +} + +/// Pushes data into UI.UIData, to be consumed later by remote_ui_flush(). +static void push_call(UI *ui, const char *name, Array args) +{ + UIData *data = ui->data; + bool pending = data->nevents_pos; + char *buf_pos_save = data->buf_wptr; + + bool new_event = prepare_call(ui, name); + + msgpack_packer pac; + data->pack_totlen = 0; + data->buf_overflow = false; + msgpack_packer_init(&pac, data, write_cb); + msgpack_rpc_from_array(args, &pac); + if (data->buf_overflow) { + data->buf_wptr = buf_pos_save; + if (new_event) { + data->cur_event = NULL; + data->nevents--; + } + if (pending) { + remote_ui_flush_buf(ui); + } + + if (data->pack_totlen > UI_BUF_SIZE - strlen(name) - 20) { + // TODO(bfredl): manually testable by setting UI_BUF_SIZE to 1024 (mode_info_set) + data->temp_buf = xmalloc(20 + strlen(name) + data->pack_totlen); + data->buf_wptr = data->temp_buf; + char **buf = &data->buf_wptr; + mpack_array(buf, 3); + mpack_uint(buf, 2); + mpack_str(buf, "redraw"); + mpack_array(buf, 1); + mpack_array(buf, 2); + mpack_str(buf, name); + } else { + prepare_call(ui, name); + } + data->pack_totlen = 0; + data->buf_overflow = false; + msgpack_rpc_from_array(args, &pac); + + if (data->temp_buf) { + size_t size = (size_t)(data->buf_wptr - data->temp_buf); + WBuffer *buf = wstream_new_buffer(data->temp_buf, size, 1, xfree); + rpc_write_raw(data->channel_id, buf); + data->temp_buf = NULL; + data->buf_wptr = data->buf; + data->nevents_pos = NULL; + } + } + data->ncalls++; } static void remote_ui_grid_clear(UI *ui, Integer grid) { - Array args = ARRAY_DICT_INIT; + UIData *data = ui->data; + Array args = data->call_buf; if (ui->ui_ext[kUILinegrid]) { - ADD(args, INTEGER_OBJ(grid)); + ADD_C(args, INTEGER_OBJ(grid)); } const char *name = ui->ui_ext[kUILinegrid] ? "grid_clear" : "clear"; push_call(ui, name, args); @@ -468,12 +659,13 @@ static void remote_ui_grid_clear(UI *ui, Integer grid) static void remote_ui_grid_resize(UI *ui, Integer grid, Integer width, Integer height) { - Array args = ARRAY_DICT_INIT; + UIData *data = ui->data; + Array args = data->call_buf; if (ui->ui_ext[kUILinegrid]) { - ADD(args, INTEGER_OBJ(grid)); + ADD_C(args, INTEGER_OBJ(grid)); } - ADD(args, INTEGER_OBJ(width)); - ADD(args, INTEGER_OBJ(height)); + ADD_C(args, INTEGER_OBJ(width)); + ADD_C(args, INTEGER_OBJ(height)); const char *name = ui->ui_ext[kUILinegrid] ? "grid_resize" : "resize"; push_call(ui, name, args); } @@ -481,35 +673,36 @@ static void remote_ui_grid_resize(UI *ui, Integer grid, Integer width, Integer h static void remote_ui_grid_scroll(UI *ui, Integer grid, Integer top, Integer bot, Integer left, Integer right, Integer rows, Integer cols) { + UIData *data = ui->data; if (ui->ui_ext[kUILinegrid]) { - Array args = ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(grid)); - ADD(args, INTEGER_OBJ(top)); - ADD(args, INTEGER_OBJ(bot)); - ADD(args, INTEGER_OBJ(left)); - ADD(args, INTEGER_OBJ(right)); - ADD(args, INTEGER_OBJ(rows)); - ADD(args, INTEGER_OBJ(cols)); + Array args = data->call_buf; + ADD_C(args, INTEGER_OBJ(grid)); + ADD_C(args, INTEGER_OBJ(top)); + ADD_C(args, INTEGER_OBJ(bot)); + ADD_C(args, INTEGER_OBJ(left)); + ADD_C(args, INTEGER_OBJ(right)); + ADD_C(args, INTEGER_OBJ(rows)); + ADD_C(args, INTEGER_OBJ(cols)); push_call(ui, "grid_scroll", args); } else { - Array args = ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(top)); - ADD(args, INTEGER_OBJ(bot - 1)); - ADD(args, INTEGER_OBJ(left)); - ADD(args, INTEGER_OBJ(right - 1)); + Array args = data->call_buf; + ADD_C(args, INTEGER_OBJ(top)); + ADD_C(args, INTEGER_OBJ(bot - 1)); + ADD_C(args, INTEGER_OBJ(left)); + ADD_C(args, INTEGER_OBJ(right - 1)); push_call(ui, "set_scroll_region", args); - args = (Array)ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(rows)); + args = data->call_buf; + ADD_C(args, INTEGER_OBJ(rows)); push_call(ui, "scroll", args); // some clients have "clear" being affected by scroll region, // so reset it. - args = (Array)ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(0)); - ADD(args, INTEGER_OBJ(ui->height - 1)); - ADD(args, INTEGER_OBJ(0)); - ADD(args, INTEGER_OBJ(ui->width - 1)); + args = data->call_buf; + ADD_C(args, INTEGER_OBJ(0)); + ADD_C(args, INTEGER_OBJ(ui->height - 1)); + ADD_C(args, INTEGER_OBJ(0)); + ADD_C(args, INTEGER_OBJ(ui->width - 1)); push_call(ui, "set_scroll_region", args); } } @@ -520,26 +713,27 @@ static void remote_ui_default_colors_set(UI *ui, Integer rgb_fg, Integer rgb_bg, if (!ui->ui_ext[kUITermColors]) { HL_SET_DEFAULT_COLORS(rgb_fg, rgb_bg, rgb_sp); } - Array args = ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(rgb_fg)); - ADD(args, INTEGER_OBJ(rgb_bg)); - ADD(args, INTEGER_OBJ(rgb_sp)); - ADD(args, INTEGER_OBJ(cterm_fg)); - ADD(args, INTEGER_OBJ(cterm_bg)); + UIData *data = ui->data; + Array args = data->call_buf; + ADD_C(args, INTEGER_OBJ(rgb_fg)); + ADD_C(args, INTEGER_OBJ(rgb_bg)); + ADD_C(args, INTEGER_OBJ(rgb_sp)); + ADD_C(args, INTEGER_OBJ(cterm_fg)); + ADD_C(args, INTEGER_OBJ(cterm_bg)); push_call(ui, "default_colors_set", args); // Deprecated if (!ui->ui_ext[kUILinegrid]) { - args = (Array)ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(ui->rgb ? rgb_fg : cterm_fg - 1)); + args = data->call_buf; + ADD_C(args, INTEGER_OBJ(ui->rgb ? rgb_fg : cterm_fg - 1)); push_call(ui, "update_fg", args); - args = (Array)ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(ui->rgb ? rgb_bg : cterm_bg - 1)); + args = data->call_buf; + ADD_C(args, INTEGER_OBJ(ui->rgb ? rgb_bg : cterm_bg - 1)); push_call(ui, "update_bg", args); - args = (Array)ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(ui->rgb ? rgb_sp : -1)); + args = data->call_buf; + ADD_C(args, INTEGER_OBJ(ui->rgb ? rgb_sp : -1)); push_call(ui, "update_sp", args); } } @@ -550,25 +744,29 @@ static void remote_ui_hl_attr_define(UI *ui, Integer id, HlAttrs rgb_attrs, HlAt if (!ui->ui_ext[kUILinegrid]) { return; } - Array args = ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(id)); - ADD(args, DICTIONARY_OBJ(hlattrs2dict(rgb_attrs, true))); - ADD(args, DICTIONARY_OBJ(hlattrs2dict(cterm_attrs, false))); + UIData *data = ui->data; + Array args = data->call_buf; + ADD_C(args, INTEGER_OBJ(id)); + ADD_C(args, DICTIONARY_OBJ(hlattrs2dict(rgb_attrs, true))); + ADD_C(args, DICTIONARY_OBJ(hlattrs2dict(cterm_attrs, false))); if (ui->ui_ext[kUIHlState]) { - ADD(args, ARRAY_OBJ(copy_array(info))); + ADD_C(args, ARRAY_OBJ(info)); } else { - ADD(args, ARRAY_OBJ((Array)ARRAY_DICT_INIT)); + ADD_C(args, ARRAY_OBJ((Array)ARRAY_DICT_INIT)); } push_call(ui, "hl_attr_define", args); + // TODO(bfredl): could be elided + api_free_dictionary(kv_A(args, 1).data.dictionary); + api_free_dictionary(kv_A(args, 2).data.dictionary); } static void remote_ui_highlight_set(UI *ui, int id) { - Array args = ARRAY_DICT_INIT; UIData *data = ui->data; + Array args = data->call_buf; if (data->hl_id == id) { return; @@ -576,18 +774,20 @@ static void remote_ui_highlight_set(UI *ui, int id) data->hl_id = id; Dictionary hl = hlattrs2dict(syn_attr2entry(id), ui->rgb); - ADD(args, DICTIONARY_OBJ(hl)); + ADD_C(args, DICTIONARY_OBJ(hl)); push_call(ui, "highlight_set", args); + api_free_dictionary(kv_A(args, 0).data.dictionary); } /// "true" cursor used only for input focus static void remote_ui_grid_cursor_goto(UI *ui, Integer grid, Integer row, Integer col) { if (ui->ui_ext[kUILinegrid]) { - Array args = ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(grid)); - ADD(args, INTEGER_OBJ(row)); - ADD(args, INTEGER_OBJ(col)); + UIData *data = ui->data; + Array args = data->call_buf; + ADD_C(args, INTEGER_OBJ(grid)); + ADD_C(args, INTEGER_OBJ(row)); + ADD_C(args, INTEGER_OBJ(col)); push_call(ui, "grid_cursor_goto", args); } else { UIData *data = ui->data; @@ -606,9 +806,9 @@ static void remote_ui_cursor_goto(UI *ui, Integer row, Integer col) } data->client_row = row; data->client_col = col; - Array args = ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(row)); - ADD(args, INTEGER_OBJ(col)); + Array args = data->call_buf; + ADD_C(args, INTEGER_OBJ(row)); + ADD_C(args, INTEGER_OBJ(col)); push_call(ui, "cursor_goto", args); } @@ -616,8 +816,8 @@ static void remote_ui_put(UI *ui, const char *cell) { UIData *data = ui->data; data->client_col++; - Array args = ARRAY_DICT_INIT; - ADD(args, STRING_OBJ(cstr_to_string(cell))); + Array args = data->call_buf; + ADD_C(args, STRING_OBJ(cstr_as_string((char *)cell))); push_call(ui, "put", args); } @@ -627,41 +827,64 @@ static void remote_ui_raw_line(UI *ui, Integer grid, Integer row, Integer startc { UIData *data = ui->data; if (ui->ui_ext[kUILinegrid]) { - Array args = ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(grid)); - ADD(args, INTEGER_OBJ(row)); - ADD(args, INTEGER_OBJ(startcol)); - Array cells = ARRAY_DICT_INIT; - int repeat = 0; + prepare_call(ui, "grid_line"); + data->ncalls++; + + char **buf = &data->buf_wptr; + mpack_array(buf, 4); + mpack_uint(buf, (uint32_t)grid); + mpack_uint(buf, (uint32_t)row); + mpack_uint(buf, (uint32_t)startcol); + char *lenpos = mpack_array_dyn16(buf); + + uint32_t repeat = 0; size_t ncells = (size_t)(endcol - startcol); int last_hl = -1; + uint32_t nelem = 0; for (size_t i = 0; i < ncells; i++) { repeat++; if (i == ncells - 1 || attrs[i] != attrs[i + 1] || STRCMP(chunk[i], chunk[i + 1])) { - Array cell = ARRAY_DICT_INIT; - ADD(cell, STRING_OBJ(cstr_to_string((const char *)chunk[i]))); - if (attrs[i] != last_hl || repeat > 1) { - ADD(cell, INTEGER_OBJ(attrs[i])); - last_hl = attrs[i]; + if (UI_BUF_SIZE - BUF_POS(data) < 2 * (1 + 2 + sizeof(schar_T) + 5 + 5)) { + // close to overflowing the redraw buffer. finish this event, + // flush, and start a new "grid_line" event at the current position. + // For simplicity leave place for the final "clear" element + // as well, hence the factor of 2 in the check. + mpack_w2(&lenpos, nelem); + remote_ui_flush_buf(ui); + + prepare_call(ui, "grid_line"); + data->ncalls++; + mpack_array(buf, 4); + mpack_uint(buf, (uint32_t)grid); + mpack_uint(buf, (uint32_t)row); + mpack_uint(buf, (uint32_t)startcol + (uint32_t)i - repeat + 1); + lenpos = mpack_array_dyn16(buf); + nelem = 0; + last_hl = -1; } - if (repeat > 1) { - ADD(cell, INTEGER_OBJ(repeat)); + uint32_t csize = (repeat > 1) ? 3 : ((attrs[i] != last_hl) ? 2 : 1); + nelem++; + mpack_array(buf, csize); + mpack_str(buf, (const char *)chunk[i]); + if (csize >= 2) { + mpack_uint(buf, (uint32_t)attrs[i]); + if (csize >= 3) { + mpack_uint(buf, repeat); + } } - ADD(cells, ARRAY_OBJ(cell)); + last_hl = attrs[i]; repeat = 0; } } if (endcol < clearcol) { - Array cell = ARRAY_DICT_INIT; - ADD(cell, STRING_OBJ(cstr_to_string(" "))); - ADD(cell, INTEGER_OBJ(clearattr)); - ADD(cell, INTEGER_OBJ(clearcol - endcol)); - ADD(cells, ARRAY_OBJ(cell)); + nelem++; + mpack_array(buf, 3); + mpack_str(buf, " "); + mpack_uint(buf, (uint32_t)clearattr); + mpack_uint(buf, (uint32_t)(clearcol - endcol)); } - ADD(args, ARRAY_OBJ(cells)); - - push_call(ui, "grid_line", args); + mpack_w2(&lenpos, nelem); } else { for (int i = 0; i < endcol - startcol; i++) { remote_ui_cursor_goto(ui, row, startcol + i); @@ -688,16 +911,47 @@ static void remote_ui_raw_line(UI *ui, Integer grid, Integer row, Integer startc } } +/// Flush the internal packing buffer to the client. +/// +/// This might happen multiple times before the actual ui_flush, if the +/// total redraw size is large! +static void remote_ui_flush_buf(UI *ui) +{ + UIData *data = ui->data; + if (!data->nevents_pos) { + return; + } + if (data->cur_event) { + flush_event(data); + } + mpack_w2(&data->nevents_pos, data->nevents); + data->nevents = 0; + data->nevents_pos = NULL; + + // TODO(bfredl): elide copy by a length one free-list like the arena + size_t size = BUF_POS(data); + WBuffer *buf = wstream_new_buffer(xmemdup(data->buf, size), size, 1, xfree); + rpc_write_raw(data->channel_id, buf); + data->buf_wptr = data->buf; + // we have sent events to the client, but possibly not yet the final "flush" + // event. + data->flushed_events = true; +} + +/// An intentional flush (vsync) when Nvim is finished redrawing the screen +/// +/// Clients can know this happened by a final "flush" event at the end of the +/// "redraw" batch. static void remote_ui_flush(UI *ui) { UIData *data = ui->data; - if (data->buffer.size > 0) { + if (data->nevents > 0 || data->flushed_events) { if (!ui->ui_ext[kUILinegrid]) { remote_ui_cursor_goto(ui, data->cursor_row, data->cursor_col); } push_call(ui, "flush", (Array)ARRAY_DICT_INIT); - rpc_send_event(data->channel_id, "redraw", data->buffer); - data->buffer = (Array)ARRAY_DICT_INIT; + remote_ui_flush_buf(ui); + data->flushed_events = false; } } @@ -732,7 +986,7 @@ static Array translate_firstarg(UI *ui, Array args) return new_args; } -static void remote_ui_event(UI *ui, char *name, Array args, bool *args_consumed) +static void remote_ui_event(UI *ui, char *name, Array args) { UIData *data = ui->data; if (!ui->ui_ext[kUILinegrid]) { @@ -741,21 +995,24 @@ static void remote_ui_event(UI *ui, char *name, Array args, bool *args_consumed) if (strequal(name, "cmdline_show")) { Array new_args = translate_firstarg(ui, args); push_call(ui, name, new_args); + api_free_array(new_args); return; } else if (strequal(name, "cmdline_block_show")) { - Array new_args = ARRAY_DICT_INIT; + Array new_args = data->call_buf; Array block = args.items[0].data.array; Array new_block = ARRAY_DICT_INIT; for (size_t i = 0; i < block.size; i++) { ADD(new_block, ARRAY_OBJ(translate_contents(ui, block.items[i].data.array))); } - ADD(new_args, ARRAY_OBJ(new_block)); + ADD_C(new_args, ARRAY_OBJ(new_block)); push_call(ui, name, new_args); + api_free_array(new_block); return; } else if (strequal(name, "cmdline_block_append")) { Array new_args = translate_firstarg(ui, args); push_call(ui, name, new_args); + api_free_array(new_args); return; } } @@ -766,18 +1023,19 @@ static void remote_ui_event(UI *ui, char *name, Array args, bool *args_consumed) data->wildmenu_active = (args.items[4].data.integer == -1) || !ui->ui_ext[kUIPopupmenu]; if (data->wildmenu_active) { - Array new_args = ARRAY_DICT_INIT; + Array new_args = data->call_buf; Array items = args.items[0].data.array; Array new_items = ARRAY_DICT_INIT; for (size_t i = 0; i < items.size; i++) { ADD(new_items, copy_object(items.items[i].data.array.items[0])); } - ADD(new_args, ARRAY_OBJ(new_items)); + ADD_C(new_args, ARRAY_OBJ(new_items)); push_call(ui, "wildmenu_show", new_args); + api_free_array(new_items); if (args.items[1].data.integer != -1) { - Array new_args2 = ARRAY_DICT_INIT; - ADD(new_args2, args.items[1]); - push_call(ui, "wildmenu_select", new_args); + Array new_args2 = data->call_buf; + ADD_C(new_args2, args.items[1]); + push_call(ui, "wildmenu_select", new_args2); } return; } @@ -792,18 +1050,7 @@ static void remote_ui_event(UI *ui, char *name, Array args, bool *args_consumed) } } - Array my_args = ARRAY_DICT_INIT; - // Objects are currently single-reference - // make a copy, but only if necessary - if (*args_consumed) { - for (size_t i = 0; i < args.size; i++) { - ADD(my_args, copy_object(args.items[i])); - } - } else { - my_args = args; - *args_consumed = true; - } - push_call(ui, name, my_args); + push_call(ui, name, args); } static void remote_ui_inspect(UI *ui, Dictionary *info) diff --git a/src/nvim/api/ui_events.in.h b/src/nvim/api/ui_events.in.h index 0030f9edf7..8b7e01e1c3 100644 --- a/src/nvim/api/ui_events.in.h +++ b/src/nvim/api/ui_events.in.h @@ -99,7 +99,7 @@ void raw_line(Integer grid, Integer row, Integer startcol, LineFlags flags, const schar_T *chunk, const sattr_T *attrs) FUNC_API_NOEXPORT FUNC_API_COMPOSITOR_IMPL; -void event(char *name, Array args, bool *args_consumed) +void event(char *name, Array args) FUNC_API_NOEXPORT; void win_pos(Integer grid, Window win, Integer startrow, diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 28b1a7580d..a65e89a9f5 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -7272,7 +7272,7 @@ void tabpage_close_other(tabpage_T *tp, int forceit) redraw_tabline = true; if (h != tabline_height()) { - shell_new_rows(); + win_new_screen_rows(); } } diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index 3065dc462b..211eeff45e 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -3309,45 +3309,53 @@ draw_cmdline_no_arabicshape: static void ui_ext_cmdline_show(CmdlineInfo *line) { + Arena arena = ARENA_EMPTY; + arena_start(&arena, &ui_ext_fixblk); Array content = ARRAY_DICT_INIT; if (cmdline_star) { + content = arena_array(&arena, 1); size_t len = 0; for (char_u *p = ccline.cmdbuff; *p; MB_PTR_ADV(p)) { len++; } - char *buf = xmallocz(len); + char *buf = arena_alloc(&arena, len, false); memset(buf, '*', len); - Array item = ARRAY_DICT_INIT; - ADD(item, INTEGER_OBJ(0)); - ADD(item, STRING_OBJ(((String) { .data = buf, .size = len }))); - ADD(content, ARRAY_OBJ(item)); + Array item = arena_array(&arena, 2); + ADD_C(item, INTEGER_OBJ(0)); + ADD_C(item, STRING_OBJ(cbuf_as_string(buf, len))); + ADD_C(content, ARRAY_OBJ(item)); } else if (kv_size(line->last_colors.colors)) { + content = arena_array(&arena, kv_size(line->last_colors.colors)); for (size_t i = 0; i < kv_size(line->last_colors.colors); i++) { CmdlineColorChunk chunk = kv_A(line->last_colors.colors, i); - Array item = ARRAY_DICT_INIT; - ADD(item, INTEGER_OBJ(chunk.attr)); + Array item = arena_array(&arena, 2); + ADD_C(item, INTEGER_OBJ(chunk.attr)); assert(chunk.end >= chunk.start); - ADD(item, STRING_OBJ(cbuf_to_string((char *)line->cmdbuff + chunk.start, - (size_t)(chunk.end - chunk.start)))); - ADD(content, ARRAY_OBJ(item)); + ADD_C(item, STRING_OBJ(cbuf_as_string((char *)line->cmdbuff + chunk.start, + (size_t)(chunk.end - chunk.start)))); + ADD_C(content, ARRAY_OBJ(item)); } } else { - Array item = ARRAY_DICT_INIT; - ADD(item, INTEGER_OBJ(0)); - ADD(item, STRING_OBJ(cstr_to_string((char *)(line->cmdbuff)))); - ADD(content, ARRAY_OBJ(item)); + Array item = arena_array(&arena, 2); + ADD_C(item, INTEGER_OBJ(0)); + ADD_C(item, STRING_OBJ(cstr_as_string((char *)(line->cmdbuff)))); + content = arena_array(&arena, 1); + ADD_C(content, ARRAY_OBJ(item)); } + char charbuf[2] = { (char)line->cmdfirstc, 0 }; ui_call_cmdline_show(content, line->cmdpos, - cchar_to_string((char)line->cmdfirstc), - cstr_to_string((char *)(line->cmdprompt)), + cstr_as_string(charbuf), + cstr_as_string((char *)(line->cmdprompt)), line->cmdindent, line->level); if (line->special_char) { - ui_call_cmdline_special_char(cchar_to_string(line->special_char), + charbuf[0] = line->special_char; + ui_call_cmdline_special_char(cstr_as_string(charbuf), line->special_shift, line->level); } + arena_mem_free(arena_finish(&arena), &ui_ext_fixblk); } void ui_ext_cmdline_block_append(size_t indent, const char *line) @@ -3363,9 +3371,9 @@ void ui_ext_cmdline_block_append(size_t indent, const char *line) ADD(content, ARRAY_OBJ(item)); ADD(cmdline_block, ARRAY_OBJ(content)); if (cmdline_block.size > 1) { - ui_call_cmdline_block_append(copy_array(content)); + ui_call_cmdline_block_append(content); } else { - ui_call_cmdline_block_show(copy_array(cmdline_block)); + ui_call_cmdline_block_show(cmdline_block); } } @@ -3385,7 +3393,7 @@ void cmdline_screen_cleared(void) } if (cmdline_block.size) { - ui_call_cmdline_block_show(copy_array(cmdline_block)); + ui_call_cmdline_block_show(cmdline_block); } int prev_level = ccline.level - 1; @@ -3442,7 +3450,8 @@ void putcmdline(char c, int shift) } msg_no_more = false; } else if (ccline.redraw_state != kCmdRedrawAll) { - ui_call_cmdline_special_char(cchar_to_string(c), shift, + char charbuf[2] = { c, 0 }; + ui_call_cmdline_special_char(cstr_as_string(charbuf), shift, ccline.level); } cursorcmd(); diff --git a/src/nvim/generators/gen_api_ui_events.lua b/src/nvim/generators/gen_api_ui_events.lua index 99dfac05e8..93bbaab74c 100755 --- a/src/nvim/generators/gen_api_ui_events.lua +++ b/src/nvim/generators/gen_api_ui_events.lua @@ -36,20 +36,12 @@ local function write_signature(output, ev, prefix, notype) output:write(')') end -local function write_arglist(output, ev, need_copy) - output:write(' Array args = ARRAY_DICT_INIT;\n') +local function write_arglist(output, ev) for j = 1, #ev.parameters do local param = ev.parameters[j] local kind = string.upper(param[1]) - local do_copy = need_copy and (kind == "ARRAY" or kind == "DICTIONARY" or kind == "STRING" or kind == "OBJECT") - output:write(' ADD(args, ') - if do_copy then - output:write('copy_object(') - end + output:write(' ADD_C(args, ') output:write(kind..'_OBJ('..param[2]..')') - if do_copy then - output:write(')') - end output:write(');\n') end end @@ -119,7 +111,9 @@ for i = 1, #events do remote_output:write('static void remote_ui_'..ev.name) write_signature(remote_output, ev, 'UI *ui') remote_output:write('\n{\n') - write_arglist(remote_output, ev, true) + remote_output:write(' UIData *data = ui->data;\n') + remote_output:write(' Array args = data->call_buf;\n') + write_arglist(remote_output, ev) remote_output:write(' push_call(ui, "'..ev.name..'", args);\n') remote_output:write('}\n\n') end @@ -186,9 +180,10 @@ for i = 1, #events do write_signature(call_output, ev, '') call_output:write('\n{\n') if ev.remote_only then - write_arglist(call_output, ev, false) + call_output:write(' Array args = call_buf;\n') + write_arglist(call_output, ev) call_output:write(' UI_LOG('..ev.name..');\n') - call_output:write(' ui_event("'..ev.name..'", args);\n') + call_output:write(' ui_call_event("'..ev.name..'", args);\n') elseif ev.compositor_impl then call_output:write(' UI_CALL') write_signature(call_output, ev, '!ui->composed, '..ev.name..', ui', true) diff --git a/src/nvim/lib/kvec.h b/src/nvim/lib/kvec.h index 4238088b1c..b5b3adf7d2 100644 --- a/src/nvim/lib/kvec.h +++ b/src/nvim/lib/kvec.h @@ -121,6 +121,9 @@ #define kv_push(v, x) \ (*kv_pushp(v) = (x)) +#define kv_pushp_c(v) ((v).items + ((v).size++)) +#define kv_push_c(v, x) (*kv_pushp_c(v) = (x)) + #define kv_a(v, i) \ (*(((v).capacity <= (size_t)(i) \ ? ((v).capacity = (v).size = (i) + 1, \ diff --git a/src/nvim/memory.c b/src/nvim/memory.c index dd06419391..9d02b83e5f 100644 --- a/src/nvim/memory.c +++ b/src/nvim/memory.c @@ -786,6 +786,7 @@ void free_all_mem(void) decor_free_all_mem(); nlua_free_all_mem(); + ui_free_all_mem(); } #endif diff --git a/src/nvim/memory.h b/src/nvim/memory.h index 0c048e1b5b..63d607c2ce 100644 --- a/src/nvim/memory.h +++ b/src/nvim/memory.h @@ -51,6 +51,10 @@ typedef struct { // inits an empty arena. use arena_start() to actually allocate space! #define ARENA_EMPTY { .cur_blk = NULL, .pos = 0, .size = 0 } +#define kv_fixsize_arena(a, v, s) \ + ((v).capacity = (s), \ + (v).items = (void *)arena_alloc(a, sizeof((v).items[0]) * (v).capacity, true)) + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "memory.h.generated.h" #endif diff --git a/src/nvim/message.c b/src/nvim/message.c index acc3122e93..e192af6aad 100644 --- a/src/nvim/message.c +++ b/src/nvim/message.c @@ -1068,6 +1068,7 @@ void ex_messages(void *const eap_p) } } ui_call_msg_history_show(entries); + api_free_array(entries); msg_ext_history_visible = true; wait_return(false); } else { @@ -1262,11 +1263,11 @@ void wait_return(int redraw) msg_ext_keep_after_cmdline = true; } - // If the window size changed set_shellsize() will redraw the screen. + // If the screen size changed screen_resize() will redraw the screen. // Otherwise the screen is only redrawn if 'redraw' is set and no ':' // typed. tmpState = State; - State = oldState; // restore State before set_shellsize + State = oldState; // restore State before screen_resize() setmouse(); msg_check(); need_wait_return = false; @@ -3136,12 +3137,13 @@ void msg_ext_ui_flush(void) msg_ext_emit_chunk(); if (msg_ext_chunks.size > 0) { - ui_call_msg_show(cstr_to_string(msg_ext_kind), + ui_call_msg_show(cstr_as_string((char *)msg_ext_kind), msg_ext_chunks, msg_ext_overwrite); if (!msg_ext_overwrite) { msg_ext_visible++; } msg_ext_kind = NULL; + api_free_array(msg_ext_chunks); msg_ext_chunks = (Array)ARRAY_DICT_INIT; msg_ext_cur_len = 0; msg_ext_overwrite = false; @@ -3155,6 +3157,7 @@ void msg_ext_flush_showmode(void) if (ui_has(kUIMessages)) { msg_ext_emit_chunk(); ui_call_msg_showmode(msg_ext_chunks); + api_free_array(msg_ext_chunks); msg_ext_chunks = (Array)ARRAY_DICT_INIT; msg_ext_cur_len = 0; } diff --git a/src/nvim/msgpack_rpc/channel.c b/src/nvim/msgpack_rpc/channel.c index b4f39c4c61..388fa2584c 100644 --- a/src/nvim/msgpack_rpc/channel.c +++ b/src/nvim/msgpack_rpc/channel.c @@ -360,6 +360,17 @@ free_ret: api_clear_error(&error); } +bool rpc_write_raw(uint64_t id, WBuffer *buffer) +{ + Channel *channel = find_rpc_channel(id); + if (!channel) { + wstream_release_wbuffer(buffer); + return false; + } + + return channel_write(channel, buffer); +} + static bool channel_write(Channel *channel, WBuffer *buffer) { bool success; diff --git a/src/nvim/msgpack_rpc/unpacker.c b/src/nvim/msgpack_rpc/unpacker.c index 419349e74d..26c1843026 100644 --- a/src/nvim/msgpack_rpc/unpacker.c +++ b/src/nvim/msgpack_rpc/unpacker.c @@ -11,10 +11,6 @@ # include "msgpack_rpc/unpacker.c.generated.h" #endif -#define kv_fixsize_arena(a, v, s) \ - ((v).capacity = (s), \ - (v).items = (void *)arena_alloc(a, sizeof((v).items[0]) * (v).capacity, true)) - Object unpack(const char *data, size_t size, Error *err) { Unpacker unpacker; diff --git a/src/nvim/normal.c b/src/nvim/normal.c index 35a7ee3319..8e4f78818b 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -2755,13 +2755,13 @@ static void display_showcmd(void) showcmd_is_clear = (len == 0); if (ui_has(kUIMessages)) { - Array content = ARRAY_DICT_INIT; + MAXSIZE_TEMP_ARRAY(content, 1); + MAXSIZE_TEMP_ARRAY(chunk, 2); if (len > 0) { - Array chunk = ARRAY_DICT_INIT; // placeholder for future highlight support - ADD(chunk, INTEGER_OBJ(0)); - ADD(chunk, STRING_OBJ(cstr_to_string((char *)showcmd_buf))); - ADD(content, ARRAY_OBJ(chunk)); + ADD_C(chunk, INTEGER_OBJ(0)); + ADD_C(chunk, STRING_OBJ(cstr_as_string((char *)showcmd_buf))); + ADD_C(content, ARRAY_OBJ(chunk)); } ui_call_msg_showcmd(content); return; diff --git a/src/nvim/option.c b/src/nvim/option.c index 075dd3d4bc..c97c027740 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -4528,7 +4528,7 @@ static char *set_num_option(int opt_idx, char_u *varp, long value, char *errbuf, last_status(false); // (re)set last window status line. } else if (pp == &p_stal) { // (re)set tab page line - shell_new_rows(); // recompute window positions and heights + win_new_screen_rows(); // recompute window positions and heights } else if (pp == &curwin->w_p_fdl) { newFoldLevel(); } else if (pp == &curwin->w_p_fml) { @@ -4617,7 +4617,7 @@ static char *set_num_option(int opt_idx, char_u *varp, long value, char *errbuf, p_columns = MIN_COLUMNS; } - // True max size is defined by check_shellsize() + // True max size is defined by check_screensize() p_lines = MIN(p_lines, INT_MAX); p_columns = MIN(p_columns, INT_MAX); @@ -4635,7 +4635,7 @@ static char *set_num_option(int opt_idx, char_u *varp, long value, char *errbuf, // messages. Rows = (int)p_lines; Columns = (int)p_columns; - check_shellsize(); + check_screensize(); if (cmdline_row > Rows - p_ch && Rows > p_ch) { assert(p_ch >= 0 && Rows - p_ch <= INT_MAX); cmdline_row = (int)(Rows - p_ch); diff --git a/src/nvim/popupmnu.c b/src/nvim/popupmnu.c index 7233a3bb72..db1054eb1f 100644 --- a/src/nvim/popupmnu.c +++ b/src/nvim/popupmnu.c @@ -151,17 +151,20 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i if (pum_external) { if (array_changed) { - Array arr = ARRAY_DICT_INIT; + Arena arena = ARENA_EMPTY; + arena_start(&arena, &ui_ext_fixblk); + Array arr = arena_array(&arena, (size_t)size); for (int i = 0; i < size; i++) { - Array item = ARRAY_DICT_INIT; - ADD(item, STRING_OBJ(cstr_to_string((char *)array[i].pum_text))); - ADD(item, STRING_OBJ(cstr_to_string((char *)array[i].pum_kind))); - ADD(item, STRING_OBJ(cstr_to_string((char *)array[i].pum_extra))); - ADD(item, STRING_OBJ(cstr_to_string((char *)array[i].pum_info))); - ADD(arr, ARRAY_OBJ(item)); + Array item = arena_array(&arena, 4); + ADD_C(item, STRING_OBJ(cstr_as_string((char *)array[i].pum_text))); + ADD_C(item, STRING_OBJ(cstr_as_string((char *)array[i].pum_kind))); + ADD_C(item, STRING_OBJ(cstr_as_string((char *)array[i].pum_extra))); + ADD_C(item, STRING_OBJ(cstr_as_string((char *)array[i].pum_info))); + ADD_C(arr, ARRAY_OBJ(item)); } ui_call_popupmenu_show(arr, selected, pum_win_row, cursor_col, pum_anchor_grid); + arena_mem_free(arena_finish(&arena), &ui_ext_fixblk); } else { ui_call_popupmenu_select(selected); return; @@ -438,7 +441,7 @@ void pum_redraw(void) if (ui_has(kUIMultigrid)) { const char *anchor = pum_above ? "SW" : "NW"; int row_off = pum_above ? -pum_height : 0; - ui_call_win_float_pos(pum_grid.handle, -1, cstr_to_string(anchor), + ui_call_win_float_pos(pum_grid.handle, -1, cstr_as_string((char *)anchor), pum_anchor_grid, pum_row - row_off, pum_col - col_off, false, pum_grid.zindex); } diff --git a/src/nvim/screen.c b/src/nvim/screen.c index 2ee7cd44f7..98f8e30391 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -5578,9 +5578,9 @@ void check_for_delay(bool check_msg_scroll) /// /// There may be some time between setting Rows and Columns and (re)allocating /// default_grid arrays. This happens when starting up and when -/// (manually) changing the shell size. Always use default_grid.rows and +/// (manually) changing the screen size. Always use default_grid.rows and /// default_grid.Columns to access items in default_grid.chars[]. Use Rows -/// and Columns for positioning text etc. where the final size of the shell is +/// and Columns for positioning text etc. where the final size of the screen is /// needed. void screenalloc(void) { @@ -5615,14 +5615,14 @@ retry: */ ++RedrawingDisabled; - // win_new_shellsize will recompute floats position, but tell the + // win_new_screensize will recompute floats position, but tell the // compositor to not redraw them yet ui_comp_set_screen_valid(false); if (msg_grid.chars) { msg_grid_invalid = true; } - win_new_shellsize(); // fit the windows in the new sized shell + win_new_screensize(); // fit the windows in the new sized screen comp_col(); // recompute columns for shown command and ruler @@ -6341,35 +6341,49 @@ void draw_tabline(void) void ui_ext_tabline_update(void) { - Array tabs = ARRAY_DICT_INIT; + Arena arena = ARENA_EMPTY; + arena_start(&arena, &ui_ext_fixblk); + + size_t n_tabs = 0; FOR_ALL_TABS(tp) { - Dictionary tab_info = ARRAY_DICT_INIT; - PUT(tab_info, "tab", TABPAGE_OBJ(tp->handle)); + n_tabs++; + } + + Array tabs = arena_array(&arena, n_tabs); + FOR_ALL_TABS(tp) { + Dictionary tab_info = arena_dict(&arena, 2); + PUT_C(tab_info, "tab", TABPAGE_OBJ(tp->handle)); win_T *cwp = (tp == curtab) ? curwin : tp->tp_curwin; get_trans_bufname(cwp->w_buffer); - PUT(tab_info, "name", STRING_OBJ(cstr_to_string((char *)NameBuff))); + PUT_C(tab_info, "name", STRING_OBJ(arena_string(&arena, cstr_as_string((char *)NameBuff)))); + + ADD_C(tabs, DICTIONARY_OBJ(tab_info)); + } - ADD(tabs, DICTIONARY_OBJ(tab_info)); + size_t n_buffers = 0; + FOR_ALL_BUFFERS(buf) { + n_buffers += buf->b_p_bl ? 1 : 0; } - Array buffers = ARRAY_DICT_INIT; + Array buffers = arena_array(&arena, n_buffers); FOR_ALL_BUFFERS(buf) { // Do not include unlisted buffers if (!buf->b_p_bl) { continue; } - Dictionary buffer_info = ARRAY_DICT_INIT; - PUT(buffer_info, "buffer", BUFFER_OBJ(buf->handle)); + Dictionary buffer_info = arena_dict(&arena, 2); + PUT_C(buffer_info, "buffer", BUFFER_OBJ(buf->handle)); get_trans_bufname(buf); - PUT(buffer_info, "name", STRING_OBJ(cstr_to_string((char *)NameBuff))); + PUT_C(buffer_info, "name", STRING_OBJ(arena_string(&arena, cstr_as_string((char *)NameBuff)))); - ADD(buffers, DICTIONARY_OBJ(buffer_info)); + ADD_C(buffers, DICTIONARY_OBJ(buffer_info)); } ui_call_tabline_update(curtab->handle, tabs, curbuf->handle, buffers); + arena_mem_free(arena_finish(&arena), &ui_ext_fixblk); } /* @@ -6618,11 +6632,11 @@ static void win_redr_ruler(win_T *wp, bool always) } if (ui_has(kUIMessages) && !part_of_status) { - Array content = ARRAY_DICT_INIT; - Array chunk = ARRAY_DICT_INIT; - ADD(chunk, INTEGER_OBJ(attr)); - ADD(chunk, STRING_OBJ(cstr_to_string((char *)buffer))); - ADD(content, ARRAY_OBJ(chunk)); + MAXSIZE_TEMP_ARRAY(content, 1); + MAXSIZE_TEMP_ARRAY(chunk, 2); + ADD_C(chunk, INTEGER_OBJ(attr)); + ADD_C(chunk, STRING_OBJ(cstr_as_string((char *)buffer))); + ADD_C(content, ARRAY_OBJ(chunk)); ui_call_msg_ruler(content); did_show_ext_ruler = true; } else { @@ -6744,7 +6758,7 @@ static void margin_columns_win(win_T *wp, int *left_col, int *right_col) prev_col_off = cur_col_off; } -/// Set dimensions of the Nvim application "shell". +/// Set dimensions of the Nvim application "screen". void screen_resize(int width, int height) { // Avoid recursiveness, can happen when setting the window size causes @@ -6775,7 +6789,7 @@ void screen_resize(int width, int height) Rows = height; Columns = width; - check_shellsize(); + check_screensize(); int max_p_ch = Rows - min_rows() + 1; if (!ui_has(kUIMessages) && p_ch > 0 && p_ch > max_p_ch) { p_ch = max_p_ch ? max_p_ch : 1; @@ -6852,49 +6866,23 @@ void screen_resize(int width, int height) resizing_screen = false; } -/// Check if the new Nvim application "shell" dimensions are valid. +/// Check if the new Nvim application "screen" dimensions are valid. /// Correct it if it's too small or way too big. -void check_shellsize(void) +void check_screensize(void) { + // Limit Rows and Columns to avoid an overflow in Rows * Columns. if (Rows < min_rows()) { // need room for one window and command line Rows = min_rows(); + } else if (Rows > 1000) { + Rows = 1000; } - limit_screen_size(); -} -// Limit Rows and Columns to avoid an overflow in Rows * Columns. -void limit_screen_size(void) -{ if (Columns < MIN_COLUMNS) { Columns = MIN_COLUMNS; } else if (Columns > 10000) { Columns = 10000; } - - if (Rows > 1000) { - Rows = 1000; - } -} - -void win_new_shellsize(void) -{ - static long old_Rows = 0; - static long old_Columns = 0; - - if (old_Rows != Rows) { - // If 'window' uses the whole screen, keep it using that. - // Don't change it when set with "-w size" on the command line. - if (p_window == old_Rows - 1 || (old_Rows == 0 && p_window == 0)) { - p_window = Rows - 1; - } - old_Rows = Rows; - shell_new_rows(); // update window sizes - } - if (old_Columns != Columns) { - old_Columns = Columns; - shell_new_columns(); // update window sizes - } } win_T *get_win_by_grid_handle(handle_T handle) diff --git a/src/nvim/ui.c b/src/nvim/ui.c index 3e715793e6..0ac7f4a55f 100644 --- a/src/nvim/ui.c +++ b/src/nvim/ui.c @@ -64,6 +64,8 @@ static handle_T cursor_grid_handle = DEFAULT_GRID_HANDLE; static bool has_mouse = false; static int pending_has_mouse = -1; +static Array call_buf = ARRAY_DICT_INIT; + #if MIN_LOG_LEVEL > LOGLVL_DBG # define UI_LOG(funname) #else @@ -123,6 +125,12 @@ void ui_init(void) default_grid.handle = 1; msg_grid_adj.target = &default_grid; ui_comp_init(); + kv_ensure_space(call_buf, 16); +} + +void ui_free_all_mem(void) +{ + kv_destroy(call_buf); } void ui_builtin_start(void) @@ -173,15 +181,6 @@ bool ui_active(void) return ui_count > 1; } -void ui_event(char *name, Array args) -{ - bool args_consumed = false; - ui_call_event(name, args, &args_consumed); - if (!args_consumed) { - api_free_array(args); - } -} - void ui_refresh(void) { if (!ui_active()) { diff --git a/src/nvim/ui.h b/src/nvim/ui.h index 1d278010e8..7dd2f5bce3 100644 --- a/src/nvim/ui.h +++ b/src/nvim/ui.h @@ -8,6 +8,7 @@ #include "nvim/api/private/defs.h" #include "nvim/globals.h" #include "nvim/highlight_defs.h" +#include "nvim/memory.h" typedef enum { kUICmdline = 0, @@ -46,6 +47,8 @@ enum { typedef int LineFlags; +EXTERN ArenaMem ui_ext_fixblk INIT(= NULL); + struct ui_t { bool rgb; bool override; ///< Force highest-requested UI capabilities. diff --git a/src/nvim/window.c b/src/nvim/window.c index 1487759d60..1a1f62f2c0 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -893,7 +893,7 @@ void ui_ext_win_position(win_T *wp) wp->w_grid_alloc.zindex = wp->w_float_config.zindex; if (ui_has(kUIMultigrid)) { - String anchor = cstr_to_string(float_anchor_str[c.anchor]); + String anchor = cstr_as_string((char *)float_anchor_str[c.anchor]); ui_call_win_float_pos(wp->w_grid_alloc.handle, wp->handle, anchor, grid->handle, row, col, c.focusable, wp->w_grid_alloc.zindex); @@ -967,8 +967,8 @@ static int check_split_disallowed(void) * "flags": * WSP_ROOM: require enough room for new window * WSP_VERT: vertical split. - * WSP_TOP: open window at the top-left of the shell (help window). - * WSP_BOT: open window at the bottom-right of the shell (quickfix window). + * WSP_TOP: open window at the top-left of the screen (help window). + * WSP_BOT: open window at the bottom-right of the screen (quickfix window). * WSP_HELP: creating the help window, keep layout snapshot * * return FAIL for failure, OK otherwise @@ -2499,7 +2499,7 @@ void close_windows(buf_T *buf, bool keep_curwin) redraw_tabline = true; if (h != tabline_height()) { - shell_new_rows(); + win_new_screen_rows(); } } @@ -2606,7 +2606,7 @@ static bool close_last_window_tabpage(win_T *win, bool free_buf, tabpage_T *prev win_close_othertab(win, free_buf, prev_curtab); if (h != tabline_height()) { - shell_new_rows(); + win_new_screen_rows(); } } entering_window(curwin); @@ -4350,10 +4350,10 @@ static void enter_tabpage(tabpage_T *tp, buf_T *old_curbuf, bool trigger_enter_a } if (curtab->tp_old_Rows != Rows || (old_off != firstwin->w_winrow)) { - shell_new_rows(); + win_new_screen_rows(); } if (curtab->tp_old_Columns != Columns && starting == 0) { - shell_new_columns(); // update window widths + win_new_screen_cols(); // update window widths } lastused_tabpage = old_curtab; @@ -5277,11 +5277,29 @@ static void frame_remove(frame_T *frp) } } -/* - * Called from win_new_shellsize() after Rows changed. - * This only does the current tab page, others must be done when made active. - */ -void shell_new_rows(void) +void win_new_screensize(void) +{ + static long old_Rows = 0; + static long old_Columns = 0; + + if (old_Rows != Rows) { + // If 'window' uses the whole screen, keep it using that. + // Don't change it when set with "-w size" on the command line. + if (p_window == old_Rows - 1 || (old_Rows == 0 && p_window == 0)) { + p_window = Rows - 1; + } + old_Rows = Rows; + win_new_screen_rows(); // update window sizes + } + if (old_Columns != Columns) { + old_Columns = Columns; + win_new_screen_cols(); // update window sizes + } +} +/// Called from win_new_screensize() after Rows changed. +/// +/// This only does the current tab page, others must be done when made active. +void win_new_screen_rows(void) { int h = (int)ROWS_AVAIL; @@ -5305,10 +5323,8 @@ void shell_new_rows(void) curtab->tp_ch_used = p_ch; } -/* - * Called from win_new_shellsize() after Columns changed. - */ -void shell_new_columns(void) +/// Called from win_new_screensize() after Columns changed. +void win_new_screen_cols(void) { if (firstwin == NULL) { // not initialized yet return; diff --git a/test/functional/ui/multibyte_spec.lua b/test/functional/ui/multibyte_spec.lua index c3c79ea574..d4e237bcb4 100644 --- a/test/functional/ui/multibyte_spec.lua +++ b/test/functional/ui/multibyte_spec.lua @@ -6,6 +6,9 @@ local feed = helpers.feed local feed_command = helpers.feed_command local insert = helpers.insert local funcs = helpers.funcs +local meths = helpers.meths +local split = helpers.split +local dedent = helpers.dedent describe("multibyte rendering", function() local screen @@ -115,6 +118,35 @@ describe("multibyte rendering", function() {4:-- INSERT --} | ]]) end) + + it('works with a lot of unicode (zalgo) text', function() + screen:try_resize(65, 10) + meths.buf_set_lines(0,0,-1,true, split(dedent [[ + L̓̉̑̒̌̚ơ̗̌̒̄̀ŕ̈̈̎̐̕è̇̅̄̄̐m̖̟̟̅̄̚ ̛̓̑̆̇̍i̗̟̞̜̅̐p̗̞̜̉̆̕s̟̜̘̍̑̏ū̟̞̎̃̉ḿ̘̙́́̐ ̖̍̌̇̉̚d̞̄̃̒̉̎ò́̌̌̂̐l̞̀̄̆̌̚ȯ̖̞̋̀̐r̓̇̌̃̃̚ ̗̘̀̏̍́s̜̀̎̎̑̕i̟̗̐̄̄̚t̝̎̆̓̐̒ ̘̇̔̓̊̚ȃ̛̟̗̏̅m̜̟̙̞̈̓é̘̞̟̔̆t̝̂̂̈̑̔,̜̜̖̅̄̍ ̛̗̊̓̆̚c̟̍̆̍̈̔ȯ̖̖̝̑̀n̜̟̎̊̃̚s̟̏̇̎̒̚e̙̐̈̓̌̚c̙̍̈̏̅̕ť̇̄̇̆̓e̛̓̌̈̓̈t̟̍̀̉̆̅u̝̞̎̂̄̚r̘̀̅̈̅̐ ̝̞̓́̇̉ã̏̀̆̅̕d̛̆̐̉̆̋ȉ̞̟̍̃̚p̛̜̊̍̂̓ȋ̏̅̃̋̚ṥ̛̏̃̕č̛̞̝̀̂í̗̘̌́̎n̔̎́̒̂̕ǧ̗̜̋̇̂ ̛̜̔̄̎̃ê̛̔̆̇̕l̘̝̏̐̊̏ĩ̛̍̏̏̄t̟̐́̀̐̎,̙̘̍̆̉̐ ̋̂̏̄̌̅s̙̓̌̈́̇e̛̗̋̒̎̏d̜̗̊̍̊̚ + ď̘̋̌̌̕ǒ̝̗̔̇̕ ̙̍́̄̄̉è̛̛̞̌̌i̜̖̐̈̆̚ȕ̇̈̓̃̓ŝ̛̞̙̉̋m̜̐̂̄̋̂ȯ̈̎̎̅̕d̜̙̓̔̋̑ ̞̗̄̂̂̚t̝̊́̃́̄e̛̘̜̞̓̑m̊̅̏̉̌̕p̛̈̂̇̀̐ỏ̙̘̈̉̔r̘̞̋̍̃̚ ̝̄̀̇̅̇ỉ̛̖̍̓̈n̛̛̝̎̕̕c̛̛̊̅́̐ĭ̗̓̀̍̐d̞̜̋̐̅̚i̟̙̇̄̊̄d̞̊̂̀̇̚ủ̝̉̑̃̕n̜̏̇̄̐̋ť̗̜̞̋̉ ̝̒̓̌̓̚ȕ̖̙̀̚̕t̖̘̎̉̂̌ ̛̝̄̍̌̂l̛̟̝̃̑̋á̛̝̝̔̅b̝̙̜̗̅̒ơ̖̌̒̄̆r̒̇̓̎̈̄e̛̛̖̅̏̇ ̖̗̜̝̃́e̛̛̘̅̔̌ẗ̛̙̗̐̕ ̖̟̇̋̌̈d̞̙̀̉̑̕ŏ̝̂́̐̑l̞̟̗̓̓̀ơ̘̎̃̄̂r̗̗̖̔̆̍ẻ̖̝̞̋̅ ̜̌̇̍̈̊m̈̉̇̄̒̀a̜̞̘̔̅̆g̗̖̈̃̈̉n̙̖̄̈̉̄â̛̝̜̄̃ ̛́̎̕̕̚ā̊́́̆̌l̟̙̞̃̒́i̖̇̎̃̀̋q̟̇̒̆́̊ủ́̌̇̑̚ã̛̘̉̐̚.̛́̏̐̍̊ + U̝̙̎̈̐̆t̜̍̌̀̔̏ ̞̉̍̇̈̃e̟̟̊̄̕̕n̝̜̒̓̆̕i̖̒̌̅̇̚m̞̊̃̔̊̂ ̛̜̊̎̄̂a̘̜̋̒̚̚d̟̊̎̇̂̍ ̜̖̏̑̉̕m̜̒̎̅̄̚i̝̖̓̂̍̕n̙̉̒̑̀̔ỉ̖̝̌̒́m̛̖̘̅̆̎ ̖̉̎̒̌̕v̖̞̀̔́̎e̖̙̗̒̎̉n̛̗̝̎̀̂ȉ̞̗̒̕̚ȧ̟̜̝̅̚m̆̉̐̐̇̈,̏̐̎́̍́ ̜̞̙̘̏̆q̙̖̙̅̓̂ủ̇́̀̔̚í̙̟̟̏̐s̖̝̍̏̂̇ ̛̘̋̈̕̕ń̛̞̜̜̎o̗̜̔̔̈̆s̞̘̘̄̒̋t̛̅̋́̔̈ȓ̓̒́̇̅ủ̜̄̃̒̍d̙̝̘̊̏̚ ̛̟̞̄́̔e̛̗̝̍̃̀x̞̖̃̄̂̅e̖̅̇̐̔̃r̗̞̖̔̎̚c̘̜̖̆̊̏ï̙̝̙̂̕t̖̏́̓̋̂ă̖̄̆̑̒t̜̟̍̉̑̏i̛̞̞̘̒̑ǒ̜̆̅̃̉ṅ̖̜̒̎̚ + u̗̞̓̔̈̏ĺ̟̝́̎̚l̛̜̅̌̎̆a̒̑̆̔̇̃m̜̗̈̊̎̚ċ̘̋̇̂̚ơ̟̖̊́̕ ̖̟̍̉̏̚l̙̔̓̀̅̏ä̞̗̘̙̅ḃ̟̎̄̃̕o̞̎̓̓̓̚r̗̜̊̓̈̒ï̗̜̃̃̅s̀̒̌̂̎̂ ̖̗̗̋̎̐n̝̟̝̘̄̚i̜̒̀̒̐̕s̘̘̄̊̃̀ī̘̜̏̌̕ ̗̖̞̐̈̒ư̙̞̄́̌t̟̘̖̙̊̚ ̌̅̋̆̚̚ä̇̊̇̕̕l̝̞̘̋̔̅i̍̋́̆̑̈q̛̆̐̈̐̚ư̏̆̊́̚î̜̝̑́̊p̗̓̅̑̆̏ ̆́̓̔̋̋e̟̊̋̏̓̚x̗̍̑̊̎̈ ̟̞̆̄̂̍ë̄̎̄̃̅a̛̜̅́̃̈ ̔̋̀̎̐̀c̖̖̍̀̒̂ơ̛̙̖̄̒m̘̔̍̏̆̕ḿ̖̙̝̏̂ȍ̓̋̈̀̕d̆̂̊̅̓̚o̖̔̌̑̚̕ ̙̆́̔̊̒c̖̘̖̀̄̍o̓̄̑̐̓̒ñ̞̒̎̈̚s̞̜̘̈̄̄e̙̊̀̇̌̋q̐̒̓́̔̃ư̗̟̔̔̚å̖̙̞̄̏t̛̙̟̒̇̏.̙̗̓̃̓̎ + D̜̖̆̏̌̌ư̑̃̌̍̕i̝̊̊̊̊̄s̛̙̒́̌̇ ̛̃̔̄̆̌ă̘̔̅̅̀ú̟̟̟̃̃t̟̂̄̈̈̃e̘̅̌̒̂̆ ̖̟̐̉̉̌î̟̟̙̜̇r̛̙̞̗̄̌ú̗̗̃̌̎r̛̙̘̉̊̕e̒̐̔̃̓̋ ̊̊̍̋̑̉d̛̝̙̉̀̓o̘̜̐̐̓̐l̞̋̌̆̍́o̊̊̐̃̃̚ṙ̛̖̘̃̕ ̞̊̀̍̒̕ȉ́̑̐̇̅ǹ̜̗̜̞̏ ̛̜̐̄̄̚r̜̖̈̇̅̋ĕ̗̉̃̔̚p̟̝̀̓̔̆r̜̈̆̇̃̃e̘̔̔̏̎̓h̗̒̉̑̆̚ė̛̘̘̈̐n̘̂̀̒̕̕d̗̅̂̋̅́ê̗̜̜̜̕r̟̋̄̐̅̂i̛̔̌̒̂̕t̛̗̓̎̀̎ ̙̗̀̉̂̚ȉ̟̗̐̓̚n̙̂̍̏̓̉ ̙̘̊̋̍̕v̜̖̀̎̆̐ő̜̆̉̃̎l̑̋̒̉̔̆ư̙̓̓́̚p̝̘̖̎̏̒t̛̘̝̞̂̓ȁ̘̆̔́̊t̖̝̉̒̐̎e̞̟̋̀̅̄ ̆̌̃̀̑̔v̝̘̝̍̀̇ȅ̝̊̄̓̕l̞̝̑̔̂̋ĭ̝̄̅̆̍t̝̜̉̂̈̇ + ē̟̊̇̕̚s̖̘̘̒̄̑s̛̘̀̊̆̇e̛̝̘̒̏̚ ̉̅̑̂̐̎c̛̟̙̎̋̓i̜̇̒̏̆̆l̟̄́̆̊̌l̍̊̋̃̆̌ủ̗̙̒̔̚m̛̘̘̖̅̍ ̖̙̈̎̂̕d̞̟̏̋̈̔ơ̟̝̌̃̄l̗̙̝̂̉̒õ̒̃̄̄̚ŕ̗̏̏̊̍ê̞̝̞̋̈ ̜̔̒̎̃̚e̞̟̞̒̃̄ư̖̏̄̑̃ ̛̗̜̄̓̎f̛̖̞̅̓̃ü̞̏̆̋̕g̜̝̞̑̑̆i̛̘̐̐̅̚à̜̖̌̆̎t̙̙̎̉̂̍ ̋̔̈̎̎̉n̞̓́̔̊̕ư̘̅̋̔̚l̗̍̒̄̀̚l̞̗̘̙̓̍â̘̔̒̎̚ ̖̓̋̉̃̆p̛̛̘̋̌̀ä̙̔́̒̕r̟̟̖̋̐̋ì̗̙̎̓̓ȃ̔̋̑̚̕t̄́̎̓̂̋ư̏̈̂̑̃r̖̓̋̊̚̚.̒̆̑̆̊̎ ̘̜̍̐̂̚E̞̅̐̇́̂x̄́̈̌̉̕ć̘̃̉̃̕è̘̂̑̏̑p̝̘̑̂̌̆t̔̐̅̍̌̂ȇ̞̈̐̚̕ű̝̞̜́̚ŕ̗̝̉̆́ + š̟́̔̏̀ȉ̝̟̝̏̅n̑̆̇̒̆̚t̝̒́̅̋̏ ̗̑̌̋̇̚ơ̙̗̟̆̅c̙̞̙̎̊̎c̘̟̍̔̊̊a̛̒̓̉́̐e̜̘̙̒̅̇ć̝̝̂̇̕ả̓̍̎̂̚t̗̗̗̟̒̃ ̘̒̓̐̇́c̟̞̉̐̓̄ȕ̙̗̅́̏p̛̍̋̈́̅i̖̓̒̍̈̄d̞̃̈̌̆̐a̛̗̝̎̋̉t̞̙̀̊̆̇a̛̙̒̆̉̚t̜̟̘̉̓̚ ̝̘̗̐̇̕n̛̘̑̏̂́ō̑̋̉̏́ň̞̊̆̄̃ ̙̙̙̜̄̏p̒̆̋̋̓̏r̖̖̅̉́̚ơ̜̆̑̈̚i̟̒̀̃̂̌d̛̏̃̍̋̚ë̖̞̙̗̓n̛̘̓̒̅̎t̟̗̙̊̆̚,̘̙̔̊̚̕ ̟̗̘̜̑̔s̜̝̍̀̓̌û̞̙̅̇́n̘̗̝̒̃̎t̗̅̀̅̊̈ ̗̖̅̅̀̄i̛̖̍̅̋̂n̙̝̓̓̎̚ ̞̋̅̋̃̚c̗̒̀̆̌̎ū̞̂̑̌̓ĺ̛̐̍̑́p̝̆̌̎̈̚a̖̙̒̅̈̌ ̝̝̜̂̈̀q̝̖̔̍̒̚ư̔̐̂̎̊ǐ̛̟̖̘̕ + o̖̜̔̋̅̚f̛̊̀̉́̕f̏̉̀̔̃̃i̘̍̎̐̔̎c̙̅̑̂̐̅ȋ̛̜̀̒̚a̋̍̇̏̀̋ ̖̘̒̅̃̒d̗̘̓̈̇̋é̝́̎̒̄š̙̒̊̉̋e̖̓̐̀̍̕r̗̞̂̅̇̄ù̘̇̐̉̀n̐̑̀̄̍̐t̟̀̂̊̄̚ ̟̝̂̍̏́m̜̗̈̂̏̚ő̞̊̑̇̒l̘̑̏́̔̄l̛̛̇̃̋̊i̓̋̒̃̉̌t̛̗̜̏̀̋ ̙̟̒̂̌̐a̙̝̔̆̏̅n̝̙̙̗̆̅i̍̔́̊̃̕m̖̝̟̒̍̚ ̛̃̃̑̌́ǐ̘̉̔̅̚d̝̗̀̌̏̒ ̖̝̓̑̊̚ȇ̞̟̖̌̕š̙̙̈̔̀t̂̉̒̍̄̄ ̝̗̊̋̌̄l̛̞̜̙̘̔å̝̍̂̍̅b̜̆̇̈̉̌ǒ̜̙̎̃̆r̝̀̄̍́̕ư̋̊́̊̕m̜̗̒̐̕̚.̟̘̀̒̌̚]], + '\n')) + + -- tests that we can handle overflow of the buffer + -- for redraw events (4096 bytes) gracefully + screen:expect{grid=[[ + ^L̓̉̑̒̌̚ơ̗̌̒̄̀ŕ̈̈̎̐̕è̇̅̄̄̐m̖̟̟̅̄̚ ̛̓̑̆̇̍i̗̟̞̜̅̐p̗̞̜̉̆̕s̟̜̘̍̑̏ū̟̞̎̃̉ḿ̘̙́́̐ ̖̍̌̇̉̚d̞̄̃̒̉̎ò́̌̌̂̐l̞̀̄̆̌̚ȯ̖̞̋̀̐r̓̇̌̃̃̚ ̗̘̀̏̍́s̜̀̎̎̑̕i̟̗̐̄̄̚t̝̎̆̓̐̒ ̘̇̔̓̊̚ȃ̛̟̗̏̅m̜̟̙̞̈̓é̘̞̟̔̆t̝̂̂̈̑̔,̜̜̖̅̄̍ ̛̗̊̓̆̚c̟̍̆̍̈̔ȯ̖̖̝̑̀n̜̟̎̊̃̚s̟̏̇̎̒̚e̙̐̈̓̌̚c̙̍̈̏̅̕ť̇̄̇̆̓e̛̓̌̈̓̈t̟̍̀̉̆̅u̝̞̎̂̄̚r̘̀̅̈̅̐ ̝̞̓́̇̉ã̏̀̆̅̕d̛̆̐̉̆̋ȉ̞̟̍̃̚p̛̜̊̍̂̓ȋ̏̅̃̋̚ṥ̛̏̃̕č̛̞̝̀̂í̗̘̌́̎n̔̎́̒̂̕ǧ̗̜̋̇̂ ̛̜̔̄̎̃ê̛̔̆̇̕l̘̝̏̐̊̏ĩ̛̍̏̏̄t̟̐́̀̐̎,̙̘̍̆̉̐ ̋̂̏̄̌̅s̙̓̌̈́̇e̛̗̋̒̎̏d̜̗̊̍̊̚ | + ď̘̋̌̌̕ǒ̝̗̔̇̕ ̙̍́̄̄̉è̛̛̞̌̌i̜̖̐̈̆̚ȕ̇̈̓̃̓ŝ̛̞̙̉̋m̜̐̂̄̋̂ȯ̈̎̎̅̕d̜̙̓̔̋̑ ̞̗̄̂̂̚t̝̊́̃́̄e̛̘̜̞̓̑m̊̅̏̉̌̕p̛̈̂̇̀̐ỏ̙̘̈̉̔r̘̞̋̍̃̚ ̝̄̀̇̅̇ỉ̛̖̍̓̈n̛̛̝̎̕̕c̛̛̊̅́̐ĭ̗̓̀̍̐d̞̜̋̐̅̚i̟̙̇̄̊̄d̞̊̂̀̇̚ủ̝̉̑̃̕n̜̏̇̄̐̋ť̗̜̞̋̉ ̝̒̓̌̓̚ȕ̖̙̀̚̕t̖̘̎̉̂̌ ̛̝̄̍̌̂l̛̟̝̃̑̋á̛̝̝̔̅b̝̙̜̗̅̒ơ̖̌̒̄̆r̒̇̓̎̈̄e̛̛̖̅̏̇ ̖̗̜̝̃́e̛̛̘̅̔̌ẗ̛̙̗̐̕ ̖̟̇̋̌̈d̞̙̀̉̑̕ŏ̝̂́̐̑l̞̟̗̓̓̀ơ̘̎̃̄̂r̗̗̖̔̆̍ẻ̖̝̞̋̅ ̜̌̇̍̈̊m̈̉̇̄̒̀a̜̞̘̔̅̆g̗̖̈̃̈̉n̙̖̄̈̉̄â̛̝̜̄̃ ̛́̎̕̕̚ā̊́́̆̌l̟̙̞̃̒́i̖̇̎̃̀̋q̟̇̒̆́̊ủ́̌̇̑̚ã̛̘̉̐̚.̛́̏̐̍̊ | + U̝̙̎̈̐̆t̜̍̌̀̔̏ ̞̉̍̇̈̃e̟̟̊̄̕̕n̝̜̒̓̆̕i̖̒̌̅̇̚m̞̊̃̔̊̂ ̛̜̊̎̄̂a̘̜̋̒̚̚d̟̊̎̇̂̍ ̜̖̏̑̉̕m̜̒̎̅̄̚i̝̖̓̂̍̕n̙̉̒̑̀̔ỉ̖̝̌̒́m̛̖̘̅̆̎ ̖̉̎̒̌̕v̖̞̀̔́̎e̖̙̗̒̎̉n̛̗̝̎̀̂ȉ̞̗̒̕̚ȧ̟̜̝̅̚m̆̉̐̐̇̈,̏̐̎́̍́ ̜̞̙̘̏̆q̙̖̙̅̓̂ủ̇́̀̔̚í̙̟̟̏̐s̖̝̍̏̂̇ ̛̘̋̈̕̕ń̛̞̜̜̎o̗̜̔̔̈̆s̞̘̘̄̒̋t̛̅̋́̔̈ȓ̓̒́̇̅ủ̜̄̃̒̍d̙̝̘̊̏̚ ̛̟̞̄́̔e̛̗̝̍̃̀x̞̖̃̄̂̅e̖̅̇̐̔̃r̗̞̖̔̎̚c̘̜̖̆̊̏ï̙̝̙̂̕t̖̏́̓̋̂ă̖̄̆̑̒t̜̟̍̉̑̏i̛̞̞̘̒̑ǒ̜̆̅̃̉ṅ̖̜̒̎̚ | + u̗̞̓̔̈̏ĺ̟̝́̎̚l̛̜̅̌̎̆a̒̑̆̔̇̃m̜̗̈̊̎̚ċ̘̋̇̂̚ơ̟̖̊́̕ ̖̟̍̉̏̚l̙̔̓̀̅̏ä̞̗̘̙̅ḃ̟̎̄̃̕o̞̎̓̓̓̚r̗̜̊̓̈̒ï̗̜̃̃̅s̀̒̌̂̎̂ ̖̗̗̋̎̐n̝̟̝̘̄̚i̜̒̀̒̐̕s̘̘̄̊̃̀ī̘̜̏̌̕ ̗̖̞̐̈̒ư̙̞̄́̌t̟̘̖̙̊̚ ̌̅̋̆̚̚ä̇̊̇̕̕l̝̞̘̋̔̅i̍̋́̆̑̈q̛̆̐̈̐̚ư̏̆̊́̚î̜̝̑́̊p̗̓̅̑̆̏ ̆́̓̔̋̋e̟̊̋̏̓̚x̗̍̑̊̎̈ ̟̞̆̄̂̍ë̄̎̄̃̅a̛̜̅́̃̈ ̔̋̀̎̐̀c̖̖̍̀̒̂ơ̛̙̖̄̒m̘̔̍̏̆̕ḿ̖̙̝̏̂ȍ̓̋̈̀̕d̆̂̊̅̓̚o̖̔̌̑̚̕ ̙̆́̔̊̒c̖̘̖̀̄̍o̓̄̑̐̓̒ñ̞̒̎̈̚s̞̜̘̈̄̄e̙̊̀̇̌̋q̐̒̓́̔̃ư̗̟̔̔̚å̖̙̞̄̏t̛̙̟̒̇̏.̙̗̓̃̓̎ | + D̜̖̆̏̌̌ư̑̃̌̍̕i̝̊̊̊̊̄s̛̙̒́̌̇ ̛̃̔̄̆̌ă̘̔̅̅̀ú̟̟̟̃̃t̟̂̄̈̈̃e̘̅̌̒̂̆ ̖̟̐̉̉̌î̟̟̙̜̇r̛̙̞̗̄̌ú̗̗̃̌̎r̛̙̘̉̊̕e̒̐̔̃̓̋ ̊̊̍̋̑̉d̛̝̙̉̀̓o̘̜̐̐̓̐l̞̋̌̆̍́o̊̊̐̃̃̚ṙ̛̖̘̃̕ ̞̊̀̍̒̕ȉ́̑̐̇̅ǹ̜̗̜̞̏ ̛̜̐̄̄̚r̜̖̈̇̅̋ĕ̗̉̃̔̚p̟̝̀̓̔̆r̜̈̆̇̃̃e̘̔̔̏̎̓h̗̒̉̑̆̚ė̛̘̘̈̐n̘̂̀̒̕̕d̗̅̂̋̅́ê̗̜̜̜̕r̟̋̄̐̅̂i̛̔̌̒̂̕t̛̗̓̎̀̎ ̙̗̀̉̂̚ȉ̟̗̐̓̚n̙̂̍̏̓̉ ̙̘̊̋̍̕v̜̖̀̎̆̐ő̜̆̉̃̎l̑̋̒̉̔̆ư̙̓̓́̚p̝̘̖̎̏̒t̛̘̝̞̂̓ȁ̘̆̔́̊t̖̝̉̒̐̎e̞̟̋̀̅̄ ̆̌̃̀̑̔v̝̘̝̍̀̇ȅ̝̊̄̓̕l̞̝̑̔̂̋ĭ̝̄̅̆̍t̝̜̉̂̈̇ | + ē̟̊̇̕̚s̖̘̘̒̄̑s̛̘̀̊̆̇e̛̝̘̒̏̚ ̉̅̑̂̐̎c̛̟̙̎̋̓i̜̇̒̏̆̆l̟̄́̆̊̌l̍̊̋̃̆̌ủ̗̙̒̔̚m̛̘̘̖̅̍ ̖̙̈̎̂̕d̞̟̏̋̈̔ơ̟̝̌̃̄l̗̙̝̂̉̒õ̒̃̄̄̚ŕ̗̏̏̊̍ê̞̝̞̋̈ ̜̔̒̎̃̚e̞̟̞̒̃̄ư̖̏̄̑̃ ̛̗̜̄̓̎f̛̖̞̅̓̃ü̞̏̆̋̕g̜̝̞̑̑̆i̛̘̐̐̅̚à̜̖̌̆̎t̙̙̎̉̂̍ ̋̔̈̎̎̉n̞̓́̔̊̕ư̘̅̋̔̚l̗̍̒̄̀̚l̞̗̘̙̓̍â̘̔̒̎̚ ̖̓̋̉̃̆p̛̛̘̋̌̀ä̙̔́̒̕r̟̟̖̋̐̋ì̗̙̎̓̓ȃ̔̋̑̚̕t̄́̎̓̂̋ư̏̈̂̑̃r̖̓̋̊̚̚.̒̆̑̆̊̎ ̘̜̍̐̂̚E̞̅̐̇́̂x̄́̈̌̉̕ć̘̃̉̃̕è̘̂̑̏̑p̝̘̑̂̌̆t̔̐̅̍̌̂ȇ̞̈̐̚̕ű̝̞̜́̚ŕ̗̝̉̆́ | + š̟́̔̏̀ȉ̝̟̝̏̅n̑̆̇̒̆̚t̝̒́̅̋̏ ̗̑̌̋̇̚ơ̙̗̟̆̅c̙̞̙̎̊̎c̘̟̍̔̊̊a̛̒̓̉́̐e̜̘̙̒̅̇ć̝̝̂̇̕ả̓̍̎̂̚t̗̗̗̟̒̃ ̘̒̓̐̇́c̟̞̉̐̓̄ȕ̙̗̅́̏p̛̍̋̈́̅i̖̓̒̍̈̄d̞̃̈̌̆̐a̛̗̝̎̋̉t̞̙̀̊̆̇a̛̙̒̆̉̚t̜̟̘̉̓̚ ̝̘̗̐̇̕n̛̘̑̏̂́ō̑̋̉̏́ň̞̊̆̄̃ ̙̙̙̜̄̏p̒̆̋̋̓̏r̖̖̅̉́̚ơ̜̆̑̈̚i̟̒̀̃̂̌d̛̏̃̍̋̚ë̖̞̙̗̓n̛̘̓̒̅̎t̟̗̙̊̆̚,̘̙̔̊̚̕ ̟̗̘̜̑̔s̜̝̍̀̓̌û̞̙̅̇́n̘̗̝̒̃̎t̗̅̀̅̊̈ ̗̖̅̅̀̄i̛̖̍̅̋̂n̙̝̓̓̎̚ ̞̋̅̋̃̚c̗̒̀̆̌̎ū̞̂̑̌̓ĺ̛̐̍̑́p̝̆̌̎̈̚a̖̙̒̅̈̌ ̝̝̜̂̈̀q̝̖̔̍̒̚ư̔̐̂̎̊ǐ̛̟̖̘̕ | + o̖̜̔̋̅̚f̛̊̀̉́̕f̏̉̀̔̃̃i̘̍̎̐̔̎c̙̅̑̂̐̅ȋ̛̜̀̒̚a̋̍̇̏̀̋ ̖̘̒̅̃̒d̗̘̓̈̇̋é̝́̎̒̄š̙̒̊̉̋e̖̓̐̀̍̕r̗̞̂̅̇̄ù̘̇̐̉̀n̐̑̀̄̍̐t̟̀̂̊̄̚ ̟̝̂̍̏́m̜̗̈̂̏̚ő̞̊̑̇̒l̘̑̏́̔̄l̛̛̇̃̋̊i̓̋̒̃̉̌t̛̗̜̏̀̋ ̙̟̒̂̌̐a̙̝̔̆̏̅n̝̙̙̗̆̅i̍̔́̊̃̕m̖̝̟̒̍̚ ̛̃̃̑̌́ǐ̘̉̔̅̚d̝̗̀̌̏̒ ̖̝̓̑̊̚ȇ̞̟̖̌̕š̙̙̈̔̀t̂̉̒̍̄̄ ̝̗̊̋̌̄l̛̞̜̙̘̔å̝̍̂̍̅b̜̆̇̈̉̌ǒ̜̙̎̃̆r̝̀̄̍́̕ư̋̊́̊̕m̜̗̒̐̕̚.̟̘̀̒̌̚ | + {1:~ }| + | + ]]} + end) end) describe('multibyte rendering: statusline', function() |