From cb9ae4e373af5d436499531698cec96ffc9bc39d Mon Sep 17 00:00:00 2001 From: Thiago de Arruda Date: Tue, 25 Aug 2015 09:04:13 -0300 Subject: tui: move term_input.inl into the input.c module --- src/nvim/tui/input.c | 323 +++++++++++++++++++++++++++++++++++++++++++ src/nvim/tui/input.h | 22 +++ src/nvim/tui/term_input.inl | 325 -------------------------------------------- src/nvim/tui/tui.c | 7 +- 4 files changed, 348 insertions(+), 329 deletions(-) create mode 100644 src/nvim/tui/input.c create mode 100644 src/nvim/tui/input.h delete mode 100644 src/nvim/tui/term_input.inl diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c new file mode 100644 index 0000000000..dd62df964b --- /dev/null +++ b/src/nvim/tui/input.c @@ -0,0 +1,323 @@ + +#include "nvim/tui/input.h" +#include "nvim/vim.h" +#include "nvim/api/vim.h" +#include "nvim/api/private/helpers.h" +#include "nvim/ascii.h" +#include "nvim/misc2.h" +#include "nvim/os/os.h" +#include "nvim/os/input.h" +#include "nvim/event/rstream.h" + +#define PASTETOGGLE_KEY "" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "tui/input.c.generated.h" +#endif + +TermInput *term_input_new(void) +{ + TermInput *rv = xmalloc(sizeof(TermInput)); + rv->paste_enabled = false; + rv->in_fd = 0; + + const char *term = os_getenv("TERM"); + if (!term) { + term = ""; // termkey_new_abstract assumes non-null (#2745) + } + rv->tk = termkey_new_abstract(term, 0); + int curflags = termkey_get_canonflags(rv->tk); + termkey_set_canonflags(rv->tk, curflags | TERMKEY_CANON_DELBS); + // setup input handle + rstream_init_fd(&loop, &rv->read_stream, rv->in_fd, 0xfff, rv); + rstream_start(&rv->read_stream, read_cb); + // initialize a timer handle for handling ESC with libtermkey + time_watcher_init(&loop, &rv->timer_handle, 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; +} + +void term_input_destroy(TermInput *input) +{ + time_watcher_stop(&input->timer_handle); + time_watcher_close(&input->timer_handle, NULL); + rstream_stop(&input->read_stream); + stream_close(&input->read_stream, NULL); + termkey_destroy(input->tk); + // Run once to remove references to input/timer handles + loop_poll_events(&loop, 0); + xfree(input); +} + +void term_input_set_encoding(TermInput *input, char* enc) +{ + int enc_flag = strcmp(enc, "utf-8") == 0 ? TERMKEY_FLAG_UTF8 + : TERMKEY_FLAG_RAW; + termkey_set_flags(input->tk, enc_flag); +} + +static void forward_simple_utf8(TermKeyKey *key) +{ + size_t len = 0; + char buf[64]; + char *ptr = key->utf8; + + while (*ptr) { + if (*ptr == '<') { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, ""); + } else { + buf[len++] = *ptr; + } + ptr++; + } + + buf[len] = 0; + input_enqueue((String){.data = buf, .size = len}); +} + +static void forward_modified_utf8(TermKey *tk, TermKeyKey *key) +{ + size_t len; + char buf[64]; + + if (key->type == TERMKEY_TYPE_KEYSYM + && key->code.sym == TERMKEY_SYM_ESCAPE) { + len = (size_t)snprintf(buf, sizeof(buf), ""); + } else { + len = termkey_strfkey(tk, buf, sizeof(buf), key, TERMKEY_FORMAT_VIM); + } + + input_enqueue((String){.data = buf, .size = len}); +} + +static void forward_mouse_event(TermKey *tk, TermKeyKey *key) +{ + char buf[64]; + size_t len = 0; + int button, row, col; + TermKeyMouseEvent ev; + termkey_interpret_mouse(tk, key, &ev, &button, &row, &col); + + if (ev != TERMKEY_MOUSE_PRESS && ev != TERMKEY_MOUSE_DRAG) { + return; + } + + row--; col--; // Termkey uses 1-based coordinates + buf[len++] = '<'; + + if (key->modifiers & TERMKEY_KEYMOD_SHIFT) { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "S-"); + } + + if (key->modifiers & TERMKEY_KEYMOD_CTRL) { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "C-"); + } + + if (key->modifiers & TERMKEY_KEYMOD_ALT) { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "A-"); + } + + if (button == 1) { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Left"); + } else if (button == 2) { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Middle"); + } else if (button == 3) { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Right"); + } + + if (ev == TERMKEY_MOUSE_PRESS) { + if (button == 4) { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "ScrollWheelUp"); + } else if (button == 5) { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "ScrollWheelDown"); + } else { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Mouse"); + } + } else if (ev == TERMKEY_MOUSE_DRAG) { + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Drag"); + } + + len += (size_t)snprintf(buf + len, sizeof(buf) - len, "><%d,%d>", col, row); + input_enqueue((String){.data = buf, .size = len}); +} + +static TermKeyResult tk_getkey(TermKey *tk, TermKeyKey *key, bool force) +{ + return force ? termkey_getkey_force(tk, key) : termkey_getkey(tk, key); +} + +static void timer_cb(TimeWatcher *watcher, void *data); + +static int get_key_code_timeout(void) +{ + Integer ms = -1; + // Check 'ttimeout' to determine if we should send ESC after 'ttimeoutlen'. + // See :help 'ttimeout' for more information + Error err = ERROR_INIT; + if (vim_get_option(cstr_as_string("ttimeout"), &err).data.boolean) { + ms = vim_get_option(cstr_as_string("ttimeoutlen"), &err).data.integer; + } + + return (int)ms; +} + +static void tk_getkeys(TermInput *input, bool force) +{ + TermKeyKey key; + TermKeyResult result; + + while ((result = tk_getkey(input->tk, &key, force)) == TERMKEY_RES_KEY) { + if (key.type == TERMKEY_TYPE_UNICODE && !key.modifiers) { + forward_simple_utf8(&key); + } else if (key.type == TERMKEY_TYPE_UNICODE || + key.type == TERMKEY_TYPE_FUNCTION || + key.type == TERMKEY_TYPE_KEYSYM) { + forward_modified_utf8(input->tk, &key); + } else if (key.type == TERMKEY_TYPE_MOUSE) { + forward_mouse_event(input->tk, &key); + } + } + + if (result != TERMKEY_RES_AGAIN) { + return; + } + + int ms = get_key_code_timeout(); + + if (ms > 0) { + // Stop the current timer if already running + time_watcher_stop(&input->timer_handle); + time_watcher_start(&input->timer_handle, timer_cb, (uint32_t)ms, 0); + } else { + tk_getkeys(input, true); + } +} + +static void timer_cb(TimeWatcher *watcher, void *data) +{ + tk_getkeys(data, true); +} + +static bool handle_bracketed_paste(TermInput *input) +{ + if (rbuffer_size(input->read_stream.buffer) > 5 && + (!rbuffer_cmp(input->read_stream.buffer, "\x1b[200~", 6) || + !rbuffer_cmp(input->read_stream.buffer, "\x1b[201~", 6))) { + bool enable = *rbuffer_get(input->read_stream.buffer, 4) == '0'; + // Advance past the sequence + rbuffer_consumed(input->read_stream.buffer, 6); + if (input->paste_enabled == enable) { + return true; + } + 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)); + input->paste_enabled = enable; + return true; + } + return false; +} + +static bool handle_forced_escape(TermInput *input) +{ + if (rbuffer_size(input->read_stream.buffer) > 1 + && !rbuffer_cmp(input->read_stream.buffer, "\x1b\x00", 2)) { + // skip the ESC and NUL and push one to the input buffer + size_t rcnt; + termkey_push_bytes(input->tk, rbuffer_read_ptr(input->read_stream.buffer, + &rcnt), 1); + rbuffer_consumed(input->read_stream.buffer, 2); + tk_getkeys(input, true); + return true; + } + return false; +} + +static void restart_reading(void **argv); + +static void read_cb(Stream *stream, RBuffer *buf, size_t c, void *data, + bool eof) +{ + TermInput *input = data; + + if (eof) { + if (input->in_fd == 0 && !os_isatty(0) && os_isatty(2)) { + // Started reading from stdin which is not a pty but failed. Switch to + // stderr since it is a pty. + // + // This is how we support commands like: + // + // echo q | nvim -es + // + // and + // + // ls *.md | xargs nvim + input->in_fd = 2; + stream_close(&input->read_stream, NULL); + queue_put(loop.fast_events, restart_reading, 1, input); + } else { + input_done(); + } + return; + } + + do { + if (handle_bracketed_paste(input) || handle_forced_escape(input)) { + continue; + } + + // Find the next 'esc' and push everything up to it(excluding). This is done + // so the `handle_bracketed_paste`/`handle_forced_escape` calls above work + // as expected. + size_t count = 0; + RBUFFER_EACH(input->read_stream.buffer, c, i) { + count = i + 1; + if (c == '\x1b' && count > 1) { + count--; + break; + } + } + + RBUFFER_UNTIL_EMPTY(input->read_stream.buffer, ptr, len) { + size_t consumed = termkey_push_bytes(input->tk, ptr, MIN(count, len)); + // termkey_push_bytes can return (size_t)-1, so it is possible that + // `consumed > input->read_stream.buffer->size`, but since tk_getkeys is + // called soon, it shouldn't happen + assert(consumed <= input->read_stream.buffer->size); + rbuffer_consumed(input->read_stream.buffer, consumed); + // Need to process the keys now since there's no guarantee "count" will + // fit into libtermkey's input buffer. + tk_getkeys(input, false); + if (!(count -= consumed)) { + break; + } + } + } while (rbuffer_size(input->read_stream.buffer)); + + // Make sure the next input escape sequence fits into the ring buffer + // without wrap around, otherwise it could be misinterpreted. + rbuffer_reset(input->read_stream.buffer); +} + +static void restart_reading(void **argv) +{ + TermInput *input = argv[0]; + rstream_init_fd(&loop, &input->read_stream, input->in_fd, 0xfff, input); + rstream_start(&input->read_stream, read_cb); +} diff --git a/src/nvim/tui/input.h b/src/nvim/tui/input.h new file mode 100644 index 0000000000..4886f14001 --- /dev/null +++ b/src/nvim/tui/input.h @@ -0,0 +1,22 @@ +#ifndef NVIM_TUI_INPUT_H +#define NVIM_TUI_INPUT_H + +#include + +#include +#include "nvim/event/stream.h" +#include "nvim/event/time.h" + +typedef struct term_input { + int in_fd; + bool paste_enabled; + TermKey *tk; + TimeWatcher timer_handle; + Stream read_stream; +} TermInput; + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "tui/input.h.generated.h" +#endif + +#endif // NVIM_TUI_INPUT_H diff --git a/src/nvim/tui/term_input.inl b/src/nvim/tui/term_input.inl deleted file mode 100644 index c396557160..0000000000 --- a/src/nvim/tui/term_input.inl +++ /dev/null @@ -1,325 +0,0 @@ -#include - -#include "nvim/ascii.h" -#include "nvim/misc2.h" -#include "nvim/os/os.h" -#include "nvim/os/input.h" -#include "nvim/event/rstream.h" -#include "nvim/event/time.h" - -#define PASTETOGGLE_KEY "" - -struct term_input { - int in_fd; - bool paste_enabled; - TermKey *tk; - TimeWatcher timer_handle; - Stream read_stream; -}; - -static void forward_simple_utf8(TermKeyKey *key) -{ - size_t len = 0; - char buf[64]; - char *ptr = key->utf8; - - while (*ptr) { - if (*ptr == '<') { - len += (size_t)snprintf(buf + len, sizeof(buf) - len, ""); - } else { - buf[len++] = *ptr; - } - ptr++; - } - - buf[len] = 0; - input_enqueue((String){.data = buf, .size = len}); -} - -static void forward_modified_utf8(TermKey *tk, TermKeyKey *key) -{ - size_t len; - char buf[64]; - - if (key->type == TERMKEY_TYPE_KEYSYM - && key->code.sym == TERMKEY_SYM_ESCAPE) { - len = (size_t)snprintf(buf, sizeof(buf), ""); - } else { - len = termkey_strfkey(tk, buf, sizeof(buf), key, TERMKEY_FORMAT_VIM); - } - - input_enqueue((String){.data = buf, .size = len}); -} - -static void forward_mouse_event(TermKey *tk, TermKeyKey *key) -{ - char buf[64]; - size_t len = 0; - int button, row, col; - TermKeyMouseEvent ev; - termkey_interpret_mouse(tk, key, &ev, &button, &row, &col); - - if (ev != TERMKEY_MOUSE_PRESS && ev != TERMKEY_MOUSE_DRAG) { - return; - } - - row--; col--; // Termkey uses 1-based coordinates - buf[len++] = '<'; - - if (key->modifiers & TERMKEY_KEYMOD_SHIFT) { - len += (size_t)snprintf(buf + len, sizeof(buf) - len, "S-"); - } - - if (key->modifiers & TERMKEY_KEYMOD_CTRL) { - len += (size_t)snprintf(buf + len, sizeof(buf) - len, "C-"); - } - - if (key->modifiers & TERMKEY_KEYMOD_ALT) { - len += (size_t)snprintf(buf + len, sizeof(buf) - len, "A-"); - } - - if (button == 1) { - len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Left"); - } else if (button == 2) { - len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Middle"); - } else if (button == 3) { - len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Right"); - } - - if (ev == TERMKEY_MOUSE_PRESS) { - if (button == 4) { - len += (size_t)snprintf(buf + len, sizeof(buf) - len, "ScrollWheelUp"); - } else if (button == 5) { - len += (size_t)snprintf(buf + len, sizeof(buf) - len, "ScrollWheelDown"); - } else { - len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Mouse"); - } - } else if (ev == TERMKEY_MOUSE_DRAG) { - len += (size_t)snprintf(buf + len, sizeof(buf) - len, "Drag"); - } - - len += (size_t)snprintf(buf + len, sizeof(buf) - len, "><%d,%d>", col, row); - input_enqueue((String){.data = buf, .size = len}); -} - -static TermKeyResult tk_getkey(TermKey *tk, TermKeyKey *key, bool force) -{ - return force ? termkey_getkey_force(tk, key) : termkey_getkey(tk, key); -} - -static void timer_cb(TimeWatcher *watcher, void *data); - -static int get_key_code_timeout(void) -{ - Integer ms = -1; - // Check 'ttimeout' to determine if we should send ESC after 'ttimeoutlen'. - // See :help 'ttimeout' for more information - Error err = ERROR_INIT; - if (vim_get_option(cstr_as_string("ttimeout"), &err).data.boolean) { - ms = vim_get_option(cstr_as_string("ttimeoutlen"), &err).data.integer; - } - - return (int)ms; -} - -static void tk_getkeys(TermInput *input, bool force) -{ - TermKeyKey key; - TermKeyResult result; - - while ((result = tk_getkey(input->tk, &key, force)) == TERMKEY_RES_KEY) { - if (key.type == TERMKEY_TYPE_UNICODE && !key.modifiers) { - forward_simple_utf8(&key); - } else if (key.type == TERMKEY_TYPE_UNICODE || - key.type == TERMKEY_TYPE_FUNCTION || - key.type == TERMKEY_TYPE_KEYSYM) { - forward_modified_utf8(input->tk, &key); - } else if (key.type == TERMKEY_TYPE_MOUSE) { - forward_mouse_event(input->tk, &key); - } - } - - if (result != TERMKEY_RES_AGAIN) { - return; - } - - int ms = get_key_code_timeout(); - - if (ms > 0) { - // Stop the current timer if already running - time_watcher_stop(&input->timer_handle); - time_watcher_start(&input->timer_handle, timer_cb, (uint32_t)ms, 0); - } else { - tk_getkeys(input, true); - } -} - -static void timer_cb(TimeWatcher *watcher, void *data) -{ - tk_getkeys(data, true); -} - -static bool handle_bracketed_paste(TermInput *input) -{ - if (rbuffer_size(input->read_stream.buffer) > 5 && - (!rbuffer_cmp(input->read_stream.buffer, "\x1b[200~", 6) || - !rbuffer_cmp(input->read_stream.buffer, "\x1b[201~", 6))) { - bool enable = *rbuffer_get(input->read_stream.buffer, 4) == '0'; - // Advance past the sequence - rbuffer_consumed(input->read_stream.buffer, 6); - if (input->paste_enabled == enable) { - return true; - } - 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)); - input->paste_enabled = enable; - return true; - } - return false; -} - -static bool handle_forced_escape(TermInput *input) -{ - if (rbuffer_size(input->read_stream.buffer) > 1 - && !rbuffer_cmp(input->read_stream.buffer, "\x1b\x00", 2)) { - // skip the ESC and NUL and push one to the input buffer - size_t rcnt; - termkey_push_bytes(input->tk, rbuffer_read_ptr(input->read_stream.buffer, - &rcnt), 1); - rbuffer_consumed(input->read_stream.buffer, 2); - tk_getkeys(input, true); - return true; - } - return false; -} - -static void restart_reading(void **argv); - -static void read_cb(Stream *stream, RBuffer *buf, size_t c, void *data, - bool eof) -{ - TermInput *input = data; - - if (eof) { - if (input->in_fd == 0 && !os_isatty(0) && os_isatty(2)) { - // Started reading from stdin which is not a pty but failed. Switch to - // stderr since it is a pty. - // - // This is how we support commands like: - // - // echo q | nvim -es - // - // and - // - // ls *.md | xargs nvim - input->in_fd = 2; - stream_close(&input->read_stream, NULL); - queue_put(loop.fast_events, restart_reading, 1, input); - } else { - input_done(); - } - return; - } - - do { - if (handle_bracketed_paste(input) || handle_forced_escape(input)) { - continue; - } - - // Find the next 'esc' and push everything up to it(excluding). This is done - // so the `handle_bracketed_paste`/`handle_forced_escape` calls above work - // as expected. - size_t count = 0; - RBUFFER_EACH(input->read_stream.buffer, c, i) { - count = i + 1; - if (c == '\x1b' && count > 1) { - count--; - break; - } - } - - RBUFFER_UNTIL_EMPTY(input->read_stream.buffer, ptr, len) { - size_t consumed = termkey_push_bytes(input->tk, ptr, MIN(count, len)); - // termkey_push_bytes can return (size_t)-1, so it is possible that - // `consumed > input->read_stream.buffer->size`, but since tk_getkeys is - // called soon, it shouldn't happen - assert(consumed <= input->read_stream.buffer->size); - rbuffer_consumed(input->read_stream.buffer, consumed); - // Need to process the keys now since there's no guarantee "count" will - // fit into libtermkey's input buffer. - tk_getkeys(input, false); - if (!(count -= consumed)) { - break; - } - } - } while (rbuffer_size(input->read_stream.buffer)); - - // Make sure the next input escape sequence fits into the ring buffer - // without wrap around, otherwise it could be misinterpreted. - rbuffer_reset(input->read_stream.buffer); -} - -static void restart_reading(void **argv) -{ - TermInput *input = argv[0]; - rstream_init_fd(&loop, &input->read_stream, input->in_fd, 0xfff, input); - rstream_start(&input->read_stream, read_cb); -} - -static TermInput *term_input_new(void) -{ - TermInput *rv = xmalloc(sizeof(TermInput)); - rv->paste_enabled = false; - rv->in_fd = 0; - - const char *term = os_getenv("TERM"); - if (!term) { - term = ""; // termkey_new_abstract assumes non-null (#2745) - } - rv->tk = termkey_new_abstract(term, 0); - int curflags = termkey_get_canonflags(rv->tk); - termkey_set_canonflags(rv->tk, curflags | TERMKEY_CANON_DELBS); - // setup input handle - rstream_init_fd(&loop, &rv->read_stream, rv->in_fd, 0xfff, rv); - rstream_start(&rv->read_stream, read_cb); - // initialize a timer handle for handling ESC with libtermkey - time_watcher_init(&loop, &rv->timer_handle, 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; -} - -static void term_input_destroy(TermInput *input) -{ - time_watcher_stop(&input->timer_handle); - time_watcher_close(&input->timer_handle, NULL); - rstream_stop(&input->read_stream); - stream_close(&input->read_stream, NULL); - termkey_destroy(input->tk); - // Run once to remove references to input/timer handles - loop_poll_events(&loop, 0); - xfree(input); -} - -static void term_input_set_encoding(TermInput *input, char* enc) -{ - int enc_flag = strcmp(enc, "utf-8") == 0 ? TERMKEY_FLAG_UTF8 - : TERMKEY_FLAG_RAW; - termkey_set_flags(input->tk, enc_flag); -} diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index acc2ccc682..235fa3117f 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -17,6 +17,9 @@ #include "nvim/event/loop.h" #include "nvim/event/signal.h" #include "nvim/tui/tui.h" +#include "nvim/tui/input.h" +#include "nvim/os/input.h" +#include "nvim/os/os.h" #include "nvim/strings.h" // Space reserved in the output buffer to restore the cursor to normal when @@ -24,10 +27,6 @@ #define CNORM_COMMAND_MAX_SIZE 32 #define OUTBUF_SIZE 0xffff -typedef struct term_input TermInput; - -#include "term_input.inl" - typedef struct { int top, bot, left, right; } Rect; -- cgit From f5c5cdb306704e40019fb0eb9457e64bda8ee7cc Mon Sep 17 00:00:00 2001 From: Thiago de Arruda Date: Wed, 26 Aug 2015 09:07:04 -0300 Subject: tui: Move screen state tracking to new "ugrid" module The ugrid module implements a unicode "drawing" grid and is used to store TUI screen state. Later this module will be reused in other layers. --- src/nvim/tui/tui.c | 208 +++++++++++++++++------------------------------------ src/nvim/ugrid.c | 137 +++++++++++++++++++++++++++++++++++ src/nvim/ugrid.h | 40 +++++++++++ 3 files changed, 244 insertions(+), 141 deletions(-) create mode 100644 src/nvim/ugrid.c create mode 100644 src/nvim/ugrid.h diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 235fa3117f..a5a6783927 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -21,6 +21,7 @@ #include "nvim/os/input.h" #include "nvim/os/os.h" #include "nvim/strings.h" +#include "nvim/ugrid.h" // Space reserved in the output buffer to restore the cursor to normal when // flushing. No existing terminal will require 32 bytes to do that. @@ -31,11 +32,6 @@ typedef struct { int top, bot, left, right; } Rect; -typedef struct { - char data[7]; - HlAttrs attrs; -} Cell; - typedef struct { unibi_var_t params[9]; char buf[OUTBUF_SIZE]; @@ -45,17 +41,13 @@ typedef struct { unibi_term *ut; uv_tty_t output_handle; SignalWatcher winch_handle; - Rect scroll_region; + UGrid grid; kvec_t(Rect) invalid_regions; - int row, col; - int bg, fg; int out_fd; - int old_height; bool can_use_terminal_scroll; bool mouse_enabled; bool busy; - HlAttrs attrs, print_attrs; - Cell **screen; + HlAttrs print_attrs; int showing_mode; struct { int enable_mouse, disable_mouse; @@ -71,32 +63,14 @@ static bool volatile got_winch = false; # include "tui/tui.c.generated.h" #endif -#define EMPTY_ATTRS ((HlAttrs){false, false, false, false, false, -1, -1}) - -#define FOREACH_CELL(ui, top, bot, left, right, go, code) \ - do { \ - TUIData *data = ui->data; \ - for (int row = top; row <= bot; ++row) { \ - Cell *cells = data->screen[row]; \ - if (go) { \ - unibi_goto(ui, row, left); \ - } \ - for (int col = left; col <= right; ++col) { \ - Cell *cell = cells + col; \ - (void)(cell); \ - code; \ - } \ - } \ - } while (0) - UI *tui_start(void) { TUIData *data = xcalloc(1, sizeof(TUIData)); UI *ui = xcalloc(1, sizeof(UI)); ui->data = data; - data->attrs = data->print_attrs = EMPTY_ATTRS; - data->fg = data->bg = -1; + data->print_attrs = EMPTY_ATTRS; + ugrid_init(&data->grid); data->can_use_terminal_scroll = true; data->bufpos = 0; data->bufsize = sizeof(data->buf) - CNORM_COMMAND_MAX_SIZE; @@ -201,7 +175,7 @@ static void tui_stop(UI *ui) } xfree(data->write_loop); unibi_destroy(data->ut); - destroy_screen(data); + ugrid_free(&data->grid); xfree(data); ui_detach(ui); xfree(ui); @@ -233,9 +207,10 @@ static void update_attrs(UI *ui, HlAttrs attrs) data->print_attrs = attrs; unibi_out(ui, unibi_exit_attribute_mode); + UGrid *grid = &data->grid; - int fg = attrs.foreground != -1 ? attrs.foreground : data->fg; - int bg = attrs.background != -1 ? attrs.background : data->bg; + int fg = attrs.foreground != -1 ? attrs.foreground : grid->fg; + int bg = attrs.background != -1 ? attrs.background : grid->bg; if (ui->rgb) { if (fg != -1) { @@ -277,25 +252,25 @@ static void update_attrs(UI *ui, HlAttrs attrs) } } -static void print_cell(UI *ui, Cell *ptr) +static void print_cell(UI *ui, UCell *ptr) { update_attrs(ui, ptr->attrs); out(ui, ptr->data, strlen(ptr->data)); } -static void clear_region(UI *ui, int top, int bot, int left, int right, - bool refresh) +static void clear_region(UI *ui, int top, int bot, int left, int right) { TUIData *data = ui->data; - HlAttrs clear_attrs = EMPTY_ATTRS; - clear_attrs.foreground = data->fg; - clear_attrs.background = data->bg; - update_attrs(ui, clear_attrs); + UGrid *grid = &data->grid; bool cleared = false; - if (refresh && data->bg == -1 && right == ui->width -1) { + if (grid->bg == -1 && right == ui->width -1) { // Background is set to the default color and the right edge matches the // screen end, try to use terminal codes for clearing the requested area. + HlAttrs clear_attrs = EMPTY_ATTRS; + clear_attrs.foreground = grid->fg; + clear_attrs.background = grid->bg; + update_attrs(ui, clear_attrs); if (left == 0) { if (bot == ui->height - 1) { if (top == 0) { @@ -318,36 +293,26 @@ static void clear_region(UI *ui, int top, int bot, int left, int right, } } - bool clear = refresh && !cleared; - FOREACH_CELL(ui, top, bot, left, right, clear, { - cell->data[0] = ' '; - cell->data[1] = 0; - cell->attrs = clear_attrs; - if (clear) { + if (!cleared) { + // could not clear using faster terminal codes, refresh the whole region + int currow = -1; + UGRID_FOREACH_CELL(grid, top, bot, left, right, { + if (currow != row) { + unibi_goto(ui, row, col); + currow = row; + } print_cell(ui, cell); - } - }); + }); + } // restore cursor - unibi_goto(ui, data->row, data->col); + unibi_goto(ui, grid->row, grid->col); } static void tui_resize(UI *ui, int width, int height) { TUIData *data = ui->data; - destroy_screen(data); - - data->screen = xmalloc((size_t)height * sizeof(Cell *)); - for (int i = 0; i < height; i++) { - data->screen[i] = xcalloc((size_t)width, sizeof(Cell)); - } - - data->old_height = height; - data->scroll_region.top = 0; - data->scroll_region.bot = height - 1; - data->scroll_region.left = 0; - data->scroll_region.right = width - 1; - data->row = data->col = 0; + ugrid_resize(&data->grid, width, height); if (!got_winch) { // Try to resize the terminal window. char r[16]; // enough for 9999x9999 @@ -361,22 +326,23 @@ static void tui_resize(UI *ui, int width, int height) static void tui_clear(UI *ui) { TUIData *data = ui->data; - clear_region(ui, data->scroll_region.top, data->scroll_region.bot, - data->scroll_region.left, data->scroll_region.right, true); + UGrid *grid = &data->grid; + ugrid_clear(grid); + clear_region(ui, grid->top, grid->bot, grid->left, grid->right); } static void tui_eol_clear(UI *ui) { TUIData *data = ui->data; - clear_region(ui, data->row, data->row, data->col, - data->scroll_region.right, true); + UGrid *grid = &data->grid; + ugrid_eol_clear(grid); + clear_region(ui, grid->row, grid->row, grid->col, grid->right); } static void tui_cursor_goto(UI *ui, int row, int col) { TUIData *data = ui->data; - data->row = row; - data->col = col; + ugrid_goto(&data->grid, row, col); unibi_goto(ui, row, col); } @@ -434,11 +400,7 @@ static void tui_set_scroll_region(UI *ui, int top, int bot, int left, int right) { TUIData *data = ui->data; - data->scroll_region.top = top; - data->scroll_region.bot = bot; - data->scroll_region.left = left; - data->scroll_region.right = right; - + ugrid_set_scroll_region(&data->grid, top, bot, left, right); data->can_use_terminal_scroll = left == 0 && right == ui->width - 1 && ((top == 0 && bot == ui->height - 1) @@ -448,31 +410,24 @@ static void tui_set_scroll_region(UI *ui, int top, int bot, int left, static void tui_scroll(UI *ui, int count) { TUIData *data = ui->data; - int top = data->scroll_region.top; - int bot = data->scroll_region.bot; - int left = data->scroll_region.left; - int right = data->scroll_region.right; + UGrid *grid = &data->grid; + int clear_top, clear_bot; + ugrid_scroll(grid, count, &clear_top, &clear_bot); if (data->can_use_terminal_scroll) { // Change terminal scroll region and move cursor to the top - data->params[0].i = top; - data->params[1].i = bot; + data->params[0].i = grid->top; + data->params[1].i = grid->bot; unibi_out(ui, unibi_change_scroll_region); - unibi_goto(ui, top, left); + unibi_goto(ui, grid->top, grid->left); // also set default color attributes or some terminals can become funny HlAttrs clear_attrs = EMPTY_ATTRS; - clear_attrs.foreground = data->fg; - clear_attrs.background = data->bg; + clear_attrs.foreground = grid->fg; + clear_attrs.background = grid->bg; update_attrs(ui, clear_attrs); } - // Compute start/stop/step for the loop below, also use terminal scroll - // if possible - int start, stop, step; if (count > 0) { - start = top; - stop = bot - count + 1; - step = 1; if (data->can_use_terminal_scroll) { if (count == 1) { unibi_out(ui, unibi_delete_line); @@ -483,9 +438,6 @@ static void tui_scroll(UI *ui, int count) } } else { - start = bot; - stop = top - count - 1; - step = -1; if (data->can_use_terminal_scroll) { if (count == -1) { unibi_out(ui, unibi_insert_line); @@ -501,52 +453,30 @@ static void tui_scroll(UI *ui, int count) data->params[0].i = 0; data->params[1].i = ui->height - 1; unibi_out(ui, unibi_change_scroll_region); - unibi_goto(ui, data->row, data->col); - } - - int i; - // Scroll internal screen - for (i = start; i != stop; i += step) { - Cell *target_row = data->screen[i] + left; - Cell *source_row = data->screen[i + count] + left; - memcpy(target_row, source_row, sizeof(Cell) * (size_t)(right - left + 1)); - } - - // clear emptied region, updating the terminal if its builtin scrolling - // facility was used. This is done when the background color is not the - // default, since scrolling may leave wrong background in the cleared area. - bool update_clear = data->bg != -1 && data->can_use_terminal_scroll; - if (count > 0) { - clear_region(ui, stop, stop + count - 1, left, right, update_clear); + unibi_goto(ui, grid->row, grid->col); + + if (grid->bg != -1) { + // Update the cleared area of the terminal if its builtin scrolling + // facility was used and the background color is not the default. This is + // required because scrolling may leave wrong background in the cleared + // area. + clear_region(ui, clear_top, clear_bot, grid->left, grid->right); + } } else { - clear_region(ui, stop + count + 1, stop, left, right, update_clear); - } - - if (!data->can_use_terminal_scroll) { // Mark the entire scroll region as invalid for redrawing later - invalidate(ui, data->scroll_region.top, data->scroll_region.bot, - data->scroll_region.left, data->scroll_region.right); + invalidate(ui, grid->top, grid->bot, grid->left, grid->right); } } static void tui_highlight_set(UI *ui, HlAttrs attrs) { - ((TUIData *)ui->data)->attrs = attrs; + ((TUIData *)ui->data)->grid.attrs = attrs; } static void tui_put(UI *ui, uint8_t *text, size_t size) { TUIData *data = ui->data; - Cell *cell = data->screen[data->row] + data->col; - cell->data[size] = 0; - cell->attrs = data->attrs; - - if (text) { - memcpy(cell->data, text, size); - } - - print_cell(ui, cell); - data->col += 1; + print_cell(ui, ugrid_put(&data->grid, text, size)); } static void tui_bell(UI *ui) @@ -561,26 +491,32 @@ static void tui_visual_bell(UI *ui) static void tui_update_fg(UI *ui, int fg) { - ((TUIData *)ui->data)->fg = fg; + ((TUIData *)ui->data)->grid.fg = fg; } static void tui_update_bg(UI *ui, int bg) { - ((TUIData *)ui->data)->bg = bg; + ((TUIData *)ui->data)->grid.bg = bg; } static void tui_flush(UI *ui) { TUIData *data = ui->data; + UGrid *grid = &data->grid; while (kv_size(data->invalid_regions)) { Rect r = kv_pop(data->invalid_regions); - FOREACH_CELL(ui, r.top, r.bot, r.left, r.right, true, { + int currow = -1; + UGRID_FOREACH_CELL(grid, r.top, r.bot, r.left, r.right, { + if (currow != row) { + unibi_goto(ui, row, col); + currow = row; + } print_cell(ui, cell); }); } - unibi_goto(ui, data->row, data->col); + unibi_goto(ui, grid->row, grid->col); flush_buf(ui); } @@ -890,13 +826,3 @@ static void flush_buf(UI *ui) unibi_out(ui, unibi_cursor_invisible); } } - -static void destroy_screen(TUIData *data) -{ - if (data->screen) { - for (int i = 0; i < data->old_height; i++) { - xfree(data->screen[i]); - } - xfree(data->screen); - } -} diff --git a/src/nvim/ugrid.c b/src/nvim/ugrid.c new file mode 100644 index 0000000000..127b18feb6 --- /dev/null +++ b/src/nvim/ugrid.c @@ -0,0 +1,137 @@ +#include +#include +#include +#include + +#include "nvim/vim.h" +#include "nvim/ui.h" +#include "nvim/ugrid.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "ugrid.c.generated.h" +#endif + +void ugrid_init(UGrid *grid) +{ + grid->attrs = EMPTY_ATTRS; + grid->fg = grid->bg = -1; + grid->cells = NULL; +} + +void ugrid_free(UGrid *grid) +{ + destroy_cells(grid); +} + +void ugrid_resize(UGrid *grid, int width, int height) +{ + destroy_cells(grid); + grid->cells = xmalloc((size_t)height * sizeof(UCell *)); + for (int i = 0; i < height; i++) { + grid->cells[i] = xcalloc((size_t)width, sizeof(UCell)); + } + + grid->top = 0; + grid->bot = height - 1; + grid->left = 0; + grid->right = width - 1; + grid->row = grid->col = 0; + grid->width = width; + grid->height = height; +} + +void ugrid_clear(UGrid *grid) +{ + clear_region(grid, grid->top, grid->bot, grid->left, grid->right); +} + +void ugrid_eol_clear(UGrid *grid) +{ + clear_region(grid, grid->row, grid->row, grid->col, grid->right); +} + +void ugrid_goto(UGrid *grid, int row, int col) +{ + grid->row = row; + grid->col = col; +} + +void ugrid_set_scroll_region(UGrid *grid, int top, int bot, int left, int right) +{ + grid->top = top; + grid->bot = bot; + grid->left = left; + grid->right = right; +} + +void ugrid_scroll(UGrid *grid, int count, int *clear_top, int *clear_bot) +{ + // Compute start/stop/step for the loop below + int start, stop, step; + if (count > 0) { + start = grid->top; + stop = grid->bot - count + 1; + step = 1; + } else { + start = grid->bot; + stop = grid->top - count - 1; + step = -1; + } + + int i; + + // Copy cell data + for (i = start; i != stop; i += step) { + UCell *target_row = grid->cells[i] + grid->left; + UCell *source_row = grid->cells[i + count] + grid->left; + memcpy(target_row, source_row, + sizeof(UCell) * (size_t)(grid->right - grid->left + 1)); + } + + // clear cells in the emptied region, + if (count > 0) { + *clear_top = stop; + *clear_bot = stop + count - 1; + } else { + *clear_bot = stop; + *clear_top = stop + count + 1; + } + clear_region(grid, *clear_top, *clear_bot, grid->left, grid->right); +} + +UCell *ugrid_put(UGrid *grid, uint8_t *text, size_t size) +{ + UCell *cell = grid->cells[grid->row] + grid->col; + cell->data[size] = 0; + cell->attrs = grid->attrs; + + if (text) { + memcpy(cell->data, text, size); + } + + grid->col += 1; + return cell; +} + +static void clear_region(UGrid *grid, int top, int bot, int left, int right) +{ + HlAttrs clear_attrs = EMPTY_ATTRS; + clear_attrs.foreground = grid->fg; + clear_attrs.background = grid->bg; + UGRID_FOREACH_CELL(grid, top, bot, left, right, { + cell->data[0] = ' '; + cell->data[1] = 0; + cell->attrs = clear_attrs; + }); +} + +static void destroy_cells(UGrid *grid) +{ + if (grid->cells) { + for (int i = 0; i < grid->height; i++) { + xfree(grid->cells[i]); + } + xfree(grid->cells); + } +} + diff --git a/src/nvim/ugrid.h b/src/nvim/ugrid.h new file mode 100644 index 0000000000..e41461fa16 --- /dev/null +++ b/src/nvim/ugrid.h @@ -0,0 +1,40 @@ +#ifndef NVIM_UGRID_H +#define NVIM_UGRID_H + +#include "nvim/ui.h" + +typedef struct ucell UCell; +typedef struct ugrid UGrid; + +struct ucell { + char data[7]; + HlAttrs attrs; +}; + +struct ugrid { + int top, bot, left, right; + int row, col; + int bg, fg; + int width, height; + HlAttrs attrs; + UCell **cells; +}; + +#define EMPTY_ATTRS ((HlAttrs){false, false, false, false, false, -1, -1}) + +#define UGRID_FOREACH_CELL(grid, top, bot, left, right, code) \ + do { \ + for (int row = top; row <= bot; ++row) { \ + UCell *row_cells = (grid)->cells[row]; \ + for (int col = left; col <= right; ++col) { \ + UCell *cell = row_cells + col; \ + (void)(cell); \ + code; \ + } \ + } \ + } while (0) + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "ugrid.h.generated.h" +#endif +#endif // NVIM_UGRID_H -- cgit From 5bb4139e6c98539564431de105dc9e0d06e207ac Mon Sep 17 00:00:00 2001 From: Thiago de Arruda Date: Wed, 26 Aug 2015 09:08:52 -0300 Subject: contrib: Implement gdb pretty printer for UGrid --- contrib/gdb/neovim_gdb.vim | 344 ++++++++++++++++++++++++++++++++ contrib/gdb/nvim-gdb-pretty-printers.py | 92 +++++++++ contrib/neovim_gdb/neovim_gdb.vim | 344 -------------------------------- 3 files changed, 436 insertions(+), 344 deletions(-) create mode 100644 contrib/gdb/neovim_gdb.vim create mode 100644 contrib/gdb/nvim-gdb-pretty-printers.py delete mode 100644 contrib/neovim_gdb/neovim_gdb.vim diff --git a/contrib/gdb/neovim_gdb.vim b/contrib/gdb/neovim_gdb.vim new file mode 100644 index 0000000000..d61e7bc0cc --- /dev/null +++ b/contrib/gdb/neovim_gdb.vim @@ -0,0 +1,344 @@ +sign define GdbBreakpoint text=● +sign define GdbCurrentLine text=⇒ + + +let s:gdb_port = 7778 +let s:run_gdb = "gdb -q -f build/bin/nvim" +let s:breakpoints = {} +let s:max_breakpoint_sign_id = 0 + + +let s:GdbServer = {} + + +function s:GdbServer.new(gdb) + let this = copy(self) + let this._gdb = a:gdb + return this +endfunction + + +function s:GdbServer.on_exit() + let self._gdb._server_exited = 1 +endfunction + + +let s:GdbPaused = vimexpect#State([ + \ ['Continuing.', 'continue'], + \ ['\v[\o32]{2}([^:]+):(\d+):\d+', 'jump'], + \ ['Remote communication error. Target disconnected.:', 'retry'], + \ ]) + + +function s:GdbPaused.continue(...) + call self._parser.switch(s:GdbRunning) + call self.update_current_line_sign(0) +endfunction + + +function s:GdbPaused.jump(file, line, ...) + if tabpagenr() != self._tab + " Don't jump if we are not in the debugger tab + return + endif + let window = winnr() + exe self._jump_window 'wincmd w' + let self._current_buf = bufnr('%') + let target_buf = bufnr(a:file, 1) + if bufnr('%') != target_buf + exe 'buffer ' target_buf + let self._current_buf = target_buf + endif + exe ':' a:line + let self._current_line = a:line + exe window 'wincmd w' + call self.update_current_line_sign(1) +endfunction + + +function s:GdbPaused.retry(...) + if self._server_exited + return + endif + sleep 1 + call self.attach() + call self.send('continue') +endfunction + + +let s:GdbRunning = vimexpect#State([ + \ ['\v^Breakpoint \d+', 'pause'], + \ ['\v\[Inferior\ +.{-}\ +exited\ +normally', 'disconnected'], + \ ['(gdb)', 'pause'], + \ ]) + + +function s:GdbRunning.pause(...) + call self._parser.switch(s:GdbPaused) + if !self._initialized + call self.send('set confirm off') + call self.send('set pagination off') + if !empty(self._server_addr) + call self.send('set remotetimeout 50') + call self.attach() + call s:RefreshBreakpoints() + call self.send('c') + endif + let self._initialized = 1 + endif +endfunction + + +function s:GdbRunning.disconnected(...) + if !self._server_exited && self._reconnect + " Refresh to force a delete of all watchpoints + call s:RefreshBreakpoints() + sleep 1 + call self.attach() + call self.send('continue') + endif +endfunction + + +let s:Gdb = {} + + +function s:Gdb.kill() + tunmap + tunmap + tunmap + tunmap + call self.update_current_line_sign(0) + exe 'bd! '.self._client_buf + if self._server_buf != -1 + exe 'bd! '.self._server_buf + endif + exe 'tabnext '.self._tab + tabclose + unlet g:gdb +endfunction + + +function! s:Gdb.send(data) + call jobsend(self._client_id, a:data."\") +endfunction + + +function! s:Gdb.attach() + call self.send(printf('target remote %s', self._server_addr)) +endfunction + + +function! s:Gdb.update_current_line_sign(add) + " to avoid flicker when removing/adding the sign column(due to the change in + " line width), we switch ids for the line sign and only remove the old line + " sign after marking the new one + let old_line_sign_id = get(self, '_line_sign_id', 4999) + let self._line_sign_id = old_line_sign_id == 4999 ? 4998 : 4999 + if a:add && self._current_line != -1 && self._current_buf != -1 + exe 'sign place '.self._line_sign_id.' name=GdbCurrentLine line=' + \.self._current_line.' buffer='.self._current_buf + endif + exe 'sign unplace '.old_line_sign_id +endfunction + + +function! s:Spawn(server_cmd, client_cmd, server_addr, reconnect) + if exists('g:gdb') + throw 'Gdb already running' + endif + let gdb = vimexpect#Parser(s:GdbRunning, copy(s:Gdb)) + " gdbserver port + let gdb._server_addr = a:server_addr + let gdb._reconnect = a:reconnect + let gdb._initialized = 0 + " window number that will be displaying the current file + let gdb._jump_window = 1 + let gdb._current_buf = -1 + let gdb._current_line = -1 + let gdb._has_breakpoints = 0 + let gdb._server_exited = 0 + " Create new tab for the debugging view + tabnew + let gdb._tab = tabpagenr() + " create horizontal split to display the current file and maybe gdbserver + sp + let gdb._server_buf = -1 + if type(a:server_cmd) == type('') + " spawn gdbserver in a vertical split + let server = s:GdbServer.new(gdb) + vsp | enew | let gdb._server_id = termopen(a:server_cmd, server) + let gdb._jump_window = 2 + let gdb._server_buf = bufnr('%') + endif + " go to the bottom window and spawn gdb client + wincmd j + enew | let gdb._client_id = termopen(a:client_cmd, gdb) + let gdb._client_buf = bufnr('%') + tnoremap :GdbContinuei + tnoremap :GdbNexti + tnoremap :GdbStepi + tnoremap :GdbFinishi + " go to the window that displays the current file + exe gdb._jump_window 'wincmd w' + let g:gdb = gdb +endfunction + + +function! s:Test(bang, filter) + let cmd = "GDB=1 make test" + if a:bang == '!' + let server_addr = '| vgdb' + let cmd = printf('VALGRIND=1 %s', cmd) + else + let server_addr = printf('localhost:%d', s:gdb_port) + let cmd = printf('GDBSERVER_PORT=%d %s', s:gdb_port, cmd) + endif + if a:filter != '' + let cmd = printf('TEST_SCREEN_TIMEOUT=1000000 TEST_FILTER="%s" %s', a:filter, cmd) + endif + call s:Spawn(cmd, s:run_gdb, server_addr, 1) +endfunction + + +function! s:ToggleBreak() + let file_name = bufname('%') + let file_breakpoints = get(s:breakpoints, file_name, {}) + let linenr = line('.') + if has_key(file_breakpoints, linenr) + call remove(file_breakpoints, linenr) + else + let file_breakpoints[linenr] = 1 + endif + let s:breakpoints[file_name] = file_breakpoints + call s:RefreshBreakpointSigns() + call s:RefreshBreakpoints() +endfunction + + +function! s:ClearBreak() + let s:breakpoints = {} + call s:RefreshBreakpointSigns() + call s:RefreshBreakpoints() +endfunction + + +function! s:RefreshBreakpointSigns() + let buf = bufnr('%') + let i = 5000 + while i <= s:max_breakpoint_sign_id + exe 'sign unplace '.i + let i += 1 + endwhile + let s:max_breakpoint_sign_id = 0 + let id = 5000 + for linenr in keys(get(s:breakpoints, bufname('%'), {})) + exe 'sign place '.id.' name=GdbBreakpoint line='.linenr.' buffer='.buf + let s:max_breakpoint_sign_id = id + let id += 1 + endfor +endfunction + + +function! s:RefreshBreakpoints() + if !exists('g:gdb') + return + endif + if g:gdb._parser.state() == s:GdbRunning + " pause first + call jobsend(g:gdb._client_id, "\") + endif + if g:gdb._has_breakpoints + call g:gdb.send('delete') + endif + let g:gdb._has_breakpoints = 0 + for [file, breakpoints] in items(s:breakpoints) + for linenr in keys(breakpoints) + let g:gdb._has_breakpoints = 1 + call g:gdb.send('break '.file.':'.linenr) + endfor + endfor +endfunction + + +function! s:GetExpression(...) range + let [lnum1, col1] = getpos("'<")[1:2] + let [lnum2, col2] = getpos("'>")[1:2] + let lines = getline(lnum1, lnum2) + let lines[-1] = lines[-1][:col2 - 1] + let lines[0] = lines[0][col1 - 1:] + return join(lines, "\n") +endfunction + + +function! s:Send(data) + if !exists('g:gdb') + throw 'Gdb is not running' + endif + call g:gdb.send(a:data) +endfunction + + +function! s:Eval(expr) + call s:Send(printf('print %s', a:expr)) +endfunction + + +function! s:Watch(expr) + let expr = a:expr + if expr[0] != '&' + let expr = '&' . expr + endif + + call s:Eval(expr) + call s:Send('watch *$') +endfunction + + +function! s:Interrupt() + if !exists('g:gdb') + throw 'Gdb is not running' + endif + call jobsend(g:gdb._client_id, "\info line\") +endfunction + + +function! s:Kill() + if !exists('g:gdb') + throw 'Gdb is not running' + endif + call g:gdb.kill() +endfunction + + +command! GdbDebugNvim call s:Spawn(printf('make && gdbserver localhost:%d build/bin/nvim', s:gdb_port), s:run_gdb, printf('localhost:%d', s:gdb_port), 0) +command! -nargs=1 GdbDebugServer call s:Spawn(0, s:run_gdb, 'localhost:'., 0) +command! -bang -nargs=? GdbDebugTest call s:Test(, ) +command! -nargs=1 -complete=file GdbInspectCore call s:Spawn(0, printf('gdb -q -f -c %s build/bin/nvim', ), 0, 0) +command! GdbDebugStop call s:Kill() +command! GdbToggleBreakpoint call s:ToggleBreak() +command! GdbClearBreakpoints call s:ClearBreak() +command! GdbContinue call s:Send("c") +command! GdbNext call s:Send("n") +command! GdbStep call s:Send("s") +command! GdbFinish call s:Send("finish") +command! GdbFrameUp call s:Send("up") +command! GdbFrameDown call s:Send("down") +command! GdbInterrupt call s:Interrupt() +command! GdbEvalWord call s:Eval(expand('')) +command! -range GdbEvalRange call s:Eval(s:GetExpression()) +command! GdbWatchWord call s:Watch(expand('') +command! -range GdbWatchRange call s:Watch(s:GetExpression()) + + +nnoremap :GdbContinue +nnoremap :GdbNext +nnoremap :GdbStep +nnoremap :GdbFinish +nnoremap :GdbToggleBreakpoint +nnoremap :GdbFrameUp +nnoremap :GdbFrameDown +nnoremap :GdbEvalWord +vnoremap :GdbEvalRange +nnoremap :GdbWatchWord +vnoremap :GdbWatchRange diff --git a/contrib/gdb/nvim-gdb-pretty-printers.py b/contrib/gdb/nvim-gdb-pretty-printers.py new file mode 100644 index 0000000000..609ceeb7ab --- /dev/null +++ b/contrib/gdb/nvim-gdb-pretty-printers.py @@ -0,0 +1,92 @@ +# Register a gdb pretty printer for UGrid instances. Usage: +# +# - start gdb +# - run `source contrib/gdb/nvim-gdb-pretty-printers.py` +# - when a `UGrid` pointer can be evaluated in the current frame, just print +# it's value normally: `p *grid` (assuming `grid` is the variable name +# holding the pointer) +# - highlighting can be activated by setting the NVIM_GDB_HIGHLIGHT_UGRID +# environment variable(only xterm-compatible terminals supported). This +# can be done while gdb is running through the python interface: +# `python os.environ['NVIM_GDB_HIGHLIGHT_UGRID'] = '1'` +import os +import gdb +import gdb.printing + + +SGR0 = '\x1b(B\x1b[m' + + +def get_color_code(bg, color_num): + if color_num < 16: + prefix = 3 + if color_num > 7: + prefix = 9 + if bg: + prefix += 1 + color_num %= 8 + else: + prefix = '48;5;' if bg else '38;5;' + return '\x1b[{0}{1}m'.format(prefix, color_num) + + +def highlight(attrs): + fg, bg = [int(attrs['foreground']), int(attrs['background'])] + rv = [SGR0] # start with sgr0 + if fg != -1: + rv.append(get_color_code(False, fg)) + if bg != -1: + rv.append(get_color_code(True, bg)) + if bool(attrs['bold']): + rv.append('\x1b[1m') + if bool(attrs['italic']): + rv.append('\x1b[3m') + if bool(attrs['undercurl']) or bool(attrs['underline']): + rv.append('\x1b[4m') + if bool(attrs['reverse']): + rv.append('\x1b[7m') + return ''.join(rv) + + +class UGridPrinter(object): + def __init__(self, val): + self.val = val + + def to_string(self): + do_hl = (os.getenv('NVIM_GDB_HIGHLIGHT_UGRID') and + os.getenv('NVIM_GDB_HIGHLIGHT_UGRID') != '0') + grid = self.val + height = int(grid['height']) + width = int(grid['width']) + delimiter = '-' * (width + 2) + rows = [delimiter] + for row in range(height): + cols = [] + if do_hl: + cols.append(SGR0) + curhl = None + for col in range(width): + cell = grid['cells'][row][col] + if do_hl: + hl = highlight(cell['attrs']) + if hl != curhl: + cols.append(hl) + curhl = hl + cols.append(cell['data'].string('utf-8')) + if do_hl: + cols.append(SGR0) + rows.append('|' + ''.join(cols) + '|') + rows.append(delimiter) + return '\n' + '\n'.join(rows) + + def display_hint(self): + return 'hint' + + +def pretty_printers(): + pp = gdb.printing.RegexpCollectionPrettyPrinter('nvim') + pp.add_printer('UGrid', '^ugrid$', UGridPrinter) + return pp + + +gdb.printing.register_pretty_printer(gdb, pretty_printers(), replace=True) diff --git a/contrib/neovim_gdb/neovim_gdb.vim b/contrib/neovim_gdb/neovim_gdb.vim deleted file mode 100644 index d61e7bc0cc..0000000000 --- a/contrib/neovim_gdb/neovim_gdb.vim +++ /dev/null @@ -1,344 +0,0 @@ -sign define GdbBreakpoint text=● -sign define GdbCurrentLine text=⇒ - - -let s:gdb_port = 7778 -let s:run_gdb = "gdb -q -f build/bin/nvim" -let s:breakpoints = {} -let s:max_breakpoint_sign_id = 0 - - -let s:GdbServer = {} - - -function s:GdbServer.new(gdb) - let this = copy(self) - let this._gdb = a:gdb - return this -endfunction - - -function s:GdbServer.on_exit() - let self._gdb._server_exited = 1 -endfunction - - -let s:GdbPaused = vimexpect#State([ - \ ['Continuing.', 'continue'], - \ ['\v[\o32]{2}([^:]+):(\d+):\d+', 'jump'], - \ ['Remote communication error. Target disconnected.:', 'retry'], - \ ]) - - -function s:GdbPaused.continue(...) - call self._parser.switch(s:GdbRunning) - call self.update_current_line_sign(0) -endfunction - - -function s:GdbPaused.jump(file, line, ...) - if tabpagenr() != self._tab - " Don't jump if we are not in the debugger tab - return - endif - let window = winnr() - exe self._jump_window 'wincmd w' - let self._current_buf = bufnr('%') - let target_buf = bufnr(a:file, 1) - if bufnr('%') != target_buf - exe 'buffer ' target_buf - let self._current_buf = target_buf - endif - exe ':' a:line - let self._current_line = a:line - exe window 'wincmd w' - call self.update_current_line_sign(1) -endfunction - - -function s:GdbPaused.retry(...) - if self._server_exited - return - endif - sleep 1 - call self.attach() - call self.send('continue') -endfunction - - -let s:GdbRunning = vimexpect#State([ - \ ['\v^Breakpoint \d+', 'pause'], - \ ['\v\[Inferior\ +.{-}\ +exited\ +normally', 'disconnected'], - \ ['(gdb)', 'pause'], - \ ]) - - -function s:GdbRunning.pause(...) - call self._parser.switch(s:GdbPaused) - if !self._initialized - call self.send('set confirm off') - call self.send('set pagination off') - if !empty(self._server_addr) - call self.send('set remotetimeout 50') - call self.attach() - call s:RefreshBreakpoints() - call self.send('c') - endif - let self._initialized = 1 - endif -endfunction - - -function s:GdbRunning.disconnected(...) - if !self._server_exited && self._reconnect - " Refresh to force a delete of all watchpoints - call s:RefreshBreakpoints() - sleep 1 - call self.attach() - call self.send('continue') - endif -endfunction - - -let s:Gdb = {} - - -function s:Gdb.kill() - tunmap - tunmap - tunmap - tunmap - call self.update_current_line_sign(0) - exe 'bd! '.self._client_buf - if self._server_buf != -1 - exe 'bd! '.self._server_buf - endif - exe 'tabnext '.self._tab - tabclose - unlet g:gdb -endfunction - - -function! s:Gdb.send(data) - call jobsend(self._client_id, a:data."\") -endfunction - - -function! s:Gdb.attach() - call self.send(printf('target remote %s', self._server_addr)) -endfunction - - -function! s:Gdb.update_current_line_sign(add) - " to avoid flicker when removing/adding the sign column(due to the change in - " line width), we switch ids for the line sign and only remove the old line - " sign after marking the new one - let old_line_sign_id = get(self, '_line_sign_id', 4999) - let self._line_sign_id = old_line_sign_id == 4999 ? 4998 : 4999 - if a:add && self._current_line != -1 && self._current_buf != -1 - exe 'sign place '.self._line_sign_id.' name=GdbCurrentLine line=' - \.self._current_line.' buffer='.self._current_buf - endif - exe 'sign unplace '.old_line_sign_id -endfunction - - -function! s:Spawn(server_cmd, client_cmd, server_addr, reconnect) - if exists('g:gdb') - throw 'Gdb already running' - endif - let gdb = vimexpect#Parser(s:GdbRunning, copy(s:Gdb)) - " gdbserver port - let gdb._server_addr = a:server_addr - let gdb._reconnect = a:reconnect - let gdb._initialized = 0 - " window number that will be displaying the current file - let gdb._jump_window = 1 - let gdb._current_buf = -1 - let gdb._current_line = -1 - let gdb._has_breakpoints = 0 - let gdb._server_exited = 0 - " Create new tab for the debugging view - tabnew - let gdb._tab = tabpagenr() - " create horizontal split to display the current file and maybe gdbserver - sp - let gdb._server_buf = -1 - if type(a:server_cmd) == type('') - " spawn gdbserver in a vertical split - let server = s:GdbServer.new(gdb) - vsp | enew | let gdb._server_id = termopen(a:server_cmd, server) - let gdb._jump_window = 2 - let gdb._server_buf = bufnr('%') - endif - " go to the bottom window and spawn gdb client - wincmd j - enew | let gdb._client_id = termopen(a:client_cmd, gdb) - let gdb._client_buf = bufnr('%') - tnoremap :GdbContinuei - tnoremap :GdbNexti - tnoremap :GdbStepi - tnoremap :GdbFinishi - " go to the window that displays the current file - exe gdb._jump_window 'wincmd w' - let g:gdb = gdb -endfunction - - -function! s:Test(bang, filter) - let cmd = "GDB=1 make test" - if a:bang == '!' - let server_addr = '| vgdb' - let cmd = printf('VALGRIND=1 %s', cmd) - else - let server_addr = printf('localhost:%d', s:gdb_port) - let cmd = printf('GDBSERVER_PORT=%d %s', s:gdb_port, cmd) - endif - if a:filter != '' - let cmd = printf('TEST_SCREEN_TIMEOUT=1000000 TEST_FILTER="%s" %s', a:filter, cmd) - endif - call s:Spawn(cmd, s:run_gdb, server_addr, 1) -endfunction - - -function! s:ToggleBreak() - let file_name = bufname('%') - let file_breakpoints = get(s:breakpoints, file_name, {}) - let linenr = line('.') - if has_key(file_breakpoints, linenr) - call remove(file_breakpoints, linenr) - else - let file_breakpoints[linenr] = 1 - endif - let s:breakpoints[file_name] = file_breakpoints - call s:RefreshBreakpointSigns() - call s:RefreshBreakpoints() -endfunction - - -function! s:ClearBreak() - let s:breakpoints = {} - call s:RefreshBreakpointSigns() - call s:RefreshBreakpoints() -endfunction - - -function! s:RefreshBreakpointSigns() - let buf = bufnr('%') - let i = 5000 - while i <= s:max_breakpoint_sign_id - exe 'sign unplace '.i - let i += 1 - endwhile - let s:max_breakpoint_sign_id = 0 - let id = 5000 - for linenr in keys(get(s:breakpoints, bufname('%'), {})) - exe 'sign place '.id.' name=GdbBreakpoint line='.linenr.' buffer='.buf - let s:max_breakpoint_sign_id = id - let id += 1 - endfor -endfunction - - -function! s:RefreshBreakpoints() - if !exists('g:gdb') - return - endif - if g:gdb._parser.state() == s:GdbRunning - " pause first - call jobsend(g:gdb._client_id, "\") - endif - if g:gdb._has_breakpoints - call g:gdb.send('delete') - endif - let g:gdb._has_breakpoints = 0 - for [file, breakpoints] in items(s:breakpoints) - for linenr in keys(breakpoints) - let g:gdb._has_breakpoints = 1 - call g:gdb.send('break '.file.':'.linenr) - endfor - endfor -endfunction - - -function! s:GetExpression(...) range - let [lnum1, col1] = getpos("'<")[1:2] - let [lnum2, col2] = getpos("'>")[1:2] - let lines = getline(lnum1, lnum2) - let lines[-1] = lines[-1][:col2 - 1] - let lines[0] = lines[0][col1 - 1:] - return join(lines, "\n") -endfunction - - -function! s:Send(data) - if !exists('g:gdb') - throw 'Gdb is not running' - endif - call g:gdb.send(a:data) -endfunction - - -function! s:Eval(expr) - call s:Send(printf('print %s', a:expr)) -endfunction - - -function! s:Watch(expr) - let expr = a:expr - if expr[0] != '&' - let expr = '&' . expr - endif - - call s:Eval(expr) - call s:Send('watch *$') -endfunction - - -function! s:Interrupt() - if !exists('g:gdb') - throw 'Gdb is not running' - endif - call jobsend(g:gdb._client_id, "\info line\") -endfunction - - -function! s:Kill() - if !exists('g:gdb') - throw 'Gdb is not running' - endif - call g:gdb.kill() -endfunction - - -command! GdbDebugNvim call s:Spawn(printf('make && gdbserver localhost:%d build/bin/nvim', s:gdb_port), s:run_gdb, printf('localhost:%d', s:gdb_port), 0) -command! -nargs=1 GdbDebugServer call s:Spawn(0, s:run_gdb, 'localhost:'., 0) -command! -bang -nargs=? GdbDebugTest call s:Test(, ) -command! -nargs=1 -complete=file GdbInspectCore call s:Spawn(0, printf('gdb -q -f -c %s build/bin/nvim', ), 0, 0) -command! GdbDebugStop call s:Kill() -command! GdbToggleBreakpoint call s:ToggleBreak() -command! GdbClearBreakpoints call s:ClearBreak() -command! GdbContinue call s:Send("c") -command! GdbNext call s:Send("n") -command! GdbStep call s:Send("s") -command! GdbFinish call s:Send("finish") -command! GdbFrameUp call s:Send("up") -command! GdbFrameDown call s:Send("down") -command! GdbInterrupt call s:Interrupt() -command! GdbEvalWord call s:Eval(expand('')) -command! -range GdbEvalRange call s:Eval(s:GetExpression()) -command! GdbWatchWord call s:Watch(expand('') -command! -range GdbWatchRange call s:Watch(s:GetExpression()) - - -nnoremap :GdbContinue -nnoremap :GdbNext -nnoremap :GdbStep -nnoremap :GdbFinish -nnoremap :GdbToggleBreakpoint -nnoremap :GdbFrameUp -nnoremap :GdbFrameDown -nnoremap :GdbEvalWord -vnoremap :GdbEvalRange -nnoremap :GdbWatchWord -vnoremap :GdbWatchRange -- cgit From 7eaeed112e889499e5d98a2cb785518ca0059652 Mon Sep 17 00:00:00 2001 From: Thiago de Arruda Date: Fri, 28 Aug 2015 12:29:15 -0300 Subject: travis: Enable TSAN for sanity checking the terminal UI --- .ci/common/build.sh | 24 +++++++++++++++--------- .ci/run_tests.sh | 8 ++++++-- .travis.yml | 7 +++---- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/.ci/common/build.sh b/.ci/common/build.sh index 0140be3637..34bba42af3 100644 --- a/.ci/common/build.sh +++ b/.ci/common/build.sh @@ -61,19 +61,25 @@ build_nvim() { exit 1 fi - echo "Building libnvim." - if ! ${MAKE_CMD} libnvim; then - exit 1 - fi + if [ "$CLANG_SANITIZER" != "TSAN" ]; then + echo "Building libnvim." + if ! ${MAKE_CMD} libnvim; then + exit 1 + fi - echo "Building nvim-test." - if ! ${MAKE_CMD} nvim-test; then - exit 1 + echo "Building nvim-test." + if ! ${MAKE_CMD} nvim-test; then + exit 1 + fi fi # Invoke nvim to trigger *San early. - bin/nvim --version - bin/nvim -u NONE -e -c ':qall' + if ! (bin/nvim --version && bin/nvim -u NONE -e -c ':qall'); then + asan_check "${LOG_DIR}" + exit 1 + fi + asan_check "${LOG_DIR}" + cd "${TRAVIS_BUILD_DIR}" } diff --git a/.ci/run_tests.sh b/.ci/run_tests.sh index f3a6466c5e..656ea16e37 100755 --- a/.ci/run_tests.sh +++ b/.ci/run_tests.sh @@ -17,8 +17,12 @@ source "${CI_DIR}/common/test.sh" build_nvim -run_unittests -run_functionaltests +if [ "$CLANG_SANITIZER" != "TSAN" ]; then + # Additional threads are only created when the builtin UI starts, which + # doesn't happen in the unit/functional tests + run_unittests + run_functionaltests +fi run_oldtests install_nvim diff --git a/.travis.yml b/.travis.yml index becc7e8d3d..f9b0026c1f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -77,10 +77,9 @@ matrix: - os: linux compiler: clang-3.6 env: GCOV=llvm-cov-3.6 CLANG_SANITIZER=MSAN - # FIXME (tarruda): Uncomment when TSan tests don't hang anymore. - #- os: linux - # compiler: clang-3.6 - # env: GCOV=llvm-cov-3.6 CLANG_SANITIZER=TSAN + - os: linux + compiler: clang-3.6 + env: GCOV=llvm-cov-3.6 CLANG_SANITIZER=TSAN - os: osx compiler: clang env: GCOV=gcov -- cgit From 9ec240e03fb81f9d9476cf413556ede2c8a6092b Mon Sep 17 00:00:00 2001 From: Thiago de Arruda