Vim statusline separation (%=) with Unicode characters - vim

When I set fillchars=stl:x statusline=a%=b, I see the following statusline:
axxxxxb
When I set fillchars=stl:· statusline=a%=b (· = middle dot, U+00B7), the status line becomes:
a-----b
The same happens with Unicode box drawing characters (they become -).
I would have expected:
a·····b
What am I missing or doing wrong?
If I don't set statusline to anything, then the default statusline draws the Unicode character correctly, something like:
~/.vimrc·····1,1·····Top

Looks like Vim does not currently support multi-byte fill characters in custom status lines (version 8.1.2203):
/* Can't handle a multi-byte fill character yet. */
else if (mb_char2len(fillchar) > 1)
fillchar = '-';
To work around the issue, I created a function that draws the line instead:
function StatusLine()
let left = 'a'
let right = 'b'
let spacer_width = winwidth(0) - len(left) - len(right)
let spacer = repeat('·', spacer_width)
return left . spacer . right
endfunction
set statusline=%{StatusLine()}

This is currently not supported.
Quote from :h 'fcs':
for "stl" and "stlnc" only single-byte values are supported.

Related

Vim (vimscript) get exact character under the cursor

I am getting the character under the cursor in vimscript the following way:
getline('.')[col('.')-1]
It works exactly like it should, however there is something I dislike. consider this [] the cursor. When there is a bracket next to the cursor like so:
}[] , ][] , )[] or {[] the cursor actually returns the bracket. What do I have to set so it will always return the character exactly under the cursor or atleast ignore if there is a bracket to it's left?
Note: I suspect that it might have to do with the brackets highlight, though I am not sure.
Note2: for the situation to occur there has to be a matching bracket.
Though I cannot reproduce the problem you're describing, there's another problem with your code: Because of the string indexing (and this is one of the uglier sides of Vimscript), it only works with single-byte characters, but will fail to capture chars like Ä or 𠔻 (depending on the encoding used). This is a better way of capturing the character under the cursor:
:echo matchstr(getline('.'), '\%' . col('.') . 'c.')
Edit: Since about Vim 7.4.1742, Vim has new strgetchar() and strcharpart() functions that work with character indexes, not byte addressing. This is helpful in many circumstances, but not here, because you still can only get the byte-index position of the cursor (or the screen column with virtcol(), but that's not the same as character index).
nr2char(strgetchar(getline('.')[col('.') - 1:], 0))
or
strcharpart(getline('.')[col('.') - 1:], 0, 1)
Another way to get the character index under cursor that deal with both ASCII and non-ASCII characters is the like the following:
function! CharAtIdx(str, idx) abort
" Get char at idx from str. Note that this is based on character index
" instead of the byte index.
return strcharpart(a:str, a:idx, 1)
endfunction
function! CursorCharIdx() abort
" A more concise way to get character index under cursor.
let cursor_byte_idx = col('.')
if cursor_byte_idx == 1
return 0
endif
let pre_cursor_text = getline('.')[:col('.')-2]
return strchars(pre_cursor_text)
endfunction
Then if you want to get char under cursor, use the following command:
let cur_char_idx = CursorCharIdx()
let cur_char = CharAtIdx(getline('.'), cur_char_idx)
See also this post on how to get pre-cursor char.

Context sensitivity in commentstring

When setting folds manually, it would be handy if it were possible to set the commentstring in a context sensitive manner. Consider the case in which the language uses BCPL-style comment markers (i.e., comments begin with // and are terminated by a newline), the first line of the visual block contains a comment, and the last line does not. Currently, if commentstring is set to //%s, redundant // characters will be appended to the first line when zf is used to create a fold.
Is it possible to set commentstring so that the // characters are added only if they do not already appear on the line?
According to :help fold-create-marker, automatic folding marker insertion
does not work properly when:
The line already contains a marker with a level number. Vim then doesn't
know what to do.
Folds nearby use a level number in their marker which gets in the way.
The line is inside a comment, commentstring isn't empty and nested
comments don't work. For example with C: adding /* {{{ */ inside
a comment will truncate the existing comment.
Thus, it is not possible to change the default zf behavior by modifying the
commentstring setting.
However, it is possible to create a custom version of the zf command that
would take into consideration that starting or ending (or both) lines of
a fold could have comments. For example, consider the following mappings, one
for marking a fold by visual selection, another for using with a motion
command.
nnoremap <silent> <leader>zf :set opfunc=CreateMarkerFold<cr>g#
vnoremap <silent> <leader>zf :<c-u>call CreateMarkerFold(visualmode(), 1)<cr>
function! CreateMarkerFold(vt, ...)
let range = map(['[<', ']>'], 'line("''".v:val[a:0])')
let mark = split(&foldmarker, ',')
let pat = escape(&commentstring, '\')
let pat = '\V' . substitute(pat, '\\\#<!%s', '\\zs\\ze\\.\\{-}', '')
for i in [1, 0]
let line = getline(range[i])
if line =~ pat
let line = substitute(line, pat, escape(mark[i], '\'), '')
else
let line .= printf(&commentstring, mark[i])
endif
call setline(range[i], line)
endfor
endfunction
Both of the mappings follow the same routine. Before adding starting and
ending folding markers, it checks separately whether the first and the last
lines of the block to fold match the commentstring pattern. For each of the
two that do match, it inserts the corresponding marker inside the first found
comment, at the very beginning of its text. Otherwise, the marker is
decorated according to the commentstring template and added at the end of
the line.
If in the latter case it is preferable to separate the marker on its own line,
one can change the for loop as shown below.
for i in [1, 0]
let line = getline(range[i])
if line =~ pat
let line = substitute(line, pat, escape(mark[i], '\'), '')
call setline(range[i], line)
else
call append(range[i] - !i, printf(&commentstring, mark[i]))
endif
endfor
Unlike the previous version of the loop, the order of processing the two
lines is important: The ending marker line should be added first, if
necessary, because inserting a line for the beginning marker would shift the
following lines changing their numbers.

Vim, word frequency function and French accents

I have recently discovered the Vim Tip n° 1531 (Word frequency statistics for a file).
As suggested I put the following code in my .vimrc
function! WordFrequency() range
let all = split(join(getline(a:firstline, a:lastline)), '\A\+')
let frequencies = {}
for word in all
let frequencies[word] = get(frequencies, word, 0) + 1
endfor
new
setlocal buftype=nofile bufhidden=hide noswapfile tabstop=20
for [key,value] in items(frequencies)
call append('$', key."\t".value)
endfor
sort i
endfunction
command! -range=% WordFrequency <line1>,<line2>call WordFrequency()
It works fine except for accents and other french specifics (latin small ligature a or o, etc…).
What am I supposed to add in this function to make it suit my needs ?
Thanks in advance
For 8-bit characters you can try to change the split pattern from \A\+ to
[^[:alpha:]]\+.
The pattern \A\+ matches any number of consecutive non-alphabetic characters which — unfortunately — includes multibytes characters like our beloved çàéô and friends.
That means that your text is split at spaces AND at multibyte characters.
With \A\+, the phrase
Rendez-vous après l'apéritif.
gives:
ap 1
apr 1
l 1
Rendez 1
ritif 1
s 1
vous 1
If you are sure your text doesn't include fancy spaces you could replace this pattern with \s\+ that matches whitespace only but it's probably to liberal.
With this pattern, \s\+, the same phrase gives:
après 1
l'apéritif. 1
Rendez-vous 1
which, I think, is closer to what you want.
Some customizing may be necessary to exclude punctuations.
function! WordFrequency() range
" Whitespace and all punctuation characters except dash and single quote
let wordSeparators = '[[:blank:],.;:!?%#*+^#&/~_|=<>\[\](){}]\+'
let all = split(join(getline(a:firstline, a:lastline)), wordSeparators)
"...
endfunction
If all punctuation characters should be word separators, the expression shortens to
let wordSeparators = '[[:blank:][:punct:]]\+'

vim - set auto indent to fill the leading space with space or tabstop

It seems if we enable 'ai', vim will fill the the leading space with tabstop.
I can make it fill with just space with 'et'. I don't like a C file mixed with space and tabstop.
My vimrc:
set ts=4 et
set ai
set hlsearch
syntax on
filetype plugin indent on
autocmd FileType make setlocal noexpandtab
However, in some condition I do need to input tabstop when I hit the 'TAB' on keyboard, for example, in makefile and some others.
The 'autocmd FileType' command is not good: I can't add every file type in vimrc.
What I want is simple:
autoindent to fill leading area with
space;
when hit 'TAB' on keyboard, tabstop
input, not space (so no 'et')
How to do it?
inoremap <expr> <tab> ((getline('.')[:col('.')-2]=~'\S')?("\<C-v>\t"):(repeat(' ', &ts-((virtcol('.')-1)%&ts))))
It does the same as #Lynch answer if I read it correctly.
You can also use <C-v><Tab>: this will insert <Tab> without invoking any mappings and ignores expandtab unless you remapped <C-v> or <C-v><Tab> for some reason.
If you want to just insert tab do
inoremap <Tab> <C-v><Tab>
It will ignore expandtab setting.
I did it using a function. I tested it, but maybe in some particular case you will have to fix some bugs. Try adding this to your vimrc:
set et
function! Inserttab()
let insert = ""
let line = getline('.')
let pos = getpos('.')[2]
let before = ""
let after = line
if pos != 1
let before = line[ 0: pos - 1]
let after = line[pos : strlen(line) ]
endif
if pos != 1 && substitute(before, "[ \t]", "", "g") != ""
let insert = "\t"
else
let insert = " "
endif
let line = before . insert . after
call setline('.', line)
call cursor(line('.'), strlen(before . insert))
endfunction
inoremap <tab> <esc>:call Inserttab()<CR>a
Basicaly it does remap your key in visual mode to the function Inserttab(). Also note that if you change ts for something other than 4 it will still output 4 spaces instead of two because the value is hard coded.
Also im not very familiar with vim scripts, but I think all the variables used will be global which is a bad thing.
I forgot to mention that to "see" white spaces you can use set list. You disable this with set nolist. Also in normal mode you can use ga to see information about the character your cursor is on.
Edit
I realise that you may want to insert tab at the beginin of the line. My script insert space at the begining and tab anywhere else.
If you really want a tab every time you hit tab key you could simply use this:
set et
function! Inserttab()
let insert = ""
let line = getline('.')
let pos = getpos('.')[2]
let before = ""
let after = line
if pos != 1
let before = line[ 0: pos - 1]
let after = line[pos : strlen(line) ]
endif
let insert = "\t"
let line = before . insert . after
call setline('.', line)
call cursor(line('.'), strlen(before . insert))
endfunction
inoremap <tab> <esc>:call Inserttab()<CR>a
But I dont see the point, with this version you will never be able to indent manually from insert mode.
One way to do it is
:set sw=4 (or whatever you want)
:set ts=46 (or some large number)
Then autoindent will not insert tabs unless you reach 46 spaces, in which case you can put in a higher number.
Only drag about this is if someone else is using tabs, then you have to reset ts to agree with the file you are editing. On the other hand, it will make the tabs immediately obvious, which can be desirable as well.

Vim: creating a C string literal around a block of text with vertically aligned quotes?

I would like to create a macro or a script in Vim that does the following:
wrap a block of text in double quotes
escaping any quotes that appear in the text itself
have the quotes on the right side in vertical alignment
For example:
<html>
<head></head>
<body>
<h1>High Score Server</h1>
<table>
ROWS
</table>
</body>
</html>
would become:
"<html> "
"<head></head> "
"<body> "
"<h1>High Score Server</h1>"
"<table> "
"ROWS "
"</table> "
"</body> "
"</html> ";
I am able to achieve this with a macro, but without the vertical alignment of the quotes on the right side. Can anyone help me with this one?
What I'd do :
With "surround" and "Align" plugins :
1) with cursor on first line (0,0), type <C-V>)$s"
2) then <S-V>):Align " and <Enter>.
Another solution without plugins :
1) set virtual mode
:set ve=all
2) <C-V> to go in block-wise selection, with cursor at the position 0,0
3) go down to the bottom of the text, then Shift-I, type " and Esc. This should prepend the quotes.
4) now go on the left end (since ve=all, you can go where there is no text)
5) <C-V>, go down to bottom, type r"
This is long to explain, but easy to do and reproduce. Also useful in lots of case.
function Enquote()
let [startline, endline]=sort([line("'<"), line("'>")])
let lines=getline(startline, endline)
let lengths=map(copy(lines), 'len(split(v:val, ''\zs''))')
let maxlen=max(lengths)
call map(lines, '''"''.v:val.repeat(" ", maxlen-lengths[v:key]).''"''')
return setline(startline, lines)
endfunction
Explanation:
line("'<") and line("'>") get the line numbers of start and end of last visual selection.
sort([...]) sorts this line numbers since you may have started selecting lines from the end of the selection.
let [a, b]=[c, d] is a parallel assignment: sort will produce a sorted list of two items, where first item is lesser or equal to second item. Obviously, lesser is a first selected line.
len(split(v:val, '\zs')) is an advanced strlen() which supports unicode.
max(list) finds a maximum value. Obvious.
So, map(copy(lines), 'len(split(v:val, ''\zs''))') applies this strlen to all items in list. copy() is required since we do not want our list to be modified.
map(lines, '''"''.v:val.repeat(" ", maxlen-lengths[v:key]).''"''') modifies an lines in a way you require. I switched from printf to repeat because printf does not handle multibyte characters correctly (by «correctly» I mean that «¥» is one character long, while printf considers it two bytes long).
setlines(linenumber, listoflines) actually modifies buffer.
Making use of the unix program "par" to do this may well solve your problem. There's a Vimcast showing how to integrate it into vim over at http://vimcasts.org/episodes/formatting-text-with-par/
Is it possible to make two passes over the list of lines in vim script? Then you can do something like this (pseudocode):
let N = length of longest line
for each line L:
insert a " character at the beginning
append N - len(L) spaces
append a " character
best i got is a 3-pass regex.
select block in visual mode, then use:
:'<,'>s#^#"#
:'<,'>s#$# #
:'<,'>s#\(.\{28\}\).*#\1"
with the 28 being the length of your longest line.
By all means heed the previous answers and get your vim-fu in shape. Or install/modify/poke-the-author of this plugin:
http://www.vim.org/scripts/script.php?script_id=4727
From the plugin's page:
This script converts multi-line text in a C++ file to a multi-line
string literal, escaping the quote and tab characters. It also does
the reverse conversion, un-escaping some characters. It's not too
complete for now, but it will be someday if needs come.
If you need to make changes use the source-code repository:
https://bitbucket.org/dsign/stringliteral.vim
In two passes:
let l = max(map(getline("'<", "'>"), 'strwidth(v:val)'))
'<,'>s/.*/\=('"'.submatch(0).repeat(' ', l-strwidth(submatch(0)) )).'"'

Resources