Literature adds to reality, it does not simply describe it. It enriches the necessary competencies that daily life requires and provides; and in this respect, it irrigates the deserts that our lives have already become.

– C. S. Lewis

What are Text Objects?

Even if you are a newcomer to vim, you’re likely familiar with the concept of a text object. In vimtutor, this concept is introduced pretty early on. Text objects are “commands that can only be used while in Visual mode or after an operator”, as is explained in :help text-objects. Essentially, text objects are descriptors that tell an operator what to operate on. If you think about an operator as a verb, then metaphorically, a text object is like a direct object, or what the verb is operating on. They do this by creating a visual selection that the operator then operates on.

One of the most commonly-used examples is iw, which is short for or inner-word. If you type diw, for instance, this will delete the word under the cursor. d is the operator: delete; iw is the text object: inner-word. We could easily change the operator to c for change, or we could even use tpope’s vim-surround plugin in order to gain access to the ys operator and do something like ysiw" to surround the inner-word with double-quotes. We could also change the text object to something like aw (short for around-word), which includes the inner-word as well as any surrounding whitespace.

In the same way that “literature adds to reality”, text objects add to vim’s ability to describe transformations on text. There are a handful of very useful built-in text objects–which you can read about at :help text-objects–but we can make our own that add to this in order to both increase our productivity and sometimes save those extra couple of keystrokes. I use my own text objects quite frequently, and the rest of this article is concerned with creating text objects. Hopefully, it will be of use to you.

Simple Text Object Example

In order to make a new text object, you should map both the visual-mode mapping as well as the operator-pending mapping. Respectively, this can be achieved using the commands :xnoremap and :onoremap.

Text Object inner-line

Here is a very simple example that creates the text object il for inner-line:

" "in line" (entire line sans white-space; cursor at beginning--ie, ^)
xnoremap <silent> il :<c-u>normal! g_v^<cr>
onoremap <silent> il :<c-u>normal! g_v^<cr>

In order to fully understand the mappings, it might be useful to review Markzen’s pun-filled article, “For Mappings And A Tutorial”. However, I’ll briefly describe each component and its function below and then how each of these components work together to achieve our goal: a new text object.

Both of these commands–xnoremap and onoremap–take a left-hand-side (LHS) and a right-hand side (RHS) and bind the LHS to the RHS. The LHS is just a sequence of keys. In our example, the LHS is il. When the LHS is typed in the correct mode–for example in visual-mode when xnoremap is used–then the RHS will be executed. In our example, the RHS is :<c-u>normal! g_v^<cr>. This means that when we type il in either visual-mode or when vim is expecting an operator (we used both xnoremap and onoremap to bind both of these respective modes), then vim will execute the RHS.

inner-line LHS: il

The <silent> simply means that when the RHS is being executed, vim should not display any output in the command-line. If we were to not use <silent>, then we would see :normal! g_v^ in the command-line every time we pressed il in visual-mode or when there is a pending operator, and that could get fairly annoying.

Of course, the LHS is il, which is what we must press in visual-mode or when there is a pending operator in order to execute the RHS and fulfil our text object’s purpose. This can be anything we want, but I find that il for inner-line makes the most sense. You can even use special-keys, such as <F12> or <c-bslash>, to map special keys. However, note that there are very limited options and vim maps quite a few of them already in visual mode.

inner-line RHS: :<c-u>normal! g_v^<cr>

The : begins command-line mode, and it operates as if you, the user, had typed : yourself. However, there is a gotcha. When : is pressed in visual mode, vim automatically puts the range, '<,'>, in the command-line because it assumes that you want to use the range for the command you’re about to type. This leaves us with :'<,'> in total. Since the range, '<,'>, will interfere with our mapping, we can use <c-u> (:help c_CTRL-U) to clear the command-line and restore it from :'<,'> to just :.

Then, we start the actual command we want to execute: :normal! g_v^. Essentially, this command moves to the last non-whitespace character on the line with g_, then enters visual mode with v, and finally moves to the first non-whitespace character on the line with ^. To read more about :normal, check out :help :normal, and of course, if you’re not familiar with the motions or visual mode check out :help g_, :help ^, and :help visual-mode.

All Together

What have we achieved? We have created a simple mapping such that when we press il in visual mode or after an operator, vim will visually select or operate on (respectively) the inner line. For example, vil will select from the first non-whitespace character until the last non-whitespace character. Also, cil will change what the visual selection would have selected. As a final example, yil will yank the inner line.

More Text Object Examples

Now that we have covered in depth how to create a simple text object, let’s cover a few more simple example before we dive into some more complex ones.

Text Object around-line

Since we have created a text object that selects the inner line (the whole line sans any trailing whitespace), lets make one for around-line that selects the entire line with the exception of the newline at the end.

" "around line" (entire line sans trailing newline; cursor at beginning--ie, 0)
xnoremap <silent> al :<c-u>normal! $v0<cr>
onoremap <silent> al :<c-u>normal! $v0<cr>

Well, that was easy. We simply needed to replace g_ with $ (instead of going to the last non-whitespace character, we simply want to go to the last character) as well as replace ^ with 0 (instead of going to the soft start of the line, go to the beginning of the line). The relevant motions are :help $ and :help 0.

Now we can do things like "+yal to yank the line (without the newline at the end) into the system clipboard. Or we can use val to select the current line sans the newline at the end.

Text Object inner-document

Suppose we wanted to make a text object that selected the entire document we’re currently working in, since it might be annoying to have to use something like ggcG–or, more philosophically, ggVGc. Well, we know that we can describe this operator with a text object and give it a name, so let’s do just that for id, or inner-document.

How do we achieve this? We know that we will need to move to the end of the document, start visual mode, and finally move to the beginning of the document. If you’ve been using vim, you might easily recognize that the two motions we need are :help G and :help gg. Let’s piece it all together based on what we’ve learned:

" "in document" (from first line to last; cursor at top--ie, gg)
xnoremap <silent> id :<c-u>normal! G$Vgg0<cr>
onoremap <silent> id :<c-u>normal! GVgg<cr>

Now, we can be as philosophical as we like (for example, with vidc rather than cid) but we can save one character. While This isn’t an amazing feat, we can do even better since the RHS can be as arbitrarily complex as we want it to be–within reason.

Complex Text Object Examples

The above text objects were simply for saving one or two key strokes. However, text objects can quickly become unwieldy. In such cases it is useful to map a text object to a function, or to illustrate it with pseudocode: [xo]noremap {LHS} :<c-u>call MyTextObjectFunc()<cr>. The commands in the function will achieve the same thing that the simple :normal! command achieved above–that is to visually select the proper region of text.

Text Objects in-number and around-number

Suppose we want to make a text object to select a number, which can be a binary, a hex, or a decimal number. It would not be easy to put this in a one-line command, but it is certainly possible to make a function that will do just that. That’s exactly what the two functions below do. The first function will only select the number with in, while the second function will select the number and any surrounding whitespace with an.

  • in-number
" regular expressions that match numbers (order matters .. keep '\d' last!)
" note: \+ will be appended to the end of each
let s:regNums = [ '0b[01]', '0x\x', '\d' ]

function! s:inNumber()
	" select the next number on the line
	" this can handle the following three formats (so long as s:regNums is
	" defined as it should be above this function):
	"   1. binary  (eg: "0b1010", "0b0000", etc)
	"   2. hex     (eg: "0xffff", "0x0000", "0x10af", etc)
	"   3. decimal (eg: "0", "0000", "10", "01", etc)
	" NOTE: if there is no number on the rest of the line starting at the
	"       current cursor position, then visual selection mode is ended (if
	"       called via an omap) or nothing is selected (if called via xmap)

	" need magic for this to work properly
	let l:magic = &magic
	set magic

	let l:lineNr = line('.')

	" create regex pattern matching any binary, hex, decimal number
	let l:pat = join(s:regNums, '\+\|') . '\+'

	" move cursor to end of number
	if (!search(l:pat, 'ce', l:lineNr))
		" if it fails, there was not match on the line, so return prematurely
		return
	endif

	" start visually selecting from end of number
	normal! v

	" move cursor to beginning of number
	call search(l:pat, 'cb', l:lineNr)

	" restore magic
	let &magic = l:magic
endfunction

" "in number" (next number after cursor on current line)
xnoremap <silent> in :<c-u>call <sid>inNumber()<cr>
onoremap <silent> in :<c-u>call <sid>inNumber()<cr>
  • around-number
function! s:aroundNumber()
	" select the next number on the line and any surrounding white-space;
	" this can handle the following three formats (so long as s:regNums is
	" defined as it should be above these functions):
	"   1. binary  (eg: "0b1010", "0b0000", etc)
	"   2. hex     (eg: "0xffff", "0x0000", "0x10af", etc)
	"   3. decimal (eg: "0", "0000", "10", "01", etc)
	" NOTE: if there is no number on the rest of the line starting at the
	"       current cursor position, then visual selection mode is ended (if
	"       called via an omap) or nothing is selected (if called via xmap);
	"       this is true even if on the space following a number

	" need magic for this to work properly
	let l:magic = &magic
	set magic

	let l:lineNr = line('.')

	" create regex pattern matching any binary, hex, decimal number
	let l:pat = join(s:regNums, '\+\|') . '\+'

	" move cursor to end of number
	if (!search(l:pat, 'ce', l:lineNr))
		" if it fails, there was not match on the line, so return prematurely
		return
	endif

	" move cursor to end of any trailing white-space (if there is any)
	call search('\%'.(virtcol('.')+1).'v\s*', 'ce', l:lineNr)

	" start visually selecting from end of number + potential trailing whitspace
	normal! v

	" move cursor to beginning of number
	call search(l:pat, 'cb', l:lineNr)

	" move cursor to beginning of any white-space preceding number (if any)
	call search('\s*\%'.virtcol('.').'v', 'b', l:lineNr)

	" restore magic
	let &magic = l:magic
endfunction

" "around number" (next number on line and possible surrounding white-space)
xnoremap <silent> an :<c-u>call <sid>aroundNumber()<cr>
onoremap <silent> an :<c-u>call <sid>aroundNumber()<cr>

Brief analysis of in-number and around-number

These text objects have served me very well when editing code. When it comes to editing css, in particular, which has a lot of things like left: 10px;, it can be useful to change the next number on the line. I must use cin at least a handful of times each day, but I probably use it closer to hundreds of times.

I won’t go too in-depth, but essentially the function figures out if there is a number on the line that matches a binary, hex, or decimal regex, and if there is a match, then it visually selects the number (around-number also selects surrounding whitespace). let l:lineNr = line('.') gets the current line number and let l:pat = join(s:regNums, '\+\|') builds the regex for a valid number based on the list, s:regNums. Next, if (!search(l:pat, 'ce', l:lineNr)) attempts to search until the end of the number; if this fails, then we will return without visually selecting anything, since search() will fail if it does not match anything but will move the cursor to the end of the match if it succeeds. Finally, we call normal! v to begin the visual selection and then move to the beginning of the same match. The most important thing to become acquainted with here is :help search().

Text Objects in-indentation and around-indentation

The last two text objects that I’ll cover are quite useful for python code and coders who meticulously keep their code indented properly (as most of us do). They select (and quickly might I add) an entire region of indentation! The first one, in-indentation (ii), selects only the indentation without any surrounding empty lines, and the second one, around-indentation (ai), selects the current indentation level in addition to any surround empty lines. Cool!

  • in-indentation
function! s:inIndentation()
	" select all text in current indentation level excluding any empty lines
	" that precede or follow the current indentationt level;
	"
	" the current implementation is pretty fast, even for many lines since it
	" uses "search()" with "\%v" to find the unindented levels
	"
	" NOTE: if the current level of indentation is 1 (ie in virtual column 1),
	"       then the entire buffer will be selected
	"
	" WARNING: python devs have been known to become addicted to this

	" magic is needed for this
	let l:magic = &magic
	set magic

	" move to beginning of line and get virtcol (current indentation level)
	" BRAM: there is no searchpairvirtpos() ;)
	normal! ^
	let l:vCol = virtcol(getline('.') =~# '^\s*$' ? '$' : '.')

	" pattern matching anything except empty lines and lines with recorded
	" indentation level
	let l:pat = '^\(\s*\%'.l:vCol.'v\|^$\)\@!'

	" find first match (backwards & don't wrap or move cursor)
	let l:start = search(l:pat, 'bWn') + 1

	" next, find first match (forwards & don't wrap or move cursor)
	let l:end = search(l:pat, 'Wn')

	if (l:end !=# 0)
		" if search succeeded, it went too far, so subtract 1
		let l:end -= 1
	endif

	" go to start (this includes empty lines) and--importantly--column 0
	execute 'normal! '.l:start.'G0'

	" skip empty lines (unless already on one .. need to be in column 0)
	call search('^[^\n\r]', 'Wc')

	" go to end (this includes empty lines)
	execute 'normal! Vo'.l:end.'G'

	" skip backwards to last selected non-empty line
	call search('^[^\n\r]', 'bWc')

	" go to end-of-line 'cause why not
	normal! $o

	" restore magic
	let &magic = l:magic
endfunction

" "in indentation" (indentation level sans any surrounding empty lines)
xnoremap <silent> ii :<c-u>call <sid>inIndentation()<cr>
onoremap <silent> ii :<c-u>call <sid>inIndentation()<cr>
  • around-indentation
function! s:aroundIndentation()
	" select all text in the current indentation level including any emtpy
	" lines that precede or follow the current indentation level;
	"
	" the current implementation is pretty fast, even for many lines since it
	" uses "search()" with "\%v" to find the unindented levels
	"
	" NOTE: if the current level of indentation is 1 (ie in virtual column 1),
	"       then the entire buffer will be selected
	"
	" WARNING: python devs have been known to become addicted to this

	" magic is needed for this (/\v doesn't seem work)
	let l:magic = &magic
	set magic

	" move to beginning of line and get virtcol (current indentation level)
	" BRAM: there is no searchpairvirtpos() ;)
	normal! ^
	let l:vCol = virtcol(getline('.') =~# '^\s*$' ? '$' : '.')

	" pattern matching anything except empty lines and lines with recorded
	" indentation level
	let l:pat = '^\(\s*\%'.l:vCol.'v\|^$\)\@!'

	" find first match (backwards & don't wrap or move cursor)
	let l:start = search(l:pat, 'bWn') + 1

	" NOTE: if l:start is 0, then search() failed; otherwise search() succeeded
	"       and l:start does not equal line('.')
	" FORMER: l:start is 0; so, if we add 1 to l:start, then it will match
	"         everything from beginning of the buffer (if you don't like
	"         this, then you can modify the code) since this will be the
	"         equivalent of "norm! 1G" below
	" LATTER: l:start is not 0 but is also not equal to line('.'); therefore,
	"         we want to add one to l:start since it will always match one
	"         line too high if search() succeeds

	" next, find first match (forwards & don't wrap or move cursor)
	let l:end = search(l:pat, 'Wn')

	" NOTE: if l:end is 0, then search() failed; otherwise, if l:end is not
	"       equal to line('.'), then the search succeeded.
	" FORMER: l:end is 0; we want this to match until the end-of-buffer if it
	"         fails to find a match for same reason as mentioned above;
	"         again, modify code if you do not like this); therefore, keep
	"         0--see "NOTE:" below inside the if block comment
	" LATTER: l:end is not 0, so the search() must have succeeded, which means
	"         that l:end will match a different line than line('.')

	if (l:end !=# 0)
		" if l:end is 0, then the search() failed; if we subtract 1, then it
		" will effectively do "norm! -1G" which is definitely not what is
		" desired for probably every circumstance; therefore, only subtract one
		" if the search() succeeded since this means that it will match at least
		" one line too far down
		" NOTE: exec "norm! 0G" still goes to end-of-buffer just like "norm! G",
		"       so it's ok if l:end is kept as 0. As mentioned above, this means
		"       that it will match until end of buffer, but that is what I want
		"       anyway (change code if you don't want)
		let l:end -= 1
	endif

	" finally, select from l:start to l:end
	execute 'normal! '.l:start.'G0V'.l:end.'G$o'

	" restore magic
	let &magic = l:magic
endfunction

" "around indentation" (indentation level and any surrounding empty lines)
xnoremap <silent> ai :<c-u>call <sid>aroundIndentation()<cr>
onoremap <silent> ai :<c-u>call <sid>aroundIndentation()<cr>

Brief analysis of in-indentation and around-indentation

I will keep this one very brief, but I will say that I worked pretty hard to make this as fast as possible. It uses /\%v after noting the current level of indentation in order to quickly match the current indentation level. It is a vast improvement over its predecessor that used regex on each line in a for loop to find the indentation region. Hopefully, the comments are helpful.

Conclusion

If you find yourself typing a motion followed by an operator and another motion, then maybe you want to create a quick text object. It might be valuable to create such text objects that can save some time, such as selecting a number or an indentation level. It’s simply so neat that vim is able to provide such an intricate and powerful way to describe how to manipulate text. Indeed, that is its primary objective, and it does a damn good job at it.

Also, if you want, you are welcome to copy the code snippets above–there is no license on them. Good luck vimming. Try to stay productive but also curious and remember to explore vim and other software, places, cultures, and ideas when you get the opportunity.