이 글은 2022년 4월 2일 트위터에 쓴 글을 백업한 것입니다.

vim-surround

다시 밤이 되었군. 이번엔 surround 이야기를 해볼까.

팀 포프의 vim-surround는 vim의 텍스트 오브젝트에 "감싸기" 동사를 추가한다는 점에서 상당한 의의를 갖는 프로젝트라 할 수 있는데, 이게 특정 텍스트 오브젝트를 감싸는 다른 글자 쌍을 다루는 경우가 생각보다 많다는 점에서 기인한다.

https://github.com/tpope/vim-surround

예를 들어 코딩을 하다 보면 doSomething(b, c, d) 이런 스타일의 괄호가 들어가는 코드를 작성하는 일이 엄청나게 많음. 문제는 괄호가 중괄호도 있고 소괄호도 있고 대괄호도 있고 <> 이런 부등호 괄호도 있고… 따옴표 쌍따옴표도 있다는 것.

vim-surround는 여기에 ys, ds, cs 라는 동사를 추가한다.

  • ys: 씌운다.
  • ds: 씌운 것을 지운다.
  • cs: 씌운 것을 바꾼다.

뒤의 s는 surround라는 뜻. 그러므로 세 글자만 외우면 된다. y, d, c.

이걸 쓰면 이런 게 가능. 특정 텍스트 오브젝트를 특정 문자로 감싸거나 삭제하거나 교체하는 것이 가능하다. 특히 html tag 도 수정하고 지우고 씌우는 것이 가능해서 html 편집할 때에도 좋다. surround는 가끔 생각날 때 쓰는 기능이 아니라 하루에도 여러 차례 사용하는 필수 기능이라 할 수 있다.

괄호, 따옴표, html tag가 겹겹이 있어도 수정이 쉽고 간편해서 굉장히 즐겁다. 이런 변경이 cs 두 글자로 되는 마법. 게다가 뒤에 오는 건 텍스트 오브젝트라서 vim에 익숙 하다면 굳이 암기할 필요가 없는 것들.

수없이 많은 vim 플러그인들 중에서 압도적으로 유명하고, vim 언어를 아주 자연스럽게 확장한다는 점에서 선택의 여지가 없는 플러그인이라 생각한다. 그냥 vim에 빌트인으로 들어갔으면 좋겠다는 생각도 한다. 아무튼 이렇다보니 꽤 유명한 플러그인들이 surround를 확장하는 경우도 많다.

text object

이야기를 하다 보니 텍스트 오브젝트에 대한 보충 설명이 필요할지 고민되는데, 걍 풀어보자. vim에는 텍스트 오브젝트라는 것이 있다. 화면에 보이는 글자 뭉치를 특정 규칙으로 그룹화하는 것을 말하는데, 텍스트 오브젝트는 2글자로 이뤄진다는 특이한 특징이 있다.

prefix는 ia.

  • i: inner
  • a: a

여기에 글뭉치를 의미하는 한 글자를 붙인다. 예를 들어 w는 word 인데… 위의 두 개랑 조합하면…

iw, aw가 된다. 단어 안쪽, 단어 하나라고 이해하면 된다.

이제 이걸 vim 명령이랑 조합을 할 수 있다.

daw

  • d: 지워라
  • aw: 단어 하나를

많이 쓰는 텍스트 오브젝트 중에는 p도 있다. paragraph.

  • dap: 지워라. 문단 하나를.
  • 3dap: 3번 지워라. 문단 하나를.

이렇게 숫자 조합으로 여러 텍스트 오브젝트를 지울 수 있다.

지우는 것 뿐만 아니라 복사도 할 수 있고 비주얼로 선택도 할 수 있고 여러가지 가능한데, 이 중에서도 좀 특이한 텍스트 오브젝트가 있다 그것이 바로 괄호 텍스트 오브젝트. 괄호는 쌍이 나뉘니까 여는 괄호 닫는 괄호가 있는데, 두 괄호가 surround에서 기능이 조금 다르다.

똑같이 ysiw(단어 하나에 괄호를 씌워라) 라고 했는데 ysiw) 하면 (1235)가 되고, ysiw(하면 ( 1235 ) 가 된다. 여는 괄호는 안쪽에 공백을 하나씩 주는 것.

이야기가 잠깐 surround로 샜는데, 다시 빌트인 vim 기능으로 돌아와서, ia가 단어나 문단에서는 같은 의미로 쓰인다. 하지만 괄호나 따옴표에서 ia는 다른 의미를 갖는다. 괄호나 따옴표는 단어나 문단과는 달리 안쪽과 바깥쪽 개념이 있기 때문.

vi{ 중괄호 안쪽 선택. va{ 중괄호까지 선택. yi{ 중괄호 안쪽 복사. di{ 중괄호 안쪽 삭제. di" 쌍따옴표 안쪽 삭제. da" 쌍따옴표 포함 삭제. ca" 쌍따옴표 포함 삭제하고 인서트 모드… 조합은 무궁무진하다. 그래서 vim 명령은 외워서 쓰는 단축키가 아니라 이해하고 쓰는 일회성 함수가 된다.

custom text object 정의하기

그러면 이쯤에서 궁금한 게 하나 생길 수 있다. 앗 그러면 텍스트 오브젝트는 주어진 것만 써야 하나요? 오 그렇지 않다. 새로운 텍스트 오브젝트를 내가 만들어 쓸 수도 있음. 실제로 몇몇 플러그인들은 자체적인 텍스트 오브젝트를 제공해서 통합적은 사용 경험을 제공하기도 한다.

가령 lua 프로그래밍 언어는 함수의 시작이 function 이고, 함수의 끝이 end 키워드인데 lua를 지원하는 vim 플러그인들은 함수 텍스트 오브젝트를 제공할 것이다. 아마 if, af 정도 되지 않을까. 그러면 daf로 커서를 감싸고 있는 함수 하나를 지울 수 있는 것. 보통 랭귀지 플러그인들이 이렇다.

Clojure 프로그래밍 언어는 s-expression을 쓰고 있어 form의 의미가 (와 비슷하지만 좀 더 집합적인 의미를 갖고 있다. 따라서 Clojure 플러그인도 if, af 라는 텍스트 오브젝트를 제공한다. 따라서 언어 구문에 맞는 뭉치로 삭제하거나 뭘 씌우거나 복사하거나 교체하는 데 사용한다.

아무튼 이렇게 커스텀 텍스트 오브젝트를 만들 수 있는데 직접 만드는 건 좀 귀찮은 일이고, 텍스트 오브젝트를 만드는 걸 도와주는 플러그인도 있다. 그 중에서 가장 유명한 건 vim-textobj-user.

https://github.com/kana/vim-textobj-user

내 경우엔 이걸 써서 iE, aE를 정의해 두었다. 이건 파일 전체를 의미하는데 yiE로 파일 전체(첫 줄부터 마지막 줄까지)를 복사할 수 있다. 상상하기 나름으로 다양한 텍스트 오브젝트를 만들 수 있는데 요즘은 안 쓰지만 전에 썼던 것 중 하나는 ii, ai가 있었다. 이건 인덴트가 같은 텍스트들.

코딩할 때 대부분의 경우 인덴트가 같은 스코프를 의미하기에 같이 묶어서 복사하거나 지우거나 할 일이 있는데 그럴 때 쓰기 좋았던 기억. 물론 보통 그런 것들은 한 번 더 중괄호로 감싸지긴 하지만 중괄호가 없거나 그런 의미로 사용하지 않거나 중괄호를 무시한 인덴트가 있는 경우도 있으니까.

한편 iE, aE를 Entire file 의 텍스트 오브젝트로 삼은 건 이유가 있다. 지난 몇년동안 ie, ae로 써왔는데, 이게 최근 사용하기 시작한 Clojure 프로그래밍 언어 플러그인에서 ie, ae를 element로 사용하고 있길래 겹치는 문제가 발생한 것. 그래서 파일 전체를 iE, aE로 바꾼 것이다.

바꿔놓고 보니까 잘 바꿨다는 생각이 들음. 왜냐하면 편집중에 파일 전체를 복사하거나 선택하거나 지우거나 할 일은 생각보다 흔하지 않고 (아마 많은 사람들은 command+a 나 control+a 로 선택할 것이다) e는 누르기 쉬워서 아쉽다. 그래서 잘 누를 일 없는 E로 바꾼 것.

이쯤되면 이미 dw, yw, cw, vw같은 문법을 알고 있는 사람이라면 앗 dw가 있는데 왜 diw를 써야 하죠 같은 생각을 할 수 있다. 하지만 여기에는 텍스트 오브젝트가 들어가기 때문에 dw 는 커서의 위치를 기준으로 다음 단어까지를 삭제하고, diw는 해당되는 단어 자체를 삭제한다는 차이가 있어서 대상을 타게팅하고 작업한다는 의미가 있음.

쉽게 말하자면 dw는 논 타게팅 광역기고(커서가 지나가면서 지움), diw는 타게팅 기술(대상을 지움). 그래서 텍스트 오브젝트를 알면 vim 편집 기술의 상당 부분을 이해할 수 있고, 이게 다른 기법으로도 줄줄이 연결된다.