# How can I treat LaTeX quotes as a text object?

Quotation marks in LaTeX are different than in normal text, in that they're formatted like this'' (or like this' for single quotes). This makes it difficult to navigate through them in Vim.

I usually use something like Tct' (backwards until , change until '). However, this is clunky and unnatural. I would prefer to be able to use something along the lines of ciq ("change in quotes"), just like I can do ciw, ca), ci", ca', etc.

Is there any way to tell Vim to treat LaTeX'' quotes' as text objects? (As a plus, is there any way to only trigger this while editing a .tex file?)

I don't know LaTeX but this seems to work:

vnoremap iq :<C-U>silent! normal! t'vT<CR>
omap iq :normal viq<CR>


I based this off the information over here: http://vim.wikia.com/wiki/Creating_new_text_objects

You can add support for aq as well:

vnoremap aq :<C-U>silent! normal! f'vF<CR>
omap aq :normal vaq<CR>

• This works for iq, but doesn't seem to work with, for example, daq. Feb 13 '15 at 22:13
• Right. You can make it work with aq as well with an additional similar mapping. I edited the answer. I also swapped T with t' in the mapping so that it searches forward first, which seems more natural to me. Feb 16 '15 at 23:21
• This works for single quotes, but daq on foo'' just results in foo'. Feb 16 '15 at 23:57
• I don't think this method can work for more than single quotes. You would need to do deeper inspection of the text than the f and t operators allow, which would require writing a whole function with variables and everything. I don't have time to do so right now. Feb 17 '15 at 1:49

Edit: I just discovered the vim-textobject-latex plugin, which adds the exact functionality you're looking for (in addition to a few other things). From the plugin's README:

Currently supported text objects are:

a   i   Description
a\\ i\\ Inline math surrounded by \$$ and \$$.
a$i$  Inline math surrounded by dollar signs.
aq  iq  Single-quoted text \like this'.
aQ  iQ  Double-quoted text \like this''.
ae  ie  Environment \begin{...}–\end{...}


So you can use caq and the like as you wanted. I tested it, and it works as advertised, but note that it is dependent on the vim-textobject-user plugin. Also note you must make sure that the textobject-user plugin loads first, otherwise things won't work.

If you don't want to mess with plugins, however, my original answer remains useful:

Adding these two lines to your .vimrc will cover most of what you want.

onoremap aq :<c-u>normal! Fvf'<cr>
onoremap iq :<c-u>normal! Tvt'<cr>


These create new text objects (aq and iq) which go back to the previous , enter visual selection mode, then select up to the next '.

This will work for ciq, caq, yiq, and diq. However, yaq and daq will not yank/delete the outer quotes, so I created another text object, a", to handle that (i" is not needed, but I added to keep things consistent:

onoremap a" :<c-u>normal! 2Fv2f'<cr>
onoremap i" :<c-u>normal! 2Tv2t'<cr>


The major downside here is that this will break normal ca", etc. use. If you preface them with au FileType tex, you'll only have that issue in .tex files, but it still might occasionally be an annoyance. You could also name the object something like ad/id ('d' for 'double' quote), but that seemed less intuitive to me (I was aiming for the least difference from normal operation).

The only way I can think of to wrap these up into just two text objects would be to write a function that checks for another quote outside of the first set and set the aq text objects to call that.

Note: There are a couple other minor issues with these:

1. Yanking might leave your cursor in an unexpected place (this could be fixed by adding using a mark in the commands (e.g. ma2Fv2f'<cr>a), just make sure not to use one you frequently use manually)
2. The cursor must be inside the quotes; weird things happen if it is on the quotes.

To wrap all of that up, this is what I personally would add to my .vimrc for this issue:

augroup filetype_latex
au!
au FileType tex,plaintex onoremap a' :<c-u>normal! muFvf'<cr>u
au FileType tex,plaintex onoremap i' :<c-u>normal! muTvt'<cr>u
au FileType tex,plaintex onoremap a" :<c-u>normal! mu2Fv2f'<cr>u
au FileType tex,plaintex onoremap i" :<c-u>normal! mu2Tv2t'<cr>u
end augroup


As you can see, I opted for breaking normal ca'/ya"/etc functionality so I wouldn't have to learn new commands for them since I wouldn't be using the normal functionality as much within LaTeX anyways. That's just a personal preference; not hard to switch the object names. I also added in the filetype detection and marks.

This is an old question but for the benefit of future visitors: I find its easier to change the latex quotes with:

\usepackage{csquotes} % change " " into nice double quotes
\MakeOuterQuote{"}


And then This thing is "Foobar"! will work fine in vim and latex.

### Plugin vim-sandwich

Another useful plugin for this is vim-sandwich which has defined ft-specific surroundings

• l or l' for text',
• l" for text'' and
• textobjects to easily operate on them.

With the default mappings following work

• textobject: replace inner text of text' with cisl' or if auto detection of nearest surrounding is fine cib (e.g. cursor on h in a (phony) text' requires to press vibibc). However, you can define following additional textobject iq (OP example)  omap <unique> iq <Plug>(textobj-sandwich-query-i)l' xmap <unique> iq <Plug>(textobj-sandwich-query-i)l' omap <unique> aq <Plug>(textobj-sandwich-query-a)l' xmap <unique> aq <Plug>(textobj-sandwich-query-a)l' omap <unique> iQ <Plug>(textobj-sandwich-query-i)l" xmap <unique> iQ <Plug>(textobj-sandwich-query-i)l" omap <unique> aQ <Plug>(textobj-sandwich-query-a)l" xmap <unique> aQ <Plug>(textobj-sandwich-query-a)l"  Now simply pressing ciq or ciQ works.
• add to text with saiwl' single apostrophes to get text'
The pattern of the command is sa{motion/textobject}{surrounding} and means invoke operator add surrounding on inner word and surround type is latex single quote.
• delete with sdl' or with sdb
• change with srl'l" or with srbl"

It supports

I spent a bit of time today throwing together some code for this exact purpose, and since the existing answers either use plugins or are slightly fragile, I figured I may as well post it here. To use it, just place the code inside an ftplugin file such as ~/.vim/after/ftplugin/tex.vim. Key "features":

• Provides iq, aq, iQ, aQ text objects (duh).

• No plugins needed; should work even on ancient versions of Vim. [Well, I could turn this into a plugin, I suppose; but if you want a plugin solution then others exist, as has been mentioned already.]

• Rigorously differentiates between iq and iQ, that is to say, iq will never select between double quotes.

• Works with edge cases, including if the cursor is on a quote character, or if the line ends with opening quotes / starts with closing quotes. However, it still has some weird behaviour if you have a pair of empty quotes, like ''. :-(

• If you're not within a block of quotes, this tries its best to intelligently look forward / backwards for quotes. [This might not be perfect, but it does work well for me.]

Finally, I'm not really a Vimscript expert, so refinements / suggestions are more than welcome.

" iQ, iq, aQ, aq text objects {{{1
function! s:QuotesTextObj(textobj_type, quote_type) abort
" textobj_type: 'i' for inner, 'a' for around
" quote_type: 'q' for single quote (iq/aq), 'Q' for double quotes (iQ/aQ)

" String comparisons must use ==# to avoid issues with 'ignorecase'!
let l:start_quote = (a:quote_type ==# 'Q') ? '' : '$$^\|[^]$$\zs\ze$$\|[^]$$'
let l:end_quote = (a:quote_type ==# 'Q') ? "''" : '$$^\|[^'']$$\zs''\ze$$\|[^'']$$'
let l:quote_length = (a:quote_type ==# 'Q') ? 2 : 1

" First, assume that we're within quotes.
let l:cursor = getpos('.')
let l:start_pos = searchpos(l:start_quote, 'bcnW')
if l:start_pos != [0, 0]
call setpos('.', [l:cursor[0], l:start_pos[0], l:start_pos[1], 0])
let l:end_pos = searchpos(l:end_quote, 'cnW')
" Exit if there are no ending quotes
if l:end_pos == [0, 0]
call setpos('.', l:cursor)
return
endif
" Exit if there's literally nothing between the quotes
if l:start_pos[0] == l:end_pos[0] && l:start_pos[1] + l:quote_length >= l:end_pos[1]
call setpos('.', l:cursor)
return
endif
endif

" If we aren't, then try to do intelligent searches
if l:start_pos == [0, 0] || l:end_pos[0] < l:cursor[1] ||
\ (l:end_pos[0] == l:cursor[1] && l:end_pos[1] + l:quote_length <= l:cursor[2])
" Reset cursor
call setpos('.', l:cursor)
" Search first in the current line
let l:start_pos = searchpos(l:start_quote, 'bcnW', line('.'))
" If couldn't find, try to 'jump ahead'
if l:start_pos == [0, 0]
let l:start_pos = searchpos(l:start_quote, 'cnW')
endif
" If still couldn't find, try to search in previous lines
if l:start_pos == [0, 0]
let l:start_pos = searchpos(l:start_quote, 'bcnW')
endif
endif
" If still couldn't find opening quotes, exit
if l:start_pos == [0, 0]
call setpos('.', l:cursor)
return
endif

" Using our knowledge of the starting position, search for the ending
call setpos('.', [l:cursor[0], l:start_pos[0], l:start_pos[1], 0])
let l:end_pos = searchpos(l:end_quote, 'cnW')
if l:end_pos == [0, 0]
call setpos('.', l:cursor)
return
endif

" Adjust positions according to 'i'/'a' usage
let l:start_pos = [l:start_pos[0],
\ l:start_pos[1] + (a:textobj_type == 'i' ? l:quote_length : 0)]
let l:end_pos = [l:end_pos[0],
\ l:end_pos[1] + (a:textobj_type == 'i' ? -1 : l:quote_length - 1)]
" Adjust positions if they overflow/underflow across lines
if col([l:start_pos[0], '$']) - 1 < l:start_pos[1] let l:start_pos = [l:start_pos[0] + 1, 1] endif if l:end_pos[1] < 1 let l:end_pos = [l:end_pos[0] - 1, col([l:end_pos[0] - 1, '$']) - 1]
endif

" Set marks and enter visual mode
call setpos("'<", [l:cursor[0], l:start_pos[0], l:start_pos[1], 0])
call setpos("'>", [l:cursor[0], l:end_pos[0], l:end_pos[1], 0])
normal! <v>
endfunction
xnoremap <buffer><silent> iQ :<C-U>call <SID>QuotesTextObj('i', 'Q')<CR>
xnoremap <buffer><silent> iq :<C-U>call <SID>QuotesTextObj('i', 'q')<CR>
xnoremap <buffer><silent> aQ :<C-U>call <SID>QuotesTextObj('a', 'Q')<CR>
xnoremap <buffer><silent> aq :<C-U>call <SID>QuotesTextObj('a', 'q')<CR>
omap <buffer><silent> iQ :normal viQ<CR>
omap <buffer><silent> iq :normal viq<CR>
omap <buffer><silent> aQ :normal vaQ<CR>
omap <buffer><silent> aq :normal vaq<CR>

• Prefer xnoremap over vnoremap unless you need to map select mode. Stylistically, it’s uncommon to compress lines too much with | in scripts. You might be interested in <Cmd> which is able to run the Ex commands of a mapping without changing modes Dec 30 '21 at 12:52
• @D.BenKnoble, thank you! I'll look into <Cmd> when I can. Edited for the others. Dec 30 '21 at 13:03