I’ve been a long-time emacs user and a short-time NixOS user. These posts will describe the trials and tribulations that I’ve gone through to get the two working well together.
These days everyone is busy. Whether we track our to-dos on paper, in the computer, in the cloud or in our heads, we all have a list of priorities – the have-tos and the want-tos – that demand our attention. The way each of us prioritizes things differs, of course, and for me it usually means doing just enough of a thing to get by. In this context, it means that my emacs configuration has always been a bit lacking because I’ve always been able to get it to a point where it was serviceable, to where I could write some code, if a bit inefficiently, and that’s been enough. Though I’ve certainly had plenty of emacs envy whenever I’ve seen someone showcase their awesome emacs setup doing things I didn’t realize it could do, it has never really prompted me to put the time and effort in to get a really great emacs set-up.
But recently I’ve been interviewing for my next role and found myself in the situation of having to live code in front of strangers, <sarcasm>which, of course, is loads of fun</sarcasm>. It turns out, this was enough of a motivation for me to do something about my pitiful emacs coding config.
Current State
Here’s the state of my haskell-mode
configuration before making the changes discussed later:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
(use-package haskell-mode :hook ((haskell-mode . (lambda () (turn-on-haskell-indentation) (interactive-haskell-mode) (custom-set-variables '(haskell-tags-on-save t)) (custom-set-variables '(ormolu-format-on-save t)))) (haskell-cabal-mode . (lambda () (setq indent-tabs-mode nil)))) :bind (([f8] . haskell-navigate-imports) ("C-c C-l" . 'haskell-process-load-or-reload) ("C-c C-z" . 'haskell-interactive-switch) ("C-c C-n C-t" . 'haskell-process-do-type) ("C-c C-n C-i" . 'haskell-process-do-info) ("C-c C-n C-c" . 'haskell-process-cabal-build) ("C-c C-n c" . 'haskell-process-cabal) ;;("M-." . 'haskell-mode-jump-to-def-or-tag) )) |
I put this configuration together based on a perusal of the haskell-mode manual, especially the Interactive Haskell section. It’s been so long since I did any of this that I probably couldn’t explain the presence or absence of particular configuration values. I have no idea why I commented out the key binding for haskell-mode-jump-to-def-or-tag
. I could never get jump-to-definition working well and have just been resorting to using projectile’s projectile-grep
to search for the thing I wanted to jump to. It works but I feel like an amateur working that way. So let’s see if I can get that working.
Jump to Definition using TAGS
Probably the easiest way to get jump-to-defintion working is by using a TAGS file. This requires having a process generate this TAGS file, preferably on every save so that it is always up-to-date. There are several programs that one could use to do this for Haskell like hasktags, ghc-tags or fast-tags. I’ll be using hasktags since it seems to be the most popular.
NixOS Configuration
I’m running NixOS and I like to configure development environments using a project-specific configuration file. As I work on getting haskell-mode
working better I’m working on one of my personal projects called rainbow-hash-ld. The default.nix file specifies how to build the project using Nix. This line is where I pull in hasktags
. Using this in conjunction with direnv means that I don’t need to have hasktags
globally installed but am able to have it available on a project-by-project basis. Neat, right?
But such a setup complicates things for Emacs because it doesn’t see the correct environment and so hasktags
will not be available as it’s not on Emacs’ exec-path
unless the path to hasktags
is explicitly added. That is, until I use direnv-mode. Enabling direnv-mode in Emacs is a part of the solution; one also needs to activate direnv
by using a .envrc
file in the project root directory. On NixOS, the content of this file should be:
1 |
use nix |
Once direnv
is set up, opening a file in a direnv
-controlled directory causes Emacs to see the correct environment and can then run hasktags
. One unfortunate side effect is a rather long delay in Emacs while this happens (is there any remedy for this?).
Emacs Configuration
Now, I just need to configure Emacs to generate the TAGS file on every source file save. After some changes and cleanup my haskell-mode
configuration now looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 |
(use-package haskell-mode :hook (haskell-mode . direnv-mode) :bind (([f8] . haskell-navigate-imports) ("C-c C-l" . 'haskell-process-load-or-reload) ("C-c C-z" . 'haskell-interactive-switch) ("C-c C-n C-t" . 'haskell-process-do-type) ("C-c C-n C-i" . 'haskell-process-do-info) ("C-c C-n C-c" . 'haskell-process-cabal-build) ("C-c C-n c" . 'haskell-process-cabal) ("M-." . 'haskell-mode-jump-to-tag)) :custom (haskell-tags-on-save t)) |
Note that in addition to installing hasktags
I’ve also installed stylish-haskell
an am configuring that to run on every save as well.
Doing M-. jumps to the definition of the symbol under point or, if there are multiple options, opens a buffer that allows the user to select the destination they want.
Jump to Definition using Interactive Haskell Mode
haskell-mode-jump-to-tag
works but it’s not ideal. It’s pretty dumb. It doesn’t seem to understand the difference between a definition and usages. So let’s try an upgrade. Here’s my haskell-mode
config after activating interactive-haskell-mode
and binding haskell-mode-jump-to-def-or-tag
to M-.
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
(use-package haskell-mode :hook ((haskell-mode . direnv-mode) (haskell-mode . interactive-haskell-mode)) :bind (([f8] . haskell-navigate-imports) ("C-c C-l" . 'haskell-process-load-or-reload) ("C-c C-c" . 'haskell-compile) ("C-c C-z" . 'haskell-interactive-switch) ("C-c C-n C-t" . 'haskell-process-do-type) ("C-c C-n C-i" . 'haskell-process-do-info) ("C-c C-n C-c" . 'haskell-process-cabal-build) ("C-c C-n c" . 'haskell-process-cabal) ("M-." . 'haskell-mode-jump-to-def-or-tag)) :custom (haskell-tags-on-save t)) |
Now, when I load up one of the modules from the library of my project using C-c C-l
, and then do M-.
with the cursor over a symbol, a jump directly to the definition is performed.
But all is not well. If I attempt to load a file from a non-default target (i.e., the first one), I get a compiler error about not being able to load a module – a module whose package is in the build-depends
section of the executable, but not in the build-depends
of default target. There is a remedy, however: do M-x haskell-session-change-target
and select the correct target (hitting TAB shows you possible completions). You’ll be asked to restart the session and once you do, doing M-.
will then work . . . sort of. If the definition that is to be jumped to is in a module that has not yet been loaded, the jump fails. So you first must load that module. Once you do, it works.
In general, I think this feature is nearly unusable whether you use TAGS or interactive-haskell-mode
. There are a lot of old, open issues on the Github issue tracker. It seems the project, though active, is not often maintained. I suspect that since the advent of LSP, the motivation to fix issues that are provided by LSP has dwindled.
That’s enough for Part 1. In the next part, I’ll take a look at dante which is another integration with GHCi and may perform better than interactive-haskell-mode
.
P.S.: Soon after I published this post, I received issue 477 of Haskell Weekly in my email and, coincidentally, it had an entry for a blog post by Well-Typed titled “Making GHCi compatible with multiple home units“. I haven’t tried it yet, but I’m hopeful that this update will fix the issue I mentioned above when loading a module in an interactive Haskell session. Hopefully, one will not have to run the haskell-session-change-target
command.
Comments are closed, but trackbacks and pingbacks are open.