commit 033f1123cd9132d211bca874fc9f42b9e50e122e
parent 8d3742ae8da4988f073be29b42dbee401c19f8f7
Author: Jaehwang Jung <tomtomjhj@gmail.com>
Date: Mon, 22 Dec 2025 18:03:50 +0900
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>
Diffstat:
4 files changed, 90 insertions(+), 75 deletions(-)
diff --git a/src/nvim/eval.c b/src/nvim/eval.c
@@ -5260,14 +5260,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;
@@ -5279,7 +5282,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;
}
@@ -5291,9 +5294,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.
@@ -5328,18 +5331,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;
}
@@ -5349,7 +5352,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;
}
@@ -5359,31 +5362,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;
diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c
@@ -670,33 +670,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;
}
@@ -704,11 +698,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;
@@ -718,10 +712,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
@@ -2039,7 +2029,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);
@@ -3935,22 +3925,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) {
@@ -7665,39 +7651,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++;
}
@@ -7710,10 +7690,6 @@ theend:
} else {
rettv->vval.v_number = vcol_end;
}
-
- if (winchanged) {
- restore_win_noblock(&switchwin, true);
- }
}
/// "visualmode()" function
diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c
@@ -4229,7 +4229,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;
}
diff --git a/test/functional/ui/decorations_spec.lua b/test/functional/ui/decorations_spec.lua
@@ -3571,6 +3571,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()