" Tests for 'listchars' display with 'list' and :list

source check.vim
source view_util.vim
source screendump.vim

func Check_listchars(expected, end_lnum, end_scol = -1, leftcol = 0)
  if a:leftcol > 0
    let save_wrap = &wrap
    set nowrap
    call cursor(1, 1)
    exe 'normal! ' .. a:leftcol .. 'zl'
  endif

  redraw!
  for i in range(1, a:end_lnum)
    if a:leftcol > 0
      let col = virtcol2col(0, i, a:leftcol)
      let col += getline(i)->strpart(col - 1, 1, v:true)->len()
      call cursor(i, col)
      redraw
      call assert_equal(a:leftcol, winsaveview().leftcol)
    else
      call cursor(i, 1)
    end

    let end_scol = a:end_scol < 0 ? '$'->virtcol() - a:leftcol : a:end_scol
    call assert_equal([a:expected[i - 1]->strcharpart(a:leftcol)],
          \ ScreenLines(i, end_scol))
  endfor

  if a:leftcol > 0
    let &wrap = save_wrap
  endif
endfunc

func Test_listchars()
  enew!
  set ff=unix
  set list

  set listchars+=tab:>-,space:.,trail:<
  call append(0, [
	      \ '	aa	',
	      \ '  bb	  ',
	      \ '   cccc	 ',
	      \ 'dd        ee  	',
	      \ ' '
	      \ ])
  let expected = [
	      \ '>-------aa>-----$',
	      \ '..bb>---<<$',
	      \ '...cccc><$',
	      \ 'dd........ee<<>-$',
	      \ '<$'
	      \ ]
  call Check_listchars(expected, 5)
  call Check_listchars(expected, 4, -1, 5)

  set listchars-=trail:<
  let expected = [
	      \ '>-------aa>-----$',
	      \ '..bb>---..$',
	      \ '...cccc>.$',
	      \ 'dd........ee..>-$',
	      \ '.$'
	      \ ]
  call Check_listchars(expected, 5)
  call Check_listchars(expected, 4, -1, 5)

  " tab with 3rd character.
  set listchars-=tab:>-
  set listchars+=tab:<=>,trail:-
  let expected = [
	      \ '<======>aa<====>$',
	      \ '..bb<==>--$',
	      \ '...cccc>-$',
	      \ 'dd........ee--<>$',
	      \ '-$'
	      \ ]
  call Check_listchars(expected, 5)
  call Check_listchars(expected, 4, -1, 5)

  " tab with 3rd character and linebreak set
  set listchars-=tab:<=>
  set listchars+=tab:<·>
  set linebreak
  let expected = [
	      \ '<······>aa<····>$',
	      \ '..bb<··>--$',
	      \ '...cccc>-$',
	      \ 'dd........ee--<>$',
	      \ '-$'
	      \ ]
  call Check_listchars(expected, 5)
  set nolinebreak
  set listchars-=tab:<·>
  set listchars+=tab:<=>

  set listchars-=trail:-
  let expected = [
	      \ '<======>aa<====>$',
	      \ '..bb<==>..$',
	      \ '...cccc>.$',
	      \ 'dd........ee..<>$',
	      \ '.$'
	      \ ]
  call Check_listchars(expected, 5)
  call Check_listchars(expected, 4, -1, 5)

  set listchars-=tab:<=>
  set listchars+=tab:>-
  set listchars+=trail:<
  set nolist
  normal ggdG
  call append(0, [
	      \ '  fff	  ',
	      \ '	gg	',
	      \ '     h	',
	      \ 'iii    	  ',
	      \ ])
  let l = split(execute("%list"), "\n")
  call assert_equal([
	      \ '..fff>--<<$',
	      \ '>-------gg>-----$',
	      \ '.....h>-$',
	      \ 'iii<<<<><<$',
	      \ '$'], l)

  " Test lead and trail
  normal ggdG
  set listchars&
  set listchars+=lead:>,trail:<,space:x
  set list

  call append(0, [
	      \ '    ffff    ',
	      \ '          gg',
	      \ 'h           ',
	      \ '            ',
	      \ '    0  0    ',
	      \ ])

  let expected = [
	      \ '>>>>ffff<<<<$',
	      \ '>>>>>>>>>>gg$',
	      \ 'h<<<<<<<<<<<$',
	      \ '<<<<<<<<<<<<$',
	      \ '>>>>0xx0<<<<$',
	      \ '$'
	      \ ]
  call Check_listchars(expected, 6)
  call Check_listchars(expected, 5, -1, 6)
  call assert_equal(expected, split(execute("%list"), "\n"))

  " Test multispace
  normal ggdG
  set listchars&
  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$',
	      \ '$'
	      \ ]
  call Check_listchars(expected, 6)
  call Check_listchars(expected, 5, -1, 6)
  call assert_equal(expected, split(execute("%list"), "\n"))

  " Test leadmultispace + multispace
  normal ggdG
  set listchars=eol:$,multispace:yYzZ,nbsp:S
  set listchars+=leadmultispace:.-+*
  set list

  call append(0, [
	      \ '    ffff    ',
	      \ '  i i     gg',
	      \ ' h          ',
	      \ '          j ',
	      \ '    0  0    ',
	      \ ])

  let expected = [
	      \ '.-+*ffffyYzZ$',
	      \ '.-i iSyYzZgg$',
	      \ ' hyYzZyYzZyY$',
	      \ '.-+*.-+*.-j $',
	      \ '.-+*0yY0yYzZ$',
	      \ '$'
	      \ ]
  call assert_equal('eol:$,multispace:yYzZ,nbsp:S,leadmultispace:.-+*', &listchars)
  call Check_listchars(expected, 6)
  call Check_listchars(expected, 5, -1, 1)
  call Check_listchars(expected, 5, -1, 2)
  call Check_listchars(expected, 5, -1, 3)
  call Check_listchars(expected, 5, -1, 6)
  call assert_equal(expected, split(execute("%list"), "\n"))

  " Test leadmultispace without multispace
  normal ggdG
  set listchars-=multispace:yYzZ
  set listchars+=space:+,trail:>,eol:$
  set list

  call append(0, [
	      \ '    ffff    ',
	      \ '  i i     gg',
	      \ ' h          ',
	      \ '          j ',
	      \ '    0  0    ',
	      \ ])

  let expected = [
	      \ '.-+*ffff>>>>$',
	      \ '.-i+i+++++gg$',
	      \ '+h>>>>>>>>>>$',
	      \ '.-+*.-+*.-j>$',
	      \ '.-+*0++0>>>>$',
	      \ '$'
	      \ ]
  call assert_equal('eol:$,nbsp:S,leadmultispace:.-+*,space:+,trail:>,eol:$', &listchars)
  call Check_listchars(expected, 6)
  call Check_listchars(expected, 5, -1, 1)
  call Check_listchars(expected, 5, -1, 2)
  call Check_listchars(expected, 5, -1, 3)
  call Check_listchars(expected, 5, -1, 6)
  call assert_equal(expected, split(execute("%list"), "\n"))

  " Test leadmultispace only
  normal ggdG
  set listchars&
  set listchars=leadmultispace:.-+*
  set list

  call append(0, [
	      \ '    ffff    ',
	      \ '  i i     gg',
	      \ ' h          ',
	      \ '          j ',
	      \ '    0  0    ',
	      \ ])

  let expected = [
	      \ '.-+*ffff    ',
	      \ '.-i i     gg',
	      \ ' h          ',
	      \ '.-+*.-+*.-j ',
	      \ '.-+*0  0    ',
	      \ ' '
	      \ ]
  call assert_equal('leadmultispace:.-+*', &listchars)
  call Check_listchars(expected, 5, 12)
  call assert_equal(expected, split(execute("%list"), "\n"))

  " Changing the value of 'ambiwidth' twice shouldn't cause double-free when
  " "leadmultispace" is specified.
  set ambiwidth=double
  set ambiwidth&

  " Test leadmultispace and lead and space
  normal ggdG
  set listchars&
  set listchars+=lead:<,space:-
  set listchars+=leadmultispace:.-+*
  set list

  call append(0, [
	      \ '    ffff    ',
	      \ '  i i     gg',
	      \ ' h          ',
	      \ '          j ',
	      \ '    0  0    ',
	      \ ])

  let expected = [
	      \ '.-+*ffff----$',
	      \ '.-i-i-----gg$',
	      \ '<h----------$',
	      \ '.-+*.-+*.-j-$',
	      \ '.-+*0--0----$',
	      \ '$'
	      \ ]
  call assert_equal('eol:$,lead:<,space:-,leadmultispace:.-+*', &listchars)
  call Check_listchars(expected, 6)
  call Check_listchars(expected, 5, -1, 1)
  call Check_listchars(expected, 5, -1, 2)
  call Check_listchars(expected, 5, -1, 3)
  call Check_listchars(expected, 5, -1, 6)
  call assert_equal(expected, split(execute("%list"), "\n"))

  " the last occurrence of 'multispace:' is used
  set listchars&
  set listchars+=multispace:yYzZ
  set listchars+=space:x,multispace:XyY

  let expected = [
	      \ 'XyYXffffXyYX$',
	      \ 'XyixiXyYXygg$',
	      \ 'xhXyYXyYXyYX$',
	      \ 'XyYXyYXyYXjx$',
	      \ 'XyYX0Xy0XyYX$',
	      \ '$'
	      \ ]
  call assert_equal('eol:$,multispace:yYzZ,space:x,multispace:XyY', &listchars)
  call Check_listchars(expected, 6)
  call Check_listchars(expected, 5, -1, 6)
  call assert_equal(expected, split(execute("%list"), "\n"))

  set listchars+=lead:>,trail:<

  let expected = [
	      \ '>>>>ffff<<<<$',
	      \ '>>ixiXyYXygg$',
	      \ '>h<<<<<<<<<<$',
	      \ '>>>>>>>>>>j<$',
	      \ '>>>>0Xy0<<<<$',
	      \ '$'
	      \ ]
  call Check_listchars(expected, 6)
  call Check_listchars(expected, 5, -1, 6)
  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<<<<$',
	      \ '$'
	      \ ]
  call Check_listchars(expected, 6)
  call Check_listchars(expected, 5, -1, 6)
  call assert_equal(expected, split(execute("%list"), "\n"))

  " test nbsp
  normal ggdG
  set listchars=nbsp:X,trail:Y
  set list
  " Non-breaking space
  let nbsp = nr2char(0xa0)
  call append(0, [ ">" .. nbsp .. "<" ])

  let expected = '>X< '
  call Check_listchars([expected], 1)

  set listchars=nbsp:X
  call Check_listchars([expected], 1)

  " test extends
  normal ggdG
  set listchars=extends:Z
  set nowrap
  set nolist
  call append(0, [ repeat('A', &columns + 1) ])

  let expected = repeat('A', &columns)
  call Check_listchars([expected], 1, &columns)

  set list
  let expected = expected[:-2] . 'Z'
  call Check_listchars([expected], 1, &columns)

  enew!
  set listchars& ff&
endfunc

" Test that unicode listchars characters get properly inserted
func Test_listchars_unicode()
  enew!
  let oldencoding=&encoding
  set encoding=utf-8
  set ff=unix

  set listchars=eol:⇔,space:␣,multispace:≡≢≣,nbsp:≠,tab:←↔→
  set list

  let nbsp = nr2char(0xa0)
  call append(0, ["        a\tb c" .. nbsp .. "d  "])
  let expected = ['≡≢≣≡≢≣≡≢a←↔↔↔↔↔→b␣c≠d≡≢⇔']
  call Check_listchars(expected, 1)
  call Check_listchars(expected, 1, -1, 3)
  call Check_listchars(expected, 1, -1, 13)

  set listchars=eol:\\u21d4,space:\\u2423,multispace:≡\\u2262\\U00002263,nbsp:\\U00002260,tab:←↔\\u2192
  call Check_listchars(expected, 1)
  call Check_listchars(expected, 1, -1, 3)
  call Check_listchars(expected, 1, -1, 13)

  set listchars+=lead:⇨,trail:⇦
  let expected = ['⇨⇨⇨⇨⇨⇨⇨⇨a←↔↔↔↔↔→b␣c≠d⇦⇦⇔']
  call Check_listchars(expected, 1)
  call Check_listchars(expected, 1, -1, 3)
  call Check_listchars(expected, 1, -1, 13)

  let &encoding=oldencoding
  enew!
  set listchars& ff&
endfunction

func Test_listchars_invalid()
  enew!
  set ff=unix

  set listchars&
  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:')
  call assert_fails('set listchars=leadmultispace', '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:')
  call assert_fails('set listchars=leadmultispace:', '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:')
  call assert_fails('set listchars=leadmultispace:,leadmultispace:x', 'E474:')
  call assert_fails('set listchars=leadmultispace:x,leadmultispace:', 'E474:')

  " Too long
  call assert_fails('set listchars=space:xx', 'E474:')
  call assert_fails('set listchars=tab:xxxx', 'E474:')

  " Has double-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:')
  call assert_fails('set listchars=leadmultispace:·', 'E474:')
  call assert_fails('set listchars=leadmultispace:xxx·', 'E474:')

  " Has control character
  call assert_fails("set listchars=space:\x01", 'E474:')
  call assert_fails("set listchars=tab:\x01x", 'E474:')
  call assert_fails("set listchars=tab:x\x01", 'E474:')
  call assert_fails("set listchars=tab:xx\x01", 'E474:')
  call assert_fails("set listchars=multispace:\x01", 'E474:')
  call assert_fails("set listchars=multispace:xxx\x01", 'E474:')
  call assert_fails('set listchars=space:\\x01', 'E474:')
  call assert_fails('set listchars=tab:\\x01x', 'E474:')
  call assert_fails('set listchars=tab:x\\x01', 'E474:')
  call assert_fails('set listchars=tab:xx\\x01', 'E474:')
  call assert_fails('set listchars=multispace:\\x01', 'E474:')
  call assert_fails('set listchars=multispace:xxx\\x01', 'E474:')
  call assert_fails("set listchars=leadmultispace:\x01", 'E474:')
  call assert_fails('set listchars=leadmultispace:\\x01', 'E474:')
  call assert_fails("set listchars=leadmultispace:xxx\x01", 'E474:')
  call assert_fails('set listchars=leadmultispace:xxx\\x01', 'E474:')

  enew!
  set ambiwidth& listchars& ff&
endfunction

" Tests that space characters following composing character won't get replaced
" by listchars.
func Test_listchars_composing()
  enew!
  let oldencoding=&encoding
  set encoding=utf-8
  set ff=unix
  set list

  set listchars=eol:$,space:_,nbsp:=

  let nbsp1 = nr2char(0xa0)
  let nbsp2 = nr2char(0x202f)
  call append(0, [
        \ "  \u3099\t \u309A" .. nbsp1 .. nbsp1 .. "\u0302" .. nbsp2 .. nbsp2 .. "\u0302",
        \ ])
  let expected = [
        \ "_ \u3099^I \u309A=" .. nbsp1 .. "\u0302=" .. nbsp2 .. "\u0302$"
        \ ]
  call Check_listchars(expected, 1)
  let &encoding=oldencoding
  enew!
  set listchars& ff&
endfunction

" Check for the value of the 'listchars' option
func s:CheckListCharsValue(expected)
  call assert_equal(a:expected, &listchars)
  call assert_equal(a:expected, getwinvar(0, '&listchars'))
endfunc

" Test for using a window local value for 'listchars'
func Test_listchars_window_local()
  %bw!
  set list listchars&
  new
  " set a local value for 'listchars'
  setlocal listchars=tab:+-,eol:#
  call s:CheckListCharsValue('tab:+-,eol:#')
  " When local value is reset, global value should be used
  setlocal listchars=
  call s:CheckListCharsValue('eol:$')
  " Use 'setlocal <' to copy global value
  setlocal listchars=space:.,extends:>
  setlocal listchars<
  call s:CheckListCharsValue('eol:$')
  " Use 'set <' to copy global value
  setlocal listchars=space:.,extends:>
  set listchars<
  call s:CheckListCharsValue('eol:$')
  " Changing global setting should not change the local setting
  setlocal listchars=space:.,extends:>
  setglobal listchars=tab:+-,eol:#
  call s:CheckListCharsValue('space:.,extends:>')
  " when split opening a new window, local value should be copied
  split
  call s:CheckListCharsValue('space:.,extends:>')
  " clearing local value in one window should not change the other window
  set listchars&
  call s:CheckListCharsValue('eol:$')
  close
  call s:CheckListCharsValue('space:.,extends:>')

  " use different values for 'listchars' items in two different windows
  call setline(1, ["\t  one  two  "])
  setlocal listchars=tab:<->,lead:_,space:.,trail:@,eol:#
  split
  setlocal listchars=tab:[.],lead:#,space:_,trail:.,eol:&
  split
  set listchars=tab:+-+,lead:^,space:>,trail:<,eol:%
  call assert_equal(['+------+^^one>>two<<%'], ScreenLines(1, virtcol('$')))
  close
  call assert_equal(['[......]##one__two..&'], ScreenLines(1, virtcol('$')))
  close
  call assert_equal(['<------>__one..two@@#'], ScreenLines(1, virtcol('$')))
  " changing the global setting should not change the local value
  setglobal listchars=tab:[.],lead:#,space:_,trail:.,eol:&
  call assert_equal(['<------>__one..two@@#'], ScreenLines(1, virtcol('$')))
  set listchars<
  call assert_equal(['[......]##one__two..&'], ScreenLines(1, virtcol('$')))

  " Using setglobal in a window with local setting should not affect the
  " window. But should impact other windows using the global setting.
  enew! | only
  call setline(1, ["\t  one  two  "])
  set listchars=tab:[.],lead:#,space:_,trail:.,eol:&
  split
  setlocal listchars=tab:+-+,lead:^,space:>,trail:<,eol:%
  split
  setlocal listchars=tab:<->,lead:_,space:.,trail:@,eol:#
  setglobal listchars=tab:{.},lead:-,space:=,trail:#,eol:$
  call assert_equal(['<------>__one..two@@#'], ScreenLines(1, virtcol('$')))
  close
  call assert_equal(['+------+^^one>>two<<%'], ScreenLines(1, virtcol('$')))
  close
  call assert_equal(['{......}--one==two##$'], ScreenLines(1, virtcol('$')))

  " Setting the global setting to the default value should not impact a window
  " using a local setting.
  split
  setlocal listchars=tab:<->,lead:_,space:.,trail:@,eol:#
  setglobal listchars&vim
  call assert_equal(['<------>__one..two@@#'], ScreenLines(1, virtcol('$')))
  close
  call assert_equal(['^I  one  two  $'], ScreenLines(1, virtcol('$')))

  " Setting the local setting to the default value should not impact a window
  " using a global setting.
  set listchars=tab:{.},lead:-,space:=,trail:#,eol:$
  split
  setlocal listchars=tab:<->,lead:_,space:.,trail:@,eol:#
  call assert_equal(['<------>__one..two@@#'], ScreenLines(1, virtcol('$')))
  setlocal listchars&vim
  call assert_equal(['^I  one  two  $'], ScreenLines(1, virtcol('$')))
  close
  call assert_equal(['{......}--one==two##$'], ScreenLines(1, virtcol('$')))

  " Using set in a window with a local setting should change it to use the
  " global setting and also impact other windows using the global setting.
  split
  setlocal listchars=tab:<->,lead:_,space:.,trail:@,eol:#
  call assert_equal(['<------>__one..two@@#'], ScreenLines(1, virtcol('$')))
  set listchars=tab:+-+,lead:^,space:>,trail:<,eol:%
  call assert_equal(['+------+^^one>>two<<%'], ScreenLines(1, virtcol('$')))
  close
  call assert_equal(['+------+^^one>>two<<%'], ScreenLines(1, virtcol('$')))

  " Setting invalid value for a local setting should not impact the local and
  " global settings.
  split
  setlocal listchars=tab:<->,lead:_,space:.,trail:@,eol:#
  let cmd = 'setlocal listchars=tab:{.},lead:-,space:=,trail:#,eol:$,x'
  call assert_fails(cmd, 'E474:')
  call assert_equal(['<------>__one..two@@#'], ScreenLines(1, virtcol('$')))
  close
  call assert_equal(['+------+^^one>>two<<%'], ScreenLines(1, virtcol('$')))

  " Setting invalid value for a global setting should not impact the local and
  " global settings.
  split
  setlocal listchars=tab:<->,lead:_,space:.,trail:@,eol:#
  let cmd = 'setglobal listchars=tab:{.},lead:-,space:=,trail:#,eol:$,x'
  call assert_fails(cmd, 'E474:')
  call assert_equal(['<------>__one..two@@#'], ScreenLines(1, virtcol('$')))
  close
  call assert_equal(['+------+^^one>>two<<%'], ScreenLines(1, virtcol('$')))

  " Closing window with local lcs-multispace should not cause a memory leak.
  setlocal listchars=multispace:---+
  split
  call s:CheckListCharsValue('multispace:---+')
  close

  %bw!
  set list& listchars&
endfunc

func Test_listchars_foldcolumn()
  CheckScreendump

  let lines =<< trim END
      call setline(1, ['aaa', '', 'a', 'aaaaaa'])
      vsplit
      vsplit
      windo set signcolumn=yes foldcolumn=1 winminwidth=0 nowrap list listchars=extends:>,precedes:<
  END
  call writefile(lines, 'XTest_listchars', 'D')

  let buf = RunVimInTerminal('-S XTest_listchars', {'rows': 10, 'cols': 60})

  call term_sendkeys(buf, "13\<C-W>>")
  call VerifyScreenDump(buf, 'Test_listchars_01', {})
  call term_sendkeys(buf, "\<C-W>>")
  call VerifyScreenDump(buf, 'Test_listchars_02', {})
  call term_sendkeys(buf, "\<C-W>>")
  call VerifyScreenDump(buf, 'Test_listchars_03', {})
  call term_sendkeys(buf, "\<C-W>>")
  call VerifyScreenDump(buf, 'Test_listchars_04', {})
  call term_sendkeys(buf, "\<C-W>>")
  call VerifyScreenDump(buf, 'Test_listchars_05', {})
  call term_sendkeys(buf, "\<C-W>h")
  call term_sendkeys(buf, ":set nowrap foldcolumn=4\<CR>")
  call term_sendkeys(buf, "15\<C-W><")
  call VerifyScreenDump(buf, 'Test_listchars_06', {})
  call term_sendkeys(buf, "4\<C-W><")
  call VerifyScreenDump(buf, 'Test_listchars_07', {})

  " clean up
  call StopVimInTerminal(buf)
endfunc


" vim: shiftwidth=2 sts=2 expandtab
