diff options
39 files changed, 857 insertions, 795 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index a47869a3c6..cb5cb09595 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -143,9 +143,9 @@ set(NVIM_VERSION_PATCH 0) set(NVIM_VERSION_PRERELEASE "-dev") # for package maintainers # API level -set(NVIM_API_LEVEL 10) # Bump this after any API change. +set(NVIM_API_LEVEL 11) # Bump this after any API change. set(NVIM_API_LEVEL_COMPAT 0) # Adjust this after a _breaking_ API change. -set(NVIM_API_PRERELEASE false) +set(NVIM_API_PRERELEASE true) set(NVIM_VERSION_BUILD_TYPE "${CMAKE_BUILD_TYPE}") # NVIM_VERSION_CFLAGS set further below. @@ -498,34 +498,30 @@ if(MSGPACK_HAS_FLOAT32) add_definitions(-DNVIM_MSGPACK_HAS_FLOAT32) endif() -option(FEAT_TUI "Enable the Terminal UI" ON) - -if(FEAT_TUI) - find_package(UNIBILIUM 2.0 REQUIRED) - include_directories(SYSTEM ${UNIBILIUM_INCLUDE_DIRS}) - - list(APPEND CMAKE_REQUIRED_INCLUDES "${UNIBILIUM_INCLUDE_DIRS}") - list(APPEND CMAKE_REQUIRED_LIBRARIES "${UNIBILIUM_LIBRARIES}") - check_c_source_compiles(" - #include <unibilium.h> - - int - main(void) - { - unibi_str_from_var(unibi_var_from_str(\"\")); - return unibi_num_from_var(unibi_var_from_num(0)); - } - " UNIBI_HAS_VAR_FROM) - list(REMOVE_ITEM CMAKE_REQUIRED_INCLUDES "${UNIBILIUM_INCLUDE_DIRS}") - list(REMOVE_ITEM CMAKE_REQUIRED_LIBRARIES "${UNIBILIUM_LIBRARIES}") - if(UNIBI_HAS_VAR_FROM) - add_definitions(-DNVIM_UNIBI_HAS_VAR_FROM) - endif() +find_package(UNIBILIUM 2.0 REQUIRED) +include_directories(SYSTEM ${UNIBILIUM_INCLUDE_DIRS}) + +list(APPEND CMAKE_REQUIRED_INCLUDES "${UNIBILIUM_INCLUDE_DIRS}") +list(APPEND CMAKE_REQUIRED_LIBRARIES "${UNIBILIUM_LIBRARIES}") +check_c_source_compiles(" +#include <unibilium.h> - find_package(LibTermkey 0.22 REQUIRED) - include_directories(SYSTEM ${LIBTERMKEY_INCLUDE_DIRS}) +int +main(void) +{ + unibi_str_from_var(unibi_var_from_str(\"\")); + return unibi_num_from_var(unibi_var_from_num(0)); +} +" UNIBI_HAS_VAR_FROM) +list(REMOVE_ITEM CMAKE_REQUIRED_INCLUDES "${UNIBILIUM_INCLUDE_DIRS}") +list(REMOVE_ITEM CMAKE_REQUIRED_LIBRARIES "${UNIBILIUM_LIBRARIES}") +if(UNIBI_HAS_VAR_FROM) + add_definitions(-DNVIM_UNIBI_HAS_VAR_FROM) endif() +find_package(LibTermkey 0.22 REQUIRED) +include_directories(SYSTEM ${LIBTERMKEY_INCLUDE_DIRS}) + find_package(LIBVTERM 0.3 REQUIRED) include_directories(SYSTEM ${LIBVTERM_INCLUDE_DIRS}) diff --git a/cmake.config/config.h.in b/cmake.config/config.h.in index 59be83fb5e..283a1d0c47 100644 --- a/cmake.config/config.h.in +++ b/cmake.config/config.h.in @@ -56,8 +56,6 @@ #endif #cmakedefine HAVE_FORKPTY -#cmakedefine FEAT_TUI - #ifndef UNIT_TESTING #cmakedefine LOG_LIST_ACTIONS #endif diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 3cd4578750..26679f0330 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -3471,6 +3471,12 @@ nvim_ui_pum_set_height({height}) *nvim_ui_pum_set_height()* Parameters: ~ • {height} Popupmenu height, must be greater than zero. +nvim_ui_set_focus({gained}) *nvim_ui_set_focus()* + Tells the nvim server if focus was gained or lost by the GUI. + + Attributes: ~ + |RPC| only + nvim_ui_set_option({name}, {value}) *nvim_ui_set_option()* TODO: Documentation diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index d6f6464f78..25e753aaed 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -101,6 +101,11 @@ The following new APIs or features were added. See https://github.com/neovim/neovim/pull/14537. +• |--remote-ui| option was added to connect to a remote instance and display + in it in a |TUI| in the local terminal. This can be used run a headless nvim + instance in the background and display its UI on demand, which previously + only was possible usiing a external UI implementation. + ============================================================================== CHANGED FEATURES *news-changes* @@ -109,6 +114,14 @@ The following changes to existing APIs or features add new behavior. • 'exrc' now supports `.nvim.lua` file. • 'exrc' is no longer marked deprecated. +• The |TUI| is changed to run in a separate process (previously, a separate + thread was used). This is not supposed to be a visible change to the user, + but might be the cause of subtle changes of behavior and bugs. + + Previously, the TUI could be disabled as a build time feature (+tui/-tui), + resulting in a nvim binary which only could be run headless or embedded + in an external process. As of this version, TUI is always avalibale. + ============================================================================== REMOVED FEATURES *news-removed* diff --git a/runtime/doc/remote.txt b/runtime/doc/remote.txt index 0c1e3438de..4610088ab0 100644 --- a/runtime/doc/remote.txt +++ b/runtime/doc/remote.txt @@ -52,6 +52,10 @@ The following command line arguments are available: *--remote-expr* --remote-expr {expr} Evaluate {expr} in server and print the result on stdout. + *--remote-ui* + --remote-ui Display the UI of the server in the terminal. + Fully interactive: keyboard and mouse input + are forwarded to the server. *--server* --server {addr} Connect to the named pipe or socket at the given address for executing remote commands. diff --git a/runtime/doc/ui.txt b/runtime/doc/ui.txt index 1cffe1f902..a2ae9f22ce 100644 --- a/runtime/doc/ui.txt +++ b/runtime/doc/ui.txt @@ -56,6 +56,8 @@ with these (optional) keys: - `stdin_fd` Read buffer from `fd` as if it was a stdin pipe This option can only used by |--embed| ui, see |ui-startup-stdin|. + `stdin_tty` Tells if `stdin` is a `tty` or not. + `stdout_tty` Tells if `stdout` is a `tty` or not. Specifying an unknown option is an error; UIs can check the |api-metadata| `ui_options` key for supported options. diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index 5a64128bcb..6b92d0caa8 100755 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -39,7 +39,6 @@ set(GENERATED_FUNCS_METADATA ${GENERATED_DIR}/api/private/funcs_metadata.generat set(GENERATED_UI_EVENTS ${GENERATED_DIR}/ui_events.generated.h) set(GENERATED_UI_EVENTS_CALL ${GENERATED_DIR}/ui_events_call.generated.h) set(GENERATED_UI_EVENTS_REMOTE ${GENERATED_DIR}/ui_events_remote.generated.h) -set(GENERATED_UI_EVENTS_BRIDGE ${GENERATED_DIR}/ui_events_bridge.generated.h) set(GENERATED_UI_EVENTS_CLIENT ${GENERATED_DIR}/ui_events_client.generated.h) set(GENERATED_UI_EVENTS_METADATA ${GENERATED_DIR}/api/private/ui_events_metadata.generated.h) set(GENERATED_EX_CMDS_ENUM ${GENERATED_INCLUDES_DIR}/ex_cmds_enum.generated.h) @@ -111,9 +110,6 @@ foreach(subdir viml viml/parser ) - if(${subdir} MATCHES "tui" AND NOT FEAT_TUI) - continue() - endif() file(MAKE_DIRECTORY ${GENERATED_DIR}/${subdir}) file(MAKE_DIRECTORY ${GENERATED_INCLUDES_DIR}/${subdir}) @@ -220,7 +216,6 @@ foreach(sfile ${NVIM_SOURCES} ${GENERATED_API_DISPATCH} "${GENERATED_UI_EVENTS_CALL}" "${GENERATED_UI_EVENTS_REMOTE}" - "${GENERATED_UI_EVENTS_BRIDGE}" "${GENERATED_KEYSETS}" "${GENERATED_UI_EVENTS_CLIENT}" ) @@ -318,7 +313,6 @@ add_custom_command( OUTPUT ${GENERATED_UI_EVENTS} ${GENERATED_UI_EVENTS_CALL} ${GENERATED_UI_EVENTS_REMOTE} - ${GENERATED_UI_EVENTS_BRIDGE} ${GENERATED_UI_EVENTS_METADATA} ${GENERATED_UI_EVENTS_CLIENT} COMMAND ${LUA_PRG} ${API_UI_EVENTS_GENERATOR} ${CMAKE_CURRENT_LIST_DIR} @@ -326,7 +320,6 @@ add_custom_command( ${GENERATED_UI_EVENTS} ${GENERATED_UI_EVENTS_CALL} ${GENERATED_UI_EVENTS_REMOTE} - ${GENERATED_UI_EVENTS_BRIDGE} ${GENERATED_UI_EVENTS_METADATA} ${GENERATED_UI_EVENTS_CLIENT} DEPENDS diff --git a/src/nvim/api/keysets.lua b/src/nvim/api/keysets.lua index 8ded9cfa5d..8f909e937f 100644 --- a/src/nvim/api/keysets.lua +++ b/src/nvim/api/keysets.lua @@ -126,6 +126,8 @@ return { "global_link"; "fallback"; "blend"; + "fg_indexed"; + "bg_indexed"; }; highlight_cterm = { "bold"; diff --git a/src/nvim/api/ui.c b/src/nvim/api/ui.c index aeccddb9ea..e4134133ac 100644 --- a/src/nvim/api/ui.c +++ b/src/nvim/api/ui.c @@ -14,9 +14,6 @@ #include "nvim/api/private/helpers.h" #include "nvim/api/ui.h" #include "nvim/channel.h" -#include "nvim/event/loop.h" -#include "nvim/event/wstream.h" -#include "nvim/globals.h" #include "nvim/grid.h" #include "nvim/highlight.h" #include "nvim/main.h" @@ -30,6 +27,7 @@ #include "nvim/types.h" #include "nvim/ui.h" #include "nvim/vim.h" +#include "nvim/window.h" typedef struct { uint64_t channel_id; @@ -285,6 +283,19 @@ void ui_attach(uint64_t channel_id, Integer width, Integer height, Boolean enabl api_free_dictionary(opts); } +/// Tells the nvim server if focus was gained or lost by the GUI +void nvim_ui_set_focus(uint64_t channel_id, Boolean gained, Error *error) + FUNC_API_SINCE(11) FUNC_API_REMOTE_ONLY +{ + if (!pmap_has(uint64_t)(&connected_uis, channel_id)) { + api_set_error(error, kErrorTypeException, + "UI not attached to channel: %" PRId64, channel_id); + return; + } + + do_autocmd_focusgained((bool)gained); +} + /// Deactivates UI events on the channel. /// /// Removes the client from the list of UIs. |nvim_list_uis()| @@ -404,6 +415,24 @@ static void ui_set_option(UI *ui, bool init, String name, Object value, Error *e return; } + if (strequal(name.data, "stdin_tty")) { + if (value.type != kObjectTypeBoolean) { + api_set_error(error, kErrorTypeValidation, "stdin_tty must be a Boolean"); + return; + } + stdin_isatty = value.data.boolean; + return; + } + + if (strequal(name.data, "stdout_tty")) { + if (value.type != kObjectTypeBoolean) { + api_set_error(error, kErrorTypeValidation, "stdout_tty must be a Boolean"); + return; + } + stdout_isatty = value.data.boolean; + return; + } + // LEGACY: Deprecated option, use `ext_cmdline` instead. bool is_popupmenu = strequal(name.data, "popupmenu_external"); diff --git a/src/nvim/api/ui_events.in.h b/src/nvim/api/ui_events.in.h index 21400862b9..d5c79272b7 100644 --- a/src/nvim/api/ui_events.in.h +++ b/src/nvim/api/ui_events.in.h @@ -31,7 +31,7 @@ void visual_bell(void) void flush(void) FUNC_API_SINCE(3) FUNC_API_REMOTE_IMPL; void suspend(void) - FUNC_API_SINCE(3) FUNC_API_BRIDGE_IMPL; + FUNC_API_SINCE(3); void set_title(String title) FUNC_API_SINCE(3); void set_icon(String icon) @@ -39,7 +39,7 @@ void set_icon(String icon) void screenshot(String path) FUNC_API_SINCE(7) FUNC_API_REMOTE_IMPL; void option_set(String name, Object value) - FUNC_API_SINCE(4) FUNC_API_BRIDGE_IMPL; + FUNC_API_SINCE(4); // Stop event is not exported as such, represented by EOF in the msgpack stream. void stop(void) FUNC_API_NOEXPORT; @@ -73,9 +73,9 @@ void default_colors_set(Integer rgb_fg, Integer rgb_bg, Integer rgb_sp, Integer Integer cterm_bg) FUNC_API_SINCE(4) FUNC_API_REMOTE_IMPL; void hl_attr_define(Integer id, HlAttrs rgb_attrs, HlAttrs cterm_attrs, Array info) - FUNC_API_SINCE(5) FUNC_API_REMOTE_IMPL FUNC_API_BRIDGE_IMPL; + FUNC_API_SINCE(5) FUNC_API_REMOTE_IMPL; void hl_group_set(String name, Integer id) - FUNC_API_SINCE(6) FUNC_API_BRIDGE_IMPL; + FUNC_API_SINCE(6); void grid_resize(Integer grid, Integer width, Integer height) FUNC_API_SINCE(5) FUNC_API_REMOTE_IMPL FUNC_API_COMPOSITOR_IMPL FUNC_API_CLIENT_IMPL; void grid_clear(Integer grid) @@ -112,8 +112,9 @@ void win_hide(Integer grid) FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY; void win_close(Integer grid) FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY; + void msg_set_pos(Integer grid, Integer row, Boolean scrolled, String sep_char) - FUNC_API_SINCE(6) FUNC_API_BRIDGE_IMPL FUNC_API_COMPOSITOR_IMPL; + FUNC_API_SINCE(6) FUNC_API_COMPOSITOR_IMPL; void win_viewport(Integer grid, Window win, Integer topline, Integer botline, Integer curline, Integer curcol, Integer line_count) @@ -149,11 +150,11 @@ void cmdline_block_hide(void) FUNC_API_SINCE(3) FUNC_API_REMOTE_ONLY; void wildmenu_show(Array items) - FUNC_API_SINCE(3) FUNC_API_REMOTE_IMPL FUNC_API_BRIDGE_IMPL; + FUNC_API_SINCE(3) FUNC_API_REMOTE_IMPL; void wildmenu_select(Integer selected) - FUNC_API_SINCE(3) FUNC_API_REMOTE_IMPL FUNC_API_BRIDGE_IMPL; + FUNC_API_SINCE(3) FUNC_API_REMOTE_IMPL; void wildmenu_hide(void) - FUNC_API_SINCE(3) FUNC_API_REMOTE_IMPL FUNC_API_BRIDGE_IMPL; + FUNC_API_SINCE(3) FUNC_API_REMOTE_IMPL; void msg_show(String kind, Array content, Boolean replace_last) FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY; diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c index ab6c22ff6c..3ed3f7edf2 100644 --- a/src/nvim/autocmd.c +++ b/src/nvim/autocmd.c @@ -2732,22 +2732,7 @@ void do_autocmd_uienter(uint64_t chanid, bool attached) // FocusGained -static void focusgained_event(void **argv) -{ - bool *gainedp = argv[0]; - do_autocmd_focusgained(*gainedp); - xfree(gainedp); -} - -void autocmd_schedule_focusgained(bool gained) -{ - bool *gainedp = xmalloc(sizeof(*gainedp)); - *gainedp = gained; - loop_schedule_deferred(&main_loop, - event_create(focusgained_event, 1, gainedp)); -} - -static void do_autocmd_focusgained(bool gained) +void do_autocmd_focusgained(bool gained) { static bool recursive = false; static Timestamp last_time = (time_t)0; diff --git a/src/nvim/event/libuv_process.c b/src/nvim/event/libuv_process.c index c5d3b94c95..811d96ff93 100644 --- a/src/nvim/event/libuv_process.c +++ b/src/nvim/event/libuv_process.c @@ -40,11 +40,19 @@ int libuv_process_spawn(LibuvProcess *uvproc) #endif uvproc->uvopts.exit_cb = exit_cb; uvproc->uvopts.cwd = proc->cwd; + uvproc->uvopts.stdio = uvproc->uvstdio; uvproc->uvopts.stdio_count = 3; uvproc->uvstdio[0].flags = UV_IGNORE; uvproc->uvstdio[1].flags = UV_IGNORE; uvproc->uvstdio[2].flags = UV_IGNORE; + + if (ui_client_forward_stdin) { + assert(UI_CLIENT_STDIN_FD == 3); + uvproc->uvopts.stdio_count = 4; + uvproc->uvstdio[3].data.fd = 0; + uvproc->uvstdio[3].flags = UV_INHERIT_FD; + } uvproc->uv.data = proc; if (proc->env) { diff --git a/src/nvim/event/libuv_process.h b/src/nvim/event/libuv_process.h index 8f987847d8..4472839944 100644 --- a/src/nvim/event/libuv_process.h +++ b/src/nvim/event/libuv_process.h @@ -10,7 +10,7 @@ typedef struct libuv_process { Process process; uv_process_t uv; uv_process_options_t uvopts; - uv_stdio_container_t uvstdio[3]; + uv_stdio_container_t uvstdio[4]; } LibuvProcess; static inline LibuvProcess libuv_process_init(Loop *loop, void *data) diff --git a/src/nvim/event/process.c b/src/nvim/event/process.c index e74e95669d..2fa789ac9a 100644 --- a/src/nvim/event/process.c +++ b/src/nvim/event/process.c @@ -14,6 +14,7 @@ #include "nvim/globals.h" #include "nvim/log.h" #include "nvim/macros.h" +#include "nvim/main.h" #include "nvim/os/process.h" #include "nvim/os/pty_process.h" #include "nvim/os/shell.h" @@ -35,6 +36,9 @@ void __gcov_flush(void); static bool process_is_tearing_down = false; +// Delay exit until handles are closed, to avoid deadlocks +static int exit_need_delay = 0; + /// @returns zero on success, or negative error code int process_spawn(Process *proc, bool in, bool out, bool err) FUNC_ATTR_NONNULL_ALL @@ -398,12 +402,41 @@ static void process_close_handles(void **argv) exit_need_delay--; } +static void exit_delay_cb(uv_timer_t *handle) +{ + uv_timer_stop(&main_loop.exit_delay_timer); + multiqueue_put(main_loop.fast_events, exit_event, 1, main_loop.exit_delay_timer.data); +} + +static void exit_event(void **argv) +{ + int status = (int)(intptr_t)argv[0]; + if (exit_need_delay) { + main_loop.exit_delay_timer.data = argv[0]; + uv_timer_start(&main_loop.exit_delay_timer, exit_delay_cb, 0, 0); + return; + } + + if (!exiting) { + os_exit(status); + } +} + +void exit_from_channel(int status) +{ + multiqueue_put(main_loop.fast_events, exit_event, 1, status); +} + static void on_process_exit(Process *proc) { Loop *loop = proc->loop; ILOG("exited: pid=%d status=%d stoptime=%" PRIu64, proc->pid, proc->status, proc->stopped_time); + if (ui_client_channel_id) { + exit_from_channel(proc->status); + } + // Process has terminated, but there could still be data to be read from the // OS. We are still in the libuv loop, so we cannot call code that polls for // more data directly. Instead delay the reading after the libuv loop by diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 1d4c7b4aab..5b0adba650 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -1931,9 +1931,11 @@ static char *do_one_cmd(char **cmdlinep, int flags, cstack_T *cstack, LineGetter profile_cmd(&ea, cstack, fgetline, cookie); - // May go to debug mode. If this happens and the ">quit" debug command is - // used, throw an interrupt exception and skip the next command. - dbg_check_breakpoint(&ea); + if (!exiting) { + // May go to debug mode. If this happens and the ">quit" debug command is + // used, throw an interrupt exception and skip the next command. + dbg_check_breakpoint(&ea); + } if (!ea.skip && got_int) { ea.skip = true; (void)do_intthrow(cstack); diff --git a/src/nvim/generators/gen_api_dispatch.lua b/src/nvim/generators/gen_api_dispatch.lua index d4fe455f82..55ab9fdf0e 100644 --- a/src/nvim/generators/gen_api_dispatch.lua +++ b/src/nvim/generators/gen_api_dispatch.lua @@ -60,6 +60,12 @@ for i = 6, #arg do if public and not fn.noexport then functions[#functions + 1] = tmp[j] function_names[fn.name] = true + if #fn.parameters >= 2 and fn.parameters[2][1] == 'Array' and fn.parameters[2][2] == 'uidata' then + -- function recieves the "args" as a parameter + fn.receives_array_args = true + -- remove the args parameter + table.remove(fn.parameters, 2) + end if #fn.parameters ~= 0 and fn.parameters[1][2] == 'channel_id' then -- this function should receive the channel id fn.receives_channel_id = true @@ -159,7 +165,7 @@ local exported_attributes = {'name', 'return_type', 'method', 'since', 'deprecated_since'} local exported_functions = {} for _,f in ipairs(functions) do - if not (startswith(f.name, "nvim__") or f.name == "nvim_error_event") then + if not (startswith(f.name, "nvim__") or f.name == "nvim_error_event" or f.name == "redraw") then local f_exported = {} for _,attr in ipairs(exported_attributes) do f_exported[attr] = f[attr] @@ -264,11 +270,13 @@ for i = 1, #functions do output:write('\n '..rt..' '..converted..';') end output:write('\n') - output:write('\n if (args.size != '..#fn.parameters..') {') - output:write('\n api_set_error(error, kErrorTypeException, \ - "Wrong number of arguments: expecting '..#fn.parameters..' but got %zu", args.size);') - output:write('\n goto cleanup;') - output:write('\n }\n') + if not fn.receives_array_args then + output:write('\n if (args.size != '..#fn.parameters..') {') + output:write('\n api_set_error(error, kErrorTypeException, \ + "Wrong number of arguments: expecting '..#fn.parameters..' but got %zu", args.size);') + output:write('\n goto cleanup;') + output:write('\n }\n') + end -- Validation/conversion for each argument for j = 1, #fn.parameters do @@ -350,12 +358,28 @@ for i = 1, #functions do if fn.receives_channel_id then -- if the function receives the channel id, pass it as first argument if #args > 0 or fn.can_fail then - output:write('channel_id, '..call_args) + output:write('channel_id, ') + if fn.receives_array_args then + -- if the function recieves the array args, pass it the second argument + output:write('args, ') + end + output:write(call_args) else output:write('channel_id') + if fn.receives_array_args then + output:write(', args') + end end else - output:write(call_args) + if fn.receives_array_args then + if #args > 0 or fn.call_fail then + output:write('args, '..call_args) + else + output:write('args') + end + else + output:write(call_args) + end end if fn.arena_return then diff --git a/src/nvim/generators/gen_api_ui_events.lua b/src/nvim/generators/gen_api_ui_events.lua index ea66be7ee8..c9a2e3ff66 100755 --- a/src/nvim/generators/gen_api_ui_events.lua +++ b/src/nvim/generators/gen_api_ui_events.lua @@ -3,14 +3,13 @@ local mpack = require('mpack') local nvimdir = arg[1] package.path = nvimdir .. '/?.lua;' .. package.path -assert(#arg == 8) +assert(#arg == 7) local input = io.open(arg[2], 'rb') local proto_output = io.open(arg[3], 'wb') local call_output = io.open(arg[4], 'wb') local remote_output = io.open(arg[5], 'wb') -local bridge_output = io.open(arg[6], 'wb') -local metadata_output = io.open(arg[7], 'wb') -local client_output = io.open(arg[8], 'wb') +local metadata_output = io.open(arg[6], 'wb') +local client_output = io.open(arg[7], 'wb') local c_grammar = require('generators.c_grammar') local events = c_grammar.grammar:match(input:read('*all')) @@ -119,62 +118,6 @@ for i = 1, #events do remote_output:write(' push_call(ui, "'..ev.name..'", args);\n') remote_output:write('}\n\n') end - - if not ev.bridge_impl and not ev.noexport then - local send, argv, recv, recv_argv, recv_cleanup = '', '', '', '', '' - local argc = 1 - for j = 1, #ev.parameters do - local param = ev.parameters[j] - local copy = 'copy_'..param[2] - if param[1] == 'String' then - send = send..' String copy_'..param[2]..' = copy_string('..param[2]..', NULL);\n' - argv = argv..', '..copy..'.data, INT2PTR('..copy..'.size)' - recv = (recv..' String '..param[2].. - ' = (String){.data = argv['..argc..'],'.. - '.size = (size_t)argv['..(argc+1)..']};\n') - recv_argv = recv_argv..', '..param[2] - recv_cleanup = recv_cleanup..' api_free_string('..param[2]..');\n' - argc = argc+2 - elseif param[1] == 'Array' then - send = send..' Array '..copy..' = copy_array('..param[2]..', NULL);\n' - argv = argv..', '..copy..'.items, INT2PTR('..copy..'.size)' - recv = (recv..' Array '..param[2].. - ' = (Array){.items = argv['..argc..'],'.. - '.size = (size_t)argv['..(argc+1)..']};\n') - recv_argv = recv_argv..', '..param[2] - recv_cleanup = recv_cleanup..' api_free_array('..param[2]..');\n' - argc = argc+2 - elseif param[1] == 'Object' then - send = send..' Object *'..copy..' = xmalloc(sizeof(Object));\n' - send = send..' *'..copy..' = copy_object('..param[2]..', NULL);\n' - argv = argv..', '..copy - recv = recv..' Object '..param[2]..' = *(Object *)argv['..argc..'];\n' - recv_argv = recv_argv..', '..param[2] - recv_cleanup = (recv_cleanup..' api_free_object('..param[2]..');\n'.. - ' xfree(argv['..argc..']);\n') - argc = argc+1 - elseif param[1] == 'Integer' or param[1] == 'Boolean' then - argv = argv..', INT2PTR('..param[2]..')' - recv_argv = recv_argv..', PTR2INT(argv['..argc..'])' - argc = argc+1 - else - assert(false) - end - end - bridge_output:write('static void ui_bridge_'..ev.name.. - '_event(void **argv)\n{\n') - bridge_output:write(' UI *ui = UI(argv[0]);\n') - bridge_output:write(recv) - bridge_output:write(' ui->'..ev.name..'(ui'..recv_argv..');\n') - bridge_output:write(recv_cleanup) - bridge_output:write('}\n\n') - - bridge_output:write('static void ui_bridge_'..ev.name) - write_signature(bridge_output, ev, 'UI *ui') - bridge_output:write('\n{\n') - bridge_output:write(send) - bridge_output:write(' UI_BRIDGE_CALL(ui, '..ev.name..', '..argc..', ui'..argv..');\n}\n\n') - end end if not (ev.remote_only and ev.remote_impl) then diff --git a/src/nvim/globals.h b/src/nvim/globals.h index 52a48a8389..cce74df9a8 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -328,9 +328,6 @@ EXTERN sctx_T current_sctx INIT(= { 0, 0, 0 }); // ID of the current channel making a client API call EXTERN uint64_t current_channel_id INIT(= 0); -// ID of the client channel. Used by ui client -EXTERN uint64_t ui_client_channel_id INIT(= 0); - EXTERN bool did_source_packages INIT(= false); // Scope information for the code that indirectly triggered the current @@ -494,9 +491,9 @@ EXTERN bool exiting INIT(= false); // internal value of v:dying EXTERN int v_dying INIT(= 0); // is stdin a terminal? -EXTERN int stdin_isatty INIT(= true); +EXTERN bool stdin_isatty INIT(= true); // is stdout a terminal? -EXTERN int stdout_isatty INIT(= true); +EXTERN bool stdout_isatty INIT(= true); /// filedesc set by embedder for reading first buffer like `cmd | nvim -` EXTERN int stdin_fd INIT(= -1); @@ -1088,8 +1085,6 @@ typedef enum { // Only filled for Win32. EXTERN char windowsVersion[20] INIT(= { 0 }); -EXTERN int exit_need_delay INIT(= 0); - /// While executing a regexp and set to OPTION_MAGIC_ON or OPTION_MAGIC_OFF this /// overrules p_magic. Otherwise set to OPTION_MAGIC_NOT_SET. EXTERN optmagic_T magic_overruled INIT(= OPTION_MAGIC_NOT_SET); diff --git a/src/nvim/highlight.c b/src/nvim/highlight.c index 774551242f..2c55e840f1 100644 --- a/src/nvim/highlight.c +++ b/src/nvim/highlight.c @@ -933,22 +933,26 @@ HlAttrs dict2hlattrs(Dict(highlight) *dict, bool use_rgb, int *link_id, Error *e CHECK_FLAG(dict, mask, italic, , HL_ITALIC); CHECK_FLAG(dict, mask, reverse, , HL_INVERSE); CHECK_FLAG(dict, mask, strikethrough, , HL_STRIKETHROUGH); + if (use_rgb) { + CHECK_FLAG(dict, mask, fg_indexed, , HL_FG_INDEXED); + CHECK_FLAG(dict, mask, bg_indexed, , HL_BG_INDEXED); + } CHECK_FLAG(dict, mask, nocombine, , HL_NOCOMBINE); CHECK_FLAG(dict, mask, default, _, HL_DEFAULT); if (HAS_KEY(dict->fg)) { - fg = object_to_color(dict->fg, "fg", true, err); + fg = object_to_color(dict->fg, "fg", use_rgb, err); } else if (HAS_KEY(dict->foreground)) { - fg = object_to_color(dict->foreground, "foreground", true, err); + fg = object_to_color(dict->foreground, "foreground", use_rgb, err); } if (ERROR_SET(err)) { return hlattrs; } if (HAS_KEY(dict->bg)) { - bg = object_to_color(dict->bg, "bg", true, err); + bg = object_to_color(dict->bg, "bg", use_rgb, err); } else if (HAS_KEY(dict->background)) { - bg = object_to_color(dict->background, "background", true, err); + bg = object_to_color(dict->background, "background", use_rgb, err); } if (ERROR_SET(err)) { return hlattrs; @@ -1035,11 +1039,11 @@ HlAttrs dict2hlattrs(Dict(highlight) *dict, bool use_rgb, int *link_id, Error *e } } - // apply gui mask as default for cterm mask - if (!cterm_mask_provided) { - cterm_mask = mask; - } if (use_rgb) { + // apply gui mask as default for cterm mask + if (!cterm_mask_provided) { + cterm_mask = mask; + } hlattrs.rgb_ae_attr = mask; hlattrs.rgb_bg_color = bg; hlattrs.rgb_fg_color = fg; @@ -1049,9 +1053,9 @@ HlAttrs dict2hlattrs(Dict(highlight) *dict, bool use_rgb, int *link_id, Error *e hlattrs.cterm_fg_color = ctermfg == -1 ? 0 : ctermfg + 1; hlattrs.cterm_ae_attr = cterm_mask; } else { - hlattrs.cterm_bg_color = ctermbg == -1 ? 0 : ctermbg + 1; - hlattrs.cterm_fg_color = ctermfg == -1 ? 0 : ctermfg + 1; - hlattrs.cterm_ae_attr = cterm_mask; + hlattrs.cterm_bg_color = bg == -1 ? 0 : bg + 1; + hlattrs.cterm_fg_color = fg == -1 ? 0 : fg + 1; + hlattrs.cterm_ae_attr = mask; } return hlattrs; diff --git a/src/nvim/main.c b/src/nvim/main.c index 9e14e911ff..c20be202e8 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -97,6 +97,7 @@ #include "nvim/msgpack_rpc/helpers.h" #include "nvim/msgpack_rpc/server.h" #include "nvim/os/signal.h" +#include "nvim/tui/tui.h" // values for "window_layout" enum { @@ -278,8 +279,6 @@ int main(int argc, char **argv) // argument list "global_alist". command_line_scan(¶ms); - open_script_files(¶ms); - nlua_init(); TIME_MSG("init lua interpreter"); @@ -291,9 +290,29 @@ int main(int argc, char **argv) } } - server_init(params.listen_addr); + bool use_builtin_ui = (!headless_mode && !embedded_mode && !silent_mode); + + // don't bind the server yet, if we are using builtin ui. + // This will be done when nvim server has been forked from the ui process + if (!use_builtin_ui) { + server_init(params.listen_addr); + } + if (params.remote) { - remote_request(¶ms, params.remote, params.server_addr, argc, argv); + remote_request(¶ms, params.remote, params.server_addr, argc, argv, + use_builtin_ui); + } + + bool remote_ui = (ui_client_channel_id != 0); + + if (use_builtin_ui && !remote_ui) { + ui_client_forward_stdin = !params.input_isatty; + uint64_t rv = ui_client_start_server(params.argc, params.argv); + if (!rv) { + os_errmsg("Failed to start Nvim server!\n"); + getout(1); + } + ui_client_channel_id = rv; } if (GARGCOUNT > 0) { @@ -349,17 +368,16 @@ int main(int argc, char **argv) input_start(STDIN_FILENO); } + if (ui_client_channel_id) { + ui_client_run(remote_ui); // NORETURN + } + // Wait for UIs to set up Nvim or show early messages // and prompts (--cmd, swapfile dialog, …). bool use_remote_ui = (embedded_mode && !headless_mode); - bool use_builtin_ui = (!headless_mode && !embedded_mode && !silent_mode); - if (use_remote_ui || use_builtin_ui) { + if (use_remote_ui) { TIME_MSG("waiting for UI"); - if (use_remote_ui) { - remote_ui_wait_for_attach(); - } else { - ui_builtin_start(); - } + remote_ui_wait_for_attach(); TIME_MSG("done waiting for UI"); firstwin->w_prev_height = firstwin->w_height; // may have changed } @@ -370,12 +388,13 @@ int main(int argc, char **argv) win_new_screensize(); TIME_MSG("clear screen"); - if (ui_client_channel_id) { - ui_client_init(ui_client_channel_id); - ui_client_execute(ui_client_channel_id); - abort(); // unreachable + // Handle "foo | nvim". EDIT_FILE may be overwritten now. #6299 + if (edit_stdin(¶ms)) { + params.edit_type = EDIT_STDIN; } + open_script_files(¶ms); + // Default mappings (incl. menus) Error err = ERROR_INIT; Object o = NLUA_EXEC_STATIC("return vim._init_default_mappings()", @@ -384,6 +403,7 @@ int main(int argc, char **argv) api_clear_error(&err); assert(o.type == kObjectTypeNil); api_free_object(o); + TIME_MSG("init default mappings"); init_default_autocmds(); @@ -483,7 +503,7 @@ int main(int argc, char **argv) // writing end of the pipe doesn't like, e.g., in case stdin and stderr // are the same terminal: "cat | vim -". // Using autocommands here may cause trouble... - if ((params.edit_type == EDIT_STDIN || stdin_fd >= 0) && !recoverymode) { + if (params.edit_type == EDIT_STDIN && !recoverymode) { read_stdin(); } @@ -837,15 +857,24 @@ static uint64_t server_connect(char *server_addr, const char **errmsg) /// Handle remote subcommands static void remote_request(mparm_T *params, int remote_args, char *server_addr, int argc, - char **argv) + char **argv, bool ui_only) { + bool is_ui = strequal(argv[remote_args], "--remote-ui"); + if (ui_only && !is_ui) { + // TODO(bfredl): this implies always starting the TUI. + // if we be smart we could delay this past should_exit + return; + } + const char *connect_error = NULL; uint64_t chan = server_connect(server_addr, &connect_error); Object rvobj = OBJECT_INIT; - if (strequal(argv[remote_args], "--remote-ui-test")) { + if (is_ui) { if (!chan) { - emsg(connect_error); + os_errmsg("Remote ui failed to start: "); + os_errmsg(connect_error); + os_errmsg("\n"); exit(1); } @@ -925,14 +954,14 @@ static void remote_request(mparm_T *params, int remote_args, char *server_addr, /// Decides whether text (as opposed to commands) will be read from stdin. /// @see EDIT_STDIN -static bool edit_stdin(bool explicit, mparm_T *parmp) +static bool edit_stdin(mparm_T *parmp) { bool implicit = !headless_mode - && !embedded_mode + && !(embedded_mode && stdin_fd <= 0) && (!exmode_active || parmp->input_neverscript) && !parmp->input_isatty && parmp->scriptin == NULL; // `-s -` was not given. - return explicit || implicit; + return parmp->had_stdin_file || implicit; } /// Scan the command line arguments. @@ -941,7 +970,6 @@ static void command_line_scan(mparm_T *parmp) int argc = parmp->argc; char **argv = parmp->argv; int argv_idx; // index in argv[n][] - bool had_stdin_file = false; // found explicit "-" argument bool had_minmin = false; // found "--" argument int want_argument; // option argument with argument long n; @@ -978,7 +1006,7 @@ static void command_line_scan(mparm_T *parmp) && parmp->edit_type != EDIT_STDIN) { mainerr(err_too_many_args, argv[0]); } - had_stdin_file = true; + parmp->had_stdin_file = true; parmp->edit_type = EDIT_STDIN; } argv_idx = -1; // skip to next argument @@ -1345,7 +1373,7 @@ scripterror: path_fix_case(p); #endif - int alist_fnum_flag = edit_stdin(had_stdin_file, parmp) + int alist_fnum_flag = edit_stdin(parmp) ? 1 // add buffer nr after exp. : 2; // add buffer number now and use curbuf alist_add(&global_alist, p, alist_fnum_flag); @@ -1373,11 +1401,6 @@ scripterror: xfree(swcmd); } - // Handle "foo | nvim". EDIT_FILE may be overwritten now. #6299 - if (edit_stdin(had_stdin_file, parmp)) { - parmp->edit_type = EDIT_STDIN; - } - TIME_MSG("parsing arguments"); } @@ -1527,11 +1550,16 @@ static void open_script_files(mparm_T *parmp) if (parmp->scriptin) { int error; if (strequal(parmp->scriptin, "-")) { - const int stdin_dup_fd = os_dup(STDIN_FILENO); + int stdin_dup_fd; + if (stdin_fd > 0) { + stdin_dup_fd = stdin_fd; + } else { + stdin_dup_fd = os_dup(STDIN_FILENO); #ifdef MSWIN - // Replace the original stdin with the console input handle. - os_replace_stdin_to_conin(); + // Replace the original stdin with the console input handle. + os_replace_stdin_to_conin(); #endif + } FileDescriptor *const stdin_dup = file_open_fd_new(&error, stdin_dup_fd, kFileReadOnly|kFileNonBlocking); assert(stdin_dup != NULL); diff --git a/src/nvim/main.h b/src/nvim/main.h index 4cf8dfe026..c39fc1ed4a 100644 --- a/src/nvim/main.h +++ b/src/nvim/main.h @@ -45,6 +45,7 @@ typedef struct { char *scriptin; // -s {filename} char *scriptout; // -w/-W {filename} bool scriptout_append; // append (-w) instead of overwrite (-W) + bool had_stdin_file; // explicit - as a file to edit } mparm_T; #ifdef INCLUDE_GENERATED_DECLARATIONS diff --git a/src/nvim/msgpack_rpc/channel.c b/src/nvim/msgpack_rpc/channel.c index 516af20fe9..4d011dc079 100644 --- a/src/nvim/msgpack_rpc/channel.c +++ b/src/nvim/msgpack_rpc/channel.c @@ -250,8 +250,8 @@ static void parse_msgpack(Channel *channel) ui_client_event_raw_line(p->grid_line_event); } else if (p->ui_handler.fn != NULL && p->result.type == kObjectTypeArray) { p->ui_handler.fn(p->result.data.array); - arena_mem_free(arena_finish(&p->arena)); } + arena_mem_free(arena_finish(&p->arena)); } else if (p->type == kMessageTypeResponse) { ChannelCallFrame *frame = kv_last(channel->rpc.call_stack); if (p->request_id != frame->request_id) { @@ -299,7 +299,7 @@ static void handle_request(Channel *channel, Unpacker *p, Array args) assert(p->type == kMessageTypeRequest || p->type == kMessageTypeNotification); if (!p->handler.fn) { - send_error(channel, p->type, p->request_id, p->unpack_error.msg); + send_error(channel, p->handler, p->type, p->request_id, p->unpack_error.msg); api_clear_error(&p->unpack_error); arena_mem_free(arena_finish(&p->arena)); return; @@ -358,6 +358,7 @@ static void request_event(void **argv) msgpack_packer response; msgpack_packer_init(&response, &out_buffer, msgpack_sbuffer_write); channel_write(channel, serialize_response(channel->id, + e->handler, e->type, e->request_id, &error, @@ -440,11 +441,13 @@ static void internal_read_event(void **argv) wstream_release_wbuffer(buffer); } -static void send_error(Channel *chan, MessageType type, uint32_t id, char *err) +static void send_error(Channel *chan, MsgpackRpcRequestHandler handler, MessageType type, + uint32_t id, char *err) { Error e = ERROR_INIT; api_set_error(&e, kErrorTypeException, "%s", err); channel_write(chan, serialize_response(chan->id, + handler, type, id, &e, @@ -543,26 +546,8 @@ void rpc_close(Channel *channel) channel_decref(channel); if (channel->streamtype == kChannelStreamStdio - || channel->id == ui_client_channel_id) { - multiqueue_put(main_loop.fast_events, exit_event, 0); - } -} - -static void exit_delay_cb(uv_timer_t *handle) -{ - uv_timer_stop(&main_loop.exit_delay_timer); - multiqueue_put(main_loop.fast_events, exit_event, 0); -} - -static void exit_event(void **argv) -{ - if (exit_need_delay) { - uv_timer_start(&main_loop.exit_delay_timer, exit_delay_cb, 0, 0); - return; - } - - if (!exiting) { - os_exit(0); + || (channel->id == ui_client_channel_id && channel->streamtype != kChannelStreamProc)) { + exit_from_channel(0); } } @@ -612,18 +597,27 @@ static WBuffer *serialize_request(uint64_t channel_id, uint32_t request_id, cons return rv; } -static WBuffer *serialize_response(uint64_t channel_id, MessageType type, uint32_t response_id, - Error *err, Object arg, msgpack_sbuffer *sbuffer) +static WBuffer *serialize_response(uint64_t channel_id, MsgpackRpcRequestHandler handler, + MessageType type, uint32_t response_id, Error *err, Object arg, + msgpack_sbuffer *sbuffer) { msgpack_packer pac; msgpack_packer_init(&pac, sbuffer, msgpack_sbuffer_write); if (ERROR_SET(err) && type == kMessageTypeNotification) { - Array args = ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(err->type)); - ADD(args, STRING_OBJ(cstr_to_string(err->msg))); - msgpack_rpc_serialize_request(0, cstr_as_string("nvim_error_event"), - args, &pac); - api_free_array(args); + if (handler.fn == handle_nvim_paste) { + // TODO(bfredl): this is pretty much ad-hoc. maybe TUI and UI:s should be + // allowed to ask nvim to just scream directly in the users face + // instead of sending nvim_error_event, in general. + semsg("paste: %s", err->msg); + api_clear_error(err); + } else { + Array args = ARRAY_DICT_INIT; + ADD(args, INTEGER_OBJ(err->type)); + ADD(args, STRING_OBJ(cstr_to_string(err->msg))); + msgpack_rpc_serialize_request(0, cstr_as_string("nvim_error_event"), + args, &pac); + api_free_array(args); + } } else { msgpack_rpc_serialize_response(response_id, err, arg, &pac); } diff --git a/src/nvim/msgpack_rpc/unpacker.c b/src/nvim/msgpack_rpc/unpacker.c index 897ea1f768..44a16beb48 100644 --- a/src/nvim/msgpack_rpc/unpacker.c +++ b/src/nvim/msgpack_rpc/unpacker.c @@ -298,7 +298,7 @@ error: // // When method is "grid_line", we furthermore decode a cell at a time like: // -// <0>[2, "redraw", <10>[{11}["grid_line", <13>[g, r, c, [<14>[cell], <14>[cell], ...]], ...], <11>[...], ...]] +// <0>[2, "redraw", <10>[{11}["grid_line", <14>[g, r, c, [<15>[cell], <15>[cell], ...]], ...], <11>[...], ...]] // // where [cell] is [char, repeat, attr], where 'repeat' and 'attr' is optional @@ -318,17 +318,19 @@ bool unpacker_advance(Unpacker *p) } } - if (p->state >= 10 && p->state != 12) { + if (p->state >= 10 && p->state != 13) { if (!unpacker_parse_redraw(p)) { return false; } - if (p->state == 14) { + if (p->state == 15) { // grid_line event already unpacked goto done; } else { + assert(p->state == 12); // unpack other ui events using mpack_parse() p->arena = (Arena)ARENA_EMPTY; + p->state = 13; } } @@ -355,11 +357,11 @@ done: case 2: p->state = 0; return true; - case 12: - case 14: + case 13: + case 15: p->ncalls--; if (p->ncalls > 0) { - p->state = (p->state == 14) ? 13 : 12; + p->state = (p->state == 15) ? 14 : 12; } else if (p->nevents > 0) { p->state = 11; } else { @@ -428,14 +430,14 @@ redo: } return true; } else { - p->state = 13; + p->state = 14; p->arena = (Arena)ARENA_EMPTY; p->grid_line_event = arena_alloc(&p->arena, sizeof *p->grid_line_event, true); g = p->grid_line_event; } FALLTHROUGH; - case 13: + case 14: NEXT_TYPE(tok, MPACK_TOKEN_ARRAY); int eventarrsize = (int)tok.length; if (eventarrsize != 4) { @@ -456,10 +458,10 @@ redo: p->read_ptr = data; p->read_size = size; - p->state = 14; + p->state = 15; FALLTHROUGH; - case 14: + case 15: assert(g->icell < g->ncells); NEXT_TYPE(tok, MPACK_TOKEN_ARRAY); @@ -513,6 +515,9 @@ redo: } goto redo; + case 12: + return true; + default: abort(); } diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index 9171f79c37..c6066597f0 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -32,6 +32,7 @@ #endif #include "nvim/event/rstream.h" #include "nvim/msgpack_rpc/channel.h" +#include "nvim/ui.h" #define KEY_BUFFER_SIZE 0xfff @@ -219,16 +220,11 @@ static void tinput_wait_enqueue(void **argv) const size_t len = rbuffer_size(input->key_buffer); String keys = { .data = xmallocz(len), .size = len }; rbuffer_read(input->key_buffer, keys.data, len); - if (ui_client_channel_id) { - Array args = ARRAY_DICT_INIT; - ADD(args, STRING_OBJ(keys)); // 'data' - ADD(args, BOOLEAN_OBJ(true)); // 'crlf' - ADD(args, INTEGER_OBJ(input->paste)); // 'phase' - rpc_send_event(ui_client_channel_id, "nvim_paste", args); - } else { - multiqueue_put(main_loop.events, tinput_paste_event, 3, - keys.data, keys.size, (intptr_t)input->paste); - } + Array args = ARRAY_DICT_INIT; + ADD(args, STRING_OBJ(keys)); // 'data' + ADD(args, BOOLEAN_OBJ(true)); // 'crlf' + ADD(args, INTEGER_OBJ(input->paste)); // 'phase' + rpc_send_event(ui_client_channel_id, "nvim_paste", args); if (input->paste == 1) { // Paste phase: "continue" input->paste = 2; @@ -237,60 +233,22 @@ static void tinput_wait_enqueue(void **argv) } else { // enqueue input for the main thread or Nvim server RBUFFER_UNTIL_EMPTY(input->key_buffer, buf, len) { const String keys = { .data = buf, .size = len }; - size_t consumed; - if (ui_client_channel_id) { - Array args = ARRAY_DICT_INIT; - Error err = ERROR_INIT; - ADD(args, STRING_OBJ(copy_string(keys, NULL))); - // TODO(bfredl): could be non-blocking now with paste? - ArenaMem res_mem = NULL; - Object result = rpc_send_call(ui_client_channel_id, "nvim_input", args, &res_mem, &err); - consumed = result.type == kObjectTypeInteger ? (size_t)result.data.integer : 0; - arena_mem_free(res_mem); - } else { - consumed = input_enqueue(keys); - } - if (consumed) { - rbuffer_consumed(input->key_buffer, consumed); - } + Array args = ARRAY_DICT_INIT; + ADD(args, STRING_OBJ(copy_string(keys, NULL))); + // NOTE: This is non-blocking and won't check partially processed input, + // but should be fine as all big sends are handled with nvim_paste, not nvim_input + rpc_send_event(ui_client_channel_id, "nvim_input", args); + rbuffer_consumed(input->key_buffer, len); rbuffer_reset(input->key_buffer); - if (consumed < len) { - break; - } } } - uv_mutex_lock(&input->key_buffer_mutex); - input->waiting = false; - uv_cond_signal(&input->key_buffer_cond); - uv_mutex_unlock(&input->key_buffer_mutex); -} - -static void tinput_paste_event(void **argv) -{ - String keys = { .data = argv[0], .size = (size_t)argv[1] }; - intptr_t phase = (intptr_t)argv[2]; - - Error err = ERROR_INIT; - nvim_paste(keys, true, phase, &err); - if (ERROR_SET(&err)) { - semsg("paste: %s", err.msg); - api_clear_error(&err); - } - - api_free_string(keys); } static void tinput_flush(TermInput *input, bool wait_until_empty) { size_t drain_boundary = wait_until_empty ? 0 : 0xff; do { - uv_mutex_lock(&input->key_buffer_mutex); - loop_schedule_fast(&main_loop, event_create(tinput_wait_enqueue, 1, input)); - input->waiting = true; - while (input->waiting) { - uv_cond_wait(&input->key_buffer_cond, &input->key_buffer_mutex); - } - uv_mutex_unlock(&input->key_buffer_mutex); + tinput_wait_enqueue((void **)&input); } while (rbuffer_size(input->key_buffer) > drain_boundary); } @@ -570,7 +528,10 @@ static bool handle_focus_event(TermInput *input) bool focus_gained = *rbuffer_get(input->read_stream.buffer, 2) == 'I'; // Advance past the sequence rbuffer_consumed(input->read_stream.buffer, 3); - autocmd_schedule_focusgained(focus_gained); + + Array args = ARRAY_DICT_INIT; + ADD(args, BOOLEAN_OBJ(focus_gained)); + rpc_send_event(ui_client_channel_id, "nvim_ui_set_focus", args); return true; } return false; @@ -617,10 +578,15 @@ static HandleState handle_bracketed_paste(TermInput *input) return kNotApplicable; } -static void set_bg_deferred(void **argv) +static void set_bg(char *bgvalue) { - char *bgvalue = argv[0]; - set_tty_background(bgvalue); + if (ui_client_attached) { + Array args = ARRAY_DICT_INIT; + ADD(args, STRING_OBJ(cstr_to_string("term_background"))); + ADD(args, STRING_OBJ(cstr_as_string(xstrdup(bgvalue)))); + + rpc_send_event(ui_client_channel_id, "nvim_ui_set_option", args); + } } // During startup, tui.c requests the background color (see `ext.get_bg`). @@ -702,10 +668,11 @@ static HandleState handle_background_color(TermInput *input) double g = (double)rgb[1] / (double)rgb_max[1]; double b = (double)rgb[2] / (double)rgb_max[2]; double luminance = (0.299 * r) + (0.587 * g) + (0.114 * b); // CCIR 601 - char *bgvalue = luminance < 0.5 ? "dark" : "light"; + bool is_dark = luminance < 0.5; + char *bgvalue = is_dark ? "dark" : "light"; DLOG("bg response: %s", bgvalue); - loop_schedule_deferred(&main_loop, - event_create(set_bg_deferred, 1, bgvalue)); + ui_client_bg_respose = is_dark ? kTrue : kFalse; + set_bg(bgvalue); input->waiting_for_bg_response = 0; } else if (!done && !bad) { // An incomplete sequence was found, waiting for the next input. diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index e32e48ce91..984f96354a 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -1,7 +1,7 @@ // This is an open source non-commercial project. Dear PVS-Studio, please check // it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com -// Terminal UI functions. Invoked (by ui_bridge.c) on the TUI thread. +// Terminal UI functions. Invoked (by UI_CALL) on the UI process. #include <assert.h> #include <signal.h> @@ -18,6 +18,7 @@ #include "nvim/api/private/helpers.h" #include "nvim/api/vim.h" #include "nvim/ascii.h" +#include "nvim/cursor_shape.h" #include "nvim/event/defs.h" #include "nvim/event/loop.h" #include "nvim/event/multiqueue.h" @@ -37,18 +38,15 @@ #include "nvim/os/input.h" #include "nvim/os/os.h" #include "nvim/os/signal.h" -#include "nvim/ui.h" -#include "nvim/vim.h" #ifdef MSWIN # include "nvim/os/os_win_console.h" #endif -#include "nvim/cursor_shape.h" -#include "nvim/macros.h" #include "nvim/tui/input.h" #include "nvim/tui/terminfo.h" #include "nvim/tui/tui.h" #include "nvim/ugrid.h" -#include "nvim/ui_bridge.h" +#include "nvim/ui.h" +#include "nvim/vim.h" // Space reserved in two output buffers to make the cursor normal or invisible // when flushing. No existing terminal will require 32 bytes to do that. @@ -91,7 +89,7 @@ typedef struct { } Rect; struct TUIData { - UIBridgeData *bridge; + UI *ui; Loop *loop; unibi_var_t params[9]; char buf[OUTBUF_SIZE]; @@ -108,8 +106,8 @@ struct TUIData { uv_pipe_t pipe; } output_handle; bool out_isatty; - SignalWatcher winch_handle, cont_handle; - bool cont_received; + SignalWatcher winch_handle; + uv_timer_t startup_delay_timer; UGrid grid; kvec_t(Rect) invalid_regions; int row, col; @@ -159,18 +157,18 @@ struct TUIData { int get_extkeys; } unibi_ext; char *space_buf; + bool stopped; }; static int got_winch = 0; static bool cursor_style_enabled = false; - #ifdef INCLUDE_GENERATED_DECLARATIONS # include "tui/tui.c.generated.h" #endif UI *tui_start(void) { - UI *ui = xcalloc(1, sizeof(UI)); // Freed by ui_bridge_stop(). + UI *ui = xcalloc(1, sizeof(UI)); // Freed by tui_data_destroy(). ui->stop = tui_stop; ui->grid_resize = tui_grid_resize; ui->grid_clear = tui_grid_clear; @@ -199,14 +197,39 @@ UI *tui_start(void) ui->ui_ext[kUILinegrid] = true; ui->ui_ext[kUITermColors] = true; - return ui_bridge_attach(ui, tui_main, tui_scheduler); + TUIData *data = xcalloc(1, sizeof(TUIData)); + ui->data = data; + data->ui = ui; + data->is_starting = true; + data->screenshot = NULL; + data->stopped = false; + data->loop = &main_loop; + kv_init(data->invalid_regions); + signal_watcher_init(data->loop, &data->winch_handle, ui); + + // TODO(bfredl): zero hl is empty, send this explicitly? + kv_push(data->attrs, HLATTRS_INIT); + + data->input.tk_ti_hook_fn = tui_tk_ti_getstr; + tinput_init(&data->input, &main_loop); + ugrid_init(&data->grid); + tui_terminal_start(ui); + + uv_timer_init(&data->loop->uv, &data->startup_delay_timer); + data->startup_delay_timer.data = ui; + uv_timer_start(&data->startup_delay_timer, after_startup_cb, + 100, 0); + + ui_attach_impl(ui, 0); + + return ui; } void tui_enable_extkeys(TUIData *data) { TermInput input = data->input; unibi_term *ut = data->ut; - UI *ui = data->bridge->ui; + UI *ui = data->ui; switch (input.extkeys_type) { case kExtkeysCSIu: @@ -237,13 +260,6 @@ static size_t unibi_pre_fmt_str(TUIData *data, unsigned int unibi_index, char *b return unibi_run(str, data->params, buf, len); } -static void termname_set_event(void **argv) -{ - char *termname = argv[0]; - set_tty_option("term", termname); - // Do not free termname, it is freed by set_tty_option. -} - static void terminfo_start(UI *ui) { TUIData *data = ui->data; @@ -294,22 +310,19 @@ static void terminfo_start(UI *ui) #endif // Set up unibilium/terminfo. - char *termname = NULL; + ui_client_termname = NULL; if (term) { - os_env_var_lock(); data->ut = unibi_from_term(term); - os_env_var_unlock(); if (data->ut) { - termname = xstrdup(term); - data->term = xstrdup(term); + ui_client_termname = xstrdup(term); + if (!data->term) { + data->term = xstrdup(term); + } } } if (!data->ut) { - data->ut = terminfo_from_builtin(term, &termname); + data->ut = terminfo_from_builtin(term, &ui_client_termname); } - // Update 'term' option. - loop_schedule_deferred(&main_loop, - event_create(termname_set_event, 1, termname)); // None of the following work over SSH; see :help TERM . const char *colorterm = os_getenv("COLORTERM"); @@ -443,13 +456,18 @@ static void tui_terminal_start(UI *ui) { TUIData *data = ui->data; data->print_attr_id = -1; - ugrid_init(&data->grid); terminfo_start(ui); tui_guess_size(ui); signal_watcher_start(&data->winch_handle, sigwinch_cb, SIGWINCH); tinput_start(&data->input); } +static void after_startup_cb(uv_timer_t *handle) +{ + UI *ui = handle->data; + tui_terminal_after_startup(ui); +} + static void tui_terminal_after_startup(UI *ui) FUNC_ATTR_NONNULL_ALL { @@ -461,98 +479,50 @@ static void tui_terminal_after_startup(UI *ui) flush_buf(ui); } +/// stop the terminal but allow it to restart later (like after suspend) static void tui_terminal_stop(UI *ui) { TUIData *data = ui->data; if (uv_is_closing(STRUCT_CAST(uv_handle_t, &data->output_handle))) { // Race between SIGCONT (tui.c) and SIGHUP (os/signal.c)? #8075 ELOG("TUI already stopped (race?)"); - ui->data = NULL; // Flag UI as "stopped". + data->stopped = true; return; } tinput_stop(&data->input); signal_watcher_stop(&data->winch_handle); terminfo_stop(ui); - ugrid_free(&data->grid); } static void tui_stop(UI *ui) { + TUIData *data = ui->data; tui_terminal_stop(ui); - ui->data = NULL; // Flag UI as "stopped". + tinput_destroy(&data->input); + data->stopped = true; + signal_watcher_close(&data->winch_handle, NULL); + uv_close((uv_handle_t *)&data->startup_delay_timer, NULL); + ui_detach_impl(ui, 0); } /// Returns true if UI `ui` is stopped. static bool tui_is_stopped(UI *ui) { - return ui->data == NULL; + TUIData *data = ui->data; + return data->stopped; } -/// Main function of the TUI thread. -static void tui_main(UIBridgeData *bridge, UI *ui) +#ifdef EXITFREE +void tui_free_all_mem(UI *ui) { - Loop tui_loop; - loop_init(&tui_loop, NULL); - TUIData *data = xcalloc(1, sizeof(TUIData)); - ui->data = data; - data->bridge = bridge; - data->loop = &tui_loop; - data->is_starting = true; - data->screenshot = NULL; - kv_init(data->invalid_regions); - signal_watcher_init(data->loop, &data->winch_handle, ui); - signal_watcher_init(data->loop, &data->cont_handle, data); -#ifdef UNIX - signal_watcher_start(&data->cont_handle, sigcont_cb, SIGCONT); -#endif - - // TODO(bfredl): zero hl is empty, send this explicitly? - kv_push(data->attrs, HLATTRS_INIT); - - data->input.tk_ti_hook_fn = tui_tk_ti_getstr; - tinput_init(&data->input, &tui_loop); - tui_terminal_start(ui); - - // Allow main thread to continue, we are ready to handle UI callbacks. - CONTINUE(bridge); - - // "Active" loop: first ~100 ms of startup. - for (size_t ms = 0; ms < 100 && !tui_is_stopped(ui);) { - ms += (loop_poll_events(&tui_loop, 20) ? 20 : 1); - } - if (!tui_is_stopped(ui)) { - tui_terminal_after_startup(ui); - } - // "Passive" (I/O-driven) loop: TUI thread "main loop". - while (!tui_is_stopped(ui)) { - loop_poll_events(&tui_loop, -1); // tui_loop.events is never processed - } - - ui_bridge_stopped(bridge); - tinput_destroy(&data->input); - signal_watcher_stop(&data->cont_handle); - signal_watcher_close(&data->cont_handle, NULL); - signal_watcher_close(&data->winch_handle, NULL); - loop_close(&tui_loop, false); + TUIData *data = ui->data; + ugrid_free(&data->grid); kv_destroy(data->invalid_regions); kv_destroy(data->attrs); xfree(data->space_buf); xfree(data->term); xfree(data); -} - -/// Handoff point between the main (ui_bridge) thread and the TUI thread. -static void tui_scheduler(Event event, void *d) -{ - UI *ui = d; - TUIData *data = ui->data; - loop_schedule_fast(data->loop, event); // `tui_loop` local to tui_main(). -} - -#ifdef UNIX -static void sigcont_cb(SignalWatcher *watcher, int signum, void *data) -{ - ((TUIData *)data)->cont_received = true; + xfree(ui); } #endif @@ -565,7 +535,6 @@ static void sigwinch_cb(SignalWatcher *watcher, int signum, void *data) } tui_guess_size(ui); - ui_schedule_refresh(); } static bool attrs_differ(UI *ui, int id1, int id2, bool rgb) @@ -1324,6 +1293,9 @@ static void tui_grid_scroll(UI *ui, Integer g, Integer startrow, // -V751 static void tui_hl_attr_define(UI *ui, Integer id, HlAttrs attrs, HlAttrs cterm_attrs, Array info) { TUIData *data = ui->data; + attrs.cterm_ae_attr = cterm_attrs.cterm_ae_attr; + attrs.cterm_fg_color = cterm_attrs.cterm_fg_color; + attrs.cterm_bg_color = cterm_attrs.cterm_bg_color; kv_a(data->attrs, (size_t)id) = attrs; } @@ -1421,32 +1393,13 @@ static void show_verbose_terminfo(TUIData *data) ADD(end_fold, STRING_OBJ(cstr_to_string("Title"))); ADD(chunks, ARRAY_OBJ(end_fold)); - if (ui_client_channel_id) { - Array args = ARRAY_DICT_INIT; - ADD(args, ARRAY_OBJ(chunks)); - ADD(args, BOOLEAN_OBJ(true)); // history - Dictionary opts = ARRAY_DICT_INIT; - PUT(opts, "verbose", BOOLEAN_OBJ(true)); - ADD(args, DICTIONARY_OBJ(opts)); - rpc_send_event(ui_client_channel_id, "nvim_echo", args); - } else { - loop_schedule_deferred(&main_loop, event_create(verbose_terminfo_event, 2, - chunks.items, chunks.size)); - } -} - -static void verbose_terminfo_event(void **argv) -{ - Array chunks = { .items = argv[0], .size = (size_t)argv[1] }; - Dict(echo_opts) opts = { .verbose = BOOLEAN_OBJ(true) }; - Error err = ERROR_INIT; - nvim_echo(chunks, true, &opts, &err); - api_free_array(chunks); - if (ERROR_SET(&err)) { - fprintf(stderr, "TUI bought the farm: %s\n", err.msg); - exit(1); - } - api_clear_error(&err); + Array args = ARRAY_DICT_INIT; + ADD(args, ARRAY_OBJ(chunks)); + ADD(args, BOOLEAN_OBJ(true)); // history + Dictionary opts = ARRAY_DICT_INIT; + PUT(opts, "verbose", BOOLEAN_OBJ(true)); + ADD(args, DICTIONARY_OBJ(opts)); + rpc_send_event(ui_client_channel_id, "nvim_echo", args); } #ifdef UNIX @@ -1456,38 +1409,28 @@ static void suspend_event(void **argv) TUIData *data = ui->data; bool enable_mouse = data->mouse_enabled; tui_terminal_stop(ui); - data->cont_received = false; stream_set_blocking(input_global_fd(), true); // normalize stream (#2598) - signal_stop(); + kill(0, SIGTSTP); - signal_start(); - while (!data->cont_received) { - // poll the event loop until SIGCONT is received - loop_poll_events(data->loop, -1); - } + tui_terminal_start(ui); tui_terminal_after_startup(ui); if (enable_mouse) { tui_mouse_on(ui); } stream_set_blocking(input_global_fd(), false); // libuv expects this - // resume the main thread - CONTINUE(data->bridge); } #endif static void tui_suspend(UI *ui) { - TUIData *data = ui->data; +// on a non-UNIX system, this is a no-op #ifdef UNIX // kill(0, SIGTSTP) won't stop the UI thread, so we must poll for SIGCONT // before continuing. This is done in another callback to avoid // loop_poll_events recursion - multiqueue_put_event(data->loop->fast_events, + multiqueue_put_event(resize_events, event_create(suspend_event, 1, ui)); -#else - // Resume the main thread as suspending isn't implemented. - CONTINUE(data->bridge); #endif } @@ -1547,6 +1490,13 @@ static void tui_option_set(UI *ui, String name, Object value) ui->rgb = value.data.boolean; data->print_attr_id = -1; invalidate(ui, 0, data->grid.height, 0, data->grid.width); + + if (ui_client_channel_id) { + Array args = ARRAY_DICT_INIT; + ADD(args, STRING_OBJ(cstr_as_string(xstrdup("rgb")))); + ADD(args, BOOLEAN_OBJ(value.data.boolean)); + rpc_send_event(ui_client_channel_id, "nvim_ui_set_option", args); + } } else if (strequal(name.data, "ttimeout")) { data->input.ttimeout = value.data.boolean; } else if (strequal(name.data, "ttimeoutlen")) { @@ -1659,8 +1609,11 @@ static void tui_guess_size(UI *ui) height = DFLT_ROWS; } - data->bridge->bridge.width = ui->width = width; - data->bridge->bridge.height = ui->height = height; + ui->width = width; + ui->height = height; + + // TODO(bfredl): only if different from last value + ui_schedule_refresh(); } static void unibi_goto(UI *ui, int row, int col) diff --git a/src/nvim/ui.c b/src/nvim/ui.c index 7bde8d2f5a..33c7d0c1fe 100644 --- a/src/nvim/ui.c +++ b/src/nvim/ui.c @@ -10,6 +10,7 @@ #include "auto/config.h" #include "klib/kvec.h" +#include "nvim/api/private/helpers.h" #include "nvim/ascii.h" #include "nvim/autocmd.h" #include "nvim/buffer_defs.h" @@ -31,16 +32,11 @@ #include "nvim/option.h" #include "nvim/os/time.h" #include "nvim/strings.h" +#include "nvim/tui/tui.h" #include "nvim/ui.h" #include "nvim/ui_compositor.h" #include "nvim/vim.h" #include "nvim/window.h" -#ifdef FEAT_TUI -# include "nvim/tui/tui.h" -#else -# include "nvim/msgpack_rpc/server.h" -#endif -#include "nvim/api/private/helpers.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "ui.c.generated.h" @@ -126,28 +122,28 @@ void ui_init(void) kv_ensure_space(call_buf, 16); } +static UI *builtin_ui = NULL; + +#ifdef EXITFREE void ui_free_all_mem(void) { kv_destroy(call_buf); + if (builtin_ui) { + tui_free_all_mem(builtin_ui); + builtin_ui = NULL; + } } +#endif void ui_builtin_start(void) { -#ifdef FEAT_TUI - tui_start(); -#else - fprintf(stderr, "Nvim headless-mode started.\n"); - size_t len; - char **addrs = server_address_list(&len); - if (addrs != NULL) { - fprintf(stderr, "Listening on:\n"); - for (size_t i = 0; i < len; i++) { - fprintf(stderr, "\t%s\n", addrs[i]); - } - xfree(addrs); - } - fprintf(stderr, "Press CTRL+C to exit.\n"); -#endif + builtin_ui = tui_start(); +} + +UI *ui_get_by_index(int idx) +{ + assert(idx < 16); + return uis[idx]; } bool ui_rgb_attached(void) @@ -186,7 +182,7 @@ void ui_refresh(void) } if (updating_screen) { - deferred_refresh_event(NULL); + ui_schedule_refresh(); return; } @@ -228,10 +224,20 @@ void ui_refresh(void) screen_resize(width, height); p_lz = save_p_lz; } else { - Array args = ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ((int)width)); - ADD(args, INTEGER_OBJ((int)height)); - rpc_send_event(ui_client_channel_id, "nvim_ui_try_resize", args); + if (ui_client_attached) { + // TODO(bfredl): ui_refresh() should only be used on the server + // we are in the client process. forward the resize + Array args = ARRAY_DICT_INIT; + ADD(args, INTEGER_OBJ((int)width)); + ADD(args, INTEGER_OBJ((int)height)); + rpc_send_event(ui_client_channel_id, "nvim_ui_try_resize", args); + } else { + /// TODO(bfredl): Messy! The screen does not yet exist, but we need to + /// communicate its size from the TUI to the client. Clean this up + /// in The UI Devirtualization Project. + Rows = height; + Columns = width; + } } if (ext_widgets[kUIMessages]) { @@ -279,10 +285,6 @@ static void ui_refresh_event(void **argv) void ui_schedule_refresh(void) { - loop_schedule_fast(&main_loop, event_create(deferred_refresh_event, 0)); -} -static void deferred_refresh_event(void **argv) -{ multiqueue_put(resize_events, ui_refresh_event, 0); } diff --git a/src/nvim/ui_bridge.c b/src/nvim/ui_bridge.c deleted file mode 100644 index 25c230a941..0000000000 --- a/src/nvim/ui_bridge.c +++ /dev/null @@ -1,223 +0,0 @@ -// This is an open source non-commercial project. Dear PVS-Studio, please check -// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com - -// UI wrapper that sends requests to the UI thread. -// Used by the built-in TUI and libnvim-based UIs. - -#include <stdbool.h> -#include <stdint.h> -#include <stdlib.h> - -#include "nvim/api/private/defs.h" -#include "nvim/api/private/helpers.h" -#include "nvim/event/loop.h" -#include "nvim/grid_defs.h" -#include "nvim/highlight_defs.h" -#include "nvim/main.h" -#include "nvim/memory.h" -#include "nvim/ui.h" -#include "nvim/ui_bridge.h" - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "ui_bridge.c.generated.h" -#endif - -#define UI(b) (((UIBridgeData *)b)->ui) - -// Schedule a function call on the UI bridge thread. -#define UI_BRIDGE_CALL(ui, name, argc, ...) \ - ((UIBridgeData *)ui)->scheduler(event_create(ui_bridge_##name##_event, argc, __VA_ARGS__), UI(ui)) - -#define INT2PTR(i) ((void *)(intptr_t)i) -#define PTR2INT(p) ((Integer)(intptr_t)p) - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "ui_events_bridge.generated.h" -#endif - -UI *ui_bridge_attach(UI *ui, ui_main_fn ui_main, event_scheduler scheduler) -{ - UIBridgeData *rv = xcalloc(1, sizeof(UIBridgeData)); - rv->ui = ui; - rv->bridge.rgb = ui->rgb; - rv->bridge.width = ui->width; - rv->bridge.height = ui->height; - rv->bridge.stop = ui_bridge_stop; - rv->bridge.grid_resize = ui_bridge_grid_resize; - rv->bridge.grid_clear = ui_bridge_grid_clear; - rv->bridge.grid_cursor_goto = ui_bridge_grid_cursor_goto; - rv->bridge.mode_info_set = ui_bridge_mode_info_set; - rv->bridge.update_menu = ui_bridge_update_menu; - rv->bridge.busy_start = ui_bridge_busy_start; - rv->bridge.busy_stop = ui_bridge_busy_stop; - rv->bridge.mouse_on = ui_bridge_mouse_on; - rv->bridge.mouse_off = ui_bridge_mouse_off; - rv->bridge.mode_change = ui_bridge_mode_change; - rv->bridge.grid_scroll = ui_bridge_grid_scroll; - rv->bridge.hl_attr_define = ui_bridge_hl_attr_define; - rv->bridge.bell = ui_bridge_bell; - rv->bridge.visual_bell = ui_bridge_visual_bell; - rv->bridge.default_colors_set = ui_bridge_default_colors_set; - rv->bridge.flush = ui_bridge_flush; - rv->bridge.suspend = ui_bridge_suspend; - rv->bridge.set_title = ui_bridge_set_title; - rv->bridge.set_icon = ui_bridge_set_icon; - rv->bridge.screenshot = ui_bridge_screenshot; - rv->bridge.option_set = ui_bridge_option_set; - rv->bridge.raw_line = ui_bridge_raw_line; - rv->bridge.inspect = ui_bridge_inspect; - rv->scheduler = scheduler; - - for (UIExtension i = 0; (int)i < kUIExtCount; i++) { - rv->bridge.ui_ext[i] = ui->ui_ext[i]; - } - - rv->ui_main = ui_main; - uv_mutex_init(&rv->mutex); - uv_cond_init(&rv->cond); - uv_mutex_lock(&rv->mutex); - rv->ready = false; - - if (uv_thread_create(&rv->ui_thread, ui_thread_run, rv)) { - abort(); - } - - // Suspend the main thread until CONTINUE is called by the UI thread. - while (!rv->ready) { - uv_cond_wait(&rv->cond, &rv->mutex); - } - uv_mutex_unlock(&rv->mutex); - - ui_attach_impl(&rv->bridge, 0); - - return &rv->bridge; -} - -void ui_bridge_stopped(UIBridgeData *bridge) -{ - uv_mutex_lock(&bridge->mutex); - bridge->stopped = true; - uv_mutex_unlock(&bridge->mutex); -} - -static void ui_thread_run(void *data) -{ - UIBridgeData *bridge = data; - bridge->ui_main(bridge, bridge->ui); -} - -static void ui_bridge_stop(UI *b) -{ - // Detach bridge first, so that "stop" is the last event the TUI loop - // receives from the main thread. #8041 - ui_detach_impl(b, 0); - - UIBridgeData *bridge = (UIBridgeData *)b; - bool stopped = bridge->stopped = false; - UI_BRIDGE_CALL(b, stop, 1, b); - for (;;) { - uv_mutex_lock(&bridge->mutex); - stopped = bridge->stopped; - uv_mutex_unlock(&bridge->mutex); - if (stopped) { // -V547 - break; - } - // TODO(justinmk): Remove this. Use a cond-wait above. #9274 - loop_poll_events(&main_loop, 10); // Process one event. - } - uv_thread_join(&bridge->ui_thread); - uv_mutex_destroy(&bridge->mutex); - uv_cond_destroy(&bridge->cond); - xfree(bridge->ui); // Threads joined, now safe to free UI container. #7922 - xfree(b); -} -static void ui_bridge_stop_event(void **argv) -{ - UI *ui = UI(argv[0]); - ui->stop(ui); -} - -static void ui_bridge_hl_attr_define(UI *ui, Integer id, HlAttrs attrs, HlAttrs cterm_attrs, - Array info) -{ - HlAttrs *a = xmalloc(sizeof(HlAttrs)); - *a = attrs; - UI_BRIDGE_CALL(ui, hl_attr_define, 3, ui, INT2PTR(id), a); -} -static void ui_bridge_hl_attr_define_event(void **argv) -{ - UI *ui = UI(argv[0]); - Array info = ARRAY_DICT_INIT; - ui->hl_attr_define(ui, PTR2INT(argv[1]), *((HlAttrs *)argv[2]), - *((HlAttrs *)argv[2]), info); - xfree(argv[2]); -} - -static void ui_bridge_raw_line_event(void **argv) -{ - UI *ui = UI(argv[0]); - ui->raw_line(ui, PTR2INT(argv[1]), PTR2INT(argv[2]), PTR2INT(argv[3]), - PTR2INT(argv[4]), PTR2INT(argv[5]), PTR2INT(argv[6]), - (LineFlags)PTR2INT(argv[7]), argv[8], argv[9]); - xfree(argv[8]); - xfree(argv[9]); -} -static void ui_bridge_raw_line(UI *ui, Integer grid, Integer row, Integer startcol, Integer endcol, - Integer clearcol, Integer clearattr, LineFlags flags, - const schar_T *chunk, const sattr_T *attrs) -{ - size_t ncol = (size_t)(endcol - startcol); - schar_T *c = xmemdup(chunk, ncol * sizeof(schar_T)); - sattr_T *hl = xmemdup(attrs, ncol * sizeof(sattr_T)); - UI_BRIDGE_CALL(ui, raw_line, 10, ui, INT2PTR(grid), INT2PTR(row), - INT2PTR(startcol), INT2PTR(endcol), INT2PTR(clearcol), - INT2PTR(clearattr), INT2PTR(flags), c, hl); -} - -static void ui_bridge_suspend(UI *b) -{ - UIBridgeData *data = (UIBridgeData *)b; - uv_mutex_lock(&data->mutex); - UI_BRIDGE_CALL(b, suspend, 1, b); - data->ready = false; - // Suspend the main thread until CONTINUE is called by the UI thread. - while (!data->ready) { - uv_cond_wait(&data->cond, &data->mutex); - } - uv_mutex_unlock(&data->mutex); -} -static void ui_bridge_suspend_event(void **argv) -{ - UI *ui = UI(argv[0]); - ui->suspend(ui); -} - -static void ui_bridge_option_set(UI *ui, String name, Object value) -{ - String copy_name = copy_string(name, NULL); - Object *copy_value = xmalloc(sizeof(Object)); - *copy_value = copy_object(value, NULL); - UI_BRIDGE_CALL(ui, option_set, 4, ui, copy_name.data, - INT2PTR(copy_name.size), copy_value); - // TODO(bfredl): when/if TUI/bridge teardown is refactored to use events, the - // commit that introduced this special case can be reverted. - // For now this is needed for nvim_list_uis(). - if (strequal(name.data, "termguicolors")) { - ui->rgb = value.data.boolean; - } -} -static void ui_bridge_option_set_event(void **argv) -{ - UI *ui = UI(argv[0]); - String name = (String){ .data = argv[1], .size = (size_t)argv[2] }; - Object value = *(Object *)argv[3]; - ui->option_set(ui, name, value); - api_free_string(name); - api_free_object(value); - xfree(argv[3]); -} - -static void ui_bridge_inspect(UI *ui, Dictionary *info) -{ - PUT(*info, "chan", INTEGER_OBJ(0)); -} diff --git a/src/nvim/ui_bridge.h b/src/nvim/ui_bridge.h deleted file mode 100644 index 094367126a..0000000000 --- a/src/nvim/ui_bridge.h +++ /dev/null @@ -1,47 +0,0 @@ -// Bridge for communication between a UI thread and nvim core. -// Used by the built-in TUI and libnvim-based UIs. -#ifndef NVIM_UI_BRIDGE_H -#define NVIM_UI_BRIDGE_H - -#include <stdbool.h> -#include <uv.h> - -#include "nvim/event/defs.h" -#include "nvim/ui.h" - -struct ui_bridge_data; - -typedef struct ui_bridge_data UIBridgeData; -typedef void (*ui_main_fn)(UIBridgeData *bridge, UI *ui); -struct ui_bridge_data { - UI bridge; // actual UI passed to ui_attach - UI *ui; // UI pointer that will have its callback called in - // another thread - event_scheduler scheduler; - uv_thread_t ui_thread; - ui_main_fn ui_main; - uv_mutex_t mutex; - uv_cond_t cond; - // When the UI thread is called, the main thread will suspend until - // the call returns. This flag is used as a condition for the main - // thread to continue. - bool ready; - // When a stop request is sent from the main thread, it must wait until the UI - // thread finishes handling all events. This flag is set by the UI thread as a - // signal that it will no longer send messages to the main thread. - bool stopped; -}; - -#define CONTINUE(b) \ - do { \ - UIBridgeData *d = (UIBridgeData *)b; \ - uv_mutex_lock(&d->mutex); \ - d->ready = true; \ - uv_cond_signal(&d->cond); \ - uv_mutex_unlock(&d->mutex); \ - } while (0) - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "ui_bridge.h.generated.h" -#endif -#endif // NVIM_UI_BRIDGE_H diff --git a/src/nvim/ui_client.c b/src/nvim/ui_client.c index 27c63433a7..6fc3a05e96 100644 --- a/src/nvim/ui_client.c +++ b/src/nvim/ui_client.c @@ -6,6 +6,7 @@ #include <stdlib.h> #include "nvim/api/private/helpers.h" +#include "nvim/eval.h" #include "nvim/event/loop.h" #include "nvim/event/multiqueue.h" #include "nvim/globals.h" @@ -24,23 +25,71 @@ #endif // uncrustify:on -void ui_client_init(uint64_t chan) +uint64_t ui_client_start_server(int argc, char **argv) { + varnumber_T exit_status; + char **args = xmalloc(((size_t)(2 + argc)) * sizeof(char *)); + int args_idx = 0; + args[args_idx++] = xstrdup((const char *)get_vim_var_str(VV_PROGPATH)); + args[args_idx++] = xstrdup("--embed"); + for (int i = 1; i < argc; i++) { + args[args_idx++] = xstrdup(argv[i]); + } + args[args_idx++] = NULL; + + Channel *channel = channel_job_start(args, CALLBACK_READER_INIT, + CALLBACK_READER_INIT, CALLBACK_NONE, + false, true, true, false, kChannelStdinPipe, + NULL, 0, 0, NULL, &exit_status); + if (ui_client_forward_stdin) { + close(0); + dup(2); + } + + return channel->id; +} + +void ui_client_run(bool remote_ui) + FUNC_ATTR_NORETURN +{ + ui_builtin_start(); + + loop_poll_events(&main_loop, 1); + Array args = ARRAY_DICT_INIT; - int width = Columns; - int height = Rows; Dictionary opts = ARRAY_DICT_INIT; PUT(opts, "rgb", BOOLEAN_OBJ(true)); PUT(opts, "ext_linegrid", BOOLEAN_OBJ(true)); PUT(opts, "ext_termcolors", BOOLEAN_OBJ(true)); - ADD(args, INTEGER_OBJ((int)width)); - ADD(args, INTEGER_OBJ((int)height)); + if (ui_client_termname) { + PUT(opts, "term_name", STRING_OBJ(cstr_as_string(ui_client_termname))); + } + if (ui_client_bg_respose != kNone) { + bool is_dark = (ui_client_bg_respose == kTrue); + PUT(opts, "term_background", STRING_OBJ(cstr_as_string(is_dark ? "dark" : "light"))); + } + PUT(opts, "term_colors", INTEGER_OBJ(t_colors)); + if (!remote_ui) { + PUT(opts, "stdin_tty", BOOLEAN_OBJ(stdin_isatty)); + PUT(opts, "stdout_tty", BOOLEAN_OBJ(stdout_isatty)); + if (ui_client_forward_stdin) { + PUT(opts, "stdin_fd", INTEGER_OBJ(UI_CLIENT_STDIN_FD)); + } + } + + ADD(args, INTEGER_OBJ(Columns)); + ADD(args, INTEGER_OBJ(Rows)); ADD(args, DICTIONARY_OBJ(opts)); - rpc_send_event(chan, "nvim_ui_attach", args); - ui_client_channel_id = chan; + rpc_send_event(ui_client_channel_id, "nvim_ui_attach", args); + ui_client_attached = true; + + // os_exit() will be invoked when the client channel detaches + while (true) { + LOOP_PROCESS_EVENTS(&main_loop, resize_events, -1); + } } UIClientHandler ui_client_get_redraw_handler(const char *name, size_t name_len, Error *error) @@ -63,20 +112,6 @@ Object handle_ui_client_redraw(uint64_t channel_id, Array args, Arena *arena, Er return NIL; } -/// run the main thread in ui client mode -/// -/// This is just a stub. the full version will handle input, resizing, etc -void ui_client_execute(uint64_t chan) - FUNC_ATTR_NORETURN -{ - while (true) { - loop_poll_events(&main_loop, -1); - multiqueue_process_events(resize_events); - } - - getout(0); -} - static HlAttrs ui_client_dict2hlattrs(Dictionary d, bool rgb) { Error err = ERROR_INIT; @@ -85,7 +120,7 @@ static HlAttrs ui_client_dict2hlattrs(Dictionary d, bool rgb) // TODO(bfredl): log "err" return HLATTRS_INIT; } - return dict2hlattrs(&dict, true, NULL, &err); + return dict2hlattrs(&dict, rgb, NULL, &err); } void ui_client_event_grid_resize(Array args) diff --git a/src/nvim/ui_client.h b/src/nvim/ui_client.h index bed73d83d3..0b1f1ecb29 100644 --- a/src/nvim/ui_client.h +++ b/src/nvim/ui_client.h @@ -17,6 +17,26 @@ EXTERN size_t grid_line_buf_size INIT(= 0); EXTERN schar_T *grid_line_buf_char INIT(= NULL); EXTERN sattr_T *grid_line_buf_attr INIT(= NULL); +// ID of the ui client channel. If zero, the client is not running. +EXTERN uint64_t ui_client_channel_id INIT(= 0); + +// TODO(bfredl): the current structure for how tui and ui_client.c communicate is a bit awkward. +// This will be restructured as part of The UI Devirtualization Project. + +/// Whether ui client has sent nvim_ui_attach yet +EXTERN bool ui_client_attached INIT(= false); + +/// Whether ui client has gotten a response about the bg color of the terminal, +/// kTrue=dark, kFalse=light, kNone=no response yet +EXTERN TriState ui_client_bg_respose INIT(= kNone); + +/// The ui client should forward its stdin to the nvim process +/// by convention, this uses fd=3 (next free number after stdio) +EXTERN bool ui_client_forward_stdin INIT(= false); + +EXTERN char *ui_client_termname INIT(= "null"); + +#define UI_CLIENT_STDIN_FD 3 #ifdef INCLUDE_GENERATED_DECLARATIONS # include "ui_client.h.generated.h" diff --git a/src/nvim/version.c b/src/nvim/version.c index 98719fce4a..92122b2523 100644 --- a/src/nvim/version.c +++ b/src/nvim/version.c @@ -69,11 +69,7 @@ static char *features[] = { "-iconv", #endif -#ifdef FEAT_TUI "+tui", -#else - "-tui", -#endif NULL }; diff --git a/test/functional/autocmd/focus_spec.lua b/test/functional/autocmd/focus_spec.lua index 024dccbe8e..d7a87e17ed 100644 --- a/test/functional/autocmd/focus_spec.lua +++ b/test/functional/autocmd/focus_spec.lua @@ -33,18 +33,37 @@ describe('autoread TUI FocusGained/FocusLost', function() helpers.write_file(path, '') lfs.touch(path, os.time() - 10) - feed_command('edit '..path) - feed_data('\027[O') screen:expect{grid=[[ {1: } | {4:~ }| {4:~ }| {4:~ }| + {5:[No Name] }| + | + {3:-- TERMINAL --} | + ]]} + feed_command('edit '..path) + screen:expect{grid=[[ + {1: } | + {4:~ }| + {4:~ }| + {4:~ }| {5:xtest-foo }| :edit xtest-foo | {3:-- TERMINAL --} | ]]} + feed_data('\027[O') + feed_data('\027[O') + screen:expect{grid=[[ + {1: } | + {4:~ }| + {4:~ }| + {4:~ }| + {5:xtest-foo }| + :edit xtest-foo | + {3:-- TERMINAL --} | + ]], unchanged=true} helpers.write_file(path, expected_addition) diff --git a/test/functional/core/main_spec.lua b/test/functional/core/main_spec.lua index 53461f505a..ab11e14a67 100644 --- a/test/functional/core/main_spec.lua +++ b/test/functional/core/main_spec.lua @@ -56,7 +56,7 @@ describe('Command-line option', function() screen:attach() local args = { nvim_prog_abs(), '-u', 'NONE', '-i', 'NONE', - '--cmd', 'set noswapfile shortmess+=IFW fileformats=unix', + '--cmd', '"set noswapfile shortmess+=IFW fileformats=unix"', '-s', '-' } diff --git a/test/functional/core/startup_spec.lua b/test/functional/core/startup_spec.lua index 9e22efa545..72d8f3a103 100644 --- a/test/functional/core/startup_spec.lua +++ b/test/functional/core/startup_spec.lua @@ -544,7 +544,7 @@ describe('sysinit', function() nvim_exec() | cmd: aunmenu * | > | - <" -u NONE -i NONE --cmd "set noruler" -D 1,1 All| + <" -u NONE -i NONE --cmd "set noruler" -D 1,0-1 All| | ]]) command([[call chansend(g:id, "cont\n")]]) diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua index ca59eb3182..a14bedbbbd 100644 --- a/test/functional/helpers.lua +++ b/test/functional/helpers.lua @@ -243,7 +243,7 @@ function module.run_session(lsession, request_cb, notification_cb, setup_cb, tim end loop_running = true - session:run(on_request, on_notification, on_setup, timeout) + lsession:run(on_request, on_notification, on_setup, timeout) loop_running = false if last_error then local err = last_error @@ -251,7 +251,7 @@ function module.run_session(lsession, request_cb, notification_cb, setup_cb, tim error(err) end - return session.eof_err + return lsession.eof_err end function module.run(request_cb, notification_cb, setup_cb, timeout) @@ -465,8 +465,14 @@ end -- clear('-e') -- clear{args={'-e'}, args_rm={'-i'}, env={TERM=term}} function module.clear(...) + module.set_session(module.spawn_argv(false, ...)) +end + +-- same params as clear, but does returns the session instead +-- of replacing the default session +function module.spawn_argv(keep, ...) local argv, env, io_extra = module.new_argv(...) - module.set_session(module.spawn(argv, nil, env, nil, io_extra)) + return module.spawn(argv, nil, env, keep, io_extra) end -- Builds an argument list for use in clear(). diff --git a/test/functional/terminal/api_spec.lua b/test/functional/terminal/api_spec.lua index 38ef7c2a61..724791343d 100644 --- a/test/functional/terminal/api_spec.lua +++ b/test/functional/terminal/api_spec.lua @@ -19,6 +19,16 @@ describe('api', function() end) it("qa! RPC request during insert-mode", function() + screen:expect{grid=[[ + {1: } | + {4:~ }| + {4:~ }| + {4:~ }| + {4:~ }| + | + {3:-- TERMINAL --} | + ]]} + -- Start the socket from the child nvim. child_session.feed_data(":echo serverstart('"..socket_name.."')\n") diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 1807001842..964fc8941d 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -24,6 +24,11 @@ local funcs = helpers.funcs local meths = helpers.meths local is_ci = helpers.is_ci local is_os = helpers.is_os +local new_pipename = helpers.new_pipename +local spawn_argv = helpers.spawn_argv +local set_session = helpers.set_session +local feed = helpers.feed +local eval = helpers.eval if helpers.skip(helpers.is_os('win')) then return end @@ -34,7 +39,7 @@ describe('TUI', function() before_each(function() clear() - local child_server = helpers.new_pipename() + local child_server = new_pipename() screen = thelpers.screen_setup(0, string.format([=[["%s", "--listen", "%s", "-u", "NONE", "-i", "NONE", "--cmd", "%s laststatus=2 background=dark"]]=], nvim_prog, child_server, nvim_set)) @@ -1191,6 +1196,15 @@ describe('TUI', function() it('paste: split "start paste" code', function() feed_data('i') + screen:expect{grid=[[ + {1: } | + {4:~ }| + {4:~ }| + {4:~ }| + {5:[No Name] }| + {3:-- INSERT --} | + {3:-- TERMINAL --} | + ]]} -- Send split "start paste" sequence. feed_data('\027[2') feed_data('00~pasted from terminal\027[201~') @@ -1207,6 +1221,15 @@ describe('TUI', function() it('paste: split "stop paste" code', function() feed_data('i') + screen:expect{grid=[[ + {1: } | + {4:~ }| + {4:~ }| + {4:~ }| + {5:[No Name] }| + {3:-- INSERT --} | + {3:-- TERMINAL --} | + ]]} -- Send split "stop paste" sequence. feed_data('\027[200~pasted from terminal\027[20') feed_data('1~') @@ -1232,6 +1255,15 @@ describe('TUI', function() end)(vim.paste) ]], {}) feed_data('i') + screen:expect{grid=[[ + {1: } | + {4:~ }| + {4:~ }| + {4:~ }| + {5:[No Name] }| + {3:-- INSERT --} | + {3:-- TERMINAL --} | + ]]} feed_data('\027[200~pasted') -- phase 1 screen:expect([[ pasted{1: } | @@ -1365,7 +1397,7 @@ describe('TUI', function() | {4:~ }| {5: }| - [[['chan', 0], ['height', 6], ['override', v:false| + [[['chan', 1], ['height', 6], ['override', v:false| ], ['rgb', v:false], ['width', 50]]] | {10:Press ENTER or type command to continue}{1: } | {3:-- TERMINAL --} | @@ -1467,6 +1499,15 @@ describe('TUI', function() it('<C-h> #10134', function() local screen = thelpers.screen_setup(0, '["'..nvim_prog ..[[", "-u", "NONE", "-i", "NONE", "--cmd", "set noruler", "--cmd", ':nnoremap <C-h> :echomsg "\<C-h\>"<CR>']]..']') + screen:expect{grid=[[ + {1: } | + {4:~ }| + {4:~ }| + {4:~ }| + {5:[No Name] }| + | + {3:-- TERMINAL --} | + ]]} command([[call chansend(b:terminal_job_id, "\<C-h>")]]) screen:expect([[ @@ -1493,6 +1534,15 @@ describe('TUI UIEnter/UILeave', function() ..[[, "--cmd", "autocmd VimEnter * :call add(g:evs, 'VimEnter')"]] ..']' ) + screen:expect{grid=[[ + {1: } | + {4:~ }| + {4:~ }| + {4:~ }| + {5:[No Name] }| + | + {3:-- TERMINAL --} | + ]]} feed_data(":echo g:evs\n") screen:expect{grid=[[ {1: } | @@ -1513,61 +1563,88 @@ describe('TUI FocusGained/FocusLost', function() clear() screen = thelpers.screen_setup(0, '["'..nvim_prog ..'", "-u", "NONE", "-i", "NONE", "--cmd", "set noswapfile noshowcmd noruler"]') - feed_data(":autocmd FocusGained * echo 'gained'\n") - feed_data(":autocmd FocusLost * echo 'lost'\n") - feed_data("\034\016") -- CTRL-\ CTRL-N - end) - - it('in normal-mode', function() - retry(2, 3 * screen.timeout, function() - feed_data('\027[I') - screen:expect([[ + screen:expect{grid=[[ {1: } | {4:~ }| {4:~ }| {4:~ }| {5:[No Name] }| - gained | + | {3:-- TERMINAL --} | - ]]) + ]]} + feed_data(":autocmd FocusGained * echo 'gained'\n") + feed_data(":autocmd FocusLost * echo 'lost'\n") + feed_data("\034\016") -- CTRL-\ CTRL-N + end) - feed_data('\027[O') - screen:expect([[ + it('in normal-mode', function() + screen:expect{grid=[[ {1: } | {4:~ }| {4:~ }| {4:~ }| {5:[No Name] }| - lost | + :autocmd FocusLost * echo 'lost' | {3:-- TERMINAL --} | - ]]) + ]]} + retry(2, 3 * screen.timeout, function() + feed_data('\027[I') + screen:expect([[ + {1: } | + {4:~ }| + {4:~ }| + {4:~ }| + {5:[No Name] }| + gained | + {3:-- TERMINAL --} | + ]]) + + feed_data('\027[O') + screen:expect([[ + {1: } | + {4:~ }| + {4:~ }| + {4:~ }| + {5:[No Name] }| + lost | + {3:-- TERMINAL --} | + ]]) end) end) it('in insert-mode', function() feed_command('set noshowmode') feed_data('i') - retry(2, 3 * screen.timeout, function() - feed_data('\027[I') - screen:expect([[ + screen:expect{grid=[[ {1: } | {4:~ }| {4:~ }| {4:~ }| {5:[No Name] }| - gained | + :set noshowmode | {3:-- TERMINAL --} | - ]]) - feed_data('\027[O') - screen:expect([[ - {1: } | - {4:~ }| - {4:~ }| - {4:~ }| - {5:[No Name] }| - lost | - {3:-- TERMINAL --} | - ]]) + ]]} + retry(2, 3 * screen.timeout, function() + feed_data('\027[I') + screen:expect([[ + {1: } | + {4:~ }| + {4:~ }| + {4:~ }| + {5:[No Name] }| + gained | + {3:-- TERMINAL --} | + ]]) + feed_data('\027[O') + screen:expect([[ + {1: } | + {4:~ }| + {4:~ }| + {4:~ }| + {5:[No Name] }| + lost | + {3:-- TERMINAL --} | + ]]) end) end) @@ -1604,6 +1681,15 @@ describe('TUI FocusGained/FocusLost', function() feed_data(":autocmd!\n") feed_data(":autocmd FocusLost * call append(line('$'), 'lost')\n") feed_data(":autocmd FocusGained * call append(line('$'), 'gained')\n") + screen:expect{grid=[[ + {1: } | + {4:~ }| + {4:~ }| + {4:~ }| + {5:[No Name] }| + | + {3:-- TERMINAL --} | + ]]} retry(2, 3 * screen.timeout, function() -- Enter cmdline-mode. feed_data(':') @@ -1662,9 +1748,18 @@ describe('TUI FocusGained/FocusLost', function() feed_data(":echom 'msg1'|echom 'msg2'|echom 'msg3'|echom 'msg4'|echom 'msg5'\n") -- Execute :messages to provoke the press-enter prompt. feed_data(":messages\n") + screen:expect{grid=[[ + msg1 | + msg2 | + msg3 | + msg4 | + msg5 | + {10:Press ENTER or type command to continue}{1: } | + {3:-- TERMINAL --} | + ]]} feed_data('\027[I') feed_data('\027[I') - screen:expect([[ + screen:expect{grid=[[ msg1 | msg2 | msg3 | @@ -1672,7 +1767,7 @@ describe('TUI FocusGained/FocusLost', function() msg5 | {10:Press ENTER or type command to continue}{1: } | {3:-- TERMINAL --} | - ]]) + ]], unchanged=true} end) end) @@ -2040,7 +2135,7 @@ describe("TUI", function() retry(nil, 3000, function() -- Wait for log file to be flushed. local log = read_file('Xtest_tui_verbose_log') or '' - eq('--- Terminal info --- {{{\n', string.match(log, '%-%-%- Terminal.-\n')) + eq('--- Terminal info --- {{{\n', string.match(log, '%-%-%- Terminal.-\n')) -- }}} ok(#log > 50) end) end) @@ -2164,3 +2259,165 @@ describe('TUI bg color', function() screen:expect{any='new_bg=dark'} end) end) + +-- These tests require `thelpers` because --headless/--embed +-- does not initialize the TUI. +describe("TUI as a client", function() + + it("connects to remote instance (with its own TUI)", function() + local server_super = spawn_argv(false) -- equivalent to clear() + local client_super = spawn_argv(true) + + set_session(server_super) + local server_pipe = new_pipename() + local screen_server = thelpers.screen_setup(0, + string.format([=[["%s", "--listen", "%s", "-u", "NONE", "-i", "NONE", "--cmd", "%s laststatus=2 background=dark"]]=], + nvim_prog, server_pipe, nvim_set)) + + feed_data("iHello, World") + screen_server:expect{grid=[[ + Hello, World{1: } | + {4:~ }| + {4:~ }| + {4:~ }| + {5:[No Name] [+] }| + {3:-- INSERT --} | + {3:-- TERMINAL --} | + ]]} + feed_data("\027") + screen_server:expect{grid=[[ + Hello, Worl{1:d} | + {4:~ }| + {4:~ }| + {4:~ }| + {5:[No Name] [+] }| + | + {3:-- TERMINAL --} | + ]]} + + set_session(client_super) + local screen_client = thelpers.screen_setup(0, + string.format([=[["%s", "-u", "NONE", "-i", "NONE", "--server", "%s", "--remote-ui"]]=], + nvim_prog, server_pipe)) + + screen_client:expect{grid=[[ + Hello, Worl{1:d} | + {4:~ }| + {4:~ }| + {4:~ }| + {5:[No Name] [+] }| + | + {3:-- TERMINAL --} | + ]]} + + feed_data(":q!\n") + + server_super:close() + client_super:close() + end) + + it("connects to remote instance (--headless)", function() + local server = helpers.spawn_argv(false) -- equivalent to clear() + local client_super = spawn_argv(true) + + set_session(server) + local server_pipe = eval'v:servername' + feed'iHalloj!<esc>' + + set_session(client_super) + local screen = thelpers.screen_setup(0, + string.format([=[["%s", "-u", "NONE", "-i", "NONE", "--server", "%s", "--remote-ui"]]=], + nvim_prog, server_pipe)) + + screen:expect{grid=[[ + Halloj{1:!} | + {4:~ }| + {4:~ }| + {4:~ }| + {4:~ }| + | + {3:-- TERMINAL --} | + ]]} + + client_super:close() + server:close() + end) + + + it("throws error when no server exists", function() + clear() + local screen = thelpers.screen_setup(0, + string.format([=[["%s", "-u", "NONE", "-i", "NONE", "--server", "127.0.0.1:2436546", "--remote-ui"]]=], + nvim_prog)) + + screen:try_resize(60, 7) + + screen:expect([[ + Remote ui failed to start: {MATCH:.*}| + | + [Process exited 1]{1: } | + | + | + | + {3:-- TERMINAL --} | + ]]) + end) + + it("exits when server quits", function() + local server_super = spawn_argv(false) -- equivalent to clear() + local client_super = spawn_argv(true) + + set_session(server_super) + local server_pipe = new_pipename() + local screen_server = thelpers.screen_setup(0, + string.format([=[["%s", "--listen", "%s", "-u", "NONE", "-i", "NONE", "--cmd", "%s laststatus=2 background=dark"]]=], + nvim_prog, server_pipe, nvim_set)) + + feed_data("iHello, World") + screen_server:expect{grid=[[ + Hello, World{1: } | + {4:~ }| + {4:~ }| + {4:~ }| + {5:[No Name] [+] }| + {3:-- INSERT --} | + {3:-- TERMINAL --} | + ]]} + feed_data("\027") + screen_server:expect{grid=[[ + Hello, Worl{1:d} | + {4:~ }| + {4:~ }| + {4:~ }| + {5:[No Name] [+] }| + | + {3:-- TERMINAL --} | + ]]} + + set_session(client_super) + local screen_client = thelpers.screen_setup(0, + string.format([=[["%s", "-u", "NONE", "-i", "NONE", "--server", "%s", "--remote-ui"]]=], + nvim_prog, server_pipe)) + + screen_client:expect{grid=[[ + Hello, Worl{1:d} | + {4:~ }| + {4:~ }| + {4:~ }| + {5:[No Name] [+] }| + | + {3:-- TERMINAL --} | + ]]} + + -- quitting the server + set_session(server_super) + feed_data(":q!\n") + screen_server:expect({any="Process exited 0"}) + + -- assert that client has exited + screen_client:expect({any="Process exited 0"}) + + server_super:close() + client_super:close() + end) +end) diff --git a/test/functional/ui/screen.lua b/test/functional/ui/screen.lua index 79927273a6..3b9cce0e6f 100644 --- a/test/functional/ui/screen.lua +++ b/test/functional/ui/screen.lua @@ -1550,7 +1550,8 @@ function Screen:_get_attr_id(attr_state, attrs, hl_id) attr_state.modified = true return id end - return "UNEXPECTED "..self:_pprint_attrs(self._attr_table[hl_id][1]) + local kind = self._options.rgb and 1 or 2 + return "UNEXPECTED "..self:_pprint_attrs(self._attr_table[hl_id][kind]) else if self:_equal_attrs(attrs, {}) then -- ignore this attrs diff --git a/test/unit/tui_spec.lua b/test/unit/tui_spec.lua index 36ce4a1493..15b019edd1 100644 --- a/test/unit/tui_spec.lua +++ b/test/unit/tui_spec.lua @@ -33,7 +33,7 @@ itp('handle_background_color', function() term_input.waiting_for_bg_response = 1 eq(kComplete, handle_background_color(term_input)) eq(0, term_input.waiting_for_bg_response) - eq(1, multiqueue.multiqueue_size(events)) + eq(0, multiqueue.multiqueue_size(events)) local event = multiqueue.multiqueue_get(events) local bg_event = ffi.cast("Event*", event.argv[1]) |