Skip to content

Commit 3d7cb3d

Browse files
committed
feat: "try this" messages with support for interactivity
1 parent 9fc18b8 commit 3d7cb3d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+392
-317
lines changed

src/Lean/Meta/Hint.lean

Lines changed: 121 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,61 @@ namespace Lean.Meta.Hint
2222

2323
open Elab Tactic PrettyPrinter TryThis
2424

25+
def textInsertionWidget : Widget.Module where
26+
javascript := "
27+
import * as React from 'react';
28+
import { EditorContext, EnvPosContext } from '@leanprover/infoview';
29+
30+
const e = React.createElement;
31+
export default function ({ range, suggestion, acceptSuggestionProps }) {
32+
const pos = React.useContext(EnvPosContext)
33+
const editorConnection = React.useContext(EditorContext)
34+
function onClick() {
35+
editorConnection.api.applyEdit({
36+
changes: { [pos.uri]: [{ range, newText: suggestion }] }
37+
})
38+
}
39+
40+
if (acceptSuggestionProps.kind === 'text') {
41+
return e('span', {
42+
onClick,
43+
title: acceptSuggestionProps.hoverText,
44+
className: 'link pointer dim font-code',
45+
style: { color: 'var(--vscode-textLink-foreground)' }
46+
},
47+
acceptSuggestionProps.linkText)
48+
} else if (acceptSuggestionProps.kind === 'icon') {
49+
if (acceptSuggestionProps.gaps) {
50+
const icon = e('span', {
51+
className: `codicon codicon-${acceptSuggestionProps.codiconName}`,
52+
style: {
53+
verticalAlign: 'sub',
54+
fontSize: 'var(--vscode-editor-font-size)'
55+
}
56+
})
57+
return e('span', {
58+
onClick,
59+
title: acceptSuggestionProps.hoverText,
60+
className: `link pointer dim font-code`,
61+
style: { color: 'var(--vscode-textLink-foreground)' }
62+
}, ' ', icon, ' ')
63+
} else {
64+
return e('span', {
65+
onClick,
66+
title: acceptSuggestionProps.hoverText,
67+
className: `link pointer dim font-code codicon codicon-${acceptSuggestionProps.codiconName}`,
68+
style: {
69+
color: 'var(--vscode-textLink-foreground)',
70+
verticalAlign: 'sub',
71+
fontSize: 'var(--vscode-editor-font-size)'
72+
}
73+
})
74+
}
75+
76+
}
77+
throw new Error('Unexpected `acceptSuggestionProps` kind: ' + acceptSuggestionProps.kind)
78+
}"
79+
2580
/--
2681
A widget for rendering code action suggestions in error messages. Generally, this widget should not
2782
be used directly; instead, use `MessageData.hint`. Note that this widget is intended only for use
@@ -333,52 +388,73 @@ def mkSuggestionsMessage (suggestions : Array Suggestion) (ref : Syntax)
333388
(codeActionPrefix? : Option String) (forceList : Bool) : CoreM MessageData := do
334389
let mut msg := m!""
335390
for suggestion in suggestions do
336-
if let some range := (suggestion.span?.getD ref).getRange? then
337-
let { info, suggestions := suggestionArr, range := lspRange } ←
338-
processSuggestions ref range #[suggestion.toTryThisSuggestion] codeActionPrefix?
339-
pushInfoLeaf info
340-
-- The following access is safe because
341-
-- `suggestionsArr = #[suggestion.toTryThisSuggestion].map ...` (see `processSuggestions`)
342-
let suggestionText := suggestionArr[0]!.2.1
343-
let map ← getFileMap
344-
let rangeContents := map.source.extract range.start range.stop
345-
let edits ← do
346-
if let some msgData := suggestion.messageData? then
347-
pure #[(.insert, toString <| ← msgData.format)]
348-
else
349-
pure <| readableDiff rangeContents suggestionText suggestion.diffGranularity
350-
let mut edits := edits
351-
if let some previewRange := suggestion.previewSpan? >>= Syntax.getRange? then
352-
if previewRange.includes range then
353-
let map ← getFileMap
354-
if previewRange.start < range.start then
355-
edits := #[(.skip, (map.source.extract previewRange.start range.start))] ++ edits
356-
if range.stop < previewRange.stop then
357-
edits := edits.push (.skip, (map.source.extract range.stop previewRange.stop))
358-
let diffJson := mkDiffJson edits
359-
let json := json% {
360-
diff: $diffJson,
361-
suggestion: $suggestionText,
362-
range: $lspRange
363-
}
364-
let preInfo := suggestion.preInfo?.getD ""
365-
let postInfo := suggestion.postInfo?.getD ""
366-
let diffString :=
367-
if suggestion.diffGranularity matches .none then
368-
edits.foldl (· ++ ·.2) ""
369-
else
370-
mkDiffString edits
371-
let widget := MessageData.ofWidget {
372-
id := ``tryThisDiffWidget
373-
javascriptHash := tryThisDiffWidget.javascriptHash
374-
props := return json
375-
} diffString
376-
let widgetMsg := m!"{preInfo}{widget}{postInfo}"
377-
let suggestionMsg := if suggestions.size == 1 && !forceList then
378-
m!"\n{widgetMsg}"
391+
let some range := suggestion.span?.getD ref |>.getRange?
392+
| continue
393+
let { info, suggestions := suggestionArr, range := lspRange } ←
394+
processSuggestions ref range #[suggestion.toTryThisSuggestion] codeActionPrefix?
395+
pushInfoLeaf info
396+
-- The following access is safe because
397+
-- `suggestionsArr = #[suggestion.toTryThisSuggestion].map ...` (see `processSuggestions`)
398+
let suggestionText := suggestionArr[0]!
399+
let map ← getFileMap
400+
let rangeContents := map.source.extract range.start range.stop
401+
let edits ← do
402+
if let some msgData := suggestion.messageData? then
403+
pure #[(.insert, toString <| ← msgData.format)]
404+
else
405+
pure <| readableDiff rangeContents suggestionText suggestion.diffGranularity
406+
let mut edits := edits
407+
if let some previewRange := suggestion.previewSpan? >>= Syntax.getRange? then
408+
if previewRange.includes range then
409+
let map ← getFileMap
410+
if previewRange.start < range.start then
411+
edits := #[(.skip, (map.source.extract previewRange.start range.start))] ++ edits
412+
if range.stop < previewRange.stop then
413+
edits := edits.push (.skip, (map.source.extract range.stop previewRange.stop))
414+
let preInfo := suggestion.preInfo?.getD ""
415+
let postInfo := suggestion.postInfo?.getD ""
416+
let isDiffSuggestion :=
417+
! (suggestion.diffGranularity matches .none) && suggestion.messageData?.isNone
418+
|| suggestion.previewSpan?.isSome
419+
let suggestionMsg :=
420+
if ! isDiffSuggestion then
421+
let applyButton := MessageData.ofWidget {
422+
id := ``textInsertionWidget
423+
javascriptHash := textInsertionWidget.javascriptHash
424+
props := return json% {
425+
range: $lspRange,
426+
suggestion: $suggestionText,
427+
acceptSuggestionProps: {
428+
kind: "text",
429+
hoverText: "Apply suggestion",
430+
linkText: "[apply]"
431+
}
432+
}
433+
} "[apply]"
434+
m!"\n{applyButton} {preInfo}{toMessageData suggestion}{postInfo}"
379435
else
380-
m!"\n" ++ MessageData.nest 2 m!"• {widgetMsg}"
381-
msg := msg ++ MessageData.nestD suggestionMsg
436+
let diffJson := mkDiffJson edits
437+
let json := json% {
438+
diff: $diffJson,
439+
suggestion: $suggestionText,
440+
range: $lspRange
441+
}
442+
let diffString :=
443+
if suggestion.diffGranularity matches .none then
444+
edits.foldl (· ++ ·.2) ""
445+
else
446+
mkDiffString edits
447+
let diffWidget := MessageData.ofWidget {
448+
id := ``tryThisDiffWidget
449+
javascriptHash := tryThisDiffWidget.javascriptHash
450+
props := return json
451+
} diffString
452+
let msg := m!"{preInfo}{diffWidget}{postInfo}"
453+
if suggestions.size == 1 && !forceList then
454+
m!"\n{msg}"
455+
else
456+
m!"\n" ++ MessageData.nest 2 m!"• {msg}"
457+
msg := msg ++ MessageData.nestD suggestionMsg
382458
return msg
383459

384460
/--

src/Lean/Meta/Tactic/TryThis.lean

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ open Lean Elab Tactic PrettyPrinter Meta Server RequestM
3434

3535
-- Because we can't use `builtin_widget_module` in `Lean.Meta.Hint`, we add the attribute here
3636
attribute [builtin_widget_module] Hint.tryThisDiffWidget
37+
attribute [builtin_widget_module] Hint.textInsertionWidget
3738

3839
/-! # Code action -/
3940

@@ -72,7 +73,7 @@ def delabToRefinableSyntax (e : Expr) : MetaM Term :=
7273

7374
/-- Delaborate `e` into a suggestion suitable for use by `refine`. -/
7475
def delabToRefinableSuggestion (e : Expr) : MetaM Suggestion :=
75-
return { suggestion := ← delabToRefinableSyntax e }
76+
return { suggestion := ← delabToRefinableSyntax e, messageData? := e }
7677

7778
/-- Add a "try this" suggestion. This has two effects:
7879
@@ -187,19 +188,19 @@ Remark: We cannot determine if a tactic requires `expose_names` merely by inspec
187188
(shadowed variables are delaborated without daggers) nor the underlying `Expr` used to produce it
188189
(some inaccessible names may be implicit arguments that do not appear in the delaborated syntax).
189190
-/
190-
private def mkValidatedTactic (tac : TSyntax `tactic)
191+
private def mkValidatedTactic (tac : TSyntax `tactic) (msg : MessageData)
191192
(initialState : Tactic.SavedState) (expectedType? : Option Expr := none) :
192-
TacticM (Option (TSyntax `tactic)) := do
193+
TacticM (Option (TSyntax `tactic × MessageData)) := do
193194
try
194195
evalTacticWithState initialState tac expectedType?
195-
return some tac
196+
return some (tac, msg)
196197
catch _ =>
197198
-- Note: we must use `(expose_names; _)` and not `· expose_names; _` to avoid generating
198199
-- spurious tactic-abort exceptions, since these tactics may not close the goal
199200
let tac ← `(tactic| (expose_names; $tac))
200201
try
201202
evalTacticWithState initialState tac expectedType?
202-
return some tac
203+
return some (tac, m!"(expose_names; {msg})")
203204
catch _ =>
204205
return none
205206

@@ -231,7 +232,7 @@ private def addExactSuggestionCore (addSubgoalsMsg : Bool) (checkState? : Option
231232
let hasMVars := !mvars.isEmpty
232233
let (suggestion, messageData) ← mkExactSuggestionSyntax e (useRefine := hasMVars)
233234
let some checkState := checkState? | return .inl suggestion
234-
let some suggestion ← mkValidatedTactic suggestion checkState
235+
let some (suggestion, messageData) ← mkValidatedTactic suggestion messageData checkState
235236
| let messageData := m!"(expose_names; {messageData})"
236237
return .inr <| mkFailedToMakeTacticMsg m!"a {if hasMVars then "partial " else ""}proof" messageData
237238
let postInfo? ← if !addSubgoalsMsg || mvars.isEmpty then pure none else
@@ -241,7 +242,7 @@ private def addExactSuggestionCore (addSubgoalsMsg : Bool) (checkState? : Option
241242
let e ← withExposedNames <| PrettyPrinter.ppExpr (← instantiateMVars (← g.getType))
242243
str := str ++ Format.pretty ("\n-- ⊢ " ++ e)
243244
pure str
244-
return .inl { suggestion := suggestion, postInfo? }
245+
return .inl { suggestion := suggestion, postInfo?, messageData? := messageData }
245246

246247
/-- Add an `exact e` or `refine e` suggestion.
247248
@@ -374,11 +375,12 @@ def addHaveSuggestion (ref : Syntax) (h? : Option Name) (t? : Option Expr) (e :
374375
pure (← `(tactic| let $(mkIdent h):ident := $estx), m!"let {h} := {e}")
375376
pure (tac, ← addMessageContext msg)
376377
if let some checkState := checkState? then
377-
let some tac' ← mkValidatedTactic tac checkState
378+
let some (tac', msg') ← mkValidatedTactic tac msg checkState
378379
| logInfo <| mkFailedToMakeTacticMsg "a proof" msg
379380
return
380381
tac := tac'
381-
addSuggestion ref (s := { suggestion := tac }) origSpan?
382+
msg := msg'
383+
addSuggestion ref (s := { suggestion := tac, messageData? := msg }) origSpan?
382384

383385
open Lean.Parser.Tactic
384386
open Lean.Syntax
@@ -400,7 +402,7 @@ def addRewriteSuggestion (ref : Syntax) (rules : List (Expr × Bool))
400402
(type? : LOption Expr := .undef) (loc? : Option Expr := none)
401403
(origSpan? : Option Syntax := none) (checkState? : Option Tactic.SavedState := none) :
402404
TacticM Unit := do
403-
let mut (tac, tacMsg, extraMsg, extraStr) ← withExposedNames do
405+
let mut (tac, tacMsg, extraMsg) ← withExposedNames do
404406
let rulesStx := TSepArray.ofElems <| ← rules.toArray.mapM fun ⟨e, symm⟩ => do
405407
let t ← delabToRefinableSyntax e
406408
if symm then `(rwRule| ← $t:term) else `(rwRule| $t:term)
@@ -423,22 +425,23 @@ def addRewriteSuggestion (ref : Syntax) (rules : List (Expr × Bool))
423425
else
424426
m!"rw {rulesMsg}"
425427

426-
let (extraMsg, extraStr)
428+
let extraMsg ←
427429
match type? with
428430
| .some type =>
429-
pure (← addMessageContext m!"\n-- {type}", s!"\n-- {← PrettyPrinter.ppExpr type}")
430-
| .none => pure (m!"\n-- no goals", "\n-- no goals")
431-
| .undef => pure (m!"", "")
432-
return (tac, tacMsg, extraMsg, extraStr)
431+
pure <| ← addMessageContext m!"\n-- {type}"
432+
| .none => pure m!"\n-- no goals"
433+
| .undef => pure m!""
434+
return (tac, tacMsg, extraMsg)
433435

434436
if let some checkState := checkState? then
435437
let type? := match type? with
436438
| .some type => some type
437439
| _ => none
438-
let some tac' ← mkValidatedTactic tac checkState type?
440+
let some (tac', tacMsg') ← mkValidatedTactic tac tacMsg checkState type?
439441
| tacMsg := m!"(expose_names; {tacMsg})"
440442
logInfo <| mkFailedToMakeTacticMsg "an applicable rewrite lemma" (tacMsg ++ extraMsg)
441443
return
442444
tac := tac'
443-
addSuggestion ref (s := { suggestion := tac, postInfo? := extraStr })
445+
tacMsg := tacMsg'
446+
addSuggestion ref (s := { suggestion := tac, messageData? := tacMsg ++ extraMsg })
444447
origSpan?

src/Lean/Meta/TryThis.lean

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ attribute [deprecated "The `style?` property is not used anymore." (since := "20
175175

176176
/- Use `toMessageData` of the suggestion text. -/
177177
instance : ToMessageData Suggestion where
178-
toMessageData s := toMessageData s.suggestion
178+
toMessageData s := s.messageData?.getD (toMessageData s.suggestion)
179179

180180
instance : Coe SuggestionText Suggestion where
181181
coe t := { suggestion := t }
@@ -233,13 +233,10 @@ code action title if `toCodeActionTitle?` is provided.
233233
234234
If `w := none`, then `w := getInputWidth (← getOptions)` is used.
235235
-/
236-
def Suggestion.toJsonAndInfoM (s : Suggestion) (w : Option Nat := none) (indent column : Nat := 0) :
237-
CoreM (Json × String × Option String) := do
236+
def Suggestion.process (s : Suggestion) (w : Option Nat := none) (indent column : Nat := 0) :
237+
CoreM (String × Option String) := do
238238
let text ← s.suggestion.prettyExtra w indent column
239-
let mut json := [("suggestion", (text : Json))]
240-
if let some preInfo := s.preInfo? then json := ("preInfo", preInfo) :: json
241-
if let some postInfo := s.postInfo? then json := ("postInfo", postInfo) :: json
242-
return (Json.mkObj json, text, s.toCodeActionTitle?.map (· text))
239+
return (text, s.toCodeActionTitle?.map (· text))
243240

244241
/--
245242
Represents processed data for a collection of suggestions that can be passed to a widget and pushed
@@ -253,7 +250,7 @@ It contains the following data:
253250
* `range`: the range at which the suggestion is to be applied.
254251
-/
255252
structure ProcessedSuggestions where
256-
suggestions : Array (Json × String × Option String)
253+
suggestions : Array String
257254
info : Elab.Info
258255
range : Lsp.Range
259256

@@ -268,12 +265,11 @@ def processSuggestions (ref : Syntax) (range : String.Range) (suggestions : Arra
268265
-- replacing `tac` in `by tac`, because the next line will only be 2 space indented
269266
-- (less than `tac` which starts at column 3)
270267
let (indent, column) := getIndentAndColumn map range
271-
let suggestions ← suggestions.mapM (·.toJsonAndInfoM (indent := indent) (column := column))
272-
let suggestionTexts := suggestions.map (·.2)
268+
let suggestions ← suggestions.mapM (·.process (indent := indent) (column := column))
273269
let ref := Syntax.ofRange <| ref.getRange?.getD range
274270
let range := map.utf8RangeToLspRange range
275271
let info := .ofCustomInfo {
276272
stx := ref
277-
value := Dynamic.mk { range, suggestionTexts, codeActionPrefix? : TryThisInfo }
273+
value := Dynamic.mk { range, suggestionTexts := suggestions, codeActionPrefix? : TryThisInfo }
278274
}
279-
return { info, suggestions, range }
275+
return { info, suggestions := suggestions.map (·.1), range }

tests/lean/4591.lean.expected.out

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
Try this:
2-
simp only [Nat.foo]
2+
[apply] simp only [Nat.foo]

0 commit comments

Comments
 (0)