diff options
-rw-r--r-- | src/nvim/os/input.c | 38 | ||||
-rw-r--r-- | src/nvim/tui/term_input.inl | 62 | ||||
-rw-r--r-- | src/nvim/tui/tui.c | 155 | ||||
-rw-r--r-- | test/functional/legacy/mapping_spec.lua | 5 | ||||
-rw-r--r-- | test/functional/ui/input_spec.lua | 8 |
5 files changed, 179 insertions, 89 deletions
diff --git a/src/nvim/os/input.c b/src/nvim/os/input.c index 2c2b0fc871..00efa28161 100644 --- a/src/nvim/os/input.c +++ b/src/nvim/os/input.c @@ -148,22 +148,34 @@ size_t input_enqueue(String keys) uint8_t buf[6] = {0}; unsigned int new_size = trans_special((uint8_t **)&ptr, buf, true); - if (!new_size) { - if (*ptr == '<') { - // Invalid key sequence, skip until the next '>' or until *end - do { - ptr++; - } while (ptr < end && *ptr != '>'); + if (new_size) { + new_size = handle_mouse_event(&ptr, buf, new_size); + rbuffer_write(input_buffer, (char *)buf, new_size); + continue; + } + + if (*ptr == '<') { + // Invalid key sequence, skip until the next '>' or until *end + do { ptr++; - continue; - } - // copy the character unmodified - *buf = (uint8_t)*ptr++; - new_size = 1; + } while (ptr < end && *ptr != '>'); + ptr++; + continue; } - new_size = handle_mouse_event(&ptr, buf, new_size); - rbuffer_write(input_buffer, (char *)buf, new_size); + // copy the character, escaping CSI and K_SPECIAL + if ((uint8_t)*ptr == CSI) { + rbuffer_write(input_buffer, (char *)&(uint8_t){K_SPECIAL}, 1); + rbuffer_write(input_buffer, (char *)&(uint8_t){KS_EXTRA}, 1); + rbuffer_write(input_buffer, (char *)&(uint8_t){KE_CSI}, 1); + } else if ((uint8_t)*ptr == K_SPECIAL) { + rbuffer_write(input_buffer, (char *)&(uint8_t){K_SPECIAL}, 1); + rbuffer_write(input_buffer, (char *)&(uint8_t){KS_SPECIAL}, 1); + rbuffer_write(input_buffer, (char *)&(uint8_t){KE_FILLER}, 1); + } else { + rbuffer_write(input_buffer, ptr, 1); + } + ptr++; } size_t rv = (size_t)(ptr - keys.data); diff --git a/src/nvim/tui/term_input.inl b/src/nvim/tui/term_input.inl index 0c0e6c07c9..dba970e8f2 100644 --- a/src/nvim/tui/term_input.inl +++ b/src/nvim/tui/term_input.inl @@ -1,10 +1,12 @@ #include <termkey.h> #include "nvim/ascii.h" +#include "nvim/misc2.h" #include "nvim/os/os.h" #include "nvim/os/input.h" #include "nvim/os/rstream.h" +#define PASTETOGGLE_KEY "<f37>" struct term_input { int in_fd; @@ -113,7 +115,7 @@ static int get_key_code_timeout(void) bool timeout = false; // Check 'timeout' and 'ttimeout' to determine if we should send ESC // after 'ttimeoutlen'. See :help 'ttimeout' for more information - Error err; + Error err = ERROR_INIT; timeout = vim_get_option(cstr_as_string("timeout"), &err).data.boolean; if (!timeout) { timeout = vim_get_option(cstr_as_string("ttimeout"), &err).data.boolean; @@ -164,6 +166,49 @@ static void timer_cb(uv_timer_t *handle) tk_getkeys(handle->data, true); } +static bool handle_bracketed_paste(TermInput *input) +{ + char *ptr = rbuffer_read_ptr(input->read_buffer); + size_t len = rbuffer_pending(input->read_buffer); + if (len > 5 && (!strncmp(ptr, "\x1b[200~", 6) + || !strncmp(ptr, "\x1b[201~", 6))) { + bool enable = ptr[4] == '0'; + // Advance past the sequence + rbuffer_consumed(input->read_buffer, 6); + if (enable) { + // Get the current mode + int state = get_real_state(); + if (state & NORMAL) { + // Enter insert mode + input_enqueue(cstr_as_string("i")); + } else if (state & VISUAL) { + // Remove the selected text and enter insert mode + input_enqueue(cstr_as_string("c")); + } else if (!(state & INSERT)) { + // Don't mess with the paste option + return true; + } + } + input_enqueue(cstr_as_string(PASTETOGGLE_KEY)); + return true; + } + return false; +} + +static bool handle_forced_escape(TermInput *input) +{ + char *ptr = rbuffer_read_ptr(input->read_buffer); + size_t len = rbuffer_pending(input->read_buffer); + if (len > 1 && ptr[0] == ESC && ptr[1] == NUL) { + // skip the ESC and NUL and push one <esc> to the input buffer + termkey_push_bytes(input->tk, ptr, 1); + rbuffer_consumed(input->read_buffer, 2); + tk_getkeys(input, true); + return true; + } + return false; +} + static void read_cb(RStream *rstream, void *rstream_data, bool eof) { if (eof) { @@ -174,15 +219,11 @@ static void read_cb(RStream *rstream, void *rstream_data, bool eof) TermInput *input = rstream_data; do { - char *ptr = rbuffer_read_ptr(input->read_buffer); - size_t len = rbuffer_pending(input->read_buffer); - if (len > 1 && ptr[0] == ESC && ptr[1] == NUL) { - // skip the ESC and NUL and push one <esc> to the input buffer - termkey_push_bytes(input->tk, ptr, 1); - rbuffer_consumed(input->read_buffer, 2); - tk_getkeys(input, true); + if (handle_bracketed_paste(input) || handle_forced_escape(input)) { continue; } + char *ptr = rbuffer_read_ptr(input->read_buffer); + size_t len = rbuffer_pending(input->read_buffer); // Find the next 'esc' and push everything up to it(excluding) size_t i; for (i = ptr[0] == ESC ? 1 : 0; i < len; i++) { @@ -228,6 +269,11 @@ static TermInput *term_input_new(void) // initialize a timer handle for handling ESC with libtermkey uv_timer_init(uv_default_loop(), &rv->timer_handle); rv->timer_handle.data = rv; + // Set the pastetoggle option to a special key that will be sent when + // \e[20{0,1}~/ are received + Error err = ERROR_INIT; + vim_set_option(cstr_as_string("pastetoggle"), + STRING_OBJ(cstr_as_string(PASTETOGGLE_KEY)), &err); return rv; } diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 720970c424..57119b9030 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -50,6 +50,8 @@ typedef struct { Cell **screen; struct { size_t enable_mouse, disable_mouse; + size_t enable_bracketed_paste, disable_bracketed_paste; + size_t enter_insert_mode, exit_insert_mode; } unibi_ext; } TUIData; @@ -101,8 +103,10 @@ void tui_start(void) } fix_terminfo(data); // Enter alternate screen and clear - unibi_out(ui, unibi_enter_ca_mode, NULL); - unibi_out(ui, unibi_clear_screen, NULL); + unibi_out(ui, unibi_enter_ca_mode); + unibi_out(ui, unibi_clear_screen); + // Enable bracketed paste + unibi_out(ui, (int)data->unibi_ext.enable_bracketed_paste); // setup output handle in a separate event loop(we wanna do synchronous // write to the tty) @@ -159,9 +163,11 @@ static void tui_stop(UI *ui) // Destroy output stuff tui_normal_mode(ui); tui_mouse_off(ui); - unibi_out(ui, unibi_exit_attribute_mode, NULL); - unibi_out(ui, unibi_cursor_normal, NULL); - unibi_out(ui, unibi_exit_ca_mode, NULL); + unibi_out(ui, unibi_exit_attribute_mode); + unibi_out(ui, unibi_cursor_normal); + unibi_out(ui, unibi_exit_ca_mode); + // Disable bracketed paste + unibi_out(ui, (int)data->unibi_ext.disable_bracketed_paste); flush_buf(ui); uv_close((uv_handle_t *)&data->output_handle, NULL); uv_run(data->write_loop, UV_RUN_DEFAULT); @@ -177,8 +183,8 @@ static void tui_stop(UI *ui) pmap_free(cstr_t)(data->option_cache); destroy_screen(data); free(data); - free(ui); ui_detach(ui); + free(ui); } static void try_resize(Event ev) @@ -208,39 +214,41 @@ static bool attrs_differ(HlAttrs a1, HlAttrs a2) static void update_attrs(UI *ui, HlAttrs attrs) { TUIData *data = ui->data; - unibi_out(ui, unibi_exit_attribute_mode, NULL); + + if (!attrs_differ(attrs, data->print_attrs)) { + return; + } + + data->print_attrs = attrs; + unibi_out(ui, unibi_exit_attribute_mode); data->params[0].i = attrs.foreground != -1 ? attrs.foreground : data->fg; if (data->params[0].i != -1) { - unibi_out(ui, unibi_set_a_foreground, NULL); + unibi_out(ui, unibi_set_a_foreground); } data->params[0].i = attrs.background != -1 ? attrs.background : data->bg; if (data->params[0].i != -1) { - unibi_out(ui, unibi_set_a_background, NULL); + unibi_out(ui, unibi_set_a_background); } if (attrs.bold) { - unibi_out(ui, unibi_enter_bold_mode, NULL); + unibi_out(ui, unibi_enter_bold_mode); } if (attrs.italic) { - unibi_out(ui, unibi_enter_italics_mode, NULL); + unibi_out(ui, unibi_enter_italics_mode); } if (attrs.underline) { - unibi_out(ui, unibi_enter_underline_mode, NULL); + unibi_out(ui, unibi_enter_underline_mode); } if (attrs.reverse) { - unibi_out(ui, unibi_enter_reverse_mode, NULL); + unibi_out(ui, unibi_enter_reverse_mode); } } static void print_cell(UI *ui, Cell *ptr) { - TUIData *data = ui->data; - if (attrs_differ(ptr->attrs, data->print_attrs)) { - update_attrs(ui, ptr->attrs); - data->print_attrs = ptr->attrs; - } + update_attrs(ui, ptr->attrs); out(ui, ptr->data); } @@ -251,6 +259,7 @@ static void clear_region(UI *ui, int top, int bot, int left, int right, HlAttrs clear_attrs = EMPTY_ATTRS; clear_attrs.foreground = data->fg; clear_attrs.background = data->bg; + update_attrs(ui, clear_attrs); bool cleared = false; if (refresh && data->bg == -1 && right == ui->width -1) { @@ -259,10 +268,10 @@ static void clear_region(UI *ui, int top, int bot, int left, int right, if (left == 0) { if (bot == ui->height - 1) { if (top == 0) { - unibi_out(ui, unibi_clear_screen, NULL); + unibi_out(ui, unibi_clear_screen); } else { unibi_goto(ui, top, 0); - unibi_out(ui, unibi_clr_eos, NULL); + unibi_out(ui, unibi_clr_eos); } cleared = true; } @@ -272,7 +281,7 @@ static void clear_region(UI *ui, int top, int bot, int left, int right, // iterate through each line and clear with clr_eol for (int row = top; row <= bot; ++row) { unibi_goto(ui, row, left); - unibi_out(ui, unibi_clr_eol, NULL); + unibi_out(ui, unibi_clr_eol); } cleared = true; } @@ -334,34 +343,36 @@ static void tui_cursor_goto(UI *ui, int row, int col) static void tui_cursor_on(UI *ui) { - unibi_out(ui, unibi_cursor_normal, NULL); + unibi_out(ui, unibi_cursor_normal); } static void tui_cursor_off(UI *ui) { - unibi_out(ui, unibi_cursor_invisible, NULL); + unibi_out(ui, unibi_cursor_invisible); } static void tui_mouse_on(UI *ui) { TUIData *data = ui->data; - unibi_out(ui, (int)data->unibi_ext.enable_mouse, NULL); + unibi_out(ui, (int)data->unibi_ext.enable_mouse); } static void tui_mouse_off(UI *ui) { TUIData *data = ui->data; - unibi_out(ui, (int)data->unibi_ext.disable_mouse, NULL); + unibi_out(ui, (int)data->unibi_ext.disable_mouse); } static void tui_insert_mode(UI *ui) { - unibi_out(ui, -1, "t_SI"); + TUIData *data = ui->data; + unibi_out(ui, (int)data->unibi_ext.enter_insert_mode); } static void tui_normal_mode(UI *ui) { - unibi_out(ui, -1, "t_EI"); + TUIData *data = ui->data; + unibi_out(ui, (int)data->unibi_ext.exit_insert_mode); } static void tui_set_scroll_region(UI *ui, int top, int bot, int left, @@ -391,7 +402,7 @@ static void tui_scroll(UI *ui, int count) // Change terminal scroll region and move cursor to the top data->params[0].i = top; data->params[1].i = bot; - unibi_out(ui, unibi_change_scroll_region, NULL); + unibi_out(ui, unibi_change_scroll_region); unibi_goto(ui, top, left); } @@ -404,10 +415,10 @@ static void tui_scroll(UI *ui, int count) step = 1; if (data->can_use_terminal_scroll) { if (count == 1) { - unibi_out(ui, unibi_delete_line, NULL); + unibi_out(ui, unibi_delete_line); } else { data->params[0].i = count; - unibi_out(ui, unibi_parm_delete_line, NULL); + unibi_out(ui, unibi_parm_delete_line); } } @@ -417,10 +428,10 @@ static void tui_scroll(UI *ui, int count) step = -1; if (data->can_use_terminal_scroll) { if (count == -1) { - unibi_out(ui, unibi_insert_line, NULL); + unibi_out(ui, unibi_insert_line); } else { data->params[0].i = -count; - unibi_out(ui, unibi_parm_insert_line, NULL); + unibi_out(ui, unibi_parm_insert_line); } } } @@ -429,7 +440,7 @@ static void tui_scroll(UI *ui, int count) // Restore terminal scroll region and cursor data->params[0].i = 0; data->params[1].i = ui->height - 1; - unibi_out(ui, unibi_change_scroll_region, NULL); + unibi_out(ui, unibi_change_scroll_region); unibi_goto(ui, data->row, data->col); } @@ -480,12 +491,12 @@ static void tui_put(UI *ui, uint8_t *text, size_t size) static void tui_bell(UI *ui) { - unibi_out(ui, unibi_bell, NULL); + unibi_out(ui, unibi_bell); } static void tui_visual_bell(UI *ui) { - unibi_out(ui, unibi_flash_screen, NULL); + unibi_out(ui, unibi_flash_screen); } static void tui_update_fg(UI *ui, int fg) @@ -527,9 +538,9 @@ static void tui_set_title(UI *ui, char *title) && unibi_get_str(data->ut, unibi_from_status_line))) { return; } - unibi_out(ui, unibi_to_status_line, NULL); + unibi_out(ui, unibi_to_status_line); out(ui, title); - unibi_out(ui, unibi_from_status_line, NULL); + unibi_out(ui, unibi_from_status_line); } static void tui_set_icon(UI *ui, char *icon) @@ -613,23 +624,19 @@ static void unibi_goto(UI *ui, int row, int col) TUIData *data = ui->data; data->params[0].i = row; data->params[1].i = col; - unibi_out(ui, unibi_cursor_address, NULL); + unibi_out(ui, unibi_cursor_address); } -static void unibi_out(UI *ui, int unibi_index, char *nvim_override) +static void unibi_out(UI *ui, int unibi_index) { TUIData *data = ui->data; const char *str = NULL; - if (nvim_override) { - str = get_term_option(ui, nvim_override); - } else if (unibi_index >= 0) { - if (unibi_index < unibi_string_begin_) { - str = unibi_get_ext_str(data->ut, (unsigned)unibi_index); - } else { - str = unibi_get_str(data->ut, (unsigned)unibi_index); - } + if (unibi_index < unibi_string_begin_) { + str = unibi_get_ext_str(data->ut, (unsigned)unibi_index); + } else { + str = unibi_get_str(data->ut, (unsigned)unibi_index); } if (str) { @@ -662,12 +669,17 @@ static void fix_terminfo(TUIData *data) goto end; } + bool inside_tmux = os_getenv("TMUX") != NULL; + #define STARTS_WITH(str, prefix) (!memcmp(str, prefix, sizeof(prefix) - 1)) if (STARTS_WITH(term, "rxvt")) { unibi_set_if_empty(ut, unibi_exit_attribute_mode, "\x1b[m\x1b(B"); unibi_set_if_empty(ut, unibi_flash_screen, "\x1b[?5h$<20/>\x1b[?5l"); unibi_set_if_empty(ut, unibi_enter_italics_mode, "\x1b[3m"); + unibi_set_if_empty(ut, unibi_to_status_line, "\x1b]2"); + } else if (STARTS_WITH(term, "xterm")) { + unibi_set_if_empty(ut, unibi_to_status_line, "\x1b]0;"); } else if (STARTS_WITH(term, "screen")) { unibi_set_if_empty(ut, unibi_to_status_line, "\x1b_"); unibi_set_if_empty(ut, unibi_from_status_line, "\x1b\\"); @@ -680,17 +692,45 @@ static void fix_terminfo(TUIData *data) unibi_set_if_empty(ut, unibi_exit_attribute_mode, "\x1b(B\x1b[m"); unibi_set_if_empty(ut, unibi_change_scroll_region, "\x1b[%i%p1%d;%p2%dr"); unibi_set_if_empty(ut, unibi_clear_screen, "\x1b[H\x1b[2J"); - unibi_set_if_empty(ut, unibi_to_status_line, "\x1b]2"); unibi_set_if_empty(ut, unibi_from_status_line, "\x07"); } + if (STARTS_WITH(term, "xterm") || STARTS_WITH(term, "rxvt") || inside_tmux) { + data->unibi_ext.enable_bracketed_paste = unibi_add_ext_str(ut, NULL, + "\x1b[?2004h"); + data->unibi_ext.disable_bracketed_paste = unibi_add_ext_str(ut, NULL, + "\x1b[?2004l"); + } + + if (os_getenv("NVIM_TUI_ENABLE_CURSOR_SHAPE") == NULL) { + goto end; + } + +#define TMUX_WRAP(seq) (inside_tmux ? "\x1bPtmux;\x1b" seq "\x1b\\" : seq) + // Support changing cursor shape on some popular terminals. + const char *term_prog = os_getenv("TERM_PROGRAM"); + + if ((term_prog && !strcmp(term_prog, "iTerm.app")) + || os_getenv("ITERM_SESSION_ID") != NULL) { + // iterm + data->unibi_ext.enter_insert_mode = unibi_add_ext_str(ut, NULL, + TMUX_WRAP("\x1b]50;CursorShape=1;BlinkingCursorEnabled=1\x07")); + data->unibi_ext.exit_insert_mode = unibi_add_ext_str(ut, NULL, + TMUX_WRAP("\x1b]50;CursorShape=0;BlinkingCursorEnabled=0\x07")); + } else { + // xterm-like sequences for blinking bar and solid block + data->unibi_ext.enter_insert_mode = unibi_add_ext_str(ut, NULL, + TMUX_WRAP("\x1b[5 q")); + data->unibi_ext.exit_insert_mode = unibi_add_ext_str(ut, NULL, + TMUX_WRAP("\x1b[2 q")); + } + end: // Fill some empty slots with common terminal strings data->unibi_ext.enable_mouse = unibi_add_ext_str(ut, NULL, "\x1b[?1002h\x1b[?1006h"); data->unibi_ext.disable_mouse = unibi_add_ext_str(ut, NULL, "\x1b[?1002l\x1b[?1006l"); - unibi_set_if_empty(ut, unibi_cursor_address, "\x1b[%i%p1%d;%p2%dH"); unibi_set_if_empty(ut, unibi_exit_attribute_mode, "\x1b[0;10m"); unibi_set_if_empty(ut, unibi_set_a_foreground, @@ -724,23 +764,6 @@ static void flush_buf(UI *ui) data->bufpos = 0; } -static char *get_term_option(UI *ui, char *option) -{ - TUIData *data = ui->data; - - char *rv = pmap_get(cstr_t)(data->option_cache, option); - if (!rv) { - Error err; - Object val = vim_get_option(cstr_as_string(option), &err); - if (val.type == kObjectTypeString) { - rv = val.data.string.data; - pmap_put(cstr_t)(data->option_cache, option, rv); - } - } - - return rv; -} - static void destroy_screen(TUIData *data) { if (data->screen) { diff --git a/test/functional/legacy/mapping_spec.lua b/test/functional/legacy/mapping_spec.lua index 1451b268c5..c387d7484c 100644 --- a/test/functional/legacy/mapping_spec.lua +++ b/test/functional/legacy/mapping_spec.lua @@ -16,7 +16,7 @@ describe('mapping', function() -- Abbreviations with р (0x80) should work. execute('inoreab чкпр vim') - feed('GAчкпр <cr><esc>') + feed('GAчкпр <esc>') -- langmap should not get remapped in insert mode. execute('inoremap { FAIL_ilangmap') @@ -27,10 +27,11 @@ describe('mapping', function() execute('inoremap <expr> { "FAIL_iexplangmap"') feed('o+<esc>') + -- Assert buffer contents. expect([[ test starts here: - vim + vim + +]]) end) diff --git a/test/functional/ui/input_spec.lua b/test/functional/ui/input_spec.lua index 60a49c4ed7..81af908045 100644 --- a/test/functional/ui/input_spec.lua +++ b/test/functional/ui/input_spec.lua @@ -1,6 +1,7 @@ local helpers = require('test.functional.helpers') local clear, execute, nvim = helpers.clear, helpers.execute, helpers.nvim local feed, next_message, eq = helpers.feed, helpers.next_message, helpers.eq +local expect = helpers.expect describe('mappings', function() local cid @@ -38,3 +39,10 @@ describe('mappings', function() check_mapping('<a-s-c-up>', '<c-s-a-up>') end) end) + +describe('input utf sequences that contain CSI/K_SPECIAL', function() + it('ok', function() + feed('i…<esc>') + expect('…') + end) +end) |