#include <assert.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "nvim/api/private/defs.h"
#include "nvim/api/private/helpers.h"
#include "nvim/ascii_defs.h"
#include "nvim/buffer.h"
#include "nvim/buffer_defs.h"
#include "nvim/charset.h"
#include "nvim/digraph.h"
#include "nvim/drawline.h"
#include "nvim/drawscreen.h"
#include "nvim/eval.h"
#include "nvim/eval/typval_defs.h"
#include "nvim/eval/vars.h"
#include "nvim/gettext_defs.h"
#include "nvim/globals.h"
#include "nvim/grid.h"
#include "nvim/grid_defs.h"
#include "nvim/highlight.h"
#include "nvim/highlight_defs.h"
#include "nvim/highlight_group.h"
#include "nvim/macros_defs.h"
#include "nvim/mbyte.h"
#include "nvim/memline.h"
#include "nvim/memline_defs.h"
#include "nvim/memory.h"
#include "nvim/memory_defs.h"
#include "nvim/message.h"
#include "nvim/normal.h"
#include "nvim/option.h"
#include "nvim/option_vars.h"
#include "nvim/os/os.h"
#include "nvim/os/os_defs.h"
#include "nvim/path.h"
#include "nvim/plines.h"
#include "nvim/pos_defs.h"
#include "nvim/sign.h"
#include "nvim/sign_defs.h"
#include "nvim/state_defs.h"
#include "nvim/statusline.h"
#include "nvim/strings.h"
#include "nvim/types_defs.h"
#include "nvim/ui.h"
#include "nvim/ui_defs.h"
#include "nvim/undo.h"
#include "nvim/window.h"

// Determines how deeply nested %{} blocks will be evaluated in statusline.
#define MAX_STL_EVAL_DEPTH 100

/// Enumeration specifying the valid numeric bases that can
/// be used when printing numbers in the status line.
typedef enum {
  kNumBaseDecimal = 10,
  kNumBaseHexadecimal = 16,
} NumberBase;

/// Redraw the status line of window `wp`.
///
/// If inversion is possible we use it. Else '=' characters are used.
void win_redr_status(win_T *wp)
{
  bool is_stl_global = global_stl_height() > 0;
  static bool busy = false;

  // May get here recursively when 'statusline' (indirectly)
  // invokes ":redrawstatus".  Simply ignore the call then.
  if (busy
      // Also ignore if wildmenu is showing.
      || (wild_menu_showing != 0 && !ui_has(kUIWildmenu))) {
    return;
  }
  busy = true;
  wp->w_redr_status = false;
  if (wp->w_status_height == 0 && !(is_stl_global && wp == curwin)) {
    // no status line, either global statusline is enabled or the window is a last window
    redraw_cmdline = true;
  } else if (!redrawing()) {
    // Don't redraw right now, do it later. Don't update status line when
    // popup menu is visible and may be drawn over it
    wp->w_redr_status = true;
  } else if (*wp->w_p_stl != NUL || !wp->w_floating || (is_stl_global && wp == curwin)) {
    // redraw custom status line
    redraw_custom_statusline(wp);
  }

  hlf_T group = HLF_C;
  // May need to draw the character below the vertical separator.
  if (wp->w_vsep_width != 0 && wp->w_status_height != 0 && redrawing()) {
    schar_T fillchar;
    if (stl_connected(wp)) {
      fillchar = fillchar_status(&group, wp);
    } else {
      fillchar = wp->w_p_fcs_chars.vert;
    }
    int attr = win_hl_attr(wp, (int)group);
    grid_line_start(&default_gridview, W_ENDROW(wp));
    grid_line_put_schar(W_ENDCOL(wp), fillchar, attr);
    grid_line_flush();
  }
  busy = false;
}

void get_trans_bufname(buf_T *buf)
{
  if (buf_spname(buf) != NULL) {
    xstrlcpy(NameBuff, buf_spname(buf), MAXPATHL);
  } else {
    home_replace(buf, buf->b_fname, NameBuff, MAXPATHL, true);
  }
  trans_characters(NameBuff, MAXPATHL);
}

/// Only call if (wp->w_vsep_width != 0).
///
/// @return  true if the status line of window "wp" is connected to the status
/// line of the window right of it.  If not, then it's a vertical separator.
bool stl_connected(win_T *wp)
{
  frame_T *fr = wp->w_frame;
  while (fr->fr_parent != NULL) {
    if (fr->fr_parent->fr_layout == FR_COL) {
      if (fr->fr_next != NULL) {
        break;
      }
    } else {
      if (fr->fr_next != NULL) {
        return true;
      }
    }
    fr = fr->fr_parent;
  }
  return false;
}

/// Clear status line, window bar or tab page line click definition table
///
/// @param[out]  tpcd  Table to clear.
/// @param[in]  tpcd_size  Size of the table.
void stl_clear_click_defs(StlClickDefinition *const click_defs, const size_t click_defs_size)
{
  if (click_defs != NULL) {
    for (size_t i = 0; i < click_defs_size; i++) {
      if (i == 0 || click_defs[i].func != click_defs[i - 1].func) {
        xfree(click_defs[i].func);
      }
    }
    memset(click_defs, 0, click_defs_size * sizeof(click_defs[0]));
  }
}

/// Allocate or resize the click definitions array if needed.
StlClickDefinition *stl_alloc_click_defs(StlClickDefinition *cdp, int width, size_t *size)
{
  if (*size < (size_t)width) {
    xfree(cdp);
    *size = (size_t)width;
    cdp = xcalloc(*size, sizeof(StlClickDefinition));
  }
  return cdp;
}

/// Fill the click definitions array if needed.
void stl_fill_click_defs(StlClickDefinition *click_defs, StlClickRecord *click_recs,
                         const char *buf, int width, bool tabline)
{
  if (click_defs == NULL) {
    return;
  }

  int col = 0;
  int len = 0;

  StlClickDefinition cur_click_def = {
    .type = kStlClickDisabled,
  };
  for (int i = 0; click_recs[i].start != NULL; i++) {
    len += vim_strnsize(buf, (int)(click_recs[i].start - buf));
    assert(len <= width);
    if (col < len) {
      while (col < len) {
        click_defs[col++] = cur_click_def;
      }
    } else {
      xfree(cur_click_def.func);
    }
    buf = click_recs[i].start;
    cur_click_def = click_recs[i].def;
    if (!tabline && !(cur_click_def.type == kStlClickDisabled
                      || cur_click_def.type == kStlClickFuncRun)) {
      // window bar and status line only support click functions
      cur_click_def.type = kStlClickDisabled;
    }
  }
  if (col < width) {
    while (col < width) {
      click_defs[col++] = cur_click_def;
    }
  } else {
    xfree(cur_click_def.func);
  }
}

static bool did_show_ext_ruler = false;
/// Redraw the status line, window bar, ruler or tabline.
/// @param wp  target window, NULL for 'tabline'
/// @param draw_winbar  redraw 'winbar'
/// @param draw_ruler  redraw 'rulerformat'
/// @param ui_event  emit UI-event instead of drawing
static void win_redr_custom(win_T *wp, bool draw_winbar, bool draw_ruler, bool ui_event)
{
  static bool entered = false;
  int col = 0;
  int attr, row, maxwidth;
  hlf_T group;
  schar_T fillchar;
  char buf[MAXPATHL];
  char transbuf[MAXPATHL];
  char *stl;
  OptIndex opt_idx = kOptInvalid;
  int opt_scope = 0;
  stl_hlrec_t *hltab;
  StlClickRecord *tabtab;
  bool is_stl_global = global_stl_height() > 0;

  ScreenGrid *grid = wp && wp->w_floating && !is_stl_global ? &wp->w_grid_alloc : &default_grid;

  // There is a tiny chance that this gets called recursively: When
  // redrawing a status line triggers redrawing the ruler or tabline.
  // Avoid trouble by not allowing recursion.
  if (entered) {
    return;
  }
  entered = true;

  // setup environment for the task at hand
  if (wp == NULL) {
    // Use 'tabline'.  Always at the first line of the screen.
    stl = p_tal;
    row = 0;
    fillchar = schar_from_ascii(' ');
    group = HLF_TPF;
    attr = HL_ATTR(group);
    maxwidth = Columns;
    opt_idx = kOptTabline;
  } else if (draw_winbar) {
    opt_idx = kOptWinbar;
    stl = ((*wp->w_p_wbr != NUL) ? wp->w_p_wbr : p_wbr);
    opt_scope = ((*wp->w_p_wbr != NUL) ? OPT_LOCAL : 0);
    row = -1;  // row zero is first row of text
    col = 0;
    grid = grid_adjust(&wp->w_grid, &row, &col);

    if (row < 0) {
      goto theend;
    }

    fillchar = wp->w_p_fcs_chars.wbr;
    group = (wp == curwin) ? HLF_WBR : HLF_WBRNC;
    attr = win_hl_attr(wp, (int)group);
    maxwidth = wp->w_view_width;
    stl_clear_click_defs(wp->w_winbar_click_defs, wp->w_winbar_click_defs_size);
    wp->w_winbar_click_defs = stl_alloc_click_defs(wp->w_winbar_click_defs, maxwidth,
                                                   &wp->w_winbar_click_defs_size);
  } else {
    const bool in_status_line = wp->w_status_height != 0 || is_stl_global;
    if (wp->w_floating && !is_stl_global && !draw_ruler) {
      row = wp->w_winrow_off + wp->w_view_height;
      col = wp->w_wincol_off;
      maxwidth = wp->w_view_width;
    } else {
      row = is_stl_global ? (Rows - (int)p_ch - 1) : W_ENDROW(wp);
      maxwidth = in_status_line && !is_stl_global ? wp->w_width : Columns;
    }
    fillchar = fillchar_status(&group, wp);
    stl_clear_click_defs(wp->w_status_click_defs, wp->w_status_click_defs_size);
    wp->w_status_click_defs = stl_alloc_click_defs(wp->w_status_click_defs, maxwidth,
                                                   &wp->w_status_click_defs_size);

    if (draw_ruler) {
      stl = p_ruf;
      opt_idx = kOptRulerformat;
      // advance past any leading group spec - implicit in ru_col
      if (*stl == '%') {
        if (*++stl == '-') {
          stl++;
        }
        if (atoi(stl)) {
          while (ascii_isdigit(*stl)) {
            stl++;
          }
        }
        if (*stl++ != '(') {
          stl = p_ruf;
        }
      }
      col = MAX(ru_col - (Columns - maxwidth), (maxwidth + 1) / 2);
      maxwidth -= col;
      if (!in_status_line) {
        row = Rows - 1;
        grid = grid_adjust(&msg_grid_adj, &row, &col);
        maxwidth--;  // writing in last column may cause scrolling
        fillchar = schar_from_ascii(' ');
        group = HLF_MSG;
      }
    } else {
      opt_idx = kOptStatusline;
      stl = ((*wp->w_p_stl != NUL) ? wp->w_p_stl : p_stl);
      opt_scope = ((*wp->w_p_stl != NUL) ? OPT_LOCAL : 0);
    }

    attr = win_hl_attr(wp, (int)group);
    if (!wp->w_floating && in_status_line && !is_stl_global) {
      col += wp->w_wincol;
    }
  }

  if (maxwidth <= 0) {
    goto theend;
  }

  // Temporarily reset 'cursorbind', we don't want a side effect from moving
  // the cursor away and back.
  win_T *ewp = wp == NULL ? curwin : wp;
  int p_crb_save = ewp->w_p_crb;
  ewp->w_p_crb = false;

  // Make a copy, because the statusline may include a function call that
  // might change the option value and free the memory.
  stl = xstrdup(stl);
  build_stl_str_hl(ewp, buf, sizeof(buf), stl, opt_idx, opt_scope,
                   fillchar, maxwidth, &hltab, NULL, &tabtab, NULL);

  xfree(stl);
  ewp->w_p_crb = p_crb_save;

  int len = (int)strlen(buf);
  int start_col = col;

  if (!ui_event) {
    // Draw each snippet with the specified highlighting.
    screengrid_line_start(grid, row, 0);
  }

  char *p = buf;
  int curattr = attr;
  int curgroup = (int)group;
  Array content = ARRAY_DICT_INIT;
  for (stl_hlrec_t *sp = hltab;; sp++) {
    int textlen = (int)(sp->start ? sp->start - p : buf + len - p);
    // Make all characters printable. Use an empty string instead of p, if p is beyond buf + len.
    size_t tsize = transstr_buf(p >= buf + len ? "" : p, textlen, transbuf, sizeof transbuf, true);
    if (!ui_event) {
      col += grid_line_puts(col, transbuf, (int)tsize, curattr);
    } else {
      Array chunk = ARRAY_DICT_INIT;
      ADD(chunk, INTEGER_OBJ(curattr));
      ADD(chunk, STRING_OBJ(cbuf_as_string(xmemdupz(transbuf, tsize), tsize)));
      ADD(chunk, INTEGER_OBJ(curgroup));
      ADD(content, ARRAY_OBJ(chunk));
    }
    p = sp->start;

    if (p == NULL) {
      break;
    } else if (sp->userhl == 0) {
      curattr = attr;
      curgroup = (int)group;
    } else if (sp->userhl < 0) {
      int new_attr = syn_id2attr(-sp->userhl);
      if (sp->item == STL_HIGHLIGHT_COMB) {
        curattr = hl_combine_attr(curattr, new_attr);
      } else {
        curattr = new_attr;
      }
      curgroup = -sp->userhl;
    } else {
      int *userhl = (wp != NULL && wp != curwin && wp->w_status_height != 0)
                    ? highlight_stlnc : highlight_user;
      char userbuf[5] = "User";
      userbuf[4] = (char)sp->userhl + '0';
      curattr = userhl[sp->userhl - 1];
      curgroup = syn_name2id_len(userbuf, 5);
    }
    if (curattr != attr) {
      curattr = hl_combine_attr(attr, curattr);
    }
  }

  if (ui_event) {
    ui_call_msg_ruler(content);
    did_show_ext_ruler = true;
    api_free_array(content);
    goto theend;
  }

  int maxcol = start_col + maxwidth;

  // fill up with "fillchar"
  grid_line_fill(col, maxcol, fillchar, curattr);
  grid_line_flush();

  // Fill the tab_page_click_defs, w_status_click_defs or w_winbar_click_defs array for clicking
  // in the tab page line, status line or window bar
  StlClickDefinition *click_defs = (wp == NULL) ? tab_page_click_defs
                                                : draw_winbar ? wp->w_winbar_click_defs
                                                              : wp->w_status_click_defs;

  stl_fill_click_defs(click_defs, tabtab, buf, maxwidth, wp == NULL);

theend:
  entered = false;
}

void win_redr_winbar(win_T *wp)
{
  static bool entered = false;

  // Return when called recursively. This can happen when the winbar contains an expression
  // that triggers a redraw.
  if (entered) {
    return;
  }
  entered = true;

  if (wp->w_winbar_height == 0 || !redrawing()) {
    // Do nothing.
  } else if (*p_wbr != NUL || *wp->w_p_wbr != NUL) {
    win_redr_custom(wp, true, false, false);
  }
  entered = false;
}

void redraw_ruler(void)
{
  static int did_ruler_col = -1;
  win_T *wp = curwin->w_status_height == 0 ? curwin : lastwin_nofloating(NULL);
  bool is_stl_global = global_stl_height() > 0;

  // Check if ruler should be drawn, clear if it was drawn before.
  if (!p_ru || wp->w_status_height > 0 || is_stl_global || (p_ch == 0 && !ui_has(kUIMessages))) {
    if (did_show_ext_ruler && ui_has(kUIMessages)) {
      ui_call_msg_ruler((Array)ARRAY_DICT_INIT);
      did_show_ext_ruler = false;
    } else if (did_ruler_col > 0) {
      msg_col = did_ruler_col;
      msg_row = Rows - 1;
      msg_clr_eos();
    }
    did_ruler_col = -1;
    return;
  }

  // Check if cursor.lnum is valid, since redraw_ruler() may be called
  // after deleting lines, before cursor.lnum is corrected.
  if (wp->w_cursor.lnum > wp->w_buffer->b_ml.ml_line_count) {
    return;
  }

  // Don't draw the ruler while doing insert-completion, it might overwrite
  // the (long) mode message.
  if (wp->w_status_height == 0 && !is_stl_global && edit_submode != NULL) {
    return;
  }

  bool part_of_status = wp->w_status_height || is_stl_global;
  if (*p_ruf && (p_ch > 0 || (ui_has(kUIMessages) && !part_of_status))) {
    win_redr_custom(wp, false, true, ui_has(kUIMessages));
    return;
  }

  hlf_T group = HLF_MSG;
  int off = wp->w_status_height ? wp->w_wincol : 0;
  int width = wp->w_status_height ? wp->w_width : Columns;
  schar_T fillchar = part_of_status ? fillchar_status(&group, wp) : schar_from_ascii(' ');
  int attr = part_of_status ? win_hl_attr(wp, (int)group) : HL_ATTR(group);

  // In list mode virtcol needs to be recomputed
  colnr_T virtcol = wp->w_virtcol;
  if (wp->w_p_list && wp->w_p_lcs_chars.tab1 == NUL) {
    wp->w_p_list = false;
    getvvcol(wp, &wp->w_cursor, NULL, &virtcol, NULL);
    wp->w_p_list = true;
  }

  // Check if not in Insert mode and the line is empty (will show "0-1").
  int empty_line = (State & MODE_INSERT) == 0
                   && *ml_get_buf(wp->w_buffer, wp->w_cursor.lnum) == NUL;

#define RULER_BUF_LEN 70
  char buffer[RULER_BUF_LEN];

  // row number, column number is appended
  // l10n: leave as-is unless a space after the comma is preferred
  // l10n: do not add any row/column label, due to the limited space
  int bufferlen = vim_snprintf(buffer, RULER_BUF_LEN, _("%" PRId64 ","),
                               (wp->w_buffer->b_ml.ml_flags & ML_EMPTY)
                               ? 0
                               : (int64_t)wp->w_cursor.lnum);
  bufferlen += col_print(buffer + bufferlen, RULER_BUF_LEN - (size_t)bufferlen,
                         empty_line ? 0 : (int)wp->w_cursor.col + 1,
                         (int)virtcol + 1);

  // Add a "50%" if there is room for it.
  // On the last line, don't print in the last column (scrolls the
  // screen up on some terminals).
  char rel_pos[RULER_BUF_LEN];
  int rel_poslen = get_rel_pos(wp, rel_pos, RULER_BUF_LEN);
  int n1 = bufferlen + vim_strsize(rel_pos);
  if (wp->w_status_height == 0 && !is_stl_global) {  // can't use last char of screen
    n1++;
  }

  int this_ru_col = ru_col - (Columns - width);
  // Never use more than half the window/screen width, leave the other half
  // for the filename.
  int n2 = (width + 1) / 2;
  this_ru_col = MAX(this_ru_col, n2);
  if (this_ru_col + n1 < width) {
    // need at least space for rel_pos + NUL
    while (this_ru_col + n1 < width
           && RULER_BUF_LEN > bufferlen + rel_poslen + 1) {  // +1 for NUL
      bufferlen += (int)schar_get(buffer + bufferlen, fillchar);
      n1++;
    }
    bufferlen += vim_snprintf(buffer + bufferlen, RULER_BUF_LEN - (size_t)bufferlen,
                              "%s", rel_pos);
  }
  (void)bufferlen;

  if (ui_has(kUIMessages) && !part_of_status) {
    MAXSIZE_TEMP_ARRAY(content, 1);
    MAXSIZE_TEMP_ARRAY(chunk, 3);
    ADD_C(chunk, INTEGER_OBJ(attr));
    ADD_C(chunk, CSTR_AS_OBJ(buffer));
    ADD_C(chunk, INTEGER_OBJ(HLF_MSG));
    assert(attr == HL_ATTR(HLF_MSG));
    ADD_C(content, ARRAY_OBJ(chunk));
    ui_call_msg_ruler(content);
    did_show_ext_ruler = true;
    did_ruler_col = 1;
  } else {
    if (did_show_ext_ruler) {
      ui_call_msg_ruler((Array)ARRAY_DICT_INIT);
      did_show_ext_ruler = false;
    }
    // Truncate at window boundary.
    for (n1 = 0, n2 = 0; buffer[n1] != NUL; n1 += utfc_ptr2len(buffer + n1)) {
      n2 += utf_ptr2cells(buffer + n1);
      if (this_ru_col + n2 > width) {
        bufferlen = n1;
        buffer[bufferlen] = NUL;
        break;
      }
    }

    grid_line_start(&msg_grid_adj, Rows - 1);
    did_ruler_col = off + this_ru_col;
    int w = grid_line_puts(did_ruler_col, buffer, -1, attr);
    grid_line_fill(did_ruler_col + w, off + width, fillchar, attr);
    grid_line_flush();
  }
}

/// Get the character to use in a status line.  Get its attributes in "*attr".
schar_T fillchar_status(hlf_T *group, win_T *wp)
{
  if (wp == curwin) {
    *group = HLF_S;
    return wp->w_p_fcs_chars.stl;
  } else {
    *group = HLF_SNC;
    return wp->w_p_fcs_chars.stlnc;
  }
}

/// Redraw the status line according to 'statusline' and take care of any
/// errors encountered.
void redraw_custom_statusline(win_T *wp)
{
  static bool entered = false;

  // When called recursively return.  This can happen when the statusline
  // contains an expression that triggers a redraw.
  if (entered) {
    return;
  }
  entered = true;

  win_redr_custom(wp, false, false, false);
  entered = false;
}

static void ui_ext_tabline_update(void)
{
  Arena arena = ARENA_EMPTY;

  size_t n_tabs = 0;
  FOR_ALL_TABS(tp) {
    n_tabs++;
  }

  Array tabs = arena_array(&arena, n_tabs);
  FOR_ALL_TABS(tp) {
    Dict tab_info = arena_dict(&arena, 2);
    PUT_C(tab_info, "tab", TABPAGE_OBJ(tp->handle));

    win_T *cwp = (tp == curtab) ? curwin : tp->tp_curwin;
    get_trans_bufname(cwp->w_buffer);
    PUT_C(tab_info, "name", CSTR_TO_ARENA_OBJ(&arena, NameBuff));

    ADD_C(tabs, DICT_OBJ(tab_info));
  }

  size_t n_buffers = 0;
  FOR_ALL_BUFFERS(buf) {
    n_buffers += buf->b_p_bl ? 1 : 0;
  }

  Array buffers = arena_array(&arena, n_buffers);
  FOR_ALL_BUFFERS(buf) {
    // Do not include unlisted buffers
    if (!buf->b_p_bl) {
      continue;
    }

    Dict buffer_info = arena_dict(&arena, 2);
    PUT_C(buffer_info, "buffer", BUFFER_OBJ(buf->handle));

    get_trans_bufname(buf);
    PUT_C(buffer_info, "name", CSTR_TO_ARENA_OBJ(&arena, NameBuff));

    ADD_C(buffers, DICT_OBJ(buffer_info));
  }

  ui_call_tabline_update(curtab->handle, tabs, curbuf->handle, buffers);
  arena_mem_free(arena_finish(&arena));
}

/// Draw the tab pages line at the top of the Vim window.
void draw_tabline(void)
{
  win_T *wp;
  int attr_nosel = HL_ATTR(HLF_TP);
  int attr_fill = HL_ATTR(HLF_TPF);
  bool use_sep_chars = (t_colors < 8);

  if (default_grid.chars == NULL) {
    return;
  }
  redraw_tabline = false;

  if (ui_has(kUITabline)) {
    ui_ext_tabline_update();
    return;
  }

  if (tabline_height() < 1) {
    return;
  }

  // Clear tab_page_click_defs: Clicking outside of tabs has no effect.
  assert(tab_page_click_defs_size >= (size_t)Columns);
  stl_clear_click_defs(tab_page_click_defs, tab_page_click_defs_size);

  // Use the 'tabline' option if it's set.
  if (*p_tal != NUL) {
    win_redr_custom(NULL, false, false, false);
  } else {
    int tabcount = 0;
    int col = 0;
    win_T *cwp;
    int wincount;
    grid_line_start(&default_gridview, 0);
    FOR_ALL_TABS(tp) {
      tabcount++;
    }

    int tabwidth = MAX(tabcount > 0 ? (Columns - 1 + tabcount / 2) / tabcount : 0, 6);

    int attr = attr_nosel;
    tabcount = 0;

    FOR_ALL_TABS(tp) {
      if (col >= Columns - 4) {
        break;
      }

      int scol = col;

      if (tp == curtab) {
        cwp = curwin;
        wp = firstwin;
      } else {
        cwp = tp->tp_curwin;
        wp = tp->tp_firstwin;
      }

      if (tp->tp_topframe == topframe) {
        attr = win_hl_attr(cwp, HLF_TPS);
      }
      if (use_sep_chars && col > 0) {
        grid_line_put_schar(col++, schar_from_ascii('|'), attr);
      }

      if (tp->tp_topframe != topframe) {
        attr = win_hl_attr(cwp, HLF_TP);
      }

      grid_line_put_schar(col++, schar_from_ascii(' '), attr);

      bool modified = false;

      for (wincount = 0; wp != NULL; wp = wp->w_next, wincount++) {
        if (!wp->w_config.focusable || wp->w_config.hide) {
          wincount--;
        } else if (bufIsChanged(wp->w_buffer)) {
          modified = true;
        }
      }

      if (modified || wincount > 1) {
        if (wincount > 1) {
          int len = vim_snprintf(NameBuff, MAXPATHL, "%d", wincount);
          if (col + len >= Columns - 3) {
            break;
          }
          grid_line_puts(col, NameBuff, len,
                         hl_combine_attr(attr, win_hl_attr(cwp, HLF_T)));
          col += len;
        }
        if (modified) {
          grid_line_put_schar(col++, schar_from_ascii('+'), attr);
        }
        grid_line_put_schar(col++, schar_from_ascii(' '), attr);
      }

      int room = scol - col + tabwidth - 1;
      if (room > 0) {
        // Get buffer name in NameBuff[]
        get_trans_bufname(cwp->w_buffer);
        shorten_dir(NameBuff);
        int len = vim_strsize(NameBuff);
        char *p = NameBuff;
        while (len > room) {
          len -= ptr2cells(p);
          MB_PTR_ADV(p);
        }
        int n = Columns - col - 1;
        len = MIN(len, n);

        grid_line_puts(col, p, -1, attr);
        col += len;
      }
      grid_line_put_schar(col++, schar_from_ascii(' '), attr);

      // Store the tab page number in tab_page_click_defs[], so that
      // jump_to_mouse() knows where each one is.
      tabcount++;
      while (scol < col) {
        tab_page_click_defs[scol++] = (StlClickDefinition) {
          .type = kStlClickTabSwitch,
          .tabnr = tabcount,
          .func = NULL,
        };
      }
    }

    for (int scol = col; scol < Columns; scol++) {
      // Use 0 as tabpage number here, so that double-click opens a tabpage
      // after the last one, and single-click goes to the next tabpage.
      tab_page_click_defs[scol] = (StlClickDefinition) {
        .type = kStlClickTabSwitch,
        .tabnr = 0,
        .func = NULL,
      };
    }

    char c = use_sep_chars ? '_' : ' ';
    grid_line_fill(col, Columns, schar_from_ascii(c), attr_fill);

    // Draw the 'showcmd' information if 'showcmdloc' == "tabline".
    if (p_sc && *p_sloc == 't') {
      int n = Columns - col - (tabcount > 1) * 3;
      const int sc_width = MIN(10, n);

      if (sc_width > 0) {
        grid_line_puts(Columns - sc_width - (tabcount > 1) * 2,
                       showcmd_buf, sc_width, attr_nosel);
      }
    }

    // Put an "X" for closing the current tab if there are several.
    if (tabcount > 1) {
      grid_line_put_schar(Columns - 1, schar_from_ascii('X'), attr_nosel);
      tab_page_click_defs[Columns - 1] = (StlClickDefinition) {
        .type = kStlClickTabClose,
        .tabnr = 999,
        .func = NULL,
      };
    }

    grid_line_flush();
  }

  // Reset the flag here again, in case evaluating 'tabline' causes it to be
  // set.
  redraw_tabline = false;
}

/// Build the 'statuscolumn' string for line "lnum". When "relnum" == -1,
/// the v:lnum and v:relnum variables don't have to be updated.
///
/// @return  The width of the built status column string for line "lnum"
int build_statuscol_str(win_T *wp, linenr_T lnum, linenr_T relnum, char *buf, statuscol_T *stcp)
{
  // Only update click definitions once per window per redraw.
  // Don't update when current width is 0, since it will be redrawn again if not empty.
  const bool fillclick = relnum >= 0 && stcp->width > 0 && lnum == wp->w_topline;

  if (relnum >= 0) {
    set_vim_var_nr(VV_LNUM, lnum);
    set_vim_var_nr(VV_RELNUM, relnum);
  }

  StlClickRecord *clickrec;
  char *stc = xstrdup(wp->w_p_stc);
  int width = build_stl_str_hl(wp, buf, MAXPATHL, stc, kOptStatuscolumn, OPT_LOCAL, 0,
                               stcp->width, &stcp->hlrec, NULL, fillclick ? &clickrec : NULL, stcp);
  xfree(stc);

  if (fillclick) {
    stl_clear_click_defs(wp->w_statuscol_click_defs, wp->w_statuscol_click_defs_size);
    wp->w_statuscol_click_defs = stl_alloc_click_defs(wp->w_statuscol_click_defs, width,
                                                      &wp->w_statuscol_click_defs_size);
    stl_fill_click_defs(wp->w_statuscol_click_defs, clickrec, buf, width, false);
  }

  return width;
}

/// Build a string from the status line items in "fmt".
/// Return length of string in screen cells.
///
/// Normally works for window "wp", except when working for 'tabline' then it
/// is "curwin".
///
/// Items are drawn interspersed with the text that surrounds it
/// Specials: %-<wid>(xxx%) => group, %= => separation marker, %< => truncation
/// Item: %-<minwid>.<maxwid><itemch> All but <itemch> are optional
///
/// If maxwidth is not zero, the string will be filled at any middle marker
/// or truncated if too long, fillchar is used for all whitespace.
///
/// @param wp  The window to build a statusline for
/// @param out  The output buffer to write the statusline to
///             Note: This should not be NameBuff
/// @param outlen  The length of the output buffer
/// @param fmt  The statusline format string
/// @param opt_idx  Index of the option corresponding to "fmt"
/// @param opt_scope  The scope corresponding to "opt_idx"
/// @param fillchar  Character to use when filling empty space in the statusline
/// @param maxwidth  The maximum width to make the statusline
/// @param hltab  HL attributes (can be NULL)
/// @param tabtab  Tab clicks definition (can be NULL)
/// @param stcp  Status column attributes (can be NULL)
///
/// @return  The final width of the statusline
int build_stl_str_hl(win_T *wp, char *out, size_t outlen, char *fmt, OptIndex opt_idx,
                     int opt_scope, schar_T fillchar, int maxwidth, stl_hlrec_t **hltab,
                     size_t *hltab_len, StlClickRecord **tabtab, statuscol_T *stcp)
{
  static size_t stl_items_len = 20;  // Initial value, grows as needed.
  static stl_item_t *stl_items = NULL;
  static int *stl_groupitems = NULL;
  static stl_hlrec_t *stl_hltab = NULL;
  static StlClickRecord *stl_tabtab = NULL;
  static int *stl_separator_locations = NULL;
  static int curitem = 0;

#define TMPLEN 70
  char buf_tmp[TMPLEN];
  char *usefmt = fmt;
  const bool save_redraw_not_allowed = redraw_not_allowed;
  const bool save_KeyTyped = KeyTyped;
  // TODO(Bram): find out why using called_emsg_before makes tests fail, does it
  // matter?
  // const int called_emsg_before = called_emsg;
  const int did_emsg_before = did_emsg;

  // When inside update_screen() we do not want redrawing a statusline,
  // ruler, title, etc. to trigger another redraw, it may cause an endless
  // loop.
  if (updating_screen) {
    redraw_not_allowed = true;
  }

  if (stl_items == NULL) {
    stl_items = xmalloc(sizeof(stl_item_t) * stl_items_len);
    stl_groupitems = xmalloc(sizeof(int) * stl_items_len);

    // Allocate one more, because the last element is used to indicate the
    // end of the list.
    stl_hltab = xmalloc(sizeof(stl_hlrec_t) * (stl_items_len + 1));
    stl_tabtab = xmalloc(sizeof(StlClickRecord) * (stl_items_len + 1));

    stl_separator_locations = xmalloc(sizeof(int) * stl_items_len);
  }

  // If "fmt" was set insecurely it needs to be evaluated in the sandbox.
  // "opt_idx" will be kOptInvalid when caller is nvim_eval_statusline().
  const bool use_sandbox = (opt_idx != kOptInvalid) ? was_set_insecurely(wp, opt_idx, opt_scope)
                                                    : false;

  // When the format starts with "%!" then evaluate it as an expression and
  // use the result as the actual format string.
  if (fmt[0] == '%' && fmt[1] == '!') {
    typval_T tv = {
      .v_type = VAR_NUMBER,
      .vval.v_number = wp->handle,
    };
    set_var(S_LEN("g:statusline_winid"), &tv, false);

    usefmt = eval_to_string_safe(fmt + 2, use_sandbox, false);
    if (usefmt == NULL) {
      usefmt = fmt;
    }

    do_unlet(S_LEN("g:statusline_winid"), true);
  }

  if (fillchar == 0) {
    fillchar = schar_from_ascii(' ');
  }

  // The cursor in windows other than the current one isn't always
  // up-to-date, esp. because of autocommands and timers.
  linenr_T lnum = wp->w_cursor.lnum;
  if (lnum > wp->w_buffer->b_ml.ml_line_count) {
    lnum = wp->w_buffer->b_ml.ml_line_count;
    wp->w_cursor.lnum = lnum;
  }

  // Get line & check if empty (cursorpos will show "0-1").
  const char *line_ptr = ml_get_buf(wp->w_buffer, lnum);
  bool empty_line = (*line_ptr == NUL);

  // Get the byte value now, in case we need it below. This is more
  // efficient than making a copy of the line.
  int byteval;
  const colnr_T len = ml_get_buf_len(wp->w_buffer, lnum);
  if (wp->w_cursor.col > len) {
    // Line may have changed since checking the cursor column, or the lnum
    // was adjusted above.
    wp->w_cursor.col = len;
    wp->w_cursor.coladd = 0;
    byteval = 0;
  } else {
    byteval = utf_ptr2char(line_ptr + wp->w_cursor.col);
  }

  int groupdepth = 0;
  int evaldepth = 0;

  // nvim_eval_statusline() can be called from inside a {-expression item so
  // this may be a recursive call. Keep track of the start index into "stl_items".
  // During post-processing only treat items filled in a certain recursion level.
  int evalstart = curitem;

  bool prevchar_isflag = true;
  bool prevchar_isitem = false;

  // out_p is the current position in the output buffer
  char *out_p = out;

  // out_end_p is the last valid character in the output buffer
  // Note: The null termination character must occur here or earlier,
  //       so any user-visible characters must occur before here.
  char *out_end_p = (out + outlen) - 1;

  // Proceed character by character through the statusline format string
  // fmt_p is the current position in the input buffer
  for (char *fmt_p = usefmt; *fmt_p != NUL;) {
    if (curitem == (int)stl_items_len) {
      size_t new_len = stl_items_len * 3 / 2;

      stl_items = xrealloc(stl_items, sizeof(stl_item_t) * new_len);
      stl_groupitems = xrealloc(stl_groupitems, sizeof(int) * new_len);
      stl_hltab = xrealloc(stl_hltab, sizeof(stl_hlrec_t) * (new_len + 1));
      stl_tabtab = xrealloc(stl_tabtab, sizeof(StlClickRecord) * (new_len + 1));
      stl_separator_locations =
        xrealloc(stl_separator_locations, sizeof(int) * new_len);

      stl_items_len = new_len;
    }

    if (*fmt_p != '%') {
      prevchar_isflag = prevchar_isitem = false;
    }

    // Copy the formatting verbatim until we reach the end of the string
    // or find a formatting item (denoted by `%`)
    // or run out of room in our output buffer.
    while (*fmt_p != NUL && *fmt_p != '%' && out_p < out_end_p) {
      *out_p++ = *fmt_p++;
    }

    // If we have processed the entire format string or run out of
    // room in our output buffer, exit the loop.
    if (*fmt_p == NUL || out_p >= out_end_p) {
      break;
    }

    // The rest of this loop will handle a single `%` item.
    // Note: We increment here to skip over the `%` character we are currently
    //       on so we can process the item's contents.
    fmt_p++;

    // Ignore `%` at the end of the format string
    if (*fmt_p == NUL) {
      break;
    }

    // Two `%` in a row is the escape sequence to print a
    // single `%` in the output buffer.
    if (*fmt_p == '%') {
      *out_p++ = *fmt_p++;
      prevchar_isflag = prevchar_isitem = false;
      continue;
    }

    // STL_SEPARATE: Separation between items, filled with white space.
    if (*fmt_p == STL_SEPARATE) {
      fmt_p++;
      // Ignored when we are inside of a grouping
      if (groupdepth > 0) {
        continue;
      }
      stl_items[curitem].type = Separate;
      stl_items[curitem++].start = out_p;
      continue;
    }

    // STL_TRUNCMARK: Where to begin truncating if the statusline is too long.
    if (*fmt_p == STL_TRUNCMARK) {
      fmt_p++;
      stl_items[curitem].type = Trunc;
      stl_items[curitem++].start = out_p;
      continue;
    }

    // The end of a grouping
    if (*fmt_p == ')') {
      fmt_p++;
      // Ignore if we are not actually inside a group currently
      if (groupdepth < 1) {
        continue;
      }
      groupdepth--;

      // Determine how long the group is.
      // Note: We set the current output position to null
      //       so `vim_strsize` will work.
      char *t = stl_items[stl_groupitems[groupdepth]].start;
      *out_p = NUL;
      ptrdiff_t group_len = vim_strsize(t);

      // If the group contained internal items
      // and the group did not have a minimum width,
      // and if there were no normal items in the group,
      // move the output pointer back to where the group started.
      // Note: This erases any non-item characters that were in the group.
      //       Otherwise there would be no reason to do this step.
      if (curitem > stl_groupitems[groupdepth] + 1
          && stl_items[stl_groupitems[groupdepth]].minwid == 0) {
        int group_start_userhl = 0;
        int group_end_userhl = 0;
        int n;
        // remove group if all items are empty and highlight group
        // doesn't change
        for (n = stl_groupitems[groupdepth] - 1; n >= 0; n--) {
          if (stl_items[n].type == Highlight || stl_items[n].type == HighlightCombining) {
            group_start_userhl = group_end_userhl = stl_items[n].minwid;
            break;
          }
        }
        for (n = stl_groupitems[groupdepth] + 1; n < curitem; n++) {
          if (stl_items[n].type == Normal) {
            break;
          }
          if (stl_items[n].type == Highlight || stl_items[n].type == HighlightCombining) {
            group_end_userhl = stl_items[n].minwid;
          }
        }
        if (n == curitem && group_start_userhl == group_end_userhl) {
          // empty group
          out_p = t;
          group_len = 0;
          for (n = stl_groupitems[groupdepth] + 1; n < curitem; n++) {
            // do not use the highlighting from the removed group
            if (stl_items[n].type == Highlight || stl_items[n].type == HighlightCombining) {
              stl_items[n].type = Empty;
            }
            // adjust the start position of TabPage to the next
            // item position
            if (stl_items[n].type == TabPage) {
              stl_items[n].start = out_p;
            }
          }
        }
      }

      // If the group is longer than it is allowed to be truncate by removing
      // bytes from the start of the group text. Don't truncate when item is a
      // 'statuscolumn' fold item to ensure correctness of the mouse clicks.
      if (group_len > stl_items[stl_groupitems[groupdepth]].maxwid
          && stl_items[stl_groupitems[groupdepth]].type != HighlightFold) {
        // { Determine the number of bytes to remove

        // Find the first character that should be included.
        int n = 0;
        while (group_len >= stl_items[stl_groupitems[groupdepth]].maxwid) {
          group_len -= ptr2cells(t + n);
          n += utfc_ptr2len(t + n);
        }
        // }

        // Prepend the `<` to indicate that the output was truncated.
        *t = '<';

        // { Move the truncated output
        memmove(t + 1, t + n, (size_t)(out_p - (t + n)));
        out_p = out_p - n + 1;
        // Fill up space left over by half a double-wide char.
        while (++group_len < stl_items[stl_groupitems[groupdepth]].minwid) {
          schar_get_adv(&out_p, fillchar);
        }
        // }

        // correct the start of the items for the truncation
        for (int idx = stl_groupitems[groupdepth] + 1; idx < curitem; idx++) {
          // Shift everything back by the number of removed bytes
          // Minus one for the leading '<' added above.
          stl_items[idx].start -= n - 1;

          // If the item was partially or completely truncated, set its
          // start to the start of the group
          stl_items[idx].start = MAX(stl_items[idx].start, t);
        }
        // If the group is shorter than the minimum width, add padding characters.
      } else if (abs(stl_items[stl_groupitems[groupdepth]].minwid) > group_len) {
        ptrdiff_t min_group_width = stl_items[stl_groupitems[groupdepth]].minwid;
        // If the group is left-aligned, add characters to the right.
        if (min_group_width < 0) {
          min_group_width = 0 - min_group_width;
          while (group_len++ < min_group_width && out_p < out_end_p) {
            schar_get_adv(&out_p, fillchar);
          }
          // If the group is right-aligned, shift everything to the right and
          // prepend with filler characters.
        } else {
          // { Move the group to the right
          group_len = (min_group_width - group_len) * (int)schar_len(fillchar);
          memmove(t + group_len, t, (size_t)(out_p - t));
          if (out_p + group_len >= (out_end_p + 1)) {
            group_len = out_end_p - out_p;
          }
          out_p += group_len;
          // }

          // Adjust item start positions
          for (int n = stl_groupitems[groupdepth] + 1; n < curitem; n++) {
            stl_items[n].start += group_len;
          }

          // Prepend the fill characters
          for (; group_len > 0; group_len--) {
            schar_get_adv(&t, fillchar);
          }
        }
      }
      continue;
    }
    int minwid = 0;
    int maxwid = 9999;
    int foldsignitem = -1;        // Start of fold or sign item
    bool left_align_num = false;  // Number item for should be left-aligned
    bool left_align = false;

    // Denotes that numbers should be left-padded with zeros
    bool zeropad = (*fmt_p == '0');
    if (zeropad) {
      fmt_p++;
    }

    // Denotes that the item should be left-aligned.
    // This is tracked by using a negative length.
    if (*fmt_p == '-') {
      fmt_p++;
      left_align = true;
    }

    // The first digit group is the item's min width
    if (ascii_isdigit(*fmt_p)) {
      minwid = getdigits_int(&fmt_p, false, 0);
    }

    // User highlight groups override the min width field
    // to denote the styling to use.
    if (*fmt_p == STL_USER_HL) {
      stl_items[curitem].type = Highlight;
      stl_items[curitem].start = out_p;
      stl_items[curitem].minwid = minwid > 9 ? 1 : minwid;
      fmt_p++;
      curitem++;
      continue;
    }

    // TABPAGE pairs are used to denote a region that when clicked will
    // either switch to or close a tab.
    //
    // Ex: tabline=%1Ttab\ one%X
    //   This tabline has a TABPAGENR item with minwid `1`,
    //   which is then closed with a TABCLOSENR item.
    //   Clicking on this region with mouse enabled will switch to tab 1.
    //   Setting the minwid to a different value will switch
    //   to that tab, if it exists
    //
    // Ex: tabline=%1Xtab\ one%X
    //   This tabline has a TABCLOSENR item with minwid `1`,
    //   which is then closed with a TABCLOSENR item.
    //   Clicking on this region with mouse enabled will close tab 1.
    //
    // Note: These options are only valid when creating a tabline.
    if (*fmt_p == STL_TABPAGENR || *fmt_p == STL_TABCLOSENR) {
      if (*fmt_p == STL_TABCLOSENR) {
        if (minwid == 0) {
          // %X ends the close label, go back to the previous tab label nr.
          for (int n = curitem - 1; n >= 0; n--) {
            if (stl_items[n].type == TabPage && stl_items[n].minwid >= 0) {
              minwid = stl_items[n].minwid;
              break;
            }
          }
        } else {
          // close nrs are stored as negative values
          minwid = -minwid;
        }
      }
      stl_items[curitem].type = TabPage;
      stl_items[curitem].start = out_p;
      stl_items[curitem].minwid = minwid;
      fmt_p++;
      curitem++;
      continue;
    }

    if (*fmt_p == STL_CLICK_FUNC) {
      fmt_p++;
      char *t = fmt_p;
      while (*fmt_p != STL_CLICK_FUNC && *fmt_p) {
        fmt_p++;
      }
      if (*fmt_p != STL_CLICK_FUNC) {
        break;
      }
      stl_items[curitem].type = ClickFunc;
      stl_items[curitem].start = out_p;
      stl_items[curitem].cmd = tabtab ? xmemdupz(t, (size_t)(fmt_p - t)) : NULL;
      stl_items[curitem].minwid = minwid;
      fmt_p++;
      curitem++;
      continue;
    }

    // Denotes the end of the minwid
    // the maxwid may follow immediately after
    if (*fmt_p == '.') {
      fmt_p++;
      if (ascii_isdigit(*fmt_p)) {
        maxwid = getdigits_int(&fmt_p, false, 50);
      }
    }

    // Bound the minimum width at 50.
    // Make the number negative to denote left alignment of the item
    minwid = (minwid > 50 ? 50 : minwid) * (left_align ? -1 : 1);

    // Denotes the start of a new group
    if (*fmt_p == '(') {
      stl_groupitems[groupdepth++] = curitem;
      stl_items[curitem].type = Group;
      stl_items[curitem].start = out_p;
      stl_items[curitem].minwid = minwid;
      stl_items[curitem].maxwid = maxwid;
      fmt_p++;
      curitem++;
      continue;
    }

    // Denotes end of expanded %{} block
    if (*fmt_p == '}' && evaldepth > 0) {
      fmt_p++;
      evaldepth--;
      continue;
    }

    // An invalid item was specified.
    // Continue processing on the next character of the format string.
    if (vim_strchr(STL_ALL, (uint8_t)(*fmt_p)) == NULL) {
      if (*fmt_p == NUL) {  // can happen with "%0"
        break;
      }
      fmt_p++;
      continue;
    }

    // The status line item type
    char opt = *fmt_p++;

    // OK - now for the real work
    NumberBase base = kNumBaseDecimal;
    bool itemisflag = false;
    bool fillable = true;
    int num = -1;
    char *str = NULL;
    switch (opt) {
    case STL_FILEPATH:
    case STL_FULLPATH:
    case STL_FILENAME: {
      // Set fillable to false so that ' ' in the filename will not
      // get replaced with the fillchar
      fillable = false;
      char *name = buf_spname(wp->w_buffer);
      if (name != NULL) {
        xstrlcpy(NameBuff, name, MAXPATHL);
      } else {
        char *t = (opt == STL_FULLPATH) ? wp->w_buffer->b_ffname
                                        : wp->w_buffer->b_fname;
        home_replace(wp->w_buffer, t, NameBuff, MAXPATHL, true);
      }
      trans_characters(NameBuff, MAXPATHL);
      if (opt != STL_FILENAME) {
        str = NameBuff;
      } else {
        str = path_tail(NameBuff);
      }
      break;
    }

    case STL_VIM_EXPR:     // '{'
    {
      char *block_start = fmt_p - 1;
      bool reevaluate = (*fmt_p == '%');
      itemisflag = true;

      if (reevaluate) {
        fmt_p++;
      }

      // Attempt to copy the expression to evaluate into
      // the output buffer as a null-terminated string.
      char *t = out_p;
      while ((*fmt_p != '}' || (reevaluate && fmt_p[-1] != '%'))
             && *fmt_p != NUL && out_p < out_end_p) {
        *out_p++ = *fmt_p++;
      }
      if (*fmt_p != '}') {          // missing '}' or out of space
        break;
      }
      fmt_p++;
      if (reevaluate && out_p > out) {
        out_p[-1] = NUL;  // remove the % at the end of %{% expr %}
      } else {
        *out_p = NUL;
      }

      // Move our position in the output buffer
      // to the beginning of the expression
      out_p = t;

      // { Evaluate the expression

      // Store the current buffer number as a string variable
      vim_snprintf(buf_tmp, sizeof(buf_tmp), "%d", curbuf->b_fnum);
      set_internal_string_var("g:actual_curbuf", buf_tmp);
      vim_snprintf(buf_tmp, sizeof(buf_tmp), "%d", curwin->handle);
      set_internal_string_var("g:actual_curwin", buf_tmp);

      buf_T *const save_curbuf = curbuf;
      win_T *const save_curwin = curwin;
      const int save_VIsual_active = VIsual_active;
      curwin = wp;
      curbuf = wp->w_buffer;
      // Visual mode is only valid in the current window.
      if (curwin != save_curwin) {
        VIsual_active = false;
      }

      // Note: The result stored in `t` is unused.
      str = eval_to_string_safe(out_p, use_sandbox, false);

      curwin = save_curwin;
      curbuf = save_curbuf;
      VIsual_active = save_VIsual_active;

      // Remove the variable we just stored
      do_unlet(S_LEN("g:actual_curbuf"), true);
      do_unlet(S_LEN("g:actual_curwin"), true);

      // }

      // Check if the evaluated result is a number.
      // If so, convert the number to an int and free the string.
      if (str != NULL && *str != NUL) {
        if (*skipdigits(str) == NUL) {
          num = atoi(str);
          XFREE_CLEAR(str);
          itemisflag = false;
        }
      }

      // If the output of the expression needs to be evaluated
      // replace the %{} block with the result of evaluation
      if (reevaluate && str != NULL && *str != NUL
          && strchr(str, '%') != NULL
          && evaldepth < MAX_STL_EVAL_DEPTH) {
        size_t parsed_usefmt = (size_t)(block_start - usefmt);
        size_t str_length = strlen(str);
        size_t fmt_length = strlen(fmt_p);
        size_t new_fmt_len = parsed_usefmt + str_length + fmt_length + 3;
        char *new_fmt = xmalloc(new_fmt_len * sizeof(char));
        char *new_fmt_p = new_fmt;

        new_fmt_p = (char *)memcpy(new_fmt_p, usefmt, parsed_usefmt) + parsed_usefmt;
        new_fmt_p = (char *)memcpy(new_fmt_p, str, str_length) + str_length;
        new_fmt_p = (char *)memcpy(new_fmt_p, "%}", 2) + 2;
        new_fmt_p = (char *)memcpy(new_fmt_p, fmt_p, fmt_length) + fmt_length;
        *new_fmt_p = 0;
        new_fmt_p = NULL;

        if (usefmt != fmt) {
          xfree(usefmt);
        }
        XFREE_CLEAR(str);
        usefmt = new_fmt;
        fmt_p = usefmt + parsed_usefmt;
        evaldepth++;
        continue;
      }
      break;
    }

    case STL_LINE:
      // Overload %l with v:(re)lnum for 'statuscolumn'. Place a sign when 'signcolumn'
      // is set to "number". Take care of alignment for 'number' + 'relativenumber'.
      if (stcp != NULL && (wp->w_p_nu || wp->w_p_rnu) && get_vim_var_nr(VV_VIRTNUM) == 0) {
        if (wp->w_maxscwidth == SCL_NUM && stcp->sattrs[0].text[0]) {
          goto stcsign;
        }
        int relnum = (int)get_vim_var_nr(VV_RELNUM);
        num = (!wp->w_p_rnu || (wp->w_p_nu && relnum == 0)) ? (int)get_vim_var_nr(VV_LNUM) : relnum;
        left_align_num = wp->w_p_rnu && wp->w_p_nu && relnum == 0;
        if (!left_align_num) {
          stl_items[curitem].type = Separate;
          stl_items[curitem++].start = out_p;
        }
      } else if (stcp == NULL) {
        num = (wp->w_buffer->b_ml.ml_flags & ML_EMPTY) ? 0 : wp->w_cursor.lnum;
      }
      break;

    case STL_NUMLINES:
      num = wp->w_buffer->b_ml.ml_line_count;
      break;

    case STL_COLUMN:
      num = (State & MODE_INSERT) == 0 && empty_line ? 0 : (int)wp->w_cursor.col + 1;
      break;

    case STL_VIRTCOL:
    case STL_VIRTCOL_ALT: {
      colnr_T virtcol = wp->w_virtcol + 1;
      // Don't display %V if it's the same as %c.
      if (opt == STL_VIRTCOL_ALT
          && (virtcol == (colnr_T)((State & MODE_INSERT) == 0 && empty_line
                                   ? 0 : (int)wp->w_cursor.col + 1))) {
        break;
      }
      num = virtcol;
      break;
    }

    case STL_PERCENTAGE:
      num = calc_percentage(wp->w_cursor.lnum, wp->w_buffer->b_ml.ml_line_count);
      break;

    case STL_ALTPERCENT:
      // Store the position percentage in our temporary buffer.
      // Note: We cannot store the value in `num` because
      //       `get_rel_pos` can return a named position. Ex: "Top"
      (void)get_rel_pos(wp, buf_tmp, TMPLEN);
      str = buf_tmp;
      break;

    case STL_SHOWCMD:
      if (p_sc && (opt_idx == kOptInvalid || find_option(p_sloc) == opt_idx)) {
        str = showcmd_buf;
      }
      break;

    case STL_ARGLISTSTAT:
      fillable = false;
      buf_tmp[0] = NUL;
      if (append_arg_number(wp, buf_tmp, sizeof(buf_tmp)) > 0) {
        str = buf_tmp;
      }
      break;

    case STL_KEYMAP:
      fillable = false;
      if (get_keymap_str(wp, "<%s>", buf_tmp, TMPLEN) > 0) {
        str = buf_tmp;
      }
      break;
    case STL_PAGENUM:
      num = 0;
      break;

    case STL_BUFNO:
      num = wp->w_buffer->b_fnum;
      break;

    case STL_OFFSET_X:
      base = kNumBaseHexadecimal;
      FALLTHROUGH;
    case STL_OFFSET: {
      int l = ml_find_line_or_offset(wp->w_buffer, wp->w_cursor.lnum, NULL,
                                     false);
      num = (wp->w_buffer->b_ml.ml_flags & ML_EMPTY) || l < 0
            ? 0 : l + 1 + ((State & MODE_INSERT) == 0 && empty_line
                           ? 0 : (int)wp->w_cursor.col);
      break;
    }
    case STL_BYTEVAL_X:
      base = kNumBaseHexadecimal;
      FALLTHROUGH;
    case STL_BYTEVAL:
      num = byteval;
      if (num == NL) {
        num = 0;
      } else if (num == CAR && get_fileformat(wp->w_buffer) == EOL_MAC) {
        num = NL;
      }
      break;

    case STL_ROFLAG:
    case STL_ROFLAG_ALT:
      itemisflag = true;
      if (wp->w_buffer->b_p_ro) {
        str = (opt == STL_ROFLAG_ALT) ? ",RO" : _("[RO]");
      }
      break;

    case STL_HELPFLAG:
    case STL_HELPFLAG_ALT:
      itemisflag = true;
      if (wp->w_buffer->b_help) {
        str = (opt == STL_HELPFLAG_ALT) ? ",HLP" : _("[Help]");
      }
      break;

    case STL_FOLDCOL:    // 'C' for 'statuscolumn'
    case STL_SIGNCOL: {  // 's' for 'statuscolumn'
stcsign:
      if (stcp == NULL) {
        break;
      }
      int fdc = opt == STL_FOLDCOL ? compute_foldcolumn(wp, 0) : 0;
      int width = opt == STL_FOLDCOL ? fdc > 0 : opt == STL_SIGNCOL ? wp->w_scwidth : 1;

      if (width <= 0) {
        break;
      }
      foldsignitem = curitem;
      lnum = (linenr_T)get_vim_var_nr(VV_LNUM);

      if (fdc > 0) {
        schar_T fold_buf[9];
        fill_foldcolumn(wp, stcp->foldinfo, lnum, 0, fdc, NULL, stcp->fold_vcol, fold_buf);
        stl_items[curitem].minwid = -(use_cursor_line_highlight(wp, lnum) ? HLF_CLF : HLF_FC);
        size_t buflen = 0;
        // TODO(bfredl): this is very backwards. we must support schar_T
        // being used directly in 'statuscolumn'
        for (int i = 0; i < fdc; i++) {
          buflen += schar_get(buf_tmp + buflen, fold_buf[i]);
        }
      }

      size_t signlen = 0;
      for (int i = 0; i < width; i++) {
        stl_items[curitem].start = out_p + signlen;
        if (fdc == 0) {
          SignTextAttrs sattr = stcp->sattrs[i];
          if (sattr.text[0] && get_vim_var_nr(VV_VIRTNUM) == 0) {
            signlen += describe_sign_text(buf_tmp + signlen, sattr.text);
            stl_items[curitem].minwid = -(stcp->sign_cul_id ? stcp->sign_cul_id : sattr.hl_id);
          } else {
            buf_tmp[signlen++] = ' ';
            buf_tmp[signlen++] = ' ';
            buf_tmp[signlen] = NUL;
            stl_items[curitem].minwid = 0;
          }
        }
        stl_items[curitem++].type = fdc > 0 ? HighlightFold : HighlightSign;
      }
      str = buf_tmp;
      break;
    }

    case STL_FILETYPE:
      // Copy the filetype if it is not null and the formatted string will fit
      // in the temporary buffer
      // (including the brackets and null terminating character)
      if (*wp->w_buffer->b_p_ft != NUL
          && strlen(wp->w_buffer->b_p_ft) < TMPLEN - 3) {
        vim_snprintf(buf_tmp, sizeof(buf_tmp), "[%s]",
                     wp->w_buffer->b_p_ft);
        str = buf_tmp;
      }
      break;

    case STL_FILETYPE_ALT:
      itemisflag = true;
      // Copy the filetype if it is not null and the formatted string will fit
      // in the temporary buffer
      // (including the comma and null terminating character)
      if (*wp->w_buffer->b_p_ft != NUL
          && strlen(wp->w_buffer->b_p_ft) < TMPLEN - 2) {
        vim_snprintf(buf_tmp, sizeof(buf_tmp), ",%s", wp->w_buffer->b_p_ft);
        // Uppercase the file extension
        for (char *t = buf_tmp; *t != 0; t++) {
          *t = (char)TOUPPER_LOC((uint8_t)(*t));
        }
        str = buf_tmp;
      }
      break;
    case STL_PREVIEWFLAG:
    case STL_PREVIEWFLAG_ALT:
      itemisflag = true;
      if (wp->w_p_pvw) {
        str = (opt == STL_PREVIEWFLAG_ALT) ? ",PRV" : _("[Preview]");
      }
      break;

    case STL_QUICKFIX:
      if (bt_quickfix(wp->w_buffer)) {
        str = wp->w_llist_ref ? _(msg_loclist) : _(msg_qflist);
      }
      break;

    case STL_MODIFIED:
    case STL_MODIFIED_ALT:
      itemisflag = true;
      switch ((opt == STL_MODIFIED_ALT)
              + bufIsChanged(wp->w_buffer) * 2
              + (!MODIFIABLE(wp->w_buffer)) * 4) {
      case 2:
        str = "[+]"; break;
      case 3:
        str = ",+"; break;
      case 4:
        str = "[-]"; break;
      case 5:
        str = ",-"; break;
      case 6:
        str = "[+-]"; break;
      case 7:
        str = ",+-"; break;
      }
      break;

    case STL_HIGHLIGHT_COMB:
    case STL_HIGHLIGHT: {
      // { The name of the highlight is surrounded by `#` or `$`
      char *t = fmt_p;
      while (*fmt_p != opt && *fmt_p != NUL) {
        fmt_p++;
      }
      // }

      // Create a highlight item based on the name
      if (*fmt_p == opt) {
        stl_items[curitem].type = opt == STL_HIGHLIGHT_COMB ? HighlightCombining : Highlight;
        stl_items[curitem].start = out_p;
        stl_items[curitem].minwid = -syn_name2id_len(t, (size_t)(fmt_p - t));
        curitem++;
        fmt_p++;
      }
      continue;
    }
    }

    // If we made it this far, the item is normal and starts at
    // our current position in the output buffer.
    // Non-normal items would have `continued`.
    stl_items[curitem].start = out_p;
    stl_items[curitem].type = Normal;

    // Copy the item string into the output buffer
    if (str != NULL && *str) {
      // { Skip the leading `,` or ` ` if the item is a flag
      //  and the proper conditions are met
      char *t = str;
      if (itemisflag) {
        if ((t[0] && t[1])
            && ((!prevchar_isitem && *t == ',')
                || (prevchar_isflag && *t == ' '))) {
          t++;
        }
        prevchar_isflag = true;
      }
      // }

      int l = vim_strsize(t);

      // If this item is non-empty, record that the last thing
      // we put in the output buffer was an item
      if (l > 0) {
        prevchar_isitem = true;
      }

      // If the item is too wide, truncate it from the beginning
      if (l > maxwid) {
        while (l >= maxwid) {
          l -= ptr2cells(t);
          t += utfc_ptr2len(t);
        }

        // Early out if there isn't enough room for the truncation marker
        if (out_p >= out_end_p) {
          break;
        }

        // Add the truncation marker
        *out_p++ = '<';
      }

      // If the item is right aligned and not wide enough,
      // pad with fill characters.
      if (minwid > 0) {
        for (; l < minwid && out_p < out_end_p; l++) {
          // Don't put a "-" in front of a digit.
          if (l + 1 == minwid && fillchar == '-' && ascii_isdigit(*t)) {
            *out_p++ = ' ';
          } else {
            schar_get_adv(&out_p, fillchar);
          }
        }
        minwid = 0;
        // For a 'statuscolumn' sign or fold item, shift the added items
        if (foldsignitem >= 0) {
          ptrdiff_t offset = out_p - stl_items[foldsignitem].start;
          for (int i = foldsignitem; i < curitem; i++) {
            stl_items[i].start += offset;
          }
        }
      } else {
        // Note: The negative value denotes a left aligned item.
        //       Here we switch the minimum width back to a positive value.
        minwid *= -1;
      }

      // { Copy the string text into the output buffer
      for (; *t && out_p < out_end_p; t++) {
        // Change a space by fillchar, unless fillchar is '-' and a
        // digit follows.
        if (fillable && *t == ' '
            && (!ascii_isdigit(*(t + 1)) || fillchar != '-')) {
          schar_get_adv(&out_p, fillchar);
        } else {
          *out_p++ = *t;
        }
      }
      // }

      // For a 'statuscolumn' sign or fold item, add an item to reset the highlight group
      if (foldsignitem >= 0) {
        stl_items[curitem].type = Highlight;
        stl_items[curitem].start = out_p;
        stl_items[curitem].minwid = 0;
      }

      // For left-aligned items, fill any remaining space with the fillchar
      for (; l < minwid && out_p < out_end_p; l++) {
        schar_get_adv(&out_p, fillchar);
      }

      // Otherwise if the item is a number, copy that to the output buffer.
    } else if (num >= 0) {
      if (out_p + 20 > out_end_p) {
        break;                  // not sufficient space
      }
      prevchar_isitem = true;

      // { Build the formatting string
      char nstr[20];
      char *t = nstr;
      if (opt == STL_VIRTCOL_ALT) {
        *t++ = '-';
        minwid--;
      }
      *t++ = '%';
      if (zeropad) {
        *t++ = '0';
      }

      // Note: The `*` means we take the width as one of the arguments
      *t++ = '*';
      *t++ = base == kNumBaseHexadecimal ? 'X' : 'd';
      *t = NUL;
      // }

      // { Determine how many characters the number will take up when printed
      //  Note: We have to cast the base because the compiler uses
      //        unsigned ints for the enum values.
      int num_chars = 1;
      for (int n = num; n >= (int)base; n /= (int)base) {
        num_chars++;
      }

      // VIRTCOL_ALT takes up an extra character because
      // of the `-` we added above.
      if (opt == STL_VIRTCOL_ALT) {
        num_chars++;
      }
      // }

      assert(out_end_p >= out_p);
      size_t remaining_buf_len = (size_t)(out_end_p - out_p) + 1;

      // If the number is going to take up too much room
      // Figure out the approximate number in "scientific" type notation.
      // Ex: 14532 with maxwid of 4 -> '14>3'
      if (num_chars > maxwid) {
        // Add two to the width because the power piece will take
        // two extra characters
        num_chars += 2;

        // How many extra characters there are
        int n = num_chars - maxwid;

        // { Reduce the number by base^n
        while (num_chars-- > maxwid) {
          num /= (int)base;
        }
        // }

        // { Add the format string for the exponent bit
        *t++ = '>';
        *t++ = '%';
        // Use the same base as the first number
        *t = t[-3];
        *++t = NUL;
        // }

        out_p += vim_snprintf_safelen(out_p, remaining_buf_len, nstr, 0, num, n);
      } else {
        out_p += vim_snprintf_safelen(out_p, remaining_buf_len, nstr, minwid, num);
      }

      // Otherwise, there was nothing to print so mark the item as empty
    } else {
      stl_items[curitem].type = Empty;
    }

    if (num >= 0 || (!itemisflag && str && *str)) {
      prevchar_isflag = false;              // Item not NULL, but not a flag
    }

    // Only free the string buffer if we allocated it.
    // Note: This is not needed if `str` is pointing at `tmp`
    if (opt == STL_VIM_EXPR) {
      XFREE_CLEAR(str);
    }

    // Item processed, move to the next
    curitem++;
    // For a 'statuscolumn' number item that is left aligned, add a separator item.
    if (left_align_num) {
      stl_items[curitem].type = Separate;
      stl_items[curitem++].start = out_p;
    }
  }

  *out_p = NUL;
  // Length of out[] used (excluding the NUL)
  size_t outputlen = (size_t)(out_p - out);
  // Subtract offset from `itemcnt` and restore `curitem` to previous recursion level.
  int itemcnt = curitem - evalstart;
  curitem = evalstart;

  // Free the format buffer if we allocated it internally
  if (usefmt != fmt) {
    xfree(usefmt);
  }

  // We have now processed the entire statusline format string.
  // What follows is post-processing to handle alignment and highlighting.

  int width = vim_strsize(out);
  if (maxwidth > 0 && width > maxwidth && (!stcp || width > MAX_STCWIDTH)) {
    // Result is too long, must truncate somewhere.
    int item_idx = evalstart;
    char *trunc_p;

    // If there are no items, truncate from beginning
    if (itemcnt == 0) {
      trunc_p = out;

      // Otherwise, look for the truncation item
    } else {
      // Default to truncating at the first item
      trunc_p = stl_items[item_idx].start;

      for (int i = evalstart; i < itemcnt + evalstart; i++) {
        if (stl_items[i].type == Trunc) {
          // Truncate at %< stl_items.
          trunc_p = stl_items[i].start;
          item_idx = i;
          break;
        }
      }
    }

    // If the truncation point we found is beyond the maximum
    // length of the string, truncate the end of the string.
    if (width - vim_strsize(trunc_p) >= maxwidth) {
      // Walk from the beginning of the
      // string to find the last character that will fit.
      trunc_p = out;
      width = 0;
      while (true) {
        width += ptr2cells(trunc_p);
        if (width >= maxwidth) {
          break;
        }

        // Note: Only advance the pointer if the next
        //       character will fit in the available output space
        trunc_p += utfc_ptr2len(trunc_p);
      }

      // Ignore any items in the statusline that occur after
      // the truncation point
      for (int i = evalstart; i < itemcnt + evalstart; i++) {
        if (stl_items[i].start > trunc_p) {
          for (int j = i; j < itemcnt + evalstart; j++) {
            if (stl_items[j].type == ClickFunc) {
              XFREE_CLEAR(stl_items[j].cmd);
            }
          }
          itemcnt = i;
          break;
        }
      }

      // Truncate the output
      *trunc_p++ = '>';
      *trunc_p = NUL;

      // Truncate at the truncation point we found
    } else {
      char *end = out + outputlen;

      // { Determine how many bytes to remove
      int trunc_len = 0;
      while (width >= maxwidth) {
        width -= ptr2cells(trunc_p + trunc_len);
        trunc_len += utfc_ptr2len(trunc_p + trunc_len);
      }
      // }

      // { Truncate the string
      char *trunc_end_p = trunc_p + trunc_len;
      memmove(trunc_p + 1, trunc_end_p, (size_t)(end - trunc_end_p) + 1);  // +1 for NUL
      end -= (size_t)(trunc_end_p - (trunc_p + 1));

      // Put a `<` to mark where we truncated at
      *trunc_p = '<';
      // }

      // { Change the start point for items based on
      //  their position relative to our truncation point

      // Note: The offset is one less than the truncation length because
      //       the truncation marker `<` is not counted.
      int item_offset = trunc_len - 1;

      for (int i = item_idx; i < itemcnt + evalstart; i++) {
        // Items starting at or after the end of the truncated section need
        // to be moved backwards.
        if (stl_items[i].start >= trunc_end_p) {
          stl_items[i].start -= item_offset;
        } else {
          // Anything inside the truncated area is set to start
          // at the `<` truncation character.
          stl_items[i].start = trunc_p;
        }
      }
      // }

      if (width + 1 < maxwidth) {
        // Advance the pointer to the end of the string
        trunc_p = end;
      }

      // Fill up for half a double-wide character.
      while (++width < maxwidth) {
        schar_get_adv(&trunc_p, fillchar);
        end = trunc_p;
      }
      (void)end;
    }
    width = maxwidth;

    // If there is room left in our statusline, and room left in our buffer,
    // add characters at the separate marker (if there is one) to
    // fill up the available space.
  } else if (width < maxwidth
             && outputlen + (size_t)(maxwidth - width) * schar_len(fillchar) + 1 < outlen) {
    // Find how many separators there are, which we will use when
    // figuring out how many groups there are.
    int num_separators = 0;
    for (int i = evalstart; i < itemcnt + evalstart; i++) {
      if (stl_items[i].type == Separate) {
        // Create an array of the start location for each separator mark.
        stl_separator_locations[num_separators] = i;
        num_separators++;
      }
    }

    // If we have separated groups, then we deal with it now
    if (num_separators) {
      int standard_spaces = (maxwidth - width) / num_separators;
      int final_spaces = (maxwidth - width) -
                         standard_spaces * (num_separators - 1);

      for (int l = 0; l < num_separators; l++) {
        int dislocation = (l == (num_separators - 1)) ? final_spaces : standard_spaces;
        dislocation *= (int)schar_len(fillchar);
        char *start = stl_items[stl_separator_locations[l]].start;
        char *seploc = start + dislocation;
        STRMOVE(seploc, start);
        for (char *s = start; s < seploc;) {
          schar_get_adv(&s, fillchar);
        }

        for (int item_idx = stl_separator_locations[l] + 1;
             item_idx < itemcnt + evalstart;
             item_idx++) {
          stl_items[item_idx].start += dislocation;
        }
      }

      width = maxwidth;
    }
  }

  // Store the info about highlighting.
  if (hltab != NULL) {
    *hltab = stl_hltab;
    stl_hlrec_t *sp = stl_hltab;
    for (int l = evalstart; l < itemcnt + evalstart; l++) {
      if (stl_items[l].type == Highlight || stl_items[l].type == HighlightCombining
          || stl_items[l].type == HighlightFold || stl_items[l].type == HighlightSign) {
        sp->start = stl_items[l].start;
        sp->userhl = stl_items[l].minwid;
        unsigned type = stl_items[l].type;
        sp->item = type == HighlightSign ? STL_SIGNCOL : type ==
                   HighlightFold ? STL_FOLDCOL : type ==
                   HighlightCombining ? STL_HIGHLIGHT_COMB : 0;
        sp++;
      }
    }
    sp->start = NULL;
    sp->userhl = 0;
  }
  if (hltab_len) {
    *hltab_len = (size_t)itemcnt;
  }

  // Store the info about tab pages labels.
  if (tabtab != NULL) {
    *tabtab = stl_tabtab;
    StlClickRecord *cur_tab_rec = stl_tabtab;
    for (int l = evalstart; l < itemcnt + evalstart; l++) {
      if (stl_items[l].type == TabPage) {
        cur_tab_rec->start = stl_items[l].start;
        if (stl_items[l].minwid == 0) {
          cur_tab_rec->def.type = kStlClickDisabled;
          cur_tab_rec->def.tabnr = 0;
        } else {
          int tabnr = stl_items[l].minwid;
          if (stl_items[l].minwid > 0) {
            cur_tab_rec->def.type = kStlClickTabSwitch;
          } else {
            cur_tab_rec->def.type = kStlClickTabClose;
            tabnr = -tabnr;
          }
          cur_tab_rec->def.tabnr = tabnr;
        }
        cur_tab_rec->def.func = NULL;
        cur_tab_rec++;
      } else if (stl_items[l].type == ClickFunc) {
        cur_tab_rec->start = stl_items[l].start;
        cur_tab_rec->def.type = kStlClickFuncRun;
        cur_tab_rec->def.tabnr = stl_items[l].minwid;
        cur_tab_rec->def.func = stl_items[l].cmd;
        cur_tab_rec++;
      }
    }
    cur_tab_rec->start = NULL;
    cur_tab_rec->def.type = kStlClickDisabled;
    cur_tab_rec->def.tabnr = 0;
    cur_tab_rec->def.func = NULL;
  }

  redraw_not_allowed = save_redraw_not_allowed;

  // Check for an error.  If there is one the display will be messed up and
  // might loop redrawing.  Avoid that by setting the option to its default.
  // TODO(Bram): find out why using called_emsg_before makes tests fail, does it
  // matter?
  // if (called_emsg > called_emsg_before)
  if (opt_idx != kOptInvalid && did_emsg > did_emsg_before) {
    set_option_direct(opt_idx, get_option_default(opt_idx, opt_scope), opt_scope, SID_ERROR);
  }

  // A user function may reset KeyTyped, restore it.
  KeyTyped = save_KeyTyped;

  return width;
}
