fix(marks): wrong line('w$', win) with conceal_lines (#37047)

Background:
Suppose a window has concealed lines, and sets conceallevel>2,
concealcursor="". The concealed lines are displayed if the window is
curwin and the cursor is on the those lines.

Problem:
line('w$', win) switches curwin to win, and then does validate_botline
for curwin. It computes botline assuming the concealed lines displayed,
resulting in a smaller value than the actual botline that the user sees.

Solution:
Evaluate line('w$', win) without switching curwin.
Apply similar changes to other functions that switches curwin.

Co-authored-by: zeertzjq <zeertzjq@outlook.com>
(cherry picked from commit 033f1123cd)
This commit is contained in:
Jaehwang Jung
2025-12-22 18:03:50 +09:00
committed by github-actions[bot]
parent d1cd79a4b6
commit 124c18261c
4 changed files with 90 additions and 75 deletions

View File

@@ -6431,14 +6431,17 @@ int buf_charidx_to_byteidx(buf_T *buf, linenr_T lnum, int charidx)
/// @param[in] dollar_lnum True when "$" is last line.
/// @param[out] ret_fnum Set to fnum for marks.
/// @param[in] charcol True to return character column.
/// @param[in] wp Window for which to get the position.
///
/// @return Pointer to position or NULL in case of error (e.g. invalid type).
pos_T *var2fpos(const typval_T *const tv, const bool dollar_lnum, int *const ret_fnum,
const bool charcol)
const bool charcol, win_T *wp)
FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL
{
static pos_T pos;
buf_T *bp = wp->w_buffer;
// Argument can be [lnum, col, coladd].
if (tv->v_type == VAR_LIST) {
bool error = false;
@@ -6450,7 +6453,7 @@ pos_T *var2fpos(const typval_T *const tv, const bool dollar_lnum, int *const ret
// Get the line number.
pos.lnum = (linenr_T)tv_list_find_nr(l, 0, &error);
if (error || pos.lnum <= 0 || pos.lnum > curbuf->b_ml.ml_line_count) {
if (error || pos.lnum <= 0 || pos.lnum > bp->b_ml.ml_line_count) {
// Invalid line number.
return NULL;
}
@@ -6462,9 +6465,9 @@ pos_T *var2fpos(const typval_T *const tv, const bool dollar_lnum, int *const ret
}
int len;
if (charcol) {
len = mb_charlen(ml_get(pos.lnum));
len = mb_charlen(ml_get_buf(bp, pos.lnum));
} else {
len = ml_get_len(pos.lnum);
len = ml_get_buf_len(bp, pos.lnum);
}
// We accept "$" for the column number: last column.
@@ -6499,18 +6502,18 @@ pos_T *var2fpos(const typval_T *const tv, const bool dollar_lnum, int *const ret
pos.lnum = 0;
if (name[0] == '.') {
// cursor
pos = curwin->w_cursor;
pos = wp->w_cursor;
} else if (name[0] == 'v' && name[1] == NUL) {
// Visual start
if (VIsual_active) {
if (VIsual_active && wp == curwin) {
pos = VIsual;
} else {
pos = curwin->w_cursor;
pos = wp->w_cursor;
}
} else if (name[0] == '\'') {
// mark
int mname = (uint8_t)name[1];
const fmark_T *const fm = mark_get(curbuf, curwin, NULL, kMarkAll, mname);
const fmark_T *const fm = mark_get(bp, wp, NULL, kMarkAll, mname);
if (fm == NULL || fm->mark.lnum <= 0) {
return NULL;
}
@@ -6520,7 +6523,7 @@ pos_T *var2fpos(const typval_T *const tv, const bool dollar_lnum, int *const ret
}
if (pos.lnum != 0) {
if (charcol) {
pos.col = buf_byteidx_to_charidx(curbuf, pos.lnum, pos.col);
pos.col = buf_byteidx_to_charidx(bp, pos.lnum, pos.col);
}
return &pos;
}
@@ -6530,31 +6533,31 @@ pos_T *var2fpos(const typval_T *const tv, const bool dollar_lnum, int *const ret
if (name[0] == 'w' && dollar_lnum) {
// the "w_valid" flags are not reset when moving the cursor, but they
// do matter for update_topline() and validate_botline().
check_cursor_moved(curwin);
check_cursor_moved(wp);
pos.col = 0;
if (name[1] == '0') { // "w0": first visible line
update_topline(curwin);
update_topline(wp);
// In silent Ex mode topline is zero, but that's not a valid line
// number; use one instead.
pos.lnum = curwin->w_topline > 0 ? curwin->w_topline : 1;
pos.lnum = wp->w_topline > 0 ? wp->w_topline : 1;
return &pos;
} else if (name[1] == '$') { // "w$": last visible line
validate_botline(curwin);
validate_botline(wp);
// In silent Ex mode botline is zero, return zero then.
pos.lnum = curwin->w_botline > 0 ? curwin->w_botline - 1 : 0;
pos.lnum = wp->w_botline > 0 ? wp->w_botline - 1 : 0;
return &pos;
}
} else if (name[0] == '$') { // last column or line
if (dollar_lnum) {
pos.lnum = curbuf->b_ml.ml_line_count;
pos.lnum = bp->b_ml.ml_line_count;
pos.col = 0;
} else {
pos.lnum = curwin->w_cursor.lnum;
pos.lnum = wp->w_cursor.lnum;
if (charcol) {
pos.col = (colnr_T)mb_charlen(get_cursor_line_ptr());
pos.col = (colnr_T)mb_charlen(ml_get_buf(bp, wp->w_cursor.lnum));
} else {
pos.col = get_cursor_line_len();
pos.col = ml_get_buf_len(bp, wp->w_cursor.lnum);
}
}
return &pos;

View File

@@ -718,33 +718,27 @@ static void get_col(typval_T *argvars, typval_T *rettv, bool charcol)
return;
}
switchwin_T switchwin;
bool winchanged = false;
win_T *wp = curwin;
if (argvars[1].v_type != VAR_UNKNOWN) {
// use the window specified in the second argument
tabpage_T *tp;
win_T *wp = win_id2wp_tp((int)tv_get_number(&argvars[1]), &tp);
wp = win_id2wp_tp((int)tv_get_number(&argvars[1]), &tp);
if (wp == NULL || tp == NULL) {
return;
}
if (switch_win_noblock(&switchwin, wp, tp, true) != OK) {
return;
}
check_cursor(curwin);
winchanged = true;
check_cursor(wp);
}
buf_T *bp = wp->w_buffer;
colnr_T col = 0;
int fnum = curbuf->b_fnum;
pos_T *fp = var2fpos(&argvars[0], false, &fnum, charcol);
if (fp != NULL && fnum == curbuf->b_fnum) {
int fnum = bp->b_fnum;
pos_T *fp = var2fpos(&argvars[0], false, &fnum, charcol, wp);
if (fp != NULL && fnum == bp->b_fnum) {
if (fp->col == MAXCOL) {
// '> can be MAXCOL, get the length of the line then
if (fp->lnum <= curbuf->b_ml.ml_line_count) {
col = ml_get_len(fp->lnum) + 1;
if (fp->lnum <= bp->b_ml.ml_line_count) {
col = ml_get_buf_len(bp, fp->lnum) + 1;
} else {
col = MAXCOL;
}
@@ -752,11 +746,11 @@ static void get_col(typval_T *argvars, typval_T *rettv, bool charcol)
col = fp->col + 1;
// col(".") when the cursor is on the NUL at the end of the line
// because of "coladd" can be seen as an extra column.
if (virtual_active(curwin) && fp == &curwin->w_cursor) {
char *p = get_cursor_pos_ptr();
if (curwin->w_cursor.coladd >=
(colnr_T)win_chartabsize(curwin, p,
curwin->w_virtcol - curwin->w_cursor.coladd)) {
if (virtual_active(wp) && fp == &wp->w_cursor) {
char *p = ml_get_buf(bp, wp->w_cursor.lnum) + wp->w_cursor.col;
if (wp->w_cursor.coladd >=
(colnr_T)win_chartabsize(wp, p,
wp->w_virtcol - wp->w_cursor.coladd)) {
int l;
if (*p != NUL && p[(l = utfc_ptr2len(p))] == NUL) {
col += l;
@@ -766,10 +760,6 @@ static void get_col(typval_T *argvars, typval_T *rettv, bool charcol)
}
}
rettv->vval.v_number = col;
if (winchanged) {
restore_win_noblock(&switchwin, true);
}
}
/// "charcol()" function
@@ -2460,7 +2450,7 @@ static void getpos_both(typval_T *argvars, typval_T *rettv, bool getcurpos, bool
fp = &pos;
}
} else {
fp = var2fpos(&argvars[0], true, &fnum, charcol);
fp = var2fpos(&argvars[0], true, &fnum, charcol, curwin);
}
list_T *const l = tv_list_alloc_ret(rettv, 4 + getcurpos);
@@ -4435,22 +4425,18 @@ static void f_line(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
tabpage_T *tp;
win_T *wp = win_id2wp_tp(id, &tp);
if (wp != NULL && tp != NULL) {
switchwin_T switchwin;
if (switch_win_noblock(&switchwin, wp, tp, true) == OK) {
// With 'splitkeep' != cursor and in diff mode, prevent that the
// window scrolls and keep the topline.
if (*p_spk != 'c' || (curwin->w_p_diff && switchwin.sw_curwin->w_p_diff)) {
skip_update_topline = true;
}
check_cursor(curwin);
fp = var2fpos(&argvars[0], true, &fnum, false);
// With 'splitkeep' != cursor and in diff mode, prevent that the
// window scrolls and keep the topline.
if (*p_spk != 'c' || (wp->w_p_diff && curwin->w_p_diff)) {
skip_update_topline = true;
}
check_cursor(wp);
fp = var2fpos(&argvars[0], true, &fnum, false, wp);
skip_update_topline = false;
restore_win_noblock(&switchwin, true);
}
} else {
// use current window
fp = var2fpos(&argvars[0], true, &fnum, false);
fp = var2fpos(&argvars[0], true, &fnum, false, curwin);
}
if (fp != NULL) {
@@ -8245,39 +8231,33 @@ static void f_virtcol(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{
colnr_T vcol_start = 0;
colnr_T vcol_end = 0;
switchwin_T switchwin;
bool winchanged = false;
win_T *wp = curwin;
if (argvars[1].v_type != VAR_UNKNOWN && argvars[2].v_type != VAR_UNKNOWN) {
// use the window specified in the third argument
tabpage_T *tp;
win_T *wp = win_id2wp_tp((int)tv_get_number(&argvars[2]), &tp);
wp = win_id2wp_tp((int)tv_get_number(&argvars[2]), &tp);
if (wp == NULL || tp == NULL) {
goto theend;
}
if (switch_win_noblock(&switchwin, wp, tp, true) != OK) {
goto theend;
}
check_cursor(curwin);
winchanged = true;
check_cursor(wp);
}
int fnum = curbuf->b_fnum;
pos_T *fp = var2fpos(&argvars[0], false, &fnum, false);
if (fp != NULL && fp->lnum <= curbuf->b_ml.ml_line_count
&& fnum == curbuf->b_fnum) {
buf_T *bp = wp->w_buffer;
int fnum = bp->b_fnum;
pos_T *fp = var2fpos(&argvars[0], false, &fnum, false, wp);
if (fp != NULL && fp->lnum <= bp->b_ml.ml_line_count
&& fnum == bp->b_fnum) {
// Limit the column to a valid value, getvvcol() doesn't check.
if (fp->col < 0) {
fp->col = 0;
} else {
const colnr_T len = ml_get_len(fp->lnum);
const colnr_T len = ml_get_buf_len(bp, fp->lnum);
if (fp->col > len) {
fp->col = len;
}
}
getvvcol(curwin, fp, &vcol_start, NULL, &vcol_end);
getvvcol(wp, fp, &vcol_start, NULL, &vcol_end);
vcol_start++;
vcol_end++;
}
@@ -8290,10 +8270,6 @@ theend:
} else {
rettv->vval.v_number = vcol_end;
}
if (winchanged) {
restore_win_noblock(&switchwin, true);
}
}
/// "visualmode()" function

View File

@@ -4235,7 +4235,7 @@ linenr_T tv_get_lnum(const typval_T *const tv)
if (lnum <= 0 && did_emsg_before == did_emsg && tv->v_type != VAR_NUMBER) {
int fnum;
// No valid number, try using same function as line() does.
pos_T *const fp = var2fpos(tv, true, &fnum, false);
pos_T *const fp = var2fpos(tv, true, &fnum, false, curwin);
if (fp != NULL) {
lnum = fp->lnum;
}

View File

@@ -3088,6 +3088,42 @@ describe('extmark decorations', function()
]]
})
end)
it('line("w$", win) considers conceal_lines', function()
api.nvim_buf_set_lines(0, 0, -1, true, { 'line 1', 'line 2', 'line 3' })
api.nvim_buf_set_extmark(0, ns, 0, 0, { conceal_lines = '' }) -- conceal line 1
local win = exec_lua(function()
local provider_ns = vim.api.nvim_create_namespace('test_f_line')
_G.line_w_dollar = {}
vim.api.nvim_set_decoration_provider(provider_ns, {
on_start = function()
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
table.insert(_G.line_w_dollar, { win, vim.fn.line('w$', win) })
end
end,
})
local win = vim.api.nvim_open_win(0, false, {
relative = 'editor',
width = 20,
height = 1,
row = 0,
col = 0,
border = 'single',
})
vim.api.nvim_set_option_value('conceallevel', 2, { scope = 'local', win = win })
return win
end)
local line_w_dollar = exec_lua('return _G.line_w_dollar')
for _, win_line in ipairs(line_w_dollar) do
if win_line[1] == win then
eq(2, win_line[2])
end
end
end)
end)
describe('decorations: inline virtual text', function()