From 8b6cfff6a18d839d11900cd1fade5938dc9a02d5 Mon Sep 17 00:00:00 2001 From: Thiago de Arruda Date: Thu, 4 Dec 2014 10:38:10 -0300 Subject: [PATCH 1/5] msgpack-rpc: Allow registration of handlers by other modules --- scripts/msgpack-gen.lua | 7 ++++++- src/nvim/msgpack_rpc/defs.h | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/msgpack-gen.lua b/scripts/msgpack-gen.lua index 9645784f00..d31a062a68 100644 --- a/scripts/msgpack-gen.lua +++ b/scripts/msgpack-gen.lua @@ -252,6 +252,11 @@ end output:write([[ static Map(String, MsgpackRpcRequestHandler) *methods = NULL; +void msgpack_rpc_add_method_handler(String method, MsgpackRpcRequestHandler handler) +{ + map_put(String, MsgpackRpcRequestHandler)(methods, method, handler); +} + void msgpack_rpc_init_method_table(void) { methods = map_new(String, MsgpackRpcRequestHandler)(); @@ -263,7 +268,7 @@ void msgpack_rpc_init_method_table(void) local max_fname_len = 0 for i = 1, #functions do local fn = functions[i] - output:write(' map_put(String, MsgpackRpcRequestHandler)(methods, '.. + output:write(' msgpack_rpc_add_method_handler('.. '(String) {.data = "'..fn.name..'", '.. '.size = sizeof("'..fn.name..'") - 1}, '.. '(MsgpackRpcRequestHandler) {.fn = handle_'.. fn.name.. diff --git a/src/nvim/msgpack_rpc/defs.h b/src/nvim/msgpack_rpc/defs.h index 13067fb7b4..0492a65290 100644 --- a/src/nvim/msgpack_rpc/defs.h +++ b/src/nvim/msgpack_rpc/defs.h @@ -19,6 +19,10 @@ typedef struct { /// Initializes the msgpack-rpc method table void msgpack_rpc_init_method_table(void); +// Add a handler to the method table +void msgpack_rpc_add_method_handler(String method, + MsgpackRpcRequestHandler handler); + void msgpack_rpc_init_function_metadata(Dictionary *metadata); /// Dispatches to the actual API function after basic payload validation by From 07e569a25dba4bf6d9743102a34666964efb45cb Mon Sep 17 00:00:00 2001 From: Thiago de Arruda Date: Sat, 6 Dec 2014 11:27:36 -0300 Subject: [PATCH 2/5] ui: Add abstract_ui termcap and split UI layer This is how Nvim behaves when the "abstract_ui" termcap is activated: - No data is written/read to stdout/stdin by default. - Instead of sending data to stdout, ui_write will parse the termcap codes and invoke dispatch functions in the ui.c module. - The dispatch functions will forward the calls to all attached UI instances(each UI instance is an implementation of the UI layer and is registered with ui_attach). - Like with the "builtin_gui" termcap, "abstract_ui" does not contain any key sequences. Instead, vim key strings(, , etc) are parsed directly by input_enqueue and the translated strings are pushed to the input buffer. With this new input model, its not possible to send mouse events yet. Thats because mouse sequence parsing happens in term.c/check_termcodes which must return early when "abstract_ui" is activated. --- src/nvim/ex_getln.c | 4 - src/nvim/getchar.c | 7 + src/nvim/globals.h | 2 + src/nvim/main.c | 36 ++-- src/nvim/mouse.c | 11 +- src/nvim/os/input.c | 30 ++- src/nvim/os/signal.c | 2 +- src/nvim/screen.c | 43 ++-- src/nvim/term.c | 68 +++++-- src/nvim/ui.c | 461 +++++++++++++++++++++++++++++++++++++++++-- src/nvim/ui.h | 32 +++ 11 files changed, 615 insertions(+), 81 deletions(-) diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index e56592923d..d3051c5202 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -1984,10 +1984,6 @@ void free_cmdline_buf(void) */ static void draw_cmdline(int start, int len) { - if (embedded_mode) { - return; - } - int i; if (cmdline_star > 0) diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c index d0bdcde9e8..5dec7e38fd 100644 --- a/src/nvim/getchar.c +++ b/src/nvim/getchar.c @@ -2513,6 +2513,13 @@ fix_input_buffer ( int script /* TRUE when reading from a script */ ) { + if (abstract_ui) { + // Should not escape K_SPECIAL/CSI while in embedded mode because vim key + // codes keys are processed in input.c/input_enqueue. + buf[len] = NUL; + return len; + } + int i; char_u *p = buf; diff --git a/src/nvim/globals.h b/src/nvim/globals.h index ea91135194..d94ff58f77 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -1251,6 +1251,8 @@ EXTERN int curr_tmode INIT(= TMODE_COOK); /* contains current terminal mode */ // If a msgpack-rpc channel should be started over stdin/stdout EXTERN bool embedded_mode INIT(= false); +// Using the "abstract_ui" termcap +EXTERN bool abstract_ui INIT(= false); /// Used to track the status of external functions. /// Currently only used for iconv(). diff --git a/src/nvim/main.c b/src/nvim/main.c index 8e19cf3686..c806431872 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -265,13 +265,6 @@ int main(int argc, char **argv) term_init(); TIME_MSG("shell init"); - event_init(); - - if (!embedded_mode) { - // Print a warning if stdout is not a terminal. - check_tty(¶ms); - } - /* This message comes before term inits, but after setting "silent_mode" * when the input is not a tty. */ if (GARGCOUNT > 1 && !silent_mode) @@ -283,6 +276,7 @@ int main(int argc, char **argv) // initial screen size of 80x20 full_screen = true; screen_resize(80, 20, false); + termcapinit((uint8_t *)"abstract_ui"); } else { // set terminal name and get terminal capabilities (will set full_screen) // Do some initialization of the screen @@ -292,6 +286,16 @@ int main(int argc, char **argv) TIME_MSG("Termcap init"); } + event_init(); + + if (abstract_ui) { + t_colors = 256; + } else { + // Print a warning if stdout is not a terminal TODO(tarruda): Remove this + // check once the new terminal UI is implemented + check_tty(¶ms); + } + /* * Set the default values for the options that use Rows and Columns. */ @@ -424,19 +428,17 @@ int main(int argc, char **argv) TIME_MSG("waiting for return"); } - if (!embedded_mode) { - starttermcap(); // start termcap if not done by wait_return() - TIME_MSG("start termcap"); - may_req_ambiguous_char_width(); - setmouse(); // may start using the mouse + starttermcap(); // start termcap if not done by wait_return() + TIME_MSG("start termcap"); + may_req_ambiguous_char_width(); + setmouse(); // may start using the mouse - if (scroll_region) { - scroll_region_reset(); // In case Rows changed - } - - scroll_start(); // may scroll the screen to the right position + if (scroll_region) { + scroll_region_reset(); // In case Rows changed } + scroll_start(); // may scroll the screen to the right position + /* * Don't clear the screen when starting in Ex mode, unless using the GUI. */ diff --git a/src/nvim/mouse.c b/src/nvim/mouse.c index 439cdbd5c8..9f67bd1760 100644 --- a/src/nvim/mouse.c +++ b/src/nvim/mouse.c @@ -452,7 +452,7 @@ void setmouse(void) return; /* don't switch mouse on when not in raw mode (Ex mode) */ - if (cur_tmode != TMODE_RAW) { + if (!abstract_ui && cur_tmode != TMODE_RAW) { mch_setmouse(false); return; } @@ -470,10 +470,11 @@ void setmouse(void) else checkfor = MOUSE_NORMAL; /* assume normal mode */ - if (mouse_has(checkfor)) - mch_setmouse(true); - else - mch_setmouse(false); + if (mouse_has(checkfor)) { + ui_mouse_on(); + } else { + ui_mouse_off(); + } } /* diff --git a/src/nvim/os/input.c b/src/nvim/os/input.c index 686fe1f06d..246ebf123c 100644 --- a/src/nvim/os/input.c +++ b/src/nvim/os/input.c @@ -46,7 +46,7 @@ void input_init(void) { input_buffer = rbuffer_new(INPUT_BUFFER_SIZE + MAX_KEY_CODE_LEN); - if (embedded_mode) { + if (abstract_ui) { return; } @@ -57,7 +57,7 @@ void input_init(void) void input_teardown(void) { - if (embedded_mode) { + if (abstract_ui) { return; } @@ -67,7 +67,7 @@ void input_teardown(void) // Listen for input void input_start(void) { - if (embedded_mode) { + if (abstract_ui) { return; } @@ -77,7 +77,7 @@ void input_start(void) // Stop listening for input void input_stop(void) { - if (embedded_mode) { + if (abstract_ui) { return; } @@ -180,7 +180,23 @@ void input_buffer_restore(String str) size_t input_enqueue(String keys) { - size_t rv = rbuffer_write(input_buffer, keys.data, keys.size); + char *ptr = keys.data, *end = ptr + keys.size; + + while (rbuffer_available(input_buffer) >= 6 && ptr < end) { + int new_size = trans_special((char_u **)&ptr, + (char_u *)rbuffer_write_ptr(input_buffer), + false); + if (!new_size) { + // copy the character unmodified + *rbuffer_write_ptr(input_buffer) = *ptr++; + new_size = 1; + } + // TODO(tarruda): Don't produce past unclosed '<' characters, except if + // there's a lot of characters after the '<' + rbuffer_produced(input_buffer, (size_t)new_size); + } + + size_t rv = (size_t)(ptr - keys.data); process_interrupts(); return rv; } @@ -255,7 +271,7 @@ static void read_cb(RStream *rstream, void *data, bool at_eof) static void convert_input(void) { - if (embedded_mode || !rbuffer_available(input_buffer)) { + if (abstract_ui || !rbuffer_available(input_buffer)) { // No input buffer space return; } @@ -335,7 +351,7 @@ static bool input_ready(void) return typebuf_was_filled || // API call filled typeahead rbuffer_pending(input_buffer) > 0 || // Stdin input event_has_deferred() || // Events must be processed - (!embedded_mode && eof); // Stdin closed + (!abstract_ui && eof); // Stdin closed } // Exit because of an input read error. diff --git a/src/nvim/os/signal.c b/src/nvim/os/signal.c index cf8ba85ed5..ca3ba052d7 100644 --- a/src/nvim/os/signal.c +++ b/src/nvim/os/signal.c @@ -45,7 +45,7 @@ void signal_init(void) uv_signal_start(&shup, signal_cb, SIGHUP); uv_signal_start(&squit, signal_cb, SIGQUIT); uv_signal_start(&sterm, signal_cb, SIGTERM); - if (!embedded_mode) { + if (!abstract_ui) { // TODO(tarruda): There must be an API function for resizing window uv_signal_start(&swinch, signal_cb, SIGWINCH); } diff --git a/src/nvim/screen.c b/src/nvim/screen.c index 0225eb72c1..c0a909f147 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -5824,9 +5824,12 @@ static void screen_start_highlight(int attr) attrentry_T *aep = NULL; screen_attr = attr; - if (full_screen - ) { - { + if (full_screen) { + if (abstract_ui) { + char buf[20]; + sprintf(buf, "\033|%dh", attr); + OUT_STR(buf); + } else { if (attr > HL_ALL) { /* special HL attr. */ if (t_colors > 1) aep = syn_cterm_attr2entry(attr); @@ -5877,9 +5880,13 @@ void screen_stop_highlight(void) { int do_ME = FALSE; /* output T_ME code */ - if (screen_attr != 0 - ) { - { + if (screen_attr != 0) { + if (abstract_ui) { + // Handled in ui.c + char buf[20]; + sprintf(buf, "\033|%dH", screen_attr); + OUT_STR(buf); + } else { if (screen_attr > HL_ALL) { /* special HL attr. */ attrentry_T *aep; @@ -6558,11 +6565,14 @@ static void screenclear2(void) { int i; - if (starting == NO_SCREEN || ScreenLines == NULL - ) + if (starting == NO_SCREEN || ScreenLines == NULL) { return; + } + + if (!abstract_ui) { + screen_attr = -1; /* force setting the Normal colors */ + } - screen_attr = -1; /* force setting the Normal colors */ screen_stop_highlight(); /* don't want highlighting here */ @@ -8156,14 +8166,19 @@ void screen_resize(int width, int height, int mustset) ++busy; - - if (mustset || (ui_get_shellsize() == FAIL && height != 0)) { + // TODO(tarruda): "mustset" is still used in the old tests, which don't use + // "abstract_ui" yet. This will change when a new TUI is merged. + if (abstract_ui || mustset || (ui_get_shellsize() == FAIL && height != 0)) { Rows = height; Columns = width; - check_shellsize(); + } + check_shellsize(); + + if (abstract_ui) { + ui_resize(width, height); + } else { mch_set_shellsize(); - } else - check_shellsize(); + } /* The window layout used to be adjusted here, but it now happens in * screenalloc() (also invoked from screenclear()). That is because the diff --git a/src/nvim/term.c b/src/nvim/term.c index 54508b1daa..40d6b4c170 100644 --- a/src/nvim/term.c +++ b/src/nvim/term.c @@ -161,6 +161,33 @@ static bool detected_8bit = false; // detected 8-bit terminal static struct builtin_term builtin_termcaps[] = { + // abstract UI pseudo termcap, based on vim's "builtin_gui" termcap + {(int)KS_NAME, "abstract_ui"}, + {(int)KS_CE, "\033|$"}, + {(int)KS_AL, "\033|i"}, + {(int)KS_CAL, "\033|%p1%dI"}, + {(int)KS_DL, "\033|d"}, + {(int)KS_CDL, "\033|%p1%dD"}, + {(int)KS_CS, "\033|%p1%d;%p2%dR"}, + {(int)KS_CL, "\033|C"}, + // attributes switched on with 'h', off with * 'H' + {(int)KS_ME, "\033|31H"}, // HL_ALL + {(int)KS_MR, "\033|1h"}, // HL_INVERSE + {(int)KS_MD, "\033|2h"}, // HL_BOLD + {(int)KS_SE, "\033|16H"}, // HL_STANDOUT + {(int)KS_SO, "\033|16h"}, // HL_STANDOUT + {(int)KS_UE, "\033|8H"}, // HL_UNDERLINE + {(int)KS_US, "\033|8h"}, // HL_UNDERLINE + {(int)KS_CZR, "\033|4H"}, // HL_ITALIC + {(int)KS_CZH, "\033|4h"}, // HL_ITALIC + {(int)KS_VB, "\033|f"}, + {(int)KS_MS, "y"}, + {(int)KS_UT, "y"}, + {(int)KS_LE, "\b"}, // cursor-left = BS + {(int)KS_ND, "\014"}, // cursor-right = CTRL-L + {(int)KS_CM, "\033|%p1%d;%p2%dM"}, + // there are no key sequences here, for "abstract_ui" vim key codes are + // parsed directly in input_enqueue() #ifndef NO_BUILTIN_TCAPS @@ -1162,6 +1189,10 @@ int set_termname(char_u *term) if (silent_mode) return OK; + if (!STRCMP(term, "abstract_ui")) { + abstract_ui = true; + } + detected_8bit = false; // reset 8-bit detection if (term_is_builtin(term)) { @@ -1829,18 +1860,6 @@ void termcapinit(char_u *name) /// Write s[len] to the screen. void term_write(char_u *s, size_t len) { - if (embedded_mode) { - // TODO(tarruda): This is a temporary hack to stop Neovim from writing - // messages to stdout in embedded mode. In the future, embedded mode will - // be the only possibility(GUIs will always start neovim with a msgpack-rpc - // over stdio) and this function won't exist. - // - // The reason for this is because before Neovim fully migrates to a - // msgpack-rpc-driven architecture, we must have a fully functional - // UI working - return; - } - (void) fwrite(s, len, 1, stdout); #ifdef UNIX @@ -2296,7 +2315,7 @@ void shell_resized_check(void) */ void settmode(int tmode) { - if (embedded_mode) { + if (abstract_ui) { return; } @@ -2340,7 +2359,7 @@ void starttermcap(void) out_flush(); termcap_active = TRUE; screen_start(); /* don't know where cursor is now */ - { + if (!abstract_ui) { may_req_termresponse(); /* Immediately check for a response. If t_Co changes, we don't * want to redraw with wrong colors first. */ @@ -2356,7 +2375,7 @@ void stoptermcap(void) screen_stop_highlight(); reset_cterm_colors(); if (termcap_active) { - { + if (!abstract_ui) { /* May need to discard T_CRV or T_U7 response. */ if (crv_status == CRV_SENT || u7_status == U7_SENT) { # ifdef UNIX @@ -2545,6 +2564,11 @@ static int cursor_is_off = FALSE; */ void cursor_on(void) { + if (abstract_ui) { + ui_cursor_on(); + return; + } + if (cursor_is_off) { out_str(T_VE); cursor_is_off = FALSE; @@ -2556,6 +2580,11 @@ void cursor_on(void) */ void cursor_off(void) { + if (abstract_ui) { + ui_cursor_off(); + return; + } + if (full_screen) { if (!cursor_is_off) out_str(T_VI); /* disable cursor */ @@ -2852,6 +2881,11 @@ void set_mouse_topline(win_T *wp) */ int check_termcode(int max_offset, char_u *buf, int bufsize, int *buflen) { + if (abstract_ui) { + // codes are parsed by input.c/input_enqueue + return 0; + } + char_u *tp; char_u *p; int slen = 0; /* init for GCC */ @@ -3883,6 +3917,10 @@ int find_term_bykeys(char_u *src) */ static void gather_termleader(void) { + if (abstract_ui) { + return; + } + int len = 0; if (check_for_codes) diff --git a/src/nvim/ui.c b/src/nvim/ui.c index eab6251288..fb3325f163 100644 --- a/src/nvim/ui.c +++ b/src/nvim/ui.c @@ -15,20 +15,24 @@ * 3. Input buffer stuff. */ +#include #include #include #include #include "nvim/vim.h" #include "nvim/ui.h" +#include "nvim/charset.h" #include "nvim/cursor.h" #include "nvim/diff.h" #include "nvim/ex_cmds2.h" #include "nvim/fold.h" #include "nvim/main.h" #include "nvim/mbyte.h" +#include "nvim/ascii.h" #include "nvim/misc1.h" #include "nvim/misc2.h" +#include "nvim/mbyte.h" #include "nvim/garray.h" #include "nvim/memory.h" #include "nvim/move.h" @@ -39,27 +43,74 @@ #include "nvim/os/input.h" #include "nvim/os/signal.h" #include "nvim/screen.h" +#include "nvim/syntax.h" #include "nvim/term.h" #include "nvim/window.h" -void ui_write(char_u *s, int len) +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "ui.c.generated.h" +#endif + +#define MAX_UI_COUNT 16 + +static UI *uis[MAX_UI_COUNT]; +static size_t ui_count = 0; +static int row, col; +static struct { + int top, bot, left, right; +} sr; +static int current_highlight_mask = 0; +static HlAttrs current_attrs = { + false, false, false, false, false, false, -1, -1 +}; +static bool cursor_enabled = true; +static int height = INT_MAX, width = INT_MAX; + +// This set of macros allow us to use UI_CALL to invoke any function on +// registered UI instances. The functions can have 0-5 arguments(configurable +// by SELECT_NTH) +// +// See http://stackoverflow.com/a/11172679 for a better explanation of how it +// works. +#define UI_CALL(...) \ + do { \ + for (size_t i = 0; i < ui_count; i++) { \ + UI *ui = uis[i]; \ + UI_CALL_HELPER(CNT(__VA_ARGS__), __VA_ARGS__); \ + } \ + } while (0) +#define CNT(...) SELECT_NTH(__VA_ARGS__, MORE, MORE, MORE, MORE, ZERO, ignore) +#define SELECT_NTH(a1, a2, a3, a4, a5, a6, ...) a6 +#define UI_CALL_HELPER(c, ...) UI_CALL_HELPER2(c, __VA_ARGS__) +#define UI_CALL_HELPER2(c, ...) UI_CALL_##c(__VA_ARGS__) +#define UI_CALL_MORE(method, ...) ui->method(ui, __VA_ARGS__) +#define UI_CALL_ZERO(method) ui->method(ui) + +void ui_write(uint8_t *s, int len) { - /* Don't output anything in silent mode ("ex -s") unless 'verbose' set */ - if (!(silent_mode && p_verbose == 0)) { - char_u *tofree = NULL; - - if (output_conv.vc_type != CONV_NONE) { - /* Convert characters from 'encoding' to 'termencoding'. */ - tofree = string_convert(&output_conv, s, &len); - if (tofree != NULL) - s = tofree; - } - - term_write(s, len); - - if (output_conv.vc_type != CONV_NONE) - free(tofree); + if (silent_mode && !p_verbose) { + // Don't output anything in silent mode ("ex -s") unless 'verbose' set + return; } + + if (abstract_ui) { + parse_abstract_ui_codes(s, len); + return; + } + + char_u *tofree = NULL; + + if (output_conv.vc_type != CONV_NONE) { + /* Convert characters from 'encoding' to 'termencoding'. */ + tofree = string_convert(&output_conv, s, &len); + if (tofree != NULL) + s = tofree; + } + + term_write(s, len); + + if (output_conv.vc_type != CONV_NONE) + free(tofree); } /* @@ -69,7 +120,11 @@ void ui_write(char_u *s, int len) */ void ui_suspend(void) { - mch_suspend(); + if (abstract_ui) { + UI_CALL(suspend); + } else { + mch_suspend(); + } } /* @@ -79,6 +134,10 @@ void ui_suspend(void) */ int ui_get_shellsize(void) { + if (abstract_ui) { + return FAIL; + } + int retval; retval = mch_get_shellsize(); @@ -98,7 +157,373 @@ int ui_get_shellsize(void) */ void ui_cursor_shape(void) { - term_cursor_shape(); + if (abstract_ui) { + ui_change_mode(); + } else { + term_cursor_shape(); + conceal_check_cursur_line(); + } +} + +void ui_resize(int width, int height) +{ + sr.top = 0; + sr.bot = height - 1; + sr.left = 0; + sr.right = width - 1; + UI_CALL(resize, width, height); +} + +void ui_cursor_on(void) +{ + if (!cursor_enabled) { + UI_CALL(cursor_on); + cursor_enabled = true; + } +} + +void ui_cursor_off(void) +{ + if (full_screen) { + if (cursor_enabled) { + UI_CALL(cursor_off); + } + cursor_enabled = false; + } +} + +void ui_mouse_on(void) +{ + if (abstract_ui) { + UI_CALL(mouse_on); + } else { + mch_setmouse(true); + } +} + +void ui_mouse_off(void) +{ + if (abstract_ui) { + UI_CALL(mouse_off); + } else { + mch_setmouse(false); + } +} + +// Notify that the current mode has changed. Can be used to change cursor +// shape, for example. +void ui_change_mode(void) +{ + static int showing_insert_mode = MAYBE; + + if (!full_screen) + return; + + if (State & INSERT) { + if (showing_insert_mode != TRUE) { + UI_CALL(insert_mode); + } + showing_insert_mode = TRUE; + } else { + if (showing_insert_mode != FALSE) { + UI_CALL(normal_mode); + } + showing_insert_mode = FALSE; + } conceal_check_cursur_line(); } +void ui_attach(UI *ui) +{ + if (ui_count == MAX_UI_COUNT) { + abort(); + } + + uis[ui_count++] = ui; + resized(ui); +} + +void ui_detach(UI *ui) +{ + size_t shift_index = MAX_UI_COUNT; + + // Find the index that will be removed + for (size_t i = 0; i < ui_count; i++) { + if (uis[i] == ui) { + shift_index = i; + break; + } + } + + if (shift_index == MAX_UI_COUNT) { + abort(); + } + + // Shift UIs at "shift_index" + while (shift_index < ui_count - 1) { + uis[shift_index] = uis[shift_index + 1]; + shift_index++; + } + + ui_count--; + + if (ui->width == width || ui->height == height) { + // It is possible that the UI being detached had the smallest screen, + // so check for the new minimum dimensions + width = height = INT_MAX; + for (size_t i = 0; i < ui_count; i++) { + check_dimensions(uis[i]); + } + } + + if (ui_count) { + screen_resize(width, height, true); + } +} + +static void highlight_start(int mask) +{ + if (mask > HL_ALL) { + // attribute code + current_highlight_mask = mask; + } else { + // attribute mask + current_highlight_mask |= mask; + } + + if (!ui_count) { + return; + } + + set_highlight_args(current_highlight_mask, ¤t_attrs); + UI_CALL(highlight_set, current_attrs); +} + +static void highlight_stop(int mask) +{ + if (mask > HL_ALL) { + // attribute code + current_highlight_mask = HL_NORMAL; + } else { + // attribute mask + current_highlight_mask &= ~mask; + } + + set_highlight_args(current_highlight_mask, ¤t_attrs); + UI_CALL(highlight_set, current_attrs); +} + +static void set_highlight_args(int mask, HlAttrs *attrs) +{ + attrentry_T *aep = NULL; + attrs->foreground = -1; + attrs->background = -1; + + if (mask > HL_ALL) { + aep = syn_cterm_attr2entry(mask); + mask = aep ? aep->ae_attr : 0; + } + + attrs->bold = mask & HL_BOLD; + attrs->standout = mask & HL_STANDOUT; + attrs->underline = mask & HL_UNDERLINE; + attrs->undercurl = mask & HL_UNDERCURL; + attrs->italic = mask & HL_ITALIC; + attrs->reverse = mask & HL_INVERSE; + + if (aep && aep->ae_u.cterm.fg_color + && (cterm_normal_fg_color != aep->ae_u.cterm.fg_color)) { + attrs->foreground = aep->ae_u.cterm.fg_color - 1; + } + + if (aep && aep->ae_u.cterm.bg_color + && (cterm_normal_bg_color != aep->ae_u.cterm.bg_color)) { + attrs->background = aep->ae_u.cterm.bg_color - 1; + } +} + +static void parse_abstract_ui_codes(uint8_t *ptr, int len) +{ + int arg1 = 0, arg2 = 0; + uint8_t *end = ptr + len, *p, c; + bool update_cursor = false; + + while (ptr < end) { + if (ptr < end - 1 && ptr[0] == ESC && ptr[1] == '|') { + p = ptr + 2; + assert(p != end); + + if (VIM_ISDIGIT(*p)) { + arg1 = (int)getdigits(&p); + if (p >= end) { + break; + } + + if (*p == ';') { + p++; + arg2 = (int)getdigits(&p); + if (p >= end) + break; + } + } + + switch (*p) { + case 'C': + UI_CALL(clear); + break; + case 'M': + ui_cursor_goto(arg1, arg2); + break; + case 's': + update_cursor = true; + break; + case 'R': + if (arg1 < arg2) { + sr.top = arg1; + sr.bot = arg2; + UI_CALL(set_scroll_region, sr.top, sr.bot, sr.left, sr.right); + } else { + sr.top = arg2; + sr.bot = arg1; + UI_CALL(set_scroll_region, sr.top, sr.bot, sr.left, sr.right); + } + break; + case 'V': + if (arg1 < arg2) { + sr.left = arg1; + sr.right = arg2; + UI_CALL(set_scroll_region, sr.top, sr.bot, sr.left, sr.right); + } else { + sr.left = arg2; + sr.right = arg1; + UI_CALL(set_scroll_region, sr.top, sr.bot, sr.left, sr.right); + } + break; + case 'd': + UI_CALL(scroll, 1); + break; + case 'D': + UI_CALL(scroll, arg1); + break; + case 'i': + UI_CALL(scroll, -1); + break; + case 'I': + UI_CALL(scroll, -arg1); + break; + case '$': + UI_CALL(eol_clear); + break; + case 'h': + highlight_start(arg1); + break; + case 'H': + highlight_stop(arg1); + break; + case 'f': + UI_CALL(visual_bell); + break; + default: + // Skip the ESC + p = ptr + 1; + break; + } + ptr = ++p; + } else if ((c = *ptr) < 0x20) { + // Ctrl character + if (c == '\n') { + ui_linefeed(); + } else if (c == '\r') { + ui_carriage_return(); + } else if (c == '\b') { + ui_cursor_left(); + } else if (c == Ctrl_L) { // cursor right + ui_cursor_right(); + } else if (c == Ctrl_G) { + UI_CALL(bell); + } + ptr++; + } else { + p = ptr; + while (p < end && (*p >= 0x20)) { + size_t clen = (size_t)mb_ptr2len(p); + UI_CALL(put, p, (size_t)clen); + col++; + if (mb_ptr2cells(p) > 1) { + // double cell character, blank the next cell + UI_CALL(put, NULL, 0); + col++; + } + p += clen; + } + ptr = p; + } + } + + if (update_cursor) { + ui_cursor_shape(); + } + + UI_CALL(flush); +} + +static void resized(UI *ui) +{ + check_dimensions(ui); + screen_resize(width, height, true); +} + +static void check_dimensions(UI *ui) +{ + // The internal screen dimensions are always the minimum required to fit on + // all connected screens + if (ui->width < width) { + width = ui->width; + } + + if (ui->height < height) { + height = ui->height; + } +} + +static void ui_linefeed(void) +{ + int new_col = 0; + int new_row = row; + if (new_row < sr.bot) { + new_row++; + } else { + UI_CALL(scroll, 1); + } + ui_cursor_goto(new_row, new_col); +} + +static void ui_carriage_return(void) +{ + int new_col = 0; + ui_cursor_goto(row, new_col); +} + +static void ui_cursor_left(void) +{ + int new_col = col - 1; + assert(new_col >= 0); + ui_cursor_goto(row, new_col); +} + +static void ui_cursor_right(void) +{ + int new_col = col + 1; + assert(new_col < width); + ui_cursor_goto(row, new_col); +} + +static void ui_cursor_goto(int new_row, int new_col) +{ + if (new_row == row && new_col == col) { + return; + } + row = new_row; + col = new_col; + UI_CALL(cursor_goto, row, col); +} diff --git a/src/nvim/ui.h b/src/nvim/ui.h index b174af9abe..d0933055cc 100644 --- a/src/nvim/ui.h +++ b/src/nvim/ui.h @@ -1,7 +1,39 @@ #ifndef NVIM_UI_H #define NVIM_UI_H +#include #include +#include + +typedef struct { + bool bold, standout, underline, undercurl, italic, reverse; + int foreground, background; +} HlAttrs; + +typedef struct ui_t UI; + +struct ui_t { + int width, height; + void *data; + void (*resize)(UI *ui, int rows, int columns); + void (*clear)(UI *ui); + void (*eol_clear)(UI *ui); + void (*cursor_goto)(UI *ui, int row, int col); + void (*cursor_on)(UI *ui); + void (*cursor_off)(UI *ui); + void (*mouse_on)(UI *ui); + void (*mouse_off)(UI *ui); + void (*insert_mode)(UI *ui); + void (*normal_mode)(UI *ui); + void (*set_scroll_region)(UI *ui, int top, int bot, int left, int right); + void (*scroll)(UI *ui, int count); + void (*highlight_set)(UI *ui, HlAttrs attrs); + void (*put)(UI *ui, uint8_t *str, size_t len); + void (*bell)(UI *ui); + void (*visual_bell)(UI *ui); + void (*flush)(UI *ui); + void (*suspend)(UI *ui); +}; #ifdef INCLUDE_GENERATED_DECLARATIONS # include "ui.h.generated.h" From 86542c6fd0677a712c6b87049a04d8e67cda3e3d Mon Sep 17 00:00:00 2001 From: Thiago de Arruda Date: Mon, 8 Dec 2014 22:24:00 -0300 Subject: [PATCH 3/5] syntax: Use RGB/GUI attribute information for "abstract_ui" Instead of using classic cterm color numbers and attributes, treat "abstract_ui" as a GUI: Pass rgb color numbers and gui attributes when the "highlight_set" UI method is called. The terminal UI will have to translate RGB color information to an appropriate color number, and the "term"/"cterm" :highlight keys will eventually be deprecated. --- src/nvim/api/vim.c | 6 + src/nvim/globals.h | 2 + src/nvim/syntax.c | 292 +++++++++++++++++++++++++++++++++++------ src/nvim/syntax.h | 2 - src/nvim/syntax_defs.h | 3 + src/nvim/ui.c | 14 +- 6 files changed, 268 insertions(+), 51 deletions(-) diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index fe5fa6274b..eab79d970e 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -21,6 +21,7 @@ #include "nvim/message.h" #include "nvim/eval.h" #include "nvim/misc2.h" +#include "nvim/syntax.h" #include "nvim/term.h" #include "nvim/getchar.h" #include "nvim/os/input.h" @@ -546,6 +547,11 @@ void vim_unsubscribe(uint64_t channel_id, String event) channel_unsubscribe(channel_id, e); } +Integer vim_name_to_color(String name) +{ + return name_to_color((uint8_t *)name.data); +} + Array vim_get_api_info(uint64_t channel_id) { Array rv = ARRAY_DICT_INIT; diff --git a/src/nvim/globals.h b/src/nvim/globals.h index d94ff58f77..233d326a40 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -465,6 +465,8 @@ EXTERN int highlight_stlnc[9]; /* On top of user */ EXTERN int cterm_normal_fg_color INIT(= 0); EXTERN int cterm_normal_fg_bold INIT(= 0); EXTERN int cterm_normal_bg_color INIT(= 0); +EXTERN RgbValue normal_fg INIT(= -1); +EXTERN RgbValue normal_bg INIT(= -1); EXTERN int autocmd_busy INIT(= FALSE); /* Is apply_autocmds() busy? */ EXTERN int autocmd_no_enter INIT(= FALSE); /* *Enter autocmds disabled */ diff --git a/src/nvim/syntax.c b/src/nvim/syntax.c index 69d6479cf3..45b42730bc 100644 --- a/src/nvim/syntax.c +++ b/src/nvim/syntax.c @@ -11,6 +11,7 @@ */ #include +#include #include #include #include @@ -68,9 +69,10 @@ struct hl_group { int sg_cterm_attr; /* Screen attr for color term mode */ /* Store the sp color name for the GUI or synIDattr() */ int sg_gui; /* "gui=" highlighting attributes */ - char_u *sg_gui_fg_name; /* GUI foreground color name */ - char_u *sg_gui_bg_name; /* GUI background color name */ - char_u *sg_gui_sp_name; /* GUI special color name */ + RgbValue sg_rgb_fg; // RGB foreground color + RgbValue sg_rgb_bg; // RGB background color + uint8_t *sg_rgb_fg_name; // RGB foreground color name + uint8_t *sg_rgb_bg_name; // RGB background color name int sg_link; /* link to this highlight group ID */ int sg_set; /* combination of SG_* flags */ scid_T sg_scriptID; /* script in which the group was last set */ @@ -6518,34 +6520,39 @@ do_highlight ( if (!init) HL_TABLE()[idx].sg_set |= SG_GUI; - free(HL_TABLE()[idx].sg_gui_fg_name); - if (STRCMP(arg, "NONE")) - HL_TABLE()[idx].sg_gui_fg_name = vim_strsave(arg); - else - HL_TABLE()[idx].sg_gui_fg_name = NULL; + free(HL_TABLE()[idx].sg_rgb_fg_name); + if (STRCMP(arg, "NONE")) { + HL_TABLE()[idx].sg_rgb_fg_name = (uint8_t *)xstrdup((char *)arg); + HL_TABLE()[idx].sg_rgb_fg = name_to_color(arg); + } else { + HL_TABLE()[idx].sg_rgb_fg_name = NULL; + HL_TABLE()[idx].sg_rgb_fg = -1; + } + } + + if (is_normal_group) { + normal_fg = HL_TABLE()[idx].sg_rgb_fg; } } else if (STRCMP(key, "GUIBG") == 0) { if (!init || !(HL_TABLE()[idx].sg_set & SG_GUI)) { if (!init) HL_TABLE()[idx].sg_set |= SG_GUI; - free(HL_TABLE()[idx].sg_gui_bg_name); - if (STRCMP(arg, "NONE") != 0) - HL_TABLE()[idx].sg_gui_bg_name = vim_strsave(arg); - else - HL_TABLE()[idx].sg_gui_bg_name = NULL; + free(HL_TABLE()[idx].sg_rgb_bg_name); + if (STRCMP(arg, "NONE") != 0) { + HL_TABLE()[idx].sg_rgb_bg_name = (uint8_t *)xstrdup((char *)arg); + HL_TABLE()[idx].sg_rgb_bg = name_to_color(arg); + } else { + HL_TABLE()[idx].sg_rgb_bg_name = NULL; + HL_TABLE()[idx].sg_rgb_bg = -1; + } + } + + if (is_normal_group) { + normal_bg = HL_TABLE()[idx].sg_rgb_bg; } } else if (STRCMP(key, "GUISP") == 0) { - if (!init || !(HL_TABLE()[idx].sg_set & SG_GUI)) { - if (!init) - HL_TABLE()[idx].sg_set |= SG_GUI; - - free(HL_TABLE()[idx].sg_gui_sp_name); - if (STRCMP(arg, "NONE") != 0) - HL_TABLE()[idx].sg_gui_sp_name = vim_strsave(arg); - else - HL_TABLE()[idx].sg_gui_sp_name = NULL; - } + // Ignored } else if (STRCMP(key, "START") == 0 || STRCMP(key, "STOP") == 0) { char_u buf[100]; char_u *tname; @@ -6670,6 +6677,8 @@ void free_highlight(void) */ void restore_cterm_colors(void) { + normal_fg = -1; + normal_bg = -1; cterm_normal_fg_color = 0; cterm_normal_fg_bold = 0; cterm_normal_bg_color = 0; @@ -6705,12 +6714,12 @@ static void highlight_clear(int idx) HL_TABLE()[idx].sg_cterm_bg = 0; HL_TABLE()[idx].sg_cterm_attr = 0; HL_TABLE()[idx].sg_gui = 0; - free(HL_TABLE()[idx].sg_gui_fg_name); - HL_TABLE()[idx].sg_gui_fg_name = NULL; - free(HL_TABLE()[idx].sg_gui_bg_name); - HL_TABLE()[idx].sg_gui_bg_name = NULL; - free(HL_TABLE()[idx].sg_gui_sp_name); - HL_TABLE()[idx].sg_gui_sp_name = NULL; + HL_TABLE()[idx].sg_rgb_fg = -1; + HL_TABLE()[idx].sg_rgb_bg = -1; + free(HL_TABLE()[idx].sg_rgb_fg_name); + HL_TABLE()[idx].sg_rgb_fg_name = NULL; + free(HL_TABLE()[idx].sg_rgb_bg_name); + HL_TABLE()[idx].sg_rgb_bg_name = NULL; /* Clear the script ID only when there is no link, since that is not * cleared. */ if (HL_TABLE()[idx].sg_link == 0) @@ -6771,7 +6780,11 @@ static int get_attr_entry(garray_T *table, attrentry_T *aep) && aep->ae_u.cterm.fg_color == taep->ae_u.cterm.fg_color && aep->ae_u.cterm.bg_color - == taep->ae_u.cterm.bg_color) + == taep->ae_u.cterm.bg_color + && aep->fg_color + == taep->fg_color + && aep->bg_color + == taep->bg_color) )) return i + ATTR_OFF; @@ -6818,6 +6831,8 @@ static int get_attr_entry(garray_T *table, attrentry_T *aep) } else if (table == &cterm_attr_table) { taep->ae_u.cterm.fg_color = aep->ae_u.cterm.fg_color; taep->ae_u.cterm.bg_color = aep->ae_u.cterm.bg_color; + taep->fg_color = aep->fg_color; + taep->bg_color = aep->bg_color; } return table->ga_len - 1 + ATTR_OFF; @@ -6880,6 +6895,10 @@ int hl_combine_attr(int char_attr, int prim_attr) new_en.ae_u.cterm.fg_color = spell_aep->ae_u.cterm.fg_color; if (spell_aep->ae_u.cterm.bg_color > 0) new_en.ae_u.cterm.bg_color = spell_aep->ae_u.cterm.bg_color; + if (spell_aep->fg_color >= 0) + new_en.fg_color = spell_aep->fg_color; + if (spell_aep->bg_color >= 0) + new_en.bg_color = spell_aep->bg_color; } } return get_attr_entry(&cterm_attr_table, &new_en); @@ -6974,11 +6993,11 @@ static void highlight_list_one(int id) didh = highlight_list_arg(id, didh, LIST_ATTR, sgp->sg_gui, NULL, "gui"); didh = highlight_list_arg(id, didh, LIST_STRING, - 0, sgp->sg_gui_fg_name, "guifg"); + 0, sgp->sg_rgb_fg_name, "guifg"); didh = highlight_list_arg(id, didh, LIST_STRING, - 0, sgp->sg_gui_bg_name, "guibg"); + 0, sgp->sg_rgb_bg_name, "guibg"); didh = highlight_list_arg(id, didh, LIST_STRING, - 0, sgp->sg_gui_sp_name, "guisp"); + 0, NULL, "guisp"); if (sgp->sg_link && !got_int) { (void)syn_list_header(didh, 9999, id); @@ -7092,10 +7111,10 @@ highlight_color ( return NULL; if (modec == 'g') { if (fg) - return HL_TABLE()[id - 1].sg_gui_fg_name; + return HL_TABLE()[id - 1].sg_rgb_fg_name; if (sp) - return HL_TABLE()[id - 1].sg_gui_sp_name; - return HL_TABLE()[id - 1].sg_gui_bg_name; + return NULL; + return HL_TABLE()[id - 1].sg_rgb_bg_name; } if (font || sp) return NULL; @@ -7192,9 +7211,14 @@ set_hl_attr ( if (sgp->sg_cterm_fg == 0 && sgp->sg_cterm_bg == 0) sgp->sg_cterm_attr = sgp->sg_cterm; else { - at_en.ae_attr = sgp->sg_cterm; + at_en.ae_attr = abstract_ui ? sgp->sg_gui : sgp->sg_cterm; at_en.ae_u.cterm.fg_color = sgp->sg_cterm_fg; at_en.ae_u.cterm.bg_color = sgp->sg_cterm_bg; + // FIXME(tarruda): The "unset value" for rgb is -1, but since hlgroup is + // initialized with 0(by garray functions), check for sg_rgb_{f,b}g_name + // before setting attr_entry->{f,g}g_color to a other than -1 + at_en.fg_color = sgp->sg_rgb_fg_name ? sgp->sg_rgb_fg : -1; + at_en.bg_color = sgp->sg_rgb_bg_name ? sgp->sg_rgb_bg : -1; sgp->sg_cterm_attr = get_attr_entry(&cterm_attr_table, &at_en); } } @@ -7633,6 +7657,200 @@ char_u *get_highlight_name(expand_T *xp, int idx) } +RgbValue name_to_color(uint8_t *name) +{ +#define RGB(r, g, b) ((r << 16) | (g << 8) | b) + static struct { + char *name; + RgbValue color; + } color_name_table[] = { + // Color names taken from + // http://www.rapidtables.com/web/color/RGB_Color.htm + {"Maroon", RGB(0x80, 0x00, 0x00)}, + {"DarkRed", RGB(0x8b, 0x00, 0x00)}, + {"Brown", RGB(0xa5, 0x2a, 0x2a)}, + {"Firebrick", RGB(0xb2, 0x22, 0x22)}, + {"Crimson", RGB(0xdc, 0x14, 0x3c)}, + {"Red", RGB(0xff, 0x00, 0x00)}, + {"Tomato", RGB(0xff, 0x63, 0x47)}, + {"Coral", RGB(0xff, 0x7f, 0x50)}, + {"IndianRed", RGB(0xcd, 0x5c, 0x5c)}, + {"LightCoral", RGB(0xf0, 0x80, 0x80)}, + {"DarkSalmon", RGB(0xe9, 0x96, 0x7a)}, + {"Salmon", RGB(0xfa, 0x80, 0x72)}, + {"LightSalmon", RGB(0xff, 0xa0, 0x7a)}, + {"OrangeRed", RGB(0xff, 0x45, 0x00)}, + {"DarkOrange", RGB(0xff, 0x8c, 0x00)}, + {"Orange", RGB(0xff, 0xa5, 0x00)}, + {"Gold", RGB(0xff, 0xd7, 0x00)}, + {"DarkGoldenRod", RGB(0xb8, 0x86, 0x0b)}, + {"GoldenRod", RGB(0xda, 0xa5, 0x20)}, + {"PaleGoldenRod", RGB(0xee, 0xe8, 0xaa)}, + {"DarkKhaki", RGB(0xbd, 0xb7, 0x6b)}, + {"Khaki", RGB(0xf0, 0xe6, 0x8c)}, + {"Olive", RGB(0x80, 0x80, 0x00)}, + {"Yellow", RGB(0xff, 0xff, 0x00)}, + {"YellowGreen", RGB(0x9a, 0xcd, 0x32)}, + {"DarkOliveGreen", RGB(0x55, 0x6b, 0x2f)}, + {"OliveDrab", RGB(0x6b, 0x8e, 0x23)}, + {"LawnGreen", RGB(0x7c, 0xfc, 0x00)}, + {"ChartReuse", RGB(0x7f, 0xff, 0x00)}, + {"GreenYellow", RGB(0xad, 0xff, 0x2f)}, + {"DarkGreen", RGB(0x00, 0x64, 0x00)}, + {"Green", RGB(0x00, 0x80, 0x00)}, + {"ForestGreen", RGB(0x22, 0x8b, 0x22)}, + {"Lime", RGB(0x00, 0xff, 0x00)}, + {"LimeGreen", RGB(0x32, 0xcd, 0x32)}, + {"LightGreen", RGB(0x90, 0xee, 0x90)}, + {"PaleGreen", RGB(0x98, 0xfb, 0x98)}, + {"DarkSeaGreen", RGB(0x8f, 0xbc, 0x8f)}, + {"MediumSpringGreen", RGB(0x00, 0xfa, 0x9a)}, + {"SpringGreen", RGB(0x00, 0xff, 0x7f)}, + {"SeaGreen", RGB(0x2e, 0x8b, 0x57)}, + {"MediumAquamarine", RGB(0x66, 0xcd, 0xaa)}, + {"MediumSeaGreen", RGB(0x3c, 0xb3, 0x71)}, + {"LightSeaGreen", RGB(0x20, 0xb2, 0xaa)}, + {"DarkSlateGray", RGB(0x2f, 0x4f, 0x4f)}, + {"Teal", RGB(0x00, 0x80, 0x80)}, + {"DarkCyan", RGB(0x00, 0x8b, 0x8b)}, + {"Aqua", RGB(0x00, 0xff, 0xff)}, + {"Cyan", RGB(0x00, 0xff, 0xff)}, + {"LightCyan", RGB(0xe0, 0xff, 0xff)}, + {"DarkTurquoise", RGB(0x00, 0xce, 0xd1)}, + {"Turquoise", RGB(0x40, 0xe0, 0xd0)}, + {"MediumTurquoise", RGB(0x48, 0xd1, 0xcc)}, + {"PaleTurquoise", RGB(0xaf, 0xee, 0xee)}, + {"Aquamarine", RGB(0x7f, 0xff, 0xd4)}, + {"PowderBlue", RGB(0xb0, 0xe0, 0xe6)}, + {"CadetBlue", RGB(0x5f, 0x9e, 0xa0)}, + {"SteelBlue", RGB(0x46, 0x82, 0xb4)}, + {"CornFlowerBlue", RGB(0x64, 0x95, 0xed)}, + {"DeepSkyBlue", RGB(0x00, 0xbf, 0xff)}, + {"DodgerBlue", RGB(0x1e, 0x90, 0xff)}, + {"LightBlue", RGB(0xad, 0xd8, 0xe6)}, + {"SkyBlue", RGB(0x87, 0xce, 0xeb)}, + {"LightSkyBlue", RGB(0x87, 0xce, 0xfa)}, + {"MidnightBlue", RGB(0x19, 0x19, 0x70)}, + {"Navy", RGB(0x00, 0x00, 0x80)}, + {"DarkBlue", RGB(0x00, 0x00, 0x8b)}, + {"MediumBlue", RGB(0x00, 0x00, 0xcd)}, + {"Blue", RGB(0x00, 0x00, 0xff)}, + {"RoyalBlue", RGB(0x41, 0x69, 0xe1)}, + {"BlueViolet", RGB(0x8a, 0x2b, 0xe2)}, + {"Indigo", RGB(0x4b, 0x00, 0x82)}, + {"DarkSlateBlue", RGB(0x48, 0x3d, 0x8b)}, + {"SlateBlue", RGB(0x6a, 0x5a, 0xcd)}, + {"MediumSlateBlue", RGB(0x7b, 0x68, 0xee)}, + {"MediumPurple", RGB(0x93, 0x70, 0xdb)}, + {"DarkMagenta", RGB(0x8b, 0x00, 0x8b)}, + {"DarkViolet", RGB(0x94, 0x00, 0xd3)}, + {"DarkOrchid", RGB(0x99, 0x32, 0xcc)}, + {"MediumOrchid", RGB(0xba, 0x55, 0xd3)}, + {"Purple", RGB(0x80, 0x00, 0x80)}, + {"Thistle", RGB(0xd8, 0xbf, 0xd8)}, + {"Plum", RGB(0xdd, 0xa0, 0xdd)}, + {"Violet", RGB(0xee, 0x82, 0xee)}, + {"Magenta", RGB(0xff, 0x00, 0xff)}, + {"Fuchsia", RGB(0xff, 0x00, 0xff)}, + {"Orchid", RGB(0xda, 0x70, 0xd6)}, + {"MediumVioletRed", RGB(0xc7, 0x15, 0x85)}, + {"PaleVioletRed", RGB(0xdb, 0x70, 0x93)}, + {"DeepPink", RGB(0xff, 0x14, 0x93)}, + {"HotPink", RGB(0xff, 0x69, 0xb4)}, + {"LightPink", RGB(0xff, 0xb6, 0xc1)}, + {"Pink", RGB(0xff, 0xc0, 0xcb)}, + {"AntiqueWhite", RGB(0xfa, 0xeb, 0xd7)}, + {"Beige", RGB(0xf5, 0xf5, 0xdc)}, + {"Bisque", RGB(0xff, 0xe4, 0xc4)}, + {"BlanchedAlmond", RGB(0xff, 0xeb, 0xcd)}, + {"Wheat", RGB(0xf5, 0xde, 0xb3)}, + {"Cornsilk", RGB(0xff, 0xf8, 0xdc)}, + {"LemonChiffon", RGB(0xff, 0xfa, 0xcd)}, + {"LightGoldenRodYellow", RGB(0xfa, 0xfa, 0xd2)}, + {"LightYellow", RGB(0xff, 0xff, 0xe0)}, + {"SaddleBrown", RGB(0x8b, 0x45, 0x13)}, + {"Sienna", RGB(0xa0, 0x52, 0x2d)}, + {"Chocolate", RGB(0xd2, 0x69, 0x1e)}, + {"Peru", RGB(0xcd, 0x85, 0x3f)}, + {"SandyBrown", RGB(0xf4, 0xa4, 0x60)}, + {"BurlyWood", RGB(0xde, 0xb8, 0x87)}, + {"Tan", RGB(0xd2, 0xb4, 0x8c)}, + {"RosyBrown", RGB(0xbc, 0x8f, 0x8f)}, + {"Moccasin", RGB(0xff, 0xe4, 0xb5)}, + {"NavajoWhite", RGB(0xff, 0xde, 0xad)}, + {"PeachPuff", RGB(0xff, 0xda, 0xb9)}, + {"MistyRose", RGB(0xff, 0xe4, 0xe1)}, + {"LavenderBlush", RGB(0xff, 0xf0, 0xf5)}, + {"Linen", RGB(0xfa, 0xf0, 0xe6)}, + {"Oldlace", RGB(0xfd, 0xf5, 0xe6)}, + {"PapayaWhip", RGB(0xff, 0xef, 0xd5)}, + {"SeaShell", RGB(0xff, 0xf5, 0xee)}, + {"MintCream", RGB(0xf5, 0xff, 0xfa)}, + {"SlateGray", RGB(0x70, 0x80, 0x90)}, + {"LightSlateGray", RGB(0x77, 0x88, 0x99)}, + {"LightSteelBlue", RGB(0xb0, 0xc4, 0xde)}, + {"Lavender", RGB(0xe6, 0xe6, 0xfa)}, + {"FloralWhite", RGB(0xff, 0xfa, 0xf0)}, + {"AliceBlue", RGB(0xf0, 0xf8, 0xff)}, + {"GhostWhite", RGB(0xf8, 0xf8, 0xff)}, + {"Honeydew", RGB(0xf0, 0xff, 0xf0)}, + {"Ivory", RGB(0xff, 0xff, 0xf0)}, + {"Azure", RGB(0xf0, 0xff, 0xff)}, + {"Snow", RGB(0xff, 0xfa, 0xfa)}, + {"Black", RGB(0x00, 0x00, 0x00)}, + {"DimGray", RGB(0x69, 0x69, 0x69)}, + {"DimGrey", RGB(0x69, 0x69, 0x69)}, + {"Gray", RGB(0x80, 0x80, 0x80)}, + {"Grey", RGB(0x80, 0x80, 0x80)}, + {"DarkGray", RGB(0xa9, 0xa9, 0xa9)}, + {"DarkGrey", RGB(0xa9, 0xa9, 0xa9)}, + {"Silver", RGB(0xc0, 0xc0, 0xc0)}, + {"LightGray", RGB(0xd3, 0xd3, 0xd3)}, + {"LightGrey", RGB(0xd3, 0xd3, 0xd3)}, + {"Gainsboro", RGB(0xdc, 0xdc, 0xdc)}, + {"WhiteSmoke", RGB(0xf5, 0xf5, 0xf5)}, + {"White", RGB(0xff, 0xff, 0xff)}, + // The color names below were taken from gui_x11.c in vim source + {"LightRed", RGB(0xff, 0xbb, 0xbb)}, + {"LightMagenta",RGB(0xff, 0xbb, 0xff)}, + {"DarkYellow", RGB(0xbb, 0xbb, 0x00)}, + {"Gray10", RGB(0x1a, 0x1a, 0x1a)}, + {"Grey10", RGB(0x1a, 0x1a, 0x1a)}, + {"Gray20", RGB(0x33, 0x33, 0x33)}, + {"Grey20", RGB(0x33, 0x33, 0x33)}, + {"Gray30", RGB(0x4d, 0x4d, 0x4d)}, + {"Grey30", RGB(0x4d, 0x4d, 0x4d)}, + {"Gray40", RGB(0x66, 0x66, 0x66)}, + {"Grey40", RGB(0x66, 0x66, 0x66)}, + {"Gray50", RGB(0x7f, 0x7f, 0x7f)}, + {"Grey50", RGB(0x7f, 0x7f, 0x7f)}, + {"Gray60", RGB(0x99, 0x99, 0x99)}, + {"Grey60", RGB(0x99, 0x99, 0x99)}, + {"Gray70", RGB(0xb3, 0xb3, 0xb3)}, + {"Grey70", RGB(0xb3, 0xb3, 0xb3)}, + {"Gray80", RGB(0xcc, 0xcc, 0xcc)}, + {"Grey80", RGB(0xcc, 0xcc, 0xcc)}, + {"Gray90", RGB(0xe5, 0xe5, 0xe5)}, + {"Grey90", RGB(0xe5, 0xe5, 0xe5)}, + {NULL, 0}, + }; + + if (name[0] == '#' && isxdigit(name[1]) && isxdigit(name[2]) + && isxdigit(name[3]) && isxdigit(name[4]) && isxdigit(name[5]) + && isxdigit(name[6]) && name[7] == NUL) { + // rgb hex string + return strtol((char *)(name + 1), NULL, 16); + } + + for (int i = 0; color_name_table[i].name != NULL; i++) { + if (!STRICMP(name, color_name_table[i].name)) { + return color_name_table[i].color; + } + } + + return -1; +} + /************************************** * End of Highlighting stuff * **************************************/ diff --git a/src/nvim/syntax.h b/src/nvim/syntax.h index a03bd1e604..9a284c8a8d 100644 --- a/src/nvim/syntax.h +++ b/src/nvim/syntax.h @@ -5,8 +5,6 @@ #include "nvim/buffer_defs.h" -typedef int guicolor_T; - /* * Terminal highlighting attribute bits. * Attributes above HL_ALL are used for syntax highlighting. diff --git a/src/nvim/syntax_defs.h b/src/nvim/syntax_defs.h index 11e342f870..abf7ea5a7d 100644 --- a/src/nvim/syntax_defs.h +++ b/src/nvim/syntax_defs.h @@ -3,6 +3,8 @@ #include "nvim/regexp_defs.h" +typedef int32_t RgbValue; + # define SST_MIN_ENTRIES 150 /* minimal size for state stack array */ # define SST_MAX_ENTRIES 1000 /* maximal size for state stack array */ # define SST_FIX_STATES 7 /* size of sst_stack[]. */ @@ -70,6 +72,7 @@ struct syn_state { */ typedef struct attr_entry { short ae_attr; /* HL_BOLD, etc. */ + RgbValue fg_color, bg_color; union { struct { char_u *start; /* start escape sequence */ diff --git a/src/nvim/ui.c b/src/nvim/ui.c index fb3325f163..9c58193e8c 100644 --- a/src/nvim/ui.c +++ b/src/nvim/ui.c @@ -316,8 +316,6 @@ static void highlight_stop(int mask) static void set_highlight_args(int mask, HlAttrs *attrs) { attrentry_T *aep = NULL; - attrs->foreground = -1; - attrs->background = -1; if (mask > HL_ALL) { aep = syn_cterm_attr2entry(mask); @@ -330,16 +328,8 @@ static void set_highlight_args(int mask, HlAttrs *attrs) attrs->undercurl = mask & HL_UNDERCURL; attrs->italic = mask & HL_ITALIC; attrs->reverse = mask & HL_INVERSE; - - if (aep && aep->ae_u.cterm.fg_color - && (cterm_normal_fg_color != aep->ae_u.cterm.fg_color)) { - attrs->foreground = aep->ae_u.cterm.fg_color - 1; - } - - if (aep && aep->ae_u.cterm.bg_color - && (cterm_normal_bg_color != aep->ae_u.cterm.bg_color)) { - attrs->background = aep->ae_u.cterm.bg_color - 1; - } + attrs->foreground = aep && aep->fg_color >= 0 ? aep->fg_color : normal_fg; + attrs->background = aep && aep->bg_color >= 0 ? aep->bg_color : normal_bg; } static void parse_abstract_ui_codes(uint8_t *ptr, int len) From f8c3a14dc32cd27df7f9772ab59690e56626d807 Mon Sep 17 00:00:00 2001 From: Thiago de Arruda Date: Mon, 8 Dec 2014 22:31:35 -0300 Subject: [PATCH 4/5] msgpack-rpc: Add remote_ui module The remote_ui module is an implementation of the UI layer, and it attaches UI instances that redirect redraw notifications to connected clients. --- src/nvim/msgpack_rpc/channel.c | 16 ++ src/nvim/msgpack_rpc/remote_ui.c | 280 +++++++++++++++++++++++++++++++ src/nvim/msgpack_rpc/remote_ui.h | 9 + 3 files changed, 305 insertions(+) create mode 100644 src/nvim/msgpack_rpc/remote_ui.c create mode 100644 src/nvim/msgpack_rpc/remote_ui.h diff --git a/src/nvim/msgpack_rpc/channel.c b/src/nvim/msgpack_rpc/channel.c index b6ac3fab82..4c35cce09a 100644 --- a/src/nvim/msgpack_rpc/channel.c +++ b/src/nvim/msgpack_rpc/channel.c @@ -10,6 +10,7 @@ #include "nvim/api/private/helpers.h" #include "nvim/api/vim.h" #include "nvim/msgpack_rpc/channel.h" +#include "nvim/msgpack_rpc/remote_ui.h" #include "nvim/os/event.h" #include "nvim/os/rstream.h" #include "nvim/os/rstream_defs.h" @@ -100,6 +101,17 @@ void channel_init(void) if (embedded_mode) { channel_from_stdio(); } + + if (abstract_ui) { + // Add handler for "attach_ui" + remote_ui_init(); + String method = cstr_as_string("attach_ui"); + MsgpackRpcRequestHandler handler = {.fn = remote_ui_attach, .defer = true}; + msgpack_rpc_add_method_handler(method, handler); + method = cstr_as_string("detach_ui"); + handler.fn = remote_ui_detach; + msgpack_rpc_add_method_handler(method, handler); + } } /// Teardown the module @@ -645,6 +657,10 @@ static void on_stdio_close(Event e) static void free_channel(Channel *channel) { + if (abstract_ui) { + remote_ui_disconnect(channel->id); + } + pmap_del(uint64_t)(channels, channel->id); msgpack_unpacker_free(channel->unpacker); diff --git a/src/nvim/msgpack_rpc/remote_ui.c b/src/nvim/msgpack_rpc/remote_ui.c new file mode 100644 index 0000000000..f980a77b4c --- /dev/null +++ b/src/nvim/msgpack_rpc/remote_ui.c @@ -0,0 +1,280 @@ +#include +#include +#include +#include + +#include "nvim/vim.h" +#include "nvim/ui.h" +#include "nvim/memory.h" +#include "nvim/map.h" +#include "nvim/msgpack_rpc/remote_ui.h" +#include "nvim/msgpack_rpc/channel.h" +#include "nvim/api/private/defs.h" +#include "nvim/api/private/helpers.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "msgpack_rpc/remote_ui.c.generated.h" +#endif + +typedef struct { + uint64_t channel_id; + Array buffer; +} UIData; + +static PMap(uint64_t) *connected_uis = NULL; + +void remote_ui_init(void) +{ + connected_uis = pmap_new(uint64_t)(); +} + +Object remote_ui_attach(uint64_t channel_id, uint64_t request_id, Array args, + Error *error) +{ + if (pmap_has(uint64_t)(connected_uis, channel_id)) { + api_set_error(error, Exception, _("UI already attached for channel")); + return NIL; + } + + if (args.size != 2 || args.items[0].type != kObjectTypeInteger + || args.items[1].type != kObjectTypeInteger + || args.items[0].data.integer <= 0 || args.items[1].data.integer <= 0) { + api_set_error(error, Validation, + _("Arguments must be a pair of positive integers " + "representing the remote screen width/height")); + return NIL; + } + UIData *data = xmalloc(sizeof(UIData)); + data->channel_id = channel_id; + data->buffer = (Array)ARRAY_DICT_INIT; + UI *ui = xcalloc(1, sizeof(UI)); + ui->width = (int)args.items[0].data.integer; + ui->height = (int)args.items[1].data.integer; + ui->data = data; + ui->resize = remote_ui_resize; + ui->clear = remote_ui_clear; + ui->eol_clear = remote_ui_eol_clear; + ui->cursor_goto = remote_ui_cursor_goto; + ui->cursor_on = remote_ui_cursor_on; + ui->cursor_off = remote_ui_cursor_off; + ui->mouse_on = remote_ui_mouse_on; + ui->mouse_off = remote_ui_mouse_off; + ui->insert_mode = remote_ui_insert_mode; + ui->normal_mode = remote_ui_normal_mode; + ui->set_scroll_region = remote_ui_set_scroll_region; + ui->scroll = remote_ui_scroll; + ui->highlight_set = remote_ui_highlight_set; + ui->put = remote_ui_put; + ui->bell = remote_ui_bell; + ui->visual_bell = remote_ui_visual_bell; + ui->flush = remote_ui_flush; + ui->suspend = remote_ui_suspend; + pmap_put(uint64_t)(connected_uis, channel_id, ui); + ui_attach(ui); + + return NIL; +} + +Object remote_ui_detach(uint64_t channel_id, uint64_t request_id, Array args, + Error *error) +{ + if (!pmap_has(uint64_t)(connected_uis, channel_id)) { + api_set_error(error, Exception, _("UI is not attached for channel")); + } + remote_ui_disconnect(channel_id); + + return NIL; +} + +void remote_ui_disconnect(uint64_t channel_id) +{ + UI *ui = pmap_get(uint64_t)(connected_uis, channel_id); + if (!ui) { + return; + } + UIData *data = ui->data; + // destroy pending screen updates + api_free_array(data->buffer); + pmap_del(uint64_t)(connected_uis, channel_id); + free(ui->data); + ui_detach(ui); + free(ui); +} + +static void push_call(UI *ui, char *name, Array args) +{ + Array call = ARRAY_DICT_INIT; + UIData *data = ui->data; + + // To optimize data transfer(especially for "put"), 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))); + } + + ADD(call, ARRAY_OBJ(args)); + kv_A(data->buffer, kv_size(data->buffer) - 1).data.array = call; +} + +static void remote_ui_resize(UI *ui, int width, int height) +{ + Array args = ARRAY_DICT_INIT; + ADD(args, INTEGER_OBJ(width)); + ADD(args, INTEGER_OBJ(height)); + push_call(ui, "resize", args); +} + +static void remote_ui_clear(UI *ui) +{ + Array args = ARRAY_DICT_INIT; + push_call(ui, "clear", args); +} + +static void remote_ui_eol_clear(UI *ui) +{ + Array args = ARRAY_DICT_INIT; + push_call(ui, "eol_clear", args); +} + +static void remote_ui_cursor_goto(UI *ui, int row, int col) +{ + Array args = ARRAY_DICT_INIT; + ADD(args, INTEGER_OBJ(row)); + ADD(args, INTEGER_OBJ(col)); + push_call(ui, "cursor_goto", args); +} + +static void remote_ui_cursor_on(UI *ui) +{ + Array args = ARRAY_DICT_INIT; + push_call(ui, "cursor_on", args); +} + +static void remote_ui_cursor_off(UI *ui) +{ + Array args = ARRAY_DICT_INIT; + push_call(ui, "cursor_off", args); +} + +static void remote_ui_mouse_on(UI *ui) +{ + Array args = ARRAY_DICT_INIT; + push_call(ui, "mouse_on", args); +} + +static void remote_ui_mouse_off(UI *ui) +{ + Array args = ARRAY_DICT_INIT; + push_call(ui, "mouse_off", args); +} + +static void remote_ui_insert_mode(UI *ui) +{ + Array args = ARRAY_DICT_INIT; + push_call(ui, "insert_mode", args); +} + +static void remote_ui_normal_mode(UI *ui) +{ + Array args = ARRAY_DICT_INIT; + push_call(ui, "normal_mode", args); +} + +static void remote_ui_set_scroll_region(UI *ui, int top, int bot, int left, + int right) +{ + Array args = ARRAY_DICT_INIT; + ADD(args, INTEGER_OBJ(top)); + ADD(args, INTEGER_OBJ(bot)); + ADD(args, INTEGER_OBJ(left)); + ADD(args, INTEGER_OBJ(right)); + push_call(ui, "set_scroll_region", args); +} + +static void remote_ui_scroll(UI *ui, int count) +{ + Array args = ARRAY_DICT_INIT; + ADD(args, INTEGER_OBJ(count)); + push_call(ui, "scroll", args); +} + +static void remote_ui_highlight_set(UI *ui, HlAttrs attrs) +{ + Array args = ARRAY_DICT_INIT; + Dictionary hl = ARRAY_DICT_INIT; + + if (attrs.bold) { + PUT(hl, "bold", BOOLEAN_OBJ(true)); + } + + if (attrs.standout) { + PUT(hl, "standout", BOOLEAN_OBJ(true)); + } + + if (attrs.underline) { + PUT(hl, "underline", BOOLEAN_OBJ(true)); + } + + if (attrs.undercurl) { + PUT(hl, "undercurl", BOOLEAN_OBJ(true)); + } + + if (attrs.italic) { + PUT(hl, "italic", BOOLEAN_OBJ(true)); + } + + if (attrs.reverse) { + PUT(hl, "reverse", BOOLEAN_OBJ(true)); + } + + if (attrs.foreground != -1) { + PUT(hl, "foreground", INTEGER_OBJ(attrs.foreground)); + } + + if (attrs.background != -1) { + PUT(hl, "background", INTEGER_OBJ(attrs.background)); + } + + ADD(args, DICTIONARY_OBJ(hl)); + push_call(ui, "highlight_set", args); +} + +static void remote_ui_put(UI *ui, uint8_t *data, size_t size) +{ + Array args = ARRAY_DICT_INIT; + String str = {.data = xmemdupz(data, size), .size = size}; + ADD(args, STRING_OBJ(str)); + push_call(ui, "put", args); +} + +static void remote_ui_bell(UI *ui) +{ + Array args = ARRAY_DICT_INIT; + push_call(ui, "bell", args); +} + +static void remote_ui_visual_bell(UI *ui) +{ + Array args = ARRAY_DICT_INIT; + push_call(ui, "visual_bell", args); +} + +static void remote_ui_flush(UI *ui) +{ + UIData *data = ui->data; + channel_send_event(data->channel_id, "redraw", data->buffer); + data->buffer = (Array)ARRAY_DICT_INIT; +} + +static void remote_ui_suspend(UI *ui) +{ + UIData *data = ui->data; + remote_ui_disconnect(data->channel_id); +} diff --git a/src/nvim/msgpack_rpc/remote_ui.h b/src/nvim/msgpack_rpc/remote_ui.h new file mode 100644 index 0000000000..8af86dc1b8 --- /dev/null +++ b/src/nvim/msgpack_rpc/remote_ui.h @@ -0,0 +1,9 @@ +#ifndef NVIM_MSGPACK_RPC_REMOTE_UI_H +#define NVIM_MSGPACK_RPC_REMOTE_UI_H + +#include "nvim/ui.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "msgpack_rpc/remote_ui.h.generated.h" +#endif +#endif // NVIM_MSGPACK_RPC_REMOTE_UI_H From 1192fbd08a054cece0b48dfb695e77e689997980 Mon Sep 17 00:00:00 2001 From: Thiago de Arruda Date: Mon, 8 Dec 2014 22:31:45 -0300 Subject: [PATCH 5/5] test: Add screen test facility - Add screen.lua which implements a remote screen to verify screen state by tests under functional/ui - Add some basic screen/highlight tests --- test/functional/helpers.lua | 51 ++- test/functional/ui/highlight_spec.lua | 184 +++++++++++ test/functional/ui/input_spec.lua | 40 +++ test/functional/ui/screen.lua | 380 +++++++++++++++++++++++ test/functional/ui/screen_basic_spec.lua | 224 +++++++++++++ 5 files changed, 851 insertions(+), 28 deletions(-) create mode 100644 test/functional/ui/highlight_spec.lua create mode 100644 test/functional/ui/input_spec.lua create mode 100644 test/functional/ui/screen.lua create mode 100644 test/functional/ui/screen_basic_spec.lua diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua index 15adc3b1c3..fc699d22a3 100644 --- a/test/functional/helpers.lua +++ b/test/functional/helpers.lua @@ -47,12 +47,6 @@ local function request(method, ...) error(rv[2]) end end - -- Make sure this will only return after all buffered characters have been - -- processed - if not loop_stopped then - -- Except when the loop has been stopped by a notification triggered - -- by the initial request, for example. - end return rv end @@ -70,23 +64,30 @@ local function call_and_stop_on_error(...) return result end -local function run(request_cb, notification_cb, setup_cb) +local function run(request_cb, notification_cb, setup_cb, timeout) + local on_request, on_notification, on_setup - local function on_request(method, args) - return call_and_stop_on_error(request_cb, method, args) + if request_cb then + function on_request(method, args) + return call_and_stop_on_error(request_cb, method, args) + end end - local function on_notification(method, args) - call_and_stop_on_error(notification_cb, method, args) + if notification_cb then + function on_notification(method, args) + call_and_stop_on_error(notification_cb, method, args) + end end - local function on_setup() - call_and_stop_on_error(setup_cb) + if setup_cb then + function on_setup() + call_and_stop_on_error(setup_cb) + end end loop_stopped = false loop_running = true - session:run(on_request, on_notification, on_setup) + session:run(on_request, on_notification, on_setup, timeout) loop_running = false if last_error then local err = last_error @@ -115,15 +116,6 @@ local function nvim_feed(input) end end -local function nvim_replace_termcodes(input) - -- small hack to stop from being replaced by the internal - -- representation(which is different and won't work for vim_input) - local temp_replacement = 'CCCCCCCCC@@@@@@@@@@' - input = input:gsub('<[Cc][-]@>', temp_replacement) - local rv = request('vim_replace_termcodes', input, false, true, true) - return rv:gsub(temp_replacement, '\000') -end - local function dedent(str) -- find minimum common indent across lines local indent = nil @@ -148,7 +140,7 @@ end local function feed(...) for _, v in ipairs({...}) do - nvim_feed(nvim_replace_termcodes(dedent(v))) + nvim_feed(dedent(v)) end end @@ -172,8 +164,11 @@ end local function insert(...) nvim_feed('i') - rawfeed(...) - nvim_feed(nvim_replace_termcodes('')) + for _, v in ipairs({...}) do + local escaped = v:gsub('<', '') + rawfeed(escaped) + end + nvim_feed('') end local function execute(...) @@ -182,8 +177,8 @@ local function execute(...) -- not a search command, prefix with colon nvim_feed(':') end - nvim_feed(v) - nvim_feed(nvim_replace_termcodes('')) + nvim_feed(v:gsub('<', '')) + nvim_feed('') end end diff --git a/test/functional/ui/highlight_spec.lua b/test/functional/ui/highlight_spec.lua new file mode 100644 index 0000000000..3c55c09f95 --- /dev/null +++ b/test/functional/ui/highlight_spec.lua @@ -0,0 +1,184 @@ +local helpers = require('test.functional.helpers') +local Screen = require('test.functional.ui.screen') +local clear, feed, nvim = helpers.clear, helpers.feed, helpers.nvim +local execute = helpers.execute + +describe('Default highlight groups', function() + -- Test the default attributes for highlight groups shown by the :highlight + -- command + local screen, hlgroup_colors + + setup(function() + hlgroup_colors = { + NonText = nvim('name_to_color', 'Blue'), + Question = nvim('name_to_color', 'SeaGreen') + } + end) + + before_each(function() + clear() + screen = Screen.new() + screen:attach() + end) + + after_each(function() + screen:detach() + end) + + it('window status bar', function() + screen:set_default_attr_ids({ + [1] = {reverse = true, bold = true}, -- StatusLine + [2] = {reverse = true} -- StatusLineNC + }) + execute('sp', 'vsp', 'vsp') + screen:expect([[ + ^ {2:|} {2:|} | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + {1:[No Name] }{2:[No Name] [No Name] }| + | + ~ | + ~ | + ~ | + ~ | + {2:[No Name] }| + | + ]]) + -- navigate to verify that the attributes are properly moved + feed('j') + screen:expect([[ + {2:|} {2:|} | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + {2:[No Name] [No Name] [No Name] }| + ^ | + ~ | + ~ | + ~ | + ~ | + {1:[No Name] }| + | + ]]) + -- note that when moving to a window with small width nvim will increase + -- the width of the new active window at the expense of a inactive window + -- (upstream vim has the same behavior) + feed('kl') + screen:expect([[ + {2:|}^ {2:|} | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + {2:[No Name] }{1:[No Name] }{2:[No Name] }| + | + ~ | + ~ | + ~ | + ~ | + {2:[No Name] }| + | + ]]) + feed('l') + screen:expect([[ + {2:|} {2:|}^ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + {2:[No Name] [No Name] }{1:[No Name] }| + | + ~ | + ~ | + ~ | + ~ | + {2:[No Name] }| + | + ]]) + feed('hh') + screen:expect([[ + ^ {2:|} {2:|} | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + ~ {2:|}~ {2:|}~ | + {1:[No Name] }{2:[No Name] [No Name] }| + | + ~ | + ~ | + ~ | + ~ | + {2:[No Name] }| + | + ]]) + end) + + it('insert mode text', function() + feed('i') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + {1:-- INSERT --} | + ]], {[1] = {bold = true}}) + end) + + it('end of file markers', function() + nvim('command', 'hi Normal guibg=black') + screen:expect([[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + | + ]], {[1] = {bold = true, foreground = hlgroup_colors.NonText}}) + end) + + it('"wait return" text', function() + feed(':ls') + screen:expect([[ + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + :ls | + 1 %a "[No Name]" line 1 | + {1:Press ENTER or type command to continue}^ | + ]], {[1] = {bold = true, foreground = hlgroup_colors.Question}}) + feed('') -- skip the "Press ENTER..." state or tests will hang + end) +end) diff --git a/test/functional/ui/input_spec.lua b/test/functional/ui/input_spec.lua new file mode 100644 index 0000000000..60a49c4ed7 --- /dev/null +++ b/test/functional/ui/input_spec.lua @@ -0,0 +1,40 @@ +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 + +describe('mappings', function() + local cid + + local add_mapping = function(mapping, send) + local str = 'mapped '..mapping + local cmd = "nnoremap "..mapping.." :call rpcnotify("..cid..", 'mapped', '" + ..send:gsub('<', '').."')" + execute(cmd) + end + + local check_mapping = function(mapping, expected) + feed(mapping) + eq({'notification', 'mapped', {expected}}, next_message()) + end + + before_each(function() + clear() + cid = nvim('get_api_info')[1] + add_mapping('', '') + add_mapping('', '') + add_mapping('', '') + add_mapping('', '') + end) + + it('ok', function() + check_mapping('', '') + check_mapping('', '') + check_mapping('', '') + check_mapping('', '') + check_mapping('', '') + check_mapping('', '') + check_mapping('', '') + check_mapping('', '') + check_mapping('', '') + end) +end) diff --git a/test/functional/ui/screen.lua b/test/functional/ui/screen.lua new file mode 100644 index 0000000000..ff22321e4e --- /dev/null +++ b/test/functional/ui/screen.lua @@ -0,0 +1,380 @@ +-- This module contains the Screen class, a complete Nvim screen implementation +-- designed for functional testing. The goal is to provide a simple and +-- intuitive API for verifying screen state after a set of actions. +-- +-- The screen class exposes a single assertion method, "Screen:expect". This +-- method takes a string representing the expected screen state and an optional +-- set of attribute identifiers for checking highlighted characters(more on +-- this later). +-- +-- The string passed to "expect" will be processed according to these rules: +-- +-- - Each line of the string represents and is matched individually against +-- a screen row. +-- - The entire string is stripped of common indentation +-- - Expected screen rows are stripped of the last character. The last +-- character should be used to write pipes(|) that make clear where the +-- screen ends +-- - The last line is stripped, so the string must have (row count + 1) +-- lines. +-- +-- Example usage: +-- +-- local screen = Screen.new(25, 10) +-- -- attach the screen to the current Nvim instance +-- screen:attach() +-- --enter insert mode and type some text +-- feed('ihello screen') +-- -- declare an expectation for the eventual screen state +-- screen:expect([[ +-- hello screen | +-- ~ | +-- ~ | +-- ~ | +-- ~ | +-- ~ | +-- ~ | +-- ~ | +-- ~ | +-- -- INSERT -- | +-- ]]) -- <- Last line is stripped +-- +-- Since screen updates are received asynchronously, "expect" is actually +-- specifying the eventual screen state. This is how "expect" works: It will +-- start the event loop with a timeout of 5 seconds. Each time it receives an +-- update the expected state will be checked against the updated state. +-- +-- If the expected state matches the current state, the event loop will be +-- stopped and "expect" will return. If the timeout expires, the last match +-- error will be reported and the test will fail. +-- +-- If the second argument is passed to "expect", the screen rows will be +-- transformed before being matched against the string lines. The +-- transformation rule is simple: Each substring "S" composed with characters +-- having the exact same set of attributes will be substituted by "{K:S}", +-- where K is a key associated the attribute set via the second argument of +-- "expect". +-- +-- Too illustrate how this works, let's say that in the above example we wanted +-- to assert that the "-- INSERT --" string is highlighted with the bold +-- attribute(which normally is), here's how the call to "expect" should look +-- like: +-- +-- screen:expect([[ +-- hello screen \ +-- ~ \ +-- ~ \ +-- ~ \ +-- ~ \ +-- ~ \ +-- ~ \ +-- ~ \ +-- ~ \ +-- {b:-- INSERT --} \ +-- ]], {b = {bold = true}}) +-- +-- In this case "b" is a string associated with the set composed of one +-- attribute: bold. Note that since the {b:} markup is not a real part of the +-- screen, the delimiter(|) had to be moved right +local helpers = require('test.functional.helpers') +local request, run, stop = helpers.request, helpers.run, helpers.stop +local eq, dedent = helpers.eq, helpers.dedent + +local Screen = {} +Screen.__index = Screen + +function Screen.new(width, height) + if not width then + width = 53 + end + if not height then + height = 14 + end + return setmetatable({ + _default_attr_ids = nil, + _width = width, + _height = height, + _rows = new_cell_grid(width, height), + _mode = 'normal', + _mouse_enabled = true, + _bell = false, + _visual_bell = false, + _suspended = true, + _attrs = {}, + _cursor = { + enabled = true, row = 1, col = 1 + }, + _scroll_region = { + top = 1, bot = height, left = 1, right = width + } + }, Screen) +end + +function Screen:set_default_attr_ids(attr_ids) + self._default_attr_ids = attr_ids +end + +function Screen:attach() + request('attach_ui', self._width, self._height) + self._suspended = false +end + +function Screen:detach() + request('detach_ui') + self._suspended = true +end + +function Screen:expect(expected, attr_ids) + -- remove the last line and dedent + expected = dedent(expected:gsub('\n[ ]+$', '')) + local expected_rows = {} + for row in expected:gmatch('[^\n]+') do + -- the last character should be the screen delimiter + row = row:sub(1, #row - 1) + table.insert(expected_rows, row) + end + local ids = attr_ids or self._default_attr_ids + self:_wait(function() + for i = 1, self._height do + local expected_row = expected_rows[i] + local actual_row = self:_row_repr(self._rows[i], ids) + if expected_row ~= actual_row then + return 'Row '..tostring(i)..' didnt match.\nExpected: "'.. + expected_row..'"\nActual: "'..actual_row..'"' + end + end + end) +end + +function Screen:_wait(check, timeout) + local err + local function notification_cb(method, args) + assert(method == 'redraw') + self:_redraw(args) + err = check() + if not err then + stop() + end + return true + end + run(nil, notification_cb, nil, timeout or 5000) + if err then + error(err) + end +end + +function Screen:_redraw(updates) + for _, update in ipairs(updates) do + -- print('--') + -- print(require('inspect')(update)) + local method = update[1] + for i = 2, #update do + local handler = self['_handle_'..method] + handler(self, unpack(update[i])) + end + -- print(self:_current_screen()) + end +end + +function Screen:_handle_resize(width, height) + self._rows = new_cell_grid(width, height) +end + +function Screen:_handle_clear() + self:_clear_block(1, self._height, 1, self._width) +end + +function Screen:_handle_eol_clear() + local row, col = self._cursor.row, self._cursor.col + self:_clear_block(row, 1, col, self._width - col) +end + +function Screen:_handle_cursor_goto(row, col) + self._cursor.row = row + 1 + self._cursor.col = col + 1 +end + +function Screen:_handle_cursor_on() + self._cursor.enabled = true +end + +function Screen:_handle_cursor_off() + self._cursor.enabled = false +end + +function Screen:_handle_mouse_on() + self._mouse_enabled = true +end + +function Screen:_handle_mouse_off() + self._mouse_enabled = false +end + +function Screen:_handle_insert_mode() + self._mode = 'insert' +end + +function Screen:_handle_normal_mode() + self._mode = 'normal' +end + +function Screen:_handle_set_scroll_region(top, bot, left, right) + self._scroll_region.top = top + 1 + self._scroll_region.bot = bot + 1 + self._scroll_region.left = left + 1 + self._scroll_region.right = right + 1 +end + +function Screen:_handle_scroll(count) + local top = self._scroll_region.top + local bot = self._scroll_region.bot + local left = self._scroll_region.left + local right = self._scroll_region.right + local start, stop, step + + if count > 0 then + start = top + stop = bot - count + step = 1 + else + start = bot + stop = top - count + step = -1 + end + + -- shift scroll region + for i = start, stop, step do + local target = self._rows[i] + local source = self._rows[i + count] + self:_copy_row_section(target, source, left, right) + end + + -- clear invalid rows + for i = stop + 1, stop + count, step do + self:_clear_row_section(i, left, right) + end +end + +function Screen:_handle_highlight_set(attrs) + self._attrs = attrs +end + +function Screen:_handle_put(str) + local cell = self._rows[self._cursor.row][self._cursor.col] + cell.text = str + cell.attrs = self._attrs + self._cursor.col = self._cursor.col + 1 +end + +function Screen:_handle_bell() + self._bell = true +end + +function Screen:_handle_visual_bell() + self._visual_bell = true +end + +function Screen:_handle_suspend() + self._suspended = true +end + +function Screen:_clear_block(top, lines, left, columns) + for i = top, top + lines - 1 do + self:_clear_row_section(i, left, left + columns - 1) + end +end + +function Screen:_clear_row_section(rownum, startcol, stopcol) + local row = self._rows[rownum] + for i = startcol, stopcol do + row[i].text = ' ' + row[i].attrs = {} + end +end + +function Screen:_copy_row_section(target, source, startcol, stopcol) + for i = startcol, stopcol do + target[i].text = source[i].text + target[i].attrs = source[i].attrs + end +end + +function Screen:_row_repr(row, attr_ids) + local rv = {} + local current_attr_id + for i = 1, self._width do + local attr_id = get_attr_id(attr_ids, row[i].attrs) + if current_attr_id and attr_id ~= current_attr_id then + -- close current attribute bracket, add it before any whitespace + -- up to the current cell + -- table.insert(rv, backward_find_meaningful(rv, i), '}') + table.insert(rv, '}') + current_attr_id = nil + end + if not current_attr_id and attr_id then + -- open a new attribute bracket + table.insert(rv, '{' .. attr_id .. ':') + current_attr_id = attr_id + end + if self._rows[self._cursor.row] == row and self._cursor.col == i then + table.insert(rv, '^') + else + table.insert(rv, row[i].text) + end + end + if current_attr_id then + table.insert(rv, '}') + end + -- return the line representation, but remove empty attribute brackets and + -- trailing whitespace + return table.concat(rv, '')--:gsub('%s+$', '') +end + + +function Screen:_current_screen() + -- get a string that represents the current screen state(debugging helper) + local rv = {} + for i = 1, self._height do + table.insert(rv, "'"..self:_row_repr(self._rows[i]).."'") + end + return table.concat(rv, '\n') +end + +function backward_find_meaningful(tbl, from) + for i = from or #tbl, 1, -1 do + if tbl[i] ~= ' ' then + return i + 1 + end + end + return from +end + +function new_cell_grid(width, height) + local rows = {} + for i = 1, height do + local cols = {} + for j = 1, width do + table.insert(cols, {text = ' ', attrs = {}}) + end + table.insert(rows, cols) + end + return rows +end + +function get_attr_id(attr_ids, attrs) + if not attr_ids then + return + end + for id, a in pairs(attr_ids) do + if a.bold == attrs.bold and a.standout == attrs.standout and + a.underline == attrs.underline and a.undercurl == attrs.undercurl and + a.italic == attrs.italic and a.reverse == attrs.reverse and + a.foreground == attrs.foreground and + a.background == attrs.background then + return id + end + end + return nil +end + +return Screen diff --git a/test/functional/ui/screen_basic_spec.lua b/test/functional/ui/screen_basic_spec.lua new file mode 100644 index 0000000000..a1110b3231 --- /dev/null +++ b/test/functional/ui/screen_basic_spec.lua @@ -0,0 +1,224 @@ +local helpers = require('test.functional.helpers') +local Screen = require('test.functional.ui.screen') +local clear, feed, execute = helpers.clear, helpers.feed, helpers.execute +local insert = helpers.insert + +describe('Screen', function() + local screen + + before_each(function() + clear() + screen = Screen.new() + screen:attach() + end) + + after_each(function() + screen:detach() + end) + + describe('window', function() + describe('split', function() + it('horizontal', function() + execute('sp') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + ~ | + ~ | + [No Name] | + | + ~ | + ~ | + ~ | + ~ | + [No Name] | + :sp | + ]]) + end) + + it('horizontal and resize', function() + execute('sp') + execute('resize 8') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + [No Name] | + | + ~ | + ~ | + [No Name] | + :resize 8 | + ]]) + end) + + it('horizontal and vertical', function() + execute('sp', 'vsp', 'vsp') + screen:expect([[ + ^ | | | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + [No Name] [No Name] [No Name] | + | + ~ | + ~ | + ~ | + ~ | + [No Name] | + | + ]]) + insert('hello') + screen:expect([[ + hell^ |hello |hello | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + [No Name] [+] [No Name] [+] [No Name] [+] | + hello | + ~ | + ~ | + ~ | + ~ | + [No Name] [+] | + | + ]]) + end) + end) + end) + + describe('tabnew', function() + it('creates a new buffer', function() + execute('sp', 'vsp', 'vsp') + insert('hello') + screen:expect([[ + hell^ |hello |hello | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + [No Name] [+] [No Name] [+] [No Name] [+] | + hello | + ~ | + ~ | + ~ | + ~ | + [No Name] [+] | + | + ]]) + execute('tabnew') + insert('hello2') + feed('h') + screen:expect([[ + 4+ [No Name] + [No Name] X| + hell^2 | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + | + ]]) + execute('tabprevious') + screen:expect([[ + 4+ [No Name] + [No Name] X| + hell^ |hello |hello | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + ~ |~ |~ | + [No Name] [+] [No Name] [+] [No Name] [+] | + hello | + ~ | + ~ | + ~ | + [No Name] [+] | + | + ]]) + end) + end) + + describe('insert mode', function() + it('move to next line with ', function() + feed('iline 1line 2') + screen:expect([[ + line 1 | + line 2 | + ^ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + -- INSERT -- | + ]]) + end) + end) + + describe('command mode', function() + it('typing commands', function() + feed(':ls') + screen:expect([[ + | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + :ls^ | + ]]) + end) + + it('execute command with multi-line output', function() + feed(':ls') + screen:expect([[ + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + :ls | + 1 %a "[No Name]" line 1 | + Press ENTER or type command to continue^ | + ]]) + feed('') -- skip the "Press ENTER..." state or tests will hang + end) + end) +end)