Creating custom text objects in (neo)vim

Posted on 04 Sep 2020 - filed under: vim, texteditor

2020-09-10 update: see at the bottom for a better way to create the mappings

Custom text objects ?!

I’ve been using the Vim for ~11 years now (well, neovim really), and somehow I’ve only today realized users can create their own text objects !

If you don’t already know what text objects are, read the man page (:h text-objects), you’re really missing out on Vim’s modal superpowers.

I also won’t bore you with the exact details on how to create your own text objects, Vimways has an excellent article on the subject.

How it got there

Long story short (not), I’m using Tim Pope’s vim-dadbod plugin to interact with my database (another epic plugin by the legend btw), and I have a markdown file with some queries fenced in an SQL code block with some description, literate programing style. I store queries I frequently run, and explain in detais more complex ones.

An example of my “queries” file

This setup plays nicely with vim-dadbod because alongside being able to run queries from the vim command line (eg. :DB SELECT id FROM mytable;<CR>) you can also visually select a bunch of lines and call :DB on it.

All my queries are fenced, and was looking for a way to execute the whole block without having to visually and manually select them.

Productivity level: -100

I then set out to find a way to do that, I first found this great blog post about using text objects with dadbod, but it turns out the feature is already baked in dadbod.

Then there is also this issue from 4 years ago about having a text object for code fence blocks in vim-markdown (this is getting to be a pretty deep rabbit hole).

Time for some action.

Markdown Code Fence Text objects !

Just slip this somewhere in your config file and

function! s:inCodeFence()
    " Search backwards for the opening of the code fence.
	call search('^```.*$', 'bceW')
    " Move one line down
	normal! j
    " Move to the begining of the line at start selecting
	normal! 0v
    " Search forward for the closing of the code fence.
	call search("```", 'ceW')

	normal! kg_
endfunction

function! s:aroundCodeFence()
    " Search backwards for the opening of the code fence.
	call search('^```.*$', 'bcW')
	normal! v$
    " Search forward for the closing of the code fence.
	call search('```', 'eW')
endfunction

autocmd Filetype markdown xnoremap <silent> if :<c-u>call <sid>inCodeFence()<cr>
autocmd Filetype markdown onoremap <silent> if :<c-u>call <sid>inCodeFence()<cr>
autocmd Filetype markdown xnoremap <silent> af :<c-u>call <sid>aroundCodeFence()<cr>
autocmd Filetype markdown onoremap <silent> af :<c-u>call <sid>aroundCodeFence()<cr>

Bonus: SQL Queries Text Object

And as a little bonus from this github issue that gives text objects directly for queries ! So you can use that outside of fenced markdown code blocks.

vnoremap aq <esc>:call search(";", "cWz")<cr>:call search(";\\<bar>\\%^", "bsWz")<cr>:call search("\\v\\c^(select<bar>with<bar>insert<bar>update<bar>delete<bar>create)\>", "Wz")<cr>vg`'
omap aq :normal vaq<cr>

Creating a mapping

Now that we have two types of text objects (queries and code fenced blocks), all we need is a mapping to run the queries via vim-dadbod !

Here is mine (my leader key is <Space>):

nmap <expr> <leader>d db#op_exec()
xmap <expr> <leader>d db#op_exec()

And now all you need to do is (in normal mode) <Space>dif to run a query that’s in a fenced code block, and <Space>daq to run a query !

Here is a little demo

🎉

2020-09-10 Update

As pointed by this kind dude a better way to provide those mappings is to define them as buffer local mappings. And creating an after ftplugin file for markdown is even more idiomatic (my initial autocmd would be run each time the markdown filetype was set).

To quote the VimWays page on mappings:

<buffer> makes the new mapping buffer-local, ie. it will be defined only for the buffer that was current when the mapping was created. It is useful in filetype-specific settings, eg. in ~/.vim/after/ftplugin/help.vim […] A buffer-local mapping […] is created on buffers with the help filetype, ie. help buffers created with :help. […] Obviously, this only makes sense in help buffers, so we should not make this mapping global.

So the updated final version, in ~/.vim/after/ftplugin/markdown.vim:

function! s:inCodeFence()
    " Search backwards for the opening of the code fence.
	call search('^```.*$', 'bceW')
    " Move one line down
	normal! j
    " Move to the begining of the line at start selecting
	normal! 0v
    " Search forward for the closing of the code fence.
	call search("```", 'ceW')

	normal! kg_
endfunction

function! s:aroundCodeFence()
    " Search backwards for the opening of the code fence.
	call search('^```.*$', 'bcW')
	normal! v$
    " Search forward for the closing of the code fence.
	call search('```', 'eW')
endfunction

xnoremap <buffer> <silent> if :call <sid>inCodeFence()<cr>
onoremap <buffer> <silent> if :call <sid>inCodeFence()<cr>
xnoremap <buffer> <silent> af :call <sid>aroundCodeFence()<cr>
onoremap <buffer> <silent> af :call <sid>aroundCodeFence()<cr>

Comments

Comment on github