Haskell Language Server - Rename Plugin
See the Code!
The main pull request for the project is #2108.
This Google Summer of Code project aimed to implement a symbol renaming plugin (as specified by the Language Server Protocol) for the Haskell Language Server (HLS). The motivation was that renaming is a fundamental feature of an integrated development environment that the HLS did not provide.
The deliverables of the plugin outlined in the initial proposal were to:
- perform workspace-wide renames;
- work for all identifiers, including types and type constructors;
- work for qualified identifier;
- have thorough unit tests.
These were all achieved!
Using HieDb allowed a relatively simple method of finding the correct name reference locations. However, naively replacing identifiers at these locations would be incorrect since Haskell can be written in a layout-sensitive manner. Therefore, a more robust method of rewriting the source code was required.
Rewriting the source code
Throughout the project, many different solutions to this task were considered. Initially, using Retrie was suggested. However, when drafting this solution, several limitations became apparent.
- Retrie was not working on windows.
- Retrie had dropped support for older GHC versions (<8.8.0).
- Using Retrie's "unfold" feature would only work to rename identifiers defined at the top level.
Therefore, replacing the relevant identifier nodes using SYB to transform the abstract syntax tree (AST) was a more suitable solution.
Reprinting the AST
ExactPrint for GHC was used to write the source code and create the workspace edit required to respond to the client.
The HieDb Limitation
The use of HieDb in the HLS currently has limitations since the database is populated when each project is indexed, which only happens when the editor loads a project file. This issue also impacts other features, such as reference finding and definition finding.
The work towards resolving this has been started: #2009, #7498.
Due to this issue, the rename plugin currently has limited support in multi-project workspaces and is disabled by default.
Update: April 2022
While the work on full multi-component support continues, we have enabled the rename plugin for use across files within the same module. We also introduced a configuration option for experimental cross-module renaming.
Update: July 2022
Haskell's "Record Puns" language extension was not considered during the initial solution. This language extension allows a single identifier to refer to both a record field and variable binding. As a result, when renaming an identifier, we must either remove the pun (i.e. rewrite the source code to use an explicit binding) or rename all punned identifiers.
We opt for the latter. The implementation can be found at #3013. To find references to all punned identifiers, we find the transitive closure of the direct references. This is found in a single pass since each direct reference can only pun a maximum of one additional identifier.