ghcide: type of selected expression via haskell/hoverRange custom method (#709) - #4999
ghcide: type of selected expression via haskell/hoverRange custom method (#709)#4999HCHogan wants to merge 2 commits into
Conversation
…haskell#709) Adds a custom LSP request `haskell/hoverRange`, modelled after rust-analyzer's "Hover Range" extension: given a text document and a range (typically the current selection), it returns a standard `Hover | null` describing the smallest expression that fully contains the range, so clients can show the type of an arbitrary selected expression. Implementation notes: * The range lookup generalises the existing hover machinery: `pointCommand` becomes a special case of the new `rangeCommand` (`selectSmallestContaining` with a non-zero-width span), and `atPoint`/`getAtPoint` gain range-aware variants. Position-based hover behaviour is unchanged. * The HieAST deliberately omits the types of intermediate expression nodes such as applications (see `skipDesugaring` in GHC.Iface.Ext.Ast), so a selection usually lands on a node with no hover information. In that case we recover the type from the typechecked source instead: `exprTypeAtSpan` finds the smallest `LHsExpr GhcTc` containing the span (SYB over `tcg_binds`, pruning subtrees whose spans do not contain the target) and computes its type with `lhsExprType`. This is per-request, proportional to a single expression, and follows the same `tcg_binds` traversal pattern already used by the type lenses and explicit-record-fields plugins. The fallback only applies to files of interest; if GHC ever records these types in the HieAST it can be removed without changing the interface. * A custom method is used because `lsp-types` cannot parse a `Range` in the `position` field of `textDocument/hover`, so rust-analyzer's wire format is not implementable as-is (microsoft/language-server-protocol#377 is still open). Clients should treat `MethodNotFound` as "not supported".
Restructures HoverRangeTests into a table-driven suite covering: * basic: sub-expression selection, misaligned selection snapping to the enclosing expression (with the reported range checked), literals, selections exactly covering an identifier (rich hover), operators, multi-line selections, if-branches (typechecked-source fallback with exact range), string literals, empty range behaving like positional hover, and null responses for selections spanning declarations or outside any expression. * GADTs/DataKinds: partially applied GADT constructors report their instantiated type (including promoted type-level indices), nested constructor applications, and existentials hiding the index.
|
Hi, thank you for PR! I think it would have been a good idea to discuss the implementation approach before working on the implementation itself. See for example this PR https://gitlab.haskell.org/ghc/ghc/-/merge_requests/3344 and https://gitlab.haskell.org/ghc/ghc/-/merge_requests/6078 which aims to bring this information into .hie files. The main advantage would be to have a single source of truth for all IDE related information.
This is quite a big disadvantage, and it might destroy our capability to provide hover information before the session finished initialising.
this trade off seems weird, the editor will not request the type of a range for a non-FOI file, right? |
Implements #709 (type information for arbitrary selected expressions), one of the two features called out in #3715.
What
A custom LSP request
haskell/hoverRange, with the same semantics as rust-analyzer's "Hover Range" extension:The response is a standard
Hover | nullfor the smallest expression that fully contains the range (the returnedHover.rangeis that expression's span, so clients can highlight it). An empty range behaves exactly liketextDocument/hover; selections that align with a single identifier get the existing rich hover (docs, definition site, evidence).A custom method rather than rust-analyzer's wire format (a
RangeinHoverParams.position), becauselsp-typesparses hover params strictly — aRangethere is rejected before reaching HLS. Upstream LSP support (microsoft/language-server-protocol#377) is still open. Clients should treatMethodNotFoundas "not supported"; advertisingcapabilities.experimentallike rust-analyzer does would need a small hook inlspfirst (possible follow-up).How
Two layers:
HieAST:
pointCommandis generalised torangeCommand—selectSmallestContainingwith a non-zero-width span. Position hover is the degenerate case; its behaviour is unchanged (all existing hover tests pass).Typechecked-source fallback: the HieAST deliberately omits types of intermediate expression nodes (
skipDesugaringinGHC.Iface.Ext.AstskipsHsApp/OpApp/if/case/...), e.g. forcombined = negate 3 + 7thenegate 3node hastypes=[]. So when the selected node yields nothing,exprTypeAtSpanfinds the smallestLHsExpr GhcTccontaining the span (SYB overtcg_binds, pruning subtrees whose spans don't contain the target) and computes its type withlhsExprType. Sametcg_bindstraversal pattern as TypeLenses / explicit-record-fields; cost is per-request and proportional to one expression.Trade-offs (stated upfront)
TypeCheckresult, i.e. works for files of interest only; forHieFromDiskyou get whatever the AST has. If GHC ever records these types in the HieAST, the fallback can be deleted with no interface change.Q Exp). Splice interiors report the expansion's type.Testing
ghcide-test/exe/HoverRangeTests.hs: exact selection, multi-line selection, misaligned selection, empty range behaving like hover, null for spans without an enclosing expression / spanning declarations.docs/features.md.