0

N.B I use the word blank here due to lack of a better word. By blank I mean the remaining part of a tab that is visualized after the starting part. Please feel free to edit this question.

I know there are questions already answered about vim's indent with tabs and align with spaces. But those questions have answers that says it's not possible or possible with supertab plugin. And moreover, my question is a little bit specific.

Take this example,

function! Format_tabs() abort
>...let s = ''
>...let s .= repeat(' ', &ts)
>...let t = '>..>...hello there>>...hey'
>...echo substitute(t, '\v[^\t]+\zs\t+\ze', s, 'g')
endfunction

where all > are start of a tab and following . are the remaining blank part throughout the tabstop.

And my tabstop settings are all set to 4.

In the above function I'm trying to convert the tabs in the string that are not at the beginning of a line with equivalent number of spaces. At the beginning I thought, I could replace each tabs, that are not at the beginning, with &tabstop number of spaces. But it turns out due to some settings, that I'm not trying to change, the tabs show different amount of blanks to figure out the alignment. So I need to figure out how many spaces I need to substitute the tabs for. But I have no idea!

Any solution?

  • You only want to convert the tabs in the string, is that correct? NOT the tabs at the beginning of lines? – D. Ben Knoble Feb 8 at 15:27
  • As a guess at a fix: you might need an expression in the substitute (something like \=repeat(' ', len(\1)), but im rusty on that syntax). – D. Ben Knoble Feb 8 at 15:29
  • @D.BenKnoble, yes, that is correct. I took a string with tabs at the beginning to simulate lines where tabs are present at the beginning of lines to indent. And the other tabs are there just because of set noet. I want to change those other tabs – klaus Feb 8 at 15:50
  • substitute(t, '\v[^\t]+\zs\t+\ze', repeat(' ', len(\1)), 'g')? doesn't work! – klaus Feb 8 at 15:58
  • 1
    Ill take a look this afternoon or evening. – D. Ben Knoble Feb 8 at 16:01
1

This seems to work:

function! ReplaceMiddleTabs() abort
    let s:linenum = line('.')
    let s:current_line = getline(s:linenum)
    let s:temp = Format_tabs(s:current_line)
    call setline(s:linenum, s:temp)
endfunction

function! Format_tabs(line) abort
    let s:tablen = &ts
    let s:start = match(a:line, '^\s\+\zs.\ze')

    let s:temp = a:line
    let s:control = 1
    while s:control > 0
        let s:next = match(s:temp, '\t', s:start + 1)
        if s:next > 0
            let s:mod = (s:next % s:tablen)
            if s:mod == 0
                let s:mod = 4
            endif
            let s:substlen = s:tablen - s:mod + 1

            let s:temp = s:temp[0:s:next - 1] . repeat(' ', s:substlen) . s:temp[s:next + 1:]
        else
            let s:control = 0
        endif
    endwhile

    return s:temp
endfunction

The trick here is to calculate how many visual spaces each tab fills. To test it, I've used this:

|...|...|...|...|...|...|...|...|...|...|...|
>...|...>...|...|...|...|...|...|...|...|...|
>...|...|>..|...|...|...|...|...|...|...|...|
>...|...|.>.|...|...|...|...|...|...|...|...|
>...|...|..>|...|...|...|...|...|...|...|...|
>...|...|...>...|...|...|...|...|...|...|...|
>...|...>...|...|>..>...>...>...>...|...|...|
>...|...|>..|...|.>.|.>.|.>.|.>.|...|...|...|
>...|...|.>.|...|..>|..>|..>|..>|...|...|...|
>...|...|..>|...|...>...>...|...>...|...|...|
>...|...|...>...|...|...>...|..>|.>.|>..|...|
|...|...|...|...|...|...|...|...|...|...|...|

:%s/>\.*/\t/g
:call ReplaceMiddleTabs()
:%s/\t/>###/g

|...|...|...|...|...|...|...|...|...|...|...|
>###|...    |...|...|...|...|...|...|...|...|
>###|...|   |...|...|...|...|...|...|...|...|
>###|...|.  |...|...|...|...|...|...|...|...|
>###|...|.. |...|...|...|...|...|...|...|...|
>###|...|...    |...|...|...|...|...|...|...|
>###|...    |...|                   |...|...|
>###|...|   |...|.  |.  |.  |.  |...|...|...|
>###|...|.  |...|.. |.. |.. |.. |...|...|...|
>###|...|.. |...|...        |...    |...|...|
>###|...|...    |...|...    |.. |.  |   |...|
|...|...|...|...|...|...|...|...|...|...|...|

But this is my first vimscript function, so look at it with a grain of salt.

Edited: use % (thanks, @Enan Ajmain)

  • First of all, remainder(a, b) is equivalent to a%b. – klaus Feb 9 at 13:38
  • Thanks! I've looked for that but couldn't find it... Need newer glasses, I think :) – João A. Toledo Feb 11 at 10:55
  • This worked. But I decided to just map my <Tab> key to do the right thing. It's little faster that way, and more intuitive. Thanks for your effort. – klaus Mar 30 at 20:51
0

Based on the docs for submatch(), I would use something like (untested)

function! Format_tabs() abort
  let t = '>..>...hello there>>...hey'
  echo substitute(t, '\v[^\t]+\zs\t+\ze', '\=repeat(" ", len(submatch(1)) * &tabstop)', 'g')
endfunction

For reference, the example in the docs is

:echo substitute(text, '\d\+', '\=submatch(0) + 1', '')
  • I already tried that approach, and that should be \=repeat(" ", len(submatch(0)) * &ts). But it doesn't work, because my main problem was, tabs, in case of alignment, i.e. used NOT at the beginning, would not always take up &ts amount of space. I think a simple substitution would not suffice, I need to check at what column the tab is and insert (col+&ts)%&ts amount of spaces. I'm working on this, but this would need to check every single tab of the file one by one, and this amount of overhead feels stupid to me. – klaus Feb 10 at 3:39
  • Yes realizing i missed a few pieces. Ill leave the answer here, but its a bummer it doesnt work. – D. Ben Knoble Feb 10 at 4:10
  • This function doesn't work as expected. But I've found some solutions. So, it all worked out. Thanks. – klaus Mar 30 at 20:50
0

1st Method

Use this command to convert tabs to spaces except the tabs used for indentation over a range of lines:

com! -bar -range=% RemoveTabs call s:remove_tabs(<line1>,<line2>)
function! s:remove_tabs(line1, line2) abort
    let view = winsaveview()
    let mods = 'sil keepj keepp'
    let range = a:line1 . ',' . a:line2
    let pat = '\%(^\s*\)\@!\&\(.\)\t'
    let l:Rep = {-> submatch(1) . repeat(' ', strdisplaywidth("\t", col('.') == 1 ? 0 : virtcol('.') ))}
    let g = 0
    while search("\t", 'n') && g < 999
        exe mods . ' ' . range .'s/' . pat . '/\=l:Rep()/ge'
        let g += 1
    endwhile
    call winrestview(view)
endfunction

2nd Method

Use this function to bind your Tab to a function that inserts tabs for indentation and spaces for alignment:

function! SpecialTab() abort
    if (col('.') == 1) || (matchstr(getline('.'), '\%'.(col('.') - 1).'c.') =~ '\t')
        return "\<Tab>"
    else
        return repeat("\<Space>", (&tabstop - (virtcol('.') % &tabstop)) + 1)
    endif
endfunction
inoremap <Tab> <C-R>=SpecialTab()<CR>

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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