Skip to content

Instantly share code, notes, and snippets.

@numEricL
Last active January 2, 2026 08:24
Show Gist options
  • Select an option

  • Save numEricL/211ff922871cd246235da808e89231bc to your computer and use it in GitHub Desktop.

Select an option

Save numEricL/211ff922871cd246235da808e89231bc to your computer and use it in GitHub Desktop.
Create vim text object for unified brackets ( { { etc
" install cyclops.vim to enable dot or pair repeat functionality
let s:begin_delimiters = '({['
let s:end_delimiters = ')}]'
let s:delimiters = s:begin_delimiters .. s:end_delimiters
xmap ad <cmd>call <sid>VisualBracketTextObj('expand', 'all', v:count1)<cr>
omap ad <cmd>call <sid>OpPendingBracketTextObj('expand', 'all', v:count1)<cr>
xmap id <cmd>call <sid>VisualBracketTextObj('expand', 'inner', v:count1)<cr>
omap id <cmd>call <sid>OpPendingBracketTextObj('expand', 'inner', v:count1)<cr>
" xmap ac <cmd>call <sid>VisualBracketTextObj('contract', 'all', v:count1)<cr>
" omap ac <cmd>call <sid>OpPendingBracketTextObj('contract', 'all', v:count1)<cr>
" xmap ic <cmd>call <sid>VisualBracketTextObj('contract', 'inner', v:count1)<cr>
" omap ic <cmd>call <sid>OpPendingBracketTextObj('contract', 'inner', v:count1)<cr>
" try
" call pair#SetMap('xmap', ['ad', 'ac'])
" call pair#SetMap('omap', ['ad', 'ac'])
" call pair#SetMap('xmap', ['id', 'ic'])
" call pair#SetMap('omap', ['id', 'ic'])
" catch //
" endtry
try
call dot#SetMap('xmap', 'ad')
call dot#SetMap('omap', 'ad')
call dot#SetMap('xmap', 'id')
call dot#SetMap('omap', 'id')
" call dot#SetMap('xmap', 'ac')
" call dot#SetMap('omap', 'ac')
" call dot#SetMap('xmap', 'ic')
" call dot#SetMap('omap', 'ic')
catch //
endtry
function s:VisualBracketTextObj(type, scope, count) abort
if mode() !~# '\v^[vV]$'
return
endif
let l:v_mode = mode()
let l:v_start = (s:PosLessThan(getpos('.'), getpos('v')))? getpos('.') : getpos('v')
let l:v_end = (s:PosLessThan(getpos('.'), getpos('v')))? getpos('v') : getpos('.')
if a:scope ==# 'inner'
call s:AdjustPosition(l:v_start, 'backward')
call s:AdjustPosition(l:v_end, 'forward')
endif
let l:offset = 0
let l:text_obj = s:FindBracketTextObj(l:v_end, a:type, 1)
if l:text_obj['valid'] && s:PosEqual(l:text_obj['start'], l:v_start) && s:PosEqual(l:text_obj['end'], l:v_end)
let l:offset = 1
endif
if a:count > 1 || l:offset > 0
let l:text_obj = s:FindBracketTextObj(l:v_end, a:type, l:offset + a:count)
endif
if a:scope ==# 'inner'
call s:AdjustPosition(l:text_obj['start'], 'forward')
call s:AdjustPosition(l:text_obj['end'], 'backward')
endif
call s:SetVisualSelection(l:v_mode, l:text_obj)
endfunction
function s:OpPendingBracketTextObj(type, scope, count) abort
let l:text_obj = s:FindBracketTextObj(getpos('.'), a:type, a:count)
if a:scope ==# 'inner'
call s:AdjustPosition(l:text_obj['start'], 'forward')
call s:AdjustPosition(l:text_obj['end'], 'backward')
endif
let l:v_mode = mode(1)[-1:]
let l:v_mode = (l:v_mode =~# '\v^[vV]$')? l:v_mode : 'v'
call s:SetVisualSelection(l:v_mode, l:text_obj)
endfunction
function s:FindBracketTextObj(pos, type, count) abort
if a:type ==# 'expand'
return s:FindExpandedBracketTextObj(a:pos, a:count)
else
return s:FindContractedBracketTextObj(a:pos, a:count)
endif
endfunction
function s:FindExpandedBracketTextObj(pos, count) abort
let l:entry_pos = getcurpos()
let l:pos = (len(a:pos) > 2)? a:pos[1:2] : a:pos
let l:char = strpart(getline(l:pos[0]), l:pos[1]-1, 1)
let l:on_begin_delim = (l:char =~# '\V\[' .. escape(s:begin_delimiters, ']') .. ']')
" pass by 'reference' so that count is incremeted across calls
let l:args = {
\ 'count' : (l:on_begin_delim)? 1 : 0,
\ 'target' : a:count,
\ }
let l:Skip = { -> s:Skip(l:args) }
call cursor(l:pos)
let l:found_pos = searchpos('\V\[' .. escape(s:delimiters, ']') .. ']', 'cnW', 0, 0, l:Skip)
call setpos('.', l:entry_pos)
if l:args.count == l:args.target
return s:MakeBracketTextObj(l:found_pos)
else
return { 'valid' : v:false }
endif
endfunction
function s:Skip(args) abort
let l:char = strpart(getline('.'), col('.')-1, 1)
if l:char =~# '\V\[' .. escape(s:begin_delimiters, ']') .. ']'
let a:args.count -= 1
elseif l:char =~# '\V\[' .. escape(s:end_delimiters, ']') .. ']'
let a:args.count += 1
endif
return a:args.count != a:args.target
endfunction
function s:FindContractedBracketTextObj(pos, count) abort
let l:entry_pos = getcurpos()
let l:pos = (len(a:pos) > 2)? a:pos[1:2] : a:pos
call cursor(l:pos)
let l:found = v:false
for l:i in range(a:count)
" search forward
let l:pos = searchpos('\V\[' .. escape(s:delimiters, ']') .. ']', 'nW')
let l:char = strpart(getline(l:pos[0]), l:pos[1]-1, 1)
let l:found = (l:char =~# '\V\[' .. escape(s:begin_delimiters, ']') .. ']')
if !l:found
" search backward
let l:pos = searchpos('\V\[' .. escape(s:delimiters, ']') .. ']', 'bnW')
let l:char = strpart(getline(l:pos[0]), l:pos[1]-1, 1)
let l:found = (l:char =~# '\V\[' .. escape(s:end_delimiters, ']') .. ']')
endif
if l:found
call cursor(l:pos)
else
break
endif
endfor
call setpos('.', l:entry_pos)
if l:found
return s:MakeBracketTextObj(l:pos)
else
return { 'valid' : v:false }
endif
endfunction
function s:MakeBracketTextObj(delimiter_pos) abort
let l:delim_pos = (len(a:delimiter_pos) > 2)? a:delimiter_pos[1:2] : a:delimiter_pos
let entry_pos = getcurpos()
call cursor(l:delim_pos)
normal! %
let l:other_pos = getpos('.')[1:2]
let l:text_obj = {}
let l:text_obj['valid'] = ( l:delim_pos[0] != l:other_pos[0] || l:delim_pos[1] != l:other_pos[1] )
if l:text_obj['valid']
if s:PosLessThan(l:delim_pos, l:other_pos)
let l:text_obj['start'] = l:delim_pos
let l:text_obj['end'] = l:other_pos
else
let l:text_obj['start'] = l:other_pos
let l:text_obj['end'] = l:delim_pos
endif
endif
call setpos('.', entry_pos)
return l:text_obj
endfunction
function s:AdjustPosition(pos, direction) abort
let l:idx = (len(a:pos) > 2)? 1 : 0
if a:direction ==# 'forward'
if a:pos[idx+1] < col([a:pos[idx], '$']) - 1
let a:pos[idx+1] += 1
elseif a:pos[idx] < line('$')
let a:pos[idx] += 1
let a:pos[idx+1] = 1
endif
else
if a:pos[idx+1] > 1
let a:pos[idx+1] -= 1
elseif a:pos[idx] > 1
let a:pos[idx] -= 1
let a:pos[idx+1] = col([a:pos[idx], '$']) - 1
endif
endif
return a:pos
endfunction
function s:SetVisualSelection(v_mode, text_obj) abort
if a:text_obj['valid']
call cursor(a:text_obj['start'])
execute "normal! \<esc>" .. a:v_mode
call cursor(a:text_obj['end'])
endif
endfunction
function s:PosLessThan(pos1, pos2) abort
let l:pos1 = len(a:pos1) > 2? a:pos1[1:2] : a:pos1
let l:pos2 = len(a:pos2) > 2? a:pos2[1:2] : a:pos2
if l:pos1[0] < l:pos2[0]
return v:true
elseif l:pos1[0] == l:pos2[0] && l:pos1[1] < l:pos2[1]
return v:true
endif
return v:false
endfunction
function s:PosEqual(pos1, pos2) abort
let l:pos1 = len(a:pos1) > 2? a:pos1[1:2] : a:pos1
let l:pos2 = len(a:pos2) > 2? a:pos2[1:2] : a:pos2
return l:pos1[0] == l:pos2[0] && l:pos1[1] == l:pos2[1]
endfunction
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment