-
-
Notifications
You must be signed in to change notification settings - Fork 368
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
Provide explicit import in inlay hints #4235
Conversation
to writing test before new release of haskell/lsp.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, so the things we definitely need before we can land this:
- Clicking the hints needs to do something. I think that means either including the
textEdits
field or working out how to connect things to a command. - We need to be able to configure these so you can have one or the other. A simple thing to do would be to add some generic config for "inlay hints on or off", like we have for code lenses etc. Then people at least have some way of configuring it.
hls-plugin-api/src/Ide/Types.hs
Outdated
@@ -803,6 +809,12 @@ instance PluginRequestMethod Method_TextDocumentSemanticTokensFull where | |||
instance PluginRequestMethod Method_TextDocumentSemanticTokensFullDelta where | |||
combineResponses _ _ _ _ (x :| _) = x | |||
|
|||
instance PluginRequestMethod Method_TextDocumentInlayHint where | |||
combineResponses _ _ _ _ (x :| _) = x |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't seem right: the inlay hints request returns a list! So it's a really easy example where we can combine the responses nicely, just concatenate them.
hls-plugin-api/src/Ide/Types.hs
Outdated
combineResponses _ _ _ _ (x :| _) = x | ||
|
||
instance PluginRequestMethod Method_InlayHintResolve where | ||
combineResponses _ _ _ _ (x :| _) = x |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Include the reference to the note explaining why there should only be one resolve handler. We can also consider implementing resolve later.
-- This plugin provides code actions | ||
-- This plugin provides inlay hints | ||
<> mkPluginHandler SMethod_TextDocumentInlayHint (inlayHintProvider recorder) | ||
-- <> mkResolveHandler SMethod_InlayHintResolve (inlayHintResolveProvider recorder) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think just delete this for now, we can worry about resolve later.
(ImportActionsResult {forLens, forResolve}, pm) <- runActionE "ImportActions" state $ useWithStaleE ImportActions nfp | ||
let inlayHints = [ generateInlayHints newRange ie | ||
| (range, int) <- forLens | ||
, range < visibleRange |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you want isSubRangeOf
, I'm not sure this does what you want.
InlayHint { _position = _end | ||
, _label = mkLabel ie | ||
, _kind = Nothing | ||
, _textEdits = Nothing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should definitely set this, it's kind of the point!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
God, the spec is weird. Inlay hints themselves only have text edits, but label parts can have commands??? I'm not even sure what you're meant to do there.
@@ -444,6 +479,10 @@ abbreviateImportTitle input = | |||
else actualPrefix <> suffixText | |||
in title | |||
|
|||
squashedAbbreviateImportTitle :: T.Text -> T.Text |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is this different to what we do on line 170? Why are we doing different things?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I worked it out: the previous thing has the whole import spec in it. I think it would be better to just create the two bits of the import spec separately when we generate the title, then we can use only the pieces we need.
We'd then need to tweak abbreviateImportTitle
to just abbreviate the list of imported items, and then we can glue it onto the previous part of the import or not depending on whether we're creating an inlay hint or a code lens.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not only that, abbreviateImportTitle
will retain a lot of indentation (shown as spaces after cutting by line), and inlay hints label will need remove those spaces because they are displayed at the end of each imports statement to avoid being too long.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The thing I was suggesting is that in the ImportActions
rule we could compute both a) the prefix of the import (import Foo
) and the suffix ((bar, baz)
). Then for the code lenses we can concatenate them together and abbreviate, and for the lens we can just use the suffix and abbreviate?
cabal.project
Outdated
@@ -16,6 +16,24 @@ benchmarks: True | |||
|
|||
write-ghc-environment-files: never | |||
|
|||
source-repository-package |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can use the released version now, just bump the index-state
where | ||
generateInlayHints :: Range -> ImportEdit -> InlayHint | ||
generateInlayHints Range {_end} ie = | ||
InlayHint { _position = _end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs a comment. We are assuming that the appropriate position for the hint to begin is the end of the range for the lens, we should document that carefully.
This reverts commit 245049a.
and remove `instance PluginRequestMethod Method_InlayHintResolve` since have not decide how to combine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking generally good! It's also quite non-obvious to me why the failing test is failing, it doesn't look like you've changed anything there. I'll think about it. Maybe @fendor has an idea?
} | ||
|
||
isInlayHintsSupported :: MonadIO m => IdeState -> m Bool | ||
isInlayHintsSupported state = do | ||
Shake.ShakeExtras{lspEnv} <- liftIO $ runAction "" state Shake.getShakeExtras |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems a bit complicated, compare with https://github.com/haskell/haskell-language-server/blob/master/ghcide/src/Development/IDE/Plugin/Completions.hs#L208 (I just searched for "clientCaps"!)
, Just newRange <- [toCurrentRange pm range]] | ||
pure $ InL lens | ||
lensProvider _ state _ CodeLensParams {_textDocument = TextDocumentIdentifier {_uri}} = do | ||
isIHSupported <- liftIO $ isInlayHintsSupported state |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need a note explaining what we're doing here.
lensProvider _ state _ CodeLensParams {_textDocument = TextDocumentIdentifier {_uri}} = do | ||
isIHSupported <- liftIO $ isInlayHintsSupported state | ||
if isIHSupported | ||
then do pure $ InR Null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might be able to return a more useful error here. I think we can maybe use PluginRequestRefused
? Or maybe it's just the most sensible thing to succeed but return nothing.
{ _position = Position {_line = 3, _character = 16} | ||
, _label = InL "( b1 )" | ||
, _kind = Just InlayHintKind_Type | ||
, _textEdits = Just [TextEdit (Range (Position 3 0) (Position 3 16)) "import ExplicitB ( b1 )"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These text edits are arguably overly aggressive. We could just insert the export list where it needs to go, rather than replacing the entire import. That would have the advantage of preserving as much as possible of what the user wrote.
I'm not sure it makes a big difference, but we should think about it and write down why we chose this one. I think in general the expectation is that an inlay hint inserts just the text in the hint...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, append is what I did first, but I did run into some problems, like this code:
import Development.IDE hiding (pluginHandlers,
pluginRules)
If we just append it, it will obviously break the import statement.
So I chose the same behavior as the original code lens & code action, replacing the entire import statement, so that users only need to re-format it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting! How does the inlay hint display in that case? I guess here the "perfect" thing to do would be to append the import list and also delete any "hiding" clauses. Complicated, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, needs a comment, and maybe a test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess here the "perfect" thing to do would be to append the import list and also delete any "hiding" clauses. Complicated, though.
The current approach is to append inlay hints directly at the end.
I don't think it's possible to have inlay hints "cover" a part of your code.
Also, needs a comment, and maybe a test?
Okay, I'll add both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's possible to have inlay hints "cover" a part of your code.
Right, that's definitely true for the hint. I meant that the text edits could only overwrite the hiding clause rather than the entire line.
, _kind = Just InlayHintKind_Type -- for type annotations | ||
, _textEdits = fmap singleton $ toTEdit pm ie | ||
, _tooltip = Nothing | ||
, _paddingLeft = Just True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you figure out what these padding fields do? It has been very unclear to me form the spec!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, so it basically means "show an extra space before the inlay hint. I guess that makes sense.
, codeActionOnlyGoldenTest "ExplicitOnlyThis" 3 0 | ||
, codeActionOnlyResolveGoldenTest "ExplicitOnlyThis" 3 0 | ||
, inlayHintsTest "ExplicitOnlyThis" 3 $ (@=?) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should test that we don't get code actions when we have inlay hints enabled and vice versa.
Which test is failing unexpectedly? |
Yes. The failed started appearing after this commit. This line is where I can find the error raised. I can't get any more information out of this error.
|
I debugged this to the point that replacing |
@jetjinser Change the client capabilities of However, the error message itself is extremely puzzeling. |
Yes, this works on my machine too. Should I apply this modification? I'll add a comment pointing to the discussion here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do think we still need a way to turn these off at the plugin level, like we have for code lenses etc. See https://github.com/haskell/haskell-language-server/blob/master/hls-plugin-api/src/Ide/Types.hs#L262
isInlayHintsSupported :: Applicative f => IdeState -> f Bool | ||
isInlayHintsSupported ideState = do | ||
let clientCaps = Shake.clientCapabilities $ shakeExtras ideState | ||
pure $ case clientCaps of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could do
isJust $ clientCaps ^? textDocument . _Just . inlayHint . _Just
-- otherwise it will be provided as a fallback | ||
isIHSupported <- liftIO $ isInlayHintsSupported state | ||
if isIHSupported | ||
then do pure $ InL [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment about why this is the right thing to return here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm trying to figure out what's the difference between the two, and from this comment (microsoft/language-server-protocol#1200 (comment)), it seems like []
is no different from null
.
But we do need to uniformly choose a way to indicate “no information”. Do you think Right []
is better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that seems reasonable to me.
pure $ InL inlayHints | ||
-- When the client does not support inlay hints, fallback to the code lens, | ||
-- so this is Null | ||
else do pure $ InR Null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we return null here but an empty list of code lenses above? It helps to be consistent!
Also, we should add: if the client doesn't support inlay hints, this method should never get called!
, _kind = Just InlayHintKind_Type -- for type annotations | ||
, _textEdits = fmap singleton $ toTEdit pm ie | ||
, _tooltip = Nothing | ||
, _paddingLeft = Just True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, so it basically means "show an extra space before the inlay hint. I guess that makes sense.
mkLabel :: ImportEdit -> T.Text | ||
mkLabel (ImportEdit{ieResType, ieText}) = | ||
let title ExplicitImport = abbreviateImportTitle . T.dropWhile (/= '(') $ ieText | ||
title RefineImport = "Refine imports to " <> T.intercalate ", " (T.lines ieText) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems odd. Do we have any tests showing this? These perhaps shouldn't be shown as inlay hints? I'm unsure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's possible these need to always be code lenses, since they don't represent implicit content that can be added to the document by accepting it, which is pretty much what inlay hints are supposed to be. Maybe I'm wrong though, not sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I was going to support refine with inlay hints, it may looks like
import qualified RefineA <inlay>from RefineB</inlay> as RA
or
import RefineD <inlay>from RefineE</inlay><inlay> ( e2 )</inlay>
But this is a little tricky for me, I'm trying to locate where the module name position is. In addition, I don't know if I understand it wrong, there may be many original modules? This makes inlay hints longer and jagged.
So I want to ask your opinion.
EDIT:
Another, easier, but equally jag way to do this is to put original module name at the beginning of the import.
This may not be very good... (especially I can't think of what word to use, or not use it -- it definitely can't be from
) I want to just ignore refine in inlay hints?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think it would be pretty sensible to leave refine as a code lens and not an inlay hint. Let's do that for now and maybe open an issue to discuss what the best UX would be for refine imports?
generateInlayHints (Range _ end) ie pm = | ||
InlayHint { _position = end | ||
, _label = InL $ mkLabel ie | ||
, _kind = Just InlayHintKind_Type -- for type annotations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is surely the wrong kind. I think this should just be omitted.
, _label = InL $ mkLabel ie | ||
, _kind = Just InlayHintKind_Type -- for type annotations | ||
, _textEdits = fmap singleton $ toTEdit pm ie | ||
, _tooltip = Nothing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should definitely consider using this! For example, the tooltip for the minimal imports inlay hint could say "This is an import list for exactly the names used by the current module", or something. That way it's a bit more discoverable what it means. It's a nice UX feature!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be as simple as "Make this import explicit"? This doesn't even require resolve, even though the LSP spec recommends that the tooltip be obtained via resolve, but it's simple enough.
edit: it may be different when refining, but this still simple enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think either of those would be fine. I think we can just make it a constant string; the hint itself shows the things that are going to be inserted. So no need for resolve. Resolve is only for things that are expensive to compute.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One last thing then I think this is good to go!
-- otherwise it will be provided as a fallback | ||
if isInlayHintsSupported state | ||
-- `[]` is no different from `null`, we chose to use all `[]` to indicate "no information" | ||
then pure $ InL [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, so I think the only thing remaining here is that we were going to leave the refine import actions as code lenses, so we need to return them in both cases. And never return them for inlay hints.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be checked at lens resolve? If I want to know whether ImportEdit
is explicit or refine, it seems that can't do it in lensProvider
.
If I can't tell whether the lensProvider
should respond at the time, then every import statement actually needs to be resolved even that is not refine. I'm worried this will rely on the expensive resolve.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, that's annoying. I would have thought we could determine what type it is beforehand? Perhaps we need to tweak the rule to include that information?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think now should be okay.
Great stuff, let's merge it 🎉 |
This PR introduces new feature to provide explicit imports via inlay hints.
textEdits
are applied when the editor accepts inlay hints (double click in vscode). The effect of applyingtextEdits
is to complete imports.This behavior is consistent with code lens.(For example, in vscode) When the cursor is hover on the label of inlay hints, the tooltip will be displayed as a popup. tooltip is always "Make this import explicit".
Special logic, when the lsp client supports inlay hints, explicit import will be provided in the inlay hints, otherwise it will fallback to the code lens.