// move.c: Functions for moving the cursor and scrolling text.
//
// There are two ways to move the cursor:
// 1. Move the cursor directly, the text is scrolled to keep the cursor in the
//    window.
// 2. Scroll the text, the cursor is moved into the text visible in the
//    window.
// The 'scrolloff' option makes this a bit complicated.

#include <assert.h>
#include <limits.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>

#include "nvim/ascii_defs.h"
#include "nvim/buffer.h"
#include "nvim/buffer_defs.h"
#include "nvim/cursor.h"
#include "nvim/decoration.h"
#include "nvim/diff.h"
#include "nvim/drawscreen.h"
#include "nvim/edit.h"
#include "nvim/errors.h"
#include "nvim/eval/typval.h"
#include "nvim/eval/window.h"
#include "nvim/fold.h"
#include "nvim/getchar.h"
#include "nvim/gettext_defs.h"
#include "nvim/globals.h"
#include "nvim/grid.h"
#include "nvim/grid_defs.h"
#include "nvim/macros_defs.h"
#include "nvim/mark_defs.h"
#include "nvim/mbyte.h"
#include "nvim/memline.h"
#include "nvim/message.h"
#include "nvim/mouse.h"
#include "nvim/move.h"
#include "nvim/normal.h"
#include "nvim/normal_defs.h"
#include "nvim/option.h"
#include "nvim/option_vars.h"
#include "nvim/plines.h"
#include "nvim/pos_defs.h"
#include "nvim/strings.h"
#include "nvim/types_defs.h"
#include "nvim/vim_defs.h"
#include "nvim/window.h"
#include "nvim/winfloat.h"

typedef struct {
  linenr_T lnum;                // line number
  int fill;                     // filler lines
  int height;                   // height of added line
} lineoff_T;

#include "move.c.generated.h"

/// Get the number of screen lines skipped with "wp->w_skipcol".
static int adjust_plines_for_skipcol(win_T *wp)
{
  if (wp->w_skipcol == 0) {
    return 0;
  }

  int width = wp->w_view_width - win_col_off(wp);
  int w2 = width + win_col_off2(wp);
  if (wp->w_skipcol >= width && w2 > 0) {
    return (wp->w_skipcol - width) / w2 + 1;
  }

  return 0;
}

/// Return how many lines "lnum" will take on the screen, taking into account
/// whether it is the first line, whether w_skipcol is non-zero and limiting to
/// the window height.
int plines_correct_topline(win_T *wp, linenr_T lnum, linenr_T *nextp, bool limit_winheight,
                           bool *foldedp)
{
  int n = plines_win_full(wp, lnum, nextp, foldedp, true, false);
  if (lnum == wp->w_topline) {
    n -= adjust_plines_for_skipcol(wp);
  }
  if (limit_winheight && n > wp->w_view_height) {
    return wp->w_view_height;
  }
  return n;
}

// Compute wp->w_botline for the current wp->w_topline.  Can be called after
// wp->w_topline changed.
static void comp_botline(win_T *wp)
{
  linenr_T lnum;
  int done;

  // If w_cline_row is valid, start there.
  // Otherwise have to start at w_topline.
  check_cursor_moved(wp);
  if (wp->w_valid & VALID_CROW) {
    lnum = wp->w_cursor.lnum;
    done = wp->w_cline_row;
  } else {
    lnum = wp->w_topline;
    done = 0;
  }

  for (; lnum <= wp->w_buffer->b_ml.ml_line_count; lnum++) {
    linenr_T last = lnum;
    bool folded;
    int n = plines_correct_topline(wp, lnum, &last, true, &folded);
    if (lnum <= wp->w_cursor.lnum && last >= wp->w_cursor.lnum) {
      wp->w_cline_row = done;
      wp->w_cline_height = n;
      wp->w_cline_folded = folded;
      redraw_for_cursorline(wp);
      wp->w_valid |= (VALID_CROW|VALID_CHEIGHT);
    }
    if (done + n > wp->w_view_height) {
      break;
    }
    done += n;
    lnum = last;
  }

  // wp->w_botline is the line that is just below the window
  wp->w_botline = lnum;
  wp->w_valid |= VALID_BOTLINE|VALID_BOTLINE_AP;
  wp->w_viewport_invalid = true;

  set_empty_rows(wp, done);

  win_check_anchored_floats(wp);
}

/// Redraw when w_cline_row changes and 'relativenumber' or 'cursorline' is set.
/// Also when concealing is on and 'concealcursor' is not active.
static void redraw_for_cursorline(win_T *wp)
  FUNC_ATTR_NONNULL_ALL
{
  if (wp->w_valid & VALID_CROW) {
    return;
  }

  if (wp->w_p_rnu || win_cursorline_standout(wp)) {
    // win_line() will redraw the number column and cursorline only.
    redraw_later(wp, UPD_VALID);
  }
}

/// Redraw when 'concealcursor' is active, or when w_virtcol changes and:
/// - 'cursorcolumn' is set, or
/// - 'cursorlineopt' contains "screenline", or
/// - Visual mode is active.
static void redraw_for_cursorcolumn(win_T *wp)
  FUNC_ATTR_NONNULL_ALL
{
  // If the cursor moves horizontally when 'concealcursor' is active, then the
  // current line needs to be redrawn to calculate the correct cursor position.
  if (wp == curwin && wp->w_p_cole > 0 && conceal_cursor_line(wp)) {
    redrawWinline(wp, wp->w_cursor.lnum);
  }

  if (wp->w_valid & VALID_VIRTCOL) {
    return;
  }

  if (wp->w_p_cuc) {
    // When 'cursorcolumn' is set need to redraw with UPD_SOME_VALID.
    redraw_later(wp, UPD_SOME_VALID);
  } else if (wp->w_p_cul && (wp->w_p_culopt_flags & kOptCuloptFlagScreenline)) {
    // When 'cursorlineopt' contains "screenline" need to redraw with UPD_VALID.
    redraw_later(wp, UPD_VALID);
  }

  // When current buffer's cursor moves in Visual mode, redraw it with UPD_INVERTED.
  if (VIsual_active && wp->w_buffer == curbuf) {
    redraw_buf_later(curbuf, UPD_INVERTED);
  }
}

/// Set wp->w_virtcol to a value ("vcol") that is already valid.
/// Handles redrawing if wp->w_virtcol was previously invalid.
void set_valid_virtcol(win_T *wp, colnr_T vcol)
{
  wp->w_virtcol = vcol;
  redraw_for_cursorcolumn(wp);
  wp->w_valid |= VALID_VIRTCOL;
}

/// Calculates how much the 'listchars' "precedes" or 'smoothscroll' "<<<"
/// marker overlaps with buffer text for window "wp".
/// Parameter "extra2" should be the padding on the 2nd line, not the first
/// line. When "extra2" is -1 calculate the padding.
/// Returns the number of columns of overlap with buffer text, excluding the
/// extra padding on the ledge.
int sms_marker_overlap(win_T *wp, int extra2)
{
  if (extra2 == -1) {
    extra2 = win_col_off(wp) - win_col_off2(wp);
  }
  // There is no marker overlap when in showbreak mode, thus no need to
  // account for it.  See wlv_put_linebuf().
  if (*get_showbreak_value(wp) != NUL) {
    return 0;
  }

  // Overlap when 'list' and 'listchars' "precedes" are set is 1.
  if (wp->w_p_list && wp->w_p_lcs_chars.prec) {
    return 1;
  }

  return extra2 > 3 ? 0 : 3 - extra2;
}

/// Calculates the skipcol offset for window "wp" given how many
/// physical lines we want to scroll down.
static int skipcol_from_plines(win_T *wp, int plines_off)
{
  int width1 = wp->w_view_width - win_col_off(wp);

  int skipcol = 0;
  if (plines_off > 0) {
    skipcol += width1;
  }
  if (plines_off > 1) {
    skipcol += (width1 + win_col_off2(wp)) * (plines_off - 1);
  }
  return skipcol;
}

/// Set wp->w_skipcol to zero and redraw later if needed.
static void reset_skipcol(win_T *wp)
{
  if (wp->w_skipcol == 0) {
    return;
  }

  wp->w_skipcol = 0;

  // Should use the least expensive way that displays all that changed.
  // UPD_NOT_VALID is too expensive, UPD_REDRAW_TOP does not redraw
  // enough when the top line gets another screen line.
  redraw_later(wp, UPD_SOME_VALID);
}

// Update wp->w_topline to move the cursor onto the screen.
void update_topline(win_T *wp)
{
  bool check_botline = false;
  OptInt *so_ptr = wp->w_p_so >= 0 ? &wp->w_p_so : &p_so;
  OptInt save_so = *so_ptr;

  // Cursor is updated instead when this is true for 'splitkeep'.
  if (skip_update_topline) {
    return;
  }

  // If there is no valid screen and when the window height is zero just use
  // the cursor line.
  if (!default_grid.chars || wp->w_view_height == 0) {
    check_cursor_lnum(wp);
    wp->w_topline = wp->w_cursor.lnum;
    wp->w_botline = wp->w_topline;
    wp->w_viewport_invalid = true;
    wp->w_scbind_pos = 1;
    return;
  }

  check_cursor_moved(wp);
  if (wp->w_valid & VALID_TOPLINE) {
    return;
  }

  // When dragging with the mouse, don't scroll that quickly
  if (mouse_dragging > 0) {
    *so_ptr = mouse_dragging - 1;
  }

  linenr_T old_topline = wp->w_topline;
  int old_topfill = wp->w_topfill;

  // If the buffer is empty, always set topline to 1.
  if (buf_is_empty(wp->w_buffer)) {  // special case - file is empty
    if (wp->w_topline != 1) {
      redraw_later(wp, UPD_NOT_VALID);
    }
    wp->w_topline = 1;
    wp->w_botline = 2;
    wp->w_skipcol = 0;
    wp->w_valid |= VALID_BOTLINE|VALID_BOTLINE_AP;
    wp->w_viewport_invalid = true;
    wp->w_scbind_pos = 1;
  } else {
    bool check_topline = false;
    // If the cursor is above or near the top of the window, scroll the window
    // to show the line the cursor is in, with 'scrolloff' context.
    if (wp->w_topline > 1 || wp->w_skipcol > 0) {
      // If the cursor is above topline, scrolling is always needed.
      // If the cursor is far below topline and there is no folding,
      // scrolling down is never needed.
      if (wp->w_cursor.lnum < wp->w_topline) {
        check_topline = true;
      } else if (check_top_offset(wp)) {
        check_topline = true;
      } else if (wp->w_skipcol > 0 && wp->w_cursor.lnum == wp->w_topline) {
        colnr_T vcol;

        // Check that the cursor position is visible.  Add columns for
        // the marker displayed in the top-left if needed.
        getvvcol(wp, &wp->w_cursor, &vcol, NULL, NULL);
        int overlap = sms_marker_overlap(wp, -1);
        if (wp->w_skipcol + overlap > vcol) {
          check_topline = true;
        }
      }
    }
    // Check if there are more filler lines than allowed.
    if (!check_topline && wp->w_topfill > win_get_fill(wp, wp->w_topline)) {
      check_topline = true;
    }

    if (check_topline) {
      int halfheight = wp->w_view_height / 2 - 1;
      if (halfheight < 2) {
        halfheight = 2;
      }
      int64_t n;
      if (win_lines_concealed(wp)) {
        // Count the number of logical lines between the cursor and
        // topline + p_so (approximation of how much will be
        // scrolled).
        n = 0;
        for (linenr_T lnum = wp->w_cursor.lnum; lnum < wp->w_topline + *so_ptr; lnum++) {
          // stop at end of file or when we know we are far off
          assert(wp->w_buffer != 0);
          if (lnum >= wp->w_buffer->b_ml.ml_line_count
              || (n += !decor_conceal_line(wp, lnum, false)) >= halfheight) {
            break;
          }
          hasFolding(wp, lnum, NULL, &lnum);
        }
      } else {
        n = wp->w_topline + *so_ptr - wp->w_cursor.lnum;
      }

      // If we weren't very close to begin with, we scroll to put the
      // cursor in the middle of the window.  Otherwise put the cursor
      // near the top of the window.
      if (n >= halfheight) {
        scroll_cursor_halfway(wp, false, false);
      } else {
        scroll_cursor_top(wp, scrolljump_value(wp), false);
        check_botline = true;
      }
    } else {
      // Make sure topline is the first line of a fold.
      hasFolding(wp, wp->w_topline, &wp->w_topline, NULL);
      check_botline = true;
    }
  }

  // If the cursor is below the bottom of the window, scroll the window
  // to put the cursor on the window.
  // When w_botline is invalid, recompute it first, to avoid a redraw later.
  // If w_botline was approximated, we might need a redraw later in a few
  // cases, but we don't want to spend (a lot of) time recomputing w_botline
  // for every small change.
  if (check_botline) {
    if (!(wp->w_valid & VALID_BOTLINE_AP)) {
      validate_botline_win(wp);
    }

    assert(wp->w_buffer != 0);
    if (wp->w_botline <= wp->w_buffer->b_ml.ml_line_count) {
      if (wp->w_cursor.lnum < wp->w_botline) {
        if ((wp->w_cursor.lnum >= wp->w_botline - *so_ptr || win_lines_concealed(wp))) {
          lineoff_T loff;

          // Cursor is (a few lines) above botline, check if there are
          // 'scrolloff' window lines below the cursor.  If not, need to
          // scroll.
          int n = wp->w_empty_rows;
          loff.lnum = wp->w_cursor.lnum;
          // In a fold go to its last line.
          hasFolding(wp, loff.lnum, NULL, &loff.lnum);
          loff.fill = 0;
          n += wp->w_filler_rows;
          loff.height = 0;
          while (loff.lnum < wp->w_botline
                 && (loff.lnum + 1 < wp->w_botline || loff.fill == 0)) {
            n += loff.height;
            if (n >= *so_ptr) {
              break;
            }
            botline_forw(wp, &loff);
          }
          if (n >= *so_ptr) {
            // sufficient context, no need to scroll
            check_botline = false;
          }
        } else {
          // sufficient context, no need to scroll
          check_botline = false;
        }
      }
      if (check_botline) {
        int64_t n = 0;
        if (win_lines_concealed(wp)) {
          // Count the number of logical lines between the cursor and
          // botline - p_so (approximation of how much will be
          // scrolled).
          for (linenr_T lnum = wp->w_cursor.lnum; lnum >= wp->w_botline - *so_ptr; lnum--) {
            // stop at end of file or when we know we are far off
            if (lnum <= 0
                || (n += !decor_conceal_line(wp, lnum - 1, false)) > wp->w_view_height + 1) {
              break;
            }
            hasFolding(wp, lnum, &lnum, NULL);
          }
        } else {
          n = wp->w_cursor.lnum - wp->w_botline + 1 + *so_ptr;
        }
        if (n <= wp->w_view_height + 1) {
          scroll_cursor_bot(wp, scrolljump_value(wp), false);
        } else {
          scroll_cursor_halfway(wp, false, false);
        }
      }
    }
  }
  wp->w_valid |= VALID_TOPLINE;
  wp->w_viewport_invalid = true;
  win_check_anchored_floats(wp);

  // Need to redraw when topline changed.
  if (wp->w_topline != old_topline
      || wp->w_topfill != old_topfill) {
    dollar_vcol = -1;
    redraw_later(wp, UPD_VALID);

    // When 'smoothscroll' is not set, should reset w_skipcol.
    if (!wp->w_p_sms) {
      reset_skipcol(wp);
    } else if (wp->w_skipcol != 0) {
      redraw_later(wp, UPD_SOME_VALID);
    }

    // May need to set w_skipcol when cursor in w_topline.
    if (wp->w_cursor.lnum == wp->w_topline) {
      validate_cursor(wp);
    }
  }

  *so_ptr = save_so;
}

/// Return the scrolljump value to use for the window "wp".
/// When 'scrolljump' is positive use it as-is.
/// When 'scrolljump' is negative use it as a percentage of the window height.
static int scrolljump_value(win_T *wp)
{
  int result = p_sj >= 0 ? (int)p_sj : (wp->w_view_height * (int)(-p_sj)) / 100;
  return result;
}

/// Return true when there are not 'scrolloff' lines above the cursor for window "wp".
static bool check_top_offset(win_T *wp)
{
  int64_t so = get_scrolloff_value(wp);
  if (wp->w_cursor.lnum < wp->w_topline + so || win_lines_concealed(wp)) {
    lineoff_T loff;
    loff.lnum = wp->w_cursor.lnum;
    loff.fill = 0;
    int n = wp->w_topfill;  // always have this context
    // Count the visible screen lines above the cursor line.
    while (n < so) {
      topline_back(wp, &loff);
      // Stop when included a line above the window.
      if (loff.lnum < wp->w_topline
          || (loff.lnum == wp->w_topline && loff.fill > 0)) {
        break;
      }
      n += loff.height;
    }
    if (n < so) {
      return true;
    }
  }
  return false;
}

/// Update w_curswant.
void update_curswant_force(void)
{
  validate_virtcol(curwin);
  curwin->w_curswant = curwin->w_virtcol;
  curwin->w_set_curswant = false;
}

/// Update w_curswant if w_set_curswant is set.
void update_curswant(void)
{
  if (curwin->w_set_curswant) {
    update_curswant_force();
  }
}

// Check if the cursor has moved.  Set the w_valid flag accordingly.
void check_cursor_moved(win_T *wp)
{
  if (wp->w_cursor.lnum != wp->w_valid_cursor.lnum) {
    wp->w_valid &= ~(VALID_WROW|VALID_WCOL|VALID_VIRTCOL
                     |VALID_CHEIGHT|VALID_CROW|VALID_TOPLINE);

    // Concealed line visibility toggled.
    if (wp == curwin && wp->w_valid_cursor.lnum > 0 && wp->w_p_cole >= 2
        && !conceal_cursor_line(wp)
        && (decor_conceal_line(wp, wp->w_cursor.lnum - 1, true)
            || decor_conceal_line(wp, wp->w_valid_cursor.lnum - 1, true))) {
      changed_window_setting(wp);
    }
    wp->w_valid_cursor = wp->w_cursor;
    wp->w_valid_leftcol = wp->w_leftcol;
    wp->w_valid_skipcol = wp->w_skipcol;
    wp->w_viewport_invalid = true;
  } else if (wp->w_skipcol != wp->w_valid_skipcol) {
    wp->w_valid &= ~(VALID_WROW|VALID_WCOL|VALID_VIRTCOL
                     |VALID_CHEIGHT|VALID_CROW
                     |VALID_BOTLINE|VALID_BOTLINE_AP);
    wp->w_valid_cursor = wp->w_cursor;
    wp->w_valid_leftcol = wp->w_leftcol;
    wp->w_valid_skipcol = wp->w_skipcol;
  } else if (wp->w_cursor.col != wp->w_valid_cursor.col
             || wp->w_leftcol != wp->w_valid_leftcol
             || wp->w_cursor.coladd !=
             wp->w_valid_cursor.coladd) {
    wp->w_valid &= ~(VALID_WROW|VALID_WCOL|VALID_VIRTCOL);
    wp->w_valid_cursor.col = wp->w_cursor.col;
    wp->w_valid_leftcol = wp->w_leftcol;
    wp->w_valid_cursor.coladd = wp->w_cursor.coladd;
    wp->w_viewport_invalid = true;
  }
}

// Call this function when some window settings have changed, which require
// the cursor position, botline and topline to be recomputed and the window to
// be redrawn.  E.g, when changing the 'wrap' option or folding.
void changed_window_setting(win_T *wp)
{
  wp->w_lines_valid = 0;
  changed_line_abv_curs_win(wp);
  wp->w_valid &= ~(VALID_BOTLINE|VALID_BOTLINE_AP|VALID_TOPLINE);
  redraw_later(wp, UPD_NOT_VALID);
}

/// Call changed_window_setting() for every window.
void changed_window_setting_all(void)
{
  FOR_ALL_TAB_WINDOWS(tp, wp) {
    changed_window_setting(wp);
  }
}

// Set wp->w_topline to a certain number.
void set_topline(win_T *wp, linenr_T lnum)
{
  linenr_T prev_topline = wp->w_topline;

  // go to first of folded lines
  hasFolding(wp, lnum, &lnum, NULL);
  // Approximate the value of w_botline
  wp->w_botline += lnum - wp->w_topline;
  if (wp->w_botline > wp->w_buffer->b_ml.ml_line_count + 1) {
    wp->w_botline = wp->w_buffer->b_ml.ml_line_count + 1;
  }
  wp->w_topline = lnum;
  wp->w_topline_was_set = true;
  if (lnum != prev_topline) {
    // Keep the filler lines when the topline didn't change.
    wp->w_topfill = 0;
  }
  wp->w_valid &= ~(VALID_WROW|VALID_CROW|VALID_BOTLINE|VALID_TOPLINE);
  // Don't set VALID_TOPLINE here, 'scrolloff' needs to be checked.
  redraw_later(wp, UPD_VALID);
}

/// Call this function when the length of the cursor line (in screen
/// characters) has changed, and the change is before the cursor.
/// If the line length changed the number of screen lines might change,
/// requiring updating w_topline.  That may also invalidate w_crow.
/// Need to take care of w_botline separately!
void changed_cline_bef_curs(win_T *wp)
{
  wp->w_valid &= ~(VALID_WROW|VALID_WCOL|VALID_VIRTCOL|VALID_CROW
                   |VALID_CHEIGHT|VALID_TOPLINE);
}

// Call this function when the length of a line (in screen characters) above
// the cursor have changed.
// Need to take care of w_botline separately!
void changed_line_abv_curs(void)
{
  curwin->w_valid &= ~(VALID_WROW|VALID_WCOL|VALID_VIRTCOL|VALID_CROW
                       |VALID_CHEIGHT|VALID_TOPLINE);
}

void changed_line_abv_curs_win(win_T *wp)
{
  wp->w_valid &= ~(VALID_WROW|VALID_WCOL|VALID_VIRTCOL|VALID_CROW
                   |VALID_CHEIGHT|VALID_TOPLINE);
}

/// Make sure the value of wp->w_botline is valid.
void validate_botline_win(win_T *wp)
  FUNC_ATTR_NONNULL_ALL
{
  if (!(wp->w_valid & VALID_BOTLINE)) {
    comp_botline(wp);
  }
}

/// Mark wp->w_botline as invalid (because of some change in the buffer).
void invalidate_botline_win(win_T *wp)
  FUNC_ATTR_NONNULL_ALL
{
  wp->w_valid &= ~(VALID_BOTLINE|VALID_BOTLINE_AP);
}

void approximate_botline_win(win_T *wp)
{
  wp->w_valid &= ~VALID_BOTLINE;
}

// Return true if wp->w_wrow and wp->w_wcol are valid.
int cursor_valid(win_T *wp)
{
  check_cursor_moved(wp);
  return (wp->w_valid & (VALID_WROW|VALID_WCOL)) == (VALID_WROW|VALID_WCOL);
}

// Validate cursor position.  Makes sure w_wrow and w_wcol are valid.
// w_topline must be valid, you may need to call update_topline() first!
void validate_cursor(win_T *wp)
{
  check_cursor_lnum(wp);
  check_cursor_moved(wp);
  if ((wp->w_valid & (VALID_WCOL|VALID_WROW)) != (VALID_WCOL|VALID_WROW)) {
    curs_columns(wp, true);
  }
}

// Compute wp->w_cline_row and wp->w_cline_height, based on the current value
// of wp->w_topline.
static void curs_rows(win_T *wp)
{
  // Check if wp->w_lines[].wl_size is invalid
  bool all_invalid = (!redrawing()
                      || wp->w_lines_valid == 0
                      || wp->w_lines[0].wl_lnum > wp->w_topline);
  int i = 0;
  wp->w_cline_row = 0;
  for (linenr_T lnum = wp->w_topline; lnum < wp->w_cursor.lnum; i++) {
    bool valid = false;
    if (!all_invalid && i < wp->w_lines_valid) {
      if (wp->w_lines[i].wl_lnum < lnum || !wp->w_lines[i].wl_valid) {
        continue;                       // skip changed or deleted lines
      }
      if (wp->w_lines[i].wl_lnum == lnum) {
        // Check for newly inserted lines below this row, in which
        // case we need to check for folded lines.
        if (!wp->w_buffer->b_mod_set
            || wp->w_lines[i].wl_lastlnum < wp->w_cursor.lnum
            || wp->w_buffer->b_mod_top
            > wp->w_lines[i].wl_lastlnum + 1) {
          valid = true;
        }
      } else if (wp->w_lines[i].wl_lnum > lnum) {
        i--;                            // hold at inserted lines
      }
    }
    if (valid && (lnum != wp->w_topline || (wp->w_skipcol == 0 && !win_may_fill(wp)))) {
      lnum = wp->w_lines[i].wl_lastlnum + 1;
      // Cursor inside folded or concealed lines, don't count this row
      if (lnum > wp->w_cursor.lnum) {
        break;
      }
      wp->w_cline_row += wp->w_lines[i].wl_size;
    } else {
      linenr_T last = lnum;
      bool folded;
      int n = plines_correct_topline(wp, lnum, &last, true, &folded);
      lnum = last + 1;
      if (lnum + decor_conceal_line(wp, lnum - 1, false) > wp->w_cursor.lnum) {
        break;
      }
      wp->w_cline_row += n;
    }
  }

  check_cursor_moved(wp);
  if (!(wp->w_valid & VALID_CHEIGHT)) {
    if (all_invalid
        || i == wp->w_lines_valid
        || (i < wp->w_lines_valid
            && (!wp->w_lines[i].wl_valid
                || wp->w_lines[i].wl_lnum != wp->w_cursor.lnum))) {
      wp->w_cline_height = plines_win_full(wp, wp->w_cursor.lnum, NULL,
                                           &wp->w_cline_folded, true, true);
    } else if (i > wp->w_lines_valid) {
      // a line that is too long to fit on the last screen line
      wp->w_cline_height = 0;
      wp->w_cline_folded = hasFolding(wp, wp->w_cursor.lnum, NULL, NULL);
    } else {
      wp->w_cline_height = wp->w_lines[i].wl_size;
      wp->w_cline_folded = wp->w_lines[i].wl_folded;
    }
  }

  redraw_for_cursorline(wp);
  wp->w_valid |= VALID_CROW|VALID_CHEIGHT;
}

// Validate wp->w_virtcol only.
void validate_virtcol(win_T *wp)
{
  check_cursor_moved(wp);

  if (wp->w_valid & VALID_VIRTCOL) {
    return;
  }

  getvvcol(wp, &wp->w_cursor, NULL, &(wp->w_virtcol), NULL);
  redraw_for_cursorcolumn(wp);
  wp->w_valid |= VALID_VIRTCOL;
}

// Validate wp->w_cline_height only.
void validate_cheight(win_T *wp)
{
  check_cursor_moved(wp);

  if (wp->w_valid & VALID_CHEIGHT) {
    return;
  }

  wp->w_cline_height = plines_win_full(wp, wp->w_cursor.lnum,
                                       NULL, &wp->w_cline_folded,
                                       true, true);
  wp->w_valid |= VALID_CHEIGHT;
}

// Validate w_wcol and w_virtcol only.
void validate_cursor_col(win_T *wp)
{
  validate_virtcol(wp);

  if (wp->w_valid & VALID_WCOL) {
    return;
  }

  colnr_T col = wp->w_virtcol;
  colnr_T off = win_col_off(wp);
  col += off;
  int width = wp->w_view_width - off + win_col_off2(wp);

  // long line wrapping, adjust wp->w_wrow
  if (wp->w_p_wrap && col >= (colnr_T)wp->w_view_width && width > 0) {
    // use same formula as what is used in curs_columns()
    col -= ((col - wp->w_view_width) / width + 1) * width;
  }
  if (col > (int)wp->w_leftcol) {
    col -= wp->w_leftcol;
  } else {
    col = 0;
  }
  wp->w_wcol = col;

  wp->w_valid |= VALID_WCOL;
}

// Compute offset of a window, occupied by absolute or relative line number,
// fold column and sign column (these don't move when scrolling horizontally).
int win_col_off(win_T *wp)
{
  return ((wp->w_p_nu || wp->w_p_rnu || *wp->w_p_stc != NUL)
          ? (number_width(wp) + (*wp->w_p_stc == NUL)) : 0)
         + ((wp != cmdwin_win) ? 0 : 1)
         + win_fdccol_count(wp) + (wp->w_scwidth * SIGN_WIDTH);
}

// Return the difference in column offset for the second screen line of a
// wrapped line.  It's positive if 'number' or 'relativenumber' is on and 'n'
// is in 'cpoptions'.
int win_col_off2(win_T *wp)
{
  if ((wp->w_p_nu || wp->w_p_rnu || *wp->w_p_stc != NUL)
      && vim_strchr(p_cpo, CPO_NUMCOL) != NULL) {
    return number_width(wp) + (*wp->w_p_stc == NUL);
  }
  return 0;
}

// Compute wp->w_wcol and wp->w_virtcol.
// Also updates wp->w_wrow and wp->w_cline_row.
// Also updates wp->w_leftcol.
// @param may_scroll when true, may scroll horizontally
void curs_columns(win_T *wp, int may_scroll)
{
  colnr_T startcol;
  colnr_T endcol;

  // First make sure that w_topline is valid (after moving the cursor).
  update_topline(wp);

  // Next make sure that w_cline_row is valid.
  if (!(wp->w_valid & VALID_CROW)) {
    curs_rows(wp);
  }

  // Compute the number of virtual columns.
  if (wp->w_cline_folded) {
    // In a folded line the cursor is always in the first column
    startcol = wp->w_virtcol = endcol = wp->w_leftcol;
  } else {
    getvvcol(wp, &wp->w_cursor, &startcol, &(wp->w_virtcol), &endcol);
  }

  // remove '$' from change command when cursor moves onto it
  if (startcol > dollar_vcol) {
    dollar_vcol = -1;
  }

  int extra = win_col_off(wp);
  wp->w_wcol = wp->w_virtcol + extra;
  endcol += extra;

  // Now compute w_wrow, counting screen lines from w_cline_row.
  wp->w_wrow = wp->w_cline_row;

  int n;
  int width1 = wp->w_view_width - extra;  // text width for first screen line
  int width2 = 0;                          // text width for second and later screen line
  bool did_sub_skipcol = false;
  if (width1 <= 0) {
    // No room for text, put cursor in last char of window.
    // If not wrapping, the last non-empty line.
    wp->w_wcol = wp->w_view_width - 1;
    if (wp->w_p_wrap) {
      wp->w_wrow = wp->w_view_height - 1;
    } else {
      wp->w_wrow = wp->w_view_height - 1 - wp->w_empty_rows;
    }
  } else if (wp->w_p_wrap && wp->w_view_width != 0) {
    width2 = width1 + win_col_off2(wp);

    // skip columns that are not visible
    if (wp->w_cursor.lnum == wp->w_topline
        && wp->w_skipcol > 0
        && wp->w_wcol >= wp->w_skipcol) {
      // Deduct by multiples of width2.  This allows the long line wrapping
      // formula below to correctly calculate the w_wcol value when wrapping.
      if (wp->w_skipcol <= width1) {
        wp->w_wcol -= width2;
      } else {
        wp->w_wcol -= width2 * (((wp->w_skipcol - width1) / width2) + 1);
      }

      did_sub_skipcol = true;
    }

    // long line wrapping, adjust wp->w_wrow
    if (wp->w_wcol >= wp->w_view_width) {
      // this same formula is used in validate_cursor_col()
      n = (wp->w_wcol - wp->w_view_width) / width2 + 1;
      wp->w_wcol -= n * width2;
      wp->w_wrow += n;
    }
  } else if (may_scroll
             && !wp->w_cline_folded) {
    // No line wrapping: compute wp->w_leftcol if scrolling is on and line
    // is not folded.
    // If scrolling is off, wp->w_leftcol is assumed to be 0

    // If Cursor is left of the screen, scroll rightwards.
    // If Cursor is right of the screen, scroll leftwards
    // If we get closer to the edge than 'sidescrolloff', scroll a little
    // extra
    int64_t siso = get_sidescrolloff_value(wp);
    int64_t off_left = startcol - wp->w_leftcol - siso;
    int64_t off_right = endcol - wp->w_leftcol - (wp->w_view_width - siso) + 1;
    if (off_left < 0 || off_right > 0) {
      int64_t diff = (off_left < 0) ? -off_left : off_right;

      // When far off or not enough room on either side, put cursor in
      // middle of window.
      int new_leftcol;
      if (p_ss == 0 || diff >= width1 / 2 || off_right >= off_left) {
        new_leftcol = wp->w_wcol - extra - width1 / 2;
      } else {
        if (diff < p_ss) {
          assert(p_ss <= INT_MAX);
          diff = p_ss;
        }
        if (off_left < 0) {
          new_leftcol = wp->w_leftcol - (int)diff;
        } else {
          new_leftcol = wp->w_leftcol + (int)diff;
        }
      }
      new_leftcol = MAX(new_leftcol, 0);
      if (new_leftcol != (int)wp->w_leftcol) {
        wp->w_leftcol = new_leftcol;
        win_check_anchored_floats(wp);
        // screen has to be redrawn with new wp->w_leftcol
        redraw_later(wp, UPD_NOT_VALID);
      }
    }
    wp->w_wcol -= wp->w_leftcol;
  } else if (wp->w_wcol > (int)wp->w_leftcol) {
    wp->w_wcol -= wp->w_leftcol;
  } else {
    wp->w_wcol = 0;
  }

  // Skip over filler lines.  At the top use w_topfill, there
  // may be some filler lines above the window.
  if (wp->w_cursor.lnum == wp->w_topline) {
    wp->w_wrow += wp->w_topfill;
  } else {
    wp->w_wrow += win_get_fill(wp, wp->w_cursor.lnum);
  }

  int plines = 0;
  int64_t so = get_scrolloff_value(wp);
  colnr_T prev_skipcol = wp->w_skipcol;
  if ((wp->w_wrow >= wp->w_view_height
       || ((prev_skipcol > 0
            || wp->w_wrow + so >= wp->w_view_height)
           && (plines = plines_win_nofill(wp, wp->w_cursor.lnum, false)) - 1
           >= wp->w_view_height))
      && wp->w_view_height != 0
      && wp->w_cursor.lnum == wp->w_topline
      && width2 > 0
      && wp->w_view_width != 0) {
    // Cursor past end of screen.  Happens with a single line that does
    // not fit on screen.  Find a skipcol to show the text around the
    // cursor.  Avoid scrolling all the time. compute value of "extra":
    // 1: Less than "p_so" lines above
    // 2: Less than "p_so" lines below
    // 3: both of them
    extra = 0;
    if (wp->w_skipcol + so * width2 > wp->w_virtcol) {
      extra = 1;
    }
    // Compute last display line of the buffer line that we want at the
    // bottom of the window.
    if (plines == 0) {
      plines = plines_win(wp, wp->w_cursor.lnum, false);
    }
    plines--;
    if (plines > wp->w_wrow + so) {
      assert(wp->w_wrow + so <= INT_MAX);
      n = (int)(wp->w_wrow + so);
    } else {
      n = plines;
    }
    if ((colnr_T)n >= wp->w_view_height + wp->w_skipcol / width2 - so) {
      extra += 2;
    }

    if (extra == 3 || wp->w_view_height <= so * 2) {
      // not enough room for 'scrolloff', put cursor in the middle
      n = wp->w_virtcol / width2;
      if (n > wp->w_view_height / 2) {
        n -= wp->w_view_height / 2;
      } else {
        n = 0;
      }
      // don't skip more than necessary
      if (n > plines - wp->w_view_height + 1) {
        n = plines - wp->w_view_height + 1;
      }
      wp->w_skipcol = n > 0 ? width1 + (n - 1) * width2
                            : 0;
    } else if (extra == 1) {
      // less than 'scrolloff' lines above, decrease skipcol
      assert(so <= INT_MAX);
      extra = (int)((wp->w_skipcol + so * width2 - wp->w_virtcol
                     + width2 - 1) / width2);
      if (extra > 0) {
        if ((colnr_T)(extra * width2) > wp->w_skipcol) {
          extra = wp->w_skipcol / width2;
        }
        wp->w_skipcol -= extra * width2;
      }
    } else if (extra == 2) {
      // less than 'scrolloff' lines below, increase skipcol
      endcol = (n - wp->w_view_height + 1) * width2;
      while (endcol > wp->w_virtcol) {
        endcol -= width2;
      }
      wp->w_skipcol = MAX(wp->w_skipcol, endcol);
    }

    // adjust w_wrow for the changed w_skipcol
    if (did_sub_skipcol) {
      wp->w_wrow -= (wp->w_skipcol - prev_skipcol) / width2;
    } else {
      wp->w_wrow -= wp->w_skipcol / width2;
    }

    if (wp->w_wrow >= wp->w_view_height) {
      // small window, make sure cursor is in it
      extra = wp->w_wrow - wp->w_view_height + 1;
      wp->w_skipcol += extra * width2;
      wp->w_wrow -= extra;
    }

    // extra could be either positive or negative
    extra = (prev_skipcol - wp->w_skipcol) / width2;
    // TODO(bfredl): this is very suspicious when not called by win_update()
    // We should not randomly alter screen state outside of update_screen() :(
    if (wp->w_grid.target) {
      win_scroll_lines(wp, 0, extra);
    }
  } else if (!wp->w_p_sms) {
    wp->w_skipcol = 0;
  }
  if (prev_skipcol != wp->w_skipcol) {
    redraw_later(wp, UPD_SOME_VALID);
  }

  redraw_for_cursorcolumn(wp);

  // now w_leftcol and w_skipcol are valid, avoid check_cursor_moved()
  // thinking otherwise
  wp->w_valid_leftcol = wp->w_leftcol;
  wp->w_valid_skipcol = wp->w_skipcol;

  wp->w_valid |= VALID_WCOL|VALID_WROW|VALID_VIRTCOL;
}

/// Compute the screen position of text character at "pos" in window "wp"
/// The resulting values are one-based, zero when character is not visible.
///
/// @param[out] rowp screen row
/// @param[out] scolp start screen column
/// @param[out] ccolp cursor screen column
/// @param[out] ecolp end screen column
void textpos2screenpos(win_T *wp, pos_T *pos, int *rowp, int *scolp, int *ccolp, int *ecolp,
                       bool local)
{
  colnr_T scol = 0;
  colnr_T ccol = 0;
  colnr_T ecol = 0;
  int row = 0;
  colnr_T coloff = 0;
  bool visible_row = false;
  bool is_folded = false;

  linenr_T lnum = pos->lnum;
  if (lnum >= wp->w_topline && lnum <= wp->w_botline) {
    is_folded = hasFolding(wp, lnum, &lnum, NULL);
    row = plines_m_win(wp, wp->w_topline, lnum - 1, INT_MAX);
    // "row" should be the screen line where line "lnum" begins, which can
    // be negative if "lnum" is "w_topline" and "w_skipcol" is non-zero.
    row -= adjust_plines_for_skipcol(wp);
    // Add filler lines above this buffer line.
    row += lnum == wp->w_topline ? wp->w_topfill : win_get_fill(wp, lnum);
    visible_row = true;
  } else if (!local || lnum < wp->w_topline) {
    row = 0;
  } else {
    row = wp->w_view_height - 1;
  }

  bool existing_row = (lnum > 0 && lnum <= wp->w_buffer->b_ml.ml_line_count);

  if ((local || visible_row) && existing_row) {
    const colnr_T off = win_col_off(wp);
    if (is_folded) {
      row += (local ? 0 : wp->w_winrow + wp->w_winrow_off) + 1;
      coloff = (local ? 0 : wp->w_wincol + wp->w_wincol_off) + 1 + off;
    } else {
      assert(lnum == pos->lnum);
      getvcol(wp, pos, &scol, &ccol, &ecol);

      // similar to what is done in validate_cursor_col()
      colnr_T col = scol;
      col += off;
      int width = wp->w_view_width - off + win_col_off2(wp);

      // long line wrapping, adjust row
      if (wp->w_p_wrap && col >= (colnr_T)wp->w_view_width && width > 0) {
        // use same formula as what is used in curs_columns()
        int rowoff = visible_row ? ((col - wp->w_view_width) / width + 1) : 0;
        col -= rowoff * width;
        row += rowoff;
      }

      col -= wp->w_leftcol;

      if (col >= 0 && col < wp->w_view_width && row >= 0 && row < wp->w_view_height) {
        coloff = col - scol + (local ? 0 : wp->w_wincol + wp->w_wincol_off) + 1;
        row += (local ? 0 : wp->w_winrow + wp->w_winrow_off) + 1;
      } else {
        // character is left, right or below of the window
        scol = ccol = ecol = 0;
        if (local) {
          coloff = col < 0 ? -1 : wp->w_view_width + 1;
        } else {
          row = 0;
        }
      }
    }
  }
  *rowp = row;
  *scolp = scol + coloff;
  *ccolp = ccol + coloff;
  *ecolp = ecol + coloff;
}

/// "screenpos({winid}, {lnum}, {col})" function
void f_screenpos(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{
  tv_dict_alloc_ret(rettv);
  dict_T *dict = rettv->vval.v_dict;

  win_T *wp = find_win_by_nr_or_id(&argvars[0]);
  if (wp == NULL) {
    return;
  }

  pos_T pos = {
    .lnum = (linenr_T)tv_get_number(&argvars[1]),
    .col = (colnr_T)tv_get_number(&argvars[2]) - 1,
    .coladd = 0
  };
  if (pos.lnum > wp->w_buffer->b_ml.ml_line_count) {
    semsg(_(e_invalid_line_number_nr), pos.lnum);
    return;
  }
  pos.col = MAX(pos.col, 0);
  int row = 0;
  int scol = 0;
  int ccol = 0;
  int ecol = 0;
  textpos2screenpos(wp, &pos, &row, &scol, &ccol, &ecol, false);

  tv_dict_add_nr(dict, S_LEN("row"), row);
  tv_dict_add_nr(dict, S_LEN("col"), scol);
  tv_dict_add_nr(dict, S_LEN("curscol"), ccol);
  tv_dict_add_nr(dict, S_LEN("endcol"), ecol);
}

/// Convert a virtual (screen) column to a character column.  The first column
/// is one.  For a multibyte character, the column number of the first byte is
/// returned.
static int virtcol2col(win_T *wp, linenr_T lnum, int vcol)
{
  int offset = vcol2col(wp, lnum, vcol - 1, NULL);
  char *line = ml_get_buf(wp->w_buffer, lnum);
  char *p = line + offset;

  if (*p == NUL) {
    if (p == line) {  // empty line
      return 0;
    }
    // Move to the first byte of the last char.
    MB_PTR_BACK(line, p);
  }
  return (int)(p - line + 1);
}

/// "virtcol2col({winid}, {lnum}, {col})" function
void f_virtcol2col(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{
  rettv->vval.v_number = -1;

  if (tv_check_for_number_arg(argvars, 0) == FAIL
      || tv_check_for_number_arg(argvars, 1) == FAIL
      || tv_check_for_number_arg(argvars, 2) == FAIL) {
    return;
  }

  win_T *wp = find_win_by_nr_or_id(&argvars[0]);
  if (wp == NULL) {
    return;
  }

  bool error = false;
  linenr_T lnum = (linenr_T)tv_get_number_chk(&argvars[1], &error);
  if (error || lnum < 0 || lnum > wp->w_buffer->b_ml.ml_line_count) {
    return;
  }

  int screencol = (int)tv_get_number_chk(&argvars[2], &error);
  if (error || screencol < 0) {
    return;
  }

  rettv->vval.v_number = virtcol2col(wp, lnum, screencol);
}

/// Make sure the cursor is in the visible part of the topline after scrolling
/// the screen with 'smoothscroll'.
static void cursor_correct_sms(win_T *wp)
{
  if (!wp->w_p_sms || !wp->w_p_wrap || wp->w_cursor.lnum != wp->w_topline) {
    return;
  }

  int64_t so = get_scrolloff_value(wp);
  int width1 = wp->w_view_width - win_col_off(wp);
  int width2 = width1 + win_col_off2(wp);
  int64_t so_cols = so == 0 ? 0 : width1 + (so - 1) * width2;
  int space_cols = (wp->w_view_height - 1) * width2;
  int size = so == 0 ? 0 : linetabsize_eol(wp, wp->w_topline);

  if (wp->w_topline == 1 && wp->w_skipcol == 0) {
    so_cols = 0;               // Ignore 'scrolloff' at top of buffer.
  } else if (so_cols > space_cols / 2) {
    so_cols = space_cols / 2;  // Not enough room: put cursor in the middle.
  }

  // Not enough screen lines in topline: ignore 'scrolloff'.
  while (so_cols > size && so_cols - width2 >= width1 && width1 > 0) {
    so_cols -= width2;
  }
  if (so_cols >= width1 && so_cols > size) {
    so_cols -= width1;
  }

  int overlap = wp->w_skipcol == 0
                ? 0 : sms_marker_overlap(wp, wp->w_view_width - width2);
  // If we have non-zero scrolloff, ignore marker overlap.
  int64_t top = wp->w_skipcol + (so_cols != 0 ? so_cols : overlap);
  int64_t bot = wp->w_skipcol + width1 + (wp->w_view_height - 1) * width2 - so_cols;

  validate_virtcol(wp);
  colnr_T col = wp->w_virtcol;

  if (col < top) {
    if (col < width1) {
      col += width1;
    }
    while (width2 > 0 && col < top) {
      col += width2;
    }
  } else {
    while (width2 > 0 && col >= bot) {
      col -= width2;
    }
  }

  if (col != wp->w_virtcol) {
    wp->w_curswant = col;
    int rc = coladvance(wp, wp->w_curswant);
    // validate_virtcol() marked various things as valid, but after
    // moving the cursor they need to be recomputed
    wp->w_valid &= ~(VALID_WROW|VALID_WCOL|VALID_CHEIGHT|VALID_CROW|VALID_VIRTCOL);
    if (rc == FAIL && wp->w_skipcol > 0
        && wp->w_cursor.lnum < wp->w_buffer->b_ml.ml_line_count) {
      validate_virtcol(wp);
      if (wp->w_virtcol < wp->w_skipcol + overlap) {
        // Cursor still not visible: move it to the next line instead.
        wp->w_cursor.lnum++;
        wp->w_cursor.col = 0;
        wp->w_cursor.coladd = 0;
        wp->w_curswant = 0;
        wp->w_valid &= ~VALID_VIRTCOL;
      }
    }
  }
}

/// Scroll "count" lines up or down, and redraw.
void scroll_redraw(int up, linenr_T count)
{
  linenr_T prev_topline = curwin->w_topline;
  int prev_skipcol = curwin->w_skipcol;
  int prev_topfill = curwin->w_topfill;
  linenr_T prev_lnum = curwin->w_cursor.lnum;

  bool moved = up
               ? scrollup(curwin, count, true)
               : scrolldown(curwin, count, true);

  if (get_scrolloff_value(curwin) > 0) {
    // Adjust the cursor position for 'scrolloff'.  Mark w_topline as
    // valid, otherwise the screen jumps back at the end of the file.
    cursor_correct(curwin);
    check_cursor_moved(curwin);
    curwin->w_valid |= VALID_TOPLINE;

    // If moved back to where we were, at least move the cursor, otherwise
    // we get stuck at one position.  Don't move the cursor up if the
    // first line of the buffer is already on the screen
    while (curwin->w_topline == prev_topline
           && curwin->w_skipcol == prev_skipcol
           && curwin->w_topfill == prev_topfill) {
      if (up) {
        if (curwin->w_cursor.lnum > prev_lnum
            || cursor_down(1L, false) == FAIL) {
          break;
        }
      } else {
        if (curwin->w_cursor.lnum < prev_lnum
            || prev_topline == 1L
            || cursor_up(1L, false) == FAIL) {
          break;
        }
      }
      // Mark w_topline as valid, otherwise the screen jumps back at the
      // end of the file.
      check_cursor_moved(curwin);
      curwin->w_valid |= VALID_TOPLINE;
    }
  }

  if (moved) {
    curwin->w_viewport_invalid = true;
  }

  cursor_correct_sms(curwin);
  if (curwin->w_cursor.lnum != prev_lnum) {
    coladvance(curwin, curwin->w_curswant);
  }
  redraw_later(curwin, UPD_VALID);
}

/// Scroll a window down by "line_count" logical lines.  "CTRL-Y"
///
/// @param line_count number of lines to scroll
/// @param byfold if true, count a closed fold as one line
bool scrolldown(win_T *wp, linenr_T line_count, int byfold)
{
  int done = 0;                // total # of physical lines done
  int width1 = 0;
  int width2 = 0;
  bool do_sms = wp->w_p_wrap && wp->w_p_sms;

  if (do_sms) {
    width1 = wp->w_view_width - win_col_off(wp);
    width2 = width1 + win_col_off2(wp);
  }

  // Make sure w_topline is at the first of a sequence of folded lines.
  hasFolding(wp, wp->w_topline, &wp->w_topline, NULL);
  validate_cursor(wp);            // w_wrow needs to be valid
  for (int todo = line_count; todo > 0; todo--) {
    bool can_fill = wp->w_topfill < wp->w_view_height - 1
                    && wp->w_topfill < win_get_fill(wp, wp->w_topline);
    // break when at the very top
    if (wp->w_topline == 1 && !can_fill && (!do_sms || wp->w_skipcol < width1)) {
      break;
    }
    if (do_sms && wp->w_skipcol >= width1) {
      // scroll a screen line down
      if (wp->w_skipcol >= width1 + width2) {
        wp->w_skipcol -= width2;
      } else {
        wp->w_skipcol -= width1;
      }
      redraw_later(wp, UPD_NOT_VALID);
      done++;
    } else if (can_fill) {
      wp->w_topfill++;
      done++;
    } else {
      // scroll a text line down
      wp->w_topline--;
      wp->w_skipcol = 0;
      wp->w_topfill = 0;
      // A sequence of folded lines only counts for one logical line
      linenr_T first;
      if (hasFolding(wp, wp->w_topline, &first, NULL)) {
        done += !decor_conceal_line(wp, first - 1, false);
        if (!byfold) {
          todo -= wp->w_topline - first - 1;
        }
        wp->w_botline -= wp->w_topline - first;
        wp->w_topline = first;
      } else if (decor_conceal_line(wp, wp->w_topline - 1, false)) {
        todo++;
      } else {
        if (do_sms) {
          int size = linetabsize_eol(wp, wp->w_topline);
          if (size > width1) {
            wp->w_skipcol = width1;
            size -= width1;
            redraw_later(wp, UPD_NOT_VALID);
          }
          while (size > width2) {
            wp->w_skipcol += width2;
            size -= width2;
          }
          done++;
        } else {
          done += plines_win_nofill(wp, wp->w_topline, true);
        }
      }
    }
    wp->w_botline--;                // approximate w_botline
    invalidate_botline_win(wp);
  }

  // Adjust for concealed lines above w_topline
  while (wp->w_topline > 1 && decor_conceal_line(wp, wp->w_topline - 2, false)) {
    wp->w_topline--;
    hasFolding(wp, wp->w_topline, &wp->w_topline, NULL);
  }

  wp->w_wrow += done;               // keep w_wrow updated
  wp->w_cline_row += done;          // keep w_cline_row updated

  if (wp->w_cursor.lnum == wp->w_topline) {
    wp->w_cline_row = 0;
  }
  check_topfill(wp, true);

  // Compute the row number of the last row of the cursor line
  // and move the cursor onto the displayed part of the window.
  int wrow = wp->w_wrow;
  if (wp->w_p_wrap && wp->w_view_width != 0) {
    validate_virtcol(wp);
    validate_cheight(wp);
    wrow += wp->w_cline_height - 1 -
            wp->w_virtcol / wp->w_view_width;
  }
  bool moved = false;
  while (wrow >= wp->w_view_height && wp->w_cursor.lnum > 1) {
    linenr_T first;
    if (hasFolding(wp, wp->w_cursor.lnum, &first, NULL)) {
      wrow -= !decor_conceal_line(wp, wp->w_cursor.lnum - 1, false);
      wp->w_cursor.lnum = MAX(first - 1, 1);
    } else {
      wrow -= plines_win(wp, wp->w_cursor.lnum--, true);
    }
    wp->w_valid &=
      ~(VALID_WROW|VALID_WCOL|VALID_CHEIGHT|VALID_CROW|VALID_VIRTCOL);
    moved = true;
  }
  if (moved) {
    // Move cursor to first line of closed fold.
    foldAdjustCursor(wp);
    coladvance(wp, wp->w_curswant);
  }
  wp->w_cursor.lnum = MAX(wp->w_cursor.lnum, wp->w_topline);

  return moved;
}

/// Scroll a window up by "line_count" logical lines.  "CTRL-E"
///
/// @param line_count number of lines to scroll
/// @param byfold if true, count a closed fold as one line
bool scrollup(win_T *wp, linenr_T line_count, bool byfold)
{
  linenr_T topline = wp->w_topline;
  linenr_T botline = wp->w_botline;
  bool do_sms = wp->w_p_wrap && wp->w_p_sms;

  if (do_sms || (byfold && win_lines_concealed(wp)) || win_may_fill(wp)) {
    int width1 = wp->w_view_width - win_col_off(wp);
    int width2 = width1 + win_col_off2(wp);
    int size = 0;
    const colnr_T prev_skipcol = wp->w_skipcol;

    if (do_sms) {
      size = linetabsize_eol(wp, wp->w_topline);
    }

    // diff mode: first consume "topfill"
    // 'smoothscroll': increase "w_skipcol" until it goes over the end of
    // the line, then advance to the next line.
    // folding: count each sequence of folded lines as one logical line.
    for (int todo = line_count; todo > 0; todo--) {
      todo += decor_conceal_line(wp, wp->w_topline - 1, false);
      if (wp->w_topfill > 0) {
        wp->w_topfill--;
      } else {
        linenr_T lnum = wp->w_topline;
        if (byfold) {
          // for a closed fold: go to the last line in the fold
          hasFolding(wp, lnum, NULL, &lnum);
        }
        if (lnum == wp->w_topline && do_sms) {
          // 'smoothscroll': increase "w_skipcol" until it goes over
          // the end of the line, then advance to the next line.
          int add = wp->w_skipcol > 0 ? width2 : width1;
          wp->w_skipcol += add;
          if (wp->w_skipcol >= size) {
            if (lnum == wp->w_buffer->b_ml.ml_line_count) {
              // at the last screen line, can't scroll further
              wp->w_skipcol -= add;
              break;
            }
            lnum++;
          }
        } else {
          if (lnum >= wp->w_buffer->b_ml.ml_line_count) {
            break;
          }
          lnum++;
        }

        if (lnum > wp->w_topline) {
          // approximate w_botline
          wp->w_botline += lnum - wp->w_topline;
          wp->w_topline = lnum;
          wp->w_topfill = win_get_fill(wp, lnum);
          wp->w_skipcol = 0;
          if (todo > 1 && do_sms) {
            size = linetabsize_eol(wp, wp->w_topline);
          }
        }
      }
    }

    if (prev_skipcol > 0 || wp->w_skipcol > 0) {
      // need to redraw more, because wl_size of the (new) topline may
      // now be invalid
      redraw_later(wp, UPD_NOT_VALID);
    }
  } else {
    wp->w_topline += line_count;
    wp->w_botline += line_count;            // approximate w_botline
  }

  wp->w_topline = MIN(wp->w_topline, wp->w_buffer->b_ml.ml_line_count);
  wp->w_botline = MIN(wp->w_botline, wp->w_buffer->b_ml.ml_line_count + 1);

  check_topfill(wp, false);

  // Make sure w_topline is at the first of a sequence of folded lines.
  hasFolding(wp, wp->w_topline, &wp->w_topline, NULL);

  wp->w_valid &= ~(VALID_WROW|VALID_CROW|VALID_BOTLINE);
  if (wp->w_cursor.lnum < wp->w_topline) {
    wp->w_cursor.lnum = wp->w_topline;
    wp->w_valid &=
      ~(VALID_WROW|VALID_WCOL|VALID_CHEIGHT|VALID_CROW|VALID_VIRTCOL);
    coladvance(wp, wp->w_curswant);
  }

  bool moved = topline != wp->w_topline || botline != wp->w_botline;

  return moved;
}

/// Called after changing the cursor column: make sure that curwin->w_skipcol is
/// valid for 'smoothscroll'.
void adjust_skipcol(void)
{
  if (!curwin->w_p_wrap || !curwin->w_p_sms || curwin->w_cursor.lnum != curwin->w_topline) {
    return;
  }

  int width1 = curwin->w_view_width - win_col_off(curwin);
  if (width1 <= 0) {
    return;  // no text will be displayed
  }
  int width2 = width1 + win_col_off2(curwin);
  int64_t so = get_scrolloff_value(curwin);
  int64_t scrolloff_cols = so == 0 ? 0 : width1 + (so - 1) * width2;
  bool scrolled = false;

  validate_cheight(curwin);
  if (curwin->w_cline_height == curwin->w_view_height
      // w_cline_height may be capped at w_view_height, check there aren't
      // actually more lines.
      && plines_win(curwin, curwin->w_cursor.lnum, false) <= curwin->w_view_height) {
    // the line just fits in the window, don't scroll
    reset_skipcol(curwin);
    return;
  }

  validate_virtcol(curwin);
  int overlap = sms_marker_overlap(curwin, curwin->w_view_width - width2);
  while (curwin->w_skipcol > 0
         && curwin->w_virtcol < curwin->w_skipcol + overlap + scrolloff_cols) {
    // scroll a screen line down
    if (curwin->w_skipcol >= width1 + width2) {
      curwin->w_skipcol -= width2;
    } else {
      curwin->w_skipcol -= width1;
    }
    scrolled = true;
  }
  if (scrolled) {
    validate_virtcol(curwin);
    redraw_later(curwin, UPD_NOT_VALID);
    return;  // don't scroll in the other direction now
  }
  int row = 0;
  int64_t col = curwin->w_virtcol + scrolloff_cols;

  // Avoid adjusting for 'scrolloff' beyond the text line height.
  if (scrolloff_cols > 0) {
    int size = linetabsize_eol(curwin, curwin->w_topline);
    size = width1 + width2 * ((size - width1 + width2 - 1) / width2);
    while (col > size) {
      col -= width2;
    }
  }
  col -= curwin->w_skipcol;

  if (col >= width1) {
    col -= width1;
    row++;
  }
  if (col > width2) {
    row += (int)(col / width2);
  }
  if (row >= curwin->w_view_height) {
    if (curwin->w_skipcol == 0) {
      curwin->w_skipcol += width1;
      row--;
    }
    if (row >= curwin->w_view_height) {
      curwin->w_skipcol += (row - curwin->w_view_height) * width2;
    }
    redraw_later(curwin, UPD_NOT_VALID);
  }
}

/// Don't end up with too many filler lines in the window.
///
/// @param down  when true scroll down when not enough space
void check_topfill(win_T *wp, bool down)
{
  if (wp->w_topfill > 0) {
    int n = plines_win_nofill(wp, wp->w_topline, true);
    if (wp->w_topfill + n > wp->w_view_height) {
      if (down && wp->w_topline > 1) {
        wp->w_topline--;
        wp->w_topfill = 0;
      } else {
        wp->w_topfill = wp->w_view_height - n;
        wp->w_topfill = MAX(wp->w_topfill, 0);
      }
    }
  }
  win_check_anchored_floats(wp);
}

// Scroll the screen one line down, but don't do it if it would move the
// cursor off the screen.
void scrolldown_clamp(void)
{
  bool can_fill = (curwin->w_topfill < win_get_fill(curwin, curwin->w_topline));

  if (curwin->w_topline <= 1
      && !can_fill) {
    return;
  }

  validate_cursor(curwin);        // w_wrow needs to be valid

  // Compute the row number of the last row of the cursor line
  // and make sure it doesn't go off the screen. Make sure the cursor
  // doesn't go past 'scrolloff' lines from the screen end.
  int end_row = curwin->w_wrow;
  if (can_fill) {
    end_row++;
  } else {
    end_row += plines_win_nofill(curwin, curwin->w_topline - 1, true);
  }
  if (curwin->w_p_wrap && curwin->w_view_width != 0) {
    validate_cheight(curwin);
    validate_virtcol(curwin);
    end_row += curwin->w_cline_height - 1 -
               curwin->w_virtcol / curwin->w_view_width;
  }
  if (end_row < curwin->w_view_height - get_scrolloff_value(curwin)) {
    if (can_fill) {
      curwin->w_topfill++;
      check_topfill(curwin, true);
    } else {
      curwin->w_topline--;
      curwin->w_topfill = 0;
    }
    hasFolding(curwin, curwin->w_topline, &curwin->w_topline, NULL);
    curwin->w_botline--;            // approximate w_botline
    curwin->w_valid &= ~(VALID_WROW|VALID_CROW|VALID_BOTLINE);
  }
}

// Scroll the screen one line up, but don't do it if it would move the cursor
// off the screen.
void scrollup_clamp(void)
{
  if (curwin->w_topline == curbuf->b_ml.ml_line_count
      && curwin->w_topfill == 0) {
    return;
  }

  validate_cursor(curwin);        // w_wrow needs to be valid

  // Compute the row number of the first row of the cursor line
  // and make sure it doesn't go off the screen. Make sure the cursor
  // doesn't go before 'scrolloff' lines from the screen start.
  int start_row = (curwin->w_wrow
                   - plines_win_nofill(curwin, curwin->w_topline, true)
                   - curwin->w_topfill);
  if (curwin->w_p_wrap && curwin->w_view_width != 0) {
    validate_virtcol(curwin);
    start_row -= curwin->w_virtcol / curwin->w_view_width;
  }
  if (start_row >= get_scrolloff_value(curwin)) {
    if (curwin->w_topfill > 0) {
      curwin->w_topfill--;
    } else {
      hasFolding(curwin, curwin->w_topline, NULL, &curwin->w_topline);
      curwin->w_topline++;
    }
    curwin->w_botline++;                // approximate w_botline
    curwin->w_valid &= ~(VALID_WROW|VALID_CROW|VALID_BOTLINE);
  }
}

// Add one line above "lp->lnum".  This can be a filler line, a closed fold or
// a (wrapped) text line.  Uses and sets "lp->fill".
// Returns the height of the added line in "lp->height".
// Lines above the first one are incredibly high: MAXCOL.
// When "winheight" is true limit to window height.
static void topline_back_winheight(win_T *wp, lineoff_T *lp, int winheight)
{
  if (lp->fill < win_get_fill(wp, lp->lnum)) {
    // Add a filler line
    lp->fill++;
    lp->height = 1;
  } else {
    lp->lnum--;
    lp->fill = 0;
    if (lp->lnum < 1) {
      lp->height = MAXCOL;
    } else if (hasFolding(wp, lp->lnum, &lp->lnum, NULL)) {
      // Add a closed fold unless concealed.
      lp->height = !decor_conceal_line(wp, lp->lnum - 1, false);
    } else {
      lp->height = plines_win_nofill(wp, lp->lnum, winheight);
    }
  }
}

static void topline_back(win_T *wp, lineoff_T *lp)
{
  topline_back_winheight(wp, lp, true);
}

// Add one line below "lp->lnum".  This can be a filler line, a closed fold or
// a (wrapped) text line.  Uses and sets "lp->fill".
// Returns the height of the added line in "lp->height".
// Lines below the last one are incredibly high.
static void botline_forw(win_T *wp, lineoff_T *lp)
{
  if (lp->fill < win_get_fill(wp, lp->lnum + 1)) {
    // Add a filler line.
    lp->fill++;
    lp->height = 1;
  } else {
    lp->lnum++;
    lp->fill = 0;
    assert(wp->w_buffer != 0);
    if (lp->lnum > wp->w_buffer->b_ml.ml_line_count) {
      lp->height = MAXCOL;
    } else if (hasFolding(wp, lp->lnum, NULL, &lp->lnum)) {
      // Add a closed fold unless concealed.
      lp->height = !decor_conceal_line(wp, lp->lnum - 1, false);
    } else {
      lp->height = plines_win_nofill(wp, lp->lnum, true);
    }
  }
}

// Recompute topline to put the cursor at the top of the window.
// Scroll at least "min_scroll" lines.
// If "always" is true, always set topline (for "zt").
void scroll_cursor_top(win_T *wp, int min_scroll, int always)
{
  linenr_T old_topline = wp->w_topline;
  int old_skipcol = wp->w_skipcol;
  linenr_T old_topfill = wp->w_topfill;
  int64_t off = get_scrolloff_value(wp);

  if (mouse_dragging > 0) {
    off = mouse_dragging - 1;
  }

  // Decrease topline until:
  // - it has become 1
  // - (part of) the cursor line is moved off the screen or
  // - moved at least 'scrolljump' lines and
  // - at least 'scrolloff' lines above and below the cursor
  validate_cheight(wp);
  int scrolled = 0;
  int used = wp->w_cline_height;  // includes filler lines above
  if (wp->w_cursor.lnum < wp->w_topline) {
    scrolled = used;
  }

  linenr_T top;  // just above displayed lines
  linenr_T bot;  // just below displayed lines
  if (hasFolding(wp, wp->w_cursor.lnum, &top, &bot)) {
    top--;
    bot++;
  } else {
    top = wp->w_cursor.lnum - 1;
    bot = wp->w_cursor.lnum + 1;
  }
  linenr_T new_topline = top + 1;

  // "used" already contains the number of filler lines above, don't add it
  // again.
  // Hide filler lines above cursor line by adding them to "extra".
  int extra = win_get_fill(wp, wp->w_cursor.lnum);

  // Check if the lines from "top" to "bot" fit in the window.  If they do,
  // set new_topline and advance "top" and "bot" to include more lines.
  while (top > 0) {
    int i = plines_win_nofill(wp, top, true);
    hasFolding(wp, top, &top, NULL);
    if (top < wp->w_topline) {
      scrolled += i;
    }

    // If scrolling is needed, scroll at least 'sj' lines.
    if ((new_topline >= wp->w_topline || scrolled > min_scroll) && extra >= off) {
      break;
    }

    used += i;
    if (extra + i <= off && bot < wp->w_buffer->b_ml.ml_line_count) {
      used += plines_win_full(wp, bot, &bot, NULL, true, true);
    }
    if (used > wp->w_view_height) {
      break;
    }

    extra += i;
    new_topline = top;
    top--;
    bot++;
  }

  // If we don't have enough space, put cursor in the middle.
  // This makes sure we get the same position when using "k" and "j"
  // in a small window.
  if (used > wp->w_view_height) {
    scroll_cursor_halfway(wp, false, false);
  } else {
    // If "always" is false, only adjust topline to a lower value, higher
    // value may happen with wrapping lines.
    if (new_topline < wp->w_topline || always) {
      wp->w_topline = new_topline;
    }
    wp->w_topline = MIN(wp->w_topline, wp->w_cursor.lnum);
    wp->w_topfill = win_get_fill(wp, wp->w_topline);
    if (wp->w_topfill > 0 && extra > off) {
      wp->w_topfill -= extra - (int)off;
      wp->w_topfill = MAX(wp->w_topfill, 0);
    }
    check_topfill(wp, false);
    if (wp->w_topline != old_topline) {
      reset_skipcol(wp);
    } else if (wp->w_topline == wp->w_cursor.lnum) {
      validate_virtcol(wp);
      if (wp->w_skipcol >= wp->w_virtcol) {
        // TODO(vim): if the line doesn't fit may optimize w_skipcol instead
        // of making it zero
        reset_skipcol(wp);
      }
    }
    if (wp->w_topline != old_topline
        || wp->w_skipcol != old_skipcol
        || wp->w_topfill != old_topfill) {
      wp->w_valid &=
        ~(VALID_WROW|VALID_CROW|VALID_BOTLINE|VALID_BOTLINE_AP);
    }
    wp->w_valid |= VALID_TOPLINE;
    wp->w_viewport_invalid = true;
  }
}

// Set w_empty_rows and w_filler_rows for window "wp", having used up "used"
// screen lines for text lines.
void set_empty_rows(win_T *wp, int used)
{
  wp->w_filler_rows = 0;
  if (used == 0) {
    wp->w_empty_rows = 0;  // single line that doesn't fit
  } else {
    wp->w_empty_rows = wp->w_view_height - used;
    if (wp->w_botline <= wp->w_buffer->b_ml.ml_line_count) {
      wp->w_filler_rows = win_get_fill(wp, wp->w_botline);
      if (wp->w_empty_rows > wp->w_filler_rows) {
        wp->w_empty_rows -= wp->w_filler_rows;
      } else {
        wp->w_filler_rows = wp->w_empty_rows;
        wp->w_empty_rows = 0;
      }
    }
  }
}

/// Recompute topline to put the cursor at the bottom of the window.
/// When scrolling scroll at least "min_scroll" lines.
/// If "set_topbot" is true, set topline and botline first (for "zb").
/// This is messy stuff!!!
void scroll_cursor_bot(win_T *wp, int min_scroll, bool set_topbot)
{
  lineoff_T loff;
  linenr_T old_topline = wp->w_topline;
  int old_skipcol = wp->w_skipcol;
  int old_topfill = wp->w_topfill;
  linenr_T old_botline = wp->w_botline;
  int old_valid = wp->w_valid;
  int old_empty_rows = wp->w_empty_rows;
  linenr_T cln = wp->w_cursor.lnum;  // Cursor Line Number
  bool do_sms = wp->w_p_wrap && wp->w_p_sms;

  if (set_topbot) {
    int used = 0;
    linenr_T cln_last = cln;
    hasFolding(wp, cln, NULL, &cln_last);
    wp->w_botline = cln_last + 1;
    loff.lnum = cln_last + 1;
    loff.fill = 0;
    while (true) {
      topline_back_winheight(wp, &loff, false);
      if (loff.height == MAXCOL) {
        break;
      }
      if (used + loff.height > wp->w_view_height) {
        if (do_sms) {
          // 'smoothscroll' and 'wrap' are set.  The above line is
          // too long to show in its entirety, so we show just a part
          // of it.
          if (used < wp->w_view_height) {
            int plines_offset = used + loff.height - wp->w_view_height;
            used = wp->w_view_height;
            wp->w_topfill = loff.fill;
            wp->w_topline = loff.lnum;
            wp->w_skipcol = skipcol_from_plines(wp, plines_offset);
          }
        }
        break;
      }
      wp->w_topfill = loff.fill;
      wp->w_topline = loff.lnum;
      used += loff.height;
    }

    set_empty_rows(wp, used);
    wp->w_valid |= VALID_BOTLINE|VALID_BOTLINE_AP;
    if (wp->w_topline != old_topline
        || wp->w_topfill != old_topfill
        || wp->w_skipcol != old_skipcol
        || wp->w_skipcol != 0) {
      wp->w_valid &= ~(VALID_WROW|VALID_CROW);
      if (wp->w_skipcol != old_skipcol) {
        redraw_later(wp, UPD_NOT_VALID);
      } else {
        reset_skipcol(wp);
      }
    }
  } else {
    validate_botline_win(wp);
  }

  // The lines of the cursor line itself are always used.
  int used = plines_win_nofill(wp, cln, true);

  int scrolled = 0;
  // If the cursor is on or below botline, we will at least scroll by the
  // height of the cursor line, which is "used".  Correct for empty lines,
  // which are really part of botline.
  if (cln >= wp->w_botline) {
    scrolled = used;
    if (cln == wp->w_botline) {
      scrolled -= wp->w_empty_rows;
    }
    if (do_sms) {
      // 'smoothscroll' and 'wrap' are set.
      // Calculate how many screen lines the current top line of window
      // occupies. If it is occupying more than the entire window, we
      // need to scroll the additional clipped lines to scroll past the
      // top line before we can move on to the other lines.
      int top_plines = plines_win_nofill(wp, wp->w_topline, false);
      int width1 = wp->w_view_width - win_col_off(wp);

      if (width1 > 0) {
        int width2 = width1 + win_col_off2(wp);
        int skip_lines = 0;

        // A similar formula is used in curs_columns().
        if (wp->w_skipcol > width1) {
          skip_lines += (wp->w_skipcol - width1) / width2 + 1;
        } else if (wp->w_skipcol > 0) {
          skip_lines = 1;
        }

        top_plines -= skip_lines;
        if (top_plines > wp->w_view_height) {
          scrolled += (top_plines - wp->w_view_height);
        }
      }
    }
  }

  lineoff_T boff;
  // Stop counting lines to scroll when
  // - hitting start of the file
  // - scrolled nothing or at least 'sj' lines
  // - at least 'so' lines below the cursor
  // - lines between botline and cursor have been counted
  if (!hasFolding(wp, wp->w_cursor.lnum, &loff.lnum, &boff.lnum)) {
    loff.lnum = cln;
    boff.lnum = cln;
  }
  loff.fill = 0;
  boff.fill = 0;
  int fill_below_window = win_get_fill(wp, wp->w_botline) - wp->w_filler_rows;

  int extra = 0;
  int64_t so = get_scrolloff_value(wp);
  while (loff.lnum > 1) {
    // Stop when scrolled nothing or at least "min_scroll", found "extra"
    // context for 'scrolloff' and counted all lines below the window.
    if ((((scrolled <= 0 || scrolled >= min_scroll)
          && extra >= (mouse_dragging > 0 ? mouse_dragging - 1 : so))
         || boff.lnum + 1 > wp->w_buffer->b_ml.ml_line_count)
        && loff.lnum <= wp->w_botline
        && (loff.lnum < wp->w_botline
            || loff.fill >= fill_below_window)) {
      break;
    }

    // Add one line above
    topline_back(wp, &loff);
    if (loff.height == MAXCOL) {
      used = MAXCOL;
    } else {
      used += loff.height;
    }
    if (used > wp->w_view_height) {
      break;
    }
    if (loff.lnum >= wp->w_botline
        && (loff.lnum > wp->w_botline
            || loff.fill <= fill_below_window)) {
      // Count screen lines that are below the window.
      scrolled += loff.height;
      if (loff.lnum == wp->w_botline
          && loff.fill == 0) {
        scrolled -= wp->w_empty_rows;
      }
    }

    if (boff.lnum < wp->w_buffer->b_ml.ml_line_count) {
      // Add one line below
      botline_forw(wp, &boff);
      assert(boff.height != MAXCOL);
      used += boff.height;
      if (used > wp->w_view_height) {
        break;
      }
      if (extra < (mouse_dragging > 0 ? mouse_dragging - 1 : so)
          || scrolled < min_scroll) {
        extra += boff.height;
        if (boff.lnum >= wp->w_botline
            || (boff.lnum + 1 == wp->w_botline
                && boff.fill > wp->w_filler_rows)) {
          // Count screen lines that are below the window.
          scrolled += boff.height;
          if (boff.lnum == wp->w_botline
              && boff.fill == 0) {
            scrolled -= wp->w_empty_rows;
          }
        }
      }
    }
  }

  linenr_T line_count;
  // wp->w_empty_rows is larger, no need to scroll
  if (scrolled <= 0) {
    line_count = 0;
    // more than a screenfull, don't scroll but redraw
  } else if (used > wp->w_view_height) {
    line_count = used;
    // scroll minimal number of lines
  } else {
    line_count = 0;
    boff.fill = wp->w_topfill;
    boff.lnum = wp->w_topline - 1;
    int i;
    for (i = 0; i < scrolled && boff.lnum < wp->w_botline;) {
      botline_forw(wp, &boff);
      i += boff.height;
      line_count++;
    }
    if (i < scrolled) {         // below wp->w_botline, don't scroll
      line_count = 9999;
    }
  }

  // Scroll up if the cursor is off the bottom of the screen a bit.
  // Otherwise put it at 1/2 of the screen.
  if (line_count >= wp->w_view_height && line_count > min_scroll) {
    scroll_cursor_halfway(wp, false, true);
  } else if (line_count > 0) {
    if (do_sms) {
      scrollup(wp, scrolled, true);  // TODO(vim):
    } else {
      scrollup(wp, line_count, true);
    }
  }

  // If topline didn't change we need to restore w_botline and w_empty_rows
  // (we changed them).
  // If topline did change, update_screen() will set botline.
  if (wp->w_topline == old_topline && wp->w_skipcol == old_skipcol && set_topbot) {
    wp->w_botline = old_botline;
    wp->w_empty_rows = old_empty_rows;
    wp->w_valid = old_valid;
  }
  wp->w_valid |= VALID_TOPLINE;
  wp->w_viewport_invalid = true;

  // Make sure cursor is still visible after adjusting skipcol for "zb".
  if (set_topbot) {
    cursor_correct_sms(wp);
  }
}

/// Recompute topline to put the cursor halfway across the window
///
/// @param atend if true, also put the cursor halfway to the end of the file.
///
void scroll_cursor_halfway(win_T *wp, bool atend, bool prefer_above)
{
  linenr_T old_topline = wp->w_topline;
  lineoff_T loff = { .lnum = wp->w_cursor.lnum };
  lineoff_T boff = { .lnum = wp->w_cursor.lnum };
  hasFolding(wp, loff.lnum, &loff.lnum, &boff.lnum);
  int used = plines_win_nofill(wp, loff.lnum, true);
  loff.fill = 0;
  boff.fill = 0;
  linenr_T topline = loff.lnum;
  colnr_T skipcol = 0;

  int want_height;
  bool do_sms = wp->w_p_wrap && wp->w_p_sms;
  if (do_sms) {
    // 'smoothscroll' and 'wrap' are set
    if (atend) {
      want_height = (wp->w_view_height - used) / 2;
      used = 0;
    } else {
      want_height = wp->w_view_height;
    }
  }

  int topfill = 0;
  while (topline > 1) {
    // If using smoothscroll, we can precisely scroll to the
    // exact point where the cursor is halfway down the screen.
    if (do_sms) {
      topline_back_winheight(wp, &loff, false);
      if (loff.height == MAXCOL) {
        break;
      }
      used += loff.height;
      if (!atend && boff.lnum < wp->w_buffer->b_ml.ml_line_count) {
        botline_forw(wp, &boff);
        used += boff.height;
      }
      if (used > want_height) {
        if (used - loff.height < want_height) {
          topline = loff.lnum;
          topfill = loff.fill;
          skipcol = skipcol_from_plines(wp, used - want_height);
        }
        break;
      }
      topline = loff.lnum;
      topfill = loff.fill;
      continue;
    }

    // If not using smoothscroll, we have to iteratively find how many
    // lines to scroll down to roughly fit the cursor.
    // This may not be right in the middle if the lines'
    // physical height > 1 (e.g. 'wrap' is on).

    // Depending on "prefer_above" we add a line above or below first.
    // Loop twice to avoid duplicating code.
    bool done = false;
    int above = 0;
    int below = 0;
    for (int round = 1; round <= 2; round++) {
      if (prefer_above
          ? (round == 2 && below < above)
          : (round == 1 && below <= above)) {
        // add a line below the cursor
        if (boff.lnum < wp->w_buffer->b_ml.ml_line_count) {
          botline_forw(wp, &boff);
          used += boff.height;
          if (used > wp->w_view_height) {
            done = true;
            break;
          }
          below += boff.height;
        } else {
          below++;                    // count a "~" line
          if (atend) {
            used++;
          }
        }
      }

      if (prefer_above
          ? (round == 1 && below >= above)
          : (round == 1 && below > above)) {
        // add a line above the cursor
        topline_back(wp, &loff);
        if (loff.height == MAXCOL) {
          used = MAXCOL;
        } else {
          used += loff.height;
        }
        if (used > wp->w_view_height) {
          done = true;
          break;
        }
        above += loff.height;
        topline = loff.lnum;
        topfill = loff.fill;
      }
    }
    if (done) {
      break;
    }
  }

  if (!hasFolding(wp, topline, &wp->w_topline, NULL)
      && (wp->w_topline != topline || skipcol != 0 || wp->w_skipcol != 0)) {
    wp->w_topline = topline;
    if (skipcol != 0) {
      wp->w_skipcol = skipcol;
      redraw_later(wp, UPD_NOT_VALID);
    } else if (do_sms) {
      reset_skipcol(wp);
    }
  }
  wp->w_topfill = topfill;
  if (old_topline > wp->w_topline + wp->w_view_height) {
    wp->w_botfill = false;
  }
  check_topfill(wp, false);
  wp->w_valid &= ~(VALID_WROW|VALID_CROW|VALID_BOTLINE|VALID_BOTLINE_AP);
  wp->w_valid |= VALID_TOPLINE;
}

// Correct the cursor position so that it is in a part of the screen at least
// 'so' lines from the top and bottom, if possible.
// If not possible, put it at the same position as scroll_cursor_halfway().
// When called topline must be valid!
void cursor_correct(win_T *wp)
{
  // How many lines we would like to have above/below the cursor depends on
  // whether the first/last line of the file is on screen.
  int64_t above_wanted = get_scrolloff_value(wp);
  int64_t below_wanted = get_scrolloff_value(wp);
  if (mouse_dragging > 0) {
    above_wanted = mouse_dragging - 1;
    below_wanted = mouse_dragging - 1;
  }
  if (wp->w_topline == 1) {
    above_wanted = 0;
    int max_off = wp->w_view_height / 2;
    below_wanted = MIN(below_wanted, max_off);
  }
  validate_botline_win(wp);
  if (wp->w_botline == wp->w_buffer->b_ml.ml_line_count + 1
      && mouse_dragging == 0) {
    below_wanted = 0;
    int max_off = (wp->w_view_height - 1) / 2;
    above_wanted = MIN(above_wanted, max_off);
  }

  // If there are sufficient file-lines above and below the cursor, we can
  // return now.
  linenr_T cln = wp->w_cursor.lnum;  // Cursor Line Number
  if (cln >= wp->w_topline + above_wanted
      && cln < wp->w_botline - below_wanted
      && !win_lines_concealed(wp)) {
    return;
  }

  if (wp->w_p_sms && !wp->w_p_wrap) {
    // 'smoothscroll' is active
    if (wp->w_cline_height == wp->w_view_height) {
      // The cursor line just fits in the window, don't scroll.
      reset_skipcol(wp);
      return;
    }
    // TODO(vim): If the cursor line doesn't fit in the window then only adjust w_skipcol.
  }

  // Narrow down the area where the cursor can be put by taking lines from
  // the top and the bottom until:
  // - the desired context lines are found
  // - the lines from the top is past the lines from the bottom
  linenr_T topline = wp->w_topline;
  linenr_T botline = wp->w_botline - 1;
  // count filler lines as context
  int above = wp->w_topfill;  // screen lines above topline
  int below = wp->w_filler_rows;  // screen lines below botline
  while ((above < above_wanted || below < below_wanted) && topline < botline) {
    if (below < below_wanted && (below <= above || above >= above_wanted)) {
      below += plines_win_full(wp, botline, NULL, NULL, true, true);
      hasFolding(wp, botline, &botline, NULL);
      botline--;
    }
    if (above < above_wanted && (above < below || below >= below_wanted)) {
      above += plines_win_nofill(wp, topline, true);
      hasFolding(wp, topline, NULL, &topline);

      // Count filler lines below this line as context.
      if (topline < botline) {
        above += win_get_fill(wp, topline + 1);
      }
      topline++;
    }
  }
  if (topline == botline || botline == 0) {
    wp->w_cursor.lnum = topline;
  } else if (topline > botline) {
    wp->w_cursor.lnum = botline;
  } else {
    if (cln < topline && wp->w_topline > 1) {
      wp->w_cursor.lnum = topline;
      wp->w_valid &=
        ~(VALID_WROW|VALID_WCOL|VALID_CHEIGHT|VALID_CROW);
    }
    if (cln > botline && wp->w_botline <= wp->w_buffer->b_ml.ml_line_count) {
      wp->w_cursor.lnum = botline;
      wp->w_valid &=
        ~(VALID_WROW|VALID_WCOL|VALID_CHEIGHT|VALID_CROW);
    }
  }
  check_cursor_moved(wp);
  wp->w_valid |= VALID_TOPLINE;
  wp->w_viewport_invalid = true;
}

/// Decide how much overlap to use for page-up or page-down scrolling.
/// This is symmetric, so that doing both keeps the same lines displayed.
/// Three lines are examined:
///
///  before CTRL-F          after CTRL-F / before CTRL-B
///     etc.                    l1
///  l1 last but one line       ------------
///  l2 last text line          l2 top text line
///  -------------              l3 second text line
///  l3                            etc.
static int get_scroll_overlap(Direction dir)
{
  lineoff_T loff;
  int min_height = curwin->w_view_height - 2;

  validate_botline_win(curwin);
  if ((dir == BACKWARD && curwin->w_topline == 1)
      || (dir == FORWARD && curwin->w_botline > curbuf->b_ml.ml_line_count)) {
    return min_height + 2;  // no overlap, still handle 'smoothscroll'
  }

  loff.lnum = dir == FORWARD ? curwin->w_botline : curwin->w_topline - 1;
  loff.fill = win_get_fill(curwin, loff.lnum + (dir == BACKWARD))
              - (dir == FORWARD ? curwin->w_filler_rows : curwin->w_topfill);
  loff.height = loff.fill > 0 ? 1 : plines_win_nofill(curwin, loff.lnum, true);

  int h1 = loff.height;
  if (h1 > min_height) {
    return min_height + 2;  // no overlap
  }
  if (dir == FORWARD) {
    topline_back(curwin, &loff);
  } else {
    botline_forw(curwin, &loff);
  }

  int h2 = loff.height;
  if (h2 == MAXCOL || h2 + h1 > min_height) {
    return min_height + 2;  // no overlap
  }
  if (dir == FORWARD) {
    topline_back(curwin, &loff);
  } else {
    botline_forw(curwin, &loff);
  }

  int h3 = loff.height;
  if (h3 == MAXCOL || h3 + h2 > min_height) {
    return min_height + 2;  // no overlap
  }
  if (dir == FORWARD) {
    topline_back(curwin, &loff);
  } else {
    botline_forw(curwin, &loff);
  }

  int h4 = loff.height;
  if (h4 == MAXCOL || h4 + h3 + h2 > min_height || h3 + h2 + h1 > min_height) {
    return min_height + 1;  // 1 line overlap
  } else {
    return min_height;      // 2 lines overlap
  }
}

/// Scroll "count" lines with 'smoothscroll' in direction "dir". Return true
/// when scrolling happened. Adjust "curscount" for scrolling different amount
/// of lines when 'smoothscroll' is disabled.
static bool scroll_with_sms(Direction dir, int count, int *curscount)
{
  int prev_sms = curwin->w_p_sms;
  colnr_T prev_skipcol = curwin->w_skipcol;
  linenr_T prev_topline = curwin->w_topline;
  int prev_topfill = curwin->w_topfill;

  curwin->w_p_sms = true;
  scroll_redraw(dir == FORWARD, count);

  // Not actually smoothscrolling but ended up with partially visible line.
  // Continue scrolling until skipcol is zero.
  if (!prev_sms && curwin->w_skipcol > 0) {
    int fixdir = dir;
    // Reverse the scroll direction when topline already changed. One line
    // extra for scrolling backward so that consuming skipcol is symmetric.
    if (labs(curwin->w_topline - prev_topline) > (dir == BACKWARD)) {
      fixdir = dir * -1;
    }

    int width1 = curwin->w_view_width - win_col_off(curwin);
    int width2 = width1 + win_col_off2(curwin);
    count = 1 + (curwin->w_skipcol - width1 - 1) / width2;
    if (fixdir == FORWARD) {
      count = 1 + (linetabsize_eol(curwin, curwin->w_topline)
                   - curwin->w_skipcol - width1 + width2 - 1) / width2;
    }
    scroll_redraw(fixdir == FORWARD, count);
    *curscount += count * (fixdir == dir ? 1 : -1);
  }
  curwin->w_p_sms = prev_sms;

  return curwin->w_topline != prev_topline
         || curwin->w_topfill != prev_topfill
         || curwin->w_skipcol != prev_skipcol;
}

/// Move screen "count" (half) pages up ("dir" is BACKWARD) or down ("dir" is
/// FORWARD) and update the screen. Handle moving the cursor and not scrolling
/// to reveal end of buffer lines for half-page scrolling with CTRL-D and CTRL-U.
///
/// @return  FAIL for failure, OK otherwise.
int pagescroll(Direction dir, int count, bool half)
{
  bool did_move = false;
  int buflen = curbuf->b_ml.ml_line_count;
  colnr_T prev_col = curwin->w_cursor.col;
  colnr_T prev_curswant = curwin->w_curswant;
  linenr_T prev_lnum = curwin->w_cursor.lnum;
  oparg_T oa = { 0 };
  cmdarg_T ca = { 0 };
  ca.oap = &oa;

  if (half) {
    // Scroll [count], 'scroll' or current window height lines.
    if (count) {
      curwin->w_p_scr = MIN(curwin->w_view_height, count);
    }
    count = MIN(curwin->w_view_height, (int)curwin->w_p_scr);

    int curscount = count;
    // Adjust count so as to not reveal end of buffer lines.
    if (dir == FORWARD
        && (curwin->w_topline + curwin->w_view_height + count > buflen
            || win_lines_concealed(curwin))) {
      int n = plines_correct_topline(curwin, curwin->w_topline, NULL, false, NULL);
      if (n - count < curwin->w_view_height && curwin->w_topline < buflen) {
        n += plines_m_win(curwin, curwin->w_topline + 1, buflen, curwin->w_view_height + count);
      }
      if (n < curwin->w_view_height + count) {
        count = n - curwin->w_view_height;
      }
    }

    // (Try to) scroll the window unless already at the end of the buffer.
    if (count > 0) {
      did_move = scroll_with_sms(dir, count, &curscount);
      curwin->w_cursor.lnum = prev_lnum;
      curwin->w_cursor.col = prev_col;
      curwin->w_curswant = prev_curswant;
    }

    // Move the cursor the same amount of screen lines, skipping over
    // concealed lines as those were not included in "curscount".
    if (curwin->w_p_wrap) {
      nv_screengo(&oa, dir, curscount, true);
    } else if (dir == FORWARD) {
      cursor_down_inner(curwin, curscount, true);
    } else {
      cursor_up_inner(curwin, curscount, true);
    }
  } else {
    // Scroll [count] times 'window' or current window height lines.
    count *= ((ONE_WINDOW && p_window > 0 && p_window < Rows - 1)
              ? MAX(1, (int)p_window - 2) : get_scroll_overlap(dir));
    did_move = scroll_with_sms(dir, count, &count);

    if (did_move) {
      // Place cursor at top or bottom of window.
      validate_botline_win(curwin);
      linenr_T lnum = (dir == FORWARD ? curwin->w_topline : curwin->w_botline - 1);
      // In silent Ex mode the value of w_botline - 1 may be 0,
      // but cursor lnum needs to be at least 1.
      curwin->w_cursor.lnum = MAX(lnum, 1);
    }
  }

  if (get_scrolloff_value(curwin) > 0) {
    cursor_correct(curwin);
  }
  // Move cursor to first line of closed fold.
  foldAdjustCursor(curwin);

  did_move = did_move
             || prev_col != curwin->w_cursor.col
             || prev_lnum != curwin->w_cursor.lnum;

  // Error if both the viewport and cursor did not change.
  if (!did_move) {
    beep_flush();
  } else if (!curwin->w_p_sms) {
    beginline(BL_SOL | BL_FIX);
  } else if (p_sol) {
    nv_g_home_m_cmd(&ca);
  }

  return did_move ? OK : FAIL;
}

void do_check_cursorbind(void)
{
  static win_T *prev_curwin = NULL;
  static pos_T prev_cursor = { 0, 0, 0 };

  if (curwin == prev_curwin && equalpos(curwin->w_cursor, prev_cursor)) {
    return;
  }
  prev_curwin = curwin;
  prev_cursor = curwin->w_cursor;

  linenr_T line = curwin->w_cursor.lnum;
  colnr_T col = curwin->w_cursor.col;
  colnr_T coladd = curwin->w_cursor.coladd;
  colnr_T curswant = curwin->w_curswant;
  bool set_curswant = curwin->w_set_curswant;
  win_T *old_curwin = curwin;
  buf_T *old_curbuf = curbuf;
  int old_VIsual_select = VIsual_select;
  int old_VIsual_active = VIsual_active;

  // loop through the cursorbound windows
  VIsual_select = VIsual_active = false;
  FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
    curwin = wp;
    curbuf = curwin->w_buffer;
    // skip original window and windows with 'nocursorbind'
    if (curwin != old_curwin && curwin->w_p_crb) {
      if (curwin->w_p_diff) {
        curwin->w_cursor.lnum =
          diff_get_corresponding_line(old_curbuf, line);
      } else {
        curwin->w_cursor.lnum = line;
      }
      curwin->w_cursor.col = col;
      curwin->w_cursor.coladd = coladd;
      curwin->w_curswant = curswant;
      curwin->w_set_curswant = set_curswant;

      // Make sure the cursor is in a valid position.  Temporarily set
      // "restart_edit" to allow the cursor to be beyond the EOL.
      {
        int restart_edit_save = restart_edit;
        restart_edit = true;
        check_cursor(curwin);

        // Avoid a scroll here for the cursor position, 'scrollbind' is
        // more important.
        if (!curwin->w_p_scb) {
          validate_cursor(curwin);
        }

        restart_edit = restart_edit_save;
      }
      // Correct cursor for multi-byte character.
      mb_adjust_cursor();
      redraw_later(curwin, UPD_VALID);

      // Only scroll when 'scrollbind' hasn't done this.
      if (!curwin->w_p_scb) {
        update_topline(curwin);
      }
      curwin->w_redr_status = true;
    }
  }

  // reset current-window
  VIsual_select = old_VIsual_select;
  VIsual_active = old_VIsual_active;
  curwin = old_curwin;
  curbuf = old_curbuf;
}
