As a Tweag Open Source
fellow, I aimed to
improve and build on the Haskell IDE experience, mainly by contributing to the
ghcide
and haskell-language-server
projects. My main goals were to polish up
the overall experience, and integrate
hiedb
, a product of a Summer of Code
project last year, into ghcide
.
The product of this fellowship was a good selection of ghcide
and
haskell-language-server
features that you can use right now, or will be able
to use very soon, including better search, richer information and more
efficient queries. Let’s go through these features.
hiedb: searching references and workspace symbols
hiedb
is a tool to index and query .hie
files that I have been working on for some time. It reads .hie
files and
extracts all sorts of useful information from them, storing it in a sqlite
database for fast and easy querying.
Integrating hiedb
with ghcide
has many obvious (and non-obvious) benefits.
For example, we can finally add support for find reference
, as well as
allowing you to search across all the symbols defined in your project.
In addition to this, the hiedb
database serves as an effective way to
persist information across ghcide
runs, allowing greater responsiveness,
ease of use and flexibility to queries. hiedb
also works extremely well for
saving information that is not local to a particular file, like definitions,
documentation, types of exported symbols and so on.
Under this paradigm, ghcide
acts as an indexing service for hiedb
,
generating .hi and .hie files which are indexed and saved in the database,
available for all future queries, even across restarts. A local cache of .hie
files/typechecked modules is maintained on top of this to answer queries for the
files the user is currently editing, while non-local information about other
files in the project is accessed through the database.
This work is being carried out in this
branch and should land in
mainline ghcide
soon.
Responsive IDEs using stale information
I discussed, in an earlier blog
post,
how ghcide
could only process a single request at a time, and was cancelling old requests,
which lead to slow response times, and features like completion being almost
unusable.
The solution mentioned in the blog post above has now been merged into mainline
ghcide
, but with a few major changes. Pepe Iborra came up with an alternative
approach that allowed managing the Shake session in a more fine grained manner,
which let us eliminate needless restarts without the need for another queue.
Here are a few graphs that demonstrate the massive improvements in response times that are achieved by using stale information:
As also mentioned in the blog post, my hiedb-4
branch of ghcide
can also pick on on
.hie
files written by the previous run of ghcide
, to allow you to immediately
use your IDE even before the initial configuring and typechecking step has run.
Typechecking your entire project
With this PR, ghcide
will
typecheck your entire project on the initial run, and when you save a file, typecheck
all the files that (transitively) depend on that file to give you
up to date and accurate diagnostics for your entire project.
This behaviour is completely configurable, so if you don’t want to see
diagnostics for your entire project and only want to see them for the files you
have open, you can configure ghcide
to do so using your editor’s LSP configuration.
We could not do this earlier because ghcide
did not know what the module
graph of your project looked like. We have plumbed in this information to the
correct places now, so that ghcide
can perform these crucial functions.
Find all variables of a given type
Sometimes, you want to know all the places that could potentially be affected if
you change the definition of a type. Maybe you want to gauge how much a type is
used, to check how painful it would be to delete it. Well, now you can, using the
power of hiedb
and ghcide
. Just find references
for a type, and your editor
will also highlight all the variables which mention it.
You can restrict the query to a particular depth. For example,
foo :: Int
foo = 1
bar :: [Maybe Int]
bar = Just foo
the type of foo
contains Int
at a depth of 0. The type of
bar
contains Int
at a depth of 2.
Here, you can see that viewing references for VFSHandle
also highlights all
the symbols that include VFSHandle
in their type.
Use the IDE on all your dependencies!
It is always fun to zip around your code using the goto definition
and find references
features. But this comes to quick stop as soon as you try to go to
the definition of a function not defined in your project, but imported from an
external dependency. Don’t worry, .hie
files can come to the rescue!
In addition to all sorts of other useful information, .hie
files also contain
the original source of the Haskell file they were generated from. If ghcide
knows about the .hie
files for your dependencies, it can use those to show you
the source.
This is also available on the hiedb-4
branch.
First, you need to generate .hie
files for your dependencies. This can be
easily done by adding the following to your cabal.project
:
package *
ghc-options: -fwrite-ide-info -hiedir <some-directory>
Then you simply need to inform ghcide of the directory you told ghc
to put
the hie
files in, and you are ready to go! Thanks to all the useful
information in .hie
files, many of the IDE features like types and
documentation on hover, go to definition, references and more will be available
on these files also, so your IDEing can continue on seamlessly.
Currently, it is not possible to navigate into boot libraries (the libraries that
ship with GHC) using this, as the standard distribution of GHC doesn’t ship with
.hie
files for these libraries, and it is quite an involved procedure to compile
these libraries yourself.
Hopefully, future versions of GHC will ship with .hie
files so that we can
navigate into those too using ghcide
.
Scope aware local completions
Recently, Sandy Maguire wrote a plugin for haskell-language-server
that added
the ability to
case-split. The
plugin required the ability to get all variables in scope at a particular point
in the source. For this, we needed a data structure that could store this
information and allow efficient queries. We settled on the an
IntervalMap
populated with scoping information obtained from the .hie
file, which allows
querying all the identifiers available at a particular point, along with their
types.
Once we had this scoping information, it was very simple to use it to
augment the ghcide
completion subsystem to generate accurate completion
for local variables. This is now available in the branch of ghcide
used by
haskell-language-server
, and will soon come to ghcide
.
A type safe interface for LSP
I have also been working on improvements to the haskell-lsp
library that
powers ghcide
, hls
and hie
, providing a Haskell interface for
the Language Server Protocol that makes it possible to communicate with editors . We are
in the process of moving to a type safe encoding of the LSP
specification, which
should make the API much more usable and less prone to errors, as well as making
it much easier to detect any deviations from the specification.
This also leads to great improvements in the interface of the lsp-test
library, so that the compiler can infer and check the shape of the data
you are sending matches the method type
The PR also brings a host of
other improvements and bug fixes for the haskell-lsp
library, simplifying and
cleaning up much of the code and making the interface much more consistent.
Coming soon
Go to instance definition and view all usages of an instance
Recently, one of my
MRs was merged into GHC, which adds information about typeclass evidence to
.hie
files. This will allow for features like going to the definition of an
instance used at a particular point, or viewing all usages of a particular
instance across all your code. These features can be added to hiedb
and
ghcide
when GHC
9.0 lands.
I’ve implemented a proof of concept for this in the haddock
hyperlinker:
Call Hierarchy graphs
The upcoming 3.16 version of the Language Server Protocol has added support for Call Hierarchies in the editor. This will allow language servers to expose the call graph of the project to user.
Fortunately for us, hiedb
has supported generating call graphs for a while,
as Graphviz graphs. It will be relatively straightforward to adapt this
functionality for LSP.
Types for all subexpressions in GHC
Currently, it is not possible to easier extract the type of arbitrary expressions from the GHC AST. Currently the most straightforward way to do this is to desugar the expression, and then extract the type from the desugared expression. Unfortunately, this approach doesn’t scale when you want to extract the types of all subexpressions in a particular program, since it means that you have to desugar an exponential amount of expressions!
I have been working on a custom compiler
pass building off
of work done by
Ben Gamari, which will annotate expressions with their types. This will allow
tooling such as .hie
files to include this information, and will allow
ghcide
to tell you the type of any arbitrary expression in your program.
Final Thoughts
The open source fellowship was a great opportunity. It was very enlightening and helpful to interact with Tweagers. I really enjoyed the freedom allowed to choose where to focus my efforts myself. The funding allowed me to devote a significant amount of time to working on open-source and improve the Haskell developer experience.
IDEs are not easy to write and maintain, and they require a lot of effort to develop and extend. Funding is absolutely critical for this. Fortunately, this year we had two IDE related GSOC projects, along with this Tweag Fellowship, so were able to make great strides with the help of all other volunteers who work and contribute to these projects.
About the author
If you enjoyed this article, you might be interested in joining the Tweag team.