Skip to content

Instantly share code, notes, and snippets.

@habamax
Last active April 15, 2024 12:43
Show Gist options
  • Star 37 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save habamax/4662821a1dad716f5c18205489203a67 to your computer and use it in GitHub Desktop.
Save habamax/4662821a1dad716f5c18205489203a67 to your computer and use it in GitHub Desktop.

Some useful custom text objects for vim

Collection of my custom text objects I use quite often.

97975602 6e90ee00 1dda 11eb 9286 6894300457e3

Numbers

Put it into your .vimrc:

"" number text object
"" only "inner" text object: vin, din, cin
func! s:number_textobj()
    let rx_num = '\d\+\(\.\d\+\)*'
    if search(rx_num, 'ceW')
        normal v
        call search(rx_num, 'bcW')
    endif
endfunc

xnoremap <silent> in :<C-u>call <sid>number_textobj()<CR>
onoremap in :<C-u>normal vin<CR>

Dates

Put it into .vimrc:

let s:mons_en = ['Jan', 'Feb', 'Mar', 'Apr',
               \ 'May', 'Jun', 'Jul', 'Aug',
               \ 'Sep', 'Oct', 'Nov', 'Dec']
let s:months_en = ['January', 'February', 'March', 'April',
                 \ 'May', 'June', 'July', 'August',
                 \ 'September', 'October', 'November', 'December']
let s:months_ru = ['января', 'февраля', 'марта', 'апреля',
                 \ 'мая', 'июня', 'июля', 'августа',
                 \ 'сентября', 'октября', 'ноября', 'декабря']

let s:months = extend(s:months_en, s:months_ru)
let s:months = extend(s:months, s:mons_en)

"" * ISO-8601 2020-03-21
"" * RU 21 марта 2020
"" * EN 10 December 2012
"" * EN December 10, 2012
"" * EN 10 Dec 2012
"" * EN Dec 10, 2012
func! s:date_textobj(inner)
    let save_cursor = getcurpos()
    let cword = expand("<cword>")
    if  cword =~ '\d\{4}'
        let rx = '^\|'
        let rx = '\%(\D\d\{1,2}\s\+\%(' . join(s:months, '\|') . '\)\)'
        let rx .= '\|'
        let rx .= '\%(\s*\%(' . join(s:months, '\|') . '\)\s\+\d\{1,2},\)'
        if !search(rx, 'bcW', line('.'))
            call search('\s*\D', 'bcW', line('.'))
        endif
    elseif cword =~ join(s:months, '\|')
        call search('^\|\D\ze\d\{1,2}\s\+', 'bceW')
    elseif cword =~ '\d\{1,2}'
        if !search('^\|\S\ze\%(' . join(s:months, '\|') . '\)\s\+\d\{1,2}', 'bceW')
            call search('^\|[^0-9\-]', 'becW')
        endif
    endif

    let rxdate = '\%(\d\{4}-\d\{2}-\d\{2}\)'
    let rxdate .= '\|'
    let rxdate .= '\%(\d\{1,2}\s\+\%(' . join(s:months, '\|') . '\)\s\+\d\{4}\)'
    let rxdate .= '\|'
    let rxdate .= '\%(\%(' . join(s:months, '\|') . '\)\s\+\d\{1,2},\s\+\d\{4}\)'
    if !a:inner
        let rxdate = '\s*\%('.rxdate.'\)\s*'
    endif

    if search(rxdate, 'cW')
        normal v
        call search(rxdate, 'ecW')
        return
    endif
    call setpos('.', save_cursor)
endfunc


xnoremap <silent> id :<C-u>call <sid>date_textobj(1)<CR>
onoremap id :<C-u>normal vid<CR>
xnoremap <silent> ad :<C-u>call <sid>date_textobj(0)<CR>
onoremap ad :<C-u>normal vad<CR>

Markdown Headers

Put it into .vim/after/ftplugin/markdown.vim:

"" Markdown header text object
" * inner object is the text between prev section header(excluded) and the next
"   section of the same level(excluded) or end of file.
" * an object is the text between prev section header(included) and the next section of the same
"   level(excluded) or end of file.
func! s:header_textobj(inner) abort
    let lnum_start = search('^#\+\s\+[^[:space:]=]', "ncbW")
    if lnum_start
        let lvlheader = matchstr(getline(lnum_start), '^#\+')
        let lnum_end = search('^#\{1,'..len(lvlheader)..'}\s', "nW")
        if !lnum_end
            let lnum_end = search('\%$', 'cnW')
        else
            let lnum_end -= 1
        endif
        if a:inner && getline(lnum_start + 1) !~ '^#\+\s\+[^[:space:]=]'
            let lnum_start += 1
        endif

        exe lnum_end
        normal! V
        exe lnum_start
    endif
endfunc


onoremap <buffer><silent> ih :<C-u>call <sid>header_textobj(v:true)<CR>
onoremap <buffer><silent> ah :<C-u>call <sid>header_textobj(v:false)<CR>
xnoremap <buffer><silent> ih :<C-u>call <sid>header_textobj(v:true)<CR>
xnoremap <buffer><silent> ah :<C-u>call <sid>header_textobj(v:false)<CR>

Indents

Put it into your .vimrc:

"" Indent text object
"" Useful for python-like indentation based programming lanugages
func! s:indent_textobj(inner)
    if getline('.') =~ '^\s*$'
        let ln_start = s:detect_nearest_line()
        let ln_end = ln_start
    else
        let ln_start = line('.')
        let ln_end = ln_start
    endif

    let indent = indent(ln_start)
    if indent > 0
        while indent(ln_start) >= indent && ln_start > 0
            let ln_start = prevnonblank(ln_start-1)
        endwhile

        while indent(ln_end) >= indent && ln_end <= line('$')
            let ln_end = s:nextnonblank(ln_end+1)
        endwhile
    else
        while indent(ln_start) == 0 && ln_start > 0 && getline(ln_start) !~ '^\s*$'
            let ln_start -= 1
        endwhile
        while indent(ln_start) > 0 && ln_start > 0
            let ln_start = prevnonblank(ln_start-1)
        endwhile
        while indent(ln_start) == 0 && ln_start > 0 && getline(ln_start) !~ '^\s*$'
            let ln_start -= 1
        endwhile

        while indent(ln_end) == 0 && ln_end <= line('$') && getline(ln_end) !~ '^\s*$'
            let ln_end += 1
        endwhile
        while indent(ln_end) > 0 && ln_end <= line('$')
            let ln_end = s:nextnonblank(ln_end+1)
        endwhile
    endif

    if a:inner || indent == 0
        let ln_start = s:nextnonblank(ln_start+1)
    endif

    if a:inner
        let ln_end = prevnonblank(ln_end-1)
    else
        let ln_end = ln_end-1
    endif

    if ln_end < ln_start
        let ln_end = ln_start
    endif

    exe ln_end
    normal! V
    exe ln_start
endfunc


func! s:nextnonblank(lnum) abort
    let res = nextnonblank(a:lnum)
    if res == 0
        let res = line('$')+1
    endif
    return res
endfunc


func! s:detect_nearest_line() abort
    let lnum = line('.')
    let nline = s:nextnonblank(lnum)
    let pline = prevnonblank(lnum)
    if abs(nline - lnum) > abs(pline - lnum) || getline(nline) =~ '^\s*$'
        return pline
    else
        return nline
    endif
endfunc


onoremap <silent>ii :<C-u>call <sid>indent_textobj(v:true)<CR>
onoremap <silent>ai :<C-u>call <sid>indent_textobj(v:false)<CR>
xnoremap <silent>ii :<C-u>call <sid>indent_textobj(v:true)<CR>
xnoremap <silent>ai :<C-u>call <sid>indent_textobj(v:false)<CR>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment