tháng 6 22, 2021

Elixir với Emacs

Elixir với Emacs

“I’m using Linux. A library that emacs uses to communicate with Intel hardware.”

– Erwin, #emacs, Freenode.

Để cài đặt các package, mình sử dụng straight.eluse-package. Nếu muốn bạn có thể tham khảo bài viết này để tìm hiểu cách chuyển đổi từ package.el sang straight.el.

Baseline: elixir-mode

Hỗ trợ font-locking, code indent và code navigation. Để cài đặt package, chỉ cần thêm đoạn code sau vào file config của emacs (ví dụ init.el)

(use-package elixir-mode
  :mode ("\\.ex\\'" "\\.exs\\'" "mix\\.lock\\'")
  :bind (:map elixir-mode-map
              ("C-c C-f" . elixir-format))
  )

Ở đây mình sử dụng C-c C-f để format code elixir. Bạn có thể đổi sang tổ hợp key khác hoặc thiết lập tự động format code khi save file.

;; Create a buffer-local hook to run elixir-format on save, only when we enable elixir-mode.
(add-hook 'elixir-mode-hook
          (lambda () (add-hook 'before-save-hook 'elixir-format nil t)))

Các tùy chỉnh khác, bạn có thể tham khảo thêm tại đây.

Elixir Language Server backend: elixir-ls

Tương tự như trong bài viết Elixir với VSCode, mình cũng sử dụng elixir-ls. Các bước cài đặt như sau:

# Di chuyển tới thư mục sẽ cài đặt elixir-ls. Ví dụ ~/.emacs.d
$ cd ~/.emacs.d
$ git clone https://github.com/elixir-lsp/elixir-ls.git
$ cd elixir-ls
$ mix deps.get
$ mix elixir_ls.release

Khi đó thư mục release được tạo ra. Bên trong có file chạy language_server.sh (macos, linux) và language_server.bat (windows) để khởi động ElixirLS.

Elixir Language Server frontend: lsp-mode

Để kết nối emacs với ElixirLS, ta cần cài package lsp-mode như bên dưới.

  (use-package lsp-mode
    :commands lsp
    :diminish lsp-mode
    :hook (elixir-mode . lsp)
    :init
    (add-to-list 'exec-path "path-to-elixir-ls/release"))

Ta cần thay path-to-elixir-ls/release bằng đường dẫn thực tế (theo ví dụ trên thư mục release ở đường dẫn ~/.emacs.d/elixir-ls/release). Khi đó khi mở file trong elixir project, file chạy language_server được gọi để khởi tạo lsp session.

Chú ý: Nếu gặp vấn đề với câu lệnh add-to-list khiến emacs không nhận ra đường dẫn của elixir-ls, bạn có thể thử thêm luôn đường dẫn vào biến env $PATH trong :init

(setenv "PATH" (concat (expand-file-name "elixir-ls/release:" user-emacs-directory) (getenv "PATH")))
Code completion: company
(use-package company
  :diminish company-mode
  :config
  (global-company-mode))

Có thể cài thêm package company-box để tùy biến giao diện danh sách các lựa chọn của company.

Realtime code checking/linting: flycheck
(use-package flycheck
  :hook (prog-mode . flycheck-mode))
Hỗ trợ giao diện cho lsp-mode: lsp-ui
(use-package lsp-ui)

Template file: web-mode

Để làm việc với Phoenix template file (*.eex và *.leex) cần cài thêm package web-mode.

(use-package web-mode
  :mode (("\\.eex\\'" . web-mode)
         ("\\.leex\\'" . web-mode)))

Snippets

Cài đặt và sử dụng package yasnippets ở minor-mode như sau.

(use-package yasnippet
  :commands (yas-reload-all)
  :hook ((prog-mode . yas-minor-mode)
         (snippet-mode . yas-minor-mode))
  :config
  (yas-reload-all))

Chú ý là với đoạn code trên, ta chỉ bật yasnippet với prog-modesnippet-mode. Nếu muốn bật yasnippet với tất cả các chế độ, ta có thể sử dụng (yas-global-mode 1).

Để định nghĩa snippets, ta có thể cài đặt thêm package yasnippet-snippets để tải về các snippets được chia sẻ sẵn (có hỗ trợ elixir) hoặc tự định nghĩa theo hướng dẫn tại đây.

(use-package yasnippet-snippets
  :after yasnippet)
Tích hợp yasnippet + company

company có hỗ trợ yasnippet với backend company-yasnippet. Ta cần thêm backend này vào danh sách company-backends. Nếu thêm trực tiếp backend này vào danh sách, company sẽ bỏ qua kết quả của các backend nằm sau nó trong danh sách nên cách tốt nhất sử dụng từ khóa :width để nhóm backend này với các backend khác.

Dưới đây là đoạn code sẽ nhóm backend này với tất cả các backend trong company-backends:

;; Add yasnippet support for all company backends
;; https://github.com/syl20bnr/spacemacs/pull/179
(defvar company-mode/enable-yas t "Enable yasnippet for all backends.")

(defun company-mode/backend-with-yas (backend)
  (if (or (not company-mode/enable-yas) (and (listp backend) (member 'company-yasnippet backend)))
      backend
    (append (if (consp backend) backend (list backend))
            '(:with company-yasnippet))))
(use-package company
  :after yasnippet
  :diminish company-mode
  :init
  (setq company-minimum-prefix-length 2)
  (setq company-idle-delay 0.5)
  (setq company-transformers '(company-sort-prefer-same-case-prefix))
  :config
  (global-company-mode)
  (setq company-backends (mapcar #'company-mode/backend-with-yas company-backends)))

Một số tùy biến

  • company-minimum-prefix-length: độ dài tối thiểu của tên snippet. Ví dụ: nếu muốn sử dụng snippet df để định nghĩa cho one-line function, thì phải để giá trị là 2.
  • company-transformers: tùy biến danh sách lựa chọn của company. Khi để giá trị là company-sort-prefer-same-case-prefix thì các kết quả yasnippet được đưa lên trên nếu trùng với giá trị đã gõ. Ví dụ khi gõ df, snippet df sẽ hiển thị trước def của backend khác.
  • company-idle-delay: thời gian chờ trước khi hiển thị danh sách lựa chọn. Thông thường để 0.5 s.
Khắc phục lỗi không hiển thị kết quả yasnippet trong lsp-mode

Lý do: lsp-mode mặc định sử dụng company-capf backend để hiển thị danh sách lựa chọn. Khi gọi (use-package lsp-mode), company-capf backend được thêm vào đầu danh sách company-backends nên các kết quả của company-capf sẽ bỏ qua company-yasnippet.

Do đó, cần cài đặt lsp-completion-provider thành :nonelsp-mode không sử dụng company-capf nữa.

(use-package lsp-mode
    :commands lsp
    :diminish lsp-mode
    :hook (elixir-mode . lsp)
    :init
    (setq lsp-completion-provider :none) ;; SET HERE
    (add-to-list 'exec-path "path-to-elixir-ls/release"))

P/S: Cài đặt emacs hiện tại, mình đã đẩy lên github để tham khảo nếu cần.