vertico에서 한글 증분 완성(incremental completion) 지원하기

3 minute read

helm에서 vertico로 바꾼 이유는

doom emacs로 전환하면서 기존에 사용 중인 helm에서 vertico로 대화형(interactive) 완성(completion) 패키지를 갈아탔다. 완성 패키지는 파일을 찾을 때, M-x 로 함수를 실행할 때 등에서 폭넓게 사용한다. 이름을 다 기억하지 못하니 일부분만 입력해도 해당 문자가 들어간 후보군을 출력해줘서 쉽게 찾을 수 있게 해준다.

The search engine for life and love

doom emacs에서 기본으로 사용할 정도로 밀고 있고 doom emacs 메인테이너(maintainer)가 직접 관리하고 있어서 신뢰가 갔기 때문이다.

vertico로 완성(completion) 패키지를 옮긴 후 괴로움

빠르고 doom emacs와 통합도 잘 되어 있어 좋은데, 한글로 찾을 때, 문제가 된다. 영문은 증분 완성(incremental completion)이 잘 되는데, 한글은 되지 않는다.

바로 후보군을 추려내는 증분 완성이 동작하지 않으니 너무 불편하다. 다시 helm으로 돌아갈까도 생각해봤지만 vertico가 대세인 것 같아서 원인을 파악하고 고쳐보기로 했다.

vertico--update 함수 로깅

vertico--update 함수는 입력받는 미니 버퍼(minibuffer)에서 읽어 후보군을 출력하는 역할을 한다. 이 함수에서 로깅을 해서 제대로 읽고 있는지를 확인했다.

(defun vertico--update (&optional interruptible)
  "Update state, optionally INTERRUPTIBLE."
  (let* ((pt (max 0 (- (point) (minibuffer-prompt-end))))
         (content (minibuffer-contents-no-properties))
         (input (cons content pt)))
    (message "vertico point: %S" (point))
    (message "vertico point end: %S" (minibuffer-prompt-end))
    (message "vertico pt: %S" pt)
    (message "vertico input: %S" input)
    ;;...
    ))

여기서 input 값이 가장 중요하다. 증분 완성을 지원하려면 문자를 입력할 때, 갱신돼야 한다.

[!] point: 7
[!] point end: 7
[!] vertico pt: 0
[!] vertico input: ("" . 0)

[!] point: 8
[!] point end: 7
[!] vertico pt: 1
[!] vertico input: ("a" . 1)

[!] point: 9
[!] point end: 7
[!] vertico pt: 2
[!] vertico input: ("ab" . 2)

영문을 입력하니 잘 갱신이 된다. 한글을 입력하면 어떻게 될까?

[!] point: 7
[!] point end: 7
[!] vertico pt: 0
[!] vertico input: ("" . 0)

갱신이 되지 않는다. 그래서 한글을 입력 중일 때는 증분 완성이 안 됐다.

언제 vertico-update 함수가 호출되는가?

(defun vertico--exhibit ()
  "Exhibit completion UI."
  (let ((buffer-undo-list t)) ;; Overlays affect point position and undo list!
    (vertico--update 'interruptible)
    (vertico--prompt-selection)
    (vertico--display-count)
    (vertico--display-candidates (vertico--arrange-candidates))))

vertico--exhibit 함수에서 호출한다. 그럼 vertico--exhibit 함수는 언제 호출되는가?

(defun vertico--setup ()
  ;;..;.
  (add-hook 'post-command-hook #'vertico--exhibit nil 'local))

vertico-mode 가 활성화될 때, 호출하는 vertico--setup 함수에서 post-command-hook 으로 설치한다.

입력을 조합하는 한글은 post-command-hook 이 호출되지 않는다

M-x z 키에 바인딩 되어 있는 repeat 함수는 이전 커맨드를 반복한다. 영문을 입력하고 repeat 함수를 호출하면 입력이 반복되는데, 한글은 반복되지 않는다. 입력이 하나하나가 완성이 아니라 조합 단계를 거쳐서 그렇지 않을까 추정한다. 익숙하지 않은 emacs 소스 코드를 해매다가 이 정도로 결론짓고 덮었다.

helm은 어떻게 한글 증분 완성을 지원하는가?

helm이 잘 지원해서 vertico로 바꿨을 때, 바로 불편함을 느꼈다. 만약 helm이 지원하지 않았다면 한글 입력은 원래 이런 거라며 포기했을지도 모르겠다. 관련 helm 소스 코드를 살펴봤다.

(minibuffer-with-setup-hook
    (lambda ()
      ;; ...
      (setq timer
            (run-with-idle-timer
             (max (with-helm-buffer helm-input-idle-delay)
                  0.001)
             'repeat
             (lambda ()
               ;; Stop updating in persistent action
               ;; or when `helm-suspend-update-flag'
               ;; is non-`nil'.
               (unless (or helm-in-persistent-action
                           helm-suspend-update-flag)
                 (save-selected-window
                   (helm-check-minibuffer-input)
                   (helm-print-error-messages))))))
      ;; minibuffer has already been filled here.
      (helm--update-header-line))
  (read-from-minibuffer (propertize (or prompt "pattern: ")
                                    'face 'helm-minibuffer-prompt)
                        input helm-map
                        nil hist tap
                        helm-inherit-input-method))

timer를 돌린다. 일정 주기마다 미니 버퍼 내용을 읽어서 그걸로 후보 추출을 한다. vertico와 다르게 completing-read-function 함수를 advice하는 게 아니라 read-from-minibuffer 함수를 호출해 프롬프트를 띄우고 timer에서 호출하는 (helm-check-minibuffer-input) 함수로 입력을 체크하고 후보를 추출한다.

한글 입력 시 post-command-hook 을 발동되게 하거나 timer를 돌리거나 둘 중 하나를 선택하면 된다. 전자는 어려워서 timer를 돌리게 vertico를 수정해본다.

vertico에서 일정 주기마다 미니 버퍼에 입력한 내용을 사용하게 수정

vertico 패키지를 직접 수정하지 않았다. advice를 사용해 vertico 함수 동작을 변경한다. 이렇게 하면 기존 패키지를 직접 수정하는 게 아니라서 패키지 업데이트를 잘 따라갈 수 있고 내가 원하는 부분만 변경할 수 있다.

(after! vertico
  (defun my/vertico-setup-then-remove-post-command-hook (&rest args)
    "vertico--setup 함수에서 추가하는 post-command-hook을 제거한다.

입력 조합으로 표현하는 한글 입력 시 post-command-hook이 입력되지 않는다.
한글 증분 완성을 위해 timer로 호출하기 때문에 제거한다"
    (remove-hook 'post-command-hook #'vertico--exhibit 'local))

  (defun my/vertico-exhibit-with-timer (&rest args)
    "타이머를 넣어 타이머 이벤트 발생 시 vertico--exhibit을 호출해 미니버퍼 완성(completion) 후보 리스트를 갱신한다

post-command-hook이 발동하지 않는 한글 입력 시에도 한글 증분 완성을 하기 위해 timer를 사용한다"
    (let (timer)
      (unwind-protect
          (progn
            (setq timer (run-with-idle-timer
                         0.01
                         'repeat
                         (lambda ()
                           (with-selected-window (or (active-minibuffer-window)
                                                     (minibuffer-window))
                             (vertico--exhibit))
                           )))
            (apply args))
        (when timer (cancel-timer timer)))))

  (advice-add #'vertico--setup :after #'my/vertico-setup-then-remove-post-command-hook)
  (advice-add #'vertico--advice :around #'my/vertico-exhibit-with-timer))

vertico--setup 함수에서 각종 설정을 하는데, post-command-hook 에 설치하는 vertico--exhibit 함수를 제거했다. timer를 돌려 호출하기 때문이다.

vertico--advice 함수를 다시 advice해서 timer를 돌린다. 일정 주기마다 vertico--exhibit 함수를 호출해서 post-command-hook 에서 호출하는 것과 똑같은 결과가 나오게 한다. vertico--advice 함수를 advice 했기 때문에 vertico에서 completing-read-default 함수 advice를 제거할 때, 같이 제거된다.

일정 기간 사용해보고 잘 동작하면 PR을 보내볼 생각이다. 퍼포먼스 문제가 생기는 경우에도 효과를 볼 수 있다고 약을 팔아볼 거다.

내가 한 실수

갱신 시점을 좀 더 파고들었어야 했다. 미니 버퍼에서 제대로 값을 못 읽어오는 거라 진단해서 시간을 허비했다. 한글 입력은 post-command-hook 을 발동시키지 않는다는 것을 확인하고 helm은 어떻게 처리했는지 소스 코드를 살펴보면서 빠르게 진행됐다.

링크

C-x C-s C-x C-c