haskell / haskell-language-server

Official haskell ide support via language server (LSP). Successor of ghcide & haskell-ide-engine.
Apache License 2.0
2.65k stars 355 forks source link

OverloadedRecordDot support - Type Awareness / Completion #2732

Open Dessix opened 2 years ago

Dessix commented 2 years ago

Given the following (GHC 9.2+) snippet:

{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NoFieldSelectors #-}

data MyRecord = MyRecord
  { a :: String
  , b :: Integer
  } deriving (Eq, Show)

x :: MyRecord
x = MyRecord { a = "Hello", b = 12 }

y = x.a

z = x.b

HLS now understands (without record-dot-preprocessor!) that y is a String and z is an Integer, seen in both above-line hints and on hover.

Unfortunately, it has no type information available when hovering over the a in x.a or the b in x.b. Additionally, it lacks the ability to suggest- when entering a dot after a record type- the fields accessible on that type.

Describe the solution you'd like

The ability to "dot into" types is both a small form of IDE-driven API exploration and a way to easily manage larger applications without needing a Haddock window on the side at all times.

Pressing '.' after a type (or manually opening auto-completion with the cursor directly following a dot following a type) should summon auto-completion with the context narrowed to the prior type's fields.

For a step further, but one worth considering when designing a solution, a useful addition would be allowing automatic import of the type information required if necessary, similar to when accepting a completion of an unimported trait function in rust-analyzer. Documentation on the HasField requirements is available here (HasField constraints) and here (OverloadedRecordDot). NoFieldSelectors may complicate this functionality.

Describe alternatives you've considered

I've built my glue-code in Rust instead, so far, to avoid having to manage large numbers of one-off record types in Haskell, but I'd love to unify my codebase, and Record Dot Syntax has been the selling point that would get me there, assuming the presence of IDE support. It appears that 9.2.1 support is now functioning, but dot-accesses appear opaque to the language server.

michaelpj commented 2 years ago

Worth noting that we can probably do this for records easily enough, but I'm not sure how easy it would be to handle arbitrary "fields" created by user-provided HasField instances.

ocharles commented 2 years ago

@michaelpj Maybe it's possible to find all instances of HasField in scope for a particular type? That would handle both record fields and user defined fields, I believe.

michaelpj commented 2 years ago

(Just to clarify, I'm talking about completions, hovers should be fine in all cases.)

Well, typeclass instances can apply non-obviously, can depend on what's in scope from local type signatures, etc. In the limit, that amounts to "run the typechecker in some special way", I think.

So in increasing order of difficulty:

  1. Actual record fields.
  2. "Simple" instances of HasField with literal Symbols and no superclass constraints
  3. Arbitrary HasField instances.
Dessix commented 2 years ago

That implementation order would cover the largest portion of use cases with just part 1, while covering niche cases like Haskell-Methods in part 3. I think "superrecord"-style types and such fall in part 2?

Regardless- part 1 would bring support to the most obvious case of record dot syntax, and also covers a lot of scenarios like the ReaderIO / RIO pattern. I'd suggest splitting this into three implementation/feature parts, if it helps close on part 1 more easily.

michaelpj commented 2 years ago

This is going to be tackled as part of GSoC by @coltenwebb.

coltenwebb commented 2 years ago

I'll be addressing these features in separate PRs, and track them here.

coltenwebb commented 2 years ago

I've started brainstorming how to do the record field completions, so I'll write my thoughts here:

For the record field completions, it looks like I might be able to extend this to get the name of the record that is dotted. From there, I should be able to calculate the type by searching the HieAST for the text that's dotted. My question then is how to get a fresh HieAST while calculating the completions, and whether computing the types on the fly will be fast enough. Perhaps there is a way to pass the HieAST to this, or to access it with LspM. Thoughts/feedback on this would be appreciated.

Edit: It turns out that types get inferred top-down, so using HieAst directly will cause completions not to show for some edge cases. For example in y = "hello" ++ (x.c), the x.c will have [Char] type no matter what c actually should be, so completions for x.c. will break. A standalone record will always have the correct type it seems, so x. completions will still work.

If that works, I need a way to get the fields for the record. I don't think HieDB provides a way to read the record fields. One workaround could be querying with hiedb for functions of type MyRecord -> * but this doesn't seem robust, especially since NoFieldSelectors would ruin it. Thoughts would be appreciated here too.

guibou commented 3 weeks ago

A few note about the status of hover and error message recording OverloadedRecordDot:

See how when I have my point on x, all the other reference to x are highlighted:

image

michaelpj commented 3 weeks ago

Is this something which should be upstreamed to GHC so they add a suggestion we can use?

I think that would be good. We very much rely on GHC to drive our import suggestions. Since HasField is a magic typeclass, it would be very helpful if GHC could suggest imports that could be added to make that constraint solvable!

guibou commented 2 weeks ago

There are already an issue opened on GHC side related to suggesting import or using another name in case of typo: https://gitlab.haskell.org/ghc/ghc/-/issues/18776