diff options
author | Josh Rahm <joshuarahm@gmail.com> | 2023-11-29 22:39:54 +0000 |
---|---|---|
committer | Josh Rahm <joshuarahm@gmail.com> | 2023-11-29 22:39:54 +0000 |
commit | 21cb7d04c387e4198ca8098a884c78b56ffcf4c2 (patch) | |
tree | 84fe5690df1551f0bb2bdfe1a13aacd29ebc1de7 /src/nvim/api | |
parent | d9c904f85a23a496df4eb6be42aa43f007b22d50 (diff) | |
parent | 4a8bf24ac690004aedf5540fa440e788459e5e34 (diff) | |
download | rneovim-colorcolchar.tar.gz rneovim-colorcolchar.tar.bz2 rneovim-colorcolchar.zip |
Merge remote-tracking branch 'upstream/master' into colorcolcharcolorcolchar
Diffstat (limited to 'src/nvim/api')
36 files changed, 3664 insertions, 2814 deletions
diff --git a/src/nvim/api/autocmd.c b/src/nvim/api/autocmd.c index 931363e199..08d9d8e117 100644 --- a/src/nvim/api/autocmd.c +++ b/src/nvim/api/autocmd.c @@ -1,27 +1,27 @@ -// 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 <lauxlib.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> -#include "lauxlib.h" +#include "klib/kvec.h" #include "nvim/api/autocmd.h" +#include "nvim/api/keysets_defs.h" #include "nvim/api/private/defs.h" +#include "nvim/api/private/dispatch.h" #include "nvim/api/private/helpers.h" -#include "nvim/ascii.h" +#include "nvim/api/private/validate.h" #include "nvim/autocmd.h" #include "nvim/buffer.h" #include "nvim/eval/typval.h" -#include "nvim/eval/typval_defs.h" #include "nvim/ex_cmds_defs.h" +#include "nvim/func_attr.h" #include "nvim/globals.h" #include "nvim/lua/executor.h" #include "nvim/memory.h" -#include "nvim/vim.h" +#include "nvim/vim_defs.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/autocmd.c.generated.h" @@ -32,13 +32,11 @@ // 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"); \ + event_name2nr_str(event_str.data.string); \ + VALIDATE_S((event_nr < NUM_EVENTS), "event", event_str.data.string.data, { \ goto goto_name; \ - } + }); // ID for associating autocmds created via nvim_create_autocmd // Used to delete autocmds from nvim_del_autocmd @@ -47,19 +45,20 @@ 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>lua -/// -- 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> +/// ```lua +/// -- 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", +/// }) +/// ``` /// /// NOTE: When multiple patterns or events are provided, it will find all the autocommands that /// match any combination of them. @@ -107,25 +106,24 @@ Array nvim_get_autocmds(Dict(get_autocmds) *opts, Error *err) break; case kObjectTypeString: group = augroup_find(opts->group.data.string.data); - if (group < 0) { - api_set_error(err, kErrorTypeValidation, "invalid augroup passed."); + VALIDATE_S((group >= 0), "group", opts->group.data.string.data, { 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."); + char *name = group == 0 ? NULL : augroup_name(group); + VALIDATE_INT(augroup_exists(name), "group", opts->group.data.integer, { goto cleanup; - } + }); break; default: - api_set_error(err, kErrorTypeValidation, "group must be a string or an integer."); - goto cleanup; + VALIDATE_EXP(false, "group", "String or Integer", api_typename(opts->group.type), { + goto cleanup; + }); } - if (opts->event.type != kObjectTypeNil) { + if (HAS_KEY(opts, get_autocmds, event)) { check_event = true; Object v = opts->event; @@ -134,57 +132,49 @@ Array nvim_get_autocmds(Dict(get_autocmds) *opts, Error *err) 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'"); + VALIDATE_T("event item", kObjectTypeString, event_v.type, { 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; + VALIDATE_EXP(false, "event", "String or Array", NULL, { + goto cleanup; + }); } } - if (opts->pattern.type != kObjectTypeNil && opts->buffer.type != kObjectTypeNil) { - api_set_error(err, kErrorTypeValidation, - "Cannot use both 'pattern' and 'buffer'"); + VALIDATE((!HAS_KEY(opts, get_autocmds, pattern) || !HAS_KEY(opts, get_autocmds, buffer)), + "%s", "Cannot use both 'pattern' and 'buffer'", { goto cleanup; - } + }); int pattern_filter_count = 0; - if (opts->pattern.type != kObjectTypeNil) { + if (HAS_KEY(opts, get_autocmds, pattern)) { 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); + VALIDATE((v.data.array.size <= AUCMD_MAX_PATTERNS), + "Too many patterns (maximum of %d)", 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"); + VALIDATE_T("pattern", kObjectTypeString, item.type, { 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; + VALIDATE_EXP(false, "pattern", "String or Array", api_typename(v.type), { + goto cleanup; + }); } } @@ -194,34 +184,33 @@ Array nvim_get_autocmds(Dict(get_autocmds) *opts, Error *err) goto cleanup; } - snprintf((char *)pattern_buflocal, BUFLOCAL_PAT_LEN, "<buffer=%d>", (int)buf->handle); - ADD(buffers, CSTR_TO_OBJ((char *)pattern_buflocal)); + snprintf(pattern_buflocal, BUFLOCAL_PAT_LEN, "<buffer=%d>", (int)buf->handle); + ADD(buffers, CSTR_TO_OBJ(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); + api_set_error(err, kErrorTypeValidation, "Too many buffers (maximum of %d)", + 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"); + VALIDATE_EXP((bufnr.type == kObjectTypeInteger || bufnr.type == kObjectTypeBuffer), + "buffer", "Integer", api_typename(bufnr.type), { 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)); + snprintf(pattern_buflocal, BUFLOCAL_PAT_LEN, "<buffer=%d>", (int)buf->handle); + ADD(buffers, CSTR_TO_OBJ(pattern_buflocal)); + }); + } else if (HAS_KEY(opts, get_autocmds, buffer)) { + VALIDATE_EXP(false, "buffer", "Integer or Array", api_typename(opts->buffer.type), { + goto cleanup; }); - } 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, { @@ -234,8 +223,12 @@ Array nvim_get_autocmds(Dict(get_autocmds) *opts, Error *err) continue; } - for (AutoPat *ap = au_get_autopat_for_event(event); ap != NULL; ap = ap->next) { - if (ap->cmds == NULL) { + AutoCmdVec *acs = au_get_autocmds_for_event(event); + for (size_t i = 0; i < kv_size(*acs); i++) { + AutoCmd *const ac = &kv_A(*acs, i); + AutoPat *const ap = ac->pat; + + if (ap == NULL) { continue; } @@ -247,19 +240,16 @@ Array nvim_get_autocmds(Dict(get_autocmds) *opts, Error *err) // 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]); + for (int j = 0; j < pattern_filter_count; j++) { + assert(j < AUCMD_MAX_PATTERNS); + assert(pattern_filters[j]); - char *pat = pattern_filters[i]; + char *pat = pattern_filters[j]; int patlen = (int)strlen(pat); if (aupat_is_buflocal(pat, patlen)) { - aupat_normalize_buflocal_pat(pattern_buflocal, - pat, - patlen, + aupat_normalize_buflocal_pat(pattern_buflocal, pat, patlen, aupat_get_buflocal_nr(pat, patlen)); - pat = pattern_buflocal; } @@ -274,85 +264,71 @@ Array nvim_get_autocmds(Dict(get_autocmds) *opts, Error *err) } } - for (AutoCmd *ac = ap->cmds; ac != NULL; ac = ac->next) { - if (aucmd_exec_is_deleted(ac->exec)) { - continue; - } + Dictionary autocmd_info = ARRAY_DICT_INIT; - 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 (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->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->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)); + 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(); + 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))); } - } else { - PUT(autocmd_info, - "command", - STRING_OBJ(cstr_as_string(xstrdup(ac->exec.callable.cmd)))); + break; + case kCallbackFuncref: + case kCallbackPartial: + PUT(autocmd_info, "callback", CSTR_AS_OBJ(callback_to_string(cb))); + break; + case kCallbackNone: + abort(); } + } else { + PUT(autocmd_info, "command", CSTR_TO_OBJ(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)); - } + PUT(autocmd_info, "pattern", CSTR_TO_OBJ(ap->pat)); + PUT(autocmd_info, "event", CSTR_TO_OBJ(event_nr2name(event))); + PUT(autocmd_info, "once", BOOLEAN_OBJ(ac->once)); - // 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)); + 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)); } } @@ -365,28 +341,31 @@ cleanup: /// function _name_ string) or `command` (Ex command string). /// /// Example using Lua callback: -/// <pre>lua -/// vim.api.nvim_create_autocmd({"BufEnter", "BufWinEnter"}, { -/// pattern = {"*.c", "*.h"}, -/// callback = function(ev) -/// print(string.format('event fired: %s', vim.inspect(ev))) -/// end -/// }) -/// </pre> +/// +/// ```lua +/// vim.api.nvim_create_autocmd({"BufEnter", "BufWinEnter"}, { +/// pattern = {"*.c", "*.h"}, +/// callback = function(ev) +/// print(string.format('event fired: %s', vim.inspect(ev))) +/// end +/// }) +/// ``` /// /// Example using an Ex command as the handler: -/// <pre>lua -/// vim.api.nvim_create_autocmd({"BufEnter", "BufWinEnter"}, { -/// pattern = {"*.c", "*.h"}, -/// command = "echo 'Entering a C or C++ file'", -/// }) -/// </pre> +/// +/// ```lua +/// vim.api.nvim_create_autocmd({"BufEnter", "BufWinEnter"}, { +/// pattern = {"*.c", "*.h"}, +/// command = "echo 'Entering a C or C++ file'", +/// }) +/// ``` /// /// Note: `pattern` is NOT automatically expanded (unlike with |:autocmd|), thus names like "$HOME" /// and "~" must be expanded explicitly: -/// <pre>lua -/// pattern = vim.fn.expand("~") .. "/some/path/*.py" -/// </pre> +/// +/// ```lua +/// pattern = vim.fn.expand("~") .. "/some/path/*.py" +/// ``` /// /// @param event (string|array) Event(s) that will trigger the handler (`callback` or `command`). /// @param opts Options dict: @@ -404,7 +383,7 @@ cleanup: /// - match: (string) expanded value of |<amatch>| /// - buf: (number) expanded value of |<abuf>| /// - file: (string) expanded value of |<afile>| -/// - data: (any) arbitrary data passed to |nvim_exec_autocmds()| +/// - data: (any) arbitrary data passed from |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 @@ -421,10 +400,8 @@ Integer nvim_create_autocmd(uint64_t channel_id, Object event, Dict(create_autoc { 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; @@ -432,30 +409,23 @@ Integer nvim_create_autocmd(uint64_t channel_id, Object event, Dict(create_autoc goto cleanup; } - if (opts->callback.type != kObjectTypeNil && opts->command.type != kObjectTypeNil) { - api_set_error(err, kErrorTypeValidation, "specify either 'callback' or 'command', not both"); + VALIDATE((!HAS_KEY(opts, create_autocmd, callback) || !HAS_KEY(opts, create_autocmd, command)), + "%s", "Cannot use both 'callback' and 'command'", { 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. + }); + + if (HAS_KEY(opts, create_autocmd, callback)) { + // NOTE: We could accept callable tables, but that isn't common in the API. 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"); + VALIDATE_S((callback->data.luaref != LUA_NOREF), "callback", "<no value>", { goto cleanup; - } - - if (!nlua_ref_is_function(callback->data.luaref)) { - api_set_error(err, - kErrorTypeValidation, - "must pass a function for callback"); + }); + VALIDATE_S(nlua_ref_is_function(callback->data.luaref), "callback", "<not a function>", { goto cleanup; - } + }); cb.type = kCallbackLua; cb.data.luaref = api_new_luaref(callback->data.luaref); @@ -465,61 +435,50 @@ Integer nvim_create_autocmd(uint64_t channel_id, Object event, Dict(create_autoc 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; + VALIDATE_EXP(false, "callback", "Lua function or Vim function name", + api_typename(callback->type), { + 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 if (HAS_KEY(opts, create_autocmd, command)) { + aucmd.type = CALLABLE_EX; + aucmd.callable.cmd = string_to_cstr(opts->command); } else { - api_set_error(err, kErrorTypeValidation, "must pass one of: 'command', 'callback'"); - goto cleanup; + VALIDATE(false, "%s", "Required: 'command' or '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)) { + bool has_buffer = HAS_KEY(opts, create_autocmd, buffer); + + VALIDATE((!HAS_KEY(opts, create_autocmd, pattern) || !has_buffer), + "%s", "Cannot use both 'pattern' and 'buffer' for the same autocmd", { + goto cleanup; + }); + + if (!get_patterns_from_pattern_or_buf(&patterns, opts->pattern, has_buffer, 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 (HAS_KEY(opts, create_autocmd, desc)) { + desc = opts->desc.data; } if (patterns.size == 0) { - ADD(patterns, STRING_OBJ(STATIC_CSTR_TO_STRING("*"))); + ADD(patterns, STATIC_CSTR_TO_OBJ("*")); } - if (event_array.size == 0) { - api_set_error(err, kErrorTypeValidation, "'event' is a required key"); + VALIDATE_R((event_array.size > 0), "event", { goto cleanup; - } + }); autocmd_id = next_autocmd_id++; FOREACH_ITEM(event_array, event_str, { @@ -535,8 +494,8 @@ Integer nvim_create_autocmd(uint64_t channel_id, Object event, Dict(create_autoc pat.data.string.data, (int)pat.data.string.size, au_group, - is_once, - is_nested, + opts->once, + opts->nested, desc, aucmd); }); @@ -556,25 +515,22 @@ cleanup: return autocmd_id; } -/// Delete an autocommand by id. +/// Deletes 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()| +/// @param id Integer Autocommand id returned by |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"); + VALIDATE_INT((id > 0), "autocmd id", 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()|. +/// Clears all autocommands selected by {opts}. To delete autocmds see |nvim_del_autocmd()|. +/// /// @param opts Parameters /// - event: (string|table) /// Examples: @@ -610,25 +566,26 @@ void nvim_clear_autocmds(Dict(clear_autocmds) *opts, Error *err) goto cleanup; } - if (opts->pattern.type != kObjectTypeNil && opts->buffer.type != kObjectTypeNil) { - api_set_error(err, kErrorTypeValidation, - "Cannot use both 'pattern' and 'buffer'"); + bool has_buffer = HAS_KEY(opts, clear_autocmds, buffer); + + VALIDATE((!HAS_KEY(opts, clear_autocmds, pattern) || !has_buffer), + "%s", "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)) { + if (!get_patterns_from_pattern_or_buf(&patterns, opts->pattern, has_buffer, 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(""))); + ADD(patterns, STATIC_CSTR_TO_OBJ("")); } // If we didn't pass any events, that means clear all events. @@ -636,7 +593,7 @@ void nvim_clear_autocmds(Dict(clear_autocmds) *opts, Error *err) 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)) { + if (!clear_autocmd(event, pat, au_group, err)) { goto cleanup; } }); @@ -647,7 +604,7 @@ void nvim_clear_autocmds(Dict(clear_autocmds) *opts, Error *err) FOREACH_ITEM(patterns, pat_object, { char *pat = pat_object.data.string.data; - if (!clear_autocmd(event_nr, (char *)pat, au_group, err)) { + if (!clear_autocmd(event_nr, pat, au_group, err)) { goto cleanup; } }); @@ -662,11 +619,12 @@ cleanup: /// Create or get an autocommand group |autocmd-groups|. /// /// To get an existing group id, do: -/// <pre>lua -/// local id = vim.api.nvim_create_augroup("MyGroup", { -/// clear = false -/// }) -/// </pre> +/// +/// ```lua +/// local id = vim.api.nvim_create_augroup("MyGroup", { +/// clear = false +/// }) +/// ``` /// /// @param name String: The name of the group /// @param opts Dictionary Parameters @@ -691,7 +649,7 @@ Integer nvim_create_augroup(uint64_t channel_id, String name, Dict(create_augrou if (clear_autocmds) { FOR_ALL_AUEVENTS(event) { - aupat_del_for_event_and_group(event, augroup); + aucmd_del_for_event_and_group(event, augroup); } } }); @@ -711,11 +669,9 @@ Integer nvim_create_augroup(uint64_t channel_id, String name, Dict(create_augrou void nvim_del_augroup_by_id(Integer id, Error *err) FUNC_API_SINCE(9) { - TRY_WRAP({ - try_start(); - char *name = augroup_name((int)id); + TRY_WRAP(err, { + char *name = id == 0 ? NULL : augroup_name((int)id); augroup_del(name, false); - try_end(err); }); } @@ -728,10 +684,8 @@ void nvim_del_augroup_by_id(Integer id, Error *err) void nvim_del_augroup_by_name(String name, Error *err) FUNC_API_SINCE(9) { - TRY_WRAP({ - try_start(); + TRY_WRAP(err, { augroup_del(name.data, false); - try_end(err); }); } @@ -772,62 +726,59 @@ void nvim_exec_autocmds(Object event, Dict(exec_autocmds) *opts, Error *err) 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); + VALIDATE_S((au_group != AUGROUP_ERROR), "group", 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); + char *name = au_group == 0 ? NULL : augroup_name(au_group); + VALIDATE_INT(augroup_exists(name), "group", (int64_t)au_group, { goto cleanup; - } + }); break; default: - api_set_error(err, kErrorTypeValidation, "'group' must be a string or an integer."); - goto cleanup; + VALIDATE_EXP(false, "group", "String or Integer", api_typename(opts->group.type), { + 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); + bool has_buffer = false; + if (HAS_KEY(opts, exec_autocmds, buffer)) { + VALIDATE((!HAS_KEY(opts, exec_autocmds, pattern)), + "%s", "Cannot use both 'pattern' and 'buffer' for the same autocmd", { goto cleanup; - } + }); - buf = find_buffer_by_handle((Buffer)buf_obj.data.integer, err); + has_buffer = true; + buf = find_buffer_by_handle(opts->buffer, err); if (ERROR_SET(err)) { goto cleanup; } } - if (!get_patterns_from_pattern_or_buf(&patterns, opts->pattern, opts->buffer, err)) { + if (!get_patterns_from_pattern_or_buf(&patterns, opts->pattern, has_buffer, opts->buffer, err)) { goto cleanup; } if (patterns.size == 0) { - ADD(patterns, STRING_OBJ(STATIC_CSTR_TO_STRING(""))); + ADD(patterns, STATIC_CSTR_TO_OBJ("")); } - if (opts->data.type != kObjectTypeNil) { + if (HAS_KEY(opts, exec_autocmds, data)) { data = &opts->data; } - modeline = api_object_to_bool(opts->modeline, "modeline", true, err); + modeline = GET_BOOL_OR_TRUE(opts, exec_autocmds, modeline); 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); + char *fname = !has_buffer ? pat.data.string.data : NULL; + did_aucmd |= apply_autocmds_group(event_nr, fname, NULL, true, au_group, buf, NULL, data); }) }) @@ -840,45 +791,19 @@ cleanup: 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, NULL)); } else if (v->type == kObjectTypeArray) { - if (!check_autocmd_string_array(v->data.array, k, err)) { + if (!check_string_array(v->data.array, k, true, err)) { return false; } *array = copy_array(v->data.array, NULL); } else { - if (required) { - api_set_error(err, - kErrorTypeValidation, - "'%s' must be an array or a string.", - k); + VALIDATE_EXP(!required, k, "Array or String", api_typename(v->type), { return false; - } + }); } return true; @@ -894,88 +819,71 @@ static int get_augroup_from_object(Object group, Error *err) 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); - + VALIDATE_S((au_group != AUGROUP_ERROR), "group", 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); + char *name = au_group == 0 ? NULL : augroup_name(au_group); + VALIDATE_INT(augroup_exists(name), "group", (int64_t)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; + VALIDATE_EXP(false, "group", "String or Integer", api_typename(group.type), { + return AUGROUP_ERROR; + }); } } -static bool get_patterns_from_pattern_or_buf(Array *patterns, Object pattern, Object buffer, - Error *err) +static bool get_patterns_from_pattern_or_buf(Array *patterns, Object pattern, bool has_buffer, + Buffer 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) { + if (pattern.type != kObjectTypeNil) { Object *v = &pattern; if (v->type == kObjectTypeString) { - char *pat = v->data.string.data; + const 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))); + ADD(*patterns, STRING_OBJ(cbuf_to_string(pat, patlen))); pat = aucmd_next_pattern(pat, patlen); patlen = aucmd_pattern_length(pat); } } else if (v->type == kObjectTypeArray) { - if (!check_autocmd_string_array(v->data.array, "pattern", err)) { + if (!check_string_array(v->data.array, "pattern", true, err)) { return false; } Array array = v->data.array; FOREACH_ITEM(array, entry, { - char *pat = entry.data.string.data; + const 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))); + ADD(*patterns, STRING_OBJ(cbuf_to_string(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; + VALIDATE_EXP(false, "pattern", "String or Table", api_typename(v->type), { + return false; + }); } - - buf_T *buf = find_buffer_by_handle((Buffer)buffer.data.integer, err); + } else if (has_buffer) { + buf_T *buf = find_buffer_by_handle(buffer, 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))); + ADD(*patterns, CSTR_TO_OBJ(pattern_buflocal)); } return true; diff --git a/src/nvim/api/autocmd.h b/src/nvim/api/autocmd.h index f9432830d9..4ab3ddb943 100644 --- a/src/nvim/api/autocmd.h +++ b/src/nvim/api/autocmd.h @@ -1,11 +1,10 @@ -#ifndef NVIM_API_AUTOCMD_H -#define NVIM_API_AUTOCMD_H +#pragma once -#include <stdint.h> +#include <stdint.h> // IWYU pragma: keep -#include "nvim/api/private/defs.h" +#include "nvim/api/keysets_defs.h" // IWYU pragma: keep +#include "nvim/api/private/defs.h" // IWYU pragma: keep #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 fe9e6077d6..0df231868d 100644 --- a/src/nvim/api/buffer.c +++ b/src/nvim/api/buffer.c @@ -1,10 +1,6 @@ -// 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 - // Some of this code was adapted from 'if_py_both.h' from the original // vim source -#include <assert.h> #include <lauxlib.h> #include <stdbool.h> #include <stddef.h> @@ -14,9 +10,11 @@ #include "klib/kvec.h" #include "lua.h" #include "nvim/api/buffer.h" +#include "nvim/api/keysets_defs.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/helpers.h" -#include "nvim/ascii.h" +#include "nvim/api/private/validate.h" +#include "nvim/ascii_defs.h" #include "nvim/autocmd.h" #include "nvim/buffer.h" #include "nvim/buffer_defs.h" @@ -26,6 +24,7 @@ #include "nvim/drawscreen.h" #include "nvim/ex_cmds.h" #include "nvim/extmark.h" +#include "nvim/func_attr.h" #include "nvim/globals.h" #include "nvim/lua/executor.h" #include "nvim/mapping.h" @@ -34,10 +33,11 @@ #include "nvim/memory.h" #include "nvim/move.h" #include "nvim/ops.h" -#include "nvim/pos.h" -#include "nvim/types.h" +#include "nvim/pos_defs.h" +#include "nvim/state_defs.h" +#include "nvim/types_defs.h" #include "nvim/undo.h" -#include "nvim/vim.h" +#include "nvim/vim_defs.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/buffer.c.generated.h" @@ -47,7 +47,7 @@ /// /// \brief For more information on buffers, see |buffers| /// -/// Unloaded Buffers:~ +/// Unloaded Buffers: ~ /// /// Buffers may be unloaded by the |:bunload| command or the buffer's /// |'bufhidden'| option. When a buffer is unloaded its file contents are freed @@ -83,12 +83,16 @@ Integer nvim_buf_line_count(Buffer buffer, Error *err) /// Activates buffer-update events on a channel, or as Lua callbacks. /// /// Example (Lua): capture buffer updates in a global `events` variable -/// (use "print(vim.inspect(events))" to see its contents): -/// <pre>lua -/// events = {} -/// vim.api.nvim_buf_attach(0, false, { -/// on_lines=function(...) table.insert(events, {...}) end}) -/// </pre> +/// (use "vim.print(events)" to see its contents): +/// +/// ```lua +/// events = {} +/// vim.api.nvim_buf_attach(0, false, { +/// on_lines = function(...) +/// table.insert(events, {...}) +/// end, +/// }) +/// ``` /// /// @see |nvim_buf_detach()| /// @see |api-buffer-updates-lua| @@ -111,7 +115,7 @@ Integer nvim_buf_line_count(Buffer buffer, Error *err) /// - byte count of previous contents /// - deleted_codepoints (if `utf_sizes` is true) /// - deleted_codeunits (if `utf_sizes` is true) -/// - on_bytes: lua callback invoked on change. +/// - on_bytes: Lua callback invoked on change. /// This callback receives more granular information about the /// change compared to on_lines. /// Return `true` to detach. @@ -179,11 +183,9 @@ Boolean nvim_buf_attach(uint64_t channel_id, Buffer buffer, Boolean send_buffer, if (is_lua) { for (size_t j = 0; cbs[j].name; j++) { if (strequal(cbs[j].name, k.data)) { - if (v->type != kObjectTypeLuaRef) { - api_set_error(err, kErrorTypeValidation, - "%s is not a function", cbs[j].name); + VALIDATE_T(cbs[j].name, kObjectTypeLuaRef, v->type, { goto error; - } + }); *(cbs[j].dest) = v->data.luaref; v->data.luaref = LUA_NOREF; key_used = true; @@ -194,26 +196,23 @@ Boolean nvim_buf_attach(uint64_t channel_id, Buffer buffer, Boolean send_buffer, if (key_used) { continue; } else if (strequal("utf_sizes", k.data)) { - if (v->type != kObjectTypeBoolean) { - api_set_error(err, kErrorTypeValidation, "utf_sizes must be boolean"); + VALIDATE_T("utf_sizes", kObjectTypeBoolean, v->type, { goto error; - } + }); cb.utf_sizes = v->data.boolean; key_used = true; } else if (strequal("preview", k.data)) { - if (v->type != kObjectTypeBoolean) { - api_set_error(err, kErrorTypeValidation, "preview must be boolean"); + VALIDATE_T("preview", kObjectTypeBoolean, v->type, { goto error; - } + }); cb.preview = v->data.boolean; key_used = true; } } - if (!key_used) { - api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data); + VALIDATE_S(key_used, "'opts' key", k.data, { goto error; - } + }); } return buf_updates_register(buf, channel_id, cb, send_buffer); @@ -252,6 +251,9 @@ void nvim__buf_redraw_range(Buffer buffer, Integer first, Integer last, Error *e if (!buf) { return; } + if (last < 0) { + last = buf->b_ml.ml_line_count; + } redraw_buf_range_later(buf, (linenr_T)first + 1, (linenr_T)last); } @@ -297,10 +299,9 @@ ArrayOf(String) nvim_buf_get_lines(uint64_t channel_id, 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"); + VALIDATE((!strict_indexing || !oob), "%s", "Index out of bounds", { return rv; - } + }); if (start >= end) { // Return 0-length array @@ -325,28 +326,6 @@ end: return rv; } -static bool check_string_array(Array arr, bool disallow_nl, Error *err) -{ - for (size_t i = 0; i < arr.size; i++) { - if (arr.items[i].type != kObjectTypeString) { - api_set_error(err, - kErrorTypeValidation, - "All items in the replacement array must be strings"); - return false; - } - // Disallow newlines in the middle of the line. - if (disallow_nl) { - const String l = arr.items[i].data.string; - if (memchr(l.data, NL, l.size)) { - api_set_error(err, kErrorTypeValidation, - "String cannot contain newlines"); - return false; - } - } - } - return true; -} - /// Sets (replaces) a line-range in the buffer. /// /// Indexing is zero-based, end-exclusive. Negative indices are interpreted @@ -371,7 +350,7 @@ static bool check_string_array(Array arr, bool disallow_nl, Error *err) void nvim_buf_set_lines(uint64_t channel_id, Buffer buffer, Integer start, Integer end, Boolean strict_indexing, ArrayOf(String) replacement, Error *err) FUNC_API_SINCE(1) - FUNC_API_CHECK_TEXTLOCK + FUNC_API_TEXTLOCK_ALLOW_CMDWIN { buf_T *buf = find_buffer_by_handle(buffer, err); @@ -379,24 +358,27 @@ void nvim_buf_set_lines(uint64_t channel_id, Buffer buffer, Integer start, Integ return; } + // load buffer first if it's not loaded + if (buf->b_ml.ml_mfp == NULL) { + if (!buf_ensure_loaded(buf)) { + api_set_error(err, kErrorTypeException, "Failed to load buffer"); + return; + } + } + bool oob = false; 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"); + VALIDATE((!strict_indexing || !oob), "%s", "Index out of bounds", { return; - } - - if (start > end) { - api_set_error(err, - kErrorTypeValidation, - "Argument \"start\" is higher than \"end\""); + }); + VALIDATE((start <= end), "%s", "'start' is higher than 'end'", { return; - } + }); bool disallow_nl = (channel_id != VIML_INTERNAL_CALL); - if (!check_string_array(replacement, disallow_nl, err)) { + if (!check_string_array(replacement, "replacement string", disallow_nl, err)) { return; } @@ -415,27 +397,25 @@ void nvim_buf_set_lines(uint64_t channel_id, Buffer buffer, Integer start, Integ } try_start(); - aco_save_T aco; - aucmd_prepbuf(&aco, buf); if (!MODIFIABLE(buf)) { api_set_error(err, kErrorTypeException, "Buffer is not 'modifiable'"); goto end; } - if (u_save((linenr_T)(start - 1), (linenr_T)end) == FAIL) { + if (u_save_buf(buf, (linenr_T)(start - 1), (linenr_T)end) == FAIL) { api_set_error(err, kErrorTypeException, "Failed to save undo information"); goto end; } - bcount_t deleted_bytes = get_region_bytecount(curbuf, (linenr_T)start, (linenr_T)end, 0, 0); + bcount_t deleted_bytes = get_region_bytecount(buf, (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 // repeatedly deleting line "start". - size_t to_delete = (new_len < old_len) ? (size_t)(old_len - new_len) : 0; + size_t to_delete = (new_len < old_len) ? old_len - new_len : 0; for (size_t i = 0; i < to_delete; i++) { - if (ml_delete((linenr_T)start, false) == FAIL) { + if (ml_delete_buf(buf, (linenr_T)start, false) == FAIL) { api_set_error(err, kErrorTypeException, "Failed to delete line"); goto end; } @@ -453,12 +433,11 @@ void nvim_buf_set_lines(uint64_t channel_id, Buffer buffer, Integer start, Integ for (size_t i = 0; i < to_replace; i++) { int64_t lnum = start + (int64_t)i; - if (lnum >= MAXLNUM) { - api_set_error(err, kErrorTypeValidation, "Index value is too high"); + VALIDATE(lnum < MAXLNUM, "%s", "Index out of bounds", { goto end; - } + }); - if (ml_replace((linenr_T)lnum, lines[i], false) == FAIL) { + if (ml_replace_buf(buf, (linenr_T)lnum, lines[i], false) == FAIL) { api_set_error(err, kErrorTypeException, "Failed to replace line"); goto end; } @@ -473,12 +452,11 @@ void nvim_buf_set_lines(uint64_t channel_id, Buffer buffer, Integer start, Integ for (size_t i = to_replace; i < new_len; i++) { int64_t lnum = start + (int64_t)i - 1; - if (lnum >= MAXLNUM) { - api_set_error(err, kErrorTypeValidation, "Index value is too high"); + VALIDATE(lnum < MAXLNUM, "%s", "Index out of bounds", { goto end; - } + }); - if (ml_append((linenr_T)lnum, lines[i], 0, false) == FAIL) { + if (ml_append_buf(buf, (linenr_T)lnum, lines[i], 0, false) == FAIL) { api_set_error(err, kErrorTypeException, "Failed to insert line"); goto end; } @@ -493,20 +471,21 @@ void nvim_buf_set_lines(uint64_t channel_id, Buffer buffer, Integer start, Integ // Adjust marks. Invalidate any which lie in the // changed range, and move any in the remainder of the buffer. - // Only adjust marks if we managed to switch to a window that holds - // the buffer, otherwise line numbers will be invalid. - mark_adjust((linenr_T)start, - (linenr_T)(end - 1), - MAXLNUM, - (linenr_T)extra, - kExtmarkNOOP); - - extmark_splice(curbuf, (int)start - 1, 0, (int)(end - start), 0, + linenr_T adjust = end > start ? MAXLNUM : 0; + mark_adjust_buf(buf, (linenr_T)start, (linenr_T)(end - 1), adjust, (linenr_T)extra, + true, true, kExtmarkNOOP); + + extmark_splice(buf, (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, (linenr_T)extra, true); - fix_cursor((linenr_T)start, (linenr_T)end, (linenr_T)extra); + changed_lines(buf, (linenr_T)start, 0, (linenr_T)end, (linenr_T)extra, true); + + FOR_ALL_TAB_WINDOWS(tp, win) { + if (win->w_buffer == buf) { + fix_cursor(win, (linenr_T)start, (linenr_T)end, (linenr_T)extra); + } + } end: for (size_t i = 0; i < new_len; i++) { @@ -514,7 +493,6 @@ end: } xfree(lines); - aucmd_restbuf(&aco); try_end(err); } @@ -533,7 +511,10 @@ end: /// /// Prefer |nvim_buf_set_lines()| if you are only adding or deleting entire lines. /// +/// Prefer |nvim_put()| if you want to insert text at the cursor position. +/// /// @see |nvim_buf_set_lines()| +/// @see |nvim_put()| /// /// @param channel_id /// @param buffer Buffer handle, or 0 for current buffer @@ -546,10 +527,11 @@ end: void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, Integer start_col, Integer end_row, Integer end_col, ArrayOf(String) replacement, Error *err) FUNC_API_SINCE(7) + FUNC_API_TEXTLOCK_ALLOW_CMDWIN { MAXSIZE_TEMP_ARRAY(scratch, 1); if (replacement.size == 0) { - ADD_C(scratch, STRING_OBJ(STATIC_CSTR_AS_STRING(""))); + ADD_C(scratch, STATIC_CSTR_AS_OBJ("")); replacement = scratch; } @@ -558,48 +540,54 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, In return; } + // load buffer first if it's not loaded + if (buf->b_ml.ml_mfp == NULL) { + if (!buf_ensure_loaded(buf)) { + api_set_error(err, kErrorTypeException, "Failed to load buffer"); + return; + } + } + bool oob = false; // 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, false, &oob); - if (oob) { - api_set_error(err, kErrorTypeValidation, "start_row out of bounds"); + VALIDATE_RANGE((!oob), "start_row", { return; - } + }); end_row = normalize_index(buf, end_row, false, &oob); - if (oob) { - api_set_error(err, kErrorTypeValidation, "end_row out of bounds"); + VALIDATE_RANGE((!oob), "end_row", { return; - } + }); char *str_at_start = NULL; char *str_at_end = NULL; // Another call to ml_get_buf() may free the line, so make a copy. - str_at_start = xstrdup(ml_get_buf(buf, (linenr_T)start_row, false)); + str_at_start = xstrdup(ml_get_buf(buf, (linenr_T)start_row)); size_t len_at_start = strlen(str_at_start); - if (start_col < 0 || (size_t)start_col > len_at_start) { - api_set_error(err, kErrorTypeValidation, "start_col out of bounds"); + start_col = start_col < 0 ? (int64_t)len_at_start + start_col + 1 : start_col; + VALIDATE_RANGE((start_col >= 0 && (size_t)start_col <= len_at_start), "start_col", { goto early_end; - } + }); // Another call to ml_get_buf() may free the line, so make a copy. - str_at_end = xstrdup(ml_get_buf(buf, (linenr_T)end_row, false)); + str_at_end = xstrdup(ml_get_buf(buf, (linenr_T)end_row)); 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"); + end_col = end_col < 0 ? (int64_t)len_at_end + end_col + 1 : end_col; + VALIDATE_RANGE((end_col >= 0 && (size_t)end_col <= len_at_end), "end_col", { goto early_end; - } + }); - if (start_row > end_row || (end_row == start_row && start_col > end_col)) { - api_set_error(err, kErrorTypeValidation, "start is higher than end"); + VALIDATE((start_row <= end_row && !(end_row == start_row && start_col > end_col)), + "%s", "'start' is higher than 'end'", { goto early_end; - } + }); bool disallow_nl = (channel_id != VIML_INTERNAL_CALL); - if (!check_string_array(replacement, disallow_nl, err)) { + if (!check_string_array(replacement, "replacement string", disallow_nl, err)) { goto early_end; } @@ -616,7 +604,7 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, In for (int64_t i = 1; i < end_row - start_row; i++) { int64_t lnum = start_row + i; - const char *bufline = ml_get_buf(buf, (linenr_T)lnum, false); + const char *bufline = ml_get_buf(buf, (linenr_T)lnum); old_byte += (bcount_t)(strlen(bufline)) + 1; } old_byte += (bcount_t)end_col + 1; @@ -662,8 +650,6 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, In } try_start(); - aco_save_T aco; - aucmd_prepbuf(&aco, buf); if (!MODIFIABLE(buf)) { api_set_error(err, kErrorTypeException, "Buffer is not 'modifiable'"); @@ -672,7 +658,7 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, In // Small note about undo states: unlike set_lines, we want to save the // undo state of one past the end_row, since end_row is inclusive. - if (u_save((linenr_T)start_row - 1, (linenr_T)end_row + 1) == FAIL) { + if (u_save_buf(buf, (linenr_T)start_row - 1, (linenr_T)end_row + 1) == FAIL) { api_set_error(err, kErrorTypeException, "Failed to save undo information"); goto end; } @@ -683,9 +669,9 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, In // 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 // repeatedly deleting line "start". - size_t to_delete = (new_len < old_len) ? (size_t)(old_len - new_len) : 0; + size_t to_delete = (new_len < old_len) ? old_len - new_len : 0; for (size_t i = 0; i < to_delete; i++) { - if (ml_delete((linenr_T)start_row, false) == FAIL) { + if (ml_delete_buf(buf, (linenr_T)start_row, false) == FAIL) { api_set_error(err, kErrorTypeException, "Failed to delete line"); goto end; } @@ -702,12 +688,11 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, In for (size_t i = 0; i < to_replace; i++) { int64_t lnum = start_row + (int64_t)i; - if (lnum >= MAXLNUM) { - api_set_error(err, kErrorTypeValidation, "Index value is too high"); + VALIDATE((lnum < MAXLNUM), "%s", "Index out of bounds", { goto end; - } + }); - if (ml_replace((linenr_T)lnum, lines[i], false) == FAIL) { + if (ml_replace_buf(buf, (linenr_T)lnum, lines[i], false) == FAIL) { api_set_error(err, kErrorTypeException, "Failed to replace line"); goto end; } @@ -720,12 +705,11 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, In for (size_t i = to_replace; i < new_len; i++) { int64_t lnum = start_row + (int64_t)i - 1; - if (lnum >= MAXLNUM) { - api_set_error(err, kErrorTypeValidation, "Index value is too high"); + VALIDATE((lnum < MAXLNUM), "%s", "Index out of bounds", { goto end; - } + }); - if (ml_append((linenr_T)lnum, lines[i], 0, false) == FAIL) { + if (ml_append_buf(buf, (linenr_T)lnum, lines[i], 0, false) == FAIL) { api_set_error(err, kErrorTypeException, "Failed to insert line"); goto end; } @@ -736,35 +720,39 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, In extra++; } + colnr_T col_extent = (colnr_T)(end_col + - ((end_row == start_row) ? start_col : 0)); + // Adjust marks. Invalidate any which lie in the // changed range, and move any in the remainder of the buffer. - mark_adjust((linenr_T)start_row, - (linenr_T)end_row, - MAXLNUM, - (linenr_T)extra, - kExtmarkNOOP); + // Do not adjust any cursors. need to use column-aware logic (below) + linenr_T adjust = end_row >= start_row ? MAXLNUM : 0; + mark_adjust_buf(buf, (linenr_T)start_row, (linenr_T)end_row, adjust, (linenr_T)extra, + true, true, 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, kExtmarkUndo); - changed_lines((linenr_T)start_row, 0, (linenr_T)end_row + 1, (linenr_T)extra, true); + changed_lines(buf, (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) { - curwin->w_cursor.col -= col_extent - (colnr_T)last_item.size; + FOR_ALL_TAB_WINDOWS(tp, win) { + if (win->w_buffer == buf) { + if (win->w_cursor.lnum >= start_row && win->w_cursor.lnum <= end_row) { + fix_cursor_cols(win, (linenr_T)start_row, (colnr_T)start_col, (linenr_T)end_row, + (colnr_T)end_col, (linenr_T)new_len, (colnr_T)last_item.size); + } else { + fix_cursor(win, (linenr_T)start_row, (linenr_T)end_row, (linenr_T)extra); + } + } } - fix_cursor((linenr_T)start_row, (linenr_T)end_row, (linenr_T)extra); end: for (size_t i = 0; i < new_len; i++) { xfree(lines[i]); } xfree(lines); - aucmd_restbuf(&aco); try_end(err); early_end: @@ -800,10 +788,9 @@ ArrayOf(String) nvim_buf_get_text(uint64_t channel_id, Buffer buffer, { Array rv = ARRAY_DICT_INIT; - if (opts.size > 0) { - api_set_error(err, kErrorTypeValidation, "opts dict isn't empty"); + VALIDATE((opts.size == 0), "%s", "opts dict isn't empty", { return rv; - } + }); buf_T *buf = find_buffer_by_handle(buffer, err); @@ -820,18 +807,16 @@ ArrayOf(String) nvim_buf_get_text(uint64_t channel_id, Buffer buffer, 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"); + VALIDATE((!oob), "%s", "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"); + VALIDATE((start_row <= end_row), "%s", "'start' is higher than 'end'", { return rv; - } + }); bool replace_nl = (channel_id != VIML_INTERNAL_CALL); @@ -907,10 +892,9 @@ Integer nvim_buf_get_offset(Buffer buffer, Integer index, Error *err) return -1; } - if (index < 0 || index > buf->b_ml.ml_line_count) { - api_set_error(err, kErrorTypeValidation, "Index out of bounds"); + VALIDATE((index >= 0 && index <= buf->b_ml.ml_line_count), "%s", "Index out of bounds", { return 0; - } + }); return ml_find_line_or_offset(buf, (int)index + 1, NULL, true); } @@ -1100,7 +1084,7 @@ Boolean nvim_buf_is_loaded(Buffer buffer) /// - unload: Unloaded only, do not delete. See |:bunload| void nvim_buf_delete(Buffer buffer, Dictionary opts, Error *err) FUNC_API_SINCE(7) - FUNC_API_CHECK_TEXTLOCK + FUNC_API_TEXTLOCK { buf_T *buf = find_buffer_by_handle(buffer, err); @@ -1118,8 +1102,9 @@ void nvim_buf_delete(Buffer buffer, Dictionary opts, Error *err) } else if (strequal("unload", k.data)) { unload = api_object_to_bool(v, "unload", false, err); } else { - api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data); - return; + VALIDATE_S(false, "'opts' key", k.data, { + return; + }); } } @@ -1174,20 +1159,16 @@ Boolean nvim_buf_del_mark(Buffer buffer, String name, Error *err) return res; } - if (name.size != 1) { - api_set_error(err, kErrorTypeValidation, - "Mark name must be a single character"); + VALIDATE_S((name.size == 1), "mark name (must be a single char)", name.data, { return res; - } + }); fmark_T *fm = mark_get(buf, curwin, NULL, kMarkAllNoResolve, *name.data); // 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); + VALIDATE_S((fm != NULL), "mark name", name.data, { return res; - } + }); // 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) { @@ -1224,19 +1205,18 @@ Boolean nvim_buf_set_mark(Buffer buffer, String name, Integer line, Integer col, return res; } - if (name.size != 1) { - api_set_error(err, kErrorTypeValidation, - "Mark name must be a single character"); + VALIDATE_S((name.size == 1), "mark name (must be a single char)", name.data, { return res; - } + }); res = set_mark(buf, name, line, col, err); return res; } -/// Returns a tuple (row,col) representing the position of the named mark. See -/// |mark-motions|. +/// Returns a `(row,col)` tuple representing the position of the named mark. +/// "End of line" column position is returned as |v:maxcol| (big number). +/// See |mark-motions|. /// /// Marks are (1,0)-indexed. |api-indexing| /// @@ -1257,21 +1237,18 @@ ArrayOf(Integer, 2) nvim_buf_get_mark(Buffer buffer, String name, Error *err) return rv; } - if (name.size != 1) { - api_set_error(err, kErrorTypeValidation, - "Mark name must be a single character"); + VALIDATE_S((name.size == 1), "mark name (must be a single char)", name.data, { return rv; - } + }); fmark_T *fm; pos_T pos; char mark = *name.data; fm = mark_get(buf, curwin, NULL, kMarkAllNoResolve, mark); - if (fm == NULL) { - api_set_error(err, kErrorTypeValidation, "Invalid mark name"); + VALIDATE_S((fm != NULL), "mark name", name.data, { return rv; - } + }); // (0, 0) uppercase/file mark set in another buffer. if (fm->fnum != buf->handle) { pos.lnum = 0; @@ -1295,15 +1272,15 @@ ArrayOf(Integer, 2) nvim_buf_get_mark(Buffer buffer, String name, Error *err) /// Otherwise a temporary scratch window (called the "autocmd window" for /// historical reasons) will be used. /// -/// This is useful e.g. to call vimL functions that only work with the current -/// buffer/window currently, like |termopen()|. +/// This is useful e.g. to call Vimscript functions that only work with the +/// current buffer/window currently, like |termopen()|. /// /// @param buffer Buffer handle, or 0 for current buffer -/// @param fun Function to call inside the buffer (currently lua callable +/// @param fun Function to call inside the buffer (currently Lua callable /// only) /// @param[out] err Error details, if any -/// @return Return value of function. NB: will deepcopy lua values -/// currently, use upvalues to send lua references in and out. +/// @return Return value of function. NB: will deepcopy Lua values +/// currently, use upvalues to send Lua references in and out. Object nvim_buf_call(Buffer buffer, LuaRef fun, Error *err) FUNC_API_SINCE(7) FUNC_API_LUA_ONLY @@ -1362,42 +1339,92 @@ Dictionary nvim__buf_stats(Buffer buffer, Error *err) // Check if deleting lines made the cursor position invalid. // Changed lines from `lo` to `hi`; added `extra` lines (negative if deleted). -static void fix_cursor(linenr_T lo, linenr_T hi, linenr_T extra) +static void fix_cursor(win_T *win, linenr_T lo, linenr_T hi, linenr_T extra) { - if (curwin->w_cursor.lnum >= lo) { + if (win->w_cursor.lnum >= lo) { // Adjust cursor position if it's in/after the changed lines. - if (curwin->w_cursor.lnum >= hi) { - curwin->w_cursor.lnum += extra; - check_cursor_col(); + if (win->w_cursor.lnum >= hi) { + win->w_cursor.lnum += extra; } else if (extra < 0) { - check_cursor(); - } else { - check_cursor_col(); + check_cursor_lnum(win); } - changed_cline_bef_curs(); + check_cursor_col_win(win); + changed_cline_bef_curs(win); } - invalidate_botline(); + invalidate_botline(win); } -// Normalizes 0-based indexes to buffer line numbers -static int64_t normalize_index(buf_T *buf, int64_t index, bool end_exclusive, bool *oob) +/// Fix cursor position after replacing text +/// between (start_row, start_col) and (end_row, end_col). +/// +/// win->w_cursor.lnum is assumed to be >= start_row and <= end_row. +static void fix_cursor_cols(win_T *win, linenr_T start_row, colnr_T start_col, linenr_T end_row, + colnr_T end_col, linenr_T new_rows, colnr_T new_cols_at_end_row) { - 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 ? max_index + index + 1 : index; - - // Check for oob - if (index > max_index) { - *oob = true; - index = max_index; - } else if (index < 0) { - *oob = true; - index = 0; - } - // Convert the index to a vim line number - index++; - return index; + colnr_T mode_col_adj = win == curwin && (State & MODE_INSERT) ? 0 : 1; + + colnr_T end_row_change_start = new_rows == 1 ? start_col : 0; + colnr_T end_row_change_end = end_row_change_start + new_cols_at_end_row; + + // check if cursor is after replaced range or not + if (win->w_cursor.lnum == end_row && win->w_cursor.col + mode_col_adj > end_col) { + // if cursor is after replaced range, it's shifted + // to keep it's position the same, relative to end_col + + linenr_T old_rows = end_row - start_row + 1; + win->w_cursor.lnum += new_rows - old_rows; + win->w_cursor.col += end_row_change_end - end_col; + } else { + // if cursor is inside replaced range + // and the new range got smaller, + // it's shifted to keep it inside the new range + // + // if cursor is before range or range did not + // got smaller, position is not changed + + colnr_T old_coladd = win->w_cursor.coladd; + + // it's easier to work with a single value here. + // col and coladd are fixed by a later call + // to check_cursor_col_win when necessary + win->w_cursor.col += win->w_cursor.coladd; + win->w_cursor.coladd = 0; + + linenr_T new_end_row = start_row + new_rows - 1; + + // make sure cursor row is in the new row range + if (win->w_cursor.lnum > new_end_row) { + win->w_cursor.lnum = new_end_row; + + // don't simply move cursor up, but to the end + // of new_end_row, if it's not at or after + // it already (in case virtualedit is active) + // column might be additionally adjusted below + // to keep it inside col range if needed + colnr_T len = (colnr_T)strlen(ml_get_buf(win->w_buffer, new_end_row)); + if (win->w_cursor.col < len) { + win->w_cursor.col = len; + } + } + + // if cursor is at the last row and + // it wasn't after eol before, move it exactly + // to end_row_change_end + if (win->w_cursor.lnum == new_end_row + && win->w_cursor.col > end_row_change_end && old_coladd == 0) { + win->w_cursor.col = end_row_change_end; + + // make sure cursor is inside range, not after it, + // except when doing so would move it before new range + if (win->w_cursor.col - mode_col_adj >= end_row_change_start) { + win->w_cursor.col -= mode_col_adj; + } + } + } + + check_cursor_col_win(win); + changed_cline_bef_curs(win); + invalidate_botline(win); } /// Initialise a string array either: @@ -1481,7 +1508,7 @@ bool buf_collect_lines(buf_T *buf, size_t n, linenr_T start, int start_idx, bool return false; } - char *bufstr = ml_get_buf(buf, lnum, false); + char *bufstr = ml_get_buf(buf, lnum); push_linestr(lstate, l, bufstr, strlen(bufstr), start_idx + (int)i, replace_nl); } diff --git a/src/nvim/api/buffer.h b/src/nvim/api/buffer.h index 0814da63cd..f3971c1d30 100644 --- a/src/nvim/api/buffer.h +++ b/src/nvim/api/buffer.h @@ -1,12 +1,12 @@ -#ifndef NVIM_API_BUFFER_H -#define NVIM_API_BUFFER_H +#pragma once -#include <lauxlib.h> +#include <lua.h> // IWYU pragma: keep +#include <stdint.h> // IWYU pragma: keep -#include "nvim/api/private/defs.h" -#include "nvim/buffer_defs.h" +#include "nvim/api/keysets_defs.h" // IWYU pragma: keep +#include "nvim/api/private/defs.h" // IWYU pragma: keep +#include "nvim/buffer_defs.h" // IWYU pragma: keep #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/buffer.h.generated.h" #endif -#endif // NVIM_API_BUFFER_H diff --git a/src/nvim/api/command.c b/src/nvim/api/command.c index abd265f2cf..2a57ce9a19 100644 --- a/src/nvim/api/command.c +++ b/src/nvim/api/command.c @@ -1,36 +1,37 @@ -// 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 <inttypes.h> +#include <lauxlib.h> #include <stdbool.h> #include <stdio.h> #include <string.h> #include "klib/kvec.h" -#include "lauxlib.h" #include "nvim/api/command.h" +#include "nvim/api/keysets_defs.h" #include "nvim/api/private/defs.h" +#include "nvim/api/private/dispatch.h" #include "nvim/api/private/helpers.h" -#include "nvim/ascii.h" +#include "nvim/api/private/validate.h" +#include "nvim/ascii_defs.h" #include "nvim/autocmd.h" #include "nvim/buffer_defs.h" -#include "nvim/decoration.h" +#include "nvim/cmdexpand_defs.h" #include "nvim/ex_cmds.h" #include "nvim/ex_docmd.h" #include "nvim/ex_eval.h" +#include "nvim/func_attr.h" #include "nvim/garray.h" #include "nvim/globals.h" #include "nvim/lua/executor.h" -#include "nvim/macros.h" +#include "nvim/macros_defs.h" #include "nvim/mbyte.h" #include "nvim/memory.h" #include "nvim/ops.h" -#include "nvim/pos.h" +#include "nvim/pos_defs.h" #include "nvim/regexp.h" #include "nvim/strings.h" -#include "nvim/types.h" +#include "nvim/types_defs.h" #include "nvim/usercmd.h" -#include "nvim/vim.h" +#include "nvim/vim_defs.h" #include "nvim/window.h" #ifdef INCLUDE_GENERATED_DECLARATIONS @@ -99,16 +100,15 @@ Dictionary nvim_parse_cmd(String str, Dictionary opts, Error *err) { Dictionary result = ARRAY_DICT_INIT; - if (opts.size > 0) { - api_set_error(err, kErrorTypeValidation, "opts dict isn't empty"); + VALIDATE((opts.size == 0), "%s", "opts dict isn't empty", { return result; - } + }); // Parse command line exarg_T ea; CmdParseInfo cmdinfo; char *cmdline = string_to_cstr(str); - char *errormsg = NULL; + const char *errormsg = NULL; if (!parse_cmdline(cmdline, &ea, &cmdinfo, &errormsg)) { if (errormsg != NULL) { @@ -127,7 +127,7 @@ Dictionary nvim_parse_cmd(String str, Dictionary opts, Error *err) // 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))); + ADD(args, STRING_OBJ(cstrn_to_string(ea.arg, length))); } } else { size_t end = 0; @@ -153,9 +153,9 @@ Dictionary nvim_parse_cmd(String str, Dictionary opts, Error *err) } if (cmd != NULL) { - PUT(result, "cmd", CSTR_TO_OBJ((char *)cmd->uc_name)); + PUT(result, "cmd", CSTR_TO_OBJ(cmd->uc_name)); } else { - PUT(result, "cmd", CSTR_TO_OBJ((char *)get_command_name(NULL, ea.cmdidx))); + PUT(result, "cmd", CSTR_TO_OBJ(get_command_name(NULL, ea.cmdidx))); } if (ea.argt & EX_RANGE) { @@ -237,14 +237,14 @@ Dictionary nvim_parse_cmd(String str, Dictionary opts, Error *err) break; } PUT(result, "addr", CSTR_TO_OBJ(addr)); - PUT(result, "nextcmd", CSTR_TO_OBJ((char *)ea.nextcmd)); + PUT(result, "nextcmd", CSTR_TO_OBJ(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(""))); + : STATIC_CSTR_TO_OBJ("")); PUT(filter, "force", BOOLEAN_OBJ(cmdinfo.cmdmod.cmod_filter_force)); PUT(mods, "filter", DICTIONARY_OBJ(filter)); @@ -305,9 +305,9 @@ end: /// make their usage simpler with |vim.cmd()|. For example, instead of /// `vim.cmd.bdelete{ count = 2 }`, you may do `vim.cmd.bdelete(2)`. /// -/// On execution error: fails with VimL error, updates v:errmsg. +/// On execution error: fails with Vimscript error, updates v:errmsg. /// -/// @see |nvim_exec()| +/// @see |nvim_exec2()| /// @see |nvim_command()| /// /// @param cmd Command to execute. Must be a Dictionary that can contain the same values as @@ -340,32 +340,22 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Error } \ } while (0) -#define OBJ_TO_CMOD_FLAG(flag, value, default, varname) \ +#define VALIDATE_MOD(cond, mod_, name_) \ do { \ - if (api_object_to_bool(value, varname, default, err)) { \ - cmdinfo.cmdmod.cmod_flags |= (flag); \ - } \ - if (ERROR_SET(err)) { \ + if (!(cond)) { \ + api_set_error(err, kErrorTypeValidation, "Command cannot accept %s: %s", (mod_), (name_)); \ 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"); - } + VALIDATE_R(HAS_KEY(cmd, cmd, cmd), "cmd", { + goto end; + }); + VALIDATE_EXP((cmd->cmd.data[0] != NUL), "cmd", "non-empty String", NULL, { + goto end; + }); - cmdname = string_to_cstr(cmd->cmd.data.string); + cmdname = string_to_cstr(cmd->cmd); ea.cmd = cmdname; char *p = find_ex_command(&ea, NULL); @@ -382,12 +372,18 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Error 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); - } + VALIDATE((p != NULL && ea.cmdidx != CMD_SIZE), "Command not found: %s", cmdname, { + goto end; + }); + VALIDATE(!is_cmd_ni(ea.cmdidx), "Command not implemented: %s", cmdname, { + goto end; + }); + const char *fullname = IS_USER_CMDIDX(ea.cmdidx) + ? get_user_command_name(ea.useridx, ea.cmdidx) + : get_command_name(NULL, ea.cmdidx); + VALIDATE(strncmp(fullname, cmdname, strlen(cmdname)) == 0, "Invalid command: \"%s\"", cmdname, { + goto end; + }); // 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. @@ -396,15 +392,11 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Error } // 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"); - } - + if (HAS_KEY(cmd, cmd, args)) { // Process all arguments. Convert non-String arguments to String and check if String arguments // have non-whitespace characters. - for (size_t i = 0; i < cmd->args.data.array.size; i++) { - Object elem = cmd->args.data.array.items[i]; + for (size_t i = 0; i < cmd->args.size; i++) { + Object elem = cmd->args.items[i]; char *data_str; switch (elem.type) { @@ -421,18 +413,19 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Error snprintf(data_str, NUMBUFLEN, "%" PRId64, elem.data.integer); break; case kObjectTypeString: - if (string_iswhite(elem.data.string)) { - VALIDATION_ERROR("String command argument must have at least one non-whitespace " - "character"); - } - data_str = xstrndup(elem.data.string.data, elem.data.string.size); + VALIDATE_EXP(!string_iswhite(elem.data.string), "command arg", "non-whitespace", NULL, { + goto end; + }); + data_str = string_to_cstr(elem.data.string); break; default: - VALIDATION_ERROR("Invalid type for command argument"); + VALIDATE_EXP(false, "command arg", "valid type", api_typename(elem.type), { + goto end; + }); break; } - ADD(args, STRING_OBJ(cstr_as_string(data_str))); + ADD(args, CSTR_AS_OBJ(data_str)); } bool argc_valid; @@ -456,32 +449,30 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Error break; } - if (!argc_valid) { - VALIDATION_ERROR("Incorrect number of arguments supplied"); - } + VALIDATE(argc_valid, "%s", "Wrong number of arguments", { + goto end; + }); } // 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, args.size > 0 ? args.items[0].data.string.data : 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"); - } + if (HAS_KEY(cmd, cmd, range)) { + VALIDATE_MOD((ea.argt & EX_RANGE), "range", cmd->cmd.data); + VALIDATE_EXP((cmd->range.size <= 2), "range", "<=2 elements", NULL, { + goto end; + }); - Array range = cmd->range.data.array; + Array range = cmd->range; 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"); - } + VALIDATE_EXP((elem.type == kObjectTypeInteger && elem.data.integer >= 0), + "range element", "non-negative Integer", NULL, { + goto end; + }); } if (range.size > 0) { @@ -489,9 +480,9 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Error ea.line2 = (linenr_T)range.items[range.size - 1].data.integer; } - if (invalid_range(&ea) != NULL) { - VALIDATION_ERROR("Invalid range provided"); - } + VALIDATE_S((invalid_range(&ea) == NULL), "range", "", { + goto end; + }); } if (ea.addr_count == 0) { if (ea.argt & EX_DFLALL) { @@ -506,48 +497,42 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Error } } - 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, cmd, count)) { + VALIDATE_MOD((ea.argt & EX_COUNT), "count", cmd->cmd.data); + VALIDATE_EXP((cmd->count >= 0), "count", "non-negative Integer", NULL, { + goto end; + }); + set_cmd_count(&ea, (linenr_T)cmd->count, 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); - } + if (HAS_KEY(cmd, cmd, reg)) { + VALIDATE_MOD((ea.argt & EX_REGSTR), "register", cmd->cmd.data); + VALIDATE_EXP((cmd->reg.size == 1), + "reg", "single character", cmd->reg.data, { + goto end; + }); + char regname = cmd->reg.data[0]; + VALIDATE((regname != '='), "%s", "Cannot use register \"=", { + goto end; + }); + VALIDATE(valid_yank_reg(regname, ea.cmdidx != CMD_put && !IS_USER_CMDIDX(ea.cmdidx)), + "Invalid register: \"%c", regname, { + goto end; + }); 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"); - } + ea.forceit = cmd->bang; + VALIDATE_MOD((!ea.forceit || (ea.argt & EX_BANG)), "bang", cmd->cmd.data); - Dict(cmd_magic) magic = { 0 }; - if (!api_dict_to_keydict(&magic, KeyDict_cmd_magic_get_field, - cmd->magic.data.dictionary, err)) { + if (HAS_KEY(cmd, cmd, magic)) { + Dict(cmd_magic) magic[1] = { 0 }; + if (!api_dict_to_keydict(magic, KeyDict_cmd_magic_get_field, cmd->magic, 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'"); + cmdinfo.magic.file = HAS_KEY(magic, cmd_magic, file) ? magic->file : (ea.argt & EX_XFILE); + cmdinfo.magic.bar = HAS_KEY(magic, cmd_magic, bar) ? magic->bar : (ea.argt & EX_TRLBAR); if (cmdinfo.magic.file) { ea.argt |= EX_XFILE; } else { @@ -558,107 +543,90 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Error 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)) { + if (HAS_KEY(cmd, cmd, mods)) { + Dict(cmd_mods) mods[1] = { 0 }; + if (!api_dict_to_keydict(mods, KeyDict_cmd_mods_get_field, cmd->mods, 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 (HAS_KEY(mods, cmd_mods, filter)) { + Dict(cmd_mods_filter) filter[1] = { 0 }; if (!api_dict_to_keydict(&filter, KeyDict_cmd_mods_filter_get_field, - mods.filter.data.dictionary, err)) { + mods->filter, 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'"); + if (HAS_KEY(filter, cmd_mods_filter, pattern)) { + cmdinfo.cmdmod.cmod_filter_force = 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); + if (*filter->pattern.data != NUL || cmdinfo.cmdmod.cmod_filter_force) { + cmdinfo.cmdmod.cmod_filter_pat = string_to_cstr(filter->pattern); 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) { - VALIDATION_ERROR("'mods.tab' must be an Integer"); - } else if ((int)mods.tab.data.integer >= 0) { + if (HAS_KEY(mods, cmd_mods, tab)) { + if ((int)mods->tab >= 0) { // Silently ignore negative integers to allow mods.tab to be set to -1. - cmdinfo.cmdmod.cmod_tab = (int)mods.tab.data.integer + 1; + cmdinfo.cmdmod.cmod_tab = (int)mods->tab + 1; } } - if (HAS_KEY(mods.verbose)) { - if (mods.verbose.type != kObjectTypeInteger) { - VALIDATION_ERROR("'mods.verbose' must be an Integer"); - } else if ((int)mods.verbose.data.integer >= 0) { + if (HAS_KEY(mods, cmd_mods, verbose)) { + if ((int)mods->verbose >= 0) { // Silently ignore negative integers to allow mods.verbose to be set to -1. - cmdinfo.cmdmod.cmod_verbose = (int)mods.verbose.data.integer + 1; + cmdinfo.cmdmod.cmod_verbose = (int)mods->verbose + 1; } } - bool vertical; - OBJ_TO_BOOL(vertical, mods.vertical, false, "'mods.vertical'"); - cmdinfo.cmdmod.cmod_split |= (vertical ? WSP_VERT : 0); + cmdinfo.cmdmod.cmod_split |= (mods->vertical ? WSP_VERT : 0); - bool horizontal; - OBJ_TO_BOOL(horizontal, mods.horizontal, false, "'mods.horizontal'"); - cmdinfo.cmdmod.cmod_split |= (horizontal ? WSP_HOR : 0); + cmdinfo.cmdmod.cmod_split |= (mods->horizontal ? WSP_HOR : 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) { + if (HAS_KEY(mods, cmd_mods, split)) { + if (*mods->split.data == NUL) { // Empty string, do nothing. - } else if (strcmp(mods.split.data.string.data, "aboveleft") == 0 - || strcmp(mods.split.data.string.data, "leftabove") == 0) { + } else if (strcmp(mods->split.data, "aboveleft") == 0 + || strcmp(mods->split.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) { + } else if (strcmp(mods->split.data, "belowright") == 0 + || strcmp(mods->split.data, "rightbelow") == 0) { cmdinfo.cmdmod.cmod_split |= WSP_BELOW; - } else if (strcmp(mods.split.data.string.data, "topleft") == 0) { + } else if (strcmp(mods->split.data, "topleft") == 0) { cmdinfo.cmdmod.cmod_split |= WSP_TOP; - } else if (strcmp(mods.split.data.string.data, "botright") == 0) { + } else if (strcmp(mods->split.data, "botright") == 0) { cmdinfo.cmdmod.cmod_split |= WSP_BOT; } else { - VALIDATION_ERROR("Invalid value for 'mods.split'"); + VALIDATE_S(false, "mods.split", "", { + goto end; + }); } } - 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.unsilent, 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'"); +#define OBJ_TO_CMOD_FLAG(flag, value) \ + if (value) { \ + cmdinfo.cmdmod.cmod_flags |= (flag); \ + } + + OBJ_TO_CMOD_FLAG(CMOD_SILENT, mods->silent); + OBJ_TO_CMOD_FLAG(CMOD_ERRSILENT, mods->emsg_silent); + OBJ_TO_CMOD_FLAG(CMOD_UNSILENT, mods->unsilent); + OBJ_TO_CMOD_FLAG(CMOD_SANDBOX, mods->sandbox); + OBJ_TO_CMOD_FLAG(CMOD_NOAUTOCMD, mods->noautocmd); + OBJ_TO_CMOD_FLAG(CMOD_BROWSE, mods->browse); + OBJ_TO_CMOD_FLAG(CMOD_CONFIRM, mods->confirm); + OBJ_TO_CMOD_FLAG(CMOD_HIDE, mods->hide); + OBJ_TO_CMOD_FLAG(CMOD_KEEPALT, mods->keepalt); + OBJ_TO_CMOD_FLAG(CMOD_KEEPJUMPS, mods->keepjumps); + OBJ_TO_CMOD_FLAG(CMOD_KEEPMARKS, mods->keepmarks); + OBJ_TO_CMOD_FLAG(CMOD_KEEPPATTERNS, mods->keeppatterns); + OBJ_TO_CMOD_FLAG(CMOD_LOCKMARKS, mods->lockmarks); + OBJ_TO_CMOD_FLAG(CMOD_NOSWAPFILE, mods->noswapfile); if (cmdinfo.cmdmod.cmod_flags & CMOD_ERRSILENT) { // CMOD_ERRSILENT must imply CMOD_SILENT, otherwise apply_cmdmod() and undo_cmdmod() won't @@ -666,9 +634,10 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Error cmdinfo.cmdmod.cmod_flags |= CMOD_SILENT; } - if ((cmdinfo.cmdmod.cmod_flags & CMOD_SANDBOX) && !(ea.argt & EX_SBOXOK)) { - VALIDATION_ERROR("Command cannot be run in sandbox"); - } + VALIDATE(!((cmdinfo.cmdmod.cmod_flags & CMOD_SANDBOX) && !(ea.argt & EX_SBOXOK)), + "%s", "Command cannot be run in sandbox", { + goto end; + }); } // Finally, build the command line string that will be stored inside ea.cmdlinep. @@ -681,14 +650,13 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Error garray_T * const save_capture_ga = capture_ga; const int save_msg_col = msg_col; - if (output) { + if (opts->output) { ga_init(&capture_local, 1, 80); capture_ga = &capture_local; } - TRY_WRAP({ - try_start(); - if (output) { + TRY_WRAP(err, { + if (opts->output) { msg_silent++; msg_col = 0; // prevent leading spaces } @@ -697,21 +665,19 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Error execute_cmd(&ea, &cmdinfo, false); }); - if (output) { + if (opts->output) { capture_ga = save_capture_ga; msg_silent = save_msg_silent; // Put msg_col back where it was, since nothing should have been written. msg_col = save_msg_col; } - - try_end(err); }); if (ERROR_SET(err)) { goto clear_ga; } - if (output && capture_local.ga_len > 1) { + if (opts->output && capture_local.ga_len > 1) { retv = (String){ .data = capture_local.ga_data, .size = (size_t)capture_local.ga_len, @@ -725,7 +691,7 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Error goto end; } clear_ga: - if (output) { + if (opts->output) { ga_clear(&capture_local); } end: @@ -739,7 +705,7 @@ end: #undef OBJ_TO_BOOL #undef OBJ_TO_CMOD_FLAG -#undef VALIDATION_ERROR +#undef VALIDATE_MOD } /// Check if a string contains only whitespace characters. @@ -870,8 +836,8 @@ static void build_cmdline_str(char **cmdlinep, exarg_T *eap, CmdParseInfo *cmdin offset += eap->arglens[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 - 1; // Subtract 1 to account for NUL + eap->arg = argc > 0 ? eap->args[0] + : cmdline.items + cmdline.size - 1; // Subtract 1 to account for NUL // Finally, make cmdlinep point to the cmdline string. *cmdlinep = cmdline.items; @@ -888,18 +854,17 @@ static void build_cmdline_str(char **cmdlinep, exarg_T *eap, CmdParseInfo *cmdin } } -/// Create a new user command |user-commands| +/// Creates a global |user-commands| command. /// -/// {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. +/// For Lua usage see |lua-guide-commands-create|. /// /// Example: -/// <pre>vim -/// :call nvim_create_user_command('SayHello', 'echo "Hello world!"', {}) -/// :SayHello -/// Hello world! -/// </pre> +/// +/// ```vim +/// :call nvim_create_user_command('SayHello', 'echo "Hello world!"', {'bang': v:true}) +/// :SayHello +/// Hello world! +/// ``` /// /// @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 @@ -909,6 +874,7 @@ static void build_cmdline_str(char **cmdlinep, exarg_T *eap, CmdParseInfo *cmdin /// - 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>| +/// - nargs: (string) Number of arguments |:command-nargs| /// - 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>| @@ -918,20 +884,22 @@ static void build_cmdline_str(char **cmdlinep, exarg_T *eap, CmdParseInfo *cmdin /// - 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 opts Optional |command-attributes|. +/// - Set boolean attributes such as |:command-bang| or |:command-bar| to true (but +/// not |:command-buffer|, use |nvim_buf_create_user_command()| instead). +/// - "complete" |:command-complete| also accepts a Lua function which works like +/// |:command-completion-customlist|. +/// - Other 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) +void nvim_create_user_command(uint64_t channel_id, String name, Object command, + Dict(user_command) *opts, Error *err) FUNC_API_SINCE(9) { - create_user_command(name, command, opts, 0, err); + create_user_command(channel_id, name, command, opts, 0, err); } /// Delete a user-defined command. @@ -944,12 +912,12 @@ void nvim_del_user_command(String name, Error *err) nvim_buf_del_user_command(-1, name, err); } -/// Create a new user command |user-commands| in the given buffer. +/// Creates a buffer-local command |user-commands|. /// /// @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, +void nvim_buf_create_user_command(uint64_t channel_id, Buffer buffer, String name, Object command, Dict(user_command) *opts, Error *err) FUNC_API_SINCE(9) { @@ -960,7 +928,7 @@ void nvim_buf_create_user_command(Buffer buffer, String name, Object command, buf_T *save_curbuf = curbuf; curbuf = target_buf; - create_user_command(name, command, opts, UC_BUFFER, err); + create_user_command(channel_id, name, command, opts, UC_BUFFER, err); curbuf = save_curbuf; } @@ -998,36 +966,33 @@ void nvim_buf_del_user_command(Buffer buffer, String name, Error *err) } } - api_set_error(err, kErrorTypeException, "No such user-defined command: %s", name.data); + api_set_error(err, kErrorTypeException, "Invalid command (not found): %s", name.data); } -void create_user_command(String name, Object command, Dict(user_command) *opts, int flags, - Error *err) +void create_user_command(uint64_t channel_id, String name, Object command, Dict(user_command) *opts, + int flags, Error *err) { uint32_t argt = 0; - long def = -1; + int64_t def = -1; cmd_addr_T addr_type_arg = ADDR_NONE; - int compl = EXPAND_NOTHING; + int context = EXPAND_NOTHING; char *compl_arg = NULL; const 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"); + VALIDATE_S(uc_validate_name(name.data), "command name", name.data, { goto err; - } - - if (mb_islower(name.data[0])) { - api_set_error(err, kErrorTypeValidation, "'name' must begin with an uppercase letter"); + }); + VALIDATE_S(!mb_islower(name.data[0]), "command name (must start with uppercase)", + name.data, { goto err; - } - - if (HAS_KEY(opts->range) && HAS_KEY(opts->count)) { - api_set_error(err, kErrorTypeValidation, "'range' and 'count' are mutually exclusive"); + }); + VALIDATE((!HAS_KEY(opts, user_command, range) || !HAS_KEY(opts, user_command, count)), "%s", + "Cannot use both 'range' and 'count'", { goto err; - } + }); if (opts->nargs.type == kObjectTypeInteger) { switch (opts->nargs.data.integer) { @@ -1038,14 +1003,14 @@ void create_user_command(String name, Object command, Dict(user_command) *opts, argt |= EX_EXTRA | EX_NOSPC | EX_NEEDARG; break; default: - api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); - goto err; + VALIDATE_INT(false, "nargs", (int64_t)opts->nargs.data.integer, { + goto err; + }); } } else if (opts->nargs.type == kObjectTypeString) { - if (opts->nargs.data.string.size > 1) { - api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); + VALIDATE_S((opts->nargs.data.string.size <= 1), "nargs", opts->nargs.data.string.data, { goto err; - } + }); switch (opts->nargs.data.string.data[0]) { case '*': @@ -1058,18 +1023,20 @@ void create_user_command(String name, Object command, Dict(user_command) *opts, argt |= EX_EXTRA | EX_NEEDARG; break; default: - api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); - goto err; + VALIDATE_S(false, "nargs", opts->nargs.data.string.data, { + goto err; + }); } - } else if (HAS_KEY(opts->nargs)) { - api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); - goto err; + } else if (HAS_KEY(opts, user_command, nargs)) { + VALIDATE_S(false, "nargs", "", { + goto err; + }); } - if (HAS_KEY(opts->complete) && !argt) { - api_set_error(err, kErrorTypeValidation, "'complete' used without 'nargs'"); + VALIDATE((!HAS_KEY(opts, user_command, complete) || argt), + "%s", "'complete' used without 'nargs'", { goto err; - } + }); if (opts->range.type == kObjectTypeBoolean) { if (opts->range.data.boolean) { @@ -1077,20 +1044,20 @@ void create_user_command(String name, Object command, Dict(user_command) *opts, 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'"); + VALIDATE_S((opts->range.data.string.data[0] == '%' && opts->range.data.string.size == 1), + "range", "", { goto err; - } + }); + argt |= EX_RANGE | EX_DFLALL; + addr_type_arg = ADDR_LINES; } 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; + } else if (HAS_KEY(opts, user_command, range)) { + VALIDATE_S(false, "range", "", { + goto err; + }); } if (opts->count.type == kObjectTypeBoolean) { @@ -1103,76 +1070,72 @@ void create_user_command(String name, Object command, Dict(user_command) *opts, 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; + } else if (HAS_KEY(opts, user_command, count)) { + VALIDATE_S(false, "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'"); + if (HAS_KEY(opts, user_command, addr)) { + VALIDATE_T("addr", kObjectTypeString, opts->addr.type, { goto err; - } + }); + + VALIDATE_S(OK == parse_addr_type_arg(opts->addr.data.string.data, + (int)opts->addr.data.string.size, &addr_type_arg), "addr", + opts->addr.data.string.data, { + 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)) { + if (opts->bang) { argt |= EX_BANG; - } else if (ERROR_SET(err)) { - goto err; } - if (api_object_to_bool(opts->bar, "bar", false, err)) { + if (opts->bar) { argt |= EX_TRLBAR; - } else if (ERROR_SET(err)) { - goto err; } - if (api_object_to_bool(opts->register_, "register", false, err)) { + if (opts->register_) { argt |= EX_REGSTR; - } else if (ERROR_SET(err)) { - goto err; } - if (api_object_to_bool(opts->keepscript, "keepscript", false, err)) { + if (opts->keepscript) { argt |= EX_KEEPSCRIPT; - } else if (ERROR_SET(err)) { - goto err; } - bool force = api_object_to_bool(opts->force, "force", true, err); + bool force = GET_BOOL_OR_TRUE(opts, user_command, force); if (ERROR_SET(err)) { goto err; } if (opts->complete.type == kObjectTypeLuaRef) { - compl = EXPAND_USER_LUA; + context = 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'"); + VALIDATE_S(OK == parse_compl_arg(opts->complete.data.string.data, + (int)opts->complete.data.string.size, &context, &argt, + &compl_arg), + "complete", opts->complete.data.string.data, { goto err; - } - } else if (HAS_KEY(opts->complete)) { - api_set_error(err, kErrorTypeValidation, "Invalid value for 'complete'"); - goto err; + }); + } else if (HAS_KEY(opts, user_command, complete)) { + VALIDATE_EXP(false, "complete", "Function or String", NULL, { + goto err; + }); } - if (opts->preview.type == kObjectTypeLuaRef) { + if (HAS_KEY(opts, user_command, preview)) { + VALIDATE_T("preview", kObjectTypeLuaRef, opts->preview.type, { + goto err; + }); + 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) { @@ -1188,15 +1151,18 @@ void create_user_command(String name, Object command, Dict(user_command) *opts, rep = command.data.string.data; break; default: - api_set_error(err, kErrorTypeValidation, "'command' must be a string or Lua function"); - goto err; + VALIDATE_EXP(false, "command", "Function or String", NULL, { + 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 - } + WITH_SCRIPT_CONTEXT(channel_id, { + if (uc_add_command(name.data, name.size, rep, argt, def, flags, context, 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; @@ -1209,6 +1175,8 @@ err: /// /// Currently only |user-commands| are supported, not builtin Ex commands. /// +/// @see |nvim_get_all_options_info()| +/// /// @param opts Optional parameters. Currently only supports /// {"builtin":false} /// @param[out] err Error details, if any. @@ -1231,13 +1199,12 @@ Dictionary nvim_buf_get_commands(Buffer buffer, Dict(get_commands) *opts, Error 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) { + if (opts->builtin) { api_set_error(err, kErrorTypeValidation, "builtin=true not implemented"); return (Dictionary)ARRAY_DICT_INIT; } @@ -1245,7 +1212,7 @@ Dictionary nvim_buf_get_commands(Buffer buffer, Dict(get_commands) *opts, Error } buf_T *buf = find_buffer_by_handle(buffer, err); - if (builtin || !buf) { + if (opts->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 index b1c9230551..1cccbfb4c7 100644 --- a/src/nvim/api/command.h +++ b/src/nvim/api/command.h @@ -1,11 +1,10 @@ -#ifndef NVIM_API_COMMAND_H -#define NVIM_API_COMMAND_H +#pragma once -#include "nvim/api/private/defs.h" -#include "nvim/decoration.h" -#include "nvim/ex_cmds.h" +#include <stdint.h> // IWYU pragma: keep + +#include "nvim/api/keysets_defs.h" // IWYU pragma: keep +#include "nvim/api/private/defs.h" // IWYU pragma: keep #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 332e2b5fc3..2ec11236d7 100644 --- a/src/nvim/api/deprecated.c +++ b/src/nvim/api/deprecated.c @@ -1,36 +1,50 @@ -// 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 <string.h> #include "nvim/api/buffer.h" #include "nvim/api/deprecated.h" #include "nvim/api/extmark.h" +#include "nvim/api/keysets_defs.h" +#include "nvim/api/options.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/helpers.h" +#include "nvim/api/private/validate.h" #include "nvim/api/vimscript.h" #include "nvim/buffer_defs.h" #include "nvim/decoration.h" #include "nvim/extmark.h" +#include "nvim/func_attr.h" #include "nvim/globals.h" +#include "nvim/highlight.h" +#include "nvim/highlight_group.h" #include "nvim/lua/executor.h" #include "nvim/memory.h" -#include "nvim/pos.h" -#include "nvim/types.h" +#include "nvim/option.h" +#include "nvim/pos_defs.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/deprecated.c.generated.h" #endif +/// @deprecated Use nvim_exec2() instead. +/// @see nvim_exec2 +String nvim_exec(uint64_t channel_id, String src, Boolean output, Error *err) + FUNC_API_SINCE(7) + FUNC_API_DEPRECATED_SINCE(11) +{ + Dict(exec_opts) opts = { .output = output }; + return exec_impl(channel_id, src, &opts, err); +} + /// @deprecated -/// @see nvim_exec +/// @see nvim_exec2 String nvim_command_output(uint64_t channel_id, String command, Error *err) FUNC_API_SINCE(1) FUNC_API_DEPRECATED_SINCE(7) { - return nvim_exec(channel_id, command, true, err); + Dict(exec_opts) opts = { .output = true }; + return exec_impl(channel_id, command, &opts, err); } /// @deprecated Use nvim_exec_lua() instead. @@ -140,25 +154,71 @@ 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); + DecorVirtText *existing = decor_find_virttext(buf, (int)line, ns_id); if (existing) { - clear_virttext(&existing->virt_text); - existing->virt_text = virt_text; - existing->virt_text_width = width; + clear_virttext(&existing->data.virt_text); + existing->data.virt_text = virt_text; + existing->width = width; return src_id; } - Decoration decor = DECORATION_INIT; - decor.virt_text = virt_text; - decor.virt_text_width = width; - decor.priority = 0; + DecorVirtText *vt = xmalloc(sizeof *vt); + *vt = (DecorVirtText)DECOR_VIRT_TEXT_INIT; + vt->data.virt_text = virt_text; + vt->width = width; + vt->priority = 0; - extmark_set(buf, ns_id, NULL, (int)line, 0, -1, -1, &decor, true, - false, kExtmarkNoUndo); + DecorInline decor = { .ext = true, .data.ext.vt = vt, .data.ext.sh_idx = DECOR_ID_INVALID }; + + extmark_set(buf, ns_id, NULL, (int)line, 0, -1, -1, decor, 0, true, + false, false, false, NULL); return src_id; } +/// Gets a highlight definition by id. |hlID()| +/// +/// @deprecated use |nvim_get_hl()| instead +/// +/// @param hl_id Highlight id as returned by |hlID()| +/// @param rgb Export RGB colors +/// @param[out] err Error details, if any +/// @return Highlight definition map +/// @see nvim_get_hl_by_name +Dictionary nvim_get_hl_by_id(Integer hl_id, Boolean rgb, Arena *arena, Error *err) + FUNC_API_SINCE(3) + FUNC_API_DEPRECATED_SINCE(9) +{ + Dictionary dic = ARRAY_DICT_INIT; + VALIDATE_INT((syn_get_final_id((int)hl_id) != 0), "highlight id", hl_id, { + return dic; + }); + int attrcode = syn_id2attr((int)hl_id); + return hl_get_attr_by_id(attrcode, rgb, arena, err); +} + +/// Gets a highlight definition by name. +/// +/// @deprecated use |nvim_get_hl()| instead +/// +/// @param name Highlight group name +/// @param rgb Export RGB colors +/// @param[out] err Error details, if any +/// @return Highlight definition map +/// @see nvim_get_hl_by_id +Dictionary nvim_get_hl_by_name(String name, Boolean rgb, Arena *arena, Error *err) + FUNC_API_SINCE(3) + FUNC_API_DEPRECATED_SINCE(9) +{ + Dictionary result = ARRAY_DICT_INIT; + int id = syn_name2id(name.data); + + VALIDATE_S((id != 0), "highlight name", name.data, { + return result; + }); + return nvim_get_hl_by_id(id, rgb, arena, err); +} + /// Inserts a sequence of lines to a buffer at a certain index /// /// @deprecated use nvim_buf_set_lines(buffer, lnum, lnum, true, lines) @@ -449,3 +509,195 @@ static int64_t convert_index(int64_t index) { return index < 0 ? index - 1 : index; } + +/// Gets the option information for one option +/// +/// @deprecated Use @ref nvim_get_option_info2 instead. +/// +/// @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) + FUNC_API_DEPRECATED_SINCE(11) +{ + return get_vimoption(name, OPT_GLOBAL, curbuf, curwin, err); +} + +/// Sets the global value of an option. +/// +/// @deprecated +/// @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) + FUNC_API_DEPRECATED_SINCE(11) +{ + set_option_to(channel_id, NULL, kOptReqGlobal, name, value, err); +} + +/// Gets the global value of an option. +/// +/// @deprecated +/// @param name Option name +/// @param[out] err Error details, if any +/// @return Option value (global) +Object nvim_get_option(String name, Arena *arena, Error *err) + FUNC_API_SINCE(1) + FUNC_API_DEPRECATED_SINCE(11) +{ + return get_option_from(NULL, kOptReqGlobal, name, err); +} + +/// Gets a buffer option value +/// +/// @deprecated +/// @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, Arena *arena, Error *err) + FUNC_API_SINCE(1) + FUNC_API_DEPRECATED_SINCE(11) +{ + buf_T *buf = find_buffer_by_handle(buffer, err); + + if (!buf) { + return (Object)OBJECT_INIT; + } + + return get_option_from(buf, kOptReqBuf, name, err); +} + +/// Sets a buffer option value. Passing `nil` as value deletes the option (only +/// works if there's a global fallback) +/// +/// @deprecated +/// @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) + FUNC_API_DEPRECATED_SINCE(11) +{ + buf_T *buf = find_buffer_by_handle(buffer, err); + + if (!buf) { + return; + } + + set_option_to(channel_id, buf, kOptReqBuf, name, value, err); +} + +/// Gets a window option value +/// +/// @deprecated +/// @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, Arena *arena, Error *err) + FUNC_API_SINCE(1) + FUNC_API_DEPRECATED_SINCE(11) +{ + win_T *win = find_window_by_handle(window, err); + + if (!win) { + return (Object)OBJECT_INIT; + } + + return get_option_from(win, kOptReqWin, name, err); +} + +/// Sets a window option value. Passing `nil` as value deletes the option (only +/// works if there's a global fallback) +/// +/// @deprecated +/// @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) + FUNC_API_DEPRECATED_SINCE(11) +{ + win_T *win = find_window_by_handle(window, err); + + if (!win) { + return; + } + + set_option_to(channel_id, win, kOptReqWin, name, value, err); +} + +/// Gets the value of a global or local (buffer, window) option. +/// +/// @param[in] from Pointer to buffer or window for local option value. +/// @param req_scope Requested option scope. See OptReqScope in option.h. +/// @param name The option name. +/// @param[out] err Details of an error that may have occurred. +/// +/// @return the option value. +static Object get_option_from(void *from, OptReqScope req_scope, String name, Error *err) +{ + VALIDATE_S(name.size > 0, "option name", "<empty>", { + return (Object)OBJECT_INIT; + }); + + OptVal value = get_option_value_strict(name.data, req_scope, from, err); + if (ERROR_SET(err)) { + return (Object)OBJECT_INIT; + } + + VALIDATE_S(value.type != kOptValTypeNil, "option name", name.data, { + return (Object)OBJECT_INIT; + }); + + return optval_as_object(value); +} + +/// Sets the value of a global or local (buffer, window) option. +/// +/// @param[in] to Pointer to buffer or window for local option value. +/// @param req_scope Requested option scope. See OptReqScope in option.h. +/// @param name The option name. +/// @param value New option value. +/// @param[out] err Details of an error that may have occurred. +static void set_option_to(uint64_t channel_id, void *to, OptReqScope req_scope, String name, + Object value, Error *err) +{ + VALIDATE_S(name.size > 0, "option name", "<empty>", { + return; + }); + + int flags = get_option_attrs(name.data); + VALIDATE_S(flags != 0, "option name", name.data, { + return; + }); + + bool error = false; + OptVal optval = object_as_optval(value, &error); + + // Handle invalid option value type. + // Don't use `name` in the error message here, because `name` can be any String. + // No need to check if value type actually matches the types for the option, as set_option_value() + // already handles that. + VALIDATE_EXP(!error, "value", "valid option type", api_typename(value.type), { + return; + }); + + // For global-win-local options -> setlocal + // For win-local options -> setglobal and setlocal (opt_flags == 0) + const int opt_flags = (req_scope == kOptReqWin && !(flags & SOPT_GLOBAL)) + ? 0 + : (req_scope == kOptReqGlobal) ? OPT_GLOBAL : OPT_LOCAL; + + WITH_SCRIPT_CONTEXT(channel_id, { + set_option_value_for(name.data, optval, opt_flags, req_scope, to, err); + }); +} diff --git a/src/nvim/api/deprecated.h b/src/nvim/api/deprecated.h index 79095167e1..e20d8304e0 100644 --- a/src/nvim/api/deprecated.h +++ b/src/nvim/api/deprecated.h @@ -1,11 +1,9 @@ -#ifndef NVIM_API_DEPRECATED_H -#define NVIM_API_DEPRECATED_H +#pragma once -#include <stdint.h> +#include <stdint.h> // IWYU pragma: keep -#include "nvim/api/private/defs.h" +#include "nvim/api/private/defs.h" // IWYU pragma: keep #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/deprecated.h.generated.h" #endif -#endif // NVIM_API_DEPRECATED_H diff --git a/src/nvim/api/extmark.c b/src/nvim/api/extmark.c index ab3b3485e4..d71498d6ed 100644 --- a/src/nvim/api/extmark.c +++ b/src/nvim/api/extmark.c @@ -1,29 +1,31 @@ -// 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 <lauxlib.h> #include <stdbool.h> #include <stdint.h> #include <string.h> #include "klib/kvec.h" -#include "lauxlib.h" #include "nvim/api/extmark.h" +#include "nvim/api/keysets_defs.h" #include "nvim/api/private/defs.h" +#include "nvim/api/private/dispatch.h" #include "nvim/api/private/helpers.h" +#include "nvim/api/private/validate.h" #include "nvim/buffer_defs.h" #include "nvim/charset.h" #include "nvim/decoration.h" #include "nvim/decoration_provider.h" #include "nvim/drawscreen.h" #include "nvim/extmark.h" +#include "nvim/func_attr.h" +#include "nvim/grid.h" #include "nvim/highlight_group.h" +#include "nvim/marktree.h" #include "nvim/mbyte.h" #include "nvim/memline.h" #include "nvim/memory.h" -#include "nvim/pos.h" -#include "nvim/strings.h" -#include "nvim/vim.h" +#include "nvim/pos_defs.h" +#include "nvim/sign.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/extmark.c.generated.h" @@ -32,12 +34,10 @@ void api_extmark_free_all_mem(void) { String name; - handle_T id; - map_foreach(&namespace_ids, name, id, { - (void)id; + map_foreach_key(&namespace_ids, name, { xfree(name.data); }) - map_destroy(String, handle_T)(&namespace_ids); + map_destroy(String, &namespace_ids); } /// Creates a new namespace or gets an existing one. \*namespace\* @@ -54,14 +54,14 @@ void api_extmark_free_all_mem(void) Integer nvim_create_namespace(String name) FUNC_API_SINCE(5) { - handle_T id = map_get(String, handle_T)(&namespace_ids, name); + handle_T id = map_get(String, int)(&namespace_ids, name); if (id > 0) { return id; } id = next_namespace_id++; if (name.size > 0) { String name_alloc = copy_string(name, NULL); - map_put(String, handle_T)(&namespace_ids, name_alloc, id); + map_put(String, int)(&namespace_ids, name_alloc, id); } return (Integer)id; } @@ -83,7 +83,7 @@ Dictionary nvim_get_namespaces(void) return retval; } -const char *describe_ns(NS ns_id) +const char *describe_ns(NS ns_id, const char *unknown) { String name; handle_T id; @@ -92,7 +92,7 @@ const char *describe_ns(NS ns_id) return name.data; } }) - return "(UNKNOWN PLUGIN)"; + return unknown; } // Is the Namespace in use? @@ -104,92 +104,73 @@ bool ns_initialized(uint32_t ns) return ns < (uint32_t)next_namespace_id; } -static Array extmark_to_array(const ExtmarkInfo *extmark, bool id, bool add_dict) +Array virt_text_to_array(VirtText vt, bool hl_name) { + Array chunks = ARRAY_DICT_INIT; + Array hl_array = ARRAY_DICT_INIT; + for (size_t i = 0; i < kv_size(vt); i++) { + char *text = kv_A(vt, i).text; + int hl_id = kv_A(vt, i).hl_id; + if (text == NULL) { + if (hl_id > 0) { + ADD(hl_array, hl_group_name(hl_id, hl_name)); + } + continue; + } + Array chunk = ARRAY_DICT_INIT; + ADD(chunk, CSTR_TO_OBJ(text)); + if (hl_array.size > 0) { + if (hl_id > 0) { + ADD(hl_array, hl_group_name(hl_id, hl_name)); + } + ADD(chunk, ARRAY_OBJ(hl_array)); + hl_array = (Array)ARRAY_DICT_INIT; + } else if (hl_id > 0) { + ADD(chunk, hl_group_name(hl_id, hl_name)); + } + ADD(chunks, ARRAY_OBJ(chunk)); + } + assert(hl_array.size == 0); + return chunks; +} + +static Array extmark_to_array(MTPair extmark, bool id, bool add_dict, bool hl_name) +{ + MTKey start = extmark.start; Array rv = ARRAY_DICT_INIT; if (id) { - ADD(rv, INTEGER_OBJ((Integer)extmark->mark_id)); + ADD(rv, INTEGER_OBJ((Integer)start.id)); } - ADD(rv, INTEGER_OBJ(extmark->row)); - ADD(rv, INTEGER_OBJ(extmark->col)); + ADD(rv, INTEGER_OBJ(start.pos.row)); + ADD(rv, INTEGER_OBJ(start.pos.col)); if (add_dict) { Dictionary dict = ARRAY_DICT_INIT; - PUT(dict, "right_gravity", BOOLEAN_OBJ(extmark->right_gravity)); + PUT(dict, "ns_id", INTEGER_OBJ((Integer)start.ns)); - 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)); - } + PUT(dict, "right_gravity", BOOLEAN_OBJ(mt_right(start))); - 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.end_pos.row >= 0) { + PUT(dict, "end_row", INTEGER_OBJ(extmark.end_pos.row)); + PUT(dict, "end_col", INTEGER_OBJ(extmark.end_pos.col)); + PUT(dict, "end_right_gravity", BOOLEAN_OBJ(extmark.end_right_gravity)); } - 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)); - } - 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 (mt_no_undo(start)) { + PUT(dict, "undo_restore", BOOLEAN_OBJ(false)); } - if (decor->ui_watched) { - PUT(dict, "ui_watched", BOOLEAN_OBJ(true)); + if (mt_invalidate(start)) { + PUT(dict, "invalidate", 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; - 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 = &vt->items[j]; - 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)); - } - 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 (mt_invalid(start)) { + PUT(dict, "invalid", BOOLEAN_OBJ(true)); } - if (decor->hl_id || kv_size(decor->virt_text) || decor->ui_watched) { - PUT(dict, "priority", INTEGER_OBJ(decor->priority)); - } + decor_to_dict_legacy(&dict, mt_decor(start), hl_name); - if (dict.size) { - ADD(rv, DICTIONARY_OBJ(dict)); - } + ADD(rv, DICTIONARY_OBJ(dict)); } return rv; @@ -202,6 +183,7 @@ static Array extmark_to_array(const ExtmarkInfo *extmark, bool id, bool add_dict /// @param id Extmark id /// @param opts Optional parameters. Keys: /// - details: Whether to include the details dict +/// - hl_name: Whether to include highlight group name instead of id, true if omitted /// @param[out] err Error details, if any /// @return 0-indexed (row, col) tuple or empty list () if extmark id was /// absent @@ -218,80 +200,92 @@ ArrayOf(Integer) nvim_buf_get_extmark_by_id(Buffer buffer, Integer ns_id, return rv; } - if (!ns_initialized((uint32_t)ns_id)) { - api_set_error(err, kErrorTypeValidation, "Invalid ns_id"); + VALIDATE_INT(ns_initialized((uint32_t)ns_id), "ns_id", ns_id, { return rv; - } + }); bool details = false; + bool hl_name = true; for (size_t i = 0; i < opts.size; i++) { String k = opts.items[i].key; Object *v = &opts.items[i].value; if (strequal("details", k.data)) { - if (v->type == kObjectTypeBoolean) { - details = v->data.boolean; - } else if (v->type == kObjectTypeInteger) { - details = v->data.integer; - } else { - api_set_error(err, kErrorTypeValidation, "details is not an boolean"); + details = api_object_to_bool(*v, "details", false, err); + if (ERROR_SET(err)) { + return rv; + } + } else if (strequal("hl_name", k.data)) { + hl_name = api_object_to_bool(*v, "hl_name", false, err); + if (ERROR_SET(err)) { return rv; } } else { - api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data); - return rv; + VALIDATE_S(false, "'opts' key", k.data, { + return rv; + }); } } - ExtmarkInfo extmark = extmark_from_id(buf, (uint32_t)ns_id, (uint32_t)id); - if (extmark.row < 0) { + MTPair extmark = extmark_from_id(buf, (uint32_t)ns_id, (uint32_t)id); + if (extmark.start.pos.row < 0) { return rv; } - return extmark_to_array(&extmark, false, details); + return extmark_to_array(extmark, false, details, hl_name); } -/// Gets |extmarks| in "traversal order" from a |charwise| region defined by -/// buffer positions (inclusive, 0-indexed |api-indexing|). +/// Gets |extmarks| (including |signs|) in "traversal order" from a |charwise| +/// region defined by buffer positions (inclusive, 0-indexed |api-indexing|). /// /// Region can be given as (row,col) tuples, or valid extmark ids (whose /// positions define the bounds). 0 and -1 are understood as (0,0) and (-1,-1) /// respectively, thus the following are equivalent: -/// <pre>lua -/// vim.api.nvim_buf_get_extmarks(0, my_ns, 0, -1, {}) -/// vim.api.nvim_buf_get_extmarks(0, my_ns, {0,0}, {-1,-1}, {}) -/// </pre> +/// +/// ```lua +/// vim.api.nvim_buf_get_extmarks(0, my_ns, 0, -1, {}) +/// vim.api.nvim_buf_get_extmarks(0, my_ns, {0,0}, {-1,-1}, {}) +/// ``` /// /// If `end` is less than `start`, traversal works backwards. (Useful /// with `limit`, to get the first marks prior to a given position.) /// +/// Note: when using extmark ranges (marks with a end_row/end_col position) +/// the `overlap` option might be useful. Otherwise only the start position +/// of an extmark will be considered. +/// /// Example: -/// <pre>lua -/// local a = vim.api -/// 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, {}) -/// -- Create new extmark at line 3, column 1. -/// local m2 = a.nvim_buf_set_extmark(0, ns, 2, 0, {}) -/// -- 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. -/// local all = a.nvim_buf_get_extmarks(0, ns, 0, -1, {}) -/// print(vim.inspect(ms)) -/// </pre> +/// +/// ```lua +/// local api = vim.api +/// local pos = api.nvim_win_get_cursor(0) +/// local ns = api.nvim_create_namespace('my-plugin') +/// -- Create new extmark at line 1, column 1. +/// local m1 = api.nvim_buf_set_extmark(0, ns, 0, 0, {}) +/// -- Create new extmark at line 3, column 1. +/// local m2 = api.nvim_buf_set_extmark(0, ns, 2, 0, {}) +/// -- Get extmarks only from line 3. +/// local ms = api.nvim_buf_get_extmarks(0, ns, {2,0}, {2,0}, {}) +/// -- Get all marks in this buffer + namespace. +/// local all = api.nvim_buf_get_extmarks(0, ns, 0, -1, {}) +/// vim.print(ms) +/// ``` /// /// @param buffer Buffer handle, or 0 for current buffer -/// @param ns_id Namespace id from |nvim_create_namespace()| +/// @param ns_id Namespace id from |nvim_create_namespace()| or -1 for all namespaces /// @param start Start of range: a 0-indexed (row, col) or valid extmark id /// (whose position defines the bound). |api-indexing| /// @param end End of range (inclusive): a 0-indexed (row, col) or valid /// extmark id (whose position defines the bound). |api-indexing| /// @param opts Optional parameters. Keys: /// - limit: Maximum number of marks to return -/// - details Whether to include the details dict +/// - details: Whether to include the details dict +/// - hl_name: Whether to include highlight group name instead of id, true if omitted +/// - overlap: Also include marks which overlap the range, even if +/// their start position is less than `start` +/// - type: Filter marks by type: "highlight", "sign", "virt_text" and "virt_lines" /// @param[out] err Error details, if any /// @return List of [extmark_id, row, col] tuples in "traversal order". -Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object end, Dictionary opts, - Error *err) +Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object end, + Dict(get_extmarks) *opts, Error *err) FUNC_API_SINCE(7) { Array rv = ARRAY_DICT_INIT; @@ -301,38 +295,32 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e return rv; } - if (!ns_initialized((uint32_t)ns_id)) { - api_set_error(err, kErrorTypeValidation, "Invalid ns_id"); + VALIDATE_INT(ns_id == -1 || ns_initialized((uint32_t)ns_id), "ns_id", ns_id, { return rv; - } - - Integer limit = -1; - bool details = false; - - for (size_t i = 0; i < opts.size; i++) { - String k = opts.items[i].key; - Object *v = &opts.items[i].value; - if (strequal("limit", k.data)) { - if (v->type != kObjectTypeInteger) { - api_set_error(err, kErrorTypeValidation, "limit is not an integer"); - return rv; - } - limit = v->data.integer; - } else if (strequal("details", k.data)) { - if (v->type == kObjectTypeBoolean) { - details = v->data.boolean; - } else if (v->type == kObjectTypeInteger) { - details = v->data.integer; - } else { - api_set_error(err, kErrorTypeValidation, "details is not an boolean"); - return rv; - } + }); + + bool details = opts->details; + bool hl_name = GET_BOOL_OR_TRUE(opts, get_extmarks, hl_name); + + ExtmarkType type = kExtmarkNone; + if (HAS_KEY(opts, get_extmarks, type)) { + if (strequal(opts->type.data, "sign")) { + type = kExtmarkSign; + } else if (strequal(opts->type.data, "virt_text")) { + type = kExtmarkVirtText; + } else if (strequal(opts->type.data, "virt_lines")) { + type = kExtmarkVirtLines; + } else if (strequal(opts->type.data, "highlight")) { + type = kExtmarkHighlight; } else { - api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data); - return rv; + VALIDATE_EXP(false, "type", "sign, virt_text, virt_lines or highlight", opts->type.data, { + return rv; + }); } } + Integer limit = HAS_KEY(opts, get_extmarks, limit) ? opts->limit : -1; + if (limit == 0) { return rv; } else if (limit < 0) { @@ -357,11 +345,12 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e reverse = true; } - ExtmarkInfoArray marks = extmark_get(buf, (uint32_t)ns_id, l_row, l_col, - u_row, u_col, (int64_t)limit, reverse); + // note: ns_id=-1 allowed, represented as UINT32_MAX + ExtmarkInfoArray marks = extmark_get(buf, (uint32_t)ns_id, l_row, l_col, u_row, + u_col, (int64_t)limit, reverse, type, opts->overlap); 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, details, hl_name))); } kv_destroy(marks); @@ -379,6 +368,11 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e /// 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. /// +/// If present, the position defined by `end_col` and `end_row` should be after +/// the start position in order for the extmark to cover a range. +/// An earlier end position is not an error, but then it behaves like an empty +/// range (no highlighting). +/// /// @param buffer Buffer handle, or 0 for current buffer /// @param ns_id Namespace id from |nvim_create_namespace()| /// @param line Line where to place the mark, 0-based. |api-indexing| @@ -402,24 +396,28 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e /// either as a string or as an integer, the latter which /// can be obtained using |nvim_get_hl_id_by_name()|. /// - virt_text_pos : position of virtual text. Possible values: -/// - "eol": right after eol character (default) +/// - "eol": right after eol character (default). /// - "overlay": display over the specified column, without /// shifting the underlying text. /// - "right_align": display right aligned in the window. +/// - "inline": display at the specified column, and +/// shift the buffer text to the right as needed. /// - virt_text_win_col : position the virtual text at a fixed /// window column (starting from the first -/// text column) +/// text column of the screen line) instead +/// of "virt_text_pos". /// - virt_text_hide : hide the virtual text when the background -/// text is selected or hidden due to -/// horizontal scroll 'nowrap' +/// text is selected or hidden because of +/// scrolling with 'nowrap' or 'smoothscroll'. +/// Currently only affects "overlay" virt_text. /// - hl_mode : control how highlights are combined with the /// highlights of the text. Currently only affects /// virt_text highlights, but might affect `hl_group` /// in later versions. -/// - "replace": only show the virt_text color. This is the -/// default -/// - "combine": combine with background text color +/// - "replace": only show the virt_text color. This is the default. +/// - "combine": combine with background text color. /// - "blend": blend with background text color. +/// Not supported for "inline" virt_text. /// /// - virt_lines : virtual lines to add next to this mark /// This should be an array over lines, where each line in @@ -443,11 +441,17 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e /// buffer. /// - right_gravity : boolean that indicates the direction /// the extmark will be shifted in when new text is inserted -/// (true for right, false for left). defaults to true. +/// (true for right, false for left). Defaults to true. /// - end_right_gravity : boolean that indicates the direction /// the extmark end position (if it exists) will be shifted /// in when new text is inserted (true for right, false /// for left). Defaults to false. +/// - undo_restore : Restore the exact position of the mark +/// if text around the mark was deleted and then restored by undo. +/// Defaults to true. +/// - invalidate : boolean that indicates whether to hide the +/// extmark if the entirety of its range is deleted. If +/// "undo_restore" is false, the extmark is deleted instead. /// - priority: a priority value for the highlight group or sign /// attribute. For example treesitter highlighting uses a /// value of 100. @@ -493,286 +497,255 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer Dict(set_extmark) *opts, Error *err) FUNC_API_SINCE(7) { - Decoration decor = DECORATION_INIT; - bool has_decor = false; + DecorHighlightInline hl = DECOR_HIGHLIGHT_INLINE_INIT; + // TODO(bfredl): in principle signs with max one (1) hl group and max 4 bytes of text. + // should be a candidate for inlining as well. + DecorSignHighlight sign = DECOR_SIGN_HIGHLIGHT_INIT; + DecorVirtText virt_text = DECOR_VIRT_TEXT_INIT; + DecorVirtText virt_lines = DECOR_VIRT_LINES_INIT; + bool has_hl = false; buf_T *buf = find_buffer_by_handle(buffer, err); if (!buf) { goto error; } - if (!ns_initialized((uint32_t)ns_id)) { - api_set_error(err, kErrorTypeValidation, "Invalid ns_id"); + VALIDATE_INT(ns_initialized((uint32_t)ns_id), "ns_id", ns_id, { goto error; - } + }); uint32_t id = 0; - if (opts->id.type == kObjectTypeInteger && opts->id.data.integer > 0) { - 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; + if (HAS_KEY(opts, set_extmark, id)) { + VALIDATE_EXP((opts->id > 0), "id", "positive Integer", NULL, { + goto error; + }); + + id = (uint32_t)opts->id; } int line2 = -1; + bool did_end_line = false; // For backward compatibility we support "end_line" as an alias for "end_row" - if (HAS_KEY(opts->end_line)) { - if (HAS_KEY(opts->end_row)) { - api_set_error(err, kErrorTypeValidation, "cannot use both end_row and end_line"); + if (HAS_KEY(opts, set_extmark, end_line)) { + VALIDATE(!HAS_KEY(opts, set_extmark, end_row), + "%s", "cannot use both 'end_row' and 'end_line'", { goto error; - } - 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; \ + opts->end_row = opts->end_line; + did_end_line = true; } - bool strict = true; - OPTION_TO_BOOL(strict, strict, true); + bool strict = GET_BOOL_OR_TRUE(opts, set_extmark, strict); - if (opts->end_row.type == kObjectTypeInteger) { - Integer val = opts->end_row.data.integer; - if (val < 0 || (val > buf->b_ml.ml_line_count && strict)) { - api_set_error(err, kErrorTypeValidation, "end_row value outside range"); + if (HAS_KEY(opts, set_extmark, end_row) || did_end_line) { + Integer val = opts->end_row; + VALIDATE_RANGE((val >= 0 && !(val > buf->b_ml.ml_line_count && strict)), "end_row", { goto error; - } else { - line2 = (int)val; - } - } else if (HAS_KEY(opts->end_row)) { - api_set_error(err, kErrorTypeValidation, "end_row is not an integer"); - goto error; + }); + line2 = (int)val; } colnr_T col2 = -1; - if (opts->end_col.type == kObjectTypeInteger) { - Integer val = opts->end_col.data.integer; - if (val < 0 || val > MAXCOL) { - api_set_error(err, kErrorTypeValidation, "end_col value outside range"); + if (HAS_KEY(opts, set_extmark, end_col)) { + Integer val = opts->end_col; + VALIDATE_RANGE((val >= 0 && val <= MAXCOL), "end_col", { goto error; - } else { - col2 = (int)val; - } - } else if (HAS_KEY(opts->end_col)) { - api_set_error(err, kErrorTypeValidation, "end_col is not an integer"); - goto error; + }); + col2 = (int)val; } // uncrustify:off + // TODO(bfredl): keyset type alias for hl_group? (nil|int|string) 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 }, + { "hl_group" , &opts->hl_group , &hl.hl_id }, + { "sign_hl_group" , &opts->sign_hl_group , &sign.hl_id }, + { "number_hl_group" , &opts->number_hl_group , &sign.number_hl_id }, + { "line_hl_group" , &opts->line_hl_group , &sign.line_hl_id }, + { "cursorline_hl_group", &opts->cursorline_hl_group, &sign.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)) { + if (hls[j].opt->type != kObjectTypeNil) { + if (j > 0) { + sign.flags |= kSHIsSign; + } else { + has_hl = true; + } *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); + if (HAS_KEY(opts, set_extmark, conceal)) { + hl.flags |= kSHConceal; + has_hl = true; + String c = opts->conceal; + if (c.size > 0) { + int ch; + hl.conceal_char = utfc_ptr2schar_len(c.data, (int)c.size, &ch); + if (!hl.conceal_char || !vim_isprintc(ch)) { + api_set_error(err, kErrorTypeValidation, "conceal char has to be printable"); + goto error; + } } - 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 (HAS_KEY(opts, set_extmark, virt_text)) { + virt_text.data.virt_text = parse_virt_text(opts->virt_text, err, &virt_text.width); if (ERROR_SET(err)) { goto error; } - } else if (HAS_KEY(opts->virt_text)) { - api_set_error(err, kErrorTypeValidation, "virt_text is not an Array"); - goto error; } - if (opts->virt_text_pos.type == kObjectTypeString) { - String str = opts->virt_text_pos.data.string; + if (HAS_KEY(opts, set_extmark, virt_text_pos)) { + String str = opts->virt_text_pos; if (strequal("eol", str.data)) { - decor.virt_text_pos = kVTEndOfLine; + virt_text.pos = kVPosEndOfLine; } else if (strequal("overlay", str.data)) { - decor.virt_text_pos = kVTOverlay; + virt_text.pos = kVPosOverlay; } else if (strequal("right_align", str.data)) { - decor.virt_text_pos = kVTRightAlign; + virt_text.pos = kVPosRightAlign; + } else if (strequal("inline", str.data)) { + virt_text.pos = kVPosInline; } else { - api_set_error(err, kErrorTypeValidation, "virt_text_pos: invalid value"); - goto error; + VALIDATE_S(false, "virt_text_pos", str.data, { + goto error; + }); } - } else if (HAS_KEY(opts->virt_text_pos)) { - api_set_error(err, kErrorTypeValidation, "virt_text_pos is not a String"); - goto error; } - if (opts->virt_text_win_col.type == kObjectTypeInteger) { - decor.col = (int)opts->virt_text_win_col.data.integer; - decor.virt_text_pos = kVTWinCol; - } else if (HAS_KEY(opts->virt_text_win_col)) { - api_set_error(err, kErrorTypeValidation, - "virt_text_win_col is not a Number of the correct size"); - goto error; + if (HAS_KEY(opts, set_extmark, virt_text_win_col)) { + virt_text.col = (int)opts->virt_text_win_col; + virt_text.pos = kVPosWinCol; } - OPTION_TO_BOOL(decor.virt_text_hide, virt_text_hide, false); - OPTION_TO_BOOL(decor.hl_eol, hl_eol, false); + hl.flags |= opts->hl_eol ? kSHHlEol : 0; + virt_text.flags |= opts->virt_text_hide ? kVTHide : 0; - if (opts->hl_mode.type == kObjectTypeString) { - String str = opts->hl_mode.data.string; + if (HAS_KEY(opts, set_extmark, hl_mode)) { + String str = opts->hl_mode; if (strequal("replace", str.data)) { - decor.hl_mode = kHlModeReplace; + virt_text.hl_mode = kHlModeReplace; } else if (strequal("combine", str.data)) { - decor.hl_mode = kHlModeCombine; + virt_text.hl_mode = kHlModeCombine; } else if (strequal("blend", str.data)) { - decor.hl_mode = kHlModeBlend; + if (virt_text.pos == kVPosInline) { + VALIDATE(false, "%s", "cannot use 'blend' hl_mode with inline virtual text", { + goto error; + }); + } + virt_text.hl_mode = kHlModeBlend; } else { - api_set_error(err, kErrorTypeValidation, - "virt_text_pos: invalid value"); - goto error; + VALIDATE_S(false, "hl_mode", str.data, { + goto error; + }); } - } else if (HAS_KEY(opts->hl_mode)) { - api_set_error(err, kErrorTypeValidation, "hl_mode is not a String"); - goto error; } - bool virt_lines_leftcol = false; - OPTION_TO_BOOL(virt_lines_leftcol, virt_lines_leftcol, false); + bool virt_lines_leftcol = opts->virt_lines_leftcol; - if (opts->virt_lines.type == kObjectTypeArray) { - Array a = opts->virt_lines.data.array; + if (HAS_KEY(opts, set_extmark, virt_lines)) { + Array a = opts->virt_lines; for (size_t j = 0; j < a.size; j++) { - if (a.items[j].type != kObjectTypeArray) { - api_set_error(err, kErrorTypeValidation, "virt_text_line item is not an Array"); + VALIDATE_T("virt_text_line", kObjectTypeArray, a.items[j].type, { goto error; - } + }); int dummig; VirtText jtem = parse_virt_text(a.items[j].data.array, err, &dummig); - kv_push(decor.virt_lines, ((struct virt_line){ jtem, virt_lines_leftcol })); + kv_push(virt_lines.data.virt_lines, ((struct virt_line){ jtem, virt_lines_leftcol })); 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) { - Integer val = opts->priority.data.integer; + virt_lines.flags |= opts->virt_lines_above ? kVTLinesAbove : 0; - if (val < 0 || val > UINT16_MAX) { - api_set_error(err, kErrorTypeValidation, "priority is not a valid value"); + if (HAS_KEY(opts, set_extmark, priority)) { + VALIDATE_RANGE((opts->priority >= 0 && opts->priority <= UINT16_MAX), "priority", { goto error; - } - decor.priority = (DecorPriority)val; - } else if (HAS_KEY(opts->priority)) { - api_set_error(err, kErrorTypeValidation, "priority is not a Number of the correct size"); - goto error; + }); + hl.priority = (DecorPriority)opts->priority; + sign.priority = (DecorPriority)opts->priority; + virt_text.priority = (DecorPriority)opts->priority; + virt_lines.priority = (DecorPriority)opts->priority; } - if (opts->sign_text.type == kObjectTypeString) { - if (!init_sign_text(&decor.sign_text, - opts->sign_text.data.string.data)) { - api_set_error(err, kErrorTypeValidation, "sign_text is not a valid value"); + if (HAS_KEY(opts, set_extmark, sign_text)) { + sign.text.ptr = NULL; + VALIDATE_S(init_sign_text(NULL, &sign.text.ptr, opts->sign_text.data), + "sign_text", "", { 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; + }); + sign.flags |= kSHIsSign; } - bool right_gravity = true; - OPTION_TO_BOOL(right_gravity, right_gravity, true); + bool right_gravity = GET_BOOL_OR_TRUE(opts, set_extmark, right_gravity); // Only error out if they try to set end_right_gravity without // setting end_col or end_row - if (line2 == -1 && col2 == -1 && HAS_KEY(opts->end_right_gravity)) { - api_set_error(err, kErrorTypeValidation, - "cannot set end_right_gravity without setting end_row or end_col"); + VALIDATE(!(line2 == -1 && col2 == -1 && HAS_KEY(opts, set_extmark, end_right_gravity)), + "%s", "cannot set end_right_gravity without end_row or end_col", { goto error; - } - - bool end_right_gravity = false; - OPTION_TO_BOOL(end_right_gravity, end_right_gravity, false); + }); size_t len = 0; - bool ephemeral = false; - OPTION_TO_BOOL(ephemeral, ephemeral, false); - - if (opts->spell.type == kObjectTypeNil) { - decor.spell = kNone; - } else { - bool spell = false; - OPTION_TO_BOOL(spell, spell, false); - decor.spell = spell ? kTrue : kFalse; - has_decor = true; + if (HAS_KEY(opts, set_extmark, spell)) { + hl.flags |= (opts->spell) ? kSHSpellOn : kSHSpellOff; + has_hl = true; } - OPTION_TO_BOOL(decor.ui_watched, ui_watched, false); - if (decor.ui_watched) { - has_decor = true; + if (opts->ui_watched) { + hl.flags |= kSHUIWatched; + if (virt_text.pos == kVPosOverlay) { + // TODO(bfredl): in a revised interface this should be the default. + hl.flags |= kSHUIWatchedOverlay; + } + has_hl = true; } - if (line < 0) { - api_set_error(err, kErrorTypeValidation, "line value outside range"); + VALIDATE_RANGE((line >= 0), "line", { goto error; - } else if (line > buf->b_ml.ml_line_count) { - if (strict) { - api_set_error(err, kErrorTypeValidation, "line value outside range"); + }); + + if (line > buf->b_ml.ml_line_count) { + VALIDATE_RANGE(!strict, "line", { goto error; - } else { - line = buf->b_ml.ml_line_count; - } + }); + 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 = opts->ephemeral ? MAXCOL : strlen(ml_get_buf(buf, (linenr_T)line + 1)); } if (col == -1) { col = (Integer)len; } else if (col > (Integer)len) { - if (strict) { - api_set_error(err, kErrorTypeValidation, "col value outside range"); + VALIDATE_RANGE(!strict, "col", { goto error; - } else { - col = (Integer)len; - } + }); + col = (Integer)len; } else if (col < -1) { - api_set_error(err, kErrorTypeValidation, "col value outside range"); - goto error; + VALIDATE_RANGE(false, "col", { + goto error; + }); } if (col2 >= 0) { if (line2 >= 0 && line2 < buf->b_ml.ml_line_count) { - len = ephemeral ? MAXCOL : strlen(ml_get_buf(buf, (linenr_T)line2 + 1, false)); + len = opts->ephemeral ? MAXCOL : strlen(ml_get_buf(buf, (linenr_T)line2 + 1)); } else if (line2 == buf->b_ml.ml_line_count) { // We are trying to add an extmark past final newline len = 0; @@ -781,36 +754,96 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer line2 = (int)line; } if (col2 > (Integer)len) { - if (strict) { - api_set_error(err, kErrorTypeValidation, "end_col value outside range"); + VALIDATE_RANGE(!strict, "end_col", { goto error; - } else { - col2 = (int)len; - } + }); + col2 = (int)len; } } else if (line2 >= 0) { col2 = 0; } - // TODO(bfredl): synergize these two branches even more - if (ephemeral && decor_state.buf == buf) { - decor_add_ephemeral((int)line, (int)col, line2, col2, &decor, (uint64_t)ns_id, id); + if (opts->ephemeral && decor_state.win && decor_state.win->w_buffer == buf) { + int r = (int)line; + int c = (int)col; + if (line2 == -1) { + line2 = r; + col2 = c; + } + + if (kv_size(virt_text.data.virt_text)) { + decor_range_add_virt(&decor_state, r, c, line2, col2, decor_put_vt(virt_text, NULL), true); + } + if (kv_size(virt_lines.data.virt_lines)) { + decor_range_add_virt(&decor_state, r, c, line2, col2, decor_put_vt(virt_lines, NULL), true); + } + if (has_hl) { + DecorSignHighlight sh = decor_sh_from_inline(hl); + decor_range_add_sh(&decor_state, r, c, line2, col2, &sh, true, (uint32_t)ns_id, id); + } } else { - if (ephemeral) { + if (opts->ephemeral) { api_set_error(err, kErrorTypeException, "not yet implemented"); goto error; } + uint16_t decor_flags = 0; + + DecorVirtText *decor_alloc = NULL; + if (kv_size(virt_text.data.virt_text)) { + decor_alloc = decor_put_vt(virt_text, decor_alloc); + if (virt_text.pos == kVPosInline) { + decor_flags |= MT_FLAG_DECOR_VIRT_TEXT_INLINE; + } + } + if (kv_size(virt_lines.data.virt_lines)) { + decor_alloc = decor_put_vt(virt_lines, decor_alloc); + decor_flags |= MT_FLAG_DECOR_VIRT_LINES; + } + + uint32_t decor_indexed = DECOR_ID_INVALID; + if (sign.flags & kSHIsSign) { + decor_indexed = decor_put_sh(sign); + if (sign.text.ptr != NULL) { + decor_flags |= MT_FLAG_DECOR_SIGNTEXT; + } + if (sign.number_hl_id || sign.line_hl_id || sign.cursorline_hl_id) { + decor_flags |= MT_FLAG_DECOR_SIGNHL; + } + } + + DecorInline decor = DECOR_INLINE_INIT; + if (decor_alloc || decor_indexed != DECOR_ID_INVALID || schar_high(hl.conceal_char)) { + if (has_hl) { + DecorSignHighlight sh = decor_sh_from_inline(hl); + sh.next = decor_indexed; + decor_indexed = decor_put_sh(sh); + } + decor.ext = true; + decor.data.ext = (DecorExt){ .sh_idx = decor_indexed, .vt = decor_alloc }; + } else { + decor.data.hl = hl; + } + + if (has_hl) { + decor_flags |= MT_FLAG_DECOR_HL; + } + 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); + decor, decor_flags, right_gravity, opts->end_right_gravity, + !GET_BOOL_OR_TRUE(opts, set_extmark, undo_restore), + opts->invalidate, err); + if (ERROR_SET(err)) { + decor_free(decor); + return 0; + } } return (Integer)id; error: - clear_virttext(&decor.virt_text); - xfree(decor.sign_text); + clear_virttext(&virt_text.data.virt_text); + clear_virtlines(&virt_lines.data.virt_lines); return 0; } @@ -829,12 +862,11 @@ Boolean nvim_buf_del_extmark(Buffer buffer, Integer ns_id, Integer id, Error *er if (!buf) { return false; } - if (!ns_initialized((uint32_t)ns_id)) { - api_set_error(err, kErrorTypeValidation, "Invalid ns_id"); + VALIDATE_INT(ns_initialized((uint32_t)ns_id), "ns_id", ns_id, { return false; - } + }); - return extmark_del(buf, (uint32_t)ns_id, (uint32_t)id); + return extmark_del_id(buf, (uint32_t)ns_id, (uint32_t)id); } uint32_t src2ns(Integer *src_id) @@ -887,14 +919,13 @@ Integer nvim_buf_add_highlight(Buffer buffer, Integer ns_id, String hl_group, In return 0; } - if (line < 0 || line >= MAXLNUM) { - api_set_error(err, kErrorTypeValidation, "Line number outside range"); + VALIDATE_RANGE((line >= 0 && line < MAXLNUM), "line number", { return 0; - } - if (col_start < 0 || col_start > MAXCOL) { - api_set_error(err, kErrorTypeValidation, "Column value outside range"); + }); + VALIDATE_RANGE((col_start >= 0 && col_start <= MAXCOL), "column", { return 0; - } + }); + if (col_end < 0 || col_end > MAXCOL) { col_end = MAXCOL; } @@ -919,13 +950,11 @@ 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; + DecorInline decor = DECOR_INLINE_INIT; + decor.data.hl.hl_id = hl_id; - extmark_set(buf, ns, NULL, - (int)line, (colnr_T)col_start, - end_line, (colnr_T)col_end, - &decor, true, false, kExtmarkNoUndo); + extmark_set(buf, ns, NULL, (int)line, (colnr_T)col_start, end_line, (colnr_T)col_end, + decor, MT_FLAG_DECOR_HL, true, false, false, false, NULL); return ns_id; } @@ -950,10 +979,10 @@ void nvim_buf_clear_namespace(Buffer buffer, Integer ns_id, Integer line_start, return; } - if (line_start < 0 || line_start >= MAXLNUM) { - api_set_error(err, kErrorTypeValidation, "Line number outside range"); + VALIDATE_RANGE((line_start >= 0 && line_start < MAXLNUM), "line number", { return; - } + }); + if (line_end < 0 || line_end > MAXLNUM) { line_end = MAXLNUM; } @@ -964,14 +993,14 @@ void nvim_buf_clear_namespace(Buffer buffer, Integer ns_id, Integer line_start, /// Set or change decoration provider for a |namespace| /// -/// This is a very general purpose interface for having lua callbacks +/// This is a very general purpose interface for having Lua callbacks /// being triggered during the redraw code. /// /// The expected usage is to set |extmarks| for the currently /// redrawn buffer. |nvim_buf_set_extmark()| can be called to add marks /// on a per-window or per-lines basis. Use the `ephemeral` key to only /// use the mark for the current screen redraw (the callback will be called -/// again for the next redraw ). +/// again for the next redraw). /// /// Note: this function should not be called often. Rather, the callbacks /// themselves can be used to throttle unneeded callbacks. the `on_start` @@ -983,11 +1012,13 @@ void nvim_buf_clear_namespace(Buffer buffer, Integer ns_id, Integer line_start, /// for the extmarks set/modified inside the callback anyway. /// /// Note: doing anything other than setting extmarks is considered experimental. -/// Doing things like changing options are not expliticly forbidden, but is +/// Doing things like changing options are not explicitly forbidden, but is /// likely to have unexpected consequences (such as 100% CPU consumption). /// doing `vim.rpcnotify` should be OK, but `vim.rpcrequest` is quite dubious /// for the moment. /// +/// Note: It is not allowed to remove or update extmarks in 'on_line' callbacks. +/// /// @param ns_id Namespace id from |nvim_create_namespace()| /// @param opts Table of callbacks: /// - on_start: called first on each screen redraw @@ -996,7 +1027,8 @@ void nvim_buf_clear_namespace(Buffer buffer, Integer ns_id, Integer line_start, /// window callbacks) /// ["buf", bufnr, tick] /// - on_win: called when starting to redraw a -/// specific window. +/// specific window. botline_guess is an approximation +/// that does not exceed the last line number. /// ["win", winid, bufnr, topline, botline_guess] /// - on_line: called for each buffer line being redrawn. /// (The interaction with fold lines is subject to change) @@ -1015,7 +1047,7 @@ void nvim_set_decoration_provider(Integer ns_id, Dict(set_decoration_provider) * struct { const char *name; - Object *source; + LuaRef *source; LuaRef *dest; } cbs[] = { { "on_start", &opts->on_start, &p->redraw_start }, @@ -1029,26 +1061,18 @@ void nvim_set_decoration_provider(Integer ns_id, Dict(set_decoration_provider) * }; for (size_t i = 0; cbs[i].source && cbs[i].dest && cbs[i].name; i++) { - Object *v = cbs[i].source; - if (v->type == kObjectTypeNil) { + LuaRef *v = cbs[i].source; + if (*v <= 0) { continue; } - if (v->type != kObjectTypeLuaRef) { - api_set_error(err, kErrorTypeValidation, - "%s is not a function", cbs[i].name); - goto error; - } - *(cbs[i].dest) = v->data.luaref; - v->data.luaref = LUA_NOREF; + *(cbs[i].dest) = *v; + *v = LUA_NOREF; } p->active = true; p->hl_valid++; p->hl_cached = false; - return; -error: - decor_provider_clear(p); } /// Gets the line and column of an |extmark|. @@ -1075,75 +1099,40 @@ static bool extmark_get_index_from_obj(buf_T *buf, Integer ns_id, Object obj, in *col = MAXCOL; return true; } else if (id < 0) { - api_set_error(err, kErrorTypeValidation, "Mark id must be positive"); - return false; + VALIDATE_INT(false, "mark id", id, { + 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"); + MTPair extmark = extmark_from_id(buf, (uint32_t)ns_id, (uint32_t)id); + + VALIDATE_INT((extmark.start.pos.row >= 0), "mark id (not found)", id, { return false; - } + }); + *row = extmark.start.pos.row; + *col = extmark.start.pos.col; + return true; // 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"); + VALIDATE_EXP((pos.size == 2 + && pos.items[0].type == kObjectTypeInteger + && pos.items[1].type == kObjectTypeInteger), + "mark position", "2 Integer items", NULL, { 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); + *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, " "); + VALIDATE_EXP(false, "mark position", "mark id Integer or 2-item Array", NULL, { + return false; + }); } - - return OK; } VirtText parse_virt_text(Array chunks, Error *err, int *width) @@ -1151,17 +1140,14 @@ 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"); + VALIDATE_T("chunk", kObjectTypeArray, chunks.items[i].type, { 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"); + VALIDATE((chunk.size > 0 && chunk.size <= 2 && chunk.items[0].type == kObjectTypeString), + "%s", "Invalid chunk: expected Array with 1 or 2 Strings", { goto free_exit; - } + }); String str = chunk.items[0].data.string; @@ -1176,8 +1162,7 @@ VirtText parse_virt_text(Array chunks, Error *err, int *width) goto free_exit; } if (j < arr.size - 1) { - kv_push(virt_text, ((VirtTextChunk){ .text = NULL, - .hl_id = hl_id })); + kv_push(virt_text, ((VirtTextChunk){ .text = NULL, .hl_id = hl_id })); } } } else { @@ -1194,10 +1179,23 @@ VirtText parse_virt_text(Array chunks, Error *err, int *width) kv_push(virt_text, ((VirtTextChunk){ .text = text, .hl_id = hl_id })); } - *width = w; + if (width != NULL) { + *width = w; + } return virt_text; free_exit: clear_virttext(&virt_text); return virt_text; } + +String nvim__buf_debug_extmarks(Buffer buffer, Boolean keys, Boolean dot, Error *err) + FUNC_API_SINCE(7) +{ + buf_T *buf = find_buffer_by_handle(buffer, err); + if (!buf) { + return NULL_STRING; + } + + return mt_inspect(buf->b_marktree, keys, dot); +} diff --git a/src/nvim/api/extmark.h b/src/nvim/api/extmark.h index 0a627a889c..124feaabfb 100644 --- a/src/nvim/api/extmark.h +++ b/src/nvim/api/extmark.h @@ -1,17 +1,17 @@ -#ifndef NVIM_API_EXTMARK_H -#define NVIM_API_EXTMARK_H +#pragma once -#include "nvim/api/private/defs.h" -#include "nvim/decoration.h" -#include "nvim/macros.h" -#include "nvim/map.h" +#include <stdint.h> // IWYU pragma: keep + +#include "nvim/api/keysets_defs.h" // IWYU pragma: keep +#include "nvim/api/private/defs.h" // IWYU pragma: keep +#include "nvim/decoration_defs.h" // IWYU pragma: keep +#include "nvim/macros_defs.h" #include "nvim/map_defs.h" -#include "nvim/types.h" +#include "nvim/types_defs.h" -EXTERN Map(String, handle_T) namespace_ids INIT(= MAP_INIT); -EXTERN handle_T next_namespace_id INIT(= 1); +EXTERN Map(String, int) namespace_ids INIT( = MAP_INIT); +EXTERN handle_T next_namespace_id INIT( = 1); #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/extmark.h.generated.h" #endif -#endif // NVIM_API_EXTMARK_H diff --git a/src/nvim/api/keysets.lua b/src/nvim/api/keysets.lua deleted file mode 100644 index 30dcef6127..0000000000 --- a/src/nvim/api/keysets.lua +++ /dev/null @@ -1,229 +0,0 @@ -return { - { 'context', { - "types"; - }}; - { 'set_decoration_provider', { - "on_start"; - "on_buf"; - "on_win"; - "on_line"; - "on_end"; - "_on_hl_def"; - "_on_spell_nav"; - }}; - { 'set_extmark', { - "id"; - "end_line"; - "end_row"; - "end_col"; - "hl_group"; - "virt_text"; - "virt_text_pos"; - "virt_text_win_col"; - "virt_text_hide"; - "hl_eol"; - "hl_mode"; - "ephemeral"; - "priority"; - "right_gravity"; - "end_right_gravity"; - "virt_lines"; - "virt_lines_above"; - "virt_lines_leftcol"; - "strict"; - "sign_text"; - "sign_hl_group"; - "number_hl_group"; - "line_hl_group"; - "cursorline_hl_group"; - "conceal"; - "spell"; - "ui_watched"; - }}; - { 'keymap', { - "noremap"; - "nowait"; - "silent"; - "script"; - "expr"; - "unique"; - "callback"; - "desc"; - "replace_keycodes"; - }}; - { 'get_commands', { - "builtin"; - }}; - { 'user_command', { - "addr"; - "bang"; - "bar"; - "complete"; - "count"; - "desc"; - "force"; - "keepscript"; - "nargs"; - "preview"; - "range"; - "register"; - }}; - { 'float_config', { - "row"; - "col"; - "width"; - "height"; - "anchor"; - "relative"; - "win"; - "bufpos"; - "external"; - "focusable"; - "zindex"; - "border"; - "title"; - "title_pos"; - "style"; - "noautocmd"; - }}; - { '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"; - "altfont"; - "nocombine"; - "default"; - "cterm"; - "foreground"; "fg"; - "background"; "bg"; - "ctermfg"; - "ctermbg"; - "special"; "sp"; - "link"; - "global_link"; - "fallback"; - "blend"; - "fg_indexed"; - "bg_indexed"; - }}; - { 'highlight_cterm', { - "bold"; - "standout"; - "strikethrough"; - "underline"; - "undercurl"; - "underdouble"; - "underdotted"; - "underdashed"; - "italic"; - "reverse"; - "altfont"; - "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"; - "horizontal"; - "keepalt"; - "keepjumps"; - "keepmarks"; - "keeppatterns"; - "lockmarks"; - "noswapfile"; - "tab"; - "verbose"; - "vertical"; - "split"; - }}; - { 'cmd_mods_filter', { - "pattern"; - "force"; - }}; - { 'cmd_opts', { - "output"; - }}; - { 'echo_opts', { - "verbose"; - }}; -} diff --git a/src/nvim/api/keysets_defs.h b/src/nvim/api/keysets_defs.h new file mode 100644 index 0000000000..e59eda5686 --- /dev/null +++ b/src/nvim/api/keysets_defs.h @@ -0,0 +1,315 @@ +#pragma once + +#include "nvim/api/private/defs.h" + +typedef struct { + OptionalKeys is_set__context_; + Array types; +} Dict(context); + +typedef struct { + OptionalKeys is_set__set_decoration_provider_; + LuaRef on_start; + LuaRef on_buf; + LuaRef on_win; + LuaRef on_line; + LuaRef on_end; + LuaRef _on_hl_def; + LuaRef _on_spell_nav; +} Dict(set_decoration_provider); + +typedef struct { + OptionalKeys is_set__set_extmark_; + Integer id; + Integer end_line; + Integer end_row; + Integer end_col; + Object hl_group; + Array virt_text; + String virt_text_pos; + Integer virt_text_win_col; + Boolean virt_text_hide; + Boolean hl_eol; + String hl_mode; + Boolean invalidate; + Boolean ephemeral; + Integer priority; + Boolean right_gravity; + Boolean end_right_gravity; + Array virt_lines; + Boolean virt_lines_above; + Boolean virt_lines_leftcol; + Boolean strict; + String sign_text; + Object sign_hl_group; + Object number_hl_group; + Object line_hl_group; + Object cursorline_hl_group; + String conceal; + Boolean spell; + Boolean ui_watched; + Boolean undo_restore; +} Dict(set_extmark); + +typedef struct { + OptionalKeys is_set__get_extmarks_; + Integer limit; + Boolean details; + Boolean hl_name; + Boolean overlap; + String type; +} Dict(get_extmarks); + +typedef struct { + OptionalKeys is_set__keymap_; + Boolean noremap; + Boolean nowait; + Boolean silent; + Boolean script; + Boolean expr; + Boolean unique; + LuaRef callback; + String desc; + Boolean replace_keycodes; +} Dict(keymap); + +typedef struct { + Boolean builtin; +} Dict(get_commands); + +typedef struct { + OptionalKeys is_set__user_command_; + Object addr; + Boolean bang; + Boolean bar; + Object complete; + Object count; + Object desc; + Boolean force; + Boolean keepscript; + Object nargs; + Object preview; + Object range; + Boolean register_; +} Dict(user_command); + +typedef struct { + OptionalKeys is_set__float_config_; + Float row; + Float col; + Integer width; + Integer height; + String anchor; + String relative; + Window win; + Array bufpos; + Boolean external; + Boolean focusable; + Integer zindex; + Object border; + Object title; + String title_pos; + Object footer; + String footer_pos; + String style; + Boolean noautocmd; + Boolean fixed; + Boolean hide; +} Dict(float_config); + +typedef struct { + Boolean is_lua; + Boolean do_source; +} Dict(runtime); + +typedef struct { + OptionalKeys is_set__eval_statusline_; + Window winid; + Integer maxwidth; + String fillchar; + Boolean highlights; + Boolean use_winbar; + Boolean use_tabline; + Integer use_statuscol_lnum; +} Dict(eval_statusline); + +typedef struct { + OptionalKeys is_set__option_; + String scope; + Window win; + Buffer buf; + String filetype; +} Dict(option); + +typedef struct { + OptionalKeys is_set__highlight_; + Boolean bold; + Boolean standout; + Boolean strikethrough; + Boolean underline; + Boolean undercurl; + Boolean underdouble; + Boolean underdotted; + Boolean underdashed; + Boolean italic; + Boolean reverse; + Boolean altfont; + Boolean nocombine; + Boolean default_; + Object cterm; + Object foreground; + Object fg; + Object background; + Object bg; + Object ctermfg; + Object ctermbg; + Object special; + Object sp; + Object link; + Object global_link; + Boolean fallback; + Integer blend; + Boolean fg_indexed; + Boolean bg_indexed; + Boolean force; +} Dict(highlight); + +typedef struct { + Boolean bold; + Boolean standout; + Boolean strikethrough; + Boolean underline; + Boolean undercurl; + Boolean underdouble; + Boolean underdotted; + Boolean underdashed; + Boolean italic; + Boolean reverse; + Boolean altfont; + Boolean nocombine; +} Dict(highlight_cterm); + +typedef struct { + OptionalKeys is_set__get_highlight_; + Integer id; + String name; + Boolean link; + Boolean create; +} Dict(get_highlight); + +typedef struct { + OptionalKeys is_set__get_ns_; + Window winid; +} Dict(get_ns); + +typedef struct { + OptionalKeys is_set__win_text_height_; + Integer start_row; + Integer end_row; + Integer start_vcol; + Integer end_vcol; +} Dict(win_text_height); + +typedef struct { + OptionalKeys is_set__clear_autocmds_; + Buffer buffer; + Object event; + Object group; + Object pattern; +} Dict(clear_autocmds); + +typedef struct { + OptionalKeys is_set__create_autocmd_; + Buffer buffer; + Object callback; + String command; + String desc; + Object group; + Boolean nested; + Boolean once; + Object pattern; +} Dict(create_autocmd); + +typedef struct { + OptionalKeys is_set__exec_autocmds_; + Buffer buffer; + Object group; + Boolean modeline; + Object pattern; + Object data; +} Dict(exec_autocmds); + +typedef struct { + OptionalKeys is_set__get_autocmds_; + Object event; + Object group; + Object pattern; + Object buffer; +} Dict(get_autocmds); + +typedef struct { + Object clear; +} Dict(create_augroup); + +typedef struct { + OptionalKeys is_set__cmd_; + String cmd; + Array range; + Integer count; + String reg; + Boolean bang; + Array args; + Dictionary magic; + Dictionary mods; + Object nargs; + Object addr; + Object nextcmd; +} Dict(cmd); + +typedef struct { + OptionalKeys is_set__cmd_magic_; + Boolean file; + Boolean bar; +} Dict(cmd_magic); + +typedef struct { + OptionalKeys is_set__cmd_mods_; + Boolean silent; + Boolean emsg_silent; + Boolean unsilent; + Dictionary filter; + Boolean sandbox; + Boolean noautocmd; + Boolean browse; + Boolean confirm; + Boolean hide; + Boolean horizontal; + Boolean keepalt; + Boolean keepjumps; + Boolean keepmarks; + Boolean keeppatterns; + Boolean lockmarks; + Boolean noswapfile; + Integer tab; + Integer verbose; + Boolean vertical; + String split; +} Dict(cmd_mods); + +typedef struct { + OptionalKeys is_set__cmd_mods_filter_; + String pattern; + Boolean force; +} Dict(cmd_mods_filter); + +typedef struct { + Boolean output; +} Dict(cmd_opts); + +typedef struct { + Boolean verbose; +} Dict(echo_opts); + +typedef struct { + Boolean output; +} Dict(exec_opts); diff --git a/src/nvim/api/options.c b/src/nvim/api/options.c index bfcb99754f..c012a69c7b 100644 --- a/src/nvim/api/options.c +++ b/src/nvim/api/options.c @@ -1,80 +1,135 @@ -// 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 <inttypes.h> -#include <limits.h> +#include <assert.h> #include <stdbool.h> #include <string.h> +#include "nvim/api/keysets_defs.h" #include "nvim/api/options.h" #include "nvim/api/private/defs.h" +#include "nvim/api/private/dispatch.h" #include "nvim/api/private/helpers.h" +#include "nvim/api/private/validate.h" #include "nvim/autocmd.h" -#include "nvim/buffer_defs.h" +#include "nvim/buffer.h" #include "nvim/eval/window.h" +#include "nvim/func_attr.h" #include "nvim/globals.h" +#include "nvim/macros_defs.h" #include "nvim/memory.h" #include "nvim/option.h" -#include "nvim/vim.h" +#include "nvim/option_vars.h" +#include "nvim/vim_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, +static int validate_option_value_args(Dict(option) *opts, char *name, int *scope, + OptReqScope *req_scope, void **from, char **filetype, Error *err) { - if (opts->scope.type == kObjectTypeString) { - if (!strcmp(opts->scope.data.string.data, "local")) { +#define HAS_KEY_X(d, v) HAS_KEY(d, option, v) + if (HAS_KEY_X(opts, scope)) { + if (!strcmp(opts->scope.data, "local")) { *scope = OPT_LOCAL; - } else if (!strcmp(opts->scope.data.string.data, "global")) { + } else if (!strcmp(opts->scope.data, "global")) { *scope = OPT_GLOBAL; } else { - api_set_error(err, kErrorTypeValidation, "invalid scope: must be 'local' or 'global'"); - return FAIL; + VALIDATE_EXP(false, "scope", "'local' or 'global'", NULL, { + return FAIL; + }); } - } else if (HAS_KEY(opts->scope)) { - api_set_error(err, kErrorTypeValidation, "invalid value for key: scope"); - return FAIL; } - *opt_type = SREQ_GLOBAL; + *req_scope = kOptReqGlobal; + + if (filetype != NULL && HAS_KEY_X(opts, filetype)) { + *filetype = opts->filetype.data; + } - if (opts->win.type == kObjectTypeInteger) { - *opt_type = SREQ_WIN; - *from = find_window_by_handle((int)opts->win.data.integer, err); + if (HAS_KEY_X(opts, win)) { + *req_scope = kOptReqWin; + *from = find_window_by_handle(opts->win, 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) { + if (HAS_KEY_X(opts, buf)) { *scope = OPT_LOCAL; - *opt_type = SREQ_BUF; - *from = find_buffer_by_handle((int)opts->buf.data.integer, err); + *req_scope = kOptReqBuf; + *from = find_buffer_by_handle(opts->buf, 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"); + VALIDATE((!HAS_KEY_X(opts, filetype) + || !(HAS_KEY_X(opts, buf) || HAS_KEY_X(opts, scope) || HAS_KEY_X(opts, win))), + "%s", "cannot use 'filetype' with 'scope', 'buf' or 'win'", { return FAIL; - } + }); + + VALIDATE((!HAS_KEY_X(opts, scope) || !HAS_KEY_X(opts, buf)), "%s", + "cannot use both 'scope' and 'buf'", { + return FAIL; + }); - if (HAS_KEY(opts->win) && HAS_KEY(opts->buf)) { - api_set_error(err, kErrorTypeValidation, "buf and win cannot be used together"); + VALIDATE((!HAS_KEY_X(opts, win) || !HAS_KEY_X(opts, buf)), + "%s", "cannot use both 'buf' and 'win'", { return FAIL; + }); + + int flags = get_option_attrs(name); + if (flags == 0) { + // hidden or unknown option + api_set_error(err, kErrorTypeValidation, "Unknown option '%s'", name); + } else if (*req_scope == kOptReqBuf || *req_scope == kOptReqWin) { + // if 'buf' or 'win' is passed, make sure the option supports it + int req_flags = *req_scope == kOptReqBuf ? SOPT_BUF : SOPT_WIN; + if (!(flags & req_flags)) { + char *tgt = *req_scope & kOptReqBuf ? "buf" : "win"; + char *global = flags & SOPT_GLOBAL ? "global " : ""; + char *req = flags & SOPT_BUF ? "buffer-local " + : flags & SOPT_WIN ? "window-local " : ""; + + api_set_error(err, kErrorTypeValidation, "'%s' cannot be passed for %s%soption '%s'", + tgt, global, req, name); + } } return OK; +#undef HAS_KEY_X +} + +/// Create a dummy buffer and run the FileType autocmd on it. +static buf_T *do_ft_buf(char *filetype, aco_save_T *aco, Error *err) +{ + if (filetype == NULL) { + return NULL; + } + + // Allocate a buffer without putting it in the buffer list. + buf_T *ftbuf = buflist_new(NULL, NULL, 1, BLN_DUMMY); + if (ftbuf == NULL) { + api_set_error(err, kErrorTypeException, "Could not create internal buffer"); + return NULL; + } + + // Set curwin/curbuf to buf and save a few things. + aucmd_prepbuf(aco, ftbuf); + + TRY_WRAP(err, { + set_option_value("bufhidden", STATIC_CSTR_AS_OPTVAL("hide"), OPT_LOCAL); + set_option_value("buftype", STATIC_CSTR_AS_OPTVAL("nofile"), OPT_LOCAL); + set_option_value("swapfile", BOOLEAN_OPTVAL(false), OPT_LOCAL); + set_option_value("modeline", BOOLEAN_OPTVAL(false), OPT_LOCAL); // 'nomodeline' + + ftbuf->b_p_ft = xstrdup(filetype); + do_filetype_autocmd(ftbuf, false); + }); + + return ftbuf; } /// Gets the value of an option. The behavior of this function matches that of @@ -89,53 +144,61 @@ static int validate_option_value_args(Dict(option) *opts, int *scope, int *opt_t /// - win: |window-ID|. Used for getting window local options. /// - buf: Buffer number. Used for getting buffer local options. /// Implies {scope} is "local". +/// - filetype: |filetype|. Used to get the default option for a +/// specific filetype. Cannot be used with any other option. +/// Note: this will trigger |ftplugin| and all |FileType| +/// autocommands for the corresponding filetype. /// @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; + OptVal value = NIL_OPTVAL; int scope = 0; - int opt_type = SREQ_GLOBAL; + OptReqScope req_scope = kOptReqGlobal; void *from = NULL; - if (!validate_option_value_args(opts, &scope, &opt_type, &from, err)) { - return rv; + char *filetype = NULL; + + if (!validate_option_value_args(opts, name.data, &scope, &req_scope, &from, &filetype, err)) { + goto err; } - long numval = 0; - char *stringval = NULL; - getoption_T result = access_option_value_for(name.data, &numval, &stringval, scope, opt_type, - from, true, err); + aco_save_T aco; + + buf_T *ftbuf = do_ft_buf(filetype, &aco, err); if (ERROR_SET(err)) { - return rv; + goto err; } - switch (result) { - case gov_string: - rv = STRING_OBJ(cstr_as_string(stringval)); - break; - case gov_number: - rv = INTEGER_OBJ(numval); - break; - case gov_bool: - 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; + if (ftbuf != NULL) { + assert(!from); + from = ftbuf; + } + + bool hidden; + value = get_option_value_for(name.data, NULL, scope, &hidden, req_scope, from, err); + + if (ftbuf != NULL) { + // restore curwin/curbuf and a few other things + aucmd_restbuf(&aco); + + assert(curbuf != ftbuf); // safety check + wipe_buffer(ftbuf, false); + } + + if (ERROR_SET(err)) { + goto err; } + VALIDATE_S(!hidden && value.type != kOptValTypeNil, "option", name.data, { + goto err; + }); + + return optval_as_object(value); +err: + optval_free(value); return rv; } @@ -153,13 +216,14 @@ Object nvim_get_option_value(String name, Dict(option) *opts, Error *err) /// - 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) +void nvim_set_option_value(uint64_t channel_id, String name, Object value, Dict(option) *opts, + Error *err) FUNC_API_SINCE(9) { int scope = 0; - int opt_type = SREQ_GLOBAL; + OptReqScope req_scope = kOptReqGlobal; void *to = NULL; - if (!validate_option_value_args(opts, &scope, &opt_type, &to, err)) { + if (!validate_option_value_args(opts, name.data, &scope, &req_scope, &to, NULL, err)) { return; } @@ -169,41 +233,35 @@ void nvim_set_option_value(String name, Object value, Dict(option) *opts, Error // - 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 (req_scope == kOptReqWin && scope == 0) { + int flags = get_option_attrs(name.data); if (flags & SOPT_GLOBAL) { scope = OPT_LOCAL; } } - long numval = 0; - char *stringval = NULL; + bool error = false; + OptVal optval = object_as_optval(value, &error); - 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"); + // Handle invalid option value type. + // Don't use `name` in the error message here, because `name` can be any String. + // No need to check if value type actually matches the types for the option, as set_option_value() + // already handles that. + VALIDATE_EXP(!error, "value", "valid option type", api_typename(value.type), { return; - } + }); - access_option_value_for(name.data, &numval, &stringval, scope, opt_type, to, false, err); + WITH_SCRIPT_CONTEXT(channel_id, { + set_option_value_for(name.data, optval, scope, req_scope, to, 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()|. +/// dictionaries as detailed at |nvim_get_option_info2()|. +/// +/// @see |nvim_get_commands()| /// /// @return dictionary of all options Dictionary nvim_get_all_options_info(Error *err) @@ -212,7 +270,7 @@ Dictionary nvim_get_all_options_info(Error *err) return get_all_vimoptions(); } -/// Gets the option information for one option +/// Gets the option information for one option from arbitrary buffer or window /// /// Resulting dictionary has keys: /// - name: Name of the option (like 'filetype') @@ -231,324 +289,289 @@ Dictionary nvim_get_all_options_info(Error *err) /// - commalist: List of comma separated values /// - flaglist: List of single char flags /// +/// When {scope} is not provided, the last set information applies to the local +/// value in the current buffer or window if it is available, otherwise the +/// global value information is returned. This behavior can be disabled by +/// explicitly specifying {scope} in the {opts} table. /// -/// @param name Option name +/// @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 Information -Dictionary nvim_get_option_info(String name, Error *err) - FUNC_API_SINCE(7) +Dictionary nvim_get_option_info2(String name, Dict(option) *opts, Error *err) + FUNC_API_SINCE(11) { - 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); -} + int scope = 0; + OptReqScope req_scope = kOptReqGlobal; + void *from = NULL; + if (!validate_option_value_args(opts, name.data, &scope, &req_scope, &from, NULL, err)) { + return (Dictionary)ARRAY_DICT_INIT; + } -/// 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, Arena *arena, Error *err) - FUNC_API_SINCE(1) -{ - return get_option_from(NULL, SREQ_GLOBAL, name, err); + buf_T *buf = (req_scope == kOptReqBuf) ? (buf_T *)from : curbuf; + win_T *win = (req_scope == kOptReqWin) ? (win_T *)from : curwin; + + return get_vimoption(name, scope, buf, win, err); } -/// Gets a buffer option value +/// Switch current context to get/set option value for window/buffer. +/// +/// @param[out] ctx Current context. switchwin_T for window and aco_save_T for buffer. +/// @param req_scope Requested option scope. See OptReqScope in option.h. +/// @param[in] from Target buffer/window. +/// @param[out] err Error message, if any. /// -/// @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, Arena *arena, Error *err) - FUNC_API_SINCE(1) +/// @return true if context was switched, false otherwise. +static bool switch_option_context(void *const ctx, OptReqScope req_scope, void *const from, + Error *err) { - buf_T *buf = find_buffer_by_handle(buffer, err); + switch (req_scope) { + case kOptReqWin: { + win_T *const win = (win_T *)from; + switchwin_T *const switchwin = (switchwin_T *)ctx; + + if (win == curwin) { + return false; + } - if (!buf) { - return (Object)OBJECT_INIT; + if (switch_win_noblock(switchwin, win, win_find_tabpage(win), true) + == FAIL) { + restore_win_noblock(switchwin, true); + + if (try_end(err)) { + return false; + } + api_set_error(err, kErrorTypeException, "Problem while switching windows"); + return false; + } + return true; } + case kOptReqBuf: { + buf_T *const buf = (buf_T *)from; + aco_save_T *const aco = (aco_save_T *)ctx; - return get_option_from(buf, SREQ_BUF, name, err); + if (buf == curbuf) { + return false; + } + aucmd_prepbuf(aco, buf); + return true; + } + case kOptReqGlobal: + return false; + } + UNREACHABLE; } -/// 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) +/// Restore context after getting/setting option for window/buffer. See switch_option_context() for +/// params. +static void restore_option_context(void *const ctx, OptReqScope req_scope) { - buf_T *buf = find_buffer_by_handle(buffer, err); - - if (!buf) { - return; + switch (req_scope) { + case kOptReqWin: + restore_win_noblock((switchwin_T *)ctx, true); + break; + case kOptReqBuf: + aucmd_restbuf((aco_save_T *)ctx); + break; + case kOptReqGlobal: + break; } - - set_option_to(channel_id, buf, SREQ_BUF, name, value, err); } -/// Gets a window option value +/// Get attributes for an option. /// -/// @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, Arena *arena, Error *err) - FUNC_API_SINCE(1) +/// @param name Option name. +/// +/// @return Option attributes. +/// 0 for hidden or unknown option. +/// See SOPT_* in option_defs.h for other flags. +int get_option_attrs(char *name) { - win_T *win = find_window_by_handle(window, err); + int opt_idx = findoption(name); - if (!win) { - return (Object)OBJECT_INIT; + if (opt_idx < 0) { + return 0; } - return get_option_from(win, SREQ_WIN, name, err); -} + vimoption_T *opt = get_option(opt_idx); -/// 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 (is_tty_option(opt->fullname)) { + return SOPT_STRING | SOPT_GLOBAL; + } - if (!win) { - return; + // Hidden option + if (opt->var == NULL) { + return 0; } - set_option_to(channel_id, win, SREQ_WIN, name, value, err); -} + int attrs = 0; -/// 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 -static Object get_option_from(void *from, int type, String name, Error *err) -{ - Object rv = OBJECT_INIT; + if (opt->flags & P_BOOL) { + attrs |= SOPT_BOOL; + } else if (opt->flags & P_NUM) { + attrs |= SOPT_NUM; + } else if (opt->flags & P_STRING) { + attrs |= SOPT_STRING; + } - 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); + if (opt->indir == PV_NONE || (opt->indir & PV_BOTH)) { + attrs |= SOPT_GLOBAL; + } + if (opt->indir & PV_WIN) { + attrs |= SOPT_WIN; + } else if (opt->indir & PV_BUF) { + attrs |= SOPT_BUF; } - return rv; + return attrs; } -/// Sets the value of a global or local (buffer, window) option. +/// Check if option has a value in the requested scope. /// -/// @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) +/// @param name Option name. +/// @param req_scope Requested option scope. See OptReqScope in option.h. +/// +/// @return true if option has a value in the requested scope, false otherwise. +static bool option_has_scope(char *name, OptReqScope req_scope) { - if (name.size == 0) { - api_set_error(err, kErrorTypeValidation, "Empty option name"); - return; + int opt_idx = findoption(name); + + if (opt_idx < 0) { + return false; } - int flags = get_option_value_strict(name.data, NULL, NULL, type, to); + vimoption_T *opt = get_option(opt_idx); - if (flags == 0) { - api_set_error(err, kErrorTypeValidation, "Invalid option name '%s'", - name.data); - return; + // Hidden option. + if (opt->var == NULL) { + return false; + } + // TTY option. + if (is_tty_option(opt->fullname)) { + return req_scope == kOptReqGlobal; } - 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; - } + switch (req_scope) { + case kOptReqGlobal: + return opt->var != VAR_WIN; + case kOptReqBuf: + return opt->indir & PV_BUF; + case kOptReqWin: + return opt->indir & PV_WIN; } + UNREACHABLE; +} - long numval = 0; - char *stringval = NULL; +/// Get the option value in the requested scope. +/// +/// @param name Option name. +/// @param req_scope Requested option scope. See OptReqScope in option.h. +/// @param[in] from Pointer to buffer or window for local option value. +/// @param[out] err Error message, if any. +/// +/// @return Option value in the requested scope. Returns a Nil option value if option is not found, +/// hidden or if it isn't present in the requested scope. (i.e. has no global, window-local or +/// buffer-local value depending on opt_scope). +OptVal get_option_value_strict(char *name, OptReqScope req_scope, void *from, Error *err) +{ + OptVal retv = NIL_OPTVAL; - if (flags & SOPT_BOOL) { - if (value.type != kObjectTypeBoolean) { - api_set_error(err, - kErrorTypeValidation, - "Option '%s' requires a Boolean value", - name.data); - return; - } + if (!option_has_scope(name, req_scope)) { + return retv; + } + if (get_tty_option(name, &retv.data.string.data)) { + retv.type = kOptValTypeString; + return retv; + } - 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; - } + int opt_idx = findoption(name); + assert(opt_idx != 0); // option_has_scope() already verifies if option name is valid. - 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; - } + vimoption_T *opt = get_option(opt_idx); + switchwin_T switchwin; + aco_save_T aco; + void *ctx = req_scope == kOptReqWin ? (void *)&switchwin + : (req_scope == kOptReqBuf ? (void *)&aco : NULL); + bool switched = switch_option_context(ctx, req_scope, from, err); + if (ERROR_SET(err)) { + return retv; + } - numval = (int)value.data.integer; - } else { - if (value.type != kObjectTypeString) { - api_set_error(err, kErrorTypeValidation, - "Option '%s' requires a string value", - name.data); - return; - } + char *varp = get_varp_scope(opt, req_scope == kOptReqGlobal ? OPT_GLOBAL : OPT_LOCAL); + retv = optval_from_varp(opt_idx, varp); - stringval = value.data.string.data; + if (switched) { + restore_option_context(ctx, req_scope); } - // 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); - }); + return retv; } -static getoption_T access_option_value(char *key, long *numval, char **stringval, int opt_flags, - bool get, Error *err) +/// Get option value for buffer / window. +/// +/// @param[in] name Option name. +/// @param[out] flagsp Set to the option flags (P_xxxx) (if not NULL). +/// @param[in] scope Option scope (can be OPT_LOCAL, OPT_GLOBAL or a combination). +/// @param[out] hidden Whether option is hidden. +/// @param req_scope Requested option scope. See OptReqScope in option.h. +/// @param[in] from Target buffer/window. +/// @param[out] err Error message, if any. +/// +/// @return Option value. Must be freed by caller. +OptVal get_option_value_for(const char *const name, uint32_t *flagsp, int scope, bool *hidden, + const OptReqScope req_scope, void *const from, Error *err) { - if (get) { - return get_option_value(key, numval, stringval, NULL, opt_flags); - } else { - char *errmsg; - if ((errmsg = set_option_value(key, *numval, *stringval, opt_flags))) { - if (try_end(err)) { - return 0; - } + switchwin_T switchwin; + aco_save_T aco; + void *ctx = req_scope == kOptReqWin ? (void *)&switchwin + : (req_scope == kOptReqBuf ? (void *)&aco : NULL); - api_set_error(err, kErrorTypeException, "%s", errmsg); - } - return 0; + bool switched = switch_option_context(ctx, req_scope, from, err); + if (ERROR_SET(err)) { + return NIL_OPTVAL; + } + + OptVal retv = get_option_value(name, flagsp, scope, hidden); + + if (switched) { + restore_option_context(ctx, req_scope); } + + return retv; } -static getoption_T access_option_value_for(char *key, long *numval, char **stringval, int opt_flags, - int opt_type, void *from, bool get, Error *err) +/// Set option value for buffer / window. +/// +/// @param[in] name Option name. +/// @param[in] value Option value. +/// @param[in] opt_flags Flags: OPT_LOCAL, OPT_GLOBAL, or 0 (both). +/// @param req_scope Requested option scope. See OptReqScope in option.h. +/// @param[in] from Target buffer/window. +/// @param[out] err Error message, if any. +void set_option_value_for(const char *const name, OptVal value, const int opt_flags, + const OptReqScope req_scope, void *const from, Error *err) { - bool need_switch = false; switchwin_T switchwin; aco_save_T aco; - getoption_T 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; - } + void *ctx = req_scope == kOptReqWin ? (void *)&switchwin + : (req_scope == kOptReqBuf ? (void *)&aco : NULL); + bool switched = switch_option_context(ctx, req_scope, from, err); if (ERROR_SET(err)) { - return result; + return; } - try_end(err); + const char *const errmsg = set_option_value(name, value, opt_flags); + if (errmsg) { + api_set_error(err, kErrorTypeException, "%s", errmsg); + } - return result; + if (switched) { + restore_option_context(ctx, req_scope); + } } diff --git a/src/nvim/api/options.h b/src/nvim/api/options.h index efbfec3a6c..c16c6088b3 100644 --- a/src/nvim/api/options.h +++ b/src/nvim/api/options.h @@ -1,9 +1,11 @@ -#ifndef NVIM_API_OPTIONS_H -#define NVIM_API_OPTIONS_H +#pragma once + +#include <stdint.h> // IWYU pragma: keep + +#include "nvim/api/keysets_defs.h" // IWYU pragma: keep +#include "nvim/api/private/defs.h" // IWYU pragma: keep +#include "nvim/option_defs.h" // IWYU pragma: keep -#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 58ff552ab7..90023171e5 100644 --- a/src/nvim/api/private/converter.c +++ b/src/nvim/api/private/converter.c @@ -1,25 +1,21 @@ -// 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 <stdbool.h> #include <stddef.h> #include <stdint.h> -#include <stdlib.h> #include "klib/kvec.h" #include "nvim/api/private/converter.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/helpers.h" -#include "nvim/assert.h" +#include "nvim/assert_defs.h" #include "nvim/eval/typval.h" #include "nvim/eval/typval_defs.h" #include "nvim/eval/userfunc.h" -#include "nvim/garray.h" +#include "nvim/func_attr.h" #include "nvim/lua/executor.h" #include "nvim/memory.h" -#include "nvim/types.h" -#include "nvim/vim.h" +#include "nvim/types_defs.h" +#include "nvim/vim_defs.h" /// Helper structure for vim_to_object typedef struct { @@ -49,9 +45,9 @@ typedef struct { #define TYPVAL_ENCODE_CONV_STRING(tv, str, len) \ do { \ const size_t len_ = (size_t)(len); \ - const char *const str_ = (const char *)(str); \ + const char *const str_ = (str); \ assert(len_ == 0 || str_ != NULL); \ - kvi_push(edata->stack, STRING_OBJ(cbuf_to_string((len_?str_:""), len_))); \ + kvi_push(edata->stack, STRING_OBJ(cbuf_to_string((len_ ? str_ : ""), len_))); \ } while (0) #define TYPVAL_ENCODE_CONV_STR_STRING TYPVAL_ENCODE_CONV_STRING @@ -204,6 +200,7 @@ static inline void typval_encode_dict_end(EncodedData *const edata) #define TYPVAL_ENCODE_FIRST_ARG_TYPE EncodedData *const #define TYPVAL_ENCODE_FIRST_ARG_NAME edata #include "nvim/eval/typval_encode.c.h" + #undef TYPVAL_ENCODE_SCOPE #undef TYPVAL_ENCODE_NAME #undef TYPVAL_ENCODE_FIRST_ARG_TYPE @@ -256,12 +253,14 @@ Object vim_to_object(typval_T *obj) return ret; } -/// Converts from type Object to a VimL value. +/// Converts from type Object to a Vimscript value. /// /// @param obj Object to convert from. /// @param tv Conversion result is placed here. On failure member v_type is /// set to VAR_UNKNOWN (no allocation was made for this variable). -/// returns true if conversion is successful, otherwise false. +/// @param err Error object. +/// +/// @returns true if conversion is successful, otherwise false. bool object_to_vim(Object obj, typval_T *tv, Error *err) { tv->v_type = VAR_UNKNOWN; @@ -275,7 +274,7 @@ bool object_to_vim(Object obj, typval_T *tv, Error *err) case kObjectTypeBoolean: tv->v_type = VAR_BOOL; - tv->vval.v_bool = obj.data.boolean? kBoolVarTrue: kBoolVarFalse; + tv->vval.v_bool = obj.data.boolean ? kBoolVarTrue : kBoolVarFalse; break; case kObjectTypeBuffer: @@ -283,7 +282,7 @@ bool object_to_vim(Object obj, typval_T *tv, Error *err) case kObjectTypeTabpage: case kObjectTypeInteger: STATIC_ASSERT(sizeof(obj.data.integer) <= sizeof(varnumber_T), - "Integer size must be <= VimL number size"); + "Integer size must be <= Vimscript number size"); tv->v_type = VAR_NUMBER; tv->vval.v_number = (varnumber_T)obj.data.integer; break; @@ -363,9 +362,6 @@ bool object_to_vim(Object obj, typval_T *tv, Error *err) tv->vval.v_string = xstrdup(name); break; } - - default: - abort(); } return true; diff --git a/src/nvim/api/private/converter.h b/src/nvim/api/private/converter.h index 80ee640295..fc82abf332 100644 --- a/src/nvim/api/private/converter.h +++ b/src/nvim/api/private/converter.h @@ -1,11 +1,8 @@ -#ifndef NVIM_API_PRIVATE_CONVERTER_H -#define NVIM_API_PRIVATE_CONVERTER_H +#pragma once -#include "nvim/api/private/defs.h" -#include "nvim/eval/typval.h" +#include "nvim/api/private/defs.h" // IWYU pragma: keep +#include "nvim/eval/typval_defs.h" // IWYU pragma: keep #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/private/converter.h.generated.h" #endif - -#endif // NVIM_API_PRIVATE_CONVERTER_H diff --git a/src/nvim/api/private/defs.h b/src/nvim/api/private/defs.h index 8acbf0d9de..25c8377518 100644 --- a/src/nvim/api/private/defs.h +++ b/src/nvim/api/private/defs.h @@ -1,5 +1,4 @@ -#ifndef NVIM_API_PRIVATE_DEFS_H -#define NVIM_API_PRIVATE_DEFS_H +#pragma once #include <stdbool.h> #include <stdint.h> @@ -7,7 +6,7 @@ #include "klib/kvec.h" #include "nvim/func_attr.h" -#include "nvim/types.h" +#include "nvim/types_defs.h" #define ARRAY_DICT_INIT KV_INITIAL_VALUE #define STRING_INIT { .data = NULL, .size = 0 } @@ -42,10 +41,10 @@ typedef enum { /// Mask for all internal calls #define INTERNAL_CALL_MASK (((uint64_t)1) << (sizeof(uint64_t) * 8 - 1)) -/// Internal call from VimL code +/// Internal call from Vimscript code #define VIML_INTERNAL_CALL INTERNAL_CALL_MASK -/// Internal call from lua code +/// Internal call from Lua code #define LUA_INTERNAL_CALL (VIML_INTERNAL_CALL + 1) static inline bool is_internal_call(uint64_t channel_id) @@ -124,14 +123,18 @@ struct key_value_pair { Object value; }; -typedef Object *(*field_hash)(void *retval, const char *str, size_t len); +typedef uint64_t OptionalKeys; + +// this is the prefix of all keysets with optional keys +typedef struct { + OptionalKeys is_set_; +} OptKeySet; + typedef struct { char *str; size_t ptr_off; + ObjectType type; // kObjectTypeNil == untyped + int opt_index; } KeySetLink; -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "keysets_defs.generated.h" -#endif - -#endif // NVIM_API_PRIVATE_DEFS_H +typedef KeySetLink *(*FieldHashfn)(const char *str, size_t len); diff --git a/src/nvim/api/private/dispatch.c b/src/nvim/api/private/dispatch.c index f427bba00e..53fcd148bd 100644 --- a/src/nvim/api/private/dispatch.c +++ b/src/nvim/api/private/dispatch.c @@ -1,6 +1,3 @@ -// 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 <stddef.h> #include "nvim/api/private/defs.h" diff --git a/src/nvim/api/private/dispatch.h b/src/nvim/api/private/dispatch.h index 4ae61b2bfb..6a2c9eaf54 100644 --- a/src/nvim/api/private/dispatch.h +++ b/src/nvim/api/private/dispatch.h @@ -1,12 +1,11 @@ -#ifndef NVIM_API_PRIVATE_DISPATCH_H -#define NVIM_API_PRIVATE_DISPATCH_H +#pragma once #include <stdbool.h> #include <stdint.h> #include "nvim/api/private/defs.h" -#include "nvim/memory.h" -#include "nvim/types.h" +#include "nvim/memory_defs.h" +#include "nvim/types_defs.h" typedef Object (*ApiDispatchWrapper)(uint64_t channel_id, Array args, Arena *arena, Error *error); @@ -27,7 +26,6 @@ extern const MsgpackRpcRequestHandler method_handlers[]; #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/private/dispatch.h.generated.h" -# include "api/private/dispatch_wrappers.h.generated.h" +# include "api/private/dispatch_wrappers.h.generated.h" // IWYU pragma: export +# include "keysets_defs.generated.h" #endif - -#endif // NVIM_API_PRIVATE_DISPATCH_H diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index 519f2cc5bf..be39836a5b 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -1,13 +1,10 @@ -// 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 <limits.h> #include <msgpack/unpack.h> #include <stdarg.h> #include <stdbool.h> #include <stddef.h> +#include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -16,21 +13,26 @@ #include "nvim/api/private/converter.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/helpers.h" -#include "nvim/ascii.h" +#include "nvim/api/private/validate.h" +#include "nvim/ascii_defs.h" #include "nvim/buffer_defs.h" #include "nvim/eval/typval.h" #include "nvim/eval/typval_defs.h" +#include "nvim/eval/vars.h" #include "nvim/ex_eval.h" +#include "nvim/func_attr.h" #include "nvim/garray.h" +#include "nvim/globals.h" #include "nvim/highlight_group.h" #include "nvim/lua/executor.h" -#include "nvim/map.h" +#include "nvim/map_defs.h" #include "nvim/mark.h" #include "nvim/memline.h" #include "nvim/memory.h" #include "nvim/message.h" #include "nvim/msgpack_rpc/helpers.h" -#include "nvim/pos.h" +#include "nvim/pos_defs.h" +#include "nvim/types_defs.h" #include "nvim/ui.h" #include "nvim/version.h" @@ -40,10 +42,10 @@ # include "api/private/ui_events_metadata.generated.h" #endif -/// Start block that may cause VimL exceptions while evaluating another code +/// Start block that may cause Vimscript exceptions while evaluating another code /// -/// Used when caller is supposed to be operating when other VimL code is being -/// processed and that “other VimL code” must not be affected. +/// Used when caller is supposed to be operating when other Vimscript code is being +/// processed and that “other Vimscript code” must not be affected. /// /// @param[out] tstate Location where try state should be saved. void try_enter(TryState *const tstate) @@ -234,8 +236,7 @@ Object dict_set_var(dict_T *dict, String key, Object value, bool del, bool retva // Delete the key if (di == NULL) { // Doesn't exist, fail - api_set_error(err, kErrorTypeValidation, "Key not found: %s", - key.data); + api_set_error(err, kErrorTypeValidation, "Key not found: %s", key.data); } else { // Notify watchers if (watched) { @@ -264,13 +265,23 @@ Object dict_set_var(dict_T *dict, String key, Object value, bool del, bool retva di = tv_dict_item_alloc_len(key.data, key.size); tv_dict_add(dict, di); } else { - if (watched) { - tv_copy(&di->di_tv, &oldtv); - } // Return the old value if (retval) { rv = vim_to_object(&di->di_tv); } + bool type_error = false; + if (dict == &vimvardict + && !before_set_vvar(key.data, di, &tv, true, watched, &type_error)) { + tv_clear(&tv); + if (type_error) { + api_set_error(err, kErrorTypeValidation, + "Setting v:%s to value with wrong type", key.data); + } + return rv; + } + if (watched) { + tv_copy(&di->di_tv, &oldtv); + } tv_clear(&di->di_tv); } @@ -478,6 +489,27 @@ Array string_to_array(const String input, bool crlf) return ret; } +/// Normalizes 0-based indexes to buffer line numbers. +int64_t normalize_index(buf_T *buf, int64_t index, bool end_exclusive, bool *oob) +{ + assert(buf->b_ml.ml_line_count > 0); + int64_t max_index = buf->b_ml.ml_line_count + (int)end_exclusive - 1; + // A negative index counts from the bottom. + index = index < 0 ? max_index + index + 1 : index; + + // Check for oob and clamp. + if (index > max_index) { + *oob = true; + index = max_index; + } else if (index < 0) { + *oob = true; + index = 0; + } + // Convert the index to a 1-based line number. + index++; + return index; +} + /// Returns a substring of a buffer line /// /// @param buf Buffer handle @@ -495,7 +527,7 @@ String buf_get_text(buf_T *buf, int64_t lnum, int64_t start_col, int64_t end_col return rv; } - char *bufstr = ml_get_buf(buf, (linenr_T)lnum, false); + char *bufstr = ml_get_buf(buf, (linenr_T)lnum); size_t line_length = strlen(bufstr); start_col = start_col < 0 ? (int64_t)line_length + start_col + 1 : start_col; @@ -577,9 +609,6 @@ void api_free_object(Object value) case kObjectTypeLuaRef: api_free_luaref(value.data.luaref); break; - - default: - abort(); } } @@ -660,10 +689,10 @@ static void init_ui_event_metadata(Dictionary *metadata) msgpack_unpacked_destroy(&unpacked); PUT(*metadata, "ui_events", ui_events); Array ui_options = ARRAY_DICT_INIT; - ADD(ui_options, STRING_OBJ(cstr_to_string("rgb"))); + ADD(ui_options, CSTR_TO_OBJ("rgb")); for (UIExtension i = 0; i < kUIExtCount; i++) { if (ui_ext_names[i][0] != '_') { - ADD(ui_options, STRING_OBJ(cstr_to_string(ui_ext_names[i]))); + ADD(ui_options, CSTR_TO_OBJ(ui_ext_names[i])); } } PUT(*metadata, "ui_options", ARRAY_OBJ(ui_options)); @@ -692,17 +721,17 @@ static void init_type_metadata(Dictionary *metadata) Dictionary buffer_metadata = ARRAY_DICT_INIT; PUT(buffer_metadata, "id", INTEGER_OBJ(kObjectTypeBuffer - EXT_OBJECT_TYPE_SHIFT)); - PUT(buffer_metadata, "prefix", STRING_OBJ(cstr_to_string("nvim_buf_"))); + PUT(buffer_metadata, "prefix", CSTR_TO_OBJ("nvim_buf_")); Dictionary window_metadata = ARRAY_DICT_INIT; PUT(window_metadata, "id", INTEGER_OBJ(kObjectTypeWindow - EXT_OBJECT_TYPE_SHIFT)); - PUT(window_metadata, "prefix", STRING_OBJ(cstr_to_string("nvim_win_"))); + PUT(window_metadata, "prefix", CSTR_TO_OBJ("nvim_win_")); Dictionary tabpage_metadata = ARRAY_DICT_INIT; PUT(tabpage_metadata, "id", INTEGER_OBJ(kObjectTypeTabpage - EXT_OBJECT_TYPE_SHIFT)); - PUT(tabpage_metadata, "prefix", STRING_OBJ(cstr_to_string("nvim_tabpage_"))); + PUT(tabpage_metadata, "prefix", CSTR_TO_OBJ("nvim_tabpage_")); PUT(types, "Buffer", DICTIONARY_OBJ(buffer_metadata)); PUT(types, "Window", DICTIONARY_OBJ(window_metadata)); @@ -767,10 +796,8 @@ Object copy_object(Object obj, Arena *arena) case kObjectTypeLuaRef: return LUAREF_OBJ(api_new_luaref(obj.data.luaref)); - - default: - abort(); } + UNREACHABLE; } void api_set_error(Error *err, ErrorType errType, const char *format, ...) @@ -806,7 +833,7 @@ bool api_object_to_bool(Object obj, const char *what, bool nil_value, Error *err } else if (obj.type == kObjectTypeInteger) { return obj.data.integer; // C semantics: non-zero int is true } else if (obj.type == kObjectTypeNil) { - return nil_value; // caller decides what NIL (missing retval in lua) means + return nil_value; // caller decides what NIL (missing retval in Lua) means } else { api_set_error(err, kErrorTypeValidation, "%s is not a boolean", what); return false; @@ -821,12 +848,40 @@ int object_to_hl_id(Object obj, const char *what, Error *err) } else if (obj.type == kObjectTypeInteger) { return MAX((int)obj.data.integer, 0); } else { - api_set_error(err, kErrorTypeValidation, - "%s is not a valid highlight", what); + api_set_error(err, kErrorTypeValidation, "Invalid highlight: %s", what); return 0; } } +char *api_typename(ObjectType t) +{ + switch (t) { + case kObjectTypeNil: + return "nil"; + case kObjectTypeBoolean: + return "Boolean"; + case kObjectTypeInteger: + return "Integer"; + case kObjectTypeFloat: + return "Float"; + case kObjectTypeString: + return "String"; + case kObjectTypeArray: + return "Array"; + case kObjectTypeDictionary: + return "Dict"; + case kObjectTypeLuaRef: + return "Function"; + case kObjectTypeBuffer: + return "Buffer"; + case kObjectTypeWindow: + return "Window"; + case kObjectTypeTabpage: + return "Tabpage"; + } + UNREACHABLE; +} + HlMessage parse_hl_msg(Array chunks, Error *err) { HlMessage hl_msg = KV_INITIAL_VALUE; @@ -865,17 +920,84 @@ free_exit: return (HlMessage)KV_INITIAL_VALUE; } -bool api_dict_to_keydict(void *rv, field_hash hashy, Dictionary dict, Error *err) +// see also nlua_pop_keydict for the lua specific implementation +bool api_dict_to_keydict(void *retval, FieldHashfn hashy, Dictionary dict, Error *err) { for (size_t i = 0; i < dict.size; i++) { String k = dict.items[i].key; - Object *field = hashy(rv, k.data, k.size); + KeySetLink *field = hashy(k.data, k.size); if (!field) { api_set_error(err, kErrorTypeValidation, "Invalid key: '%.*s'", (int)k.size, k.data); return false; } - *field = dict.items[i].value; + if (field->opt_index >= 0) { + OptKeySet *ks = (OptKeySet *)retval; + ks->is_set_ |= (1ULL << field->opt_index); + } + + char *mem = ((char *)retval + field->ptr_off); + Object *value = &dict.items[i].value; + if (field->type == kObjectTypeNil) { + *(Object *)mem = *value; + } else if (field->type == kObjectTypeInteger) { + VALIDATE_T(field->str, kObjectTypeInteger, value->type, { + return false; + }); + *(Integer *)mem = value->data.integer; + } else if (field->type == kObjectTypeFloat) { + Float *val = (Float *)mem; + if (value->type == kObjectTypeInteger) { + *val = (Float)value->data.integer; + } else { + VALIDATE_T(field->str, kObjectTypeFloat, value->type, { + return false; + }); + *val = value->data.floating; + } + } else if (field->type == kObjectTypeBoolean) { + // caller should check HAS_KEY to override the nil behavior, or GET_BOOL_OR_TRUE + // to directly use true when nil + *(Boolean *)mem = api_object_to_bool(*value, field->str, false, err); + if (ERROR_SET(err)) { + return false; + } + } else if (field->type == kObjectTypeString) { + VALIDATE_T(field->str, kObjectTypeString, value->type, { + return false; + }); + *(String *)mem = value->data.string; + } else if (field->type == kObjectTypeArray) { + VALIDATE_T(field->str, kObjectTypeArray, value->type, { + return false; + }); + *(Array *)mem = value->data.array; + } else if (field->type == kObjectTypeDictionary) { + Dictionary *val = (Dictionary *)mem; + // allow empty array as empty dict for lua (directly or via lua-client RPC) + if (value->type == kObjectTypeArray && value->data.array.size == 0) { + *val = (Dictionary)ARRAY_DICT_INIT; + } else if (value->type == kObjectTypeDictionary) { + *val = value->data.dictionary; + } else { + api_err_exp(err, field->str, api_typename(field->type), api_typename(value->type)); + return false; + } + } else if (field->type == kObjectTypeBuffer || field->type == kObjectTypeWindow + || field->type == kObjectTypeTabpage) { + if (value->type == kObjectTypeInteger || value->type == field->type) { + *(handle_T *)mem = (handle_T)value->data.integer; + } else { + api_err_exp(err, field->str, api_typename(field->type), api_typename(value->type)); + return false; + } + } else if (field->type == kObjectTypeLuaRef) { + api_set_error(err, kErrorTypeValidation, "Invalid key: '%.*s' is only allowed from Lua", + (int)k.size, k.data); + return false; + } else { + abort(); + } } return true; @@ -884,7 +1006,18 @@ bool api_dict_to_keydict(void *rv, field_hash hashy, Dictionary dict, Error *err void api_free_keydict(void *dict, KeySetLink *table) { for (size_t i = 0; table[i].str; i++) { - api_free_object(*(Object *)((char *)dict + table[i].ptr_off)); + char *mem = ((char *)dict + table[i].ptr_off); + if (table[i].type == kObjectTypeNil) { + api_free_object(*(Object *)mem); + } else if (table[i].type == kObjectTypeString) { + api_free_string(*(String *)mem); + } else if (table[i].type == kObjectTypeArray) { + api_free_array(*(Array *)mem); + } else if (table[i].type == kObjectTypeDictionary) { + api_free_dictionary(*(Dictionary *)mem); + } else if (table[i].type == kObjectTypeLuaRef) { + api_free_luaref(*(LuaRef *)mem); + } } } @@ -930,12 +1063,14 @@ 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, bool use_winbar) +const char *get_default_stl_hl(win_T *wp, bool use_winbar, int stc_hl_id) { if (wp == NULL) { return "TabLineFill"; } else if (use_winbar) { return (wp == curwin) ? "WinBar" : "WinBarNC"; + } else if (stc_hl_id > 0) { + return syn_id2name(stc_hl_id); } else { return (wp == curwin) ? "StatusLine" : "StatusLineNC"; } diff --git a/src/nvim/api/private/helpers.h b/src/nvim/api/private/helpers.h index ec97ba9ec6..e61dd5f992 100644 --- a/src/nvim/api/private/helpers.h +++ b/src/nvim/api/private/helpers.h @@ -1,26 +1,28 @@ -#ifndef NVIM_API_PRIVATE_HELPERS_H -#define NVIM_API_PRIVATE_HELPERS_H +#pragma once #include <stdbool.h> #include <stddef.h> +#include <stdint.h> #include "klib/kvec.h" #include "nvim/api/private/defs.h" +#include "nvim/api/private/dispatch.h" #include "nvim/decoration.h" +#include "nvim/eval/typval_defs.h" #include "nvim/ex_eval_defs.h" #include "nvim/getchar.h" +#include "nvim/gettext.h" #include "nvim/globals.h" -#include "nvim/macros.h" -#include "nvim/map.h" +#include "nvim/macros_defs.h" +#include "nvim/map_defs.h" #include "nvim/memory.h" -#include "nvim/vim.h" +#include "nvim/message.h" #define OBJECT_OBJ(o) o #define BOOLEAN_OBJ(b) ((Object) { \ .type = kObjectTypeBoolean, \ .data.boolean = b }) -#define BOOL(b) BOOLEAN_OBJ(b) #define INTEGER_OBJ(i) ((Object) { \ .type = kObjectTypeInteger, \ @@ -34,6 +36,7 @@ .type = kObjectTypeString, \ .data.string = s }) +#define CSTR_AS_OBJ(s) STRING_OBJ(cstr_as_string(s)) #define CSTR_TO_OBJ(s) STRING_OBJ(cstr_to_string(s)) #define BUFFER_OBJ(s) ((Object) { \ @@ -63,8 +66,9 @@ #define NIL ((Object)OBJECT_INIT) #define NULL_STRING ((String)STRING_INIT) -// currently treat key=vim.NIL as if the key was missing -#define HAS_KEY(o) ((o).type != kObjectTypeNil) +#define HAS_KEY(d, typ, key) (((d)->is_set__##typ##_ & (1 << KEYSET_OPTIDX_##typ##__##key)) != 0) + +#define GET_BOOL_OR_TRUE(d, typ, key) (HAS_KEY(d, typ, key) ? (d)->key : true) #define PUT(dict, k, v) \ kv_push(dict, ((KeyValuePair) { .key = cstr_to_string(k), .value = v })) @@ -72,8 +76,6 @@ #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) @@ -94,7 +96,7 @@ #define cbuf_as_string(d, s) ((String) { .data = d, .size = s }) -#define STATIC_CSTR_AS_STRING(s) ((String) { .data = s, .size = sizeof(s) - 1 }) +#define STATIC_CSTR_AS_STRING(s) ((String) { .data = s, .size = sizeof("" s) - 1 }) /// Create a new String instance, putting data in allocated memory /// @@ -103,6 +105,9 @@ .data = xmemdupz(s, sizeof(s) - 1), \ .size = sizeof(s) - 1 }) +#define STATIC_CSTR_AS_OBJ(s) STRING_OBJ(STATIC_CSTR_AS_STRING(s)) +#define STATIC_CSTR_TO_OBJ(s) STRING_OBJ(STATIC_CSTR_TO_STRING(s)) + // Helpers used by the generated msgpack-rpc api wrappers #define api_init_boolean #define api_init_integer @@ -122,18 +127,18 @@ #define api_free_window(value) #define api_free_tabpage(value) -EXTERN PMap(handle_T) buffer_handles INIT(= MAP_INIT); -EXTERN PMap(handle_T) window_handles INIT(= MAP_INIT); -EXTERN PMap(handle_T) tabpage_handles INIT(= MAP_INIT); +EXTERN PMap(int) buffer_handles INIT( = MAP_INIT); +EXTERN PMap(int) window_handles INIT( = MAP_INIT); +EXTERN PMap(int) tabpage_handles INIT( = MAP_INIT); -#define handle_get_buffer(h) pmap_get(handle_T)(&buffer_handles, (h)) -#define handle_get_window(h) pmap_get(handle_T)(&window_handles, (h)) -#define handle_get_tabpage(h) pmap_get(handle_T)(&tabpage_handles, (h)) +#define handle_get_buffer(h) pmap_get(int)(&buffer_handles, (h)) +#define handle_get_window(h) pmap_get(int)(&window_handles, (h)) +#define handle_get_tabpage(h) pmap_get(int)(&tabpage_handles, (h)) /// Structure used for saving state for :try /// -/// Used when caller is supposed to be operating when other VimL code is being -/// processed and that “other VimL code” must not be affected. +/// Used when caller is supposed to be operating when other Vimscript code is being +/// processed and that “other Vimscript code” must not be affected. typedef struct { except_T *current_exception; msglist_T *private_msg_list; @@ -149,14 +154,26 @@ typedef struct { // which would otherwise be ignored. This pattern is from do_cmdline(). // // TODO(bfredl): prepare error-handling at "top level" (nv_event). -#define TRY_WRAP(code) \ +#define TRY_WRAP(err, code) \ do { \ msglist_T **saved_msg_list = msg_list; \ msglist_T *private_msg_list; \ msg_list = &private_msg_list; \ private_msg_list = NULL; \ - code \ - msg_list = saved_msg_list; /* Restore the exception context. */ \ + try_start(); \ + code; \ + try_end(err); \ + msg_list = saved_msg_list; /* Restore the exception context. */ \ + } while (0) + +// Execute code with cursor position saved and restored and textlock active. +#define TEXTLOCK_WRAP(code) \ + do { \ + const pos_T save_cursor = curwin->w_cursor; \ + textlock++; \ + code; \ + textlock--; \ + curwin->w_cursor = save_cursor; \ } while (0) // Useful macro for executing some `code` for each item in an array. @@ -169,18 +186,17 @@ typedef struct { #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; \ + const uint64_t save_channel_id = current_channel_id; \ 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_channel_id = save_channel_id; \ current_sctx = save_current_sctx; \ } while (0); - -#endif // NVIM_API_PRIVATE_HELPERS_H diff --git a/src/nvim/api/private/validate.c b/src/nvim/api/private/validate.c new file mode 100644 index 0000000000..e198c671eb --- /dev/null +++ b/src/nvim/api/private/validate.c @@ -0,0 +1,76 @@ +#include <inttypes.h> +#include <stdio.h> +#include <string.h> + +#include "nvim/api/private/defs.h" +#include "nvim/api/private/helpers.h" +#include "nvim/api/private/validate.h" +#include "nvim/ascii_defs.h" +#include "nvim/globals.h" + +/// Creates "Invalid …" message and sets it on `err`. +void api_err_invalid(Error *err, const char *name, const char *val_s, int64_t val_n, bool quote_val) +{ + ErrorType errtype = kErrorTypeValidation; + // Treat `name` without whitespace as a parameter (surround in quotes). + // Treat `name` with whitespace as a description (no quotes). + char *has_space = strchr(name, ' '); + + // No value. + if (val_s && val_s[0] == '\0') { + api_set_error(err, errtype, has_space ? "Invalid %s" : "Invalid '%s'", name); + return; + } + + // Number value. + if (val_s == NULL) { + api_set_error(err, errtype, has_space ? "Invalid %s: %" PRId64 : "Invalid '%s': %" PRId64, + name, val_n); + return; + } + + // String value. + if (has_space) { + api_set_error(err, errtype, quote_val ? "Invalid %s: '%s'" : "Invalid %s: %s", name, val_s); + } else { + api_set_error(err, errtype, quote_val ? "Invalid '%s': '%s'" : "Invalid '%s': %s", name, val_s); + } +} + +/// Creates "Invalid …: expected …" message and sets it on `err`. +void api_err_exp(Error *err, const char *name, const char *expected, const char *actual) +{ + ErrorType errtype = kErrorTypeValidation; + // Treat `name` without whitespace as a parameter (surround in quotes). + // Treat `name` with whitespace as a description (no quotes). + char *has_space = strchr(name, ' '); + + if (!actual) { + api_set_error(err, errtype, + has_space ? "Invalid %s: expected %s" : "Invalid '%s': expected %s", + name, expected); + return; + } + + api_set_error(err, errtype, + has_space ? "Invalid %s: expected %s, got %s" : "Invalid '%s': expected %s, got %s", + name, expected, actual); +} + +bool check_string_array(Array arr, char *name, bool disallow_nl, Error *err) +{ + snprintf(IObuff, sizeof(IObuff), "'%s' item", name); + for (size_t i = 0; i < arr.size; i++) { + VALIDATE_T(IObuff, kObjectTypeString, arr.items[i].type, { + return false; + }); + // Disallow newlines in the middle of the line. + if (disallow_nl) { + const String l = arr.items[i].data.string; + VALIDATE(!memchr(l.data, NL, l.size), "'%s' item contains newlines", name, { + return false; + }); + } + } + return true; +} diff --git a/src/nvim/api/private/validate.h b/src/nvim/api/private/validate.h new file mode 100644 index 0000000000..d1c977cd6e --- /dev/null +++ b/src/nvim/api/private/validate.h @@ -0,0 +1,96 @@ +#pragma once + +#include <stdbool.h> +#include <stddef.h> + +#include "nvim/api/private/defs.h" +#include "nvim/api/private/helpers.h" +#include "nvim/assert_defs.h" +#include "nvim/macros_defs.h" + +#define VALIDATE(cond, fmt_, fmt_arg1, code) \ + do { \ + if (!(cond)) { \ + api_set_error(err, kErrorTypeValidation, fmt_, fmt_arg1); \ + code; \ + } \ + } while (0) + +#define VALIDATE_INT(cond, name, val_, code) \ + do { \ + if (!(cond)) { \ + api_err_invalid(err, name, NULL, val_, false); \ + code; \ + } \ + } while (0) + +#define VALIDATE_S(cond, name, val_, code) \ + do { \ + if (!(cond)) { \ + api_err_invalid(err, name, val_, 0, true); \ + code; \ + } \ + } while (0) + +#define VALIDATE_EXP(cond, name, expected, actual, code) \ + do { \ + if (!(cond)) { \ + api_err_exp(err, name, expected, actual); \ + code; \ + } \ + } while (0) + +#define VALIDATE_T(name, expected_t, actual_t, code) \ + do { \ + STATIC_ASSERT(expected_t != kObjectTypeDictionary, "use VALIDATE_T_DICT"); \ + if (expected_t != actual_t) { \ + api_err_exp(err, name, api_typename(expected_t), api_typename(actual_t)); \ + code; \ + } \ + } while (0) + +/// Checks that `obj_` has type `expected_t`. +#define VALIDATE_T2(obj_, expected_t, code) \ + do { \ + STATIC_ASSERT(expected_t != kObjectTypeDictionary, "use VALIDATE_T_DICT"); \ + if ((obj_).type != expected_t) { \ + api_err_exp(err, STR(obj_), api_typename(expected_t), api_typename((obj_).type)); \ + code; \ + } \ + } while (0) + +/// Checks that `obj_` has Dict type. Also allows empty Array in a Lua context. +#define VALIDATE_T_DICT(name, obj_, code) \ + do { \ + if ((obj_).type != kObjectTypeDictionary \ + && !(channel_id == LUA_INTERNAL_CALL \ + && (obj_).type == kObjectTypeArray \ + && (obj_).data.array.size == 0)) { \ + api_err_exp(err, name, api_typename(kObjectTypeDictionary), api_typename((obj_).type)); \ + code; \ + } \ + } while (0) + +/// Checks that actual_t is either the correct handle type or a type erased handle (integer) +#define VALIDATE_T_HANDLE(name, expected_t, actual_t, code) \ + do { \ + if (expected_t != actual_t && kObjectTypeInteger != actual_t) { \ + api_err_exp(err, name, api_typename(expected_t), api_typename(actual_t)); \ + code; \ + } \ + } while (0) + +#define VALIDATE_RANGE(cond, name, code) \ + do { \ + if (!(cond)) { \ + api_err_invalid(err, name, "out of range", 0, false); \ + code; \ + } \ + } while (0) + +#define VALIDATE_R(cond, name, code) \ + VALIDATE(cond, "Required: '%s'", name, code); + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "api/private/validate.h.generated.h" +#endif diff --git a/src/nvim/api/tabpage.c b/src/nvim/api/tabpage.c index 21eb326c3b..c854a22477 100644 --- a/src/nvim/api/tabpage.c +++ b/src/nvim/api/tabpage.c @@ -1,6 +1,3 @@ -// 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 <stdlib.h> @@ -9,6 +6,7 @@ #include "nvim/api/tabpage.h" #include "nvim/api/vim.h" #include "nvim/buffer_defs.h" +#include "nvim/func_attr.h" #include "nvim/globals.h" #include "nvim/memory.h" #include "nvim/window.h" diff --git a/src/nvim/api/tabpage.h b/src/nvim/api/tabpage.h index 2689cf6ae6..2f19845bd9 100644 --- a/src/nvim/api/tabpage.h +++ b/src/nvim/api/tabpage.h @@ -1,9 +1,7 @@ -#ifndef NVIM_API_TABPAGE_H -#define NVIM_API_TABPAGE_H +#pragma once -#include "nvim/api/private/defs.h" +#include "nvim/api/private/defs.h" // IWYU pragma: keep #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/tabpage.h.generated.h" #endif -#endif // NVIM_API_TABPAGE_H diff --git a/src/nvim/api/ui.c b/src/nvim/api/ui.c index e67607a7e4..836a68546c 100644 --- a/src/nvim/api/ui.c +++ b/src/nvim/api/ui.c @@ -1,10 +1,8 @@ -// 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 <msgpack/pack.h> #include <stdbool.h> +#include <stddef.h> #include <stdint.h> #include <stdlib.h> #include <string.h> @@ -12,25 +10,27 @@ #include "klib/kvec.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/helpers.h" +#include "nvim/api/private/validate.h" #include "nvim/api/ui.h" #include "nvim/autocmd.h" #include "nvim/channel.h" +#include "nvim/eval.h" #include "nvim/event/loop.h" #include "nvim/event/wstream.h" +#include "nvim/func_attr.h" #include "nvim/globals.h" #include "nvim/grid.h" #include "nvim/highlight.h" +#include "nvim/macros_defs.h" #include "nvim/main.h" -#include "nvim/map.h" +#include "nvim/map_defs.h" #include "nvim/mbyte.h" #include "nvim/memory.h" #include "nvim/msgpack_rpc/channel.h" -#include "nvim/msgpack_rpc/channel_defs.h" #include "nvim/msgpack_rpc/helpers.h" #include "nvim/option.h" -#include "nvim/types.h" +#include "nvim/types_defs.h" #include "nvim/ui.h" -#include "nvim/vim.h" #define BUF_POS(data) ((size_t)((data)->buf_wptr - (data)->buf)) @@ -72,6 +72,11 @@ static void mpack_uint(char **buf, uint32_t val) } } +static void mpack_bool(char **buf, bool val) +{ + mpack_w(buf, 0xc2 | (val ? 1 : 0)); +} + static void mpack_array(char **buf, uint32_t len) { if (len < 0x10) { @@ -110,23 +115,32 @@ void remote_ui_disconnect(uint64_t channel_id) } UIData *data = ui->data; kv_destroy(data->call_buf); - pmap_del(uint64_t)(&connected_uis, channel_id); + pmap_del(uint64_t)(&connected_uis, channel_id, NULL); ui_detach_impl(ui, channel_id); + + // Destroy `ui`. + XFREE_CLEAR(ui->term_name); xfree(ui); } -/// Wait until ui has connected on stdio channel. -void remote_ui_wait_for_attach(void) +/// Wait until ui has connected on stdio channel if only_stdio +/// is true, otherwise any channel. +void remote_ui_wait_for_attach(bool only_stdio) { - Channel *channel = find_channel(CHAN_STDIO); - if (!channel) { - // this function should only be called in --embed mode, stdio channel - // can be assumed. - abort(); - } + if (only_stdio) { + Channel *channel = find_channel(CHAN_STDIO); + if (!channel) { + // this function should only be called in --embed mode, stdio channel + // can be assumed. + abort(); + } - LOOP_PROCESS_EVENTS_UNTIL(&main_loop, channel->events, -1, - pmap_has(uint64_t)(&connected_uis, CHAN_STDIO)); + LOOP_PROCESS_EVENTS_UNTIL(&main_loop, channel->events, -1, + map_has(uint64_t, &connected_uis, CHAN_STDIO)); + } else { + LOOP_PROCESS_EVENTS_UNTIL(&main_loop, main_loop.events, -1, + ui_active()); + } } /// Activates UI events on the channel. @@ -148,7 +162,7 @@ void nvim_ui_attach(uint64_t channel_id, Integer width, Integer height, Dictiona Error *err) FUNC_API_SINCE(1) FUNC_API_REMOTE_ONLY { - if (pmap_has(uint64_t)(&connected_uis, channel_id)) { + if (map_has(uint64_t, &connected_uis, channel_id)) { api_set_error(err, kErrorTypeException, "UI already attached to channel: %" PRId64, channel_id); return; @@ -162,15 +176,9 @@ void nvim_ui_attach(uint64_t channel_id, Integer width, Integer height, Dictiona UI *ui = xcalloc(1, sizeof(UI)); ui->width = (int)width; ui->height = (int)height; - ui->pum_nlines = 0; - ui->pum_pos = false; - ui->pum_width = 0.0; - ui->pum_height = 0.0; ui->pum_row = -1.0; ui->pum_col = -1.0; ui->rgb = true; - ui->override = false; - CLEAR_FIELD(ui->ui_ext); for (size_t i = 0; i < options.size; i++) { @@ -202,6 +210,7 @@ void nvim_ui_attach(uint64_t channel_id, Integer width, Integer height, Dictiona data->flushed_events = false; data->ncalls_pos = NULL; data->ncalls = 0; + data->ncells_pending = 0; data->buf_wptr = data->buf; data->temp_buf = NULL; data->wildmenu_active = false; @@ -210,6 +219,8 @@ void nvim_ui_attach(uint64_t channel_id, Integer width, Integer height, Dictiona pmap_put(uint64_t)(&connected_uis, channel_id, ui); ui_attach_impl(ui, channel_id); + + may_trigger_vim_suspend_resume(false); } /// @deprecated @@ -226,12 +237,16 @@ void ui_attach(uint64_t channel_id, Integer width, Integer height, Boolean enabl void nvim_ui_set_focus(uint64_t channel_id, Boolean gained, Error *error) FUNC_API_SINCE(11) FUNC_API_REMOTE_ONLY { - if (!pmap_has(uint64_t)(&connected_uis, channel_id)) { + if (!map_has(uint64_t, &connected_uis, channel_id)) { api_set_error(error, kErrorTypeException, "UI not attached to channel: %" PRId64, channel_id); return; } + if (gained) { + may_trigger_vim_suspend_resume(false); + } + do_autocmd_focusgained((bool)gained); } @@ -244,7 +259,7 @@ void nvim_ui_set_focus(uint64_t channel_id, Boolean gained, Error *error) void nvim_ui_detach(uint64_t channel_id, Error *err) FUNC_API_SINCE(1) FUNC_API_REMOTE_ONLY { - if (!pmap_has(uint64_t)(&connected_uis, channel_id)) { + if (!map_has(uint64_t, &connected_uis, channel_id)) { api_set_error(err, kErrorTypeException, "UI not attached to channel: %" PRId64, channel_id); return; @@ -252,14 +267,15 @@ void nvim_ui_detach(uint64_t channel_id, Error *err) remote_ui_disconnect(channel_id); } -// TODO(bfredl): use me to detach a specifc ui from the server +// TODO(bfredl): use me to detach a specific ui from the server void remote_ui_stop(UI *ui) -{} +{ +} void nvim_ui_try_resize(uint64_t channel_id, Integer width, Integer height, Error *err) FUNC_API_SINCE(1) FUNC_API_REMOTE_ONLY { - if (!pmap_has(uint64_t)(&connected_uis, channel_id)) { + if (!map_has(uint64_t, &connected_uis, channel_id)) { api_set_error(err, kErrorTypeException, "UI not attached to channel: %" PRId64, channel_id); return; @@ -280,7 +296,7 @@ void nvim_ui_try_resize(uint64_t channel_id, Integer width, Integer height, Erro void nvim_ui_set_option(uint64_t channel_id, String name, Object value, Error *error) FUNC_API_SINCE(1) FUNC_API_REMOTE_ONLY { - if (!pmap_has(uint64_t)(&connected_uis, channel_id)) { + if (!map_has(uint64_t, &connected_uis, channel_id)) { api_set_error(error, kErrorTypeException, "UI not attached to channel: %" PRId64, channel_id); return; @@ -290,22 +306,20 @@ void nvim_ui_set_option(uint64_t channel_id, String name, Object value, Error *e ui_set_option(ui, false, name, value, error); } -static void ui_set_option(UI *ui, bool init, String name, Object value, Error *error) +static void ui_set_option(UI *ui, bool init, String name, Object value, Error *err) { if (strequal(name.data, "override")) { - if (value.type != kObjectTypeBoolean) { - api_set_error(error, kErrorTypeValidation, "override must be a Boolean"); + VALIDATE_T("override", kObjectTypeBoolean, value.type, { return; - } + }); ui->override = value.data.boolean; return; } if (strequal(name.data, "rgb")) { - if (value.type != kObjectTypeBoolean) { - api_set_error(error, kErrorTypeValidation, "rgb must be a Boolean"); + VALIDATE_T("rgb", kObjectTypeBoolean, value.type, { return; - } + }); ui->rgb = value.data.boolean; // A little drastic, but only takes effect for legacy uis. For linegrid UI // only changes metadata for nvim_list_uis(), no refresh needed. @@ -316,63 +330,53 @@ static void ui_set_option(UI *ui, bool init, String name, Object value, Error *e } if (strequal(name.data, "term_name")) { - if (value.type != kObjectTypeString) { - api_set_error(error, kErrorTypeValidation, "term_name must be a String"); + VALIDATE_T("term_name", kObjectTypeString, value.type, { return; - } + }); set_tty_option("term", string_to_cstr(value.data.string)); + ui->term_name = 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"); + VALIDATE_T("term_colors", kObjectTypeInteger, value.type, { 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); + ui->term_colors = (int)value.data.integer; 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"); + VALIDATE_T("stdin_fd", kObjectTypeInteger, value.type, { return; - } - - if (starting != NO_SCREEN) { - api_set_error(error, kErrorTypeValidation, - "stdin_fd can only be used with first attached ui"); + }); + VALIDATE_INT((value.data.integer >= 0), "stdin_fd", value.data.integer, { return; - } + }); + VALIDATE((starting == NO_SCREEN), "%s", "stdin_fd can only be used with first attached UI", { + return; + }); stdin_fd = (int)value.data.integer; return; } if (strequal(name.data, "stdin_tty")) { - if (value.type != kObjectTypeBoolean) { - api_set_error(error, kErrorTypeValidation, "stdin_tty must be a Boolean"); + VALIDATE_T("stdin_tty", kObjectTypeBoolean, value.type, { return; - } + }); stdin_isatty = value.data.boolean; + ui->stdin_tty = value.data.boolean; return; } if (strequal(name.data, "stdout_tty")) { - if (value.type != kObjectTypeBoolean) { - api_set_error(error, kErrorTypeValidation, "stdout_tty must be a Boolean"); + VALIDATE_T("stdout_tty", kObjectTypeBoolean, value.type, { return; - } + }); stdout_isatty = value.data.boolean; + ui->stdout_tty = value.data.boolean; return; } @@ -382,17 +386,15 @@ static void ui_set_option(UI *ui, bool init, String name, Object value, Error *e for (UIExtension i = 0; i < kUIExtCount; i++) { if (strequal(name.data, ui_ext_names[i]) || (i == kUIPopupmenu && is_popupmenu)) { - if (value.type != kObjectTypeBoolean) { - api_set_error(error, kErrorTypeValidation, "%s must be a Boolean", - name.data); + VALIDATE_EXP((value.type == kObjectTypeBoolean), name.data, "Boolean", + api_typename(value.type), { return; - } + }); bool boolval = value.data.boolean; if (!init && i == kUILinegrid && boolval != ui->ui_ext[i]) { - // There shouldn't be a reason for an UI to do this ever + // There shouldn't be a reason for a UI to do this ever // so explicitly don't support this. - api_set_error(error, kErrorTypeValidation, - "ext_linegrid option cannot be changed"); + api_set_error(err, kErrorTypeValidation, "ext_linegrid option cannot be changed"); } ui->ui_ext[i] = boolval; if (!init) { @@ -402,8 +404,7 @@ static void ui_set_option(UI *ui, bool init, String name, Object value, Error *e } } - api_set_error(error, kErrorTypeValidation, "No such UI option: %s", - name.data); + api_set_error(err, kErrorTypeValidation, "No such UI option: %s", name.data); } /// Tell Nvim to resize a grid. Triggers a grid_resize event with the requested @@ -420,7 +421,7 @@ void nvim_ui_try_resize_grid(uint64_t channel_id, Integer grid, Integer width, I Error *err) FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY { - if (!pmap_has(uint64_t)(&connected_uis, channel_id)) { + if (!map_has(uint64_t, &connected_uis, channel_id)) { api_set_error(err, kErrorTypeException, "UI not attached to channel: %" PRId64, channel_id); return; @@ -442,7 +443,7 @@ void nvim_ui_try_resize_grid(uint64_t channel_id, Integer grid, Integer width, I void nvim_ui_pum_set_height(uint64_t channel_id, Integer height, Error *err) FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY { - if (!pmap_has(uint64_t)(&connected_uis, channel_id)) { + if (!map_has(uint64_t, &connected_uis, channel_id)) { api_set_error(err, kErrorTypeException, "UI not attached to channel: %" PRId64, channel_id); return; @@ -483,7 +484,7 @@ void nvim_ui_pum_set_bounds(uint64_t channel_id, Float width, Float height, Floa Error *err) FUNC_API_SINCE(7) FUNC_API_REMOTE_ONLY { - if (!pmap_has(uint64_t)(&connected_uis, channel_id)) { + if (!map_has(uint64_t, &connected_uis, channel_id)) { api_set_error(err, kErrorTypeException, "UI not attached to channel: %" PRId64, channel_id); return; @@ -511,6 +512,33 @@ void nvim_ui_pum_set_bounds(uint64_t channel_id, Float width, Float height, Floa ui->pum_pos = true; } +/// Tells Nvim when a terminal event has occurred +/// +/// The following terminal events are supported: +/// +/// - "termresponse": The terminal sent an OSC or DCS response sequence to +/// Nvim. The payload is the received response. Sets +/// |v:termresponse| and fires |TermResponse|. +/// +/// @param channel_id +/// @param event Event name +/// @param payload Event payload +/// @param[out] err Error details, if any. +void nvim_ui_term_event(uint64_t channel_id, String event, Object value, Error *err) + FUNC_API_SINCE(12) FUNC_API_REMOTE_ONLY +{ + if (strequal("termresponse", event.data)) { + if (value.type != kObjectTypeString) { + api_set_error(err, kErrorTypeValidation, "termresponse must be a string"); + return; + } + + const String termresponse = value.data.string; + set_vim_var_string(VV_TERMRESPONSE, termresponse.data, (ptrdiff_t)termresponse.size); + apply_autocmds_group(EVENT_TERMRESPONSE, NULL, NULL, false, AUGROUP_ALL, NULL, NULL, &value); + } +} + static void flush_event(UIData *data) { if (data->cur_event) { @@ -644,6 +672,8 @@ void remote_ui_grid_resize(UI *ui, Integer grid, Integer width, Integer height) Array args = data->call_buf; if (ui->ui_ext[kUILinegrid]) { ADD_C(args, INTEGER_OBJ(grid)); + } else { + data->client_col = -1; // force cursor update } ADD_C(args, INTEGER_OBJ(width)); ADD_C(args, INTEGER_OBJ(height)); @@ -731,8 +761,8 @@ void remote_ui_hl_attr_define(UI *ui, Integer id, HlAttrs rgb_attrs, HlAttrs cte ADD_C(args, INTEGER_OBJ(id)); MAXSIZE_TEMP_DICT(rgb, HLATTRS_DICT_SIZE); MAXSIZE_TEMP_DICT(cterm, HLATTRS_DICT_SIZE); - hlattrs2dict(&rgb, rgb_attrs, true); - hlattrs2dict(&cterm, rgb_attrs, false); + hlattrs2dict(&rgb, NULL, rgb_attrs, true, false); + hlattrs2dict(&cterm, NULL, rgb_attrs, false, false); ADD_C(args, DICTIONARY_OBJ(rgb)); ADD_C(args, DICTIONARY_OBJ(cterm)); @@ -755,7 +785,7 @@ void remote_ui_highlight_set(UI *ui, int id) } data->hl_id = id; MAXSIZE_TEMP_DICT(dict, HLATTRS_DICT_SIZE); - hlattrs2dict(&dict, syn_attr2entry(id), ui->rgb); + hlattrs2dict(&dict, NULL, syn_attr2entry(id), ui->rgb, false); ADD_C(args, DICTIONARY_OBJ(dict)); push_call(ui, "highlight_set", args); } @@ -798,7 +828,7 @@ void remote_ui_put(UI *ui, const char *cell) UIData *data = ui->data; data->client_col++; Array args = data->call_buf; - ADD_C(args, STRING_OBJ(cstr_as_string((char *)cell))); + ADD_C(args, CSTR_AS_OBJ((char *)cell)); push_call(ui, "put", args); } @@ -812,7 +842,7 @@ void remote_ui_raw_line(UI *ui, Integer grid, Integer row, Integer startcol, Int data->ncalls++; char **buf = &data->buf_wptr; - mpack_array(buf, 4); + mpack_array(buf, 5); mpack_uint(buf, (uint32_t)grid); mpack_uint(buf, (uint32_t)row); mpack_uint(buf, (uint32_t)startcol); @@ -822,21 +852,24 @@ void remote_ui_raw_line(UI *ui, Integer grid, Integer row, Integer startcol, Int size_t ncells = (size_t)(endcol - startcol); int last_hl = -1; uint32_t nelem = 0; + bool was_space = false; for (size_t i = 0; i < ncells; i++) { repeat++; - if (i == ncells - 1 || attrs[i] != attrs[i + 1] - || strcmp(chunk[i], chunk[i + 1]) != 0) { - if (UI_BUF_SIZE - BUF_POS(data) < 2 * (1 + 2 + sizeof(schar_T) + 5 + 5)) { + if (i == ncells - 1 || attrs[i] != attrs[i + 1] || chunk[i] != chunk[i + 1]) { + if (UI_BUF_SIZE - BUF_POS(data) < 2 * (1 + 2 + sizeof(schar_T) + 5 + 5) + 1) { // 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); + + // We only ever set the wrap field on the final "grid_line" event for the line. + mpack_bool(buf, false); remote_ui_flush_buf(ui); prepare_call(ui, "grid_line"); data->ncalls++; - mpack_array(buf, 4); + mpack_array(buf, 5); mpack_uint(buf, (uint32_t)grid); mpack_uint(buf, (uint32_t)row); mpack_uint(buf, (uint32_t)startcol + (uint32_t)i - repeat + 1); @@ -847,31 +880,46 @@ void remote_ui_raw_line(UI *ui, Integer grid, Integer row, Integer startcol, Int uint32_t csize = (repeat > 1) ? 3 : ((attrs[i] != last_hl) ? 2 : 1); nelem++; mpack_array(buf, csize); - mpack_str(buf, (const char *)chunk[i]); + char sc_buf[MAX_SCHAR_SIZE]; + schar_get(sc_buf, chunk[i]); + mpack_str(buf, sc_buf); if (csize >= 2) { mpack_uint(buf, (uint32_t)attrs[i]); if (csize >= 3) { mpack_uint(buf, repeat); } } + data->ncells_pending += MIN(repeat, 2); last_hl = attrs[i]; repeat = 0; + was_space = chunk[i] == schar_from_ascii(' '); } } - if (endcol < clearcol) { + // If the last chunk was all spaces, add a clearing chunk even if there are + // no more cells to clear, so there is no ambiguity about what to clear. + if (endcol < clearcol || was_space) { nelem++; + data->ncells_pending += 1; mpack_array(buf, 3); mpack_str(buf, " "); mpack_uint(buf, (uint32_t)clearattr); mpack_uint(buf, (uint32_t)(clearcol - endcol)); } mpack_w2(&lenpos, nelem); + mpack_bool(buf, flags & kLineFlagWrap); + + if (data->ncells_pending > 500) { + // pass off cells to UI to let it start processing them + remote_ui_flush_buf(ui); + } } else { 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((char *)chunk[i]))) { + char sc_buf[MAX_SCHAR_SIZE]; + schar_get(sc_buf, chunk[i]); + remote_ui_put(ui, sc_buf); + if (utf_ambiguous_width(utf_ptr2char(sc_buf))) { data->client_col = -1; // force cursor update } } @@ -917,6 +965,8 @@ void remote_ui_flush_buf(UI *ui) // we have sent events to the client, but possibly not yet the final "flush" // event. data->flushed_events = true; + + data->ncells_pending = 0; } /// An intentional flush (vsync) when Nvim is finished redrawing the screen @@ -945,7 +995,7 @@ static Array translate_contents(UI *ui, Array contents, Arena *arena) int attr = (int)item.items[0].data.integer; if (attr) { Dictionary rgb_attrs = arena_dict(arena, HLATTRS_DICT_SIZE); - hlattrs2dict(&rgb_attrs, syn_attr2entry(attr), ui->rgb); + hlattrs2dict(&rgb_attrs, NULL, syn_attr2entry(attr), ui->rgb, false); ADD(new_item, DICTIONARY_OBJ(rgb_attrs)); } else { ADD(new_item, DICTIONARY_OBJ((Dictionary)ARRAY_DICT_INIT)); diff --git a/src/nvim/api/ui.h b/src/nvim/api/ui.h index b3fe0fa2bb..26a91d0dbc 100644 --- a/src/nvim/api/ui.h +++ b/src/nvim/api/ui.h @@ -1,14 +1,14 @@ -#ifndef NVIM_API_UI_H -#define NVIM_API_UI_H +#pragma once -#include <stdint.h> +#include <stdint.h> // IWYU pragma: keep -#include "nvim/api/private/defs.h" -#include "nvim/map.h" +#include "nvim/api/private/defs.h" // IWYU pragma: keep +#include "nvim/highlight_defs.h" // IWYU pragma: keep +#include "nvim/map_defs.h" +#include "nvim/types_defs.h" // IWYU pragma: keep #include "nvim/ui.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/ui.h.generated.h" -# include "ui_events_remote.h.generated.h" +# include "ui_events_remote.h.generated.h" // IWYU pragma: export #endif -#endif // NVIM_API_UI_H diff --git a/src/nvim/api/ui_events.in.h b/src/nvim/api/ui_events.in.h index a08e8dbfeb..bda0c72423 100644 --- a/src/nvim/api/ui_events.in.h +++ b/src/nvim/api/ui_events.in.h @@ -1,5 +1,4 @@ -#ifndef NVIM_API_UI_EVENTS_IN_H -#define NVIM_API_UI_EVENTS_IN_H +#pragma once // This file is not compiled, just parsed for definitions #ifdef INCLUDE_GENERATED_DECLARATIONS @@ -82,7 +81,7 @@ 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) +void grid_line(Integer grid, Integer row, Integer col_start, Array data, Boolean wrap) 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) @@ -114,7 +113,7 @@ void msg_set_pos(Integer grid, Integer row, Boolean scrolled, String sep_char) FUNC_API_SINCE(6) FUNC_API_COMPOSITOR_IMPL FUNC_API_CLIENT_IGNORE; void win_viewport(Integer grid, Window win, Integer topline, Integer botline, Integer curline, - Integer curcol, Integer line_count) + Integer curcol, Integer line_count, Integer scroll_delta) FUNC_API_SINCE(7) FUNC_API_CLIENT_IGNORE; void win_extmark(Integer grid, Window win, Integer ns_id, Integer mark_id, Integer row, Integer col) @@ -167,4 +166,6 @@ 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 + +void error_exit(Integer status) + FUNC_API_SINCE(12); diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index a53b30dd8a..d631b10af9 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -1,8 +1,6 @@ -// 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 <lauxlib.h> #include <limits.h> #include <stdbool.h> #include <stddef.h> @@ -11,25 +9,29 @@ #include <string.h> #include "klib/kvec.h" -#include "lauxlib.h" #include "nvim/api/buffer.h" #include "nvim/api/deprecated.h" +#include "nvim/api/keysets_defs.h" #include "nvim/api/private/converter.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/dispatch.h" #include "nvim/api/private/helpers.h" +#include "nvim/api/private/validate.h" #include "nvim/api/vim.h" -#include "nvim/ascii.h" +#include "nvim/ascii_defs.h" #include "nvim/autocmd.h" #include "nvim/buffer.h" #include "nvim/channel.h" #include "nvim/context.h" +#include "nvim/cursor.h" +#include "nvim/decoration.h" #include "nvim/drawscreen.h" #include "nvim/eval.h" #include "nvim/eval/typval.h" -#include "nvim/eval/typval_defs.h" #include "nvim/ex_docmd.h" #include "nvim/ex_eval.h" +#include "nvim/fold.h" +#include "nvim/func_attr.h" #include "nvim/getchar.h" #include "nvim/globals.h" #include "nvim/grid.h" @@ -38,7 +40,7 @@ #include "nvim/keycodes.h" #include "nvim/log.h" #include "nvim/lua/executor.h" -#include "nvim/macros.h" +#include "nvim/macros_defs.h" #include "nvim/mapping.h" #include "nvim/mark.h" #include "nvim/mbyte.h" @@ -47,24 +49,24 @@ #include "nvim/message.h" #include "nvim/move.h" #include "nvim/msgpack_rpc/channel.h" -#include "nvim/msgpack_rpc/channel_defs.h" #include "nvim/msgpack_rpc/unpacker.h" #include "nvim/ops.h" #include "nvim/option.h" +#include "nvim/option_vars.h" #include "nvim/optionstr.h" #include "nvim/os/input.h" -#include "nvim/os/os_defs.h" #include "nvim/os/process.h" #include "nvim/popupmenu.h" -#include "nvim/pos.h" +#include "nvim/pos_defs.h" #include "nvim/runtime.h" +#include "nvim/sign.h" #include "nvim/state.h" #include "nvim/statusline.h" #include "nvim/strings.h" #include "nvim/terminal.h" -#include "nvim/types.h" +#include "nvim/types_defs.h" #include "nvim/ui.h" -#include "nvim/vim.h" +#include "nvim/vim_defs.h" #include "nvim/window.h" #define LINE_BUFFER_MIN_SIZE 4096 @@ -73,44 +75,6 @@ # include "api/vim.c.generated.h" #endif -/// Gets a highlight definition by name. -/// -/// @param name Highlight group name -/// @param rgb Export RGB colors -/// @param[out] err Error details, if any -/// @return Highlight definition map -/// @see nvim_get_hl_by_id -Dictionary nvim_get_hl_by_name(String name, Boolean rgb, Arena *arena, Error *err) - FUNC_API_SINCE(3) -{ - Dictionary result = ARRAY_DICT_INIT; - int id = syn_name2id(name.data); - - if (id == 0) { - api_set_error(err, kErrorTypeException, "Invalid highlight name: %s", name.data); - return result; - } - return nvim_get_hl_by_id(id, rgb, arena, err); -} - -/// Gets a highlight definition by id. |hlID()| -/// @param hl_id Highlight id as returned by |hlID()| -/// @param rgb Export RGB colors -/// @param[out] err Error details, if any -/// @return Highlight definition map -/// @see nvim_get_hl_by_name -Dictionary nvim_get_hl_by_id(Integer hl_id, Boolean rgb, Arena *arena, Error *err) - FUNC_API_SINCE(3) -{ - Dictionary dic = ARRAY_DICT_INIT; - if (syn_get_final_id((int)hl_id) == 0) { - api_set_error(err, kErrorTypeException, "Invalid highlight id: %" PRId64, hl_id); - return dic; - } - int attrcode = syn_id2attr((int)hl_id); - return hl_get_attr_by_id(attrcode, rgb, arena, err); -} - /// Gets a highlight group by name /// /// similar to |hlID()|, but allocates a new ID if not present. @@ -120,12 +84,26 @@ Integer nvim_get_hl_id_by_name(String name) return syn_check_group(name.data, name.size); } -Dictionary nvim__get_hl_defs(Integer ns_id, Arena *arena, Error *err) +/// Gets all or specific highlight groups in a namespace. +/// +/// @note When the `link` attribute is defined in the highlight definition +/// map, other attributes will not be taking effect (see |:hi-link|). +/// +/// @param ns_id Get highlight groups for namespace ns_id |nvim_get_namespaces()|. +/// Use 0 to get global highlight groups |:highlight|. +/// @param opts Options dict: +/// - name: (string) Get a highlight definition by name. +/// - id: (integer) Get a highlight definition by id. +/// - link: (boolean, default true) Show linked group name instead of effective definition |:hi-link|. +/// - create: (boolean, default true) When highlight group doesn't exist create it. +/// +/// @param[out] err Error details, if any. +/// @return Highlight groups as a map from group name to a highlight definition map as in |nvim_set_hl()|, +/// or only a single highlight definition map if requested by name or id. +Dictionary nvim_get_hl(Integer ns_id, Dict(get_highlight) *opts, Arena *arena, Error *err) + FUNC_API_SINCE(11) { - if (ns_id == 0) { - return get_global_hl_defs(arena); - } - abort(); + return ns_get_hl_defs((NS)ns_id, opts, arena, err); } /// Sets a highlight group. @@ -140,8 +118,14 @@ Dictionary nvim__get_hl_defs(Integer ns_id, Arena *arena, Error *err) /// values of the Normal group. If the Normal group has not been defined, /// using these values results in an error. /// +/// +/// @note If `link` is used in combination with other attributes; only the +/// `link` will take effect (see |:hi-link|). +/// /// @param ns_id Namespace id for this highlight |nvim_create_namespace()|. /// Use 0 to set a highlight group globally |:highlight|. +/// Highlights from non-global namespaces are not active by default, use +/// |nvim_set_hl_ns()| or |nvim_win_set_hl_ns()| to activate them. /// @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. @@ -166,6 +150,7 @@ Dictionary nvim__get_hl_defs(Integer ns_id, Arena *arena, Error *err) /// - cterm: cterm attribute map, like |highlight-args|. If not set, /// cterm attributes will match those from the attribute map /// documented above. +/// - force: if true force update the highlight group when it exists. /// @param[out] err Error details, if any /// // TODO(bfredl): val should take update vs reset flag @@ -173,10 +158,9 @@ 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, name.size); - if (hl_id == 0) { - api_set_error(err, kErrorTypeException, "Invalid highlight name: %s", name.data); + VALIDATE_S((hl_id != 0), "highlight name", name.data, { return; - } + }); int link_id = -1; HlAttrs attrs = dict2hlattrs(val, true, &link_id, err); @@ -185,25 +169,47 @@ void nvim_set_hl(Integer ns_id, String name, Dict(highlight) *val, Error *err) } } -/// Set active namespace for highlights. This can be set for a single window, -/// see |nvim_win_set_hl_ns()|. +/// Gets the active highlight namespace. +/// +/// @param opts Optional parameters +/// - winid: (number) |window-ID| for retrieving a window's highlight +/// namespace. A value of -1 is returned when |nvim_win_set_hl_ns()| +/// has not been called for the window (or was called with a namespace +/// of -1). +/// @param[out] err Error details, if any +/// @return Namespace id, or -1 +Integer nvim_get_hl_ns(Dict(get_ns) *opts, Error *err) + FUNC_API_SINCE(12) +{ + if (HAS_KEY(opts, get_ns, winid)) { + win_T *win = find_window_by_handle(opts->winid, err); + if (!win) { + return 0; + } + return win->w_ns_hl; + } else { + return ns_hl_global; + } +} + +/// Set active namespace for highlights defined with |nvim_set_hl()|. This can be set for +/// a single window, see |nvim_win_set_hl_ns()|. /// /// @param ns_id the namespace to use /// @param[out] err Error details, if any void nvim_set_hl_ns(Integer ns_id, Error *err) FUNC_API_SINCE(10) { - if (ns_id < 0) { - api_set_error(err, kErrorTypeValidation, "no such namespace"); + VALIDATE_INT((ns_id >= 0), "namespace", ns_id, { return; - } + }); ns_hl_global = (NS)ns_id; hl_check_ns(); redraw_all_later(UPD_NOT_VALID); } -/// Set active namespace for highlights while redrawing. +/// Set active namespace for highlights defined with |nvim_set_hl()| while redrawing. /// /// This function meant to be called while redrawing, primarily from /// |nvim_set_decoration_provider()| on_win and on_line callbacks, which @@ -229,10 +235,11 @@ void nvim_set_hl_ns_fast(Integer ns_id, Error *err) /// nvim_feedkeys(). /// /// Example: -/// <pre>vim -/// :let key = nvim_replace_termcodes("<C-o>", v:true, v:false, v:true) -/// :call nvim_feedkeys(key, 'n', v:false) -/// </pre> +/// +/// ```vim +/// :let key = nvim_replace_termcodes("<C-o>", v:true, v:false, v:true) +/// :call nvim_feedkeys(key, 'n', v:false) +/// ``` /// /// @param keys to be typed /// @param mode behavior flags, see |feedkeys()| @@ -323,6 +330,7 @@ void nvim_feedkeys(String keys, String mode, Boolean escape_ks) Integer nvim_input(String keys) FUNC_API_SINCE(1) FUNC_API_FAST { + may_trigger_vim_suspend_resume(false); return (Integer)input_enqueue(keys); } @@ -352,6 +360,8 @@ void nvim_input_mouse(String button, String action, String modifier, Integer gri Integer col, Error *err) FUNC_API_SINCE(6) FUNC_API_FAST { + may_trigger_vim_suspend_resume(false); + if (button.data == NULL || action.data == NULL) { goto error; } @@ -403,11 +413,9 @@ void nvim_input_mouse(String button, String action, String modifier, Integer gri continue; } int mod = name_to_mod_mask(byte); - if (mod == 0) { - api_set_error(err, kErrorTypeValidation, - "invalid modifier %c", byte); + VALIDATE((mod != 0), "Invalid modifier: %c", byte, { return; - } + }); modmask |= mod; } @@ -448,7 +456,7 @@ String nvim_replace_termcodes(String str, Boolean from_part, Boolean do_lt, Bool } char *ptr = NULL; - replace_termcodes(str.data, str.size, &ptr, flags, NULL, CPO_TO_CPO_FLAGS); + replace_termcodes(str.data, str.size, &ptr, 0, flags, NULL, CPO_TO_CPO_FLAGS); return cstr_as_string(ptr); } @@ -500,15 +508,14 @@ Object nvim_notify(String msg, Integer log_level, Dictionary opts, Error *err) Integer nvim_strwidth(String text, Error *err) FUNC_API_SINCE(1) { - if (text.size > INT_MAX) { - api_set_error(err, kErrorTypeValidation, "String is too long"); + VALIDATE_S((text.size <= INT_MAX), "text length", "(too long)", { return 0; - } + }); return (Integer)mb_string2cells(text.data); } -/// Gets the paths contained in 'runtimepath'. +/// Gets the paths contained in |runtime-search-path|. /// /// @return List of paths ArrayOf(String) nvim_list_runtime_paths(Error *err) @@ -542,20 +549,23 @@ ArrayOf(String) nvim_get_runtime_file(String name, Boolean all, Error *err) int flags = DIP_DIRFILE | (all ? DIP_ALL : 0); - TRY_WRAP({ - try_start(); + TRY_WRAP(err, { do_in_runtimepath((name.size ? name.data : ""), flags, find_runtime_cb, &rv); - try_end(err); }); return rv; } -static void find_runtime_cb(char *fname, void *cookie) +static bool find_runtime_cb(int num_fnames, char **fnames, bool all, void *cookie) { Array *rv = (Array *)cookie; - if (fname != NULL) { - ADD(*rv, STRING_OBJ(cstr_to_string((char *)fname))); + for (int i = 0; i < num_fnames; i++) { + ADD(*rv, CSTR_TO_OBJ(fnames[i])); + if (!all) { + return true; + } } + + return num_fnames > 0; } String nvim__get_lib_dir(void) @@ -567,28 +577,24 @@ 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 opts 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"); - } - + VALIDATE(!opts->do_source || nlua_is_deferred_safe(), "%s", "'do_source' used in fast callback", + {}); if (ERROR_SET(err)) { return (Array)ARRAY_DICT_INIT; } - ArrayOf(String) res = runtime_get_named(is_lua, pat, all); + ArrayOf(String) res = runtime_get_named(opts->is_lua, pat, all); - if (source) { + if (opts->do_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); + (void)do_source(name.data, false, DOSO_NONE, NULL); } } @@ -602,10 +608,9 @@ ArrayOf(String) nvim__get_runtime(Array pat, Boolean all, Dict(runtime) *opts, E void nvim_set_current_dir(String dir, Error *err) FUNC_API_SINCE(1) { - if (dir.size >= MAXPATHL) { - api_set_error(err, kErrorTypeValidation, "Directory name is too long"); + VALIDATE_S((dir.size < MAXPATHL), "directory name", "(too long)", { return; - } + }); char string[MAXPATHL]; memcpy(string, dir.data, dir.size); @@ -639,7 +644,7 @@ String nvim_get_current_line(Error *err) /// @param[out] err Error details, if any void nvim_set_current_line(String line, Error *err) FUNC_API_SINCE(1) - FUNC_API_CHECK_TEXTLOCK + FUNC_API_TEXTLOCK_ALLOW_CMDWIN { buffer_set_line(curbuf->handle, curwin->w_cursor.lnum - 1, line, err); } @@ -649,7 +654,7 @@ void nvim_set_current_line(String line, Error *err) /// @param[out] err Error details, if any void nvim_del_current_line(Error *err) FUNC_API_SINCE(1) - FUNC_API_CHECK_TEXTLOCK + FUNC_API_TEXTLOCK_ALLOW_CMDWIN { buffer_del_line(curbuf->handle, curwin->w_cursor.lnum - 1, err); } @@ -664,16 +669,15 @@ Object nvim_get_var(String name, Error *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); + bool found = script_autoload(name.data, name.size, false) && !aborting(); + VALIDATE(found, "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); + VALIDATE((di != NULL), "Key not found: %s", name.data, { return (Object)OBJECT_INIT; - } + }); return vim_to_object(&di->di_tv); } @@ -738,15 +742,13 @@ void nvim_echo(Array chunks, Boolean history, Dict(echo_opts) *opts, Error *err) goto error; } - bool verbose = api_object_to_bool(opts->verbose, "verbose", false, err); - - if (verbose) { + if (opts->verbose) { verbose_enter(); } msg_multiattr(hl_msg, history ? "echomsg" : "echo", history); - if (verbose) { + if (opts->verbose) { verbose_leave(); verbose_stop(); // flush now } @@ -767,7 +769,7 @@ error: void nvim_out_write(String str) FUNC_API_SINCE(1) { - write_msg(str, false); + write_msg(str, false, false); } /// Writes a message to the Vim error buffer. Does not append "\n", the @@ -777,7 +779,7 @@ void nvim_out_write(String str) void nvim_err_write(String str) FUNC_API_SINCE(1) { - write_msg(str, true); + write_msg(str, true, false); } /// Writes a message to the Vim error buffer. Appends "\n", so the buffer is @@ -788,8 +790,7 @@ void nvim_err_write(String str) void nvim_err_writeln(String str) FUNC_API_SINCE(1) { - nvim_err_write(str); - nvim_err_write((String) { .data = "\n", .size = 1 }); + write_msg(str, true, true); } /// Gets the current list of buffer handles @@ -832,7 +833,7 @@ Buffer nvim_get_current_buf(void) /// @param[out] err Error details, if any void nvim_set_current_buf(Buffer buffer, Error *err) FUNC_API_SINCE(1) - FUNC_API_CHECK_TEXTLOCK + FUNC_API_TEXTLOCK { buf_T *buf = find_buffer_by_handle(buffer, err); @@ -887,7 +888,7 @@ Window nvim_get_current_win(void) /// @param[out] err Error details, if any void nvim_set_current_win(Window window, Error *err) FUNC_API_SINCE(1) - FUNC_API_CHECK_TEXTLOCK + FUNC_API_TEXTLOCK { win_T *win = find_window_by_handle(window, err); @@ -918,7 +919,7 @@ Buffer nvim_create_buf(Boolean listed, Boolean scratch, Error *err) FUNC_API_SINCE(6) { try_start(); - buf_T *buf = buflist_new(NULL, NULL, (linenr_T)0, + buf_T *buf = buflist_new(NULL, NULL, 0, BLN_NOOPT | BLN_NEW | (listed ? BLN_LISTED : 0)); try_end(err); if (buf == NULL) { @@ -936,14 +937,23 @@ Buffer nvim_create_buf(Boolean listed, Boolean scratch, Error *err) goto fail; } + // Set last_changedtick to avoid triggering a TextChanged autocommand right + // after it was added. + buf->b_last_changedtick = buf_get_changedtick(buf); + buf->b_last_changedtick_i = buf_get_changedtick(buf); + buf->b_last_changedtick_pum = buf_get_changedtick(buf); + + // Only strictly needed for scratch, but could just as well be consistent + // and do this now. buffer is created NOW, not when it latter first happen + // to reach a window or aucmd_prepbuf() .. + buf_copy_options(buf, BCO_ENTER | BCO_NOHELP); + if (scratch) { - aco_save_T aco; - aucmd_prepbuf(&aco, buf); - set_option_value("bufhidden", 0L, "hide", OPT_LOCAL); - set_option_value("buftype", 0L, "nofile", OPT_LOCAL); - set_option_value("swapfile", 0L, NULL, OPT_LOCAL); - set_option_value("modeline", 0L, NULL, OPT_LOCAL); // 'nomodeline' - aucmd_restbuf(&aco); + set_string_option_direct_in_buf(buf, "bufhidden", -1, "hide", OPT_LOCAL, 0); + set_string_option_direct_in_buf(buf, "buftype", -1, "nofile", OPT_LOCAL, 0); + assert(buf->b_ml.ml_mfp->mf_fd < 0); // ml_open() should not have opened swapfile already + buf->b_p_swf = false; + buf->b_p_ml = false; } return buf->b_fnum; @@ -970,7 +980,7 @@ fail: /// /// @param buffer the buffer to use (expected to be empty) /// @param opts Optional parameters. -/// - on_input: lua callback for input sent, i e keypresses in terminal +/// - on_input: Lua callback for input sent, i e keypresses in terminal /// mode. Note: keypresses are sent raw as they would be to the pty /// master end. For instance, a carriage return is sent /// as a "\r", not as a "\n". |textlock| applies. It is possible @@ -980,27 +990,31 @@ fail: /// @return Channel id, or 0 on error Integer nvim_open_term(Buffer buffer, DictionaryOf(LuaRef) opts, Error *err) FUNC_API_SINCE(7) + FUNC_API_TEXTLOCK_ALLOW_CMDWIN { buf_T *buf = find_buffer_by_handle(buffer, err); if (!buf) { return 0; } + if (cmdwin_type != 0 && buf == curbuf) { + api_set_error(err, kErrorTypeException, "%s", e_cmdwin); + return 0; + } + LuaRef cb = LUA_NOREF; for (size_t i = 0; i < opts.size; i++) { String k = opts.items[i].key; Object *v = &opts.items[i].value; if (strequal("on_input", k.data)) { - if (v->type != kObjectTypeLuaRef) { - api_set_error(err, kErrorTypeValidation, - "%s is not a function", "on_input"); + VALIDATE_T("on_input", kObjectTypeLuaRef, v->type, { return 0; - } + }); cb = v->data.luaref; v->data.luaref = LUA_NOREF; break; } else { - api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data); + VALIDATE_S(false, "'opts' key", k.data, {}); } } @@ -1016,9 +1030,12 @@ Integer nvim_open_term(Buffer buffer, DictionaryOf(LuaRef) opts, Error *err) topts.write_cb = term_write; topts.resize_cb = term_resize; topts.close_cb = term_close; - Terminal *term = terminal_open(buf, topts); - terminal_check_size(term); - chan->term = term; + channel_incref(chan); + terminal_open(&chan->term, buf, topts); + if (chan->term != NULL) { + terminal_check_size(chan->term); + } + channel_decref(chan); return (Integer)chan->id; } @@ -1040,7 +1057,7 @@ static void term_write(char *buf, size_t size, void *data) // NOLINT(readabilit static void term_resize(uint16_t width, uint16_t height, void *data) { - // TODO(bfredl): lua callback + // TODO(bfredl): Lua callback } static void term_close(void *data) @@ -1075,9 +1092,7 @@ void nvim_chan_send(Integer chan, String data, Error *err) channel_send((uint64_t)chan, data.data, data.size, false, &error); - if (error) { - api_set_error(err, kErrorTypeValidation, "%s", error); - } + VALIDATE(!error, "%s", error, {}); } /// Gets the current list of tabpage handles. @@ -1117,7 +1132,7 @@ Tabpage nvim_get_current_tabpage(void) /// @param[out] err Error details, if any void nvim_set_current_tabpage(Tabpage tabpage, Error *err) FUNC_API_SINCE(1) - FUNC_API_CHECK_TEXTLOCK + FUNC_API_TEXTLOCK { tabpage_T *tp = find_tab_by_handle(tabpage, err); @@ -1159,15 +1174,14 @@ void nvim_set_current_tabpage(Tabpage tabpage, Error *err) /// - false: Client must cancel the paste. Boolean nvim_paste(String data, Boolean crlf, Integer phase, Error *err) FUNC_API_SINCE(6) - FUNC_API_CHECK_TEXTLOCK + FUNC_API_TEXTLOCK_ALLOW_CMDWIN { static bool draining = false; bool cancel = false; - if (phase < -1 || phase > 3) { - api_set_error(err, kErrorTypeValidation, "Invalid phase: %" PRId64, phase); + VALIDATE_INT((phase >= -1 && phase <= 3), "phase", phase, { return false; - } + }); Array args = ARRAY_DICT_INIT; Object rv = OBJECT_INIT; if (phase == -1 || phase == 1) { // Start of paste-stream. @@ -1231,23 +1245,20 @@ theend: /// @param[out] err Error details, if any void nvim_put(ArrayOf(String) lines, String type, Boolean after, Boolean follow, Error *err) FUNC_API_SINCE(6) - FUNC_API_CHECK_TEXTLOCK + FUNC_API_TEXTLOCK_ALLOW_CMDWIN { yankreg_T *reg = xcalloc(1, sizeof(yankreg_T)); - if (!prepare_yankreg_from_object(reg, type, lines.size)) { - api_set_error(err, kErrorTypeValidation, "Invalid type: '%s'", type.data); + VALIDATE_S((prepare_yankreg_from_object(reg, type, lines.size)), "type", type.data, { goto cleanup; - } + }); if (lines.size == 0) { goto cleanup; // Nothing to do. } for (size_t i = 0; i < lines.size; i++) { - if (lines.items[i].type != kObjectTypeString) { - api_set_error(err, kErrorTypeValidation, - "Invalid lines (expected array of strings)"); + VALIDATE_T("line", kObjectTypeString, lines.items[i].type, { goto cleanup; - } + }); String line = lines.items[i].data.string; reg->y_array[i] = xmemdupz(line.data, line.size); memchrsub(reg->y_array[i], NUL, NL, line.size); @@ -1255,14 +1266,12 @@ void nvim_put(ArrayOf(String) lines, String type, Boolean after, Boolean follow, finish_yankreg_from_object(reg, false); - TRY_WRAP({ - try_start(); + TRY_WRAP(err, { bool VIsual_was_active = VIsual_active; msg_silent++; // Avoid "N more lines" message. do_put(0, reg, after ? FORWARD : BACKWARD, 1, follow ? PUT_CURSEND : 0); msg_silent--; VIsual_active = VIsual_was_active; - try_end(err); }); cleanup: @@ -1291,9 +1300,9 @@ void nvim_subscribe(uint64_t channel_id, String event) void nvim_unsubscribe(uint64_t channel_id, String event) FUNC_API_SINCE(1) FUNC_API_REMOTE_ONLY { - size_t length = (event.size < METHOD_MAXLEN ? - event.size : - METHOD_MAXLEN); + size_t length = (event.size < METHOD_MAXLEN + ? event.size + : METHOD_MAXLEN); char e[METHOD_MAXLEN + 1]; memcpy(e, event.data, length); e[length] = NUL; @@ -1304,10 +1313,11 @@ void nvim_unsubscribe(uint64_t channel_id, String event) /// "#rrggbb" hexadecimal string. /// /// Example: -/// <pre>vim -/// :echo nvim_get_color_by_name("Pink") -/// :echo nvim_get_color_by_name("#cbcbcb") -/// </pre> +/// +/// ```vim +/// :echo nvim_get_color_by_name("Pink") +/// :echo nvim_get_color_by_name("#cbcbcb") +/// ``` /// /// @param name Color name or "#rrggbb" string /// @return 24-bit RGB value, or -1 for invalid argument. @@ -1348,11 +1358,8 @@ Dictionary nvim_get_context(Dict(context) *opts, Error *err) FUNC_API_SINCE(6) { Array types = ARRAY_DICT_INIT; - if (opts->types.type == kObjectTypeArray) { - types = opts->types.data.array; - } else if (opts->types.type != kObjectTypeNil) { - api_set_error(err, kErrorTypeValidation, "invalid value for key: types"); - return (Dictionary)ARRAY_DICT_INIT; + if (HAS_KEY(opts, context, types)) { + types = opts->types; } int int_types = types.size > 0 ? 0 : kCtxAll; @@ -1373,8 +1380,9 @@ Dictionary nvim_get_context(Dict(context) *opts, Error *err) } else if (strequal(s, "funcs")) { int_types |= kCtxFuncs; } else { - api_set_error(err, kErrorTypeValidation, "unexpected type: %s", s); - return (Dictionary)ARRAY_DICT_INIT; + VALIDATE_S(false, "type", s, { + return (Dictionary)ARRAY_DICT_INIT; + }); } } } @@ -1390,7 +1398,7 @@ Dictionary nvim_get_context(Dict(context) *opts, Error *err) /// Sets the current editor state from the given |context| map. /// /// @param dict |Context| map. -Object nvim_load_context(Dictionary dict) +Object nvim_load_context(Dictionary dict, Error *err) FUNC_API_SINCE(6) { Context ctx = CONTEXT_INIT; @@ -1398,8 +1406,8 @@ Object nvim_load_context(Dictionary dict) int save_did_emsg = did_emsg; did_emsg = false; - ctx_from_dict(dict, &ctx); - if (!did_emsg) { + ctx_from_dict(dict, &ctx, err); + if (!ERROR_SET(err)) { ctx_restore(&ctx, kCtxAll); } @@ -1421,7 +1429,7 @@ Dictionary nvim_get_mode(void) get_mode(modestr); bool blocked = input_blocking(); - PUT(rv, "mode", STRING_OBJ(cstr_to_string(modestr))); + PUT(rv, "mode", CSTR_TO_OBJ(modestr)); PUT(rv, "blocking", BOOLEAN_OBJ(blocked)); return rv; @@ -1446,29 +1454,31 @@ ArrayOf(Dictionary) nvim_get_keymap(String mode) /// Empty {rhs} is |<Nop>|. |keycodes| are replaced as usual. /// /// Example: -/// <pre>vim -/// call nvim_set_keymap('n', ' <NL>', '', {'nowait': v:true}) -/// </pre> +/// +/// ```vim +/// call nvim_set_keymap('n', ' <NL>', '', {'nowait': v:true}) +/// ``` /// /// is equivalent to: -/// <pre>vim -/// nmap <nowait> <Space><NL> <Nop> -/// </pre> +/// +/// ```vim +/// nmap <nowait> <Space><NL> <Nop> +/// ``` /// /// @param channel_id /// @param mode Mode short-name (map command prefix: "n", "i", "v", "x", …) /// or "!" for |:map!|, or empty string for |:map|. +/// "ia", "ca" or "!a" for abbreviation in Insert mode, Cmdline mode, or both, respectively /// @param lhs Left-hand-side |{lhs}| of the mapping. /// @param rhs Right-hand-side |{rhs}| of the mapping. -/// @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. -/// When "expr" is true, "replace_keycodes" (boolean) can be used to replace keycodes -/// in the resulting string (see |nvim_replace_termcodes()|), and a Lua callback -/// returning `nil` is equivalent to returning an empty string. +/// @param opts Optional parameters map: Accepts all |:map-arguments| as keys except |<buffer>|, +/// values are booleans (default false). Also: +/// - "noremap" disables |recursive_mapping|, like |:noremap| +/// - "desc" human-readable description. +/// - "callback" Lua function called in place of {rhs}. +/// - "replace_keycodes" (boolean) When "expr" is true, replace keycodes in the +/// resulting string (see |nvim_replace_termcodes()|). Returning nil from the Lua +/// "callback" is equivalent to returning an empty string. /// @param[out] err Error details, if any. void nvim_set_keymap(uint64_t channel_id, String mode, String lhs, String rhs, Dict(keymap) *opts, Error *err) @@ -1527,7 +1537,10 @@ Array nvim_get_api_info(uint64_t channel_id, Arena *arena) /// - "commit" hash or similar identifier of commit /// @param type Must be one of the following values. Client libraries should /// default to "remote" unless overridden by the user. -/// - "remote" remote client connected to Nvim. +/// - "remote" remote client connected "Nvim flavored" MessagePack-RPC (responses +/// must be in reverse order of requests). |msgpack-rpc| +/// - "msgpack-rpc" remote client connected to Nvim via fully MessagePack-RPC +/// compliant protocol. /// - "ui" gui frontend /// - "embedder" application using Nvim as a component (for example, /// IDE/editor implementing a vim mode). @@ -1651,34 +1664,20 @@ Array nvim_call_atomic(uint64_t channel_id, Array calls, Arena *arena, Error *er size_t i; // also used for freeing the variables for (i = 0; i < calls.size; i++) { - if (calls.items[i].type != kObjectTypeArray) { - api_set_error(err, - kErrorTypeValidation, - "Items in calls array must be arrays"); + VALIDATE_T("'calls' item", kObjectTypeArray, calls.items[i].type, { goto theend; - } + }); Array call = calls.items[i].data.array; - if (call.size != 2) { - api_set_error(err, - kErrorTypeValidation, - "Items in calls array must be arrays of size 2"); + VALIDATE_EXP((call.size == 2), "'calls' item", "2-item Array", NULL, { goto theend; - } - - if (call.items[0].type != kObjectTypeString) { - api_set_error(err, - kErrorTypeValidation, - "Name must be String"); + }); + VALIDATE_T("name", kObjectTypeString, call.items[0].type, { goto theend; - } + }); String name = call.items[0].data.string; - - if (call.items[1].type != kObjectTypeArray) { - api_set_error(err, - kErrorTypeValidation, - "Args must be Array"); + VALIDATE_T("call args", kObjectTypeArray, call.items[1].type, { goto theend; - } + }); Array args = call.items[1].data.array; MsgpackRpcRequestHandler handler = @@ -1726,34 +1725,44 @@ theend: /// /// @param message Message to write /// @param to_err true: message is an error (uses `emsg` instead of `msg`) -static void write_msg(String message, bool to_err) +/// @param writeln Append a trailing newline +static void write_msg(String message, bool to_err, bool writeln) { static StringBuilder out_line_buf = KV_INITIAL_VALUE; static StringBuilder err_line_buf = KV_INITIAL_VALUE; + StringBuilder *line_buf = to_err ? &err_line_buf : &out_line_buf; -#define PUSH_CHAR(i, line_buf, msg) \ - if (kv_max(line_buf) == 0) { \ - kv_resize(line_buf, LINE_BUFFER_MIN_SIZE); \ +#define PUSH_CHAR(c) \ + if (kv_max(*line_buf) == 0) { \ + kv_resize(*line_buf, LINE_BUFFER_MIN_SIZE); \ } \ - if (message.data[i] == NL) { \ - kv_push(line_buf, NUL); \ - msg(line_buf.items); \ - kv_drop(line_buf, kv_size(line_buf)); \ - kv_resize(line_buf, LINE_BUFFER_MIN_SIZE); \ - continue; \ - } \ - kv_push(line_buf, message.data[i]); + if (c == NL) { \ + kv_push(*line_buf, NUL); \ + if (to_err) { \ + emsg(line_buf->items); \ + } else { \ + msg(line_buf->items, 0); \ + } \ + if (msg_silent == 0) { \ + msg_didout = true; \ + } \ + kv_drop(*line_buf, kv_size(*line_buf)); \ + kv_resize(*line_buf, LINE_BUFFER_MIN_SIZE); \ + } else if (c == NUL) { \ + kv_push(*line_buf, NL); \ + } else { \ + kv_push(*line_buf, c); \ + } no_wait_return++; for (uint32_t i = 0; i < message.size; i++) { if (got_int) { break; } - if (to_err) { - PUSH_CHAR(i, err_line_buf, emsg); - } else { - PUSH_CHAR(i, out_line_buf, msg); - } + PUSH_CHAR(message.data[i]); + } + if (writeln) { + PUSH_CHAR(NL); } no_wait_return--; msg_end(); @@ -1850,10 +1859,9 @@ Array nvim_get_proc_children(Integer pid, Error *err) Array rvobj = ARRAY_DICT_INIT; int *proc_list = NULL; - if (pid <= 0 || pid > INT_MAX) { - api_set_error(err, kErrorTypeException, "Invalid pid: %" PRId64, pid); + VALIDATE_INT((pid > 0 && pid <= INT_MAX), "pid", pid, { goto end; - } + }); size_t proc_count; int rv = os_proc_children((int)pid, &proc_list, &proc_count); @@ -1892,10 +1900,10 @@ Object nvim_get_proc(Integer pid, Error *err) rvobj.data.dictionary = (Dictionary)ARRAY_DICT_INIT; rvobj.type = kObjectTypeDictionary; - if (pid <= 0 || pid > INT_MAX) { - api_set_error(err, kErrorTypeException, "Invalid pid: %" PRId64, pid); + VALIDATE_INT((pid > 0 && pid <= INT_MAX), "pid", pid, { return NIL; - } + }); + #ifdef MSWIN rvobj.data.dictionary = os_proc_info((int)pid); if (rvobj.data.dictionary.size == 0) { // Process not found. @@ -1937,10 +1945,9 @@ void nvim_select_popupmenu_item(Integer item, Boolean insert, Boolean finish, Di Error *err) FUNC_API_SINCE(6) { - if (opts.size > 0) { - api_set_error(err, kErrorTypeValidation, "opts dict isn't empty"); + VALIDATE((opts.size == 0), "%s", "opts dict isn't empty", { return; - } + }); if (finish) { insert = true; @@ -1961,13 +1968,10 @@ Array nvim__inspect_cell(Integer grid, Integer row, Integer col, Arena *arena, E g = &pum_grid; } else if (grid > 1) { win_T *wp = get_win_by_grid_handle((handle_T)grid); - if (wp != NULL && wp->w_grid_alloc.chars != NULL) { - g = &wp->w_grid_alloc; - } else { - api_set_error(err, kErrorTypeValidation, - "No grid with the given handle"); + VALIDATE_INT((wp != NULL && wp->w_grid_alloc.chars != NULL), "grid handle", grid, { return ret; - } + }); + g = &wp->w_grid_alloc; } if (row < 0 || row >= g->rows @@ -1976,7 +1980,9 @@ Array nvim__inspect_cell(Integer grid, Integer row, Integer col, Arena *arena, E } ret = arena_array(arena, 3); size_t off = g->line_offset[(size_t)row] + (size_t)col; - ADD_C(ret, STRING_OBJ(cstr_as_string((char *)g->chars[off]))); + char *sc_buf = arena_alloc(arena, MAX_SCHAR_SIZE, false); + schar_get(sc_buf, g->chars[off]); + ADD_C(ret, CSTR_AS_OBJ(sc_buf)); int attr = g->attrs[off]; ADD_C(ret, DICTIONARY_OBJ(hl_get_attr_by_id(attr, true, arena, err))); // will not work first time @@ -1992,6 +1998,14 @@ void nvim__screenshot(String path) ui_call_screenshot(path); } +/// For testing. The condition in schar_cache_clear_if_full is hard to +/// reach, so this function can be used to force a cache clear in a test. +void nvim__invalidate_glyph_cache(void) +{ + schar_cache_clear(); + must_redraw = UPD_CLEAR; +} + Object nvim__unpack(String str, Error *err) FUNC_API_FAST { @@ -2000,7 +2014,7 @@ Object nvim__unpack(String str, Error *err) /// Deletes an uppercase/file named mark. See |mark-motions|. /// -/// @note fails with error if a lowercase or buffer local named mark is used. +/// @note Lowercase name (or other buffer-local mark) is an error. /// @param name Mark name /// @return true if the mark was deleted, else false. /// @see |nvim_buf_del_mark()| @@ -2009,29 +2023,26 @@ Boolean nvim_del_mark(String name, Error *err) FUNC_API_SINCE(8) { bool res = false; - if (name.size != 1) { - api_set_error(err, kErrorTypeValidation, - "Mark name must be a single character"); + VALIDATE_S((name.size == 1), "mark name (must be a single char)", name.data, { return res; - } + }); // Only allow file/uppercase marks // TODO(muniter): Refactor this ASCII_ISUPPER macro to a proper function - if (ASCII_ISUPPER(*name.data) || ascii_isdigit(*name.data)) { - res = set_mark(NULL, name, 0, 0, err); - } else { - api_set_error(err, kErrorTypeValidation, - "Only file/uppercase marks allowed, invalid mark name: '%c'", - *name.data); - } + VALIDATE_S((ASCII_ISUPPER(*name.data) || ascii_isdigit(*name.data)), + "mark name (must be file/uppercase)", name.data, { + return res; + }); + res = set_mark(NULL, name, 0, 0, err); return res; } -/// Return a tuple (row, col, buffer, buffername) representing the position of -/// the uppercase/file named mark. See |mark-motions|. +/// Returns a `(row, col, buffer, buffername)` tuple representing the position +/// of the uppercase/file named mark. "End of line" column position is returned +/// as |v:maxcol| (big number). See |mark-motions|. /// /// Marks are (1,0)-indexed. |api-indexing| /// -/// @note fails with error if a lowercase or buffer local named mark is used. +/// @note Lowercase name (or other buffer-local mark) is an error. /// @param name Mark name /// @param opts Optional parameters. Reserved for future use. /// @return 4-tuple (row, col, buffer, buffername), (0, 0, 0, '') if the mark is @@ -2043,16 +2054,13 @@ Array nvim_get_mark(String name, Dictionary opts, Error *err) { Array rv = ARRAY_DICT_INIT; - if (name.size != 1) { - api_set_error(err, kErrorTypeValidation, - "Mark name must be a single character"); + VALIDATE_S((name.size == 1), "mark name (must be a single char)", name.data, { return rv; - } else if (!(ASCII_ISUPPER(*name.data) || ascii_isdigit(*name.data))) { - api_set_error(err, kErrorTypeValidation, - "Only file/uppercase marks allowed, invalid mark name: '%c'", - *name.data); + }); + VALIDATE_S((ASCII_ISUPPER(*name.data) || ascii_isdigit(*name.data)), + "mark name (must be file/uppercase)", name.data, { return rv; - } + }); xfmark_T *mark = mark_get_global(false, *name.data); // false avoids loading the mark buffer pos_T pos = mark->fmark.mark; @@ -2092,7 +2100,7 @@ Array nvim_get_mark(String name, Dictionary opts, Error *err) ADD(rv, INTEGER_OBJ(row)); ADD(rv, INTEGER_OBJ(col)); ADD(rv, INTEGER_OBJ(bufnr)); - ADD(rv, STRING_OBJ(cstr_to_string(filename))); + ADD(rv, CSTR_TO_OBJ(filename)); if (allocated) { xfree(filename); @@ -2113,6 +2121,7 @@ Array nvim_get_mark(String name, Dictionary opts, Error *err) /// - use_winbar: (boolean) Evaluate winbar instead of statusline. /// - use_tabline: (boolean) Evaluate tabline instead of statusline. When true, {winid} /// is ignored. Mutually exclusive with {use_winbar}. +/// - use_statuscol_lnum: (number) Evaluate statuscolumn for this line number instead of statusline. /// /// @param[out] err Error details, if any. /// @return Dictionary containing statusline information, with these keys: @@ -2130,106 +2139,117 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * int maxwidth; int fillchar = 0; + int statuscol_lnum = 0; Window window = 0; - bool use_winbar = false; - bool use_tabline = false; - bool highlights = false; if (str.size < 2 || memcmp(str.data, "%!", 2) != 0) { const char *const errmsg = check_stl_option(str.data); - if (errmsg) { - api_set_error(err, kErrorTypeValidation, "%s", errmsg); + VALIDATE(!errmsg, "%s", errmsg, { return result; - } + }); } - if (HAS_KEY(opts->winid)) { - if (opts->winid.type != kObjectTypeInteger) { - api_set_error(err, kErrorTypeValidation, "winid must be an integer"); - return result; - } - - window = (Window)opts->winid.data.integer; + if (HAS_KEY(opts, eval_statusline, winid)) { + window = opts->winid; } - if (HAS_KEY(opts->fillchar)) { - 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"); + if (HAS_KEY(opts, eval_statusline, fillchar)) { + VALIDATE_EXP((*opts->fillchar.data != 0 + && ((size_t)utf_ptr2len(opts->fillchar.data) == opts->fillchar.size)), + "fillchar", "single character", NULL, { return result; - } - fillchar = utf_ptr2char(opts->fillchar.data.string.data); + }); + fillchar = utf_ptr2char(opts->fillchar.data); } - if (HAS_KEY(opts->highlights)) { - highlights = api_object_to_bool(opts->highlights, "highlights", false, err); - if (ERROR_SET(err)) { - return result; - } - } - if (HAS_KEY(opts->use_winbar)) { - use_winbar = api_object_to_bool(opts->use_winbar, "use_winbar", false, err); + int use_bools = (int)opts->use_winbar + (int)opts->use_tabline; - if (ERROR_SET(err)) { - return result; - } + win_T *wp = opts->use_tabline ? curwin : find_window_by_handle(window, err); + if (wp == NULL) { + api_set_error(err, kErrorTypeException, "unknown winid %d", window); + return result; } - if (HAS_KEY(opts->use_tabline)) { - use_tabline = api_object_to_bool(opts->use_tabline, "use_tabline", false, err); - if (ERROR_SET(err)) { + if (HAS_KEY(opts, eval_statusline, use_statuscol_lnum)) { + statuscol_lnum = (int)opts->use_statuscol_lnum; + VALIDATE_RANGE(statuscol_lnum > 0 && statuscol_lnum <= wp->w_buffer->b_ml.ml_line_count, + "use_statuscol_lnum", { return result; - } + }); + use_bools++; } - if (use_winbar && use_tabline) { - api_set_error(err, kErrorTypeValidation, "use_winbar and use_tabline are mutually exclusive"); + VALIDATE(use_bools <= 1, "%s", + "Can only use one of 'use_winbar', 'use_tabline' and 'use_statuscol_lnum'", { return result; - } + }); - win_T *wp, *ewp; + int stc_hl_id = 0; + statuscol_T statuscol = { 0 }; + SignTextAttrs sattrs[SIGN_SHOW_MAX] = { 0 }; - if (use_tabline) { - wp = NULL; - ewp = curwin; + if (opts->use_tabline) { 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) { - if (use_winbar) { + if (opts->use_winbar) { fillchar = wp->w_p_fcs_chars.wbr; } else { int attr; fillchar = fillchar_status(&attr, wp); } } - } + if (statuscol_lnum) { + int line_id = 0; + int cul_id = 0; + int num_id = 0; + linenr_T lnum = statuscol_lnum; + wp->w_scwidth = win_signcol_count(wp); + decor_redraw_signs(wp, wp->w_buffer, lnum - 1, sattrs, &line_id, &cul_id, &num_id); + + statuscol.sattrs = sattrs; + statuscol.foldinfo = fold_info(wp, lnum); + wp->w_cursorline = win_cursorline_standout(wp) ? wp->w_cursor.lnum : 0; + + if (wp->w_p_cul) { + if (statuscol.foldinfo.fi_level != 0 && statuscol.foldinfo.fi_lines > 0) { + wp->w_cursorline = statuscol.foldinfo.fi_lnum; + } + statuscol.use_cul = lnum == wp->w_cursorline && (wp->w_p_culopt_flags & CULOPT_NBR); + } - if (HAS_KEY(opts->maxwidth)) { - if (opts->maxwidth.type != kObjectTypeInteger) { - api_set_error(err, kErrorTypeValidation, "maxwidth must be an integer"); - return result; + statuscol.sign_cul_id = statuscol.use_cul ? cul_id : 0; + if (num_id) { + stc_hl_id = num_id; + } else if (statuscol.use_cul) { + stc_hl_id = HLF_CLN + 1; + } else if (wp->w_p_rnu) { + stc_hl_id = (lnum < wp->w_cursor.lnum ? HLF_LNA : HLF_LNB) + 1; + } else { + stc_hl_id = HLF_N + 1; + } + + set_vim_var_nr(VV_LNUM, lnum); + set_vim_var_nr(VV_RELNUM, labs(get_cursor_rel_lnum(wp, lnum))); + set_vim_var_nr(VV_VIRTNUM, 0); } + } - maxwidth = (int)opts->maxwidth.data.integer; + if (HAS_KEY(opts, eval_statusline, maxwidth)) { + maxwidth = (int)opts->maxwidth; } else { - maxwidth = (use_tabline || (!use_winbar && global_stl_height() > 0)) ? Columns : wp->w_width; + maxwidth = statuscol_lnum ? win_col_off(wp) + : (opts->use_tabline + || (!opts->use_winbar + && global_stl_height() > 0)) ? Columns : wp->w_width; } char buf[MAXPATHL]; stl_hlrec_t *hltab; - stl_hlrec_t **hltab_ptr = highlights ? &hltab : NULL; // Temporarily reset 'cursorbind' to prevent side effects from moving the cursor away and back. - int p_crb_save = ewp->w_p_crb; - ewp->w_p_crb = false; + int p_crb_save = wp->w_p_crb; + wp->w_p_crb = false; - int width = build_stl_str_hl(ewp, + int width = build_stl_str_hl(wp, buf, sizeof(buf), str.data, @@ -2237,25 +2257,25 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * 0, fillchar, maxwidth, - hltab_ptr, + opts->highlights ? &hltab : NULL, NULL, - NULL); + statuscol_lnum ? &statuscol : NULL); PUT(result, "width", INTEGER_OBJ(width)); // Restore original value of 'cursorbind' - ewp->w_p_crb = p_crb_save; + wp->w_p_crb = p_crb_save; - if (highlights) { + if (opts->highlights) { Array hl_values = ARRAY_DICT_INIT; const char *grpname; - char user_group[6]; + char user_group[15]; // strlen("User") + strlen("2147483647") + NUL // If first character doesn't have a defined highlight, // add the default highlight at the beginning of the highlight list if (hltab->start == NULL || (hltab->start - buf) != 0) { Dictionary hl_info = ARRAY_DICT_INIT; - grpname = get_default_stl_hl(wp, use_winbar); + grpname = get_default_stl_hl(opts->use_tabline ? NULL : wp, opts->use_winbar, stc_hl_id); PUT(hl_info, "start", INTEGER_OBJ(0)); PUT(hl_info, "group", CSTR_TO_OBJ(grpname)); @@ -2266,10 +2286,10 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * for (stl_hlrec_t *sp = hltab; sp->start != NULL; sp++) { Dictionary hl_info = ARRAY_DICT_INIT; - PUT(hl_info, "start", INTEGER_OBJ((char *)sp->start - buf)); + PUT(hl_info, "start", INTEGER_OBJ(sp->start - buf)); if (sp->userhl == 0) { - grpname = get_default_stl_hl(wp, use_winbar); + grpname = get_default_stl_hl(opts->use_tabline ? NULL : wp, opts->use_winbar, stc_hl_id); } else if (sp->userhl < 0) { grpname = syn_id2name(-sp->userhl); } else { @@ -2281,7 +2301,7 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * } PUT(result, "highlights", ARRAY_OBJ(hl_values)); } - PUT(result, "str", CSTR_TO_OBJ((char *)buf)); + PUT(result, "str", CSTR_TO_OBJ(buf)); return result; } diff --git a/src/nvim/api/vim.h b/src/nvim/api/vim.h index de56c67665..b620158751 100644 --- a/src/nvim/api/vim.h +++ b/src/nvim/api/vim.h @@ -1,9 +1,10 @@ -#ifndef NVIM_API_VIM_H -#define NVIM_API_VIM_H +#pragma once -#include "nvim/api/private/defs.h" +#include <stdint.h> // IWYU pragma: keep + +#include "nvim/api/keysets_defs.h" // IWYU pragma: keep +#include "nvim/api/private/defs.h" // IWYU pragma: keep #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/vim.h.generated.h" #endif -#endif // NVIM_API_VIM_H diff --git a/src/nvim/api/vimscript.c b/src/nvim/api/vimscript.c index af1b23b712..c75bf21572 100644 --- a/src/nvim/api/vimscript.c +++ b/src/nvim/api/vimscript.c @@ -1,6 +1,3 @@ -// 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 <stdbool.h> #include <stddef.h> @@ -8,23 +5,22 @@ #include <string.h> #include "klib/kvec.h" +#include "nvim/api/keysets_defs.h" #include "nvim/api/private/converter.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/helpers.h" #include "nvim/api/vimscript.h" -#include "nvim/ascii.h" -#include "nvim/buffer_defs.h" +#include "nvim/ascii_defs.h" #include "nvim/eval.h" #include "nvim/eval/typval.h" -#include "nvim/eval/typval_defs.h" #include "nvim/eval/userfunc.h" #include "nvim/ex_docmd.h" +#include "nvim/func_attr.h" #include "nvim/garray.h" #include "nvim/globals.h" #include "nvim/memory.h" -#include "nvim/pos.h" #include "nvim/runtime.h" -#include "nvim/vim.h" +#include "nvim/vim_defs.h" #include "nvim/viml/parser/expressions.h" #include "nvim/viml/parser/parser.h" @@ -38,39 +34,57 @@ /// Unlike |nvim_command()| this function supports heredocs, script-scope (s:), /// etc. /// -/// On execution error: fails with VimL error, updates v:errmsg. +/// On execution error: fails with Vimscript 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 opts Optional parameters. +/// - output: (boolean, default false) Whether to 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(uint64_t channel_id, String src, Boolean output, Error *err) - FUNC_API_SINCE(7) +/// @return Dictionary containing information about execution, with these keys: +/// - output: (string|nil) Output if `opts.output` is true. +Dictionary nvim_exec2(uint64_t channel_id, String src, Dict(exec_opts) *opts, Error *err) + FUNC_API_SINCE(11) +{ + Dictionary result = ARRAY_DICT_INIT; + + String output = exec_impl(channel_id, src, opts, err); + if (ERROR_SET(err)) { + return result; + } + + if (opts->output) { + PUT(result, "output", STRING_OBJ(output)); + } + + return result; +} + +String exec_impl(uint64_t channel_id, String src, Dict(exec_opts) *opts, Error *err) { const int save_msg_silent = msg_silent; garray_T *const save_capture_ga = capture_ga; const int save_msg_col = msg_col; garray_T capture_local; - if (output) { + if (opts->output) { ga_init(&capture_local, 1, 80); capture_ga = &capture_local; } try_start(); - if (output) { + if (opts->output) { msg_silent++; msg_col = 0; // prevent leading spaces } const sctx_T save_current_sctx = api_set_sctx(channel_id); - do_source_str(src.data, "nvim_exec()"); - if (output) { + do_source_str(src.data, "nvim_exec2()"); + if (opts->output) { capture_ga = save_capture_ga; msg_silent = save_msg_silent; // Put msg_col back where it was, since nothing should have been written. @@ -84,7 +98,7 @@ String nvim_exec(uint64_t channel_id, String src, Boolean output, Error *err) goto theend; } - if (output && capture_local.ga_len > 1) { + if (opts->output && capture_local.ga_len > 1) { String s = (String){ .data = capture_local.ga_data, .size = (size_t)capture_local.ga_len, @@ -98,7 +112,7 @@ String nvim_exec(uint64_t channel_id, String src, Boolean output, Error *err) return s; // Caller will free the memory. } theend: - if (output) { + if (opts->output) { ga_clear(&capture_local); } return (String)STRING_INIT; @@ -106,10 +120,10 @@ theend: /// Executes an Ex command. /// -/// On execution error: fails with VimL error, updates v:errmsg. +/// On execution error: fails with Vimscript error, updates v:errmsg. /// -/// 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 +/// Prefer using |nvim_cmd()| or |nvim_exec2()| over this. To evaluate multiple lines of Vim script +/// or an Ex command directly, use |nvim_exec2()|. 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()|. /// @@ -123,12 +137,12 @@ void nvim_command(String command, Error *err) try_end(err); } -/// Evaluates a VimL |expression|. +/// Evaluates a Vimscript |expression|. /// Dictionaries and Lists are recursively expanded. /// -/// On execution error: fails with VimL error, updates v:errmsg. +/// On execution error: fails with Vimscript error, updates v:errmsg. /// -/// @param expr VimL expression string +/// @param expr Vimscript expression string /// @param[out] err Error details, if any /// @return Evaluation result or expanded object Object nvim_eval(String expr, Error *err) @@ -137,39 +151,42 @@ Object nvim_eval(String expr, Error *err) static int recursive = 0; // recursion depth Object rv = OBJECT_INIT; - TRY_WRAP({ - // Initialize `force_abort` and `suppress_errthrow` at the top level. - if (!recursive) { - force_abort = false; - suppress_errthrow = false; - did_throw = false; - // `did_emsg` is set by emsg(), which cancels execution. - did_emsg = false; - } - recursive++; - try_start(); + // Initialize `force_abort` and `suppress_errthrow` at the top level. + if (!recursive) { + force_abort = false; + suppress_errthrow = false; + did_throw = false; + // `did_emsg` is set by emsg(), which cancels execution. + did_emsg = false; + } - typval_T rettv; - int ok = eval0(expr.data, &rettv, NULL, true); + recursive++; - if (!try_end(err)) { - if (ok == FAIL) { - // Should never happen, try_end() should get the error. #8371 - api_set_error(err, kErrorTypeException, - "Failed to evaluate expression: '%.*s'", 256, expr.data); - } else { - rv = vim_to_object(&rettv); - } - } + typval_T rettv; + int ok; - tv_clear(&rettv); - recursive--; + TRY_WRAP(err, { + ok = eval0(expr.data, &rettv, NULL, &EVALARG_EVALUATE); + clear_evalarg(&EVALARG_EVALUATE, NULL); }); + if (!ERROR_SET(err)) { + if (ok == FAIL) { + // Should never happen, try_end() (in TRY_WRAP) should get the error. #8371 + api_set_error(err, kErrorTypeException, + "Failed to evaluate expression: '%.*s'", 256, expr.data); + } else { + rv = vim_to_object(&rettv); + } + } + + tv_clear(&rettv); + recursive--; + return rv; } -/// Calls a VimL function. +/// Calls a Vimscript function. /// /// @param fn Function name /// @param args Function arguments @@ -196,34 +213,37 @@ static Object _call_function(String fn, Array args, dict_T *self, Error *err) } } - TRY_WRAP({ - // Initialize `force_abort` and `suppress_errthrow` at the top level. - if (!recursive) { - force_abort = false; - suppress_errthrow = false; - did_throw = false; - // `did_emsg` is set by emsg(), which cancels execution. - did_emsg = false; - } - recursive++; - try_start(); - typval_T rettv; - funcexe_T funcexe = FUNCEXE_INIT; - funcexe.fe_firstline = curwin->w_cursor.lnum; - funcexe.fe_lastline = curwin->w_cursor.lnum; - funcexe.fe_evaluate = true; - funcexe.fe_selfdict = self; + // Initialize `force_abort` and `suppress_errthrow` at the top level. + if (!recursive) { + force_abort = false; + suppress_errthrow = false; + did_throw = false; + // `did_emsg` is set by emsg(), which cancels execution. + did_emsg = false; + } + recursive++; + + typval_T rettv; + funcexe_T funcexe = FUNCEXE_INIT; + funcexe.fe_firstline = curwin->w_cursor.lnum; + funcexe.fe_lastline = curwin->w_cursor.lnum; + funcexe.fe_evaluate = true; + funcexe.fe_selfdict = self; + + TRY_WRAP(err, { // call_func() retval is deceptive, ignore it. Instead we set `msg_list` // (see above) to capture abort-causing non-exception errors. (void)call_func(fn.data, (int)fn.size, &rettv, (int)args.size, vim_args, &funcexe); - if (!try_end(err)) { - rv = vim_to_object(&rettv); - } - tv_clear(&rettv); - recursive--; }); + if (!ERROR_SET(err)) { + rv = vim_to_object(&rettv); + } + + tv_clear(&rettv); + recursive--; + free_vim_args: while (i > 0) { tv_clear(&vim_args[--i]); @@ -232,9 +252,9 @@ free_vim_args: return rv; } -/// Calls a VimL function with the given arguments. +/// Calls a Vimscript function with the given arguments. /// -/// On execution error: fails with VimL error, updates v:errmsg. +/// On execution error: fails with Vimscript error, updates v:errmsg. /// /// @param fn Function to call /// @param args Function arguments packed in an Array @@ -246,12 +266,12 @@ Object nvim_call_function(String fn, Array args, Error *err) return _call_function(fn, args, NULL, err); } -/// Calls a VimL |Dictionary-function| with the given arguments. +/// Calls a Vimscript |Dictionary-function| with the given arguments. /// -/// On execution error: fails with VimL error, updates v:errmsg. +/// On execution error: fails with Vimscript 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 +/// @param dict Dictionary, or String evaluating to a Vimscript |self| dict +/// @param fn Name of the function defined on the Vimscript dict /// @param args Function arguments packed in an Array /// @param[out] err Error details, if any /// @return Result of the function call @@ -265,10 +285,11 @@ Object nvim_call_dict_function(Object dict, String fn, Array args, Error *err) switch (dict.type) { case kObjectTypeString: try_start(); - if (eval0(dict.data.string.data, &rettv, NULL, true) == FAIL) { + if (eval0(dict.data.string.data, &rettv, NULL, &EVALARG_EVALUATE) == FAIL) { api_set_error(err, kErrorTypeException, "Failed to evaluate dict expression"); } + clear_evalarg(&EVALARG_EVALUATE, NULL); if (try_end(err)) { return rv; } @@ -336,7 +357,7 @@ typedef struct { typedef kvec_withinit_t(ExprASTConvStackItem, 16) ExprASTConvStack; /// @endcond -/// Parse a VimL expression. +/// Parse a Vimscript expression. /// /// @param[in] expr Expression to parse. Always treated as a single line. /// @param[in] flags Flags: @@ -375,7 +396,7 @@ typedef kvec_withinit_t(ExprASTConvStackItem, 16) ExprASTConvStack; /// stringified without "kExprNode" prefix. /// - "start": a pair [line, column] describing where node is "started" /// where "line" is always 0 (will not be 0 if you will be -/// using nvim_parse_viml() on e.g. ":let", but that is not +/// using this API on e.g. ":let", but that is not /// present yet). Both elements are Integers. /// - "len": “length” of the node. This and "start" are there for /// debugging purposes primary (debugging parser and providing @@ -475,7 +496,7 @@ Dictionary nvim_parse_expression(String expr, String flags, Boolean highlight, E }; err_dict.items[0] = (KeyValuePair) { .key = STATIC_CSTR_TO_STRING("message"), - .value = STRING_OBJ(cstr_to_string(east.err.msg)), + .value = CSTR_TO_OBJ(east.err.msg), }; if (east.err.arg == NULL) { err_dict.items[1] = (KeyValuePair) { @@ -512,7 +533,7 @@ Dictionary nvim_parse_expression(String expr, String flags, Boolean highlight, E chunk_arr.items[0] = INTEGER_OBJ((Integer)chunk.start.line); chunk_arr.items[1] = INTEGER_OBJ((Integer)chunk.start.col); chunk_arr.items[2] = INTEGER_OBJ((Integer)chunk.end_col); - chunk_arr.items[3] = STRING_OBJ(cstr_to_string(chunk.group)); + chunk_arr.items[3] = CSTR_TO_OBJ(chunk.group); hl.items[i] = ARRAY_OBJ(chunk_arr); } ret.items[ret.size++] = (KeyValuePair) { @@ -589,7 +610,7 @@ Dictionary nvim_parse_expression(String expr, String flags, Boolean highlight, E kv_drop(ast_conv_stack, 1); ret_node->items[ret_node->size++] = (KeyValuePair) { .key = STATIC_CSTR_TO_STRING("type"), - .value = STRING_OBJ(cstr_to_string(east_node_type_tab[node->type])), + .value = CSTR_TO_OBJ(east_node_type_tab[node->type]), }; Array start_array = { .items = xmalloc(2 * sizeof(start_array.items[0])), @@ -674,11 +695,11 @@ Dictionary nvim_parse_expression(String expr, String flags, Boolean highlight, E case kExprNodeComparison: ret_node->items[ret_node->size++] = (KeyValuePair) { .key = STATIC_CSTR_TO_STRING("cmp_type"), - .value = STRING_OBJ(cstr_to_string(eltkn_cmp_type_tab[node->data.cmp.type])), + .value = CSTR_TO_OBJ(eltkn_cmp_type_tab[node->data.cmp.type]), }; ret_node->items[ret_node->size++] = (KeyValuePair) { .key = STATIC_CSTR_TO_STRING("ccs_strategy"), - .value = STRING_OBJ(cstr_to_string(ccs_tab[node->data.cmp.ccs])), + .value = CSTR_TO_OBJ(ccs_tab[node->data.cmp.ccs]), }; ret_node->items[ret_node->size++] = (KeyValuePair) { .key = STATIC_CSTR_TO_STRING("invert"), diff --git a/src/nvim/api/vimscript.h b/src/nvim/api/vimscript.h index be808b6b25..c315e932e9 100644 --- a/src/nvim/api/vimscript.h +++ b/src/nvim/api/vimscript.h @@ -1,9 +1,10 @@ -#ifndef NVIM_API_VIMSCRIPT_H -#define NVIM_API_VIMSCRIPT_H +#pragma once -#include "nvim/api/private/defs.h" +#include <stdint.h> // IWYU pragma: keep + +#include "nvim/api/keysets_defs.h" // IWYU pragma: keep +#include "nvim/api/private/defs.h" // IWYU pragma: keep #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/vimscript.h.generated.h" #endif -#endif // NVIM_API_VIMSCRIPT_H diff --git a/src/nvim/api/win_config.c b/src/nvim/api/win_config.c index 0ffeac1bff..4e23717dc6 100644 --- a/src/nvim/api/win_config.c +++ b/src/nvim/api/win_config.c @@ -1,30 +1,32 @@ -// 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 <string.h> #include "klib/kvec.h" #include "nvim/api/extmark.h" +#include "nvim/api/keysets_defs.h" #include "nvim/api/private/defs.h" +#include "nvim/api/private/dispatch.h" #include "nvim/api/private/helpers.h" #include "nvim/api/win_config.h" -#include "nvim/ascii.h" +#include "nvim/ascii_defs.h" +#include "nvim/autocmd.h" #include "nvim/buffer_defs.h" #include "nvim/decoration.h" #include "nvim/drawscreen.h" -#include "nvim/extmark_defs.h" +#include "nvim/func_attr.h" #include "nvim/globals.h" -#include "nvim/grid_defs.h" +#include "nvim/grid.h" #include "nvim/highlight_group.h" -#include "nvim/macros.h" +#include "nvim/macros_defs.h" #include "nvim/mbyte.h" #include "nvim/memory.h" #include "nvim/option.h" -#include "nvim/pos.h" +#include "nvim/pos_defs.h" +#include "nvim/strings.h" #include "nvim/syntax.h" #include "nvim/ui.h" #include "nvim/window.h" +#include "nvim/winfloat.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/win_config.c.generated.h" @@ -55,16 +57,19 @@ /// this should not be used to specify arbitrary WM screen positions. /// /// Example (Lua): window-relative float -/// <pre>lua -/// vim.api.nvim_open_win(0, false, -/// {relative='win', row=3, col=3, width=12, height=3}) -/// </pre> +/// +/// ```lua +/// vim.api.nvim_open_win(0, false, +/// {relative='win', row=3, col=3, width=12, height=3}) +/// ``` /// /// Example (Lua): buffer-relative float (travels as buffer is scrolled) -/// <pre>lua -/// vim.api.nvim_open_win(0, false, -/// {relative='win', width=12, height=3, bufpos={100,10}}) -/// </pre> +/// +/// ```lua +/// vim.api.nvim_open_win(0, false, +/// {relative='win', width=12, height=3, bufpos={100,10}}) +/// }) +/// ``` /// /// @param buffer Buffer to display, or 0 for current buffer /// @param enter Enter the window (make it the current window) @@ -108,8 +113,8 @@ /// The default value for floats are 50. In general, values below 100 are /// recommended, unless there is a good reason to overshadow builtin /// elements. -/// - style: Configure the appearance of the window. Currently only takes -/// one non-empty value: +/// - style: (optional) Configure the appearance of the window. Currently +/// only supports one value: /// - "minimal" Nvim will display the window with many UI options /// disabled. This is useful when displaying a temporary /// float where the text should not be edited. Disables @@ -129,7 +134,7 @@ /// - "solid": Adds padding by a single whitespace cell. /// - "shadow": A drop shadow effect by blending with the background. /// - If it is an array, it should have a length of eight or any divisor of -/// eight. The array will specifify the eight chars building up the border +/// eight. The array will specify the eight chars building up the border /// in a clockwise fashion starting with the top-left corner. As an /// example, the double box style could be specified as /// [ "╔", "═" ,"╗", "║", "╝", "═", "╚", "║" ]. @@ -144,22 +149,41 @@ /// By default, `FloatBorder` highlight is used, which links to `WinSeparator` /// when not defined. It could also be specified by character: /// [ ["+", "MyCorner"], ["x", "MyBorder"] ]. -/// - title: Title (optional) in window border, String or list. -/// List is [text, highlight] tuples. if is string the default -/// highlight group is `FloatTitle`. -/// - title_pos: Title position must set with title option. -/// value can be of `left` `center` `right` default is left. +/// - title: Title (optional) in window border, string or list. +/// List should consist of `[text, highlight]` tuples. +/// If string, the default highlight group is `FloatTitle`. +/// - title_pos: Title position. Must be set with `title` option. +/// Value can be one of "left", "center", or "right". +/// Default is `"left"`. +/// - footer: Footer (optional) in window border, string or list. +/// List should consist of `[text, highlight]` tuples. +/// If string, the default highlight group is `FloatFooter`. +/// - footer_pos: Footer position. Must be set with `footer` option. +/// Value can be one of "left", "center", or "right". +/// Default is `"left"`. /// - noautocmd: If true then no buffer-related autocommand events such as /// |BufEnter|, |BufLeave| or |BufWinEnter| may fire from /// calling this function. +/// - fixed: If true when anchor is NW or SW, the float window +/// would be kept fixed even if the window would be truncated. +/// - hide: If true the floating window will be hidden. /// /// @param[out] err Error details, if any /// /// @return Window handle, or 0 on error Window nvim_open_win(Buffer buffer, Boolean enter, Dict(float_config) *config, Error *err) FUNC_API_SINCE(6) - FUNC_API_CHECK_TEXTLOCK + FUNC_API_TEXTLOCK_ALLOW_CMDWIN { + buf_T *buf = find_buffer_by_handle(buffer, err); + if (!buf) { + return 0; + } + if (cmdwin_type != 0 && (enter || buf == curbuf)) { + api_set_error(err, kErrorTypeException, "%s", e_cmdwin); + return 0; + } + FloatConfig fconfig = FLOAT_CONFIG_INIT; if (!parse_float_config(config, &fconfig, false, true, err)) { return 0; @@ -173,7 +197,11 @@ Window nvim_open_win(Buffer buffer, Boolean enter, Dict(float_config) *config, E } // autocmds in win_enter or win_set_buf below may close the window if (win_valid(wp) && buffer > 0) { - win_set_buf(wp->handle, buffer, fconfig.noautocmd, err); + Boolean noautocmd = !enter || fconfig.noautocmd; + win_set_buf(wp, buf, noautocmd, err); + if (!fconfig.noautocmd) { + apply_autocmds(EVENT_WINNEW, NULL, NULL, false, buf); + } } if (!win_valid(wp)) { api_set_error(err, kErrorTypeException, "Window was closed immediately"); @@ -222,12 +250,56 @@ void nvim_win_set_config(Window window, Dict(float_config) *config, Error *err) win_config_float(win, fconfig); win->w_pos_changed = true; } - if (fconfig.style == kWinStyleMinimal) { - win_set_minimal_style(win); - didset_window_options(win, true); + if (HAS_KEY(config, float_config, style)) { + if (fconfig.style == kWinStyleMinimal) { + win_set_minimal_style(win); + didset_window_options(win, true); + } } } +static Dictionary config_put_bordertext(Dictionary config, FloatConfig *fconfig, + BorderTextType bordertext_type) +{ + VirtText vt; + AlignTextPos align; + char *field_name; + char *field_pos_name; + switch (bordertext_type) { + case kBorderTextTitle: + vt = fconfig->title_chunks; + align = fconfig->title_pos; + field_name = "title"; + field_pos_name = "title_pos"; + break; + case kBorderTextFooter: + vt = fconfig->footer_chunks; + align = fconfig->footer_pos; + field_name = "footer"; + field_pos_name = "footer_pos"; + break; + } + + Array bordertext = virt_text_to_array(vt, true); + PUT(config, field_name, ARRAY_OBJ(bordertext)); + + char *pos; + switch (align) { + case kAlignLeft: + pos = "left"; + break; + case kAlignCenter: + pos = "center"; + break; + case kAlignRight: + pos = "right"; + break; + } + PUT(config, field_pos_name, CSTR_TO_OBJ(pos)); + + return config; +} + /// Gets window configuration. /// /// The returned value may be given to |nvim_open_win()|. @@ -251,6 +323,7 @@ Dictionary nvim_win_get_config(Window window, Error *err) PUT(rv, "focusable", BOOLEAN_OBJ(config->focusable)); PUT(rv, "external", BOOLEAN_OBJ(config->external)); + PUT(rv, "hide", BOOLEAN_OBJ(config->hide)); if (wp->w_floating) { PUT(rv, "width", INTEGER_OBJ(config->width)); @@ -265,7 +338,7 @@ Dictionary nvim_win_get_config(Window window, Error *err) PUT(rv, "bufpos", ARRAY_OBJ(pos)); } } - PUT(rv, "anchor", STRING_OBJ(cstr_to_string(float_anchor_str[config->anchor]))); + PUT(rv, "anchor", CSTR_TO_OBJ(float_anchor_str[config->anchor])); PUT(rv, "row", FLOAT_OBJ(config->row)); PUT(rv, "col", FLOAT_OBJ(config->col)); PUT(rv, "zindex", INTEGER_OBJ(config->zindex)); @@ -275,13 +348,13 @@ Dictionary nvim_win_get_config(Window window, Error *err) for (size_t i = 0; i < 8; i++) { Array tuple = ARRAY_DICT_INIT; - String s = cstrn_to_string((const char *)config->border_chars[i], sizeof(schar_T)); + String s = cstrn_to_string(config->border_chars[i], MAX_SCHAR_SIZE); int hi_id = config->border_hl_ids[i]; char *hi_name = syn_id2name(hi_id); if (hi_name[0]) { ADD(tuple, STRING_OBJ(s)); - ADD(tuple, STRING_OBJ(cstr_to_string((const char *)hi_name))); + ADD(tuple, CSTR_TO_OBJ(hi_name)); ADD(border, ARRAY_OBJ(tuple)); } else { ADD(border, STRING_OBJ(s)); @@ -289,34 +362,17 @@ Dictionary nvim_win_get_config(Window window, Error *err) } PUT(rv, "border", ARRAY_OBJ(border)); if (config->title) { - Array titles = ARRAY_DICT_INIT; - VirtText title_datas = config->title_chunks; - for (size_t i = 0; i < title_datas.size; i++) { - Array tuple = ARRAY_DICT_INIT; - ADD(tuple, CSTR_TO_OBJ((const char *)title_datas.items[i].text)); - if (title_datas.items[i].hl_id > 0) { - ADD(tuple, - STRING_OBJ(cstr_to_string((const char *)syn_id2name(title_datas.items[i].hl_id)))); - } - ADD(titles, ARRAY_OBJ(tuple)); - } - PUT(rv, "title", ARRAY_OBJ(titles)); - char *title_pos; - if (config->title_pos == kAlignLeft) { - title_pos = "left"; - } else if (config->title_pos == kAlignCenter) { - title_pos = "center"; - } else { - title_pos = "right"; - } - PUT(rv, "title_pos", CSTR_TO_OBJ(title_pos)); + rv = config_put_bordertext(rv, config, kBorderTextTitle); + } + if (config->footer) { + rv = config_put_bordertext(rv, config, kBorderTextFooter); } } } const char *rel = (wp->w_floating && !config->external ? float_relative_str[config->relative] : ""); - PUT(rv, "relative", STRING_OBJ(cstr_to_string(rel))); + PUT(rv, "relative", CSTR_TO_OBJ(rel)); return rv; } @@ -370,68 +426,91 @@ static bool parse_float_bufpos(Array bufpos, lpos_T *out) return true; } -static void parse_border_title(Object title, Object title_pos, FloatConfig *fconfig, Error *err) +static void parse_bordertext(Object bordertext, BorderTextType bordertext_type, + FloatConfig *fconfig, Error *err) { - if (!parse_title_pos(title_pos, fconfig, err)) { - return; - } - - if (title.type == kObjectTypeString) { - if (title.data.string.size == 0) { - fconfig->title = false; + bool *is_present; + VirtText *chunks; + int *width; + int default_hl_id; + switch (bordertext_type) { + case kBorderTextTitle: + is_present = &fconfig->title; + chunks = &fconfig->title_chunks; + width = &fconfig->title_width; + default_hl_id = syn_check_group(S_LEN("FloatTitle")); + break; + case kBorderTextFooter: + is_present = &fconfig->footer; + chunks = &fconfig->footer_chunks; + width = &fconfig->footer_width; + default_hl_id = syn_check_group(S_LEN("FloatFooter")); + break; + } + + if (bordertext.type == kObjectTypeString) { + if (bordertext.data.string.size == 0) { + *is_present = false; return; } - int hl_id = syn_check_group(S_LEN("FloatTitle")); - kv_push(fconfig->title_chunks, ((VirtTextChunk){ .text = xstrdup(title.data.string.data), - .hl_id = hl_id })); - fconfig->title_width = (int)mb_string2cells(title.data.string.data); - fconfig->title = true; + kv_push(*chunks, ((VirtTextChunk){ .text = xstrdup(bordertext.data.string.data), + .hl_id = default_hl_id })); + *width = (int)mb_string2cells(bordertext.data.string.data); + *is_present = true; return; } - if (title.type != kObjectTypeArray) { + if (bordertext.type != kObjectTypeArray) { api_set_error(err, kErrorTypeValidation, "title must be string or array"); return; } - if (title.data.array.size == 0) { + if (bordertext.data.array.size == 0) { api_set_error(err, kErrorTypeValidation, "title cannot be an empty array"); return; } - fconfig->title_width = 0; - fconfig->title_chunks = parse_virt_text(title.data.array, err, &fconfig->title_width); + *width = 0; + *chunks = parse_virt_text(bordertext.data.array, err, width); - fconfig->title = true; + *is_present = true; } -static bool parse_title_pos(Object title_pos, FloatConfig *fconfig, Error *err) +static bool parse_bordertext_pos(String bordertext_pos, BorderTextType bordertext_type, + FloatConfig *fconfig, Error *err) { - if (!HAS_KEY(title_pos)) { - fconfig->title_pos = kAlignLeft; + AlignTextPos *align; + switch (bordertext_type) { + case kBorderTextTitle: + align = &fconfig->title_pos; + break; + case kBorderTextFooter: + align = &fconfig->footer_pos; + break; + } + + if (bordertext_pos.size == 0) { + *align = kAlignLeft; return true; } - if (title_pos.type != kObjectTypeString) { - api_set_error(err, kErrorTypeValidation, "title_pos must be string"); - return false; - } - - if (title_pos.data.string.size == 0) { - fconfig->title_pos = kAlignLeft; - return true; - } - - char *pos = title_pos.data.string.data; + char *pos = bordertext_pos.data; if (strequal(pos, "left")) { - fconfig->title_pos = kAlignLeft; + *align = kAlignLeft; } else if (strequal(pos, "center")) { - fconfig->title_pos = kAlignCenter; + *align = kAlignCenter; } else if (strequal(pos, "right")) { - fconfig->title_pos = kAlignRight; + *align = kAlignRight; } else { - api_set_error(err, kErrorTypeValidation, "invalid title_pos value"); + switch (bordertext_type) { + case kBorderTextTitle: + api_set_error(err, kErrorTypeValidation, "invalid title_pos value"); + break; + case kBorderTextFooter: + api_set_error(err, kErrorTypeValidation, "invalid footer_pos value"); + break; + } return false; } return true; @@ -441,7 +520,7 @@ static void parse_border_style(Object style, FloatConfig *fconfig, Error *err) { struct { const char *name; - schar_T chars[8]; + char chars[8][MAX_SCHAR_SIZE]; bool shadow_color; } defaults[] = { { "double", { "╔", "═", "╗", "║", "╝", "═", "╚", "║" }, false }, @@ -452,7 +531,7 @@ static void parse_border_style(Object style, FloatConfig *fconfig, Error *err) { NULL, { { NUL } }, false }, }; - schar_T *chars = fconfig->border_chars; + char (*chars)[MAX_SCHAR_SIZE] = fconfig->border_chars; int *hl_ids = fconfig->border_hl_ids; fconfig->border = true; @@ -521,8 +600,9 @@ static void parse_border_style(Object style, FloatConfig *fconfig, Error *err) String str = style.data.string; if (str.size == 0 || strequal(str.data, "none")) { fconfig->border = false; - // title does not work with border equal none + // border text does not work with border equal none fconfig->title = false; + fconfig->footer = false; return; } for (size_t i = 0; defaults[i].name; i++) { @@ -549,110 +629,90 @@ static void parse_border_style(Object style, FloatConfig *fconfig, Error *err) static bool parse_float_config(Dict(float_config) *config, FloatConfig *fconfig, bool reconf, bool new_win, Error *err) { +#define HAS_KEY_X(d, key) HAS_KEY(d, float_config, key) bool has_relative = false, relative_is_win = false; - if (config->relative.type == kObjectTypeString) { - // ignore empty string, to match nvim_win_get_config - if (config->relative.data.string.size > 0) { - if (!parse_float_relative(config->relative.data.string, &fconfig->relative)) { - api_set_error(err, kErrorTypeValidation, "Invalid value of 'relative' key"); - return false; - } + // ignore empty string, to match nvim_win_get_config + if (HAS_KEY_X(config, relative) && config->relative.size > 0) { + if (!parse_float_relative(config->relative, &fconfig->relative)) { + api_set_error(err, kErrorTypeValidation, "Invalid value of 'relative' key"); + return false; + } - if (!(HAS_KEY(config->row) && HAS_KEY(config->col)) && !HAS_KEY(config->bufpos)) { - api_set_error(err, kErrorTypeValidation, - "'relative' requires 'row'/'col' or 'bufpos'"); - return false; - } + if (!(HAS_KEY_X(config, row) && HAS_KEY_X(config, col)) && !HAS_KEY_X(config, bufpos)) { + api_set_error(err, kErrorTypeValidation, + "'relative' requires 'row'/'col' or 'bufpos'"); + return false; + } - has_relative = true; - fconfig->external = false; - if (fconfig->relative == kFloatRelativeWindow) { - relative_is_win = true; - fconfig->bufpos.lnum = -1; - } + has_relative = true; + fconfig->external = false; + if (fconfig->relative == kFloatRelativeWindow) { + relative_is_win = true; + fconfig->bufpos.lnum = -1; } - } else if (HAS_KEY(config->relative)) { - api_set_error(err, kErrorTypeValidation, "'relative' key must be String"); - return false; } - if (config->anchor.type == kObjectTypeString) { - if (!parse_float_anchor(config->anchor.data.string, &fconfig->anchor)) { + if (HAS_KEY_X(config, anchor)) { + if (!parse_float_anchor(config->anchor, &fconfig->anchor)) { api_set_error(err, kErrorTypeValidation, "Invalid value of 'anchor' key"); return false; } - } else if (HAS_KEY(config->anchor)) { - api_set_error(err, kErrorTypeValidation, "'anchor' key must be String"); - return false; } - if (HAS_KEY(config->row)) { + if (HAS_KEY_X(config, row)) { if (!has_relative) { api_set_error(err, kErrorTypeValidation, "non-float cannot have 'row'"); return false; - } else if (config->row.type == kObjectTypeInteger) { - fconfig->row = (double)config->row.data.integer; - } else if (config->row.type == kObjectTypeFloat) { - fconfig->row = config->row.data.floating; - } else { - api_set_error(err, kErrorTypeValidation, - "'row' key must be Integer or Float"); - return false; } + fconfig->row = config->row; } - if (HAS_KEY(config->col)) { + if (HAS_KEY_X(config, col)) { if (!has_relative) { api_set_error(err, kErrorTypeValidation, "non-float cannot have 'col'"); return false; - } else if (config->col.type == kObjectTypeInteger) { - fconfig->col = (double)config->col.data.integer; - } else if (config->col.type == kObjectTypeFloat) { - fconfig->col = config->col.data.floating; - } else { - api_set_error(err, kErrorTypeValidation, - "'col' key must be Integer or Float"); - return false; } + fconfig->col = config->col; } - if (HAS_KEY(config->bufpos)) { + if (HAS_KEY_X(config, bufpos)) { if (!has_relative) { api_set_error(err, kErrorTypeValidation, "non-float cannot have 'bufpos'"); return false; - } else if (config->bufpos.type == kObjectTypeArray) { - if (!parse_float_bufpos(config->bufpos.data.array, &fconfig->bufpos)) { + } else { + if (!parse_float_bufpos(config->bufpos, &fconfig->bufpos)) { api_set_error(err, kErrorTypeValidation, "Invalid value of 'bufpos' key"); return false; } - if (!HAS_KEY(config->row)) { + if (!HAS_KEY_X(config, row)) { fconfig->row = (fconfig->anchor & kFloatAnchorSouth) ? 0 : 1; } - if (!HAS_KEY(config->col)) { + if (!HAS_KEY_X(config, col)) { fconfig->col = 0; } - } else { - api_set_error(err, kErrorTypeValidation, "'bufpos' key must be Array"); - return false; } } - if (config->width.type == kObjectTypeInteger && config->width.data.integer > 0) { - fconfig->width = (int)config->width.data.integer; - } else if (HAS_KEY(config->width)) { - api_set_error(err, kErrorTypeValidation, "'width' key must be a positive Integer"); - return false; + if (HAS_KEY_X(config, width)) { + if (config->width > 0) { + fconfig->width = (int)config->width; + } else { + api_set_error(err, kErrorTypeValidation, "'width' key must be a positive Integer"); + return false; + } } else if (!reconf) { api_set_error(err, kErrorTypeValidation, "Must specify 'width'"); return false; } - if (config->height.type == kObjectTypeInteger && config->height.data.integer > 0) { - fconfig->height = (int)config->height.data.integer; - } else if (HAS_KEY(config->height)) { - api_set_error(err, kErrorTypeValidation, "'height' key must be a positive Integer"); - return false; + if (HAS_KEY_X(config, height)) { + if (config->height > 0) { + fconfig->height = (int)config->height; + } else { + api_set_error(err, kErrorTypeValidation, "'height' key must be a positive Integer"); + return false; + } } else if (!reconf) { api_set_error(err, kErrorTypeValidation, "Must specify 'height'"); return false; @@ -660,26 +720,20 @@ static bool parse_float_config(Dict(float_config) *config, FloatConfig *fconfig, if (relative_is_win) { fconfig->window = curwin->handle; - if (config->win.type == kObjectTypeInteger || config->win.type == kObjectTypeWindow) { - if (config->win.data.integer > 0) { - fconfig->window = (Window)config->win.data.integer; + if (HAS_KEY_X(config, win)) { + if (config->win > 0) { + fconfig->window = config->win; } - } else if (HAS_KEY(config->win)) { - api_set_error(err, kErrorTypeValidation, "'win' key must be Integer or Window"); - return false; } } else { - if (HAS_KEY(config->win)) { + if (HAS_KEY_X(config, win)) { api_set_error(err, kErrorTypeValidation, "'win' key is only valid with relative='win'"); return false; } } - if (HAS_KEY(config->external)) { - fconfig->external = api_object_to_bool(config->external, "'external' key", false, err); - if (ERROR_SET(err)) { - return false; - } + if (HAS_KEY_X(config, external)) { + fconfig->external = config->external; if (has_relative && fconfig->external) { api_set_error(err, kErrorTypeValidation, "Only one of 'relative' and 'external' must be used"); @@ -698,30 +752,22 @@ 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)) { - return false; - } - } - - if (config->zindex.type == kObjectTypeInteger && config->zindex.data.integer > 0) { - fconfig->zindex = (int)config->zindex.data.integer; - } else if (HAS_KEY(config->zindex)) { - api_set_error(err, kErrorTypeValidation, "'zindex' key must be a positive Integer"); - return false; + if (HAS_KEY_X(config, focusable)) { + fconfig->focusable = config->focusable; } - if (HAS_KEY(config->title_pos)) { - if (!HAS_KEY(config->title)) { - api_set_error(err, kErrorTypeException, "title_pos requires title to be set"); + if (HAS_KEY_X(config, zindex)) { + if (config->zindex > 0) { + fconfig->zindex = (int)config->zindex; + } else { + api_set_error(err, kErrorTypeValidation, "'zindex' key must be a positive Integer"); return false; } } - if (HAS_KEY(config->title)) { + if (HAS_KEY_X(config, title)) { // title only work with border - if (!HAS_KEY(config->border) && !fconfig->border) { + if (!HAS_KEY_X(config, border) && !fconfig->border) { api_set_error(err, kErrorTypeException, "title requires border to be set"); return false; } @@ -729,42 +775,84 @@ static bool parse_float_config(Dict(float_config) *config, FloatConfig *fconfig, if (fconfig->title) { clear_virttext(&fconfig->title_chunks); } - parse_border_title(config->title, config->title_pos, fconfig, err); + + parse_bordertext(config->title, kBorderTextTitle, fconfig, err); + if (ERROR_SET(err)) { + return false; + } + + // handles unset 'title_pos' same as empty string + if (!parse_bordertext_pos(config->title_pos, kBorderTextTitle, fconfig, err)) { + return false; + } + } else { + if (HAS_KEY_X(config, title_pos)) { + api_set_error(err, kErrorTypeException, "title_pos requires title to be set"); + return false; + } + } + + if (HAS_KEY_X(config, footer)) { + // footer only work with border + if (!HAS_KEY_X(config, border) && !fconfig->border) { + api_set_error(err, kErrorTypeException, "footer requires border to be set"); + return false; + } + + if (fconfig->footer) { + clear_virttext(&fconfig->footer_chunks); + } + + parse_bordertext(config->footer, kBorderTextFooter, fconfig, err); if (ERROR_SET(err)) { return false; } + + // handles unset 'footer_pos' same as empty string + if (!parse_bordertext_pos(config->footer_pos, kBorderTextFooter, fconfig, err)) { + return false; + } + } else { + if (HAS_KEY_X(config, footer_pos)) { + api_set_error(err, kErrorTypeException, "footer_pos requires footer to be set"); + return false; + } } - if (HAS_KEY(config->border)) { + if (HAS_KEY_X(config, border)) { parse_border_style(config->border, fconfig, err); if (ERROR_SET(err)) { return false; } } - if (config->style.type == kObjectTypeString) { - if (config->style.data.string.data[0] == NUL) { + if (HAS_KEY_X(config, style)) { + if (config->style.data[0] == NUL) { fconfig->style = kWinStyleUnused; - } else if (striequal(config->style.data.string.data, "minimal")) { + } else if (striequal(config->style.data, "minimal")) { fconfig->style = kWinStyleMinimal; } else { api_set_error(err, kErrorTypeValidation, "Invalid value of 'style' key"); + return false; } - } else if (HAS_KEY(config->style)) { - api_set_error(err, kErrorTypeValidation, "'style' key must be String"); - return false; } - if (HAS_KEY(config->noautocmd)) { + if (HAS_KEY_X(config, noautocmd)) { if (!new_win) { api_set_error(err, kErrorTypeValidation, "Invalid key: 'noautocmd'"); return false; } - fconfig->noautocmd = api_object_to_bool(config->noautocmd, "'noautocmd' key", false, err); - if (ERROR_SET(err)) { - return false; - } + fconfig->noautocmd = config->noautocmd; + } + + if (HAS_KEY_X(config, fixed)) { + fconfig->fixed = config->fixed; + } + + if (HAS_KEY_X(config, hide)) { + fconfig->hide = config->hide; } return true; +#undef HAS_KEY_X } diff --git a/src/nvim/api/win_config.h b/src/nvim/api/win_config.h index d3e5ede5e9..6df8ed13fa 100644 --- a/src/nvim/api/win_config.h +++ b/src/nvim/api/win_config.h @@ -1,9 +1,10 @@ -#ifndef NVIM_API_WIN_CONFIG_H -#define NVIM_API_WIN_CONFIG_H +#pragma once -#include "nvim/api/private/defs.h" +#include <stdint.h> // IWYU pragma: keep + +#include "nvim/api/keysets_defs.h" // IWYU pragma: keep +#include "nvim/api/private/defs.h" // IWYU pragma: keep #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/win_config.h.generated.h" #endif -#endif // NVIM_API_WIN_CONFIG_H diff --git a/src/nvim/api/window.c b/src/nvim/api/window.c index e2c234ab29..de5b40940f 100644 --- a/src/nvim/api/window.c +++ b/src/nvim/api/window.c @@ -1,27 +1,30 @@ -// 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 <limits.h> #include <stdbool.h> #include <stdint.h> #include <stdlib.h> +#include "nvim/api/keysets_defs.h" #include "nvim/api/private/defs.h" +#include "nvim/api/private/dispatch.h" #include "nvim/api/private/helpers.h" +#include "nvim/api/private/validate.h" #include "nvim/api/window.h" -#include "nvim/ascii.h" +#include "nvim/autocmd.h" #include "nvim/buffer_defs.h" #include "nvim/cursor.h" #include "nvim/drawscreen.h" #include "nvim/eval/window.h" #include "nvim/ex_docmd.h" +#include "nvim/func_attr.h" #include "nvim/gettext.h" #include "nvim/globals.h" #include "nvim/lua/executor.h" -#include "nvim/memline_defs.h" +#include "nvim/memory.h" +#include "nvim/message.h" #include "nvim/move.h" -#include "nvim/pos.h" -#include "nvim/types.h" +#include "nvim/plines.h" +#include "nvim/pos_defs.h" +#include "nvim/types_defs.h" #include "nvim/window.h" /// Gets the current buffer in a window @@ -48,15 +51,26 @@ Buffer nvim_win_get_buf(Window window, Error *err) /// @param[out] err Error details, if any void nvim_win_set_buf(Window window, Buffer buffer, Error *err) FUNC_API_SINCE(5) - FUNC_API_CHECK_TEXTLOCK + FUNC_API_TEXTLOCK_ALLOW_CMDWIN { - win_set_buf(window, buffer, false, err); + win_T *win = find_window_by_handle(window, err); + buf_T *buf = find_buffer_by_handle(buffer, err); + if (!win || !buf) { + return; + } + if (cmdwin_type != 0 && (win == curwin || win == cmdwin_old_curwin || buf == curbuf)) { + api_set_error(err, kErrorTypeException, "%s", e_cmdwin); + return; + } + win_set_buf(win, buf, false, err); } /// Gets the (1,0)-indexed, buffer-relative cursor position for a given window /// (different windows showing the same buffer have independent cursor /// positions). |api-indexing| /// +/// @see |getcurpos()| +/// /// @param window Window handle, or 0 for current window /// @param[out] err Error details, if any /// @return (row, col) tuple @@ -167,13 +181,8 @@ void nvim_win_set_height(Window window, Integer height, Error *err) return; } - win_T *savewin = curwin; - curwin = win; - curbuf = curwin->w_buffer; try_start(); - win_setheight((int)height); - curwin = savewin; - curbuf = curwin->w_buffer; + win_setheight_win((int)height, win); try_end(err); } @@ -214,13 +223,8 @@ void nvim_win_set_width(Window window, Integer width, Error *err) return; } - win_T *savewin = curwin; - curwin = win; - curbuf = curwin->w_buffer; try_start(); - win_setwidth((int)width); - curwin = savewin; - curbuf = curwin->w_buffer; + win_setwidth_win((int)width, win); try_end(err); } @@ -359,10 +363,10 @@ Boolean nvim_win_is_valid(Window window) /// @param[out] err Error details, if any void nvim_win_hide(Window window, Error *err) FUNC_API_SINCE(7) - FUNC_API_CHECK_TEXTLOCK + FUNC_API_TEXTLOCK_ALLOW_CMDWIN { win_T *win = find_window_by_handle(window, err); - if (!win) { + if (!win || !can_close_in_cmdwin(win, err)) { return; } @@ -391,19 +395,10 @@ void nvim_win_hide(Window window, Error *err) /// @param[out] err Error details, if any void nvim_win_close(Window window, Boolean force, Error *err) FUNC_API_SINCE(6) - FUNC_API_CHECK_TEXTLOCK + FUNC_API_TEXTLOCK_ALLOW_CMDWIN { win_T *win = find_window_by_handle(window, err); - if (!win) { - return; - } - - if (cmdwin_type != 0) { - if (win == curwin) { - cmdwin_result = Ctrl_C; - } else { - api_set_error(err, kErrorTypeException, "%s", _(e_cmdwin)); - } + if (!win || !can_close_in_cmdwin(win, err)) { return; } @@ -420,11 +415,11 @@ void nvim_win_close(Window window, Boolean force, Error *err) /// @see |nvim_buf_call()| /// /// @param window Window handle, or 0 for current window -/// @param fun Function to call inside the window (currently lua callable +/// @param fun Function to call inside the window (currently Lua callable /// only) /// @param[out] err Error details, if any -/// @return Return value of function. NB: will deepcopy lua values -/// currently, use upvalues to send lua references in and out. +/// @return Return value of function. NB: will deepcopy Lua values +/// currently, use upvalues to send Lua references in and out. Object nvim_win_call(Window window, LuaRef fun, Error *err) FUNC_API_SINCE(7) FUNC_API_LUA_ONLY @@ -445,8 +440,9 @@ Object nvim_win_call(Window window, LuaRef fun, Error *err) return res; } -/// Set highlight namespace for a window. This will use highlights defined in -/// this namespace, but fall back to global highlights (ns=0) when missing. +/// Set highlight namespace for a window. This will use highlights defined with +/// |nvim_set_hl()| for this namespace, but fall back to global highlights (ns=0) when +/// missing. /// /// This takes precedence over the 'winhighlight' option. /// @@ -469,3 +465,106 @@ void nvim_win_set_hl_ns(Window window, Integer ns_id, Error *err) win->w_hl_needs_update = true; redraw_later(win, UPD_NOT_VALID); } + +/// Computes the number of screen lines occupied by a range of text in a given window. +/// Works for off-screen text and takes folds into account. +/// +/// Diff filler or virtual lines above a line are counted as a part of that line, +/// unless the line is on "start_row" and "start_vcol" is specified. +/// +/// Diff filler or virtual lines below the last buffer line are counted in the result +/// when "end_row" is omitted. +/// +/// Line indexing is similar to |nvim_buf_get_text()|. +/// +/// @param window Window handle, or 0 for current window. +/// @param opts Optional parameters: +/// - start_row: Starting line index, 0-based inclusive. +/// When omitted start at the very top. +/// - end_row: Ending line index, 0-based inclusive. +/// When omitted end at the very bottom. +/// - start_vcol: Starting virtual column index on "start_row", +/// 0-based inclusive, rounded down to full screen lines. +/// When omitted include the whole line. +/// - end_vcol: Ending virtual column index on "end_row", +/// 0-based exclusive, rounded up to full screen lines. +/// When omitted include the whole line. +/// @return Dictionary containing text height information, with these keys: +/// - all: The total number of screen lines occupied by the range. +/// - fill: The number of diff filler or virtual lines among them. +/// +/// @see |virtcol()| for text width. +Dictionary nvim_win_text_height(Window window, Dict(win_text_height) *opts, Arena *arena, + Error *err) + FUNC_API_SINCE(12) +{ + Dictionary rv = arena_dict(arena, 2); + + win_T *const win = find_window_by_handle(window, err); + if (!win) { + return rv; + } + buf_T *const buf = win->w_buffer; + const linenr_T line_count = buf->b_ml.ml_line_count; + + linenr_T start_lnum = 1; + linenr_T end_lnum = line_count; + int64_t start_vcol = -1; + int64_t end_vcol = -1; + + bool oob = false; + + if (HAS_KEY(opts, win_text_height, start_row)) { + start_lnum = (linenr_T)normalize_index(buf, opts->start_row, false, &oob); + } + + if (HAS_KEY(opts, win_text_height, end_row)) { + end_lnum = (linenr_T)normalize_index(buf, opts->end_row, false, &oob); + } + + VALIDATE(!oob, "%s", "Line index out of bounds", { + return rv; + }); + VALIDATE((start_lnum <= end_lnum), "%s", "'start_row' is higher than 'end_row'", { + return rv; + }); + + if (HAS_KEY(opts, win_text_height, start_vcol)) { + VALIDATE(HAS_KEY(opts, win_text_height, start_row), + "%s", "'start_vcol' specified without 'start_row'", { + return rv; + }); + start_vcol = opts->start_vcol; + VALIDATE_RANGE((start_vcol >= 0 && start_vcol <= MAXCOL), "start_vcol", { + return rv; + }); + } + + if (HAS_KEY(opts, win_text_height, end_vcol)) { + VALIDATE(HAS_KEY(opts, win_text_height, end_row), + "%s", "'end_vcol' specified without 'end_row'", { + return rv; + }); + end_vcol = opts->end_vcol; + VALIDATE_RANGE((end_vcol >= 0 && end_vcol <= MAXCOL), "end_vcol", { + return rv; + }); + } + + if (start_lnum == end_lnum && start_vcol >= 0 && end_vcol >= 0) { + VALIDATE((start_vcol <= end_vcol), "%s", "'start_vcol' is higher than 'end_vcol'", { + return rv; + }); + } + + int64_t fill = 0; + int64_t all = win_text_height(win, start_lnum, start_vcol, end_lnum, end_vcol, &fill); + if (!HAS_KEY(opts, win_text_height, end_row)) { + const int64_t end_fill = win_get_fill(win, line_count + 1); + fill += end_fill; + all += end_fill; + } + PUT_C(rv, "all", INTEGER_OBJ(all)); + PUT_C(rv, "fill", INTEGER_OBJ(fill)); + return rv; +} diff --git a/src/nvim/api/window.h b/src/nvim/api/window.h index 0f36c12a9f..a5c9f86225 100644 --- a/src/nvim/api/window.h +++ b/src/nvim/api/window.h @@ -1,9 +1,8 @@ -#ifndef NVIM_API_WINDOW_H -#define NVIM_API_WINDOW_H +#pragma once -#include "nvim/api/private/defs.h" +#include "nvim/api/keysets_defs.h" // IWYU pragma: keep +#include "nvim/api/private/defs.h" // IWYU pragma: keep #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/window.h.generated.h" #endif -#endif // NVIM_API_WINDOW_H |