diff options
Diffstat (limited to 'src/nvim/api')
| -rw-r--r-- | src/nvim/api/autocmd.c | 707 | ||||
| -rw-r--r-- | src/nvim/api/autocmd.h | 11 | ||||
| -rw-r--r-- | src/nvim/api/buffer.c | 191 | ||||
| -rw-r--r-- | src/nvim/api/deprecated.c | 15 | ||||
| -rw-r--r-- | src/nvim/api/extmark.c | 162 | ||||
| -rw-r--r-- | src/nvim/api/keysets.lua | 73 | ||||
| -rw-r--r-- | src/nvim/api/private/converter.c | 23 | ||||
| -rw-r--r-- | src/nvim/api/private/dispatch.c | 20 | ||||
| -rw-r--r-- | src/nvim/api/private/helpers.c | 371 | ||||
| -rw-r--r-- | src/nvim/api/private/helpers.h | 19 | ||||
| -rw-r--r-- | src/nvim/api/vim.c | 171 | ||||
| -rw-r--r-- | src/nvim/api/vimscript.c | 7 | ||||
| -rw-r--r-- | src/nvim/api/window.c | 16 | 
13 files changed, 1612 insertions, 174 deletions
diff --git a/src/nvim/api/autocmd.c b/src/nvim/api/autocmd.c new file mode 100644 index 0000000000..9a8eccd22c --- /dev/null +++ b/src/nvim/api/autocmd.c @@ -0,0 +1,707 @@ +// This is an open source non-commercial project. Dear PVS-Studio, please check +// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + +#include <stdbool.h> +#include <stdio.h> + +#include "lauxlib.h" +#include "nvim/api/autocmd.h" +#include "nvim/api/private/defs.h" +#include "nvim/api/private/helpers.h" +#include "nvim/ascii.h" +#include "nvim/buffer.h" +#include "nvim/eval/typval.h" +#include "nvim/fileio.h" +#include "nvim/lua/executor.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "api/autocmd.c.generated.h" +#endif + +#define AUCMD_MAX_PATTERNS 256 + +// Copy string or array of strings into an empty array. +// Get the event number, unless it is an error. Then goto `goto_name`. +#define GET_ONE_EVENT(event_nr, event_str, goto_name) \ +  char_u *__next_ev; \ +  event_T event_nr = \ +    event_name2nr((char_u *)event_str.data.string.data, &__next_ev); \ +  if (event_nr >= NUM_EVENTS) { \ +    api_set_error(err, kErrorTypeValidation, "unexpected event"); \ +    goto goto_name; \ +  } + + +// ID for associating autocmds created via nvim_create_autocmd +// Used to delete autocmds from nvim_del_autocmd +static int64_t next_autocmd_id = 1; + +/// Get autocmds that match the requirements passed to {opts}. +/// +/// @param opts Optional Parameters: +///     - event : Name or list of name of events to match against +///     - group (string): Name of group to match against +///     - pattern: Pattern or list of patterns to match against +/// +/// @return A list of autocmds that match +Array nvim_get_autocmds(Dict(get_autocmds) *opts, Error *err) +  FUNC_API_SINCE(9) +{ +  // TODO(tjdevries): Would be cool to add nvim_get_autocmds({ id = ... }) + +  Array autocmd_list = ARRAY_DICT_INIT; +  char_u *pattern_filters[AUCMD_MAX_PATTERNS]; +  char_u pattern_buflocal[BUFLOCAL_PAT_LEN]; + +  bool event_set[NUM_EVENTS] = { false }; +  bool check_event = false; + +  int group = 0; + +  if (opts->group.type != kObjectTypeNil) { +    Object v = opts->group; +    if (v.type != kObjectTypeString) { +      api_set_error(err, kErrorTypeValidation, "group must be a string."); +      goto cleanup; +    } + +    group = augroup_find(v.data.string.data); + +    if (group < 0) { +      api_set_error(err, kErrorTypeValidation, "invalid augroup passed."); +      goto cleanup; +    } +  } + +  if (opts->event.type != kObjectTypeNil) { +    check_event = true; + +    Object v = opts->event; +    if (v.type == kObjectTypeString) { +      GET_ONE_EVENT(event_nr, v, cleanup); +      event_set[event_nr] = true; +    } else if (v.type == kObjectTypeArray) { +      FOREACH_ITEM(v.data.array, event_v, { +        if (event_v.type != kObjectTypeString) { +          api_set_error(err, +                        kErrorTypeValidation, +                        "Every event must be a string in 'event'"); +          goto cleanup; +        } + +        GET_ONE_EVENT(event_nr, event_v, cleanup); +        event_set[event_nr] = true; +      }) +    } else { +      api_set_error(err, +                    kErrorTypeValidation, +                    "Not a valid 'event' value. Must be a string or an array"); +      goto cleanup; +    } +  } + +  int pattern_filter_count = 0; +  if (opts->pattern.type != kObjectTypeNil) { +    Object v = opts->pattern; +    if (v.type == kObjectTypeString) { +      pattern_filters[pattern_filter_count] = (char_u *)v.data.string.data; +      pattern_filter_count += 1; +    } else if (v.type == kObjectTypeArray) { +      FOREACH_ITEM(v.data.array, item, { +        pattern_filters[pattern_filter_count] = (char_u *)item.data.string.data; +        pattern_filter_count += 1; +      }); +    } else { +      api_set_error(err, +                    kErrorTypeValidation, +                    "Not a valid 'pattern' value. Must be a string or an array"); +      goto cleanup; +    } + +    if (pattern_filter_count >= AUCMD_MAX_PATTERNS) { +      api_set_error(err, +                    kErrorTypeValidation, +                    "Too many patterns. Please limit yourself to less"); +      goto cleanup; +    } +  } + +  FOR_ALL_AUEVENTS(event) { +    if (check_event && !event_set[event]) { +      continue; +    } + +    for (AutoPat *ap = au_get_autopat_for_event(event); +         ap != NULL; +         ap = ap->next) { +      if (ap == NULL || ap->cmds == NULL) { +        continue; +      } + +      // Skip autocmds from invalid groups if passed. +      if (group != 0 && ap->group != group) { +        continue; +      } + +      // Skip 'pattern' from invalid patterns if passed. +      if (pattern_filter_count > 0) { +        bool passed = false; +        for (int i = 0; i < pattern_filter_count; i++) { +          assert(i < AUCMD_MAX_PATTERNS); +          assert(pattern_filters[i]); + +          char_u *pat = pattern_filters[i]; +          int patlen = (int)STRLEN(pat); + +          if (aupat_is_buflocal(pat, patlen)) { +            aupat_normalize_buflocal_pat(pattern_buflocal, +                                         pat, +                                         patlen, +                                         aupat_get_buflocal_nr(pat, patlen)); + +            pat = pattern_buflocal; +          } + +          if (strequal((char *)ap->pat, (char *)pat)) { +            passed = true; +            break; +          } +        } + +        if (!passed) { +          continue; +        } +      } + +      for (AutoCmd *ac = ap->cmds; ac != NULL; ac = ac->next) { +        if (aucmd_exec_is_deleted(ac->exec)) { +          continue; +        } + +        Dictionary autocmd_info = ARRAY_DICT_INIT; + +        if (ap->group != AUGROUP_DEFAULT) { +          PUT(autocmd_info, "group", INTEGER_OBJ(ap->group)); +        } + +        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)); +        } + +        PUT(autocmd_info, +            "command", +            STRING_OBJ(cstr_to_string(aucmd_exec_to_string(ac, ac->exec)))); + +        PUT(autocmd_info, +            "pattern", +            STRING_OBJ(cstr_to_string((char *)ap->pat))); + +        PUT(autocmd_info, +            "event", +            STRING_OBJ(cstr_to_string((char *)event_nr2name(event)))); + +        PUT(autocmd_info, "once", BOOLEAN_OBJ(ac->once)); + +        if (ap->buflocal_nr) { +          PUT(autocmd_info, "buflocal", BOOLEAN_OBJ(true)); +          PUT(autocmd_info, "buffer", INTEGER_OBJ(ap->buflocal_nr)); +        } else { +          PUT(autocmd_info, "buflocal", BOOLEAN_OBJ(false)); +        } + +        // TODO(sctx): It would be good to unify script_ctx to actually work with lua +        //  right now it's just super weird, and never really gives you the info that +        //  you would expect from this. +        // +        //  I think we should be able to get the line number, filename, etc. from lua +        //  when we're executing something, and it should be easy to then save that +        //  info here. +        // +        //  I think it's a big loss not getting line numbers of where options, autocmds, +        //  etc. are set (just getting "Sourced (lua)" or something is not that helpful. +        // +        //  Once we do that, we can put these into the autocmd_info, but I don't think it's +        //  useful to do that at this time. +        // +        // PUT(autocmd_info, "sid", INTEGER_OBJ(ac->script_ctx.sc_sid)); +        // PUT(autocmd_info, "lnum", INTEGER_OBJ(ac->script_ctx.sc_lnum)); + +        ADD(autocmd_list, DICTIONARY_OBJ(autocmd_info)); +      } +    } +  } + +cleanup: +  return autocmd_list; +} + +/// Create an autocmd. +/// +/// @param event The event or events to register this autocmd +///          Required keys: +///              event: string | ArrayOf(string) +/// +///              Examples: +///                 - event: "pat1,pat2,pat3", +///                 - event: "pat1" +///                 - event: { "pat1" } +///                 - event: { "pat1", "pat2", "pat3" } +/// +/// @param opts Optional Parameters: +///         - callback: (string|function) +///             - (string): The name of the viml function to execute when triggering this autocmd +///             - (function): The lua function to execute when triggering this autocmd +///             - NOTE: Cannot be used with {command} +///         - command: (string) command +///             - vimscript command +///             - NOTE: Cannot be used with {callback} +///                  Eg. command = "let g:value_set = v:true" +///         - pattern: (string|table) +///             - pattern or patterns to match against +///             - defaults to "*". +///             - NOTE: Cannot be used with {buffer} +///         - buffer: (bufnr) +///             - create a |autocmd-buflocal| autocmd. +///             - NOTE: Cannot be used with {pattern} +///         - group: (string) The augroup name +///         - once: (boolean) - See |autocmd-once| +///         - nested: (boolean) - See |autocmd-nested| +///         - desc: (string) - Description of the autocmd +/// +/// @returns opaque value to use with nvim_del_autocmd +Integer nvim_create_autocmd(uint64_t channel_id, Object event, Dict(create_autocmd) *opts, +                            Error *err) +  FUNC_API_SINCE(9) +{ +  int64_t autocmd_id = -1; + +  const char_u pattern_buflocal[BUFLOCAL_PAT_LEN]; +  int au_group = AUGROUP_DEFAULT; +  char *desc = NULL; + +  Array patterns = ARRAY_DICT_INIT; +  Array event_array = ARRAY_DICT_INIT; + +  AucmdExecutable aucmd = AUCMD_EXECUTABLE_INIT; +  Callback cb = CALLBACK_NONE; + + +  if (!unpack_string_or_array(&event_array, &event, "event", err)) { +    goto cleanup; +  } + +  if (opts->callback.type != kObjectTypeNil && opts->command.type != kObjectTypeNil) { +    api_set_error(err, kErrorTypeValidation, +                  "cannot pass both: 'callback' and 'command' for the same autocmd"); +    goto cleanup; +  } else if (opts->callback.type != kObjectTypeNil) { +    // TODO(tjdevries): It's possible we could accept callable tables, +    // but we don't do that many other places, so for the moment let's +    // not do that. + +    Object *callback = &opts->callback; +    if (callback->type == kObjectTypeLuaRef) { +      if (callback->data.luaref == LUA_NOREF) { +        api_set_error(err, +                      kErrorTypeValidation, +                      "must pass an actual value"); +        goto cleanup; +      } + +      if (!nlua_ref_is_function(callback->data.luaref)) { +        api_set_error(err, +                      kErrorTypeValidation, +                      "must pass a function for callback"); +        goto cleanup; +      } + +      cb.type = kCallbackLua; +      cb.data.luaref = api_new_luaref(callback->data.luaref); +    } else if (callback->type == kObjectTypeString) { +      cb.type = kCallbackFuncref; +      cb.data.funcref = vim_strsave((char_u *)callback->data.string.data); +    } else { +      api_set_error(err, +                    kErrorTypeException, +                    "'callback' must be a lua function or name of vim function"); +      goto cleanup; +    } + +    aucmd.type = CALLABLE_CB; +    aucmd.callable.cb = cb; +  } else if (opts->command.type != kObjectTypeNil) { +    Object *command = &opts->command; +    if (command->type == kObjectTypeString) { +      aucmd.type = CALLABLE_EX; +      aucmd.callable.cmd = vim_strsave((char_u *)command->data.string.data); +    } else { +      api_set_error(err, +                    kErrorTypeValidation, +                    "'command' must be a string"); +      goto cleanup; +    } +  } else { +    api_set_error(err, kErrorTypeValidation, "must pass one of: 'command', 'callback'"); +    goto cleanup; +  } + +  bool is_once = api_object_to_bool(opts->once, "once", false, err); +  bool is_nested = api_object_to_bool(opts->nested, "nested", false, err); + +  // TODO(tjdevries): accept number for namespace instead +  if (opts->group.type != kObjectTypeNil) { +    Object *v = &opts->group; +    if (v->type != kObjectTypeString) { +      api_set_error(err, kErrorTypeValidation, "'group' must be a string"); +      goto cleanup; +    } + +    au_group = augroup_find(v->data.string.data); + +    if (au_group == AUGROUP_ERROR) { +      api_set_error(err, +                    kErrorTypeException, +                    "invalid augroup: %s", v->data.string.data); + +      goto cleanup; +    } +  } + +  if (opts->pattern.type != kObjectTypeNil && opts->buffer.type != kObjectTypeNil) { +    api_set_error(err, kErrorTypeValidation, +                  "cannot pass both: 'pattern' and 'buffer' for the same autocmd"); +    goto cleanup; +  } else if (opts->pattern.type != kObjectTypeNil) { +    Object *v = &opts->pattern; + +    if (v->type == kObjectTypeString) { +      char_u *pat = (char_u *)v->data.string.data; +      size_t patlen = aucmd_pattern_length(pat); +      while (patlen) { +        ADD(patterns, STRING_OBJ(cbuf_to_string((char *)pat, patlen))); + +        pat = aucmd_next_pattern(pat, patlen); +        patlen = aucmd_pattern_length(pat); +      } +    } else if (v->type == kObjectTypeArray) { +      if (!check_autocmd_string_array(patterns, "pattern", err)) { +        goto cleanup; +      } + +      Array array = v->data.array; +      for (size_t i = 0; i < array.size; i++) { +        char_u *pat = (char_u *)array.items[i].data.string.data; +        size_t patlen = aucmd_pattern_length(pat); +        while (patlen) { +          ADD(patterns, STRING_OBJ(cbuf_to_string((char *)pat, patlen))); + +          pat = aucmd_next_pattern(pat, patlen); +          patlen = aucmd_pattern_length(pat); +        } +      } +    } else { +      api_set_error(err, +                    kErrorTypeValidation, +                    "'pattern' must be a string"); +      goto cleanup; +    } +  } else if (opts->buffer.type != kObjectTypeNil) { +    if (opts->buffer.type != kObjectTypeInteger) { +      api_set_error(err, +                    kErrorTypeValidation, +                    "'buffer' must be an integer"); +      goto cleanup; +    } + +    buf_T *buf = find_buffer_by_handle((Buffer)opts->buffer.data.integer, err); +    if (ERROR_SET(err)) { +      goto cleanup; +    } + +    snprintf((char *)pattern_buflocal, BUFLOCAL_PAT_LEN, "<buffer=%d>", (int)buf->handle); +    ADD(patterns, STRING_OBJ(cstr_to_string((char *)pattern_buflocal))); +  } + +  if (aucmd.type == CALLABLE_NONE) { +    api_set_error(err, +                  kErrorTypeValidation, +                  "'command' or 'callback' is required"); +    goto cleanup; +  } + +  if (opts->desc.type != kObjectTypeNil) { +    if (opts->desc.type == kObjectTypeString) { +      desc = opts->desc.data.string.data; +    } else { +      api_set_error(err, +                    kErrorTypeValidation, +                    "'desc' must be a string"); +      goto cleanup; +    } +  } + +  if (patterns.size == 0) { +    ADD(patterns, STRING_OBJ(STATIC_CSTR_TO_STRING("*"))); +  } + +  if (event_array.size == 0) { +    api_set_error(err, kErrorTypeValidation, "'event' is a required key"); +    goto cleanup; +  } + +  autocmd_id = next_autocmd_id++; +  FOREACH_ITEM(event_array, event_str, { +    GET_ONE_EVENT(event_nr, event_str, cleanup); + +    int retval; + +    for (size_t i = 0; i < patterns.size; i++) { +      Object pat = patterns.items[i]; + +      // See: TODO(sctx) +      WITH_SCRIPT_CONTEXT(channel_id, { +        retval = autocmd_register(autocmd_id, +                                  event_nr, +                                  (char_u *)pat.data.string.data, +                                  (int)pat.data.string.size, +                                  au_group, +                                  is_once, +                                  is_nested, +                                  desc, +                                  aucmd); +      }); + +      if (retval == FAIL) { +        api_set_error(err, kErrorTypeException, "Failed to set autocmd"); +        goto cleanup; +      } +    } +  }); + + +cleanup: +  aucmd_exec_free(&aucmd); +  api_free_array(event_array); +  api_free_array(patterns); + +  return autocmd_id; +} + +/// Delete an autocmd by {id}. Autocmds only return IDs when created +/// via the API. Will not error if called and no autocmds match +/// the {id}. +/// +/// @param id Integer The ID returned by nvim_create_autocmd +void nvim_del_autocmd(Integer id) +  FUNC_API_SINCE(9) +{ +  autocmd_delete_id(id); +} + +/// Create or get an augroup. +/// +/// To get an existing augroup ID, do: +/// <pre> +///     local id = vim.api.nvim_create_augroup(name, { +///         clear = false +///     }) +/// </pre> +/// +/// @param name String: The name of the augroup to create +/// @param opts Parameters +///                 - clear (bool): Whether to clear existing commands or not. +///                                 Defaults to true. +///                     See |autocmd-groups| +/// +/// @returns opaque value to use with nvim_del_augroup_by_id +Integer nvim_create_augroup(uint64_t channel_id, String name, Dict(create_augroup) *opts, +                            Error *err) +  FUNC_API_SINCE(9) +{ +  char *augroup_name = name.data; +  bool clear_autocmds = api_object_to_bool(opts->clear, "clear", true, err); + +  int augroup = -1; +  WITH_SCRIPT_CONTEXT(channel_id, { +    augroup = augroup_add(augroup_name); +    if (augroup == AUGROUP_ERROR) { +      api_set_error(err, kErrorTypeException, "Failed to set augroup"); +      return -1; +    } + +    if (clear_autocmds) { +      FOR_ALL_AUEVENTS(event) { +        aupat_del_for_event_and_group(event, augroup); +      } +    } +  }); + +  return augroup; +} + +/// Delete an augroup by {id}. {id} can only be returned when augroup was +/// created with |nvim_create_augroup|. +/// +/// NOTE: behavior differs from augroup-delete. +/// +/// When deleting an augroup, autocmds contained by this augroup will also be deleted and cleared. +/// This augroup will no longer exist +void nvim_del_augroup_by_id(Integer id) +  FUNC_API_SINCE(9) +{ +  char *name = augroup_name((int)id); +  augroup_del(name, false); +} + +/// Delete an augroup by {name}. +/// +/// NOTE: behavior differs from augroup-delete. +/// +/// When deleting an augroup, autocmds contained by this augroup will also be deleted and cleared. +/// This augroup will no longer exist +void nvim_del_augroup_by_name(String name) +  FUNC_API_SINCE(9) +{ +  augroup_del(name.data, false); +} + +/// Do one autocmd. +/// +/// @param event The event or events to execute +/// @param opts Optional Parameters: +///         - buffer (number) - buffer number +///             - NOTE: Cannot be used with {pattern} +///         - pattern (string|table) - optional, defaults to "*". +///             - NOTE: Cannot be used with {buffer} +///         - group (string) - autocmd group name +///         - modeline (boolean) - Default true, see |<nomodeline>| +void nvim_do_autocmd(Object event, Dict(do_autocmd) *opts, Error *err) +  FUNC_API_SINCE(9) +{ +  int au_group = AUGROUP_ALL; +  bool modeline = true; + +  buf_T *buf = curbuf; +  bool set_buf = false; + +  char_u *pattern = NULL; +  bool set_pattern = false; + +  Array event_array = ARRAY_DICT_INIT; + +  if (!unpack_string_or_array(&event_array, &event, "event", err)) { +    goto cleanup; +  } + +  if (opts->group.type != kObjectTypeNil) { +    if (opts->group.type != kObjectTypeString) { +      api_set_error(err, kErrorTypeValidation, "'group' must be a string"); +      goto cleanup; +    } + +    au_group = augroup_find(opts->group.data.string.data); + +    if (au_group == AUGROUP_ERROR) { +      api_set_error(err, +                    kErrorTypeException, +                    "invalid augroup: %s", opts->group.data.string.data); + +      goto cleanup; +    } +  } + +  if (opts->buffer.type != kObjectTypeNil) { +    Object buf_obj = opts->buffer; +    if (buf_obj.type != kObjectTypeInteger && buf_obj.type != kObjectTypeBuffer) { +      api_set_error(err, kErrorTypeException, "invalid buffer: %d", buf_obj.type); +      goto cleanup; +    } + +    buf = find_buffer_by_handle((Buffer)buf_obj.data.integer, err); +    set_buf = true; + +    if (ERROR_SET(err)) { +      goto cleanup; +    } +  } + +  if (opts->pattern.type != kObjectTypeNil) { +    if (opts->pattern.type != kObjectTypeString) { +      api_set_error(err, kErrorTypeValidation, "'pattern' must be a string"); +      goto cleanup; +    } + +    pattern = vim_strsave((char_u *)opts->pattern.data.string.data); +    set_pattern = true; +  } + +  modeline = api_object_to_bool(opts->modeline, "modeline", true, err); + +  if (set_pattern && set_buf) { +    api_set_error(err, kErrorTypeValidation, "must not set 'buffer' and 'pattern'"); +    goto cleanup; +  } + +  bool did_aucmd = false; +  FOREACH_ITEM(event_array, event_str, { +    GET_ONE_EVENT(event_nr, event_str, cleanup) + +    did_aucmd |= apply_autocmds_group(event_nr, pattern, NULL, true, au_group, buf, NULL); +  }) + +  if (did_aucmd && modeline) { +    do_modelines(0); +  } + +cleanup: +  api_free_array(event_array); +  XFREE_CLEAR(pattern); +} + +static bool check_autocmd_string_array(Array arr, char *k, Error *err) +{ +  for (size_t i = 0; i < arr.size; i++) { +    if (arr.items[i].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 = 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; +} + +static bool unpack_string_or_array(Array *array, Object *v, char *k, Error *err) +{ +  if (v->type == kObjectTypeString) { +    ADD(*array, copy_object(*v)); +  } else if (v->type == kObjectTypeArray) { +    if (!check_autocmd_string_array(v->data.array, k, err)) { +      return false; +    } +    *array = copy_array(v->data.array); +  } else { +    api_set_error(err, +                  kErrorTypeValidation, +                  "'%s' must be an array or a string.", +                  k); +    return false; +  } + +  return true; +} + +#undef GET_ONE_EVENT diff --git a/src/nvim/api/autocmd.h b/src/nvim/api/autocmd.h new file mode 100644 index 0000000000..f9432830d9 --- /dev/null +++ b/src/nvim/api/autocmd.h @@ -0,0 +1,11 @@ +#ifndef NVIM_API_AUTOCMD_H +#define NVIM_API_AUTOCMD_H + +#include <stdint.h> + +#include "nvim/api/private/defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "api/autocmd.h.generated.h" +#endif +#endif  // NVIM_API_AUTOCMD_H diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c index ce9c4e27ad..3dd647e76f 100644 --- a/src/nvim/api/buffer.c +++ b/src/nvim/api/buffer.c @@ -287,8 +287,8 @@ ArrayOf(String) nvim_buf_get_lines(uint64_t channel_id,    }    bool oob = false; -  start = normalize_index(buf, start, &oob); -  end = normalize_index(buf, end, &oob); +  start = normalize_index(buf, start, true, &oob); +  end = normalize_index(buf, end, true, &oob);    if (strict_indexing && oob) {      api_set_error(err, kErrorTypeValidation, "Index out of bounds"); @@ -374,15 +374,14 @@ void nvim_buf_set_lines(uint64_t channel_id, Buffer buffer, Integer start, Integ    }    bool oob = false; -  start = normalize_index(buf, start, &oob); -  end = normalize_index(buf, end, &oob); +  start = normalize_index(buf, start, true, &oob); +  end = normalize_index(buf, end, true, &oob);    if (strict_indexing && oob) {      api_set_error(err, kErrorTypeValidation, "Index out of bounds");      return;    } -    if (start > end) {      api_set_error(err,                    kErrorTypeValidation, @@ -554,13 +553,13 @@ void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, In    // check range is ordered and everything!    // start_row, end_row within buffer len (except add text past the end?) -  start_row = normalize_index(buf, start_row, &oob); +  start_row = normalize_index(buf, start_row, false, &oob);    if (oob || start_row == buf->b_ml.ml_line_count + 1) {      api_set_error(err, kErrorTypeValidation, "start_row out of bounds");      return;    } -  end_row = normalize_index(buf, end_row, &oob); +  end_row = normalize_index(buf, end_row, false, &oob);    if (oob || end_row == buf->b_ml.ml_line_count + 1) {      api_set_error(err, kErrorTypeValidation, "end_row out of bounds");      return; @@ -757,6 +756,108 @@ end:    try_end(err);  } +/// Gets a range from the buffer. +/// +/// This differs from |nvim_buf_get_lines()| in that it allows retrieving only +/// portions of a line. +/// +/// Indexing is zero-based. Column indices are end-exclusive. +/// +/// Prefer |nvim_buf_get_lines()| when retrieving entire lines. +/// +/// @param channel_id +/// @param buffer     Buffer handle, or 0 for current buffer +/// @param start_row  First line index +/// @param start_col  Starting byte offset of first line +/// @param end_row    Last line index +/// @param end_col    Ending byte offset of last line (exclusive) +/// @param opts       Optional parameters. Currently unused. +/// @param[out] err   Error details, if any +/// @return Array of lines, or empty array for unloaded buffer. +ArrayOf(String) nvim_buf_get_text(uint64_t channel_id, Buffer buffer, +                                  Integer start_row, Integer start_col, +                                  Integer end_row, Integer end_col, +                                  Dictionary opts, Error *err) +  FUNC_API_SINCE(9) +{ +  Array rv = ARRAY_DICT_INIT; + +  if (opts.size > 0) { +    api_set_error(err, kErrorTypeValidation, "opts dict isn't empty"); +    return rv; +  } + +  buf_T *buf = find_buffer_by_handle(buffer, err); + +  if (!buf) { +    return rv; +  } + +  // return sentinel value if the buffer isn't loaded +  if (buf->b_ml.ml_mfp == NULL) { +    return rv; +  } + +  bool oob = false; +  start_row = normalize_index(buf, start_row, false, &oob); +  end_row = normalize_index(buf, end_row, false, &oob); + +  if (oob) { +    api_set_error(err, kErrorTypeValidation, "Index out of bounds"); +    return rv; +  } + +  // nvim_buf_get_lines doesn't care if the start row is greater than the end +  // row (it will just return an empty array), but nvim_buf_get_text does in +  // order to maintain symmetry with nvim_buf_set_text. +  if (start_row > end_row) { +    api_set_error(err, kErrorTypeValidation, "start is higher than end"); +    return rv; +  } + +  bool replace_nl = (channel_id != VIML_INTERNAL_CALL); + +  if (start_row == end_row) { +    String line = buf_get_text(buf, start_row, start_col, end_col, replace_nl, err); +    if (ERROR_SET(err)) { +      return rv; +    } + +    ADD(rv, STRING_OBJ(line)); +    return rv; +  } + +  rv.size = (size_t)(end_row - start_row) + 1; +  rv.items = xcalloc(rv.size, sizeof(Object)); + +  rv.items[0] = STRING_OBJ(buf_get_text(buf, start_row, start_col, MAXCOL-1, replace_nl, err)); +  if (ERROR_SET(err)) { +    goto end; +  } + +  if (rv.size > 2) { +    Array tmp = ARRAY_DICT_INIT; +    tmp.items = &rv.items[1]; +    if (!buf_collect_lines(buf, rv.size - 2, start_row + 1, replace_nl, &tmp, err)) { +      goto end; +    } +  } + +  rv.items[rv.size-1] = STRING_OBJ(buf_get_text(buf, end_row, 0, end_col, replace_nl, err)); +  if (ERROR_SET(err)) { +    goto end; +  } + +end: +  if (ERROR_SET(err)) { +    api_free_array(rv); +    rv.size = 0; +    rv.items = NULL; +  } + +  return rv; +} +  /// Returns the byte offset of a line (0-indexed). |api-indexing|  ///  /// Line 1 (index=0) has offset 0. UTF-8 bytes are counted. EOL is one byte. @@ -835,7 +936,7 @@ Integer nvim_buf_get_changedtick(Buffer buffer, Error *err)  /// @param[out]  err   Error details, if any  /// @returns Array of maparg()-like dictionaries describing mappings.  ///          The "buffer" key holds the associated buffer handle. -ArrayOf(Dictionary) nvim_buf_get_keymap(Buffer buffer, String mode, Error *err) +ArrayOf(Dictionary) nvim_buf_get_keymap(uint64_t channel_id, Buffer buffer, String mode, Error *err)    FUNC_API_SINCE(3)  {    buf_T *buf = find_buffer_by_handle(buffer, err); @@ -844,7 +945,7 @@ ArrayOf(Dictionary) nvim_buf_get_keymap(Buffer buffer, String mode, Error *err)      return (Array)ARRAY_DICT_INIT;    } -  return keymap_array(mode, buf); +  return keymap_array(mode, buf, channel_id == LUA_INTERNAL_CALL);  }  /// Sets a buffer-local |mapping| for the given mode. @@ -852,11 +953,11 @@ ArrayOf(Dictionary) nvim_buf_get_keymap(Buffer buffer, String mode, Error *err)  /// @see |nvim_set_keymap()|  ///  /// @param  buffer  Buffer handle, or 0 for current buffer -void nvim_buf_set_keymap(Buffer buffer, String mode, String lhs, String rhs, Dict(keymap) *opts, -                         Error *err) +void nvim_buf_set_keymap(uint64_t channel_id, Buffer buffer, String mode, String lhs, String rhs, +                         Dict(keymap) *opts, Error *err)    FUNC_API_SINCE(6)  { -  modify_keymap(buffer, false, mode, lhs, rhs, opts, err); +  modify_keymap(channel_id, buffer, false, mode, lhs, rhs, opts, err);  }  /// Unmaps a buffer-local |mapping| for the given mode. @@ -864,11 +965,12 @@ void nvim_buf_set_keymap(Buffer buffer, String mode, String lhs, String rhs, Dic  /// @see |nvim_del_keymap()|  ///  /// @param  buffer  Buffer handle, or 0 for current buffer -void nvim_buf_del_keymap(Buffer buffer, String mode, String lhs, Error *err) +void nvim_buf_del_keymap(uint64_t channel_id, Buffer buffer, String mode, +                         String lhs, Error *err)    FUNC_API_SINCE(6)  {    String rhs = { .data = "", .size = 0 }; -  modify_keymap(buffer, true, mode, lhs, rhs, NULL, err); +  modify_keymap(channel_id, buffer, true, mode, lhs, rhs, NULL, err);  }  /// Gets a map of buffer-local |user-commands|. @@ -1273,6 +1375,63 @@ Object nvim_buf_call(Buffer buffer, LuaRef fun, Error *err)    return res;  } +/// Create a new user command |user-commands| in the given buffer. +/// +/// @param  buffer  Buffer handle, or 0 for current buffer. +/// @param[out] err Error details, if any. +/// @see nvim_add_user_command +void nvim_buf_add_user_command(Buffer buffer, String name, Object command, +                               Dict(user_command) *opts, Error *err) +  FUNC_API_SINCE(9) +{ +  buf_T *target_buf = find_buffer_by_handle(buffer, err); +  if (ERROR_SET(err)) { +    return; +  } + +  buf_T *save_curbuf = curbuf; +  curbuf = target_buf; +  add_user_command(name, command, opts, UC_BUFFER, err); +  curbuf = save_curbuf; +} + +/// Delete a buffer-local user-defined command. +/// +/// Only commands created with |:command-buffer| or +/// |nvim_buf_add_user_command()| can be deleted with this function. +/// +/// @param  buffer  Buffer handle, or 0 for current buffer. +/// @param  name    Name of the command to delete. +/// @param[out] err Error details, if any. +void nvim_buf_del_user_command(Buffer buffer, String name, Error *err) +  FUNC_API_SINCE(9) +{ +  garray_T *gap; +  if (buffer == -1) { +    gap = &ucmds; +  } else { +    buf_T *buf = find_buffer_by_handle(buffer, err); +    gap = &buf->b_ucmds; +  } + +  for (int i = 0; i < gap->ga_len; i++) { +    ucmd_T *cmd = USER_CMD_GA(gap, i); +    if (!STRCMP(name.data, cmd->uc_name)) { +      free_ucmd(cmd); + +      gap->ga_len -= 1; + +      if (i < gap->ga_len) { +        memmove(cmd, cmd + 1, (size_t)(gap->ga_len - i) * sizeof(ucmd_T)); +      } + +      return; +    } +  } + +  api_set_error(err, kErrorTypeException, "No such user-defined command: %s", name.data); +} +  Dictionary nvim__buf_stats(Buffer buffer, Error *err)  {    Dictionary rv = ARRAY_DICT_INIT; @@ -1329,11 +1488,11 @@ static void fix_cursor(linenr_T lo, linenr_T hi, linenr_T extra)  }  // Normalizes 0-based indexes to buffer line numbers -static int64_t normalize_index(buf_T *buf, int64_t index, bool *oob) +static int64_t normalize_index(buf_T *buf, int64_t index, bool end_exclusive, bool *oob)  {    int64_t line_count = buf->b_ml.ml_line_count;    // Fix if < 0 -  index = index < 0 ? line_count + index +1 : index; +  index = index < 0 ? line_count + index + (int)end_exclusive : index;    // Check for oob    if (index > line_count) { diff --git a/src/nvim/api/deprecated.c b/src/nvim/api/deprecated.c index 76b699800e..d2013c597c 100644 --- a/src/nvim/api/deprecated.c +++ b/src/nvim/api/deprecated.c @@ -22,11 +22,11 @@  /// @deprecated  /// @see nvim_exec -String nvim_command_output(String command, Error *err) +String nvim_command_output(uint64_t channel_id, String command, Error *err)    FUNC_API_SINCE(1)    FUNC_API_DEPRECATED_SINCE(7)  { -  return nvim_exec(command, true, err); +  return nvim_exec(channel_id, command, true, err);  }  /// @deprecated Use nvim_exec_lua() instead. @@ -130,7 +130,7 @@ Integer nvim_buf_set_virtual_text(Buffer buffer, Integer src_id, Integer line, A      return 0;    } -  uint64_t ns_id = src2ns(&src_id); +  uint32_t ns_id = src2ns(&src_id);    int width;    VirtText virt_text = parse_virt_text(chunks, err, &width); @@ -148,11 +148,12 @@ Integer nvim_buf_set_virtual_text(Buffer buffer, Integer src_id, Integer line, A      return src_id;    } -  Decoration *decor = xcalloc(1, sizeof(*decor)); -  decor->virt_text = virt_text; -  decor->virt_text_width = width; +  Decoration decor = DECORATION_INIT; +  decor.virt_text = virt_text; +  decor.virt_text_width = width; +  decor.priority = 0; -  extmark_set(buf, ns_id, NULL, (int)line, 0, -1, -1, decor, true, +  extmark_set(buf, ns_id, NULL, (int)line, 0, -1, -1, &decor, true,                false, kExtmarkNoUndo);    return src_id;  } diff --git a/src/nvim/api/extmark.c b/src/nvim/api/extmark.c index 742b953c2a..3a968f07ab 100644 --- a/src/nvim/api/extmark.c +++ b/src/nvim/api/extmark.c @@ -85,12 +85,12 @@ const char *describe_ns(NS ns_id)  }  // Is the Namespace in use? -static bool ns_initialized(uint64_t ns) +static bool ns_initialized(uint32_t ns)  {    if (ns < 1) {      return false;    } -  return ns < (uint64_t)next_namespace_id; +  return ns < (uint32_t)next_namespace_id;  } @@ -106,22 +106,55 @@ static Array extmark_to_array(ExtmarkInfo extmark, bool id, bool add_dict)    if (add_dict) {      Dictionary dict = ARRAY_DICT_INIT; +    PUT(dict, "right_gravity", BOOLEAN_OBJ(extmark.right_gravity)); +      if (extmark.end_row >= 0) {        PUT(dict, "end_row", INTEGER_OBJ(extmark.end_row));        PUT(dict, "end_col", INTEGER_OBJ(extmark.end_col)); +      PUT(dict, "end_right_gravity", BOOLEAN_OBJ(extmark.end_right_gravity));      } -    if (extmark.decor) { -      Decoration *decor = extmark.decor; -      if (decor->hl_id) { -        String name = cstr_to_string((const char *)syn_id2name(decor->hl_id)); -        PUT(dict, "hl_group", STRING_OBJ(name)); +    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 (kv_size(decor->virt_text)) { +      Array chunks = ARRAY_DICT_INIT; +      for (size_t i = 0; i < decor->virt_text.size; i++) { +        Array chunk = ARRAY_DICT_INIT; +        VirtTextChunk *vtc = &decor->virt_text.items[i]; +        ADD(chunk, STRING_OBJ(cstr_to_string(vtc->text))); +        if (vtc->hl_id > 0) { +          ADD(chunk, +              STRING_OBJ(cstr_to_string((const char *)syn_id2name(vtc->hl_id)))); +        } +        ADD(chunks, ARRAY_OBJ(chunk));        } -      if (kv_size(decor->virt_text)) { +      PUT(dict, "virt_text", ARRAY_OBJ(chunks)); +      PUT(dict, "virt_text_hide", BOOLEAN_OBJ(decor->virt_text_hide)); +      if (decor->virt_text_pos == kVTWinCol) { +        PUT(dict, "virt_text_win_col", INTEGER_OBJ(decor->col)); +      } +      PUT(dict, "virt_text_pos", +          STRING_OBJ(cstr_to_string(virt_text_pos_str[decor->virt_text_pos]))); +    } + +    if (kv_size(decor->virt_lines)) { +      Array all_chunks = ARRAY_DICT_INIT; +      bool virt_lines_leftcol = false; +      for (size_t i = 0; i < decor->virt_lines.size; i++) {          Array chunks = ARRAY_DICT_INIT; -        for (size_t i = 0; i < decor->virt_text.size; i++) { +        VirtText *vt = &decor->virt_lines.items[i].line; +        virt_lines_leftcol = decor->virt_lines.items[i].left_col; +        for (size_t j = 0; j < vt->size; j++) {            Array chunk = ARRAY_DICT_INIT; -          VirtTextChunk *vtc = &decor->virt_text.items[i]; +          VirtTextChunk *vtc = &vt->items[j];            ADD(chunk, STRING_OBJ(cstr_to_string(vtc->text)));            if (vtc->hl_id > 0) {              ADD(chunk, @@ -129,9 +162,14 @@ static Array extmark_to_array(ExtmarkInfo extmark, bool id, bool add_dict)            }            ADD(chunks, ARRAY_OBJ(chunk));          } -        PUT(dict, "virt_text", ARRAY_OBJ(chunks)); +        ADD(all_chunks, ARRAY_OBJ(chunks));        } +      PUT(dict, "virt_lines", ARRAY_OBJ(all_chunks)); +      PUT(dict, "virt_lines_above", BOOLEAN_OBJ(decor->virt_lines_above)); +      PUT(dict, "virt_lines_leftcol", BOOLEAN_OBJ(virt_lines_leftcol)); +    } +    if (decor->hl_id || kv_size(decor->virt_text)) {        PUT(dict, "priority", INTEGER_OBJ(decor->priority));      } @@ -166,7 +204,7 @@ ArrayOf(Integer) nvim_buf_get_extmark_by_id(Buffer buffer, Integer ns_id,      return rv;    } -  if (!ns_initialized((uint64_t)ns_id)) { +  if (!ns_initialized((uint32_t)ns_id)) {      api_set_error(err, kErrorTypeValidation, "Invalid ns_id");      return rv;    } @@ -191,7 +229,7 @@ ArrayOf(Integer) nvim_buf_get_extmark_by_id(Buffer buffer, Integer ns_id,    } -  ExtmarkInfo extmark = extmark_from_id(buf, (uint64_t)ns_id, (uint64_t)id); +  ExtmarkInfo extmark = extmark_from_id(buf, (uint32_t)ns_id, (uint32_t)id);    if (extmark.row < 0) {      return rv;    } @@ -252,7 +290,7 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e      return rv;    } -  if (!ns_initialized((uint64_t)ns_id)) { +  if (!ns_initialized((uint32_t)ns_id)) {      api_set_error(err, kErrorTypeValidation, "Invalid ns_id");      return rv;    } @@ -310,7 +348,7 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e    } -  ExtmarkInfoArray marks = extmark_get(buf, (uint64_t)ns_id, l_row, l_col, +  ExtmarkInfoArray marks = extmark_get(buf, (uint32_t)ns_id, l_row, l_col,                                         u_row, u_col, (int64_t)limit, reverse);    for (size_t i = 0; i < kv_size(marks); i++) { @@ -404,6 +442,10 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e  ///                   for left). Defaults to false.  ///               - priority: a priority value for the highlight group. For  ///                   example treesitter highlighting uses a value of 100. +///               - strict: boolean that indicates extmark should not be placed +///                   if the line or column value is past the end of the +///                   buffer or end of the line respectively. Defaults to true. +///  /// @param[out]  err   Error details, if any  /// @return Id of the created/updated extmark  Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer col, @@ -417,14 +459,14 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer      goto error;    } -  if (!ns_initialized((uint64_t)ns_id)) { +  if (!ns_initialized((uint32_t)ns_id)) {      api_set_error(err, kErrorTypeValidation, "Invalid ns_id");      goto error;    } -  uint64_t id = 0; +  uint32_t id = 0;    if (opts->id.type == kObjectTypeInteger && opts->id.data.integer > 0) { -    id = (uint64_t)opts->id.data.integer; +    id = (uint32_t)opts->id.data.integer;    } else if (HAS_KEY(opts->id)) {      api_set_error(err, kErrorTypeValidation, "id is not a positive integer");      goto error; @@ -441,9 +483,18 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer      opts->end_row = opts->end_line;    } +#define OPTION_TO_BOOL(target, name, val) \ +  target = api_object_to_bool(opts->name, #name, val, err); \ +  if (ERROR_SET(err)) { \ +    goto error; \ +  } + +  bool strict = true; +  OPTION_TO_BOOL(strict, strict, true); +    if (opts->end_row.type == kObjectTypeInteger) {      Integer val = opts->end_row.data.integer; -    if (val < 0 || val > buf->b_ml.ml_line_count) { +    if (val < 0 || (val > buf->b_ml.ml_line_count && strict)) {        api_set_error(err, kErrorTypeValidation, "end_row value outside range");        goto error;      } else { @@ -512,12 +563,6 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer      goto error;    } -#define OPTION_TO_BOOL(target, name, val) \ -  target = api_object_to_bool(opts->name, #name, val, err); \ -  if (ERROR_SET(err)) { \ -    goto error; \ -  } -    OPTION_TO_BOOL(decor.virt_text_hide, virt_text_hide, false);    OPTION_TO_BOOL(decor.hl_eol, hl_eol, false); @@ -596,16 +641,30 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer    bool ephemeral = false;    OPTION_TO_BOOL(ephemeral, ephemeral, false); -  if (line < 0 || line > buf->b_ml.ml_line_count) { +  if (line < 0) {      api_set_error(err, kErrorTypeValidation, "line value outside range");      goto error; +  } else if (line > buf->b_ml.ml_line_count) { +    if (strict) { +      api_set_error(err, kErrorTypeValidation, "line value outside range"); +      goto error; +    } else { +      line = buf->b_ml.ml_line_count; +    }    } else if (line < buf->b_ml.ml_line_count) {      len = ephemeral ? MAXCOL : STRLEN(ml_get_buf(buf, (linenr_T)line+1, false));    }    if (col == -1) {      col = (Integer)len; -  } else if (col < -1 || col > (Integer)len) { +  } else if (col > (Integer)len) { +    if (strict) { +      api_set_error(err, kErrorTypeValidation, "col value outside range"); +      goto error; +    } else { +      col = (Integer)len; +    } +  } else if (col < -1) {      api_set_error(err, kErrorTypeValidation, "col value outside range");      goto error;    } @@ -621,27 +680,17 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer        line2 = (int)line;      }      if (col2 > (Integer)len) { -      api_set_error(err, kErrorTypeValidation, "end_col value outside range"); -      goto error; +      if (strict) { +        api_set_error(err, kErrorTypeValidation, "end_col value outside range"); +        goto error; +      } else { +        col2 = (int)len; +      }      }    } else if (line2 >= 0) {      col2 = 0;    } -  Decoration *d = NULL; - -  if (ephemeral) { -    d = &decor; -  } else if (kv_size(decor.virt_text) || kv_size(decor.virt_lines) -             || decor.priority != DECOR_PRIORITY_BASE -             || decor.hl_eol) { -    // TODO(bfredl): this is a bit sketchy. eventually we should -    // have predefined decorations for both marks/ephemerals -    d = xcalloc(1, sizeof(*d)); -    *d = decor; -  } else if (decor.hl_id) { -    d = decor_hl(decor.hl_id); -  }    // TODO(bfredl): synergize these two branches even more    if (ephemeral && decor_state.buf == buf) { @@ -652,12 +701,8 @@ Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer        goto error;      } -    extmark_set(buf, (uint64_t)ns_id, &id, (int)line, (colnr_T)col, line2, col2, -                d, right_gravity, end_right_gravity, kExtmarkNoUndo); - -    if (kv_size(decor.virt_lines)) { -      redraw_buf_line_later(buf, MIN(buf->b_ml.ml_line_count, line+1+(decor.virt_lines_above?0:1))); -    } +    extmark_set(buf, (uint32_t)ns_id, &id, (int)line, (colnr_T)col, line2, col2, +                &decor, right_gravity, end_right_gravity, kExtmarkNoUndo);    }    return (Integer)id; @@ -682,23 +727,23 @@ Boolean nvim_buf_del_extmark(Buffer buffer, Integer ns_id, Integer id, Error *er    if (!buf) {      return false;    } -  if (!ns_initialized((uint64_t)ns_id)) { +  if (!ns_initialized((uint32_t)ns_id)) {      api_set_error(err, kErrorTypeValidation, "Invalid ns_id");      return false;    } -  return extmark_del(buf, (uint64_t)ns_id, (uint64_t)id); +  return extmark_del(buf, (uint32_t)ns_id, (uint32_t)id);  } -uint64_t src2ns(Integer *src_id) +uint32_t src2ns(Integer *src_id)  {    if (*src_id == 0) {      *src_id = nvim_create_namespace((String)STRING_INIT);    }    if (*src_id < 0) { -    return UINT64_MAX; +    return (((uint32_t)1) << 31) - 1;    } else { -    return (uint64_t)(*src_id); +    return (uint32_t)(*src_id);    }  } @@ -753,7 +798,7 @@ Integer nvim_buf_add_highlight(Buffer buffer, Integer ns_id, String hl_group, In      col_end = MAXCOL;    } -  uint64_t ns = src2ns(&ns_id); +  uint32_t ns = src2ns(&ns_id);    if (!(line < buf->b_ml.ml_line_count)) {      // safety check, we can't add marks outside the range @@ -773,10 +818,13 @@ Integer nvim_buf_add_highlight(Buffer buffer, Integer ns_id, String hl_group, In      end_line++;    } +  Decoration decor = DECORATION_INIT; +  decor.hl_id = hl_id; +    extmark_set(buf, ns, NULL,                (int)line, (colnr_T)col_start,                end_line, (colnr_T)col_end, -              decor_hl(hl_id), true, false, kExtmarkNoUndo); +              &decor, true, false, kExtmarkNoUndo);    return ns_id;  } @@ -808,7 +856,7 @@ void nvim_buf_clear_namespace(Buffer buffer, Integer ns_id, Integer line_start,    if (line_end < 0 || line_end > MAXLNUM) {      line_end = MAXLNUM;    } -  extmark_clear(buf, (ns_id < 0 ? 0 : (uint64_t)ns_id), +  extmark_clear(buf, (ns_id < 0 ? 0 : (uint32_t)ns_id),                  (int)line_start, 0,                  (int)line_end-1, MAXCOL);  } diff --git a/src/nvim/api/keysets.lua b/src/nvim/api/keysets.lua index f3e7f2f1dc..b05816f8ac 100644 --- a/src/nvim/api/keysets.lua +++ b/src/nvim/api/keysets.lua @@ -21,6 +21,7 @@ return {      "virt_lines";      "virt_lines_above";      "virt_lines_leftcol"; +    "strict";    };    keymap = {      "noremap"; @@ -29,10 +30,25 @@ return {      "script";      "expr";      "unique"; +    "callback"; +    "desc";    };    get_commands = {      "builtin";    }; +  user_command = { +    "addr"; +    "bang"; +    "bar"; +    "complete"; +    "count"; +    "desc"; +    "force"; +    "keepscript"; +    "nargs"; +    "range"; +    "register"; +  };    float_config = {      "row";      "col"; @@ -62,5 +78,62 @@ return {    option = {      "scope";    }; +  highlight = { +    "bold"; +    "standout"; +    "strikethrough"; +    "underline"; +    "undercurl"; +    "italic"; +    "reverse"; +    "nocombine"; +    "default"; +    "global"; +    "cterm"; +    "foreground"; "fg"; +    "background"; "bg"; +    "ctermfg"; +    "ctermbg"; +    "special"; "sp"; +    "link"; +    "fallback"; +    "blend"; +    "temp"; +  }; +  highlight_cterm = { +    "bold"; +    "standout"; +    "strikethrough"; +    "underline"; +    "undercurl"; +    "italic"; +    "reverse"; +    "nocombine"; +  }; +  -- Autocmds +  create_autocmd = { +    "buffer"; +    "callback"; +    "command"; +    "desc"; +    "group"; +    "once"; +    "nested"; +    "pattern"; +  }; +  do_autocmd = { +    "buffer"; +    "group"; +    "modeline"; +    "pattern"; +  }; +  get_autocmds = { +    "event"; +    "group"; +    "pattern"; +  }; +  create_augroup = { +    "clear"; +  };  } diff --git a/src/nvim/api/private/converter.c b/src/nvim/api/private/converter.c index 36da6c13a9..49e3cf7df7 100644 --- a/src/nvim/api/private/converter.c +++ b/src/nvim/api/private/converter.c @@ -10,6 +10,9 @@  #include "nvim/api/private/helpers.h"  #include "nvim/assert.h"  #include "nvim/eval/typval.h" +#include "nvim/eval/userfunc.h" +#include "nvim/lua/converter.h" +#include "nvim/lua/executor.h"  /// Helper structure for vim_to_object  typedef struct { @@ -54,7 +57,7 @@ typedef struct {      const size_t len_ = (size_t)(len); \      const blob_T *const blob_ = (blob); \      kvi_push(edata->stack, STRING_OBJ(((String) { \ -      .data = len_ != 0 ? xmemdup(blob_->bv_ga.ga_data, len_) : NULL, \ +      .data = len_ != 0 ? xmemdupz(blob_->bv_ga.ga_data, len_) : xstrdup(""), \        .size = len_ \      }))); \    } while (0) @@ -228,6 +231,14 @@ static inline void typval_encode_dict_end(EncodedData *const edata)  /// @return The converted value  Object vim_to_object(typval_T *obj)  { +  if (obj->v_type == VAR_FUNC) { +    ufunc_T *fp = find_func(obj->vval.v_string); +    assert(fp != NULL); +    if (fp->uf_cb == nlua_CFunction_func_call) { +      LuaRef ref = api_new_luaref(((LuaCFunctionState *)fp->uf_cb_state)->lua_callable.func_ref); +      return LUAREF_OBJ(ref); +    } +  }    EncodedData edata;    kvi_init(edata.stack);    const int evo_ret = encode_vim_to_object(&edata, obj, @@ -340,6 +351,16 @@ bool object_to_vim(Object obj, typval_T *tv, Error *err)      tv->vval.v_dict = dict;      break;    } + +  case kObjectTypeLuaRef: { +    LuaCFunctionState *state = xmalloc(sizeof(LuaCFunctionState)); +    state->lua_callable.func_ref = api_new_luaref(obj.data.luaref); +    char_u *name = register_cfunc(&nlua_CFunction_func_call, &nlua_CFunction_func_free, state); +    tv->v_type = VAR_FUNC; +    tv->vval.v_string = vim_strsave(name); +    break; +  } +    default:      abort();    } diff --git a/src/nvim/api/private/dispatch.c b/src/nvim/api/private/dispatch.c index 8ab7743e01..f670f06357 100644 --- a/src/nvim/api/private/dispatch.c +++ b/src/nvim/api/private/dispatch.c @@ -6,22 +6,30 @@  #include <msgpack.h>  #include <stdbool.h> -#include "nvim/api/buffer.h"  #include "nvim/api/deprecated.h" -#include "nvim/api/extmark.h"  #include "nvim/api/private/defs.h"  #include "nvim/api/private/dispatch.h"  #include "nvim/api/private/helpers.h" +#include "nvim/log.h" +#include "nvim/map.h" +#include "nvim/msgpack_rpc/helpers.h" +#include "nvim/vim.h" + +// =========================================================================== +// NEW API FILES MUST GO HERE. +// +//  When creating a new API file, you must include it here, +//  so that the dispatcher can find the C functions that you are creating! +// =========================================================================== +#include "nvim/api/autocmd.h" +#include "nvim/api/buffer.h" +#include "nvim/api/extmark.h"  #include "nvim/api/tabpage.h"  #include "nvim/api/ui.h"  #include "nvim/api/vim.h"  #include "nvim/api/vimscript.h"  #include "nvim/api/win_config.h"  #include "nvim/api/window.h" -#include "nvim/log.h" -#include "nvim/map.h" -#include "nvim/msgpack_rpc/helpers.h" -#include "nvim/vim.h"  static Map(String, MsgpackRpcRequestHandler) methods = MAP_INIT; diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index 9b407eab8b..35e8589255 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -139,10 +139,10 @@ bool try_end(Error *err)      got_int = false;    } else if (msg_list != NULL && *msg_list != NULL) {      int should_free; -    char *msg = (char *)get_exception_string(*msg_list, -                                             ET_ERROR, -                                             NULL, -                                             &should_free); +    char *msg = get_exception_string(*msg_list, +                                     ET_ERROR, +                                     NULL, +                                     &should_free);      api_set_error(err, kErrorTypeException, "%s", msg);      free_global_msglist(); @@ -396,19 +396,14 @@ void set_option_to(uint64_t channel_id, void *to, int type, String name, Object      stringval = value.data.string.data;    } -  const sctx_T save_current_sctx = current_sctx; -  current_sctx.sc_sid = -    channel_id == LUA_INTERNAL_CALL ? SID_LUA : SID_API_CLIENT; -  current_sctx.sc_lnum = 0; -  current_channel_id = channel_id; +  WITH_SCRIPT_CONTEXT(channel_id, { +    const int opt_flags = (type == SREQ_WIN && !(flags & SOPT_GLOBAL)) +                          ? 0 : (type == SREQ_GLOBAL) +                                ? OPT_GLOBAL : OPT_LOCAL; -  const int opt_flags = (type == SREQ_WIN && !(flags & SOPT_GLOBAL)) -                        ? 0 : (type == SREQ_GLOBAL) -                              ? OPT_GLOBAL : OPT_LOCAL; -  set_option_value_for(name.data, numval, stringval, -                       opt_flags, type, to, err); - -  current_sctx = save_current_sctx; +    set_option_value_for(name.data, numval, stringval, +                         opt_flags, type, to, err); +  });  } @@ -591,9 +586,10 @@ Array string_to_array(const String input, bool crlf)  /// @param  buffer    Buffer handle for a specific buffer, or 0 for the current  ///                   buffer, or -1 to signify global behavior ("all buffers")  /// @param  is_unmap  When true, removes the mapping that matches {lhs}. -void modify_keymap(Buffer buffer, bool is_unmap, String mode, String lhs, String rhs, -                   Dict(keymap) *opts, Error *err) +void modify_keymap(uint64_t channel_id, Buffer buffer, bool is_unmap, String mode, String lhs, +                   String rhs, Dict(keymap) *opts, Error *err)  { +  LuaRef lua_funcref = LUA_NOREF;    bool global = (buffer == -1);    if (global) {      buffer = 0; @@ -604,6 +600,12 @@ void modify_keymap(Buffer buffer, bool is_unmap, String mode, String lhs, String      return;    } +  const sctx_T save_current_sctx = api_set_sctx(channel_id); + +  if (opts != NULL && opts->callback.type == kObjectTypeLuaRef) { +    lua_funcref = opts->callback.data.luaref; +    opts->callback.data.luaref = LUA_NOREF; +  }    MapArguments parsed_args = MAP_ARGUMENTS_INIT;    if (opts) {  #define KEY_TO_BOOL(name) \ @@ -623,9 +625,13 @@ void modify_keymap(Buffer buffer, bool is_unmap, String mode, String lhs, String    parsed_args.buffer = !global;    set_maparg_lhs_rhs((char_u *)lhs.data, lhs.size, -                     (char_u *)rhs.data, rhs.size, +                     (char_u *)rhs.data, rhs.size, lua_funcref,                       CPO_TO_CPO_FLAGS, &parsed_args); - +  if (opts != NULL && opts->desc.type == kObjectTypeString) { +    parsed_args.desc = xstrdup(opts->desc.data.string.data); +  } else { +    parsed_args.desc = NULL; +  }    if (parsed_args.lhs_len > MAXMAPLEN) {      api_set_error(err, kErrorTypeValidation,  "LHS exceeds maximum map length: %s", lhs.data);      goto fail_and_free; @@ -658,7 +664,8 @@ void modify_keymap(Buffer buffer, bool is_unmap, String mode, String lhs, String    bool is_noremap = parsed_args.noremap;    assert(!(is_unmap && is_noremap)); -  if (!is_unmap && (parsed_args.rhs_len == 0 && !parsed_args.rhs_is_noop)) { +  if (!is_unmap && lua_funcref == LUA_NOREF +      && (parsed_args.rhs_len == 0 && !parsed_args.rhs_is_noop)) {      if (rhs.size == 0) {  // assume that the user wants RHS to be a <Nop>        parsed_args.rhs_is_noop = true;      } else { @@ -668,9 +675,13 @@ void modify_keymap(Buffer buffer, bool is_unmap, String mode, String lhs, String        api_set_error(err, kErrorTypeValidation, "Parsing of nonempty RHS failed: %s", rhs.data);        goto fail_and_free;      } -  } else if (is_unmap && parsed_args.rhs_len) { -    api_set_error(err, kErrorTypeValidation, -                  "Gave nonempty RHS in unmap command: %s", parsed_args.rhs); +  } else if (is_unmap && (parsed_args.rhs_len || parsed_args.rhs_lua != LUA_NOREF)) { +    if (parsed_args.rhs_len) { +      api_set_error(err, kErrorTypeValidation, +                    "Gave nonempty RHS in unmap command: %s", parsed_args.rhs); +    } else { +      api_set_error(err, kErrorTypeValidation, "Gave nonempty RHS for unmap"); +    }      goto fail_and_free;    } @@ -700,10 +711,13 @@ void modify_keymap(Buffer buffer, bool is_unmap, String mode, String lhs, String      goto fail_and_free;    }  // switch +  parsed_args.rhs_lua = LUA_NOREF;  // don't clear ref on success  fail_and_free: +  current_sctx = save_current_sctx; +  NLUA_CLEAR_REF(parsed_args.rhs_lua);    xfree(parsed_args.rhs);    xfree(parsed_args.orig_rhs); -  return; +  XFREE_CLEAR(parsed_args.desc);  }  /// Collects `n` buffer lines into array `l`, optionally replacing newlines @@ -742,6 +756,52 @@ bool buf_collect_lines(buf_T *buf, size_t n, int64_t start, bool replace_nl, Arr    return true;  } +/// Returns a substring of a buffer line +/// +/// @param buf          Buffer handle +/// @param lnum         Line number (1-based) +/// @param start_col    Starting byte offset into line (0-based) +/// @param end_col      Ending byte offset into line (0-based, exclusive) +/// @param replace_nl   Replace newlines ('\n') with null ('\0') +/// @param err          Error object +/// @return The text between start_col and end_col on line lnum of buffer buf +String buf_get_text(buf_T *buf, int64_t lnum, int64_t start_col, int64_t end_col, bool replace_nl, +                    Error *err) +{ +  String rv = STRING_INIT; + +  if (lnum >= MAXLNUM) { +    api_set_error(err, kErrorTypeValidation, "Line index is too high"); +    return rv; +  } + +  const char *bufstr = (char *)ml_get_buf(buf, (linenr_T)lnum, false); +  size_t line_length = strlen(bufstr); + +  start_col = start_col < 0 ? (int64_t)line_length + start_col + 1 : start_col; +  end_col = end_col < 0 ? (int64_t)line_length + end_col + 1 : end_col; + +  if (start_col >= MAXCOL || end_col >= MAXCOL) { +    api_set_error(err, kErrorTypeValidation, "Column index is too high"); +    return rv; +  } + +  if (start_col > end_col) { +    api_set_error(err, kErrorTypeValidation, "start_col must be less than end_col"); +    return rv; +  } + +  if ((size_t)start_col >= line_length) { +    return rv; +  } + +  rv = cstrn_to_string(&bufstr[start_col], (size_t)(end_col - start_col)); +  if (replace_nl) { +    strchrsub(rv.data, '\n', '\0'); +  } + +  return rv; +}  void api_free_string(String value)  { @@ -961,6 +1021,10 @@ Object copy_object(Object obj)    case kObjectTypeDictionary:      return DICTIONARY_OBJ(copy_dictionary(obj.data.dictionary)); + +  case kObjectTypeLuaRef: +    return LUAREF_OBJ(api_new_luaref(obj.data.luaref)); +    default:      abort();    } @@ -969,18 +1033,16 @@ Object copy_object(Object obj)  static void set_option_value_for(char *key, int numval, char *stringval, int opt_flags,                                   int opt_type, void *from, Error *err)  { -  win_T *save_curwin = NULL; -  tabpage_T *save_curtab = NULL; +  switchwin_T switchwin;    aco_save_T aco;    try_start();    switch (opt_type)    {    case SREQ_WIN: -    if (switch_win_noblock(&save_curwin, &save_curtab, (win_T *)from, -                           win_find_tabpage((win_T *)from), true) +    if (switch_win_noblock(&switchwin, (win_T *)from, win_find_tabpage((win_T *)from), true)          == FAIL) { -      restore_win_noblock(save_curwin, save_curtab, true); +      restore_win_noblock(&switchwin, true);        if (try_end(err)) {          return;        } @@ -990,7 +1052,7 @@ static void set_option_value_for(char *key, int numval, char *stringval, int opt        return;      }      set_option_value_err(key, numval, stringval, opt_flags, err); -    restore_win_noblock(save_curwin, save_curtab, true); +    restore_win_noblock(&switchwin, true);      break;    case SREQ_BUF:      aucmd_prepbuf(&aco, (buf_T *)from); @@ -1048,8 +1110,9 @@ void api_set_error(Error *err, ErrorType errType, const char *format, ...)  ///  /// @param  mode  The abbreviation for the mode  /// @param  buf  The buffer to get the mapping array. NULL for global +/// @param  from_lua  Whether it is called from internal lua api.  /// @returns Array of maparg()-like dictionaries describing mappings -ArrayOf(Dictionary) keymap_array(String mode, buf_T *buf) +ArrayOf(Dictionary) keymap_array(String mode, buf_T *buf, bool from_lua)  {    Array mappings = ARRAY_DICT_INIT;    dict_T *const dict = tv_dict_alloc(); @@ -1069,8 +1132,19 @@ ArrayOf(Dictionary) keymap_array(String mode, buf_T *buf)        // Check for correct mode        if (int_mode & current_maphash->m_mode) {          mapblock_fill_dict(dict, current_maphash, buffer_value, false); -        ADD(mappings, vim_to_object((typval_T[]) { { .v_type = VAR_DICT, .vval.v_dict = dict } })); - +        Object api_dict = vim_to_object((typval_T[]) { { .v_type = VAR_DICT, +                                                         .vval.v_dict = dict } }); +        if (from_lua) { +          Dictionary d = api_dict.data.dictionary; +          for (size_t j = 0; j < d.size; j++) { +            if (strequal("callback", d.items[j].key.data)) { +              d.items[j].value.type = kObjectTypeLuaRef; +              d.items[j].value.data.luaref = api_new_luaref((LuaRef)d.items[j].value.data.integer); +              break; +            } +          } +        } +        ADD(mappings, api_dict);          tv_dict_clear(dict);        }      } @@ -1108,7 +1182,7 @@ bool extmark_get_index_from_obj(buf_T *buf, Integer ns_id, Object obj, int        return false;      } -    ExtmarkInfo extmark = extmark_from_id(buf, (uint64_t)ns_id, (uint64_t)id); +    ExtmarkInfo extmark = extmark_from_id(buf, (uint32_t)ns_id, (uint32_t)id);      if (extmark.row >= 0) {        *row = extmark.row;        *col = extmark.col; @@ -1342,3 +1416,230 @@ const char *get_default_stl_hl(win_T *wp)      return "StatusLineNC";    }  } + +void add_user_command(String name, Object command, Dict(user_command) *opts, int flags, Error *err) +{ +  uint32_t argt = 0; +  long def = -1; +  cmd_addr_T addr_type_arg = ADDR_NONE; +  int compl = EXPAND_NOTHING; +  char *compl_arg = NULL; +  char *rep = NULL; +  LuaRef luaref = LUA_NOREF; +  LuaRef compl_luaref = LUA_NOREF; + +  if (!uc_validate_name(name.data)) { +    api_set_error(err, kErrorTypeValidation, "Invalid command name"); +    goto err; +  } + +  if (mb_islower(name.data[0])) { +    api_set_error(err, kErrorTypeValidation, "'name' must begin with an uppercase letter"); +    goto err; +  } + +  if (HAS_KEY(opts->range) && HAS_KEY(opts->count)) { +    api_set_error(err, kErrorTypeValidation, "'range' and 'count' are mutually exclusive"); +    goto err; +  } + +  if (opts->nargs.type == kObjectTypeInteger) { +    switch (opts->nargs.data.integer) { +    case 0: +      // Default value, nothing to do +      break; +    case 1: +      argt |= EX_EXTRA | EX_NOSPC | EX_NEEDARG; +      break; +    default: +      api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); +      goto err; +    } +  } else if (opts->nargs.type == kObjectTypeString) { +    if (opts->nargs.data.string.size > 1) { +      api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); +      goto err; +    } + +    switch (opts->nargs.data.string.data[0]) { +    case '*': +      argt |= EX_EXTRA; +      break; +    case '?': +      argt |= EX_EXTRA | EX_NOSPC; +      break; +    case '+': +      argt |= EX_EXTRA | EX_NEEDARG; +      break; +    default: +      api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); +      goto err; +    } +  } else if (HAS_KEY(opts->nargs)) { +    api_set_error(err, kErrorTypeValidation, "Invalid value for 'nargs'"); +    goto err; +  } + +  if (HAS_KEY(opts->complete) && !argt) { +    api_set_error(err, kErrorTypeValidation, "'complete' used without 'nargs'"); +    goto err; +  } + +  if (opts->range.type == kObjectTypeBoolean) { +    if (opts->range.data.boolean) { +      argt |= EX_RANGE; +      addr_type_arg = ADDR_LINES; +    } +  } else if (opts->range.type == kObjectTypeString) { +    if (opts->range.data.string.data[0] == '%' && opts->range.data.string.size == 1) { +      argt |= EX_RANGE | EX_DFLALL; +      addr_type_arg = ADDR_LINES; +    } else { +      api_set_error(err, kErrorTypeValidation, "Invalid value for 'range'"); +      goto err; +    } +  } else if (opts->range.type == kObjectTypeInteger) { +    argt |= EX_RANGE | EX_ZEROR; +    def = opts->range.data.integer; +    addr_type_arg = ADDR_LINES; +  } else if (HAS_KEY(opts->range)) { +    api_set_error(err, kErrorTypeValidation, "Invalid value for 'range'"); +    goto err; +  } + +  if (opts->count.type == kObjectTypeBoolean) { +    if (opts->count.data.boolean) { +      argt |= EX_COUNT | EX_ZEROR | EX_RANGE; +      addr_type_arg = ADDR_OTHER; +      def = 0; +    } +  } else if (opts->count.type == kObjectTypeInteger) { +    argt |= EX_COUNT | EX_ZEROR | EX_RANGE; +    addr_type_arg = ADDR_OTHER; +    def = opts->count.data.integer; +  } else if (HAS_KEY(opts->count)) { +    api_set_error(err, kErrorTypeValidation, "Invalid value for 'count'"); +    goto err; +  } + +  if (opts->addr.type == kObjectTypeString) { +    if (parse_addr_type_arg((char_u *)opts->addr.data.string.data, (int)opts->addr.data.string.size, +                            &addr_type_arg) != OK) { +      api_set_error(err, kErrorTypeValidation, "Invalid value for 'addr'"); +      goto err; +    } + +    if (addr_type_arg != ADDR_LINES) { +      argt |= EX_ZEROR; +    } +  } else if (HAS_KEY(opts->addr)) { +    api_set_error(err, kErrorTypeValidation, "Invalid value for 'addr'"); +    goto err; +  } + +  if (api_object_to_bool(opts->bang, "bang", false, err)) { +    argt |= EX_BANG; +  } else if (ERROR_SET(err)) { +    goto err; +  } + +  if (api_object_to_bool(opts->bar, "bar", false, err)) { +    argt |= EX_TRLBAR; +  } else if (ERROR_SET(err)) { +    goto err; +  } + + +  if (api_object_to_bool(opts->register_, "register", false, err)) { +    argt |= EX_REGSTR; +  } else if (ERROR_SET(err)) { +    goto err; +  } + +  if (api_object_to_bool(opts->keepscript, "keepscript", false, err)) { +    argt |= EX_KEEPSCRIPT; +  } else if (ERROR_SET(err)) { +    goto err; +  } + +  bool force = api_object_to_bool(opts->force, "force", true, err); +  if (ERROR_SET(err)) { +    goto err; +  } + +  if (opts->complete.type == kObjectTypeLuaRef) { +    compl = EXPAND_USER_LUA; +    compl_luaref = api_new_luaref(opts->complete.data.luaref); +  } else if (opts->complete.type == kObjectTypeString) { +    if (parse_compl_arg((char_u *)opts->complete.data.string.data, +                        (int)opts->complete.data.string.size, &compl, &argt, +                        (char_u **)&compl_arg) != OK) { +      api_set_error(err, kErrorTypeValidation, "Invalid value for 'complete'"); +      goto err; +    } +  } else if (HAS_KEY(opts->complete)) { +    api_set_error(err, kErrorTypeValidation, "Invalid value for 'complete'"); +    goto err; +  } + +  switch (command.type) { +  case kObjectTypeLuaRef: +    luaref = api_new_luaref(command.data.luaref); +    if (opts->desc.type == kObjectTypeString) { +      rep = opts->desc.data.string.data; +    } else { +      snprintf((char *)IObuff, IOSIZE, "<Lua function %d>", luaref); +      rep = (char *)IObuff; +    } +    break; +  case kObjectTypeString: +    rep = command.data.string.data; +    break; +  default: +    api_set_error(err, kErrorTypeValidation, "'command' must be a string or Lua function"); +    goto err; +  } + +  if (uc_add_command((char_u *)name.data, name.size, (char_u *)rep, argt, def, flags, +                     compl, (char_u *)compl_arg, compl_luaref, addr_type_arg, luaref, +                     force) != OK) { +    api_set_error(err, kErrorTypeException, "Failed to create user command"); +    goto err; +  } + +  return; + +err: +  NLUA_CLEAR_REF(luaref); +  NLUA_CLEAR_REF(compl_luaref); +} + +int find_sid(uint64_t channel_id) +{ +  switch (channel_id) { +  case VIML_INTERNAL_CALL: +  // TODO(autocmd): Figure out what this should be +  // return SID_API_CLIENT; +  case LUA_INTERNAL_CALL: +    return SID_LUA; +  default: +    return SID_API_CLIENT; +  } +} + +/// Sets sctx for API calls. +/// +/// @param channel_id     api clients id. Used to determine if it's a internal +///                       call or a rpc call. +/// @return returns       previous value of current_sctx. To be used +///                       to be used for restoring sctx to previous state. +sctx_T api_set_sctx(uint64_t channel_id) +{ +  sctx_T old_current_sctx = current_sctx; +  if (channel_id != VIML_INTERNAL_CALL) { +    current_sctx.sc_sid = +      channel_id == LUA_INTERNAL_CALL ? SID_LUA : SID_API_CLIENT; +    current_sctx.sc_lnum = 0; +  } +  return old_current_sctx; +} diff --git a/src/nvim/api/private/helpers.h b/src/nvim/api/private/helpers.h index 6d0aec9c90..bc7c2e6a60 100644 --- a/src/nvim/api/private/helpers.h +++ b/src/nvim/api/private/helpers.h @@ -138,10 +138,29 @@ typedef struct {        msg_list = saved_msg_list;  /* Restore the exception context. */ \    } while (0) +// Useful macro for executing some `code` for each item in an array. +#define FOREACH_ITEM(a, __foreach_item, code) \ +  for (size_t __foreach_i = 0; __foreach_i < (a).size; __foreach_i++) { \ +    Object __foreach_item = (a).items[__foreach_i]; \ +    code; \ +  } + +  #ifdef INCLUDE_GENERATED_DECLARATIONS  # include "api/private/helpers.h.generated.h"  # include "keysets.h.generated.h"  #endif +#define WITH_SCRIPT_CONTEXT(channel_id, code) \ +  do { \ +    const sctx_T save_current_sctx = current_sctx; \ +    current_sctx.sc_sid = \ +      (channel_id) == LUA_INTERNAL_CALL ? SID_LUA : SID_API_CLIENT; \ +    current_sctx.sc_lnum = 0; \ +    current_channel_id = channel_id; \ +    code; \ +    current_sctx = save_current_sctx; \ +  } while (0); +  #endif  // NVIM_API_PRIVATE_HELPERS_H diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index f81cdf9deb..9d0b096a36 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -19,6 +19,7 @@  #include "nvim/ascii.h"  #include "nvim/buffer.h"  #include "nvim/buffer_defs.h" +#include "nvim/charset.h"  #include "nvim/context.h"  #include "nvim/decoration.h"  #include "nvim/edit.h" @@ -30,7 +31,9 @@  #include "nvim/file_search.h"  #include "nvim/fileio.h"  #include "nvim/getchar.h" +#include "nvim/globals.h"  #include "nvim/highlight.h" +#include "nvim/highlight_defs.h"  #include "nvim/lua/executor.h"  #include "nvim/mark.h"  #include "nvim/memline.h" @@ -121,7 +124,13 @@ Dictionary nvim__get_hl_defs(Integer ns_id, Error *err)  /// Set a highlight group.  /// -/// @param ns_id number of namespace for this highlight +/// Note: unlike the `:highlight` command which can update a highlight group, +/// this function completely replaces the definition. For example: +/// `nvim_set_hl(0, 'Visual', {})` will clear the highlight group 'Visual'. +/// +/// @param ns_id number of namespace for this highlight. Use value 0 +///              to set a highlight group in the global (`:highlight`) +///              namespace.  /// @param name highlight group name, like ErrorMsg  /// @param val highlight definition map, like |nvim_get_hl_by_name|.  ///            in addition the following keys are also recognized: @@ -135,9 +144,8 @@ Dictionary nvim__get_hl_defs(Integer ns_id, Error *err)  ///                               same as attributes of gui color  /// @param[out] err Error details, if any  /// -/// TODO: ns_id = 0, should modify :highlight namespace -/// TODO val should take update vs reset flag -void nvim_set_hl(Integer ns_id, String name, Dictionary val, Error *err) +// TODO(bfredl): val should take update vs reset flag +void nvim_set_hl(Integer ns_id, String name, Dict(highlight) *val, Error *err)    FUNC_API_SINCE(7)  {    int hl_id = syn_check_group(name.data, (int)name.size); @@ -145,7 +153,7 @@ void nvim_set_hl(Integer ns_id, String name, Dictionary val, Error *err)    HlAttrs attrs = dict2hlattrs(val, true, &link_id, err);    if (!ERROR_SET(err)) { -    ns_hl_def((NS)ns_id, hl_id, attrs, link_id); +    ns_hl_def((NS)ns_id, hl_id, attrs, link_id, val);    }  } @@ -169,7 +177,7 @@ void nvim__set_hl_ns(Integer ns_id, Error *err)    // event path for redraws caused by "fast" events. This could tie in with    // better throttling of async events causing redraws, such as non-batched    // nvim_buf_set_extmark calls from async contexts. -  if (!provider_active && !ns_hl_changed) { +  if (!provider_active && !ns_hl_changed && must_redraw < NOT_VALID) {      multiqueue_put(main_loop.events, on_redraw_event, 0);    }    ns_hl_changed = true; @@ -187,21 +195,23 @@ static void on_redraw_event(void **argv)  /// On execution error: does not fail, but updates v:errmsg.  ///  /// To input sequences like <C-o> use |nvim_replace_termcodes()| (typically -/// with escape_csi=true) to replace |keycodes|, then pass the result to +/// with escape_ks=false) to replace |keycodes|, then pass the result to  /// nvim_feedkeys().  ///  /// Example:  /// <pre>  ///     :let key = nvim_replace_termcodes("<C-o>", v:true, v:false, v:true) -///     :call nvim_feedkeys(key, 'n', v:true) +///     :call nvim_feedkeys(key, 'n', v:false)  /// </pre>  ///  /// @param keys         to be typed  /// @param mode         behavior flags, see |feedkeys()| -/// @param escape_csi   If true, escape K_SPECIAL/CSI bytes in `keys` +/// @param escape_ks    If true, escape K_SPECIAL bytes in `keys` +///                     This should be false if you already used +///                     |nvim_replace_termcodes()|, and true otherwise.  /// @see feedkeys() -/// @see vim_strsave_escape_csi -void nvim_feedkeys(String keys, String mode, Boolean escape_csi) +/// @see vim_strsave_escape_ks +void nvim_feedkeys(String keys, String mode, Boolean escape_ks)    FUNC_API_SINCE(1)  {    bool remap = true; @@ -232,10 +242,10 @@ void nvim_feedkeys(String keys, String mode, Boolean escape_csi)    }    char *keys_esc; -  if (escape_csi) { -    // Need to escape K_SPECIAL and CSI before putting the string in the +  if (escape_ks) { +    // Need to escape K_SPECIAL before putting the string in the      // typeahead buffer. -    keys_esc = (char *)vim_strsave_escape_csi((char_u *)keys.data); +    keys_esc = (char *)vim_strsave_escape_ks((char_u *)keys.data);    } else {      keys_esc = keys.data;    } @@ -245,7 +255,7 @@ void nvim_feedkeys(String keys, String mode, Boolean escape_csi)      typebuf_was_filled = true;    } -  if (escape_csi) { +  if (escape_ks) {      xfree(keys_esc);    } @@ -383,7 +393,7 @@ error:  /// @param str        String to be converted.  /// @param from_part  Legacy Vim parameter. Usually true.  /// @param do_lt      Also translate <lt>. Ignored if `special` is false. -/// @param special    Replace |keycodes|, e.g. <CR> becomes a "\n" char. +/// @param special    Replace |keycodes|, e.g. <CR> becomes a "\r" char.  /// @see replace_termcodes  /// @see cpoptions  String nvim_replace_termcodes(String str, Boolean from_part, Boolean do_lt, Boolean special) @@ -491,8 +501,12 @@ ArrayOf(String) nvim_get_runtime_file(String name, Boolean all, Error *err)    int flags = DIP_DIRFILE | (all ? DIP_ALL : 0); -  do_in_runtimepath((char_u *)(name.size ? name.data : ""), -                    flags, find_runtime_cb, &rv); +  TRY_WRAP({ +    try_start(); +    do_in_runtimepath((char_u *)(name.size ? name.data : ""), +                      flags, find_runtime_cb, &rv); +    try_end(err); +  });    return rv;  } @@ -540,20 +554,19 @@ void nvim_set_current_dir(String dir, Error *err)      return;    } -  char string[MAXPATHL]; +  char_u string[MAXPATHL];    memcpy(string, dir.data, dir.size);    string[dir.size] = NUL;    try_start(); -  if (vim_chdir((char_u *)string)) { +  if (!changedir_func(string, kCdScopeGlobal)) {      if (!try_end(err)) {        api_set_error(err, kErrorTypeException, "Failed to change directory");      }      return;    } -  post_chdir(kCdScopeGlobal, true);    try_end(err);  } @@ -596,7 +609,19 @@ void nvim_del_current_line(Error *err)  Object nvim_get_var(String name, Error *err)    FUNC_API_SINCE(1)  { -  return dict_get_value(&globvardict, name, err); +  dictitem_T *di = tv_dict_find(&globvardict, name.data, (ptrdiff_t)name.size); +  if (di == NULL) {  // try to autoload script +    if (!script_autoload(name.data, name.size, false) || aborting()) { +      api_set_error(err, kErrorTypeValidation, "Key not found: %s", name.data); +      return (Object)OBJECT_INIT; +    } +    di = tv_dict_find(&globvardict, name.data, (ptrdiff_t)name.size); +  } +  if (di == NULL) { +    api_set_error(err, kErrorTypeValidation, "Key not found: %s", name.data); +    return (Object)OBJECT_INIT; +  } +  return vim_to_object(&di->di_tv);  }  /// Sets a global (g:) variable. @@ -662,7 +687,7 @@ Object nvim_get_option(String name, Error *err)  ///  /// @param name      Option name  /// @param opts      Optional parameters -///                  - scope: One of 'global' or 'local'. Analagous to +///                  - scope: One of 'global' or 'local'. Analogous to  ///                  |:setglobal| and |:setlocal|, respectively.  /// @param[out] err  Error details, if any  /// @return          Option value @@ -724,7 +749,7 @@ end:  /// @param name      Option name  /// @param value     New option value  /// @param opts      Optional parameters -///                  - scope: One of 'global' or 'local'. Analagous to +///                  - scope: One of 'global' or 'local'. Analogous to  ///                  |:setglobal| and |:setlocal|, respectively.  /// @param[out] err  Error details, if any  void nvim_set_option_value(String name, Object value, Dict(option) *opts, Error *err) @@ -1163,7 +1188,7 @@ static void term_close(void *data)  /// Send data to channel `id`. For a job, it writes it to the  /// stdin of the process. For the stdio channel |channel-stdio|,  /// it writes to Nvim's stdout.  For an internal terminal instance -/// (|nvim_open_term()|) it writes directly to terimal output. +/// (|nvim_open_term()|) it writes directly to terminal output.  /// See |channel-bytes| for more information.  ///  /// This function writes raw data, not RPC messages.  If the channel @@ -1538,10 +1563,10 @@ Dictionary nvim_get_mode(void)  /// @param  mode       Mode short-name ("n", "i", "v", ...)  /// @returns Array of maparg()-like dictionaries describing mappings.  ///          The "buffer" key is always zero. -ArrayOf(Dictionary) nvim_get_keymap(String mode) +ArrayOf(Dictionary) nvim_get_keymap(uint64_t channel_id, String mode)    FUNC_API_SINCE(3)  { -  return keymap_array(mode, NULL); +  return keymap_array(mode, NULL, channel_id == LUA_INTERNAL_CALL);  }  /// Sets a global |mapping| for the given mode. @@ -1561,18 +1586,23 @@ ArrayOf(Dictionary) nvim_get_keymap(String mode)  ///     nmap <nowait> <Space><NL> <Nop>  /// </pre>  /// +/// @param channel_id  /// @param  mode  Mode short-name (map command prefix: "n", "i", "v", "x", …)  ///               or "!" for |:map!|, or empty string for |:map|.  /// @param  lhs   Left-hand-side |{lhs}| of the mapping.  /// @param  rhs   Right-hand-side |{rhs}| of the mapping.  /// @param  opts  Optional parameters map. Accepts all |:map-arguments| -///               as keys excluding |<buffer>| but including |noremap|. +///               as keys excluding |<buffer>| but including |noremap| and "desc". +///               "desc" can be used to give a description to keymap. +///               When called from Lua, also accepts a "callback" key that takes +///               a Lua function to call when the mapping is executed.  ///               Values are Booleans. Unknown key is an error.  /// @param[out]   err   Error details, if any. -void nvim_set_keymap(String mode, String lhs, String rhs, Dict(keymap) *opts, Error *err) +void nvim_set_keymap(uint64_t channel_id, String mode, String lhs, String rhs, Dict(keymap) *opts, +                     Error *err)    FUNC_API_SINCE(6)  { -  modify_keymap(-1, false, mode, lhs, rhs, opts, err); +  modify_keymap(channel_id, -1, false, mode, lhs, rhs, opts, err);  }  /// Unmaps a global |mapping| for the given mode. @@ -1580,10 +1610,10 @@ void nvim_set_keymap(String mode, String lhs, String rhs, Dict(keymap) *opts, Er  /// To unmap a buffer-local mapping, use |nvim_buf_del_keymap()|.  ///  /// @see |nvim_set_keymap()| -void nvim_del_keymap(String mode, String lhs, Error *err) +void nvim_del_keymap(uint64_t channel_id, String mode, String lhs, Error *err)    FUNC_API_SINCE(6)  { -  nvim_buf_del_keymap(-1, mode, lhs, err); +  nvim_buf_del_keymap(channel_id, -1, mode, lhs, err);  }  /// Gets a map of global (non-buffer-local) Ex commands. @@ -1741,7 +1771,7 @@ Array nvim_list_chans(void)  /// 1. To perform several requests from an async context atomically, i.e.  ///    without interleaving redraws, RPC requests from other clients, or user  ///    interactions (however API methods may trigger autocommands or event -///    processing which have such side-effects, e.g. |:sleep| may wake timers). +///    processing which have such side effects, e.g. |:sleep| may wake timers).  /// 2. To minimize RPC overhead (roundtrips) of a sequence of many requests.  ///  /// @param channel_id @@ -1927,7 +1957,7 @@ Dictionary nvim__stats(void)    Dictionary rv = ARRAY_DICT_INIT;    PUT(rv, "fsync", INTEGER_OBJ(g_stats.fsync));    PUT(rv, "redraw", INTEGER_OBJ(g_stats.redraw)); -  PUT(rv, "lua_refcount", INTEGER_OBJ(nlua_refcount)); +  PUT(rv, "lua_refcount", INTEGER_OBJ(nlua_get_global_ref_count()));    return rv;  } @@ -2101,7 +2131,7 @@ void nvim__screenshot(String path)  } -/// Deletes a uppercase/file named mark. See |mark-motions|. +/// Deletes an uppercase/file named mark. See |mark-motions|.  ///  /// @note fails with error if a lowercase or buffer local named mark is used.  /// @param name       Mark name @@ -2231,7 +2261,7 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error *    Dictionary result = ARRAY_DICT_INIT;    int maxwidth; -  char fillchar = 0; +  int fillchar = 0;    Window window = 0;    bool use_tabline = false;    bool highlights = false; @@ -2246,12 +2276,12 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error *    }    if (HAS_KEY(opts->fillchar)) { -    if (opts->fillchar.type != kObjectTypeString || opts->fillchar.data.string.size > 1) { -      api_set_error(err, kErrorTypeValidation, "fillchar must be an ASCII character"); +    if (opts->fillchar.type != kObjectTypeString || opts->fillchar.data.string.size == 0 +        || char2cells(fillchar = utf_ptr2char((char_u *)opts->fillchar.data.string.data)) != 1 +        || (size_t)utf_char2len(fillchar) != opts->fillchar.data.string.size) { +      api_set_error(err, kErrorTypeValidation, "fillchar must be a single-width character");        return result;      } - -    fillchar = opts->fillchar.data.string.data[0];    }    if (HAS_KEY(opts->highlights)) { @@ -2278,11 +2308,16 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error *      fillchar = ' ';    } else {      wp = find_window_by_handle(window, err); + +    if (wp == NULL) { +      api_set_error(err, kErrorTypeException, "unknown winid %d", window); +      return result; +    }      ewp = wp;      if (fillchar == 0) {        int attr; -      fillchar = (char)fillchar_status(&attr, wp); +      fillchar = fillchar_status(&attr, wp);      }    } @@ -2310,7 +2345,7 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error *                                 sizeof(buf),                                 (char_u *)str.data,                                 false, -                               (char_u)fillchar, +                               fillchar,                                 maxwidth,                                 hltab_ptr,                                 NULL); @@ -2363,3 +2398,55 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error *    return result;  } + +/// Create a new user command |user-commands| +/// +/// {name} is the name of the new command. The name must begin with an uppercase letter. +/// +/// {command} is the replacement text or Lua function to execute. +/// +/// Example: +/// <pre> +///    :call nvim_add_user_command('SayHello', 'echo "Hello world!"', {}) +///    :SayHello +///    Hello world! +/// </pre> +/// +/// @param  name    Name of the new user command. Must begin with an uppercase letter. +/// @param  command Replacement command to execute when this user command is executed. When called +///                 from Lua, the command can also be a Lua function. The function is called with a +///                 single table argument that contains the following keys: +///                 - args: (string) The args passed to the command, if any |<args>| +///                 - fargs: (table) The args split by unescaped whitespace (when more than one +///                 argument is allowed), if any |<f-args>| +///                 - bang: (boolean) "true" if the command was executed with a ! modifier |<bang>| +///                 - line1: (number) The starting line of the command range |<line1>| +///                 - line2: (number) The final line of the command range |<line2>| +///                 - range: (number) The number of items in the command range: 0, 1, or 2 |<range>| +///                 - count: (number) Any count supplied |<count>| +///                 - reg: (string) The optional register, if specified |<reg>| +///                 - mods: (string) Command modifiers, if any |<mods>| +/// @param  opts    Optional command attributes. See |command-attributes| for more details. To use +///                 boolean attributes (such as |:command-bang| or |:command-bar|) set the value to +///                 "true". In addition to the string options listed in |:command-complete|, the +///                 "complete" key also accepts a Lua function which works like the "customlist" +///                 completion mode |:command-completion-customlist|. Additional parameters: +///                 - desc: (string) Used for listing the command when a Lua function is used for +///                                  {command}. +///                 - force: (boolean, default true) Override any previous definition. +/// @param[out] err Error details, if any. +void nvim_add_user_command(String name, Object command, Dict(user_command) *opts, Error *err) +  FUNC_API_SINCE(9) +{ +  add_user_command(name, command, opts, 0, err); +} + +/// Delete a user-defined command. +/// +/// @param  name    Name of the command to delete. +/// @param[out] err Error details, if any. +void nvim_del_user_command(String name, Error *err) +  FUNC_API_SINCE(9) +{ +  nvim_buf_del_user_command(-1, name, err); +} diff --git a/src/nvim/api/vimscript.c b/src/nvim/api/vimscript.c index 640144b234..4123674fe7 100644 --- a/src/nvim/api/vimscript.c +++ b/src/nvim/api/vimscript.c @@ -37,7 +37,7 @@  /// @param[out] err Error details (Vim error), if any  /// @return Output (non-error, non-shell |:!|) if `output` is true,  ///         else empty string. -String nvim_exec(String src, Boolean output, Error *err) +String nvim_exec(uint64_t channel_id, String src, Boolean output, Error *err)    FUNC_API_SINCE(7)  {    const int save_msg_silent = msg_silent; @@ -52,11 +52,16 @@ String nvim_exec(String src, Boolean output, Error *err)    if (output) {      msg_silent++;    } + +  const sctx_T save_current_sctx = api_set_sctx(channel_id); +    do_source_str(src.data, "nvim_exec()");    if (output) {      capture_ga = save_capture_ga;      msg_silent = save_msg_silent;    } + +  current_sctx = save_current_sctx;    try_end(err);    if (ERROR_SET(err)) { diff --git a/src/nvim/api/window.c b/src/nvim/api/window.c index 6e68c057dc..9c473ff724 100644 --- a/src/nvim/api/window.c +++ b/src/nvim/api/window.c @@ -39,7 +39,7 @@ Buffer nvim_win_get_buf(Window window, Error *err)    return win->w_buffer->handle;  } -/// Sets the current buffer in a window, without side-effects +/// Sets the current buffer in a window, without side effects  ///  /// @param window   Window handle, or 0 for current window  /// @param buffer   Buffer handle @@ -71,6 +71,7 @@ ArrayOf(Integer, 2) nvim_win_get_cursor(Window window, Error *err)  }  /// Sets the (1,0)-indexed cursor position in the window. |api-indexing| +/// Unlike |win_execute()| this scrolls the window.  ///  /// @param window   Window handle, or 0 for current window  /// @param pos      (row, col) tuple representing the new position @@ -118,6 +119,8 @@ void nvim_win_set_cursor(Window window, ArrayOf(Integer, 2) pos, Error *err)    update_topline_win(win);    redraw_later(win, VALID); +  redraw_for_cursorline(win); +  win->w_redr_status = true;  }  /// Gets the window height @@ -395,7 +398,7 @@ void nvim_win_hide(Window window, Error *err)    TryState tstate;    try_enter(&tstate);    if (tabpage == curtab) { -    win_close(win, false); +    win_close(win, false, false);    } else {      win_close_othertab(win, false, tabpage);    } @@ -455,17 +458,12 @@ Object nvim_win_call(Window window, LuaRef fun, Error *err)    }    tabpage_T *tabpage = win_find_tabpage(win); -  win_T *save_curwin; -  tabpage_T *save_curtab; -    try_start();    Object res = OBJECT_INIT; -  if (switch_win_noblock(&save_curwin, &save_curtab, win, tabpage, true) == -      OK) { +  WIN_EXECUTE(win, tabpage, {      Array args = ARRAY_DICT_INIT;      res = nlua_call_ref(fun, NULL, args, true, err); -  } -  restore_win_noblock(save_curwin, save_curtab, true); +  });    try_end(err);    return res;  }  | 
