Skip to content

hls-render-plugin: Proof of Concept for a mechanism for customizable type driven "Actions" #4604

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

joe-warren
Copy link
Contributor

@joe-warren joe-warren commented May 29, 2025

Hi, new contributor here

I'm opening this as much as a proof of concept as anything.

This PR adds a new plugin, which is conceptually very similar to (and cribs heavily from) hls-eval-plugin.

However, unlike hls-eval-plugin, it's not triggered by doctest comments, instead it takes a "configuration" file, containing a number of Haskell functions, and for each combination of "value in the current module" and "function in the config", if the result of applying the function to the value is IO () it generates a code lens which runs that result.

There's a lot of duplication between this and the eval plugin, and I'd be open to adding this behaviour to the eval plugin instead, but my thought process was that a standalong plugin is more modular, and people might want to turn this off, and not the eval plugin,

I've recorded a demo of the plugin in use, which I've uploaded here


There are a handful of things that I think make this un-mergable as is:

The big one is the mechanism by which I'm evaluating the configuration file; I'm doing this with runDecls (after parsing out any imports).

This seems to me to be inferior to compiling the file as a module, and then importing it qualified. As there's currently a risk of naming collisions (and the code I have for separating out imports is incomplete, and requires each import to take exactly one line).

I'm also open to changing the name "hls-render-plugin". I didn't want to use the word "action", because I'm aware that actions already means something else in the context of the Language Server Protocol.

I'm using the Pretty instance on Type, to convert the type of the applied "action" to a string in order to compare it to IO (), I highly suspect this isn't the right way to go about that.

The "stylish-haskell" post-commit action doesn't seem to work if modified files contain C++ preprocessor directives?

Finally, this code is completely untested, and I'm more than happy to fix that if you have any suggestions for sensible tests.

@deemp
Copy link

deemp commented Jun 4, 2025

@joe-warren in the demo, you state that the current doctests approach isn't very ergonomic because it requires you to write a comment and to import functions that you're going to use.

Not a HLS contributor, but I'd like to discuss the design that you propose.

Local values, result types, types on hover

New plugin

With your plugin, one can write a number of top-level functions that use local values that are unavailable in the configuration file.

One will then be able to run some of these top-level functions using code lenses that call IO () functions.

One will also be able to see the types of expressions on hover.

t = 3

f = id

f1 = f t

Doctests

When writing doctests, one can also write top-level functions that use local values that are unavailable in the configuration file.

Doctests may produce not only IO ().

Unfortunately, one can't interactively view the types of expressions in doctests. Hence, one needs to create auxiliary functions like f1 and Evaluate them.

t = 3

f = id

f1 = f t

-- >>> f1

Unnecessary function imports

New plugin

New plugin avoids imports by loading the configuration file into a GHCi session.

However, unlike hls-eval-plugin, it's not triggered by doctest comments, instead it takes a "configuration" file, containing a number of Haskell functions, and for each combination of "value in the current module" and "function in the config", if the result of applying the function to the value is IO () it generates a code lens which runs that result.

From the demo:

I'm doing the equivalent of copying and pasting the contents of that file into the GHCi session.

Doctests

Doctests do require imports.

This fact leads to two problems:

  1. If an import isn't used anywhere else apart from doctests, a warning is displayed that this import is unused.
  2. Imports from local modules in the $setup section don't work #4612

After the second problem is resolved, one will be able to write imports that are necessary only for doctests into a $setup section. Hence, it will be possible to resolve the first problem.

Wrong code lenses

New plugin

The new plugin may display code lenses for combinations of render and ordinary functions that don't make sense. Such code lenses may create a lot of visual noise.

Moreover, each part of the codebase may require own set of render functions. It may not be feasible to put all render functions into a single configuration because the visual noise from code lenses will increase even more.

Doctests

When using the doctests approach, one will see Evaluate code lenses above each doctest.
This visual noise seems acceptable.

Relation to a Haskell package

New plugin

It's unclear whether the configuration file should belong to the Haskell package where the file is used (i.e., be listed in the package's .cabal file). Can HLS show types on hover in the configuration file when you're composing that file?

Doctests

Doctests in a module use functions imported from local and non-local modules (e.g. modules from dependencies on Hackage). Local modules and dependencies are tracked in .cabal files. HLS shows types on hover in local modules.

Conclusion

To sum up, the new plugin provides a slightly less verbose way for composing and running IO () actions using definitions from a special module (a configuration file), but the plugin may introduce significant visual noise and issues with maintainability of the code in the configuration file.

Despite some overhead in the form of comments and auxiliary functions, doctests might be a more general, more precise, and less noisy solution to the problem of quickly composing and running IO () actions using definitions from local modules when #4612 is fixed.

@joe-warren
Copy link
Contributor Author

The new plugin may display code lenses for combinations of render and ordinary functions that don't make sense. Such code lenses may create a lot of visual noise.

I don't know if I'm too concerned about visual noise from render code lenses, because:

  1. the plugin can be disabled (may be disabled by default)
  2. noisy render actions (actions that typecheck when applied to a lot of values, but don't make sense to evaluate), seems more like a hypothetical issue with a bad configuration, rather than a fundamental issue with the approach

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants