// This is an open source non-commercial project. Dear PVS-Studio, please check
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// drawscreen.c: Code for updating all the windows on the screen.
// This is the top level, drawline.c is the middle and grid.c/screen.c the lower level.

// update_screen() is the function that updates all windows and status lines.
// It is called from the main loop when must_redraw is non-zero.  It may be
// called from other places when an immediate screen update is needed.
//
// The part of the buffer that is displayed in a window is set with:
// - w_topline (first buffer line in window)
// - w_topfill (filler lines above the first line)
// - w_leftcol (leftmost window cell in window),
// - w_skipcol (skipped window cells of first line)
//
// Commands that only move the cursor around in a window, do not need to take
// action to update the display.  The main loop will check if w_topline is
// valid and update it (scroll the window) when needed.
//
// Commands that scroll a window change w_topline and must call
// check_cursor() to move the cursor into the visible part of the window, and
// call redraw_later(wp, UPD_VALID) to have the window displayed by update_screen()
// later.
//
// Commands that change text in the buffer must call changed_bytes() or
// changed_lines() to mark the area that changed and will require updating
// later.  The main loop will call update_screen(), which will update each
// window that shows the changed buffer.  This assumes text above the change
// can remain displayed as it is.  Text after the change may need updating for
// scrolling, folding and syntax highlighting.
//
// Commands that change how a window is displayed (e.g., setting 'list') or
// invalidate the contents of a window in another way (e.g., change fold
// settings), must call redraw_later(wp, UPD_NOT_VALID) to have the whole window
// redisplayed by update_screen() later.
//
// Commands that change how a buffer is displayed (e.g., setting 'tabstop')
// must call redraw_curbuf_later(UPD_NOT_VALID) to have all the windows for the
// buffer redisplayed by update_screen() later.
//
// Commands that change highlighting and possibly cause a scroll too must call
// redraw_later(wp, UPD_SOME_VALID) to update the whole window but still use
// scrolling to avoid redrawing everything.  But the length of displayed lines
// must not change, use UPD_NOT_VALID then.
//
// Commands that move the window position must call redraw_later(wp, UPD_NOT_VALID).
// TODO(neovim): should minimize redrawing by scrolling when possible.
//
// Commands that change everything (e.g., resizing the screen) must call
// redraw_all_later(UPD_NOT_VALID) or redraw_all_later(UPD_CLEAR).
//
// Things that are handled indirectly:
// - When messages scroll the screen up, msg_scrolled will be set and
//   update_screen() called to redraw.

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

#include "klib/kvec.h"
#include "nvim/api/private/defs.h"
#include "nvim/ascii.h"
#include "nvim/autocmd.h"
#include "nvim/buffer.h"
#include "nvim/charset.h"
#include "nvim/cmdexpand.h"
#include "nvim/decoration.h"
#include "nvim/decoration_provider.h"
#include "nvim/diff.h"
#include "nvim/drawline.h"
#include "nvim/drawscreen.h"
#include "nvim/ex_getln.h"
#include "nvim/extmark_defs.h"
#include "nvim/fold.h"
#include "nvim/globals.h"
#include "nvim/grid.h"
#include "nvim/highlight.h"
#include "nvim/highlight_group.h"
#include "nvim/insexpand.h"
#include "nvim/match.h"
#include "nvim/mbyte.h"
#include "nvim/memline.h"
#include "nvim/message.h"
#include "nvim/move.h"
#include "nvim/normal.h"
#include "nvim/option.h"
#include "nvim/plines.h"
#include "nvim/popupmenu.h"
#include "nvim/pos.h"
#include "nvim/profile.h"
#include "nvim/regexp.h"
#include "nvim/screen.h"
#include "nvim/statusline.h"
#include "nvim/syntax.h"
#include "nvim/terminal.h"
#include "nvim/types.h"
#include "nvim/ui.h"
#include "nvim/ui_compositor.h"
#include "nvim/version.h"
#include "nvim/vim.h"
#include "nvim/window.h"

/// corner value flags for hsep_connected and vsep_connected
typedef enum {
  WC_TOP_LEFT = 0,
  WC_TOP_RIGHT,
  WC_BOTTOM_LEFT,
  WC_BOTTOM_RIGHT,
} WindowCorner;

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

static bool redraw_popupmenu = false;
static bool msg_grid_invalid = false;
static bool resizing_autocmd = false;

static char *provider_err = NULL;

/// Check if the cursor line needs to be redrawn because of 'concealcursor'.
///
/// When cursor is moved at the same time, both lines will be redrawn regardless.
void conceal_check_cursor_line(void)
{
  bool should_conceal = conceal_cursor_line(curwin);
  if (curwin->w_p_cole > 0 && (conceal_cursor_used != should_conceal)) {
    redrawWinline(curwin, curwin->w_cursor.lnum);
    // Need to recompute cursor column, e.g., when starting Visual mode
    // without concealing.
    curs_columns(curwin, true);
  }
}

/// Resize default_grid to Rows and Columns.
///
/// Allocate default_grid.chars[] and other grid arrays.
///
/// There may be some time between setting Rows and Columns and (re)allocating
/// default_grid arrays.  This happens when starting up and when
/// (manually) changing the screen size.  Always use default_grid.rows and
/// default_grid.Columns to access items in default_grid.chars[].  Use Rows
/// and Columns for positioning text etc. where the final size of the screen is
/// needed.
///
/// @return  whether resizing has been done
bool default_grid_alloc(void)
{
  static bool resizing = false;

  // It's possible that we produce an out-of-memory message below, which
  // will cause this function to be called again.  To break the loop, just
  // return here.
  if (resizing) {
    return false;
  }
  resizing = true;

  // Allocation of the screen buffers is done only when the size changes and
  // when Rows and Columns have been set and we have started doing full
  // screen stuff.
  if ((default_grid.chars != NULL
       && Rows == default_grid.rows
       && Columns == default_grid.cols)
      || Rows == 0
      || Columns == 0
      || (!full_screen && default_grid.chars == NULL)) {
    resizing = false;
    return false;
  }

  // We're changing the size of the screen.
  // - Allocate new arrays for default_grid
  // - Move lines from the old arrays into the new arrays, clear extra
  //   lines (unless the screen is going to be cleared).
  // - Free the old arrays.
  //
  // If anything fails, make grid arrays NULL, so we don't do anything!
  // Continuing with the old arrays may result in a crash, because the
  // size is wrong.

  grid_alloc(&default_grid, Rows, Columns, true, true);

  stl_clear_click_defs(tab_page_click_defs, tab_page_click_defs_size);
  tab_page_click_defs = stl_alloc_click_defs(tab_page_click_defs, Columns,
                                             &tab_page_click_defs_size);

  default_grid.comp_height = Rows;
  default_grid.comp_width = Columns;

  default_grid.row_offset = 0;
  default_grid.col_offset = 0;
  default_grid.handle = DEFAULT_GRID_HANDLE;

  resizing = false;
  return true;
}

void screenclear(void)
{
  check_for_delay(false);

  if (starting == NO_SCREEN || default_grid.chars == NULL) {
    return;
  }

  // blank out the default grid
  for (int i = 0; i < default_grid.rows; i++) {
    grid_clear_line(&default_grid, default_grid.line_offset[i],
                    default_grid.cols, true);
    default_grid.line_wraps[i] = false;
  }

  ui_call_grid_clear(1);  // clear the display
  ui_comp_set_screen_valid(true);

  ns_hl_fast = -1;

  clear_cmdline = false;
  mode_displayed = false;

  redraw_all_later(UPD_NOT_VALID);
  redraw_cmdline = true;
  redraw_tabline = true;
  redraw_popupmenu = true;
  pum_invalidate();
  FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
    if (wp->w_floating) {
      wp->w_redr_type = UPD_CLEAR;
    }
  }
  if (must_redraw == UPD_CLEAR) {
    must_redraw = UPD_NOT_VALID;  // no need to clear again
  }
  compute_cmdrow();
  msg_row = cmdline_row;  // put cursor on last line for messages
  msg_col = 0;
  msg_scrolled = 0;  // can't scroll back
  msg_didany = false;
  msg_didout = false;
  if (HL_ATTR(HLF_MSG) > 0 && msg_use_grid() && msg_grid.chars) {
    grid_invalidate(&msg_grid);
    msg_grid_validate();
    msg_grid_invalid = false;
    clear_cmdline = true;
  }
}

/// Set dimensions of the Nvim application "screen".
void screen_resize(int width, int height)
{
  // Avoid recursiveness, can happen when setting the window size causes
  // another window-changed signal.
  if (updating_screen || resizing_screen) {
    return;
  }

  if (width < 0 || height < 0) {    // just checking...
    return;
  }

  if (State == MODE_HITRETURN || State == MODE_SETWSIZE) {
    // postpone the resizing
    State = MODE_SETWSIZE;
    return;
  }

  resizing_screen = true;

  Rows = height;
  Columns = width;
  check_screensize();
  int max_p_ch = Rows - min_rows() + 1;
  if (!ui_has(kUIMessages) && p_ch > 0 && p_ch > max_p_ch) {
    p_ch = max_p_ch ? max_p_ch : 1;
  }
  height = Rows;
  width = Columns;
  p_lines = Rows;
  p_columns = Columns;

  ui_call_grid_resize(1, width, height);

  int retry_count = 0;
  resizing_autocmd = true;

  // In rare cases, autocommands may have altered Rows or Columns,
  // so retry to check if we need to allocate the screen again.
  while (default_grid_alloc()) {
    // win_new_screensize will recompute floats position, but tell the
    // compositor to not redraw them yet
    ui_comp_set_screen_valid(false);
    if (msg_grid.chars) {
      msg_grid_invalid = true;
    }

    RedrawingDisabled++;

    win_new_screensize();      // fit the windows in the new sized screen

    comp_col();           // recompute columns for shown command and ruler

    RedrawingDisabled--;

    // Do not apply autocommands more than 3 times to avoid an endless loop
    // in case applying autocommands always changes Rows or Columns.
    if (++retry_count > 3) {
      break;
    }

    apply_autocmds(EVENT_VIMRESIZED, NULL, NULL, false, curbuf);
  }

  resizing_autocmd = false;
  redraw_all_later(UPD_CLEAR);

  if (starting != NO_SCREEN) {
    maketitle();

    changed_line_abv_curs();
    invalidate_botline();

    // We only redraw when it's needed:
    // - While at the more prompt or executing an external command, don't
    //   redraw, but position the cursor.
    // - While editing the command line, only redraw that. TODO: lies
    // - in Ex mode, don't redraw anything.
    // - Otherwise, redraw right now, and position the cursor.
    if (State == MODE_ASKMORE || State == MODE_EXTERNCMD || State == MODE_CONFIRM
        || exmode_active) {
      if (msg_grid.chars) {
        msg_grid_validate();
      }
      // TODO(bfredl): sometimes messes up the output. Implement clear+redraw
      // also for the pager? (or: what if the pager was just a modal window?)
      ui_comp_set_screen_valid(true);
      repeat_message();
    } else {
      if (curwin->w_p_scb) {
        do_check_scrollbind(true);
      }
      if (State & MODE_CMDLINE) {
        redraw_popupmenu = false;
        update_screen();
        redrawcmdline();
        if (pum_drawn()) {
          cmdline_pum_display(false);
        }
      } else {
        update_topline(curwin);
        if (pum_drawn()) {
          // TODO(bfredl): ins_compl_show_pum wants to redraw the screen first.
          // For now make sure the nested update_screen() won't redraw the
          // pum at the old position. Try to untangle this later.
          redraw_popupmenu = false;
          ins_compl_show_pum();
        }
        update_screen();
        if (redrawing()) {
          setcursor();
        }
      }
    }
    ui_flush();
  }
  resizing_screen = false;
}

/// Redraw the parts of the screen that is marked for redraw.
///
/// Most code shouldn't call this directly, rather use redraw_later() and
/// and redraw_all_later() to mark parts of the screen as needing a redraw.
int update_screen(void)
{
  static bool did_intro = false;
  bool is_stl_global = global_stl_height() > 0;

  // Don't do anything if the screen structures are (not yet) valid.
  // A VimResized autocmd can invoke redrawing in the middle of a resize,
  // which would bypass the checks in screen_resize for popupmenu etc.
  if (resizing_autocmd || !default_grid.chars) {
    return FAIL;
  }

  // May have postponed updating diffs.
  if (need_diff_redraw) {
    diff_redraw(true);
  }

  // Postpone the redrawing when it's not needed and when being called
  // recursively.
  if (!redrawing() || updating_screen) {
    return FAIL;
  }

  int type = must_redraw;

  // must_redraw is reset here, so that when we run into some weird
  // reason to redraw while busy redrawing (e.g., asynchronous
  // scrolling), or update_topline() in win_update() will cause a
  // scroll, or a decoration provider requires a redraw, the screen
  // will be redrawn later or in win_update().
  must_redraw = 0;

  updating_screen = 1;

  display_tick++;  // let syntax code know we're in a next round of
                   // display updating

  // Tricky: vim code can reset msg_scrolled behind our back, so need
  // separate bookkeeping for now.
  if (msg_did_scroll) {
    msg_did_scroll = false;
    msg_scrolled_at_flush = 0;
  }

  if (type >= UPD_CLEAR || !default_grid.valid) {
    ui_comp_set_screen_valid(false);
  }

  // if the screen was scrolled up when displaying a message, scroll it down
  if (msg_scrolled || msg_grid_invalid) {
    clear_cmdline = true;
    int valid = MAX(Rows - msg_scrollsize(), 0);
    if (msg_grid.chars) {
      // non-displayed part of msg_grid is considered invalid.
      for (int i = 0; i < MIN(msg_scrollsize(), msg_grid.rows); i++) {
        grid_clear_line(&msg_grid, msg_grid.line_offset[i],
                        msg_grid.cols, false);
      }
    }
    msg_grid.throttled = false;
    bool was_invalidated = false;

    // UPD_CLEAR is already handled
    if (type == UPD_NOT_VALID && !ui_has(kUIMultigrid) && msg_scrolled) {
      was_invalidated = ui_comp_set_screen_valid(false);
      for (int i = valid; i < Rows - p_ch; i++) {
        grid_clear_line(&default_grid, default_grid.line_offset[i],
                        Columns, false);
      }
      FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
        if (wp->w_floating) {
          continue;
        }
        if (W_ENDROW(wp) > valid) {
          // TODO(bfredl): too pessimistic. type could be UPD_NOT_VALID
          // only because windows that are above the separator.
          wp->w_redr_type = MAX(wp->w_redr_type, UPD_NOT_VALID);
        }
        if (!is_stl_global && W_ENDROW(wp) + wp->w_status_height > valid) {
          wp->w_redr_status = true;
        }
      }
      if (is_stl_global && Rows - p_ch - 1 > valid) {
        curwin->w_redr_status = true;
      }
    }
    msg_grid_set_pos(Rows - (int)p_ch, false);
    msg_grid_invalid = false;
    if (was_invalidated) {
      // screen was only invalid for the msgarea part.
      // @TODO(bfredl): using the same "valid" flag
      // for both messages and floats moving is bit of a mess.
      ui_comp_set_screen_valid(true);
    }
    msg_scrolled = 0;
    msg_scrolled_at_flush = 0;
    msg_grid_scroll_discount = 0;
    need_wait_return = false;
  }

  win_ui_flush(true);
  msg_ext_check_clear();

  // reset cmdline_row now (may have been changed temporarily)
  compute_cmdrow();

  bool hl_changed = false;
  // Check for changed highlighting
  if (need_highlight_changed) {
    highlight_changed();
    hl_changed = true;
  }

  if (type == UPD_CLEAR) {          // first clear screen
    screenclear();  // will reset clear_cmdline
                    // and set UPD_NOT_VALID for each window
    cmdline_screen_cleared();   // clear external cmdline state
    type = UPD_NOT_VALID;
    // must_redraw may be set indirectly, avoid another redraw later
    must_redraw = 0;
  } else if (!default_grid.valid) {
    grid_invalidate(&default_grid);
    default_grid.valid = true;
  }

  // might need to clear space on default_grid for the message area.
  if (type == UPD_NOT_VALID && clear_cmdline && !ui_has(kUIMessages)) {
    grid_fill(&default_grid, Rows - (int)p_ch, Rows, 0, Columns, ' ', ' ', 0);
  }

  ui_comp_set_screen_valid(true);

  DecorProviders providers;
  decor_providers_start(&providers, &provider_err);

  // "start" callback could have changed highlights for global elements
  if (win_check_ns_hl(NULL)) {
    redraw_cmdline = true;
    redraw_tabline = true;
  }

  if (clear_cmdline) {          // going to clear cmdline (done below)
    check_for_delay(false);
  }

  // Force redraw when width of 'number' or 'relativenumber' column
  // changes.
  // TODO(bfredl): special casing curwin here is SÅ JÄVLA BULL.
  // Either this should be done for all windows or not at all.
  if (curwin->w_redr_type < UPD_NOT_VALID
      && curwin->w_nrwidth != ((curwin->w_p_nu || curwin->w_p_rnu)
                               ? number_width(curwin) : 0)) {
    curwin->w_redr_type = UPD_NOT_VALID;
  }

  // Redraw the tab pages line if needed.
  if (redraw_tabline || type >= UPD_NOT_VALID) {
    update_window_hl(curwin, type >= UPD_NOT_VALID);
    FOR_ALL_TABS(tp) {
      if (tp != curtab) {
        update_window_hl(tp->tp_curwin, type >= UPD_NOT_VALID);
      }
    }
    draw_tabline();
  }

  // Correct stored syntax highlighting info for changes in each displayed
  // buffer.  Each buffer must only be done once.
  FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
    update_window_hl(wp, type >= UPD_NOT_VALID || hl_changed);

    buf_T *buf = wp->w_buffer;
    if (buf->b_mod_set) {
      if (buf->b_mod_tick_syn < display_tick
          && syntax_present(wp)) {
        syn_stack_apply_changes(buf);
        buf->b_mod_tick_syn = display_tick;
      }

      if (buf->b_mod_tick_decor < display_tick) {
        decor_providers_invoke_buf(buf, &providers, &provider_err);
        buf->b_mod_tick_decor = display_tick;
      }
    }
  }

  // Go from top to bottom through the windows, redrawing the ones that need it.
  bool did_one = false;
  screen_search_hl.rm.regprog = NULL;

  FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
    if (wp->w_redr_type == UPD_CLEAR && wp->w_floating && wp->w_grid_alloc.chars) {
      grid_invalidate(&wp->w_grid_alloc);
      wp->w_redr_type = UPD_NOT_VALID;
    }

    win_check_ns_hl(wp);

    // reallocate grid if needed.
    win_grid_alloc(wp);

    if (wp->w_redr_border || wp->w_redr_type >= UPD_NOT_VALID) {
      win_redr_border(wp);
    }

    if (wp->w_redr_type != 0) {
      if (!did_one) {
        did_one = true;
        start_search_hl();
      }
      win_update(wp, &providers);
    }

    // redraw status line and window bar after the window to minimize cursor movement
    if (wp->w_redr_status) {
      win_redr_winbar(wp);
      win_redr_status(wp);
    }
  }

  end_search_hl();

  // May need to redraw the popup menu.
  if (pum_drawn() && must_redraw_pum) {
    win_check_ns_hl(curwin);
    pum_redraw();
  }

  win_check_ns_hl(NULL);

  // Reset b_mod_set flags.  Going through all windows is probably faster
  // than going through all buffers (there could be many buffers).
  FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
    wp->w_buffer->b_mod_set = false;
  }

  updating_screen = 0;

  // Clear or redraw the command line.  Done last, because scrolling may
  // mess up the command line.
  if (clear_cmdline || redraw_cmdline || redraw_mode) {
    showmode();
  }

  // May put up an introductory message when not editing a file
  if (!did_intro) {
    maybe_intro_message();
  }
  did_intro = true;

  decor_providers_invoke_end(&providers, &provider_err);
  kvi_destroy(providers);

  // either cmdline is cleared, not drawn or mode is last drawn
  cmdline_was_last_drawn = false;
  return OK;
}

static void win_border_redr_title(win_T *wp, ScreenGrid *grid, int col)
{
  VirtText title_chunks = wp->w_float_config.title_chunks;

  for (size_t i = 0; i < title_chunks.size; i++) {
    char *text = title_chunks.items[i].text;
    int cell = (int)mb_string2cells(text);
    int hl_id = title_chunks.items[i].hl_id;
    int attr = hl_id ? syn_id2attr(hl_id) : 0;
    grid_puts(grid, text, 0, col, attr);
    col += cell;
  }
}

static void win_redr_border(win_T *wp)
{
  wp->w_redr_border = false;
  if (!(wp->w_floating && wp->w_float_config.border)) {
    return;
  }

  ScreenGrid *grid = &wp->w_grid_alloc;

  schar_T *chars = wp->w_float_config.border_chars;
  int *attrs = wp->w_float_config.border_attr;

  int *adj = wp->w_border_adj;
  int irow = wp->w_height_inner + wp->w_winbar_height, icol = wp->w_width_inner;

  if (adj[0]) {
    grid_puts_line_start(grid, 0);
    if (adj[3]) {
      grid_put_schar(grid, 0, 0, chars[0], attrs[0]);
    }

    for (int i = 0; i < icol; i++) {
      grid_put_schar(grid, 0, i + adj[3], chars[1], attrs[1]);
    }

    if (wp->w_float_config.title) {
      int title_col = 0;
      int title_width = wp->w_float_config.title_width;
      AlignTextPos title_pos = wp->w_float_config.title_pos;

      if (title_pos == kAlignCenter) {
        title_col = (icol - title_width) / 2 + 1;
      } else {
        title_col = title_pos == kAlignLeft ? 1 : icol - title_width + 1;
      }

      win_border_redr_title(wp, grid, title_col);
    }
    if (adj[1]) {
      grid_put_schar(grid, 0, icol + adj[3], chars[2], attrs[2]);
    }
    grid_puts_line_flush(false);
  }

  for (int i = 0; i < irow; i++) {
    if (adj[3]) {
      grid_puts_line_start(grid, i + adj[0]);
      grid_put_schar(grid, i + adj[0], 0, chars[7], attrs[7]);
      grid_puts_line_flush(false);
    }
    if (adj[1]) {
      int ic = (i == 0 && !adj[0] && chars[2][0]) ? 2 : 3;
      grid_puts_line_start(grid, i + adj[0]);
      grid_put_schar(grid, i + adj[0], icol + adj[3], chars[ic], attrs[ic]);
      grid_puts_line_flush(false);
    }
  }

  if (adj[2]) {
    grid_puts_line_start(grid, irow + adj[0]);
    if (adj[3]) {
      grid_put_schar(grid, irow + adj[0], 0, chars[6], attrs[6]);
    }
    for (int i = 0; i < icol; i++) {
      int ic = (i == 0 && !adj[3] && chars[6][0]) ? 6 : 5;
      grid_put_schar(grid, irow + adj[0], i + adj[3], chars[ic], attrs[ic]);
    }
    if (adj[1]) {
      grid_put_schar(grid, irow + adj[0], icol + adj[3], chars[4], attrs[4]);
    }
    grid_puts_line_flush(false);
  }
}

/// Show current cursor info in ruler and various other places
///
/// @param always  if false, only show ruler if position has changed.
void show_cursor_info(bool always)
{
  if (!always && !redrawing()) {
    return;
  }

  win_check_ns_hl(curwin);
  if ((*p_stl != NUL || *curwin->w_p_stl != NUL)
      && (curwin->w_status_height || global_stl_height())) {
    redraw_custom_statusline(curwin);
  } else {
    win_redr_ruler(curwin, always);
  }
  if (*p_wbr != NUL || *curwin->w_p_wbr != NUL) {
    win_redr_winbar(curwin);
  }

  if (need_maketitle
      || (p_icon && (stl_syntax & STL_IN_ICON))
      || (p_title && (stl_syntax & STL_IN_TITLE))) {
    maketitle();
  }

  win_check_ns_hl(NULL);
  // Redraw the tab pages line if needed.
  if (redraw_tabline) {
    draw_tabline();
  }
}

static void redraw_win_signcol(win_T *wp)
{
  // If we can compute a change in the automatic sizing of the sign column
  // under 'signcolumn=auto:X' and signs currently placed in the buffer, better
  // figuring it out here so we can redraw the entire screen for it.
  int scwidth = wp->w_scwidth;
  wp->w_scwidth = win_signcol_count(wp);
  if (wp->w_scwidth != scwidth) {
    changed_line_abv_curs_win(wp);
  }
}

/// Check if horizontal separator of window "wp" at specified window corner is connected to the
/// horizontal separator of another window
/// Assumes global statusline is enabled
static bool hsep_connected(win_T *wp, WindowCorner corner)
{
  bool before = (corner == WC_TOP_LEFT || corner == WC_BOTTOM_LEFT);
  int sep_row = (corner == WC_TOP_LEFT || corner == WC_TOP_RIGHT)
                ? wp->w_winrow - 1 : W_ENDROW(wp);
  frame_T *fr = wp->w_frame;

  while (fr->fr_parent != NULL) {
    if (fr->fr_parent->fr_layout == FR_ROW && (before ? fr->fr_prev : fr->fr_next) != NULL) {
      fr = before ? fr->fr_prev : fr->fr_next;
      break;
    }
    fr = fr->fr_parent;
  }
  if (fr->fr_parent == NULL) {
    return false;
  }
  while (fr->fr_layout != FR_LEAF) {
    fr = fr->fr_child;
    if (fr->fr_parent->fr_layout == FR_ROW && before) {
      while (fr->fr_next != NULL) {
        fr = fr->fr_next;
      }
    } else {
      while (fr->fr_next != NULL && frame2win(fr)->w_winrow + fr->fr_height < sep_row) {
        fr = fr->fr_next;
      }
    }
  }

  return (sep_row == fr->fr_win->w_winrow - 1 || sep_row == W_ENDROW(fr->fr_win));
}

/// Check if vertical separator of window "wp" at specified window corner is connected to the
/// vertical separator of another window
static bool vsep_connected(win_T *wp, WindowCorner corner)
{
  bool before = (corner == WC_TOP_LEFT || corner == WC_TOP_RIGHT);
  int sep_col = (corner == WC_TOP_LEFT || corner == WC_BOTTOM_LEFT)
                ? wp->w_wincol - 1 : W_ENDCOL(wp);
  frame_T *fr = wp->w_frame;

  while (fr->fr_parent != NULL) {
    if (fr->fr_parent->fr_layout == FR_COL && (before ? fr->fr_prev : fr->fr_next) != NULL) {
      fr = before ? fr->fr_prev : fr->fr_next;
      break;
    }
    fr = fr->fr_parent;
  }
  if (fr->fr_parent == NULL) {
    return false;
  }
  while (fr->fr_layout != FR_LEAF) {
    fr = fr->fr_child;
    if (fr->fr_parent->fr_layout == FR_COL && before) {
      while (fr->fr_next != NULL) {
        fr = fr->fr_next;
      }
    } else {
      while (fr->fr_next != NULL && frame2win(fr)->w_wincol + fr->fr_width < sep_col) {
        fr = fr->fr_next;
      }
    }
  }

  return (sep_col == fr->fr_win->w_wincol - 1 || sep_col == W_ENDCOL(fr->fr_win));
}

/// Draw the vertical separator right of window "wp"
static void draw_vsep_win(win_T *wp)
{
  if (wp->w_vsep_width) {
    // draw the vertical separator right of this window
    int hl;
    int c = fillchar_vsep(wp, &hl);
    grid_fill(&default_grid, wp->w_winrow, W_ENDROW(wp),
              W_ENDCOL(wp), W_ENDCOL(wp) + 1, c, ' ', hl);
  }
}

/// Draw the horizontal separator below window "wp"
static void draw_hsep_win(win_T *wp)
{
  if (wp->w_hsep_height) {
    // draw the horizontal separator below this window
    int hl;
    int c = fillchar_hsep(wp, &hl);
    grid_fill(&default_grid, W_ENDROW(wp), W_ENDROW(wp) + 1,
              wp->w_wincol, W_ENDCOL(wp), c, c, hl);
  }
}

/// Get the separator connector for specified window corner of window "wp"
static int get_corner_sep_connector(win_T *wp, WindowCorner corner)
{
  // It's impossible for windows to be connected neither vertically nor horizontally
  // So if they're not vertically connected, assume they're horizontally connected
  if (vsep_connected(wp, corner)) {
    if (hsep_connected(wp, corner)) {
      return wp->w_p_fcs_chars.verthoriz;
    } else if (corner == WC_TOP_LEFT || corner == WC_BOTTOM_LEFT) {
      return wp->w_p_fcs_chars.vertright;
    } else {
      return wp->w_p_fcs_chars.vertleft;
    }
  } else if (corner == WC_TOP_LEFT || corner == WC_TOP_RIGHT) {
    return wp->w_p_fcs_chars.horizdown;
  } else {
    return wp->w_p_fcs_chars.horizup;
  }
}

/// Draw separator connecting characters on the corners of window "wp"
static void draw_sep_connectors_win(win_T *wp)
{
  // Don't draw separator connectors unless global statusline is enabled and the window has
  // either a horizontal or vertical separator
  if (global_stl_height() == 0 || !(wp->w_hsep_height == 1 || wp->w_vsep_width == 1)) {
    return;
  }

  int hl = win_hl_attr(wp, HLF_C);

  // Determine which edges of the screen the window is located on so we can avoid drawing separators
  // on corners contained in those edges
  bool win_at_top;
  bool win_at_bottom = wp->w_hsep_height == 0;
  bool win_at_left;
  bool win_at_right = wp->w_vsep_width == 0;
  frame_T *frp;

  for (frp = wp->w_frame; frp->fr_parent != NULL; frp = frp->fr_parent) {
    if (frp->fr_parent->fr_layout == FR_COL && frp->fr_prev != NULL) {
      break;
    }
  }
  win_at_top = frp->fr_parent == NULL;
  for (frp = wp->w_frame; frp->fr_parent != NULL; frp = frp->fr_parent) {
    if (frp->fr_parent->fr_layout == FR_ROW && frp->fr_prev != NULL) {
      break;
    }
  }
  win_at_left = frp->fr_parent == NULL;

  // Draw the appropriate separator connector in every corner where drawing them is necessary
  if (!(win_at_top || win_at_left)) {
    grid_putchar(&default_grid, get_corner_sep_connector(wp, WC_TOP_LEFT),
                 wp->w_winrow - 1, wp->w_wincol - 1, hl);
  }
  if (!(win_at_top || win_at_right)) {
    grid_putchar(&default_grid, get_corner_sep_connector(wp, WC_TOP_RIGHT),
                 wp->w_winrow - 1, W_ENDCOL(wp), hl);
  }
  if (!(win_at_bottom || win_at_left)) {
    grid_putchar(&default_grid, get_corner_sep_connector(wp, WC_BOTTOM_LEFT),
                 W_ENDROW(wp), wp->w_wincol - 1, hl);
  }
  if (!(win_at_bottom || win_at_right)) {
    grid_putchar(&default_grid, get_corner_sep_connector(wp, WC_BOTTOM_RIGHT),
                 W_ENDROW(wp), W_ENDCOL(wp), hl);
  }
}

/// Update a single window.
///
/// This may cause the windows below it also to be redrawn (when clearing the
/// screen or scrolling lines).
///
/// How the window is redrawn depends on wp->w_redr_type.  Each type also
/// implies the one below it.
/// UPD_NOT_VALID    redraw the whole window
/// UPD_SOME_VALID   redraw the whole window but do scroll when possible
/// UPD_REDRAW_TOP   redraw the top w_upd_rows window lines, otherwise like UPD_VALID
/// UPD_INVERTED     redraw the changed part of the Visual area
/// UPD_INVERTED_ALL redraw the whole Visual area
/// UPD_VALID        1. scroll up/down to adjust for a changed w_topline
///                  2. update lines at the top when scrolled down
///                  3. redraw changed text:
///                     - if wp->w_buffer->b_mod_set set, update lines between
///                       b_mod_top and b_mod_bot.
///                     - if wp->w_redraw_top non-zero, redraw lines between
///                       wp->w_redraw_top and wp->w_redr_bot.
///                     - continue redrawing when syntax status is invalid.
///                  4. if scrolled up, update lines at the bottom.
/// This results in three areas that may need updating:
/// top: from first row to top_end (when scrolled down)
/// mid: from mid_start to mid_end (update inversion or changed text)
/// bot: from bot_start to last row (when scrolled up)
static void win_update(win_T *wp, DecorProviders *providers)
{
  bool called_decor_providers = false;
win_update_start:
  ;
  int top_end = 0;              // Below last row of the top area that needs
                                // updating.  0 when no top area updating.
  int mid_start = 999;          // first row of the mid area that needs
                                // updating.  999 when no mid area updating.
  int mid_end = 0;              // Below last row of the mid area that needs
                                // updating.  0 when no mid area updating.
  int bot_start = 999;          // first row of the bot area that needs
                                // updating.  999 when no bot area updating
  bool scrolled_down = false;   // true when scrolled down when w_topline got smaller a bit
  bool top_to_mod = false;      // redraw above mod_top

  int bot_scroll_start = 999;   // first line that needs to be redrawn due to
                                // scrolling. only used for EOB

  static bool recursive = false;  // being called recursively

  // Remember what happened to the previous line.
  enum {
    DID_NONE = 1,  // didn't update a line
    DID_LINE = 2,  // updated a normal line
    DID_FOLD = 3,  // updated a folded line
  } did_update = DID_NONE;

  linenr_T syntax_last_parsed = 0;              // last parsed text line
  linenr_T mod_top = 0;
  linenr_T mod_bot = 0;

  int type = wp->w_redr_type;

  if (type >= UPD_NOT_VALID) {
    // TODO(bfredl): should only be implied for CLEAR, not NOT_VALID!
    wp->w_redr_status = true;

    wp->w_lines_valid = 0;
  }

  // Window is zero-height: Only need to draw the separator
  if (wp->w_grid.rows == 0) {
    // draw the horizontal separator below this window
    draw_hsep_win(wp);
    draw_sep_connectors_win(wp);
    wp->w_redr_type = 0;
    return;
  }

  // Window is zero-width: Only need to draw the separator.
  if (wp->w_grid.cols == 0) {
    // draw the vertical separator right of this window
    draw_vsep_win(wp);
    draw_sep_connectors_win(wp);
    wp->w_redr_type = 0;
    return;
  }

  buf_T *buf = wp->w_buffer;

  // reset got_int, otherwise regexp won't work
  int save_got_int = got_int;
  got_int = 0;
  // Set the time limit to 'redrawtime'.
  proftime_T syntax_tm = profile_setlimit(p_rdt);
  syn_set_timeout(&syntax_tm);

  win_extmark_arr.size = 0;

  decor_redraw_reset(buf, &decor_state);

  DecorProviders line_providers;
  decor_providers_invoke_win(wp, providers, &line_providers, &provider_err);
  if (must_redraw != 0) {
    must_redraw = 0;
    if (!called_decor_providers) {
      called_decor_providers = true;
      goto win_update_start;
    }
  }

  redraw_win_signcol(wp);

  init_search_hl(wp, &screen_search_hl);

  // Force redraw when width of 'number' or 'relativenumber' column
  // changes.
  int nrwidth = (wp->w_p_nu || wp->w_p_rnu) ? number_width(wp) : 0;
  if (wp->w_nrwidth != nrwidth) {
    type = UPD_NOT_VALID;
    wp->w_nrwidth = nrwidth;

    if (buf->terminal) {
      terminal_check_size(buf->terminal);
    }
  } else if (buf->b_mod_set
             && buf->b_mod_xlines != 0
             && wp->w_redraw_top != 0) {
    // When there are both inserted/deleted lines and specific lines to be
    // redrawn, w_redraw_top and w_redraw_bot may be invalid, just redraw
    // everything (only happens when redrawing is off for while).
    type = UPD_NOT_VALID;
  } else {
    // Set mod_top to the first line that needs displaying because of
    // changes.  Set mod_bot to the first line after the changes.
    mod_top = wp->w_redraw_top;
    if (wp->w_redraw_bot != 0) {
      mod_bot = wp->w_redraw_bot + 1;
    } else {
      mod_bot = 0;
    }
    if (buf->b_mod_set) {
      if (mod_top == 0 || mod_top > buf->b_mod_top) {
        mod_top = buf->b_mod_top;
        // Need to redraw lines above the change that may be included
        // in a pattern match.
        if (syntax_present(wp)) {
          mod_top -= buf->b_s.b_syn_sync_linebreaks;
          if (mod_top < 1) {
            mod_top = 1;
          }
        }
      }
      if (mod_bot == 0 || mod_bot < buf->b_mod_bot) {
        mod_bot = buf->b_mod_bot;
      }

      // When 'hlsearch' is on and using a multi-line search pattern, a
      // change in one line may make the Search highlighting in a
      // previous line invalid.  Simple solution: redraw all visible
      // lines above the change.
      // Same for a match pattern.
      if (screen_search_hl.rm.regprog != NULL
          && re_multiline(screen_search_hl.rm.regprog)) {
        top_to_mod = true;
      } else {
        const matchitem_T *cur = wp->w_match_head;
        while (cur != NULL) {
          if (cur->mit_match.regprog != NULL
              && re_multiline(cur->mit_match.regprog)) {
            top_to_mod = true;
            break;
          }
          cur = cur->mit_next;
        }
      }
    }
    if (mod_top != 0 && hasAnyFolding(wp)) {
      linenr_T lnumt, lnumb;

      // A change in a line can cause lines above it to become folded or
      // unfolded.  Find the top most buffer line that may be affected.
      // If the line was previously folded and displayed, get the first
      // line of that fold.  If the line is folded now, get the first
      // folded line.  Use the minimum of these two.

      // Find last valid w_lines[] entry above mod_top.  Set lnumt to
      // the line below it.  If there is no valid entry, use w_topline.
      // Find the first valid w_lines[] entry below mod_bot.  Set lnumb
      // to this line.  If there is no valid entry, use MAXLNUM.
      lnumt = wp->w_topline;
      lnumb = MAXLNUM;
      for (int i = 0; i < wp->w_lines_valid; i++) {
        if (wp->w_lines[i].wl_valid) {
          if (wp->w_lines[i].wl_lastlnum < mod_top) {
            lnumt = wp->w_lines[i].wl_lastlnum + 1;
          }
          if (lnumb == MAXLNUM && wp->w_lines[i].wl_lnum >= mod_bot) {
            lnumb = wp->w_lines[i].wl_lnum;
            // When there is a fold column it might need updating
            // in the next line ("J" just above an open fold).
            if (compute_foldcolumn(wp, 0) > 0) {
              lnumb++;
            }
          }
        }
      }

      (void)hasFoldingWin(wp, mod_top, &mod_top, NULL, true, NULL);
      if (mod_top > lnumt) {
        mod_top = lnumt;
      }

      // Now do the same for the bottom line (one above mod_bot).
      mod_bot--;
      (void)hasFoldingWin(wp, mod_bot, NULL, &mod_bot, true, NULL);
      mod_bot++;
      if (mod_bot < lnumb) {
        mod_bot = lnumb;
      }
    }

    // When a change starts above w_topline and the end is below
    // w_topline, start redrawing at w_topline.
    // If the end of the change is above w_topline: do like no change was
    // made, but redraw the first line to find changes in syntax.
    if (mod_top != 0 && mod_top < wp->w_topline) {
      if (mod_bot > wp->w_topline) {
        mod_top = wp->w_topline;
      } else if (syntax_present(wp)) {
        top_end = 1;
      }
    }

    // When line numbers are displayed need to redraw all lines below
    // inserted/deleted lines.
    if (mod_top != 0 && buf->b_mod_xlines != 0 && wp->w_p_nu) {
      mod_bot = MAXLNUM;
    }
  }

  wp->w_redraw_top = 0;  // reset for next time
  wp->w_redraw_bot = 0;

  // When only displaying the lines at the top, set top_end.  Used when
  // window has scrolled down for msg_scrolled.
  if (type == UPD_REDRAW_TOP) {
    long j = 0;
    for (int i = 0; i < wp->w_lines_valid; i++) {
      j += wp->w_lines[i].wl_size;
      if (j >= wp->w_upd_rows) {
        top_end = (int)j;
        break;
      }
    }
    if (top_end == 0) {
      // not found (cannot happen?): redraw everything
      type = UPD_NOT_VALID;
    } else {
      // top area defined, the rest is UPD_VALID
      type = UPD_VALID;
    }
  }

  // If there are no changes on the screen that require a complete redraw,
  // handle three cases:
  // 1: we are off the top of the screen by a few lines: scroll down
  // 2: wp->w_topline is below wp->w_lines[0].wl_lnum: may scroll up
  // 3: wp->w_topline is wp->w_lines[0].wl_lnum: find first entry in
  //    w_lines[] that needs updating.
  if ((type == UPD_VALID || type == UPD_SOME_VALID
       || type == UPD_INVERTED || type == UPD_INVERTED_ALL)
      && !wp->w_botfill && !wp->w_old_botfill) {
    if (mod_top != 0
        && wp->w_topline == mod_top
        && (!wp->w_lines[0].wl_valid
            || wp->w_topline == wp->w_lines[0].wl_lnum)) {
      // w_topline is the first changed line and window is not scrolled,
      // the scrolling from changed lines will be done further down.
    } else if (wp->w_lines[0].wl_valid
               && (wp->w_topline < wp->w_lines[0].wl_lnum
                   || (wp->w_topline == wp->w_lines[0].wl_lnum
                       && wp->w_topfill > wp->w_old_topfill))) {
      // New topline is above old topline: May scroll down.
      long j;
      if (hasAnyFolding(wp)) {
        linenr_T ln;

        // count the number of lines we are off, counting a sequence
        // of folded lines as one
        j = 0;
        for (ln = wp->w_topline; ln < wp->w_lines[0].wl_lnum; ln++) {
          j++;
          if (j >= wp->w_grid.rows - 2) {
            break;
          }
          (void)hasFoldingWin(wp, ln, NULL, &ln, true, NULL);
        }
      } else {
        j = wp->w_lines[0].wl_lnum - wp->w_topline;
      }
      if (j < wp->w_grid.rows - 2) {               // not too far off
        int i = plines_m_win(wp, wp->w_topline, wp->w_lines[0].wl_lnum - 1);
        // insert extra lines for previously invisible filler lines
        if (wp->w_lines[0].wl_lnum != wp->w_topline) {
          i += win_get_fill(wp, wp->w_lines[0].wl_lnum) - wp->w_old_topfill;
        }
        if (i != 0 && i < wp->w_grid.rows - 2) {  // less than a screen off
          // Try to insert the correct number of lines.
          // If not the last window, delete the lines at the bottom.
          // win_ins_lines may fail when the terminal can't do it.
          win_scroll_lines(wp, 0, i);
          bot_scroll_start = 0;
          if (wp->w_lines_valid != 0) {
            // Need to update rows that are new, stop at the
            // first one that scrolled down.
            top_end = i;
            scrolled_down = true;

            // Move the entries that were scrolled, disable
            // the entries for the lines to be redrawn.
            if ((wp->w_lines_valid += (linenr_T)j) > wp->w_grid.rows) {
              wp->w_lines_valid = wp->w_grid.rows;
            }
            int idx;
            for (idx = wp->w_lines_valid; idx - j >= 0; idx--) {
              wp->w_lines[idx] = wp->w_lines[idx - j];
            }
            while (idx >= 0) {
              wp->w_lines[idx--].wl_valid = false;
            }
          }
        } else {
          mid_start = 0;  // redraw all lines
        }
      } else {
        mid_start = 0;  // redraw all lines
      }
    } else {
      // New topline is at or below old topline: May scroll up.
      // When topline didn't change, find first entry in w_lines[] that
      // needs updating.

      // try to find wp->w_topline in wp->w_lines[].wl_lnum
      long j = -1;
      int row = 0;
      for (int i = 0; i < wp->w_lines_valid; i++) {
        if (wp->w_lines[i].wl_valid
            && wp->w_lines[i].wl_lnum == wp->w_topline) {
          j = i;
          break;
        }
        row += wp->w_lines[i].wl_size;
      }
      if (j == -1) {
        // if wp->w_topline is not in wp->w_lines[].wl_lnum redraw all
        // lines
        mid_start = 0;
      } else {
        // Try to delete the correct number of lines.
        // wp->w_topline is at wp->w_lines[i].wl_lnum.

        // If the topline didn't change, delete old filler lines,
        // otherwise delete filler lines of the new topline...
        if (wp->w_lines[0].wl_lnum == wp->w_topline) {
          row += wp->w_old_topfill;
        } else {
          row += win_get_fill(wp, wp->w_topline);
        }
        // ... but don't delete new filler lines.
        row -= wp->w_topfill;
        if (row > 0) {
          win_scroll_lines(wp, 0, -row);
          bot_start = wp->w_grid.rows - row;
          bot_scroll_start = bot_start;
        }
        if ((row == 0 || bot_start < 999) && wp->w_lines_valid != 0) {
          // Skip the lines (below the deleted lines) that are still
          // valid and don't need redrawing.    Copy their info
          // upwards, to compensate for the deleted lines.  Set
          // bot_start to the first row that needs redrawing.
          bot_start = 0;
          int idx = 0;
          for (;;) {
            wp->w_lines[idx] = wp->w_lines[j];
            // stop at line that didn't fit, unless it is still
            // valid (no lines deleted)
            if (row > 0 && bot_start + row
                + (int)wp->w_lines[j].wl_size > wp->w_grid.rows) {
              wp->w_lines_valid = idx + 1;
              break;
            }
            bot_start += wp->w_lines[idx++].wl_size;

            // stop at the last valid entry in w_lines[].wl_size
            if (++j >= wp->w_lines_valid) {
              wp->w_lines_valid = idx;
              break;
            }
          }

          // Correct the first entry for filler lines at the top
          // when it won't get updated below.
          if (win_may_fill(wp) && bot_start > 0) {
            wp->w_lines[0].wl_size = (uint16_t)(plines_win_nofill(wp, wp->w_topline, true)
                                                + wp->w_topfill);
          }
        }
      }
    }

    // When starting redraw in the first line, redraw all lines.
    if (mid_start == 0) {
      mid_end = wp->w_grid.rows;
    }
  } else {
    // Not UPD_VALID or UPD_INVERTED: redraw all lines.
    mid_start = 0;
    mid_end = wp->w_grid.rows;
  }

  if (type == UPD_SOME_VALID) {
    // UPD_SOME_VALID: redraw all lines.
    mid_start = 0;
    mid_end = wp->w_grid.rows;
    type = UPD_NOT_VALID;
  }

  // check if we are updating or removing the inverted part
  if ((VIsual_active && buf == curwin->w_buffer)
      || (wp->w_old_cursor_lnum != 0 && type != UPD_NOT_VALID)) {
    linenr_T from, to;

    if (VIsual_active) {
      if (VIsual_mode != wp->w_old_visual_mode || type == UPD_INVERTED_ALL) {
        // If the type of Visual selection changed, redraw the whole
        // selection.  Also when the ownership of the X selection is
        // gained or lost.
        if (curwin->w_cursor.lnum < VIsual.lnum) {
          from = curwin->w_cursor.lnum;
          to = VIsual.lnum;
        } else {
          from = VIsual.lnum;
          to = curwin->w_cursor.lnum;
        }
        // redraw more when the cursor moved as well
        if (wp->w_old_cursor_lnum < from) {
          from = wp->w_old_cursor_lnum;
        }
        if (wp->w_old_cursor_lnum > to) {
          to = wp->w_old_cursor_lnum;
        }
        if (wp->w_old_visual_lnum < from) {
          from = wp->w_old_visual_lnum;
        }
        if (wp->w_old_visual_lnum > to) {
          to = wp->w_old_visual_lnum;
        }
      } else {
        // Find the line numbers that need to be updated: The lines
        // between the old cursor position and the current cursor
        // position.  Also check if the Visual position changed.
        if (curwin->w_cursor.lnum < wp->w_old_cursor_lnum) {
          from = curwin->w_cursor.lnum;
          to = wp->w_old_cursor_lnum;
        } else {
          from = wp->w_old_cursor_lnum;
          to = curwin->w_cursor.lnum;
          if (from == 0) {              // Visual mode just started
            from = to;
          }
        }

        if (VIsual.lnum != wp->w_old_visual_lnum
            || VIsual.col != wp->w_old_visual_col) {
          if (wp->w_old_visual_lnum < from
              && wp->w_old_visual_lnum != 0) {
            from = wp->w_old_visual_lnum;
          }
          if (wp->w_old_visual_lnum > to) {
            to = wp->w_old_visual_lnum;
          }
          if (VIsual.lnum < from) {
            from = VIsual.lnum;
          }
          if (VIsual.lnum > to) {
            to = VIsual.lnum;
          }
        }
      }

      // If in block mode and changed column or curwin->w_curswant:
      // update all lines.
      // First compute the actual start and end column.
      if (VIsual_mode == Ctrl_V) {
        colnr_T fromc, toc;
        unsigned int save_ve_flags = curwin->w_ve_flags;

        if (curwin->w_p_lbr) {
          curwin->w_ve_flags = VE_ALL;
        }

        getvcols(wp, &VIsual, &curwin->w_cursor, &fromc, &toc);
        toc++;
        curwin->w_ve_flags = save_ve_flags;
        // Highlight to the end of the line, unless 'virtualedit' has
        // "block".
        if (curwin->w_curswant == MAXCOL) {
          if (get_ve_flags() & VE_BLOCK) {
            pos_T pos;
            int cursor_above = curwin->w_cursor.lnum < VIsual.lnum;

            // Need to find the longest line.
            toc = 0;
            pos.coladd = 0;
            for (pos.lnum = curwin->w_cursor.lnum;
                 cursor_above ? pos.lnum <= VIsual.lnum : pos.lnum >= VIsual.lnum;
                 pos.lnum += cursor_above ? 1 : -1) {
              colnr_T t;

              pos.col = (colnr_T)strlen(ml_get_buf(wp->w_buffer, pos.lnum, false));
              getvvcol(wp, &pos, NULL, NULL, &t);
              if (toc < t) {
                toc = t;
              }
            }
            toc++;
          } else {
            toc = MAXCOL;
          }
        }

        if (fromc != wp->w_old_cursor_fcol
            || toc != wp->w_old_cursor_lcol) {
          if (from > VIsual.lnum) {
            from = VIsual.lnum;
          }
          if (to < VIsual.lnum) {
            to = VIsual.lnum;
          }
        }
        wp->w_old_cursor_fcol = fromc;
        wp->w_old_cursor_lcol = toc;
      }
    } else {
      // Use the line numbers of the old Visual area.
      if (wp->w_old_cursor_lnum < wp->w_old_visual_lnum) {
        from = wp->w_old_cursor_lnum;
        to = wp->w_old_visual_lnum;
      } else {
        from = wp->w_old_visual_lnum;
        to = wp->w_old_cursor_lnum;
      }
    }

    // There is no need to update lines above the top of the window.
    if (from < wp->w_topline) {
      from = wp->w_topline;
    }

    // If we know the value of w_botline, use it to restrict the update to
    // the lines that are visible in the window.
    if (wp->w_valid & VALID_BOTLINE) {
      if (from >= wp->w_botline) {
        from = wp->w_botline - 1;
      }
      if (to >= wp->w_botline) {
        to = wp->w_botline - 1;
      }
    }

    // Find the minimal part to be updated.
    // Watch out for scrolling that made entries in w_lines[] invalid.
    // E.g., CTRL-U makes the first half of w_lines[] invalid and sets
    // top_end; need to redraw from top_end to the "to" line.
    // A middle mouse click with a Visual selection may change the text
    // above the Visual area and reset wl_valid, do count these for
    // mid_end (in srow).
    if (mid_start > 0) {
      linenr_T lnum = wp->w_topline;
      int idx = 0;
      int srow = 0;
      if (scrolled_down) {
        mid_start = top_end;
      } else {
        mid_start = 0;
      }
      while (lnum < from && idx < wp->w_lines_valid) {          // find start
        if (wp->w_lines[idx].wl_valid) {
          mid_start += wp->w_lines[idx].wl_size;
        } else if (!scrolled_down) {
          srow += wp->w_lines[idx].wl_size;
        }
        idx++;
        if (idx < wp->w_lines_valid && wp->w_lines[idx].wl_valid) {
          lnum = wp->w_lines[idx].wl_lnum;
        } else {
          lnum++;
        }
      }
      srow += mid_start;
      mid_end = wp->w_grid.rows;
      for (; idx < wp->w_lines_valid; idx++) {                  // find end
        if (wp->w_lines[idx].wl_valid
            && wp->w_lines[idx].wl_lnum >= to + 1) {
          // Only update until first row of this line
          mid_end = srow;
          break;
        }
        srow += wp->w_lines[idx].wl_size;
      }
    }
  }

  if (VIsual_active && buf == curwin->w_buffer) {
    wp->w_old_visual_mode = (char)VIsual_mode;
    wp->w_old_cursor_lnum = curwin->w_cursor.lnum;
    wp->w_old_visual_lnum = VIsual.lnum;
    wp->w_old_visual_col = VIsual.col;
    wp->w_old_curswant = curwin->w_curswant;
  } else {
    wp->w_old_visual_mode = 0;
    wp->w_old_cursor_lnum = 0;
    wp->w_old_visual_lnum = 0;
    wp->w_old_visual_col = 0;
  }

  bool cursorline_standout = win_cursorline_standout(wp);

  win_check_ns_hl(wp);

  // Update all the window rows.
  int idx = 0;                    // first entry in w_lines[].wl_size
  int row = 0;                    // current window row to display
  int srow = 0;                   // starting row of the current line
  linenr_T lnum = wp->w_topline;  // first line shown in window

  bool eof = false;             // if true, we hit the end of the file
  bool didline = false;         // if true, we finished the last line
  for (;;) {
    // stop updating when reached the end of the window (check for _past_
    // the end of the window is at the end of the loop)
    if (row == wp->w_grid.rows) {
      didline = true;
      break;
    }

    // stop updating when hit the end of the file
    if (lnum > buf->b_ml.ml_line_count) {
      eof = true;
      break;
    }

    // Remember the starting row of the line that is going to be dealt
    // with.  It is used further down when the line doesn't fit.
    srow = row;

    // Update a line when it is in an area that needs updating, when it
    // has changes or w_lines[idx] is invalid.
    // "bot_start" may be halfway a wrapped line after using
    // win_scroll_lines(), check if the current line includes it.
    // When syntax folding is being used, the saved syntax states will
    // already have been updated, we can't see where the syntax state is
    // the same again, just update until the end of the window.
    if (row < top_end
        || (row >= mid_start && row < mid_end)
        || top_to_mod
        || idx >= wp->w_lines_valid
        || (row + wp->w_lines[idx].wl_size > bot_start)
        || (mod_top != 0
            && (lnum == mod_top
                || (lnum >= mod_top
                    && (lnum < mod_bot
                        || did_update == DID_FOLD
                        || (did_update == DID_LINE
                            && syntax_present(wp)
                            && ((foldmethodIsSyntax(wp)
                                 && hasAnyFolding(wp))
                                || syntax_check_changed(lnum)))
                        // match in fixed position might need redraw
                        // if lines were inserted or deleted
                        || (wp->w_match_head != NULL
                            && buf->b_mod_xlines != 0)))))
        || (cursorline_standout && lnum == wp->w_cursor.lnum)
        || lnum == wp->w_last_cursorline) {
      if (lnum == mod_top) {
        top_to_mod = false;
      }

      // When at start of changed lines: May scroll following lines
      // up or down to minimize redrawing.
      // Don't do this when the change continues until the end.
      // Don't scroll when dollar_vcol >= 0, keep the "$".
      // Don't scroll when redrawing the top, scrolled already above.
      if (lnum == mod_top
          && mod_bot != MAXLNUM
          && !(dollar_vcol >= 0 && mod_bot == mod_top + 1)
          && row >= top_end) {
        int old_rows = 0;
        int new_rows = 0;
        int xtra_rows;
        linenr_T l;
        int i;

        // Count the old number of window rows, using w_lines[], which
        // should still contain the sizes for the lines as they are
        // currently displayed.
        for (i = idx; i < wp->w_lines_valid; i++) {
          // Only valid lines have a meaningful wl_lnum.  Invalid
          // lines are part of the changed area.
          if (wp->w_lines[i].wl_valid
              && wp->w_lines[i].wl_lnum == mod_bot) {
            break;
          }
          old_rows += wp->w_lines[i].wl_size;
          if (wp->w_lines[i].wl_valid
              && wp->w_lines[i].wl_lastlnum + 1 == mod_bot) {
            // Must have found the last valid entry above mod_bot.
            // Add following invalid entries.
            i++;
            while (i < wp->w_lines_valid
                   && !wp->w_lines[i].wl_valid) {
              old_rows += wp->w_lines[i++].wl_size;
            }
            break;
          }
        }

        if (i >= wp->w_lines_valid) {
          // We can't find a valid line below the changed lines,
          // need to redraw until the end of the window.
          // Inserting/deleting lines has no use.
          bot_start = 0;
          bot_scroll_start = 0;
        } else {
          // Able to count old number of rows: Count new window
          // rows, and may insert/delete lines
          long j = idx;
          for (l = lnum; l < mod_bot; l++) {
            if (hasFoldingWin(wp, l, NULL, &l, true, NULL)) {
              new_rows++;
            } else if (l == wp->w_topline) {
              new_rows += plines_win_nofill(wp, l, true) + wp->w_topfill;
            } else {
              new_rows += plines_win(wp, l, true);
            }
            j++;
            if (new_rows > wp->w_grid.rows - row - 2) {
              // it's getting too much, must redraw the rest
              new_rows = 9999;
              break;
            }
          }
          xtra_rows = new_rows - old_rows;
          if (xtra_rows < 0) {
            // May scroll text up.  If there is not enough
            // remaining text or scrolling fails, must redraw the
            // rest.  If scrolling works, must redraw the text
            // below the scrolled text.
            if (row - xtra_rows >= wp->w_grid.rows - 2) {
              mod_bot = MAXLNUM;
            } else {
              win_scroll_lines(wp, row, xtra_rows);
              bot_start = wp->w_grid.rows + xtra_rows;
              bot_scroll_start = bot_start;
            }
          } else if (xtra_rows > 0) {
            // May scroll text down.  If there is not enough
            // remaining text of scrolling fails, must redraw the
            // rest.
            if (row + xtra_rows >= wp->w_grid.rows - 2) {
              mod_bot = MAXLNUM;
            } else {
              win_scroll_lines(wp, row + old_rows, xtra_rows);
              bot_scroll_start = 0;
              if (top_end > row + old_rows) {
                // Scrolled the part at the top that requires
                // updating down.
                top_end += xtra_rows;
              }
            }
          }

          // When not updating the rest, may need to move w_lines[]
          // entries.
          if (mod_bot != MAXLNUM && i != j) {
            if (j < i) {
              int x = row + new_rows;

              // move entries in w_lines[] upwards
              for (;;) {
                // stop at last valid entry in w_lines[]
                if (i >= wp->w_lines_valid) {
                  wp->w_lines_valid = (int)j;
                  break;
                }
                wp->w_lines[j] = wp->w_lines[i];
                // stop at a line that won't fit
                if (x + (int)wp->w_lines[j].wl_size
                    > wp->w_grid.rows) {
                  wp->w_lines_valid = (int)j + 1;
                  break;
                }
                x += wp->w_lines[j++].wl_size;
                i++;
              }
              if (bot_start > x) {
                bot_start = x;
              }
            } else {       // j > i
                           // move entries in w_lines[] downwards
              j -= i;
              wp->w_lines_valid += (linenr_T)j;
              if (wp->w_lines_valid > wp->w_grid.rows) {
                wp->w_lines_valid = wp->w_grid.rows;
              }
              for (i = wp->w_lines_valid; i - j >= idx; i--) {
                wp->w_lines[i] = wp->w_lines[i - j];
              }

              // The w_lines[] entries for inserted lines are
              // now invalid, but wl_size may be used above.
              // Reset to zero.
              while (i >= idx) {
                wp->w_lines[i].wl_size = 0;
                wp->w_lines[i--].wl_valid = false;
              }
            }
          }
        }
      }

      // When lines are folded, display one line for all of them.
      // Otherwise, display normally (can be several display lines when
      // 'wrap' is on).
      foldinfo_T foldinfo = fold_info(wp, lnum);

      if (foldinfo.fi_lines == 0
          && idx < wp->w_lines_valid
          && wp->w_lines[idx].wl_valid
          && wp->w_lines[idx].wl_lnum == lnum
          && lnum > wp->w_topline
          && !(dy_flags & (DY_LASTLINE | DY_TRUNCATE))
          && srow + wp->w_lines[idx].wl_size > wp->w_grid.rows
          && win_get_fill(wp, lnum) == 0) {
        // This line is not going to fit.  Don't draw anything here,
        // will draw "@  " lines below.
        row = wp->w_grid.rows + 1;
      } else {
        prepare_search_hl(wp, &screen_search_hl, lnum);
        // Let the syntax stuff know we skipped a few lines.
        if (syntax_last_parsed != 0 && syntax_last_parsed + 1 < lnum
            && syntax_present(wp)) {
          syntax_end_parsing(wp, syntax_last_parsed + 1);
        }

        // Display one line
        row = win_line(wp, lnum, srow,
                       foldinfo.fi_lines ? srow : wp->w_grid.rows,
                       mod_top == 0, false, foldinfo, &line_providers, &provider_err);

        if (foldinfo.fi_lines == 0) {
          wp->w_lines[idx].wl_folded = false;
          wp->w_lines[idx].wl_lastlnum = lnum;
          did_update = DID_LINE;
          syntax_last_parsed = lnum;
        } else {
          foldinfo.fi_lines--;
          wp->w_lines[idx].wl_folded = true;
          wp->w_lines[idx].wl_lastlnum = lnum + foldinfo.fi_lines;
          did_update = DID_FOLD;
        }
      }

      wp->w_lines[idx].wl_lnum = lnum;
      wp->w_lines[idx].wl_valid = true;

      if (row > wp->w_grid.rows) {         // past end of grid
        // we may need the size of that too long line later on
        if (dollar_vcol == -1) {
          wp->w_lines[idx].wl_size = (uint16_t)plines_win(wp, lnum, true);
        }
        idx++;
        break;
      }
      if (dollar_vcol == -1) {
        wp->w_lines[idx].wl_size = (uint16_t)(row - srow);
      }
      idx++;
      lnum += foldinfo.fi_lines + 1;
    } else {
      if (wp->w_p_rnu && wp->w_last_cursor_lnum_rnu != wp->w_cursor.lnum) {
        // 'relativenumber' set and cursor moved vertically: The
        // text doesn't need to be drawn, but the number column does.
        foldinfo_T info = fold_info(wp, lnum);
        (void)win_line(wp, lnum, srow, wp->w_grid.rows, true, true,
                       info, &line_providers, &provider_err);
      }

      // This line does not need to be drawn, advance to the next one.
      row += wp->w_lines[idx++].wl_size;
      if (row > wp->w_grid.rows) {  // past end of screen
        break;
      }
      lnum = wp->w_lines[idx - 1].wl_lastlnum + 1;
      did_update = DID_NONE;
    }

    if (lnum > buf->b_ml.ml_line_count) {
      eof = true;
      break;
    }
  }
  // End of loop over all window lines.

  // Now that the window has been redrawn with the old and new cursor line,
  // update w_last_cursorline.
  wp->w_last_cursorline = cursorline_standout ? wp->w_cursor.lnum : 0;

  wp->w_last_cursor_lnum_rnu = wp->w_p_rnu ? wp->w_cursor.lnum : 0;

  if (idx > wp->w_lines_valid) {
    wp->w_lines_valid = idx;
  }

  // Let the syntax stuff know we stop parsing here.
  if (syntax_last_parsed != 0 && syntax_present(wp)) {
    syntax_end_parsing(wp, syntax_last_parsed + 1);
  }

  const linenr_T old_botline = wp->w_botline;

  // If we didn't hit the end of the file, and we didn't finish the last
  // line we were working on, then the line didn't fit.
  wp->w_empty_rows = 0;
  wp->w_filler_rows = 0;
  if (!eof && !didline) {
    int at_attr = hl_combine_attr(win_bg_attr(wp), win_hl_attr(wp, HLF_AT));
    if (lnum == wp->w_topline) {
      // Single line that does not fit!
      // Don't overwrite it, it can be edited.
      wp->w_botline = lnum + 1;
    } else if (win_get_fill(wp, lnum) >= wp->w_grid.rows - srow) {
      // Window ends in filler lines.
      wp->w_botline = lnum;
      wp->w_filler_rows = wp->w_grid.rows - srow;
    } else if (dy_flags & DY_TRUNCATE) {      // 'display' has "truncate"
      int scr_row = wp->w_grid.rows - 1;
      int symbol = wp->w_p_fcs_chars.lastline;
      char fillbuf[12];  // 2 characters of 6 bytes
      int charlen = utf_char2bytes(symbol, &fillbuf[0]);
      utf_char2bytes(symbol, &fillbuf[charlen]);

      // Last line isn't finished: Display "@@@" in the last screen line.
      grid_puts_len(&wp->w_grid, fillbuf, MIN(wp->w_grid.cols, 2) * charlen, scr_row, 0, at_attr);
      grid_fill(&wp->w_grid, scr_row, scr_row + 1, 2, wp->w_grid.cols, symbol, ' ', at_attr);
      set_empty_rows(wp, srow);
      wp->w_botline = lnum;
    } else if (dy_flags & DY_LASTLINE) {      // 'display' has "lastline"
      int start_col = wp->w_grid.cols - 3;
      int symbol = wp->w_p_fcs_chars.lastline;

      // Last line isn't finished: Display "@@@" at the end.
      grid_fill(&wp->w_grid, wp->w_grid.rows - 1, wp->w_grid.rows,
                MAX(start_col, 0), wp->w_grid.cols, symbol, symbol, at_attr);
      set_empty_rows(wp, srow);
      wp->w_botline = lnum;
    } else {
      win_draw_end(wp, wp->w_p_fcs_chars.lastline, ' ', true, srow, wp->w_grid.rows, HLF_AT);
      set_empty_rows(wp, srow);
      wp->w_botline = lnum;
    }
  } else {
    if (eof) {  // we hit the end of the file
      wp->w_botline = buf->b_ml.ml_line_count + 1;
      long j = win_get_fill(wp, wp->w_botline);
      if (j > 0 && !wp->w_botfill && row < wp->w_grid.rows) {
        // Display filler text below last line. win_line() will check
        // for ml_line_count+1 and only draw filler lines
        foldinfo_T info = FOLDINFO_INIT;
        row = win_line(wp, wp->w_botline, row, wp->w_grid.rows,
                       false, false, info, &line_providers, &provider_err);
      }
    } else if (dollar_vcol == -1) {
      wp->w_botline = lnum;
    }

    // Make sure the rest of the screen is blank.
    // write the "eob" character from 'fillchars' to rows that aren't part
    // of the file.
    // TODO(bfredl): just keep track of the valid EOB area from last redraw?
    int lastline = bot_scroll_start;
    if (mid_end >= row) {
      lastline = MIN(lastline, mid_start);
    }
    // if (mod_bot > buf->b_ml.ml_line_count + 1) {
    if (mod_bot > buf->b_ml.ml_line_count) {
      lastline = 0;
    }

    win_draw_end(wp, wp->w_p_fcs_chars.eob, ' ', false, MAX(lastline, row), wp->w_grid.rows,
                 HLF_EOB);
    set_empty_rows(wp, row);
  }

  kvi_destroy(line_providers);

  if (wp->w_redr_type >= UPD_REDRAW_TOP) {
    draw_vsep_win(wp);
    draw_hsep_win(wp);
    draw_sep_connectors_win(wp);
  }
  syn_set_timeout(NULL);

  // Reset the type of redrawing required, the window has been updated.
  wp->w_redr_type = 0;
  wp->w_old_topfill = wp->w_topfill;
  wp->w_old_botfill = wp->w_botfill;

  // Send win_extmarks if needed
  for (size_t n = 0; n < kv_size(win_extmark_arr); n++) {
    ui_call_win_extmark(wp->w_grid_alloc.handle, wp->handle,
                        kv_A(win_extmark_arr, n).ns_id, (Integer)kv_A(win_extmark_arr, n).mark_id,
                        kv_A(win_extmark_arr, n).win_row, kv_A(win_extmark_arr, n).win_col);
  }

  if (dollar_vcol == -1) {
    // There is a trick with w_botline.  If we invalidate it on each
    // change that might modify it, this will cause a lot of expensive
    // calls to plines_win() in update_topline() each time.  Therefore the
    // value of w_botline is often approximated, and this value is used to
    // compute the value of w_topline.  If the value of w_botline was
    // wrong, check that the value of w_topline is correct (cursor is on
    // the visible part of the text).  If it's not, we need to redraw
    // again.  Mostly this just means scrolling up a few lines, so it
    // doesn't look too bad.  Only do this for the current window (where
    // changes are relevant).
    wp->w_valid |= VALID_BOTLINE;
    wp->w_viewport_invalid = true;
    if (wp == curwin && wp->w_botline != old_botline && !recursive) {
      recursive = true;
      curwin->w_valid &= ~VALID_TOPLINE;
      update_topline(curwin);  // may invalidate w_botline again
      if (must_redraw != 0) {
        // Don't update for changes in buffer again.
        int mod_set = curbuf->b_mod_set;
        curbuf->b_mod_set = false;
        win_update(curwin, providers);
        must_redraw = 0;
        curbuf->b_mod_set = mod_set;
      }
      recursive = false;
    }
  }

  // restore got_int, unless CTRL-C was hit while redrawing
  if (!got_int) {
    got_int = save_got_int;
  }
}

/// Redraw a window later, with wp->w_redr_type >= type.
///
/// Set must_redraw only if not already set to a higher value.
/// e.g. if must_redraw is UPD_CLEAR, type UPD_NOT_VALID will do nothing.
void redraw_later(win_T *wp, int type)
  FUNC_ATTR_NONNULL_ALL
{
  if (!exiting && wp->w_redr_type < type) {
    wp->w_redr_type = type;
    if (type >= UPD_NOT_VALID) {
      wp->w_lines_valid = 0;
    }
    if (must_redraw < type) {   // must_redraw is the maximum of all windows
      must_redraw = type;
    }
  }
}

/// Mark all windows to be redrawn later.
void redraw_all_later(int type)
{
  FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
    redraw_later(wp, type);
  }
  // This may be needed when switching tabs.
  if (must_redraw < type) {
    must_redraw = type;
  }
}

void screen_invalidate_highlights(void)
{
  FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
    redraw_later(wp, UPD_NOT_VALID);
    wp->w_grid_alloc.valid = false;
  }
}

/// Mark all windows that are editing the current buffer to be updated later.
void redraw_curbuf_later(int type)
{
  redraw_buf_later(curbuf, type);
}

void redraw_buf_later(buf_T *buf, int type)
{
  FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
    if (wp->w_buffer == buf) {
      redraw_later(wp, type);
    }
  }
}

void redraw_buf_line_later(buf_T *buf, linenr_T line, bool force)
{
  FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
    if (wp->w_buffer == buf) {
      redrawWinline(wp, MIN(line, buf->b_ml.ml_line_count));
      if (force && line > buf->b_ml.ml_line_count) {
        wp->w_redraw_bot = line;
      }
    }
  }
}

void redraw_buf_range_later(buf_T *buf,  linenr_T firstline, linenr_T lastline)
{
  FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
    if (wp->w_buffer == buf
        && lastline >= wp->w_topline && firstline < wp->w_botline) {
      if (wp->w_redraw_top == 0 || wp->w_redraw_top > firstline) {
        wp->w_redraw_top = firstline;
      }
      if (wp->w_redraw_bot == 0 || wp->w_redraw_bot < lastline) {
        wp->w_redraw_bot = lastline;
      }
      redraw_later(wp, UPD_VALID);
    }
  }
}

/// called when the status bars for the buffer 'buf' need to be updated
void redraw_buf_status_later(buf_T *buf)
{
  FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
    if (wp->w_buffer == buf
        && (wp->w_status_height
            || (wp == curwin && global_stl_height())
            || wp->w_winbar_height)) {
      wp->w_redr_status = true;
      if (must_redraw < UPD_VALID) {
        must_redraw = UPD_VALID;
      }
    }
  }
}

/// Mark all status lines and window bars for redraw; used after first :cd
void status_redraw_all(void)
{
  bool is_stl_global = global_stl_height() != 0;

  FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
    if ((!is_stl_global && wp->w_status_height) || (is_stl_global && wp == curwin)
        || wp->w_winbar_height) {
      wp->w_redr_status = true;
      redraw_later(wp, UPD_VALID);
    }
  }
}

/// Marks all status lines and window bars of the current buffer for redraw.
void status_redraw_curbuf(void)
{
  status_redraw_buf(curbuf);
}

/// Marks all status lines and window bars of the given buffer for redraw.
void status_redraw_buf(buf_T *buf)
{
  bool is_stl_global = global_stl_height() != 0;

  FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
    if (wp->w_buffer == buf && ((!is_stl_global && wp->w_status_height)
                                || (is_stl_global && wp == curwin) || wp->w_winbar_height)) {
      wp->w_redr_status = true;
      redraw_later(wp, UPD_VALID);
    }
  }
}

/// Redraw all status lines that need to be redrawn.
void redraw_statuslines(void)
{
  FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
    if (wp->w_redr_status) {
      win_check_ns_hl(wp);
      win_redr_winbar(wp);
      win_redr_status(wp);
    }
  }

  win_check_ns_hl(NULL);
  if (redraw_tabline) {
    draw_tabline();
  }
}

/// Redraw all status lines at the bottom of frame "frp".
void win_redraw_last_status(const frame_T *frp)
  FUNC_ATTR_NONNULL_ARG(1)
{
  if (frp->fr_layout == FR_LEAF) {
    frp->fr_win->w_redr_status = true;
  } else if (frp->fr_layout == FR_ROW) {
    FOR_ALL_FRAMES(frp, frp->fr_child) {
      win_redraw_last_status(frp);
    }
  } else {
    assert(frp->fr_layout == FR_COL);
    frp = frp->fr_child;
    while (frp->fr_next != NULL) {
      frp = frp->fr_next;
    }
    win_redraw_last_status(frp);
  }
}

/// Changed something in the current window, at buffer line "lnum", that
/// requires that line and possibly other lines to be redrawn.
/// Used when entering/leaving Insert mode with the cursor on a folded line.
/// Used to remove the "$" from a change command.
/// Note that when also inserting/deleting lines w_redraw_top and w_redraw_bot
/// may become invalid and the whole window will have to be redrawn.
void redrawWinline(win_T *wp, linenr_T lnum)
  FUNC_ATTR_NONNULL_ALL
{
  if (lnum >= wp->w_topline
      && lnum < wp->w_botline) {
    if (wp->w_redraw_top == 0 || wp->w_redraw_top > lnum) {
      wp->w_redraw_top = lnum;
    }
    if (wp->w_redraw_bot == 0 || wp->w_redraw_bot < lnum) {
      wp->w_redraw_bot = lnum;
    }
    redraw_later(wp, UPD_VALID);
  }
}
