# How to get a good LSP experience

**URL:** <https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131>\
**Category:** Learn\
**Created:** [October 17, 2025, 9:33pm UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131 "2025-10-17T21:33:34Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![phdavis1027](https://avatars.discourse-cdn.com/v4/letter/p/f475e1/32.png) [@phdavis1027](https://discourse.haskell.org/u/phdavis1027)\
**Post date:** [October 17, 2025, 9:33pm UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/1 "2025-10-17T21:33:34Z")

</div>

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.

Any advice would be greatly appreciated.

---

<div class="post-metadata">

**Author:** ![LaurentRDC](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/laurentrdc/32/4950_2.png) [@LaurentRDC](https://discourse.haskell.org/u/LaurentRDC)\
**Post date:** [October 18, 2025, 12:31am UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/2 "2025-10-18T00:31:38Z")

</div>

Hi there!

My recommended setup is:

1. Install [GHCUp](https://www.haskell.org/ghcup/), which is a toolchain installer. If you’re familiar with Rust, this is like `rustup`
2. During its install, GHCUp will prompt you for stuff. If you don’t understand a question, just go with the default.
3. 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”.

Last step is to hook up your LSP to your favourite editor. [Here are instructions](https://haskell-language-server.readthedocs.io/en/latest/configuration.html#configuring-your-editor). I found it to be trivial to set-up in visual studio code: [literally just intall the official extension](https://github.com/haskell/vscode-haskell).

If any of this is unclear, don’t hesitate to let me know!

---

<div class="post-metadata">

**Author:** ![phdavis1027](https://avatars.discourse-cdn.com/v4/letter/p/f475e1/32.png) [@phdavis1027](https://discourse.haskell.org/u/phdavis1027)\
**Post date:** [October 18, 2025, 1:03pm UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/3 "2025-10-18T13:03:55Z")

</div>

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:

1. 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

2. 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.”

---

<div class="post-metadata">

**Author:** ![LaurentRDC](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/laurentrdc/32/4950_2.png) [@LaurentRDC](https://discourse.haskell.org/u/LaurentRDC)\
**Post date:** [October 18, 2025, 3:17pm UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/4 "2025-10-18T15:17:03Z")

</div>

Two things:

1. I don’t think HLS supports jumping to definition in a third party dependency yet, so this won’t work with any IDE ;
2. Everything else works as I expect in Helix. Unfortunately, I cannot help with other editors.

---

<div class="post-metadata">

**Author:** ![LaurentRDC](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/laurentrdc/32/4950_2.png) [@LaurentRDC](https://discourse.haskell.org/u/LaurentRDC)\
**Post date:** [October 18, 2025, 3:18pm UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/5 "2025-10-18T15:18:54Z")

</div>

Regarding the hie file: can you share its contents? If you use cabal, it should be super simple (hard to type on mobile, but it should be two lines)

---

<div class="post-metadata">

**Author:** ![george.fst](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/george.fst/32/4933_2.png) [@george.fst](https://discourse.haskell.org/u/george.fst)\
**Post date:** [October 19, 2025, 6:28pm UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/6 "2025-10-19T18:28:32Z")

</div>

> [@phdavis1027](#):
>
> I just want to know how to get an LSP, any LSP, to index all the code my project uses.

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.

---

<div class="post-metadata">

**Author:** ![Ei30metry](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/ei30metry/32/5343_2.png) [@Ei30metry](https://discourse.haskell.org/u/Ei30metry)\
**Post date:** [October 19, 2025, 8:59pm UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/7 "2025-10-19T20:59:34Z")

</div>

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](https://cabal.readthedocs.io/en/3.4/cabal-project.html) file, which in most cases, boils down to the line:  
`packages: . `

P.S: Completely irrelevant but you should definitely checkout the [consult-hoogle](https://codeberg.org/rahguzar/consult-hoogle) package for emacs.

---

<div class="post-metadata">

**Author:** ![mpilgrem](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/mpilgrem/32/2529_2.png) [@mpilgrem](https://discourse.haskell.org/u/mpilgrem)\
**Post date:** [October 19, 2025, 10:01pm UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/8 "2025-10-19T22:01:24Z")

</div>

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).

---

<div class="post-metadata">

**Author:** ![phdavis1027](https://avatars.discourse-cdn.com/v4/letter/p/f475e1/32.png) [@phdavis1027](https://discourse.haskell.org/u/phdavis1027)\
**Post date:** [October 19, 2025, 10:44pm UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/9 "2025-10-19T22:44:35Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![phdavis1027](https://avatars.discourse-cdn.com/v4/letter/p/f475e1/32.png) [@phdavis1027](https://discourse.haskell.org/u/phdavis1027)\
**Post date:** [October 19, 2025, 10:46pm UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/10 "2025-10-19T22:46:21Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![Lsmor](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/lsmor/32/3302_2.png) [@Lsmor](https://discourse.haskell.org/u/Lsmor)\
**Post date:** [October 20, 2025, 9:38am UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/11 "2025-10-20T09:38:45Z")

</div>

> [@phdavis1027](#):
>
> The LSP seems to be inaccurate about the symbol under cursor.

I am not experiencing this.

> [@phdavis1027](#):
>
> I can explicitly ask to goto on identifiers, and all symbols from third-party dependencies turn up nothing

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](https://marketplace.visualstudio.com/items?itemName=visortelle.haskell-spotlight) awesome extension in vscode. Which implements the hoogle search in vscode.

---

<div class="post-metadata">

**Author:** ![L0neGamer](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/l0negamer/32/4536_2.png) [@L0neGamer](https://discourse.haskell.org/u/L0neGamer)\
**Post date:** [October 20, 2025, 10:38am UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/12 "2025-10-20T10:38:38Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![dnikolovv](https://avatars.discourse-cdn.com/v4/letter/d/ba9def/32.png) [@dnikolovv](https://discourse.haskell.org/u/dnikolovv)\
**Post date:** [October 20, 2025, 10:48am UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/13 "2025-10-20T10:48:28Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![L0neGamer](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/l0negamer/32/4536_2.png) [@L0neGamer](https://discourse.haskell.org/u/L0neGamer)\
**Post date:** [October 20, 2025, 11:32am UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/14 "2025-10-20T11:32:27Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![dnikolovv](https://avatars.discourse-cdn.com/v4/letter/d/ba9def/32.png) [@dnikolovv](https://discourse.haskell.org/u/dnikolovv)\
**Post date:** [October 20, 2025, 11:43am UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/15 "2025-10-20T11:43:08Z")

</div>

Preferred over what? Are you saying cabal will automatically figure out if a dependency that it’s pulling will build with your existing setup?

---

<div class="post-metadata">

**Author:** ![jaror](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/jaror/32/3271_2.png) [@jaror](https://discourse.haskell.org/u/jaror)\
**Post date:** [October 20, 2025, 11:51am UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/16 "2025-10-20T11:51:47Z")

</div>

> 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](https://github.com/haskell-infra/hackage-trustees/issues) to fix the version bounds.

---

<div class="post-metadata">

**Author:** ![dnikolovv](https://avatars.discourse-cdn.com/v4/letter/d/ba9def/32.png) [@dnikolovv](https://discourse.haskell.org/u/dnikolovv)\
**Post date:** [October 20, 2025, 1:10pm UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/17 "2025-10-20T13:10:08Z")

</div>

That’s really cool, thanks! Admittedly I haven’t experimented with that in quite a few years.

---

<div class="post-metadata">

**Author:** ![george.fst](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/george.fst/32/4933_2.png) [@george.fst](https://discourse.haskell.org/u/george.fst)\
**Post date:** [October 20, 2025, 6:03pm UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/18 "2025-10-20T18:03:27Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![phdavis1027](https://avatars.discourse-cdn.com/v4/letter/p/f475e1/32.png) [@phdavis1027](https://discourse.haskell.org/u/phdavis1027)\
**Post date:** [October 21, 2025, 12:08pm UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/19 "2025-10-21T12:08:26Z")

</div>

So this is the result of my hacky attempt to `cabal unpack` the source of all my deps:

```haskell
cradle:
  multi:
    - path: "."
      config:
        cradle:
          cabal:
            component: "exe:<my_project>"

    - path: "./deps/aeson-2.2.3.0"
      config:
        cradle:
          cabal:
            component: "lib:aeson"
... 

```

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

---

<div class="post-metadata">

**Author:** ![dnikolovv](https://avatars.discourse-cdn.com/v4/letter/d/ba9def/32.png) [@dnikolovv](https://discourse.haskell.org/u/dnikolovv)\
**Post date:** [October 21, 2025, 2:10pm UTC](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131/20 "2025-10-21T14:10:57Z")

</div>

What you want to achieve used to be present in [Intero](https://github.com/chrisdone-archive/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](https://github.com/MarcWeber/hasktags) with a VS Code extension to get the IDE experience. You can try that.

[Next page](https://discourse.haskell.org/t/how-to-get-a-good-lsp-experience/13131.md?page=2)
