3 minute read

vim-surround 분석

How is vim-surround implemented?

Suddenly, I was wondering how vim-surround plug-in works.

I thought how to surround before seeing the vim-surround code.

Normal Mode1

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

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

위 방법은 현재 커서가 있는 단어에 <> 로 감싸주는 코드다.

간단하게 구현되었지만, 한가지 문제가 있다. 사용자 register “ 를 쓰고 있다.

어떤 사용자는 이 작업을 하기전에 중요한 데이터가 “ register 에 있을 수도 있으므로 플러그인이 건들면 안된다.

그래서 다음과 같이

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

알아둬야 할 내용

마지막 selection 을 복구하는 명령어는 gv

selection 에서 앞뒤로 왔다갔다 하는 명령어는 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
  " exchange marks
  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>

현재 위치를 반환하는 line 과 col built-in 함수를 통해서 어떻게 구현하기는 했는데, 영 모양이 좋지 않다.

게다가 사용자 mark set 을 건들고 있다.

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 가 “ register 에 들어간다.

추후 서술할 s:wrapreg 를 통해 “ register 내용을 char(‘]’) 으로 감싸진 문자열이 reg 에 담기게 된다.

그 내용을 gv”“p[ 을 통해 surround 를 구현하고 있다. ( [ 는 마지막으로 paste 한 내용의 처음부분으로 가기. )

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)

string 을 char 로 감싸고 그 결과를 리턴해주는 함수다.

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

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

요약

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

Updated: