Skip to content

Commit 1bacc4e

Browse files
feat: Select and insert widgets (#7260)
Builds a framework to define widgets allowing users to select some sub-expressions of the main goal to generate a tactic call. Four examples using this framework are included, generating calls to tactics `conv`, `congrm` and `gcongr` and generating new calc steps. Also includes a `calc` tactic code action to start a calc proof. Co-authored-by: Wojciech Nawrocki <wjnawrocki@protonmail.com>
1 parent 2b8dea4 commit 1bacc4e

File tree

11 files changed

+567
-13
lines changed

11 files changed

+567
-13
lines changed

Mathlib.lean

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3229,7 +3229,13 @@ import Mathlib.Tactic.UnsetOption
32293229
import Mathlib.Tactic.Use
32303230
import Mathlib.Tactic.Variable
32313231
import Mathlib.Tactic.WLOG
3232+
import Mathlib.Tactic.Widget.Calc
32323233
import Mathlib.Tactic.Widget.CommDiag
3234+
import Mathlib.Tactic.Widget.Congrm
3235+
import Mathlib.Tactic.Widget.Conv
3236+
import Mathlib.Tactic.Widget.Gcongr
3237+
import Mathlib.Tactic.Widget.SelectInsertParamsClass
3238+
import Mathlib.Tactic.Widget.SelectPanelUtils
32333239
import Mathlib.Tactic.Zify
32343240
import Mathlib.Testing.SlimCheck.Functions
32353241
import Mathlib.Testing.SlimCheck.Gen

Mathlib/AlgebraicGeometry/Morphisms/QuasiSeparated.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ theorem exists_eq_pow_mul_of_is_compact_of_quasi_separated_space_aux (X : Scheme
372372
exact e
373373
#align algebraic_geometry.exists_eq_pow_mul_of_is_compact_of_quasi_separated_space_aux AlgebraicGeometry.exists_eq_pow_mul_of_is_compact_of_quasi_separated_space_aux
374374

375-
set_option maxHeartbeats 700000 in
375+
set_option maxHeartbeats 1000000 in
376376
theorem exists_eq_pow_mul_of_isCompact_of_isQuasiSeparated (X : Scheme.{u}) (U : Opens X.carrier)
377377
(hU : IsCompact U.1) (hU' : IsQuasiSeparated U.1) (f : X.presheaf.obj (op U))
378378
(x : X.presheaf.obj (op <| X.basicOpen f)) :

Mathlib/Tactic.lean

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,5 +168,11 @@ import Mathlib.Tactic.UnsetOption
168168
import Mathlib.Tactic.Use
169169
import Mathlib.Tactic.Variable
170170
import Mathlib.Tactic.WLOG
171+
import Mathlib.Tactic.Widget.Calc
171172
import Mathlib.Tactic.Widget.CommDiag
173+
import Mathlib.Tactic.Widget.Congrm
174+
import Mathlib.Tactic.Widget.Conv
175+
import Mathlib.Tactic.Widget.Gcongr
176+
import Mathlib.Tactic.Widget.SelectInsertParamsClass
177+
import Mathlib.Tactic.Widget.SelectPanelUtils
172178
import Mathlib.Tactic.Zify

Mathlib/Tactic/Widget/Calc.lean

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/-
2+
Copyright (c) 2023 Patrick Massot. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Authors: Patrick Massot
5+
-/
6+
7+
import Std.CodeAction
8+
9+
import Mathlib.Data.String.Defs
10+
import Mathlib.Tactic.Widget.SelectPanelUtils
11+
12+
/-! # Calc widget
13+
14+
This file redefines the `calc` tactic so that it displays a widget panel allowing to create
15+
new calc steps with holes specified by selected sub-expressions in the goal.
16+
-/
17+
18+
section code_action
19+
open Std CodeAction
20+
open Lean Server RequestM
21+
22+
/-- Code action to create a `calc` tactic from the current goal. -/
23+
@[tactic_code_action calcTactic]
24+
def createCalc : TacticCodeAction := fun params _snap ctx _stack node => do
25+
let .node (.ofTacticInfo info) _ := node | return #[]
26+
if info.goalsBefore.isEmpty then return #[]
27+
let eager := {
28+
title := s!"Generate a calc block."
29+
kind? := "quickfix"
30+
}
31+
let doc ← readDoc
32+
return #[{
33+
eager
34+
lazy? := some do
35+
let tacPos := doc.meta.text.utf8PosToLspPos info.stx.getPos?.get!
36+
let endPos := doc.meta.text.utf8PosToLspPos info.stx.getTailPos?.get!
37+
let goal := info.goalsBefore[0]!
38+
let goalFmt ← ctx.runMetaM {} <| goal.withContext do Meta.ppExpr (← goal.getType)
39+
return { eager with
40+
edit? := some <|.ofTextEdit params.textDocument.uri
41+
{ range := ⟨tacPos, endPos⟩, newText := s!"calc {goalFmt} := by sorry" }
42+
}
43+
}]
44+
end code_action
45+
46+
open ProofWidgets
47+
open Lean Meta
48+
49+
open Lean Server in
50+
51+
/-- Parameters for the calc widget. -/
52+
structure CalcParams extends SelectInsertParams where
53+
/-- Is this the first calc step? -/
54+
isFirst : Bool
55+
/-- indentation level of the calc block. -/
56+
indent : Nat
57+
deriving SelectInsertParamsClass, RpcEncodable
58+
59+
open Lean Meta
60+
61+
/-- Return the link text and inserted text above and below of the calc widget. -/
62+
def suggestSteps (pos : Array Lean.SubExpr.GoalsLocation) (goalType : Expr) (params : CalcParams) :
63+
MetaM (String × String × Option (String.Pos × String.Pos)) := do
64+
let subexprPos := getGoalLocations pos
65+
let some (rel, lhs, rhs) ← Lean.Elab.Term.getCalcRelation? goalType |
66+
throwError "invalid 'calc' step, relation expected{indentExpr goalType}"
67+
let relApp := mkApp2 rel
68+
(← mkFreshExprMVar none)
69+
(← mkFreshExprMVar none)
70+
let some relStr := (← Meta.ppExpr relApp) |> toString |>.splitOn |>.get? 1
71+
| throwError "could not find relation symbol in {relApp}"
72+
let isSelectedLeft := subexprPos.any (fun L ↦ #[0, 1].isPrefixOf L.toArray)
73+
let isSelectedRight := subexprPos.any (fun L ↦ #[1].isPrefixOf L.toArray)
74+
75+
let mut goalType := goalType
76+
for pos in subexprPos do
77+
goalType ← insertMetaVar goalType pos
78+
let some (_, newLhs, newRhs) ← Lean.Elab.Term.getCalcRelation? goalType | unreachable!
79+
80+
let lhsStr := (toString <| ← Meta.ppExpr lhs).renameMetaVar
81+
let newLhsStr := (toString <| ← Meta.ppExpr newLhs).renameMetaVar
82+
let rhsStr := (toString <| ← Meta.ppExpr rhs).renameMetaVar
83+
let newRhsStr := (toString <| ← Meta.ppExpr newRhs).renameMetaVar
84+
85+
let spc := String.replicate params.indent ' '
86+
let insertedCode := match isSelectedLeft, isSelectedRight with
87+
| true, true =>
88+
if params.isFirst then
89+
s!"{lhsStr} {relStr} {newLhsStr} := by sorry\n{spc}_ {relStr} {newRhsStr} := by sorry\n" ++
90+
s!"{spc}_ {relStr} {rhsStr} := by sorry"
91+
else
92+
s!"_ {relStr} {newLhsStr} := by sorry\n{spc}_ {relStr} {newRhsStr} := by sorry\n" ++
93+
s!"{spc}_ {relStr} {rhsStr} := by sorry"
94+
| false, true =>
95+
if params.isFirst then
96+
s!"{lhsStr} {relStr} {newRhsStr} := by sorry\n{spc}_ {relStr} {rhsStr} := by sorry"
97+
else
98+
s!"_ {relStr} {newRhsStr} := by sorry\n{spc}_ {relStr} {rhsStr} := by sorry"
99+
| true, false =>
100+
if params.isFirst then
101+
s!"{lhsStr} {relStr} {newLhsStr} := by sorry\n{spc}_ {relStr} {rhsStr} := by sorry"
102+
else
103+
s!"_ {relStr} {newLhsStr} := by sorry\n{spc}_ {relStr} {rhsStr} := by sorry"
104+
| false, false => "This should not happen"
105+
106+
let stepInfo := match isSelectedLeft, isSelectedRight with
107+
| true, true => "Create two new steps"
108+
| true, false | false, true => "Create a new step"
109+
| false, false => "This should not happen"
110+
let pos : String.Pos := insertedCode.find (fun c => c == '?')
111+
return (stepInfo, insertedCode, some (pos, ⟨pos.byteIdx + 2⟩) )
112+
113+
/-- Rpc function for the calc widget. -/
114+
@[server_rpc_method]
115+
def CalcPanel.rpc := mkSelectionPanelRPC suggestSteps
116+
"Please select subterms."
117+
"Calc 🔍"
118+
119+
/-- The calc widget. -/
120+
@[widget_module]
121+
def CalcPanel : Component CalcParams :=
122+
mk_rpc_widget% CalcPanel.rpc
123+
124+
namespace Lean.Elab.Tactic
125+
open Meta
126+
127+
/-- Elaborator for the `calc` tactic mode variant with widgets. -/
128+
elab_rules : tactic
129+
| `(tactic|calc%$calcstx $stx) => do
130+
let steps : TSyntax ``calcSteps := ⟨stx⟩
131+
let some calcRange := (← getFileMap).rangeOfStx? calcstx | unreachable!
132+
let indent := calcRange.start.character
133+
let mut isFirst := true
134+
for step in ← Lean.Elab.Term.getCalcSteps steps do
135+
let some replaceRange := (← getFileMap).rangeOfStx? step | unreachable!
136+
let `(calcStep| $(_) := $proofTerm) := step | unreachable!
137+
let json := open scoped ProofWidgets.Json in json% {"replaceRange": $(replaceRange),
138+
"isFirst": $(isFirst),
139+
"indent": $(indent)}
140+
ProofWidgets.savePanelWidgetInfo proofTerm `CalcPanel (pure json)
141+
isFirst := false
142+
evalCalc (← `(tactic|calc%$calcstx $stx))

Mathlib/Tactic/Widget/Congrm.lean

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/-
2+
Copyright (c) 2023 Patrick Massot. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Authors: Patrick Massot
5+
-/
6+
7+
import Mathlib.Tactic.Widget.SelectPanelUtils
8+
import Mathlib.Tactic.Congrm
9+
10+
11+
/-! # Congrm widget
12+
13+
This file defines a `congrm?` tactic that displays a widget panel allowing to generate
14+
a `congrm` call with holes specified by selecting subexpressions in the goal.
15+
-/
16+
17+
open Lean Meta Server ProofWidgets
18+
19+
20+
/-! # Gcongr widget -/
21+
22+
/-- Return the link text and inserted text above and below of the congrm widget. -/
23+
@[nolint unusedArguments]
24+
def makeCongrmString (pos : Array Lean.SubExpr.GoalsLocation) (goalType : Expr)
25+
(_ : SelectInsertParams) : MetaM (String × String × Option (String.Pos × String.Pos)) := do
26+
let subexprPos := getGoalLocations pos
27+
unless goalType.isAppOf `Eq || goalType.isAppOf `Iff do
28+
throwError "The goal must be an equality or iff."
29+
let mut goalTypeWithMetaVars := goalType
30+
for pos in subexprPos do
31+
goalTypeWithMetaVars ← insertMetaVar goalTypeWithMetaVars pos
32+
33+
let side := if subexprPos[0]!.toArray[0]! = 0 then 1 else 2
34+
let sideExpr := goalTypeWithMetaVars.getAppArgs[side]!
35+
let res := "congrm " ++ (toString (← Meta.ppExpr sideExpr)).renameMetaVar
36+
return (res, res, none)
37+
38+
/-- Rpc function for the congrm widget. -/
39+
@[server_rpc_method]
40+
def CongrmSelectionPanel.rpc := mkSelectionPanelRPC makeCongrmString
41+
"Use shift-click to select sub-expressions in the goal that should become holes in congrm."
42+
"Congrm 🔍"
43+
44+
/-- The congrm widget. -/
45+
@[widget_module]
46+
def CongrmSelectionPanel : Component SelectInsertParams :=
47+
mk_rpc_widget% CongrmSelectionPanel.rpc
48+
49+
open scoped Json in
50+
/-- Display a widget panel allowing to generate a `congrm` call with holes specified by selecting
51+
subexpressions in the goal.-/
52+
elab stx:"congrm?" : tactic => do
53+
let some replaceRange := (← getFileMap).rangeOfStx? stx | return
54+
savePanelWidgetInfo stx ``CongrmSelectionPanel $ pure $ json% { replaceRange: $(replaceRange) }

Mathlib/Tactic/Widget/Conv.lean

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/-
2+
Copyright (c) 2023 Robin Böhne. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Authors: Robin Böhne, Wojciech Nawrocki, Patrick Massot
5+
-/
6+
import Mathlib.Tactic.Widget.SelectPanelUtils
7+
import Mathlib.Data.String.Defs
8+
9+
/-! # Conv widget
10+
11+
This is a slightly improved version of one of the examples in the ProofWidget library.
12+
It defines a `conv?` tactic that displays a widget panel allowing to generate
13+
a `conv` call zooming to the subexpression selected in the goal.
14+
-/
15+
16+
17+
open Lean Meta Server ProofWidgets
18+
19+
private structure SolveReturn where
20+
expr : Expr
21+
val? : Option String
22+
listRest : List Nat
23+
24+
private def solveLevel (expr : Expr) (path : List Nat) : MetaM SolveReturn := match expr with
25+
| Expr.app _ _ => do
26+
let mut descExp := expr
27+
let mut count := 0
28+
let mut explicitList := []
29+
30+
-- we go through the application until we reach the end, counting how many explicit arguments
31+
-- it has and noting whether they are explicit or implicit
32+
while descExp.isApp do
33+
if (←Lean.Meta.inferType descExp.appFn!).bindingInfo!.isExplicit then
34+
explicitList := true::explicitList
35+
count := count + 1
36+
else
37+
explicitList := false::explicitList
38+
descExp := descExp.appFn!
39+
40+
-- we get the correct `enter` command by subtracting the number of `true`s in our list
41+
let mut mutablePath := path
42+
let mut length := count
43+
explicitList := List.reverse explicitList
44+
while !mutablePath.isEmpty && mutablePath.head! == 0 do
45+
if explicitList.head! == true then
46+
count := count - 1
47+
explicitList := explicitList.tail!
48+
mutablePath := mutablePath.tail!
49+
50+
let mut nextExp := expr
51+
while length > count do
52+
nextExp := nextExp.appFn!
53+
length := length - 1
54+
nextExp := nextExp.appArg!
55+
56+
let pathRest := if mutablePath.isEmpty then [] else mutablePath.tail!
57+
58+
return { expr := nextExp, val? := toString count , listRest := pathRest }
59+
60+
| Expr.lam n _ b _ => do
61+
let name := match n with
62+
| Name.str _ s => s
63+
| _ => panic! "no name found"
64+
return { expr := b, val? := name, listRest := path.tail! }
65+
66+
| Expr.forallE n _ b _ => do
67+
let name := match n with
68+
| Name.str _ s => s
69+
| _ => panic! "no name found"
70+
return { expr := b, val? := name, listRest := path.tail! }
71+
72+
| Expr.mdata _ b => do
73+
match b with
74+
| Expr.mdata _ _ => return { expr := b, val? := none, listRest := path }
75+
| _ => return { expr := b.appFn!.appArg!, val? := none, listRest := path.tail!.tail! }
76+
77+
| _ => do
78+
return {
79+
expr := ←(Lean.Core.viewSubexpr path.head! expr)
80+
val? := toString (path.head! + 1)
81+
listRest := path.tail!
82+
}
83+
84+
open Lean Syntax in
85+
/-- Return the link text and inserted text above and below of the conv widget. -/
86+
@[nolint unusedArguments]
87+
def insertEnter (locations : Array Lean.SubExpr.GoalsLocation) (goalType : Expr)
88+
(params : SelectInsertParams): MetaM (String × String × Option (String.Pos × String.Pos)) := do
89+
let some pos := locations[0]? | throwError "You must select something."
90+
let ⟨_, .target subexprPos⟩ := pos | throwError "You must select something in the goal."
91+
let mut list := (SubExpr.Pos.toArray subexprPos).toList
92+
let mut expr := goalType
93+
let mut retList := []
94+
-- generate list of commands for `enter`
95+
while !list.isEmpty do
96+
let res ← solveLevel expr list
97+
expr := res.expr
98+
retList := match res.val? with
99+
| none => retList
100+
| some val => val::retList
101+
list := res.listRest
102+
103+
-- build `enter [...]` string
104+
retList := List.reverse retList
105+
-- prepare `enter` indentation
106+
let spc := String.replicate (SelectInsertParamsClass.replaceRange params).start.character ' '
107+
let mut enterval := s!"conv =>\n{spc} enter {retList}"
108+
if enterval.contains '0' then enterval := "Error: Not a valid conv target"
109+
if retList.isEmpty then enterval := ""
110+
return ("Generate conv", enterval, none)
111+
112+
/-- Rpc function for the conv widget. -/
113+
@[server_rpc_method]
114+
def ConvSelectionPanel.rpc :=
115+
mkSelectionPanelRPC insertEnter
116+
"Use shift-click to select one sub-expression in the goal that you want to zoom on."
117+
"Conv 🔍" (onlyOne := true)
118+
119+
/-- The conv widget. -/
120+
@[widget_module]
121+
def ConvSelectionPanel : Component SelectInsertParams :=
122+
mk_rpc_widget% ConvSelectionPanel.rpc
123+
124+
open scoped Json in
125+
/-- Display a widget panel allowing to generate a `conv` call zooming to the subexpression selected
126+
in the goal.-/
127+
elab stx:"conv?" : tactic => do
128+
let some replaceRange := (← getFileMap).rangeOfStx? stx | return
129+
savePanelWidgetInfo stx ``ConvSelectionPanel $ pure $ json% { replaceRange: $(replaceRange) }

0 commit comments

Comments
 (0)