diff options
-rw-r--r-- | CMakeLists.txt | 3 | ||||
-rw-r--r-- | runtime/doc/autocmd.txt | 15 | ||||
-rw-r--r-- | src/nvim/api/private/helpers.c | 17 | ||||
-rw-r--r-- | src/nvim/api/vim.c | 112 | ||||
-rw-r--r-- | src/nvim/auevents.lua | 2 | ||||
-rw-r--r-- | src/nvim/channel.c | 148 | ||||
-rw-r--r-- | src/nvim/msgpack_rpc/channel.c | 19 | ||||
-rw-r--r-- | src/nvim/msgpack_rpc/channel_defs.h | 1 | ||||
-rw-r--r-- | src/nvim/os/pty_process_unix.c | 5 | ||||
-rw-r--r-- | src/nvim/os/pty_process_win.c | 5 | ||||
-rw-r--r-- | src/nvim/terminal.c | 5 | ||||
-rw-r--r-- | test/functional/api/vim_spec.lua | 109 |
12 files changed, 395 insertions, 46 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index f5a2987f01..4cafdef73f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -290,13 +290,14 @@ option(LOG_LIST_ACTIONS "Add list actions logging" OFF) add_definitions(-DINCLUDE_GENERATED_DECLARATIONS) -if(CMAKE_COMPILER_IS_GNUCXX AND CMAKE_SYSTEM_NAME STREQUAL "Linux") +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--no-undefined") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined") set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--no-undefined") # For O_CLOEXEC, O_DIRECTORY, and O_NOFOLLOW flags on older systems # (pre POSIX.1-2008: glibc 2.11 and earlier). #4042 + # For ptsname(). #6743 add_definitions(-D_GNU_SOURCE) endif() diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt index 0b7cd5b2cd..df36969fe3 100644 --- a/runtime/doc/autocmd.txt +++ b/runtime/doc/autocmd.txt @@ -261,6 +261,8 @@ Name triggered by ~ |SwapExists| detected an existing swap file |TermOpen| when a terminal job starts |TermClose| when a terminal job ends +|ChanOpen| after a channel opened +|ChanInfo| after a channel has its state changed Options |FileType| when the 'filetype' option has been set @@ -487,6 +489,19 @@ BufWriteCmd Before writing the whole buffer to a file. *BufWritePost* BufWritePost After writing the whole buffer to a file (should undo the commands for BufWritePre). + *ChanInfo* +ChanInfo State of channel changed, for instance the + client of a RPC channel described itself. + Sets these |v:event| keys: + info + See |nvim_get_chan_info| for the format of the + info Dictionary. + *ChanOpen* +ChanOpen Just after a channel was opened. + Sets these |v:event| keys: + info + See |nvim_get_chan_info| for the format of the + info Dictionary. *CmdUndefined* CmdUndefined When a user command is used but it isn't defined. Useful for defining a command only diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index 17ee3ed711..692a0b51fd 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -1030,6 +1030,16 @@ Array copy_array(Array array) return rv; } +Dictionary copy_dictionary(Dictionary dict) +{ + Dictionary rv = ARRAY_DICT_INIT; + for (size_t i = 0; i < dict.size; i++) { + KeyValuePair item = dict.items[i]; + PUT(rv, item.key.data, copy_object(item.value)); + } + return rv; +} + /// Creates a deep clone of an object Object copy_object(Object obj) { @@ -1047,12 +1057,7 @@ Object copy_object(Object obj) return ARRAY_OBJ(copy_array(obj.data.array)); case kObjectTypeDictionary: { - Dictionary rv = ARRAY_DICT_INIT; - for (size_t i = 0; i < obj.data.dictionary.size; i++) { - KeyValuePair item = obj.data.dictionary.items[i]; - PUT(rv, item.key.data, copy_object(item.value)); - } - return DICTIONARY_OBJ(rv); + return DICTIONARY_OBJ(copy_dictionary(obj.data.dictionary)); } default: abort(); diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 7a951d4e67..f587948cf0 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -990,6 +990,118 @@ Array nvim_get_api_info(uint64_t channel_id) return rv; } +/// Identify the client for nvim. Can be called more than once, but subsequent +/// calls will remove earlier info, which should be resent if it is still +/// valid. (This could happen if a library first identifies the channel, and a +/// plugin using that library later overrides that info) +/// +/// @param name short name for the connected client +/// @param version Dictionary describing the version, with the following +/// possible keys (all optional) +/// - "major" major version (defaults to 0 if not set, for no release yet) +/// - "minor" minor version +/// - "patch" patch number +/// - "prerelease" string describing a prerelease, like "dev" or "beta1" +/// - "commit" hash or similar identifier of commit +/// @param type Must be one of the following values. A client library should +/// use "remote" if the library user hasn't specified other value. +/// - "remote" remote client that connected to nvim. +/// - "ui" gui frontend +/// - "embedder" application using nvim as a component, for instance +/// IDE/editor implementing a vim mode. +/// - "host" plugin host, typically started by nvim +/// - "plugin" single plugin, started by nvim +/// @param methods Builtin methods in the client. For a host, this does not +/// include plugin methods which will be discovered later. +/// The key should be the method name, the values are dicts with +/// the following (optional) keys: +/// - "async" if true, send as a notification. If false or unspecified, +/// use a blocking request +/// - "nargs" Number of arguments. Could be a single integer or an array +/// two integers, minimum and maximum inclusive. +/// Further keys might be added in later versions of nvim and unknown keys +/// are thus ignored. Clients must only use keys defined in this or later +/// versions of nvim! +/// +/// @param attributes Informal attributes describing the client. Clients might +/// define their own keys, but the following are suggested: +/// - "website" Website of client (for instance github repository) +/// - "license" Informal descripton of the license, such as "Apache 2", +/// "GPLv3" or "MIT" +/// - "logo" URI or path to image, preferably small logo or icon. +/// .png or .svg format is preferred. +/// +void nvim_set_client_info(uint64_t channel_id, String name, + Dictionary version, String type, + Dictionary methods, Dictionary attributes, + Error *err) + FUNC_API_SINCE(4) FUNC_API_REMOTE_ONLY +{ + Dictionary info = ARRAY_DICT_INIT; + PUT(info, "name", copy_object(STRING_OBJ(name))); + + version = copy_dictionary(version); + bool has_major = false; + for (size_t i = 0; i < version.size; i++) { + if (strequal(version.items[i].key.data, "major")) { + has_major = true; + break; + } + } + if (!has_major) { + PUT(version, "major", INTEGER_OBJ(0)); + } + PUT(info, "version", DICTIONARY_OBJ(version)); + + PUT(info, "type", copy_object(STRING_OBJ(type))); + PUT(info, "methods", DICTIONARY_OBJ(copy_dictionary(methods))); + PUT(info, "attributes", DICTIONARY_OBJ(copy_dictionary(attributes))); + + rpc_set_client_info(channel_id, info); +} + +/// Get information about a channel. +/// +/// @returns a Dictionary, describing a channel with the +/// following keys: +/// - "stream" the stream underlying the channel +/// - "stdio" stdin and stdout of this Nvim instance +/// - "stderr" stderr of this Nvim instance +/// - "socket" TCP/IP socket or named pipe +/// - "job" job with communication over its stdio +/// - "mode" how data received on the channel is interpreted +/// - "bytes" send and recieve raw bytes +/// - "terminal" a |terminal| instance interprets ASCII sequences +/// - "rpc" |RPC| communication on the channel is active +/// - "pty" Name of pseudoterminal, if one is used (optional). +/// On a POSIX system, this will be a device path like +/// /dev/pts/1. Even if the name is unknown, the key will +/// still be present to indicate a pty is used. This is +/// currently the case when using winpty on windows. +/// - "buffer" buffer with connected |terminal| instance (optional) +/// - "client" information about the client on the other end of the +/// RPC channel, if it has added it using +/// |nvim_set_client_info|. (optional) +/// +Dictionary nvim_get_chan_info(Integer chan, Error *err) + FUNC_API_SINCE(4) +{ + if (chan < 0) { + return (Dictionary)ARRAY_DICT_INIT; + } + return channel_info((uint64_t)chan); +} + +/// Get information about all open channels. +/// +/// @returns Array of Dictionaries, each describing a channel with +/// the format specified at |nvim_get_chan_info|. +Array nvim_list_chans(void) + FUNC_API_SINCE(4) +{ + return channel_all_info(); +} + /// Calls many API methods atomically. /// /// This has two main usages: diff --git a/src/nvim/auevents.lua b/src/nvim/auevents.lua index f07a92ab87..d0a3f38c6b 100644 --- a/src/nvim/auevents.lua +++ b/src/nvim/auevents.lua @@ -19,6 +19,8 @@ return { 'BufWriteCmd', -- write buffer using command 'BufWritePost', -- after writing a buffer 'BufWritePre', -- before writing a buffer + 'ChanOpen', -- channel was opened + 'ChanInfo', -- info was received about channel 'CmdLineEnter', -- after entering cmdline mode 'CmdLineLeave', -- before leaving cmdline mode 'CmdUndefined', -- command undefined diff --git a/src/nvim/channel.c b/src/nvim/channel.c index b37fa10b12..64d743891b 100644 --- a/src/nvim/channel.c +++ b/src/nvim/channel.c @@ -1,11 +1,13 @@ // 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 +#include "nvim/api/private/helpers.h" #include "nvim/api/ui.h" #include "nvim/channel.h" #include "nvim/eval.h" #include "nvim/eval/encode.h" #include "nvim/event/socket.h" +#include "nvim/fileio.h" #include "nvim/msgpack_rpc/channel.h" #include "nvim/msgpack_rpc/server.h" #include "nvim/os/shell.h" @@ -145,6 +147,9 @@ bool channel_close(uint64_t id, ChannelPart part, const char **error) return false; } break; + + default: + abort(); } return true; @@ -180,47 +185,11 @@ static Channel *channel_alloc(ChannelStreamType type) return chan; } -/// Not implemented, only logging for now void channel_create_event(Channel *chan, const char *ext_source) { #if MIN_LOG_LEVEL <= INFO_LOG_LEVEL - const char *stream_desc; - const char *mode_desc; const char *source; - switch (chan->streamtype) { - case kChannelStreamProc: - if (chan->stream.proc.type == kProcessTypePty) { - stream_desc = "pty job"; - } else { - stream_desc = "job"; - } - break; - - case kChannelStreamStdio: - stream_desc = "stdio"; - break; - - case kChannelStreamSocket: - stream_desc = "socket"; - break; - - case kChannelStreamInternal: - stream_desc = "socket (internal)"; - break; - - default: - stream_desc = "?"; - } - - if (chan->is_rpc) { - mode_desc = ", rpc"; - } else if (chan->term) { - mode_desc = ", terminal"; - } else { - mode_desc = ""; - } - if (ext_source) { // TODO(bfredl): in a future improved traceback solution, // external events should be included. @@ -230,12 +199,21 @@ void channel_create_event(Channel *chan, const char *ext_source) source = (const char *)IObuff; } - ILOG("new channel %" PRIu64 " (%s%s): %s", chan->id, stream_desc, - mode_desc, source); + Dictionary info = channel_info(chan->id); + typval_T tv = TV_INITIAL_VALUE; + // TODO(bfredl): do the conversion in one step. Also would be nice + // to pretty print top level dict in defined order + (void)object_to_vim(DICTIONARY_OBJ(info), &tv, NULL); + char *str = encode_tv2json(&tv, NULL); + ILOG("new channel %" PRIu64 " (%s) : %s", chan->id, source, str); + xfree(str); + api_free_dictionary(info); + #else - (void)chan; (void)ext_source; #endif + + channel_info_changed(chan, true); } void channel_incref(Channel *chan) @@ -755,3 +733,95 @@ static void term_close(void *data) multiqueue_put(chan->events, term_delayed_free, 1, data); } +void channel_info_changed(Channel *chan, bool new) +{ + event_T event = new ? EVENT_CHANOPEN : EVENT_CHANINFO; + if (has_event(event)) { + channel_incref(chan); + multiqueue_put(main_loop.events, set_info_event, + 2, chan, event); + } +} + +static void set_info_event(void **argv) +{ + Channel *chan = argv[0]; + event_T event = (event_T)(ptrdiff_t)argv[1]; + + dict_T *dict = get_vim_var_dict(VV_EVENT); + Dictionary info = channel_info(chan->id); + typval_T retval; + (void)object_to_vim(DICTIONARY_OBJ(info), &retval, NULL); + tv_dict_add_dict(dict, S_LEN("info"), retval.vval.v_dict); + + apply_autocmds(event, NULL, NULL, false, curbuf); + + tv_dict_clear(dict); + api_free_dictionary(info); + channel_decref(chan); +} + +Dictionary channel_info(uint64_t id) +{ + Channel *chan = find_channel(id); + if (!chan) { + return (Dictionary)ARRAY_DICT_INIT; + } + + Dictionary info = ARRAY_DICT_INIT; + PUT(info, "id", INTEGER_OBJ((Integer)chan->id)); + + const char *stream_desc, *mode_desc; + switch (chan->streamtype) { + case kChannelStreamProc: + stream_desc = "job"; + if (chan->stream.proc.type == kProcessTypePty) { + const char *name = pty_process_tty_name(&chan->stream.pty); + PUT(info, "pty", STRING_OBJ(cstr_to_string(name))); + } + break; + + case kChannelStreamStdio: + stream_desc = "stdio"; + break; + + case kChannelStreamStderr: + stream_desc = "stderr"; + break; + + case kChannelStreamInternal: + PUT(info, "internal", BOOLEAN_OBJ(true)); + // FALLTHROUGH + + case kChannelStreamSocket: + stream_desc = "socket"; + break; + + default: + abort(); + } + PUT(info, "stream", STRING_OBJ(cstr_to_string(stream_desc))); + + if (chan->is_rpc) { + mode_desc = "rpc"; + PUT(info, "client", DICTIONARY_OBJ(rpc_client_info(chan))); + } else if (chan->term) { + mode_desc = "terminal"; + PUT(info, "buffer", BUFFER_OBJ(terminal_buf(chan->term))); + } else { + mode_desc = "bytes"; + } + PUT(info, "mode", STRING_OBJ(cstr_to_string(mode_desc))); + + return info; +} + +Array channel_all_info(void) +{ + Channel *channel; + Array ret = ARRAY_DICT_INIT; + map_foreach_value(channels, channel, { + ADD(ret, DICTIONARY_OBJ(channel_info(channel->id))); + }); + return ret; +} diff --git a/src/nvim/msgpack_rpc/channel.c b/src/nvim/msgpack_rpc/channel.c index 32781cf4d9..26b84b7cc7 100644 --- a/src/nvim/msgpack_rpc/channel.c +++ b/src/nvim/msgpack_rpc/channel.c @@ -61,6 +61,7 @@ void rpc_start(Channel *channel) rpc->unpacker = msgpack_unpacker_new(MSGPACK_UNPACKER_INIT_BUFFER_SIZE); rpc->subscribed_events = pmap_new(cstr_t)(); rpc->next_request_id = 1; + rpc->info = (Dictionary)ARRAY_DICT_INIT; kv_init(rpc->call_stack); if (channel->streamtype != kChannelStreamInternal) { @@ -553,6 +554,7 @@ void rpc_free(Channel *channel) pmap_free(cstr_t)(channel->rpc.subscribed_events); kv_destroy(channel->rpc.call_stack); + api_free_dictionary(channel->rpc.info); } static bool is_rpc_response(msgpack_object *obj) @@ -642,6 +644,23 @@ static WBuffer *serialize_response(uint64_t channel_id, return rv; } +void rpc_set_client_info(uint64_t id, Dictionary info) +{ + Channel *chan = find_rpc_channel(id); + if (!chan) { + abort(); + } + + api_free_dictionary(chan->rpc.info); + chan->rpc.info = info; + channel_info_changed(chan, false); +} + +Dictionary rpc_client_info(Channel *chan) +{ + return copy_dictionary(chan->rpc.info); +} + #if MIN_LOG_LEVEL <= DEBUG_LOG_LEVEL #define REQ "[request] " #define RES "[response] " diff --git a/src/nvim/msgpack_rpc/channel_defs.h b/src/nvim/msgpack_rpc/channel_defs.h index 6d8362e8b7..bfa7f7b87c 100644 --- a/src/nvim/msgpack_rpc/channel_defs.h +++ b/src/nvim/msgpack_rpc/channel_defs.h @@ -31,6 +31,7 @@ typedef struct { msgpack_unpacker *unpacker; uint64_t next_request_id; kvec_t(ChannelCallFrame *) call_stack; + Dictionary info; } RpcState; #endif // NVIM_MSGPACK_RPC_CHANNEL_DEFS_H diff --git a/src/nvim/os/pty_process_unix.c b/src/nvim/os/pty_process_unix.c index dfe2cfbb8d..71bb2d8a5e 100644 --- a/src/nvim/os/pty_process_unix.c +++ b/src/nvim/os/pty_process_unix.c @@ -115,6 +115,11 @@ error: return status; } +const char *pty_process_tty_name(PtyProcess *ptyproc) +{ + return ptsname(ptyproc->tty_fd); +} + void pty_process_resize(PtyProcess *ptyproc, uint16_t width, uint16_t height) FUNC_ATTR_NONNULL_ALL { diff --git a/src/nvim/os/pty_process_win.c b/src/nvim/os/pty_process_win.c index a6774a6275..c5f8efadff 100644 --- a/src/nvim/os/pty_process_win.c +++ b/src/nvim/os/pty_process_win.c @@ -183,6 +183,11 @@ cleanup: return status; } +const char *pty_process_tty_name(PtyProcess *ptyproc) +{ + return "?"; +} + void pty_process_resize(PtyProcess *ptyproc, uint16_t width, uint16_t height) FUNC_ATTR_NONNULL_ALL diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index 31875fac31..39cb2b6372 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -619,6 +619,11 @@ void terminal_get_line_attributes(Terminal *term, win_T *wp, int linenr, } } +Buffer terminal_buf(const Terminal *term) +{ + return term->buf_handle; +} + // }}} // libvterm callbacks {{{ diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 71fa6632c7..1e910b6aa7 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -766,6 +766,115 @@ describe('api', function() end) end) + describe('nvim_list_chans and nvim_get_chan_info', function() + before_each(function() + command('autocmd ChanOpen * let g:opened_event = copy(v:event)') + command('autocmd ChanInfo * let g:info_event = copy(v:event)') + end) + local testinfo = { + stream = 'stdio', + id = 1, + mode = 'rpc', + client = {}, + } + local stderr = { + stream = 'stderr', + id = 2, + mode = 'bytes', + } + + it('returns {} for invalid channel', function() + eq({}, meths.get_chan_info(0)) + eq({}, meths.get_chan_info(-1)) + -- more preallocated numbers might be added, try something high + eq({}, meths.get_chan_info(10)) + end) + + it('works for stdio channel', function() + eq({[1]=testinfo,[2]=stderr}, meths.list_chans()) + eq(testinfo, meths.get_chan_info(1)) + eq(stderr, meths.get_chan_info(2)) + + meths.set_client_info("functionaltests", + {major=0, minor=3, patch=17}, + 'ui', + {do_stuff={n_args={2,3}}}, + {license= 'Apache2'}) + local info = { + stream = 'stdio', + id = 1, + mode = 'rpc', + client = { + name='functionaltests', + version={major=0, minor=3, patch=17}, + type='ui', + methods={do_stuff={n_args={2,3}}}, + attributes={license='Apache2'}, + }, + } + eq({info=info}, meths.get_var("info_event")) + eq({[1]=info, [2]=stderr}, meths.list_chans()) + eq(info, meths.get_chan_info(1)) + end) + + it('works for job channel', function() + if iswin() and os.getenv('APPVEYOR') ~= nil then + pending("jobstart(['cat']) unreliable on appveyor") + return + end + eq(3, eval("jobstart(['cat'], {'rpc': v:true})")) + local info = { + stream='job', + id=3, + mode='rpc', + client={}, + } + eq({info=info}, meths.get_var("opened_event")) + eq({[1]=testinfo,[2]=stderr,[3]=info}, meths.list_chans()) + eq(info, meths.get_chan_info(3)) + eval('rpcrequest(3, "nvim_set_client_info", "cat", {}, "remote",'.. + '{"nvim_command":{"n_args":1}},'.. -- and so on + '{"description":"The Amazing Cat"})') + info = { + stream='job', + id=3, + mode='rpc', + client = { + name='cat', + version={major=0}, + type='remote', + methods={nvim_command={n_args=1}}, + attributes={description="The Amazing Cat"}, + }, + } + eq({info=info}, meths.get_var("info_event")) + eq({[1]=testinfo,[2]=stderr,[3]=info}, meths.list_chans()) + end) + + it('works for :terminal channel', function() + command(":terminal") + eq({id=1}, meths.get_current_buf()) + eq(3, meths.buf_get_option(1, "channel")) + + local info = { + stream='job', + id=3, + mode='terminal', + buffer = 1, + pty='?', + } + local event = meths.get_var("opened_event") + if not iswin() then + info.pty = event.info.pty + neq(nil, string.match(info.pty, "^/dev/")) + end + eq({info=info}, event) + info.buffer = {id=1} + eq({[1]=testinfo,[2]=stderr,[3]=info}, meths.list_chans()) + eq(info, meths.get_chan_info(3)) + end) + end) + describe('nvim_call_atomic', function() it('works', function() meths.buf_set_lines(0, 0, -1, true, {'first'}) |