I want to conveniently remove an accidentally placed tab while using vim. The solution that jumped out to me is making an insert-mode mapping to the following vim function:
function DeleteTab()
redir => l:numSpaces "captures output of set
set tabstop?
redir END
"Strip off non-numerical output of 'set tabstop?'
let l:numSpaces = substitute(l:numSpaces, "tabstop=", "", "")
let l:numSpaces = substitute(l:numSpaces, " ", "", "g")
"all echom lines are for debugging purposes
echom "1"
if l:numSpaces > 0
echom "2"
while 1:numSpaces > 0
execute "normal i<bs>"
let l:numSpaces = l:numSpaces - 1
endwhile
endfunction
In addition to not doing what I intended, the result of calling this function is "1" in my messages, but not "2". This means that l:numSpaces is not being interpreted as a number. How do I do the equivalent of casting in vimscript. Also, am I missing a more easy approach?
Instead of doing the redir just use &tabstop the ampersand gets the value and places it in the variable.
let l:numSpaces = &tabstop
The next problem you have is with this line
while 1:numSpaces > 0
You wrote a 1 (one) instead of l (lowercase L)
So the fixed function looks something like this.
function! DeleteTab()
let l:numSpaces = &tabstop
echom "1"
if l:numSpaces > 0
echom "2"
endif
while l:numSpaces > 0
execute "normal i<bs>"
let l:numSpaces = l:numSpaces - 1
endwhile
endfunction
Also this function is kinda pointless. I believe the behavior you want should be achieved if you set the following (or to what ever value you want)
set tabstop=4
set softtabstop=4
set shiftwidth=4
Hitting the backspace key should go back a full tab if you insert an accidental tab.
If you want to access the value of an option in vimscript, you can use the syntax &option (see :help expr-option). That simplifies the first half of your function to
let numSpaces = &tabstop
As far as undoing an accidental tab, all that should require is pressing Backspace, unless you aren't inserting tab characters.
If you mean you want to "remove a level of indentation" instead of "remove a tab", then you should use the builtin command for that, pressing Ctrl+d in insert mode. Similarly, you can use Ctrl+t to add a level of indentation to the current line. Both of these work regardless of where your cursor is in the current line, unlike trying to manage the indentation manually with Backspace, as well as doing the Right Thing based on your 'shiftwidth', 'expandtab', and 'tabstop' settings.
Related
Basically I have the following settings for PSR-2 code compliance:
set tabstop=4
set shiftwidth=4
set softtabstop=4
set expandtab
set smarttab
PSR-2 requires indentions to be 4 spaces. That's fine, but I'm used to real tabs instead of spaces, so if I'm at the beginning of a line, and I move right, instead of moving to the first indented character, it moves over one space at a time.
Is there any way to make vim treat these leading spaces in the same way, that is, to "jump" to the first non-space character when navigating in normal mode and insert mode?
I know I can use ^ to place the cursor to the first non-whitespace character, but I'm not used to that, and it's not as convenient as simply navigating.
I think you would be better served getting used to utilizing vims more powerful movement commands, such as ^.
That being said, here is one way you could achieve what you want.
nnoremap <right> :silent call SkipSpace()<cr>
function! SkipSpace()
let curcol = col('.')
execute "normal ^"
let hatcol = col('.')
if curcol >= hatcol
" move one space right
let newcol = curcol + 1
elseif curcol + 4 > hatcol
" move to the start of the next word if it is less than 4 spaces away
let newcol = hatcol
else
" move 4 spaces right
let newcol = curcol + 4
endif
execute "normal " . newcol . "|"
endfunction
P.S. For a bit of fun check out :help |
Put below into your vimrc
function! s:super_left()
return getline('.')[:col('.')] =~ '^\s\+$' ? 'w' : 'l'
endfunction
augroup SuperLeft
au!
autocmd FileType php nnoremap <expr> l <sid>super_left()
augroup END
I have this loop in my .vimrc to display the tab title as "1: File1.txt" or "2: File2.tx", etc, but both tabpagenr('$') and tabpagenr() always returns 1 no matter how many tabs I open. What am I doing wrong?
for t in range(tabpagenr('$'))
if (t + 1) == tabpagenr()
let &titlestring = t + 1 . ': '
endif
endfor
let &titlestring .= expand("%:M")
if &term == "screen" || &term == "xterm"
set title
endif
It looks like there are some bits missing from your sample code: how do you expect to change your tab labels with only those few lines?
Anyway, without an argument, tabpagenr() returns the number of the current tab. Since you are always in the same tab during your loop, that function always returns the same number.
:help setting-tabline has an example, did you read it?
You didn't tell us on which events your code is executed. If you plainly put this in your ~/.vimrc, it will only be executed once during Vim startup. You need to use :autocmd to update the 'titlestring', at least on every tab page change (i.e. the TabEnter event), or better use an expression in the option to have it continuously evaluated:
:set titlestring=%{tabpagenr()}
Many people use spaces rather than tabs. I use both of them. Tabs at the beginning of line and spaces from the first non-whitespace character. No problem for starting new document and in case I have to modify one better adapt to using format. Still sometimes I need to fix the spaces issue anyway.
According to Search and replace I can just do :%s/spaces_for_tab/tab/g. It is simple and it will work for many cases. Anyway I want to refactor only spaces at the beginning of line.
This is more of a regex issue. To anchor at the beginning of the line, use the caret, e.g.
s/^ /\t/
Or do it using vim's builtin functionality:
:set tabstop=4 "four spaces will make up for one tab
:set noexpandtab "tell vim to keep tabs instead of inserting spaces
:retab "let vim handle your case
By the way, I too prefer tabs for indentation and spaces for alignment. Unfortunately, vim doesn't handle this well (and I don't know what other editors do), so I mostly use :set expandtab (maybe see :set softtabstop).
I've written a simple func for it. Anyway it will work only for 4-space tab.
fu! Fixspaces()
while search('^\t* \{4}') != 0
execute ':%s/^\t*\zs \{4}/\t/g'
endwhile
endfu
You can suggest better solution, if exists, and I will use it with pleasure.
The issue is that this func replaces spaces in strings as well.
David's response is very elegant but it doesn't address leading whitespace that has a mixture of tabs and spaces. For example to convert a line like:
<SPACE><SPACE><TAB>something...
you have to know the position of the tab to determine the number of spaces needed to replace the <TAB> and reach the next tabstop. My solution below, although not as compact as David's, addresses this. It also allows me to select which way to use leading whitespace without depending upon &expandtab. I would appreciate any suggestions to improve my code...
function! TabsToSpaces(...)
let ts = &tabstop
let pos = getpos('.')
while search('^ *\zs\t', "w") != 0
let l:curpos = getcharpos('.')
" The number of spaces needed depends upon the position of the <TAB>
let numsp = 1 + ts - ( curpos[2] % ts )
if numsp == 9
let numsp = 1
endif
silent execute ':s/^ *\zs\t/'.repeat(' ', numsp).'/'
endwhile
if a:0 == 0
echo 'Changed leading tabs to spaces'
endif
call setpos('.', pos)
endfunction
function! SpacesToTabs(...)
let ts = &tabstop
let pos = getpos('.')
" First normalize all tabs to spaces
call TabsToSpaces("quiet")
while search('^\t* \{'.ts.'}') != 0
silent execute ':%s/^\t*\zs \{'.ts.'}/\t/g'
endwhile
if a:0 == 0
echo 'Changed leading spaces to tabs'
endif
call setpos('.', pos)
endfunction
" Some keystrokes to implement the spaces/tabs functions
nmap <Leader>st :execute SpacesToTabs()<CR>
nmap <Leader>ts :execute TabsToSpaces()<CR>
I took Martin's answer and improved on it a bit if anyone's interested:
function Fixspaces()
let ts = &tabstop
let pos = getpos('.')
if &expandtab
while search('^ *\t') != 0
silent execute ':%s/^ *\zs\t/'.repeat(' ', ts).'/g'
endwhile
echo 'Changed tabs to spaces'
else
while search('^\t* \{'.ts.'}') != 0
silent execute ':%s/^\t*\zs \{'.ts.'}/\t/g'
endwhile
echo 'Changed spaces to tabs'
endif
call setpos('.', pos)
endfunction
This function does the appropriate thing depending on the values of the expandtab and tabstop settings and also remembers where the cursor is.
In Vim, one can join two lines by typing capital J.
However, these are usually joined by a space.
I seem to remember there was a way to change the character used for the joining by setting some variable, but I can't seem to find it again.
I'd appreciate it if anyone could remind me, or confirm that it can't be done.
When I want to join just a few lines I use a 3 keys combo (normal mode):
Jr,
being , the joining character.
In case I want to join more lines or even join lines in groups, I use the previous combo with a macro.
For example, to transform 3 lines in a 3 columns CSV table, I record this macro (assigned to letter j of course):
qjJr,Jr,jq
So, using #j joins 3 lines using , and goes to the next line.
10#j converts 10 lines.
There isn't a setting that allows you to do this directly, see:
:help J
in particular, the text below the list of commands.
A couple of ways you could do this:
:nnoremap J gJi.<ESC>
" or
let joinchar = ';'
nnoremap J :s/\n/\=joinchar/<CR>
The latter option allows you to change it on the fly by changing the joinchar option.
Try something like this in your .vimrc:
nnoremap Y Jxi*<Esc>
It'll remap Y to join the lines with a *.
From http://vim.wikia.com/wiki/Remap_join_to_merge_comment_lines
put this in your .vimrc:
function! JoinWithLeader(count, leaderText)
let l:linecount = a:count
" default number of lines to join is 2
if l:linecount < 2
let l:linecount = 2
endif
echo l:linecount . " lines joined"
" clear errmsg so we can determine if the search fails
let v:errmsg = ''
" save off the search register to restore it later because we will clobber
" it with a substitute command
let l:savsearch = #/
while l:linecount > 1
" do a J for each line (no mappings)
normal! J
" remove the comment leader from the current cursor position
silent! execute 'substitute/\%#\s*\%('.a:leaderText.'\)\s*/ /'
" check v:errmsg for status of the substitute command
if v:errmsg=~'Pattern not found'
" just means the line wasn't a comment - do nothing
elseif v:errmsg!=''
echo "Problem with leader pattern for JoinWithLeader()!"
else
" a successful substitute will move the cursor to line beginning,
" so move it back
normal! ``
endif
let l:linecount = l:linecount - 1
endwhile
" restore the #/ register
let #/ = l:savsearch
endfunction
nnoremap <space> :<C-U>call JoinWithLeader(v:count, '"')<CR>
This also allows you to remap J to something else.
It will quicker if you replace the end of line with a comma (or join character)
:%s/$/,
and then joining multiple lines either by providing a range, or by selecting lines in visual mode and using the join command
10J
It's mapping. You can read the tutorial in vim wikia :
Mapping keys in vim
Try the command below in command mode, and try to press . This should work :)
:map <space> J
Drawing a blank on this, and google was not helpful.
Want to make a function like this:
function JakPaste()
let tmp = :set paste?
if tmp == "paste"
set nopaste
else
set paste
endif
endfunction
map <F2> :call JakPaste()<CR>
However this does not work. I have isolated the broken line:
function JakPaste()
let tmp = set paste?
endfunction
map <F2> :call JakPaste()<CR>
Pressing F2 results in this error:
Error detected while processing function JakPaste:
line 1:
E121: Undefined variable: set
E15: Invalid expression: set paste?
Hit ENTER or type command to continue
How should I call an ex command (set) from a vim function?
This seems somewhat relevant however I still don't get it.
The reason this doesn't work is that you're trying to run a command in an expression - those are different things. The ? construct you used just causes vim to echo the value of the option; this isn't the same as a function returning the value. To be clear: the problem isn't that you're calling an ex command from a function - every other line of the function is an ex command - it's that you're calling the ex command in an expression.
But that's not the right way to carry out the task you're attempting here. Here's the shortest way, thanks to rampion's comment:
set paste!
Now, if you ever need something smarter than just inverting a boolean, you can use & to turn an option name into a usable variable. Here are two ways to use that:
" still a function, good for taking extra action (here, printing notification)"
function TogglePaste()
if (&paste)
set nopaste
echo "paste off"
else
set paste
echo "paste on"
endif
endfunction
" just set the variable directly"
let &paste = !&paste