Duplicate lines

내 이 세상 도처에서 쉴 곳을 찾아보았으나, 마침내 찾아낸, 컴퓨터가 있는 구석방보다 나은 곳은 없더라.

Duplicate lines

예전에 Eclpse를 쓸 때 알게 된 편리한 기능이 있다. 하나는 Move lines up/down 기능으로 Alt+<up> 또는 Alt+<down> 키로 현재 선택 영역 또는 현재 행을 위/아래로 이동하는 기능이고, 다른 하나는 Duplicate lines 기능으로 Cmd+Alt+<down> 키를 누르면 현재 선택 영역 또는 현재 행을 아래로 복사하는 기능이다.

이 기능이 너무 편하고 익숙해 Emacs에서도 비슷한 기능을 구현한 패키지를 설치해 키 바인딩까지 똑같이 맞춰 사용했다. move-text 패키지는 Eclipse의 Move lines up/down과 동일한 기능을 제공하고, duplicate-thing 패키지는 Duplicate lines과 비슷한 기능을 제공한다.

불만

move-text는 큰 불만 없이 사용하고 있지만, duplicate-thing은 Eclipse와 동작이 달라 거슬렸다. duplicate-thing에서 마음에 들지 않는 점은 다음과 같다.

  • 선택 영역이 확장되지 않는다.
    선택 영역이 행 중간에서 시작해 다른 행 중간에서 끝나는 경우도 duplicate-thing은 정확힌 선택된 영역만 복제한다. 실제로 이렇게 복제되는 게 필요한 경우가 있는지 모르겠다. Eclipse나 IntelliJ IDEA에서는 선택히 행 시작, 행 끝으로 확장된다.
  • 복제 후 선택 영역이 유지되지 않는다.
    영역을 선택해 Cmd+Alt+<down>을 누르면 선택 영역이 복제된 된 후 선택이 사라진다. Eclipse에서는 동일한 영역을 여러 번 복제하고 싶은 경우 Cmd+Alt+<down>을 여러 번 누르면 되지만 duplicate-thing에서는 한번 복제 후 선택이 사라지기 때문에 그렇게 할 수 없다. 물론 C-u 5 Cmd+Alt+<down>과 같은 식으로 여러 번 복제하는 것이 불가능하지는 않지만, 몇 번을 복제할 지 미리 생각해야 하는 불편함이 있다.

그동안 그냥 참고 썼는데, 불현듯 내가 직접 짜면 더 잘 만들 수 있을 것 같다는 생각이 들었다. 못 할 것도 없지 않은가. ELisp을 따로 공부한 적은 없지만 Clojure를 조금 아니까 ELisp 코드도 더듬더듬 짤 수 있을 것이다. 예전에 웹 에디터를 개발했던 경험도 도움이 될 것이다.

개선

duplicate-thing의 기능을 내 입맛에 맞게 수정할 수 있을지 소스 코드를 살펴보았다. 다행히 코드가 길지도 어렵지도 않았다. 기능 자체가 복잡한 게 아니니 당연하다 할 수 있다. 코드를 보니 duplicate-thing이란 함수 하나가 있는데 선택 영역이 있는 경우는 선택 영역을 복사하고 선택 영역이 없는 경우에는 현재 행을 복사한 붙여넣는게 전부다.

조금 끄적거리다 보니 duplicate-thing의 코드를 거의 갈아엎어 원래 코드는 거의 남지 않았다. 그래서 아예 패키지를 새로 만들고 이름을 duplicate-lines라 지었다.

나는 선택 영역이 전체 행을 포함하도록 확장되게 수정하고 싶다. 따라서 다음과 같이 선택 영역의 시작 위치 p1과 끝 위치 p2를 인자로 받았다면 다음과 같이 선택 영역을 확장하는 코드를 작성했다. 선택 영역이 있는 경우에는 선택 영역을 확장하고, 선택 영역이 없는 경우에는 현재 행을 선택한다. 선택 영역이 있는 경우에는 mark-active 변수가 t가 된다.

(defun duplicate-lines-expand-selection (p1 p2)
  "Expand selection to contain while lines.
Expand P1 to beginning of line and P2 to end of line (or more precisely)
the beginning of next line."
  (let (start end)
    (cond (mark-active
           (goto-char p1)
           (beginning-of-line)
           (setq start (point))
           (goto-char p2)
           (unless (= 0 (current-column))
             (unless (duplicate-lines-line-start-after-forward-line-p)
               (newline)))
           (setq end (point)))
          (t
           (beginning-of-line)
           (setq start (point))
           (unless (duplicate-lines-line-start-after-forward-line-p) (newline))
           (setq end (point))))
    (setq deactivate-mark nil)
    (goto-char end)
    (set-mark start)))

선택 영역을 확장하는 것은 쉽다. p1을 행의 처음으로 보내고 p2를 다음 행의 처음으로 보내면 된다. 선택 영역이 있는데 p2가 행의 맨 앞(즉 current-column = 0)이 아니면 p2를 다음 행으로 보낸다. duplicate-lines-line-start-after-forward-line-p 함수가 그 역할을 한다.

(defun duplicate-lines-line-start-after-forward-line-p ()
  "Return 't if the position is beginning of line after foward-line."
  (forward-line)
  (= 0 (current-column)))

이 함수는 포인트를 다음 행으로 옮긴 다음 (forward-line) 포인트가 행의 맨 앞에 위치하면 t를 리턴한다. 버퍼가 빈 줄로 끝나지 않는 경우 p2가 맨 마지막 행 중간에 있다면 forward-line이 무시될 것이고 이 경우에는 new-line으로 새 행을 삽입해줘야 한다.

Emacs에서 호출할 함수 duplicate-lines는 다음과 같이 작성할 수 있다.

(defun duplicate-lines (p1 p2 n)
  "Duplicate line or region N times.
If it has active mark (P1, P2), it will expand the selection and duplicate it.
If it doesn't have active mark, it will select current line and duplicate it."
  (interactive "r\np")
  (let (start end len text)
    (duplicate-lines-expand-selection p1 p2)
    (setq start (region-beginning)
          end   (region-end)
          len   (- end start)
          text  (buffer-substring start end))
    (dotimes (i (or n 1)) (insert text))
    (set-mark (- (point) len))
    (setq deactivate-mark nil)
    (setq transient-mark-mode (cons 'only t))))

(interactive "r\np")는 함수가 영역 p1, p2와 함께 반복횟수 n을 인자로 받을 수 있게 한다. duplicate-thing에서는 선택 영역 텍스트를 복사&붙여넣기 하는 데 kill-ring-saveyank를 사용했지만 여기서는 buffer-substringinsert를 사용했다.

PR

이걸 Melpa에 올리면 좋을 것 같았다. 내가 만든 패키지를 Melpa에 올려 여러 사람이 사용할 수 있게 한다면 정말 멋진 일일 것이다. Contributing a new recipe to Melpa를 자세히 읽고 가이드에 따라 package-lintcheckdoc 경고도 모두 수정했다.

그런데 내가 작성한 패키지 기능은 duplicate-thing과 비슷해 PR이 받아들여질지도 불확실했고, duplicate-thing의 작성자에게도 예의가 아닌 것 같다는 생각이 들었다. 그래서 duplicate-thing을 포크해서 내 수정 사항을 반영한 다음 PR을 보냈는데, 닷새가 지나도록 반응이 없었다.

조급한 마음에 Melpa 관리자에게 메일을 보내 어떻게 하는 게 좋겠냐고 도움말을 청했더니 바로 답장이 왔다. 닷새는 원작자에게 별로 긴 시간이 아니니 좀더 기다려보라고. duplicate-thing과 기능이 비슷한 다른 다른 패키지도 알려주며, 많은 사람들이 비슷한 패키지를 작성했다는 사실도 덧붙였다. Melpa에서 거부된 PR 링크와 함께. 역시 내가 만든 패키지를 Melpa에 올리기는 어려울 것 같다.

마침내 duplicate-thing 작성자에게도 응답이 왔다. 자기는 주석 처리 후 복제하는 기능을 좋아하는데 내가 그 기능을 제거했기 때문에 PR을 받아들일 수 없다고 했다. 작성자가 좋아하는 기능을 마음대로 없애 버렸으니 PR이 거부된 것은 당연하다.

가만히 생각해보니 원래 선택했던 부분을 주석 처리한 다음 복제하는 기능도 유용할 것 같았다. duplicate-thing 작성자가 PR을 거부한 이유도 이 기능을 뺐기 때문이니 이게 되도록 코드를 수정하면 PR을 받아줄지도 모르겠다는 생각이 들었다. 게다가 내가 작성한 코드에서 버그도 발견했다. 내가 작성한 코드에서는 인자로 영역과 반복횟수를 받기 위해 인터랙티브 코드를 r\np로 설정했는데, 마크가 설정되어 있지 않은 경우에는 에러가 발생했다.

인터랙티브 코드를 다시 P(대문자)로 바꾸고 인자를 제거한 다음 선택 영역이 있는 경우에 영역의 시작과 끝을 함수 안에서 얻게 바꾸어 버그를 수정했다. 그리고 주석 처리 후 복제하는 기능을 구현했다. 코드를 정리해 다시 PR을 보냈다. 이번에는 이틀만에 답이 왔다. 선택 영역을 유지하는 기능이 아주 유용하다며 PR을 병합해 주었고, Melpa에도 반영되었다.

정리

Emacs 기능을 수정하는 것은 매우 즐거운 경험이었다. 간단한 기능이었지만 불편한 점을 직접 수정했다. Melpa에 내 패키지를 올릴 수 있을지도 모른다는 생각에 잠시 들떴고, 이미 비슷한 패키지가 많아 올리기 힘들다는 것을 알았을 때는 실망했다.

그러나 내가 수정한 코드가 duplcate-thing에 반영되었다. Melpa로 duplcate-thing을 설치하면 내가 수정한 코드를 볼 수 있다. duplicate-thing 사용자들은 내가 수정한 코드를 사용하게 될 것이다. 충분히 만족할만한 일이다.

참고