3 minute read

vim-surround 분석

vim-surround 플러그인이 어떻게 구현되었는지 궁금해졌습니다.

코드를 보기 전에 감싸기를 어떻게 구현할지 생각해봤습니다.

Normal Mode1

function! s:Sur() 
    execute "normal! ciw< \<C-R>\" >" 
endfunction

vnoremap <leader>G :<C-U>call <SID>Sur()<CR>

위 코드는 현재 커서가 있는 단어를 <>로 감싸주는 코드입니다.

간단하게 구현되었지만, 한 가지 문제가 있습니다. 사용자 레지스터 "를 사용하고 있습니다.

어떤 사용자는 이 작업을 하기 전에 중요한 데이터가 " 레지스터에 있을 수도 있으므로 플러그인이 이를 건드리면 안 됩니다.

그래서 다음과 같이

function! s:Sur() 
    let l:temp = getreg('"')
    execute "normal! ciw< \<C-R>\" >" 
    call setreg('"', l:temp)
endfunction

레지스터를 getreg, setreg로 복구시켜줍니다.

Visual Mode1

function! s:Sur() 
    let l:temp = getreg('"')
    execute "normal! gvs< \<C-R>\" >" 
    call setreg('"', l:temp)
endfunction

vnoremap <leader>G :<C-U>call <SID>Sur()<CR>

Visual mode인 경우에는 위와 같이 구현할 수 있습니다.

gv에 대해 이해하고 싶다면 Visual mode로 셀렉트 해보고 gv를 눌러보세요.

Normal Mode2

위 구현 방법은 하나같이 레지스터를 썼다가 복구시켜줍니다.

사실은 같은 목적이라면 문서를 덜 조작하는 방향으로 플러그인을 만드는 게 맞습니다.

그래서

function! s:NSur() 
    execute "normal! bi<\<ESC>ea>"
endfunction

위 방법을 쓰면 레지스터를 건드리지 않고 목적을 달성할 수 있게 됩니다.

Visual Mode2

알아둬야 할 내용:

마지막 선택 영역을 복구하는 명령어는 gv

선택 영역에서 앞뒤로 이동하는 명령어는 o

위치를 마크하는 것은 m[아무키]

마크한 위치로 가는 것은 `[아무키]

function! s:Sur()
  exe "normal gvmboma\<Esc>"
  normal `a
  let l:lineA = line(".")
  let l:columnA = col(".")
  let l:apos = l:lineA * 100000 + l:columnA
  normal `b
  let l:lineB = line(".")
  let l:columnB = col(".")
  let l:bpos = l:lineB * 100000 + l:columnB
  " 마크 교환
  if l:apos > l:bpos
    normal mc
    normal `amb
    normal `cma
  endif
  exe "normal `ba>\<Esc>`ai<\<Esc>"
endfunction

vnoremap <leader>h :<C-U>call <SID>Sur()<CR>

현재 위치를 반환하는 linecol 내장 함수를 통해 구현할 수는 있지만, 모양이 좋지 않습니다.

게다가 사용자 마크를 건드리고 있습니다.

Visual Mode3

Vim에는 마지막 선택된 영역의 시작과 끝으로 갈 수 있는 명령어가 존재합니다.

`<`>인데 이를 이용하여 위 함수를 좀 더 간단히 하면

function! s:Sur()
  exe "normal `>a>\<ESC>`<i<"
endfunction

vim-surround의 방법

function! s:opfunc(type,...) 
  let char = s:inputreplacement()
  if char == ""
    return s:beep()
  endif
  let reg = '"'
  let sel_save = &selection
  let &selection = "inclusive"
  let cb_save  = &clipboard
  set clipboard-=unnamed clipboard-=unnamedplus
  let reg_save = getreg(reg)
  let reg_type = getregtype(reg)
  let type = a:type
  if a:type == "char"
    silent exe 'norm! v`[o`]"'.reg.'y'
    let type = 'v'
  elseif a:type == "line"
    silent exe 'norm! `[V`]"'.reg.'y'
    let type = 'V'
  elseif a:type ==# "v" || a:type ==# "V" || a:type ==# "\<C-V>"
    let &selection = sel_save
    let ve = &virtualedit
    if !(a:0 && a:1)
      set virtualedit=
    endif
    silent exe 'norm! gv"'.reg.'y'
    let &virtualedit = ve
  elseif a:type =~ '^\d\+$'
    let type = 'v'
    silent exe 'norm! ^v'.a:type.'$h"'.reg.'y'
    if mode() ==# 'v'
      norm! v
      return s:beep()
    endif
  else
    let &selection = sel_save
    let &clipboard = cb_save
    return s:beep()
  endif
  let keeper = getreg(reg)
  if type ==# "v" && a:type !=# "v"
    let append = matchstr(keeper,'\_s\@<!\s*$')
    let keeper = substitute(keeper,'\_s\@<!\s*$','','')
  endif
  call setreg(reg,keeper,type)
  call s:wrapreg(reg,char,"",a:0 && a:1)
  if type ==# "v" && a:type !=# "v" && append != ""
    call setreg(reg,append,"ac")
  endif
  silent exe 'norm! gv'.(reg == '"' ? '' : '"' . reg).'p`['
  if type ==# 'V' || (getreg(reg) =~ '\n' && type ==# 'v')
    call s:reindent()
  endif
  call setreg(reg,reg_save,reg_type)
  let &selection = sel_save
  let &clipboard = cb_save
  if a:type =~ '^\d\+$'
    silent! call repeat#set("\<Plug>Y".(a:0 && a:1 ? "S" : "s")."surround".char.s:input,a:type)
  else
    silent! call repeat#set("\<Plug>SurroundRepeat".char.s:input)
  endif
endfunction

상당히 긴데 여기서 사용자 레지스터를 복구시켜주는 것과 예외 처리를 제외하고 간단히 하면

function! s:opfunc(type,...) 
  let char = s:inputreplacement()
  let reg = '"'
  let type = a:type

  " gv""y
  silent exe 'norm! gv"'.reg.'y' 

  let keeper = getreg(reg)

  call s:wrapreg(reg,char,"",a:0 && a:1)
  " gv""p`[
  silent exe 'norm! gv'.(reg == '"' ? '' : '"' . reg).'p`['
endfunction

시나리오상 어떤 abc라는 문자열을 ]로 감싼다고 하자.

abc에서 viw를 통해 문자열을 선택하고

S]를 누르면

s:inputreplacement()]를 반환합니다.

let reg = '"'에서 임시로 쓸 레지스터를 지정해줍니다. 그리고 아래 줄에서 gv""y를 함으로써 abc" 레지스터에 들어갑니다.

추후 서술할 s:wrapreg를 통해 " 레지스터 내용이 char(']')로 감싸진 문자열이 reg에 담기게 됩니다.

그 내용을 gv""p를 통해 surround를 구현하고 있습니다. ( [ `는 마지막으로 붙여넣기 한 내용의 처음 부분으로 가기.)

s:wrapreg에 대해서

function! s:wrapreg(reg,char,removed,special)
  let orig = getreg(a:reg)
  let type = substitute(getregtype(a:reg),'\d\+$','','')
  let new = s:wrap(orig,a:char,type,a:removed,a:special)
  call setreg(a:reg,new,type)
endfunction

s:wrapreg의 실체는 s:wrap에 있습니다. s:wrap

function! s:wrap(string,char,type,removed,special)

stringchar로 감싸고 그 결과를 반환해주는 함수입니다.

이 안을 들여다보면 char에 따라 wrapping 작업을 유연하게 처리한 모습을 볼 수 있습니다.

이 부분은 상당히 길어 설명하기가 힘드므로 직접 열어서 구경하기 바랍니다.

요약

지금까지 surround를 어떻게 Vim script로 구현할 수 있는지 실제로 해봤고, 유명 플러그인 vim-surround에서 어떻게 하는지 본 결과 우리의 방법과 크게 다르지 않음을 알 수 있었습니다.

Updated: