Skip to content

ghcide: type of selected expression via haskell/hoverRange custom method (#709) - #4999

Open
HCHogan wants to merge 2 commits into
haskell:masterfrom
HCHogan:hover-range
Open

ghcide: type of selected expression via haskell/hoverRange custom method (#709)#4999
HCHogan wants to merge 2 commits into
haskell:masterfrom
HCHogan:hover-range

Conversation

@HCHogan

@HCHogan HCHogan commented Jul 3, 2026

Copy link
Copy Markdown

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:

method: "haskell/hoverRange"
params: { textDocument: TextDocumentIdentifier, range: Range }  // range = current selection

The response is a standard Hover | null for the smallest expression that fully contains the range (the returned Hover.range is that expression's span, so clients can highlight it). An empty range behaves exactly like textDocument/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 Range in HoverParams.position), because lsp-types parses hover params strictly — a Range there is rejected before reaching HLS. Upstream LSP support (microsoft/language-server-protocol#377) is still open. Clients should treat MethodNotFound as "not supported"; advertising capabilities.experimental like rust-analyzer does would need a small hook in lsp first (possible follow-up).

How

Two layers:

  1. HieAST: pointCommand is generalised to rangeCommandselectSmallestContaining with a non-zero-width span. Position hover is the degenerate case; its behaviour is unchanged (all existing hover tests pass).

  2. Typechecked-source fallback: the HieAST deliberately omits types of intermediate expression nodes (skipDesugaring in GHC.Iface.Ext.Ast skips HsApp/OpApp/if/case/...), e.g. for combined = negate 3 + 7 the negate 3 node has types=[]. So when the selected node yields nothing, exprTypeAtSpan finds the smallest LHsExpr GhcTc containing the span (SYB over tcg_binds, pruning subtrees whose spans don't contain the target) and computes its type with lhsExprType. Same tcg_binds traversal pattern as TypeLenses / explicit-record-fields; cost is per-request and proportional to one expression.

Trade-offs (stated upfront)

  • The fallback needs a TypeCheck result, i.e. works for files of interest only; for HieFromDisk you get whatever the AST has. If GHC ever records these types in the HieAST, the fallback can be deleted with no interface change.
  • Custom method ⇒ per-client adoption (I'm happy to do the vscode-haskell side; tested from neovim with ~30 lines of Lua).
  • Inside TH quotes there are no typechecked expressions; the nearest typed enclosing expression is the bracket itself (Q Exp). Splice interiors report the expansion's type.

Testing

  • New 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.
  • All 82 existing hover-related ghcide tests pass unchanged.
  • Docs for client implementors added to docs/features.md.

…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.
@HCHogan
HCHogan marked this pull request as ready for review July 4, 2026 05:58
@HCHogan
HCHogan requested review from michaelpj and wz1000 as code owners July 4, 2026 05:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant