diff --git a/client/src/components/infoview/goals.tsx b/client/src/components/infoview/goals.tsx
index d2ede29..209860a 100644
--- a/client/src/components/infoview/goals.tsx
+++ b/client/src/components/infoview/goals.tsx
@@ -2,11 +2,11 @@
import * as React from 'react'
import { InteractiveCode } from '../../../../node_modules/lean4-infoview/src/infoview/interactiveCode'
-import { InteractiveGoal, InteractiveGoals, InteractiveHypothesisBundle, InteractiveHypothesisBundle_nonAnonymousNames, MVarId, TaggedText_stripTags } from '@leanprover/infoview-api'
+import { InteractiveHypothesisBundle, InteractiveHypothesisBundle_nonAnonymousNames, MVarId, TaggedText_stripTags } from '@leanprover/infoview-api'
import { WithTooltipOnHover } from '../../../../node_modules/lean4-infoview/src/infoview/tooltips';
import { EditorContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts';
import { Locations, LocationsContext, SelectableLocation } from '../../../../node_modules/lean4-infoview/src/infoview/goalLocation';
-import { GameInteractiveGoal, GameInteractiveGoals } from './rpcApi';
+import { InteractiveGoal, InteractiveGoals } from './rpcApi';
import { Hints } from './hints';
/** Returns true if `h` is inaccessible according to Lean's default name rendering. */
@@ -14,14 +14,14 @@ function isInaccessibleName(h: string): boolean {
return h.indexOf('✝') >= 0;
}
-function goalToString(g: GameInteractiveGoal): string {
+function goalToString(g: InteractiveGoal): string {
let ret = ''
- if (g.goal.userName) {
- ret += `case ${g.goal.userName}\n`
+ if (g.userName) {
+ ret += `case ${g.userName}\n`
}
- for (const h of g.goal.hyps) {
+ for (const h of g.hyps) {
const names = InteractiveHypothesisBundle_nonAnonymousNames(h).join(' ')
ret += `${names} : ${TaggedText_stripTags(h.type)}`
if (h.val) {
@@ -30,12 +30,12 @@ function goalToString(g: GameInteractiveGoal): string {
ret += '\n'
}
- ret += `⊢ ${TaggedText_stripTags(g.goal.type)}`
+ ret += `⊢ ${TaggedText_stripTags(g.type)}`
return ret
}
-export function goalsToString(goals: GameInteractiveGoals): string {
+export function goalsToString(goals: InteractiveGoals): string {
return goals.goals.map(goalToString).join('\n\n')
}
@@ -113,7 +113,7 @@ function Hyp({ hyp: h, mvarId }: HypProps) {
}
interface GoalProps {
- goal: GameInteractiveGoal
+ goal: InteractiveGoal
filter: GoalFilterState
}
@@ -123,48 +123,48 @@ interface GoalProps {
export const Goal = React.memo((props: GoalProps) => {
const { goal, filter } = props
- const prefix = goal.goal.goalPrefix ?? 'Prove: '
- const filteredList = getFilteredHypotheses(goal.goal.hyps, filter);
+ const prefix = goal.goalPrefix ?? 'Prove: '
+ const filteredList = getFilteredHypotheses(goal.hyps, filter);
const hyps = filter.reverse ? filteredList.slice().reverse() : filteredList;
const locs = React.useContext(LocationsContext)
const goalLocs = React.useMemo(() =>
- locs && goal.goal.mvarId ?
- { ...locs, subexprTemplate: { mvarId: goal.goal.mvarId, loc: { target: '' }}} :
+ locs && goal.mvarId ?
+ { ...locs, subexprTemplate: { mvarId: goal.mvarId, loc: { target: '' }}} :
undefined,
- [locs, goal.goal.mvarId])
+ [locs, goal.mvarId])
const goalLi =
Prove:
-
+
let cn = 'font-code tl pre-wrap bl bw1 pl1 b--transparent '
- if (props.goal.goal.isInserted) cn += 'b--inserted '
- if (props.goal.goal.isRemoved) cn += 'b--removed '
+ if (props.goal.isInserted) cn += 'b--inserted '
+ if (props.goal.isRemoved) cn += 'b--removed '
const hints =
- if (goal.goal.userName) {
+ if (goal.userName) {
return
- case {goal.goal.userName}
+ case {goal.userName}
{filter.reverse && goalLi}
- {hyps.map((h, i) => )}
+ {hyps.map((h, i) => )}
{!filter.reverse && goalLi}
{hints}
} else return
{filter.reverse && goalLi}
- {hyps.map((h, i) => )}
+ {hyps.map((h, i) => )}
{!filter.reverse && goalLi}
{hints}
})
interface GoalsProps {
- goals: GameInteractiveGoals
+ goals: InteractiveGoals
filter: GoalFilterState
}
@@ -185,7 +185,7 @@ interface FilteredGoalsProps {
* When this is `undefined`, the component will not appear at all but will remember its state
* by virtue of still being mounted in the React tree. When it does appear again, the filter
* settings and collapsed state will be as before. */
- goals?: GameInteractiveGoals
+ goals?: InteractiveGoals
}
/**
diff --git a/client/src/components/infoview/info.tsx b/client/src/components/infoview/info.tsx
index 199b924..1668b1b 100644
--- a/client/src/components/infoview/info.tsx
+++ b/client/src/components/infoview/info.tsx
@@ -8,10 +8,10 @@ import { basename, DocumentPosition, RangeHelpers, useEvent, usePausableState, d
mapRpcError, useAsyncWithTrigger, PausableProps } from '../../../../node_modules/lean4-infoview/src/infoview/util';
import { ConfigContext, EditorContext, LspDiagnosticsContext, ProgressContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts';
import { lspDiagToInteractive, MessagesList } from './messages';
-import { getInteractiveGoals, getInteractiveTermGoal, InteractiveDiagnostic,
- InteractiveGoals, UserWidgetInstance, Widget_getWidgets, RpcSessionAtPos, isRpcError,
+import { getInteractiveTermGoal, InteractiveDiagnostic,
+ UserWidgetInstance, Widget_getWidgets, RpcSessionAtPos, isRpcError,
RpcErrorCode, getInteractiveDiagnostics, InteractiveTermGoal } from '@leanprover/infoview-api';
-import { GameInteractiveGoal, GameInteractiveGoals } from './rpcApi';
+import { InteractiveGoal, InteractiveGoals } from './rpcApi';
import { PanelWidgetDisplay } from '../../../../node_modules/lean4-infoview/src/infoview/userWidget'
import { RpcContext, useRpcSessionAtPos } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions';
import { GoalsLocation, Locations, LocationsContext } from '../../../../node_modules/lean4-infoview/src/infoview/goalLocation';
@@ -77,7 +77,7 @@ const InfoStatusBar = React.memo((props: InfoStatusBarProps) => {
interface InfoDisplayContentProps extends PausableProps {
pos: DocumentPosition;
messages: InteractiveDiagnostic[];
- goals?: GameInteractiveGoals;
+ goals?: InteractiveGoals;
termGoal?: InteractiveTermGoal;
error?: string;
userWidgets: UserWidgetInstance[];
@@ -121,11 +121,11 @@ const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
{goals && }
+ goals={termGoal !== undefined ? {goals: [termGoal as any]} : undefined} />
{userWidgets.map(widget =>
{widget.name}
- goal.goal) : []} termGoal={termGoal}
+ goal) : []} termGoal={termGoal}
selectedLocations={selectedLocs} widget={widget}/>
)}
@@ -153,7 +153,7 @@ interface InfoDisplayProps {
pos: DocumentPosition;
status: InfoStatus;
messages: InteractiveDiagnostic[];
- goals?: GameInteractiveGoals;
+ goals?: InteractiveGoals;
termGoal?: InteractiveTermGoal;
error?: string;
userWidgets: UserWidgetInstance[];
diff --git a/client/src/components/infoview/rpcApi.ts b/client/src/components/infoview/rpcApi.ts
index 016b6c0..f820b3e 100644
--- a/client/src/components/infoview/rpcApi.ts
+++ b/client/src/components/infoview/rpcApi.ts
@@ -1,15 +1,27 @@
-import { InteractiveGoals, InteractiveGoal } from '@leanprover/infoview-api';
+import { ContextInfo, InteractiveHypothesisBundle, CodeWithInfos, MVarId } from '@leanprover/infoview-api';
export interface GameHint {
text: string;
hidden: boolean;
}
-export interface GameInteractiveGoal {
- goal: InteractiveGoal;
+export interface InteractiveGoalCore {
+ hyps: InteractiveHypothesisBundle[];
+ type: CodeWithInfos;
+ /** Present since server version 1.1.2. */
+ ctx?: ContextInfo;
+}
+
+export interface InteractiveGoal extends InteractiveGoalCore {
+ userName?: string;
+ goalPrefix?: string;
+ /** Present since server version 1.1.2. */
+ mvarId?: MVarId;
+ isInserted?: boolean;
+ isRemoved?: boolean;
hints: GameHint[];
}
-export interface GameInteractiveGoals {
- goals: GameInteractiveGoal[];
+export interface InteractiveGoals {
+ goals: InteractiveGoal[];
}
diff --git a/server/leanserver/GameServer/InteractiveGoal.lean b/server/leanserver/GameServer/InteractiveGoal.lean
new file mode 100644
index 0000000..5cc4383
--- /dev/null
+++ b/server/leanserver/GameServer/InteractiveGoal.lean
@@ -0,0 +1,197 @@
+/- This file is mostly copied from `Lean/Widget/InteractiveGoal.lean`. -/
+
+import Lean.Widget.InteractiveGoal
+
+/-! Functionality related to tactic-mode and term-mode goals with embedded `CodeWithInfos`. -/
+
+namespace GameServer
+open Lean Lean.Widget Lean.Server
+
+structure GameHint where
+ text : String
+ hidden : Bool
+deriving FromJson, ToJson
+
+/-- In the infoview, if multiple hypotheses `h₁`, `h₂` have the same type `α`, they are rendered
+as `h₁ h₂ : α`. We call this a 'hypothesis bundle'. We use `none` instead of `some false` for
+booleans to save space in the json encoding. -/
+structure InteractiveHypothesisBundle where
+ /-- The user-friendly name for each hypothesis. -/
+ names : Array Name
+ /-- The ids for each variable. Should have the same length as `names`. -/
+ fvarIds : Array FVarId
+ type : CodeWithInfos
+ /-- The value, in the case the hypothesis is a `let`-binder. -/
+ val? : Option CodeWithInfos := none
+ /-- The hypothesis is a typeclass instance. -/
+ isInstance? : Option Bool := none
+ /-- The hypothesis is a type. -/
+ isType? : Option Bool := none
+ /-- If true, the hypothesis was not present on the previous tactic state.
+ Only present in tactic-mode goals. -/
+ isInserted? : Option Bool := none
+ /-- If true, the hypothesis will be removed in the next tactic state.
+ Only present in tactic-mode goals. -/
+ isRemoved? : Option Bool := none
+ deriving Inhabited, RpcEncodable
+
+/-- The shared parts of interactive term-mode and tactic-mode goals. -/
+structure InteractiveGoalCore where
+ hyps : Array InteractiveHypothesisBundle
+ /-- The target type. -/
+ type : CodeWithInfos
+ /-- Metavariable context that the goal is well-typed in. -/
+ ctx : WithRpcRef Elab.ContextInfo
+
+/-- An interactive tactic-mode goal. -/
+structure InteractiveGoal extends InteractiveGoalCore where
+ /-- The name `foo` in `case foo`, if any. -/
+ userName? : Option String
+ /-- The symbol to display before the target type. Usually `⊢ ` but `conv` goals use `∣ `
+ and it could be extended. -/
+ goalPrefix : String
+ /-- Identifies the goal (ie with the unique name of the MVar that it is a goal for.) -/
+ mvarId : MVarId
+ /-- If true, the goal was not present on the previous tactic state. -/
+ isInserted? : Option Bool := none
+ /-- If true, the goal will be removed on the next tactic state. -/
+ isRemoved? : Option Bool := none
+ hints : Array GameHint := #[]
+ deriving RpcEncodable
+
+/-- An interactive term-mode goal. -/
+structure InteractiveTermGoal extends InteractiveGoalCore where
+ /-- Syntactic range of the term. -/
+ range : Lsp.Range
+ /-- Information about the term whose type is the term-mode goal. -/
+ term : WithRpcRef Elab.TermInfo
+ deriving RpcEncodable
+
+def InteractiveGoalCore.pretty (g : InteractiveGoalCore) (userName? : Option String)
+ (goalPrefix : String) : Format := Id.run do
+ let indent := 2 -- Use option
+ let mut ret := match userName? with
+ | some userName => f!"case {userName}"
+ | none => Format.nil
+ for hyp in g.hyps do
+ ret := addLine ret
+ let names := hyp.names
+ |>.toList
+ |>.filter (not ∘ Name.isAnonymous)
+ |>.map toString
+ |> " ".intercalate
+ match names with
+ | "" =>
+ ret := ret ++ Format.group f!":{Format.nest indent (Format.line ++ hyp.type.stripTags)}"
+ | _ =>
+ match hyp.val? with
+ | some val =>
+ ret := ret ++ Format.group f!"{names} : {hyp.type.stripTags} :={Format.nest indent (Format.line ++ val.stripTags)}"
+ | none =>
+ ret := ret ++ Format.group f!"{names} :{Format.nest indent (Format.line ++ hyp.type.stripTags)}"
+ ret := addLine ret
+ ret ++ f!"{goalPrefix}{Format.nest indent g.type.stripTags}"
+where
+ addLine (fmt : Format) : Format :=
+ if fmt.isNil then fmt else fmt ++ Format.line
+
+def InteractiveGoal.pretty (g : InteractiveGoal) : Format :=
+ g.toInteractiveGoalCore.pretty g.userName? g.goalPrefix
+
+def InteractiveTermGoal.pretty (g : InteractiveTermGoal) : Format :=
+ g.toInteractiveGoalCore.pretty none "⊢ "
+
+structure InteractiveGoals where
+ goals : Array InteractiveGoal
+ deriving RpcEncodable
+
+def InteractiveGoals.append (l r : InteractiveGoals) : InteractiveGoals where
+ goals := l.goals ++ r.goals
+
+instance : Append InteractiveGoals := ⟨InteractiveGoals.append⟩
+instance : EmptyCollection InteractiveGoals := ⟨{goals := #[]}⟩
+
+open Meta in
+/-- Extend an array of hypothesis bundles with another bundle. -/
+def addInteractiveHypothesisBundle (hyps : Array InteractiveHypothesisBundle)
+ (ids : Array (Name × FVarId)) (type : Expr) (value? : Option Expr := none) :
+ MetaM (Array InteractiveHypothesisBundle) := do
+ if ids.size == 0 then
+ throwError "Can only add a nonzero number of ids as an InteractiveHypothesisBundle."
+ let fvarIds := ids.map Prod.snd
+ let names := ids.map Prod.fst
+ return hyps.push {
+ names
+ fvarIds
+ type := (← ppExprTagged type)
+ val? := (← value?.mapM ppExprTagged)
+ isInstance? := if (← isClass? type).isSome then true else none
+ isType? := if (← instantiateMVars type).isSort then true else none
+ }
+
+open Meta in
+variable [MonadControlT MetaM n] [Monad n] [MonadError n] [MonadOptions n] [MonadMCtx n] in
+def withGoalCtx (goal : MVarId) (action : LocalContext → MetavarDecl → n α) : n α := do
+ let mctx ← getMCtx
+ let some mvarDecl := mctx.findDecl? goal
+ | throwError "unknown goal {goal.name}"
+ let lctx := mvarDecl.lctx |>.sanitizeNames.run' {options := (← getOptions)}
+ withLCtx lctx mvarDecl.localInstances (action lctx mvarDecl)
+
+open Meta in
+/-- A variant of `Meta.ppGoal` which preserves subexpression information for interactivity. -/
+def goalToInteractive (mvarId : MVarId) (hints : Array GameHint): MetaM InteractiveGoal := do
+ let ppAuxDecls := pp.auxDecls.get (← getOptions)
+ let ppImplDetailHyps := pp.implementationDetailHyps.get (← getOptions)
+ let showLetValues := pp.showLetValues.get (← getOptions)
+ withGoalCtx mvarId fun lctx mvarDecl => do
+ let pushPending (ids : Array (Name × FVarId)) (type? : Option Expr) (hyps : Array InteractiveHypothesisBundle)
+ : MetaM (Array InteractiveHypothesisBundle) :=
+ if ids.isEmpty then
+ pure hyps
+ else
+ match type? with
+ | none => pure hyps
+ | some type => addInteractiveHypothesisBundle hyps ids type
+ let mut varNames : Array (Name × FVarId) := #[]
+ let mut prevType? : Option Expr := none
+ let mut hyps : Array InteractiveHypothesisBundle := #[]
+ for localDecl in lctx do
+ if !ppAuxDecls && localDecl.isAuxDecl || !ppImplDetailHyps && localDecl.isImplementationDetail then
+ continue
+ else
+ match localDecl with
+ | LocalDecl.cdecl _index fvarId varName type _ _ =>
+ let varName := varName.simpMacroScopes
+ let type ← instantiateMVars type
+ if prevType? == none || prevType? == some type then
+ varNames := varNames.push (varName, fvarId)
+ else
+ hyps ← pushPending varNames prevType? hyps
+ varNames := #[(varName, fvarId)]
+ prevType? := some type
+ | LocalDecl.ldecl _index fvarId varName type val _ _ => do
+ let varName := varName.simpMacroScopes
+ hyps ← pushPending varNames prevType? hyps
+ let type ← instantiateMVars type
+ let val? ← if showLetValues then pure (some (← instantiateMVars val)) else pure none
+ hyps ← addInteractiveHypothesisBundle hyps #[(varName, fvarId)] type val?
+ varNames := #[]
+ prevType? := none
+ hyps ← pushPending varNames prevType? hyps
+ let goalTp ← instantiateMVars mvarDecl.type
+ let goalFmt ← ppExprTagged goalTp
+ let userName? := match mvarDecl.userName with
+ | Name.anonymous => none
+ | name => some <| toString name.eraseMacroScopes
+ return {
+ hyps
+ type := goalFmt
+ ctx := ⟨← Elab.ContextInfo.save⟩
+ userName?
+ goalPrefix := getGoalPrefix mvarDecl
+ mvarId
+ hints
+ }
+
+end GameServer
diff --git a/server/leanserver/GameServer/RpcHandlers.lean b/server/leanserver/GameServer/RpcHandlers.lean
index c17eb40..dcebf8f 100644
--- a/server/leanserver/GameServer/RpcHandlers.lean
+++ b/server/leanserver/GameServer/RpcHandlers.lean
@@ -1,5 +1,6 @@
import Lean
import GameServer.EnvExtensions
+import GameServer.InteractiveGoal
open Lean
open Server
@@ -10,11 +11,6 @@ open Meta
/-! ## GameGoal -/
-structure GameHint where
- text : String
- hidden : Bool
-deriving FromJson, ToJson
-
namespace GameServer
-- TODO: Find a better way to pass on the file name?
@@ -72,22 +68,8 @@ def findHints (goal : MVarId) (doc : FileWorker.EditableDocument) : MetaM (Array
else return none
return hints
-structure GameInteractiveGoal where
- goal : InteractiveGoal
- hints: Array GameHint
- deriving RpcEncodable
-
-structure GameInteractiveGoals where
- goals : Array GameInteractiveGoal
- deriving RpcEncodable
-
-def GameInteractiveGoals.append (l r : GameInteractiveGoals) : GameInteractiveGoals where
- goals := l.goals ++ r.goals
-
-instance : Append GameInteractiveGoals := ⟨GameInteractiveGoals.append⟩
-
open RequestM in
-def getInteractiveGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Option GameInteractiveGoals)) := do
+def getInteractiveGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Option InteractiveGoals)) := do
let doc ← readDoc
let text := doc.meta.text
let hoverPos := text.lspPosToUtf8Pos p.position
@@ -96,7 +78,7 @@ def getInteractiveGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Optio
withWaitFindSnap doc (fun s => ¬ (s.infoTree.goalsAt? doc.meta.text hoverPos).isEmpty)
(notFoundX := return none) fun snap => do
if let rs@(_ :: _) := snap.infoTree.goalsAt? doc.meta.text hoverPos then
- let goals : List GameInteractiveGoals ← rs.mapM fun { ctxInfo := ci, tacticInfo := ti, useAfter := useAfter, .. } => do
+ let goals : List InteractiveGoals ← rs.mapM fun { ctxInfo := ci, tacticInfo := ti, useAfter := useAfter, .. } => do
let ciAfter := { ci with mctx := ti.mctxAfter }
let ci := if useAfter then ciAfter else { ci with mctx := ti.mctxBefore }
-- compute the interactive goals
@@ -105,7 +87,7 @@ def getInteractiveGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Optio
let goals ← ci.runMetaM {} do
goals.mapM fun goal => do
let hints ← findHints goal doc
- return {goal := ← Widget.goalToInteractive goal, hints}
+ return ← goalToInteractive goal hints
-- compute the goal diff
-- let goals ← ciAfter.runMetaM {} (do
-- try
@@ -123,7 +105,7 @@ builtin_initialize
registerBuiltinRpcProcedure
`Game.getInteractiveGoals
Lsp.PlainGoalParams
- (Option GameInteractiveGoals)
+ (Option InteractiveGoals)
getInteractiveGoals
end GameServer