I have a Haskell codebase at work written by a great engineer who called it “some of the worst code [he’s] ever written.” I have basically no Haskell experience, so I would like proper “goto-definition” and “show type signature” and other IDE-like features to help with exploring it.
I love Rust and Elm, and on paper Haskell is a language I really want to love, but the tooling is driving me to the mountains of madness. haskell-language-server doesn’t even officially support non-local dependencies, which just seems insane coming from cargo and rust_analyzer.
Basically, I’m using cabal and ghc 9.12.2, and I just want to know how to get an LSP, any LSP, to index all the code my project uses. I even vibe coded a janky script to `cabal unpack` every package in plan.json, but I can’t figure out how to get it in a hie.yaml in such a way that haskell-language-server will actually index it.
Install GHCUp, which is a toolchain installer. If you’re familiar with Rust, this is like rustup
During its install, GHCUp will prompt you for stuff. If you don’t understand a question, just go with the default.
When done, enter the terminal ui (ghcup tui) and install the appropriate compiler (GHC), build system (cabal), and LSP (HLS)
Now the toolchain is ready. What you need is to set up your editor. You will have installed the LSP, called HLS, and can be run using haskell-language-server-wrapper (it’s a wrapper because you might have different HLS installations for different compilers.
To check that your toolchain is valid, because setting it up in your editor, navitage to the root directory of your project (where the hie.yaml file is), and run haskell-language-server-wrapper typecheck. The typecheck command is kind of like a “healthcheck”.
Thanks for the reply. I’m up to speed with all of that, but I think what’s unclear is how to structure `hie.yaml` in a way that tells HLS, “Hey, please retain and index the source code of all my dependencies, direct and indirect.” I can get the LSP working, but I have the following two problems:
The LSP seems to be inaccurate about the symbol under cursor. I’ve observed this in Doom Emacs with OOTB haskell-mode settings, in Neovim with a cookie-cutter haskell-tools config, and in VSCode with the Haskell plugin. What I mean is that something like “Info about symbol under cursor” often gets me either something like “No information available” *or* a popup for the symbol 10-ish characters to the left (the exact offset isn’t consistent). This seems like a project structure thing but
In Emacs, I can explicitly ask to goto on identifiers, and all symbols from third-party dependencies turn up nothing. For example, if I use haskell-language-server-goto-implementation <atomically>, I get nothing. Same with “gri“ or “grt” in Neovim.. In Rust, I learn my way around by tunneling down into APIs through “gd” until I’ve read enough code to figure out what’s going on, and it’s frustrating to not have the here. It seems that some symbols which might be from deps I had to get from personal forks are better indexed, but it’s inconsistent and I would rather not have to manually specifically a “source-repository-package” entry for every dependency.
I hope any of that made sense. Basically, I’m looking for a setup where most of the IDE things I’m used to “just work.”
Obviously it’d be nice if HLS supported this, but I’m genuinely curious to know what makes it quite so important. Personally I find it pretty rare that I want to read the actual source code of my dependencies, as opposed to their documentation.
I gave HLS another try about a day ago, and everything is working fine. I use normal emacs + eglot. I have no Haskell specific configuration for eglot, and everything just works out of the box. My projects don’t have an hie.yaml file either. What they do have is a cabal.project file, which in most cases, boils down to the line: packages: .
P.S: Completely irrelevant but you should definitely checkout the consult-hoogle package for emacs.
You want to use Cabal (the tool), so the following is not directly relevant.
On Windows 11, I use VS Code (code) + ‘Haskell’ extension (with GHCup managing HLS), and Stack.
The current Stackage LTS Haskell is 24.16 (GHC 9.10.3). GHC 9.10.3 requires HLS 2.12.0.0, which is not yet provided by GHCup (it has HLS 2.11.0.0 as ‘latest’).
HLS works fine with Stack’s own code, which is currently based on LTS Haskell 24.9 (GHC 9.10.2; supported by HLS 2.11.0.0), but:
I find I have to run code in the Stack environment itself to avoid HLS reporting spurious problems (i.e. stack exec -- code .); and
you have to successfully stack build first before HLS on the executable components can find the library component on which they depend (otherwise HLS generates cannot satisfy -package stack-3.8.0 problems). (This is minor for Stack, because its executable component is a one-liner that never changes).
Well, maybe I’m just weird, but especially when learning a language I like to be able to hop around dependency code because it helps me resolve the “what the hell is even going on here” moments. Those moments are obviously inevitable in any unfamiliar tech stack, but perhaps especially so with Haskell. (This isn’t a knock on Haskell, I had a similar experience when starting Rust). For Haskell, it would be nice for example to be able to jump to `atomically` and quickly figure out what it means without having to open hoogle, get tempted to open a YouTube tab, etc.
Ah, you’re saying that stack preserves dependencies more faithfully than Cabal? I will say, I used Cabal just because it seemed like the standard, but chatter suggests that Stack has since usurped it.
As others say, jump into thirdpatry deps is not implemented yet. My workflow is to open the library page in hackage press ‘S’ and search for the name of the function. Slower, but is not that bad
There is also this awesome extension in vscode. Which implements the hoogle search in vscode.
The history as I understand it is that cabal-v1 had lots of issues with dependency management, then stack came in to solve that by using snapshots of packages that are guaranteed to work together. Cabal has since fixed those issues, and has a nicer ux for managing dependencies.
My general recommendation is that stack is nice for executables where you aren’t relying on the latest builds or older versions of packages, and for when you don’t change projects too often, and cabal is good for flexibility and contrrol of your dependencies.
How has cabal fixed the snapshot issue? I thought we still need to use a cabal.config file (usually from https://www.stackage.org/lts-24.16/cabal.config in order to get around this.
Cabal doesn’t use snapshots by default, it solves the relevant dependencies and doesn’t mess up the global state like it used to. Of course, you can use snapshots if that’s your preferred solution.
Are you saying cabal will automatically figure out if a dependency that it’s pulling will build with your existing setup?
Yes, all packages on Hackage supposed to work for any allowed versions of their dependencies and cabal will only use a package if it can solve all constraints. If you encounter a package that does not build then you can raise an issue with the maintainer or with the Hackage trustees to fix the version bounds.
That’s an interesting choice of example, because the definition, atomically (STM m) = IO (\s -> (atomically# m) s ), is unlikely to be illuminating at all! What you want are the comments attached to it, which should already appear in your editor on hover, in a nicely marked-up format.
except where all the other deps live in aeson. My hope was that this would provide the rust_analyzer experience of getting type signatures and goto-definition for every symbol in the codebase, but seems not
What you want to achieve used to be present in Intero before HLS was a thing. It didn’t always work, but when it did, it was a bliss. I’m not sure how they implemented that.
I really missed that feature after switching to HLS and remember using something like hasktags with a VS Code extension to get the IDE experience. You can try that.