I'd like to define a text object for tab-delimited fields in a tsv file. So far, I have

vnoremap af :<C-U>silent! normal! T<Tab>vf<Tab><CR>
omap af :normal Vat<CR>
vnoremap if :<C-U>silent! normal! T<Tab>vt<Tab><CR>
omap if :normal Vit<CR>

The problem is with the first and the last field because those lack a tab on one side. I've experimented with ?[\t\n]?e+1 and search() instead of T<Tab> (and analogously for the ending), using various patterns for defining text objects that I've found online, but nothing seems to work.

EDIT: Eventually I came up with this, but I can't help the feeling it's an overkill. If someone comes up with a nicer solution, I'll be very happy to learn.

function! TsvField (inner)
    " first field
    if getline('.')[:col('.')-1] !~ "\t[^[:blank:]]"
        execute a:inner ? "normal ^vt\t" : "normal ^vf\t"
    " last field
    elseif getline('.')[col('.')-1:] !~ "\t"
        execute a:inner ? "normal T\tv$h" : "normal F\tv$h"
    " middle field
        execute a:inner ? "normal T\tvt\t" : "normal T\tvf\t"
vnoremap if :<C-U>call TsvField(1)<CR>
omap if :normal vit<CR>
vnoremap af :<C-U>call TsvField(0)<CR>
omap af :normal vat<CR>

EDIT 2: After some more testing I discovered that [^\s] does not match the letter s, so the first if fails when one of the fields begins with this letter. Replacing [^\s] with \S fixes this particular case – while breaking all the others. It seems [^[:blank:]] might be the answer.

  • Something like ?^\|\t<CR> might be able to replace T<Tab>, and then you would do something like lv/$\|\t<CR>h. Untested, but maybe enough to help you get the rest of the way there?
    – D. Ben Knoble
    Commented Oct 29, 2020 at 16:15
  • PS Vat and Vit looks wrong; probably you mean vat/vit (though the actual at/it mappings throw away the original visual mode, so it probably works)
    – D. Ben Knoble
    Commented Oct 29, 2020 at 16:17
  • @D.BenKnoble Thanks but I must be still doing something wrong because this doesn't work. How do I even use search in a mapping? I'm more confused now than when I started.
    – Kamil S.
    Commented Oct 29, 2020 at 19:55
  • I would put the search commands after normal!, since that's how you structured the original. ? and / are normal commands (derived from the original use for denoting ranges)
    – D. Ben Knoble
    Commented Oct 29, 2020 at 20:21
  • 1
    Welcome to Vi and Vim!
    – filbranden
    Commented Oct 29, 2020 at 22:25

2 Answers 2


I use following to have simple text objects including <tab> one:

" 26 simple text objects
" ----------------------
" i_ i. i: i, i; i| i/ i\ i* i+ i- i# i<tab>
" a_ a. a: a, a; a| a/ a\ a* a+ a- a# a<tab>
" Usage:
" for char in [ '_', '.', ':', ',', ';', '<bar>', '/', '<bslash>', '*', '+', '-', '#', '<tab>' ]
"     execute 'xnoremap <silent> i' .. char .. ' :<C-u>call Tobj("' .. char .. '", 1)<CR>'
"     execute 'xnoremap <silent> a' .. char .. ' :<C-u>call Tobj("' .. char .. '", 0)<CR>'
"     execute 'onoremap <silent> i' .. char .. ' :normal vi' .. char .. '<CR>'
"     execute 'onoremap <silent> a' .. char .. ' :normal va' .. char .. '<CR>'
" endfor
func! Tobj(char, inner) abort
    let lnum = line('.')
    let char = escape(a:char, '.*')
    if (search('^\|'.char, 'cnbW', lnum) && search(char, 'W', lnum))
          \ || (search(char, 'nbW', lnum) && search(char.'\|$', 'cW', lnum))
        if a:inner
            call search('[^'.a:char.']', 'cbW', lnum)
        normal! v
        call search('^\|'.char, 'bW', lnum)
        if a:inner
            call search('[^'.a:char.']', 'cW', lnum)

for char in [ '_', '.', ':', ',', ';', '<bar>', '/', '<bslash>', '*', '+', '-', '#', '<tab>' ]
    execute 'xnoremap <silent> i' .. char .. ' :<C-u>call Tobj("' .. char .. '", 1)<CR>'
    execute 'xnoremap <silent> a' .. char .. ' :<C-u>call Tobj("' .. char .. '", 0)<CR>'
    execute 'onoremap <silent> i' .. char .. ' :normal vi' .. char .. '<CR>'
    execute 'onoremap <silent> a' .. char .. ' :normal va' .. char .. '<CR>'

enter image description here


One option to consider is to use the vim-textobj-user plug-in, which allows you to easily define your own text objects from simple patterns.

In your case, you can use the following to define the it and at patterns:

call textobj#user#plugin('vimrc', {
\   'tab-a': {
\     'pattern': '[^\t]\+\t*',
\     'select': 'at',
\   },
\   'tab-i': {
\     'pattern': '[^\t]\+',
\     'select': 'it',
\   },
\ })

(This of course assumes you have the plug-in properly installed.)

The plug-in will define both a visual mapping and an operator-pending mapping for these bindings.

The at mapping above will select a number of tabs following a field. If you want to limit it to a single one, use '[^\t]\+\t\?' for the pattern instead.

There are some limitations, such as you can't really use these patterns with a count (e.g. v5at to select 5 fields) and you can't use repeated its or ats to extend the selection. So there's a merit to using functions to define your mappings in that they can process counts and extend existing selections, but perhaps something as simple as this solution is all you need.

(Please note that at and it are already valid motions, to select around HTML/XML tags, so consider whether you want to override those mappings or use different ones. vim-textobj-user also supports defining mappings per filetype, in which case you can define these only for your particular filetype that contains tab-separated fields, see the full documentation for detais on how to define those.)

  • Thanks. Tbh, I'm not entirely happy with how tabs are treated in this solution. When the cursor is between the first and second field, vit selects the second field while vat selects the first. Perhaps this would be easy to fix, but as a general rule, I'd rather use pure Vim, without any plugins. Sorry I didn't think to mention this in the question.
    – Kamil S.
    Commented Oct 30, 2020 at 7:49
  • 2
    I wonder about reminding the OP that it and at are already text-objects (perhaps iT/aT are better here?)
    – D. Ben Knoble
    Commented Oct 30, 2020 at 13:18
  • 1
    I didn't know that. I don't normally work with tags, but you're right, I'll remap to if/af for 'field'
    – Kamil S.
    Commented Nov 5, 2020 at 22:14

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.