1

i want > / < and >> / << to simply insert and remove tab characters at the beginning of the line.

variant: i would even prefer to have> / < and >>/ << insert / remove a tab character at the position of the last tab character of each line within the selection (defaulting to the first character of each line within the selection whenever none such tab character is found).

can i change their behaviour to do so?

example. denoting tabs as ›––– and spaces as ·, say i have some text like the following.

›–––··A
›–––······B
›–––····C

i have preserveindent set. selecting all three lines in visual mode and pressing > thus far yields the following.

›–––··›–··A
›–––······›–··B
›–––····›–––C

clearly i want it to be as follows.

›–––›–––··A
›–––›–––······B
›–––›–––····C

variant. the variant allows to indent in a more reasonable way some text with a preceding comment # … like the following.

›–––··A
›–––······B
#›––····C

indenting the three lines by pressing > is then supposed to yield the following.

›–––›–––··A
›–––›–––······B
#›––›–––····C

context. it appears that vim/neovim does something way more complicated, namely to compute the indentation level of each line by counting its leading tabs and spaces, then increase / decrease this indentation level and finally indent the entire line fully anew by inserting a number of first tabs, then spaces corresponding to the freshly increased / decreased indentation level.

so this messes up the indentation structure when using “tabs for indentation, spaces for alignment”.

if preserveindent is on, it seems that indentation is increased / decreased by inserting a mixture of first spaces, then tabs right before the first non-white-space character, which is again way more complicated than what i want.

i want this for a sane configuration for using tabs as indentation, spaces as alignment; this is a follow-up to a previous question about leading tabs.

1 Answer 1

1

Here's the code that does the job (only > and <, but you can easily add >> or <<). Assuming you have preserveindent set, it works in visual mode, as well as range motions e.g. >2j or <}.

function! DoIndent(type, ...)
  if a:0
    " visual mode
    exec "sil! keepp '<,'>s/^/" . repeat("\t", v:count1) . "/"
  else
    " normal mode (followed by a motion)
    exec "sil! keepp '[,']s/^/\t/"
  endif
endfunction
function! DoDedent(type, ...)
  if a:0
    " visual mode
    exec "sil! keepp '<,'>s/^\t\\{," . v:count1 . "}//"
  else
    " normal mode (followed by a motion)
    exec "sil! keepp '[,']s/^\t//"
  endif
endfunction

nnoremap <silent> > :set opfunc=DoIndent<CR>g@
vnoremap <silent> > :<C-U>call DoIndent(visualmode(), 1)<CR>
nnoremap <silent> < :set opfunc=DoDedent<CR>g@
vnoremap <silent> < :<C-U>call DoDedent(visualmode(), 1)<CR>

The basic idea is: to insert a leading \t, replace start-of-line i.e. ^ with a \t; to remove one, replace ^\t with nothing. So we map > and < to perform such substitutions.

Since substitution is not affected by preserveindent, we end just just inserting/deleting one tab, without any extra spaces.

For example, the "sil! keepp '<,'>s/^/\t/" breaks down to

  1. sil! supress messages from :s
  2. keepp keep the search pattern (@/) unchanged because normally :s clobbers it
  3. '<,'> let the substitution work on visually selected lines. Similarly, '[,'] means the lines covered by a motion like 2j or }.
  4. s/^/\t/ substitute start-of-line with a tab, essentially adding a leading tab

update: Use repeat and the \{ regex to perform multiple shifts in visual mode e.g. in visual mode, 2< dedents each line of the selection by up to two \t's.

7
  • FWIW, :substitute accepts \t in both the pattern and replacement to stand for a Tab character. :execute is not needed here… just write :silent! keeppatterns '[,']substitute/^\t/, for example.
    – D. Ben Knoble
    Commented Apr 6, 2023 at 12:38
  • nice! can i extend this to use the current v:count somehow?
    – windfish
    Commented Apr 6, 2023 at 13:39
  • in the meantime i’ve been dabbling at creating a >> solution incorporating v:count, but it looks very contrived: nnoremap <expr> >> '^' . ('T<tab>' ? 'T<tab>' : '0') . ( v:count == 0 ? '' : v:count ) . 'i<c-v><tab><esc>' and nnoremap <expr> << '^T<tab>d' . ( v:count > 0 ? v:count . 'F<tab>' : 'F<tab>' ) and unindenting doesn’t work if the given v:count is greater than the number of leading tabs (the desired behaviour to unindent as many tabs as possible).
    – windfish
    Commented Apr 6, 2023 at 13:43
  • @D.BenKnoble Thank you! I always used a manual ^I but \t is definitely more portable.
    – Hoblovski
    Commented Apr 7, 2023 at 4:41
  • @windfish I guess you would like v:count to designate the number of \t inserted/deleted, but vim only does it for visual selections instead of >> or >{motion}. For example 3>2j is essentially equal to >6j, and 6>> essentially shifts six lines by one shiftwidth instead of one line by six shiftwidths. So I'm trying to keep a consistent behavior, but surely I modified the post to use v:count for visual selections.
    – Hoblovski
    Commented Apr 7, 2023 at 5:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.