diff options
30 files changed, 1042 insertions, 159 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index ab7595eb11..117519230f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -228,7 +228,7 @@ else() # On FreeBSD 64 math.h uses unguarded C11 extension, which taints clang # 3.4.1 used there. - if(CMAKE_SYSTEM_NAME STREQUAL "FreeBSD") + if(CMAKE_SYSTEM_NAME STREQUAL "FreeBSD" AND CMAKE_C_COMPILER_ID MATCHES "Clang") add_definitions(-Wno-c11-extensions) endif() endif() @@ -445,11 +445,7 @@ message(STATUS "Using the Lua interpreter ${LUA_PRG}.") find_program(BUSTED_PRG NAMES busted busted.bat) find_program(BUSTED_LUA_PRG busted-lua) if(NOT BUSTED_OUTPUT_TYPE) - if(WIN32) - set(BUSTED_OUTPUT_TYPE "plainTerminal") - else() - set(BUSTED_OUTPUT_TYPE "utfTerminal") - endif() + set(BUSTED_OUTPUT_TYPE "nvim") endif() find_program(LUACHECK_PRG luacheck) @@ -460,7 +456,6 @@ include(InstallHelpers) file(GLOB MANPAGES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} man/nvim.1) - install_helper( FILES ${MANPAGES} DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) diff --git a/cmake/InstallHelpers.cmake b/cmake/InstallHelpers.cmake index ee07ba2c66..ca20ddf354 100644 --- a/cmake/InstallHelpers.cmake +++ b/cmake/InstallHelpers.cmake @@ -1,3 +1,12 @@ +# Fix CMAKE_INSTALL_MANDIR on BSD before including GNUInstallDirs. #6771 +if(CMAKE_SYSTEM_NAME MATCHES "BSD" AND NOT DEFINED CMAKE_INSTALL_MANDIR) + if(DEFINED ENV{MANPREFIX}) + set(CMAKE_INSTALL_MANDIR "$ENV{MANPREFIX}/man") + else() + set(CMAKE_INSTALL_MANDIR "/usr/local/man") + endif() +endif() + # For $CMAKE_INSTALL_{DATAROOT,MAN, ...}DIR include(GNUInstallDirs) diff --git a/runtime/autoload/man.vim b/runtime/autoload/man.vim index 361ade59d3..dd71ede680 100644 --- a/runtime/autoload/man.vim +++ b/runtime/autoload/man.vim @@ -2,8 +2,17 @@ let s:find_arg = '-w' let s:localfile_arg = v:true " Always use -l if possible. #6683 +let s:section_arg = '-s' + +function! s:init_section_flag() + call system(['env', 'MANPAGER=cat', 'man', s:section_arg, '1', 'man']) + if v:shell_error + let s:section_arg = '-S' + endif +endfunction function! s:init() abort + call s:init_section_flag() " TODO(nhooyr): Does `man -l` on SunOS list searched directories? try if !has('win32') && $OSTYPE !~? 'cygwin\|linux' && system('uname -s') =~? 'SunOS' && system('uname -r') =~# '^5' @@ -211,7 +220,7 @@ function! s:get_path(sect, name) abort " - sections starting with '-' " - 3pcap section (found on macOS) " - commas between sections (for section priority) - return s:system(['man', s:find_arg, '-s', a:sect, a:name]) + return s:system(['man', s:find_arg, s:section_arg, a:sect, a:name]) endfunction function! s:verify_exists(sect, name) abort diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index f73599ebd9..11b42dd2e5 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -6460,11 +6460,20 @@ serverlist() *serverlist()* nvim --cmd "echo serverlist()" --cmd "q" < serverstart([{address}]) *serverstart()* - Opens a named pipe or TCP socket at {address} for clients to - connect to and returns {address}. If no address is given, it - is equivalent to: > + Opens a TCP socket (IPv4/IPv6), Unix domain socket (Unix), + or named pipe (Windows) at {address} for clients to connect + to and returns {address}. + + If {address} contains `:`, a TCP socket is used. Everything in + front of the last occurrence of `:` is the IP or hostname, + everything after it the port. If the port is empty or `0`, + a random port will be assigned. + + If no address is given, it is equivalent to: > :call serverstart(tempname()) + < |$NVIM_LISTEN_ADDRESS| is set to {address} if not already set. + *--servername* The Vim command-line option `--servername` can be imitated: > nvim --cmd "let g:server_addr = serverstart('foo')" @@ -6856,6 +6865,21 @@ sinh({expr}) *sinh()* :echo sinh(-0.9) < -1.026517 +sockconnect({mode}, {address}, {opts}) *sockconnect()* + Connect a socket to an address. If {mode} is "pipe" then + {address} should be the path of a named pipe. If {mode} is + "tcp" then {address} should be of the form "host:port" where + the host should be an ip adderess or host name, and port the + port number. Currently only rpc sockets are supported, so + {opts} must be passed with "rpc" set to |TRUE|. + + {opts} is a dictionary with these keys: + rpc : If set, |msgpack-rpc| will be used to communicate + over the socket. + Returns: + - The channel ID on success, which is used by + |rpcnotify()| and |rpcrequest()| and |rpcstop()|. + - 0 on invalid arguments or connection failure. sort({list} [, {func} [, {dict}]]) *sort()* *E702* Sort the items in {list} in-place. Returns {list}. diff --git a/runtime/doc/nvim_terminal_emulator.txt b/runtime/doc/nvim_terminal_emulator.txt index cfe011250e..801ff75647 100644 --- a/runtime/doc/nvim_terminal_emulator.txt +++ b/runtime/doc/nvim_terminal_emulator.txt @@ -93,12 +93,11 @@ Terminal sets local defaults for some options, which may differ from your global configuration. - 'list' is disabled -- 'number' is disabled -- 'relativenumber' is disabled (cannot be changed in |Terminal-mode|) - 'wrap' is disabled +- 'relativenumber' is disabled in |Terminal-mode| (and cannot be enabled) You can change the defaults with a TermOpen autocommand: > - au TermOpen * setlocal number + au TermOpen * setlocal list Terminal colors can be customized with these variables: diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 0e8feb6321..2097cbf32b 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -4368,7 +4368,8 @@ A jump table for the options with a short description can be found at |Q_op|. Note that typing <F10> in paste mode inserts "<F10>", since in paste mode everything is inserted literally, except the 'pastetoggle' key sequence. - When the value has several bytes 'ttimeoutlen' applies. + No timeout is used, this means that a multi-key 'pastetoggle' can not + be triggered manually. *'pex'* *'patchexpr'* 'patchexpr' 'pex' string (default "") diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c index 0b8d39d0d2..fc11708bd6 100644 --- a/src/nvim/api/buffer.c +++ b/src/nvim/api/buffer.c @@ -453,6 +453,26 @@ Integer nvim_buf_get_changedtick(Buffer buffer, Error *err) return buf->b_changedtick; } +/// Get a list of dictionaries describing buffer-local mappings +/// Note that the buffer key in the dictionary will represent the buffer +/// handle where the mapping is present +/// +/// @param mode The abbreviation for the mode +/// @param buffer_id Buffer handle +/// @param[out] err Error details, if any +/// @returns An array of maparg() like dictionaries describing mappings +ArrayOf(Dictionary) nvim_buf_get_keymap(Buffer buffer, String mode, Error *err) + FUNC_API_SINCE(3) +{ + buf_T *buf = find_buffer_by_handle(buffer, err); + + if (!buf) { + return (Array)ARRAY_DICT_INIT; + } + + return keymap_array(mode, buf); +} + /// Sets a buffer-scoped (b:) variable /// /// @param buffer Buffer handle diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index 1d7b305da3..ef789b3ed4 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -24,6 +24,7 @@ #include "nvim/option_defs.h" #include "nvim/version.h" #include "nvim/lib/kvec.h" +#include "nvim/getchar.h" /// Helper structure for vim_to_object typedef struct { @@ -1034,3 +1035,41 @@ void api_set_error(Error *err, ErrorType errType, const char *format, ...) err->type = errType; } + +/// Get an array containing dictionaries describing mappings +/// based on mode and buffer id +/// +/// @param mode The abbreviation for the mode +/// @param buf The buffer to get the mapping array. NULL for global +/// @returns An array of maparg() like dictionaries describing mappings +ArrayOf(Dictionary) keymap_array(String mode, buf_T *buf) +{ + Array mappings = ARRAY_DICT_INIT; + dict_T *const dict = tv_dict_alloc(); + + // Convert the string mode to the integer mode + // that is stored within each mapblock + char_u *p = (char_u *)mode.data; + int int_mode = get_map_mode(&p, 0); + + // Determine the desired buffer value + long buffer_value = (buf == NULL) ? 0 : buf->handle; + + for (int i = 0; i < MAX_MAPHASH; i++) { + for (const mapblock_T *current_maphash = get_maphash(i, buf); + current_maphash; + current_maphash = current_maphash->m_next) { + // Check for correct mode + if (int_mode & current_maphash->m_mode) { + mapblock_fill_dict(dict, current_maphash, buffer_value, false); + ADD(mappings, vim_to_object( + (typval_T[]) { { .v_type = VAR_DICT, .vval.v_dict = dict } })); + + tv_dict_clear(dict); + } + } + } + tv_dict_free(dict); + + return mappings; +} diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 53e5f71fd4..0cffb2c87d 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -742,6 +742,17 @@ Dictionary nvim_get_mode(void) return rv; } +/// Get a list of dictionaries describing global (i.e. non-buffer) mappings +/// Note that the "buffer" key will be 0 to represent false. +/// +/// @param mode The abbreviation for the mode +/// @returns An array of maparg() like dictionaries describing mappings +ArrayOf(Dictionary) nvim_get_keymap(String mode) + FUNC_API_SINCE(3) +{ + return keymap_array(mode, NULL); +} + Array nvim_get_api_info(uint64_t channel_id) FUNC_API_SINCE(1) FUNC_API_ASYNC FUNC_API_REMOTE_ONLY { diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 88fa9726a4..d96b9355f1 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -438,6 +438,9 @@ typedef TV_DICTITEM_STRUCT(sizeof("changedtick")) ChangedtickDictItem; #define BUF_HAS_QF_ENTRY 1 #define BUF_HAS_LL_ENTRY 2 +// Maximum number of maphash blocks we will have +#define MAX_MAPHASH 256 + /* * buffer: structure that holds information about one file * @@ -526,8 +529,8 @@ struct file_buffer { */ uint64_t b_chartab[4]; - /* Table used for mappings local to a buffer. */ - mapblock_T *(b_maphash[256]); + // Table used for mappings local to a buffer. + mapblock_T *(b_maphash[MAX_MAPHASH]); /* First abbreviation local to a buffer. */ mapblock_T *b_first_abbr; diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 35ca236831..7187386ec7 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -12103,22 +12103,7 @@ static void get_maparg(typval_T *argvars, typval_T *rettv, int exact) tv_dict_alloc_ret(rettv); if (rhs != NULL) { // Return a dictionary. - char_u *lhs = str2special_save(mp->m_keys, true); - char *const mapmode = map_mode_to_chars(mp->m_mode); - dict_T *dict = rettv->vval.v_dict; - - tv_dict_add_str(dict, S_LEN("lhs"), (const char *)lhs); - tv_dict_add_str(dict, S_LEN("rhs"), (const char *)mp->m_orig_str); - tv_dict_add_nr(dict, S_LEN("noremap"), mp->m_noremap ? 1 : 0); - tv_dict_add_nr(dict, S_LEN("expr"), mp->m_expr ? 1 : 0); - tv_dict_add_nr(dict, S_LEN("silent"), mp->m_silent ? 1 : 0); - tv_dict_add_nr(dict, S_LEN("sid"), (varnumber_T)mp->m_script_ID); - tv_dict_add_nr(dict, S_LEN("buffer"), (varnumber_T)buffer_local); - tv_dict_add_nr(dict, S_LEN("nowait"), mp->m_nowait ? 1 : 0); - tv_dict_add_str(dict, S_LEN("mode"), mapmode); - - xfree(lhs); - xfree(mapmode); + mapblock_fill_dict(rettv->vval.v_dict, mp, buffer_local, true); } } } @@ -12135,6 +12120,46 @@ static void f_luaeval(typval_T *argvars, typval_T *rettv, FunPtr fptr) executor_eval_lua(cstr_as_string((char *)str), &argvars[1], rettv); } +/// Fill a dictionary with all applicable maparg() like dictionaries +/// +/// @param dict The dictionary to be filled +/// @param mp The maphash that contains the mapping information +/// @param buffer_value The "buffer" value +/// @param compatible True for compatible with old maparg() dict +void mapblock_fill_dict(dict_T *const dict, + const mapblock_T *const mp, + long buffer_value, + bool compatible) + FUNC_ATTR_NONNULL_ALL +{ + char_u *lhs = str2special_save(mp->m_keys, true); + char *const mapmode = map_mode_to_chars(mp->m_mode); + varnumber_T noremap_value; + + if (compatible) { + // Keep old compatible behavior + // This is unable to determine whether a mapping is a <script> mapping + noremap_value = !!mp->m_noremap; + } else { + // Distinguish between <script> mapping + // If it's not a <script> mapping, check if it's a noremap + noremap_value = mp->m_noremap == REMAP_SCRIPT ? 2 : !!mp->m_noremap; + } + + tv_dict_add_str(dict, S_LEN("lhs"), (const char *)lhs); + tv_dict_add_str(dict, S_LEN("rhs"), (const char *)mp->m_orig_str); + tv_dict_add_nr(dict, S_LEN("noremap"), noremap_value); + tv_dict_add_nr(dict, S_LEN("expr"), mp->m_expr ? 1 : 0); + tv_dict_add_nr(dict, S_LEN("silent"), mp->m_silent ? 1 : 0); + tv_dict_add_nr(dict, S_LEN("sid"), (varnumber_T)mp->m_script_ID); + tv_dict_add_nr(dict, S_LEN("buffer"), (varnumber_T)buffer_value); + tv_dict_add_nr(dict, S_LEN("nowait"), mp->m_nowait ? 1 : 0); + tv_dict_add_str(dict, S_LEN("mode"), mapmode); + + xfree(lhs); + xfree(mapmode); +} + /* * "map()" function */ @@ -14297,22 +14322,39 @@ static void f_serverstart(typval_T *argvars, typval_T *rettv, FunPtr fptr) return; } + char *address; // If the user supplied an address, use it, otherwise use a temp. if (argvars[0].v_type != VAR_UNKNOWN) { if (argvars[0].v_type != VAR_STRING) { EMSG(_(e_invarg)); return; } else { - rettv->vval.v_string = (char_u *)xstrdup(tv_get_string(argvars)); + address = xstrdup(tv_get_string(argvars)); } } else { - rettv->vval.v_string = (char_u *)server_address_new(); + address = server_address_new(); } - int result = server_start((char *) rettv->vval.v_string); + int result = server_start(address); + xfree(address); + if (result != 0) { - EMSG2("Failed to start server: %s", uv_strerror(result)); + EMSG2("Failed to start server: %s", + result > 0 ? "Unknonwn system error" : uv_strerror(result)); + return; } + + // Since it's possible server_start adjusted the given {address} (e.g., + // "localhost:" will now have a port), return the final value to the user. + size_t n; + char **addrs = server_address_list(&n); + rettv->vval.v_string = (char_u *)addrs[n - 1]; + + n--; + for (size_t i = 0; i < n; i++) { + xfree(addrs[i]); + } + xfree(addrs); } /// "serverstop()" function @@ -15027,6 +15069,54 @@ static void f_simplify(typval_T *argvars, typval_T *rettv, FunPtr fptr) rettv->v_type = VAR_STRING; } +/// "sockconnect()" function +static void f_sockconnect(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + if (argvars[0].v_type != VAR_STRING || argvars[1].v_type != VAR_STRING) { + EMSG(_(e_invarg)); + return; + } + if (argvars[2].v_type != VAR_DICT && argvars[2].v_type != VAR_UNKNOWN) { + // Wrong argument types + EMSG2(_(e_invarg2), "expected dictionary"); + return; + } + + const char *mode = tv_get_string(&argvars[0]); + const char *address = tv_get_string(&argvars[1]); + + bool tcp; + if (strcmp(mode, "tcp") == 0) { + tcp = true; + } else if (strcmp(mode, "pipe") == 0) { + tcp = false; + } else { + EMSG2(_(e_invarg2), "invalid mode"); + return; + } + + bool rpc = false; + if (argvars[2].v_type == VAR_DICT) { + dict_T *opts = argvars[2].vval.v_dict; + rpc = tv_dict_get_number(opts, "rpc") != 0; + } + + if (!rpc) { + EMSG2(_(e_invarg2), "rpc option must be true"); + return; + } + + const char *error = NULL; + uint64_t id = channel_connect(tcp, address, 50, &error); + + if (error) { + EMSG2(_("connection failed: %s"), error); + } + + rettv->vval.v_number = (varnumber_T)id; + rettv->v_type = VAR_NUMBER; +} + /// struct used in the array that's given to qsort() typedef struct { listitem_T *item; diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index 533403b2b0..334e10eb6c 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -268,6 +268,7 @@ return { simplify={args=1}, sin={args=1, func="float_op_wrapper", data="&sin"}, sinh={args=1, func="float_op_wrapper", data="&sinh"}, + sockconnect={args={2,3}}, sort={args={1, 3}}, soundfold={args=1}, spellbadword={args={0, 1}}, diff --git a/src/nvim/event/socket.c b/src/nvim/event/socket.c index 922e9c8be8..30a71a5586 100644 --- a/src/nvim/event/socket.c +++ b/src/nvim/event/socket.c @@ -15,62 +15,56 @@ #include "nvim/vim.h" #include "nvim/strings.h" #include "nvim/path.h" +#include "nvim/main.h" #include "nvim/memory.h" #include "nvim/macros.h" +#include "nvim/charset.h" +#include "nvim/log.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "event/socket.c.generated.h" #endif -#define NVIM_DEFAULT_TCP_PORT 7450 - -void socket_watcher_init(Loop *loop, SocketWatcher *watcher, - const char *endpoint, void *data) - FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_NONNULL_ARG(2) FUNC_ATTR_NONNULL_ARG(3) +int socket_watcher_init(Loop *loop, SocketWatcher *watcher, + const char *endpoint) + FUNC_ATTR_NONNULL_ALL { - // Trim to `ADDRESS_MAX_SIZE` - if (xstrlcpy(watcher->addr, endpoint, sizeof(watcher->addr)) - >= sizeof(watcher->addr)) { - // TODO(aktau): since this is not what the user wanted, perhaps we - // should return an error here - WLOG("Address was too long, truncated to %s", watcher->addr); - } + xstrlcpy(watcher->addr, endpoint, sizeof(watcher->addr)); + char *addr = watcher->addr; + char *host_end = strrchr(addr, ':'); - bool tcp = true; - char ip[16], *ip_end = xstrchrnul(watcher->addr, ':'); + if (host_end && addr != host_end) { + // Split user specified address into two strings, addr(hostname) and port. + // The port part in watcher->addr will be updated later. + *host_end = '\0'; + char *port = host_end + 1; + intmax_t iport; - // (ip_end - addr) is always > 0, so convert to size_t - size_t addr_len = (size_t)(ip_end - watcher->addr); - - if (addr_len > sizeof(ip) - 1) { - // Maximum length of an IPv4 address buffer is 15 (eg: 255.255.255.255) - addr_len = sizeof(ip) - 1; - } + int ret = getdigits_safe(&(char_u *){ (char_u *)port }, &iport); + if (ret == FAIL || iport < 0 || iport > UINT16_MAX) { + ELOG("Invalid port: %s", port); + return UV_EINVAL; + } - // Extract the address part - xstrlcpy(ip, watcher->addr, addr_len + 1); - int port = NVIM_DEFAULT_TCP_PORT; - - if (*ip_end == ':') { - // Extract the port - long lport = strtol(ip_end + 1, NULL, 10); // NOLINT - if (lport <= 0 || lport > 0xffff) { - // Invalid port, treat as named pipe or unix socket - tcp = false; - } else { - port = (int) lport; + if (*port == NUL) { + // When no port is given, (uv_)getaddrinfo expects NULL otherwise the + // implementation may attempt to lookup the service by name (and fail) + port = NULL; } - } - if (tcp) { - // Try to parse ip address - if (uv_ip4_addr(ip, port, &watcher->uv.tcp.addr)) { - // Invalid address, treat as named pipe or unix socket - tcp = false; + uv_getaddrinfo_t request; + + int retval = uv_getaddrinfo(&loop->uv, &request, NULL, addr, port, + &(struct addrinfo){ + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_STREAM, + }); + if (retval != 0) { + ELOG("Host lookup failed: %s", endpoint); + return retval; } - } + watcher->uv.tcp.addrinfo = request.addrinfo; - if (tcp) { uv_tcp_init(&loop->uv, &watcher->uv.tcp.handle); watcher->stream = STRUCT_CAST(uv_stream_t, &watcher->uv.tcp.handle); } else { @@ -82,33 +76,60 @@ void socket_watcher_init(Loop *loop, SocketWatcher *watcher, watcher->cb = NULL; watcher->close_cb = NULL; watcher->events = NULL; + watcher->data = NULL; + + return 0; } int socket_watcher_start(SocketWatcher *watcher, int backlog, socket_cb cb) FUNC_ATTR_NONNULL_ALL { watcher->cb = cb; - int result; + int result = UV_EINVAL; if (watcher->stream->type == UV_TCP) { - result = uv_tcp_bind(&watcher->uv.tcp.handle, - (const struct sockaddr *)&watcher->uv.tcp.addr, 0); + struct addrinfo *ai = watcher->uv.tcp.addrinfo; + + for (; ai; ai = ai->ai_next) { + result = uv_tcp_bind(&watcher->uv.tcp.handle, ai->ai_addr, 0); + if (result != 0) { + continue; + } + result = uv_listen(watcher->stream, backlog, connection_cb); + if (result == 0) { + struct sockaddr_storage sas; + + // When the endpoint in socket_watcher_init() didn't specify a port + // number, a free random port number will be assigned. sin_port will + // contain 0 in this case, unless uv_tcp_getsockname() is used first. + uv_tcp_getsockname(&watcher->uv.tcp.handle, (struct sockaddr *)&sas, + &(int){ sizeof(sas) }); + uint16_t port = (uint16_t)((sas.ss_family == AF_INET) + ? ((struct sockaddr_in *)&sas)->sin_port + : ((struct sockaddr_in6 *)&sas)->sin6_port); + // v:servername uses the string from watcher->addr + size_t len = strlen(watcher->addr); + snprintf(watcher->addr+len, sizeof(watcher->addr)-len, ":%" PRIu16, + ntohs(port)); + break; + } + } + uv_freeaddrinfo(watcher->uv.tcp.addrinfo); } else { result = uv_pipe_bind(&watcher->uv.pipe.handle, watcher->addr); - } - - if (result == 0) { - result = uv_listen(watcher->stream, backlog, connection_cb); + if (result == 0) { + result = uv_listen(watcher->stream, backlog, connection_cb); + } } assert(result <= 0); // libuv should return negative error code or zero. if (result < 0) { - if (result == -EACCES) { + if (result == UV_EACCES) { // Libuv converts ENOENT to EACCES for Windows compatibility, but if // the parent directory does not exist, ENOENT would be more accurate. *path_tail((char_u *)watcher->addr) = NUL; if (!os_path_exists((char_u *)watcher->addr)) { - result = -ENOENT; + result = UV_ENOENT; } } return result; @@ -169,3 +190,76 @@ static void close_cb(uv_handle_t *handle) watcher->close_cb(watcher, watcher->data); } } + +static void connect_cb(uv_connect_t *req, int status) +{ + int *ret_status = req->data; + *ret_status = status; + if (status != 0) { + uv_close((uv_handle_t *)req->handle, NULL); + } +} + +bool socket_connect(Loop *loop, Stream *stream, + bool is_tcp, const char *address, + int timeout, const char **error) +{ + bool success = false; + int status; + uv_connect_t req; + req.data = &status; + uv_stream_t *uv_stream; + + uv_tcp_t *tcp = &stream->uv.tcp; + uv_getaddrinfo_t addr_req; + addr_req.addrinfo = NULL; + const struct addrinfo *addrinfo = NULL; + char *addr = NULL; + if (is_tcp) { + addr = xstrdup(address); + char *host_end = strrchr(addr, ':'); + if (!host_end) { + *error = _("tcp address must be host:port"); + goto cleanup; + } + *host_end = NUL; + + const struct addrinfo hints = { .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_STREAM, + .ai_flags = AI_NUMERICSERV }; + int retval = uv_getaddrinfo(&loop->uv, &addr_req, NULL, + addr, host_end+1, &hints); + if (retval != 0) { + *error = _("failed to lookup host or port"); + goto cleanup; + } + addrinfo = addr_req.addrinfo; + +tcp_retry: + uv_tcp_init(&loop->uv, tcp); + uv_tcp_connect(&req, tcp, addrinfo->ai_addr, connect_cb); + uv_stream = (uv_stream_t *)tcp; + + } else { + uv_pipe_t *pipe = &stream->uv.pipe; + uv_pipe_init(&loop->uv, pipe, 0); + uv_pipe_connect(&req, pipe, address, connect_cb); + uv_stream = (uv_stream_t *)pipe; + } + status = 1; + LOOP_PROCESS_EVENTS_UNTIL(&main_loop, NULL, timeout, status != 1); + if (status == 0) { + stream_init(NULL, stream, -1, uv_stream); + success = true; + } else if (is_tcp && addrinfo->ai_next) { + addrinfo = addrinfo->ai_next; + goto tcp_retry; + } else { + *error = _("connection refused"); + } + +cleanup: + xfree(addr); + uv_freeaddrinfo(addr_req.addrinfo); + return success; +} diff --git a/src/nvim/event/socket.h b/src/nvim/event/socket.h index eb0823c76d..d30ae45502 100644 --- a/src/nvim/event/socket.h +++ b/src/nvim/event/socket.h @@ -20,7 +20,7 @@ struct socket_watcher { union { struct { uv_tcp_t handle; - struct sockaddr_in addr; + struct addrinfo *addrinfo; } tcp; struct { uv_pipe_t handle; diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c index c3c393f1ec..4e42042959 100644 --- a/src/nvim/getchar.c +++ b/src/nvim/getchar.c @@ -18,6 +18,7 @@ #include "nvim/vim.h" #include "nvim/ascii.h" #include "nvim/getchar.h" +#include "nvim/buffer_defs.h" #include "nvim/charset.h" #include "nvim/cursor.h" #include "nvim/edit.h" @@ -47,6 +48,7 @@ #include "nvim/event/loop.h" #include "nvim/os/input.h" #include "nvim/os/os.h" +#include "nvim/api/private/handle.h" /* * These buffers are used for storing: @@ -102,11 +104,10 @@ static int block_redo = FALSE; (NORMAL + VISUAL + SELECTMODE + \ OP_PENDING)) ? (c1) : ((c1) ^ 0x80)) -/* - * Each mapping is put in one of the 256 hash lists, to speed up finding it. - */ -static mapblock_T *(maphash[256]); -static int maphash_valid = FALSE; +// Each mapping is put in one of the MAX_MAPHASH hash lists, +// to speed up finding it. +static mapblock_T *(maphash[MAX_MAPHASH]); +static bool maphash_valid = false; /* * List used for abbreviations. @@ -1907,7 +1908,7 @@ static int vgetorpeek(int advance) } if ((mp == NULL || max_mlen >= mp_match_len) - && keylen != KEYLEN_PART_MAP && keylen != KEYLEN_PART_KEY) { + && keylen != KEYLEN_PART_MAP) { // No matching mapping found or found a non-matching mapping that // matches at least what the matching mapping matched keylen = 0; @@ -4237,3 +4238,17 @@ static bool typebuf_match_len(const uint8_t *str, int *mlen) *mlen = i; return str[i] == NUL; // matched the whole string } + +/// Retrieve the mapblock at the index either globally or for a certain buffer +/// +/// @param index The index in the maphash[] +/// @param buf The buffer to get the maphash from. NULL for global +mapblock_T *get_maphash(int index, buf_T *buf) + FUNC_ATTR_PURE +{ + if (index > MAX_MAPHASH) { + return NULL; + } + + return (buf == NULL) ? maphash[index] : buf->b_maphash[index]; +} diff --git a/src/nvim/if_cscope.c b/src/nvim/if_cscope.c index e1c7df9175..f7706f5a0d 100644 --- a/src/nvim/if_cscope.c +++ b/src/nvim/if_cscope.c @@ -1972,7 +1972,7 @@ static void cs_release_csp(size_t i, int freefnpp) static int cs_reset(exarg_T *eap) { char **dblist = NULL, **pplist = NULL, **fllist = NULL; - char buf[20]; /* for snprintf " (#%zu)" */ + char buf[25]; // for snprintf " (#%zu)" if (csinfo_size == 0) return CSCOPE_SUCCESS; diff --git a/src/nvim/msgpack_rpc/channel.c b/src/nvim/msgpack_rpc/channel.c index cd64e14976..413b800af5 100644 --- a/src/nvim/msgpack_rpc/channel.c +++ b/src/nvim/msgpack_rpc/channel.c @@ -12,6 +12,7 @@ #include "nvim/api/vim.h" #include "nvim/api/ui.h" #include "nvim/msgpack_rpc/channel.h" +#include "nvim/msgpack_rpc/server.h" #include "nvim/event/loop.h" #include "nvim/event/libuv_process.h" #include "nvim/event/rstream.h" @@ -28,6 +29,7 @@ #include "nvim/map.h" #include "nvim/log.h" #include "nvim/misc1.h" +#include "nvim/path.h" #include "nvim/lib/kvec.h" #include "nvim/os/input.h" @@ -41,7 +43,8 @@ typedef enum { kChannelTypeSocket, kChannelTypeProc, - kChannelTypeStdio + kChannelTypeStdio, + kChannelTypeInternal } ChannelType; typedef struct { @@ -125,7 +128,7 @@ uint64_t channel_from_process(Process *proc, uint64_t id) wstream_init(proc->in, 0); rstream_init(proc->out, 0); - rstream_start(proc->out, parse_msgpack, channel); + rstream_start(proc->out, receive_msgpack, channel); return channel->id; } @@ -142,7 +145,36 @@ void channel_from_connection(SocketWatcher *watcher) channel->data.stream.internal_data = channel; wstream_init(&channel->data.stream, 0); rstream_init(&channel->data.stream, CHANNEL_BUFFER_SIZE); - rstream_start(&channel->data.stream, parse_msgpack, channel); + rstream_start(&channel->data.stream, receive_msgpack, channel); +} + +uint64_t channel_connect(bool tcp, const char *address, + int timeout, const char **error) +{ + if (!tcp) { + char *path = fix_fname(address); + if (server_owns_pipe_address(path)) { + // avoid deadlock + xfree(path); + return channel_create_internal(); + } + xfree(path); + } + + Channel *channel = register_channel(kChannelTypeSocket, 0, NULL); + if (!socket_connect(&main_loop, &channel->data.stream, + tcp, address, timeout, error)) { + decref(channel); + return 0; + } + + incref(channel); // close channel only after the stream is closed + channel->data.stream.internal_close_cb = close_cb; + channel->data.stream.internal_data = channel; + wstream_init(&channel->data.stream, 0); + rstream_init(&channel->data.stream, CHANNEL_BUFFER_SIZE); + rstream_start(&channel->data.stream, receive_msgpack, channel); + return channel->id; } /// Sends event/arguments to channel @@ -305,11 +337,20 @@ void channel_from_stdio(void) incref(channel); // stdio channels are only closed on exit // read stream rstream_init_fd(&main_loop, &channel->data.std.in, 0, CHANNEL_BUFFER_SIZE); - rstream_start(&channel->data.std.in, parse_msgpack, channel); + rstream_start(&channel->data.std.in, receive_msgpack, channel); // write stream wstream_init_fd(&main_loop, &channel->data.std.out, 1, 0); } +/// Creates a loopback channel. This is used to avoid deadlock +/// when an instance connects to its own named pipe. +uint64_t channel_create_internal(void) +{ + Channel *channel = register_channel(kChannelTypeInternal, 0, NULL); + incref(channel); // internal channel lives until process exit + return channel->id; +} + void channel_process_exit(uint64_t id, int status) { Channel *channel = pmap_get(uint64_t)(channels, id); @@ -318,8 +359,8 @@ void channel_process_exit(uint64_t id, int status) decref(channel); } -static void parse_msgpack(Stream *stream, RBuffer *rbuf, size_t c, void *data, - bool eof) +static void receive_msgpack(Stream *stream, RBuffer *rbuf, size_t c, + void *data, bool eof) { Channel *channel = data; incref(channel); @@ -341,6 +382,14 @@ static void parse_msgpack(Stream *stream, RBuffer *rbuf, size_t c, void *data, rbuffer_read(rbuf, msgpack_unpacker_buffer(channel->unpacker), count); msgpack_unpacker_buffer_consumed(channel->unpacker, count); + parse_msgpack(channel); + +end: + decref(channel); +} + +static void parse_msgpack(Channel *channel) +{ msgpack_unpacked unpacked; msgpack_unpacked_init(&unpacked); msgpack_unpack_return result; @@ -364,7 +413,7 @@ static void parse_msgpack(Stream *stream, RBuffer *rbuf, size_t c, void *data, } msgpack_unpacked_destroy(&unpacked); // Bail out from this event loop iteration - goto end; + return; } handle_request(channel, &unpacked.data); @@ -388,11 +437,9 @@ static void parse_msgpack(Stream *stream, RBuffer *rbuf, size_t c, void *data, "This error can also happen when deserializing " "an object with high level of nesting"); } - -end: - decref(channel); } + static void handle_request(Channel *channel, msgpack_object *request) FUNC_ATTR_NONNULL_ALL { @@ -485,7 +532,7 @@ static void on_request_event(void **argv) static bool channel_write(Channel *channel, WBuffer *buffer) { - bool success; + bool success = false; if (channel->closed) { wstream_release_wbuffer(buffer); @@ -502,8 +549,11 @@ static bool channel_write(Channel *channel, WBuffer *buffer) case kChannelTypeStdio: success = wstream_write(&channel->data.std.out, buffer); break; - default: - abort(); + case kChannelTypeInternal: + incref(channel); + CREATE_EVENT(channel->events, internal_read_event, 2, channel, buffer); + success = true; + break; } if (!success) { @@ -520,6 +570,22 @@ static bool channel_write(Channel *channel, WBuffer *buffer) return success; } +static void internal_read_event(void **argv) +{ + Channel *channel = argv[0]; + WBuffer *buffer = argv[1]; + + msgpack_unpacker_reserve_buffer(channel->unpacker, buffer->size); + memcpy(msgpack_unpacker_buffer(channel->unpacker), + buffer->data, buffer->size); + msgpack_unpacker_buffer_consumed(channel->unpacker, buffer->size); + + parse_msgpack(channel); + + decref(channel); + wstream_release_wbuffer(buffer); +} + static void send_error(Channel *channel, uint64_t id, char *err) { Error e = ERROR_INIT; @@ -636,8 +702,9 @@ static void close_channel(Channel *channel) stream_close(&channel->data.std.out, NULL, NULL); multiqueue_put(main_loop.fast_events, exit_event, 1, channel); return; - default: - abort(); + case kChannelTypeInternal: + // nothing to free. + break; } decref(channel); diff --git a/src/nvim/msgpack_rpc/server.c b/src/nvim/msgpack_rpc/server.c index b6958088ca..c9edd05dc2 100644 --- a/src/nvim/msgpack_rpc/server.c +++ b/src/nvim/msgpack_rpc/server.c @@ -97,37 +97,59 @@ char *server_address_new(void) #endif } -/// Starts listening for API calls on the TCP address or pipe path `endpoint`. +/// Check if this instance owns a pipe address. +/// The argument must already be resolved to an absolute path! +bool server_owns_pipe_address(const char *path) +{ + for (int i = 0; i < watchers.ga_len; i++) { + if (!strcmp(path, ((SocketWatcher **)watchers.ga_data)[i]->addr)) { + return true; + } + } + return false; +} + +/// Starts listening for API calls. +/// /// The socket type is determined by parsing `endpoint`: If it's a valid IPv4 -/// address in 'ip[:port]' format, then it will be TCP socket. The port is -/// optional and if omitted defaults to NVIM_DEFAULT_TCP_PORT. Otherwise it -/// will be a unix socket or named pipe. +/// or IPv6 address in 'ip:[port]' format, then it will be a TCP socket. +/// Otherwise it will be a Unix socket or named pipe (Windows). +/// +/// If no port is given, a random one will be assigned. /// -/// @param endpoint Address of the server. Either a 'ip[:port]' string or an -/// arbitrary identifier (trimmed to 256 bytes) for the unix socket or -/// named pipe. +/// @param endpoint Address of the server. Either a 'ip:[port]' string or an +/// arbitrary identifier (trimmed to 256 bytes) for the Unix +/// socket or named pipe. /// @returns 0 on success, 1 on a regular error, and negative errno -/// on failure to bind or connect. +/// on failure to bind or listen. int server_start(const char *endpoint) { - if (endpoint == NULL) { - ELOG("Attempting to start server on NULL endpoint"); + if (endpoint == NULL || endpoint[0] == '\0') { + ELOG("Empty or NULL endpoint"); return 1; } SocketWatcher *watcher = xmalloc(sizeof(SocketWatcher)); - socket_watcher_init(&main_loop, watcher, endpoint, NULL); + + int result = socket_watcher_init(&main_loop, watcher, endpoint); + if (result < 0) { + xfree(watcher); + return result; + } // Check if a watcher for the endpoint already exists for (int i = 0; i < watchers.ga_len; i++) { if (!strcmp(watcher->addr, ((SocketWatcher **)watchers.ga_data)[i]->addr)) { ELOG("Already listening on %s", watcher->addr); + if (watcher->stream->type == UV_TCP) { + uv_freeaddrinfo(watcher->uv.tcp.addrinfo); + } socket_watcher_close(watcher, free_server); return 1; } } - int result = socket_watcher_start(watcher, MAX_CONNECTIONS, connection_cb); + result = socket_watcher_start(watcher, MAX_CONNECTIONS, connection_cb); if (result < 0) { ELOG("Failed to start server: %s", uv_strerror(result)); socket_watcher_close(watcher, free_server); diff --git a/src/nvim/os/pty_process_unix.c b/src/nvim/os/pty_process_unix.c index eb9335b03c..ee3ab96a83 100644 --- a/src/nvim/os/pty_process_unix.c +++ b/src/nvim/os/pty_process_unix.c @@ -12,7 +12,7 @@ #include <sys/ioctl.h> // forkpty is not in POSIX, so headers are platform-specific -#if defined(__FreeBSD__) +#if defined(__FreeBSD__) || defined (__DragonFly__) # include <libutil.h> #elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) # include <util.h> diff --git a/src/nvim/path.c b/src/nvim/path.c index 9162b6da4d..f2339c8046 100644 --- a/src/nvim/path.c +++ b/src/nvim/path.c @@ -1715,7 +1715,7 @@ int vim_FullName(const char *fname, char *buf, size_t len, bool force) /// /// @param fname is the filename to expand /// @return [allocated] Full path (NULL for failure). -char *fix_fname(char *fname) +char *fix_fname(const char *fname) { #ifdef UNIX return FullName_save(fname, true); diff --git a/src/nvim/quickfix.c b/src/nvim/quickfix.c index 0ec0d5df9d..ff51bf289e 100644 --- a/src/nvim/quickfix.c +++ b/src/nvim/quickfix.c @@ -188,6 +188,11 @@ typedef struct { */ #define GET_LOC_LIST(wp) (IS_LL_WINDOW(wp) ? wp->w_llist_ref : wp->w_llist) +// Looking up a buffer can be slow if there are many. Remember the last one +// to make this a lot faster if there are multiple matches in the same file. +static char_u *qf_last_bufname = NULL; +static bufref_T qf_last_bufref = { NULL, 0 }; + /* * Read the errorfile "efile" into memory, line by line, building the error * list. Set the error list's title to qf_title. @@ -968,6 +973,10 @@ qf_init_ext( int retval = -1; // default: return error flag int status; + // Do not used the cached buffer, it may have been wiped out. + xfree(qf_last_bufname); + qf_last_bufname = NULL; + fields.namebuf = xmalloc(CMDBUFFSIZE + 1); fields.errmsglen = CMDBUFFSIZE + 1; fields.errmsg = xmalloc(fields.errmsglen); @@ -1399,11 +1408,6 @@ void copy_loclist(win_T *from, win_T *to) to->w_llist->qf_curlist = qi->qf_curlist; /* current list */ } -// Looking up a buffer can be slow if there are many. Remember the last one to -// make this a lot faster if there are multiple matches in the same file. -static char_u *qf_last_bufname = NULL; -static bufref_T qf_last_bufref = { NULL, 0 }; - // Get buffer number for file "directory.fname". // Also sets the b_has_qf_entry flag. static int qf_get_fnum(qf_info_T *qi, char_u *directory, char_u *fname) diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index b8b7085c5e..5b250ebf54 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -237,8 +237,6 @@ Terminal *terminal_open(TerminalOptions opts) curbuf->b_p_scbk = p_scbk; // 'scrollback' curbuf->b_p_tw = 0; // 'textwidth' set_option_value("wrap", false, NULL, OPT_LOCAL); - set_option_value("number", false, NULL, OPT_LOCAL); - set_option_value("relativenumber", false, NULL, OPT_LOCAL); set_option_value("list", false, NULL, OPT_LOCAL); buf_set_term_title(curbuf, (char *)curbuf->b_ffname); RESET_BINDING(curwin); diff --git a/test/functional/api/keymap_spec.lua b/test/functional/api/keymap_spec.lua new file mode 100644 index 0000000000..833e0d2f3c --- /dev/null +++ b/test/functional/api/keymap_spec.lua @@ -0,0 +1,246 @@ + +local helpers = require('test.functional.helpers')(after_each) +local clear = helpers.clear +local command = helpers.command +local curbufmeths = helpers.curbufmeths +local eq = helpers.eq +local funcs = helpers.funcs +local meths = helpers.meths +local source = helpers.source + +local function local_copy(t) + local copy = {} + for k,v in pairs(t) do + copy[k] = v + end + return copy +end + +describe('get_keymap', function() + before_each(clear) + + -- Basic mapping and table to be used to describe results + local foo_bar_string = 'nnoremap foo bar' + local foo_bar_map_table = { + lhs='foo', + silent=0, + rhs='bar', + expr=0, + sid=0, + buffer=0, + nowait=0, + mode='n', + noremap=1, + } + + it('returns empty list when no map', function() + eq({}, meths.get_keymap('n')) + end) + + it('returns list of all applicable mappings', function() + command(foo_bar_string) + -- Only one mapping available + -- Should be the same as the dictionary we supplied earlier + -- and the dictionary you would get from maparg + -- since this is a global map, and not script local + eq({foo_bar_map_table}, meths.get_keymap('n')) + eq({funcs.maparg('foo', 'n', false, true)}, + meths.get_keymap('n') + ) + + -- Add another mapping + command('nnoremap foo_longer bar_longer') + local foolong_bar_map_table = local_copy(foo_bar_map_table) + foolong_bar_map_table['lhs'] = 'foo_longer' + foolong_bar_map_table['rhs'] = 'bar_longer' + + eq({foolong_bar_map_table, foo_bar_map_table}, + meths.get_keymap('n') + ) + + -- Remove a mapping + command('unmap foo_longer') + eq({foo_bar_map_table}, + meths.get_keymap('n') + ) + end) + + it('works for other modes', function() + -- Add two mappings, one in insert and one normal + -- We'll only check the insert mode one + command('nnoremap not_going to_check') + + command('inoremap foo bar') + -- The table will be the same except for the mode + local insert_table = local_copy(foo_bar_map_table) + insert_table['mode'] = 'i' + + eq({insert_table}, meths.get_keymap('i')) + end) + + it('considers scope', function() + -- change the map slightly + command('nnoremap foo_longer bar_longer') + local foolong_bar_map_table = local_copy(foo_bar_map_table) + foolong_bar_map_table['lhs'] = 'foo_longer' + foolong_bar_map_table['rhs'] = 'bar_longer' + + local buffer_table = local_copy(foo_bar_map_table) + buffer_table['buffer'] = 1 + + command('nnoremap <buffer> foo bar') + + -- The buffer mapping should not show up + eq({foolong_bar_map_table}, meths.get_keymap('n')) + eq({buffer_table}, curbufmeths.get_keymap('n')) + end) + + it('considers scope for overlapping maps', function() + command('nnoremap foo bar') + + local buffer_table = local_copy(foo_bar_map_table) + buffer_table['buffer'] = 1 + + command('nnoremap <buffer> foo bar') + + eq({foo_bar_map_table}, meths.get_keymap('n')) + eq({buffer_table}, curbufmeths.get_keymap('n')) + end) + + it('can retrieve mapping for different buffers', function() + local original_buffer = curbufmeths.get_number() + -- Place something in each of the buffers to make sure they stick around + -- and set hidden so we can leave them + command('set hidden') + command('new') + command('normal! ihello 2') + command('new') + command('normal! ihello 3') + + local final_buffer = curbufmeths.get_number() + + command('nnoremap <buffer> foo bar') + -- Final buffer will have buffer mappings + local buffer_table = local_copy(foo_bar_map_table) + buffer_table['buffer'] = final_buffer + eq({buffer_table}, meths.buf_get_keymap(final_buffer, 'n')) + eq({buffer_table}, meths.buf_get_keymap(0, 'n')) + + command('buffer ' .. original_buffer) + eq(original_buffer, curbufmeths.get_number()) + -- Original buffer won't have any mappings + eq({}, meths.get_keymap('n')) + eq({}, curbufmeths.get_keymap('n')) + eq({buffer_table}, meths.buf_get_keymap(final_buffer, 'n')) + end) + + -- Test toggle switches for basic options + -- @param option The key represented in the `maparg()` result dict + local function global_and_buffer_test(map, + option, + option_token, + global_on_result, + buffer_on_result, + global_off_result, + buffer_off_result, + new_windows) + + local function make_new_windows(number_of_windows) + if new_windows == nil then + return nil + end + + for _=1,number_of_windows do + command('new') + end + end + + local mode = string.sub(map, 1,1) + -- Don't run this for the <buffer> mapping, since it doesn't make sense + if option_token ~= '<buffer>' then + it(string.format( 'returns %d for the key "%s" when %s is used globally with %s (%s)', + global_on_result, option, option_token, map, mode), function() + make_new_windows(new_windows) + command(map .. ' ' .. option_token .. ' foo bar') + local result = meths.get_keymap(mode)[1][option] + eq(global_on_result, result) + end) + end + + it(string.format('returns %d for the key "%s" when %s is used for buffers with %s (%s)', + buffer_on_result, option, option_token, map, mode), function() + make_new_windows(new_windows) + command(map .. ' <buffer> ' .. option_token .. ' foo bar') + local result = curbufmeths.get_keymap(mode)[1][option] + eq(buffer_on_result, result) + end) + + -- Don't run these for the <buffer> mapping, since it doesn't make sense + if option_token ~= '<buffer>' then + it(string.format('returns %d for the key "%s" when %s is not used globally with %s (%s)', + global_off_result, option, option_token, map, mode), function() + make_new_windows(new_windows) + command(map .. ' baz bat') + local result = meths.get_keymap(mode)[1][option] + eq(global_off_result, result) + end) + + it(string.format('returns %d for the key "%s" when %s is not used for buffers with %s (%s)', + buffer_off_result, option, option_token, map, mode), function() + make_new_windows(new_windows) + command(map .. ' <buffer> foo bar') + + local result = curbufmeths.get_keymap(mode)[1][option] + eq(buffer_off_result, result) + end) + end + end + + -- Standard modes and returns the same values in the dictionary as maparg() + local mode_list = {'nnoremap', 'nmap', 'imap', 'inoremap', 'cnoremap'} + for mode in pairs(mode_list) do + global_and_buffer_test(mode_list[mode], 'silent', '<silent>', 1, 1, 0, 0) + global_and_buffer_test(mode_list[mode], 'nowait', '<nowait>', 1, 1, 0, 0) + global_and_buffer_test(mode_list[mode], 'expr', '<expr>', 1, 1, 0, 0) + end + + -- noremap will now be 2 if script was used, which is not the same as maparg() + global_and_buffer_test('nmap', 'noremap', '<script>', 2, 2, 0, 0) + global_and_buffer_test('nnoremap', 'noremap', '<script>', 2, 2, 1, 1) + + -- buffer will return the buffer ID, which is not the same as maparg() + -- Three of these tests won't run + global_and_buffer_test('nnoremap', 'buffer', '<buffer>', nil, 3, nil, nil, 2) + + it('returns script numbers for global maps', function() + source([[ + function! s:maparg_test_function() abort + return 'testing' + endfunction + + nnoremap fizz :call <SID>maparg_test_function()<CR> + ]]) + local sid_result = meths.get_keymap('n')[1]['sid'] + eq(1, sid_result) + eq('testing', meths.call_function('<SNR>' .. sid_result .. '_maparg_test_function', {})) + end) + + it('returns script numbers for buffer maps', function() + source([[ + function! s:maparg_test_function() abort + return 'testing' + endfunction + + nnoremap <buffer> fizz :call <SID>maparg_test_function()<CR> + ]]) + local sid_result = curbufmeths.get_keymap('n')[1]['sid'] + eq(1, sid_result) + eq('testing', meths.call_function('<SNR>' .. sid_result .. '_maparg_test_function', {})) + end) + + it('works with <F12> and others', function() + command('nnoremap <F12> :let g:maparg_test_var = 1<CR>') + eq('<F12>', meths.get_keymap('n')[1]['lhs']) + eq(':let g:maparg_test_var = 1<CR>', meths.get_keymap('n')[1]['rhs']) + end) +end) diff --git a/test/functional/api/server_requests_spec.lua b/test/functional/api/server_requests_spec.lua index 658077b112..cf15062325 100644 --- a/test/functional/api/server_requests_spec.lua +++ b/test/functional/api/server_requests_spec.lua @@ -9,6 +9,8 @@ local nvim_prog, command, funcs = helpers.nvim_prog, helpers.command, helpers.fu local source, next_message = helpers.source, helpers.next_message local ok = helpers.ok local meths = helpers.meths +local spawn, nvim_argv = helpers.spawn, helpers.nvim_argv +local set_session = helpers.set_session describe('server -> client', function() local cid @@ -225,4 +227,75 @@ describe('server -> client', function() end) end) + describe('when connecting to another nvim instance', function() + local function connect_test(server, mode, address) + local serverpid = funcs.getpid() + local client = spawn(nvim_argv) + set_session(client, true) + local clientpid = funcs.getpid() + neq(serverpid, clientpid) + local id = funcs.sockconnect(mode, address, {rpc=true}) + ok(id > 0) + + funcs.rpcrequest(id, 'nvim_set_current_line', 'hello') + local client_id = funcs.rpcrequest(id, 'nvim_get_api_info')[1] + + set_session(server, true) + eq(serverpid, funcs.getpid()) + eq('hello', meths.get_current_line()) + + -- method calls work both ways + funcs.rpcrequest(client_id, 'nvim_set_current_line', 'howdy!') + eq(id, funcs.rpcrequest(client_id, 'nvim_get_api_info')[1]) + + set_session(client, true) + eq(clientpid, funcs.getpid()) + eq('howdy!', meths.get_current_line()) + + server:close() + client:close() + end + + it('over a named pipe', function() + local server = spawn(nvim_argv) + set_session(server) + local address = funcs.serverlist()[1] + local first = string.sub(address,1,1) + ok(first == '/' or first == '\\') + connect_test(server, 'pipe', address) + end) + + it('to an ip adress', function() + local server = spawn(nvim_argv) + set_session(server) + local address = funcs.serverstart("127.0.0.1:") + eq('127.0.0.1:', string.sub(address,1,10)) + connect_test(server, 'tcp', address) + end) + + it('to a hostname', function() + local server = spawn(nvim_argv) + set_session(server) + local address = funcs.serverstart("localhost:") + eq('localhost:', string.sub(address,1,10)) + connect_test(server, 'tcp', address) + end) + end) + + describe('when connecting to its own pipe adress', function() + it('it does not deadlock', function() + local address = funcs.serverlist()[1] + local first = string.sub(address,1,1) + ok(first == '/' or first == '\\') + local serverpid = funcs.getpid() + + local id = funcs.sockconnect('pipe', address, {rpc=true}) + + funcs.rpcrequest(id, 'nvim_set_current_line', 'hello') + eq('hello', meths.get_current_line()) + eq(serverpid, funcs.rpcrequest(id, "nvim_eval", "getpid()")) + + eq(id, funcs.rpcrequest(id, 'nvim_get_api_info')[1]) + end) + end) end) diff --git a/test/functional/eval/map_functions_spec.lua b/test/functional/eval/map_functions_spec.lua new file mode 100644 index 0000000000..a260522aa3 --- /dev/null +++ b/test/functional/eval/map_functions_spec.lua @@ -0,0 +1,120 @@ + +local helpers = require('test.functional.helpers')(after_each) +local clear = helpers.clear +local eq = helpers.eq +local eval = helpers.eval +local funcs = helpers.funcs +local nvim = helpers.nvim +local source = helpers.source + +describe('maparg()', function() + before_each(clear) + + local foo_bar_map_table = { + lhs='foo', + silent=0, + rhs='bar', + expr=0, + sid=0, + buffer=0, + nowait=0, + mode='n', + noremap=1, + } + + it('returns a dictionary', function() + nvim('command', 'nnoremap foo bar') + eq('bar', funcs.maparg('foo')) + eq(foo_bar_map_table, funcs.maparg('foo', 'n', false, true)) + end) + + it('returns 1 for silent when <silent> is used', function() + nvim('command', 'nnoremap <silent> foo bar') + eq(1, funcs.maparg('foo', 'n', false, true)['silent']) + + nvim('command', 'nnoremap baz bat') + eq(0, funcs.maparg('baz', 'n', false, true)['silent']) + end) + + it('returns an empty string when no map is present', function() + eq('', funcs.maparg('not a mapping')) + end) + + it('returns an empty dictionary when no map is present and dict is requested', function() + eq({}, funcs.maparg('not a mapping', 'n', false, true)) + end) + + it('returns the same value for noremap and <script>', function() + nvim('command', 'inoremap <script> hello world') + nvim('command', 'inoremap this that') + eq( + funcs.maparg('hello', 'i', false, true)['noremap'], + funcs.maparg('this', 'i', false, true)['noremap'] + ) + end) + + it('returns a boolean for buffer', function() + -- Open enough windows to know we aren't on buffer number 1 + nvim('command', 'new') + nvim('command', 'new') + nvim('command', 'new') + nvim('command', 'cnoremap <buffer> this that') + eq(1, funcs.maparg('this', 'c', false, true)['buffer']) + + -- Global will return 0 always + nvim('command', 'nnoremap other another') + eq(0, funcs.maparg('other', 'n', false, true)['buffer']) + end) + + it('returns script numbers', function() + source([[ + function! s:maparg_test_function() abort + return 'testing' + endfunction + + nnoremap fizz :call <SID>maparg_test_function()<CR> + ]]) + eq(1, funcs.maparg('fizz', 'n', false, true)['sid']) + eq('testing', nvim('call_function', '<SNR>1_maparg_test_function', {})) + end) + + it('works with <F12> and others', function() + source([[ + let g:maparg_test_var = 0 + + nnoremap <F12> :let g:maparg_test_var = 1<CR> + ]]) + eq(0, eval('g:maparg_test_var')) + source([[ + call feedkeys("\<F12>") + ]]) + eq(1, eval('g:maparg_test_var')) + + eq(':let g:maparg_test_var = 1<CR>', funcs.maparg('<F12>', 'n', false, true)['rhs']) + end) + + it('works with <expr>', function() + source([[ + let counter = 0 + inoremap <expr> <C-L> ListItem() + inoremap <expr> <C-R> ListReset() + + func ListItem() + let g:counter += 1 + return g:counter . '. ' + endfunc + + func ListReset() + let g:counter = 0 + return '' + endfunc + + call feedkeys("i\<C-L>") + ]]) + eq(1, eval('g:counter')) + + local map_dict = funcs.maparg('<C-L>', 'i', false, true) + eq(1, map_dict['expr']) + eq('i', map_dict['mode']) + end) +end) diff --git a/test/functional/eval/server_spec.lua b/test/functional/eval/server_spec.lua index 420aea04aa..115114c3c3 100644 --- a/test/functional/eval/server_spec.lua +++ b/test/functional/eval/server_spec.lua @@ -1,20 +1,27 @@ local helpers = require('test.functional.helpers')(after_each) -local nvim, eq, neq, eval = helpers.nvim, helpers.eq, helpers.neq, helpers.eval +local eq, neq, eval = helpers.eq, helpers.neq, helpers.eval +local command = helpers.command local clear, funcs, meths = helpers.clear, helpers.funcs, helpers.meths local os_name = helpers.os_name +local function clear_serverlist() + for _, server in pairs(funcs.serverlist()) do + funcs.serverstop(server) + end +end + describe('serverstart(), serverstop()', function() before_each(clear) it('sets $NVIM_LISTEN_ADDRESS on first invocation', function() -- Unset $NVIM_LISTEN_ADDRESS - nvim('command', 'let $NVIM_LISTEN_ADDRESS = ""') + command('let $NVIM_LISTEN_ADDRESS = ""') local s = eval('serverstart()') assert(s ~= nil and s:len() > 0, "serverstart() returned empty") eq(s, eval('$NVIM_LISTEN_ADDRESS')) - nvim('command', "call serverstop('"..s.."')") + command("call serverstop('"..s.."')") eq('', eval('$NVIM_LISTEN_ADDRESS')) end) @@ -47,10 +54,38 @@ describe('serverstart(), serverstop()', function() end) it('serverstop() ignores invalid input', function() - nvim('command', "call serverstop('')") - nvim('command', "call serverstop('bogus-socket-name')") + command("call serverstop('')") + command("call serverstop('bogus-socket-name')") end) + it('parses endpoints correctly', function() + clear_serverlist() + eq({}, funcs.serverlist()) + + local s = funcs.serverstart('127.0.0.1:0') -- assign random port + assert(string.match(s, '127.0.0.1:%d+')) + eq(s, funcs.serverlist()[1]) + clear_serverlist() + + s = funcs.serverstart('127.0.0.1:') -- assign random port + assert(string.match(s, '127.0.0.1:%d+')) + eq(s, funcs.serverlist()[1]) + clear_serverlist() + + funcs.serverstart('127.0.0.1:12345') + funcs.serverstart('127.0.0.1:12345') -- exists already; ignore + funcs.serverstart('::1:12345') + funcs.serverstart('::1:12345') -- exists already; ignore + local expected = { + '127.0.0.1:12345', + '::1:12345', + } + eq(expected, funcs.serverlist()) + clear_serverlist() + + funcs.serverstart('127.0.0.1:65536') -- invalid port + eq({}, funcs.serverlist()) + end) end) describe('serverlist()', function() @@ -75,7 +110,7 @@ describe('serverlist()', function() -- The new servers should be at the end of the list. for i = 1, #servs do eq(servs[i], new_servs[i + n]) - nvim('command', "call serverstop('"..servs[i].."')") + command("call serverstop('"..servs[i].."')") end -- After serverstop() the servers should NOT be in the list. eq(n, eval('len(serverlist())')) diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua index b03840b3fe..62b0ce1200 100644 --- a/test/functional/helpers.lua +++ b/test/functional/helpers.lua @@ -76,8 +76,8 @@ end local session, loop_running, last_error -local function set_session(s) - if session then +local function set_session(s, keep) + if session and not keep then session:close() end session = s @@ -609,6 +609,7 @@ local module = { nvim = nvim, nvim_async = nvim_async, nvim_prog = nvim_prog, + nvim_argv = nvim_argv, nvim_set = nvim_set, nvim_dir = nvim_dir, buffer = buffer, diff --git a/test/functional/options/pastetoggle_spec.lua b/test/functional/options/pastetoggle_spec.lua index ec3c60fe37..a1f86f73ff 100644 --- a/test/functional/options/pastetoggle_spec.lua +++ b/test/functional/options/pastetoggle_spec.lua @@ -6,32 +6,35 @@ local command = helpers.command local eq = helpers.eq local eval = helpers.eval local sleep = helpers.sleep +local expect = helpers.expect describe("'pastetoggle' option", function() before_each(function() clear() command('set nopaste') - command('set pastetoggle=a') end) + it("toggles 'paste'", function() - eq(eval('&paste'), 0) + command('set pastetoggle=a') + eq(0, eval('&paste')) feed('a') -- Need another key so that the vgetorpeek() function returns. feed('j') - eq(eval('&paste'), 1) + eq(1, eval('&paste')) end) - it("multiple key 'pastetoggle' is waited for", function() - eq(eval('&paste'), 0) - local pastetoggle = 'lllll' - command('set pastetoggle=' .. pastetoggle) - command('set timeoutlen=1 ttimeoutlen=10000') - feed(pastetoggle:sub(0, 2)) - -- sleep() for long enough that vgetorpeek() is gotten into, but short - -- enough that ttimeoutlen is not reached. - sleep(200) - feed(pastetoggle:sub(3, -1)) - -- Need another key so that the vgetorpeek() function returns. - feed('j') - eq(eval('&paste'), 1) + + + it('does not wait for timeout', function() + command('set pastetoggle=abc') + command('set ttimeoutlen=9999999') + eq(0, eval('&paste')) + -- n.b. need <esc> to return from vgetorpeek() + feed('abc<esc>') + eq(1, eval('&paste')) + feed('ab') + sleep(10) + feed('c<esc>') + expect('bc') + eq(1, eval('&paste')) end) end) diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt index b8dad75b24..db580ecc9d 100644 --- a/third-party/CMakeLists.txt +++ b/third-party/CMakeLists.txt @@ -99,8 +99,9 @@ set(LUAJIT_SHA256 620fa4eb12375021bef6e4f237cbd2dd5d49e56beb414bee052c746beef180 set(LUA_URL https://www.lua.org/ftp/lua-5.1.5.tar.gz) set(LUA_SHA256 2640fc56a795f29d28ef15e13c34a47e223960b0240e8cb0a82d9b0738695333) -set(LUAROCKS_URL https://github.com/keplerproject/luarocks/archive/5d8a16526573b36d5b22aa74866120c998466697.tar.gz) -set(LUAROCKS_SHA256 cae709111c5701235770047dfd7169f66b82ae1c7b9b79207f9df0afb722bfd9) +# NOTE: Version must match LUAROCKS_VERSION in third-party/cmake/BuildLuarocks.cmake +set(LUAROCKS_URL https://github.com/luarocks/luarocks/archive/v2.4.2.tar.gz) +set(LUAROCKS_SHA256 eef88c2429c715a7beb921e4b1ba571dddb7c74a250fbb0d3cc0d4be7a5865d9) set(UNIBILIUM_URL https://github.com/mauke/unibilium/archive/v1.2.0.tar.gz) set(UNIBILIUM_SHA256 623af1099515e673abfd3cae5f2fa808a09ca55dda1c65a7b5c9424eb304ead8) diff --git a/third-party/cmake/BuildLuarocks.cmake b/third-party/cmake/BuildLuarocks.cmake index 88ddaf44ce..ef8a8450f1 100644 --- a/third-party/cmake/BuildLuarocks.cmake +++ b/third-party/cmake/BuildLuarocks.cmake @@ -41,6 +41,9 @@ endfunction() # The luarocks binary location set(LUAROCKS_BINARY ${HOSTDEPS_BIN_DIR}/luarocks) +# NOTE: Version must match version of LuaRocks in third-party/CMakeLists.txt +set(LUAROCKS_VERSION 2.4) + # Arguments for calls to 'luarocks build' if(NOT MSVC) # In MSVC don't pass the compiler/linker to luarocks, the bundled @@ -73,13 +76,13 @@ elseif(MSVC OR MINGW) /LIB ${DEPS_LIB_DIR} /BIN ${DEPS_BIN_DIR} /INC ${DEPS_INSTALL_DIR}/include/luajit-2.0/ - /P ${DEPS_INSTALL_DIR} /TREE ${DEPS_INSTALL_DIR} + /P ${DEPS_INSTALL_DIR}/${LUAROCKS_VERSION} /TREE ${DEPS_INSTALL_DIR} /SCRIPTS ${DEPS_BIN_DIR} /CMOD ${DEPS_BIN_DIR} ${MINGW_FLAG} /LUAMOD ${DEPS_BIN_DIR}/lua) - set(LUAROCKS_BINARY ${DEPS_INSTALL_DIR}/2.2/luarocks.bat) + set(LUAROCKS_BINARY ${DEPS_INSTALL_DIR}/${LUAROCKS_VERSION}/luarocks.bat) else() message(FATAL_ERROR "Trying to build luarocks in an unsupported system ${CMAKE_SYSTEM_NAME}/${CMAKE_C_COMPILER_ID}") endif() |