33

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 T`ct' (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?)

5 Answers 5

12

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>
4
  • This works for iq, but doesn't seem to work with, for example, daq.
    – Doorknob
    Commented Feb 13, 2015 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.
    – superjer
    Commented Feb 16, 2015 at 23:21
  • This works for single quotes, but daq on ``foo'' just results in `foo'.
    – Doorknob
    Commented Feb 16, 2015 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.
    – superjer
    Commented Feb 17, 2015 at 1:49
10

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! F`vf'<cr>
onoremap iq :<c-u>normal! T`vt'<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! 2F`v2f'<cr>
onoremap i" :<c-u>normal! 2T`v2t'<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. ma2F`v2f'<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! muF`vf'<cr>`u
    au FileType tex,plaintex onoremap i' :<c-u>normal! muT`vt'<cr>`u
    au FileType tex,plaintex onoremap a" :<c-u>normal! mu2F`v2f'<cr>`u
    au FileType tex,plaintex onoremap i" :<c-u>normal! mu2T`v2t'<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.

3

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.

2

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

2

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>
2
  • 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
    – D. Ben Knoble
    Commented Dec 30, 2021 at 12:52
  • @D.BenKnoble, thank you! I'll look into <Cmd> when I can. Edited for the others. Commented Dec 30, 2021 at 13:03

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.