neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

extmark.c (47909B)


      1 #include <assert.h>
      2 #include <lauxlib.h>
      3 #include <stdbool.h>
      4 #include <stdint.h>
      5 #include <string.h>
      6 
      7 #include "klib/kvec.h"
      8 #include "nvim/api/extmark.h"
      9 #include "nvim/api/keysets_defs.h"
     10 #include "nvim/api/private/defs.h"
     11 #include "nvim/api/private/dispatch.h"
     12 #include "nvim/api/private/helpers.h"
     13 #include "nvim/api/private/validate.h"
     14 #include "nvim/buffer_defs.h"
     15 #include "nvim/charset.h"
     16 #include "nvim/decoration.h"
     17 #include "nvim/decoration_defs.h"
     18 #include "nvim/decoration_provider.h"
     19 #include "nvim/drawscreen.h"
     20 #include "nvim/extmark.h"
     21 #include "nvim/globals.h"
     22 #include "nvim/grid.h"
     23 #include "nvim/highlight_group.h"
     24 #include "nvim/map_defs.h"
     25 #include "nvim/marktree.h"
     26 #include "nvim/marktree_defs.h"
     27 #include "nvim/mbyte.h"
     28 #include "nvim/memline.h"
     29 #include "nvim/memory.h"
     30 #include "nvim/memory_defs.h"
     31 #include "nvim/move.h"
     32 #include "nvim/pos_defs.h"
     33 #include "nvim/sign.h"
     34 
     35 #include "api/extmark.c.generated.h"
     36 
     37 void api_extmark_free_all_mem(void)
     38 {
     39  String name;
     40  map_foreach_key(&namespace_ids, name, {
     41    xfree(name.data);
     42  })
     43  map_destroy(String, &namespace_ids);
     44  set_destroy(uint32_t, &namespace_localscope);
     45 }
     46 
     47 /// Creates a new namespace or gets an existing one. [namespace]()
     48 ///
     49 /// Namespaces are used for buffer highlights and virtual text, see
     50 /// |nvim_buf_set_extmark()|.
     51 ///
     52 /// Namespaces can be named or anonymous. If `name` matches an existing
     53 /// namespace, the associated id is returned. If `name` is an empty string
     54 /// a new, anonymous namespace is created.
     55 ///
     56 /// @param name Namespace name or empty string
     57 /// @return Namespace id
     58 Integer nvim_create_namespace(String name)
     59  FUNC_API_SINCE(5)
     60 {
     61  handle_T id = map_get(String, int)(&namespace_ids, name);
     62  if (id > 0) {
     63    return (Integer)id;
     64  }
     65  id = next_namespace_id++;
     66  if (name.size > 0) {
     67    String name_alloc = copy_string(name, NULL);
     68    map_put(String, int)(&namespace_ids, name_alloc, id);
     69  }
     70  return (Integer)id;
     71 }
     72 
     73 /// Gets existing, non-anonymous |namespace|s.
     74 ///
     75 /// @return dict that maps from names to namespace ids.
     76 DictOf(Integer) nvim_get_namespaces(Arena *arena)
     77  FUNC_API_SINCE(5)
     78 {
     79  Dict retval = arena_dict(arena, map_size(&namespace_ids));
     80  String name;
     81  handle_T id;
     82 
     83  map_foreach(&namespace_ids, name, id, {
     84    PUT_C(retval, name.data, INTEGER_OBJ(id));
     85  })
     86 
     87  return retval;
     88 }
     89 
     90 const char *describe_ns(NS ns_id, const char *unknown)
     91 {
     92  String name;
     93  handle_T id;
     94  map_foreach(&namespace_ids, name, id, {
     95    if ((NS)id == ns_id && name.size) {
     96      return name.data;
     97    }
     98  })
     99  return unknown;
    100 }
    101 
    102 // Is the Namespace in use?
    103 bool ns_initialized(uint32_t ns)
    104 {
    105  if (ns < 1) {
    106    return false;
    107  }
    108  return ns < (uint32_t)next_namespace_id;
    109 }
    110 
    111 Array virt_text_to_array(VirtText vt, bool hl_name, Arena *arena)
    112 {
    113  Array chunks = arena_array(arena, kv_size(vt));
    114  for (size_t i = 0; i < kv_size(vt); i++) {
    115    size_t j = i;
    116    for (; j < kv_size(vt); j++) {
    117      if (kv_A(vt, j).text != NULL) {
    118        break;
    119      }
    120    }
    121 
    122    Array hl_array = arena_array(arena, i < j ? j - i + 1 : 0);
    123    for (; i < j; i++) {
    124      int hl_id = kv_A(vt, i).hl_id;
    125      if (hl_id >= 0) {
    126        ADD_C(hl_array, hl_group_name(hl_id, hl_name));
    127      }
    128    }
    129 
    130    char *text = kv_A(vt, i).text;
    131    int hl_id = kv_A(vt, i).hl_id;
    132    Array chunk = arena_array(arena, 2);
    133    ADD_C(chunk, CSTR_AS_OBJ(text));
    134    if (hl_array.size > 0) {
    135      if (hl_id >= 0) {
    136        ADD_C(hl_array, hl_group_name(hl_id, hl_name));
    137      }
    138      ADD_C(chunk, ARRAY_OBJ(hl_array));
    139    } else if (hl_id >= 0) {
    140      ADD_C(chunk, hl_group_name(hl_id, hl_name));
    141    }
    142    ADD_C(chunks, ARRAY_OBJ(chunk));
    143  }
    144  return chunks;
    145 }
    146 
    147 static Array extmark_to_array(MTPair extmark, bool id, bool add_dict, bool hl_name, Arena *arena)
    148 {
    149  MTKey start = extmark.start;
    150  Array rv = arena_array(arena, 4);
    151  if (id) {
    152    ADD_C(rv, INTEGER_OBJ((Integer)start.id));
    153  }
    154  ADD_C(rv, INTEGER_OBJ(start.pos.row));
    155  ADD_C(rv, INTEGER_OBJ(start.pos.col));
    156 
    157  if (add_dict) {
    158    // TODO(bfredl): coding the size like this is a bit fragile.
    159    // We want ArrayOf(Dict(set_extmark)) as the return type..
    160    Dict dict = arena_dict(arena, ARRAY_SIZE(set_extmark_table));
    161 
    162    PUT_C(dict, "ns_id", INTEGER_OBJ((Integer)start.ns));
    163 
    164    PUT_C(dict, "right_gravity", BOOLEAN_OBJ(mt_right(start)));
    165 
    166    if (mt_paired(start)) {
    167      PUT_C(dict, "end_row", INTEGER_OBJ(extmark.end_pos.row));
    168      PUT_C(dict, "end_col", INTEGER_OBJ(extmark.end_pos.col));
    169      PUT_C(dict, "end_right_gravity", BOOLEAN_OBJ(extmark.end_right_gravity));
    170    }
    171 
    172    if (mt_no_undo(start)) {
    173      PUT_C(dict, "undo_restore", BOOLEAN_OBJ(false));
    174    }
    175 
    176    if (mt_invalidate(start)) {
    177      PUT_C(dict, "invalidate", BOOLEAN_OBJ(true));
    178    }
    179    if (mt_invalid(start)) {
    180      PUT_C(dict, "invalid", BOOLEAN_OBJ(true));
    181    }
    182 
    183    decor_to_dict_legacy(&dict, mt_decor(start), hl_name, arena);
    184 
    185    ADD_C(rv, DICT_OBJ(dict));
    186  }
    187 
    188  return rv;
    189 }
    190 
    191 /// Gets the position (0-indexed) of an |extmark|.
    192 ///
    193 /// @param buffer  Buffer id, or 0 for current buffer
    194 /// @param ns_id  Namespace id from |nvim_create_namespace()|
    195 /// @param id  Extmark id
    196 /// @param opts  Optional parameters. Keys:
    197 ///          - details: Whether to include the details dict
    198 ///          - hl_name: Whether to include highlight group name instead of id, true if omitted
    199 /// @param[out] err   Error details, if any
    200 /// @return 0-indexed (row, col, details?) tuple or empty list () if extmark id was absent.  The
    201 /// optional `details` dictionary contains the same keys as `opts` in |nvim_buf_set_extmark()|,
    202 /// except for `id`, `conceal_lines` and `ephemeral`. It also contains the following keys:
    203 ///
    204 /// - ns_id: |namespace| id
    205 /// - invalid: boolean that indicates whether the mark is hidden because the entirety of
    206 /// text span range is deleted. See also the key `invalidate` in |nvim_buf_set_extmark()|.
    207 Tuple(Integer, Integer, *DictAs(extmark_details))
    208 nvim_buf_get_extmark_by_id(Buffer buffer, Integer ns_id, Integer id, Dict(get_extmark) * opts,
    209                           Arena *arena, Error *err)
    210  FUNC_API_SINCE(7)
    211 {
    212  Array rv = ARRAY_DICT_INIT;
    213 
    214  buf_T *buf = find_buffer_by_handle(buffer, err);
    215 
    216  if (!buf) {
    217    return rv;
    218  }
    219 
    220  VALIDATE_INT(ns_initialized((uint32_t)ns_id), "ns_id", ns_id, {
    221    return rv;
    222  });
    223 
    224  bool details = opts->details;
    225 
    226  bool hl_name = GET_BOOL_OR_TRUE(opts, get_extmark, hl_name);
    227 
    228  MTPair extmark = extmark_from_id(buf, (uint32_t)ns_id, (uint32_t)id);
    229  if (extmark.start.pos.row < 0) {
    230    return rv;
    231  }
    232  return extmark_to_array(extmark, false, details, hl_name, arena);
    233 }
    234 
    235 /// Gets |extmarks| in "traversal order" from a |charwise| region defined by
    236 /// buffer positions (inclusive, 0-indexed |api-indexing|).
    237 ///
    238 /// Region can be given as (row,col) tuples, or valid extmark ids (whose
    239 /// positions define the bounds). 0 and -1 are understood as (0,0) and (-1,-1)
    240 /// respectively, thus the following are equivalent:
    241 ///
    242 /// ```lua
    243 /// vim.api.nvim_buf_get_extmarks(0, my_ns, 0, -1, {})
    244 /// vim.api.nvim_buf_get_extmarks(0, my_ns, {0,0}, {-1,-1}, {})
    245 /// ```
    246 ///
    247 /// If `end` is less than `start`, marks are returned in reverse order.
    248 /// (Useful with `limit`, to get the first marks prior to a given position.)
    249 ///
    250 /// Note: For a reverse range, `limit` does not actually affect the traversed
    251 /// range, just how many marks are returned
    252 ///
    253 /// Note: when using extmark ranges (marks with a end_row/end_col position)
    254 /// the `overlap` option might be useful. Otherwise only the start position
    255 /// of an extmark will be considered.
    256 ///
    257 /// Note: legacy signs placed through the |:sign| commands are implemented
    258 /// as extmarks and will show up here. Their details array will contain a
    259 /// `sign_name` field.
    260 ///
    261 /// Example:
    262 ///
    263 /// ```lua
    264 /// local api = vim.api
    265 /// local pos = api.nvim_win_get_cursor(0)
    266 /// local ns  = api.nvim_create_namespace('my-plugin')
    267 /// -- Create new extmark at line 1, column 1.
    268 /// local m1  = api.nvim_buf_set_extmark(0, ns, 0, 0, {})
    269 /// -- Create new extmark at line 3, column 1.
    270 /// local m2  = api.nvim_buf_set_extmark(0, ns, 2, 0, {})
    271 /// -- Get extmarks only from line 3.
    272 /// local ms  = api.nvim_buf_get_extmarks(0, ns, {2,0}, {2,0}, {})
    273 /// -- Get all marks in this buffer + namespace.
    274 /// local all = api.nvim_buf_get_extmarks(0, ns, 0, -1, {})
    275 /// vim.print(ms)
    276 /// ```
    277 ///
    278 /// @param buffer  Buffer id, or 0 for current buffer
    279 /// @param ns_id  Namespace id from |nvim_create_namespace()| or -1 for all namespaces
    280 /// @param start  Start of range: a 0-indexed (row, col) or valid extmark id
    281 /// (whose position defines the bound). |api-indexing|
    282 /// @param end  End of range (inclusive): a 0-indexed (row, col) or valid
    283 /// extmark id (whose position defines the bound). |api-indexing|
    284 /// @param opts  Optional parameters. Keys:
    285 ///          - limit:  Maximum number of marks to return
    286 ///          - details: Whether to include the details dict
    287 ///          - hl_name: Whether to include highlight group name instead of id, true if omitted
    288 ///          - overlap: Also include marks which overlap the range, even if
    289 ///                     their start position is less than `start`
    290 ///          - type: Filter marks by type: "highlight", "sign", "virt_text" and "virt_lines"
    291 /// @param[out] err   Error details, if any
    292 /// @return List of `[extmark_id, row, col, details?]` tuples in "traversal order". For the
    293 /// `details` dictionary, see |nvim_buf_get_extmark_by_id()|.
    294 ArrayOf(DictAs(get_extmark_item)) nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start,
    295                                                        Object end,
    296                                                        Dict(get_extmarks) *opts, Arena *arena,
    297                                                        Error *err)
    298  FUNC_API_SINCE(7)
    299 {
    300  Array rv = ARRAY_DICT_INIT;
    301 
    302  buf_T *buf = find_buffer_by_handle(buffer, err);
    303  if (!buf) {
    304    return rv;
    305  }
    306 
    307  VALIDATE_INT(ns_id == -1 || ns_initialized((uint32_t)ns_id), "ns_id", ns_id, {
    308    return rv;
    309  });
    310 
    311  bool details = opts->details;
    312  bool hl_name = GET_BOOL_OR_TRUE(opts, get_extmarks, hl_name);
    313 
    314  ExtmarkType type = kExtmarkNone;
    315  if (HAS_KEY(opts, get_extmarks, type)) {
    316    if (strequal(opts->type.data, "sign")) {
    317      type = kExtmarkSign;
    318    } else if (strequal(opts->type.data, "virt_text")) {
    319      type = kExtmarkVirtText;
    320    } else if (strequal(opts->type.data, "virt_lines")) {
    321      type = kExtmarkVirtLines;
    322    } else if (strequal(opts->type.data, "highlight")) {
    323      type = kExtmarkHighlight;
    324    } else {
    325      VALIDATE_EXP(false, "type", "sign, virt_text, virt_lines or highlight", opts->type.data, {
    326        return rv;
    327      });
    328    }
    329  }
    330 
    331  Integer limit = HAS_KEY(opts, get_extmarks, limit) ? opts->limit : -1;
    332 
    333  if (limit == 0) {
    334    return rv;
    335  } else if (limit < 0) {
    336    limit = INT64_MAX;
    337  }
    338 
    339  int l_row;
    340  colnr_T l_col;
    341  if (!extmark_get_index_from_obj(buf, ns_id, start, &l_row, &l_col, err)) {
    342    return rv;
    343  }
    344 
    345  int u_row;
    346  colnr_T u_col;
    347  if (!extmark_get_index_from_obj(buf, ns_id, end, &u_row, &u_col, err)) {
    348    return rv;
    349  }
    350 
    351  size_t rv_limit = (size_t)limit;
    352  bool reverse = l_row > u_row || (l_row == u_row && l_col > u_col);
    353  if (reverse) {
    354    limit = INT64_MAX;  // limit the return value instead
    355    int row = l_row;
    356    l_row = u_row;
    357    u_row = row;
    358    colnr_T col = l_col;
    359    l_col = u_col;
    360    u_col = col;
    361  }
    362 
    363  // note: ns_id=-1 allowed, represented as UINT32_MAX
    364  ExtmarkInfoArray marks = extmark_get(buf, (uint32_t)ns_id, l_row, l_col, u_row,
    365                                       u_col, (int64_t)limit, type, opts->overlap);
    366 
    367  rv = arena_array(arena, MIN(kv_size(marks), rv_limit));
    368  if (reverse) {
    369    for (int i = (int)kv_size(marks) - 1; i >= 0 && kv_size(rv) < rv_limit; i--) {
    370      ADD_C(rv, ARRAY_OBJ(extmark_to_array(kv_A(marks, i), true, details, hl_name, arena)));
    371    }
    372  } else {
    373    for (size_t i = 0; i < kv_size(marks); i++) {
    374      ADD_C(rv, ARRAY_OBJ(extmark_to_array(kv_A(marks, i), true, details, hl_name, arena)));
    375    }
    376  }
    377 
    378  kv_destroy(marks);
    379  return rv;
    380 }
    381 
    382 /// Creates or updates an |extmark|.
    383 ///
    384 /// By default a new extmark is created when no id is passed in, but it is also
    385 /// possible to create a new mark by passing in a previously unused id or move
    386 /// an existing mark by passing in its id. The caller must then keep track of
    387 /// existing and unused ids itself. (Useful over RPC, to avoid waiting for the
    388 /// return value.)
    389 ///
    390 /// Using the optional arguments, it is possible to use this to highlight
    391 /// a range of text, and also to associate virtual text to the mark.
    392 ///
    393 /// If present, the position defined by `end_col` and `end_row` should be after
    394 /// the start position in order for the extmark to cover a range.
    395 /// An earlier end position is not an error, but then it behaves like an empty
    396 /// range (no highlighting).
    397 ///
    398 /// @param buffer  Buffer id, or 0 for current buffer
    399 /// @param ns_id  Namespace id from |nvim_create_namespace()|
    400 /// @param line  Line where to place the mark, 0-based. |api-indexing|
    401 /// @param col  Column where to place the mark, 0-based. |api-indexing|
    402 /// @param opts  Optional parameters.
    403 ///               - id : id of the extmark to edit.
    404 ///               - end_row : ending line of the mark, 0-based inclusive.
    405 ///               - end_col : ending col of the mark, 0-based exclusive.
    406 ///               - hl_group : highlight group used for the text range. This and below
    407 ///                   highlight groups can be supplied either as a string or as an integer,
    408 ///                   the latter of which can be obtained using |nvim_get_hl_id_by_name()|.
    409 ///
    410 ///                   Multiple highlight groups can be stacked by passing an array (highest
    411 ///                   priority last).
    412 ///               - hl_eol : when true, for a multiline highlight covering the
    413 ///                          EOL of a line, continue the highlight for the rest
    414 ///                          of the screen line (just like for diff and
    415 ///                          cursorline highlight).
    416 ///               - virt_text : [](virtual-text) to link to this mark.
    417 ///                   A list of `[text, highlight]` tuples, each representing a
    418 ///                   text chunk with specified highlight. `highlight` element
    419 ///                   can either be a single highlight group, or an array of
    420 ///                   multiple highlight groups that will be stacked
    421 ///                   (highest priority last).
    422 ///               - virt_text_pos : position of virtual text. Possible values:
    423 ///                 - "eol": right after eol character (default).
    424 ///                 - "eol_right_align": display right aligned in the window
    425 ///                                      unless the virtual text is longer than
    426 ///                                      the space available. If the virtual
    427 ///                                      text is too long, it is truncated to
    428 ///                                      fit in the window after the EOL
    429 ///                                      character. If the line is wrapped, the
    430 ///                                      virtual text is shown after the end of
    431 ///                                      the line rather than the previous
    432 ///                                      screen line.
    433 ///                 - "overlay": display over the specified column, without
    434 ///                              shifting the underlying text.
    435 ///                 - "right_align": display right aligned in the window.
    436 ///                 - "inline": display at the specified column, and
    437 ///                             shift the buffer text to the right as needed.
    438 ///               - virt_text_win_col : position the virtual text at a fixed
    439 ///                                     window column (starting from the first
    440 ///                                     text column of the screen line) instead
    441 ///                                     of "virt_text_pos".
    442 ///               - virt_text_hide : hide the virtual text when the background
    443 ///                                  text is selected or hidden because of
    444 ///                                  scrolling with 'nowrap' or 'smoothscroll'.
    445 ///                                  Currently only affects "overlay" virt_text.
    446 ///               - virt_text_repeat_linebreak : repeat the virtual text on
    447 ///                                              wrapped lines.
    448 ///               - hl_mode : control how highlights are combined with the
    449 ///                           highlights of the text. Currently only affects
    450 ///                           virt_text highlights, but might affect `hl_group`
    451 ///                           in later versions.
    452 ///                 - "replace": only show the virt_text color. This is the default.
    453 ///                 - "combine": combine with background text color.
    454 ///                 - "blend": blend with background text color.
    455 ///                            Not supported for "inline" virt_text.
    456 ///
    457 ///               - virt_lines : virtual lines to add next to this mark
    458 ///                   This should be an array over lines, where each line in
    459 ///                   turn is an array over `[text, highlight]` tuples. In
    460 ///                   general, buffer and window options do not affect the
    461 ///                   display of the text. In particular 'wrap'
    462 ///                   and 'linebreak' options do not take effect, so
    463 ///                   the number of extra screen lines will always match
    464 ///                   the size of the array. However the 'tabstop' buffer
    465 ///                   option is still used for hard tabs. By default lines are
    466 ///                   placed below the buffer line containing the mark.
    467 ///
    468 ///               - virt_lines_above: place virtual lines above instead.
    469 ///               - virt_lines_leftcol: Place virtual lines in the leftmost
    470 ///                                     column of the window, bypassing
    471 ///                                     sign and number columns.
    472 ///               - virt_lines_overflow: controls how to handle virtual lines wider
    473 ///                   than the window. Currently takes the one of the following values:
    474 ///                 - "trunc": truncate virtual lines on the right (default).
    475 ///                 - "scroll": virtual lines can scroll horizontally with 'nowrap',
    476 ///                    otherwise the same as "trunc".
    477 ///               - ephemeral : for use with |nvim_set_decoration_provider()|
    478 ///                   callbacks. The mark will only be used for the current
    479 ///                   redraw cycle, and not be permanently stored in the buffer.
    480 ///               - right_gravity : boolean that indicates the direction
    481 ///                   the extmark will be shifted in when new text is inserted
    482 ///                   (true for right, false for left). Defaults to true.
    483 ///               - end_right_gravity : boolean that indicates the direction
    484 ///                   the extmark end position (if it exists) will be shifted
    485 ///                   in when new text is inserted (true for right, false
    486 ///                   for left). Defaults to false.
    487 ///               - undo_restore : Restore the exact position of the mark
    488 ///                   if text around the mark was deleted and then restored by undo.
    489 ///                   Defaults to true.
    490 ///               - invalidate : boolean that indicates whether to hide the
    491 ///                   extmark if the entirety of its range is deleted. For
    492 ///                   hidden marks, an "invalid" key is added to the "details"
    493 ///                   array of |nvim_buf_get_extmarks()| and family. If
    494 ///                   "undo_restore" is false, the extmark is deleted instead.
    495 ///               - priority: a priority value for the highlight group, sign
    496 ///                   attribute or virtual text. For virtual text, item with
    497 ///                   highest priority is drawn last. For example treesitter
    498 ///                   highlighting uses a value of 100.
    499 ///               - strict: boolean that indicates extmark should not be placed
    500 ///                   if the line or column value is past the end of the
    501 ///                   buffer or end of the line respectively. Defaults to true.
    502 ///               - sign_text: string of length 1-2 used to display in the
    503 ///                   sign column.
    504 ///               - sign_hl_group: highlight group used for the sign column text.
    505 ///               - number_hl_group: highlight group used for the number column.
    506 ///               - line_hl_group: highlight group used for the whole line.
    507 ///               - cursorline_hl_group: highlight group used for the sign
    508 ///                   column text when the cursor is on the same line as the
    509 ///                   mark and 'cursorline' is enabled.
    510 ///               - conceal: string which should be either empty or a single
    511 ///                   character. Enable concealing similar to |:syn-conceal|.
    512 ///                   When a character is supplied it is used as |:syn-cchar|.
    513 ///                   "hl_group" is used as highlight for the cchar if provided,
    514 ///                   otherwise it defaults to |hl-Conceal|.
    515 ///               - conceal_lines: string which should be empty. When
    516 ///                   provided, lines in the range are not drawn at all
    517 ///                   (according to 'conceallevel'); the next unconcealed line
    518 ///                   is drawn instead.
    519 ///               - spell: boolean indicating that spell checking should be
    520 ///                   performed within this extmark
    521 ///               - ui_watched: boolean that indicates the mark should be drawn
    522 ///                   by a UI. When set, the UI will receive win_extmark events.
    523 ///                   Note: the mark is positioned by virt_text attributes. Can be
    524 ///                   used together with virt_text.
    525 ///               - url: A URL to associate with this extmark. In the TUI, the OSC 8 control
    526 ///                   sequence is used to generate a clickable hyperlink to this URL.
    527 ///
    528 /// @param[out]  err   Error details, if any
    529 /// @return Id of the created/updated extmark
    530 Integer nvim_buf_set_extmark(Buffer buffer, Integer ns_id, Integer line, Integer col,
    531                             Dict(set_extmark) *opts, Error *err)
    532  FUNC_API_SINCE(7)
    533 {
    534  DecorHighlightInline hl = DECOR_HIGHLIGHT_INLINE_INIT;
    535  // TODO(bfredl): in principle signs with max one (1) hl group and max 4 bytes of text.
    536  // should be a candidate for inlining as well.
    537  DecorSignHighlight sign = DECOR_SIGN_HIGHLIGHT_INIT;
    538  DecorVirtText virt_text = DECOR_VIRT_TEXT_INIT;
    539  DecorVirtText virt_lines = DECOR_VIRT_LINES_INIT;
    540  char *url = NULL;
    541  bool has_hl = false;
    542  bool has_hl_multiple = false;
    543 
    544  buf_T *buf = find_buffer_by_handle(buffer, err);
    545  if (!buf) {
    546    goto error;
    547  }
    548 
    549  VALIDATE_INT(ns_initialized((uint32_t)ns_id), "ns_id", ns_id, {
    550    goto error;
    551  });
    552 
    553  uint32_t id = 0;
    554  if (HAS_KEY(opts, set_extmark, id)) {
    555    VALIDATE_EXP((opts->id > 0), "id", "positive Integer", NULL, {
    556      goto error;
    557    });
    558 
    559    id = (uint32_t)opts->id;
    560  }
    561 
    562  int line2 = -1;
    563  bool did_end_line = false;
    564 
    565  // For backward compatibility we support "end_line" as an alias for "end_row"
    566  if (HAS_KEY(opts, set_extmark, end_line)) {
    567    VALIDATE(!HAS_KEY(opts, set_extmark, end_row),
    568             "%s", "cannot use both 'end_row' and 'end_line'", {
    569      goto error;
    570    });
    571 
    572    opts->end_row = opts->end_line;
    573    did_end_line = true;
    574  }
    575 
    576  bool strict = GET_BOOL_OR_TRUE(opts, set_extmark, strict);
    577 
    578  if (HAS_KEY(opts, set_extmark, end_row) || did_end_line) {
    579    Integer val = opts->end_row;
    580    VALIDATE_RANGE((val >= 0 && !(val > buf->b_ml.ml_line_count && strict)), "end_row", {
    581      goto error;
    582    });
    583    line2 = (int)val;
    584  }
    585 
    586  colnr_T col2 = -1;
    587  if (HAS_KEY(opts, set_extmark, end_col)) {
    588    Integer val = opts->end_col;
    589    VALIDATE_RANGE((val >= 0 && val <= MAXCOL), "end_col", {
    590      goto error;
    591    });
    592    col2 = (int)val;
    593  }
    594 
    595  if (HAS_KEY(opts, set_extmark, hl_group)) {
    596    if (opts->hl_group.type == kObjectTypeArray) {
    597      Array arr = opts->hl_group.data.array;
    598      if (arr.size >= 1) {
    599        hl.hl_id = object_to_hl_id(arr.items[0], "hl_group item", err);
    600        if (ERROR_SET(err)) {
    601          goto error;
    602        }
    603      }
    604      for (size_t i = 1; i < arr.size; i++) {
    605        int hl_id = object_to_hl_id(arr.items[i], "hl_group item", err);
    606        if (ERROR_SET(err)) {
    607          goto error;
    608        }
    609        if (hl_id) {
    610          has_hl_multiple = true;
    611        }
    612      }
    613    } else {
    614      hl.hl_id = object_to_hl_id(opts->hl_group, "hl_group", err);
    615      if (ERROR_SET(err)) {
    616        goto error;
    617      }
    618    }
    619    has_hl = hl.hl_id > 0;
    620  }
    621 
    622  sign.hl_id = (int)opts->sign_hl_group;
    623  sign.cursorline_hl_id = (int)opts->cursorline_hl_group;
    624  sign.number_hl_id = (int)opts->number_hl_group;
    625  sign.line_hl_id = (int)opts->line_hl_group;
    626 
    627  if (sign.hl_id || sign.cursorline_hl_id || sign.number_hl_id || sign.line_hl_id) {
    628    sign.flags |= kSHIsSign;
    629  }
    630 
    631  if (HAS_KEY(opts, set_extmark, conceal)) {
    632    hl.flags |= kSHConceal;
    633    has_hl = true;
    634    if (opts->conceal.size > 0) {
    635      int ch;
    636      hl.conceal_char = utfc_ptr2schar(opts->conceal.data, &ch);
    637      VALIDATE(hl.conceal_char && vim_isprintc(ch), "%s", "conceal char has to be printable", {
    638        goto error;
    639      });
    640    }
    641  }
    642 
    643  if (HAS_KEY(opts, set_extmark, conceal_lines)) {
    644    hl.flags |= kSHConcealLines;
    645    has_hl = true;
    646    if (opts->conceal_lines.size > 0) {
    647      VALIDATE(*opts->conceal_lines.data == NUL, "%s", "conceal_lines has to be an empty string", {
    648        goto error;
    649      });
    650    }
    651  }
    652 
    653  if (HAS_KEY(opts, set_extmark, virt_text)) {
    654    virt_text.data.virt_text = parse_virt_text(opts->virt_text, err, &virt_text.width);
    655    if (ERROR_SET(err)) {
    656      goto error;
    657    }
    658  }
    659 
    660  if (HAS_KEY(opts, set_extmark, virt_text_pos)) {
    661    String str = opts->virt_text_pos;
    662    if (strequal("eol", str.data)) {
    663      virt_text.pos = kVPosEndOfLine;
    664    } else if (strequal("overlay", str.data)) {
    665      virt_text.pos = kVPosOverlay;
    666    } else if (strequal("right_align", str.data)) {
    667      virt_text.pos = kVPosRightAlign;
    668    } else if (strequal("eol_right_align", str.data)) {
    669      virt_text.pos = kVPosEndOfLineRightAlign;
    670    } else if (strequal("inline", str.data)) {
    671      virt_text.pos = kVPosInline;
    672    } else {
    673      VALIDATE_S(false, "virt_text_pos", str.data, {
    674        goto error;
    675      });
    676    }
    677  }
    678 
    679  if (HAS_KEY(opts, set_extmark, virt_text_win_col)) {
    680    virt_text.col = (int)opts->virt_text_win_col;
    681    virt_text.pos = kVPosWinCol;
    682  }
    683 
    684  hl.flags |= opts->hl_eol ? kSHHlEol : 0;
    685  virt_text.flags |= ((opts->virt_text_hide ? kVTHide : 0)
    686                      | (opts->virt_text_repeat_linebreak ? kVTRepeatLinebreak : 0));
    687 
    688  if (HAS_KEY(opts, set_extmark, hl_mode)) {
    689    String str = opts->hl_mode;
    690    if (strequal("replace", str.data)) {
    691      virt_text.hl_mode = kHlModeReplace;
    692    } else if (strequal("combine", str.data)) {
    693      virt_text.hl_mode = kHlModeCombine;
    694    } else if (strequal("blend", str.data)) {
    695      if (virt_text.pos == kVPosInline) {
    696        VALIDATE(false, "%s", "cannot use 'blend' hl_mode with inline virtual text", {
    697          goto error;
    698        });
    699      }
    700      virt_text.hl_mode = kHlModeBlend;
    701    } else {
    702      VALIDATE_S(false, "hl_mode", str.data, {
    703        goto error;
    704      });
    705    }
    706  }
    707 
    708  int virt_lines_flags = opts->virt_lines_leftcol ? kVLLeftcol : 0;
    709  if (HAS_KEY(opts, set_extmark, virt_lines_overflow)) {
    710    String str = opts->virt_lines_overflow;
    711    if (strequal("scroll", str.data)) {
    712      virt_lines_flags |= kVLScroll;
    713    } else if (!strequal("trunc", str.data)) {
    714      VALIDATE_S(false, "virt_lines_overflow", str.data, {
    715        goto error;
    716      });
    717    }
    718  }
    719 
    720  if (HAS_KEY(opts, set_extmark, virt_lines)) {
    721    Array a = opts->virt_lines;
    722    for (size_t j = 0; j < a.size; j++) {
    723      VALIDATE_T("virt_text_line", kObjectTypeArray, a.items[j].type, {
    724        goto error;
    725      });
    726      int dummig;
    727      VirtText jtem = parse_virt_text(a.items[j].data.array, err, &dummig);
    728      kv_push(virt_lines.data.virt_lines, ((struct virt_line){ jtem, virt_lines_flags }));
    729      if (ERROR_SET(err)) {
    730        goto error;
    731      }
    732    }
    733  }
    734 
    735  virt_lines.flags |= opts->virt_lines_above ? kVTLinesAbove : 0;
    736 
    737  if (HAS_KEY(opts, set_extmark, priority)) {
    738    VALIDATE_RANGE((opts->priority >= 0 && opts->priority <= UINT16_MAX), "priority", {
    739      goto error;
    740    });
    741    hl.priority = (DecorPriority)opts->priority;
    742    sign.priority = (DecorPriority)opts->priority;
    743    virt_text.priority = (DecorPriority)opts->priority;
    744    virt_lines.priority = (DecorPriority)opts->priority;
    745  }
    746 
    747  if (HAS_KEY(opts, set_extmark, sign_text)) {
    748    sign.text[0] = 0;
    749    VALIDATE_S(init_sign_text(NULL, sign.text, opts->sign_text.data), "sign_text", "", {
    750      goto error;
    751    });
    752    sign.flags |= kSHIsSign;
    753  }
    754 
    755  bool right_gravity = GET_BOOL_OR_TRUE(opts, set_extmark, right_gravity);
    756 
    757  // Only error out if they try to set end_right_gravity without
    758  // setting end_col or end_row
    759  VALIDATE(!(line2 == -1 && col2 == -1 && HAS_KEY(opts, set_extmark, end_right_gravity)),
    760           "%s", "cannot set end_right_gravity without end_row or end_col", {
    761    goto error;
    762  });
    763 
    764  colnr_T len = 0;
    765 
    766  if (HAS_KEY(opts, set_extmark, spell)) {
    767    hl.flags |= (opts->spell) ? kSHSpellOn : kSHSpellOff;
    768    has_hl = true;
    769  }
    770 
    771  if (HAS_KEY(opts, set_extmark, url)) {
    772    url = string_to_cstr(opts->url);
    773    has_hl = true;
    774  }
    775 
    776  if (opts->ui_watched) {
    777    hl.flags |= kSHUIWatched;
    778    if (virt_text.pos == kVPosOverlay) {
    779      // TODO(bfredl): in a revised interface this should be the default.
    780      hl.flags |= kSHUIWatchedOverlay;
    781    }
    782    has_hl = true;
    783  }
    784 
    785  VALIDATE_RANGE((line >= 0), "line", {
    786    goto error;
    787  });
    788 
    789  if (line > buf->b_ml.ml_line_count) {
    790    VALIDATE_RANGE(!strict, "line", {
    791      goto error;
    792    });
    793    line = buf->b_ml.ml_line_count;
    794  } else if (line < buf->b_ml.ml_line_count) {
    795    len = opts->ephemeral ? MAXCOL : ml_get_buf_len(buf, (linenr_T)line + 1);
    796  }
    797 
    798  if (col == -1) {
    799    col = len;
    800  } else if (col > len) {
    801    VALIDATE_RANGE(!strict, "col", {
    802      goto error;
    803    });
    804    col = len;
    805  } else if (col < -1) {
    806    VALIDATE_RANGE(false, "col", {
    807      goto error;
    808    });
    809  }
    810 
    811  if (col2 >= 0) {
    812    if (line2 >= 0 && line2 < buf->b_ml.ml_line_count) {
    813      len = opts->ephemeral ? MAXCOL : ml_get_buf_len(buf, (linenr_T)line2 + 1);
    814    } else if (line2 == buf->b_ml.ml_line_count) {
    815      // We are trying to add an extmark past final newline
    816      len = 0;
    817    } else {
    818      // reuse len from before
    819      line2 = (int)line;
    820    }
    821    if (col2 > len) {
    822      VALIDATE_RANGE(!strict, "end_col", {
    823        goto error;
    824      });
    825      col2 = len;
    826    }
    827  } else if (line2 >= 0) {
    828    col2 = 0;
    829  }
    830 
    831  if (opts->ephemeral && decor_state.win && decor_state.win->w_buffer == buf) {
    832    int r = (int)line;
    833    int c = (int)col;
    834    if (line2 == -1) {
    835      line2 = r;
    836      col2 = c;
    837    }
    838 
    839    DecorPriority subpriority = 0;
    840    if (HAS_KEY(opts, set_extmark, _subpriority)) {
    841      VALIDATE_RANGE((opts->_subpriority >= 0 && opts->_subpriority <= UINT16_MAX),
    842                     "_subpriority", {
    843        goto error;
    844      });
    845      subpriority = (DecorPriority)opts->_subpriority;
    846    }
    847 
    848    if (kv_size(virt_text.data.virt_text)) {
    849      decor_range_add_virt(&decor_state, r, c, line2, col2, decor_put_vt(virt_text, NULL), true);
    850    }
    851    if (kv_size(virt_lines.data.virt_lines)) {
    852      decor_range_add_virt(&decor_state, r, c, line2, col2, decor_put_vt(virt_lines, NULL), true);
    853    }
    854    if (has_hl) {
    855      DecorSignHighlight sh = decor_sh_from_inline(hl);
    856      sh.url = url;
    857      decor_range_add_sh(&decor_state, r, c, line2, col2, &sh, true, (uint32_t)ns_id, id,
    858                         subpriority);
    859    }
    860  } else {
    861    if (opts->ephemeral) {
    862      api_set_error(err, kErrorTypeException,
    863                    "cannot set emphemeral mark outside of a decoration provider");
    864      goto error;
    865    }
    866 
    867    uint16_t decor_flags = 0;
    868 
    869    DecorVirtText *decor_alloc = NULL;
    870    if (kv_size(virt_text.data.virt_text)) {
    871      decor_alloc = decor_put_vt(virt_text, decor_alloc);
    872      if (virt_text.pos == kVPosInline) {
    873        decor_flags |= MT_FLAG_DECOR_VIRT_TEXT_INLINE;
    874      }
    875    }
    876    if (kv_size(virt_lines.data.virt_lines)) {
    877      decor_alloc = decor_put_vt(virt_lines, decor_alloc);
    878      decor_flags |= MT_FLAG_DECOR_VIRT_LINES;
    879    }
    880 
    881    uint32_t decor_indexed = DECOR_ID_INVALID;
    882 
    883    if (sign.flags & kSHIsSign) {
    884      sign.next = decor_indexed;
    885      decor_indexed = decor_put_sh(sign);
    886      if (sign.text[0]) {
    887        decor_flags |= MT_FLAG_DECOR_SIGNTEXT;
    888      }
    889      if (sign.number_hl_id || sign.line_hl_id || sign.cursorline_hl_id) {
    890        decor_flags |= MT_FLAG_DECOR_SIGNHL;
    891      }
    892    }
    893 
    894    if (has_hl_multiple) {
    895      Array arr = opts->hl_group.data.array;
    896      for (size_t i = arr.size - 1; i > 0; i--) {  // skip hl_group[0], handled as hl.hl_id below
    897        int hl_id = object_to_hl_id(arr.items[i], "hl_group item", err);
    898        if (hl_id > 0) {
    899          DecorSignHighlight sh = DECOR_SIGN_HIGHLIGHT_INIT;
    900          sh.hl_id = hl_id;
    901          sh.flags = opts->hl_eol ? kSHHlEol : 0;
    902          sh.next = decor_indexed;
    903          decor_indexed = decor_put_sh(sh);
    904          decor_flags |= MT_FLAG_DECOR_HL;
    905        }
    906      }
    907    }
    908 
    909    if (hl.flags & kSHConcealLines) {
    910      decor_flags |= MT_FLAG_DECOR_CONCEAL_LINES;
    911    }
    912 
    913    DecorInline decor = DECOR_INLINE_INIT;
    914    if (decor_alloc || decor_indexed != DECOR_ID_INVALID || url != NULL
    915        || schar_high(hl.conceal_char)) {
    916      if (has_hl) {
    917        DecorSignHighlight sh = decor_sh_from_inline(hl);
    918        sh.url = url;
    919        sh.next = decor_indexed;
    920        decor_indexed = decor_put_sh(sh);
    921      }
    922      decor.ext = true;
    923      decor.data.ext = (DecorExt){ .sh_idx = decor_indexed, .vt = decor_alloc };
    924    } else {
    925      decor.data.hl = hl;
    926    }
    927 
    928    if (has_hl) {
    929      decor_flags |= MT_FLAG_DECOR_HL;
    930    }
    931 
    932    extmark_set(buf, (uint32_t)ns_id, &id, (int)line, (colnr_T)col, line2, col2,
    933                decor, decor_flags, right_gravity, opts->end_right_gravity,
    934                !GET_BOOL_OR_TRUE(opts, set_extmark, undo_restore),
    935                opts->invalidate, err);
    936    if (ERROR_SET(err)) {
    937      decor_free(decor);
    938      return 0;
    939    }
    940  }
    941 
    942  return (Integer)id;
    943 
    944 error:
    945  clear_virttext(&virt_text.data.virt_text);
    946  clear_virtlines(&virt_lines.data.virt_lines);
    947  if (url != NULL) {
    948    xfree(url);
    949  }
    950 
    951  return 0;
    952 }
    953 
    954 /// Removes an |extmark|.
    955 ///
    956 /// @param buffer Buffer id, or 0 for current buffer
    957 /// @param ns_id Namespace id from |nvim_create_namespace()|
    958 /// @param id Extmark id
    959 /// @param[out] err   Error details, if any
    960 /// @return true if the extmark was found, else false
    961 Boolean nvim_buf_del_extmark(Buffer buffer, Integer ns_id, Integer id, Error *err)
    962  FUNC_API_SINCE(7)
    963 {
    964  buf_T *buf = find_buffer_by_handle(buffer, err);
    965 
    966  if (!buf) {
    967    return false;
    968  }
    969  VALIDATE_INT(ns_initialized((uint32_t)ns_id), "ns_id", ns_id, {
    970    return false;
    971  });
    972 
    973  return extmark_del_id(buf, (uint32_t)ns_id, (uint32_t)id);
    974 }
    975 
    976 /// Clears |namespace|d objects (highlights, |extmarks|, virtual text) from
    977 /// a region.
    978 ///
    979 /// Lines are 0-indexed. |api-indexing|  To clear the namespace in the entire
    980 /// buffer, specify line_start=0 and line_end=-1.
    981 ///
    982 /// @param buffer     Buffer id, or 0 for current buffer
    983 /// @param ns_id      Namespace to clear, or -1 to clear all namespaces.
    984 /// @param line_start Start of range of lines to clear
    985 /// @param line_end   End of range of lines to clear (exclusive) or -1 to clear
    986 ///                   to end of buffer.
    987 /// @param[out] err   Error details, if any
    988 void nvim_buf_clear_namespace(Buffer buffer, Integer ns_id, Integer line_start, Integer line_end,
    989                              Error *err)
    990  FUNC_API_SINCE(5)
    991 {
    992  buf_T *buf = find_buffer_by_handle(buffer, err);
    993  if (!buf) {
    994    return;
    995  }
    996 
    997  VALIDATE_RANGE((line_start >= 0 && line_start < MAXLNUM), "line number", {
    998    return;
    999  });
   1000 
   1001  if (line_end < 0 || line_end > MAXLNUM) {
   1002    line_end = MAXLNUM;
   1003  }
   1004  extmark_clear(buf, (ns_id < 0 ? 0 : (uint32_t)ns_id),
   1005                (int)line_start, 0,
   1006                (int)line_end - 1, MAXCOL);
   1007 }
   1008 
   1009 /// Set or change decoration provider for a |namespace|
   1010 ///
   1011 /// This is a very general purpose interface for having Lua callbacks
   1012 /// being triggered during the redraw code.
   1013 ///
   1014 /// The expected usage is to set |extmarks| for the currently
   1015 /// redrawn buffer. |nvim_buf_set_extmark()| can be called to add marks
   1016 /// on a per-window or per-lines basis. Use the `ephemeral` key to only
   1017 /// use the mark for the current screen redraw (the callback will be called
   1018 /// again for the next redraw).
   1019 ///
   1020 /// Note: this function should not be called often. Rather, the callbacks
   1021 /// themselves can be used to throttle unneeded callbacks. the `on_start`
   1022 /// callback can return `false` to disable the provider until the next redraw.
   1023 /// Similarly, return `false` in `on_win` will skip the `on_line` and `on_range` calls
   1024 /// for that window (but any extmarks set in `on_win` will still be used).
   1025 /// A plugin managing multiple sources of decoration should ideally only set
   1026 /// one provider, and merge the sources internally. You can use multiple `ns_id`
   1027 /// for the extmarks set/modified inside the callback anyway.
   1028 ///
   1029 /// Note: doing anything other than setting extmarks is considered experimental.
   1030 /// Doing things like changing options are not explicitly forbidden, but is
   1031 /// likely to have unexpected consequences (such as 100% CPU consumption).
   1032 /// Doing `vim.rpcnotify` should be OK, but `vim.rpcrequest` is quite dubious
   1033 /// for the moment.
   1034 ///
   1035 /// Note: It is not allowed to remove or update extmarks in `on_line` or `on_range` callbacks.
   1036 ///
   1037 /// @param ns_id  Namespace id from |nvim_create_namespace()|
   1038 /// @param opts  Table of callbacks:
   1039 ///             - on_start: called first on each screen redraw
   1040 ///               ```
   1041 ///                 ["start", tick]
   1042 ///               ```
   1043 ///             - on_buf: called for each buffer being redrawn (once per edit,
   1044 ///               before window callbacks)
   1045 ///               ```
   1046 ///                 ["buf", bufnr, tick]
   1047 ///               ```
   1048 ///             - on_win: called when starting to redraw a specific window.
   1049 ///               ```
   1050 ///                 ["win", winid, bufnr, toprow, botrow]
   1051 ///               ```
   1052 ///             - on_line: (deprecated, use on_range instead)
   1053 ///               ```
   1054 ///                 ["line", winid, bufnr, row]
   1055 ///               ```
   1056 ///             - on_range: called for each buffer range being redrawn.
   1057 ///               Range is end-exclusive and may span multiple lines. Range
   1058 ///               bounds point to the first byte of a character. An end position
   1059 ///               of the form (lnum, 0), including (number of lines, 0), is valid
   1060 ///               and indicates that EOL of the preceding line is included.
   1061 ///               ```
   1062 ///                 ["range", winid, bufnr, begin_row, begin_col, end_row, end_col]
   1063 ///               ```
   1064 ///
   1065 ///               In addition to returning a boolean, it is also allowed to
   1066 ///               return a `skip_row, skip_col` pair of integers. This implies
   1067 ///               that this function does not need to be called until a range
   1068 ///               which continues beyond the skipped position. A single integer
   1069 ///               return value `skip_row` is short for `skip_row, 0`
   1070 ///
   1071 ///             - on_end: called at the end of a redraw cycle
   1072 ///               ```
   1073 ///                 ["end", tick]
   1074 ///               ```
   1075 void nvim_set_decoration_provider(Integer ns_id, Dict(set_decoration_provider) *opts, Error *err)
   1076  FUNC_API_SINCE(7) FUNC_API_LUA_ONLY
   1077 {
   1078  DecorProvider *p = get_decor_provider((NS)ns_id, true);
   1079  assert(p != NULL);
   1080  decor_provider_clear(p);
   1081 
   1082  // regardless of what happens, it seems good idea to redraw
   1083  redraw_all_later(UPD_NOT_VALID);  // TODO(bfredl): too soon?
   1084 
   1085  struct {
   1086    const char *name;
   1087    LuaRef *source;
   1088    LuaRef *dest;
   1089  } cbs[] = {
   1090    { "on_start", &opts->on_start, &p->redraw_start },
   1091    { "on_buf", &opts->on_buf, &p->redraw_buf },
   1092    { "on_win", &opts->on_win, &p->redraw_win },
   1093    { "on_line", &opts->on_line, &p->redraw_line },
   1094    { "on_range", &opts->on_range, &p->redraw_range },
   1095    { "on_end", &opts->on_end, &p->redraw_end },
   1096    { "_on_hl_def", &opts->_on_hl_def, &p->hl_def },
   1097    { "_on_spell_nav", &opts->_on_spell_nav, &p->spell_nav },
   1098    { "_on_conceal_line", &opts->_on_conceal_line, &p->conceal_line },
   1099    { NULL, NULL, NULL },
   1100  };
   1101 
   1102  for (size_t i = 0; cbs[i].source && cbs[i].dest && cbs[i].name; i++) {
   1103    LuaRef *v = cbs[i].source;
   1104    if (*v <= 0) {
   1105      continue;
   1106    }
   1107 
   1108    *(cbs[i].dest) = *v;
   1109    *v = LUA_NOREF;
   1110  }
   1111 
   1112  p->state = kDecorProviderActive;
   1113  p->hl_valid++;
   1114  p->hl_cached = false;
   1115 }
   1116 
   1117 /// Gets the line and column of an |extmark|.
   1118 ///
   1119 /// Extmarks may be queried by position, name or even special names
   1120 /// in the future such as "cursor".
   1121 ///
   1122 /// @param[out] lnum extmark line
   1123 /// @param[out] colnr extmark column
   1124 ///
   1125 /// @return true if the extmark was found, else false
   1126 static bool extmark_get_index_from_obj(buf_T *buf, Integer ns_id, Object obj, int *row,
   1127                                       colnr_T *col, Error *err)
   1128 {
   1129  // Check if it is mark id
   1130  if (obj.type == kObjectTypeInteger) {
   1131    Integer id = obj.data.integer;
   1132    if (id == 0) {
   1133      *row = 0;
   1134      *col = 0;
   1135      return true;
   1136    } else if (id == -1) {
   1137      *row = MAXLNUM;
   1138      *col = MAXCOL;
   1139      return true;
   1140    } else if (id < 0) {
   1141      VALIDATE_INT(false, "mark id", id, {
   1142        return false;
   1143      });
   1144    }
   1145 
   1146    MTPair extmark = extmark_from_id(buf, (uint32_t)ns_id, (uint32_t)id);
   1147 
   1148    VALIDATE_INT((extmark.start.pos.row >= 0), "mark id (not found)", id, {
   1149      return false;
   1150    });
   1151    *row = extmark.start.pos.row;
   1152    *col = extmark.start.pos.col;
   1153    return true;
   1154 
   1155    // Check if it is a position
   1156  } else if (obj.type == kObjectTypeArray) {
   1157    Array pos = obj.data.array;
   1158    VALIDATE_EXP((pos.size == 2
   1159                  && pos.items[0].type == kObjectTypeInteger
   1160                  && pos.items[1].type == kObjectTypeInteger),
   1161                 "mark position", "2 Integer items", NULL, {
   1162      return false;
   1163    });
   1164 
   1165    Integer pos_row = pos.items[0].data.integer;
   1166    Integer pos_col = pos.items[1].data.integer;
   1167    *row = (int)(pos_row >= 0 ? pos_row : MAXLNUM);
   1168    *col = (colnr_T)(pos_col >= 0 ? pos_col : MAXCOL);
   1169    return true;
   1170  } else {
   1171    VALIDATE_EXP(false, "mark position", "mark id Integer or 2-item Array", NULL, {
   1172      return false;
   1173    });
   1174  }
   1175 }
   1176 
   1177 VirtText parse_virt_text(Array chunks, Error *err, int *width)
   1178 {
   1179  VirtText virt_text = KV_INITIAL_VALUE;
   1180  int w = 0;
   1181  for (size_t i = 0; i < chunks.size; i++) {
   1182    VALIDATE_T("chunk", kObjectTypeArray, chunks.items[i].type, {
   1183      goto free_exit;
   1184    });
   1185    Array chunk = chunks.items[i].data.array;
   1186    VALIDATE((chunk.size > 0 && chunk.size <= 2 && chunk.items[0].type == kObjectTypeString),
   1187             "%s", "Invalid chunk: expected Array with 1 or 2 Strings", {
   1188      goto free_exit;
   1189    });
   1190 
   1191    String str = chunk.items[0].data.string;
   1192 
   1193    int hl_id = -1;
   1194    if (chunk.size == 2) {
   1195      Object hl = chunk.items[1];
   1196      if (hl.type == kObjectTypeArray) {
   1197        Array arr = hl.data.array;
   1198        for (size_t j = 0; j < arr.size; j++) {
   1199          hl_id = object_to_hl_id(arr.items[j], "virt_text highlight", err);
   1200          if (ERROR_SET(err)) {
   1201            goto free_exit;
   1202          }
   1203          if (j < arr.size - 1) {
   1204            kv_push(virt_text, ((VirtTextChunk){ .text = NULL, .hl_id = hl_id }));
   1205          }
   1206        }
   1207      } else {
   1208        hl_id = object_to_hl_id(hl, "virt_text highlight", err);
   1209        if (ERROR_SET(err)) {
   1210          goto free_exit;
   1211        }
   1212      }
   1213    }
   1214 
   1215    char *text = transstr(str.size > 0 ? str.data : "", false);  // allocates
   1216    w += (int)mb_string2cells(text);
   1217 
   1218    kv_push(virt_text, ((VirtTextChunk){ .text = text, .hl_id = hl_id }));
   1219  }
   1220 
   1221  if (width != NULL) {
   1222    *width = w;
   1223  }
   1224  return virt_text;
   1225 
   1226 free_exit:
   1227  clear_virttext(&virt_text);
   1228  return virt_text;
   1229 }
   1230 
   1231 /// @nodoc
   1232 String nvim__buf_debug_extmarks(Buffer buffer, Boolean keys, Boolean dot, Error *err)
   1233  FUNC_API_SINCE(7) FUNC_API_RET_ALLOC
   1234 {
   1235  buf_T *buf = find_buffer_by_handle(buffer, err);
   1236  if (!buf) {
   1237    return NULL_STRING;
   1238  }
   1239 
   1240  return mt_inspect(buf->b_marktree, keys, dot);
   1241 }
   1242 
   1243 /// EXPERIMENTAL: this API will change in the future.
   1244 ///
   1245 /// Set some properties for namespace
   1246 ///
   1247 /// @param ns_id Namespace
   1248 /// @param opts Optional parameters to set:
   1249 ///           - wins: a list of windows to be scoped in
   1250 ///
   1251 void nvim__ns_set(Integer ns_id, Dict(ns_opts) *opts, Error *err)
   1252 {
   1253  VALIDATE_INT(ns_initialized((uint32_t)ns_id), "ns_id", ns_id, {
   1254    return;
   1255  });
   1256 
   1257  bool set_scoped = true;
   1258 
   1259  if (HAS_KEY(opts, ns_opts, wins)) {
   1260    if (opts->wins.size == 0) {
   1261      set_scoped = false;
   1262    }
   1263 
   1264    Set(ptr_t) windows = SET_INIT;
   1265    for (size_t i = 0; i < opts->wins.size; i++) {
   1266      Integer win = opts->wins.items[i].data.integer;
   1267 
   1268      win_T *wp = find_window_by_handle((Window)win, err);
   1269      if (!wp) {
   1270        return;
   1271      }
   1272 
   1273      set_put(ptr_t, &windows, wp);
   1274    }
   1275 
   1276    FOR_ALL_TAB_WINDOWS(tp, wp) {
   1277      if (set_has(ptr_t, &windows, wp) && !set_has(uint32_t, &wp->w_ns_set, (uint32_t)ns_id)) {
   1278        set_put(uint32_t, &wp->w_ns_set, (uint32_t)ns_id);
   1279 
   1280        if (map_has(uint32_t, wp->w_buffer->b_extmark_ns, (uint32_t)ns_id)) {
   1281          changed_window_setting(wp);
   1282        }
   1283      }
   1284 
   1285      if (set_has(uint32_t, &wp->w_ns_set, (uint32_t)ns_id) && !set_has(ptr_t, &windows, wp)) {
   1286        set_del(uint32_t, &wp->w_ns_set, (uint32_t)ns_id);
   1287 
   1288        if (map_has(uint32_t, wp->w_buffer->b_extmark_ns, (uint32_t)ns_id)) {
   1289          changed_window_setting(wp);
   1290        }
   1291      }
   1292    }
   1293 
   1294    set_destroy(ptr_t, &windows);
   1295  }
   1296 
   1297  if (set_scoped && !set_has(uint32_t, &namespace_localscope, (uint32_t)ns_id)) {
   1298    set_put(uint32_t, &namespace_localscope, (uint32_t)ns_id);
   1299 
   1300    // When a namespace becomes scoped, any window which contains
   1301    // elements associated with namespace needs to be redrawn
   1302    FOR_ALL_TAB_WINDOWS(tp, wp) {
   1303      if (map_has(uint32_t, wp->w_buffer->b_extmark_ns, (uint32_t)ns_id)) {
   1304        changed_window_setting(wp);
   1305      }
   1306    }
   1307  } else if (!set_scoped && set_has(uint32_t, &namespace_localscope, (uint32_t)ns_id)) {
   1308    set_del(uint32_t, &namespace_localscope, (uint32_t)ns_id);
   1309 
   1310    // When a namespace becomes unscoped, any window which does not
   1311    // contain elements associated with namespace needs to be redrawn
   1312    FOR_ALL_TAB_WINDOWS(tp, wp) {
   1313      if (map_has(uint32_t, wp->w_buffer->b_extmark_ns, (uint32_t)ns_id)) {
   1314        changed_window_setting(wp);
   1315      }
   1316    }
   1317  }
   1318 }
   1319 
   1320 /// EXPERIMENTAL: this API will change in the future.
   1321 ///
   1322 /// Get the properties for namespace
   1323 ///
   1324 /// @param ns_id Namespace
   1325 /// @return  Map defining the namespace properties, see |nvim__ns_set()|
   1326 Dict(ns_opts) nvim__ns_get(Integer ns_id, Arena *arena, Error *err)
   1327 {
   1328  Dict(ns_opts) opts = KEYDICT_INIT;
   1329 
   1330  Array windows = ARRAY_DICT_INIT;
   1331 
   1332  PUT_KEY(opts, ns_opts, wins, windows);
   1333 
   1334  VALIDATE_INT(ns_initialized((uint32_t)ns_id), "ns_id", ns_id, {
   1335    return opts;
   1336  });
   1337 
   1338  if (!set_has(uint32_t, &namespace_localscope, (uint32_t)ns_id)) {
   1339    return opts;
   1340  }
   1341 
   1342  size_t count = 0;
   1343  FOR_ALL_TAB_WINDOWS(tp, wp) {
   1344    if (set_has(uint32_t, &wp->w_ns_set, (uint32_t)ns_id)) {
   1345      count++;
   1346    }
   1347  }
   1348 
   1349  windows = arena_array(arena, count);
   1350 
   1351  FOR_ALL_TAB_WINDOWS(tp, wp) {
   1352    if (set_has(uint32_t, &wp->w_ns_set, (uint32_t)ns_id)) {
   1353      ADD(windows, INTEGER_OBJ(wp->handle));
   1354    }
   1355  }
   1356 
   1357  PUT_KEY(opts, ns_opts, wins, windows);
   1358 
   1359  return opts;
   1360 }