haskell / haskell-language-server

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

Type-aware autocompletion #2081

Open viluon opened 3 years ago

viluon commented 3 years ago

Hi, I'm surprised that I couldn't find a similar issue, except for https://github.com/haskell/haskell-language-server/issues/817. Please excuse me if it is misplaced.

I wish HLS provided type-aware autocompletion. The autocompletion it does have often isn't very useful: image

In this case, the obvious type-correct autocompletion would be ssDynTrace, of type [DynTraceEntry]. Instead, the autocompletion is full of relevant but quite useless definitions. In fact, ssDynTrace isn't even listed until I type a d. image

Another issue is that with record wildcards, there are duplicate completions available. Here, ssDynTrace is listed once as the value from the wildcard and once more as the getter function. image

In theory, I could use a hole instead, but invoking Wingman with many bindings in scope isn't a good idea: image

Even if VS Code made the quick action list searchable, the "attempt to fill hole" and "refine hole" commands time out. Plus, Wingman doesn't seem like the right tool for the job – I know pretty well what I want to write, I just don't want to have to spell every identifier out, and the types involved don't uniquely identify the correct solution.

It'd be nice if HLS could trim the autocompletion suggestions by type and offer relevant suggestions even with no prior identifier input.

jneira commented 3 years ago

Really a good point, thanks for suggesting it. There is a similar issue which asks that import symbols would take in account types too: #754

isovector commented 3 years ago

Sweet mother of mary that's a lot of code actions from Wingman. Mind sharing the file? I'm curious how you got so many bindings in scope! Maybe we should limit it to like 8 suggestions?

pepeiborra commented 3 years ago

We could implement this feature by:

  1. Replacing the partial text (in your example ssDy) by a hole _
  2. Asking GHC to fill in the hole
  3. Filtering out all the suggestions that do not start with the partial text

The problem is that currently typed holes are hopelessly slow (#2037) and not suitable for completions. Maybe when VSCode and other editors implement the LSP spec support for partial responses, and/or when typed holes become faster.

On the other hand, someone could reimplement the typed holes machinery out of GHC with a better performance, then it would be more realistic to offer this feature.

viluon commented 3 years ago

@isovector the file I was editing is on GitHub, although ssDynTrace specifically is only in my local fork. Just use modify and a record wildcard for the StgState constructor, all the ss... bindings come from there. Using record wildcards clutters the local environment but leads to a lot more concise code.

Tarmean commented 3 years ago

I've been playing with wingman to add a 'guess' command which looks at the types of local identifiers in scope and then tries to guess functions that could be relevant. (The really halfbaked implementation is here but it needs some significant changes)

Some things I noticed:

foo :: Int
foo = x
    where
       x = _

Unfortunately the hole is fully polymorphic.

Also, having some sort of TyCon -> UniqueSet index on ExternalPackageState to quickly filter down to a set of plausible types seems useful to keep things fast. I'm not quite sure what the most efficient way to get all identifiers+types for package-local things is, though.

Edit: Does GhcSessionDeps have an up-to-date InteractiveContext?

Tarmean commented 3 years ago

I've pushed a commit that wires my quick-and-dirty-attempt into an autocompletion handler. I hacked the standard autocompletion rules to work with typechecked modules and stash the types in the completion items to avoid duplicating them, I'm not sure how significant the slowdown of this is and how much it breaks.

This still loops through all completion items and first checks whether they contain relevant type constructors, and then does a really inefficient search that tries to unify all local variables with all argument positions. The naive approach is definitely too slow, but if the types are concrete enough it sometimes give useful results:

https://user-images.githubusercontent.com/11866759/129174240-77786b43-5807-4a86-9e6d-759e89840afb.mp4

For discoverability completion for empty prefixes would be useful, but it can't give completions for empty prefixes because the ast doesn't work out. [wingman|guess|] sometimes kind of works but the correct answer often isn't the first result.

isovector commented 3 years ago

@Tarmean this is fantastic work! RE:

does a really inefficient search that tries to unify all local variables with all argument positions

Did you throw GHC's unification at the problem? It's quite good, though a little tricky to get right in the presence of skolems. Wingman wraps the problem via:

https://github.com/haskell/haskell-language-server/blob/c4eeb37ceb3f898e03af98a7b50b192718b88f4b/plugins/hls-tactics-plugin/src/Wingman/GHC.hs#L381-L390

[wingman|guess|] sometimes kind of works but the correct answer often isn't the first result.

Yeah, this is a general problem for Wingman, caused by the fact that LSP sucks and really gives us nothing we can be interactive with. Though now that I think about it --- we could experiment with showing the top 5 results from the tactic search as code actions.

Tarmean commented 3 years ago

I did use your tryUnifyUnivarsButNotSkolems and it was super useful!

I struggle more with the whole fuzzy matching part. If the candidate is Map.intersect and there are two maps with String keys involved it probably should be scored high. But zipWith7 is probably still not what you want even if you have a lot of lists in scope. There probably is something a lot smarter that I could do, but hoogles heuristics seem to break down if there is unrelated noise.

Also, I know GHC distinguishes rigid with polymorphic type variables. Is there something like this for constraints?

-- only have to search for types with this constraint
foo :: Foldable f => f a -> Int
-- also have to search for types which implement this constraint
foo :: Bool
foo = null _