Elixir in Emacs - Alchemist and Language Server Protocol hybrid approach

posted on 2019-10-20

Since the Alchemist’s Lanaguage Server Protocol support work is still largely a work-in-progress and Emac’s lsp-mode doesn’t bring all of the Alchemist’s goodies, I have decided to use both of them - below is a step-by-step guide on bringing both to Emacs and do not cause conflicts. I’m using Doom Emacs configuration framework which does have elixir module with Alchemist pre-packaged but I have decided not to use it and instead add required modules and configuration by hand to have more control over what parts of Alchemist and what of LSP-mode I want to use where. It is not a perfect solution as they spawn their own separate background processes to do the hard work of interacting with Elixir but it works (mostly).

I have prepared this article as a short guide you can follow through on how to “glue together” following Emacs packages to get a more comprehensive Elixir support:

Let’s start from installing Elixir Language Server:

Installing Elixir Language Server

I have assumed the installation directory of Elixir Language Server, needed for lsp-mode to work to be ~/Src/opensource/elixir-ls. You can install it by executing following commands in your shell:

cd ~/Src/opensource/
git clone https://github.com/JakeBecker/elixir-ls
cd elixir_ls
mix deps.get
mix compile
mix elixir_ls.release -o release

Adding required Alchemist and lsp-mode packages to doom’s config

Next step is adding required packaged into ~/.doom.d/packages.el file.

(package! dap-mode)
(package! exunit)
(package! flycheck-credo)
(package! elixir-mode)
(package! alchemist)

Configuring Alchemist package

Now we can configure alchemist package by adding following lines to ~/.doom.d/config.el (this is almost verbatim from doom’s default elixir module).

(def-package! alchemist
  :hook (elixir-mode . alchemist-mode)
  :config
  (set-lookup-handlers! 'elixir-mode
    :definition #'alchemist-goto-definition-at-point
    :documentation #'alchemist-help-search-at-point)
  (set-eval-handler! 'elixir-mode #'alchemist-eval-region)
  (set-repl-handler! 'elixir-mode #'alchemist-iex-project-run)
  (setq alchemist-mix-env "dev")
  (setq alchemist-hooks-compile-on-save t)
  (map! :map elixir-mode-map :nv "m" alchemist-mode-keymap))

Configuring lsp-mode package

To configure the lsp-mode package we’ll add to config.el this short definition:

(def-package! lsp-mode
  :commands lsp
  :hook
  (elixir-mode . lsp))

Define lsp server for lps-mode

Now let’s define language server we have previously installed for lsp-mode to use:

(after! lsp-clients
  (lsp-register-client
   (make-lsp-client :new-connection
    (lsp-stdio-connection
        (expand-file-name
          "~/Src/opensouce/elixir-ls/release/language_server.sh"))
        :major-modes '(elixir-mode)
        :priority -1
        :server-id 'elixir-ls
        :initialized-fn (lambda (workspace)
            (with-lsp-workspace workspace
             (let ((config `(:elixirLS
                             (:mixEnv "dev"
                                     :dialyzerEnabled
                                     :json-false))))
             (lsp--set-configuration config)))))))

Configure lsp-ui - user interface of lsp-mode

Now it is time for configuring some eye-candy, lsp-ui provides the visual interface. I like the documentation overlays to include header and signature and had disabled webkit’s rendering for the sake of performance. Feel free to fiddle with the settings.

(after! lsp-ui
  (setq lsp-ui-doc-max-height 13
        lsp-ui-doc-max-width 80
        lsp-ui-sideline-ignore-duplicate t
        lsp-ui-doc-header t
        lsp-ui-doc-include-signature t
        lsp-ui-doc-position 'bottom
        lsp-ui-doc-use-webkit nil
        lsp-ui-flycheck-enable t
        lsp-ui-imenu-kind-position 'left
        lsp-ui-sideline-code-actions-prefix "💡"
        ;; fix for completing candidates not showing after “Enum.”:
        company-lsp-match-candidate-predicate #'company-lsp-match-candidate-prefix
        ))

Configure DAP - debugging adapter protocol

To get step through debugging experience inside Emacs we need to configure debugging adapter protocol. I haven’t enabled dap-ui nor dap-mode as those are switched globally.

(def-package! dap-mode)

(after! lsp-mode
  (require 'dap-elixir)
  ;;(dap-ui-mode)
  ;;(dap-mode)

    (defun dap-elixir--populate-start-file-args (conf)
    "Populate CONF with the required arguments."
    (-> conf
        (dap--put-if-absent :dap-server-path `("~/Src/opensource/elixir-ls/release/debugger.sh"))
        (dap--put-if-absent :type "mix_task")
        (dap--put-if-absent :name "mix test")
        (dap--put-if-absent :request "launch")
        (dap--put-if-absent :task "test")
        (dap--put-if-absent :taskArgs (list "--trace"))
        (dap--put-if-absent :projectDir (lsp-find-session-folder (lsp-session) (buffer-file-name)))
        (dap--put-if-absent :cwd (lsp-find-session-folder (lsp-session) (buffer-file-name)))
        (dap--put-if-absent :requireFiles (list
                                            "lib/**"
                                            "test/**/test_helper.exs"
                                            "test/**/*_test.exs"))))

    (dap-register-debug-provider "Elixir" 'dap-elixir--populate-start-file-args)
    (dap-register-debug-template "Elixir Run Configuration"
                                (list :type "Elixir"
                                    :cwd nil
                                    :request "launch"
                                    :program nil
    :name "Elixir::Run"))
  )

Configure ExUnit package

Not much to add here, we’ll bind some keys for exunit commands to easily run unit and doc testing on our elixir projects.

(def-package! exunit)

Configure Credo to receive static code analysis feedback when writing code

Next part is to add Credo - static code analysis tool for Elixir. We hook it up into fly check.

(def-package! flycheck-credo
  :after flycheck
  :config
    (flycheck-credo-setup)
    (after! lsp-ui
      (flycheck-add-next-checker 'lsp-ui 'elixir-credo)))

Enable formatting on save and send reload command to Elixir’s REPL

We hooks to on file save to automatically format the buffer using lsp-mode feature and send reload command to IEX REPL.

(after! lsp
  (add-hook 'elixir-mode-hook
            (lambda ()
              (add-hook 'before-save-hook 'lsp-format-buffer nil t)
              (add-hook 'after-save-hook 'alchemist-iex-reload-module))))

Disable popup quitting for Elixir’s REPL

Default behaviour of doom’s treating of Alchemist’s REPL window is to quit the REPL when ESC or q is pressed (in normal mode). It’s quite annoying so below code disables this and set’s the size of REPL’s window to 30% of editor frame’s height.

(set-popup-rule! "^\\*Alchemist-IEx" :quit nil :size 0.3)

Setup additional key bindings

Last, we set up some of the key bindings to doom’s default code prefix to run tests via exunit and access lsp’s driven file outline.

(map! :mode elixir-mode
    :leader
    :desc "iMenu" :nve  "c/"    #'lsp-ui-imenu
    :desc "Run all tests"   :nve  "ctt"   #'exunit-verify-all
    :desc "Run all in umbrella"   :nve  "ctT"   #'exunit-verify-all-in-umbrella
    :desc "Re-run tests"   :nve  "ctx"   #'exunit-rerun
    :desc "Run single test"   :nve  "cts"   #'exunit-verify-single)

Finishing thoughts

The above configuration options are taken verbatim from my literate doom emacs configuration - feel free to take any part that you see valuable and tweak it to suit your needs. Above all - happy hacking.