feat: add `MessageData.withExprHover`

This PR adds `MessageData.withExprHover`, for creating messages that show information about an expression when hovered over. A `withExprHoverM` variant captures the current local context.
diff --git a/src/Lean/Elab/Tactic/Doc.lean b/src/Lean/Elab/Tactic/Doc.lean
index ad5b6c4..7cf39d3 100644
--- a/src/Lean/Elab/Tactic/Doc.lean
+++ b/src/Lean/Elab/Tactic/Doc.lean
@@ -90,17 +90,7 @@
     env.constants.find?' n |>.map (·.levelParams.map Level.param) |>.getD []
 
   let tok := ((← customTacticName n) <|> firsts.get? n).map Std.Format.text |>.getD (format n)
-  pure <| .ofFormatWithInfos {
-    fmt := "`" ++ .tag 0 tok ++ "`",
-    infos :=
-      .ofList [(0, .ofTermInfo {
-        lctx := .empty,
-        expr := .const n params,
-        stx := .ident .none (toString n).toRawSubstring n [.decl n []],
-        elaborator := `Delab,
-        expectedType? := none
-      })] _
-  }
+  pure m!"`{.withExprHover tok (.const n params) {}}`"
 
 /--
 Displays all available tactic tags, with documentation.
diff --git a/src/Lean/Message.lean b/src/Lean/Message.lean
index d5552f4..db09902 100644
--- a/src/Lean/Message.lean
+++ b/src/Lean/Message.lean
@@ -275,6 +275,47 @@
       return Dynamic.mk msg)
     (fun _ => false)
 
+/--
+Creates message data wrapping `fmt` that gives information about `expr` when hovered over.
+
+Options:
+- `location?` overrides what "go to definition" does
+- `docString?` allows overriding the docstring for the expression
+- `mkDocString?` allows generating a dynamic docstring; if this is provided, `docString?` is ignored
+- `explicit` makes the hover pretty print the head application in explicit mode;
+  the default value is `false` (note: delaboration expression hovers normally set this to `true`,
+  since their purpose is to get more information about a given expression)
+-/
+def withExprHover (fmt : Format) (expr : Expr) (lctx : LocalContext)
+    (location? : Option DeclarationLocation := none)
+    (docString? : Option String := none)
+    (mkDocString? : Option (PPContext → IO String) := none)
+    (explicit : Bool := false) : MessageData :=
+  .ofFormatWithInfos {
+      fmt := .tag 0 fmt
+      infos :=
+        .ofList [(0, Elab.Info.ofDelabTermInfo {
+          expr, lctx, location?, explicit,
+          stx := .missing, -- unused for delaborator hovers
+          expectedType? := none, -- unused for delaborator hovers
+          elaborator := `Delab.withExprHover,
+          mkDocString? := mkDocString? <|> docString?.map (fun _ => pure ·)
+        })]
+  }
+
+/--
+Calls `withExprHover`, but uses the current local context if one is not given.
+See `withExprHover` for a description of each parameter.
+-/
+def withExprHoverM {m} [Monad m] [MonadLCtx m]
+    (fmt : Format) (expr : Expr) (lctx? : Option LocalContext := none)
+    (location? : Option DeclarationLocation := none)
+    (docString? : Option String := none)
+    (mkDocString? : Option (PPContext → IO String) := none)
+    (explicit : Bool := false) : m MessageData := do
+  let lctx ← lctx?.getDM getLCtx
+  return withExprHover fmt expr lctx location? docString? mkDocString? explicit
+
 partial def hasSyntheticSorry (msg : MessageData) : Bool :=
   visit none msg
 where