Last active
January 2, 2026 08:24
-
-
Save numEricL/211ff922871cd246235da808e89231bc to your computer and use it in GitHub Desktop.
Create vim text object for unified brackets ( { { etc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| " 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