patch 9.2.0088: cannot display tabs for indentation
Commit:
https://github.com/vim/vim/commit/8526d32647245b3b623986949e7807b4b353e624
Author: HarshK97 <
harshka...@gmail.com>
Date: Sun Mar 1 17:50:27 2026 +0000
patch 9.2.0088: cannot display tabs for indentation
Problem: cannot display tabs for indentation
Solution: Add the "leadtab" value to the 'listchars' option to
distinguish between tabs used for indentation and tabs used
for alignment (HarshK97).
closes: #19094
Signed-off-by: HarshK97 <
harshka...@gmail.com>
Signed-off-by: zeertzjq <
zeer...@outlook.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index e1239855b..e2505b617 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -5808,6 +5808,15 @@ A jump table for the options with a short description can be found at |Q_op|.
---+---+--XXX ~
Where "XXX" denotes the first non-blank characters in
the line.
+ *lcs-leadtab*
+ leadtab:xy[z]
+ Like |lcs-tab|, but only for leading tabs. When
+ omitted, the "tab" setting is used for leading tabs.
+ |lcs-tab| must also be set for this to work. *E1572*
+ You can combine it with "tab:", for example:
+ `:set listchars=tab:>-,leadtab:.\ `
+ This shows leading tabs as periods(.) and other tabs
+ as ">--".
*lcs-trail*
trail:c Character to show for trailing spaces. When omitted,
trailing spaces are blank. Overrides the "space" and
diff --git a/runtime/doc/tags b/runtime/doc/tags
index 1cac3072d..e4478f0d7 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -4761,6 +4761,7 @@ E1569 builtin.txt /*E1569*
E157 sign.txt /*E157*
E1570 builtin.txt /*E1570*
E1571 builtin.txt /*E1571*
+E1572 options.txt /*E1572*
E158 sign.txt /*E158*
E159 sign.txt /*E159*
E16 cmdline.txt /*E16*
@@ -8851,6 +8852,7 @@ lcs-eol options.txt /*lcs-eol*
lcs-extends options.txt /*lcs-extends*
lcs-lead options.txt /*lcs-lead*
lcs-leadmultispace options.txt /*lcs-leadmultispace*
+lcs-leadtab options.txt /*lcs-leadtab*
lcs-multispace options.txt /*lcs-multispace*
lcs-nbsp options.txt /*lcs-nbsp*
lcs-precedes options.txt /*lcs-precedes*
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index 614051bc2..85a6c8032 100644
--- a/runtime/doc/version9.txt
+++ b/runtime/doc/version9.txt
@@ -52594,6 +52594,7 @@ Other ~
- |ConPTY| support is considered stable as of Windows 11.
- Support for "dap" channel mode for the |debug-adapter-protocol|.
- |status-line| can use several lines, see 'statuslineopt'.
+- New "leadtab" value for the 'listchars' setting.
*changed-9.3*
Changed~
diff --git a/src/drawline.c b/src/drawline.c
index e259e15e3..67e4a470f 100644
--- a/src/drawline.c
+++ b/src/drawline.c
@@ -1661,7 +1661,8 @@ win_line(
trailcol += (colnr_T)(ptr - line);
}
// find end of leading whitespace
- if (wp->w_lcs_chars.lead || wp->w_lcs_chars.leadmultispace != NULL)
+ if (wp->w_lcs_chars.lead || wp->w_lcs_chars.leadmultispace != NULL ||
+ wp->w_lcs_chars.leadtab1 != NUL)
{
leadcol = 0;
while (VIM_ISWHITE(ptr[leadcol]))
@@ -3260,6 +3261,18 @@ win_line(
{
int tab_len = 0;
long vcol_adjusted = wlv.vcol; // removed showbreak len
+ int lcs_tab1 = wp->w_lcs_chars.tab1;
+ int lcs_tab2 = wp->w_lcs_chars.tab2;
+ int lcs_tab3 = wp->w_lcs_chars.tab3;
+
+ // check if leadtab is set in 'listchars'
+ if (wp->w_p_list && wp->w_lcs_chars.leadtab1 != NUL &&
+ (leadcol == 0 || ptr < line + leadcol))
+ {
+ lcs_tab1 = wp->w_lcs_chars.leadtab1;
+ lcs_tab2 = wp->w_lcs_chars.leadtab2;
+ lcs_tab3 = wp->w_lcs_chars.leadtab3;
+ }
#ifdef FEAT_LINEBREAK
char_u *sbr = get_showbreak_value(wp);
@@ -3299,9 +3312,9 @@ win_line(
tab_len += wlv.vcol_off_co;
// boguscols before FIX_FOR_BOGUSCOLS macro from above
- if (wp->w_p_list && wp->w_lcs_chars.tab1
- && old_boguscols > 0
- && wlv.n_extra > tab_len)
+ if (wp->w_p_list && lcs_tab1 &&
+ old_boguscols > 0 &&
+ wlv.n_extra > tab_len)
tab_len += wlv.n_extra - tab_len;
# endif
if (tab_len > 0)
@@ -3309,14 +3322,13 @@ win_line(
// If wlv.n_extra > 0, it gives the number of chars
// to use for a tab, else we need to calculate the
// width for a tab.
- int tab2_len = mb_char2len(wp->w_lcs_chars.tab2);
+ int tab2_len = mb_char2len(lcs_tab2);
len = tab_len * tab2_len;
- if (wp->w_lcs_chars.tab3)
- len += mb_char2len(wp->w_lcs_chars.tab3)
- - tab2_len;
+ if (lcs_tab3)
+ len += mb_char2len(lcs_tab3) - tab2_len;
if (wlv.n_extra > 0)
len += wlv.n_extra - tab_len;
- c = wp->w_lcs_chars.tab1;
+ c = lcs_tab1;
p = alloc(len + 1);
if (p == NULL)
wlv.n_extra = 0;
@@ -3328,7 +3340,7 @@ win_line(
wlv.p_extra_free = p;
for (i = 0; i < tab_len; i++)
{
- int lcs = wp->w_lcs_chars.tab2;
+ int lcs = lcs_tab2;
if (*p == NUL)
{
@@ -3338,9 +3350,8 @@ win_line(
// if tab3 is given, use it for the last
// char
- if (wp->w_lcs_chars.tab3
- && i == tab_len - 1)
- lcs = wp->w_lcs_chars.tab3;
+ if (lcs_tab3 && i == tab_len - 1)
+ lcs = lcs_tab3;
p += mb_char2bytes(lcs, p);
wlv.n_extra += mb_char2len(lcs)
- (saved_nextra > 0 ? 1 : 0);
@@ -3380,17 +3391,16 @@ win_line(
mb_utf8 = FALSE; // don't draw as UTF-8
if (wp->w_p_list)
{
- c = (wlv.n_extra == 0 && wp->w_lcs_chars.tab3)
- ? wp->w_lcs_chars.tab3
- : wp->w_lcs_chars.tab1;
+ c = (wlv.n_extra == 0 && lcs_tab3) ? lcs_tab3
+ : lcs_tab1;
#ifdef FEAT_LINEBREAK
if (wp->w_p_lbr && wlv.p_extra != NULL
&& *wlv.p_extra != NUL)
wlv.c_extra = NUL; // using p_extra from above
else
#endif
- wlv.c_extra = wp->w_lcs_chars.tab2;
- wlv.c_final = wp->w_lcs_chars.tab3;
+ wlv.c_extra = lcs_tab2;
+ wlv.c_final = lcs_tab3;
n_attr = tab_len + 1;
wlv.extra_attr = hl_combine_attr(wlv.win_attr,
HL_ATTR(HLF_8));
diff --git a/src/errors.h b/src/errors.h
index e767613f3..b909c0fbf 100644
--- a/src/errors.h
+++ b/src/errors.h
@@ -3805,3 +3805,5 @@ EXTERN char e_cannot_add_redraw_listener_in_listener_callback[]
EXTERN char e_no_redraw_listener_callbacks_defined[]
INIT(= N_("E1571: Must specify at least one callback for redraw_listener_add"));
#endif
+EXTERN char e_leadtab_requires_tab[]
+ INIT(= N_("E1572: 'listchars' field \"leadtab\" requires \"tab\" to be specified"));
diff --git a/src/screen.c b/src/screen.c
index 1e9c9a7d5..716715558 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -5002,6 +5002,7 @@ static struct charstab lcstab[] =
CHARSTAB_ENTRY(&lcs_chars.prec, "precedes"),
CHARSTAB_ENTRY(&
lcs_chars.space, "space"),
CHARSTAB_ENTRY(&lcs_chars.tab2, "tab"),
+ CHARSTAB_ENTRY(&lcs_chars.leadtab2, "leadtab"),
CHARSTAB_ENTRY(&lcs_chars.trail, "trail"),
CHARSTAB_ENTRY(&lcs_chars.lead, "lead"),
#ifdef FEAT_CONCEAL
@@ -5073,6 +5074,8 @@ set_chars_option(win_T *wp, char_u *value, int is_listchars, int apply,
*(tab[i].cp) = NUL;
lcs_chars.tab1 = NUL;
lcs_chars.tab3 = NUL;
+ lcs_chars.leadtab1 = NUL;
+ lcs_chars.leadtab3 = NUL;
if (multispace_len > 0)
{
@@ -5208,7 +5211,8 @@ set_chars_option(win_T *wp, char_u *value, int is_listchars, int apply,
return field_value_err(errbuf, errbuflen,
e_wrong_character_width_for_field_str,
tab[i].name.string);
- if (tab[i].cp == &lcs_chars.tab2)
+ if (tab[i].cp == &lcs_chars.tab2 ||
+ tab[i].cp == &lcs_chars.leadtab2)
{
if (*s == NUL)
return field_value_err(errbuf, errbuflen,
@@ -5239,9 +5243,14 @@ set_chars_option(win_T *wp, char_u *value, int is_listchars, int apply,
lcs_chars.tab2 = c2;
lcs_chars.tab3 = c3;
}
+ else if (tab[i].cp == &lcs_chars.leadtab2)
+ {
+ lcs_chars.leadtab1 = c1;
+ lcs_chars.leadtab2 = c2;
+ lcs_chars.leadtab3 = c3;
+ }
else if (tab[i].cp != NULL)
*(tab[i].cp) = c1;
-
}
p = s;
break;
@@ -5260,6 +5269,9 @@ set_chars_option(win_T *wp, char_u *value, int is_listchars, int apply,
}
}
+ if (is_listchars && lcs_chars.leadtab2 != NUL && lcs_chars.tab2 == NUL)
+ return e_leadtab_requires_tab;
+
if (apply)
{
if (is_listchars)
diff --git a/src/structs.h b/src/structs.h
index f2c1188a9..1fdc2ee1b 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -3916,6 +3916,9 @@ typedef struct
int tab3;
int trail;
int lead;
+ int leadtab1;
+ int leadtab2;
+ int leadtab3;
int *multispace;
int *leadmultispace;
#ifdef FEAT_CONCEAL
diff --git a/src/testdir/test_listchars.vim b/src/testdir/test_listchars.vim
index 38963fa48..d22e9d0cf 100644
--- a/src/testdir/test_listchars.vim
+++ b/src/testdir/test_listchars.vim
@@ -347,6 +347,91 @@ func Test_listchars()
call Check_listchars(expected, 5, -1, 6)
call assert_equal(expected, split(execute("%list"), "
"))
+ " Test leadtab basic functionality
+ normal ggdG
+ set listchars=tab:>-,leadtab:+*
+ set list
+ call append(0, [
+ \ " text",
+ \ " text",
+ \ "text tab"
+ \ ])
+ let expected = [
+ \ '+*******text ',
+ \ '+*******+*******text',
+ \ 'text>---tab '
+ \ ]
+ call Check_listchars(expected, 3, 20)
+
+ " Test leadtab with unicode characters
+ normal ggdG
+ set listchars=tab:>-,leadtab:├─┤
+ call append(0, [" text"])
+ let expected = ['├──────┤text']
+ call Check_listchars(expected, 1, 12)
+
+ " Test leadtab with mixed indentation (spaces + tabs)
+ normal ggdG
+ set listchars=tab:>-,leadtab:+*,space:.
+ call append(0, [" text"])
+ let expected = ['.+******.text']
+ call Check_listchars(expected, 1, 13)
+
+ " Test leadtab with pipe character
+ normal ggdG
+ set listchars=tab:>-,leadtab:\|\
+ call append(0, [" text"])
+ let expected = ['| text']
+ call Check_listchars(expected, 1, 12)
+
+ " Test leadtab with unicode bar
+ normal ggdG
+ set listchars=tab:>-,leadtab:│\
+ call append(0, [" text"])
+ let expected = ['│ text']
+ call Check_listchars(expected, 1, 12)
+
+ " Test leadtab vs tab distinction (leading vs non-leading)
+ normal ggdG
+ set listchars=tab:>-,leadtab:+*
+ call append(0, [
+ \ " leading",
+ \ "text not leading",
+ \ " multiple leading"
+ \ ])
+ let expected = [
+ \ '+*******leading ',
+ \ 'text>---not leading ',
+ \ '+*******+*******multiple leading'
+ \ ]
+ call Check_listchars(expected, 3, 32)
+
+ " Test leadtab with trail and space
+ normal ggdG
+ set listchars=tab:>-,leadtab:+*,trail:<,space:.
+ call append(0, [
+ \ " text ",
+ \ " text",
+ \ " text "
+ \ ])
+ let expected = [
+ \ '+*******text<< ',
+ \ '..+*****text ',
+ \ '+*******..text<<'
+ \ ]
+ call Check_listchars(expected, 3, 16)
+
+ " Test leadtab with eol
+ normal ggdG
+ set listchars=tab:>-,leadtab:+*,eol:$
+ call append(0, [" text", "text tab"])
+ let expected = [
+ \ '+*******text$',
+ \ 'text>---tab$ '
+ \ ]
+ call Check_listchars(expected, 2, 13)
+
+
" test nbsp
normal ggdG
set listchars=nbsp:X,trail:Y
diff --git a/src/testdir/test_options.vim b/src/testdir/test_options.vim
index b2ca4d943..58b7620c3 100644
--- a/src/testdir/test_options.vim
+++ b/src/testdir/test_options.vim
@@ -625,7 +625,9 @@ func Test_set_completion_string_values()
call assert_equal('eol', getcompletion('set listchars+=', 'cmdline')[0])
call assert_equal(['multispace', 'leadmultispace'], getcompletion('set listchars+=', 'cmdline')[-2:])
+ call assert_equal(['tab', 'leadtab'], getcompletion('set listchars+=', 'cmdline')[5:6])
call assert_equal('eol', getcompletion('setl listchars+=', 'cmdline')[0])
+ call assert_equal(['tab', 'leadtab'], getcompletion('setl listchars+=', 'cmdline')[5:6])
call assert_equal(['multispace', 'leadmultispace'], getcompletion('setl listchars+=', 'cmdline')[-2:])
call assert_equal('stl', getcompletion('set fillchars+=', 'cmdline')[0])
call assert_equal('stl', getcompletion('setl fillchars+=', 'cmdline')[0])
diff --git a/src/testdir/util/gen_opt_test.vim b/src/testdir/util/gen_opt_test.vim
index c39642cd6..071a158eb 100644
--- a/src/testdir/util/gen_opt_test.vim
+++ b/src/testdir/util/gen_opt_test.vim
@@ -248,10 +248,10 @@ let test_values = {
\ 'langmap': [['', 'xX', 'aA,bB'], ['xxx']],
\ 'lispoptions': [['', 'expr:0', 'expr:1'], ['xxx', 'expr:x', 'expr:']],
\ 'listchars': [['', 'eol:x', 'tab:xy', 'tab:xyz', 'space:x',
- \ 'multispace:xxxy', 'lead:x', 'leadmultispace:xxxy', 'trail:x',
- \ 'extends:x', 'precedes:x', 'conceal:x', 'nbsp:x', 'eol:\x24',
- \ 'eol:\u21b5', 'eol:\U000021b5', 'eol:x,space:y'],
- \ ['xxx', 'eol:']],
+ \ 'multispace:xxxy', 'lead:x', 'tab:xy,leadtab:xyz', 'leadmultispace:xxxy',
+ \ 'trail:x', 'extends:x', 'precedes:x', 'conceal:x', 'nbsp:x',
+ \ 'eol:\x24', 'eol:\u21b5', 'eol:\U000021b5', 'eol:x,space:y'],
+ \ ['xxx', 'eol:', 'leadtab:xyz']],
\ 'matchpairs': [['', '(:)', '(:),<:>'], ['xxx']],
\ 'maxsearchcount': [[1, 10, 100, 1000], [0, -1, 10000]],
\ 'messagesopt': [['hit-enter,history:1', 'hit-enter,history:10000',
diff --git a/src/version.c b/src/version.c
index 9400e1826..8dde23bf0 100644
--- a/src/version.c
+++ b/src/version.c
@@ -734,6 +734,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 88,
/**/
87,
/**/