fix(scrollbind): properly take filler/virtual lines into account

Problem:

`'scrollbind'` does not work properly if the window being scrolled
automatically contains any filler/virtual lines (except for diff filler
lines).

This is because when the scrollbind check is done, the logic only
considers changes to topline which are represented as line numbers.

Solution:

Write the logic for determine the scroll amount to take into account
filler/virtual lines.

Fixes #29751
This commit is contained in:
Lewis Russell
2024-07-17 12:23:15 +01:00
committed by Lewis Russell
parent c9b129a02a
commit 573a71469d
11 changed files with 538 additions and 46 deletions

View File

@@ -887,7 +887,8 @@ bool decor_redraw_eol(win_T *wp, DecorState *state, int *eol_attr, int eol_col)
static const uint32_t lines_filter[4] = {[kMTMetaLines] = kMTFilterSelect };
int decor_virt_lines(win_T *wp, linenr_T lnum, VirtLines *lines)
/// @param apply_folds Only count virtual lines that are not in folds.
int decor_virt_lines(win_T *wp, int start_row, int end_row, VirtLines *lines, bool apply_folds)
{
buf_T *buf = wp->w_buffer;
if (!buf_meta_total(buf, kMTMetaLines)) {
@@ -896,15 +897,14 @@ int decor_virt_lines(win_T *wp, linenr_T lnum, VirtLines *lines)
return 0;
}
assert(lnum > 0);
int row = lnum - 1;
MarkTreeIter itr[1] = { 0 };
if (!marktree_itr_get_filter(buf->b_marktree, MAX(row - 1, 0), 0, row + 1, 0,
if (!marktree_itr_get_filter(buf->b_marktree, MAX(start_row - 1, 0), 0, end_row, 0,
lines_filter, itr)) {
return 0;
}
assert(start_row >= 0);
int virt_lines = 0;
while (true) {
MTKey mark = marktree_itr_current(itr);
@@ -915,7 +915,8 @@ int decor_virt_lines(win_T *wp, linenr_T lnum, VirtLines *lines)
bool above = vt->flags & kVTLinesAbove;
int mrow = mark.pos.row;
int draw_row = mrow + (above ? 0 : 1);
if (draw_row == row && !hasFolding(wp, mrow + 1, NULL, NULL)) {
if (draw_row >= start_row && draw_row < end_row
&& (!apply_folds || !hasFolding(wp, mrow + 1, NULL, NULL))) {
virt_lines += (int)kv_size(vt->data.virt_lines);
if (lines) {
kv_splice(*lines, vt->data.virt_lines);
@@ -926,7 +927,7 @@ int decor_virt_lines(win_T *wp, linenr_T lnum, VirtLines *lines)
}
}
if (!marktree_itr_next_filter(buf->b_marktree, itr, row + 1, 0, lines_filter)) {
if (!marktree_itr_next_filter(buf->b_marktree, itr, end_row, 0, lines_filter)) {
break;
}
}

View File

@@ -2143,7 +2143,11 @@ int diff_check_with_linestatus(win_T *wp, linenr_T lnum, int *linestatus)
return 0;
}
if (!dp->is_linematched && diff_linematch(dp)) {
// Don't run linematch when lnum is offscreen.
// Useful for scrollbind calculations which need to count all the filler lines
// above the screen.
if (lnum >= wp->w_topline && lnum < wp->w_botline
&& !dp->is_linematched && diff_linematch(dp)) {
run_linematch_algorithm(dp);
}

View File

@@ -1156,7 +1156,7 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, s
area_highlighting = true;
}
VirtLines virt_lines = KV_INITIAL_VALUE;
wlv.n_virt_lines = decor_virt_lines(wp, lnum, &virt_lines);
wlv.n_virt_lines = decor_virt_lines(wp, lnum - 1, lnum, &virt_lines, true);
wlv.filler_lines += wlv.n_virt_lines;
if (lnum == wp->w_topline) {
wlv.filler_lines = wp->w_topfill;

View File

@@ -2695,7 +2695,7 @@ int do_ecmd(int fnum, char *ffname, char *sfname, exarg_T *eap, linenr_T newlnum
*so_ptr = 999; // force cursor to be vertically centered in the window
}
update_topline(curwin);
curwin->w_scbind_pos = curwin->w_topline;
curwin->w_scbind_pos = plines_m_win_fill(curwin, 1, curwin->w_topline);
*so_ptr = n;
redraw_curbuf_later(UPD_NOT_VALID); // redraw this buffer later
}

View File

@@ -81,6 +81,7 @@
#include "nvim/os/os_defs.h"
#include "nvim/os/shell.h"
#include "nvim/path.h"
#include "nvim/plines.h"
#include "nvim/popupmenu.h"
#include "nvim/pos_defs.h"
#include "nvim/profile.h"
@@ -5580,39 +5581,43 @@ static void ex_swapname(exarg_T *eap)
/// (1998-11-02 16:21:01 R. Edward Ralston <eralston@computer.org>)
static void ex_syncbind(exarg_T *eap)
{
linenr_T topline;
linenr_T vtopline; // Target topline (including fill)
linenr_T old_linenr = curwin->w_cursor.lnum;
setpcmark();
// determine max topline
// determine max (virtual) topline
if (curwin->w_p_scb) {
topline = curwin->w_topline;
vtopline = get_vtopline(curwin);
FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
if (wp->w_p_scb && wp->w_buffer) {
topline = MIN(topline, wp->w_buffer->b_ml.ml_line_count - get_scrolloff_value(curwin));
linenr_T y = plines_m_win_fill(wp, 1, wp->w_buffer->b_ml.ml_line_count)
- get_scrolloff_value(curwin);
vtopline = MIN(vtopline, y);
}
}
topline = MAX(topline, 1);
vtopline = MAX(vtopline, 1);
} else {
topline = 1;
vtopline = 1;
}
// Set all scrollbind windows to the same topline.
FOR_ALL_WINDOWS_IN_TAB(wp, curtab) {
if (wp->w_p_scb) {
int y = topline - wp->w_topline;
int y = vtopline - get_vtopline(wp);
if (y > 0) {
scrollup(wp, y, true);
} else {
scrolldown(wp, -y, true);
}
wp->w_scbind_pos = topline;
wp->w_scbind_pos = vtopline;
redraw_later(wp, UPD_VALID);
cursor_correct(wp);
wp->w_redr_status = true;
}
}
if (curwin->w_p_scb) {
did_syncbind = true;
checkpcmark();

View File

@@ -2092,17 +2092,23 @@ static void display_showcmd(void)
grid_line_flush();
}
int get_vtopline(win_T *wp)
{
return plines_m_win_fill(wp, 1, wp->w_topline) - wp->w_topfill;
}
/// When "check" is false, prepare for commands that scroll the window.
/// When "check" is true, take care of scroll-binding after the window has
/// scrolled. Called from normal_cmd() and edit().
void do_check_scrollbind(bool check)
{
static win_T *old_curwin = NULL;
static linenr_T old_topline = 0;
static int old_topfill = 0;
static linenr_T old_vtopline = 0;
static buf_T *old_buf = NULL;
static colnr_T old_leftcol = 0;
int vtopline = get_vtopline(curwin);
if (check && curwin->w_p_scb) {
// If a ":syncbind" command was just used, don't scroll, only reset
// the values.
@@ -2115,10 +2121,9 @@ void do_check_scrollbind(bool check)
if ((curwin->w_buffer == old_buf
|| curwin->w_p_diff
)
&& (curwin->w_topline != old_topline
|| curwin->w_topfill != old_topfill
&& (vtopline != old_vtopline
|| curwin->w_leftcol != old_leftcol)) {
check_scrollbind(curwin->w_topline - old_topline, curwin->w_leftcol - old_leftcol);
check_scrollbind(vtopline - old_vtopline, curwin->w_leftcol - old_leftcol);
}
} else if (vim_strchr(p_sbo, 'j')) { // jump flag set in 'scrollopt'
// When switching between windows, make sure that the relative
@@ -2129,14 +2134,13 @@ void do_check_scrollbind(bool check)
// resync is performed, some of the other 'scrollbind' windows may
// need to jump so that the current window's relative position is
// visible on-screen.
check_scrollbind(curwin->w_topline - (linenr_T)curwin->w_scbind_pos, 0);
check_scrollbind(vtopline - curwin->w_scbind_pos, 0);
}
curwin->w_scbind_pos = curwin->w_topline;
curwin->w_scbind_pos = vtopline;
}
old_curwin = curwin;
old_topline = curwin->w_topline;
old_topfill = curwin->w_topfill;
old_vtopline = vtopline;
old_buf = curwin->w_buffer;
old_leftcol = curwin->w_leftcol;
}
@@ -2144,20 +2148,18 @@ void do_check_scrollbind(bool check)
/// Synchronize any windows that have "scrollbind" set, based on the
/// number of rows by which the current window has changed
/// (1998-11-02 16:21:01 R. Edward Ralston <eralston@computer.org>)
void check_scrollbind(linenr_T topline_diff, int leftcol_diff)
void check_scrollbind(linenr_T vtopline_diff, int leftcol_diff)
{
win_T *old_curwin = curwin;
buf_T *old_curbuf = curbuf;
int old_VIsual_select = VIsual_select;
int old_VIsual_active = VIsual_active;
colnr_T tgt_leftcol = curwin->w_leftcol;
linenr_T topline;
linenr_T y;
// check 'scrollopt' string for vertical and horizontal scroll options
bool want_ver = (vim_strchr(p_sbo, 'v') && topline_diff != 0);
want_ver |= old_curwin->w_p_diff;
bool want_hor = (vim_strchr(p_sbo, 'h') && (leftcol_diff || topline_diff != 0));
bool want_ver = old_curwin->w_p_diff
|| (vim_strchr(p_sbo, 'v') && vtopline_diff != 0);
bool want_hor = (vim_strchr(p_sbo, 'h') && (leftcol_diff || vtopline_diff != 0));
// loop through the scrollbound windows and scroll accordingly
VIsual_select = VIsual_active = 0;
@@ -2174,16 +2176,19 @@ void check_scrollbind(linenr_T topline_diff, int leftcol_diff)
if (old_curwin->w_p_diff && curwin->w_p_diff) {
diff_set_topline(old_curwin, curwin);
} else {
curwin->w_scbind_pos += topline_diff;
topline = (linenr_T)curwin->w_scbind_pos;
if (topline > curbuf->b_ml.ml_line_count) {
topline = curbuf->b_ml.ml_line_count;
}
if (topline < 1) {
topline = 1;
}
curwin->w_scbind_pos += vtopline_diff;
int curr_vtopline = get_vtopline(curwin);
y = topline - curwin->w_topline;
// Perf: reuse curr_vtopline to reduce the time in plines_m_win_fill().
// Equivalent to:
// int max_vtopline = plines_m_win_fill(curwin, 1, curbuf->b_ml.ml_line_count);
int max_vtopline = curr_vtopline + curwin->w_topfill
+ plines_m_win_fill(curwin, curwin->w_topline + 1,
curbuf->b_ml.ml_line_count);
int new_vtopline = MAX(MIN((linenr_T)curwin->w_scbind_pos, max_vtopline), 1);
int y = new_vtopline - curr_vtopline;
if (y > 0) {
scrollup(curwin, y, false);
} else {

View File

@@ -88,6 +88,7 @@
#include "nvim/os/os.h"
#include "nvim/os/os_defs.h"
#include "nvim/path.h"
#include "nvim/plines.h"
#include "nvim/popupmenu.h"
#include "nvim/pos_defs.h"
#include "nvim/regexp.h"
@@ -2474,7 +2475,7 @@ static const char *did_set_scrollbind(optset_T *args)
return NULL;
}
do_check_scrollbind(false);
win->w_scbind_pos = win->w_topline;
win->w_scbind_pos = get_vtopline(win);
return NULL;
}

View File

@@ -712,7 +712,7 @@ bool win_may_fill(win_T *wp)
/// @return Number of filler lines above lnum
int win_get_fill(win_T *wp, linenr_T lnum)
{
int virt_lines = decor_virt_lines(wp, lnum, NULL);
int virt_lines = decor_virt_lines(wp, lnum - 1, lnum, NULL, true);
// be quick when there are no filler lines
if (diffopt_filler()) {
@@ -906,6 +906,25 @@ int plines_m_win(win_T *wp, linenr_T first, linenr_T last, int max)
return MIN(max, count);
}
/// Return number of window lines a physical line range will occupy.
/// Only considers real and filler lines.
///
/// Mainly used for calculating scrolling offsets.
int plines_m_win_fill(win_T *wp, linenr_T first, linenr_T last)
{
int count = last - first + 1 + decor_virt_lines(wp, first - 1, last, NULL, false);
if (diffopt_filler()) {
for (int lnum = first; lnum <= last; lnum++) {
// Note: this also considers folds.
int n = diff_check(wp, lnum);
count += MAX(n, 0);
}
}
return MAX(count, 0);
}
/// Get the number of screen lines a range of text will take in window "wp".
///
/// @param[in] start_lnum Starting line number, 1-based inclusive.