Skip to content
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

HLS Plugin to provide Alternate Literal Formats. #2350

Merged
merged 53 commits into from Dec 1, 2021

Conversation

drsooch
Copy link
Collaborator

@drsooch drsooch commented Nov 13, 2021

The following PR is for issue #1560. Currently all functionality is "complete", however there is a lot of dead code (code that exists but the plugin doesn't need to use). I am looking to get some feedback on the plugin itself, and hopefully through the feedback most of the dead code can be culled.

General Mechanics:

A new rule CollectLiterals traverses the GHC source tree to look for literals (Literals.hs). The rule returns the exhaustive list of literals and also a list of FormatType. FormatType's are derived from currently active extensions as well as a base set that are valid no matter the extensions active.

When a user invokes a codeAction keybind, the alternateNumberFormat handler will filter out only the literals that are in the current range of the invoked codeAction. In the handler, the logic for generating possible alternatives is done, and each possibility is transformed into a codeAction sent to the user.

Most of the alternate formats are provided by the Numeric package (show* functions).

HLSInt

HLSFrac

Potential Issues:

  • Literals.hs traverses nearly the entire source tree just to make sure all of the Literals are captured. Certain types of literals end up being discarded (Literals with no source text attached or source location). When codeActions are generated we just immediately throw those out. As a result the Literal constructors of IntPrimLiteral and FracPrimLiteral aren't actually used.
  • Similarly, in Conversion.hs we transform the Extensions NumericUnderscores and NegativeLiterals (LexicalNegation in GHC > 9), into NoFormat which essentially means we don't care about them right now. I'm not sure if generating cases for this would be useful or needed. (NegativeLiterals may not actually be useful to this plugin)
  • The NumDecimals extension allows for literals such as 1.23e5 to be used places where Integer's are expected. I could've gone ahead and defaulted to using the Numeric package, but I tried to encode some logic to display different possible exponents. Ex. 12.3e4, 123.0e3, and 1.23e5 for 123000.
  • I believe all of the necessary code changes for files such as cabal.project and haskell-language-server.cabal but please let me know if there is more to do.
  • I didn't attach any bounds to the plugins cabal file as I was using Stack to build and I am not quite clear on how to generate these bounds effectively.
  • There is (presumably) no compatibility with other GHC versions other than the one I used (8.10.7). I'm not sure how to go about fixing those issues (other than just building with each possible GHC).
  • I have no idea why my Test setup is not working. I modeled the testing functionality after explicit imports plugin, but for whatever reason, the test server does not find ANY codeActions let alone just alternateNumberFormat plugins.

Let me know what you think.

Update our parsing module to return the data type we will use in our
plugin.

The datatype: Literal

Constructors:

Overloaded - Contains an overloaded literal with a SrcSpan

NonOverloaded - Contains non-overloaded literals with a SrcSpan

NoLocation - currently unused.

This data type is bound to change. This module is designed to capture
ALL numeric literals for use in the plugin. When parsing Haskell source
tree, certain cases result in getting a plain `Expr`. This means it is
not annotated with a SrcSpan. As a result, we pass along a SrcSpan
through most functions to keep track of the most recent SrcSpan
encountered. This isn't a fool proof plan and may be removed in the
future.
The traversal of Haskell source has been turned into a rule. This rule
generates hidden diagnostics which allow a user to run a code action
over (most) literals.

There is some "dead code" in the sense that some literals (prims to be
exact) are not allowed to be changed, despite existing in code. The
other "dead code" exists in the FormatType type. Certain extensions
exist that alter how numbers are shown or act, we make note of those
extensions but call them "NoFormat", which essentially means it's a
NOOP.

Most of the conversion is done via the Numeric module. There are plenty
of "show" functions that provide the translation of numbers into the
various formats. The only custom format is a slightly "dumb"
NumDecimal-extension-function that generates three separate options,
instead of using a numeric defined function.
stack.yaml Show resolved Hide resolved
plugins/hls-alternate-number-format-plugin/test/Main.hs Outdated Show resolved Hide resolved
Comment on lines +125 to +126
-- Generate up to 3 possible choices where:
-- dropWhile (\d -> val `div` d) > 1000) implies we want at MOST 3 digits to left of decimal
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
-- Generate up to 3 possible choices where:
-- dropWhile (\d -> val `div` d) > 1000) implies we want at MOST 3 digits to left of decimal
-- | Generate up to 3 possible choices where:
-- dropWhile (\d -> val `div` d) > 1000) implies we want at MOST 3 digits to left of decimal

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way it is haddock compliant and on hover you actually see the comment.

Additionally, check for valid Haddock syntax so it renders properly.

Copy link
Collaborator

@fendor fendor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, this is a great plugin! I have some general comments and ideas but feel free to disagree.

Traversing the whole AST for each change might be very expensive, so we should see whether we can optimise that somehow.

I've looked into your testsuite issue, but can't tell right now what the issue is.

@jneira
Copy link
Member

jneira commented Nov 14, 2021

Many thanks for working on this! It looks great overall

  • I didn't attach any bounds to the plugins cabal file as I was using Stack to build and I am not quite clear on how to generate these bounds effectively.

A usual way is use cabal gen-bounds but we are not being too strict with bounds in other plugins so i would follow what other plugins are doing to get consistenncy between them, take a look to:

build-depends:
, aeson
, base >=4.12 && <5
, containers
, dlist
, extra
, foldl
, ghc
, ghc-exactprint
, ghcide >=1.2 && <1.5
, hls-plugin-api >=1.1 && <1.3
, lens
, lsp
, retrie
, syb
, text
, transformers
, unliftio-core
, unordered-containers

It has bounds on base, ghcide and hls-plugin-api and nothing more

  • There is (presumably) no compatibility with other GHC versions other than the one I used (8.10.7). I'm not sure how to go about fixing those issues (other than just building with each possible GHC).

If you have the chance of building your version for other ghc versions (f.e 8.6.5, 8.8.4 and 9.0.1) it would be great. But We have the ci covering all supported versions so you can leverage it. I would add a line like

- if: matrix.test
name: Test hls-splice-plugin
run: cabal test hls-splice-plugin --test-options="-j1 --rerun-update" || cabal test hls-splice-plugin --test-options="-j1 --rerun" || LSP_TEST_LOG_COLOR=0 LSP_TEST_LOG_MESSAGES=true LSP_TEST_LOG_STDERR=true cabal test hls-splice-plugin --test-options="-j1 --rerun"

  • I have no idea why my Test setup is not working. I modeled the testing functionality after explicit imports plugin, but for whatever reason, the test server does not find ANY codeActions let alone just alternateNumberFormat plugins.

Well, a good way to trace test problems is enable full log output of lsp messages, take a look to the test suite linked just above and use the same invocation locally (the relevant part is LSP_TEST_LOG_COLOR=0 LSP_TEST_LOG_MESSAGES=true LSP_TEST_LOG_STDERR=true)

@drsooch
Copy link
Collaborator Author

drsooch commented Nov 15, 2021

  • I have no idea why my Test setup is not working. I modeled the testing functionality after explicit imports plugin, but for whatever reason, the test server does not find ANY codeActions let alone just alternateNumberFormat plugins.

Well, a good way to trace test problems is enable full log output of lsp messages, take a look to the test suite linked just above and use the same invocation locally (the relevant part is LSP_TEST_LOG_COLOR=0 LSP_TEST_LOG_MESSAGES=true LSP_TEST_LOG_STDERR=true)

I ran the tests with logs enabled. Looks like an exception occurs (which I think is relevant?) and then a shutdown message is sent to the server.
AsyncCancelled

It looks like it's attempting to do some actual work but for whatever reason a shutdown message is sent?

ShutdownMessage

@jneira
Copy link
Member

jneira commented Nov 15, 2021

@drsooch i have taken the liberty of add the plugin to the rest of cabal.project's (for ghc 9.0.1 and 9.2.1) and to the test workflow, to see how is going in ci

@jneira
Copy link
Member

jneira commented Nov 15, 2021

I ran the tests with logs enabled. Looks like an exception occurs (which I think is relevant?) and then a shutdown message is sent to the server.

That message about finishing the build is normal. I guess the test is not finding the code action, not sure why

Take a look to this test in the haddock plugin:

actions <- getCodeActions doc (Range (Position l c) (Position l $ succ c))

It is looking for a code action in a specific code range, maybe you need something similar to trigger it?

@jneira
Copy link
Member

jneira commented Nov 15, 2021

Hi, it seems the plugin is not buildable with ghc-8.6.5, a version we still support. I hope only changing plugin base constraints could make the build work:

Resolving dependencies...
cabal: Could not resolve dependencies:
[__0] trying: ghcide-1.4.2.4 (user goal)
[__1] trying: base-4.12.0.0/installed-4.12.0.0 (dependency of ghcide)
[__2] next goal: hls-alternate-number-format-plugin (user goal)
[__2] rejecting: hls-alternate-number-format-plugin-0.1.0.0 (conflict:
base==4.12.0.0/installed-4.12.0.0, hls-alternate-number-format-plugin =>
base>=4.13 && <5)
[__2] fail (backjumping, conflict set: base,
hls-alternate-number-format-plugin)
After searching the rest of the dependency tree exhaustively, these were the
goals I've had most trouble fulfilling: base, ghcide,
hls-alternate-number-format-plugin

https://github.com/haskell/haskell-language-server/runs/4209830202?check_suite_focus=true

@drsooch
Copy link
Collaborator Author

drsooch commented Nov 15, 2021

Hi, it seems the plugin is not buildable with ghc-8.6.5, a version we still support. I hope only changing plugin base constraints could make the build work:

Resolving dependencies...
cabal: Could not resolve dependencies:
[__0] trying: ghcide-1.4.2.4 (user goal)
[__1] trying: base-4.12.0.0/installed-4.12.0.0 (dependency of ghcide)
[__2] next goal: hls-alternate-number-format-plugin (user goal)
[__2] rejecting: hls-alternate-number-format-plugin-0.1.0.0 (conflict:
base==4.12.0.0/installed-4.12.0.0, hls-alternate-number-format-plugin =>
base>=4.13 && <5)
[__2] fail (backjumping, conflict set: base,
hls-alternate-number-format-plugin)
After searching the rest of the dependency tree exhaustively, these were the
goals I've had most trouble fulfilling: base, ghcide,
hls-alternate-number-format-plugin

https://github.com/haskell/haskell-language-server/runs/4209830202?check_suite_focus=true

Yeah, I started testing out 9.0.1 as well, and there are some changes needed However, those are pending @fendor's comments about using syb. I will aim to clean up the builds for each GHC version tonight.

Copy link
Member

@Ailrun Ailrun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nitpicking here

Copy link
Member

@jneira jneira left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test data files in .cabal are needed to merge this
also, could you add the plugin to the features docs page, with a demo if possible? that will make it more relevant and discoverable
thanks!

@drsooch
Copy link
Collaborator Author

drsooch commented Nov 30, 2021

test data files in .cabal are needed to merge this also, could you add the plugin to the features docs page, with a demo if possible? that will make it more relevant and discoverable thanks!

What about hackage.yml? Does that need updating?

@jneira
Copy link
Member

jneira commented Nov 30, 2021

test data files in .cabal are needed to merge this also, could you add the plugin to the features docs page, with a demo if possible? that will make it more relevant and discoverable thanks!

What about hackage.yml? Does that need updating?

oh, you are damn right, thanks for noting it
could you add it yourself, please?

@jneira
Copy link
Member

jneira commented Nov 30, 2021

There is a failing test plugin related for ubuntu and 8.10.7:

   Conversions
    Match NumDecimal:        FAIL
      *** Failed! Falsified (after 21 tests):
      10
      Use --quickcheck-replay=414296 to reproduce.
      Use -p '/Match NumDecimal/' to rerun this test only.

@jneira
Copy link
Member

jneira commented Nov 30, 2021

test data files in .cabal are needed to merge this also, could you add the plugin to the features docs page, with a demo if possible? that will make it more relevant and discoverable thanks!

What about hackage.yml? Does that need updating?

oh, you are damn right, thanks for noting it could you add it yourself, please?

done with eb05482

@jneira jneira self-requested a review November 30, 2021 07:58
Copy link
Member

@jneira jneira left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the plugin looks great, many thanks for working on this
only left fix the failing test

@jneira jneira added the merge me Label to trigger pull request merge label Nov 30, 2021
@jneira
Copy link
Member

jneira commented Dec 1, 2021

/rerun-workflow Testing

@mergify mergify bot merged commit 1a74937 into haskell:master Dec 1, 2021
drsooch added a commit to drsooch/haskell-language-server that referenced this pull request Dec 3, 2021
* Initialize and Setup Plugin Source.

* Provide Traversal Module to Haskell AST that collects Literals.

* Change Literals module to return type.

Update our parsing module to return the data type we will use in our
plugin.

The datatype: Literal

Constructors:

Overloaded - Contains an overloaded literal with a SrcSpan

NonOverloaded - Contains non-overloaded literals with a SrcSpan

NoLocation - currently unused.

This data type is bound to change. This module is designed to capture
ALL numeric literals for use in the plugin. When parsing Haskell source
tree, certain cases result in getting a plain `Expr`. This means it is
not annotated with a SrcSpan. As a result, we pass along a SrcSpan
through most functions to keep track of the most recent SrcSpan
encountered. This isn't a fool proof plan and may be removed in the
future.

* Implement Logic for actual CodeActions.

The traversal of Haskell source has been turned into a rule. This rule
generates hidden diagnostics which allow a user to run a code action
over (most) literals.

There is some "dead code" in the sense that some literals (prims to be
exact) are not allowed to be changed, despite existing in code. The
other "dead code" exists in the FormatType type. Certain extensions
exist that alter how numbers are shown or act, we make note of those
extensions but call them "NoFormat", which essentially means it's a
NOOP.

Most of the conversion is done via the Numeric module. There are plenty
of "show" functions that provide the translation of numbers into the
various formats. The only custom format is a slightly "dumb"
NumDecimal-extension-function that generates three separate options,
instead of using a numeric defined function.

* Clarify source code comments. Reformat file structure.

* Set up Test Scaffolding.

* Miscellaneous clean-up.

* Use Regex to Match Source Text.

* Remove unneccessary diagnostics from Rule.

* Update all stack versions to include new Plugin.

* Clean up Conversion.hs and AlternateNumberFormat.

Clean up function names and add supporting haddock documentation for
exported functions.

* Update cabal file bounds and add Github workflow test

* Add plugin package to all cabal.project

* Add plugin to test suite

* Push SYB changes. Test.hs is merely for testing and will be removed prior to Merge.

* Use Generics.SYB for parsing Literals from GHC Source.

Updated Literals.hs to use custom SYB traversal of the AST.

* Update hls-alternate-number-format-plugin.cabal

* WIP: run tests

* Test additions and GHC compatability changes.

* Removed dead code.

PrimLiterals were defined originally, however GHC source doesn't provide
the source text. This is one of the needs of the plugin so we don't
provide duplicate suggestions.

Swapped `Maybe Text` to `Text`. Similar to above we now ignore all
literals that have no source text attached.

Swapped `Maybe SrcSpan` to `RealSrcSpan`. Again, similar to the previous
points we now ignore literals that don't get a sourceSpan attached.
Similarly, we drop any `UselessSpan`'s as those are also not very
helpful.

* Use Set to remove duplicates from CollectLiterals Result.

Certain AST representations are traversed multiple times with the switch
to SYB. Using Set allows to easily remove duplicate Literals from our
result.

Added a test suite to monitor for regressions.

* Delete Test.hs

Inadvertently commit this file.

* Add README and bump version to 1.0.0.0.

* Update HLS cabal file for version change.

* Additional Tests.

* Update Test file to use Path library function.

* Miscellaneous Code Fixes.

- Remove unneeded imports/functions.

- Export only specified functions from each module (and add Haddock
  Comment if necessary)

- Minor text changes for accuracy

* Add README to Extra Source Files.

* Update Extra Source Files to include Test Haskell Source files.

* Update Features.md and Minor Bug Fix :)

* Add new plugins to hackage workflow

* Fix Bug caused by Minor Bug Fix.

Co-authored-by: jneira <atreyu.bbb@gmail.com>
Co-authored-by: Fendor <power.walross@gmail.com>
@drsooch drsooch linked an issue Dec 8, 2021 that may be closed by this pull request
@drsooch drsooch deleted the alternate-number-format branch August 20, 2022 02:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
merge me Label to trigger pull request merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Feature idea: alternate format for numeric literals
6 participants