// This sample is from Neovim's source code.

//
// buffer.c: functions for dealing with the buffer structure
//

//
// The buffer list is a double linked list of all buffers.
// Each buffer can be in one of these states:
// never loaded: BF_NEVERLOADED is set, only the file name is valid
//   not loaded: b_ml.ml_mfp == NULL, no memfile allocated
//       hidden: b_nwindows == 0, loaded but not displayed in a window
//       normal: loaded and displayed in a window
//
// Instead of storing file names all over the place, each file name is
// stored in the buffer list. It can be referenced by a number.
//
// The current implementation remembers all file names ever used.
//

#include <stdbool.h>
#include <string.h>
#include <inttypes.h>
#include <assert.h>

#include "nvim/api/private/handle.h"
#include "nvim/api/private/helpers.h"
#include "nvim/api/vim.h"
#include "nvim/ascii.h"
#include "nvim/assert.h"
#include "nvim/channel.h"
#include "nvim/vim.h"
#include "nvim/buffer.h"
#include "nvim/change.h"
#include "nvim/charset.h"
#include "nvim/cursor.h"
#include "nvim/diff.h"
#include "nvim/digraph.h"
#include "nvim/eval.h"
#include "nvim/ex_cmds2.h"
#include "nvim/ex_cmds.h"
#include "nvim/ex_docmd.h"
#include "nvim/ex_eval.h"
#include "nvim/ex_getln.h"
#include "nvim/fileio.h"
#include "nvim/file_search.h"
#include "nvim/fold.h"
#include "nvim/getchar.h"
#include "nvim/hashtab.h"
#include "nvim/highlight.h"
#include "nvim/indent.h"
#include "nvim/indent_c.h"
#include "nvim/main.h"
#include "nvim/mark.h"
#include "nvim/extmark.h"
#include "nvim/mbyte.h"
#include "nvim/memline.h"
#include "nvim/memory.h"
#include "nvim/message.h"
#include "nvim/misc1.h"
#include "nvim/garray.h"
#include "nvim/move.h"
#include "nvim/option.h"
#include "nvim/os_unix.h"
#include "nvim/path.h"
#include "nvim/quickfix.h"
#include "nvim/regexp.h"
#include "nvim/screen.h"
#include "nvim/sign.h"
#include "nvim/spell.h"
#include "nvim/strings.h"
#include "nvim/syntax.h"
#include "nvim/ui.h"
#include "nvim/undo.h"
#include "nvim/version.h"
#include "nvim/window.h"
#include "nvim/shada.h"
#include "nvim/os/os.h"
#include "nvim/os/time.h"
#include "nvim/os/input.h"
#include "nvim/buffer_updates.h"

#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "buffer.c.generated.h"
#endif

static char *msg_loclist = N_("[Location List]");
static char *msg_qflist = N_("[Quickfix List]");
static char *e_auabort = N_("E855: Autocommands caused command to abort");

// Number of times free_buffer() was called.
static int buf_free_count = 0;

typedef enum {
  kBffClearWinInfo = 1,
  kBffInitChangedtick = 2,
} BufFreeFlags;

// Read data from buffer for retrying.
static int
read_buffer(
    int     read_stdin,     // read file from stdin, otherwise fifo
    exarg_T *eap,           // for forced 'ff' and 'fenc' or NULL
    int     flags)          // extra flags for readfile()
{
  int       retval = OK;
  linenr_T  line_count;

  //
  // Read from the buffer which the text is already filled in and append at
  // the end.  This makes it possible to retry when 'fileformat' or
  // 'fileencoding' was guessed wrong.
  //
  line_count = curbuf->b_ml.ml_line_count;
  retval = readfile(
      read_stdin ? NULL : curbuf->b_ffname,
      read_stdin ? NULL : curbuf->b_fname,
      (linenr_T)line_count, (linenr_T)0, (linenr_T)MAXLNUM, eap,
      flags | READ_BUFFER);
  if (retval == OK) {
    // Delete the binary lines.
    while (--line_count >= 0) {
      ml_delete((linenr_T)1, false);
    }
  } else {
    // Delete the converted lines.
    while (curbuf->b_ml.ml_line_count > line_count) {
      ml_delete(line_count, false);
    }
  }
  // Put the cursor on the first line.
  curwin->w_cursor.lnum = 1;
  curwin->w_cursor.col = 0;

  if (read_stdin) {
    // Set or reset 'modified' before executing autocommands, so that
    // it can be changed there.
    if (!readonlymode && !BUFEMPTY()) {
      changed();
    } else if (retval != FAIL) {
      unchanged(curbuf, false, true);
    }

    apply_autocmds_retval(EVENT_STDINREADPOST, NULL, NULL, false,
                          curbuf, &retval);
  }
  return retval;
}

// Open current buffer, that is: open the memfile and read the file into
// memory.
// Return FAIL for failure, OK otherwise.
int open_buffer(
    int read_stdin,   // read file from stdin
    exarg_T *eap,     // for forced 'ff' and 'fenc' or NULL
    int flags         // extra flags for readfile()
)
{
  int retval = OK;
  bufref_T       old_curbuf;
  long old_tw = curbuf->b_p_tw;
  int read_fifo = false;

  /*
   * The 'readonly' flag is only set when BF_NEVERLOADED is being reset.
   * When re-entering the same buffer, it should not change, because the
   * user may have reset the flag by hand.
   */
  if (readonlymode && curbuf->b_ffname != NULL
      && (curbuf->b_flags & BF_NEVERLOADED))
    curbuf->b_p_ro = true;

  if (ml_open(curbuf) == FAIL) {
    /*
     * There MUST be a memfile, otherwise we can't do anything
     * If we can't create one for the current buffer, take another buffer
     */
    close_buffer(NULL, curbuf, 0, false);

    curbuf = NULL;
    FOR_ALL_BUFFERS(buf) {
      if (buf->b_ml.ml_mfp != NULL) {
        curbuf = buf;
        break;
      }
    }

    /*
     * if there is no memfile at all, exit
     * This is OK, since there are no changes to lose.
     */
    if (curbuf == NULL) {
      EMSG(_("E82: Cannot allocate any buffer, exiting..."));
      getout(2);
    }
    EMSG(_("E83: Cannot allocate buffer, using other one..."));
    enter_buffer(curbuf);
    if (old_tw != curbuf->b_p_tw) {
      check_colorcolumn(curwin);
    }
    return FAIL;
  }

  // The autocommands in readfile() may change the buffer, but only AFTER
  // reading the file.
  set_bufref(&old_curbuf, curbuf);
  modified_was_set = false;

  // mark cursor position as being invalid
  curwin->w_valid = 0;

  if (curbuf->b_ffname != NULL) {
    int old_msg_silent = msg_silent;
#ifdef UNIX
    int save_bin = curbuf->b_p_bin;
    int perm;

    perm = os_getperm((const char *)curbuf->b_ffname);
    if (perm >= 0 && (0
# ifdef S_ISFIFO
                      || S_ISFIFO(perm)
# endif
# ifdef S_ISSOCK
                      || S_ISSOCK(perm)
# endif
# ifdef OPEN_CHR_FILES
                      || (S_ISCHR(perm)
                          && is_dev_fd_file(curbuf->b_ffname))
# endif
                      )
        ) {
      read_fifo = true;
    }
    if (read_fifo) {
      curbuf->b_p_bin = true;
    }
#endif
    if (shortmess(SHM_FILEINFO)) {
      msg_silent = 1;
    }

    retval = readfile(curbuf->b_ffname, curbuf->b_fname,
                      (linenr_T)0, (linenr_T)0, (linenr_T)MAXLNUM, eap,
                      flags | READ_NEW | (read_fifo ? READ_FIFO : 0));
#ifdef UNIX
    if (read_fifo) {
      curbuf->b_p_bin = save_bin;
      if (retval == OK) {
        retval = read_buffer(false, eap, flags);
      }
    }
#endif
    msg_silent = old_msg_silent;

    // Help buffer is filtered.
    if (bt_help(curbuf)) {
      fix_help_buffer();
    }
  } else if (read_stdin) {
    int save_bin = curbuf->b_p_bin;

    /*
     * First read the text in binary mode into the buffer.
     * Then read from that same buffer and append at the end.  This makes
     * it possible to retry when 'fileformat' or 'fileencoding' was
     * guessed wrong.
     */
    curbuf->b_p_bin = true;
    retval = readfile(NULL, NULL, (linenr_T)0,
        (linenr_T)0, (linenr_T)MAXLNUM, NULL,
        flags | (READ_NEW + READ_STDIN));
    curbuf->b_p_bin = save_bin;
    if (retval == OK) {
      retval = read_buffer(true, eap, flags);
    }
  }

  // if first time loading this buffer, init b_chartab[]
  if (curbuf->b_flags & BF_NEVERLOADED) {
    (void)buf_init_chartab(curbuf, false);
    parse_cino(curbuf);
  }

  // Set/reset the Changed flag first, autocmds may change the buffer.
  // Apply the automatic commands, before processing the modelines.
  // So the modelines have priority over auto commands.

  // When reading stdin, the buffer contents always needs writing, so set
  // the changed flag.  Unless in readonly mode: "ls | nvim -R -".
  // When interrupted and 'cpoptions' contains 'i' set changed flag.
  if ((got_int && vim_strchr(p_cpo, CPO_INTMOD) != NULL)
      || modified_was_set               // ":set modified" used in autocmd
      || (aborting() && vim_strchr(p_cpo, CPO_INTMOD) != NULL)) {
    changed();
  } else if (retval != FAIL && !read_stdin && !read_fifo) {
    unchanged(curbuf, false, true);
  }
  save_file_ff(curbuf);                 // keep this fileformat

  // Set last_changedtick to avoid triggering a TextChanged autocommand right
  // after it was added.
  curbuf->b_last_changedtick = buf_get_changedtick(curbuf);
  curbuf->b_last_changedtick_pum = buf_get_changedtick(curbuf);

  // require "!" to overwrite the file, because it wasn't read completely
  if (aborting()) {
    curbuf->b_flags |= BF_READERR;
  }

  /* Need to update automatic folding.  Do this before the autocommands,
   * they may use the fold info. */
  foldUpdateAll(curwin);

  // need to set w_topline, unless some autocommand already did that.
  if (!(curwin->w_valid & VALID_TOPLINE)) {
    curwin->w_topline = 1;
    curwin->w_topfill = 0;
  }
  apply_autocmds_retval(EVENT_BUFENTER, NULL, NULL, false, curbuf, &retval);

  if (retval == FAIL) {
    return FAIL;
  }

  /*
   * The autocommands may have changed the current buffer.  Apply the
   * modelines to the correct buffer, if it still exists and is loaded.
   */
  if (bufref_valid(&old_curbuf) && old_curbuf.br_buf->b_ml.ml_mfp != NULL) {
    aco_save_T aco;

    // Go to the buffer that was opened.
    aucmd_prepbuf(&aco, old_curbuf.br_buf);
    do_modelines(0);
    curbuf->b_flags &= ~(BF_CHECK_RO | BF_NEVERLOADED);

    apply_autocmds_retval(EVENT_BUFWINENTER, NULL, NULL, false, curbuf,
                          &retval);

    // restore curwin/curbuf and a few other things
    aucmd_restbuf(&aco);
  }

  return retval;
}

/// Store "buf" in "bufref" and set the free count.
///
/// @param bufref Reference to be used for the buffer.
/// @param buf    The buffer to reference.
void set_bufref(bufref_T *bufref, buf_T *buf)
{
  bufref->br_buf = buf;
  bufref->br_fnum = buf == NULL ? 0 : buf->b_fnum;
  bufref->br_buf_free_count = buf_free_count;
}

/// Return true if "bufref->br_buf" points to the same buffer as when
/// set_bufref() was called and it is a valid buffer.
/// Only goes through the buffer list if buf_free_count changed.
/// Also checks if b_fnum is still the same, a :bwipe followed by :new might get
/// the same allocated memory, but it's a different buffer.
///
/// @param bufref Buffer reference to check for.
bool bufref_valid(bufref_T *bufref)
{
  return bufref->br_buf_free_count == buf_free_count
    ? true
    : buf_valid(bufref->br_buf) && bufref->br_fnum == bufref->br_buf->b_fnum;
}

/// Check that "buf" points to a valid buffer in the buffer list.
///
/// Can be slow if there are many buffers, prefer using bufref_valid().
///
/// @param buf The buffer to check for.
bool buf_valid(buf_T *buf)
  FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT
{
  if (buf == NULL) {
    return false;
  }
  // Assume that we more often have a recent buffer,
  // start with the last one.
  for (buf_T *bp = lastbuf; bp != NULL; bp = bp->b_prev) {
    if (bp == buf) {
      return true;
    }
  }
  return false;
}

/// Close the link to a buffer.
///
/// @param win    If not NULL, set b_last_cursor.
/// @param buf
/// @param action Used when there is no longer a window for the buffer.
///               Possible values:
///                 0            buffer becomes hidden
///                 DOBUF_UNLOAD buffer is unloaded
///                 DOBUF_DELETE buffer is unloaded and removed from buffer list
///                 DOBUF_WIPE   buffer is unloaded and really deleted
///               When doing all but the first one on the current buffer, the
///               caller should get a new buffer very soon!
///               The 'bufhidden' option can force freeing and deleting.
/// @param abort_if_last
///               If true, do not close the buffer if autocommands cause
///               there to be only one window with this buffer. e.g. when
///               ":quit" is supposed to close the window but autocommands
///               close all other windows.
void close_buffer(win_T *win, buf_T *buf, int action, bool abort_if_last)
{
  bool unload_buf = (action != 0);
  bool del_buf = (action == DOBUF_DEL || action == DOBUF_WIPE);
  bool wipe_buf = (action == DOBUF_WIPE);

  bool is_curwin = (curwin != NULL && curwin->w_buffer == buf);
  win_T *the_curwin = curwin;
  tabpage_T *the_curtab = curtab;

  // Force unloading or deleting when 'bufhidden' says so, but not for terminal
  // buffers.
  // The caller must take care of NOT deleting/freeing when 'bufhidden' is
  // "hide" (otherwise we could never free or delete a buffer).
  if (!buf->terminal) {
    if (buf->b_p_bh[0] == 'd') {         // 'bufhidden' == "delete"
      del_buf = true;
      unload_buf = true;
    } else if (buf->b_p_bh[0] == 'w') {  // 'bufhidden' == "wipe"
      del_buf = true;
      unload_buf = true;
      wipe_buf = true;
    } else if (buf->b_p_bh[0] == 'u')    // 'bufhidden' == "unload"
      unload_buf = true;
  }

  if (buf->terminal && (unload_buf || del_buf || wipe_buf)) {
    // terminal buffers can only be wiped
    unload_buf = true;
    del_buf = true;
    wipe_buf = true;
  }

  // Disallow deleting the buffer when it is locked (already being closed or
  // halfway a command that relies on it). Unloading is allowed.
  if (buf->b_locked > 0 && (del_buf || wipe_buf)) {
    EMSG(_("E937: Attempt to delete a buffer that is in use"));
    return;
  }

  if (win != NULL  // Avoid bogus clang warning.
      && win_valid_any_tab(win)) {
    // Set b_last_cursor when closing the last window for the buffer.
    // Remember the last cursor position and window options of the buffer.
    // This used to be only for the current window, but then options like
    // 'foldmethod' may be lost with a ":only" command.
    if (buf->b_nwindows == 1) {
      set_last_cursor(win);
    }
    buflist_setfpos(buf, win,
                    win->w_cursor.lnum == 1 ? 0 : win->w_cursor.lnum,
                    win->w_cursor.col, true);
  }

  bufref_T bufref;
  set_bufref(&bufref, buf);

  // When the buffer is no longer in a window, trigger BufWinLeave
  if (buf->b_nwindows == 1) {
    buf->b_locked++;
    if (apply_autocmds(EVENT_BUFWINLEAVE, buf->b_fname, buf->b_fname, false,
                       buf) && !bufref_valid(&bufref)) {
      // Autocommands deleted the buffer.
      EMSG(_(e_auabort));
      return;
    }
    buf->b_locked--;
    if (abort_if_last && last_nonfloat(win)) {
      // Autocommands made this the only window.
      EMSG(_(e_auabort));
      return;
    }

    // When the buffer becomes hidden, but is not unloaded, trigger
    // BufHidden
    if (!unload_buf) {
      buf->b_locked++;
      if (apply_autocmds(EVENT_BUFHIDDEN, buf->b_fname, buf->b_fname, false,
                         buf) && !bufref_valid(&bufref)) {
        // Autocommands deleted the buffer.
        EMSG(_(e_auabort));
        return;
      }
      buf->b_locked--;
      if (abort_if_last && last_nonfloat(win)) {
        // Autocommands made this the only window.
        EMSG(_(e_auabort));
        return;
      }
    }
    if (aborting()) {       // autocmds may abort script processing
      return;
    }
  }

  // If the buffer was in curwin and the window has changed, go back to that
  // window, if it still exists.  This avoids that ":edit x" triggering a
  // "tabnext" BufUnload autocmd leaves a window behind without a buffer.
  if (is_curwin && curwin != the_curwin &&  win_valid_any_tab(the_curwin)) {
    block_autocmds();
    goto_tabpage_win(the_curtab, the_curwin);
    unblock_autocmds();
  }

  int nwindows = buf->b_nwindows;

  // decrease the link count from windows (unless not in any window)
  if (buf->b_nwindows > 0) {
    buf->b_nwindows--;
  }

  if (diffopt_hiddenoff() && !unload_buf && buf->b_nwindows == 0) {
    diff_buf_delete(buf);   // Clear 'diff' for hidden buffer.
  }

  /* Return when a window is displaying the buffer or when it's not
   * unloaded. */
  if (buf->b_nwindows > 0 || !unload_buf) {
    return;
  }

  if (buf->terminal) {
    terminal_close(buf->terminal, NULL);
  }

  // Always remove the buffer when there is no file name.
  if (buf->b_ffname == NULL) {
    del_buf = true;
  }

  /*
   * Free all things allocated for this buffer.
   * Also calls the "BufDelete" autocommands when del_buf is TRUE.
   */
  /* Remember if we are closing the current buffer.  Restore the number of
   * windows, so that autocommands in buf_freeall() don't get confused. */
  bool is_curbuf = (buf == curbuf);

  // When closing the current buffer stop Visual mode before freeing
  // anything.
  if (is_curbuf && VIsual_active
#if defined(EXITFREE)
      && !entered_free_all_mem
#endif
      ) {
    end_visual_mode();
  }

  buf->b_nwindows = nwindows;

  buf_freeall(buf, (del_buf ? BFA_DEL : 0) + (wipe_buf ? BFA_WIPE : 0));

  if (!bufref_valid(&bufref)) {
    // Autocommands may have deleted the buffer.
    return;
  }
  if (aborting()) {
    // Autocmds may abort script processing.
    return;
  }

  /*
   * It's possible that autocommands change curbuf to the one being deleted.
   * This might cause the previous curbuf to be deleted unexpectedly.  But
   * in some cases it's OK to delete the curbuf, because a new one is
   * obtained anyway.  Therefore only return if curbuf changed to the
   * deleted buffer.
   */
  if (buf == curbuf && !is_curbuf) {
    return;
  }

  if (win != NULL  // Avoid bogus clang warning.
      && win_valid_any_tab(win)
      && win->w_buffer == buf) {
    win->w_buffer = NULL;  // make sure we don't use the buffer now
  }

  // Autocommands may have opened or closed windows for this buffer.
  // Decrement the count for the close we do here.
  if (buf->b_nwindows > 0) {
    buf->b_nwindows--;
  }

  // Change directories when the 'acd' option is set.
  do_autochdir();

  // Disable buffer-updates for the current buffer.
  // No need to check `unload_buf`: in that case the function returned above.
  buf_updates_unregister_all(buf);

  /*
   * Remove the buffer from the list.
   */
  if (wipe_buf) {
    xfree(buf->b_ffname);
    xfree(buf->b_sfname);
    if (buf->b_prev == NULL) {
      firstbuf = buf->b_next;
    } else {
      buf->b_prev->b_next = buf->b_next;
    }
    if (buf->b_next == NULL) {
      lastbuf = buf->b_prev;
    } else {
      buf->b_next->b_prev = buf->b_prev;
    }
    free_buffer(buf);
  } else {
    if (del_buf) {
      // Free all internal variables and reset option values, to make
      // ":bdel" compatible with Vim 5.7.
      free_buffer_stuff(buf, kBffClearWinInfo | kBffInitChangedtick);

      // Make it look like a new buffer.
      buf->b_flags = BF_CHECK_RO | BF_NEVERLOADED;

      // Init the options when loaded again.
      buf->b_p_initialized = false;
    }
    buf_clear_file(buf);
    if (del_buf) {
      buf->b_p_bl = false;
    }
  }
}

/// Make buffer not contain a file.
void buf_clear_file(buf_T *buf)
{
  buf->b_ml.ml_line_count = 1;
  unchanged(buf, true, true);
  buf->b_p_eol = true;
  buf->b_start_eol = true;
  buf->b_p_bomb = false;
  buf->b_start_bomb = false;
  buf->b_ml.ml_mfp = NULL;
  buf->b_ml.ml_flags = ML_EMPTY;                // empty buffer
}

/// Clears the current buffer contents.
void buf_clear(void)
{
  linenr_T line_count = curbuf->b_ml.ml_line_count;
  while (!(curbuf->b_ml.ml_flags & ML_EMPTY)) {
    ml_delete((linenr_T)1, false);
  }
  deleted_lines_mark(1, line_count);  // prepare for display
  ml_close(curbuf, true);             // free memline_T
  buf_clear_file(curbuf);
}

/// buf_freeall() - free all things allocated for a buffer that are related to
/// the file.  Careful: get here with "curwin" NULL when exiting.
///
/// @param flags BFA_DEL buffer is going to be deleted
///              BFA_WIPE buffer is going to be wiped out
///              BFA_KEEP_UNDO  do not free undo information
void buf_freeall(buf_T *buf, int flags)
{
  bool is_curbuf = (buf == curbuf);
  int is_curwin = (curwin != NULL && curwin->w_buffer == buf);
  win_T *the_curwin = curwin;
  tabpage_T *the_curtab = curtab;

  // Make sure the buffer isn't closed by autocommands.
  buf->b_locked++;

  bufref_T bufref;
  set_bufref(&bufref, buf);

  if ((buf->b_ml.ml_mfp != NULL)
      && apply_autocmds(EVENT_BUFUNLOAD, buf->b_fname, buf->b_fname, false, buf)
      && !bufref_valid(&bufref)) {
    // Autocommands deleted the buffer.
    return;
  }
  if ((flags & BFA_DEL)
      && buf->b_p_bl
      && apply_autocmds(EVENT_BUFDELETE, buf->b_fname, buf->b_fname, false, buf)
      && !bufref_valid(&bufref)) {
    // Autocommands may delete the buffer.
    return;
  }
  if ((flags & BFA_WIPE)
      && apply_autocmds(EVENT_BUFWIPEOUT, buf->b_fname, buf->b_fname, false,
                        buf)
      && !bufref_valid(&bufref)) {
    // Autocommands may delete the buffer.
    return;
  }
  buf->b_locked--;

  // If the buffer was in curwin and the window has changed, go back to that
  // window, if it still exists.  This avoids that ":edit x" triggering a
  // "tabnext" BufUnload autocmd leaves a window behind without a buffer.
  if (is_curwin && curwin != the_curwin &&  win_valid_any_tab(the_curwin)) {
    block_autocmds();
    goto_tabpage_win(the_curtab, the_curwin);
    unblock_autocmds();
  }
  if (aborting()) {  // autocmds may abort script processing
    return;
  }

  /*
   * It's possible that autocommands change curbuf to the one being deleted.
   * This might cause curbuf to be deleted unexpectedly.  But in some cases
   * it's OK to delete the curbuf, because a new one is obtained anyway.
   * Therefore only return if curbuf changed to the deleted buffer.
   */
  if (buf == curbuf && !is_curbuf) {
    return;
  }
  diff_buf_delete(buf);             // Can't use 'diff' for unloaded buffer.
  // Remove any ownsyntax, unless exiting.
  if (curwin != NULL && curwin->w_buffer == buf) {
    reset_synblock(curwin);
  }

  // No folds in an empty buffer.
  FOR_ALL_TAB_WINDOWS(tp, win) {
    if (win->w_buffer == buf) {
      clearFolding(win);
    }
  }

  ml_close(buf, true);              // close and delete the memline/memfile
  buf->b_ml.ml_line_count = 0;      // no lines in buffer
  if ((flags & BFA_KEEP_UNDO) == 0) {
    u_blockfree(buf);               // free the memory allocated for undo
    u_clearall(buf);                // reset all undo information
  }
  syntax_clear(&buf->b_s);          // reset syntax info
  buf->b_flags &= ~BF_READERR;      // a read error is no longer relevant
}

/*
 * Free a buffer structure and the things it contains related to the buffer
 * itself (not the file, that must have been done already).
 */
static void free_buffer(buf_T *buf)
{
  handle_unregister_buffer(buf);
  buf_free_count++;
  // b:changedtick uses an item in buf_T.
  free_buffer_stuff(buf, kBffClearWinInfo);
  if (buf->b_vars->dv_refcount > DO_NOT_FREE_CNT) {
    tv_dict_add(buf->b_vars,
                tv_dict_item_copy((dictitem_T *)(&buf->changedtick_di)));
  }
  unref_var_dict(buf->b_vars);
  aubuflocal_remove(buf);
  tv_dict_unref(buf->additional_data);
  xfree(buf->b_prompt_text);
  callback_free(&buf->b_prompt_callback);
  callback_free(&buf->b_prompt_interrupt);
  clear_fmark(&buf->b_last_cursor);
  clear_fmark(&buf->b_last_insert);
  clear_fmark(&buf->b_last_change);
  for (size_t i = 0; i < NMARKS; i++) {
    free_fmark(buf->b_namedm[i]);
  }
  for (int i = 0; i < buf->b_changelistlen; i++) {
    free_fmark(buf->b_changelist[i]);
  }
  if (autocmd_busy) {
    // Do not free the buffer structure while autocommands are executing,
    // it's still needed. Free it when autocmd_busy is reset.
    memset(&buf->b_namedm[0], 0, sizeof(buf->b_namedm));
    memset(&buf->b_changelist[0], 0, sizeof(buf->b_changelist));
    buf->b_next = au_pending_free_buf;
    au_pending_free_buf = buf;
  } else {
    xfree(buf);
  }
}

/// Free stuff in the buffer for ":bdel" and when wiping out the buffer.
///
/// @param buf  Buffer pointer
/// @param free_flags  BufFreeFlags
static void free_buffer_stuff(buf_T *buf, int free_flags)
{
  if (free_flags & kBffClearWinInfo) {
    clear_wininfo(buf);                 // including window-local options
    free_buf_options(buf, true);
    ga_clear(&buf->b_s.b_langp);
  }
  {
    // Avoid losing b:changedtick when deleting buffer: clearing variables
    // implies using clear_tv() on b:changedtick and that sets changedtick to
    // zero.
    hashitem_T *const changedtick_hi = hash_find(
        &buf->b_vars->dv_hashtab, (const char_u *)"changedtick");
    assert(changedtick_hi != NULL);
    hash_remove(&buf->b_vars->dv_hashtab, changedtick_hi);
  }
  vars_clear(&buf->b_vars->dv_hashtab);   // free all internal variables
  hash_init(&buf->b_vars->dv_hashtab);
  if (free_flags & kBffInitChangedtick) {
    buf_init_changedtick(buf);
  }
  uc_clear(&buf->b_ucmds);               // clear local user commands
  buf_delete_signs(buf, (char_u *)"*");  // delete any signs
  extmark_free_all(buf);                 // delete any extmarks
  map_clear_int(buf, MAP_ALL_MODES, true, false);    // clear local mappings
  map_clear_int(buf, MAP_ALL_MODES, true, true);     // clear local abbrevs
  XFREE_CLEAR(buf->b_start_fenc);

  buf_updates_unregister_all(buf);
}

/*
 * Free the b_wininfo list for buffer "buf".
 */
static void clear_wininfo(buf_T *buf)
{
  wininfo_T   *wip;

  while (buf->b_wininfo != NULL) {
    wip = buf->b_wininfo;
    buf->b_wininfo = wip->wi_next;
    if (wip->wi_optset) {
      clear_winopt(&wip->wi_opt);
      deleteFoldRecurse(&wip->wi_folds);
    }
    xfree(wip);
  }
}

/*
 * Go to another buffer.  Handles the result of the ATTENTION dialog.
 */
void goto_buffer(exarg_T *eap, int start, int dir, int count)
{
  bufref_T old_curbuf;
  set_bufref(&old_curbuf, curbuf);
  swap_exists_action = SEA_DIALOG;

  (void)do_buffer(*eap->cmd == 's' ? DOBUF_SPLIT : DOBUF_GOTO,
                  start, dir, count, eap->forceit);

  if (swap_exists_action == SEA_QUIT && *eap->cmd == 's') {
    cleanup_T cs;

    // Reset the error/interrupt/exception state here so that
    // aborting() returns false when closing a window.
    enter_cleanup(&cs);

    // Quitting means closing the split window, nothing else.
    win_close(curwin, true);
    swap_exists_action = SEA_NONE;
    swap_exists_did_quit = true;

    /* Restore the error/interrupt/exception state if not discarded by a
     * new aborting error, interrupt, or uncaught exception. */
    leave_cleanup(&cs);
  } else {
    handle_swap_exists(&old_curbuf);
  }
}

/// Handle the situation of swap_exists_action being set.
///
/// It is allowed for "old_curbuf" to be NULL or invalid.
///
/// @param old_curbuf The buffer to check for.
void handle_swap_exists(bufref_T *old_curbuf)
{
  cleanup_T cs;
  long old_tw = curbuf->b_p_tw;
  buf_T *buf;

  if (swap_exists_action == SEA_QUIT) {
    // Reset the error/interrupt/exception state here so that
    // aborting() returns false when closing a buffer.
    enter_cleanup(&cs);

    // User selected Quit at ATTENTION prompt.  Go back to previous
    // buffer.  If that buffer is gone or the same as the current one,
    // open a new, empty buffer.
    swap_exists_action = SEA_NONE;      // don't want it again
    swap_exists_did_quit = true;
    close_buffer(curwin, curbuf, DOBUF_UNLOAD, false);
    if (old_curbuf == NULL
        || !bufref_valid(old_curbuf)
        || old_curbuf->br_buf == curbuf) {
      buf = buflist_new(NULL, NULL, 1L, BLN_CURBUF | BLN_LISTED);
    } else {
      buf = old_curbuf->br_buf;
    }
    if (buf != NULL) {
      int old_msg_silent = msg_silent;

      if (shortmess(SHM_FILEINFO)) {
        msg_silent = 1;  // prevent fileinfo message
      }
      enter_buffer(buf);
      // restore msg_silent, so that the command line will be shown
      msg_silent = old_msg_silent;

      if (old_tw != curbuf->b_p_tw) {
        check_colorcolumn(curwin);
      }
    }
    // If "old_curbuf" is NULL we are in big trouble here...

    /* Restore the error/interrupt/exception state if not discarded by a
     * new aborting error, interrupt, or uncaught exception. */
    leave_cleanup(&cs);
  } else if (swap_exists_action == SEA_RECOVER) {
    // Reset the error/interrupt/exception state here so that
    // aborting() returns false when closing a buffer.
    enter_cleanup(&cs);

    // User selected Recover at ATTENTION prompt.
    msg_scroll = true;
    ml_recover(false);
    MSG_PUTS("\n");     // don't overwrite the last message
    cmdline_row = msg_row;
    do_modelines(0);

    /* Restore the error/interrupt/exception state if not discarded by a
     * new aborting error, interrupt, or uncaught exception. */
    leave_cleanup(&cs);
  }
  swap_exists_action = SEA_NONE;  // -V519
}

/*
 * do_bufdel() - delete or unload buffer(s)
 *
 * addr_count == 0: ":bdel" - delete current buffer
 * addr_count == 1: ":N bdel" or ":bdel N [N ..]" - first delete
 *		    buffer "end_bnr", then any other arguments.
 * addr_count == 2: ":N,N bdel" - delete buffers in range
 *
 * command can be DOBUF_UNLOAD (":bunload"), DOBUF_WIPE (":bwipeout") or
 * DOBUF_DEL (":bdel")
 *
 * Returns error message or NULL
 */
char_u *
do_bufdel(
    int command,
    char_u *arg,               // pointer to extra arguments
    int addr_count,
    int start_bnr,             // first buffer number in a range
    int end_bnr,               // buffer nr or last buffer nr in a range
    int forceit
)
{
  int do_current = 0;             // delete current buffer?
  int deleted = 0;                // number of buffers deleted
  char_u      *errormsg = NULL;   // return value
  int bnr;                        // buffer number
  char_u      *p;

  if (addr_count == 0) {
    (void)do_buffer(command, DOBUF_CURRENT, FORWARD, 0, forceit);
  } else {
    if (addr_count == 2) {
      if (*arg) {               // both range and argument is not allowed
        return (char_u *)_(e_trailing);
      }
      bnr = start_bnr;
    } else {    // addr_count == 1
      bnr = end_bnr;
    }

    for (; !got_int; os_breakcheck()) {
      /*
       * delete the current buffer last, otherwise when the
       * current buffer is deleted, the next buffer becomes
       * the current one and will be loaded, which may then
       * also be deleted, etc.
       */
      if (bnr == curbuf->b_fnum) {
        do_current = bnr;
      } else if (do_buffer(command, DOBUF_FIRST, FORWARD, bnr,
                           forceit) == OK) {
        deleted++;
      }

      /*
       * find next buffer number to delete/unload
       */
      if (addr_count == 2) {
        if (++bnr > end_bnr) {
          break;
        }
      } else {    // addr_count == 1
        arg = skipwhite(arg);
        if (*arg == NUL) {
          break;
        }
        if (!ascii_isdigit(*arg)) {
          p = skiptowhite_esc(arg);
          bnr = buflist_findpat(arg, p, command == DOBUF_WIPE,
                                false, false);
          if (bnr < 0) {                    // failed
            break;
          }
          arg = p;
        } else {
          bnr = getdigits_int(&arg, false, 0);
        }
      }
    }
    if (!got_int && do_current
        && do_buffer(command, DOBUF_FIRST,
                     FORWARD, do_current, forceit) == OK) {
      deleted++;
    }

    if (deleted == 0) {
      if (command == DOBUF_UNLOAD) {
        STRCPY(IObuff, _("E515: No buffers were unloaded"));
      } else if (command == DOBUF_DEL) {
        STRCPY(IObuff, _("E516: No buffers were deleted"));
      } else {
        STRCPY(IObuff, _("E517: No buffers were wiped out"));
      }
      errormsg = IObuff;
    } else if (deleted >= p_report) {
      if (command == DOBUF_UNLOAD) {
        if (deleted == 1) {
          MSG(_("1 buffer unloaded"));
        } else {
          smsg(_("%d buffers unloaded"), deleted);
        }
      } else if (command == DOBUF_DEL) {
        if (deleted == 1) {
          MSG(_("1 buffer deleted"));
        } else {
          smsg(_("%d buffers deleted"), deleted);
        }
      } else {
        if (deleted == 1) {
          MSG(_("1 buffer wiped out"));
        } else {
          smsg(_("%d buffers wiped out"), deleted);
        }
      }
    }
  }


  return errormsg;
}



/*
 * Make the current buffer empty.
 * Used when it is wiped out and it's the last buffer.
 */
static int empty_curbuf(int close_others, int forceit, int action)
{
  int retval;
  buf_T   *buf = curbuf;

  if (action == DOBUF_UNLOAD) {
    EMSG(_("E90: Cannot unload last buffer"));
    return FAIL;
  }

  bufref_T bufref;
  set_bufref(&bufref, buf);

  if (close_others) {
    // Close any other windows on this buffer, then make it empty.
    close_windows(buf, true);
  }

  setpcmark();
  retval = do_ecmd(0, NULL, NULL, NULL, ECMD_ONE,
      forceit ? ECMD_FORCEIT : 0, curwin);

  // do_ecmd() may create a new buffer, then we have to delete
  // the old one.  But do_ecmd() may have done that already, check
  // if the buffer still exists.
  if (buf != curbuf && bufref_valid(&bufref) && buf->b_nwindows == 0) {
    close_buffer(NULL, buf, action, false);
  }

  if (!close_others) {
    need_fileinfo = false;
  }

  return retval;
}
/*
 * Implementation of the commands for the buffer list.
 *
 * action == DOBUF_GOTO	    go to specified buffer
 * action == DOBUF_SPLIT    split window and go to specified buffer
 * action == DOBUF_UNLOAD   unload specified buffer(s)
 * action == DOBUF_DEL	    delete specified buffer(s) from buffer list
 * action == DOBUF_WIPE	    delete specified buffer(s) really
 *
 * start == DOBUF_CURRENT   go to "count" buffer from current buffer
 * start == DOBUF_FIRST	    go to "count" buffer from first buffer
 * start == DOBUF_LAST	    go to "count" buffer from last buffer
 * start == DOBUF_MOD	    go to "count" modified buffer from current buffer
 *
 * Return FAIL or OK.
 */
int
do_buffer(
    int action,
    int start,
    int dir,                        // FORWARD or BACKWARD
    int count,                      // buffer number or number of buffers
    int forceit                     // true for :...!
)
{
  buf_T       *buf;
  buf_T       *bp;
  int unload = (action == DOBUF_UNLOAD || action == DOBUF_DEL
                || action == DOBUF_WIPE);

  switch (start) {
  case DOBUF_FIRST:   buf = firstbuf; break;
  case DOBUF_LAST:    buf = lastbuf;  break;
  default:            buf = curbuf;   break;
  }
  if (start == DOBUF_MOD) {         // find next modified buffer
    while (count-- > 0) {
      do {
        buf = buf->b_next;
        if (buf == NULL) {
          buf = firstbuf;
        }
      } while (buf != curbuf && !bufIsChanged(buf));
    }
    if (!bufIsChanged(buf)) {
      EMSG(_("E84: No modified buffer found"));
      return FAIL;
    }
  } else if (start == DOBUF_FIRST && count) {  // find specified buffer number
    while (buf != NULL && buf->b_fnum != count) {
      buf = buf->b_next;
    }
  } else {
    bp = NULL;
    while (count > 0 || (!unload && !buf->b_p_bl && bp != buf)) {
      /* remember the buffer where we start, we come back there when all
       * buffers are unlisted. */
      if (bp == NULL) {
        bp = buf;
      }
      if (dir == FORWARD) {
        buf = buf->b_next;
        if (buf == NULL) {
          buf = firstbuf;
        }
      } else {
        buf = buf->b_prev;
        if (buf == NULL) {
          buf = lastbuf;
        }
      }
      // don't count unlisted buffers
      if (unload || buf->b_p_bl) {
        count--;
        bp = NULL;              // use this buffer as new starting point
      }
      if (bp == buf) {
        // back where we started, didn't find anything.
        EMSG(_("E85: There is no listed buffer"));
        return FAIL;
      }
    }
  }

  if (buf == NULL) {        // could not find it
    if (start == DOBUF_FIRST) {
      // don't warn when deleting
      if (!unload) {
        EMSGN(_(e_nobufnr), count);
      }
    } else if (dir == FORWARD) {
      EMSG(_("E87: Cannot go beyond last buffer"));
    } else {
      EMSG(_("E88: Cannot go before first buffer"));
    }
    return FAIL;
  }


  /*
   * delete buffer buf from memory and/or the list
   */
  if (unload) {
    int forward;
    bufref_T bufref;
    set_bufref(&bufref, buf);

    /* When unloading or deleting a buffer that's already unloaded and
     * unlisted: fail silently. */
    if (action != DOBUF_WIPE && buf->b_ml.ml_mfp == NULL && !buf->b_p_bl) {
      return FAIL;
    }

    if (!forceit && (buf->terminal || bufIsChanged(buf))) {
      if ((p_confirm || cmdmod.confirm) && p_write && !buf->terminal) {
        dialog_changed(buf, false);
        if (!bufref_valid(&bufref)) {
          // Autocommand deleted buffer, oops! It's not changed now.
          return FAIL;
        }
        // If it's still changed fail silently, the dialog already
        // mentioned why it fails.
        if (bufIsChanged(buf)) {
          return FAIL;
        }
      } else {
        if (buf->terminal) {
          if (p_confirm || cmdmod.confirm) {
            if (!dialog_close_terminal(buf)) {
              return FAIL;
            }
          } else {
            EMSG2(_("E89: %s will be killed (add ! to override)"),
                  (char *)buf->b_fname);
            return FAIL;
          }
        } else {
          EMSGN(_("E89: No write since last change for buffer %" PRId64
                  " (add ! to override)"),
                buf->b_fnum);
          return FAIL;
        }
      }
    }

    // When closing the current buffer stop Visual mode.
    if (buf == curbuf && VIsual_active) {
      end_visual_mode();
    }

    /*
     * If deleting the last (listed) buffer, make it empty.
     * The last (listed) buffer cannot be unloaded.
     */
    bp = NULL;
    FOR_ALL_BUFFERS(bp2) {
      if (bp2->b_p_bl && bp2 != buf) {
        bp = bp2;
        break;
      }
    }
    if (bp == NULL && buf == curbuf) {
      return empty_curbuf(true, forceit, action);
    }

    /*
     * If the deleted buffer is the current one, close the current window
     * (unless it's the only window).  Repeat this so long as we end up in
     * a window with this buffer.
     */
    while (buf == curbuf
           && !(curwin->w_closing || curwin->w_buffer->b_locked > 0)
           && (!ONE_WINDOW || first_tabpage->tp_next != NULL)) {
      if (win_close(curwin, false) == FAIL) {
        break;
      }
    }

    /*
     * If the buffer to be deleted is not the current one, delete it here.
     */
    if (buf != curbuf) {
      close_windows(buf, false);
      if (buf != curbuf && bufref_valid(&bufref) && buf->b_nwindows <= 0) {
        close_buffer(NULL, buf, action, false);
      }
      return OK;
    }

    // Deleting the current buffer: Need to find another buffer to go to.
    // There should be another, otherwise it would have been handled
    // above.  However, autocommands may have deleted all buffers.
    // First use au_new_curbuf.br_buf, if it is valid.
    // Then prefer the buffer we most recently visited.
    // Else try to find one that is loaded, after the current buffer,
    // then before the current buffer.
    // Finally use any buffer.
    buf = NULL;  // Selected buffer.
    bp = NULL;   // Used when no loaded buffer found.
    if (au_new_curbuf.br_buf != NULL && bufref_valid(&au_new_curbuf)) {
      buf = au_new_curbuf.br_buf;
    } else if (curwin->w_jumplistlen > 0) {
      int jumpidx;

      jumpidx = curwin->w_jumplistidx - 1;
      if (jumpidx < 0) {
        jumpidx = curwin->w_jumplistlen - 1;
      }

      forward = jumpidx;
      while (jumpidx != curwin->w_jumplistidx) {
        buf = buflist_findnr(curwin->w_jumplist[jumpidx].fmark.fnum);
        if (buf != NULL) {
          if (buf == curbuf || !buf->b_p_bl) {
            buf = NULL;                 // skip current and unlisted bufs
          } else if (buf->b_ml.ml_mfp == NULL) {
            // skip unloaded buf, but may keep it for later
            if (bp == NULL) {
              bp = buf;
            }
            buf = NULL;
          }
        }
        if (buf != NULL) {         // found a valid buffer: stop searching
          break;
        }
        // advance to older entry in jump list
        if (!jumpidx && curwin->w_jumplistidx == curwin->w_jumplistlen) {
          break;
        }
        if (--jumpidx < 0) {
          jumpidx = curwin->w_jumplistlen - 1;
        }
        if (jumpidx == forward) {               // List exhausted for sure
          break;
        }
      }
    }

    if (buf == NULL) {          // No previous buffer, Try 2'nd approach
      forward = true;
      buf = curbuf->b_next;
      for (;; ) {
        if (buf == NULL) {
          if (!forward) {               // tried both directions
            break;
          }
          buf = curbuf->b_prev;
          forward = false;
          continue;
        }
        // in non-help buffer, try to skip help buffers, and vv
        if (buf->b_help == curbuf->b_help && buf->b_p_bl) {
          if (buf->b_ml.ml_mfp != NULL) {           // found loaded buffer
            break;
          }
          if (bp == NULL) {             // remember unloaded buf for later
            bp = buf;
          }
        }
        if (forward) {
          buf = buf->b_next;
        } else {
          buf = buf->b_prev;
        }
      }
    }
    if (buf == NULL) {          // No loaded buffer, use unloaded one
      buf = bp;
    }
    if (buf == NULL) {          // No loaded buffer, find listed one
      FOR_ALL_BUFFERS(buf2) {
        if (buf2->b_p_bl && buf2 != curbuf) {
          buf = buf2;
          break;
        }
      }
    }
    if (buf == NULL) {          // Still no buffer, just take one
      if (curbuf->b_next != NULL) {
        buf = curbuf->b_next;
      } else {
        buf = curbuf->b_prev;
      }
    }
  }

  if (buf == NULL) {
    /* Autocommands must have wiped out all other buffers.  Only option
     * now is to make the current buffer empty. */
    return empty_curbuf(false, forceit, action);
  }

  /*
   * make buf current buffer
   */
  if (action == DOBUF_SPLIT) {      // split window first
    // If 'switchbuf' contains "useopen": jump to first window containing
    // "buf" if one exists
    if ((swb_flags & SWB_USEOPEN) && buf_jump_open_win(buf)) {
      return OK;
    }
    // If 'switchbuf' contains "usetab": jump to first window in any tab
    // page containing "buf" if one exists
    if ((swb_flags & SWB_USETAB) && buf_jump_open_tab(buf)) {
      return OK;
    }
    if (win_split(0, 0) == FAIL) {
      return FAIL;
    }
  }

  // go to current buffer - nothing to do
  if (buf == curbuf) {
    return OK;
  }

  /*
   * Check if the current buffer may be abandoned.
   */
  if (action == DOBUF_GOTO && !can_abandon(curbuf, forceit)) {
    if ((p_confirm || cmdmod.confirm) && p_write) {
      bufref_T bufref;
      set_bufref(&bufref, buf);
      dialog_changed(curbuf, false);
      if (!bufref_valid(&bufref)) {
        // Autocommand deleted buffer, oops!
        return FAIL;
      }
    }
    if (bufIsChanged(curbuf)) {
      no_write_message();
      return FAIL;
    }
  }

  // Go to the other buffer.
  set_curbuf(buf, action);

  if (action == DOBUF_SPLIT) {
    RESET_BINDING(curwin);      // reset 'scrollbind' and 'cursorbind'
  }

  if (aborting()) {         // autocmds may abort script processing
    return FAIL;
  }

  return OK;
}


/*
 * Set current buffer to "buf".  Executes autocommands and closes current
 * buffer.  "action" tells how to close the current buffer:
 * DOBUF_GOTO	    free or hide it
 * DOBUF_SPLIT	    nothing
 * DOBUF_UNLOAD	    unload it
 * DOBUF_DEL	    delete it
 * DOBUF_WIPE	    wipe it out
 */
void set_curbuf(buf_T *buf, int action)
{
  buf_T       *prevbuf;
  int unload = (action == DOBUF_UNLOAD || action == DOBUF_DEL
                || action == DOBUF_WIPE);
  long old_tw = curbuf->b_p_tw;

  setpcmark();
  if (!cmdmod.keepalt) {
    curwin->w_alt_fnum = curbuf->b_fnum;     // remember alternate file
  }
  buflist_altfpos(curwin);                       // remember curpos

  // Don't restart Select mode after switching to another buffer.
  VIsual_reselect = false;

  // close_windows() or apply_autocmds() may change curbuf and wipe out "buf"
  prevbuf = curbuf;
  bufref_T newbufref;
  bufref_T prevbufref;
  set_bufref(&prevbufref, prevbuf);
  set_bufref(&newbufref, buf);

  // Autocommands may delete the curren buffer and/or the buffer we wan to go
  // to.  In those cases don't close the buffer.
  if (!apply_autocmds(EVENT_BUFLEAVE, NULL, NULL, false, curbuf)
      || (bufref_valid(&prevbufref) && bufref_valid(&newbufref)
          && !aborting())) {
    if (prevbuf == curwin->w_buffer) {
      reset_synblock(curwin);
    }
    if (unload) {
      close_windows(prevbuf, false);
    }
    if (bufref_valid(&prevbufref) && !aborting()) {
      win_T  *previouswin = curwin;
      if (prevbuf == curbuf) {
        u_sync(false);
      }
      close_buffer(prevbuf == curwin->w_buffer ? curwin : NULL,
                   prevbuf,
                   unload
                   ? action
                   : (action == DOBUF_GOTO && !buf_hide(prevbuf)
                      && !bufIsChanged(prevbuf)) ? DOBUF_UNLOAD : 0,
                   false);
      if (curwin != previouswin && win_valid(previouswin)) {
        // autocommands changed curwin, Grr!
        curwin = previouswin;
      }
    }
  }
  /* An autocommand may have deleted "buf", already entered it (e.g., when
   * it did ":bunload") or aborted the script processing!
   * If curwin->w_buffer is null, enter_buffer() will make it valid again */
  if ((buf_valid(buf) && buf != curbuf
       && !aborting()
       ) || curwin->w_buffer == NULL
      ) {
    enter_buffer(buf);
    if (old_tw != curbuf->b_p_tw) {
      check_colorcolumn(curwin);
    }
  }

  if (bufref_valid(&prevbufref) && prevbuf->terminal != NULL) {
    terminal_check_size(prevbuf->terminal);
  }
}

/*
 * Enter a new current buffer.
 * Old curbuf must have been abandoned already!  This also means "curbuf" may
 * be pointing to freed memory.
 */
void enter_buffer(buf_T *buf)
{
  // Copy buffer and window local option values.  Not for a help buffer.
  buf_copy_options(buf, BCO_ENTER | BCO_NOHELP);
  if (!buf->b_help) {
    get_winopts(buf);
  } else {
    // Remove all folds in the window.
    clearFolding(curwin);
  }
  foldUpdateAll(curwin);        // update folds (later).

  // Get the buffer in the current window.
  curwin->w_buffer = buf;
  curbuf = buf;
  curbuf->b_nwindows++;

  if (curwin->w_p_diff) {
    diff_buf_add(curbuf);
  }

  curwin->w_s = &(curbuf->b_s);

  // Cursor on first line by default.
  curwin->w_cursor.lnum = 1;
  curwin->w_cursor.col = 0;
  curwin->w_cursor.coladd = 0;
  curwin->w_set_curswant = true;
  curwin->w_topline_was_set = false;

  // mark cursor position as being invalid
  curwin->w_valid = 0;

  // Make sure the buffer is loaded.
  if (curbuf->b_ml.ml_mfp == NULL) {    // need to load the file
    // If there is no filetype, allow for detecting one.  Esp. useful for
    // ":ball" used in an autocommand.  If there already is a filetype we
    // might prefer to keep it.
    if (*curbuf->b_p_ft == NUL) {
      did_filetype = false;
    }

    open_buffer(false, NULL, 0);
  } else {
    if (!msg_silent && !shortmess(SHM_FILEINFO)) {
      need_fileinfo = true;             // display file info after redraw
    }
    // check if file changed
    (void)buf_check_timestamp(curbuf, false);

    curwin->w_topline = 1;
    curwin->w_topfill = 0;
    apply_autocmds(EVENT_BUFENTER, NULL, NULL, false, curbuf);
    apply_autocmds(EVENT_BUFWINENTER, NULL, NULL, false, curbuf);
  }

  /* If autocommands did not change the cursor position, restore cursor lnum
   * and possibly cursor col. */
  if (curwin->w_cursor.lnum == 1 && inindent(0)) {
    buflist_getfpos();
  }

  check_arg_idx(curwin);                // check for valid arg_idx
  maketitle();
  // when autocmds didn't change it
  if (curwin->w_topline == 1 && !curwin->w_topline_was_set) {
    scroll_cursor_halfway(false);       // redisplay at correct position
  }


  // Change directories when the 'acd' option is set.
  do_autochdir();

  if (curbuf->b_kmap_state & KEYMAP_INIT) {
    (void)keymap_init();
  }
  // May need to set the spell language.  Can only do this after the buffer
  // has been properly setup.
  if (!curbuf->b_help && curwin->w_p_spell && *curwin->w_s->b_p_spl != NUL) {
    (void)did_set_spelllang(curwin);
  }

  redraw_later(NOT_VALID);
}

// Change to the directory of the current buffer.
// Don't do this while still starting up.
void do_autochdir(void)
{
  if (p_acd) {
    if (starting == 0
        && curbuf->b_ffname != NULL
        && vim_chdirfile(curbuf->b_ffname) == OK) {
      post_chdir(kCdScopeGlobal, false);
      shorten_fnames(true);
    }
  }
}

void no_write_message(void)
{
  EMSG(_("E37: No write since last change (add ! to override)"));
}

void no_write_message_nobang(void)
{
  EMSG(_("E37: No write since last change"));
}

//
// functions for dealing with the buffer list
//

static int top_file_num = 1;            ///< highest file number

/// Initialize b:changedtick and changedtick_val attribute
///
/// @param[out]  buf  Buffer to intialize for.
static inline void buf_init_changedtick(buf_T *const buf)
  FUNC_ATTR_ALWAYS_INLINE FUNC_ATTR_NONNULL_ALL
{
  STATIC_ASSERT(sizeof("changedtick") <= sizeof(buf->changedtick_di.di_key),
                "buf->changedtick_di cannot hold large enough keys");
  buf->changedtick_di = (ChangedtickDictItem) {
    .di_flags = DI_FLAGS_RO|DI_FLAGS_FIX,  // Must not include DI_FLAGS_ALLOC.
    .di_tv = (typval_T) {
      .v_type = VAR_NUMBER,
      .v_lock = VAR_FIXED,
      .vval.v_number = buf_get_changedtick(buf),
    },
    .di_key = "changedtick",
  };
  tv_dict_add(buf->b_vars, (dictitem_T *)&buf->changedtick_di);
}
