neovim

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

buffer.c (46605B)


      1 // Some of this code was adapted from 'if_py_both.h' from the original
      2 // vim source
      3 
      4 #include <lauxlib.h>
      5 #include <stdbool.h>
      6 #include <stddef.h>
      7 #include <stdint.h>
      8 #include <string.h>
      9 
     10 #include "klib/kvec.h"
     11 #include "lua.h"
     12 #include "nvim/api/buffer.h"
     13 #include "nvim/api/keysets_defs.h"
     14 #include "nvim/api/private/defs.h"
     15 #include "nvim/api/private/dispatch.h"
     16 #include "nvim/api/private/helpers.h"
     17 #include "nvim/api/private/validate.h"
     18 #include "nvim/ascii_defs.h"
     19 #include "nvim/autocmd.h"
     20 #include "nvim/autocmd_defs.h"
     21 #include "nvim/buffer.h"
     22 #include "nvim/buffer_defs.h"
     23 #include "nvim/buffer_updates.h"
     24 #include "nvim/change.h"
     25 #include "nvim/cursor.h"
     26 #include "nvim/ex_cmds.h"
     27 #include "nvim/extmark.h"
     28 #include "nvim/extmark_defs.h"
     29 #include "nvim/globals.h"
     30 #include "nvim/lua/executor.h"
     31 #include "nvim/mapping.h"
     32 #include "nvim/mark.h"
     33 #include "nvim/mark_defs.h"
     34 #include "nvim/marktree_defs.h"
     35 #include "nvim/memline.h"
     36 #include "nvim/memline_defs.h"
     37 #include "nvim/memory.h"
     38 #include "nvim/memory_defs.h"
     39 #include "nvim/move.h"
     40 #include "nvim/ops.h"
     41 #include "nvim/option_vars.h"
     42 #include "nvim/pos_defs.h"
     43 #include "nvim/state_defs.h"
     44 #include "nvim/types_defs.h"
     45 #include "nvim/undo.h"
     46 #include "nvim/undo_defs.h"
     47 #include "nvim/vim_defs.h"
     48 
     49 #include "api/buffer.c.generated.h"
     50 
     51 /// Ensures that a buffer is loaded.
     52 buf_T *api_buf_ensure_loaded(Buffer buffer, Error *err)
     53 {
     54  buf_T *buf = find_buffer_by_handle(buffer, err);
     55  if (!buf) {
     56    return NULL;
     57  }
     58 
     59  if (buf->b_ml.ml_mfp == NULL && !buf_ensure_loaded(buf)) {
     60    api_set_error(err, kErrorTypeException, "Failed to load buffer");
     61    return NULL;
     62  }
     63 
     64  return buf;
     65 }
     66 
     67 /// @brief <pre>help
     68 /// For more information on buffers, see |buffers|.
     69 ///
     70 /// Unloaded Buffers: ~
     71 ///
     72 /// Buffers may be unloaded by the |:bunload| command or the buffer's
     73 /// 'bufhidden' option. When a buffer is unloaded its file contents are freed
     74 /// from memory and vim cannot operate on the buffer lines until it is reloaded
     75 /// (usually by opening the buffer again in a new window). API methods such as
     76 /// |nvim_buf_get_lines()| and |nvim_buf_line_count()| will be affected.
     77 ///
     78 /// You can use |nvim_buf_is_loaded()| or |nvim_buf_line_count()| to check
     79 /// whether a buffer is loaded.
     80 /// </pre>
     81 
     82 /// Returns the number of lines in the given buffer.
     83 ///
     84 /// @param buffer   Buffer id, or 0 for current buffer
     85 /// @param[out] err Error details, if any
     86 /// @return Line count, or 0 for unloaded buffer. |api-buffer|
     87 Integer nvim_buf_line_count(Buffer buffer, Error *err)
     88  FUNC_API_SINCE(1)
     89 {
     90  buf_T *buf = find_buffer_by_handle(buffer, err);
     91 
     92  if (!buf) {
     93    return 0;
     94  }
     95 
     96  // return sentinel value if the buffer isn't loaded
     97  if (buf->b_ml.ml_mfp == NULL) {
     98    return 0;
     99  }
    100 
    101  return buf->b_ml.ml_line_count;
    102 }
    103 
    104 /// Activates |api-buffer-updates| events on a channel, or as Lua callbacks.
    105 ///
    106 /// Example (Lua): capture buffer updates in a global `events` variable
    107 /// (use "vim.print(events)" to see its contents):
    108 ///
    109 /// ```lua
    110 /// events = {}
    111 /// vim.api.nvim_buf_attach(0, false, {
    112 ///   on_lines = function(...)
    113 ///     table.insert(events, {...})
    114 ///   end,
    115 /// })
    116 /// ```
    117 ///
    118 /// @see |nvim_buf_detach()|
    119 /// @see |api-buffer-updates-lua|
    120 ///
    121 /// @param channel_id
    122 /// @param buffer Buffer id, or 0 for current buffer
    123 /// @param send_buffer True if the initial notification should contain the
    124 ///        whole buffer: first notification will be `nvim_buf_lines_event`.
    125 ///        Else the first notification will be `nvim_buf_changedtick_event`.
    126 ///        Not for Lua callbacks.
    127 /// @param  opts  Optional parameters.
    128 ///             - on_lines: Called on linewise changes. Not called on buffer reload (`:checktime`,
    129 ///               `:edit`, …), see `on_reload:`. Return a [lua-truthy] value to detach. Args:
    130 ///               - the string "lines"
    131 ///               - buffer id
    132 ///               - b:changedtick
    133 ///               - first line that changed (zero-indexed)
    134 ///               - last line that was changed
    135 ///               - last line in the updated range
    136 ///               - byte count of previous contents
    137 ///               - deleted_codepoints (if `utf_sizes` is true)
    138 ///               - deleted_codeunits (if `utf_sizes` is true)
    139 ///             - on_bytes: Called on granular changes (compared to on_lines). Not called on buffer
    140 ///               reload (`:checktime`, `:edit`, …), see `on_reload:`. Return a [lua-truthy] value
    141 ///               to detach. Args:
    142 ///               - the string "bytes"
    143 ///               - buffer id
    144 ///               - b:changedtick
    145 ///               - start row of the changed text (zero-indexed)
    146 ///               - start column of the changed text
    147 ///               - byte offset of the changed text (from the start of
    148 ///                   the buffer)
    149 ///               - old end row of the changed text (offset from start row)
    150 ///               - old end column of the changed text
    151 ///                 (if old end row = 0, offset from start column)
    152 ///               - old end byte length of the changed text
    153 ///               - new end row of the changed text (offset from start row)
    154 ///               - new end column of the changed text
    155 ///                 (if new end row = 0, offset from start column)
    156 ///               - new end byte length of the changed text
    157 ///             - on_changedtick: Called on [changetick] increment without text change. Args:
    158 ///               - the string "changedtick"
    159 ///               - buffer id
    160 ///               - b:changedtick
    161 ///             - on_detach: Called on detach. Args:
    162 ///               - the string "detach"
    163 ///               - buffer id
    164 ///             - on_reload: Called on whole-buffer load (`:checktime`, `:edit`, …). Clients should
    165 ///               typically re-fetch the entire buffer contents. Args:
    166 ///               - the string "reload"
    167 ///               - buffer id
    168 ///             - utf_sizes: include UTF-32 and UTF-16 size of the replaced
    169 ///               region, as args to `on_lines`.
    170 ///             - preview: also attach to command preview (i.e. 'inccommand')
    171 ///               events.
    172 /// @param[out] err Error details, if any
    173 /// @return False if attach failed (invalid parameter, or buffer isn't loaded);
    174 ///         otherwise True.
    175 Boolean nvim_buf_attach(uint64_t channel_id, Buffer buffer, Boolean send_buffer,
    176                        Dict(buf_attach) *opts, Error *err)
    177  FUNC_API_SINCE(4)
    178 {
    179  buf_T *buf = find_buffer_by_handle(buffer, err);
    180 
    181  if (!buf) {
    182    return false;
    183  }
    184 
    185  BufUpdateCallbacks cb = BUF_UPDATE_CALLBACKS_INIT;
    186 
    187  if (channel_id == LUA_INTERNAL_CALL) {
    188    if (HAS_KEY(opts, buf_attach, on_lines)) {
    189      cb.on_lines = opts->on_lines;
    190      opts->on_lines = LUA_NOREF;
    191    }
    192 
    193    if (HAS_KEY(opts, buf_attach, on_bytes)) {
    194      cb.on_bytes = opts->on_bytes;
    195      opts->on_bytes = LUA_NOREF;
    196    }
    197 
    198    if (HAS_KEY(opts, buf_attach, on_changedtick)) {
    199      cb.on_changedtick = opts->on_changedtick;
    200      opts->on_changedtick = LUA_NOREF;
    201    }
    202 
    203    if (HAS_KEY(opts, buf_attach, on_detach)) {
    204      cb.on_detach = opts->on_detach;
    205      opts->on_detach = LUA_NOREF;
    206    }
    207 
    208    if (HAS_KEY(opts, buf_attach, on_reload)) {
    209      cb.on_reload = opts->on_reload;
    210      opts->on_reload = LUA_NOREF;
    211    }
    212 
    213    cb.utf_sizes = opts->utf_sizes;
    214 
    215    cb.preview = opts->preview;
    216  }
    217 
    218  return buf_updates_register(buf, channel_id, cb, send_buffer);
    219 }
    220 
    221 /// Deactivates buffer-update events on the channel.
    222 ///
    223 /// @see |nvim_buf_attach()|
    224 /// @see |api-lua-detach| for detaching Lua callbacks
    225 ///
    226 /// @param channel_id
    227 /// @param buffer Buffer id, or 0 for current buffer
    228 /// @param[out] err Error details, if any
    229 /// @return False if detach failed (because the buffer isn't loaded);
    230 ///         otherwise True.
    231 Boolean nvim_buf_detach(uint64_t channel_id, Buffer buffer, Error *err)
    232  FUNC_API_SINCE(4) FUNC_API_REMOTE_ONLY
    233 {
    234  buf_T *buf = find_buffer_by_handle(buffer, err);
    235 
    236  if (!buf) {
    237    return false;
    238  }
    239 
    240  buf_updates_unregister(buf, channel_id);
    241  return true;
    242 }
    243 
    244 /// Gets a line-range from the buffer.
    245 ///
    246 /// Indexing is zero-based, end-exclusive. Negative indices are interpreted
    247 /// as length+1+index: -1 refers to the index past the end. So to get the
    248 /// last element use start=-2 and end=-1.
    249 ///
    250 /// Out-of-bounds indices are clamped to the nearest valid value, unless
    251 /// `strict_indexing` is set.
    252 ///
    253 /// @see |nvim_buf_get_text()|
    254 ///
    255 /// @param channel_id
    256 /// @param buffer           Buffer id, or 0 for current buffer
    257 /// @param start            First line index
    258 /// @param end              Last line index, exclusive
    259 /// @param strict_indexing  Whether out-of-bounds should be an error.
    260 /// @param[out] err         Error details, if any
    261 /// @return Array of lines, or empty array for unloaded buffer.
    262 ArrayOf(String) nvim_buf_get_lines(uint64_t channel_id,
    263                                   Buffer buffer,
    264                                   Integer start,
    265                                   Integer end,
    266                                   Boolean strict_indexing,
    267                                   Arena *arena,
    268                                   lua_State *lstate,
    269                                   Error *err)
    270  FUNC_API_SINCE(1)
    271 {
    272  Array rv = ARRAY_DICT_INIT;
    273  buf_T *buf = find_buffer_by_handle(buffer, err);
    274 
    275  if (!buf) {
    276    return rv;
    277  }
    278 
    279  // return sentinel value if the buffer isn't loaded
    280  if (buf->b_ml.ml_mfp == NULL) {
    281    return rv;
    282  }
    283 
    284  bool oob = false;
    285  start = normalize_index(buf, start, true, &oob);
    286  end = normalize_index(buf, end, true, &oob);
    287 
    288  VALIDATE((!strict_indexing || !oob), "%s", "Index out of bounds", {
    289    return rv;
    290  });
    291 
    292  if (start >= end) {
    293    // Return 0-length array
    294    return rv;
    295  }
    296 
    297  size_t size = (size_t)(end - start);
    298 
    299  init_line_array(lstate, &rv, size, arena);
    300 
    301  buf_collect_lines(buf, size, (linenr_T)start, 0, (channel_id != VIML_INTERNAL_CALL), &rv,
    302                    lstate, arena);
    303 
    304  return rv;
    305 }
    306 
    307 /// Sets (replaces) a line-range in the buffer.
    308 ///
    309 /// Indexing is zero-based, end-exclusive. Negative indices are interpreted
    310 /// as length+1+index: -1 refers to the index past the end. So to change
    311 /// or delete the last line use start=-2 and end=-1.
    312 ///
    313 /// To insert lines at a given index, set `start` and `end` to the same index.
    314 /// To delete a range of lines, set `replacement` to an empty array.
    315 ///
    316 /// Out-of-bounds indices are clamped to the nearest valid value, unless
    317 /// `strict_indexing` is set.
    318 ///
    319 /// @see |nvim_buf_set_text()|
    320 ///
    321 /// @param channel_id
    322 /// @param buffer           Buffer id, or 0 for current buffer
    323 /// @param start            First line index
    324 /// @param end              Last line index, exclusive
    325 /// @param strict_indexing  Whether out-of-bounds should be an error.
    326 /// @param replacement      Array of lines to use as replacement
    327 /// @param[out] err         Error details, if any
    328 void nvim_buf_set_lines(uint64_t channel_id, Buffer buffer, Integer start, Integer end,
    329                        Boolean strict_indexing, ArrayOf(String) replacement, Arena *arena,
    330                        Error *err)
    331  FUNC_API_SINCE(1)
    332  FUNC_API_TEXTLOCK_ALLOW_CMDWIN
    333 {
    334  buf_T *buf = api_buf_ensure_loaded(buffer, err);
    335 
    336  if (!buf) {
    337    return;
    338  }
    339 
    340  bool oob = false;
    341  start = normalize_index(buf, start, true, &oob);
    342  end = normalize_index(buf, end, true, &oob);
    343 
    344  VALIDATE((!strict_indexing || !oob), "%s", "Index out of bounds", {
    345    return;
    346  });
    347  VALIDATE((start <= end), "%s", "'start' is higher than 'end'", {
    348    return;
    349  });
    350 
    351  bool disallow_nl = (channel_id != VIML_INTERNAL_CALL);
    352  if (!check_string_array(replacement, "replacement string", disallow_nl, err)) {
    353    return;
    354  }
    355 
    356  size_t new_len = replacement.size;
    357  size_t old_len = (size_t)(end - start);
    358  ptrdiff_t extra = 0;  // lines added to text, can be negative
    359  char **lines = (new_len != 0) ? arena_alloc(arena, new_len * sizeof(char *), true) : NULL;
    360 
    361  for (size_t i = 0; i < new_len; i++) {
    362    const String l = replacement.items[i].data.string;
    363 
    364    // Fill lines[i] with l's contents. Convert NULs to newlines as required by
    365    // NL-used-for-NUL.
    366    lines[i] = arena_memdupz(arena, l.data, l.size);
    367    memchrsub(lines[i], NUL, NL, l.size);
    368  }
    369 
    370  TRY_WRAP(err, {
    371    if (!MODIFIABLE(buf)) {
    372      api_set_error(err, kErrorTypeException, "Buffer is not 'modifiable'");
    373      goto end;
    374    }
    375 
    376    if (u_save_buf(buf, (linenr_T)(start - 1), (linenr_T)end) == FAIL) {
    377      api_set_error(err, kErrorTypeException, "Failed to save undo information");
    378      goto end;
    379    }
    380 
    381    bcount_t deleted_bytes = get_region_bytecount(buf, (linenr_T)start, (linenr_T)end, 0, 0);
    382 
    383    // If the size of the range is reducing (ie, new_len < old_len) we
    384    // need to delete some old_len. We do this at the start, by
    385    // repeatedly deleting line "start".
    386    size_t to_delete = (new_len < old_len) ? old_len - new_len : 0;
    387    for (size_t i = 0; i < to_delete; i++) {
    388      if (ml_delete_buf(buf, (linenr_T)start, false) == FAIL) {
    389        api_set_error(err, kErrorTypeException, "Failed to delete line");
    390        goto end;
    391      }
    392    }
    393 
    394    if (to_delete > 0) {
    395      extra -= (ptrdiff_t)to_delete;
    396    }
    397 
    398    // For as long as possible, replace the existing old_len with the
    399    // new old_len. This is a more efficient operation, as it requires
    400    // less memory allocation and freeing.
    401    size_t to_replace = old_len < new_len ? old_len : new_len;
    402    bcount_t inserted_bytes = 0;
    403    for (size_t i = 0; i < to_replace; i++) {
    404      int64_t lnum = start + (int64_t)i;
    405 
    406      VALIDATE(lnum < MAXLNUM, "%s", "Index out of bounds", {
    407        goto end;
    408      });
    409 
    410      if (ml_replace_buf(buf, (linenr_T)lnum, lines[i], false, true) == FAIL) {
    411        api_set_error(err, kErrorTypeException, "Failed to replace line");
    412        goto end;
    413      }
    414 
    415      inserted_bytes += (bcount_t)strlen(lines[i]) + 1;
    416    }
    417 
    418    // Now we may need to insert the remaining new old_len
    419    for (size_t i = to_replace; i < new_len; i++) {
    420      int64_t lnum = start + (int64_t)i - 1;
    421 
    422      VALIDATE(lnum < MAXLNUM, "%s", "Index out of bounds", {
    423        goto end;
    424      });
    425 
    426      if (ml_append_buf(buf, (linenr_T)lnum, lines[i], 0, false) == FAIL) {
    427        api_set_error(err, kErrorTypeException, "Failed to insert line");
    428        goto end;
    429      }
    430 
    431      inserted_bytes += (bcount_t)strlen(lines[i]) + 1;
    432 
    433      extra++;
    434    }
    435 
    436    // Adjust marks. Invalidate any which lie in the
    437    // changed range, and move any in the remainder of the buffer.
    438    linenr_T adjust = end > start ? MAXLNUM : 0;
    439    mark_adjust_buf(buf, (linenr_T)start, (linenr_T)(end - 1), adjust, (linenr_T)extra,
    440                    true, kMarkAdjustApi, kExtmarkNOOP);
    441 
    442    extmark_splice(buf, (int)start - 1, 0, (int)(end - start), 0,
    443                   deleted_bytes, (int)new_len, 0, inserted_bytes,
    444                   kExtmarkUndo);
    445 
    446    changed_lines(buf, (linenr_T)start, 0, (linenr_T)end, (linenr_T)extra, true);
    447 
    448    FOR_ALL_TAB_WINDOWS(tp, win) {
    449      if (win->w_buffer == buf) {
    450        fix_cursor(win, (linenr_T)start, (linenr_T)end, (linenr_T)extra);
    451      }
    452    }
    453    end:;
    454  });
    455 }
    456 
    457 /// Sets (replaces) a range in the buffer
    458 ///
    459 /// This is recommended over |nvim_buf_set_lines()| when only modifying parts of
    460 /// a line, as extmarks will be preserved on non-modified parts of the touched
    461 /// lines.
    462 ///
    463 /// Indexing is zero-based. Row indices are end-inclusive, and column indices
    464 /// are end-exclusive.
    465 ///
    466 /// To insert text at a given `(row, column)` location, use `start_row = end_row
    467 /// = row` and `start_col = end_col = col`. To delete the text in a range, use
    468 /// `replacement = {}`.
    469 ///
    470 /// @note Prefer |nvim_buf_set_lines()| (for performance) to add or delete entire lines.
    471 /// @note Prefer |nvim_paste()| or |nvim_put()| to insert (instead of replace) text at cursor.
    472 ///
    473 /// @param channel_id
    474 /// @param buffer           Buffer id, or 0 for current buffer
    475 /// @param start_row        First line index
    476 /// @param start_col        Starting column (byte offset) on first line
    477 /// @param end_row          Last line index, inclusive
    478 /// @param end_col          Ending column (byte offset) on last line, exclusive
    479 /// @param replacement      Array of lines to use as replacement
    480 /// @param[out] err         Error details, if any
    481 void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, Integer start_col,
    482                       Integer end_row, Integer end_col, ArrayOf(String) replacement, Arena *arena,
    483                       Error *err)
    484  FUNC_API_SINCE(7)
    485  FUNC_API_TEXTLOCK_ALLOW_CMDWIN
    486 {
    487  MAXSIZE_TEMP_ARRAY(scratch, 1);
    488  if (replacement.size == 0) {
    489    ADD_C(scratch, STATIC_CSTR_AS_OBJ(""));
    490    replacement = scratch;
    491  }
    492 
    493  buf_T *buf = api_buf_ensure_loaded(buffer, err);
    494  if (!buf) {
    495    return;
    496  }
    497 
    498  bool oob = false;
    499 
    500  // check range is ordered and everything!
    501  // start_row, end_row within buffer len (except add text past the end?)
    502  start_row = normalize_index(buf, start_row, false, &oob);
    503  VALIDATE_RANGE((!oob), "start_row", {
    504    return;
    505  });
    506 
    507  end_row = normalize_index(buf, end_row, false, &oob);
    508  VALIDATE_RANGE((!oob), "end_row", {
    509    return;
    510  });
    511 
    512  // Another call to ml_get_buf() may free the lines, so we make copies
    513  char *str_at_start = ml_get_buf(buf, (linenr_T)start_row);
    514  colnr_T len_at_start = ml_get_buf_len(buf, (linenr_T)start_row);
    515  str_at_start = arena_memdupz(arena, str_at_start, (size_t)len_at_start);
    516  start_col = start_col < 0 ? len_at_start + start_col + 1 : start_col;
    517  VALIDATE_RANGE((start_col >= 0 && start_col <= len_at_start), "start_col", {
    518    return;
    519  });
    520 
    521  char *str_at_end = ml_get_buf(buf, (linenr_T)end_row);
    522  colnr_T len_at_end = ml_get_buf_len(buf, (linenr_T)end_row);
    523  str_at_end = arena_memdupz(arena, str_at_end, (size_t)len_at_end);
    524  end_col = end_col < 0 ? len_at_end + end_col + 1 : end_col;
    525  VALIDATE_RANGE((end_col >= 0 && end_col <= len_at_end), "end_col", {
    526    return;
    527  });
    528 
    529  VALIDATE((start_row <= end_row && !(end_row == start_row && start_col > end_col)),
    530           "%s", "'start' is higher than 'end'", {
    531    return;
    532  });
    533 
    534  bool disallow_nl = (channel_id != VIML_INTERNAL_CALL);
    535  if (!check_string_array(replacement, "replacement string", disallow_nl, err)) {
    536    return;
    537  }
    538 
    539  size_t new_len = replacement.size;
    540 
    541  bcount_t new_byte = 0;
    542  bcount_t old_byte = 0;
    543 
    544  // calculate byte size of old region before it gets modified/deleted
    545  if (start_row == end_row) {
    546    old_byte = (bcount_t)end_col - start_col;
    547  } else {
    548    old_byte += len_at_start - start_col;
    549    for (int64_t i = 1; i < end_row - start_row; i++) {
    550      int64_t lnum = start_row + i;
    551      old_byte += ml_get_buf_len(buf, (linenr_T)lnum) + 1;
    552    }
    553    old_byte += (bcount_t)end_col + 1;
    554  }
    555 
    556  String first_item = replacement.items[0].data.string;
    557  String last_item = replacement.items[replacement.size - 1].data.string;
    558 
    559  size_t firstlen = (size_t)start_col + first_item.size;
    560  size_t last_part_len = (size_t)len_at_end - (size_t)end_col;
    561  if (replacement.size == 1) {
    562    firstlen += last_part_len;
    563  }
    564  char *first = arena_allocz(arena, firstlen);
    565  char *last = NULL;
    566  memcpy(first, str_at_start, (size_t)start_col);
    567  memcpy(first + start_col, first_item.data, first_item.size);
    568  memchrsub(first + start_col, NUL, NL, first_item.size);
    569  if (replacement.size == 1) {
    570    memcpy(first + start_col + first_item.size, str_at_end + end_col, last_part_len);
    571  } else {
    572    last = arena_allocz(arena, last_item.size + last_part_len);
    573    memcpy(last, last_item.data, last_item.size);
    574    memchrsub(last, NUL, NL, last_item.size);
    575    memcpy(last + last_item.size, str_at_end + end_col, last_part_len);
    576  }
    577 
    578  char **lines = arena_alloc(arena, new_len * sizeof(char *), true);
    579  lines[0] = first;
    580  new_byte += (bcount_t)(first_item.size);
    581  for (size_t i = 1; i < new_len - 1; i++) {
    582    const String l = replacement.items[i].data.string;
    583 
    584    // Fill lines[i] with l's contents. Convert NULs to newlines as required by
    585    // NL-used-for-NUL.
    586    lines[i] = arena_memdupz(arena, l.data, l.size);
    587    memchrsub(lines[i], NUL, NL, l.size);
    588    new_byte += (bcount_t)(l.size) + 1;
    589  }
    590  if (replacement.size > 1) {
    591    lines[replacement.size - 1] = last;
    592    new_byte += (bcount_t)(last_item.size) + 1;
    593  }
    594 
    595  TRY_WRAP(err, {
    596    if (!MODIFIABLE(buf)) {
    597      api_set_error(err, kErrorTypeException, "Buffer is not 'modifiable'");
    598      goto end;
    599    }
    600 
    601    // Small note about undo states: unlike set_lines, we want to save the
    602    // undo state of one past the end_row, since end_row is inclusive.
    603    if (u_save_buf(buf, (linenr_T)start_row - 1, (linenr_T)end_row + 1) == FAIL) {
    604      api_set_error(err, kErrorTypeException, "Failed to save undo information");
    605      goto end;
    606    }
    607 
    608    ptrdiff_t extra = 0;  // lines added to text, can be negative
    609    size_t old_len = (size_t)(end_row - start_row + 1);
    610 
    611    // If the size of the range is reducing (ie, new_len < old_len) we
    612    // need to delete some old_len. We do this at the start, by
    613    // repeatedly deleting line "start".
    614    size_t to_delete = (new_len < old_len) ? old_len - new_len : 0;
    615    for (size_t i = 0; i < to_delete; i++) {
    616      if (ml_delete_buf(buf, (linenr_T)start_row, false) == FAIL) {
    617        api_set_error(err, kErrorTypeException, "Failed to delete line");
    618        goto end;
    619      }
    620    }
    621 
    622    if (to_delete > 0) {
    623      extra -= (ptrdiff_t)to_delete;
    624    }
    625 
    626    // For as long as possible, replace the existing old_len with the
    627    // new old_len. This is a more efficient operation, as it requires
    628    // less memory allocation and freeing.
    629    size_t to_replace = old_len < new_len ? old_len : new_len;
    630    for (size_t i = 0; i < to_replace; i++) {
    631      int64_t lnum = start_row + (int64_t)i;
    632 
    633      VALIDATE((lnum < MAXLNUM), "%s", "Index out of bounds", {
    634        goto end;
    635      });
    636 
    637      if (ml_replace_buf(buf, (linenr_T)lnum, lines[i], false, true) == FAIL) {
    638        api_set_error(err, kErrorTypeException, "Failed to replace line");
    639        goto end;
    640      }
    641    }
    642 
    643    // Now we may need to insert the remaining new old_len
    644    for (size_t i = to_replace; i < new_len; i++) {
    645      int64_t lnum = start_row + (int64_t)i - 1;
    646 
    647      VALIDATE((lnum < MAXLNUM), "%s", "Index out of bounds", {
    648        goto end;
    649      });
    650 
    651      if (ml_append_buf(buf, (linenr_T)lnum, lines[i], 0, false) == FAIL) {
    652        api_set_error(err, kErrorTypeException, "Failed to insert line");
    653        goto end;
    654      }
    655 
    656      extra++;
    657    }
    658 
    659    colnr_T col_extent = (colnr_T)(end_col
    660                                   - ((end_row == start_row) ? start_col : 0));
    661 
    662    // Adjust marks. Invalidate any which lie in the
    663    // changed range, and move any in the remainder of the buffer.
    664    // Do not adjust any cursors. need to use column-aware logic (below)
    665    linenr_T adjust = end_row >= start_row ? MAXLNUM : 0;
    666    mark_adjust_buf(buf, (linenr_T)start_row, (linenr_T)end_row - 1, adjust, (linenr_T)extra,
    667                    true, kMarkAdjustApi, kExtmarkNOOP);
    668 
    669    extmark_splice(buf, (int)start_row - 1, (colnr_T)start_col,
    670                   (int)(end_row - start_row), col_extent, old_byte,
    671                   (int)new_len - 1, (colnr_T)last_item.size, new_byte,
    672                   kExtmarkUndo);
    673 
    674    changed_lines(buf, (linenr_T)start_row, 0, (linenr_T)end_row + 1, (linenr_T)extra, true);
    675 
    676    FOR_ALL_TAB_WINDOWS(tp, win) {
    677      if (win->w_buffer == buf) {
    678        if (win->w_cursor.lnum >= start_row && win->w_cursor.lnum <= end_row) {
    679          fix_cursor_cols(win, (linenr_T)start_row, (colnr_T)start_col, (linenr_T)end_row,
    680                          (colnr_T)end_col, (linenr_T)new_len, (colnr_T)last_item.size);
    681        } else {
    682          fix_cursor(win, (linenr_T)start_row, (linenr_T)end_row, (linenr_T)extra);
    683        }
    684      }
    685    }
    686    end:;
    687  });
    688 }
    689 
    690 /// Gets a range from the buffer (may be partial lines, unlike |nvim_buf_get_lines()|).
    691 ///
    692 /// Indexing is zero-based. Row indices are end-inclusive, and column indices
    693 /// are end-exclusive.
    694 ///
    695 /// Prefer |nvim_buf_get_lines()| when retrieving entire lines.
    696 ///
    697 /// @param channel_id
    698 /// @param buffer     Buffer id, or 0 for current buffer
    699 /// @param start_row  First line index
    700 /// @param start_col  Starting column (byte offset) on first line
    701 /// @param end_row    Last line index, inclusive
    702 /// @param end_col    Ending column (byte offset) on last line, exclusive
    703 /// @param opts       Optional parameters. Currently unused.
    704 /// @param[out] err   Error details, if any
    705 /// @return Array of lines, or empty array for unloaded buffer.
    706 ArrayOf(String) nvim_buf_get_text(uint64_t channel_id, Buffer buffer,
    707                                  Integer start_row, Integer start_col,
    708                                  Integer end_row, Integer end_col,
    709                                  Dict(empty) *opts,
    710                                  Arena *arena, lua_State *lstate, Error *err)
    711  FUNC_API_SINCE(9)
    712 {
    713  Array rv = ARRAY_DICT_INIT;
    714 
    715  buf_T *buf = find_buffer_by_handle(buffer, err);
    716 
    717  if (!buf) {
    718    return rv;
    719  }
    720 
    721  // return sentinel value if the buffer isn't loaded
    722  if (buf->b_ml.ml_mfp == NULL) {
    723    return rv;
    724  }
    725 
    726  bool oob = false;
    727  start_row = normalize_index(buf, start_row, false, &oob);
    728  end_row = normalize_index(buf, end_row, false, &oob);
    729 
    730  VALIDATE((!oob), "%s", "Index out of bounds", {
    731    return rv;
    732  });
    733 
    734  // nvim_buf_get_lines doesn't care if the start row is greater than the end
    735  // row (it will just return an empty array), but nvim_buf_get_text does in
    736  // order to maintain symmetry with nvim_buf_set_text.
    737  VALIDATE((start_row <= end_row), "%s", "'start' is higher than 'end'", {
    738    return rv;
    739  });
    740 
    741  bool replace_nl = (channel_id != VIML_INTERNAL_CALL);
    742 
    743  size_t size = (size_t)(end_row - start_row) + 1;
    744 
    745  init_line_array(lstate, &rv, size, arena);
    746 
    747  if (start_row == end_row) {
    748    String line = buf_get_text(buf, start_row, start_col, end_col, err);
    749    if (ERROR_SET(err)) {
    750      goto end;
    751    }
    752    push_linestr(lstate, &rv, line.data, line.size, 0, replace_nl, arena);
    753    return rv;
    754  }
    755 
    756  String str = buf_get_text(buf, start_row, start_col, MAXCOL - 1, err);
    757  if (ERROR_SET(err)) {
    758    goto end;
    759  }
    760 
    761  push_linestr(lstate, &rv, str.data, str.size, 0, replace_nl, arena);
    762 
    763  if (size > 2) {
    764    buf_collect_lines(buf, size - 2, (linenr_T)start_row + 1, 1, replace_nl, &rv, lstate, arena);
    765  }
    766 
    767  str = buf_get_text(buf, end_row, 0, end_col, err);
    768  if (ERROR_SET(err)) {
    769    goto end;
    770  }
    771 
    772  push_linestr(lstate, &rv, str.data, str.size, (int)(size - 1), replace_nl, arena);
    773 
    774 end:
    775  if (ERROR_SET(err)) {
    776    return (Array)ARRAY_DICT_INIT;
    777  }
    778 
    779  return rv;
    780 }
    781 
    782 /// Returns the byte offset of a line (0-indexed). |api-indexing|
    783 ///
    784 /// Line 1 (index=0) has offset 0. UTF-8 bytes are counted. EOL is one byte.
    785 /// 'fileformat' and 'fileencoding' are ignored. The line index just after the
    786 /// last line gives the total byte-count of the buffer. A final EOL byte is
    787 /// counted if it would be written, see 'eol'.
    788 ///
    789 /// Unlike |line2byte()|, throws error for out-of-bounds indexing.
    790 /// Returns -1 for unloaded buffer.
    791 ///
    792 /// @param buffer     Buffer id, or 0 for current buffer
    793 /// @param index      Line index
    794 /// @param[out] err   Error details, if any
    795 /// @return Integer byte offset, or -1 for unloaded buffer.
    796 Integer nvim_buf_get_offset(Buffer buffer, Integer index, Error *err)
    797  FUNC_API_SINCE(5)
    798 {
    799  buf_T *buf = find_buffer_by_handle(buffer, err);
    800  if (!buf) {
    801    return 0;
    802  }
    803 
    804  // return sentinel value if the buffer isn't loaded
    805  if (buf->b_ml.ml_mfp == NULL) {
    806    return -1;
    807  }
    808 
    809  VALIDATE((index >= 0 && index <= buf->b_ml.ml_line_count), "%s", "Index out of bounds", {
    810    return 0;
    811  });
    812 
    813  return ml_find_line_or_offset(buf, (int)index + 1, NULL, true);
    814 }
    815 
    816 /// Gets a buffer-scoped (b:) variable.
    817 ///
    818 /// @param buffer     Buffer id, or 0 for current buffer
    819 /// @param name       Variable name
    820 /// @param[out] err   Error details, if any
    821 /// @return Variable value
    822 Object nvim_buf_get_var(Buffer buffer, String name, Arena *arena, Error *err)
    823  FUNC_API_SINCE(1)
    824 {
    825  buf_T *buf = find_buffer_by_handle(buffer, err);
    826 
    827  if (!buf) {
    828    return (Object)OBJECT_INIT;
    829  }
    830 
    831  return dict_get_value(buf->b_vars, name, arena, err);
    832 }
    833 
    834 /// Gets a changed tick of a buffer
    835 ///
    836 /// @param[in]  buffer  Buffer id, or 0 for current buffer
    837 /// @param[out] err     Error details, if any
    838 ///
    839 /// @return `b:changedtick` value.
    840 Integer nvim_buf_get_changedtick(Buffer buffer, Error *err)
    841  FUNC_API_SINCE(2)
    842 {
    843  const buf_T *const buf = find_buffer_by_handle(buffer, err);
    844 
    845  if (!buf) {
    846    return -1;
    847  }
    848 
    849  return buf_get_changedtick(buf);
    850 }
    851 
    852 /// Gets a list of buffer-local |mapping| definitions.
    853 ///
    854 /// @param  buffer     Buffer id, or 0 for current buffer
    855 /// @param  mode       Mode short-name ("n", "i", "v", ...)
    856 /// @param[out]  err   Error details, if any
    857 /// @returns Array of |maparg()|-like dictionaries describing mappings.
    858 ///          The "buffer" key holds the associated buffer id.
    859 ArrayOf(DictAs(get_keymap)) nvim_buf_get_keymap(Buffer buffer, String mode, Arena *arena,
    860                                                Error *err)
    861  FUNC_API_SINCE(3)
    862 {
    863  buf_T *buf = find_buffer_by_handle(buffer, err);
    864 
    865  if (!buf) {
    866    return (Array)ARRAY_DICT_INIT;
    867  }
    868 
    869  return keymap_array(mode, buf, arena);
    870 }
    871 
    872 /// Sets a buffer-local |mapping| for the given mode.
    873 ///
    874 /// @see |nvim_set_keymap()|
    875 ///
    876 /// @param  buffer  Buffer id, or 0 for current buffer
    877 void nvim_buf_set_keymap(uint64_t channel_id, Buffer buffer, String mode, String lhs, String rhs,
    878                         Dict(keymap) *opts, Error *err)
    879  FUNC_API_SINCE(6)
    880 {
    881  modify_keymap(channel_id, buffer, false, mode, lhs, rhs, opts, err);
    882 }
    883 
    884 /// Unmaps a buffer-local |mapping| for the given mode.
    885 ///
    886 /// @see |nvim_del_keymap()|
    887 ///
    888 /// @param  buffer  Buffer id, or 0 for current buffer
    889 void nvim_buf_del_keymap(uint64_t channel_id, Buffer buffer, String mode, String lhs, Error *err)
    890  FUNC_API_SINCE(6)
    891 {
    892  String rhs = { .data = "", .size = 0 };
    893  modify_keymap(channel_id, buffer, true, mode, lhs, rhs, NULL, err);
    894 }
    895 
    896 /// Sets a buffer-scoped (b:) variable
    897 ///
    898 /// @param buffer     Buffer id, or 0 for current buffer
    899 /// @param name       Variable name
    900 /// @param value      Variable value
    901 /// @param[out] err   Error details, if any
    902 void nvim_buf_set_var(Buffer buffer, String name, Object value, Error *err)
    903  FUNC_API_SINCE(1)
    904 {
    905  buf_T *buf = find_buffer_by_handle(buffer, err);
    906 
    907  if (!buf) {
    908    return;
    909  }
    910 
    911  dict_set_var(buf->b_vars, name, value, false, false, NULL, err);
    912 }
    913 
    914 /// Removes a buffer-scoped (b:) variable
    915 ///
    916 /// @param buffer     Buffer id, or 0 for current buffer
    917 /// @param name       Variable name
    918 /// @param[out] err   Error details, if any
    919 void nvim_buf_del_var(Buffer buffer, String name, Error *err)
    920  FUNC_API_SINCE(1)
    921 {
    922  buf_T *buf = find_buffer_by_handle(buffer, err);
    923 
    924  if (!buf) {
    925    return;
    926  }
    927 
    928  dict_set_var(buf->b_vars, name, NIL, true, false, NULL, err);
    929 }
    930 
    931 /// Gets the full file name for the buffer
    932 ///
    933 /// @param buffer     Buffer id, or 0 for current buffer
    934 /// @param[out] err   Error details, if any
    935 /// @return Buffer name
    936 String nvim_buf_get_name(Buffer buffer, Error *err)
    937  FUNC_API_SINCE(1)
    938 {
    939  String rv = STRING_INIT;
    940  buf_T *buf = find_buffer_by_handle(buffer, err);
    941 
    942  if (!buf || buf->b_ffname == NULL) {
    943    return rv;
    944  }
    945 
    946  return cstr_as_string(buf->b_ffname);
    947 }
    948 
    949 /// Sets the full file name for a buffer, like |:file_f|
    950 ///
    951 /// @param buffer     Buffer id, or 0 for current buffer
    952 /// @param name       Buffer name
    953 /// @param[out] err   Error details, if any
    954 void nvim_buf_set_name(Buffer buffer, String name, Error *err)
    955  FUNC_API_SINCE(1)
    956 {
    957  buf_T *buf = find_buffer_by_handle(buffer, err);
    958 
    959  if (!buf) {
    960    return;
    961  }
    962 
    963  int ren_ret = OK;
    964  TRY_WRAP(err, {
    965    const bool is_curbuf = buf == curbuf;
    966    const int save_acd = p_acd;
    967    if (!is_curbuf) {
    968      // Temporarily disable 'autochdir' when setting file name for another buffer.
    969      p_acd = false;
    970    }
    971 
    972    // Using aucmd_*: autocommands will be executed by rename_buffer
    973    aco_save_T aco;
    974    aucmd_prepbuf(&aco, buf);
    975    ren_ret = rename_buffer(name.data);
    976    aucmd_restbuf(&aco);
    977 
    978    if (!is_curbuf) {
    979      p_acd = save_acd;
    980    }
    981  });
    982 
    983  if (ERROR_SET(err)) {
    984    return;
    985  }
    986 
    987  if (ren_ret == FAIL) {
    988    api_set_error(err, kErrorTypeException, "Failed to rename buffer");
    989  }
    990 }
    991 
    992 /// Checks if a buffer is valid and loaded. See |api-buffer| for more info
    993 /// about unloaded buffers.
    994 ///
    995 /// @param buffer Buffer id, or 0 for current buffer
    996 /// @return true if the buffer is valid and loaded, false otherwise.
    997 Boolean nvim_buf_is_loaded(Buffer buffer)
    998  FUNC_API_SINCE(5)
    999 {
   1000  Error stub = ERROR_INIT;
   1001  buf_T *buf = find_buffer_by_handle(buffer, &stub);
   1002  api_clear_error(&stub);
   1003  return buf && buf->b_ml.ml_mfp != NULL;
   1004 }
   1005 
   1006 /// Deletes a buffer and its metadata (like |:bwipeout|).
   1007 ///
   1008 /// To get |:bdelete| behavior, reset 'buflisted' and pass `unload=true`:
   1009 /// ```lua
   1010 /// vim.bo.buflisted = false
   1011 /// vim.api.nvim_buf_delete(0, { unload = true })
   1012 /// ```
   1013 ///
   1014 /// @param buffer Buffer id, or 0 for current buffer
   1015 /// @param opts  Optional parameters. Keys:
   1016 ///          - force:  Force deletion, ignore unsaved changes.
   1017 ///          - unload: Unloaded only (|:bunload|), do not delete.
   1018 void nvim_buf_delete(Buffer buffer, Dict(buf_delete) *opts, Error *err)
   1019  FUNC_API_SINCE(7)
   1020  FUNC_API_TEXTLOCK
   1021 {
   1022  buf_T *buf = find_buffer_by_handle(buffer, err);
   1023 
   1024  if (ERROR_SET(err)) {
   1025    return;
   1026  }
   1027 
   1028  bool force = opts->force;
   1029 
   1030  bool unload = opts->unload;
   1031 
   1032  int result = do_buffer(unload ? DOBUF_UNLOAD : DOBUF_WIPE,
   1033                         DOBUF_FIRST,
   1034                         FORWARD,
   1035                         buf->handle,
   1036                         force);
   1037 
   1038  if (result == FAIL) {
   1039    api_set_error(err, kErrorTypeException, "Failed to unload buffer.");
   1040    return;
   1041  }
   1042 }
   1043 
   1044 /// Checks if a buffer is valid.
   1045 ///
   1046 /// @note Even if a buffer is valid it may have been unloaded. See |api-buffer|
   1047 /// for more info about unloaded buffers.
   1048 ///
   1049 /// @param buffer Buffer id, or 0 for current buffer
   1050 /// @return true if the buffer is valid, false otherwise.
   1051 Boolean nvim_buf_is_valid(Buffer buffer)
   1052  FUNC_API_SINCE(1)
   1053 {
   1054  Error stub = ERROR_INIT;
   1055  Boolean ret = find_buffer_by_handle(buffer, &stub) != NULL;
   1056  api_clear_error(&stub);
   1057  return ret;
   1058 }
   1059 
   1060 /// Deletes a named mark in the buffer. See |mark-motions|.
   1061 ///
   1062 /// @note only deletes marks set in the buffer, if the mark is not set
   1063 /// in the buffer it will return false.
   1064 /// @param buffer     Buffer to set the mark on
   1065 /// @param name       Mark name
   1066 /// @return true if the mark was deleted, else false.
   1067 /// @see |nvim_buf_set_mark()|
   1068 /// @see |nvim_del_mark()|
   1069 Boolean nvim_buf_del_mark(Buffer buffer, String name, Error *err)
   1070  FUNC_API_SINCE(8)
   1071 {
   1072  bool res = false;
   1073  buf_T *buf = find_buffer_by_handle(buffer, err);
   1074 
   1075  if (!buf) {
   1076    return res;
   1077  }
   1078 
   1079  VALIDATE_S((name.size == 1), "mark name (must be a single char)", name.data, {
   1080    return res;
   1081  });
   1082 
   1083  fmark_T *fm = mark_get(buf, curwin, NULL, kMarkAllNoResolve, *name.data);
   1084 
   1085  // fm is NULL when there's no mark with the given name
   1086  VALIDATE_S((fm != NULL), "mark name", name.data, {
   1087    return res;
   1088  });
   1089 
   1090  // mark.lnum is 0 when the mark is not valid in the buffer, or is not set.
   1091  if (fm->mark.lnum != 0 && fm->fnum == buf->handle) {
   1092    // since the mark belongs to the buffer delete it.
   1093    res = set_mark(buf, name, 0, 0, err);
   1094  }
   1095 
   1096  return res;
   1097 }
   1098 
   1099 /// Sets a named mark in the given buffer, all marks are allowed
   1100 /// file/uppercase, visual, last change, etc. See |mark-motions|.
   1101 ///
   1102 /// Marks are (1,0)-indexed. |api-indexing|
   1103 ///
   1104 /// @note Passing 0 as line deletes the mark
   1105 ///
   1106 /// @param buffer     Buffer to set the mark on
   1107 /// @param name       Mark name
   1108 /// @param line       Line number
   1109 /// @param col        Column/row number
   1110 /// @param opts       Optional parameters. Reserved for future use.
   1111 /// @return true if the mark was set, else false.
   1112 /// @see |nvim_buf_del_mark()|
   1113 /// @see |nvim_buf_get_mark()|
   1114 Boolean nvim_buf_set_mark(Buffer buffer, String name, Integer line, Integer col, Dict(empty) *opts,
   1115                          Error *err)
   1116  FUNC_API_SINCE(8)
   1117 {
   1118  bool res = false;
   1119  buf_T *buf = api_buf_ensure_loaded(buffer, err);
   1120 
   1121  if (!buf) {
   1122    return res;
   1123  }
   1124 
   1125  VALIDATE_S((name.size == 1), "mark name (must be a single char)", name.data, {
   1126    return res;
   1127  });
   1128 
   1129  res = set_mark(buf, name, line, col, err);
   1130 
   1131  return res;
   1132 }
   1133 
   1134 /// Returns a `(row,col)` tuple representing the position of the named mark.
   1135 /// "End of line" column position is returned as |v:maxcol| (big number).
   1136 /// See |mark-motions|.
   1137 ///
   1138 /// Marks are (1,0)-indexed. |api-indexing|
   1139 ///
   1140 /// @param buffer     Buffer id, or 0 for current buffer
   1141 /// @param name       Mark name
   1142 /// @param[out] err   Error details, if any
   1143 /// @return (row, col) tuple, (0, 0) if the mark is not set, or is an
   1144 /// uppercase/file mark set in another buffer.
   1145 /// @see |nvim_buf_set_mark()|
   1146 /// @see |nvim_buf_del_mark()|
   1147 ArrayOf(Integer, 2) nvim_buf_get_mark(Buffer buffer, String name, Arena *arena, Error *err)
   1148  FUNC_API_SINCE(1)
   1149 {
   1150  Array rv = ARRAY_DICT_INIT;
   1151  buf_T *buf = find_buffer_by_handle(buffer, err);
   1152 
   1153  if (!buf) {
   1154    return rv;
   1155  }
   1156 
   1157  VALIDATE_S((name.size == 1), "mark name (must be a single char)", name.data, {
   1158    return rv;
   1159  });
   1160 
   1161  fmark_T *fm;
   1162  pos_T pos;
   1163  char mark = *name.data;
   1164 
   1165  fm = mark_get(buf, curwin, NULL, kMarkAllNoResolve, mark);
   1166  VALIDATE_S((fm != NULL), "mark name", name.data, {
   1167    return rv;
   1168  });
   1169  // (0, 0) uppercase/file mark set in another buffer.
   1170  if (fm->fnum != buf->handle) {
   1171    pos.lnum = 0;
   1172    pos.col = 0;
   1173  } else {
   1174    pos = fm->mark;
   1175  }
   1176 
   1177  rv = arena_array(arena, 2);
   1178  ADD_C(rv, INTEGER_OBJ(pos.lnum));
   1179  ADD_C(rv, INTEGER_OBJ(pos.col));
   1180 
   1181  return rv;
   1182 }
   1183 
   1184 /// Call a function with buffer as temporary current buffer.
   1185 ///
   1186 /// This temporarily switches current buffer to "buffer".
   1187 /// If the current window already shows "buffer", the window is not switched.
   1188 /// If a window inside the current tabpage (including a float) already shows the
   1189 /// buffer, then one of those windows will be set as current window temporarily.
   1190 /// Otherwise a temporary scratch window (called the "autocmd window" for
   1191 /// historical reasons) will be used.
   1192 ///
   1193 /// This is useful e.g. to call Vimscript functions that only work with the
   1194 /// current buffer/window currently, like `jobstart(…, {'term': v:true})`.
   1195 ///
   1196 /// @param buffer     Buffer id, or 0 for current buffer
   1197 /// @param fun        Function to call inside the buffer (currently Lua callable
   1198 ///                   only)
   1199 /// @param[out] err   Error details, if any
   1200 /// @return           Return value of function.
   1201 Object nvim_buf_call(Buffer buffer, LuaRef fun, Error *err)
   1202  FUNC_API_SINCE(7)
   1203  FUNC_API_LUA_ONLY
   1204 {
   1205  buf_T *buf = find_buffer_by_handle(buffer, err);
   1206  if (!buf) {
   1207    return NIL;
   1208  }
   1209 
   1210  Object res = OBJECT_INIT;
   1211  TRY_WRAP(err, {
   1212    aco_save_T aco;
   1213    aucmd_prepbuf(&aco, buf);
   1214 
   1215    Array args = ARRAY_DICT_INIT;
   1216    res = nlua_call_ref(fun, NULL, args, kRetLuaref, NULL, err);
   1217 
   1218    aucmd_restbuf(&aco);
   1219  });
   1220 
   1221  return res;
   1222 }
   1223 
   1224 /// @nodoc
   1225 Dict nvim__buf_stats(Buffer buffer, Arena *arena, Error *err)
   1226 {
   1227  buf_T *buf = find_buffer_by_handle(buffer, err);
   1228  if (!buf) {
   1229    return (Dict)ARRAY_DICT_INIT;
   1230  }
   1231 
   1232  Dict rv = arena_dict(arena, 7);
   1233  // Number of times the cached line was flushed.
   1234  // This should generally not increase while editing the same
   1235  // line in the same mode.
   1236  PUT_C(rv, "flush_count", INTEGER_OBJ(buf->flush_count));
   1237  // lnum of current line
   1238  PUT_C(rv, "current_lnum", INTEGER_OBJ(buf->b_ml.ml_line_lnum));
   1239  // whether the line has unflushed changes.
   1240  PUT_C(rv, "line_dirty", BOOLEAN_OBJ(buf->b_ml.ml_flags & ML_LINE_DIRTY));
   1241  // NB: this should be zero at any time API functions are called,
   1242  // this exists to debug issues
   1243  PUT_C(rv, "dirty_bytes", INTEGER_OBJ((Integer)buf->deleted_bytes));
   1244  PUT_C(rv, "dirty_bytes2", INTEGER_OBJ((Integer)buf->deleted_bytes2));
   1245  PUT_C(rv, "virt_blocks", INTEGER_OBJ((Integer)buf_meta_total(buf, kMTMetaLines)));
   1246 
   1247  u_header_T *uhp = NULL;
   1248  if (buf->b_u_curhead != NULL) {
   1249    uhp = buf->b_u_curhead;
   1250  } else if (buf->b_u_newhead) {
   1251    uhp = buf->b_u_newhead;
   1252  }
   1253  if (uhp) {
   1254    PUT_C(rv, "uhp_extmark_size", INTEGER_OBJ((Integer)kv_size(uhp->uh_extmark)));
   1255  }
   1256 
   1257  return rv;
   1258 }
   1259 
   1260 // Check if deleting lines made the cursor position invalid.
   1261 // Changed lines from `lo` to `hi`; added `extra` lines (negative if deleted).
   1262 static void fix_cursor(win_T *win, linenr_T lo, linenr_T hi, linenr_T extra)
   1263 {
   1264  if (win->w_cursor.lnum >= lo) {
   1265    // Adjust cursor position if it's in/after the changed lines.
   1266    if (win->w_cursor.lnum >= hi) {
   1267      win->w_cursor.lnum += extra;
   1268    } else if (extra < 0) {
   1269      check_cursor_lnum(win);
   1270    }
   1271    check_cursor_col(win);
   1272    changed_cline_bef_curs(win);
   1273    win->w_valid &= ~(VALID_BOTLINE_AP);
   1274    update_topline(win);
   1275  } else {
   1276    invalidate_botline_win(win);
   1277  }
   1278 }
   1279 
   1280 /// Fix cursor position after replacing text
   1281 /// between (start_row, start_col) and (end_row, end_col).
   1282 ///
   1283 /// win->w_cursor.lnum is assumed to be >= start_row and <= end_row.
   1284 static void fix_cursor_cols(win_T *win, linenr_T start_row, colnr_T start_col, linenr_T end_row,
   1285                            colnr_T end_col, linenr_T new_rows, colnr_T new_cols_at_end_row)
   1286 {
   1287  colnr_T mode_col_adj = win == curwin && (State & MODE_INSERT) ? 0 : 1;
   1288 
   1289  colnr_T end_row_change_start = new_rows == 1 ? start_col : 0;
   1290  colnr_T end_row_change_end = end_row_change_start + new_cols_at_end_row;
   1291 
   1292  // check if cursor is after replaced range or not
   1293  if (win->w_cursor.lnum == end_row && win->w_cursor.col + mode_col_adj > end_col) {
   1294    // if cursor is after replaced range, it's shifted
   1295    // to keep it's position the same, relative to end_col
   1296 
   1297    linenr_T old_rows = end_row - start_row + 1;
   1298    win->w_cursor.lnum += new_rows - old_rows;
   1299    win->w_cursor.col += end_row_change_end - end_col;
   1300  } else {
   1301    // if cursor is inside replaced range
   1302    // and the new range got smaller,
   1303    // it's shifted to keep it inside the new range
   1304    //
   1305    // if cursor is before range or range did not
   1306    // got smaller, position is not changed
   1307 
   1308    colnr_T old_coladd = win->w_cursor.coladd;
   1309 
   1310    // it's easier to work with a single value here.
   1311    // col and coladd are fixed by a later call
   1312    // to check_cursor_col when necessary
   1313    win->w_cursor.col += win->w_cursor.coladd;
   1314    win->w_cursor.coladd = 0;
   1315 
   1316    linenr_T new_end_row = start_row + new_rows - 1;
   1317 
   1318    // make sure cursor row is in the new row range
   1319    if (win->w_cursor.lnum > new_end_row) {
   1320      win->w_cursor.lnum = new_end_row;
   1321 
   1322      // don't simply move cursor up, but to the end
   1323      // of new_end_row, if it's not at or after
   1324      // it already (in case virtualedit is active)
   1325      // column might be additionally adjusted below
   1326      // to keep it inside col range if needed
   1327      colnr_T len = ml_get_buf_len(win->w_buffer, new_end_row);
   1328      if (win->w_cursor.col < len) {
   1329        win->w_cursor.col = len;
   1330      }
   1331    }
   1332 
   1333    // if cursor is at the last row and
   1334    // it wasn't after eol before, move it exactly
   1335    // to end_row_change_end
   1336    if (win->w_cursor.lnum == new_end_row
   1337        && win->w_cursor.col > end_row_change_end && old_coladd == 0) {
   1338      win->w_cursor.col = end_row_change_end;
   1339 
   1340      // make sure cursor is inside range, not after it,
   1341      // except when doing so would move it before new range
   1342      if (win->w_cursor.col - mode_col_adj >= end_row_change_start) {
   1343        win->w_cursor.col -= mode_col_adj;
   1344      }
   1345    }
   1346  }
   1347 
   1348  check_cursor_col(win);
   1349  changed_cline_bef_curs(win);
   1350  invalidate_botline_win(win);
   1351 }
   1352 
   1353 /// Initialise a string array either:
   1354 /// - on the Lua stack (as a table) (if lstate is not NULL)
   1355 /// - as an API array object (if lstate is NULL).
   1356 ///
   1357 /// @param lstate  Lua state. When NULL the Array is initialized instead.
   1358 /// @param a       Array to initialize
   1359 /// @param size    Size of array
   1360 static inline void init_line_array(lua_State *lstate, Array *a, size_t size, Arena *arena)
   1361 {
   1362  if (lstate) {
   1363    lua_createtable(lstate, (int)size, 0);
   1364  } else {
   1365    *a = arena_array(arena, size);
   1366  }
   1367 }
   1368 
   1369 /// Push a string onto either the Lua stack (as a table element) or an API array object.
   1370 ///
   1371 /// For Lua, a table of the correct size must be created first.
   1372 /// API array objects must be pre allocated.
   1373 ///
   1374 /// @param lstate      Lua state. When NULL the Array is pushed to instead.
   1375 /// @param a           Array to push onto when not using Lua
   1376 /// @param s           String to push
   1377 /// @param len         Size of string
   1378 /// @param idx         0-based index to place s (only used for Lua)
   1379 /// @param replace_nl  Replace newlines ('\n') with null (NUL)
   1380 static void push_linestr(lua_State *lstate, Array *a, const char *s, size_t len, int idx,
   1381                         bool replace_nl, Arena *arena)
   1382 {
   1383  if (lstate) {
   1384    // Vim represents NULs as NLs
   1385    if (s && replace_nl && strchr(s, '\n')) {
   1386      // TODO(bfredl): could manage scratch space in the arena, for the NUL case
   1387      char *tmp = xmemdupz(s, len);
   1388      strchrsub(tmp, '\n', NUL);
   1389      lua_pushlstring(lstate, tmp, len);
   1390      xfree(tmp);
   1391    } else {
   1392      lua_pushlstring(lstate, s, len);
   1393    }
   1394    lua_rawseti(lstate, -2, idx + 1);
   1395  } else {
   1396    String str = STRING_INIT;
   1397    if (len > 0) {
   1398      str = CBUF_TO_ARENA_STR(arena, s, len);
   1399      if (replace_nl) {
   1400        // Vim represents NULs as NLs, but this may confuse clients.
   1401        strchrsub(str.data, '\n', NUL);
   1402      }
   1403    }
   1404 
   1405    ADD_C(*a, STRING_OBJ(str));
   1406  }
   1407 }
   1408 
   1409 /// Collects `n` buffer lines into array `l` and/or lua_State `lstate`, optionally replacing
   1410 /// newlines with NUL.
   1411 ///
   1412 /// @param buf Buffer to get lines from
   1413 /// @param n Number of lines to collect
   1414 /// @param replace_nl Replace newlines ("\n") with NUL
   1415 /// @param start Line number to start from
   1416 /// @param start_idx First index to push to (only used for Lua)
   1417 /// @param[out] l If not NULL, Lines are copied here
   1418 /// @param[out] lstate If not NULL, Lines are pushed into a table onto the stack
   1419 /// @param err[out] Error, if any
   1420 /// @return true unless `err` was set
   1421 void buf_collect_lines(buf_T *buf, size_t n, linenr_T start, int start_idx, bool replace_nl,
   1422                       Array *l, lua_State *lstate, Arena *arena)
   1423 {
   1424  for (size_t i = 0; i < n; i++) {
   1425    linenr_T lnum = start + (linenr_T)i;
   1426    char *bufstr = ml_get_buf(buf, lnum);
   1427    size_t bufstrlen = (size_t)ml_get_buf_len(buf, lnum);
   1428    push_linestr(lstate, l, bufstr, bufstrlen, start_idx + (int)i, replace_nl, arena);
   1429  }
   1430 }