vim-patch:8.2.3424: a sequence of spaces is hard to see in list mode

Problem:    A sequence of spaces is hard to see in list mode.
Solution:   Add the "multispace" option to 'listchars'. (closes vim/vim#8834)
f14b8ba137
This commit is contained in:
zeertzjq
2021-09-19 13:13:44 +08:00
parent 177e87525a
commit 51567db4b6
6 changed files with 251 additions and 28 deletions

View File

@@ -3751,16 +3751,25 @@ A jump table for the options with a short description can be found at |Q_op|.
*lcs-space* *lcs-space*
space:c Character to show for a space. When omitted, spaces space:c Character to show for a space. When omitted, spaces
are left blank. are left blank.
*lcs-multispace*
multispace:c...
One or more characters to use cyclically to show for
multiple consecutive spaces. Overrides the "space"
setting, except for single spaces. When omitted, the
"space" setting is used. For example,
`:set listchars=multispace:---+` shows ten consecutive
spaces as:
---+---+--
*lcs-lead* *lcs-lead*
lead:c Character to show for leading spaces. When omitted, lead:c Character to show for leading spaces. When omitted,
leading spaces are blank. Overrides the "space" leading spaces are blank. Overrides the "space" and
setting for leading spaces. You can combine it with "multispace" settings for leading spaces. You can
"tab:", for example: > combine it with "tab:", for example: >
:set listchars+=tab:>-,lead:. :set listchars+=tab:>-,lead:.
< *lcs-trail* < *lcs-trail*
trail:c Character to show for trailing spaces. When omitted, trail:c Character to show for trailing spaces. When omitted,
trailing spaces are blank. Overrides the "space" trailing spaces are blank. Overrides the "space" and
setting for trailing spaces. "multispace" settings for trailing spaces.
*lcs-extends* *lcs-extends*
extends:c Character to show in the last column, when 'wrap' is extends:c Character to show in the last column, when 'wrap' is
off and the line continues beyond the right of the off and the line continues beyond the right of the
@@ -3785,7 +3794,8 @@ A jump table for the options with a short description can be found at |Q_op|.
:set lcs=tab:>-,eol:<,nbsp:% :set lcs=tab:>-,eol:<,nbsp:%
:set lcs=extends:>,precedes:< :set lcs=extends:>,precedes:<
< |hl-NonText| highlighting will be used for "eol", "extends" and < |hl-NonText| highlighting will be used for "eol", "extends" and
"precedes". |hl-Whitespace| for "nbsp", "space", "tab" and "trail". "precedes". |hl-Whitespace| for "nbsp", "space", "tab", "multispace",
"lead" and "trail".
*'lpl'* *'nolpl'* *'loadplugins'* *'noloadplugins'* *'lpl'* *'nolpl'* *'loadplugins'* *'noloadplugins'*
'loadplugins' 'lpl' boolean (default on) 'loadplugins' 'lpl' boolean (default on)

View File

@@ -1214,6 +1214,7 @@ struct window_S {
int tab3; ///< third tab character int tab3; ///< third tab character
int lead; int lead;
int trail; int trail;
int *multispace;
int conceal; int conceal;
} w_p_lcs_chars; } w_p_lcs_chars;

View File

@@ -1707,6 +1707,8 @@ void msg_prt_line(char_u *s, int list)
int n; int n;
int attr = 0; int attr = 0;
char_u *lead = NULL; char_u *lead = NULL;
bool in_multispace = false;
int multispace_pos = 0;
char_u *trail = NULL; char_u *trail = NULL;
int l; int l;
@@ -1771,6 +1773,10 @@ void msg_prt_line(char_u *s, int list)
} else { } else {
attr = 0; attr = 0;
c = *s++; c = *s++;
in_multispace = c == ' ' && ((col > 0 && s[-2] == ' ') || *s == ' ');
if (!in_multispace) {
multispace_pos = 0;
}
if (c == TAB && (!list || curwin->w_p_lcs_chars.tab1)) { if (c == TAB && (!list || curwin->w_p_lcs_chars.tab1)) {
// tab amount depends on current column // tab amount depends on current column
n_extra = tabstop_padding(col, n_extra = tabstop_padding(col,
@@ -1786,11 +1792,11 @@ void msg_prt_line(char_u *s, int list)
: curwin->w_p_lcs_chars.tab1; : curwin->w_p_lcs_chars.tab1;
c_extra = curwin->w_p_lcs_chars.tab2; c_extra = curwin->w_p_lcs_chars.tab2;
c_final = curwin->w_p_lcs_chars.tab3; c_final = curwin->w_p_lcs_chars.tab3;
attr = HL_ATTR(HLF_8); attr = HL_ATTR(HLF_0);
} }
} else if (c == 160 && list && curwin->w_p_lcs_chars.nbsp != NUL) { } else if (c == 160 && list && curwin->w_p_lcs_chars.nbsp != NUL) {
c = curwin->w_p_lcs_chars.nbsp; c = curwin->w_p_lcs_chars.nbsp;
attr = HL_ATTR(HLF_8); attr = HL_ATTR(HLF_0);
} else if (c == NUL && list && curwin->w_p_lcs_chars.eol != NUL) { } else if (c == NUL && list && curwin->w_p_lcs_chars.eol != NUL) {
p_extra = (char_u *)""; p_extra = (char_u *)"";
c_extra = NUL; c_extra = NUL;
@@ -1807,16 +1813,24 @@ void msg_prt_line(char_u *s, int list)
c = *p_extra++; c = *p_extra++;
/* Use special coloring to be able to distinguish <hex> from /* Use special coloring to be able to distinguish <hex> from
* the same in plain text. */ * the same in plain text. */
attr = HL_ATTR(HLF_8); attr = HL_ATTR(HLF_0);
} else if (c == ' ' && lead != NULL && s <= lead) { } else if (c == ' ') {
c = curwin->w_p_lcs_chars.lead; if (lead != NULL && s <= lead) {
attr = HL_ATTR(HLF_8); c = curwin->w_p_lcs_chars.lead;
} else if (c == ' ' && trail != NULL && s > trail) { attr = HL_ATTR(HLF_0);
c = curwin->w_p_lcs_chars.trail; } else if (trail != NULL && s > trail) {
attr = HL_ATTR(HLF_8); c = curwin->w_p_lcs_chars.trail;
} else if (c == ' ' && list && curwin->w_p_lcs_chars.space != NUL) { attr = HL_ATTR(HLF_0);
c = curwin->w_p_lcs_chars.space; } else if (list && in_multispace && curwin->w_p_lcs_chars.multispace != NULL) {
attr = HL_ATTR(HLF_8); c = curwin->w_p_lcs_chars.multispace[multispace_pos++];
if (curwin->w_p_lcs_chars.multispace[multispace_pos] == NUL) {
multispace_pos = 0;
}
attr = HL_ATTR(HLF_0);
} else if (list && curwin->w_p_lcs_chars.space != NUL) {
c = curwin->w_p_lcs_chars.space;
attr = HL_ATTR(HLF_0);
}
} }
} }

View File

@@ -3442,6 +3442,8 @@ static char_u *set_chars_option(win_T *wp, char_u **varp, bool set)
int c1; int c1;
int c2 = 0; int c2 = 0;
int c3 = 0; int c3 = 0;
char_u *last_multispace; // Last occurrence of "multispace:"
int multispace_len = 0; // Length of lcs-multispace string
struct chars_tab { struct chars_tab {
int *cp; ///< char value int *cp; ///< char value
@@ -3511,6 +3513,15 @@ static char_u *set_chars_option(win_T *wp, char_u **varp, bool set)
if (varp == &p_lcs || varp == &wp->w_p_lcs) { if (varp == &p_lcs || varp == &wp->w_p_lcs) {
wp->w_p_lcs_chars.tab1 = NUL; wp->w_p_lcs_chars.tab1 = NUL;
wp->w_p_lcs_chars.tab3 = NUL; wp->w_p_lcs_chars.tab3 = NUL;
if (wp->w_p_lcs_chars.multispace != NULL) {
xfree(wp->w_p_lcs_chars.multispace);
}
if (multispace_len > 0) {
wp->w_p_lcs_chars.multispace = xmalloc((size_t)(multispace_len + 1) * sizeof(int));
wp->w_p_lcs_chars.multispace[multispace_len] = NUL;
} else {
wp->w_p_lcs_chars.multispace = NULL;
}
} }
} }
p = *varp; p = *varp;
@@ -3527,22 +3538,22 @@ static char_u *set_chars_option(win_T *wp, char_u **varp, bool set)
int c1len = utf_ptr2len(s); int c1len = utf_ptr2len(s);
c1 = mb_cptr2char_adv((const char_u **)&s); c1 = mb_cptr2char_adv((const char_u **)&s);
if (mb_char2cells(c1) > 1 || (c1len == 1 && c1 > 127)) { if (mb_char2cells(c1) > 1 || (c1len == 1 && c1 > 127)) {
continue; return e_invarg;
} }
if (tab[i].cp == &wp->w_p_lcs_chars.tab2) { if (tab[i].cp == &wp->w_p_lcs_chars.tab2) {
if (*s == NUL) { if (*s == NUL) {
continue; return e_invarg;
} }
int c2len = utf_ptr2len(s); int c2len = utf_ptr2len(s);
c2 = mb_cptr2char_adv((const char_u **)&s); c2 = mb_cptr2char_adv((const char_u **)&s);
if (mb_char2cells(c2) > 1 || (c2len == 1 && c2 > 127)) { if (mb_char2cells(c2) > 1 || (c2len == 1 && c2 > 127)) {
continue; return e_invarg;
} }
if (!(*s == ',' || *s == NUL)) { if (!(*s == ',' || *s == NUL)) {
int c3len = utf_ptr2len(s); int c3len = utf_ptr2len(s);
c3 = mb_cptr2char_adv((const char_u **)&s); c3 = mb_cptr2char_adv((const char_u **)&s);
if (mb_char2cells(c3) > 1 || (c3len == 1 && c3 > 127)) { if (mb_char2cells(c3) > 1 || (c3len == 1 && c3 > 127)) {
continue; return e_invarg;
} }
} }
} }
@@ -3563,7 +3574,42 @@ static char_u *set_chars_option(win_T *wp, char_u **varp, bool set)
} }
if (i == entries) { if (i == entries) {
return e_invarg; len = STRLEN("multispace");
if ((varp == &p_lcs || varp == &wp->w_p_lcs)
&& STRNCMP(p, "multispace", len) == 0
&& p[len] == ':'
&& p[len + 1] != NUL) {
s = p + len + 1;
if (round == 0) {
// Get length of lcs-multispace string in the first round
last_multispace = p;
multispace_len = 0;
while (*s != NUL && *s != ',') {
int c1len = utf_ptr2len(s);
c1 = mb_cptr2char_adv((const char_u **)&s);
if (mb_char2cells(c1) > 1 || (c1len == 1 && c1 > 127)) {
return e_invarg;
}
multispace_len++;
}
if (multispace_len == 0) {
// lcs-multispace cannot be an empty string
return e_invarg;
}
p = s;
} else {
int multispace_pos = 0;
while (*s != NUL && *s != ',') {
c1 = mb_cptr2char_adv((const char_u **)&s);
if (p == last_multispace) {
wp->w_p_lcs_chars.multispace[multispace_pos++] = c1;
}
}
p = s;
}
} else {
return e_invarg;
}
} }
if (*p == ',') { if (*p == ',') {
p++; p++;

View File

@@ -2081,6 +2081,8 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool noc
int change_end = -1; // last col of changed area int change_end = -1; // last col of changed area
colnr_T trailcol = MAXCOL; // start of trailing spaces colnr_T trailcol = MAXCOL; // start of trailing spaces
colnr_T leadcol = 0; // start of leading spaces colnr_T leadcol = 0; // start of leading spaces
bool in_multispace = false; // in multiple consecutive spaces
int multispace_pos = 0; // position in lcs-multispace string
bool need_showbreak = false; // overlong line, skip first x chars bool need_showbreak = false; // overlong line, skip first x chars
sign_attrs_T sattrs[SIGN_SHOW_MAX]; // attributes for signs sign_attrs_T sattrs[SIGN_SHOW_MAX]; // attributes for signs
int num_signs; // number of signs for line int num_signs; // number of signs for line
@@ -2462,6 +2464,7 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool noc
if (wp->w_p_list && !has_fold && !end_fill) { if (wp->w_p_list && !has_fold && !end_fill) {
if (wp->w_p_lcs_chars.space if (wp->w_p_lcs_chars.space
|| wp->w_p_lcs_chars.multispace != NULL
|| wp->w_p_lcs_chars.trail || wp->w_p_lcs_chars.trail
|| wp->w_p_lcs_chars.lead || wp->w_p_lcs_chars.lead
|| wp->w_p_lcs_chars.nbsp) { || wp->w_p_lcs_chars.nbsp) {
@@ -3580,6 +3583,11 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool noc
} }
} }
in_multispace = c == ' ' && ((ptr > line + 1 && ptr[-2] == ' ') || *ptr == ' ');
if (!in_multispace) {
multispace_pos = 0;
}
// 'list': Change char 160 to 'nbsp' and space to 'space'. // 'list': Change char 160 to 'nbsp' and space to 'space'.
// But not when the character is followed by a composing // But not when the character is followed by a composing
// character (use mb_l to check that). // character (use mb_l to check that).
@@ -3591,10 +3599,18 @@ static int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool noc
&& wp->w_p_lcs_chars.nbsp) && wp->w_p_lcs_chars.nbsp)
|| (c == ' ' || (c == ' '
&& mb_l == 1 && mb_l == 1
&& wp->w_p_lcs_chars.space && (wp->w_p_lcs_chars.space
|| (in_multispace && wp->w_p_lcs_chars.multispace != NULL))
&& ptr - line >= leadcol && ptr - line >= leadcol
&& ptr - line <= trailcol))) { && ptr - line <= trailcol))) {
c = (c == ' ') ? wp->w_p_lcs_chars.space : wp->w_p_lcs_chars.nbsp; if (in_multispace && wp->w_p_lcs_chars.multispace != NULL) {
c = wp->w_p_lcs_chars.multispace[multispace_pos++];
if (wp->w_p_lcs_chars.multispace[multispace_pos] == NUL) {
multispace_pos = 0;
}
} else {
c = (c == ' ') ? wp->w_p_lcs_chars.space : wp->w_p_lcs_chars.nbsp;
}
n_attr = 1; n_attr = 1;
extra_attr = win_hl_attr(wp, HLF_0); extra_attr = win_hl_attr(wp, HLF_0);
saved_attr2 = char_attr; // save current attr saved_attr2 = char_attr; // save current attr

View File

@@ -140,6 +140,93 @@ func Test_listchars()
call assert_equal(expected, split(execute("%list"), "\n")) call assert_equal(expected, split(execute("%list"), "\n"))
" Test multispace
normal ggdG
set listchars=eol:$
set listchars+=multispace:yYzZ
set list
call append(0, [
\ ' ffff ',
\ ' i i gg',
\ ' h ',
\ ' j ',
\ ' 0 0 ',
\ ])
let expected = [
\ 'yYzZffffyYzZ$',
\ 'yYi iyYzZygg$',
\ ' hyYzZyYzZyY$',
\ 'yYzZyYzZyYj $',
\ 'yYzZ0yY0yYzZ$',
\ '$'
\ ]
redraw!
for i in range(1, 5)
call cursor(i, 1)
call assert_equal([expected[i - 1]], ScreenLines(i, virtcol('$')))
endfor
call assert_equal(expected, split(execute("%list"), "\n"))
" the last occurrence of 'multispace:' is used
set listchars+=space:x,multispace:XyY
let expected = [
\ 'XyYXffffXyYX$',
\ 'XyixiXyYXygg$',
\ 'xhXyYXyYXyYX$',
\ 'XyYXyYXyYXjx$',
\ 'XyYX0Xy0XyYX$',
\ '$'
\ ]
redraw!
for i in range(1, 5)
call cursor(i, 1)
call assert_equal([expected[i - 1]], ScreenLines(i, virtcol('$')))
endfor
call assert_equal(expected, split(execute("%list"), "\n"))
set listchars+=lead:>,trail:<
let expected = [
\ '>>>>ffff<<<<$',
\ '>>ixiXyYXygg$',
\ '>h<<<<<<<<<<$',
\ '>>>>>>>>>>j<$',
\ '>>>>0Xy0<<<<$',
\ '$'
\ ]
redraw!
for i in range(1, 5)
call cursor(i, 1)
call assert_equal([expected[i - 1]], ScreenLines(i, virtcol('$')))
endfor
call assert_equal(expected, split(execute("%list"), "\n"))
" removing 'multispace:'
set listchars-=multispace:XyY
set listchars-=multispace:yYzZ
let expected = [
\ '>>>>ffff<<<<$',
\ '>>ixixxxxxgg$',
\ '>h<<<<<<<<<<$',
\ '>>>>>>>>>>j<$',
\ '>>>>0xx0<<<<$',
\ '$'
\ ]
redraw!
for i in range(1, 5)
call cursor(i, 1)
call assert_equal([expected[i - 1]], ScreenLines(i, virtcol('$')))
endfor
call assert_equal(expected, split(execute("%list"), "\n"))
" test nbsp " test nbsp
normal ggdG normal ggdG
set listchars=nbsp:X,trail:Y set listchars=nbsp:X,trail:Y
@@ -189,20 +276,69 @@ func Test_listchars_unicode()
set encoding=utf-8 set encoding=utf-8
set ff=unix set ff=unix
set listchars=eol:⇔,space:␣,nbsp:≠,tab:←↔→ set listchars=eol:⇔,space:␣,multispace:≡≢≣,nbsp:≠,tab:←↔→
set list set list
let nbsp = nr2char(0xa0) let nbsp = nr2char(0xa0)
call append(0, ["a\tb c" .. nbsp .. "d"]) call append(0, [" a\tb c" .. nbsp .. "d "])
let expected = ['a←↔↔↔↔↔→b␣c≠d⇔'] let expected = ['≡≢≣≡≢≣≡≢a←↔↔↔↔↔→b␣c≠d≡≢⇔']
redraw! redraw!
call cursor(1, 1) call cursor(1, 1)
call assert_equal(expected, ScreenLines(1, virtcol('$'))) call assert_equal(expected, ScreenLines(1, virtcol('$')))
set listchars+=lead:⇨,trail:⇦
let expected = ['⇨⇨⇨⇨⇨⇨⇨⇨a←↔↔↔↔↔→b␣c≠d⇦⇦⇔']
redraw!
call cursor(1, 1)
call assert_equal(expected, ScreenLines(1, virtcol('$')))
let &encoding=oldencoding let &encoding=oldencoding
enew! enew!
set listchars& ff& set listchars& ff&
endfunction endfunction
func Test_listchars_invalid()
enew!
set ff=unix
set listchars=eol:$
set list
set ambiwidth=double
" No colon
call assert_fails('set listchars=x', 'E474:')
call assert_fails('set listchars=x', 'E474:')
call assert_fails('set listchars=multispace', 'E474:')
" Too short
call assert_fails('set listchars=space:', 'E474:')
call assert_fails('set listchars=tab:x', 'E474:')
call assert_fails('set listchars=multispace:', 'E474:')
" One occurrence too short
call assert_fails('set listchars=space:,space:x', 'E474:')
call assert_fails('set listchars=space:x,space:', 'E474:')
call assert_fails('set listchars=tab:x,tab:xx', 'E474:')
call assert_fails('set listchars=tab:xx,tab:x', 'E474:')
call assert_fails('set listchars=multispace:,multispace:x', 'E474:')
call assert_fails('set listchars=multispace:x,multispace:', 'E474:')
" Too long
call assert_fails('set listchars=space:xx', 'E474:')
call assert_fails('set listchars=tab:xxxx', 'E474:')
" Has non-single width character
call assert_fails('set listchars=space:·', 'E474:')
call assert_fails('set listchars=tab:·x', 'E474:')
call assert_fails('set listchars=tab:x·', 'E474:')
call assert_fails('set listchars=tab:xx·', 'E474:')
call assert_fails('set listchars=multispace:·', 'E474:')
call assert_fails('set listchars=multispace:xxx·', 'E474:')
enew!
set ambiwidth& listchars& ff&
endfunction
" Tests that space characters following composing character won't get replaced " Tests that space characters following composing character won't get replaced
" by listchars. " by listchars.
func Test_listchars_composing() func Test_listchars_composing()