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:
- Alchemist - The most complete Elixir tooling for Emacs
- Lsp-mode and Elixir Language Server (with debugging support)
- Exunit - integrated elixir test runner
- Credo - static code analyser for Elixir
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.