patch 9.2.0088: cannot display tabs for indentation

Commit: 
https://github.com/vim/vim/commit/8526d32647245b3b623986949e7807b4b353e624
Author: HarshK97 <[email protected]>
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 <[email protected]>
    Signed-off-by: zeertzjq <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

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,
 /**/

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/vim_dev/E1vwl5H-001jvq-F3%40256bit.org.

Raspunde prin e-mail lui