diff options
author | Josh Rahm <rahm@google.com> | 2022-07-18 19:37:18 +0000 |
---|---|---|
committer | Josh Rahm <rahm@google.com> | 2022-07-18 19:37:18 +0000 |
commit | 308e1940dcd64aa6c344c403d4f9e0dda58d9c5c (patch) | |
tree | 35fe43e01755e0f312650667004487a44d6b7941 /src/nvim/api | |
parent | 96a00c7c588b2f38a2424aeeb4ea3581d370bf2d (diff) | |
parent | e8c94697bcbe23a5c7b07c292b90a6b70aadfa87 (diff) | |
download | rneovim-308e1940dcd64aa6c344c403d4f9e0dda58d9c5c.tar.gz rneovim-308e1940dcd64aa6c344c403d4f9e0dda58d9c5c.tar.bz2 rneovim-308e1940dcd64aa6c344c403d4f9e0dda58d9c5c.zip |
Merge remote-tracking branch 'upstream/master' into rahm
Diffstat (limited to 'src/nvim/api')
-rw-r--r-- | src/nvim/api/autocmd.c | 1016 | ||||
-rw-r--r-- | src/nvim/api/autocmd.h | 11 | ||||
-rw-r--r-- | src/nvim/api/buffer.c | 407 | ||||
-rw-r--r-- | src/nvim/api/command.c | 1189 | ||||
-rw-r--r-- | src/nvim/api/command.h | 11 | ||||
-rw-r--r-- | src/nvim/api/deprecated.c | 27 | ||||
-rw-r--r-- | src/nvim/api/extmark.c | 460 | ||||
-rw-r--r-- | src/nvim/api/extmark.h | 1 | ||||
-rw-r--r-- | src/nvim/api/keysets.lua | 130 | ||||
-rw-r--r-- | src/nvim/api/options.c | 554 | ||||
-rw-r--r-- | src/nvim/api/options.h | 9 | ||||
-rw-r--r-- | src/nvim/api/private/converter.c | 24 | ||||
-rw-r--r-- | src/nvim/api/private/defs.h | 15 | ||||
-rw-r--r-- | src/nvim/api/private/dispatch.c | 49 | ||||
-rw-r--r-- | src/nvim/api/private/dispatch.h | 1 | ||||
-rw-r--r-- | src/nvim/api/private/helpers.c | 833 | ||||
-rw-r--r-- | src/nvim/api/private/helpers.h | 32 | ||||
-rw-r--r-- | src/nvim/api/tabpage.c | 7 | ||||
-rw-r--r-- | src/nvim/api/ui.c | 556 | ||||
-rw-r--r-- | src/nvim/api/ui.h | 1 | ||||
-rw-r--r-- | src/nvim/api/ui_events.in.h | 12 | ||||
-rw-r--r-- | src/nvim/api/vim.c | 586 | ||||
-rw-r--r-- | src/nvim/api/vimscript.c | 42 | ||||
-rw-r--r-- | src/nvim/api/win_config.c | 37 | ||||
-rw-r--r-- | src/nvim/api/window.c | 55 |
25 files changed, 4400 insertions, 1665 deletions
diff --git a/src/nvim/api/autocmd.c b/src/nvim/api/autocmd.c new file mode 100644 index 0000000000..bf6402f938 --- /dev/null +++ b/src/nvim/api/autocmd.c @@ -0,0 +1,1016 @@ +// 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 <stdbool.h> +#include <stdio.h> + +#include "lauxlib.h" +#include "nvim/api/autocmd.h" +#include "nvim/api/private/defs.h" +#include "nvim/api/private/helpers.h" +#include "nvim/ascii.h" +#include "nvim/buffer.h" +#include "nvim/eval/typval.h" +#include "nvim/fileio.h" +#include "nvim/lua/executor.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "api/autocmd.c.generated.h" +#endif + +#define AUCMD_MAX_PATTERNS 256 + +// Copy string or array of strings into an empty array. +// Get the event number, unless it is an error. Then goto `goto_name`. +#define GET_ONE_EVENT(event_nr, event_str, goto_name) \ + char *__next_ev; \ + event_T event_nr = \ + event_name2nr(event_str.data.string.data, &__next_ev); \ + if (event_nr >= NUM_EVENTS) { \ + api_set_error(err, kErrorTypeValidation, "unexpected event"); \ + goto goto_name; \ + } + +// ID for associating autocmds created via nvim_create_autocmd +// Used to delete autocmds from nvim_del_autocmd +static int64_t next_autocmd_id = 1; + +/// Get all autocommands that match the corresponding {opts}. +/// +/// These examples will get autocommands matching ALL the given criteria: +/// <pre> +/// -- Matches all criteria +/// autocommands = vim.api.nvim_get_autocmds({ +/// group = "MyGroup", +/// event = {"BufEnter", "BufWinEnter"}, +/// pattern = {"*.c", "*.h"} +/// }) +/// +/// -- All commands from one group +/// autocommands = vim.api.nvim_get_autocmds({ +/// group = "MyGroup", +/// }) +/// </pre> +/// +/// NOTE: When multiple patterns or events are provided, it will find all the autocommands that +/// match any combination of them. +/// +/// @param opts Dictionary with at least one of the following: +/// - group (string|integer): the autocommand group name or id to match against. +/// - event (string|array): event or events to match against |autocmd-events|. +/// - pattern (string|array): pattern or patterns to match against |autocmd-pattern|. +/// @return Array of autocommands matching the criteria, with each item +/// containing the following fields: +/// - id (number): the autocommand id (only when defined with the API). +/// - group (integer): the autocommand group id. +/// - group_name (string): the autocommand group name. +/// - desc (string): the autocommand description. +/// - event (string): the autocommand event. +/// - command (string): the autocommand command. Note: this will be empty if a callback is set. +/// - callback (function|string|nil): Lua function or name of a Vim script function +/// which is executed when this autocommand is triggered. +/// - once (boolean): whether the autocommand is only run once. +/// - pattern (string): the autocommand pattern. +/// If the autocommand is buffer local |autocmd-buffer-local|: +/// - buflocal (boolean): true if the autocommand is buffer local. +/// - buffer (number): the buffer number. +Array nvim_get_autocmds(Dict(get_autocmds) *opts, Error *err) + FUNC_API_SINCE(9) +{ + // TODO(tjdevries): Would be cool to add nvim_get_autocmds({ id = ... }) + + Array autocmd_list = ARRAY_DICT_INIT; + char *pattern_filters[AUCMD_MAX_PATTERNS]; + char pattern_buflocal[BUFLOCAL_PAT_LEN]; + + Array buffers = ARRAY_DICT_INIT; + + bool event_set[NUM_EVENTS] = { false }; + bool check_event = false; + + int group = 0; + + switch (opts->group.type) { + case kObjectTypeNil: + break; + case kObjectTypeString: + group = augroup_find(opts->group.data.string.data); + if (group < 0) { + api_set_error(err, kErrorTypeValidation, "invalid augroup passed."); + goto cleanup; + } + break; + case kObjectTypeInteger: + group = (int)opts->group.data.integer; + char *name = augroup_name(group); + if (!augroup_exists(name)) { + api_set_error(err, kErrorTypeValidation, "invalid augroup passed."); + goto cleanup; + } + break; + default: + api_set_error(err, kErrorTypeValidation, "group must be a string or an integer."); + goto cleanup; + } + + if (opts->event.type != kObjectTypeNil) { + check_event = true; + + Object v = opts->event; + if (v.type == kObjectTypeString) { + GET_ONE_EVENT(event_nr, v, cleanup); + event_set[event_nr] = true; + } else if (v.type == kObjectTypeArray) { + FOREACH_ITEM(v.data.array, event_v, { + if (event_v.type != kObjectTypeString) { + api_set_error(err, + kErrorTypeValidation, + "Every event must be a string in 'event'"); + goto cleanup; + } + + GET_ONE_EVENT(event_nr, event_v, cleanup); + event_set[event_nr] = true; + }) + } else { + api_set_error(err, + kErrorTypeValidation, + "Not a valid 'event' value. Must be a string or an array"); + goto cleanup; + } + } + + if (opts->pattern.type != kObjectTypeNil && opts->buffer.type != kObjectTypeNil) { + api_set_error(err, kErrorTypeValidation, + "Cannot use both 'pattern' and 'buffer'"); + goto cleanup; + } + + int pattern_filter_count = 0; + if (opts->pattern.type != kObjectTypeNil) { + Object v = opts->pattern; + if (v.type == kObjectTypeString) { + pattern_filters[pattern_filter_count] = v.data.string.data; + pattern_filter_count += 1; + } else if (v.type == kObjectTypeArray) { + if (v.data.array.size > AUCMD_MAX_PATTERNS) { + api_set_error(err, kErrorTypeValidation, + "Too many patterns. Please limit yourself to %d or fewer", + AUCMD_MAX_PATTERNS); + goto cleanup; + } + + FOREACH_ITEM(v.data.array, item, { + if (item.type != kObjectTypeString) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'pattern': must be a string"); + goto cleanup; + } + + pattern_filters[pattern_filter_count] = item.data.string.data; + pattern_filter_count += 1; + }); + } else { + api_set_error(err, kErrorTypeValidation, + "Not a valid 'pattern' value. Must be a string or an array"); + goto cleanup; + } + } + + if (opts->buffer.type == kObjectTypeInteger || opts->buffer.type == kObjectTypeBuffer) { + buf_T *buf = find_buffer_by_handle((Buffer)opts->buffer.data.integer, err); + if (ERROR_SET(err)) { + goto cleanup; + } + + snprintf((char *)pattern_buflocal, BUFLOCAL_PAT_LEN, "<buffer=%d>", (int)buf->handle); + ADD(buffers, CSTR_TO_OBJ((char *)pattern_buflocal)); + } else if (opts->buffer.type == kObjectTypeArray) { + if (opts->buffer.data.array.size > AUCMD_MAX_PATTERNS) { + api_set_error(err, + kErrorTypeValidation, + "Too many buffers. Please limit yourself to %d or fewer", AUCMD_MAX_PATTERNS); + goto cleanup; + } + + FOREACH_ITEM(opts->buffer.data.array, bufnr, { + if (bufnr.type != kObjectTypeInteger && bufnr.type != kObjectTypeBuffer) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'buffer': must be an integer"); + goto cleanup; + } + + buf_T *buf = find_buffer_by_handle((Buffer)bufnr.data.integer, err); + if (ERROR_SET(err)) { + goto cleanup; + } + + snprintf((char *)pattern_buflocal, BUFLOCAL_PAT_LEN, "<buffer=%d>", (int)buf->handle); + ADD(buffers, CSTR_TO_OBJ((char *)pattern_buflocal)); + }); + } else if (opts->buffer.type != kObjectTypeNil) { + api_set_error(err, kErrorTypeValidation, + "Invalid value for 'buffer': must be an integer or array of integers"); + goto cleanup; + } + + FOREACH_ITEM(buffers, bufnr, { + pattern_filters[pattern_filter_count] = bufnr.data.string.data; + pattern_filter_count += 1; + }); + + FOR_ALL_AUEVENTS(event) { + if (check_event && !event_set[event]) { + continue; + } + + for (AutoPat *ap = au_get_autopat_for_event(event); ap != NULL; ap = ap->next) { + if (ap->cmds == NULL) { + continue; + } + + // Skip autocmds from invalid groups if passed. + if (group != 0 && ap->group != group) { + continue; + } + + // Skip 'pattern' from invalid patterns if passed. + if (pattern_filter_count > 0) { + bool passed = false; + for (int i = 0; i < pattern_filter_count; i++) { + assert(i < AUCMD_MAX_PATTERNS); + assert(pattern_filters[i]); + + char *pat = pattern_filters[i]; + int patlen = (int)STRLEN(pat); + + if (aupat_is_buflocal(pat, patlen)) { + aupat_normalize_buflocal_pat(pattern_buflocal, + pat, + patlen, + aupat_get_buflocal_nr(pat, patlen)); + + pat = pattern_buflocal; + } + + if (strequal(ap->pat, pat)) { + passed = true; + break; + } + } + + if (!passed) { + continue; + } + } + + for (AutoCmd *ac = ap->cmds; ac != NULL; ac = ac->next) { + if (aucmd_exec_is_deleted(ac->exec)) { + continue; + } + + Dictionary autocmd_info = ARRAY_DICT_INIT; + + if (ap->group != AUGROUP_DEFAULT) { + PUT(autocmd_info, "group", INTEGER_OBJ(ap->group)); + PUT(autocmd_info, "group_name", CSTR_TO_OBJ(augroup_name(ap->group))); + } + + if (ac->id > 0) { + PUT(autocmd_info, "id", INTEGER_OBJ(ac->id)); + } + + if (ac->desc != NULL) { + PUT(autocmd_info, "desc", CSTR_TO_OBJ(ac->desc)); + } + + if (ac->exec.type == CALLABLE_CB) { + PUT(autocmd_info, "command", STRING_OBJ(STRING_INIT)); + + Callback *cb = &ac->exec.callable.cb; + switch (cb->type) { + case kCallbackLua: + if (nlua_ref_is_function(cb->data.luaref)) { + PUT(autocmd_info, "callback", LUAREF_OBJ(api_new_luaref(cb->data.luaref))); + } + break; + case kCallbackFuncref: + case kCallbackPartial: + PUT(autocmd_info, "callback", STRING_OBJ(cstr_as_string(callback_to_string(cb)))); + break; + default: + abort(); + } + } else { + PUT(autocmd_info, + "command", + STRING_OBJ(cstr_as_string(xstrdup(ac->exec.callable.cmd)))); + } + + PUT(autocmd_info, + "pattern", + STRING_OBJ(cstr_to_string((char *)ap->pat))); + + PUT(autocmd_info, + "event", + STRING_OBJ(cstr_to_string((char *)event_nr2name(event)))); + + PUT(autocmd_info, "once", BOOLEAN_OBJ(ac->once)); + + if (ap->buflocal_nr) { + PUT(autocmd_info, "buflocal", BOOLEAN_OBJ(true)); + PUT(autocmd_info, "buffer", INTEGER_OBJ(ap->buflocal_nr)); + } else { + PUT(autocmd_info, "buflocal", BOOLEAN_OBJ(false)); + } + + // TODO(sctx): It would be good to unify script_ctx to actually work with lua + // right now it's just super weird, and never really gives you the info that + // you would expect from this. + // + // I think we should be able to get the line number, filename, etc. from lua + // when we're executing something, and it should be easy to then save that + // info here. + // + // I think it's a big loss not getting line numbers of where options, autocmds, + // etc. are set (just getting "Sourced (lua)" or something is not that helpful. + // + // Once we do that, we can put these into the autocmd_info, but I don't think it's + // useful to do that at this time. + // + // PUT(autocmd_info, "sid", INTEGER_OBJ(ac->script_ctx.sc_sid)); + // PUT(autocmd_info, "lnum", INTEGER_OBJ(ac->script_ctx.sc_lnum)); + + ADD(autocmd_list, DICTIONARY_OBJ(autocmd_info)); + } + } + } + +cleanup: + api_free_array(buffers); + return autocmd_list; +} + +/// Create an |autocommand| +/// +/// The API allows for two (mutually exclusive) types of actions to be executed when the autocommand +/// triggers: a callback function (Lua or Vimscript), or a command (like regular autocommands). +/// +/// Example using callback: +/// <pre> +/// -- Lua function +/// local myluafun = function() print("This buffer enters") end +/// +/// -- Vimscript function name (as a string) +/// local myvimfun = "g:MyVimFunction" +/// +/// vim.api.nvim_create_autocmd({"BufEnter", "BufWinEnter"}, { +/// pattern = {"*.c", "*.h"}, +/// callback = myluafun, -- Or myvimfun +/// }) +/// </pre> +/// +/// Lua functions receive a table with information about the autocmd event as an argument. To use +/// a function which itself accepts another (optional) parameter, wrap the function +/// in a lambda: +/// +/// <pre> +/// -- Lua function with an optional parameter. +/// -- The autocmd callback would pass a table as argument but this +/// -- function expects number|nil +/// local myluafun = function(bufnr) bufnr = bufnr or vim.api.nvim_get_current_buf() end +/// +/// vim.api.nvim_create_autocmd({"BufEnter", "BufWinEnter"}, { +/// pattern = {"*.c", "*.h"}, +/// callback = function() myluafun() end, +/// }) +/// </pre> +/// +/// Example using command: +/// <pre> +/// vim.api.nvim_create_autocmd({"BufEnter", "BufWinEnter"}, { +/// pattern = {"*.c", "*.h"}, +/// command = "echo 'Entering a C or C++ file'", +/// }) +/// </pre> +/// +/// Example values for pattern: +/// <pre> +/// pattern = "*.py" +/// pattern = { "*.py", "*.pyi" } +/// </pre> +/// +/// Example values for event: +/// <pre> +/// "BufWritePre" +/// {"CursorHold", "BufWritePre", "BufWritePost"} +/// </pre> +/// +/// @param event (string|array) The event or events to register this autocommand +/// @param opts Dictionary of autocommand options: +/// - group (string|integer) optional: the autocommand group name or +/// id to match against. +/// - pattern (string|array) optional: pattern or patterns to match +/// against |autocmd-pattern|. +/// - buffer (integer) optional: buffer number for buffer local autocommands +/// |autocmd-buflocal|. Cannot be used with {pattern}. +/// - desc (string) optional: description of the autocommand. +/// - callback (function|string) optional: if a string, the name of a Vimscript function +/// to call when this autocommand is triggered. Otherwise, a Lua function which is +/// called when this autocommand is triggered. Cannot be used with {command}. Lua +/// callbacks can return true to delete the autocommand; in addition, they accept a +/// single table argument with the following keys: +/// - id: (number) the autocommand id +/// - event: (string) the name of the event that triggered the autocommand +/// |autocmd-events| +/// - group: (number|nil) the autocommand group id, if it exists +/// - match: (string) the expanded value of |<amatch>| +/// - buf: (number) the expanded value of |<abuf>| +/// - file: (string) the expanded value of |<afile>| +/// - data: (any) arbitrary data passed to |nvim_exec_autocmds()| +/// - command (string) optional: Vim command to execute on event. Cannot be used with +/// {callback} +/// - once (boolean) optional: defaults to false. Run the autocommand +/// only once |autocmd-once|. +/// - nested (boolean) optional: defaults to false. Run nested +/// autocommands |autocmd-nested|. +/// +/// @return Integer id of the created autocommand. +/// @see |autocommand| +/// @see |nvim_del_autocmd()| +Integer nvim_create_autocmd(uint64_t channel_id, Object event, Dict(create_autocmd) *opts, + Error *err) + FUNC_API_SINCE(9) +{ + int64_t autocmd_id = -1; + char *desc = NULL; + + Array patterns = ARRAY_DICT_INIT; + Array event_array = ARRAY_DICT_INIT; + + AucmdExecutable aucmd = AUCMD_EXECUTABLE_INIT; + Callback cb = CALLBACK_NONE; + + if (!unpack_string_or_array(&event_array, &event, "event", true, err)) { + goto cleanup; + } + + if (opts->callback.type != kObjectTypeNil && opts->command.type != kObjectTypeNil) { + api_set_error(err, kErrorTypeValidation, + "cannot pass both: 'callback' and 'command' for the same autocmd"); + goto cleanup; + } else if (opts->callback.type != kObjectTypeNil) { + // TODO(tjdevries): It's possible we could accept callable tables, + // but we don't do that many other places, so for the moment let's + // not do that. + + Object *callback = &opts->callback; + switch (callback->type) { + case kObjectTypeLuaRef: + if (callback->data.luaref == LUA_NOREF) { + api_set_error(err, + kErrorTypeValidation, + "must pass an actual value"); + goto cleanup; + } + + if (!nlua_ref_is_function(callback->data.luaref)) { + api_set_error(err, + kErrorTypeValidation, + "must pass a function for callback"); + goto cleanup; + } + + cb.type = kCallbackLua; + cb.data.luaref = api_new_luaref(callback->data.luaref); + break; + case kObjectTypeString: + cb.type = kCallbackFuncref; + cb.data.funcref = string_to_cstr(callback->data.string); + break; + default: + api_set_error(err, + kErrorTypeException, + "'callback' must be a lua function or name of vim function"); + goto cleanup; + } + + aucmd.type = CALLABLE_CB; + aucmd.callable.cb = cb; + } else if (opts->command.type != kObjectTypeNil) { + Object *command = &opts->command; + if (command->type == kObjectTypeString) { + aucmd.type = CALLABLE_EX; + aucmd.callable.cmd = string_to_cstr(command->data.string); + } else { + api_set_error(err, + kErrorTypeValidation, + "'command' must be a string"); + goto cleanup; + } + } else { + api_set_error(err, kErrorTypeValidation, "must pass one of: 'command', 'callback'"); + goto cleanup; + } + + bool is_once = api_object_to_bool(opts->once, "once", false, err); + bool is_nested = api_object_to_bool(opts->nested, "nested", false, err); + + int au_group = get_augroup_from_object(opts->group, err); + if (au_group == AUGROUP_ERROR) { + goto cleanup; + } + + if (!get_patterns_from_pattern_or_buf(&patterns, opts->pattern, opts->buffer, err)) { + goto cleanup; + } + + if (opts->desc.type != kObjectTypeNil) { + if (opts->desc.type == kObjectTypeString) { + desc = opts->desc.data.string.data; + } else { + api_set_error(err, + kErrorTypeValidation, + "'desc' must be a string"); + goto cleanup; + } + } + + if (patterns.size == 0) { + ADD(patterns, STRING_OBJ(STATIC_CSTR_TO_STRING("*"))); + } + + if (event_array.size == 0) { + api_set_error(err, kErrorTypeValidation, "'event' is a required key"); + goto cleanup; + } + + autocmd_id = next_autocmd_id++; + FOREACH_ITEM(event_array, event_str, { + GET_ONE_EVENT(event_nr, event_str, cleanup); + + int retval; + + FOREACH_ITEM(patterns, pat, { + // See: TODO(sctx) + WITH_SCRIPT_CONTEXT(channel_id, { + retval = autocmd_register(autocmd_id, + event_nr, + pat.data.string.data, + (int)pat.data.string.size, + au_group, + is_once, + is_nested, + desc, + aucmd); + }); + + if (retval == FAIL) { + api_set_error(err, kErrorTypeException, "Failed to set autocmd"); + goto cleanup; + } + }) + }); + +cleanup: + aucmd_exec_free(&aucmd); + api_free_array(event_array); + api_free_array(patterns); + + return autocmd_id; +} + +/// Delete an autocommand by id. +/// +/// NOTE: Only autocommands created via the API have an id. +/// @param id Integer The id returned by nvim_create_autocmd +/// @see |nvim_create_autocmd()| +void nvim_del_autocmd(Integer id, Error *err) + FUNC_API_SINCE(9) +{ + if (id <= 0) { + api_set_error(err, kErrorTypeException, "Invalid autocmd id"); + return; + } + if (!autocmd_delete_id(id)) { + api_set_error(err, kErrorTypeException, "Failed to delete autocmd"); + } +} + +/// Clear all autocommands that match the corresponding {opts}. To delete +/// a particular autocmd, see |nvim_del_autocmd|. +/// @param opts Parameters +/// - event: (string|table) +/// Examples: +/// - event: "pat1" +/// - event: { "pat1" } +/// - event: { "pat1", "pat2", "pat3" } +/// - pattern: (string|table) +/// - pattern or patterns to match exactly. +/// - For example, if you have `*.py` as that pattern for the autocmd, +/// you must pass `*.py` exactly to clear it. `test.py` will not +/// match the pattern. +/// - defaults to clearing all patterns. +/// - NOTE: Cannot be used with {buffer} +/// - buffer: (bufnr) +/// - clear only |autocmd-buflocal| autocommands. +/// - NOTE: Cannot be used with {pattern} +/// - group: (string|int) The augroup name or id. +/// - NOTE: If not passed, will only delete autocmds *not* in any group. +/// +void nvim_clear_autocmds(Dict(clear_autocmds) *opts, Error *err) + FUNC_API_SINCE(9) +{ + // TODO(tjdevries): Future improvements: + // - once: (boolean) - Only clear autocmds with once. See |autocmd-once| + // - nested: (boolean) - Only clear autocmds with nested. See |autocmd-nested| + // - group: Allow passing "*" or true or something like that to force doing all + // autocmds, regardless of their group. + + Array patterns = ARRAY_DICT_INIT; + Array event_array = ARRAY_DICT_INIT; + + if (!unpack_string_or_array(&event_array, &opts->event, "event", false, err)) { + goto cleanup; + } + + if (opts->pattern.type != kObjectTypeNil && opts->buffer.type != kObjectTypeNil) { + api_set_error(err, kErrorTypeValidation, + "Cannot use both 'pattern' and 'buffer'"); + goto cleanup; + } + + int au_group = get_augroup_from_object(opts->group, err); + if (au_group == AUGROUP_ERROR) { + goto cleanup; + } + + if (!get_patterns_from_pattern_or_buf(&patterns, opts->pattern, opts->buffer, err)) { + goto cleanup; + } + + // When we create the autocmds, we want to say that they are all matched, so that's * + // but when we clear them, we want to say that we didn't pass a pattern, so that's NUL + if (patterns.size == 0) { + ADD(patterns, STRING_OBJ(STATIC_CSTR_TO_STRING(""))); + } + + // If we didn't pass any events, that means clear all events. + if (event_array.size == 0) { + FOR_ALL_AUEVENTS(event) { + FOREACH_ITEM(patterns, pat_object, { + char *pat = pat_object.data.string.data; + if (!clear_autocmd(event, (char *)pat, au_group, err)) { + goto cleanup; + } + }); + } + } else { + FOREACH_ITEM(event_array, event_str, { + GET_ONE_EVENT(event_nr, event_str, cleanup); + + FOREACH_ITEM(patterns, pat_object, { + char *pat = pat_object.data.string.data; + if (!clear_autocmd(event_nr, (char *)pat, au_group, err)) { + goto cleanup; + } + }); + }); + } + +cleanup: + api_free_array(event_array); + api_free_array(patterns); +} + +/// Create or get an autocommand group |autocmd-groups|. +/// +/// To get an existing group id, do: +/// <pre> +/// local id = vim.api.nvim_create_augroup("MyGroup", { +/// clear = false +/// }) +/// </pre> +/// +/// @param name String: The name of the group +/// @param opts Dictionary Parameters +/// - clear (bool) optional: defaults to true. Clear existing +/// commands if the group already exists |autocmd-groups|. +/// @return Integer id of the created group. +/// @see |autocmd-groups| +Integer nvim_create_augroup(uint64_t channel_id, String name, Dict(create_augroup) *opts, + Error *err) + FUNC_API_SINCE(9) +{ + char *augroup_name = name.data; + bool clear_autocmds = api_object_to_bool(opts->clear, "clear", true, err); + + int augroup = -1; + WITH_SCRIPT_CONTEXT(channel_id, { + augroup = augroup_add(augroup_name); + if (augroup == AUGROUP_ERROR) { + api_set_error(err, kErrorTypeException, "Failed to set augroup"); + return -1; + } + + if (clear_autocmds) { + FOR_ALL_AUEVENTS(event) { + aupat_del_for_event_and_group(event, augroup); + } + } + }); + + return augroup; +} + +/// Delete an autocommand group by id. +/// +/// To get a group id one can use |nvim_get_autocmds()|. +/// +/// NOTE: behavior differs from |augroup-delete|. When deleting a group, autocommands contained in +/// this group will also be deleted and cleared. This group will no longer exist. +/// @param id Integer The id of the group. +/// @see |nvim_del_augroup_by_name()| +/// @see |nvim_create_augroup()| +void nvim_del_augroup_by_id(Integer id, Error *err) + FUNC_API_SINCE(9) +{ + TRY_WRAP({ + try_start(); + char *name = augroup_name((int)id); + augroup_del(name, false); + try_end(err); + }); +} + +/// Delete an autocommand group by name. +/// +/// NOTE: behavior differs from |augroup-delete|. When deleting a group, autocommands contained in +/// this group will also be deleted and cleared. This group will no longer exist. +/// @param name String The name of the group. +/// @see |autocommand-groups| +void nvim_del_augroup_by_name(String name, Error *err) + FUNC_API_SINCE(9) +{ + TRY_WRAP({ + try_start(); + augroup_del(name.data, false); + try_end(err); + }); +} + +/// Execute all autocommands for {event} that match the corresponding +/// {opts} |autocmd-execute|. +/// @param event (String|Array) The event or events to execute +/// @param opts Dictionary of autocommand options: +/// - group (string|integer) optional: the autocommand group name or +/// id to match against. |autocmd-groups|. +/// - pattern (string|array) optional: defaults to "*" |autocmd-pattern|. Cannot be used +/// with {buffer}. +/// - buffer (integer) optional: buffer number |autocmd-buflocal|. Cannot be used with +/// {pattern}. +/// - modeline (bool) optional: defaults to true. Process the +/// modeline after the autocommands |<nomodeline>|. +/// - data (any): arbitrary data to send to the autocommand callback. See +/// |nvim_create_autocmd()| for details. +/// @see |:doautocmd| +void nvim_exec_autocmds(Object event, Dict(exec_autocmds) *opts, Error *err) + FUNC_API_SINCE(9) +{ + int au_group = AUGROUP_ALL; + bool modeline = true; + + buf_T *buf = curbuf; + + Array patterns = ARRAY_DICT_INIT; + Array event_array = ARRAY_DICT_INIT; + + Object *data = NULL; + + if (!unpack_string_or_array(&event_array, &event, "event", true, err)) { + goto cleanup; + } + + switch (opts->group.type) { + case kObjectTypeNil: + break; + case kObjectTypeString: + au_group = augroup_find(opts->group.data.string.data); + if (au_group == AUGROUP_ERROR) { + api_set_error(err, + kErrorTypeValidation, + "invalid augroup: %s", opts->group.data.string.data); + goto cleanup; + } + break; + case kObjectTypeInteger: + au_group = (int)opts->group.data.integer; + char *name = augroup_name(au_group); + if (!augroup_exists(name)) { + api_set_error(err, kErrorTypeValidation, "invalid augroup: %d", au_group); + goto cleanup; + } + break; + default: + api_set_error(err, kErrorTypeValidation, "'group' must be a string or an integer."); + goto cleanup; + } + + if (opts->buffer.type != kObjectTypeNil) { + Object buf_obj = opts->buffer; + if (buf_obj.type != kObjectTypeInteger && buf_obj.type != kObjectTypeBuffer) { + api_set_error(err, kErrorTypeException, "invalid buffer: %d", buf_obj.type); + goto cleanup; + } + + buf = find_buffer_by_handle((Buffer)buf_obj.data.integer, err); + + if (ERROR_SET(err)) { + goto cleanup; + } + } + + if (!get_patterns_from_pattern_or_buf(&patterns, opts->pattern, opts->buffer, err)) { + goto cleanup; + } + + if (patterns.size == 0) { + ADD(patterns, STRING_OBJ(STATIC_CSTR_TO_STRING(""))); + } + + if (opts->data.type != kObjectTypeNil) { + data = &opts->data; + } + + modeline = api_object_to_bool(opts->modeline, "modeline", true, err); + + bool did_aucmd = false; + FOREACH_ITEM(event_array, event_str, { + GET_ONE_EVENT(event_nr, event_str, cleanup) + + FOREACH_ITEM(patterns, pat, { + char *fname = opts->buffer.type == kObjectTypeNil ? pat.data.string.data : NULL; + did_aucmd |= + apply_autocmds_group(event_nr, fname, NULL, true, au_group, buf, NULL, data); + }) + }) + + if (did_aucmd && modeline) { + do_modelines(0); + } + +cleanup: + api_free_array(event_array); + api_free_array(patterns); +} + +static bool check_autocmd_string_array(Array arr, char *k, Error *err) +{ + FOREACH_ITEM(arr, entry, { + if (entry.type != kObjectTypeString) { + api_set_error(err, + kErrorTypeValidation, + "All entries in '%s' must be strings", + k); + return false; + } + + // Disallow newlines in the middle of the line. + const String l = entry.data.string; + if (memchr(l.data, NL, l.size)) { + api_set_error(err, kErrorTypeValidation, + "String cannot contain newlines"); + return false; + } + }) + return true; +} + +static bool unpack_string_or_array(Array *array, Object *v, char *k, bool required, Error *err) +{ + if (v->type == kObjectTypeString) { + ADD(*array, copy_object(*v)); + } else if (v->type == kObjectTypeArray) { + if (!check_autocmd_string_array(v->data.array, k, err)) { + return false; + } + *array = copy_array(v->data.array); + } else { + if (required) { + api_set_error(err, + kErrorTypeValidation, + "'%s' must be an array or a string.", + k); + return false; + } + } + + return true; +} + +// Returns AUGROUP_ERROR if there was a problem with {group} +static int get_augroup_from_object(Object group, Error *err) +{ + int au_group = AUGROUP_ERROR; + + switch (group.type) { + case kObjectTypeNil: + return AUGROUP_DEFAULT; + case kObjectTypeString: + au_group = augroup_find(group.data.string.data); + if (au_group == AUGROUP_ERROR) { + api_set_error(err, + kErrorTypeValidation, + "invalid augroup: %s", group.data.string.data); + + return AUGROUP_ERROR; + } + + return au_group; + case kObjectTypeInteger: + au_group = (int)group.data.integer; + char *name = augroup_name(au_group); + if (!augroup_exists(name)) { + api_set_error(err, kErrorTypeValidation, "invalid augroup: %d", au_group); + return AUGROUP_ERROR; + } + + return au_group; + default: + api_set_error(err, kErrorTypeValidation, "'group' must be a string or an integer."); + return AUGROUP_ERROR; + } +} + +static bool get_patterns_from_pattern_or_buf(Array *patterns, Object pattern, Object buffer, + Error *err) +{ + const char pattern_buflocal[BUFLOCAL_PAT_LEN]; + + if (pattern.type != kObjectTypeNil && buffer.type != kObjectTypeNil) { + api_set_error(err, kErrorTypeValidation, + "cannot pass both: 'pattern' and 'buffer' for the same autocmd"); + return false; + } else if (pattern.type != kObjectTypeNil) { + Object *v = &pattern; + + if (v->type == kObjectTypeString) { + char *pat = v->data.string.data; + size_t patlen = aucmd_pattern_length(pat); + while (patlen) { + ADD(*patterns, STRING_OBJ(cbuf_to_string((char *)pat, patlen))); + + pat = aucmd_next_pattern(pat, patlen); + patlen = aucmd_pattern_length(pat); + } + } else if (v->type == kObjectTypeArray) { + if (!check_autocmd_string_array(*patterns, "pattern", err)) { + return false; + } + + Array array = v->data.array; + FOREACH_ITEM(array, entry, { + char *pat = entry.data.string.data; + size_t patlen = aucmd_pattern_length(pat); + while (patlen) { + ADD(*patterns, STRING_OBJ(cbuf_to_string((char *)pat, patlen))); + + pat = aucmd_next_pattern(pat, patlen); + patlen = aucmd_pattern_length(pat); + } + }) + } else { + api_set_error(err, + kErrorTypeValidation, + "'pattern' must be a string or table"); + return false; + } + } else if (buffer.type != kObjectTypeNil) { + if (buffer.type != kObjectTypeInteger && buffer.type != kObjectTypeBuffer) { + api_set_error(err, + kErrorTypeValidation, + "'buffer' must be an integer"); + return false; + } + + buf_T *buf = find_buffer_by_handle((Buffer)buffer.data.integer, err); + if (ERROR_SET(err)) { + return false; + } + + snprintf((char *)pattern_buflocal, BUFLOCAL_PAT_LEN, "<buffer=%d>", (int)buf->handle); + ADD(*patterns, STRING_OBJ(cstr_to_string((char *)pattern_buflocal))); + } + + return true; +} + +static bool clear_autocmd(event_T event, char *pat, int au_group, Error *err) +{ + if (do_autocmd_event(event, pat, false, false, "", true, au_group) == FAIL) { + api_set_error(err, kErrorTypeException, "Failed to clear autocmd"); + return false; + } + + return true; +} + +#undef GET_ONE_EVENT diff --git a/src/nvim/api/autocmd.h b/src/nvim/api/autocmd.h new file mode 100644 index 0000000000..f9432830d9 --- /dev/null +++ b/src/nvim/api/autocmd.h @@ -0,0 +1,11 @@ +#ifndef NVIM_API_AUTOCMD_H +#define NVIM_API_AUTOCMD_H + +#include <stdint.h> + +#include "nvim/api/private/defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "api/autocmd.h.generated.h" +#endif +#endif // NVIM_API_AUTOCMD_H diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c index 2d5403d4b8..806b649ce6 100644 --- a/src/nvim/api/buffer.c +++ b/src/nvim/api/buffer.c @@ -22,6 +22,7 @@ #include "nvim/ex_docmd.h" #include "nvim/extmark.h" #include "nvim/lua/executor.h" +#include "nvim/mapping.h" #include "nvim/mark.h" #include "nvim/memline.h" #include "nvim/memory.h" @@ -35,7 +36,6 @@ # include "api/buffer.c.generated.h" #endif - /// \defgroup api-buffer /// /// \brief For more information on buffers, see |buffers| @@ -51,8 +51,7 @@ /// You can use |nvim_buf_is_loaded()| or |nvim_buf_line_count()| to check /// whether a buffer is loaded. - -/// Gets the buffer line count +/// Returns the number of lines in the given buffer. /// /// @param buffer Buffer handle, or 0 for current buffer /// @param[out] err Error details, if any @@ -133,7 +132,7 @@ Integer nvim_buf_line_count(Buffer buffer, Error *err) /// - buffer handle /// - on_reload: Lua callback invoked on reload. The entire buffer /// content should be considered changed. Args: -/// - the string "detach" +/// - the string "reload" /// - buffer handle /// - utf_sizes: include UTF-32 and UTF-16 size of the replaced /// region, as args to `on_lines`. @@ -247,7 +246,7 @@ void nvim__buf_redraw_range(Buffer buffer, Integer first, Integer last, Error *e return; } - redraw_buf_range_later(buf, (linenr_T)first+1, (linenr_T)last); + redraw_buf_range_later(buf, (linenr_T)first + 1, (linenr_T)last); } /// Gets a line-range from the buffer. @@ -262,7 +261,7 @@ void nvim__buf_redraw_range(Buffer buffer, Integer first, Integer last, Error *e /// @param channel_id /// @param buffer Buffer handle, or 0 for current buffer /// @param start First line index -/// @param end Last line index (exclusive) +/// @param end Last line index, exclusive /// @param strict_indexing Whether out-of-bounds should be an error. /// @param[out] err Error details, if any /// @return Array of lines, or empty array for unloaded buffer. @@ -287,8 +286,8 @@ ArrayOf(String) nvim_buf_get_lines(uint64_t channel_id, } bool oob = false; - start = normalize_index(buf, start, &oob); - end = normalize_index(buf, end, &oob); + start = normalize_index(buf, start, true, &oob); + end = normalize_index(buf, end, true, &oob); if (strict_indexing && oob) { api_set_error(err, kErrorTypeValidation, "Index out of bounds"); @@ -358,7 +357,7 @@ static bool check_string_array(Array arr, bool disallow_nl, Error *err) /// @param channel_id /// @param buffer Buffer handle, or 0 for current buffer /// @param start First line index -/// @param end Last line index (exclusive) +/// @param end Last line index, exclusive /// @param strict_indexing Whether out-of-bounds should be an error. /// @param replacement Array of lines to use as replacement /// @param[out] err Error details, if any @@ -374,15 +373,14 @@ void nvim_buf_set_lines(uint64_t channel_id, Buffer buffer, Integer start, Integ } bool oob = false; - start = normalize_index(buf, start, &oob); - end = normalize_index(buf, end, &oob); + start = normalize_index(buf, start, true, &oob); + end = normalize_index(buf, end, true, &oob); if (strict_indexing && oob) { api_set_error(err, kErrorTypeValidation, "Index out of bounds"); return; } - if (start > end) { api_set_error(err, kErrorTypeValidation, @@ -423,7 +421,7 @@ void nvim_buf_set_lines(uint64_t channel_id, Buffer buffer, Integer start, Integ goto end; } - bcount_t deleted_bytes = get_region_bytecount(curbuf, start, end, 0, 0); + bcount_t deleted_bytes = get_region_bytecount(curbuf, (linenr_T)start, (linenr_T)end, 0, 0); // If the size of the range is reducing (ie, new_len < old_len) we // need to delete some old_len. We do this at the start, by @@ -453,7 +451,7 @@ void nvim_buf_set_lines(uint64_t channel_id, Buffer buffer, Integer start, Integ goto end; } - if (ml_replace((linenr_T)lnum, (char_u *)lines[i], false) == FAIL) { + if (ml_replace((linenr_T)lnum, lines[i], false) == FAIL) { api_set_error(err, kErrorTypeException, "Failed to replace line"); goto end; } @@ -473,7 +471,7 @@ void nvim_buf_set_lines(uint64_t channel_id, Buffer buffer, Integer start, Integ goto end; } - if (ml_append((linenr_T)lnum, (char_u *)lines[i], 0, false) == FAIL) { + if (ml_append((linenr_T)lnum, lines[i], 0, false) == FAIL) { api_set_error(err, kErrorTypeException, "Failed to insert line"); goto end; } @@ -493,14 +491,14 @@ void nvim_buf_set_lines(uint64_t channel_id, Buffer buffer, Integer start, Integ mark_adjust((linenr_T)start, (linenr_T)(end - 1), MAXLNUM, - (long)extra, + (linenr_T)extra, kExtmarkNOOP); - extmark_splice(curbuf, (int)start-1, 0, (int)(end-start), 0, + extmark_splice(curbuf, (int)start - 1, 0, (int)(end - start), 0, deleted_bytes, (int)new_len, 0, inserted_bytes, kExtmarkUndo); - changed_lines((linenr_T)start, 0, (linenr_T)end, (long)extra, true); + changed_lines((linenr_T)start, 0, (linenr_T)end, (linenr_T)extra, true); fix_cursor((linenr_T)start, (linenr_T)end, (linenr_T)extra); end: @@ -515,24 +513,25 @@ end: /// Sets (replaces) a range in the buffer /// -/// This is recommended over nvim_buf_set_lines when only modifying parts of a -/// line, as extmarks will be preserved on non-modified parts of the touched +/// This is recommended over |nvim_buf_set_lines()| when only modifying parts of +/// a line, as extmarks will be preserved on non-modified parts of the touched /// lines. /// -/// Indexing is zero-based and end-exclusive. +/// Indexing is zero-based. Row indices are end-inclusive, and column indices +/// are end-exclusive. /// -/// To insert text at a given index, set `start` and `end` ranges to the same -/// index. To delete a range, set `replacement` to an array containing -/// an empty string, or simply an empty array. +/// To insert text at a given `(row, column)` location, use `start_row = end_row +/// = row` and `start_col = end_col = col`. To delete the text in a range, use +/// `replacement = {}`. /// -/// Prefer nvim_buf_set_lines when adding or deleting entire lines only. +/// Prefer |nvim_buf_set_lines()| if you are only adding or deleting entire lines. /// /// @param channel_id /// @param buffer Buffer handle, or 0 for current buffer /// @param start_row First line index -/// @param start_column First column -/// @param end_row Last line index -/// @param end_column Last column +/// @param start_col Starting column (byte offset) on first line +/// @param end_row Last line index, inclusive +/// @param end_col Ending column (byte offset) on last line, exclusive /// @param replacement Array of lines to use as replacement /// @param[out] err Error details, if any void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, Integer start_col, @@ -554,25 +553,25 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, In // check range is ordered and everything! // start_row, end_row within buffer len (except add text past the end?) - start_row = normalize_index(buf, start_row, &oob); - if (oob || start_row == buf->b_ml.ml_line_count + 1) { + start_row = normalize_index(buf, start_row, false, &oob); + if (oob) { api_set_error(err, kErrorTypeValidation, "start_row out of bounds"); return; } - end_row = normalize_index(buf, end_row, &oob); - if (oob || end_row == buf->b_ml.ml_line_count + 1) { + end_row = normalize_index(buf, end_row, false, &oob); + if (oob) { api_set_error(err, kErrorTypeValidation, "end_row out of bounds"); return; } - char *str_at_start = (char *)ml_get_buf(buf, start_row, false); + char *str_at_start = (char *)ml_get_buf(buf, (linenr_T)start_row, false); if (start_col < 0 || (size_t)start_col > strlen(str_at_start)) { api_set_error(err, kErrorTypeValidation, "start_col out of bounds"); return; } - char *str_at_end = (char *)ml_get_buf(buf, end_row, false); + char *str_at_end = (char *)ml_get_buf(buf, (linenr_T)end_row, false); size_t len_at_end = strlen(str_at_end); if (end_col < 0 || (size_t)end_col > len_at_end) { api_set_error(err, kErrorTypeValidation, "end_col out of bounds"); @@ -598,21 +597,20 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, In if (start_row == end_row) { old_byte = (bcount_t)end_col - start_col; } else { - const char *bufline; old_byte += (bcount_t)strlen(str_at_start) - start_col; for (int64_t i = 1; i < end_row - start_row; i++) { int64_t lnum = start_row + i; - bufline = (char *)ml_get_buf(buf, lnum, false); - old_byte += (bcount_t)(strlen(bufline))+1; + const char *bufline = (char *)ml_get_buf(buf, (linenr_T)lnum, false); + old_byte += (bcount_t)(strlen(bufline)) + 1; } - old_byte += (bcount_t)end_col+1; + old_byte += (bcount_t)end_col + 1; } String first_item = replacement.items[0].data.string; - String last_item = replacement.items[replacement.size-1].data.string; + String last_item = replacement.items[replacement.size - 1].data.string; - size_t firstlen = (size_t)start_col+first_item.size; + size_t firstlen = (size_t)start_col + first_item.size; size_t last_part_len = strlen(str_at_end) - (size_t)end_col; if (replacement.size == 1) { firstlen += last_part_len; @@ -620,32 +618,32 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, In char *first = xmallocz(firstlen); char *last = NULL; memcpy(first, str_at_start, (size_t)start_col); - memcpy(first+start_col, first_item.data, first_item.size); - memchrsub(first+start_col, NUL, NL, first_item.size); + memcpy(first + start_col, first_item.data, first_item.size); + memchrsub(first + start_col, NUL, NL, first_item.size); if (replacement.size == 1) { - memcpy(first+start_col+first_item.size, str_at_end+end_col, last_part_len); + memcpy(first + start_col + first_item.size, str_at_end + end_col, last_part_len); } else { - last = xmallocz(last_item.size+last_part_len); + last = xmallocz(last_item.size + last_part_len); memcpy(last, last_item.data, last_item.size); memchrsub(last, NUL, NL, last_item.size); - memcpy(last+last_item.size, str_at_end+end_col, last_part_len); + memcpy(last + last_item.size, str_at_end + end_col, last_part_len); } char **lines = xcalloc(new_len, sizeof(char *)); lines[0] = first; new_byte += (bcount_t)(first_item.size); - for (size_t i = 1; i < new_len-1; i++) { + for (size_t i = 1; i < new_len - 1; i++) { const String l = replacement.items[i].data.string; // Fill lines[i] with l's contents. Convert NULs to newlines as required by // NL-used-for-NUL. lines[i] = xmemdupz(l.data, l.size); memchrsub(lines[i], NUL, NL, l.size); - new_byte += (bcount_t)(l.size)+1; + new_byte += (bcount_t)(l.size) + 1; } if (replacement.size > 1) { - lines[replacement.size-1] = last; - new_byte += (bcount_t)(last_item.size)+1; + lines[replacement.size - 1] = last; + new_byte += (bcount_t)(last_item.size) + 1; } try_start(); @@ -665,7 +663,7 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, In } ptrdiff_t extra = 0; // lines added to text, can be negative - size_t old_len = (size_t)(end_row-start_row+1); + size_t old_len = (size_t)(end_row - start_row + 1); // If the size of the range is reducing (ie, new_len < old_len) we // need to delete some old_len. We do this at the start, by @@ -694,7 +692,7 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, In goto end; } - if (ml_replace((linenr_T)lnum, (char_u *)lines[i], false) == FAIL) { + if (ml_replace((linenr_T)lnum, lines[i], false) == FAIL) { api_set_error(err, kErrorTypeException, "Failed to replace line"); goto end; } @@ -712,7 +710,7 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, In goto end; } - if (ml_append((linenr_T)lnum, (char_u *)lines[i], 0, false) == FAIL) { + if (ml_append((linenr_T)lnum, lines[i], 0, false) == FAIL) { api_set_error(err, kErrorTypeException, "Failed to insert line"); goto end; } @@ -728,19 +726,17 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, In mark_adjust((linenr_T)start_row, (linenr_T)end_row, MAXLNUM, - (long)extra, + (linenr_T)extra, kExtmarkNOOP); colnr_T col_extent = (colnr_T)(end_col - ((end_row == start_row) ? start_col : 0)); - extmark_splice(buf, (int)start_row-1, (colnr_T)start_col, - (int)(end_row-start_row), col_extent, old_byte, - (int)new_len-1, (colnr_T)last_item.size, new_byte, + extmark_splice(buf, (int)start_row - 1, (colnr_T)start_col, + (int)(end_row - start_row), col_extent, old_byte, + (int)new_len - 1, (colnr_T)last_item.size, new_byte, kExtmarkUndo); - - changed_lines((linenr_T)start_row, 0, (linenr_T)end_row + 1, - (long)extra, true); + changed_lines((linenr_T)start_row, 0, (linenr_T)end_row + 1, (linenr_T)extra, true); // adjust cursor like an extmark ( i e it was inside last_part_len) if (curwin->w_cursor.lnum == end_row && curwin->w_cursor.col > end_col) { @@ -757,6 +753,109 @@ end: try_end(err); } +/// Gets a range from the buffer. +/// +/// This differs from |nvim_buf_get_lines()| in that it allows retrieving only +/// portions of a line. +/// +/// Indexing is zero-based. Row indices are end-inclusive, and column indices +/// are end-exclusive. +/// +/// Prefer |nvim_buf_get_lines()| when retrieving entire lines. +/// +/// @param channel_id +/// @param buffer Buffer handle, or 0 for current buffer +/// @param start_row First line index +/// @param start_col Starting column (byte offset) on first line +/// @param end_row Last line index, inclusive +/// @param end_col Ending column (byte offset) on last line, exclusive +/// @param opts Optional parameters. Currently unused. +/// @param[out] err Error details, if any +/// @return Array of lines, or empty array for unloaded buffer. +ArrayOf(String) nvim_buf_get_text(uint64_t channel_id, Buffer buffer, + Integer start_row, Integer start_col, + Integer end_row, Integer end_col, + Dictionary opts, Error *err) + FUNC_API_SINCE(9) +{ + Array rv = ARRAY_DICT_INIT; + + if (opts.size > 0) { + api_set_error(err, kErrorTypeValidation, "opts dict isn't empty"); + return rv; + } + + buf_T *buf = find_buffer_by_handle(buffer, err); + + if (!buf) { + return rv; + } + + // return sentinel value if the buffer isn't loaded + if (buf->b_ml.ml_mfp == NULL) { + return rv; + } + + bool oob = false; + start_row = normalize_index(buf, start_row, false, &oob); + end_row = normalize_index(buf, end_row, false, &oob); + + if (oob) { + api_set_error(err, kErrorTypeValidation, "Index out of bounds"); + return rv; + } + + // nvim_buf_get_lines doesn't care if the start row is greater than the end + // row (it will just return an empty array), but nvim_buf_get_text does in + // order to maintain symmetry with nvim_buf_set_text. + if (start_row > end_row) { + api_set_error(err, kErrorTypeValidation, "start is higher than end"); + return rv; + } + + bool replace_nl = (channel_id != VIML_INTERNAL_CALL); + + if (start_row == end_row) { + String line = buf_get_text(buf, start_row, start_col, end_col, replace_nl, err); + if (ERROR_SET(err)) { + return rv; + } + + ADD(rv, STRING_OBJ(line)); + return rv; + } + + rv.size = (size_t)(end_row - start_row) + 1; + rv.items = xcalloc(rv.size, sizeof(Object)); + + rv.items[0] = STRING_OBJ(buf_get_text(buf, start_row, start_col, MAXCOL - 1, replace_nl, err)); + if (ERROR_SET(err)) { + goto end; + } + + if (rv.size > 2) { + Array tmp = ARRAY_DICT_INIT; + tmp.items = &rv.items[1]; + if (!buf_collect_lines(buf, rv.size - 2, start_row + 1, replace_nl, &tmp, err)) { + goto end; + } + } + + rv.items[rv.size - 1] = STRING_OBJ(buf_get_text(buf, end_row, 0, end_col, replace_nl, err)); + if (ERROR_SET(err)) { + goto end; + } + +end: + if (ERROR_SET(err)) { + api_free_array(rv); + rv.size = 0; + rv.items = NULL; + } + + return rv; +} + /// Returns the byte offset of a line (0-indexed). |api-indexing| /// /// Line 1 (index=0) has offset 0. UTF-8 bytes are counted. EOL is one byte. @@ -789,7 +888,7 @@ Integer nvim_buf_get_offset(Buffer buffer, Integer index, Error *err) return 0; } - return ml_find_line_or_offset(buf, (int)index+1, NULL, true); + return ml_find_line_or_offset(buf, (int)index + 1, NULL, true); } /// Gets a buffer-scoped (b:) variable. @@ -852,11 +951,11 @@ ArrayOf(Dictionary) nvim_buf_get_keymap(uint64_t channel_id, Buffer buffer, Stri /// @see |nvim_set_keymap()| /// /// @param buffer Buffer handle, or 0 for current buffer -void nvim_buf_set_keymap(Buffer buffer, String mode, String lhs, String rhs, Dict(keymap) *opts, - Error *err) +void nvim_buf_set_keymap(uint64_t channel_id, Buffer buffer, String mode, String lhs, String rhs, + Dict(keymap) *opts, Error *err) FUNC_API_SINCE(6) { - modify_keymap(buffer, false, mode, lhs, rhs, opts, err); + modify_keymap(channel_id, buffer, false, mode, lhs, rhs, opts, err); } /// Unmaps a buffer-local |mapping| for the given mode. @@ -864,42 +963,11 @@ void nvim_buf_set_keymap(Buffer buffer, String mode, String lhs, String rhs, Dic /// @see |nvim_del_keymap()| /// /// @param buffer Buffer handle, or 0 for current buffer -void nvim_buf_del_keymap(Buffer buffer, String mode, String lhs, Error *err) +void nvim_buf_del_keymap(uint64_t channel_id, Buffer buffer, String mode, String lhs, Error *err) FUNC_API_SINCE(6) { String rhs = { .data = "", .size = 0 }; - modify_keymap(buffer, true, mode, lhs, rhs, NULL, err); -} - -/// Gets a map of buffer-local |user-commands|. -/// -/// @param buffer Buffer handle, or 0 for current buffer -/// @param opts Optional parameters. Currently not used. -/// @param[out] err Error details, if any. -/// -/// @returns Map of maps describing commands. -Dictionary nvim_buf_get_commands(Buffer buffer, Dict(get_commands) *opts, Error *err) - FUNC_API_SINCE(4) -{ - bool global = (buffer == -1); - bool builtin = api_object_to_bool(opts->builtin, "builtin", false, err); - if (ERROR_SET(err)) { - return (Dictionary)ARRAY_DICT_INIT; - } - - if (global) { - if (builtin) { - api_set_error(err, kErrorTypeValidation, "builtin=true not implemented"); - return (Dictionary)ARRAY_DICT_INIT; - } - return commands_array(NULL); - } - - buf_T *buf = find_buffer_by_handle(buffer, err); - if (builtin || !buf) { - return (Dictionary)ARRAY_DICT_INIT; - } - return commands_array(buf); + modify_keymap(channel_id, buffer, true, mode, lhs, rhs, NULL, err); } /// Sets a buffer-scoped (b:) variable @@ -937,45 +1005,6 @@ void nvim_buf_del_var(Buffer buffer, String name, Error *err) dict_set_var(buf->b_vars, name, NIL, true, false, err); } - -/// Gets a buffer option value -/// -/// @param buffer Buffer handle, or 0 for current buffer -/// @param name Option name -/// @param[out] err Error details, if any -/// @return Option value -Object nvim_buf_get_option(Buffer buffer, String name, Error *err) - FUNC_API_SINCE(1) -{ - buf_T *buf = find_buffer_by_handle(buffer, err); - - if (!buf) { - return (Object)OBJECT_INIT; - } - - return get_option_from(buf, SREQ_BUF, name, err); -} - -/// Sets a buffer option value. Passing 'nil' as value deletes the option (only -/// works if there's a global fallback) -/// -/// @param channel_id -/// @param buffer Buffer handle, or 0 for current buffer -/// @param name Option name -/// @param value Option value -/// @param[out] err Error details, if any -void nvim_buf_set_option(uint64_t channel_id, Buffer buffer, String name, Object value, Error *err) - FUNC_API_SINCE(1) -{ - buf_T *buf = find_buffer_by_handle(buffer, err); - - if (!buf) { - return; - } - - set_option_to(channel_id, buf, SREQ_BUF, name, value, err); -} - /// Gets the full file name for the buffer /// /// @param buffer Buffer handle, or 0 for current buffer @@ -1013,7 +1042,7 @@ void nvim_buf_set_name(Buffer buffer, String name, Error *err) // Using aucmd_*: autocommands will be executed by rename_buffer aco_save_T aco; aucmd_prepbuf(&aco, buf); - int ren_ret = rename_buffer((char_u *)name.data); + int ren_ret = rename_buffer(name.data); aucmd_restbuf(&aco); if (try_end(err)) { @@ -1127,17 +1156,17 @@ Boolean nvim_buf_del_mark(Buffer buffer, String name, Error *err) return res; } - pos_T *pos = getmark_buf(buf, *name.data, false); + fmark_T *fm = mark_get(buf, curwin, NULL, kMarkAllNoResolve, *name.data); - // pos point to NULL when there's no mark with name - if (pos == NULL) { + // fm is NULL when there's no mark with the given name + if (fm == NULL) { api_set_error(err, kErrorTypeValidation, "Invalid mark name: '%c'", *name.data); return res; } - // pos->lnum is 0 when the mark is not valid in the buffer, or is not set. - if (pos->lnum != 0) { + // mark.lnum is 0 when the mark is not valid in the buffer, or is not set. + if (fm->mark.lnum != 0 && fm->fnum == buf->handle) { // since the mark belongs to the buffer delete it. res = set_mark(buf, name, 0, 0, err); } @@ -1210,31 +1239,29 @@ ArrayOf(Integer, 2) nvim_buf_get_mark(Buffer buffer, String name, Error *err) return rv; } - pos_T *posp; + fmark_T *fm; + pos_T pos; char mark = *name.data; - try_start(); - bufref_T save_buf; - switch_buffer(&save_buf, buf); - posp = getmark(mark, false); - restore_buffer(&save_buf); - - if (try_end(err)) { - return rv; - } - - if (posp == NULL) { + fm = mark_get(buf, curwin, NULL, kMarkAllNoResolve, mark); + if (fm == NULL) { api_set_error(err, kErrorTypeValidation, "Invalid mark name"); return rv; } + // (0, 0) uppercase/file mark set in another buffer. + if (fm->fnum != buf->handle) { + pos.lnum = 0; + pos.col = 0; + } else { + pos = fm->mark; + } - ADD(rv, INTEGER_OBJ(posp->lnum)); - ADD(rv, INTEGER_OBJ(posp->col)); + ADD(rv, INTEGER_OBJ(pos.lnum)); + ADD(rv, INTEGER_OBJ(pos.col)); return rv; } - /// call a function with buffer as temporary current buffer /// /// This temporarily switches current buffer to "buffer". @@ -1273,63 +1300,6 @@ Object nvim_buf_call(Buffer buffer, LuaRef fun, Error *err) return res; } -/// Create a new user command |user-commands| in the given buffer. -/// -/// @param buffer Buffer handle, or 0 for current buffer. -/// @param[out] err Error details, if any. -/// @see nvim_add_user_command -void nvim_buf_add_user_command(Buffer buffer, String name, Object command, - Dict(user_command) *opts, Error *err) - FUNC_API_SINCE(9) -{ - buf_T *target_buf = find_buffer_by_handle(buffer, err); - if (ERROR_SET(err)) { - return; - } - - buf_T *save_curbuf = curbuf; - curbuf = target_buf; - add_user_command(name, command, opts, UC_BUFFER, err); - curbuf = save_curbuf; -} - -/// Delete a buffer-local user-defined command. -/// -/// Only commands created with |:command-buffer| or -/// |nvim_buf_add_user_command()| can be deleted with this function. -/// -/// @param buffer Buffer handle, or 0 for current buffer. -/// @param name Name of the command to delete. -/// @param[out] err Error details, if any. -void nvim_buf_del_user_command(Buffer buffer, String name, Error *err) - FUNC_API_SINCE(9) -{ - garray_T *gap; - if (buffer == -1) { - gap = &ucmds; - } else { - buf_T *buf = find_buffer_by_handle(buffer, err); - gap = &buf->b_ucmds; - } - - for (int i = 0; i < gap->ga_len; i++) { - ucmd_T *cmd = USER_CMD_GA(gap, i); - if (!STRCMP(name.data, cmd->uc_name)) { - free_ucmd(cmd); - - gap->ga_len -= 1; - - if (i < gap->ga_len) { - memmove(cmd, cmd + 1, (size_t)(gap->ga_len - i) * sizeof(ucmd_T)); - } - - return; - } - } - - api_set_error(err, kErrorTypeException, "No such user-defined command: %s", name.data); -} - Dictionary nvim__buf_stats(Buffer buffer, Error *err) { Dictionary rv = ARRAY_DICT_INIT; @@ -1386,16 +1356,17 @@ static void fix_cursor(linenr_T lo, linenr_T hi, linenr_T extra) } // Normalizes 0-based indexes to buffer line numbers -static int64_t normalize_index(buf_T *buf, int64_t index, bool *oob) +static int64_t normalize_index(buf_T *buf, int64_t index, bool end_exclusive, bool *oob) { - int64_t line_count = buf->b_ml.ml_line_count; + assert(buf->b_ml.ml_line_count > 0); + int64_t max_index = buf->b_ml.ml_line_count + (int)end_exclusive - 1; // Fix if < 0 - index = index < 0 ? line_count + index +1 : index; + index = index < 0 ? max_index + index + 1 : index; // Check for oob - if (index > line_count) { + if (index > max_index) { *oob = true; - index = line_count; + index = max_index; } else if (index < 0) { *oob = true; index = 0; diff --git a/src/nvim/api/command.c b/src/nvim/api/command.c new file mode 100644 index 0000000000..4c2404a0d8 --- /dev/null +++ b/src/nvim/api/command.c @@ -0,0 +1,1189 @@ +// 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 <stdbool.h> +#include <stdint.h> +#include <stdlib.h> + +#include "nvim/api/command.h" +#include "nvim/api/private/converter.h" +#include "nvim/api/private/defs.h" +#include "nvim/api/private/helpers.h" +#include "nvim/autocmd.h" +#include "nvim/ex_docmd.h" +#include "nvim/lua/executor.h" +#include "nvim/ops.h" +#include "nvim/regexp.h" +#include "nvim/window.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "api/command.c.generated.h" +#endif + +/// Parse command line. +/// +/// Doesn't check the validity of command arguments. +/// +/// @param str Command line string to parse. Cannot contain "\n". +/// @param opts Optional parameters. Reserved for future use. +/// @param[out] err Error details, if any. +/// @return Dictionary containing command information, with these keys: +/// - cmd: (string) Command name. +/// - range: (array) Command <range>. Can have 0-2 elements depending on how many items the +/// range contains. Has no elements if command doesn't accept a range or if +/// no range was specified, one element if only a single range item was +/// specified and two elements if both range items were specified. +/// - count: (number) Any |<count>| that was supplied to the command. -1 if command cannot +/// take a count. +/// - reg: (number) The optional command |<register>|, if specified. Empty string if not +/// specified or if command cannot take a register. +/// - bang: (boolean) Whether command contains a |<bang>| (!) modifier. +/// - args: (array) Command arguments. +/// - addr: (string) Value of |:command-addr|. Uses short name. +/// - nargs: (string) Value of |:command-nargs|. +/// - nextcmd: (string) Next command if there are multiple commands separated by a |:bar|. +/// Empty if there isn't a next command. +/// - magic: (dictionary) Which characters have special meaning in the command arguments. +/// - file: (boolean) The command expands filenames. Which means characters such as "%", +/// "#" and wildcards are expanded. +/// - bar: (boolean) The "|" character is treated as a command separator and the double +/// quote character (\") is treated as the start of a comment. +/// - mods: (dictionary) |:command-modifiers|. +/// - filter: (dictionary) |:filter|. +/// - pattern: (string) Filter pattern. Empty string if there is no filter. +/// - force: (boolean) Whether filter is inverted or not. +/// - silent: (boolean) |:silent|. +/// - emsg_silent: (boolean) |:silent!|. +/// - unsilent: (boolean) |:unsilent|. +/// - sandbox: (boolean) |:sandbox|. +/// - noautocmd: (boolean) |:noautocmd|. +/// - browse: (boolean) |:browse|. +/// - confirm: (boolean) |:confirm|. +/// - hide: (boolean) |:hide|. +/// - keepalt: (boolean) |:keepalt|. +/// - keepjumps: (boolean) |:keepjumps|. +/// - keepmarks: (boolean) |:keepmarks|. +/// - keeppatterns: (boolean) |:keeppatterns|. +/// - lockmarks: (boolean) |:lockmarks|. +/// - noswapfile: (boolean) |:noswapfile|. +/// - tab: (integer) |:tab|. +/// - verbose: (integer) |:verbose|. -1 when omitted. +/// - vertical: (boolean) |:vertical|. +/// - split: (string) Split modifier string, is an empty string when there's no split +/// modifier. If there is a split modifier it can be one of: +/// - "aboveleft": |:aboveleft|. +/// - "belowright": |:belowright|. +/// - "topleft": |:topleft|. +/// - "botright": |:botright|. +Dictionary nvim_parse_cmd(String str, Dictionary opts, Error *err) + FUNC_API_SINCE(10) FUNC_API_FAST +{ + Dictionary result = ARRAY_DICT_INIT; + + if (opts.size > 0) { + api_set_error(err, kErrorTypeValidation, "opts dict isn't empty"); + return result; + } + + // Parse command line + exarg_T ea; + CmdParseInfo cmdinfo; + char *cmdline = string_to_cstr(str); + char *errormsg = NULL; + + if (!parse_cmdline(cmdline, &ea, &cmdinfo, &errormsg)) { + if (errormsg != NULL) { + api_set_error(err, kErrorTypeException, "Error while parsing command line: %s", errormsg); + } else { + api_set_error(err, kErrorTypeException, "Error while parsing command line"); + } + goto end; + } + + // Parse arguments + Array args = ARRAY_DICT_INIT; + size_t length = STRLEN(ea.arg); + + // For nargs = 1 or '?', pass the entire argument list as a single argument, + // otherwise split arguments by whitespace. + if (ea.argt & EX_NOSPC) { + if (*ea.arg != NUL) { + ADD(args, STRING_OBJ(cstrn_to_string((char *)ea.arg, length))); + } + } else { + size_t end = 0; + size_t len = 0; + char *buf = xcalloc(length, sizeof(char)); + bool done = false; + + while (!done) { + done = uc_split_args_iter(ea.arg, length, &end, buf, &len); + if (len > 0) { + ADD(args, STRING_OBJ(cstrn_to_string(buf, len))); + } + } + + xfree(buf); + } + + ucmd_T *cmd = NULL; + if (ea.cmdidx == CMD_USER) { + cmd = USER_CMD(ea.useridx); + } else if (ea.cmdidx == CMD_USER_BUF) { + cmd = USER_CMD_GA(&curbuf->b_ucmds, ea.useridx); + } + + if (cmd != NULL) { + PUT(result, "cmd", CSTR_TO_OBJ((char *)cmd->uc_name)); + } else { + PUT(result, "cmd", CSTR_TO_OBJ((char *)get_command_name(NULL, ea.cmdidx))); + } + + if ((ea.argt & EX_RANGE) && ea.addr_count > 0) { + Array range = ARRAY_DICT_INIT; + if (ea.addr_count > 1) { + ADD(range, INTEGER_OBJ(ea.line1)); + } + ADD(range, INTEGER_OBJ(ea.line2)); + PUT(result, "range", ARRAY_OBJ(range)); + } else { + PUT(result, "range", ARRAY_OBJ(ARRAY_DICT_INIT)); + } + + if (ea.argt & EX_COUNT) { + if (ea.addr_count > 0) { + PUT(result, "count", INTEGER_OBJ(ea.line2)); + } else if (cmd != NULL) { + PUT(result, "count", INTEGER_OBJ(cmd->uc_def)); + } else { + PUT(result, "count", INTEGER_OBJ(0)); + } + } else { + PUT(result, "count", INTEGER_OBJ(-1)); + } + + char reg[2]; + reg[0] = (char)ea.regname; + reg[1] = '\0'; + PUT(result, "reg", CSTR_TO_OBJ(reg)); + + PUT(result, "bang", BOOLEAN_OBJ(ea.forceit)); + PUT(result, "args", ARRAY_OBJ(args)); + + char nargs[2]; + if (ea.argt & EX_EXTRA) { + if (ea.argt & EX_NOSPC) { + if (ea.argt & EX_NEEDARG) { + nargs[0] = '1'; + } else { + nargs[0] = '?'; + } + } else if (ea.argt & EX_NEEDARG) { + nargs[0] = '+'; + } else { + nargs[0] = '*'; + } + } else { + nargs[0] = '0'; + } + nargs[1] = '\0'; + PUT(result, "nargs", CSTR_TO_OBJ(nargs)); + + const char *addr; + switch (ea.addr_type) { + case ADDR_LINES: + addr = "line"; + break; + case ADDR_ARGUMENTS: + addr = "arg"; + break; + case ADDR_BUFFERS: + addr = "buf"; + break; + case ADDR_LOADED_BUFFERS: + addr = "load"; + break; + case ADDR_WINDOWS: + addr = "win"; + break; + case ADDR_TABS: + addr = "tab"; + break; + case ADDR_QUICKFIX: + addr = "qf"; + break; + case ADDR_NONE: + addr = "none"; + break; + default: + addr = "?"; + break; + } + PUT(result, "addr", CSTR_TO_OBJ(addr)); + PUT(result, "nextcmd", CSTR_TO_OBJ((char *)ea.nextcmd)); + + Dictionary mods = ARRAY_DICT_INIT; + + Dictionary filter = ARRAY_DICT_INIT; + PUT(filter, "pattern", cmdinfo.cmdmod.cmod_filter_pat + ? CSTR_TO_OBJ(cmdinfo.cmdmod.cmod_filter_pat) + : STRING_OBJ(STATIC_CSTR_TO_STRING(""))); + PUT(filter, "force", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_filter_force)); + PUT(mods, "filter", DICTIONARY_OBJ(filter)); + + PUT(mods, "silent", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_SILENT)); + PUT(mods, "emsg_silent", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_ERRSILENT)); + PUT(mods, "unsilent", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_UNSILENT)); + PUT(mods, "sandbox", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_SANDBOX)); + PUT(mods, "noautocmd", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_NOAUTOCMD)); + PUT(mods, "tab", INTEGER_OBJ(cmdinfo.cmdmod.cmod_tab)); + PUT(mods, "verbose", INTEGER_OBJ(cmdinfo.cmdmod.cmod_verbose - 1)); + PUT(mods, "browse", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_BROWSE)); + PUT(mods, "confirm", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_CONFIRM)); + PUT(mods, "hide", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_HIDE)); + PUT(mods, "keepalt", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_KEEPALT)); + PUT(mods, "keepjumps", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_KEEPJUMPS)); + PUT(mods, "keepmarks", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_KEEPMARKS)); + PUT(mods, "keeppatterns", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_KEEPPATTERNS)); + PUT(mods, "lockmarks", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_LOCKMARKS)); + PUT(mods, "noswapfile", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_flags & CMOD_NOSWAPFILE)); + PUT(mods, "vertical", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_split & WSP_VERT)); + + const char *split; + if (cmdinfo.cmdmod.cmod_split & WSP_BOT) { + split = "botright"; + } else if (cmdinfo.cmdmod.cmod_split & WSP_TOP) { + split = "topleft"; + } else if (cmdinfo.cmdmod.cmod_split & WSP_BELOW) { + split = "belowright"; + } else if (cmdinfo.cmdmod.cmod_split & WSP_ABOVE) { + split = "aboveleft"; + } else { + split = ""; + } + PUT(mods, "split", CSTR_TO_OBJ(split)); + + PUT(result, "mods", DICTIONARY_OBJ(mods)); + + Dictionary magic = ARRAY_DICT_INIT; + PUT(magic, "file", BOOLEAN_OBJ(cmdinfo.magic.file)); + PUT(magic, "bar", BOOLEAN_OBJ(cmdinfo.magic.bar)); + PUT(result, "magic", DICTIONARY_OBJ(magic)); + + undo_cmdmod(&cmdinfo.cmdmod); +end: + xfree(cmdline); + return result; +} + +/// Executes an Ex command. +/// +/// Unlike |nvim_command()| this command takes a structured Dictionary instead of a String. This +/// allows for easier construction and manipulation of an Ex command. This also allows for things +/// such as having spaces inside a command argument, expanding filenames in a command that otherwise +/// doesn't expand filenames, etc. +/// +/// On execution error: fails with VimL error, updates v:errmsg. +/// +/// @see |nvim_exec()| +/// @see |nvim_command()| +/// +/// @param cmd Command to execute. Must be a Dictionary that can contain the same values as +/// the return value of |nvim_parse_cmd()| except "addr", "nargs" and "nextcmd" +/// which are ignored if provided. All values except for "cmd" are optional. +/// @param opts Optional parameters. +/// - output: (boolean, default false) Whether to return command output. +/// @param[out] err Error details, if any. +/// @return Command output (non-error, non-shell |:!|) if `output` is true, else empty string. +String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Error *err) + FUNC_API_SINCE(10) +{ + exarg_T ea; + memset(&ea, 0, sizeof(ea)); + + CmdParseInfo cmdinfo; + memset(&cmdinfo, 0, sizeof(cmdinfo)); + + char *cmdline = NULL; + char *cmdname = NULL; + char **args = NULL; + size_t argc = 0; + + String retv = (String)STRING_INIT; + +#define OBJ_TO_BOOL(var, value, default, varname) \ + do { \ + (var) = api_object_to_bool(value, varname, default, err); \ + if (ERROR_SET(err)) { \ + goto end; \ + } \ + } while (0) + +#define OBJ_TO_CMOD_FLAG(flag, value, default, varname) \ + do { \ + if (api_object_to_bool(value, varname, default, err)) { \ + cmdinfo.cmdmod.cmod_flags |= (flag); \ + } \ + if (ERROR_SET(err)) { \ + goto end; \ + } \ + } while (0) + +#define VALIDATION_ERROR(...) \ + do { \ + api_set_error(err, kErrorTypeValidation, __VA_ARGS__); \ + goto end; \ + } while (0) + + bool output; + OBJ_TO_BOOL(output, opts->output, false, "'output'"); + + // First, parse the command name and check if it exists and is valid. + if (!HAS_KEY(cmd->cmd) || cmd->cmd.type != kObjectTypeString + || cmd->cmd.data.string.data[0] == NUL) { + VALIDATION_ERROR("'cmd' must be a non-empty String"); + } + + cmdname = string_to_cstr(cmd->cmd.data.string); + ea.cmd = cmdname; + + char *p = find_ex_command(&ea, NULL); + + // If this looks like an undefined user command and there are CmdUndefined + // autocommands defined, trigger the matching autocommands. + if (p != NULL && ea.cmdidx == CMD_SIZE && ASCII_ISUPPER(*ea.cmd) + && has_event(EVENT_CMDUNDEFINED)) { + p = xstrdup(cmdname); + int ret = apply_autocmds(EVENT_CMDUNDEFINED, p, p, true, NULL); + xfree(p); + // If the autocommands did something and didn't cause an error, try + // finding the command again. + p = (ret && !aborting()) ? find_ex_command(&ea, NULL) : ea.cmd; + } + + if (p == NULL || ea.cmdidx == CMD_SIZE) { + VALIDATION_ERROR("Command not found: %s", cmdname); + } + if (is_cmd_ni(ea.cmdidx)) { + VALIDATION_ERROR("Command not implemented: %s", cmdname); + } + + // Get the command flags so that we can know what type of arguments the command uses. + // Not required for a user command since `find_ex_command` already deals with it in that case. + if (!IS_USER_CMDIDX(ea.cmdidx)) { + ea.argt = get_cmd_argt(ea.cmdidx); + } + + // Parse command arguments since it's needed to get the command address type. + if (HAS_KEY(cmd->args)) { + if (cmd->args.type != kObjectTypeArray) { + VALIDATION_ERROR("'args' must be an Array"); + } + // Check if every argument is valid + for (size_t i = 0; i < cmd->args.data.array.size; i++) { + Object elem = cmd->args.data.array.items[i]; + if (elem.type != kObjectTypeString) { + VALIDATION_ERROR("Command argument must be a String"); + } else if (string_iswhite(elem.data.string)) { + VALIDATION_ERROR("Command argument must have non-whitespace characters"); + } + } + + argc = cmd->args.data.array.size; + bool argc_valid; + + // Check if correct number of arguments is used. + switch (ea.argt & (EX_EXTRA | EX_NOSPC | EX_NEEDARG)) { + case EX_EXTRA | EX_NOSPC | EX_NEEDARG: + argc_valid = argc == 1; + break; + case EX_EXTRA | EX_NOSPC: + argc_valid = argc <= 1; + break; + case EX_EXTRA | EX_NEEDARG: + argc_valid = argc >= 1; + break; + case EX_EXTRA: + argc_valid = true; + break; + default: + argc_valid = argc == 0; + break; + } + + if (!argc_valid) { + argc = 0; // Ensure that args array isn't erroneously freed at the end. + VALIDATION_ERROR("Incorrect number of arguments supplied"); + } + + if (argc != 0) { + args = xcalloc(argc, sizeof(char *)); + + for (size_t i = 0; i < argc; i++) { + args[i] = string_to_cstr(cmd->args.data.array.items[i].data.string); + } + } + } + + // Simply pass the first argument (if it exists) as the arg pointer to `set_cmd_addr_type()` + // since it only ever checks the first argument. + set_cmd_addr_type(&ea, argc > 0 ? args[0] : NULL); + + if (HAS_KEY(cmd->range)) { + if (!(ea.argt & EX_RANGE)) { + VALIDATION_ERROR("Command cannot accept a range"); + } else if (cmd->range.type != kObjectTypeArray) { + VALIDATION_ERROR("'range' must be an Array"); + } else if (cmd->range.data.array.size > 2) { + VALIDATION_ERROR("'range' cannot contain more than two elements"); + } + + Array range = cmd->range.data.array; + ea.addr_count = (int)range.size; + + for (size_t i = 0; i < range.size; i++) { + Object elem = range.items[i]; + if (elem.type != kObjectTypeInteger || elem.data.integer < 0) { + VALIDATION_ERROR("'range' element must be a non-negative Integer"); + } + } + + if (range.size > 0) { + ea.line1 = (linenr_T)range.items[0].data.integer; + ea.line2 = (linenr_T)range.items[range.size - 1].data.integer; + } + + if (invalid_range(&ea) != NULL) { + VALIDATION_ERROR("Invalid range provided"); + } + } + if (ea.addr_count == 0) { + if (ea.argt & EX_DFLALL) { + set_cmd_dflall_range(&ea); // Default range for range=% + } else { + ea.line1 = ea.line2 = get_cmd_default_range(&ea); // Default range. + + if (ea.addr_type == ADDR_OTHER) { + // Default is 1, not cursor. + ea.line2 = 1; + } + } + } + + if (HAS_KEY(cmd->count)) { + if (!(ea.argt & EX_COUNT)) { + VALIDATION_ERROR("Command cannot accept a count"); + } else if (cmd->count.type != kObjectTypeInteger || cmd->count.data.integer < 0) { + VALIDATION_ERROR("'count' must be a non-negative Integer"); + } + set_cmd_count(&ea, cmd->count.data.integer, true); + } + + if (HAS_KEY(cmd->reg)) { + if (!(ea.argt & EX_REGSTR)) { + VALIDATION_ERROR("Command cannot accept a register"); + } else if (cmd->reg.type != kObjectTypeString || cmd->reg.data.string.size != 1) { + VALIDATION_ERROR("'reg' must be a single character"); + } + char regname = cmd->reg.data.string.data[0]; + if (regname == '=') { + VALIDATION_ERROR("Cannot use register \"="); + } else if (!valid_yank_reg(regname, ea.cmdidx != CMD_put && !IS_USER_CMDIDX(ea.cmdidx))) { + VALIDATION_ERROR("Invalid register: \"%c", regname); + } + ea.regname = (uint8_t)regname; + } + + OBJ_TO_BOOL(ea.forceit, cmd->bang, false, "'bang'"); + if (ea.forceit && !(ea.argt & EX_BANG)) { + VALIDATION_ERROR("Command cannot accept a bang"); + } + + if (HAS_KEY(cmd->magic)) { + if (cmd->magic.type != kObjectTypeDictionary) { + VALIDATION_ERROR("'magic' must be a Dictionary"); + } + + Dict(cmd_magic) magic = { 0 }; + if (!api_dict_to_keydict(&magic, KeyDict_cmd_magic_get_field, + cmd->magic.data.dictionary, err)) { + goto end; + } + + OBJ_TO_BOOL(cmdinfo.magic.file, magic.file, ea.argt & EX_XFILE, "'magic.file'"); + OBJ_TO_BOOL(cmdinfo.magic.bar, magic.bar, ea.argt & EX_TRLBAR, "'magic.bar'"); + } else { + cmdinfo.magic.file = ea.argt & EX_XFILE; + cmdinfo.magic.bar = ea.argt & EX_TRLBAR; + } + + if (HAS_KEY(cmd->mods)) { + if (cmd->mods.type != kObjectTypeDictionary) { + VALIDATION_ERROR("'mods' must be a Dictionary"); + } + + Dict(cmd_mods) mods = { 0 }; + if (!api_dict_to_keydict(&mods, KeyDict_cmd_mods_get_field, cmd->mods.data.dictionary, err)) { + goto end; + } + + if (HAS_KEY(mods.filter)) { + if (mods.filter.type != kObjectTypeDictionary) { + VALIDATION_ERROR("'mods.filter' must be a Dictionary"); + } + + Dict(cmd_mods_filter) filter = { 0 }; + + if (!api_dict_to_keydict(&filter, KeyDict_cmd_mods_filter_get_field, + mods.filter.data.dictionary, err)) { + goto end; + } + + if (HAS_KEY(filter.pattern)) { + if (filter.pattern.type != kObjectTypeString) { + VALIDATION_ERROR("'mods.filter.pattern' must be a String"); + } + + OBJ_TO_BOOL(cmdinfo.cmdmod.cmod_filter_force, filter.force, false, "'mods.filter.force'"); + + // "filter! // is not no-op, so add a filter if either the pattern is non-empty or if filter + // is inverted. + if (*filter.pattern.data.string.data != NUL || cmdinfo.cmdmod.cmod_filter_force) { + cmdinfo.cmdmod.cmod_filter_pat = string_to_cstr(filter.pattern.data.string); + cmdinfo.cmdmod.cmod_filter_regmatch.regprog = vim_regcomp(cmdinfo.cmdmod.cmod_filter_pat, + RE_MAGIC); + } + } + } + + if (HAS_KEY(mods.tab)) { + if (mods.tab.type != kObjectTypeInteger || mods.tab.data.integer < 0) { + VALIDATION_ERROR("'mods.tab' must be a non-negative Integer"); + } + cmdinfo.cmdmod.cmod_tab = (int)mods.tab.data.integer + 1; + } + + if (HAS_KEY(mods.verbose)) { + if (mods.verbose.type != kObjectTypeInteger) { + VALIDATION_ERROR("'mods.verbose' must be a Integer"); + } else if ((int)mods.verbose.data.integer >= 0) { + // Silently ignore negative integers to allow mods.verbose to be set to -1. + cmdinfo.cmdmod.cmod_verbose = (int)mods.verbose.data.integer + 1; + } + } + + bool vertical; + OBJ_TO_BOOL(vertical, mods.vertical, false, "'mods.vertical'"); + cmdinfo.cmdmod.cmod_split |= (vertical ? WSP_VERT : 0); + + if (HAS_KEY(mods.split)) { + if (mods.split.type != kObjectTypeString) { + VALIDATION_ERROR("'mods.split' must be a String"); + } + + if (*mods.split.data.string.data == NUL) { + // Empty string, do nothing. + } else if (STRCMP(mods.split.data.string.data, "aboveleft") == 0 + || STRCMP(mods.split.data.string.data, "leftabove") == 0) { + cmdinfo.cmdmod.cmod_split |= WSP_ABOVE; + } else if (STRCMP(mods.split.data.string.data, "belowright") == 0 + || STRCMP(mods.split.data.string.data, "rightbelow") == 0) { + cmdinfo.cmdmod.cmod_split |= WSP_BELOW; + } else if (STRCMP(mods.split.data.string.data, "topleft") == 0) { + cmdinfo.cmdmod.cmod_split |= WSP_TOP; + } else if (STRCMP(mods.split.data.string.data, "botright") == 0) { + cmdinfo.cmdmod.cmod_split |= WSP_BOT; + } else { + VALIDATION_ERROR("Invalid value for 'mods.split'"); + } + } + + OBJ_TO_CMOD_FLAG(CMOD_SILENT, mods.silent, false, "'mods.silent'"); + OBJ_TO_CMOD_FLAG(CMOD_ERRSILENT, mods.emsg_silent, false, "'mods.emsg_silent'"); + OBJ_TO_CMOD_FLAG(CMOD_UNSILENT, mods.silent, false, "'mods.unsilent'"); + OBJ_TO_CMOD_FLAG(CMOD_SANDBOX, mods.sandbox, false, "'mods.sandbox'"); + OBJ_TO_CMOD_FLAG(CMOD_NOAUTOCMD, mods.noautocmd, false, "'mods.noautocmd'"); + OBJ_TO_CMOD_FLAG(CMOD_BROWSE, mods.browse, false, "'mods.browse'"); + OBJ_TO_CMOD_FLAG(CMOD_CONFIRM, mods.confirm, false, "'mods.confirm'"); + OBJ_TO_CMOD_FLAG(CMOD_HIDE, mods.hide, false, "'mods.hide'"); + OBJ_TO_CMOD_FLAG(CMOD_KEEPALT, mods.keepalt, false, "'mods.keepalt'"); + OBJ_TO_CMOD_FLAG(CMOD_KEEPJUMPS, mods.keepjumps, false, "'mods.keepjumps'"); + OBJ_TO_CMOD_FLAG(CMOD_KEEPMARKS, mods.keepmarks, false, "'mods.keepmarks'"); + OBJ_TO_CMOD_FLAG(CMOD_KEEPPATTERNS, mods.keeppatterns, false, "'mods.keeppatterns'"); + OBJ_TO_CMOD_FLAG(CMOD_LOCKMARKS, mods.lockmarks, false, "'mods.lockmarks'"); + OBJ_TO_CMOD_FLAG(CMOD_NOSWAPFILE, mods.noswapfile, false, "'mods.noswapfile'"); + + if ((cmdinfo.cmdmod.cmod_flags & CMOD_SANDBOX) && !(ea.argt & EX_SBOXOK)) { + VALIDATION_ERROR("Command cannot be run in sandbox"); + } + } + + // Finally, build the command line string that will be stored inside ea.cmdlinep. + // This also sets the values of ea.cmd, ea.arg, ea.args and ea.arglens. + build_cmdline_str(&cmdline, &ea, &cmdinfo, args, argc); + ea.cmdlinep = &cmdline; + + garray_T capture_local; + const int save_msg_silent = msg_silent; + garray_T * const save_capture_ga = capture_ga; + + if (output) { + ga_init(&capture_local, 1, 80); + capture_ga = &capture_local; + } + + TRY_WRAP({ + try_start(); + if (output) { + msg_silent++; + } + + WITH_SCRIPT_CONTEXT(channel_id, { + execute_cmd(&ea, &cmdinfo, false); + }); + + if (output) { + capture_ga = save_capture_ga; + msg_silent = save_msg_silent; + } + + try_end(err); + }); + + if (ERROR_SET(err)) { + goto clear_ga; + } + + if (output && capture_local.ga_len > 1) { + retv = (String){ + .data = capture_local.ga_data, + .size = (size_t)capture_local.ga_len, + }; + // redir usually (except :echon) prepends a newline. + if (retv.data[0] == '\n') { + memmove(retv.data, retv.data + 1, retv.size - 1); + retv.data[retv.size - 1] = '\0'; + retv.size = retv.size - 1; + } + goto end; + } +clear_ga: + if (output) { + ga_clear(&capture_local); + } +end: + xfree(cmdline); + xfree(cmdname); + xfree(ea.args); + xfree(ea.arglens); + for (size_t i = 0; i < argc; i++) { + xfree(args[i]); + } + xfree(args); + + return retv; + +#undef OBJ_TO_BOOL +#undef OBJ_TO_CMOD_FLAG +#undef VALIDATION_ERROR +} + +/// Check if a string contains only whitespace characters. +static bool string_iswhite(String str) +{ + for (size_t i = 0; i < str.size; i++) { + if (!ascii_iswhite(str.data[i])) { + // Found a non-whitespace character + return false; + } else if (str.data[i] == NUL) { + // Terminate at first occurrence of a NUL character + break; + } + } + return true; +} + +/// Build cmdline string for command, used by `nvim_cmd()`. +/// +/// @return OK or FAIL. +static void build_cmdline_str(char **cmdlinep, exarg_T *eap, CmdParseInfo *cmdinfo, char **args, + size_t argc) +{ + StringBuilder cmdline = KV_INITIAL_VALUE; + + // Add command modifiers + if (cmdinfo->cmdmod.cmod_tab != 0) { + kv_printf(cmdline, "%dtab ", cmdinfo->cmdmod.cmod_tab - 1); + } + if (cmdinfo->cmdmod.cmod_verbose > 0) { + kv_printf(cmdline, "%dverbose ", cmdinfo->cmdmod.cmod_verbose - 1); + } + + if (cmdinfo->cmdmod.cmod_flags & CMOD_ERRSILENT) { + kv_concat(cmdline, "silent! "); + } else if (cmdinfo->cmdmod.cmod_flags & CMOD_SILENT) { + kv_concat(cmdline, "silent "); + } + + if (cmdinfo->cmdmod.cmod_flags & CMOD_UNSILENT) { + kv_concat(cmdline, "unsilent "); + } + + switch (cmdinfo->cmdmod.cmod_split & (WSP_ABOVE | WSP_BELOW | WSP_TOP | WSP_BOT)) { + case WSP_ABOVE: + kv_concat(cmdline, "aboveleft "); + break; + case WSP_BELOW: + kv_concat(cmdline, "belowright "); + break; + case WSP_TOP: + kv_concat(cmdline, "topleft "); + break; + case WSP_BOT: + kv_concat(cmdline, "botright "); + break; + default: + break; + } + +#define CMDLINE_APPEND_IF(cond, str) \ + do { \ + if (cond) { \ + kv_concat(cmdline, str); \ + } \ + } while (0) + + CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_split & WSP_VERT, "vertical "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_SANDBOX, "sandbox "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_NOAUTOCMD, "noautocmd "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_BROWSE, "browse "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_CONFIRM, "confirm "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_HIDE, "hide "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_KEEPALT, "keepalt "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_KEEPJUMPS, "keepjumps "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_KEEPMARKS, "keepmarks "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_KEEPPATTERNS, "keeppatterns "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_LOCKMARKS, "lockmarks "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.cmod_flags & CMOD_NOSWAPFILE, "noswapfile "); +#undef CMDLINE_APPEND_IF + + // Command range / count. + if (eap->argt & EX_RANGE) { + if (eap->addr_count == 1) { + kv_printf(cmdline, "%" PRIdLINENR, eap->line2); + } else if (eap->addr_count > 1) { + kv_printf(cmdline, "%" PRIdLINENR ",%" PRIdLINENR, eap->line1, eap->line2); + eap->addr_count = 2; // Make sure address count is not greater than 2 + } + } + + // Keep the index of the position where command name starts, so eap->cmd can point to it. + size_t cmdname_idx = cmdline.size; + kv_printf(cmdline, "%s", eap->cmd); + + // Command bang. + if (eap->argt & EX_BANG && eap->forceit) { + kv_printf(cmdline, "!"); + } + + // Command register. + if (eap->argt & EX_REGSTR && eap->regname) { + kv_printf(cmdline, " %c", eap->regname); + } + + // Iterate through each argument and store the starting index and length of each argument + size_t *argidx = xcalloc(argc, sizeof(size_t)); + eap->argc = argc; + eap->arglens = xcalloc(argc, sizeof(size_t)); + for (size_t i = 0; i < argc; i++) { + argidx[i] = cmdline.size + 1; // add 1 to account for the space. + eap->arglens[i] = STRLEN(args[i]); + kv_printf(cmdline, " %s", args[i]); + } + + // Now that all the arguments are appended, use the command index and argument indices to set the + // values of eap->cmd, eap->arg and eap->args. + eap->cmd = cmdline.items + cmdname_idx; + eap->args = xcalloc(argc, sizeof(char *)); + for (size_t i = 0; i < argc; i++) { + eap->args[i] = cmdline.items + argidx[i]; + } + // If there isn't an argument, make eap->arg point to end of cmdline. + eap->arg = argc > 0 ? eap->args[0] : cmdline.items + cmdline.size; + + // Finally, make cmdlinep point to the cmdline string. + *cmdlinep = cmdline.items; + xfree(argidx); + + // Replace, :make and :grep with 'makeprg' and 'grepprg'. + char *p = replace_makeprg(eap, eap->arg, cmdlinep); + if (p != eap->arg) { + // If replace_makeprg modified the cmdline string, correct the argument pointers. + assert(argc == 1); + eap->arg = p; + eap->args[0] = p; + } +} + +/// Create a new user command |user-commands| +/// +/// {name} is the name of the new command. The name must begin with an uppercase letter. +/// +/// {command} is the replacement text or Lua function to execute. +/// +/// Example: +/// <pre> +/// :call nvim_create_user_command('SayHello', 'echo "Hello world!"', {}) +/// :SayHello +/// Hello world! +/// </pre> +/// +/// @param name Name of the new user command. Must begin with an uppercase letter. +/// @param command Replacement command to execute when this user command is executed. When called +/// from Lua, the command can also be a Lua function. The function is called with a +/// single table argument that contains the following keys: +/// - args: (string) The args passed to the command, if any |<args>| +/// - fargs: (table) The args split by unescaped whitespace (when more than one +/// argument is allowed), if any |<f-args>| +/// - bang: (boolean) "true" if the command was executed with a ! modifier |<bang>| +/// - line1: (number) The starting line of the command range |<line1>| +/// - line2: (number) The final line of the command range |<line2>| +/// - range: (number) The number of items in the command range: 0, 1, or 2 |<range>| +/// - count: (number) Any count supplied |<count>| +/// - reg: (string) The optional register, if specified |<reg>| +/// - mods: (string) Command modifiers, if any |<mods>| +/// - smods: (table) Command modifiers in a structured format. Has the same +/// structure as the "mods" key of |nvim_parse_cmd()|. +/// @param opts Optional command attributes. See |command-attributes| for more details. To use +/// boolean attributes (such as |:command-bang| or |:command-bar|) set the value to +/// "true". In addition to the string options listed in |:command-complete|, the +/// "complete" key also accepts a Lua function which works like the "customlist" +/// completion mode |:command-completion-customlist|. Additional parameters: +/// - desc: (string) Used for listing the command when a Lua function is used for +/// {command}. +/// - force: (boolean, default true) Override any previous definition. +/// - preview: (function) Preview callback for 'inccommand' |:command-preview| +/// @param[out] err Error details, if any. +void nvim_create_user_command(String name, Object command, Dict(user_command) *opts, Error *err) + FUNC_API_SINCE(9) +{ + create_user_command(name, command, opts, 0, err); +} + +/// Delete a user-defined command. +/// +/// @param name Name of the command to delete. +/// @param[out] err Error details, if any. +void nvim_del_user_command(String name, Error *err) + FUNC_API_SINCE(9) +{ + nvim_buf_del_user_command(-1, name, err); +} + +/// Create a new user command |user-commands| in the given buffer. +/// +/// @param buffer Buffer handle, or 0 for current buffer. +/// @param[out] err Error details, if any. +/// @see nvim_create_user_command +void nvim_buf_create_user_command(Buffer buffer, String name, Object command, + Dict(user_command) *opts, Error *err) + FUNC_API_SINCE(9) +{ + buf_T *target_buf = find_buffer_by_handle(buffer, err); + if (ERROR_SET(err)) { + return; + } + + buf_T *save_curbuf = curbuf; + curbuf = target_buf; + create_user_command(name, command, opts, UC_BUFFER, err); + curbuf = save_curbuf; +} + +/// Delete a buffer-local user-defined command. +/// +/// Only commands created with |:command-buffer| or +/// |nvim_buf_create_user_command()| can be deleted with this function. +/// +/// @param buffer Buffer handle, or 0 for current buffer. +/// @param name Name of the command to delete. +/// @param[out] err Error details, if any. +void nvim_buf_del_user_command(Buffer buffer, String name, Error *err) + FUNC_API_SINCE(9) +{ + garray_T *gap; + if (buffer == -1) { + gap = &ucmds; + } else { + buf_T *buf = find_buffer_by_handle(buffer, err); + gap = &buf->b_ucmds; + } + + for (int i = 0; i < gap->ga_len; i++) { + ucmd_T *cmd = USER_CMD_GA(gap, i); + if (!STRCMP(name.data, cmd->uc_name)) { + free_ucmd(cmd); + + gap->ga_len -= 1; + + if (i < gap->ga_len) { + memmove(cmd, cmd + 1, (size_t)(gap->ga_len - i) * sizeof(ucmd_T)); + } + + return; + } + } + + api_set_error(err, kErrorTypeException, "No such user-defined command: %s", name.data); +} + +void create_user_command(String name, Object command, Dict(user_command) *opts, int flags, + Error *err) +{ + uint32_t argt = 0; + long def = -1; + cmd_addr_T addr_type_arg = ADDR_NONE; + int compl = EXPAND_NOTHING; + char *compl_arg = NULL; + char *rep = NULL; + LuaRef luaref = LUA_NOREF; + LuaRef compl_luaref = LUA_NOREF; + LuaRef preview_luaref = LUA_NOREF; + + if (!uc_validate_name(name.data)) { + api_set_error(err, kErrorTypeValidation, "Invalid command name"); + goto err; + } + + if (mb_islower(name.data[0])) { + api_set_error(err, kErrorTypeValidation, "'name' must begin with an uppercase letter"); + goto err; + } + + if (HAS_KEY(opts->range) && HAS_KEY(opts->count)) { + api_set_error(err, kErrorTypeValidation, "'range' and 'count' are mutually exclusive"); + goto err; + } + + if (opts->nargs.type == kObjectTypeInteger) { + switch (opts->nargs.data.integer) { + case 0: + // Default value, nothing to do + break; + case 1: + argt |= EX_EXTRA | EX_NOSPC | EX_NEEDARG; + break; + default: + api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); + goto err; + } + } else if (opts->nargs.type == kObjectTypeString) { + if (opts->nargs.data.string.size > 1) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); + goto err; + } + + switch (opts->nargs.data.string.data[0]) { + case '*': + argt |= EX_EXTRA; + break; + case '?': + argt |= EX_EXTRA | EX_NOSPC; + break; + case '+': + argt |= EX_EXTRA | EX_NEEDARG; + break; + default: + api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); + goto err; + } + } else if (HAS_KEY(opts->nargs)) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); + goto err; + } + + if (HAS_KEY(opts->complete) && !argt) { + api_set_error(err, kErrorTypeValidation, "'complete' used without 'nargs'"); + goto err; + } + + if (opts->range.type == kObjectTypeBoolean) { + if (opts->range.data.boolean) { + argt |= EX_RANGE; + addr_type_arg = ADDR_LINES; + } + } else if (opts->range.type == kObjectTypeString) { + if (opts->range.data.string.data[0] == '%' && opts->range.data.string.size == 1) { + argt |= EX_RANGE | EX_DFLALL; + addr_type_arg = ADDR_LINES; + } else { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'range'"); + goto err; + } + } else if (opts->range.type == kObjectTypeInteger) { + argt |= EX_RANGE | EX_ZEROR; + def = opts->range.data.integer; + addr_type_arg = ADDR_LINES; + } else if (HAS_KEY(opts->range)) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'range'"); + goto err; + } + + if (opts->count.type == kObjectTypeBoolean) { + if (opts->count.data.boolean) { + argt |= EX_COUNT | EX_ZEROR | EX_RANGE; + addr_type_arg = ADDR_OTHER; + def = 0; + } + } else if (opts->count.type == kObjectTypeInteger) { + argt |= EX_COUNT | EX_ZEROR | EX_RANGE; + addr_type_arg = ADDR_OTHER; + def = opts->count.data.integer; + } else if (HAS_KEY(opts->count)) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'count'"); + goto err; + } + + if (opts->addr.type == kObjectTypeString) { + if (parse_addr_type_arg(opts->addr.data.string.data, (int)opts->addr.data.string.size, + &addr_type_arg) != OK) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'addr'"); + goto err; + } + + if (addr_type_arg != ADDR_LINES) { + argt |= EX_ZEROR; + } + } else if (HAS_KEY(opts->addr)) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'addr'"); + goto err; + } + + if (api_object_to_bool(opts->bang, "bang", false, err)) { + argt |= EX_BANG; + } else if (ERROR_SET(err)) { + goto err; + } + + if (api_object_to_bool(opts->bar, "bar", false, err)) { + argt |= EX_TRLBAR; + } else if (ERROR_SET(err)) { + goto err; + } + + if (api_object_to_bool(opts->register_, "register", false, err)) { + argt |= EX_REGSTR; + } else if (ERROR_SET(err)) { + goto err; + } + + if (api_object_to_bool(opts->keepscript, "keepscript", false, err)) { + argt |= EX_KEEPSCRIPT; + } else if (ERROR_SET(err)) { + goto err; + } + + bool force = api_object_to_bool(opts->force, "force", true, err); + if (ERROR_SET(err)) { + goto err; + } + + if (opts->complete.type == kObjectTypeLuaRef) { + compl = EXPAND_USER_LUA; + compl_luaref = api_new_luaref(opts->complete.data.luaref); + } else if (opts->complete.type == kObjectTypeString) { + if (parse_compl_arg(opts->complete.data.string.data, + (int)opts->complete.data.string.size, &compl, &argt, + &compl_arg) != OK) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'complete'"); + goto err; + } + } else if (HAS_KEY(opts->complete)) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'complete'"); + goto err; + } + + if (opts->preview.type == kObjectTypeLuaRef) { + argt |= EX_PREVIEW; + preview_luaref = api_new_luaref(opts->preview.data.luaref); + } else if (HAS_KEY(opts->preview)) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'preview'"); + goto err; + } + + switch (command.type) { + case kObjectTypeLuaRef: + luaref = api_new_luaref(command.data.luaref); + if (opts->desc.type == kObjectTypeString) { + rep = opts->desc.data.string.data; + } else { + snprintf((char *)IObuff, IOSIZE, "<Lua function %d>", luaref); + rep = (char *)IObuff; + } + break; + case kObjectTypeString: + rep = command.data.string.data; + break; + default: + api_set_error(err, kErrorTypeValidation, "'command' must be a string or Lua function"); + goto err; + } + + if (uc_add_command(name.data, name.size, rep, argt, def, flags, compl, compl_arg, compl_luaref, + preview_luaref, addr_type_arg, luaref, force) != OK) { + api_set_error(err, kErrorTypeException, "Failed to create user command"); + // Do not goto err, since uc_add_command now owns luaref, compl_luaref, and compl_arg + } + + return; + +err: + NLUA_CLEAR_REF(luaref); + NLUA_CLEAR_REF(compl_luaref); + xfree(compl_arg); +} +/// Gets a map of global (non-buffer-local) Ex commands. +/// +/// Currently only |user-commands| are supported, not builtin Ex commands. +/// +/// @param opts Optional parameters. Currently only supports +/// {"builtin":false} +/// @param[out] err Error details, if any. +/// +/// @returns Map of maps describing commands. +Dictionary nvim_get_commands(Dict(get_commands) *opts, Error *err) + FUNC_API_SINCE(4) +{ + return nvim_buf_get_commands(-1, opts, err); +} + +/// Gets a map of buffer-local |user-commands|. +/// +/// @param buffer Buffer handle, or 0 for current buffer +/// @param opts Optional parameters. Currently not used. +/// @param[out] err Error details, if any. +/// +/// @returns Map of maps describing commands. +Dictionary nvim_buf_get_commands(Buffer buffer, Dict(get_commands) *opts, Error *err) + FUNC_API_SINCE(4) +{ + bool global = (buffer == -1); + bool builtin = api_object_to_bool(opts->builtin, "builtin", false, err); + if (ERROR_SET(err)) { + return (Dictionary)ARRAY_DICT_INIT; + } + + if (global) { + if (builtin) { + api_set_error(err, kErrorTypeValidation, "builtin=true not implemented"); + return (Dictionary)ARRAY_DICT_INIT; + } + return commands_array(NULL); + } + + buf_T *buf = find_buffer_by_handle(buffer, err); + if (builtin || !buf) { + return (Dictionary)ARRAY_DICT_INIT; + } + return commands_array(buf); +} diff --git a/src/nvim/api/command.h b/src/nvim/api/command.h new file mode 100644 index 0000000000..b1c9230551 --- /dev/null +++ b/src/nvim/api/command.h @@ -0,0 +1,11 @@ +#ifndef NVIM_API_COMMAND_H +#define NVIM_API_COMMAND_H + +#include "nvim/api/private/defs.h" +#include "nvim/decoration.h" +#include "nvim/ex_cmds.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "api/command.h.generated.h" +#endif +#endif // NVIM_API_COMMAND_H diff --git a/src/nvim/api/deprecated.c b/src/nvim/api/deprecated.c index 76b699800e..abaac07755 100644 --- a/src/nvim/api/deprecated.c +++ b/src/nvim/api/deprecated.c @@ -22,11 +22,11 @@ /// @deprecated /// @see nvim_exec -String nvim_command_output(String command, Error *err) +String nvim_command_output(uint64_t channel_id, String command, Error *err) FUNC_API_SINCE(1) FUNC_API_DEPRECATED_SINCE(7) { - return nvim_exec(command, true, err); + return nvim_exec(channel_id, command, true, err); } /// @deprecated Use nvim_exec_lua() instead. @@ -51,11 +51,10 @@ Integer nvim_buf_get_number(Buffer buffer, Error *err) FUNC_API_SINCE(1) FUNC_API_DEPRECATED_SINCE(2) { - Integer rv = 0; buf_T *buf = find_buffer_by_handle(buffer, err); if (!buf) { - return rv; + return 0; } return buf->b_fnum; @@ -79,7 +78,6 @@ void nvim_buf_clear_highlight(Buffer buffer, Integer ns_id, Integer line_start, nvim_buf_clear_namespace(buffer, ns_id, line_start, line_end, err); } - /// Set the virtual text (annotation) for a buffer line. /// /// @deprecated use nvim_buf_set_extmark to use full virtual text @@ -130,7 +128,7 @@ Integer nvim_buf_set_virtual_text(Buffer buffer, Integer src_id, Integer line, A return 0; } - uint64_t ns_id = src2ns(&src_id); + uint32_t ns_id = src2ns(&src_id); int width; VirtText virt_text = parse_virt_text(chunks, err, &width); @@ -138,7 +136,6 @@ Integer nvim_buf_set_virtual_text(Buffer buffer, Integer src_id, Integer line, A return 0; } - Decoration *existing = decor_find_virttext(buf, (int)line, ns_id); if (existing) { @@ -148,11 +145,12 @@ Integer nvim_buf_set_virtual_text(Buffer buffer, Integer src_id, Integer line, A return src_id; } - Decoration *decor = xcalloc(1, sizeof(*decor)); - decor->virt_text = virt_text; - decor->virt_text_width = width; + Decoration decor = DECORATION_INIT; + decor.virt_text = virt_text; + decor.virt_text_width = width; + decor.priority = 0; - extmark_set(buf, ns_id, NULL, (int)line, 0, -1, -1, decor, true, + extmark_set(buf, ns_id, NULL, (int)line, 0, -1, -1, &decor, true, false, kExtmarkNoUndo); return src_id; } @@ -192,7 +190,7 @@ String buffer_get_line(Buffer buffer, Integer index, Error *err) String rv = { .size = 0 }; index = convert_index(index); - Array slice = nvim_buf_get_lines(0, buffer, index, index+1, true, err); + Array slice = nvim_buf_get_lines(0, buffer, index, index + 1, true, err); if (!ERROR_SET(err) && slice.size) { rv = slice.items[0].data.string; @@ -221,7 +219,7 @@ void buffer_set_line(Buffer buffer, Integer index, String line, Error *err) Object l = STRING_OBJ(line); Array array = { .items = &l, .size = 1 }; index = convert_index(index); - nvim_buf_set_lines(0, buffer, index, index+1, true, array, err); + nvim_buf_set_lines(0, buffer, index, index + 1, true, array, err); } /// Deletes a buffer line @@ -239,7 +237,7 @@ void buffer_del_line(Buffer buffer, Integer index, Error *err) { Array array = ARRAY_DICT_INIT; index = convert_index(index); - nvim_buf_set_lines(0, buffer, index, index+1, true, array, err); + nvim_buf_set_lines(0, buffer, index, index + 1, true, array, err); } /// Retrieves a line range from the buffer @@ -292,7 +290,6 @@ void buffer_set_line_slice(Buffer buffer, Integer start, Integer end, Boolean in nvim_buf_set_lines(0, buffer, start, end, false, replacement, err); } - /// Sets a buffer-scoped (b:) variable /// /// @deprecated diff --git a/src/nvim/api/extmark.c b/src/nvim/api/extmark.c index 742b953c2a..da1b6beeda 100644 --- a/src/nvim/api/extmark.c +++ b/src/nvim/api/extmark.c @@ -8,11 +8,13 @@ #include "nvim/api/extmark.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/helpers.h" +#include "nvim/charset.h" +#include "nvim/decoration_provider.h" #include "nvim/extmark.h" +#include "nvim/highlight_group.h" #include "nvim/lua/executor.h" #include "nvim/memline.h" #include "nvim/screen.h" -#include "nvim/syntax.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/extmark.c.generated.h" @@ -85,43 +87,79 @@ const char *describe_ns(NS ns_id) } // Is the Namespace in use? -static bool ns_initialized(uint64_t ns) +static bool ns_initialized(uint32_t ns) { if (ns < 1) { return false; } - return ns < (uint64_t)next_namespace_id; + return ns < (uint32_t)next_namespace_id; } - -static Array extmark_to_array(ExtmarkInfo extmark, bool id, bool add_dict) +static Array extmark_to_array(const ExtmarkInfo *extmark, bool id, bool add_dict) { Array rv = ARRAY_DICT_INIT; if (id) { - ADD(rv, INTEGER_OBJ((Integer)extmark.mark_id)); + ADD(rv, INTEGER_OBJ((Integer)extmark->mark_id)); } - ADD(rv, INTEGER_OBJ(extmark.row)); - ADD(rv, INTEGER_OBJ(extmark.col)); + ADD(rv, INTEGER_OBJ(extmark->row)); + ADD(rv, INTEGER_OBJ(extmark->col)); if (add_dict) { Dictionary dict = ARRAY_DICT_INIT; - if (extmark.end_row >= 0) { - PUT(dict, "end_row", INTEGER_OBJ(extmark.end_row)); - PUT(dict, "end_col", INTEGER_OBJ(extmark.end_col)); + PUT(dict, "right_gravity", BOOLEAN_OBJ(extmark->right_gravity)); + + if (extmark->end_row >= 0) { + PUT(dict, "end_row", INTEGER_OBJ(extmark->end_row)); + PUT(dict, "end_col", INTEGER_OBJ(extmark->end_col)); + PUT(dict, "end_right_gravity", BOOLEAN_OBJ(extmark->end_right_gravity)); + } + + const Decoration *decor = &extmark->decor; + if (decor->hl_id) { + String name = cstr_to_string((const char *)syn_id2name(decor->hl_id)); + PUT(dict, "hl_group", STRING_OBJ(name)); + PUT(dict, "hl_eol", BOOLEAN_OBJ(decor->hl_eol)); + } + if (decor->hl_mode) { + PUT(dict, "hl_mode", STRING_OBJ(cstr_to_string(hl_mode_str[decor->hl_mode]))); } - if (extmark.decor) { - Decoration *decor = extmark.decor; - if (decor->hl_id) { - String name = cstr_to_string((const char *)syn_id2name(decor->hl_id)); - PUT(dict, "hl_group", STRING_OBJ(name)); + if (kv_size(decor->virt_text)) { + Array chunks = ARRAY_DICT_INIT; + for (size_t i = 0; i < decor->virt_text.size; i++) { + Array chunk = ARRAY_DICT_INIT; + VirtTextChunk *vtc = &decor->virt_text.items[i]; + ADD(chunk, STRING_OBJ(cstr_to_string(vtc->text))); + if (vtc->hl_id > 0) { + ADD(chunk, + STRING_OBJ(cstr_to_string((const char *)syn_id2name(vtc->hl_id)))); + } + ADD(chunks, ARRAY_OBJ(chunk)); } - if (kv_size(decor->virt_text)) { + PUT(dict, "virt_text", ARRAY_OBJ(chunks)); + PUT(dict, "virt_text_hide", BOOLEAN_OBJ(decor->virt_text_hide)); + if (decor->virt_text_pos == kVTWinCol) { + PUT(dict, "virt_text_win_col", INTEGER_OBJ(decor->col)); + } + PUT(dict, "virt_text_pos", + STRING_OBJ(cstr_to_string(virt_text_pos_str[decor->virt_text_pos]))); + } + + if (decor->ui_watched) { + PUT(dict, "ui_watched", BOOLEAN_OBJ(true)); + } + + if (kv_size(decor->virt_lines)) { + Array all_chunks = ARRAY_DICT_INIT; + bool virt_lines_leftcol = false; + for (size_t i = 0; i < decor->virt_lines.size; i++) { Array chunks = ARRAY_DICT_INIT; - for (size_t i = 0; i < decor->virt_text.size; i++) { + VirtText *vt = &decor->virt_lines.items[i].line; + virt_lines_leftcol = decor->virt_lines.items[i].left_col; + for (size_t j = 0; j < vt->size; j++) { Array chunk = ARRAY_DICT_INIT; - VirtTextChunk *vtc = &decor->virt_text.items[i]; + VirtTextChunk *vtc = &vt->items[j]; ADD(chunk, STRING_OBJ(cstr_to_string(vtc->text))); if (vtc->hl_id > 0) { ADD(chunk, @@ -129,9 +167,14 @@ static Array extmark_to_array(ExtmarkInfo extmark, bool id, bool add_dict) } ADD(chunks, ARRAY_OBJ(chunk)); } - PUT(dict, "virt_text", ARRAY_OBJ(chunks)); + ADD(all_chunks, ARRAY_OBJ(chunks)); } + PUT(dict, "virt_lines", ARRAY_OBJ(all_chunks)); + PUT(dict, "virt_lines_above", BOOLEAN_OBJ(decor->virt_lines_above)); + PUT(dict, "virt_lines_leftcol", BOOLEAN_OBJ(virt_lines_leftcol)); + } + if (decor->hl_id || kv_size(decor->virt_text) || decor->ui_watched) { PUT(dict, "priority", INTEGER_OBJ(decor->priority)); } @@ -166,7 +209,7 @@ ArrayOf(Integer) nvim_buf_get_extmark_by_id(Buffer buffer, Integer ns_id, return rv; } - if (!ns_initialized((uint64_t)ns_id)) { + if (!ns_initialized((uint32_t)ns_id)) { api_set_error(err, kErrorTypeValidation, "Invalid ns_id"); return rv; } @@ -190,12 +233,11 @@ ArrayOf(Integer) nvim_buf_get_extmark_by_id(Buffer buffer, Integer ns_id, } } - - ExtmarkInfo extmark = extmark_from_id(buf, (uint64_t)ns_id, (uint64_t)id); + ExtmarkInfo extmark = extmark_from_id(buf, (uint32_t)ns_id, (uint32_t)id); if (extmark.row < 0) { return rv; } - return extmark_to_array(extmark, false, details); + return extmark_to_array(&extmark, false, details); } /// Gets extmarks in "traversal order" from a |charwise| region defined by @@ -220,9 +262,9 @@ ArrayOf(Integer) nvim_buf_get_extmark_by_id(Buffer buffer, Integer ns_id, /// local pos = a.nvim_win_get_cursor(0) /// local ns = a.nvim_create_namespace('my-plugin') /// -- Create new extmark at line 1, column 1. -/// local m1 = a.nvim_buf_set_extmark(0, ns, 0, 0, 0, {}) +/// local m1 = a.nvim_buf_set_extmark(0, ns, 0, 0, {}) /// -- Create new extmark at line 3, column 1. -/// local m2 = a.nvim_buf_set_extmark(0, ns, 0, 2, 0, {}) +/// local m2 = a.nvim_buf_set_extmark(0, ns, 0, 2, {}) /// -- Get extmarks only from line 3. /// local ms = a.nvim_buf_get_extmarks(0, ns, {2,0}, {2,0}, {}) /// -- Get all marks in this buffer + namespace. @@ -252,7 +294,7 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e return rv; } - if (!ns_initialized((uint64_t)ns_id)) { + if (!ns_initialized((uint32_t)ns_id)) { api_set_error(err, kErrorTypeValidation, "Invalid ns_id"); return rv; } @@ -290,7 +332,6 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e limit = INT64_MAX; } - bool reverse = false; int l_row; @@ -309,12 +350,11 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e reverse = true; } - - ExtmarkInfoArray marks = extmark_get(buf, (uint64_t)ns_id, l_row, l_col, + ExtmarkInfoArray marks = extmark_get(buf, (uint32_t)ns_id, l_row, l_col, u_row, u_col, (int64_t)limit, reverse); for (size_t i = 0; i < kv_size(marks); i++) { - ADD(rv, ARRAY_OBJ(extmark_to_array(kv_A(marks, i), true, (bool)details))); + ADD(rv, ARRAY_OBJ(extmark_to_array(&kv_A(marks, i), true, (bool)details))); } kv_destroy(marks); @@ -323,12 +363,11 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e /// Creates or updates an extmark. /// -/// To create a new extmark, pass id=0. The extmark id will be returned. -/// To move an existing mark, pass its id. -/// -/// It is also allowed to create a new mark by passing in a previously unused -/// id, but the caller must then keep track of existing and unused ids itself. -/// (Useful over RPC, to avoid waiting for the return value.) +/// By default a new extmark is created when no id is passed in, but it is also +/// possible to create a new mark by passing in a previously unused id or move +/// an existing mark by passing in its id. The caller must then keep track of +/// existing and unused ids itself. (Useful over RPC, to avoid waiting for the +/// return value.) /// /// Using the optional arguments, it is possible to use this to highlight /// a range of text, and also to associate virtual text to the mark. @@ -404,6 +443,40 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e /// for left). Defaults to false. /// - priority: a priority value for the highlight group. For /// example treesitter highlighting uses a value of 100. +/// - strict: boolean that indicates extmark should not be placed +/// if the line or column value is past the end of the +/// buffer or end of the line respectively. Defaults to true. +/// - sign_text: string of length 1-2 used to display in the +/// sign column. +/// Note: ranges are unsupported and decorations are only +/// applied to start_row +/// - sign_hl_group: name of the highlight group used to +/// highlight the sign column text. +/// Note: ranges are unsupported and decorations are only +/// applied to start_row +/// - number_hl_group: name of the highlight group used to +/// highlight the number column. +/// Note: ranges are unsupported and decorations are only +/// applied to start_row +/// - line_hl_group: name of the highlight group used to +/// highlight the whole line. +/// Note: ranges are unsupported and decorations are only +/// applied to start_row +/// - cursorline_hl_group: name of the highlight group used to +/// highlight the line when the cursor is on the same line +/// as the mark and 'cursorline' is enabled. +/// Note: ranges are unsupported and decorations are only +/// applied to start_row +/// - conceal: string which should be either empty or a single +/// character. Enable concealing similar to |:syn-conceal|. +/// When a character is supplied it is used as |:syn-cchar|. +/// "hl_group" is used as highlight for the cchar if provided, +/// otherwise it defaults to |hl-Conceal|. +/// - ui_watched: boolean that indicates the mark should be drawn +/// by a UI. When set, the UI will receive win_extmark events. +/// Note: the mark is positioned by virt_text attributes. Can be +/// used together with virt_text. +/// /// @param[out] err Error details, if any /// @return Id of the created/updated extmark Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer col, @@ -411,20 +484,21 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer FUNC_API_SINCE(7) { Decoration decor = DECORATION_INIT; + bool has_decor = false; buf_T *buf = find_buffer_by_handle(buffer, err); if (!buf) { goto error; } - if (!ns_initialized((uint64_t)ns_id)) { + if (!ns_initialized((uint32_t)ns_id)) { api_set_error(err, kErrorTypeValidation, "Invalid ns_id"); goto error; } - uint64_t id = 0; + uint32_t id = 0; if (opts->id.type == kObjectTypeInteger && opts->id.data.integer > 0) { - id = (uint64_t)opts->id.data.integer; + id = (uint32_t)opts->id.data.integer; } else if (HAS_KEY(opts->id)) { api_set_error(err, kErrorTypeValidation, "id is not a positive integer"); goto error; @@ -441,9 +515,18 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer opts->end_row = opts->end_line; } +#define OPTION_TO_BOOL(target, name, val) \ + target = api_object_to_bool(opts->name, #name, val, err); \ + if (ERROR_SET(err)) { \ + goto error; \ + } + + bool strict = true; + OPTION_TO_BOOL(strict, strict, true); + if (opts->end_row.type == kObjectTypeInteger) { Integer val = opts->end_row.data.integer; - if (val < 0 || val > buf->b_ml.ml_line_count) { + if (val < 0 || (val > buf->b_ml.ml_line_count && strict)) { api_set_error(err, kErrorTypeValidation, "end_row value outside range"); goto error; } else { @@ -468,16 +551,49 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer goto error; } - if (HAS_KEY(opts->hl_group)) { - decor.hl_id = object_to_hl_id(opts->hl_group, "hl_group", err); - if (ERROR_SET(err)) { - goto error; + // uncrustify:off + + struct { + const char *name; + Object *opt; + int *dest; + } hls[] = { + { "hl_group" , &opts->hl_group , &decor.hl_id }, + { "sign_hl_group" , &opts->sign_hl_group , &decor.sign_hl_id }, + { "number_hl_group" , &opts->number_hl_group , &decor.number_hl_id }, + { "line_hl_group" , &opts->line_hl_group , &decor.line_hl_id }, + { "cursorline_hl_group", &opts->cursorline_hl_group, &decor.cursorline_hl_id }, + { NULL, NULL, NULL }, + }; + + // uncrustify:on + + for (int j = 0; hls[j].name && hls[j].dest; j++) { + if (HAS_KEY(*hls[j].opt)) { + *hls[j].dest = object_to_hl_id(*hls[j].opt, hls[j].name, err); + if (ERROR_SET(err)) { + goto error; + } + has_decor = true; } } + if (opts->conceal.type == kObjectTypeString) { + String c = opts->conceal.data.string; + decor.conceal = true; + if (c.size) { + decor.conceal_char = utf_ptr2char(c.data); + } + has_decor = true; + } else if (HAS_KEY(opts->conceal)) { + api_set_error(err, kErrorTypeValidation, "conceal is not a String"); + goto error; + } + if (opts->virt_text.type == kObjectTypeArray) { decor.virt_text = parse_virt_text(opts->virt_text.data.array, err, &decor.virt_text_width); + has_decor = true; if (ERROR_SET(err)) { goto error; } @@ -512,12 +628,6 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer goto error; } -#define OPTION_TO_BOOL(target, name, val) \ - target = api_object_to_bool(opts->name, #name, val, err); \ - if (ERROR_SET(err)) { \ - goto error; \ - } - OPTION_TO_BOOL(decor.virt_text_hide, virt_text_hide, false); OPTION_TO_BOOL(decor.hl_eol, hl_eol, false); @@ -555,13 +665,13 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer if (ERROR_SET(err)) { goto error; } + has_decor = true; } } else if (HAS_KEY(opts->virt_lines)) { api_set_error(err, kErrorTypeValidation, "virt_lines is not an Array"); goto error; } - OPTION_TO_BOOL(decor.virt_lines_above, virt_lines_above, false); if (opts->priority.type == kObjectTypeInteger) { @@ -577,6 +687,18 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer goto error; } + if (opts->sign_text.type == kObjectTypeString) { + if (!init_sign_text((char **)&decor.sign_text, + opts->sign_text.data.string.data)) { + api_set_error(err, kErrorTypeValidation, "sign_text is not a valid value"); + goto error; + } + has_decor = true; + } else if (HAS_KEY(opts->sign_text)) { + api_set_error(err, kErrorTypeValidation, "sign_text is not a String"); + goto error; + } + bool right_gravity = true; OPTION_TO_BOOL(right_gravity, right_gravity, true); @@ -596,16 +718,35 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer bool ephemeral = false; OPTION_TO_BOOL(ephemeral, ephemeral, false); - if (line < 0 || line > buf->b_ml.ml_line_count) { + OPTION_TO_BOOL(decor.ui_watched, ui_watched, false); + if (decor.ui_watched) { + has_decor = true; + } + + if (line < 0) { api_set_error(err, kErrorTypeValidation, "line value outside range"); goto error; + } else if (line > buf->b_ml.ml_line_count) { + if (strict) { + api_set_error(err, kErrorTypeValidation, "line value outside range"); + goto error; + } else { + line = buf->b_ml.ml_line_count; + } } else if (line < buf->b_ml.ml_line_count) { - len = ephemeral ? MAXCOL : STRLEN(ml_get_buf(buf, (linenr_T)line+1, false)); + len = ephemeral ? MAXCOL : STRLEN(ml_get_buf(buf, (linenr_T)line + 1, false)); } if (col == -1) { col = (Integer)len; - } else if (col < -1 || col > (Integer)len) { + } else if (col > (Integer)len) { + if (strict) { + api_set_error(err, kErrorTypeValidation, "col value outside range"); + goto error; + } else { + col = (Integer)len; + } + } else if (col < -1) { api_set_error(err, kErrorTypeValidation, "col value outside range"); goto error; } @@ -621,49 +762,36 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer line2 = (int)line; } if (col2 > (Integer)len) { - api_set_error(err, kErrorTypeValidation, "end_col value outside range"); - goto error; + if (strict) { + api_set_error(err, kErrorTypeValidation, "end_col value outside range"); + goto error; + } else { + col2 = (int)len; + } } } else if (line2 >= 0) { col2 = 0; } - Decoration *d = NULL; - - if (ephemeral) { - d = &decor; - } else if (kv_size(decor.virt_text) || kv_size(decor.virt_lines) - || decor.priority != DECOR_PRIORITY_BASE - || decor.hl_eol) { - // TODO(bfredl): this is a bit sketchy. eventually we should - // have predefined decorations for both marks/ephemerals - d = xcalloc(1, sizeof(*d)); - *d = decor; - } else if (decor.hl_id) { - d = decor_hl(decor.hl_id); - } - // TODO(bfredl): synergize these two branches even more if (ephemeral && decor_state.buf == buf) { - decor_add_ephemeral((int)line, (int)col, line2, col2, &decor); + decor_add_ephemeral((int)line, (int)col, line2, col2, &decor, (uint64_t)ns_id, id); } else { if (ephemeral) { api_set_error(err, kErrorTypeException, "not yet implemented"); goto error; } - extmark_set(buf, (uint64_t)ns_id, &id, (int)line, (colnr_T)col, line2, col2, - d, right_gravity, end_right_gravity, kExtmarkNoUndo); - - if (kv_size(decor.virt_lines)) { - redraw_buf_line_later(buf, MIN(buf->b_ml.ml_line_count, line+1+(decor.virt_lines_above?0:1))); - } + extmark_set(buf, (uint32_t)ns_id, &id, (int)line, (colnr_T)col, line2, col2, + has_decor ? &decor : NULL, right_gravity, end_right_gravity, + kExtmarkNoUndo); } return (Integer)id; error: clear_virttext(&decor.virt_text); + xfree(decor.sign_text); return 0; } @@ -682,23 +810,23 @@ Boolean nvim_buf_del_extmark(Buffer buffer, Integer ns_id, Integer id, Error *er if (!buf) { return false; } - if (!ns_initialized((uint64_t)ns_id)) { + if (!ns_initialized((uint32_t)ns_id)) { api_set_error(err, kErrorTypeValidation, "Invalid ns_id"); return false; } - return extmark_del(buf, (uint64_t)ns_id, (uint64_t)id); + return extmark_del(buf, (uint32_t)ns_id, (uint32_t)id); } -uint64_t src2ns(Integer *src_id) +uint32_t src2ns(Integer *src_id) { if (*src_id == 0) { *src_id = nvim_create_namespace((String)STRING_INIT); } if (*src_id < 0) { - return UINT64_MAX; + return (((uint32_t)1) << 31) - 1; } else { - return (uint64_t)(*src_id); + return (uint32_t)(*src_id); } } @@ -753,7 +881,7 @@ Integer nvim_buf_add_highlight(Buffer buffer, Integer ns_id, String hl_group, In col_end = MAXCOL; } - uint64_t ns = src2ns(&ns_id); + uint32_t ns = src2ns(&ns_id); if (!(line < buf->b_ml.ml_line_count)) { // safety check, we can't add marks outside the range @@ -762,7 +890,7 @@ Integer nvim_buf_add_highlight(Buffer buffer, Integer ns_id, String hl_group, In int hl_id = 0; if (hl_group.size > 0) { - hl_id = syn_check_group(hl_group.data, (int)hl_group.size); + hl_id = syn_check_group(hl_group.data, hl_group.size); } else { return ns_id; } @@ -773,10 +901,13 @@ Integer nvim_buf_add_highlight(Buffer buffer, Integer ns_id, String hl_group, In end_line++; } + Decoration decor = DECORATION_INIT; + decor.hl_id = hl_id; + extmark_set(buf, ns, NULL, (int)line, (colnr_T)col_start, end_line, (colnr_T)col_end, - decor_hl(hl_id), true, false, kExtmarkNoUndo); + &decor, true, false, kExtmarkNoUndo); return ns_id; } @@ -808,9 +939,9 @@ void nvim_buf_clear_namespace(Buffer buffer, Integer ns_id, Integer line_start, if (line_end < 0 || line_end > MAXLNUM) { line_end = MAXLNUM; } - extmark_clear(buf, (ns_id < 0 ? 0 : (uint64_t)ns_id), + extmark_clear(buf, (ns_id < 0 ? 0 : (uint32_t)ns_id), (int)line_start, 0, - (int)line_end-1, MAXCOL); + (int)line_end - 1, MAXCOL); } /// Set or change decoration provider for a namespace @@ -903,3 +1034,154 @@ void nvim_set_decoration_provider(Integer ns_id, DictionaryOf(LuaRef) opts, Erro error: decor_provider_clear(p); } + +/// Gets the line and column of an extmark. +/// +/// Extmarks may be queried by position, name or even special names +/// in the future such as "cursor". +/// +/// @param[out] lnum extmark line +/// @param[out] colnr extmark column +/// +/// @return true if the extmark was found, else false +static bool extmark_get_index_from_obj(buf_T *buf, Integer ns_id, Object obj, int *row, + colnr_T *col, Error *err) +{ + // Check if it is mark id + if (obj.type == kObjectTypeInteger) { + Integer id = obj.data.integer; + if (id == 0) { + *row = 0; + *col = 0; + return true; + } else if (id == -1) { + *row = MAXLNUM; + *col = MAXCOL; + return true; + } else if (id < 0) { + api_set_error(err, kErrorTypeValidation, "Mark id must be positive"); + return false; + } + + ExtmarkInfo extmark = extmark_from_id(buf, (uint32_t)ns_id, (uint32_t)id); + if (extmark.row >= 0) { + *row = extmark.row; + *col = extmark.col; + return true; + } else { + api_set_error(err, kErrorTypeValidation, "No mark with requested id"); + return false; + } + + // Check if it is a position + } else if (obj.type == kObjectTypeArray) { + Array pos = obj.data.array; + if (pos.size != 2 + || pos.items[0].type != kObjectTypeInteger + || pos.items[1].type != kObjectTypeInteger) { + api_set_error(err, kErrorTypeValidation, + "Position must have 2 integer elements"); + return false; + } + Integer pos_row = pos.items[0].data.integer; + Integer pos_col = pos.items[1].data.integer; + *row = (int)(pos_row >= 0 ? pos_row : MAXLNUM); + *col = (colnr_T)(pos_col >= 0 ? pos_col : MAXCOL); + return true; + } else { + api_set_error(err, kErrorTypeValidation, + "Position must be a mark id Integer or position Array"); + return false; + } +} +// adapted from sign.c:sign_define_init_text. +// TODO(lewis6991): Consider merging +static int init_sign_text(char **sign_text, char *text) +{ + char *s; + + char *endp = text + (int)STRLEN(text); + + // Count cells and check for non-printable chars + int cells = 0; + for (s = text; s < endp; s += utfc_ptr2len(s)) { + if (!vim_isprintc(utf_ptr2char(s))) { + break; + } + cells += utf_ptr2cells(s); + } + // Currently must be empty, one or two display cells + if (s != endp || cells > 2) { + return FAIL; + } + if (cells < 1) { + return OK; + } + + // Allocate one byte more if we need to pad up + // with a space. + size_t len = (size_t)(endp - text + ((cells == 1) ? 1 : 0)); + *sign_text = xstrnsave(text, len); + + if (cells == 1) { + STRCPY(*sign_text + len - 1, " "); + } + + return OK; +} + +VirtText parse_virt_text(Array chunks, Error *err, int *width) +{ + VirtText virt_text = KV_INITIAL_VALUE; + int w = 0; + for (size_t i = 0; i < chunks.size; i++) { + if (chunks.items[i].type != kObjectTypeArray) { + api_set_error(err, kErrorTypeValidation, "Chunk is not an array"); + goto free_exit; + } + Array chunk = chunks.items[i].data.array; + if (chunk.size == 0 || chunk.size > 2 + || chunk.items[0].type != kObjectTypeString) { + api_set_error(err, kErrorTypeValidation, + "Chunk is not an array with one or two strings"); + goto free_exit; + } + + String str = chunk.items[0].data.string; + + int hl_id = 0; + if (chunk.size == 2) { + Object hl = chunk.items[1]; + if (hl.type == kObjectTypeArray) { + Array arr = hl.data.array; + for (size_t j = 0; j < arr.size; j++) { + hl_id = object_to_hl_id(arr.items[j], "virt_text highlight", err); + if (ERROR_SET(err)) { + goto free_exit; + } + if (j < arr.size - 1) { + kv_push(virt_text, ((VirtTextChunk){ .text = NULL, + .hl_id = hl_id })); + } + } + } else { + hl_id = object_to_hl_id(hl, "virt_text highlight", err); + if (ERROR_SET(err)) { + goto free_exit; + } + } + } + + char *text = transstr(str.size > 0 ? str.data : "", false); // allocates + w += (int)mb_string2cells(text); + + kv_push(virt_text, ((VirtTextChunk){ .text = text, .hl_id = hl_id })); + } + + *width = w; + return virt_text; + +free_exit: + clear_virttext(&virt_text); + return virt_text; +} diff --git a/src/nvim/api/extmark.h b/src/nvim/api/extmark.h index c5e463cd86..74802c6efb 100644 --- a/src/nvim/api/extmark.h +++ b/src/nvim/api/extmark.h @@ -2,6 +2,7 @@ #define NVIM_API_EXTMARK_H #include "nvim/api/private/defs.h" +#include "nvim/decoration.h" #include "nvim/map.h" EXTERN Map(String, handle_T) namespace_ids INIT(= MAP_INIT); diff --git a/src/nvim/api/keysets.lua b/src/nvim/api/keysets.lua index df271c805f..1f1fa1e63a 100644 --- a/src/nvim/api/keysets.lua +++ b/src/nvim/api/keysets.lua @@ -21,6 +21,14 @@ return { "virt_lines"; "virt_lines_above"; "virt_lines_leftcol"; + "strict"; + "sign_text"; + "sign_hl_group"; + "number_hl_group"; + "line_hl_group"; + "cursorline_hl_group"; + "conceal"; + "ui_watched"; }; keymap = { "noremap"; @@ -43,7 +51,9 @@ return { "count"; "desc"; "force"; + "keepscript"; "nargs"; + "preview"; "range"; "register"; }; @@ -67,16 +77,136 @@ return { }; runtime = { "is_lua"; + "do_source"; }; eval_statusline = { "winid"; "maxwidth"; "fillchar"; "highlights"; + "use_winbar"; "use_tabline"; }; option = { "scope"; + "win"; + "buf"; + }; + highlight = { + "bold"; + "standout"; + "strikethrough"; + "underline"; + "undercurl"; + "underdouble"; + "underdotted"; + "underdashed"; + "italic"; + "reverse"; + "nocombine"; + "default"; + "global"; + "cterm"; + "foreground"; "fg"; + "background"; "bg"; + "ctermfg"; + "ctermbg"; + "special"; "sp"; + "link"; + "fallback"; + "blend"; + "temp"; + }; + highlight_cterm = { + "bold"; + "standout"; + "strikethrough"; + "underline"; + "undercurl"; + "underdouble"; + "underdotted"; + "underdashed"; + "italic"; + "reverse"; + "nocombine"; + }; + -- Autocmds + clear_autocmds = { + "buffer"; + "event"; + "group"; + "pattern"; + }; + create_autocmd = { + "buffer"; + "callback"; + "command"; + "desc"; + "group"; + "nested"; + "once"; + "pattern"; + }; + exec_autocmds = { + "buffer"; + "group"; + "modeline"; + "pattern"; + "data"; + }; + get_autocmds = { + "event"; + "group"; + "pattern"; + "buffer"; + }; + create_augroup = { + "clear"; + }; + cmd = { + "cmd"; + "range"; + "count"; + "reg"; + "bang"; + "args"; + "magic"; + "mods"; + "nargs"; + "addr"; + "nextcmd"; + }; + cmd_magic = { + "file"; + "bar"; + }; + cmd_mods = { + "silent"; + "emsg_silent"; + "unsilent"; + "filter"; + "sandbox"; + "noautocmd"; + "browse"; + "confirm"; + "hide"; + "keepalt"; + "keepjumps"; + "keepmarks"; + "keeppatterns"; + "lockmarks"; + "noswapfile"; + "tab"; + "verbose"; + "vertical"; + "split"; + }; + cmd_mods_filter = { + "pattern"; + "force"; + }; + cmd_opts = { + "output"; }; } diff --git a/src/nvim/api/options.c b/src/nvim/api/options.c new file mode 100644 index 0000000000..4ed676e613 --- /dev/null +++ b/src/nvim/api/options.c @@ -0,0 +1,554 @@ +// 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 <assert.h> +#include <inttypes.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> + +#include "nvim/api/options.h" +#include "nvim/api/private/converter.h" +#include "nvim/api/private/helpers.h" +#include "nvim/autocmd.h" +#include "nvim/buffer.h" +#include "nvim/option.h" +#include "nvim/option_defs.h" +#include "nvim/window.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "api/options.c.generated.h" +#endif + +static int validate_option_value_args(Dict(option) *opts, int *scope, int *opt_type, void **from, + Error *err) +{ + if (opts->scope.type == kObjectTypeString) { + if (!strcmp(opts->scope.data.string.data, "local")) { + *scope = OPT_LOCAL; + } else if (!strcmp(opts->scope.data.string.data, "global")) { + *scope = OPT_GLOBAL; + } else { + api_set_error(err, kErrorTypeValidation, "invalid scope: must be 'local' or 'global'"); + return FAIL; + } + } else if (HAS_KEY(opts->scope)) { + api_set_error(err, kErrorTypeValidation, "invalid value for key: scope"); + return FAIL; + } + + *opt_type = SREQ_GLOBAL; + + if (opts->win.type == kObjectTypeInteger) { + *opt_type = SREQ_WIN; + *from = find_window_by_handle((int)opts->win.data.integer, err); + if (ERROR_SET(err)) { + return FAIL; + } + } else if (HAS_KEY(opts->win)) { + api_set_error(err, kErrorTypeValidation, "invalid value for key: win"); + return FAIL; + } + + if (opts->buf.type == kObjectTypeInteger) { + *scope = OPT_LOCAL; + *opt_type = SREQ_BUF; + *from = find_buffer_by_handle((int)opts->buf.data.integer, err); + if (ERROR_SET(err)) { + return FAIL; + } + } else if (HAS_KEY(opts->buf)) { + api_set_error(err, kErrorTypeValidation, "invalid value for key: buf"); + return FAIL; + } + + if (HAS_KEY(opts->scope) && HAS_KEY(opts->buf)) { + api_set_error(err, kErrorTypeValidation, "scope and buf cannot be used together"); + return FAIL; + } + + if (HAS_KEY(opts->win) && HAS_KEY(opts->buf)) { + api_set_error(err, kErrorTypeValidation, "buf and win cannot be used together"); + return FAIL; + } + + return OK; +} + +/// Gets the value of an option. The behavior of this function matches that of +/// |:set|: the local value of an option is returned if it exists; otherwise, +/// the global value is returned. Local values always correspond to the current +/// buffer or window, unless "buf" or "win" is set in {opts}. +/// +/// @param name Option name +/// @param opts Optional parameters +/// - scope: One of "global" or "local". Analogous to +/// |:setglobal| and |:setlocal|, respectively. +/// - win: |window-ID|. Used for getting window local options. +/// - buf: Buffer number. Used for getting buffer local options. +/// Implies {scope} is "local". +/// @param[out] err Error details, if any +/// @return Option value +Object nvim_get_option_value(String name, Dict(option) *opts, Error *err) + FUNC_API_SINCE(9) +{ + Object rv = OBJECT_INIT; + + int scope = 0; + int opt_type = SREQ_GLOBAL; + void *from = NULL; + if (!validate_option_value_args(opts, &scope, &opt_type, &from, err)) { + return rv; + } + + long numval = 0; + char *stringval = NULL; + int result = access_option_value_for(name.data, &numval, &stringval, scope, opt_type, from, + true, err); + if (ERROR_SET(err)) { + return rv; + } + + switch (result) { + case 0: + rv = STRING_OBJ(cstr_as_string(stringval)); + break; + case 1: + rv = INTEGER_OBJ(numval); + break; + case 2: + switch (numval) { + case 0: + case 1: + rv = BOOLEAN_OBJ(numval); + break; + default: + // Boolean options that return something other than 0 or 1 should return nil. Currently this + // only applies to 'autoread' which uses -1 as a local value to indicate "unset" + rv = NIL; + break; + } + break; + default: + api_set_error(err, kErrorTypeValidation, "unknown option '%s'", name.data); + return rv; + } + + return rv; +} + +/// Sets the value of an option. The behavior of this function matches that of +/// |:set|: for global-local options, both the global and local value are set +/// unless otherwise specified with {scope}. +/// +/// Note the options {win} and {buf} cannot be used together. +/// +/// @param name Option name +/// @param value New option value +/// @param opts Optional parameters +/// - scope: One of 'global' or 'local'. Analogous to +/// |:setglobal| and |:setlocal|, respectively. +/// - win: |window-ID|. Used for setting window local option. +/// - buf: Buffer number. Used for setting buffer local option. +/// @param[out] err Error details, if any +void nvim_set_option_value(String name, Object value, Dict(option) *opts, Error *err) + FUNC_API_SINCE(9) +{ + int scope = 0; + int opt_type = SREQ_GLOBAL; + void *to = NULL; + if (!validate_option_value_args(opts, &scope, &opt_type, &to, err)) { + return; + } + + // If: + // - window id is provided + // - scope is not provided + // - option is global or local to window (global-local) + // + // Then force scope to local since we don't want to change the global option + if (opt_type == SREQ_WIN && scope == 0) { + int flags = get_option_value_strict(name.data, NULL, NULL, opt_type, to); + if (flags & SOPT_GLOBAL) { + scope = OPT_LOCAL; + } + } + + long numval = 0; + char *stringval = NULL; + + switch (value.type) { + case kObjectTypeInteger: + numval = value.data.integer; + break; + case kObjectTypeBoolean: + numval = value.data.boolean ? 1 : 0; + break; + case kObjectTypeString: + stringval = value.data.string.data; + break; + case kObjectTypeNil: + scope |= OPT_CLEAR; + break; + default: + api_set_error(err, kErrorTypeValidation, "invalid value for option"); + return; + } + + access_option_value_for(name.data, &numval, &stringval, scope, opt_type, to, false, err); +} + +/// Gets the option information for all options. +/// +/// The dictionary has the full option names as keys and option metadata +/// dictionaries as detailed at |nvim_get_option_info|. +/// +/// @return dictionary of all options +Dictionary nvim_get_all_options_info(Error *err) + FUNC_API_SINCE(7) +{ + return get_all_vimoptions(); +} + +/// Gets the option information for one option +/// +/// Resulting dictionary has keys: +/// - name: Name of the option (like 'filetype') +/// - shortname: Shortened name of the option (like 'ft') +/// - type: type of option ("string", "number" or "boolean") +/// - default: The default value for the option +/// - was_set: Whether the option was set. +/// +/// - last_set_sid: Last set script id (if any) +/// - last_set_linenr: line number where option was set +/// - last_set_chan: Channel where option was set (0 for local) +/// +/// - scope: one of "global", "win", or "buf" +/// - global_local: whether win or buf option has a global value +/// +/// - commalist: List of comma separated values +/// - flaglist: List of single char flags +/// +/// +/// @param name Option name +/// @param[out] err Error details, if any +/// @return Option Information +Dictionary nvim_get_option_info(String name, Error *err) + FUNC_API_SINCE(7) +{ + return get_vimoption(name, err); +} +/// Sets the global value of an option. +/// +/// @param channel_id +/// @param name Option name +/// @param value New option value +/// @param[out] err Error details, if any +void nvim_set_option(uint64_t channel_id, String name, Object value, Error *err) + FUNC_API_SINCE(1) +{ + set_option_to(channel_id, NULL, SREQ_GLOBAL, name, value, err); +} + +/// Gets the global value of an option. +/// +/// @param name Option name +/// @param[out] err Error details, if any +/// @return Option value (global) +Object nvim_get_option(String name, Error *err) + FUNC_API_SINCE(1) +{ + return get_option_from(NULL, SREQ_GLOBAL, name, err); +} + +/// Gets a buffer option value +/// +/// @param buffer Buffer handle, or 0 for current buffer +/// @param name Option name +/// @param[out] err Error details, if any +/// @return Option value +Object nvim_buf_get_option(Buffer buffer, String name, Error *err) + FUNC_API_SINCE(1) +{ + buf_T *buf = find_buffer_by_handle(buffer, err); + + if (!buf) { + return (Object)OBJECT_INIT; + } + + return get_option_from(buf, SREQ_BUF, name, err); +} + +/// Sets a buffer option value. Passing 'nil' as value deletes the option (only +/// works if there's a global fallback) +/// +/// @param channel_id +/// @param buffer Buffer handle, or 0 for current buffer +/// @param name Option name +/// @param value Option value +/// @param[out] err Error details, if any +void nvim_buf_set_option(uint64_t channel_id, Buffer buffer, String name, Object value, Error *err) + FUNC_API_SINCE(1) +{ + buf_T *buf = find_buffer_by_handle(buffer, err); + + if (!buf) { + return; + } + + set_option_to(channel_id, buf, SREQ_BUF, name, value, err); +} + +/// Gets a window option value +/// +/// @param window Window handle, or 0 for current window +/// @param name Option name +/// @param[out] err Error details, if any +/// @return Option value +Object nvim_win_get_option(Window window, String name, Error *err) + FUNC_API_SINCE(1) +{ + win_T *win = find_window_by_handle(window, err); + + if (!win) { + return (Object)OBJECT_INIT; + } + + return get_option_from(win, SREQ_WIN, name, err); +} + +/// Sets a window option value. Passing 'nil' as value deletes the option(only +/// works if there's a global fallback) +/// +/// @param channel_id +/// @param window Window handle, or 0 for current window +/// @param name Option name +/// @param value Option value +/// @param[out] err Error details, if any +void nvim_win_set_option(uint64_t channel_id, Window window, String name, Object value, Error *err) + FUNC_API_SINCE(1) +{ + win_T *win = find_window_by_handle(window, err); + + if (!win) { + return; + } + + set_option_to(channel_id, win, SREQ_WIN, name, value, err); +} + +/// Gets the value of a global or local(buffer, window) option. +/// +/// @param from If `type` is `SREQ_WIN` or `SREQ_BUF`, this must be a pointer +/// to the window or buffer. +/// @param type One of `SREQ_GLOBAL`, `SREQ_WIN` or `SREQ_BUF` +/// @param name The option name +/// @param[out] err Details of an error that may have occurred +/// @return the option value +Object get_option_from(void *from, int type, String name, Error *err) +{ + Object rv = OBJECT_INIT; + + if (name.size == 0) { + api_set_error(err, kErrorTypeValidation, "Empty option name"); + return rv; + } + + // Return values + int64_t numval; + char *stringval = NULL; + int flags = get_option_value_strict(name.data, &numval, &stringval, + type, from); + + if (!flags) { + api_set_error(err, kErrorTypeValidation, "Invalid option name: '%s'", + name.data); + return rv; + } + + if (flags & SOPT_BOOL) { + rv.type = kObjectTypeBoolean; + rv.data.boolean = numval ? true : false; + } else if (flags & SOPT_NUM) { + rv.type = kObjectTypeInteger; + rv.data.integer = numval; + } else if (flags & SOPT_STRING) { + if (stringval) { + rv.type = kObjectTypeString; + rv.data.string.data = stringval; + rv.data.string.size = strlen(stringval); + } else { + api_set_error(err, kErrorTypeException, + "Failed to get value for option '%s'", + name.data); + } + } else { + api_set_error(err, + kErrorTypeException, + "Unknown type for option '%s'", + name.data); + } + + return rv; +} + +/// Sets the value of a global or local(buffer, window) option. +/// +/// @param to If `type` is `SREQ_WIN` or `SREQ_BUF`, this must be a pointer +/// to the window or buffer. +/// @param type One of `SREQ_GLOBAL`, `SREQ_WIN` or `SREQ_BUF` +/// @param name The option name +/// @param[out] err Details of an error that may have occurred +void set_option_to(uint64_t channel_id, void *to, int type, String name, Object value, Error *err) +{ + if (name.size == 0) { + api_set_error(err, kErrorTypeValidation, "Empty option name"); + return; + } + + int flags = get_option_value_strict(name.data, NULL, NULL, type, to); + + if (flags == 0) { + api_set_error(err, kErrorTypeValidation, "Invalid option name '%s'", + name.data); + return; + } + + if (value.type == kObjectTypeNil) { + if (type == SREQ_GLOBAL) { + api_set_error(err, kErrorTypeException, "Cannot unset option '%s'", + name.data); + return; + } else if (!(flags & SOPT_GLOBAL)) { + api_set_error(err, + kErrorTypeException, + "Cannot unset option '%s' " + "because it doesn't have a global value", + name.data); + return; + } else { + unset_global_local_option(name.data, to); + return; + } + } + + long numval = 0; + char *stringval = NULL; + + if (flags & SOPT_BOOL) { + if (value.type != kObjectTypeBoolean) { + api_set_error(err, + kErrorTypeValidation, + "Option '%s' requires a Boolean value", + name.data); + return; + } + + numval = value.data.boolean; + } else if (flags & SOPT_NUM) { + if (value.type != kObjectTypeInteger) { + api_set_error(err, kErrorTypeValidation, + "Option '%s' requires an integer value", + name.data); + return; + } + + if (value.data.integer > INT_MAX || value.data.integer < INT_MIN) { + api_set_error(err, kErrorTypeValidation, + "Value for option '%s' is out of range", + name.data); + return; + } + + numval = (int)value.data.integer; + } else { + if (value.type != kObjectTypeString) { + api_set_error(err, kErrorTypeValidation, + "Option '%s' requires a string value", + name.data); + return; + } + + stringval = value.data.string.data; + } + + // For global-win-local options -> setlocal + // For win-local options -> setglobal and setlocal (opt_flags == 0) + const int opt_flags = (type == SREQ_WIN && !(flags & SOPT_GLOBAL)) ? 0 : + (type == SREQ_GLOBAL) ? OPT_GLOBAL : OPT_LOCAL; + + WITH_SCRIPT_CONTEXT(channel_id, { + access_option_value_for(name.data, &numval, &stringval, opt_flags, type, to, false, err); + }); +} + +static int access_option_value(char *key, long *numval, char **stringval, int opt_flags, bool get, + Error *err) +{ + if (get) { + return get_option_value(key, numval, stringval, opt_flags); + } else { + char *errmsg; + if ((errmsg = set_option_value(key, *numval, *stringval, opt_flags))) { + if (try_end(err)) { + return 0; + } + + api_set_error(err, kErrorTypeException, "%s", errmsg); + } + return 0; + } +} + +static int access_option_value_for(char *key, long *numval, char **stringval, int opt_flags, + int opt_type, void *from, bool get, Error *err) +{ + bool need_switch = false; + switchwin_T switchwin; + aco_save_T aco; + int result = 0; + + try_start(); + switch (opt_type) { + case SREQ_WIN: + need_switch = (win_T *)from != curwin; + if (need_switch) { + if (switch_win_noblock(&switchwin, (win_T *)from, win_find_tabpage((win_T *)from), true) + == FAIL) { + restore_win_noblock(&switchwin, true); + if (try_end(err)) { + return result; + } + api_set_error(err, kErrorTypeException, "Problem while switching windows"); + return result; + } + } + result = access_option_value(key, numval, stringval, opt_flags, get, err); + if (need_switch) { + restore_win_noblock(&switchwin, true); + } + break; + case SREQ_BUF: + need_switch = (buf_T *)from != curbuf; + if (need_switch) { + aucmd_prepbuf(&aco, (buf_T *)from); + } + result = access_option_value(key, numval, stringval, opt_flags, get, err); + if (need_switch) { + aucmd_restbuf(&aco); + } + break; + case SREQ_GLOBAL: + result = access_option_value(key, numval, stringval, opt_flags, get, err); + break; + } + + if (ERROR_SET(err)) { + return result; + } + + try_end(err); + + return result; +} diff --git a/src/nvim/api/options.h b/src/nvim/api/options.h new file mode 100644 index 0000000000..efbfec3a6c --- /dev/null +++ b/src/nvim/api/options.h @@ -0,0 +1,9 @@ +#ifndef NVIM_API_OPTIONS_H +#define NVIM_API_OPTIONS_H + +#include "nvim/api/private/defs.h" +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "api/options.h.generated.h" +#endif + +#endif // NVIM_API_OPTIONS_H diff --git a/src/nvim/api/private/converter.c b/src/nvim/api/private/converter.c index 36da6c13a9..8724ef4432 100644 --- a/src/nvim/api/private/converter.c +++ b/src/nvim/api/private/converter.c @@ -10,6 +10,9 @@ #include "nvim/api/private/helpers.h" #include "nvim/assert.h" #include "nvim/eval/typval.h" +#include "nvim/eval/userfunc.h" +#include "nvim/lua/converter.h" +#include "nvim/lua/executor.h" /// Helper structure for vim_to_object typedef struct { @@ -54,14 +57,20 @@ typedef struct { const size_t len_ = (size_t)(len); \ const blob_T *const blob_ = (blob); \ kvi_push(edata->stack, STRING_OBJ(((String) { \ - .data = len_ != 0 ? xmemdup(blob_->bv_ga.ga_data, len_) : NULL, \ + .data = len_ != 0 ? xmemdupz(blob_->bv_ga.ga_data, len_) : xstrdup(""), \ .size = len_ \ }))); \ } while (0) #define TYPVAL_ENCODE_CONV_FUNC_START(tv, fun) \ do { \ - TYPVAL_ENCODE_CONV_NIL(tv); \ + ufunc_T *fp = find_func(fun); \ + if (fp != NULL && fp->uf_cb == nlua_CFunction_func_call) { \ + LuaRef ref = api_new_luaref(((LuaCFunctionState *)fp->uf_cb_state)->lua_callable.func_ref); \ + kvi_push(edata->stack, LUAREF_OBJ(ref)); \ + } else { \ + TYPVAL_ENCODE_CONV_NIL(tv); \ + } \ goto typval_encode_stop_converting_one_item; \ } while (0) @@ -340,6 +349,17 @@ bool object_to_vim(Object obj, typval_T *tv, Error *err) tv->vval.v_dict = dict; break; } + + case kObjectTypeLuaRef: { + LuaCFunctionState *state = xmalloc(sizeof(LuaCFunctionState)); + state->lua_callable.func_ref = api_new_luaref(obj.data.luaref); + char *name = + (char *)register_cfunc(&nlua_CFunction_func_call, &nlua_CFunction_func_free, state); + tv->v_type = VAR_FUNC; + tv->vval.v_string = xstrdup(name); + break; + } + default: abort(); } diff --git a/src/nvim/api/private/defs.h b/src/nvim/api/private/defs.h index 396fab721d..b1e0dd364c 100644 --- a/src/nvim/api/private/defs.h +++ b/src/nvim/api/private/defs.h @@ -6,9 +6,10 @@ #include <string.h> #include "nvim/func_attr.h" +#include "nvim/lib/kvec.h" #include "nvim/types.h" -#define ARRAY_DICT_INIT { .size = 0, .capacity = 0, .items = NULL } +#define ARRAY_DICT_INIT KV_INITIAL_VALUE #define STRING_INIT { .data = NULL, .size = 0 } #define OBJECT_INIT { .type = kObjectTypeNil } #define ERROR_INIT { .type = kErrorTypeNone, .msg = NULL } @@ -84,18 +85,10 @@ REMOTE_TYPE(Window); REMOTE_TYPE(Tabpage); typedef struct object Object; - -typedef struct { - Object *items; - size_t size, capacity; -} Array; +typedef kvec_t(Object) Array; typedef struct key_value_pair KeyValuePair; - -typedef struct { - KeyValuePair *items; - size_t size, capacity; -} Dictionary; +typedef kvec_t(KeyValuePair) Dictionary; typedef enum { kObjectTypeNil = 0, diff --git a/src/nvim/api/private/dispatch.c b/src/nvim/api/private/dispatch.c index 8ab7743e01..d6a6fc1219 100644 --- a/src/nvim/api/private/dispatch.c +++ b/src/nvim/api/private/dispatch.c @@ -6,47 +6,50 @@ #include <msgpack.h> #include <stdbool.h> -#include "nvim/api/buffer.h" #include "nvim/api/deprecated.h" -#include "nvim/api/extmark.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/dispatch.h" #include "nvim/api/private/helpers.h" +#include "nvim/log.h" +#include "nvim/map.h" +#include "nvim/msgpack_rpc/helpers.h" +#include "nvim/vim.h" + +// =========================================================================== +// NEW API FILES MUST GO HERE. +// +// When creating a new API file, you must include it here, +// so that the dispatcher can find the C functions that you are creating! +// =========================================================================== +#include "nvim/api/autocmd.h" +#include "nvim/api/buffer.h" +#include "nvim/api/command.h" +#include "nvim/api/extmark.h" +#include "nvim/api/options.h" #include "nvim/api/tabpage.h" #include "nvim/api/ui.h" #include "nvim/api/vim.h" #include "nvim/api/vimscript.h" #include "nvim/api/win_config.h" #include "nvim/api/window.h" -#include "nvim/log.h" -#include "nvim/map.h" -#include "nvim/msgpack_rpc/helpers.h" -#include "nvim/vim.h" - -static Map(String, MsgpackRpcRequestHandler) methods = MAP_INIT; +#include "nvim/ui_client.h" -static void msgpack_rpc_add_method_handler(String method, MsgpackRpcRequestHandler handler) -{ - map_put(String, MsgpackRpcRequestHandler)(&methods, method, handler); -} +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "api/private/dispatch_wrappers.generated.h" +#endif /// @param name API method name /// @param name_len name size (includes terminating NUL) MsgpackRpcRequestHandler msgpack_rpc_get_handler_for(const char *name, size_t name_len, Error *error) { - String m = { .data = (char *)name, .size = name_len }; - MsgpackRpcRequestHandler rv = - map_get(String, MsgpackRpcRequestHandler)(&methods, m); + int hash = msgpack_rpc_get_handler_for_hash(name, name_len); - if (!rv.fn) { + if (hash < 0) { api_set_error(error, kErrorTypeException, "Invalid method: %.*s", - m.size > 0 ? (int)m.size : (int)sizeof("<empty>"), - m.size > 0 ? m.data : "<empty>"); + name_len > 0 ? (int)name_len : (int)sizeof("<empty>"), + name_len > 0 ? name : "<empty>"); + return (MsgpackRpcRequestHandler){ 0 }; } - return rv; + return method_handlers[hash]; } - -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "api/private/dispatch_wrappers.generated.h" -#endif diff --git a/src/nvim/api/private/dispatch.h b/src/nvim/api/private/dispatch.h index bad5a13934..4b7c394944 100644 --- a/src/nvim/api/private/dispatch.h +++ b/src/nvim/api/private/dispatch.h @@ -10,6 +10,7 @@ typedef Object (*ApiDispatchWrapper)(uint64_t channel_id, /// The rpc_method_handlers table, used in msgpack_rpc_dispatch(), stores /// functions of this type. typedef struct { + const char *name; ApiDispatchWrapper fn; bool fast; // Function is safe to be executed immediately while running the // uv loop (the loop is run very frequently due to breakcheck). diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index f9603acbda..fad75d55be 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -16,12 +16,12 @@ #include "nvim/assert.h" #include "nvim/buffer.h" #include "nvim/charset.h" -#include "nvim/decoration.h" #include "nvim/eval.h" #include "nvim/eval/typval.h" +#include "nvim/ex_cmds_defs.h" #include "nvim/extmark.h" #include "nvim/fileio.h" -#include "nvim/getchar.h" +#include "nvim/highlight_group.h" #include "nvim/lib/kvec.h" #include "nvim/lua/executor.h" #include "nvim/map.h" @@ -30,9 +30,6 @@ #include "nvim/memline.h" #include "nvim/memory.h" #include "nvim/msgpack_rpc/helpers.h" -#include "nvim/option.h" -#include "nvim/option_defs.h" -#include "nvim/syntax.h" #include "nvim/ui.h" #include "nvim/version.h" #include "nvim/vim.h" @@ -111,7 +108,7 @@ bool try_leave(const TryState *const tstate, Error *const err) /// try_enter()/try_leave() pair should be used instead. void try_start(void) { - ++trylevel; + trylevel++; } /// End try block, set the error message if any and return true if an error @@ -139,10 +136,10 @@ bool try_end(Error *err) got_int = false; } else if (msg_list != NULL && *msg_list != NULL) { int should_free; - char *msg = (char *)get_exception_string(*msg_list, - ET_ERROR, - NULL, - &should_free); + char *msg = get_exception_string(*msg_list, + ET_ERROR, + NULL, + &should_free); api_set_error(err, kErrorTypeException, "%s", msg); free_global_msglist(); @@ -261,157 +258,6 @@ Object dict_set_var(dict_T *dict, String key, Object value, bool del, bool retva return rv; } -/// Gets the value of a global or local(buffer, window) option. -/// -/// @param from If `type` is `SREQ_WIN` or `SREQ_BUF`, this must be a pointer -/// to the window or buffer. -/// @param type One of `SREQ_GLOBAL`, `SREQ_WIN` or `SREQ_BUF` -/// @param name The option name -/// @param[out] err Details of an error that may have occurred -/// @return the option value -Object get_option_from(void *from, int type, String name, Error *err) -{ - Object rv = OBJECT_INIT; - - if (name.size == 0) { - api_set_error(err, kErrorTypeValidation, "Empty option name"); - return rv; - } - - // Return values - int64_t numval; - char *stringval = NULL; - int flags = get_option_value_strict(name.data, &numval, &stringval, - type, from); - - if (!flags) { - api_set_error(err, kErrorTypeValidation, "Invalid option name: '%s'", - name.data); - return rv; - } - - if (flags & SOPT_BOOL) { - rv.type = kObjectTypeBoolean; - rv.data.boolean = numval ? true : false; - } else if (flags & SOPT_NUM) { - rv.type = kObjectTypeInteger; - rv.data.integer = numval; - } else if (flags & SOPT_STRING) { - if (stringval) { - rv.type = kObjectTypeString; - rv.data.string.data = stringval; - rv.data.string.size = strlen(stringval); - } else { - api_set_error(err, kErrorTypeException, - "Failed to get value for option '%s'", - name.data); - } - } else { - api_set_error(err, - kErrorTypeException, - "Unknown type for option '%s'", - name.data); - } - - return rv; -} - -/// Sets the value of a global or local(buffer, window) option. -/// -/// @param to If `type` is `SREQ_WIN` or `SREQ_BUF`, this must be a pointer -/// to the window or buffer. -/// @param type One of `SREQ_GLOBAL`, `SREQ_WIN` or `SREQ_BUF` -/// @param name The option name -/// @param[out] err Details of an error that may have occurred -void set_option_to(uint64_t channel_id, void *to, int type, String name, Object value, Error *err) -{ - if (name.size == 0) { - api_set_error(err, kErrorTypeValidation, "Empty option name"); - return; - } - - int flags = get_option_value_strict(name.data, NULL, NULL, type, to); - - if (flags == 0) { - api_set_error(err, kErrorTypeValidation, "Invalid option name '%s'", - name.data); - return; - } - - if (value.type == kObjectTypeNil) { - if (type == SREQ_GLOBAL) { - api_set_error(err, kErrorTypeException, "Cannot unset option '%s'", - name.data); - return; - } else if (!(flags & SOPT_GLOBAL)) { - api_set_error(err, - kErrorTypeException, - "Cannot unset option '%s' " - "because it doesn't have a global value", - name.data); - return; - } else { - unset_global_local_option(name.data, to); - return; - } - } - - int numval = 0; - char *stringval = NULL; - - if (flags & SOPT_BOOL) { - if (value.type != kObjectTypeBoolean) { - api_set_error(err, - kErrorTypeValidation, - "Option '%s' requires a Boolean value", - name.data); - return; - } - - numval = value.data.boolean; - } else if (flags & SOPT_NUM) { - if (value.type != kObjectTypeInteger) { - api_set_error(err, kErrorTypeValidation, - "Option '%s' requires an integer value", - name.data); - return; - } - - if (value.data.integer > INT_MAX || value.data.integer < INT_MIN) { - api_set_error(err, kErrorTypeValidation, - "Value for option '%s' is out of range", - name.data); - return; - } - - numval = (int)value.data.integer; - } else { - if (value.type != kObjectTypeString) { - api_set_error(err, kErrorTypeValidation, - "Option '%s' requires a string value", - name.data); - return; - } - - stringval = value.data.string.data; - } - - const sctx_T save_current_sctx = current_sctx; - current_sctx.sc_sid = - channel_id == LUA_INTERNAL_CALL ? SID_LUA : SID_API_CLIENT; - current_sctx.sc_lnum = 0; - current_channel_id = channel_id; - - const int opt_flags = (type == SREQ_WIN && !(flags & SOPT_GLOBAL)) - ? 0 : (type == SREQ_GLOBAL) - ? OPT_GLOBAL : OPT_LOCAL; - set_option_value_for(name.data, numval, stringval, - opt_flags, type, to, err); - - current_sctx = save_current_sctx; -} - - buf_T *find_buffer_by_handle(Buffer buffer, Error *err) { if (buffer == 0) { @@ -493,6 +339,16 @@ String cstr_to_string(const char *str) }; } +/// Copies a String to an allocated, NUL-terminated C string. +/// +/// @param str the String to copy +/// @return the resulting C string +char *string_to_cstr(String str) + FUNC_ATTR_NONNULL_RET FUNC_ATTR_WARN_UNUSED_RESULT +{ + return xstrndup(str.data, str.size); +} + /// Copies buffer to an allocated String. /// The resulting string is also NUL-terminated, to facilitate interoperating /// with code using C strings. @@ -584,145 +440,6 @@ Array string_to_array(const String input, bool crlf) return ret; } -/// Set, tweak, or remove a mapping in a mode. Acts as the implementation for -/// functions like @ref nvim_buf_set_keymap. -/// -/// Arguments are handled like @ref nvim_set_keymap unless noted. -/// @param buffer Buffer handle for a specific buffer, or 0 for the current -/// buffer, or -1 to signify global behavior ("all buffers") -/// @param is_unmap When true, removes the mapping that matches {lhs}. -void modify_keymap(Buffer buffer, bool is_unmap, String mode, String lhs, String rhs, - Dict(keymap) *opts, Error *err) -{ - LuaRef lua_funcref = LUA_NOREF; - bool global = (buffer == -1); - if (global) { - buffer = 0; - } - buf_T *target_buf = find_buffer_by_handle(buffer, err); - - if (!target_buf) { - return; - } - - if (opts != NULL && opts->callback.type == kObjectTypeLuaRef) { - lua_funcref = opts->callback.data.luaref; - opts->callback.data.luaref = LUA_NOREF; - } - MapArguments parsed_args = MAP_ARGUMENTS_INIT; - if (opts) { -#define KEY_TO_BOOL(name) \ - parsed_args.name = api_object_to_bool(opts->name, #name, false, err); \ - if (ERROR_SET(err)) { \ - goto fail_and_free; \ - } - - KEY_TO_BOOL(nowait); - KEY_TO_BOOL(noremap); - KEY_TO_BOOL(silent); - KEY_TO_BOOL(script); - KEY_TO_BOOL(expr); - KEY_TO_BOOL(unique); -#undef KEY_TO_BOOL - } - parsed_args.buffer = !global; - - set_maparg_lhs_rhs((char_u *)lhs.data, lhs.size, - (char_u *)rhs.data, rhs.size, lua_funcref, - CPO_TO_CPO_FLAGS, &parsed_args); - if (opts != NULL && opts->desc.type == kObjectTypeString) { - parsed_args.desc = xstrdup(opts->desc.data.string.data); - } else { - parsed_args.desc = NULL; - } - if (parsed_args.lhs_len > MAXMAPLEN) { - api_set_error(err, kErrorTypeValidation, "LHS exceeds maximum map length: %s", lhs.data); - goto fail_and_free; - } - - if (mode.size > 1) { - api_set_error(err, kErrorTypeValidation, "Shortname is too long: %s", mode.data); - goto fail_and_free; - } - int mode_val; // integer value of the mapping mode, to be passed to do_map() - char_u *p = (char_u *)((mode.size) ? mode.data : "m"); - if (STRNCMP(p, "!", 2) == 0) { - mode_val = get_map_mode(&p, true); // mapmode-ic - } else { - mode_val = get_map_mode(&p, false); - if ((mode_val == VISUAL + SELECTMODE + NORMAL + OP_PENDING) - && mode.size > 0) { - // get_map_mode() treats unrecognized mode shortnames as ":map". - // This is an error unless the given shortname was empty string "". - api_set_error(err, kErrorTypeValidation, "Invalid mode shortname: \"%s\"", (char *)p); - goto fail_and_free; - } - } - - if (parsed_args.lhs_len == 0) { - api_set_error(err, kErrorTypeValidation, "Invalid (empty) LHS"); - goto fail_and_free; - } - - bool is_noremap = parsed_args.noremap; - assert(!(is_unmap && is_noremap)); - - if (!is_unmap && lua_funcref == LUA_NOREF - && (parsed_args.rhs_len == 0 && !parsed_args.rhs_is_noop)) { - if (rhs.size == 0) { // assume that the user wants RHS to be a <Nop> - parsed_args.rhs_is_noop = true; - } else { - // the given RHS was nonempty and not a <Nop>, but was parsed as if it - // were empty? - assert(false && "Failed to parse nonempty RHS!"); - api_set_error(err, kErrorTypeValidation, "Parsing of nonempty RHS failed: %s", rhs.data); - goto fail_and_free; - } - } else if (is_unmap && (parsed_args.rhs_len || parsed_args.rhs_lua != LUA_NOREF)) { - if (parsed_args.rhs_len) { - api_set_error(err, kErrorTypeValidation, - "Gave nonempty RHS in unmap command: %s", parsed_args.rhs); - } else { - api_set_error(err, kErrorTypeValidation, "Gave nonempty RHS for unmap"); - } - goto fail_and_free; - } - - // buf_do_map() reads noremap/unmap as its own argument. - int maptype_val = 0; - if (is_unmap) { - maptype_val = 1; - } else if (is_noremap) { - maptype_val = 2; - } - - switch (buf_do_map(maptype_val, &parsed_args, mode_val, 0, target_buf)) { - case 0: - break; - case 1: - api_set_error(err, kErrorTypeException, (char *)e_invarg, 0); - goto fail_and_free; - case 2: - api_set_error(err, kErrorTypeException, (char *)e_nomap, 0); - goto fail_and_free; - case 5: - api_set_error(err, kErrorTypeException, - "E227: mapping already exists for %s", parsed_args.lhs); - goto fail_and_free; - default: - assert(false && "Unrecognized return code!"); - goto fail_and_free; - } // switch - - parsed_args.rhs_lua = LUA_NOREF; // don't clear ref on success -fail_and_free: - NLUA_CLEAR_REF(parsed_args.rhs_lua); - xfree(parsed_args.rhs); - xfree(parsed_args.orig_rhs); - XFREE_CLEAR(parsed_args.desc); - return; -} - /// Collects `n` buffer lines into array `l`, optionally replacing newlines /// with NUL. /// @@ -759,6 +476,52 @@ bool buf_collect_lines(buf_T *buf, size_t n, int64_t start, bool replace_nl, Arr return true; } +/// Returns a substring of a buffer line +/// +/// @param buf Buffer handle +/// @param lnum Line number (1-based) +/// @param start_col Starting byte offset into line (0-based) +/// @param end_col Ending byte offset into line (0-based, exclusive) +/// @param replace_nl Replace newlines ('\n') with null ('\0') +/// @param err Error object +/// @return The text between start_col and end_col on line lnum of buffer buf +String buf_get_text(buf_T *buf, int64_t lnum, int64_t start_col, int64_t end_col, bool replace_nl, + Error *err) +{ + String rv = STRING_INIT; + + if (lnum >= MAXLNUM) { + api_set_error(err, kErrorTypeValidation, "Line index is too high"); + return rv; + } + + const char *bufstr = (char *)ml_get_buf(buf, (linenr_T)lnum, false); + size_t line_length = strlen(bufstr); + + start_col = start_col < 0 ? (int64_t)line_length + start_col + 1 : start_col; + end_col = end_col < 0 ? (int64_t)line_length + end_col + 1 : end_col; + + if (start_col >= MAXCOL || end_col >= MAXCOL) { + api_set_error(err, kErrorTypeValidation, "Column index is too high"); + return rv; + } + + if (start_col > end_col) { + api_set_error(err, kErrorTypeValidation, "start_col must be less than end_col"); + return rv; + } + + if ((size_t)start_col >= line_length) { + return rv; + } + + rv = cstrn_to_string(&bufstr[start_col], (size_t)(end_col - start_col)); + if (replace_nl) { + strchrsub(rv.data, '\n', '\0'); + } + + return rv; +} void api_free_string(String value) { @@ -769,6 +532,29 @@ void api_free_string(String value) xfree(value.data); } +Array arena_array(Arena *arena, size_t max_size) +{ + Array arr = ARRAY_DICT_INIT; + kv_fixsize_arena(arena, arr, max_size); + return arr; +} + +Dictionary arena_dict(Arena *arena, size_t max_size) +{ + Dictionary dict = ARRAY_DICT_INIT; + kv_fixsize_arena(arena, dict, max_size); + return dict; +} + +String arena_string(Arena *arena, String str) +{ + if (str.size) { + return cbuf_as_string(arena_memdupz(arena, str.data, str.size), str.size); + } else { + return (String)STRING_INIT; + } +} + void api_free_object(Object value) { switch (value.type) { @@ -987,63 +773,6 @@ Object copy_object(Object obj) } } -static void set_option_value_for(char *key, int numval, char *stringval, int opt_flags, - int opt_type, void *from, Error *err) -{ - win_T *save_curwin = NULL; - tabpage_T *save_curtab = NULL; - aco_save_T aco; - - try_start(); - switch (opt_type) - { - case SREQ_WIN: - if (switch_win_noblock(&save_curwin, &save_curtab, (win_T *)from, - win_find_tabpage((win_T *)from), true) - == FAIL) { - restore_win_noblock(save_curwin, save_curtab, true); - if (try_end(err)) { - return; - } - api_set_error(err, - kErrorTypeException, - "Problem while switching windows"); - return; - } - set_option_value_err(key, numval, stringval, opt_flags, err); - restore_win_noblock(save_curwin, save_curtab, true); - break; - case SREQ_BUF: - aucmd_prepbuf(&aco, (buf_T *)from); - set_option_value_err(key, numval, stringval, opt_flags, err); - aucmd_restbuf(&aco); - break; - case SREQ_GLOBAL: - set_option_value_err(key, numval, stringval, opt_flags, err); - break; - } - - if (ERROR_SET(err)) { - return; - } - - try_end(err); -} - - -static void set_option_value_err(char *key, int numval, char *stringval, int opt_flags, Error *err) -{ - char *errmsg; - - if ((errmsg = set_option_value(key, numval, stringval, opt_flags))) { - if (try_end(err)) { - return; - } - - api_set_error(err, kErrorTypeException, "%s", errmsg); - } -} - void api_set_error(Error *err, ErrorType errType, const char *format, ...) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_PRINTF(3, 4) { @@ -1064,171 +793,6 @@ 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 -/// @param from_lua Whether it is called from internal lua api. -/// @returns Array of maparg()-like dictionaries describing mappings -ArrayOf(Dictionary) keymap_array(String mode, buf_T *buf, bool from_lua) -{ - 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); - Object api_dict = vim_to_object((typval_T[]) { { .v_type = VAR_DICT, - .vval.v_dict = dict } }); - if (from_lua) { - Dictionary d = api_dict.data.dictionary; - for (size_t j = 0; j < d.size; j++) { - if (strequal("callback", d.items[j].key.data)) { - d.items[j].value.type = kObjectTypeLuaRef; - d.items[j].value.data.luaref = api_new_luaref((LuaRef)d.items[j].value.data.integer); - break; - } - } - } - ADD(mappings, api_dict); - tv_dict_clear(dict); - } - } - } - tv_dict_free(dict); - - return mappings; -} - -/// Gets the line and column of an extmark. -/// -/// Extmarks may be queried by position, name or even special names -/// in the future such as "cursor". -/// -/// @param[out] lnum extmark line -/// @param[out] colnr extmark column -/// -/// @return true if the extmark was found, else false -bool extmark_get_index_from_obj(buf_T *buf, Integer ns_id, Object obj, int - *row, colnr_T *col, Error *err) -{ - // Check if it is mark id - if (obj.type == kObjectTypeInteger) { - Integer id = obj.data.integer; - if (id == 0) { - *row = 0; - *col = 0; - return true; - } else if (id == -1) { - *row = MAXLNUM; - *col = MAXCOL; - return true; - } else if (id < 0) { - api_set_error(err, kErrorTypeValidation, "Mark id must be positive"); - return false; - } - - ExtmarkInfo extmark = extmark_from_id(buf, (uint64_t)ns_id, (uint64_t)id); - if (extmark.row >= 0) { - *row = extmark.row; - *col = extmark.col; - return true; - } else { - api_set_error(err, kErrorTypeValidation, "No mark with requested id"); - return false; - } - - // Check if it is a position - } else if (obj.type == kObjectTypeArray) { - Array pos = obj.data.array; - if (pos.size != 2 - || pos.items[0].type != kObjectTypeInteger - || pos.items[1].type != kObjectTypeInteger) { - api_set_error(err, kErrorTypeValidation, - "Position must have 2 integer elements"); - return false; - } - Integer pos_row = pos.items[0].data.integer; - Integer pos_col = pos.items[1].data.integer; - *row = (int)(pos_row >= 0 ? pos_row : MAXLNUM); - *col = (colnr_T)(pos_col >= 0 ? pos_col : MAXCOL); - return true; - } else { - api_set_error(err, kErrorTypeValidation, - "Position must be a mark id Integer or position Array"); - return false; - } -} - -VirtText parse_virt_text(Array chunks, Error *err, int *width) -{ - VirtText virt_text = KV_INITIAL_VALUE; - int w = 0; - for (size_t i = 0; i < chunks.size; i++) { - if (chunks.items[i].type != kObjectTypeArray) { - api_set_error(err, kErrorTypeValidation, "Chunk is not an array"); - goto free_exit; - } - Array chunk = chunks.items[i].data.array; - if (chunk.size == 0 || chunk.size > 2 - || chunk.items[0].type != kObjectTypeString) { - api_set_error(err, kErrorTypeValidation, - "Chunk is not an array with one or two strings"); - goto free_exit; - } - - String str = chunk.items[0].data.string; - - int hl_id = 0; - if (chunk.size == 2) { - Object hl = chunk.items[1]; - if (hl.type == kObjectTypeArray) { - Array arr = hl.data.array; - for (size_t j = 0; j < arr.size; j++) { - hl_id = object_to_hl_id(arr.items[j], "virt_text highlight", err); - if (ERROR_SET(err)) { - goto free_exit; - } - if (j < arr.size-1) { - kv_push(virt_text, ((VirtTextChunk){ .text = NULL, - .hl_id = hl_id })); - } - } - } else { - hl_id = object_to_hl_id(hl, "virt_text highlight", err); - if (ERROR_SET(err)) { - goto free_exit; - } - } - } - - char *text = transstr(str.size > 0 ? str.data : "", false); // allocates - w += (int)mb_string2cells((char_u *)text); - - kv_push(virt_text, ((VirtTextChunk){ .text = text, .hl_id = hl_id })); - } - - *width = w; - return virt_text; - -free_exit: - clear_virttext(&virt_text); - return virt_text; -} - /// Force obj to bool. /// If it fails, returns false and sets err /// @param obj The object to coerce to a boolean @@ -1253,7 +817,7 @@ int object_to_hl_id(Object obj, const char *what, Error *err) { if (obj.type == kObjectTypeString) { String str = obj.data.string; - return str.size ? syn_check_group(str.data, (int)str.size) : 0; + return str.size ? syn_check_group(str.data, str.size) : 0; } else if (obj.type == kObjectTypeInteger) { return MAX((int)obj.data.integer, 0); } else { @@ -1287,7 +851,7 @@ HlMessage parse_hl_msg(Array chunks, Error *err) String hl = chunk.items[1].data.string; if (hl.size > 0) { // TODO(bfredl): use object_to_hl_id and allow integer - int hl_id = syn_check_group(hl.data, (int)hl.size); + int hl_id = syn_check_group(hl.data, hl.size); attr = hl_id > 0 ? syn_id2attr(hl_id) : 0; } } @@ -1297,8 +861,8 @@ HlMessage parse_hl_msg(Array chunks, Error *err) return hl_msg; free_exit: - clear_hl_msg(&hl_msg); - return hl_msg; + hl_msg_free(hl_msg); + return (HlMessage)KV_INITIAL_VALUE; } bool api_dict_to_keydict(void *rv, field_hash hashy, Dictionary dict, Error *err) @@ -1350,8 +914,9 @@ bool set_mark(buf_T *buf, String name, Integer line, Integer col, Error *err) return res; } } - pos_T pos = { line, (int)col, (int)col }; - res = setmark_pos(*name.data, &pos, buf->handle); + assert(INT32_MIN <= line && line <= INT32_MAX); + pos_T pos = { (linenr_T)line, (int)col, (int)col }; + res = setmark_pos(*name.data, &pos, buf->handle, NULL); if (!res) { if (deleting) { api_set_error(err, kErrorTypeException, @@ -1365,199 +930,43 @@ bool set_mark(buf_T *buf, String name, Integer line, Integer col, Error *err) } /// Get default statusline highlight for window -const char *get_default_stl_hl(win_T *wp) +const char *get_default_stl_hl(win_T *wp, bool use_winbar) { if (wp == NULL) { return "TabLineFill"; - } else if (wp == curwin) { - return "StatusLine"; + } else if (use_winbar) { + return (wp == curwin) ? "WinBar" : "WinBarNC"; } else { - return "StatusLineNC"; + return (wp == curwin) ? "StatusLine" : "StatusLineNC"; } } -void add_user_command(String name, Object command, Dict(user_command) *opts, int flags, Error *err) +int find_sid(uint64_t channel_id) { - uint32_t argt = 0; - long def = -1; - cmd_addr_T addr_type_arg = ADDR_NONE; - int compl = EXPAND_NOTHING; - char *compl_arg = NULL; - char *rep = NULL; - LuaRef luaref = LUA_NOREF; - LuaRef compl_luaref = LUA_NOREF; - - if (mb_islower(name.data[0])) { - api_set_error(err, kErrorTypeValidation, "'name' must begin with an uppercase letter"); - goto err; - } - - if (HAS_KEY(opts->range) && HAS_KEY(opts->count)) { - api_set_error(err, kErrorTypeValidation, "'range' and 'count' are mutually exclusive"); - goto err; - } - - if (opts->nargs.type == kObjectTypeInteger) { - switch (opts->nargs.data.integer) { - case 0: - // Default value, nothing to do - break; - case 1: - argt |= EX_EXTRA | EX_NOSPC | EX_NEEDARG; - break; - default: - api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); - goto err; - } - } else if (opts->nargs.type == kObjectTypeString) { - if (opts->nargs.data.string.size > 1) { - api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); - goto err; - } - - switch (opts->nargs.data.string.data[0]) { - case '*': - argt |= EX_EXTRA; - break; - case '?': - argt |= EX_EXTRA | EX_NOSPC; - break; - case '+': - argt |= EX_EXTRA | EX_NEEDARG; - break; - default: - api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); - goto err; - } - } else if (HAS_KEY(opts->nargs)) { - api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); - goto err; - } - - if (HAS_KEY(opts->complete) && !argt) { - api_set_error(err, kErrorTypeValidation, "'complete' used without 'nargs'"); - goto err; - } - - if (opts->range.type == kObjectTypeBoolean) { - if (opts->range.data.boolean) { - argt |= EX_RANGE; - addr_type_arg = ADDR_LINES; - } - } else if (opts->range.type == kObjectTypeString) { - if (opts->range.data.string.data[0] == '%' && opts->range.data.string.size == 1) { - argt |= EX_RANGE | EX_DFLALL; - addr_type_arg = ADDR_LINES; - } else { - api_set_error(err, kErrorTypeValidation, "Invalid value for 'range'"); - goto err; - } - } else if (opts->range.type == kObjectTypeInteger) { - argt |= EX_RANGE | EX_ZEROR; - def = opts->range.data.integer; - addr_type_arg = ADDR_LINES; - } else if (HAS_KEY(opts->range)) { - api_set_error(err, kErrorTypeValidation, "Invalid value for 'range'"); - goto err; - } - - if (opts->count.type == kObjectTypeBoolean) { - if (opts->count.data.boolean) { - argt |= EX_COUNT | EX_ZEROR | EX_RANGE; - addr_type_arg = ADDR_OTHER; - def = 0; - } - } else if (opts->count.type == kObjectTypeInteger) { - argt |= EX_COUNT | EX_ZEROR | EX_RANGE; - addr_type_arg = ADDR_OTHER; - def = opts->count.data.integer; - } else if (HAS_KEY(opts->count)) { - api_set_error(err, kErrorTypeValidation, "Invalid value for 'count'"); - goto err; - } - - if (opts->addr.type == kObjectTypeString) { - if (parse_addr_type_arg((char_u *)opts->addr.data.string.data, (int)opts->addr.data.string.size, - &addr_type_arg) != OK) { - api_set_error(err, kErrorTypeValidation, "Invalid value for 'addr'"); - goto err; - } - - if (addr_type_arg != ADDR_LINES) { - argt |= EX_ZEROR; - } - } else if (HAS_KEY(opts->addr)) { - api_set_error(err, kErrorTypeValidation, "Invalid value for 'addr'"); - goto err; - } - - if (api_object_to_bool(opts->bang, "bang", false, err)) { - argt |= EX_BANG; - } else if (ERROR_SET(err)) { - goto err; - } - - if (api_object_to_bool(opts->bar, "bar", false, err)) { - argt |= EX_TRLBAR; - } else if (ERROR_SET(err)) { - goto err; - } - - - if (api_object_to_bool(opts->register_, "register", false, err)) { - argt |= EX_REGSTR; - } else if (ERROR_SET(err)) { - goto err; - } - - bool force = api_object_to_bool(opts->force, "force", true, err); - if (ERROR_SET(err)) { - goto err; - } - - if (opts->complete.type == kObjectTypeLuaRef) { - compl = EXPAND_USER_LUA; - compl_luaref = api_new_luaref(opts->complete.data.luaref); - } else if (opts->complete.type == kObjectTypeString) { - if (parse_compl_arg((char_u *)opts->complete.data.string.data, - (int)opts->complete.data.string.size, &compl, &argt, - (char_u **)&compl_arg) != OK) { - api_set_error(err, kErrorTypeValidation, "Invalid value for 'complete'"); - goto err; - } - } else if (HAS_KEY(opts->complete)) { - api_set_error(err, kErrorTypeValidation, "Invalid value for 'complete'"); - goto err; - } - - switch (command.type) { - case kObjectTypeLuaRef: - luaref = api_new_luaref(command.data.luaref); - if (opts->desc.type == kObjectTypeString) { - rep = opts->desc.data.string.data; - } else { - snprintf((char *)IObuff, IOSIZE, "<Lua function %d>", luaref); - rep = (char *)IObuff; - } - break; - case kObjectTypeString: - rep = command.data.string.data; - break; + switch (channel_id) { + case VIML_INTERNAL_CALL: + // TODO(autocmd): Figure out what this should be + // return SID_API_CLIENT; + case LUA_INTERNAL_CALL: + return SID_LUA; default: - api_set_error(err, kErrorTypeValidation, "'command' must be a string or Lua function"); - goto err; - } - - if (uc_add_command((char_u *)name.data, name.size, (char_u *)rep, argt, def, flags, - compl, (char_u *)compl_arg, compl_luaref, addr_type_arg, luaref, - force) != OK) { - api_set_error(err, kErrorTypeException, "Failed to create user command"); - goto err; + return SID_API_CLIENT; } +} - return; - -err: - NLUA_CLEAR_REF(luaref); - NLUA_CLEAR_REF(compl_luaref); +/// Sets sctx for API calls. +/// +/// @param channel_id api clients id. Used to determine if it's a internal +/// call or a rpc call. +/// @return returns previous value of current_sctx. To be used +/// to be used for restoring sctx to previous state. +sctx_T api_set_sctx(uint64_t channel_id) +{ + sctx_T old_current_sctx = current_sctx; + if (channel_id != VIML_INTERNAL_CALL) { + current_sctx.sc_sid = + channel_id == LUA_INTERNAL_CALL ? SID_LUA : SID_API_CLIENT; + current_sctx.sc_lnum = 0; + } + return old_current_sctx; } diff --git a/src/nvim/api/private/helpers.h b/src/nvim/api/private/helpers.h index 6d0aec9c90..a4348d8b44 100644 --- a/src/nvim/api/private/helpers.h +++ b/src/nvim/api/private/helpers.h @@ -63,17 +63,31 @@ #define PUT(dict, k, v) \ kv_push(dict, ((KeyValuePair) { .key = cstr_to_string(k), .value = v })) +#define PUT_C(dict, k, v) \ + kv_push_c(dict, ((KeyValuePair) { .key = cstr_as_string(k), .value = v })) + #define PUT_BOOL(dict, name, condition) PUT(dict, name, BOOLEAN_OBJ(condition)); #define ADD(array, item) \ kv_push(array, item) +#define ADD_C(array, item) \ + kv_push_c(array, item) + #define FIXED_TEMP_ARRAY(name, fixsize) \ Array name = ARRAY_DICT_INIT; \ Object name##__items[fixsize]; \ name.size = fixsize; \ name.items = name##__items; \ +#define MAXSIZE_TEMP_ARRAY(name, maxsize) \ + Array name = ARRAY_DICT_INIT; \ + Object name##__items[maxsize]; \ + name.capacity = maxsize; \ + name.items = name##__items; \ + +#define cbuf_as_string(d, s) ((String) { .data = d, .size = s }) + #define STATIC_CSTR_AS_STRING(s) ((String) { .data = s, .size = sizeof(s) - 1 }) /// Create a new String instance, putting data in allocated memory @@ -138,10 +152,28 @@ typedef struct { msg_list = saved_msg_list; /* Restore the exception context. */ \ } while (0) +// Useful macro for executing some `code` for each item in an array. +#define FOREACH_ITEM(a, __foreach_item, code) \ + for (size_t (__foreach_item##_index) = 0; (__foreach_item##_index) < (a).size; \ + (__foreach_item##_index)++) { \ + Object __foreach_item = (a).items[__foreach_item##_index]; \ + code; \ + } + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/private/helpers.h.generated.h" # include "keysets.h.generated.h" #endif +#define WITH_SCRIPT_CONTEXT(channel_id, code) \ + do { \ + const sctx_T save_current_sctx = current_sctx; \ + current_sctx.sc_sid = \ + (channel_id) == LUA_INTERNAL_CALL ? SID_LUA : SID_API_CLIENT; \ + current_sctx.sc_lnum = 0; \ + current_channel_id = channel_id; \ + code; \ + current_sctx = save_current_sctx; \ + } while (0); #endif // NVIM_API_PRIVATE_HELPERS_H diff --git a/src/nvim/api/tabpage.c b/src/nvim/api/tabpage.c index 14b6be8eeb..b81fc3b7d7 100644 --- a/src/nvim/api/tabpage.c +++ b/src/nvim/api/tabpage.c @@ -102,11 +102,10 @@ void nvim_tabpage_del_var(Tabpage tabpage, String name, Error *err) Window nvim_tabpage_get_win(Tabpage tabpage, Error *err) FUNC_API_SINCE(1) { - Window rv = 0; tabpage_T *tab = find_tab_by_handle(tabpage, err); if (!tab || !valid_tabpage(tab)) { - return rv; + return 0; } if (tab == curtab) { @@ -130,11 +129,10 @@ Window nvim_tabpage_get_win(Tabpage tabpage, Error *err) Integer nvim_tabpage_get_number(Tabpage tabpage, Error *err) FUNC_API_SINCE(1) { - Integer rv = 0; tabpage_T *tab = find_tab_by_handle(tabpage, err); if (!tab) { - return rv; + return 0; } return tabpage_index(tab); @@ -152,4 +150,3 @@ Boolean nvim_tabpage_is_valid(Tabpage tabpage) api_clear_error(&stub); return ret; } - diff --git a/src/nvim/api/ui.c b/src/nvim/api/ui.c index d86aecc318..54ce838b9b 100644 --- a/src/nvim/api/ui.c +++ b/src/nvim/api/ui.c @@ -9,25 +9,46 @@ #include "nvim/api/private/defs.h" #include "nvim/api/private/helpers.h" #include "nvim/api/ui.h" +#include "nvim/channel.h" #include "nvim/cursor_shape.h" #include "nvim/highlight.h" #include "nvim/map.h" #include "nvim/memory.h" #include "nvim/msgpack_rpc/channel.h" +#include "nvim/msgpack_rpc/helpers.h" +#include "nvim/option.h" #include "nvim/popupmnu.h" #include "nvim/screen.h" #include "nvim/ui.h" #include "nvim/vim.h" #include "nvim/window.h" -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "api/ui.c.generated.h" -# include "ui_events_remote.generated.h" -#endif - typedef struct { uint64_t channel_id; - Array buffer; + +#define UI_BUF_SIZE 4096 ///< total buffer size for pending msgpack data. + /// guranteed size available for each new event (so packing of simple events + /// and the header of grid_line will never fail) +#define EVENT_BUF_SIZE 256 + char buf[UI_BUF_SIZE]; ///< buffer of packed but not yet sent msgpack data + char *buf_wptr; ///< write head of buffer + const char *cur_event; ///< name of current event (might get multiple arglists) + Array call_buf; ///< buffer for constructing a single arg list (max 16 elements!) + + // state for write_cb, while packing a single arglist to msgpack. This + // might fail due to buffer overflow. + size_t pack_totlen; + bool buf_overflow; + char *temp_buf; + + // We start packing the two outermost msgpack arrays before knowing the total + // number of elements. Thus track the location where array size will need + // to be written in the msgpack buffer, once the specifc array is finished. + char *nevents_pos; + char *ncalls_pos; + uint32_t nevents; ///< number of distinct events (top-level args to "redraw" + uint32_t ncalls; ///< number of calls made to the current event (plus one for the name!) + bool flushed_events; ///< events where sent to client without "flush" event int hl_id; // Current highlight for legacy put event. Integer cursor_row, cursor_col; // Intended visible cursor position. @@ -37,8 +58,76 @@ typedef struct { bool wildmenu_active; } UIData; +#define BUF_POS(data) ((size_t)((data)->buf_wptr - (data)->buf)) + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "api/ui.c.generated.h" +# include "ui_events_remote.generated.h" +#endif + static PMap(uint64_t) connected_uis = MAP_INIT; +#define mpack_w(b, byte) *(*b)++ = (char)(byte); +static void mpack_w2(char **b, uint32_t v) +{ + *(*b)++ = (char)((v >> 8) & 0xff); + *(*b)++ = (char)(v & 0xff); +} + +static void mpack_w4(char **b, uint32_t v) +{ + *(*b)++ = (char)((v >> 24) & 0xff); + *(*b)++ = (char)((v >> 16) & 0xff); + *(*b)++ = (char)((v >> 8) & 0xff); + *(*b)++ = (char)(v & 0xff); +} + +static void mpack_uint(char **buf, uint32_t val) +{ + if (val > 0xffff) { + mpack_w(buf, 0xce); + mpack_w4(buf, val); + } else if (val > 0xff) { + mpack_w(buf, 0xcd); + mpack_w2(buf, val); + } else if (val > 0x7f) { + mpack_w(buf, 0xcc); + mpack_w(buf, val); + } else { + mpack_w(buf, val); + } +} + +static void mpack_array(char **buf, uint32_t len) +{ + if (len < 0x10) { + mpack_w(buf, 0x90 | len); + } else if (len < 0x10000) { + mpack_w(buf, 0xdc); + mpack_w2(buf, len); + } else { + mpack_w(buf, 0xdd); + mpack_w4(buf, len); + } +} + +static char *mpack_array_dyn16(char **buf) +{ + mpack_w(buf, 0xdc); + char *pos = *buf; + mpack_w2(buf, 0xFFEF); + return pos; +} + +static void mpack_str(char **buf, const char *str) +{ + assert(sizeof(schar_T) - 1 < 0x20); + size_t len = STRLEN(str); + mpack_w(buf, 0xa0 | len); + memcpy(*buf, str, len); + *buf += len; +} + void remote_ui_disconnect(uint64_t channel_id) { UI *ui = pmap_get(uint64_t)(&connected_uis, channel_id); @@ -46,9 +135,9 @@ void remote_ui_disconnect(uint64_t channel_id) return; } UIData *data = ui->data; - api_free_array(data->buffer); // Destroy pending screen updates. + kv_destroy(data->call_buf); pmap_del(uint64_t)(&connected_uis, channel_id); - xfree(ui->data); + xfree(data); ui->data = NULL; // Flag UI as "stopped". ui_detach_impl(ui, channel_id); xfree(ui); @@ -158,10 +247,19 @@ void nvim_ui_attach(uint64_t channel_id, Integer width, Integer height, Dictiona UIData *data = xmalloc(sizeof(UIData)); data->channel_id = channel_id; - data->buffer = (Array)ARRAY_DICT_INIT; + data->cur_event = NULL; data->hl_id = 0; data->client_col = -1; + data->nevents_pos = NULL; + data->nevents = 0; + data->flushed_events = false; + data->ncalls_pos = NULL; + data->ncalls = 0; + data->buf_wptr = data->buf; + data->temp_buf = NULL; data->wildmenu_active = false; + data->call_buf = (Array)ARRAY_DICT_INIT; + kv_ensure_space(data->call_buf, 16); ui->data = data; pmap_put(uint64_t)(&connected_uis, channel_id, ui); @@ -195,7 +293,6 @@ void nvim_ui_detach(uint64_t channel_id, Error *err) remote_ui_disconnect(channel_id); } - void nvim_ui_try_resize(uint64_t channel_id, Integer width, Integer height, Error *err) FUNC_API_SINCE(1) FUNC_API_REMOTE_ONLY { @@ -255,6 +352,49 @@ static void ui_set_option(UI *ui, bool init, String name, Object value, Error *e return; } + if (strequal(name.data, "term_name")) { + if (value.type != kObjectTypeString) { + api_set_error(error, kErrorTypeValidation, "term_name must be a String"); + return; + } + set_tty_option("term", string_to_cstr(value.data.string)); + return; + } + + if (strequal(name.data, "term_colors")) { + if (value.type != kObjectTypeInteger) { + api_set_error(error, kErrorTypeValidation, "term_colors must be a Integer"); + return; + } + t_colors = (int)value.data.integer; + return; + } + + if (strequal(name.data, "term_background")) { + if (value.type != kObjectTypeString) { + api_set_error(error, kErrorTypeValidation, "term_background must be a String"); + return; + } + set_tty_background(value.data.string.data); + return; + } + + if (strequal(name.data, "stdin_fd")) { + if (value.type != kObjectTypeInteger || value.data.integer < 0) { + api_set_error(error, kErrorTypeValidation, "stdin_fd must be a non-negative Integer"); + return; + } + + if (starting != NO_SCREEN) { + api_set_error(error, kErrorTypeValidation, + "stdin_fd can only be used with first attached ui"); + return; + } + + stdin_fd = (int)value.data.integer; + return; + } + // LEGACY: Deprecated option, use `ext_cmdline` instead. bool is_popupmenu = strequal(name.data, "popupmenu_external"); @@ -305,7 +445,11 @@ void nvim_ui_try_resize_grid(uint64_t channel_id, Integer grid, Integer width, I return; } - ui_grid_resize((handle_T)grid, (int)width, (int)height, err); + if (grid == DEFAULT_GRID_HANDLE) { + nvim_ui_try_resize(channel_id, width, height, err); + } else { + ui_grid_resize((handle_T)grid, (int)width, (int)height, err); + } } /// Tells Nvim the number of elements displaying in the popumenu, to decide @@ -386,34 +530,128 @@ void nvim_ui_pum_set_bounds(uint64_t channel_id, Float width, Float height, Floa ui->pum_pos = true; } -/// Pushes data into UI.UIData, to be consumed later by remote_ui_flush(). -static void push_call(UI *ui, const char *name, Array args) +static void flush_event(UIData *data) +{ + if (data->cur_event) { + mpack_w2(&data->ncalls_pos, data->ncalls); + data->cur_event = NULL; + } + if (!data->nevents_pos) { + assert(BUF_POS(data) == 0); + char **buf = &data->buf_wptr; + // [2, "redraw", [...]] + mpack_array(buf, 3); + mpack_uint(buf, 2); + mpack_str(buf, "redraw"); + data->nevents_pos = mpack_array_dyn16(buf); + } +} + +static inline int write_cb(void *vdata, const char *buf, size_t len) +{ + UIData *data = (UIData *)vdata; + if (!buf) { + return 0; + } + + data->pack_totlen += len; + if (!data->temp_buf && UI_BUF_SIZE - BUF_POS(data) < len) { + data->buf_overflow = true; + return 0; + } + + memcpy(data->buf_wptr, buf, len); + data->buf_wptr += len; + + return 0; +} + +static bool prepare_call(UI *ui, const char *name) { - Array call = ARRAY_DICT_INIT; UIData *data = ui->data; - // To optimize data transfer(especially for "put"), we bundle adjacent + if (BUF_POS(data) > UI_BUF_SIZE - EVENT_BUF_SIZE) { + remote_ui_flush_buf(ui); + } + + // To optimize data transfer(especially for "grid_line"), we bundle adjacent // calls to same method together, so only add a new call entry if the last // method call is different from "name" - if (kv_size(data->buffer)) { - call = kv_A(data->buffer, kv_size(data->buffer) - 1).data.array; - } - if (!kv_size(call) || strcmp(kv_A(call, 0).data.string.data, name)) { - call = (Array)ARRAY_DICT_INIT; - ADD(data->buffer, ARRAY_OBJ(call)); - ADD(call, STRING_OBJ(cstr_to_string(name))); + if (!data->cur_event || !strequal(data->cur_event, name)) { + flush_event(data); + data->cur_event = name; + char **buf = &data->buf_wptr; + data->ncalls_pos = mpack_array_dyn16(buf); + mpack_str(buf, name); + data->nevents++; + data->ncalls = 1; + return true; } - ADD(call, ARRAY_OBJ(args)); - kv_A(data->buffer, kv_size(data->buffer) - 1).data.array = call; + return false; +} + +/// Pushes data into UI.UIData, to be consumed later by remote_ui_flush(). +static void push_call(UI *ui, const char *name, Array args) +{ + UIData *data = ui->data; + bool pending = data->nevents_pos; + char *buf_pos_save = data->buf_wptr; + + bool new_event = prepare_call(ui, name); + + msgpack_packer pac; + data->pack_totlen = 0; + data->buf_overflow = false; + msgpack_packer_init(&pac, data, write_cb); + msgpack_rpc_from_array(args, &pac); + if (data->buf_overflow) { + data->buf_wptr = buf_pos_save; + if (new_event) { + data->cur_event = NULL; + data->nevents--; + } + if (pending) { + remote_ui_flush_buf(ui); + } + + if (data->pack_totlen > UI_BUF_SIZE - strlen(name) - 20) { + // TODO(bfredl): manually testable by setting UI_BUF_SIZE to 1024 (mode_info_set) + data->temp_buf = xmalloc(20 + strlen(name) + data->pack_totlen); + data->buf_wptr = data->temp_buf; + char **buf = &data->buf_wptr; + mpack_array(buf, 3); + mpack_uint(buf, 2); + mpack_str(buf, "redraw"); + mpack_array(buf, 1); + mpack_array(buf, 2); + mpack_str(buf, name); + } else { + prepare_call(ui, name); + } + data->pack_totlen = 0; + data->buf_overflow = false; + msgpack_rpc_from_array(args, &pac); + + if (data->temp_buf) { + size_t size = (size_t)(data->buf_wptr - data->temp_buf); + WBuffer *buf = wstream_new_buffer(data->temp_buf, size, 1, xfree); + rpc_write_raw(data->channel_id, buf); + data->temp_buf = NULL; + data->buf_wptr = data->buf; + data->nevents_pos = NULL; + } + } + data->ncalls++; } static void remote_ui_grid_clear(UI *ui, Integer grid) { - Array args = ARRAY_DICT_INIT; + UIData *data = ui->data; + Array args = data->call_buf; if (ui->ui_ext[kUILinegrid]) { - ADD(args, INTEGER_OBJ(grid)); + ADD_C(args, INTEGER_OBJ(grid)); } const char *name = ui->ui_ext[kUILinegrid] ? "grid_clear" : "clear"; push_call(ui, name, args); @@ -421,12 +659,13 @@ static void remote_ui_grid_clear(UI *ui, Integer grid) static void remote_ui_grid_resize(UI *ui, Integer grid, Integer width, Integer height) { - Array args = ARRAY_DICT_INIT; + UIData *data = ui->data; + Array args = data->call_buf; if (ui->ui_ext[kUILinegrid]) { - ADD(args, INTEGER_OBJ(grid)); + ADD_C(args, INTEGER_OBJ(grid)); } - ADD(args, INTEGER_OBJ(width)); - ADD(args, INTEGER_OBJ(height)); + ADD_C(args, INTEGER_OBJ(width)); + ADD_C(args, INTEGER_OBJ(height)); const char *name = ui->ui_ext[kUILinegrid] ? "grid_resize" : "resize"; push_call(ui, name, args); } @@ -434,35 +673,36 @@ static void remote_ui_grid_resize(UI *ui, Integer grid, Integer width, Integer h static void remote_ui_grid_scroll(UI *ui, Integer grid, Integer top, Integer bot, Integer left, Integer right, Integer rows, Integer cols) { + UIData *data = ui->data; if (ui->ui_ext[kUILinegrid]) { - Array args = ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(grid)); - ADD(args, INTEGER_OBJ(top)); - ADD(args, INTEGER_OBJ(bot)); - ADD(args, INTEGER_OBJ(left)); - ADD(args, INTEGER_OBJ(right)); - ADD(args, INTEGER_OBJ(rows)); - ADD(args, INTEGER_OBJ(cols)); + Array args = data->call_buf; + ADD_C(args, INTEGER_OBJ(grid)); + ADD_C(args, INTEGER_OBJ(top)); + ADD_C(args, INTEGER_OBJ(bot)); + ADD_C(args, INTEGER_OBJ(left)); + ADD_C(args, INTEGER_OBJ(right)); + ADD_C(args, INTEGER_OBJ(rows)); + ADD_C(args, INTEGER_OBJ(cols)); push_call(ui, "grid_scroll", args); } else { - Array args = ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(top)); - ADD(args, INTEGER_OBJ(bot-1)); - ADD(args, INTEGER_OBJ(left)); - ADD(args, INTEGER_OBJ(right-1)); + Array args = data->call_buf; + ADD_C(args, INTEGER_OBJ(top)); + ADD_C(args, INTEGER_OBJ(bot - 1)); + ADD_C(args, INTEGER_OBJ(left)); + ADD_C(args, INTEGER_OBJ(right - 1)); push_call(ui, "set_scroll_region", args); - args = (Array)ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(rows)); + args = data->call_buf; + ADD_C(args, INTEGER_OBJ(rows)); push_call(ui, "scroll", args); // some clients have "clear" being affected by scroll region, // so reset it. - args = (Array)ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(0)); - ADD(args, INTEGER_OBJ(ui->height-1)); - ADD(args, INTEGER_OBJ(0)); - ADD(args, INTEGER_OBJ(ui->width-1)); + args = data->call_buf; + ADD_C(args, INTEGER_OBJ(0)); + ADD_C(args, INTEGER_OBJ(ui->height - 1)); + ADD_C(args, INTEGER_OBJ(0)); + ADD_C(args, INTEGER_OBJ(ui->width - 1)); push_call(ui, "set_scroll_region", args); } } @@ -473,26 +713,27 @@ static void remote_ui_default_colors_set(UI *ui, Integer rgb_fg, Integer rgb_bg, if (!ui->ui_ext[kUITermColors]) { HL_SET_DEFAULT_COLORS(rgb_fg, rgb_bg, rgb_sp); } - Array args = ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(rgb_fg)); - ADD(args, INTEGER_OBJ(rgb_bg)); - ADD(args, INTEGER_OBJ(rgb_sp)); - ADD(args, INTEGER_OBJ(cterm_fg)); - ADD(args, INTEGER_OBJ(cterm_bg)); + UIData *data = ui->data; + Array args = data->call_buf; + ADD_C(args, INTEGER_OBJ(rgb_fg)); + ADD_C(args, INTEGER_OBJ(rgb_bg)); + ADD_C(args, INTEGER_OBJ(rgb_sp)); + ADD_C(args, INTEGER_OBJ(cterm_fg)); + ADD_C(args, INTEGER_OBJ(cterm_bg)); push_call(ui, "default_colors_set", args); // Deprecated if (!ui->ui_ext[kUILinegrid]) { - args = (Array)ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(ui->rgb ? rgb_fg : cterm_fg - 1)); + args = data->call_buf; + ADD_C(args, INTEGER_OBJ(ui->rgb ? rgb_fg : cterm_fg - 1)); push_call(ui, "update_fg", args); - args = (Array)ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(ui->rgb ? rgb_bg : cterm_bg - 1)); + args = data->call_buf; + ADD_C(args, INTEGER_OBJ(ui->rgb ? rgb_bg : cterm_bg - 1)); push_call(ui, "update_bg", args); - args = (Array)ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(ui->rgb ? rgb_sp : -1)); + args = data->call_buf; + ADD_C(args, INTEGER_OBJ(ui->rgb ? rgb_sp : -1)); push_call(ui, "update_sp", args); } } @@ -503,26 +744,29 @@ static void remote_ui_hl_attr_define(UI *ui, Integer id, HlAttrs rgb_attrs, HlAt if (!ui->ui_ext[kUILinegrid]) { return; } - Array args = ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(id)); - ADD(args, DICTIONARY_OBJ(hlattrs2dict(rgb_attrs, true))); - ADD(args, DICTIONARY_OBJ(hlattrs2dict(cterm_attrs, false))); + UIData *data = ui->data; + Array args = data->call_buf; + ADD_C(args, INTEGER_OBJ(id)); + ADD_C(args, DICTIONARY_OBJ(hlattrs2dict(rgb_attrs, true))); + ADD_C(args, DICTIONARY_OBJ(hlattrs2dict(cterm_attrs, false))); if (ui->ui_ext[kUIHlState]) { - ADD(args, ARRAY_OBJ(copy_array(info))); + ADD_C(args, ARRAY_OBJ(info)); } else { - ADD(args, ARRAY_OBJ((Array)ARRAY_DICT_INIT)); + ADD_C(args, ARRAY_OBJ((Array)ARRAY_DICT_INIT)); } push_call(ui, "hl_attr_define", args); + // TODO(bfredl): could be elided + api_free_dictionary(kv_A(args, 1).data.dictionary); + api_free_dictionary(kv_A(args, 2).data.dictionary); } static void remote_ui_highlight_set(UI *ui, int id) { - Array args = ARRAY_DICT_INIT; UIData *data = ui->data; - + Array args = data->call_buf; if (data->hl_id == id) { return; @@ -530,18 +774,20 @@ static void remote_ui_highlight_set(UI *ui, int id) data->hl_id = id; Dictionary hl = hlattrs2dict(syn_attr2entry(id), ui->rgb); - ADD(args, DICTIONARY_OBJ(hl)); + ADD_C(args, DICTIONARY_OBJ(hl)); push_call(ui, "highlight_set", args); + api_free_dictionary(kv_A(args, 0).data.dictionary); } /// "true" cursor used only for input focus static void remote_ui_grid_cursor_goto(UI *ui, Integer grid, Integer row, Integer col) { if (ui->ui_ext[kUILinegrid]) { - Array args = ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(grid)); - ADD(args, INTEGER_OBJ(row)); - ADD(args, INTEGER_OBJ(col)); + UIData *data = ui->data; + Array args = data->call_buf; + ADD_C(args, INTEGER_OBJ(grid)); + ADD_C(args, INTEGER_OBJ(row)); + ADD_C(args, INTEGER_OBJ(col)); push_call(ui, "grid_cursor_goto", args); } else { UIData *data = ui->data; @@ -560,9 +806,9 @@ static void remote_ui_cursor_goto(UI *ui, Integer row, Integer col) } data->client_row = row; data->client_col = col; - Array args = ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(row)); - ADD(args, INTEGER_OBJ(col)); + Array args = data->call_buf; + ADD_C(args, INTEGER_OBJ(row)); + ADD_C(args, INTEGER_OBJ(col)); push_call(ui, "cursor_goto", args); } @@ -570,8 +816,8 @@ static void remote_ui_put(UI *ui, const char *cell) { UIData *data = ui->data; data->client_col++; - Array args = ARRAY_DICT_INIT; - ADD(args, STRING_OBJ(cstr_to_string(cell))); + Array args = data->call_buf; + ADD_C(args, STRING_OBJ(cstr_as_string((char *)cell))); push_call(ui, "put", args); } @@ -581,47 +827,70 @@ static void remote_ui_raw_line(UI *ui, Integer grid, Integer row, Integer startc { UIData *data = ui->data; if (ui->ui_ext[kUILinegrid]) { - Array args = ARRAY_DICT_INIT; - ADD(args, INTEGER_OBJ(grid)); - ADD(args, INTEGER_OBJ(row)); - ADD(args, INTEGER_OBJ(startcol)); - Array cells = ARRAY_DICT_INIT; - int repeat = 0; - size_t ncells = (size_t)(endcol-startcol); + prepare_call(ui, "grid_line"); + data->ncalls++; + + char **buf = &data->buf_wptr; + mpack_array(buf, 4); + mpack_uint(buf, (uint32_t)grid); + mpack_uint(buf, (uint32_t)row); + mpack_uint(buf, (uint32_t)startcol); + char *lenpos = mpack_array_dyn16(buf); + + uint32_t repeat = 0; + size_t ncells = (size_t)(endcol - startcol); int last_hl = -1; + uint32_t nelem = 0; for (size_t i = 0; i < ncells; i++) { repeat++; - if (i == ncells-1 || attrs[i] != attrs[i+1] - || STRCMP(chunk[i], chunk[i+1])) { - Array cell = ARRAY_DICT_INIT; - ADD(cell, STRING_OBJ(cstr_to_string((const char *)chunk[i]))); - if (attrs[i] != last_hl || repeat > 1) { - ADD(cell, INTEGER_OBJ(attrs[i])); - last_hl = attrs[i]; + if (i == ncells - 1 || attrs[i] != attrs[i + 1] + || STRCMP(chunk[i], chunk[i + 1])) { + if (UI_BUF_SIZE - BUF_POS(data) < 2 * (1 + 2 + sizeof(schar_T) + 5 + 5)) { + // close to overflowing the redraw buffer. finish this event, + // flush, and start a new "grid_line" event at the current position. + // For simplicity leave place for the final "clear" element + // as well, hence the factor of 2 in the check. + mpack_w2(&lenpos, nelem); + remote_ui_flush_buf(ui); + + prepare_call(ui, "grid_line"); + data->ncalls++; + mpack_array(buf, 4); + mpack_uint(buf, (uint32_t)grid); + mpack_uint(buf, (uint32_t)row); + mpack_uint(buf, (uint32_t)startcol + (uint32_t)i - repeat + 1); + lenpos = mpack_array_dyn16(buf); + nelem = 0; + last_hl = -1; } - if (repeat > 1) { - ADD(cell, INTEGER_OBJ(repeat)); + uint32_t csize = (repeat > 1) ? 3 : ((attrs[i] != last_hl) ? 2 : 1); + nelem++; + mpack_array(buf, csize); + mpack_str(buf, (const char *)chunk[i]); + if (csize >= 2) { + mpack_uint(buf, (uint32_t)attrs[i]); + if (csize >= 3) { + mpack_uint(buf, repeat); + } } - ADD(cells, ARRAY_OBJ(cell)); + last_hl = attrs[i]; repeat = 0; } } if (endcol < clearcol) { - Array cell = ARRAY_DICT_INIT; - ADD(cell, STRING_OBJ(cstr_to_string(" "))); - ADD(cell, INTEGER_OBJ(clearattr)); - ADD(cell, INTEGER_OBJ(clearcol-endcol)); - ADD(cells, ARRAY_OBJ(cell)); + nelem++; + mpack_array(buf, 3); + mpack_str(buf, " "); + mpack_uint(buf, (uint32_t)clearattr); + mpack_uint(buf, (uint32_t)(clearcol - endcol)); } - ADD(args, ARRAY_OBJ(cells)); - - push_call(ui, "grid_line", args); + mpack_w2(&lenpos, nelem); } else { - for (int i = 0; i < endcol-startcol; i++) { - remote_ui_cursor_goto(ui, row, startcol+i); + for (int i = 0; i < endcol - startcol; i++) { + remote_ui_cursor_goto(ui, row, startcol + i); remote_ui_highlight_set(ui, attrs[i]); remote_ui_put(ui, (const char *)chunk[i]); - if (utf_ambiguous_width(utf_ptr2char(chunk[i]))) { + if (utf_ambiguous_width(utf_ptr2char((char *)chunk[i]))) { data->client_col = -1; // force cursor update } } @@ -642,16 +911,47 @@ static void remote_ui_raw_line(UI *ui, Integer grid, Integer row, Integer startc } } +/// Flush the internal packing buffer to the client. +/// +/// This might happen multiple times before the actual ui_flush, if the +/// total redraw size is large! +static void remote_ui_flush_buf(UI *ui) +{ + UIData *data = ui->data; + if (!data->nevents_pos) { + return; + } + if (data->cur_event) { + flush_event(data); + } + mpack_w2(&data->nevents_pos, data->nevents); + data->nevents = 0; + data->nevents_pos = NULL; + + // TODO(bfredl): elide copy by a length one free-list like the arena + size_t size = BUF_POS(data); + WBuffer *buf = wstream_new_buffer(xmemdup(data->buf, size), size, 1, xfree); + rpc_write_raw(data->channel_id, buf); + data->buf_wptr = data->buf; + // we have sent events to the client, but possibly not yet the final "flush" + // event. + data->flushed_events = true; +} + +/// An intentional flush (vsync) when Nvim is finished redrawing the screen +/// +/// Clients can know this happened by a final "flush" event at the end of the +/// "redraw" batch. static void remote_ui_flush(UI *ui) { UIData *data = ui->data; - if (data->buffer.size > 0) { + if (data->nevents > 0 || data->flushed_events) { if (!ui->ui_ext[kUILinegrid]) { remote_ui_cursor_goto(ui, data->cursor_row, data->cursor_col); } push_call(ui, "flush", (Array)ARRAY_DICT_INIT); - rpc_send_event(data->channel_id, "redraw", data->buffer); - data->buffer = (Array)ARRAY_DICT_INIT; + remote_ui_flush_buf(ui); + data->flushed_events = false; } } @@ -686,7 +986,7 @@ static Array translate_firstarg(UI *ui, Array args) return new_args; } -static void remote_ui_event(UI *ui, char *name, Array args, bool *args_consumed) +static void remote_ui_event(UI *ui, char *name, Array args) { UIData *data = ui->data; if (!ui->ui_ext[kUILinegrid]) { @@ -695,21 +995,24 @@ static void remote_ui_event(UI *ui, char *name, Array args, bool *args_consumed) if (strequal(name, "cmdline_show")) { Array new_args = translate_firstarg(ui, args); push_call(ui, name, new_args); + api_free_array(new_args); return; } else if (strequal(name, "cmdline_block_show")) { - Array new_args = ARRAY_DICT_INIT; + Array new_args = data->call_buf; Array block = args.items[0].data.array; Array new_block = ARRAY_DICT_INIT; for (size_t i = 0; i < block.size; i++) { ADD(new_block, ARRAY_OBJ(translate_contents(ui, block.items[i].data.array))); } - ADD(new_args, ARRAY_OBJ(new_block)); + ADD_C(new_args, ARRAY_OBJ(new_block)); push_call(ui, name, new_args); + api_free_array(new_block); return; } else if (strequal(name, "cmdline_block_append")) { Array new_args = translate_firstarg(ui, args); push_call(ui, name, new_args); + api_free_array(new_args); return; } } @@ -720,18 +1023,19 @@ static void remote_ui_event(UI *ui, char *name, Array args, bool *args_consumed) data->wildmenu_active = (args.items[4].data.integer == -1) || !ui->ui_ext[kUIPopupmenu]; if (data->wildmenu_active) { - Array new_args = ARRAY_DICT_INIT; + Array new_args = data->call_buf; Array items = args.items[0].data.array; Array new_items = ARRAY_DICT_INIT; for (size_t i = 0; i < items.size; i++) { ADD(new_items, copy_object(items.items[i].data.array.items[0])); } - ADD(new_args, ARRAY_OBJ(new_items)); + ADD_C(new_args, ARRAY_OBJ(new_items)); push_call(ui, "wildmenu_show", new_args); + api_free_array(new_items); if (args.items[1].data.integer != -1) { - Array new_args2 = ARRAY_DICT_INIT; - ADD(new_args2, args.items[1]); - push_call(ui, "wildmenu_select", new_args); + Array new_args2 = data->call_buf; + ADD_C(new_args2, args.items[1]); + push_call(ui, "wildmenu_select", new_args2); } return; } @@ -746,19 +1050,7 @@ static void remote_ui_event(UI *ui, char *name, Array args, bool *args_consumed) } } - - Array my_args = ARRAY_DICT_INIT; - // Objects are currently single-reference - // make a copy, but only if necessary - if (*args_consumed) { - for (size_t i = 0; i < args.size; i++) { - ADD(my_args, copy_object(args.items[i])); - } - } else { - my_args = args; - *args_consumed = true; - } - push_call(ui, name, my_args); + push_call(ui, name, args); } static void remote_ui_inspect(UI *ui, Dictionary *info) diff --git a/src/nvim/api/ui.h b/src/nvim/api/ui.h index b3af14f8a8..bc70406acb 100644 --- a/src/nvim/api/ui.h +++ b/src/nvim/api/ui.h @@ -4,6 +4,7 @@ #include <stdint.h> #include "nvim/api/private/defs.h" +#include "nvim/map.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/ui.h.generated.h" diff --git a/src/nvim/api/ui_events.in.h b/src/nvim/api/ui_events.in.h index 03fe5c5058..8b7e01e1c3 100644 --- a/src/nvim/api/ui_events.in.h +++ b/src/nvim/api/ui_events.in.h @@ -78,13 +78,13 @@ void hl_attr_define(Integer id, HlAttrs rgb_attrs, HlAttrs cterm_attrs, void hl_group_set(String name, Integer id) FUNC_API_SINCE(6) FUNC_API_BRIDGE_IMPL; void grid_resize(Integer grid, Integer width, Integer height) - FUNC_API_SINCE(5) FUNC_API_REMOTE_IMPL FUNC_API_COMPOSITOR_IMPL; + FUNC_API_SINCE(5) FUNC_API_REMOTE_IMPL FUNC_API_COMPOSITOR_IMPL FUNC_API_CLIENT_IMPL; void grid_clear(Integer grid) FUNC_API_SINCE(5) FUNC_API_REMOTE_IMPL; void grid_cursor_goto(Integer grid, Integer row, Integer col) FUNC_API_SINCE(5) FUNC_API_REMOTE_IMPL FUNC_API_COMPOSITOR_IMPL; void grid_line(Integer grid, Integer row, Integer col_start, Array data) - FUNC_API_SINCE(5) FUNC_API_REMOTE_ONLY; + FUNC_API_SINCE(5) FUNC_API_REMOTE_ONLY FUNC_API_CLIENT_IMPL; void grid_scroll(Integer grid, Integer top, Integer bot, Integer left, Integer right, Integer rows, Integer cols) FUNC_API_SINCE(5) FUNC_API_REMOTE_IMPL FUNC_API_COMPOSITOR_IMPL; @@ -99,7 +99,7 @@ void raw_line(Integer grid, Integer row, Integer startcol, LineFlags flags, const schar_T *chunk, const sattr_T *attrs) FUNC_API_NOEXPORT FUNC_API_COMPOSITOR_IMPL; -void event(char *name, Array args, bool *args_consumed) +void event(char *name, Array args) FUNC_API_NOEXPORT; void win_pos(Integer grid, Window win, Integer startrow, @@ -123,6 +123,10 @@ void win_viewport(Integer grid, Window win, Integer topline, Integer line_count) FUNC_API_SINCE(7) FUNC_API_REMOTE_ONLY; +void win_extmark(Integer grid, Window win, Integer ns_id, Integer mark_id, + Integer row, Integer col) + FUNC_API_SINCE(10) FUNC_API_REMOTE_ONLY; + void popupmenu_show(Array items, Integer selected, Integer row, Integer col, Integer grid) FUNC_API_SINCE(3) FUNC_API_REMOTE_ONLY; @@ -170,4 +174,6 @@ void msg_ruler(Array content) FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY; void msg_history_show(Array entries) FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY; +void msg_history_clear(void) + FUNC_API_SINCE(10) FUNC_API_REMOTE_ONLY; #endif // NVIM_API_UI_EVENTS_IN_H diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 59db12f2c0..56516b2ac7 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -19,19 +19,26 @@ #include "nvim/ascii.h" #include "nvim/buffer.h" #include "nvim/buffer_defs.h" +#include "nvim/charset.h" #include "nvim/context.h" #include "nvim/decoration.h" +#include "nvim/decoration_provider.h" #include "nvim/edit.h" #include "nvim/eval.h" #include "nvim/eval/typval.h" #include "nvim/eval/userfunc.h" #include "nvim/ex_cmds2.h" +#include "nvim/ex_cmds_defs.h" #include "nvim/ex_docmd.h" #include "nvim/file_search.h" #include "nvim/fileio.h" #include "nvim/getchar.h" +#include "nvim/globals.h" #include "nvim/highlight.h" +#include "nvim/highlight_defs.h" +#include "nvim/highlight_group.h" #include "nvim/lua/executor.h" +#include "nvim/mapping.h" #include "nvim/mark.h" #include "nvim/memline.h" #include "nvim/memory.h" @@ -39,6 +46,7 @@ #include "nvim/move.h" #include "nvim/msgpack_rpc/channel.h" #include "nvim/msgpack_rpc/helpers.h" +#include "nvim/msgpack_rpc/unpacker.h" #include "nvim/ops.h" #include "nvim/option.h" #include "nvim/os/input.h" @@ -46,7 +54,6 @@ #include "nvim/popupmnu.h" #include "nvim/screen.h" #include "nvim/state.h" -#include "nvim/syntax.h" #include "nvim/types.h" #include "nvim/ui.h" #include "nvim/vim.h" @@ -108,7 +115,7 @@ Dictionary nvim_get_hl_by_id(Integer hl_id, Boolean rgb, Error *err) Integer nvim_get_hl_id_by_name(String name) FUNC_API_SINCE(7) { - return syn_check_group(name.data, (int)name.size); + return syn_check_group(name.data, name.size); } Dictionary nvim__get_hl_defs(Integer ns_id, Error *err) @@ -119,33 +126,56 @@ Dictionary nvim__get_hl_defs(Integer ns_id, Error *err) abort(); } -/// Set a highlight group. -/// -/// @param ns_id number of namespace for this highlight -/// @param name highlight group name, like ErrorMsg -/// @param val highlight definition map, like |nvim_get_hl_by_name|. -/// in addition the following keys are also recognized: -/// `default`: don't override existing definition, -/// like `hi default` -/// `ctermfg`: sets foreground of cterm color -/// `ctermbg`: sets background of cterm color -/// `cterm` : cterm attribute map. sets attributed for -/// cterm colors. similer to `hi cterm` -/// Note: by default cterm attributes are -/// same as attributes of gui color +/// Sets a highlight group. +/// +/// @note Unlike the `:highlight` command which can update a highlight group, +/// this function completely replaces the definition. For example: +/// ``nvim_set_hl(0, 'Visual', {})`` will clear the highlight group +/// 'Visual'. +/// +/// @note The fg and bg keys also accept the string values `"fg"` or `"bg"` +/// which act as aliases to the corresponding foreground and background +/// values of the Normal group. If the Normal group has not been defined, +/// using these values results in an error. +/// +/// @param ns_id Namespace id for this highlight |nvim_create_namespace()|. +/// Use 0 to set a highlight group globally |:highlight|. +/// @param name Highlight group name, e.g. "ErrorMsg" +/// @param val Highlight definition map, accepts the following keys: +/// - fg (or foreground): color name or "#RRGGBB", see note. +/// - bg (or background): color name or "#RRGGBB", see note. +/// - sp (or special): color name or "#RRGGBB" +/// - blend: integer between 0 and 100 +/// - bold: boolean +/// - standout: boolean +/// - underline: boolean +/// - undercurl: boolean +/// - underdouble: boolean +/// - underdotted: boolean +/// - underdashed: boolean +/// - strikethrough: boolean +/// - italic: boolean +/// - reverse: boolean +/// - nocombine: boolean +/// - link: name of another highlight group to link to, see |:hi-link|. +/// - default: Don't override existing definition |:hi-default| +/// - ctermfg: Sets foreground of cterm color |highlight-ctermfg| +/// - ctermbg: Sets background of cterm color |highlight-ctermbg| +/// - cterm: cterm attribute map, like |highlight-args|. If not set, +/// cterm attributes will match those from the attribute map +/// documented above. /// @param[out] err Error details, if any /// -/// TODO: ns_id = 0, should modify :highlight namespace -/// TODO val should take update vs reset flag -void nvim_set_hl(Integer ns_id, String name, Dictionary val, Error *err) +// TODO(bfredl): val should take update vs reset flag +void nvim_set_hl(Integer ns_id, String name, Dict(highlight) *val, Error *err) FUNC_API_SINCE(7) { - int hl_id = syn_check_group(name.data, (int)name.size); + int hl_id = syn_check_group(name.data, name.size); int link_id = -1; HlAttrs attrs = dict2hlattrs(val, true, &link_id, err); if (!ERROR_SET(err)) { - ns_hl_def((NS)ns_id, hl_id, attrs, link_id); + ns_hl_def((NS)ns_id, hl_id, attrs, link_id, val); } } @@ -180,28 +210,29 @@ static void on_redraw_event(void **argv) redraw_all_later(NOT_VALID); } - /// Sends input-keys to Nvim, subject to various quirks controlled by `mode` /// flags. This is a blocking call, unlike |nvim_input()|. /// /// On execution error: does not fail, but updates v:errmsg. /// /// To input sequences like <C-o> use |nvim_replace_termcodes()| (typically -/// with escape_csi=true) to replace |keycodes|, then pass the result to +/// with escape_ks=false) to replace |keycodes|, then pass the result to /// nvim_feedkeys(). /// /// Example: /// <pre> /// :let key = nvim_replace_termcodes("<C-o>", v:true, v:false, v:true) -/// :call nvim_feedkeys(key, 'n', v:true) +/// :call nvim_feedkeys(key, 'n', v:false) /// </pre> /// /// @param keys to be typed /// @param mode behavior flags, see |feedkeys()| -/// @param escape_csi If true, escape K_SPECIAL/CSI bytes in `keys` +/// @param escape_ks If true, escape K_SPECIAL bytes in `keys` +/// This should be false if you already used +/// |nvim_replace_termcodes()|, and true otherwise. /// @see feedkeys() -/// @see vim_strsave_escape_csi -void nvim_feedkeys(String keys, String mode, Boolean escape_csi) +/// @see vim_strsave_escape_ks +void nvim_feedkeys(String keys, String mode, Boolean escape_ks) FUNC_API_SINCE(1) { bool remap = true; @@ -210,7 +241,7 @@ void nvim_feedkeys(String keys, String mode, Boolean escape_csi) bool execute = false; bool dangerous = false; - for (size_t i = 0; i < mode.size; ++i) { + for (size_t i = 0; i < mode.size; i++) { switch (mode.data[i]) { case 'n': remap = false; break; @@ -232,20 +263,20 @@ void nvim_feedkeys(String keys, String mode, Boolean escape_csi) } char *keys_esc; - if (escape_csi) { - // Need to escape K_SPECIAL and CSI before putting the string in the + if (escape_ks) { + // Need to escape K_SPECIAL before putting the string in the // typeahead buffer. - keys_esc = (char *)vim_strsave_escape_csi((char_u *)keys.data); + keys_esc = vim_strsave_escape_ks(keys.data); } else { keys_esc = keys.data; } - ins_typebuf((char_u *)keys_esc, (remap ? REMAP_YES : REMAP_NONE), + ins_typebuf(keys_esc, (remap ? REMAP_YES : REMAP_NONE), insert ? 0 : typebuf.tb_len, !typed, false); if (vgetc_busy) { typebuf_was_filled = true; } - if (escape_csi) { + if (escape_ks) { xfree(keys_esc); } @@ -394,13 +425,22 @@ String nvim_replace_termcodes(String str, Boolean from_part, Boolean do_lt, Bool return (String) { .data = NULL, .size = 0 }; } + int flags = 0; + if (from_part) { + flags |= REPTERM_FROM_PART; + } + if (do_lt) { + flags |= REPTERM_DO_LT; + } + if (!special) { + flags |= REPTERM_NO_SPECIAL; + } + char *ptr = NULL; - replace_termcodes((char_u *)str.data, str.size, (char_u **)&ptr, - from_part, do_lt, special, CPO_TO_CPO_FLAGS); + replace_termcodes(str.data, str.size, &ptr, flags, NULL, CPO_TO_CPO_FLAGS); return cstr_as_string(ptr); } - /// Execute Lua code. Parameters (if any) are available as `...` inside the /// chunk. The chunk can return a value. /// @@ -441,7 +481,7 @@ Object nvim_notify(String msg, Integer log_level, Dictionary opts, Error *err) } /// Calculates the number of display cells occupied by `text`. -/// <Tab> counts as one cell. +/// Control characters including <Tab> count as one cell. /// /// @param text Some text /// @param[out] err Error details, if any @@ -454,7 +494,7 @@ Integer nvim_strwidth(String text, Error *err) return 0; } - return (Integer)mb_string2cells((char_u *)text.data); + return (Integer)mb_string2cells(text.data); } /// Gets the paths contained in 'runtimepath'. @@ -491,12 +531,15 @@ ArrayOf(String) nvim_get_runtime_file(String name, Boolean all, Error *err) int flags = DIP_DIRFILE | (all ? DIP_ALL : 0); - do_in_runtimepath((char_u *)(name.size ? name.data : ""), - flags, find_runtime_cb, &rv); + TRY_WRAP({ + try_start(); + do_in_runtimepath((name.size ? name.data : ""), flags, find_runtime_cb, &rv); + try_end(err); + }); return rv; } -static void find_runtime_cb(char_u *fname, void *cookie) +static void find_runtime_cb(char *fname, void *cookie) { Array *rv = (Array *)cookie; if (fname != NULL) { @@ -513,20 +556,33 @@ String nvim__get_lib_dir(void) /// /// @param pat pattern of files to search for /// @param all whether to return all matches or only the first -/// @param options -/// is_lua: only search lua subdirs +/// @param opts is_lua: only search lua subdirs /// @return list of absolute paths to the found files ArrayOf(String) nvim__get_runtime(Array pat, Boolean all, Dict(runtime) *opts, Error *err) FUNC_API_SINCE(8) FUNC_API_FAST { bool is_lua = api_object_to_bool(opts->is_lua, "is_lua", false, err); + bool source = api_object_to_bool(opts->do_source, "do_source", false, err); + if (source && !nlua_is_deferred_safe()) { + api_set_error(err, kErrorTypeValidation, "'do_source' cannot be used in fast callback"); + } + if (ERROR_SET(err)) { return (Array)ARRAY_DICT_INIT; } - return runtime_get_named(is_lua, pat, all); -} + ArrayOf(String) res = runtime_get_named(is_lua, pat, all); + + if (source) { + for (size_t i = 0; i < res.size; i++) { + String name = res.items[i].data.string; + (void)do_source(name.data, false, DOSO_NONE); + } + } + + return res; +} /// Changes the global working directory. /// @@ -546,14 +602,13 @@ void nvim_set_current_dir(String dir, Error *err) try_start(); - if (vim_chdir((char_u *)string)) { + if (!changedir_func(string, kCdScopeGlobal)) { if (!try_end(err)) { api_set_error(err, kErrorTypeException, "Failed to change directory"); } return; } - post_chdir(kCdScopeGlobal, true); try_end(err); } @@ -596,7 +651,19 @@ void nvim_del_current_line(Error *err) Object nvim_get_var(String name, Error *err) FUNC_API_SINCE(1) { - return dict_get_value(&globvardict, name, err); + dictitem_T *di = tv_dict_find(&globvardict, name.data, (ptrdiff_t)name.size); + if (di == NULL) { // try to autoload script + if (!script_autoload(name.data, name.size, false) || aborting()) { + api_set_error(err, kErrorTypeValidation, "Key not found: %s", name.data); + return (Object)OBJECT_INIT; + } + di = tv_dict_find(&globvardict, name.data, (ptrdiff_t)name.size); + } + if (di == NULL) { + api_set_error(err, kErrorTypeValidation, "Key not found: %s", name.data); + return (Object)OBJECT_INIT; + } + return vim_to_object(&di->di_tv); } /// Sets a global (g:) variable. @@ -642,189 +709,6 @@ void nvim_set_vvar(String name, Object value, Error *err) dict_set_var(&vimvardict, name, value, false, false, err); } -/// Gets the global value of an option. -/// -/// @param name Option name -/// @param[out] err Error details, if any -/// @return Option value (global) -Object nvim_get_option(String name, Error *err) - FUNC_API_SINCE(1) -{ - return get_option_from(NULL, SREQ_GLOBAL, name, err); -} - -/// Gets the value of an option. The behavior of this function matches that of -/// |:set|: the local value of an option is returned if it exists; otherwise, -/// the global value is returned. Local values always correspond to the current -/// buffer or window. To get a buffer-local or window-local option for a -/// specific buffer or window, use |nvim_buf_get_option()| or -/// |nvim_win_get_option()|. -/// -/// @param name Option name -/// @param opts Optional parameters -/// - scope: One of 'global' or 'local'. Analogous to -/// |:setglobal| and |:setlocal|, respectively. -/// @param[out] err Error details, if any -/// @return Option value -Object nvim_get_option_value(String name, Dict(option) *opts, Error *err) - FUNC_API_SINCE(9) -{ - Object rv = OBJECT_INIT; - - int scope = 0; - if (opts->scope.type == kObjectTypeString) { - if (!strcmp(opts->scope.data.string.data, "local")) { - scope = OPT_LOCAL; - } else if (!strcmp(opts->scope.data.string.data, "global")) { - scope = OPT_GLOBAL; - } else { - api_set_error(err, kErrorTypeValidation, "invalid scope: must be 'local' or 'global'"); - goto end; - } - } else if (HAS_KEY(opts->scope)) { - api_set_error(err, kErrorTypeValidation, "invalid value for key: scope"); - goto end; - } - - long numval = 0; - char *stringval = NULL; - switch (get_option_value(name.data, &numval, (char_u **)&stringval, scope)) { - case 0: - rv = STRING_OBJ(cstr_as_string(stringval)); - break; - case 1: - rv = INTEGER_OBJ(numval); - break; - case 2: - switch (numval) { - case 0: - case 1: - rv = BOOLEAN_OBJ(numval); - break; - default: - // Boolean options that return something other than 0 or 1 should return nil. Currently this - // only applies to 'autoread' which uses -1 as a local value to indicate "unset" - rv = NIL; - break; - } - break; - default: - api_set_error(err, kErrorTypeValidation, "unknown option '%s'", name.data); - goto end; - } - -end: - return rv; -} - -/// Sets the value of an option. The behavior of this function matches that of -/// |:set|: for global-local options, both the global and local value are set -/// unless otherwise specified with {scope}. -/// -/// @param name Option name -/// @param value New option value -/// @param opts Optional parameters -/// - scope: One of 'global' or 'local'. Analogous to -/// |:setglobal| and |:setlocal|, respectively. -/// @param[out] err Error details, if any -void nvim_set_option_value(String name, Object value, Dict(option) *opts, Error *err) - FUNC_API_SINCE(9) -{ - int scope = 0; - if (opts->scope.type == kObjectTypeString) { - if (!strcmp(opts->scope.data.string.data, "local")) { - scope = OPT_LOCAL; - } else if (!strcmp(opts->scope.data.string.data, "global")) { - scope = OPT_GLOBAL; - } else { - api_set_error(err, kErrorTypeValidation, "invalid scope: must be 'local' or 'global'"); - return; - } - } else if (HAS_KEY(opts->scope)) { - api_set_error(err, kErrorTypeValidation, "invalid value for key: scope"); - return; - } - - long numval = 0; - char *stringval = NULL; - - switch (value.type) { - case kObjectTypeInteger: - numval = value.data.integer; - break; - case kObjectTypeBoolean: - numval = value.data.boolean ? 1 : 0; - break; - case kObjectTypeString: - stringval = value.data.string.data; - break; - case kObjectTypeNil: - scope |= OPT_CLEAR; - break; - default: - api_set_error(err, kErrorTypeValidation, "invalid value for option"); - return; - } - - char *e = set_option_value(name.data, numval, stringval, scope); - if (e) { - api_set_error(err, kErrorTypeException, "%s", e); - } -} - -/// Gets the option information for all options. -/// -/// The dictionary has the full option names as keys and option metadata -/// dictionaries as detailed at |nvim_get_option_info|. -/// -/// @return dictionary of all options -Dictionary nvim_get_all_options_info(Error *err) - FUNC_API_SINCE(7) -{ - return get_all_vimoptions(); -} - -/// Gets the option information for one option -/// -/// Resulting dictionary has keys: -/// - name: Name of the option (like 'filetype') -/// - shortname: Shortened name of the option (like 'ft') -/// - type: type of option ("string", "number" or "boolean") -/// - default: The default value for the option -/// - was_set: Whether the option was set. -/// -/// - last_set_sid: Last set script id (if any) -/// - last_set_linenr: line number where option was set -/// - last_set_chan: Channel where option was set (0 for local) -/// -/// - scope: one of "global", "win", or "buf" -/// - global_local: whether win or buf option has a global value -/// -/// - commalist: List of comma separated values -/// - flaglist: List of single char flags -/// -/// -/// @param name Option name -/// @param[out] err Error details, if any -/// @return Option Information -Dictionary nvim_get_option_info(String name, Error *err) - FUNC_API_SINCE(7) -{ - return get_vimoption(name, err); -} - -/// Sets the global value of an option. -/// -/// @param channel_id -/// @param name Option name -/// @param value New option value -/// @param[out] err Error details, if any -void nvim_set_option(uint64_t channel_id, String name, Object value, Error *err) - FUNC_API_SINCE(1) -{ - set_option_to(channel_id, NULL, SREQ_GLOBAL, name, value, err); -} - /// Echo a message. /// /// @param chunks A list of [text, hl_group] arrays, each representing a @@ -845,26 +729,15 @@ void nvim_echo(Array chunks, Boolean history, Dictionary opts, Error *err) goto error; } - no_wait_return++; - msg_start(); - msg_clr_eos(); - bool need_clear = false; - for (uint32_t i = 0; i < kv_size(hl_msg); i++) { - HlMessageChunk chunk = kv_A(hl_msg, i); - msg_multiline_attr((const char *)chunk.text.data, chunk.attr, - true, &need_clear); - } + msg_multiattr(hl_msg, history ? "echomsg" : "echo", history); + if (history) { - msg_ext_set_kind("echomsg"); - add_hl_msg_hist(hl_msg); - } else { - msg_ext_set_kind("echo"); + // history takes ownership + return; } - no_wait_return--; - msg_end(); error: - clear_hl_msg(&hl_msg); + hl_msg_free(hl_msg); } /// Writes a message to the Vim output buffer. Does not append "\n", the @@ -1114,6 +987,7 @@ Integer nvim_open_term(Buffer buffer, DictionaryOf(LuaRef) opts, Error *err) TerminalOptions topts; Channel *chan = channel_alloc(kChannelStreamInternal); chan->stream.internal.cb = cb; + chan->stream.internal.closed = false; topts.data = chan; // NB: overridden in terminal_check_size if a window is already // displaying the buffer @@ -1152,14 +1026,12 @@ static void term_resize(uint16_t width, uint16_t height, void *data) static void term_close(void *data) { Channel *chan = data; - terminal_destroy(chan->term); - chan->term = NULL; + terminal_destroy(&chan->term); api_free_luaref(chan->stream.internal.cb); chan->stream.internal.cb = LUA_NOREF; channel_decref(chan); } - /// Send data to channel `id`. For a job, it writes it to the /// stdin of the process. For the stdio channel |channel-stdio|, /// it writes to Nvim's stdout. For an internal terminal instance @@ -1293,25 +1165,25 @@ Boolean nvim_paste(String data, Boolean crlf, Integer phase, Error *err) draining = true; goto theend; } - if (!(State & (CMDLINE | INSERT)) && (phase == -1 || phase == 1)) { + if (!(State & (MODE_CMDLINE | MODE_INSERT)) && (phase == -1 || phase == 1)) { ResetRedobuff(); AppendCharToRedobuff('a'); // Dot-repeat. } // vim.paste() decides if client should cancel. Errors do NOT cancel: we // want to drain remaining chunks (rather than divert them to main input). cancel = (rv.type == kObjectTypeBoolean && !rv.data.boolean); - if (!cancel && !(State & CMDLINE)) { // Dot-repeat. + if (!cancel && !(State & MODE_CMDLINE)) { // Dot-repeat. for (size_t i = 0; i < lines.size; i++) { String s = lines.items[i].data.string; - assert(data.size <= INT_MAX); - AppendToRedobuffLit((char_u *)s.data, (int)s.size); + assert(s.size <= INT_MAX); + AppendToRedobuffLit(s.data, (int)s.size); // readfile()-style: "\n" is indicated by presence of N+1 item. if (i + 1 < lines.size) { AppendCharToRedobuff(NL); } } } - if (!(State & (CMDLINE | INSERT)) && (phase == -1 || phase == 3)) { + if (!(State & (MODE_CMDLINE | MODE_INSERT)) && (phase == -1 || phase == 3)) { AppendCharToRedobuff(ESC); // Dot-repeat. } theend: @@ -1357,7 +1229,7 @@ void nvim_put(ArrayOf(String) lines, String type, Boolean after, Boolean follow, goto cleanup; } String line = lines.items[i].data.string; - reg->y_array[i] = (char_u *)xmemdupz(line.data, line.size); + reg->y_array[i] = xmemdupz(line.data, line.size); memchrsub(reg->y_array[i], NUL, NL, line.size); } @@ -1422,7 +1294,8 @@ void nvim_unsubscribe(uint64_t channel_id, String event) Integer nvim_get_color_by_name(String name) FUNC_API_SINCE(1) { - return name_to_color(name.data); + int dummy; + return name_to_color(name.data, &dummy); } /// Returns a map of color names and RGB values. @@ -1524,10 +1397,11 @@ Dictionary nvim_get_mode(void) FUNC_API_SINCE(2) FUNC_API_FAST { Dictionary rv = ARRAY_DICT_INIT; - char *modestr = get_mode(); + char modestr[MODE_MAX_LENGTH]; + get_mode(modestr); bool blocked = input_blocking(); - PUT(rv, "mode", STRING_OBJ(cstr_as_string(modestr))); + PUT(rv, "mode", STRING_OBJ(cstr_to_string(modestr))); PUT(rv, "blocking", BOOLEAN_OBJ(blocked)); return rv; @@ -1561,21 +1435,24 @@ ArrayOf(Dictionary) nvim_get_keymap(uint64_t channel_id, String mode) /// nmap <nowait> <Space><NL> <Nop> /// </pre> /// +/// @param channel_id /// @param mode Mode short-name (map command prefix: "n", "i", "v", "x", …) /// or "!" for |:map!|, or empty string for |:map|. /// @param lhs Left-hand-side |{lhs}| of the mapping. /// @param rhs Right-hand-side |{rhs}| of the mapping. -/// @param opts Optional parameters map. Accepts all |:map-arguments| -/// as keys excluding |<buffer>| but including |noremap| and "desc". -/// |desc| can be used to give a description to keymap. -/// When called from Lua, also accepts a "callback" key that takes -/// a Lua function to call when the mapping is executed. -/// Values are Booleans. Unknown key is an error. +/// @param opts Optional parameters map: keys are |:map-arguments|, values +/// are booleans (default false). Accepts all |:map-arguments| as +/// keys excluding |<buffer>| but including |noremap| and "desc". +/// Unknown key is an error. "desc" can be used to give a +/// description to the mapping. When called from Lua, also accepts a +/// "callback" key that takes a Lua function to call when the +/// mapping is executed. /// @param[out] err Error details, if any. -void nvim_set_keymap(String mode, String lhs, String rhs, Dict(keymap) *opts, Error *err) +void nvim_set_keymap(uint64_t channel_id, String mode, String lhs, String rhs, Dict(keymap) *opts, + Error *err) FUNC_API_SINCE(6) { - modify_keymap(-1, false, mode, lhs, rhs, opts, err); + modify_keymap(channel_id, -1, false, mode, lhs, rhs, opts, err); } /// Unmaps a global |mapping| for the given mode. @@ -1583,25 +1460,10 @@ void nvim_set_keymap(String mode, String lhs, String rhs, Dict(keymap) *opts, Er /// To unmap a buffer-local mapping, use |nvim_buf_del_keymap()|. /// /// @see |nvim_set_keymap()| -void nvim_del_keymap(String mode, String lhs, Error *err) +void nvim_del_keymap(uint64_t channel_id, String mode, String lhs, Error *err) FUNC_API_SINCE(6) { - nvim_buf_del_keymap(-1, mode, lhs, err); -} - -/// Gets a map of global (non-buffer-local) Ex commands. -/// -/// Currently only |user-commands| are supported, not builtin Ex commands. -/// -/// @param opts Optional parameters. Currently only supports -/// {"builtin":false} -/// @param[out] err Error details, if any. -/// -/// @returns Map of maps describing commands. -Dictionary nvim_get_commands(Dict(get_commands) *opts, Error *err) - FUNC_API_SINCE(4) -{ - return nvim_buf_get_commands(-1, opts, err); + nvim_buf_del_keymap(channel_id, -1, mode, lhs, err); } /// Returns a 2-tuple (Array), where item 0 is the current channel id and item @@ -1713,7 +1575,7 @@ void nvim_set_client_info(uint64_t channel_id, String name, Dictionary version, /// - "pty" (optional) Name of pseudoterminal. On a POSIX system this /// is a device path like "/dev/pts/1". If the name is unknown, /// the key will still be present if a pty is used (e.g. for -/// winpty on Windows). +/// conpty on Windows). /// - "buffer" (optional) Buffer with connected |terminal| instance. /// - "client" (optional) Info about the peer (client on the other end of /// the RPC channel), if provided by it via @@ -1845,15 +1707,15 @@ static void write_msg(String message, bool to_err) static char out_line_buf[LINE_BUFFER_SIZE], err_line_buf[LINE_BUFFER_SIZE]; #define PUSH_CHAR(i, pos, line_buf, msg) \ - if (message.data[i] == NL || pos == LINE_BUFFER_SIZE - 1) { \ - line_buf[pos] = NUL; \ + if (message.data[i] == NL || (pos) == LINE_BUFFER_SIZE - 1) { \ + (line_buf)[pos] = NUL; \ msg(line_buf); \ - pos = 0; \ + (pos) = 0; \ continue; \ } \ - line_buf[pos++] = message.data[i]; + (line_buf)[(pos)++] = message.data[i]; - ++no_wait_return; + no_wait_return++; for (uint32_t i = 0; i < message.size; i++) { if (got_int) { break; @@ -1864,7 +1726,7 @@ static void write_msg(String message, bool to_err) PUSH_CHAR(i, out_pos, out_line_buf, msg); } } - --no_wait_return; + no_wait_return--; msg_end(); } @@ -1929,8 +1791,9 @@ Dictionary nvim__stats(void) { Dictionary rv = ARRAY_DICT_INIT; PUT(rv, "fsync", INTEGER_OBJ(g_stats.fsync)); + PUT(rv, "log_skip", INTEGER_OBJ(g_stats.log_skip)); + PUT(rv, "lua_refcount", INTEGER_OBJ(nlua_get_global_ref_count())); PUT(rv, "redraw", INTEGER_OBJ(g_stats.redraw)); - PUT(rv, "lua_refcount", INTEGER_OBJ(nlua_refcount)); return rv; } @@ -1964,14 +1827,13 @@ Array nvim_get_proc_children(Integer pid, Error *err) size_t proc_count; int rv = os_proc_children((int)pid, &proc_list, &proc_count); - if (rv != 0) { + if (rv == 2) { // syscall failed (possibly because of kernel options), try shelling out. DLOG("fallback to vim._os_proc_children()"); Array a = ARRAY_DICT_INIT; ADD(a, INTEGER_OBJ(pid)); - String s = cstr_to_string("return vim._os_proc_children(select(1, ...))"); + String s = STATIC_CSTR_AS_STRING("return vim._os_proc_children(...)"); Object o = nlua_exec(s, a, err); - api_free_string(s); api_free_array(a); if (o.type == kObjectTypeArray) { rvobj = o.data.array; @@ -2082,8 +1944,8 @@ Array nvim__inspect_cell(Integer grid, Integer row, Integer col, Error *err) } } - if (row < 0 || row >= g->Rows - || col < 0 || col >= g->Columns) { + if (row < 0 || row >= g->rows + || col < 0 || col >= g->cols) { return ret; } size_t off = g->line_offset[(size_t)row] + (size_t)col; @@ -2103,6 +1965,11 @@ void nvim__screenshot(String path) ui_call_screenshot(path); } +Object nvim__unpack(String str, Error *err) + FUNC_API_FAST +{ + return unpack(str.data, str.size, err); +} /// Deletes an uppercase/file named mark. See |mark-motions|. /// @@ -2160,20 +2027,20 @@ Array nvim_get_mark(String name, Dictionary opts, Error *err) return rv; } - xfmark_T mark = get_global_mark(*name.data); - pos_T pos = mark.fmark.mark; + xfmark_T *mark = mark_get_global(false, *name.data); // false avoids loading the mark buffer + pos_T pos = mark->fmark.mark; bool allocated = false; int bufnr; char *filename; // Marks are from an open buffer it fnum is non zero - if (mark.fmark.fnum != 0) { - bufnr = mark.fmark.fnum; + if (mark->fmark.fnum != 0) { + bufnr = mark->fmark.fnum; filename = (char *)buflist_nr2name(bufnr, true, true); allocated = true; // Marks comes from shada } else { - filename = (char *)mark.fname; + filename = mark->fname; bufnr = 0; } @@ -2214,10 +2081,11 @@ Array nvim_get_mark(String name, Dictionary opts, Error *err) /// - winid: (number) |window-ID| of the window to use as context for statusline. /// - maxwidth: (number) Maximum width of statusline. /// - fillchar: (string) Character to fill blank spaces in the statusline (see -/// 'fillchars'). +/// 'fillchars'). Treated as single-width even if it isn't. /// - highlights: (boolean) Return highlight information. +/// - use_winbar: (boolean) Evaluate winbar instead of statusline. /// - use_tabline: (boolean) Evaluate tabline instead of statusline. When |TRUE|, {winid} -/// is ignored. +/// is ignored. Mutually exclusive with {use_winbar}. /// /// @param[out] err Error details, if any. /// @return Dictionary containing statusline information, with these keys: @@ -2234,11 +2102,20 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * Dictionary result = ARRAY_DICT_INIT; int maxwidth; - char fillchar = 0; + int fillchar = 0; Window window = 0; + bool use_winbar = false; bool use_tabline = false; bool highlights = false; + if (str.size < 2 || memcmp(str.data, "%!", 2)) { + const char *const errmsg = check_stl_option(str.data); + if (errmsg) { + api_set_error(err, kErrorTypeValidation, "%s", errmsg); + return result; + } + } + if (HAS_KEY(opts->winid)) { if (opts->winid.type != kObjectTypeInteger) { api_set_error(err, kErrorTypeValidation, "winid must be an integer"); @@ -2247,16 +2124,15 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * window = (Window)opts->winid.data.integer; } - if (HAS_KEY(opts->fillchar)) { - if (opts->fillchar.type != kObjectTypeString || opts->fillchar.data.string.size > 1) { - api_set_error(err, kErrorTypeValidation, "fillchar must be an ASCII character"); + if (opts->fillchar.type != kObjectTypeString || opts->fillchar.data.string.size == 0 + || ((size_t)utf_ptr2len(opts->fillchar.data.string.data) + != opts->fillchar.data.string.size)) { + api_set_error(err, kErrorTypeValidation, "fillchar must be a single character"); return result; } - - fillchar = opts->fillchar.data.string.data[0]; + fillchar = utf_ptr2char(opts->fillchar.data.string.data); } - if (HAS_KEY(opts->highlights)) { highlights = api_object_to_bool(opts->highlights, "highlights", false, err); @@ -2264,7 +2140,13 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * return result; } } + if (HAS_KEY(opts->use_winbar)) { + use_winbar = api_object_to_bool(opts->use_winbar, "use_winbar", false, err); + if (ERROR_SET(err)) { + return result; + } + } if (HAS_KEY(opts->use_tabline)) { use_tabline = api_object_to_bool(opts->use_tabline, "use_tabline", false, err); @@ -2272,6 +2154,10 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * return result; } } + if (use_winbar && use_tabline) { + api_set_error(err, kErrorTypeValidation, "use_winbar and use_tabline are mutually exclusive"); + return result; + } win_T *wp, *ewp; @@ -2281,11 +2167,19 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * fillchar = ' '; } else { wp = find_window_by_handle(window, err); + if (wp == NULL) { + api_set_error(err, kErrorTypeException, "unknown winid %d", window); + return result; + } ewp = wp; if (fillchar == 0) { - int attr; - fillchar = (char)fillchar_status(&attr, wp); + if (use_winbar) { + fillchar = wp->w_p_fcs_chars.wbr; + } else { + int attr; + fillchar = fillchar_status(&attr, wp); + } } } @@ -2297,7 +2191,7 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * maxwidth = (int)opts->maxwidth.data.integer; } else { - maxwidth = use_tabline ? Columns : wp->w_width; + maxwidth = (use_tabline || (!use_winbar && global_stl_height() > 0)) ? Columns : wp->w_width; } char buf[MAXPATHL]; @@ -2309,11 +2203,11 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * ewp->w_p_crb = false; int width = build_stl_str_hl(ewp, - (char_u *)buf, + buf, sizeof(buf), - (char_u *)str.data, + str.data, false, - (char_u)fillchar, + fillchar, maxwidth, hltab_ptr, NULL); @@ -2332,7 +2226,7 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * // add the default highlight at the beginning of the highlight list if (hltab->start == NULL || ((char *)hltab->start - buf) != 0) { Dictionary hl_info = ARRAY_DICT_INIT; - grpname = get_default_stl_hl(wp); + grpname = get_default_stl_hl(wp, use_winbar); PUT(hl_info, "start", INTEGER_OBJ(0)); PUT(hl_info, "group", CSTR_TO_OBJ(grpname)); @@ -2346,73 +2240,19 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * PUT(hl_info, "start", INTEGER_OBJ((char *)sp->start - buf)); if (sp->userhl == 0) { - grpname = get_default_stl_hl(wp); + grpname = get_default_stl_hl(wp, use_winbar); } else if (sp->userhl < 0) { grpname = (char *)syn_id2name(-sp->userhl); } else { snprintf(user_group, sizeof(user_group), "User%d", sp->userhl); grpname = user_group; } - PUT(hl_info, "group", CSTR_TO_OBJ(grpname)); - ADD(hl_values, DICTIONARY_OBJ(hl_info)); } - PUT(result, "highlights", ARRAY_OBJ(hl_values)); } - PUT(result, "str", CSTR_TO_OBJ((char *)buf)); return result; } - -/// Create a new user command |user-commands| -/// -/// {name} is the name of the new command. The name must begin with an uppercase letter. -/// -/// {command} is the replacement text or Lua function to execute. -/// -/// Example: -/// <pre> -/// :call nvim_add_user_command('SayHello', 'echo "Hello world!"', {}) -/// :SayHello -/// Hello world! -/// </pre> -/// -/// @param name Name of the new user command. Must begin with an uppercase letter. -/// @param command Replacement command to execute when this user command is executed. When called -/// from Lua, the command can also be a Lua function. The function is called with a -/// single table argument that contains the following keys: -/// - args: (string) The args passed to the command, if any |<args>| -/// - bang: (boolean) "true" if the command was executed with a ! modifier |<bang>| -/// - line1: (number) The starting line of the command range |<line1>| -/// - line2: (number) The final line of the command range |<line2>| -/// - range: (number) The number of items in the command range: 0, 1, or 2 |<range>| -/// - count: (number) Any count supplied |<count>| -/// - reg: (string) The optional register, if specified |<reg>| -/// - mods: (string) Command modifiers, if any |<mods>| -/// @param opts Optional command attributes. See |command-attributes| for more details. To use -/// boolean attributes (such as |:command-bang| or |:command-bar|) set the value to -/// "true". In addition to the string options listed in |:command-complete|, the -/// "complete" key also accepts a Lua function which works like the "customlist" -/// completion mode |:command-completion-customlist|. Additional parameters: -/// - desc: (string) Used for listing the command when a Lua function is used for -/// {command}. -/// - force: (boolean, default true) Override any previous definition. -/// @param[out] err Error details, if any. -void nvim_add_user_command(String name, Object command, Dict(user_command) *opts, Error *err) - FUNC_API_SINCE(9) -{ - add_user_command(name, command, opts, 0, err); -} - -/// Delete a user-defined command. -/// -/// @param name Name of the command to delete. -/// @param[out] err Error details, if any. -void nvim_del_user_command(String name, Error *err) - FUNC_API_SINCE(9) -{ - nvim_buf_del_user_command(-1, name, err); -} diff --git a/src/nvim/api/vimscript.c b/src/nvim/api/vimscript.c index 640144b234..478e146781 100644 --- a/src/nvim/api/vimscript.c +++ b/src/nvim/api/vimscript.c @@ -10,34 +10,40 @@ #include "nvim/api/private/helpers.h" #include "nvim/api/vimscript.h" #include "nvim/ascii.h" +#include "nvim/autocmd.h" #include "nvim/eval.h" #include "nvim/eval/typval.h" #include "nvim/eval/userfunc.h" #include "nvim/ex_cmds2.h" +#include "nvim/ops.h" +#include "nvim/strings.h" +#include "nvim/vim.h" #include "nvim/viml/parser/expressions.h" #include "nvim/viml/parser/parser.h" +#include "nvim/window.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/vimscript.c.generated.h" #endif -/// Executes Vimscript (multiline block of Ex-commands), like anonymous +/// Executes Vimscript (multiline block of Ex commands), like anonymous /// |:source|. /// /// Unlike |nvim_command()| this function supports heredocs, script-scope (s:), /// etc. /// -/// On execution error: fails with VimL error, does not update v:errmsg. +/// On execution error: fails with VimL error, updates v:errmsg. /// /// @see |execute()| /// @see |nvim_command()| +/// @see |nvim_cmd()| /// /// @param src Vimscript code /// @param output Capture and return all (non-error, non-shell |:!|) output /// @param[out] err Error details (Vim error), if any /// @return Output (non-error, non-shell |:!|) if `output` is true, /// else empty string. -String nvim_exec(String src, Boolean output, Error *err) +String nvim_exec(uint64_t channel_id, String src, Boolean output, Error *err) FUNC_API_SINCE(7) { const int save_msg_silent = msg_silent; @@ -52,11 +58,16 @@ String nvim_exec(String src, Boolean output, Error *err) if (output) { msg_silent++; } + + const sctx_T save_current_sctx = api_set_sctx(channel_id); + do_source_str(src.data, "nvim_exec()"); if (output) { capture_ga = save_capture_ga; msg_silent = save_msg_silent; } + + current_sctx = save_current_sctx; try_end(err); if (ERROR_SET(err)) { @@ -83,13 +94,16 @@ theend: return (String)STRING_INIT; } -/// Executes an ex-command. +/// Executes an Ex command. /// -/// On execution error: fails with VimL error, does not update v:errmsg. +/// On execution error: fails with VimL error, updates v:errmsg. /// -/// @see |nvim_exec()| +/// Prefer using |nvim_cmd()| or |nvim_exec()| over this. To evaluate multiple lines of Vim script +/// or an Ex command directly, use |nvim_exec()|. To construct an Ex command using a structured +/// format and then execute it, use |nvim_cmd()|. To modify an Ex command before evaluating it, use +/// |nvim_parse_cmd()| in conjunction with |nvim_cmd()|. /// -/// @param command Ex-command string +/// @param command Ex command string /// @param[out] err Error details (Vim error), if any void nvim_command(String command, Error *err) FUNC_API_SINCE(1) @@ -102,7 +116,7 @@ void nvim_command(String command, Error *err) /// Evaluates a VimL |expression|. /// Dictionaries and Lists are recursively expanded. /// -/// On execution error: fails with VimL error, does not update v:errmsg. +/// On execution error: fails with VimL error, updates v:errmsg. /// /// @param expr VimL expression string /// @param[out] err Error details, if any @@ -126,7 +140,7 @@ Object nvim_eval(String expr, Error *err) try_start(); typval_T rettv; - int ok = eval0((char_u *)expr.data, &rettv, NULL, true); + int ok = eval0(expr.data, &rettv, NULL, true); if (!try_end(err)) { if (ok == FAIL) { @@ -191,7 +205,7 @@ static Object _call_function(String fn, Array args, dict_T *self, Error *err) funcexe.selfdict = self; // call_func() retval is deceptive, ignore it. Instead we set `msg_list` // (see above) to capture abort-causing non-exception errors. - (void)call_func((char_u *)fn.data, (int)fn.size, &rettv, (int)args.size, + (void)call_func(fn.data, (int)fn.size, &rettv, (int)args.size, vim_args, &funcexe); if (!try_end(err)) { rv = vim_to_object(&rettv); @@ -210,7 +224,7 @@ free_vim_args: /// Calls a VimL function with the given arguments. /// -/// On execution error: fails with VimL error, does not update v:errmsg. +/// On execution error: fails with VimL error, updates v:errmsg. /// /// @param fn Function to call /// @param args Function arguments packed in an Array @@ -224,7 +238,7 @@ Object nvim_call_function(String fn, Array args, Error *err) /// Calls a VimL |Dictionary-function| with the given arguments. /// -/// On execution error: fails with VimL error, does not update v:errmsg. +/// On execution error: fails with VimL error, updates v:errmsg. /// /// @param dict Dictionary, or String evaluating to a VimL |self| dict /// @param fn Name of the function defined on the VimL dict @@ -241,7 +255,7 @@ Object nvim_call_dict_function(Object dict, String fn, Array args, Error *err) switch (dict.type) { case kObjectTypeString: try_start(); - if (eval0((char_u *)dict.data.string.data, &rettv, NULL, true) == FAIL) { + if (eval0(dict.data.string.data, &rettv, NULL, true) == FAIL) { api_set_error(err, kErrorTypeException, "Failed to evaluate dict expression"); } @@ -284,7 +298,7 @@ Object nvim_call_dict_function(Object dict, String fn, Array args, Error *err) goto end; } fn = (String) { - .data = (char *)di->di_tv.vval.v_string, + .data = di->di_tv.vval.v_string, .size = STRLEN(di->di_tv.vval.v_string), }; } diff --git a/src/nvim/api/win_config.c b/src/nvim/api/win_config.c index 255e1e55cc..d36c5bfb95 100644 --- a/src/nvim/api/win_config.c +++ b/src/nvim/api/win_config.c @@ -10,6 +10,7 @@ #include "nvim/api/private/helpers.h" #include "nvim/api/win_config.h" #include "nvim/ascii.h" +#include "nvim/highlight_group.h" #include "nvim/option.h" #include "nvim/screen.h" #include "nvim/strings.h" @@ -21,7 +22,6 @@ # include "api/win_config.c.generated.h" #endif - /// Open a new window. /// /// Currently this is used to open floating and external windows. @@ -125,13 +125,13 @@ /// [ "╔", "═" ,"╗", "║", "╝", "═", "╚", "║" ]. /// If the number of chars are less than eight, they will be repeated. Thus /// an ASCII border could be specified as -/// [ "/", "-", "\\", "|" ], +/// [ "/", "-", \"\\\\\", "|" ], /// or all chars the same as /// [ "x" ]. /// An empty string can be used to turn off a specific border, for instance, /// [ "", "", "", ">", "", "", "", "<" ] /// will only make vertical borders but not horizontal ones. -/// By default, `FloatBorder` highlight is used, which links to `VertSplit` +/// By default, `FloatBorder` highlight is used, which links to `WinSeparator` /// when not defined. It could also be specified by character: /// [ {"+", "MyCorner"}, {"x", "MyBorder"} ]. /// - noautocmd: If true then no buffer-related autocommand events such as @@ -149,7 +149,7 @@ Window nvim_open_win(Buffer buffer, Boolean enter, Dict(float_config) *config, E if (!parse_float_config(config, &fconfig, false, true, err)) { return 0; } - win_T *wp = win_new_float(NULL, fconfig, err); + win_T *wp = win_new_float(NULL, false, fconfig, err); if (!wp) { return 0; } @@ -199,7 +199,7 @@ void nvim_win_set_config(Window window, Dict(float_config) *config, Error *err) return; } if (new_float) { - if (!win_new_float(win, fconfig, err)) { + if (!win_new_float(win, false, fconfig, err)) { return; } redraw_later(win, NOT_VALID); @@ -263,7 +263,7 @@ Dictionary nvim_win_get_config(Window window, Error *err) String s = cstrn_to_string((const char *)config->border_chars[i], sizeof(schar_T)); int hi_id = config->border_hl_ids[i]; - char_u *hi_name = syn_id2name(hi_id); + char *hi_name = (char *)syn_id2name(hi_id); if (hi_name[0]) { ADD(tuple, STRING_OBJ(s)); ADD(tuple, STRING_OBJ(cstr_to_string((const char *)hi_name))); @@ -325,7 +325,7 @@ static bool parse_float_bufpos(Array bufpos, lpos_T *out) || bufpos.items[1].type != kObjectTypeInteger) { return false; } - out->lnum = bufpos.items[0].data.integer; + out->lnum = (linenr_T)bufpos.items[0].data.integer; out->col = (colnr_T)bufpos.items[1].data.integer; return true; } @@ -353,7 +353,7 @@ static void parse_border_style(Object style, FloatConfig *fconfig, Error *err) if (style.type == kObjectTypeArray) { Array arr = style.data.array; size_t size = arr.size; - if (!size || size > 8 || (size & (size-1))) { + if (!size || size > 8 || (size & (size - 1))) { api_set_error(err, kErrorTypeValidation, "invalid number of border chars"); return; @@ -386,12 +386,12 @@ static void parse_border_style(Object style, FloatConfig *fconfig, Error *err) return; } if (string.size - && mb_string2cells_len((char_u *)string.data, string.size) > 1) { + && mb_string2cells_len(string.data, string.size) > 1) { api_set_error(err, kErrorTypeValidation, "border chars must be one cell"); return; } - size_t len = MIN(string.size, sizeof(*chars)-1); + size_t len = MIN(string.size, sizeof(*chars) - 1); if (len) { memcpy(chars[i], string.data, len); } @@ -399,8 +399,8 @@ static void parse_border_style(Object style, FloatConfig *fconfig, Error *err) hl_ids[i] = hl_id; } while (size < 8) { - memcpy(chars+size, chars, sizeof(*chars) * size); - memcpy(hl_ids+size, hl_ids, sizeof(*hl_ids) * size); + memcpy(chars + size, chars, sizeof(*chars) * size); + memcpy(hl_ids + size, hl_ids, sizeof(*hl_ids) * size); size <<= 1; } if ((chars[7][0] && chars[1][0] && !chars[0][0]) @@ -440,18 +440,18 @@ static void parse_border_style(Object style, FloatConfig *fconfig, Error *err) static bool parse_title(FloatConfig* out, String s) { // The raw title is going to be at most the length of the string. - char_u* out_title_raw = xcalloc(sizeof(char_u), s.size + 1); + char* out_title_raw = xcalloc(sizeof(char), s.size + 1); size_t out_cursor = 0; - char_u* data = (char_u*) s.data; + char* data = s.data; size_t out_hlrec_nalloc = 4; stl_hlrec_t* out_hlrec = xcalloc(sizeof(stl_hlrec_t), out_hlrec_nalloc); - out_hlrec[0].start = out_title_raw; + out_hlrec[0].start = (char*) out_title_raw; out_hlrec[0].userhl = 0; size_t out_hl_cur = 1; - char_u hlbuf[128]; + char hlbuf[128]; size_t hlbuf_cur = 0; int hl; @@ -471,7 +471,7 @@ static bool parse_title(FloatConfig* out, String s) i ++; } hlbuf[hlbuf_cur++] = 0; - hl = syn_check_group(hlbuf, (int) strlen((char*)hlbuf)); + hl = syn_check_group(hlbuf, strlen(hlbuf)); hlbuf_cur = 0; if (out_hl_cur >= out_hlrec_nalloc - 1) { // Leave room for last. @@ -479,7 +479,7 @@ static bool parse_title(FloatConfig* out, String s) xrealloc(out_hlrec, sizeof(stl_hlrec_t) * (out_hlrec_nalloc *= 2)); } - out_hlrec[out_hl_cur].start = out_title_raw + out_cursor; + out_hlrec[out_hl_cur].start = (out_title_raw + out_cursor); out_hlrec[out_hl_cur++].userhl = -hl; } else { out_title_raw[out_cursor++] = data[i]; @@ -653,7 +653,6 @@ static bool parse_float_config(Dict(float_config) *config, FloatConfig *fconfig, return false; } - if (HAS_KEY(config->focusable)) { fconfig->focusable = api_object_to_bool(config->focusable, "'focusable' key", false, err); if (ERROR_SET(err)) { diff --git a/src/nvim/api/window.c b/src/nvim/api/window.c index 907306da7b..5a4ff70257 100644 --- a/src/nvim/api/window.c +++ b/src/nvim/api/window.c @@ -71,6 +71,7 @@ ArrayOf(Integer, 2) nvim_win_get_cursor(Window window, Error *err) } /// Sets the (1,0)-indexed cursor position in the window. |api-indexing| +/// This scrolls the window even if it is not the current one. /// /// @param window Window handle, or 0 for current window /// @param pos (row, col) tuple representing the new position @@ -118,6 +119,7 @@ void nvim_win_set_cursor(Window window, ArrayOf(Integer, 2) pos, Error *err) update_topline_win(win); redraw_later(win, VALID); + win->w_redr_status = true; } /// Gets the window height @@ -137,8 +139,7 @@ Integer nvim_win_get_height(Window window, Error *err) return win->w_height; } -/// Sets the window height. This will only succeed if the screen is split -/// horizontally. +/// Sets the window height. /// /// @param window Window handle, or 0 for current window /// @param height Height as a count of rows @@ -263,44 +264,6 @@ void nvim_win_del_var(Window window, String name, Error *err) dict_set_var(win->w_vars, name, NIL, true, false, err); } -/// Gets a window option value -/// -/// @param window Window handle, or 0 for current window -/// @param name Option name -/// @param[out] err Error details, if any -/// @return Option value -Object nvim_win_get_option(Window window, String name, Error *err) - FUNC_API_SINCE(1) -{ - win_T *win = find_window_by_handle(window, err); - - if (!win) { - return (Object)OBJECT_INIT; - } - - return get_option_from(win, SREQ_WIN, name, err); -} - -/// Sets a window option value. Passing 'nil' as value deletes the option(only -/// works if there's a global fallback) -/// -/// @param channel_id -/// @param window Window handle, or 0 for current window -/// @param name Option name -/// @param value Option value -/// @param[out] err Error details, if any -void nvim_win_set_option(uint64_t channel_id, Window window, String name, Object value, Error *err) - FUNC_API_SINCE(1) -{ - win_T *win = find_window_by_handle(window, err); - - if (!win) { - return; - } - - set_option_to(channel_id, win, SREQ_WIN, name, value, err); -} - /// Gets the window position in display cells. First position is zero. /// /// @param window Window handle, or 0 for current window @@ -372,7 +335,6 @@ Boolean nvim_win_is_valid(Window window) return ret; } - /// Closes the window and hide the buffer it contains (like |:hide| with a /// |window-ID|). /// @@ -395,7 +357,7 @@ void nvim_win_hide(Window window, Error *err) TryState tstate; try_enter(&tstate); if (tabpage == curtab) { - win_close(win, false); + win_close(win, false, false); } else { win_close_othertab(win, false, tabpage); } @@ -455,17 +417,12 @@ Object nvim_win_call(Window window, LuaRef fun, Error *err) } tabpage_T *tabpage = win_find_tabpage(win); - win_T *save_curwin; - tabpage_T *save_curtab; - try_start(); Object res = OBJECT_INIT; - if (switch_win_noblock(&save_curwin, &save_curtab, win, tabpage, true) == - OK) { + WIN_EXECUTE(win, tabpage, { Array args = ARRAY_DICT_INIT; res = nlua_call_ref(fun, NULL, args, true, err); - } - restore_win_noblock(save_curwin, save_curtab, true); + }); try_end(err); return res; } |