diff --git a/client/src/components/hints.tsx b/client/src/components/hints.tsx index e488464..e57db5f 100644 --- a/client/src/components/hints.tsx +++ b/client/src/components/hints.tsx @@ -5,15 +5,33 @@ import { DeletedChatContext, ProofContext } from "./infoview/context"; import { lastStepHasErrors } from "./infoview/goals"; import { Button } from "./button"; +/** Plug-in the variable names in a hint. We do this client-side to prepare + * for i18n in the future. i.e. one should be able translate the `rawText` + * and have the variables substituted just before displaying. + */ +function getHintText(hint: GameHint): string { + if (hint.rawText) { + // Replace the variable names used in the hint with the ones used by the player + // variable names are marked like `«{g}»` inside the text. + return hint.rawText.replace(/«\{(.*?)\}»/, ((_, v) => + // `hint.varNames` contains tuples `[oldName, newName]` + (hint.varNames.find(x => x[0] == v))[1])) + } else { + // hints created in the frontend do not have a `rawText` + // TODO: `hint.text` could be removed in theory. + return hint.text + } +} + export function Hint({hint, step, selected, toggleSelection, lastLevel} : {hint: GameHint, step: number, selected: number, toggleSelection: any, lastLevel?: boolean}) { return
- {hint.text} + {getHintText(hint)}
} export function HiddenHint({hint, step, selected, toggleSelection, lastLevel} : {hint: GameHint, step: number, selected: number, toggleSelection: any, lastLevel?: boolean}) { return
- {hint.text} + {getHintText(hint)}
} @@ -31,7 +49,7 @@ export function Hints({hints, showHidden, step, selected, toggleSelection, lastL export function DeletedHint({hint} : {hint: GameHint}) { return
- {hint.text} + {getHintText(hint)}
} diff --git a/client/src/components/infoview/rpc_api.ts b/client/src/components/infoview/rpc_api.ts index c499e93..3a6acba 100644 --- a/client/src/components/infoview/rpc_api.ts +++ b/client/src/components/infoview/rpc_api.ts @@ -49,6 +49,8 @@ export interface InteractiveTermGoal extends InteractiveGoalCore { export interface GameHint { text: string; hidden: boolean; + rawText: string; + varNames: string[][]; // in Lean: `Array (Name × Name)` } export interface InteractiveGoalWithHints { diff --git a/server/GameServer/RpcHandlers.lean b/server/GameServer/RpcHandlers.lean index 61d46c8..c815abf 100644 --- a/server/GameServer/RpcHandlers.lean +++ b/server/GameServer/RpcHandlers.lean @@ -121,14 +121,45 @@ def findHints (goal : MVarId) (m : DocumentMeta) (initParams : Lsp.InitializePar openAbstractCtxResult hint.goal fun hintFVars hintGoal => do if let some fvarBij := matchExpr (← instantiateMVars $ hintGoal) (← instantiateMVars $ ← inferType $ mkMVar goal) then - let lctx := (← goal.getDecl).lctx - if let some bij ← matchDecls hintFVars lctx.getFVars (strict := hint.strict) (initBij := fvarBij) + + -- NOTE: This is a bit a hack of somebody who does not know how meta-programming works. + -- All we want here is a list of `userNames` for the `FVarId`s in `hintFVars`... + -- and we wrap them in `«{}»` here since I don't know how to do it later. + let mut hintFVarsNames : Array Expr := #[] + for fvar in hintFVars do + let name₁ ← fvar.fvarId!.getUserName + hintFVarsNames := hintFVarsNames.push <| Expr.fvar ⟨s!"«\{{name₁}}»"⟩ + + let lctx := (← goal.getDecl).lctx -- the player's local context + if let some bij ← matchDecls hintFVars lctx.getFVars + (strict := hint.strict) (initBij := fvarBij) then let userFVars := hintFVars.map fun v => bij.forward.findD v.fvarId! v.fvarId! + -- Evaluate the text in the player's context to get the new variable names. let text := (← evalHintMessage hint.text) (userFVars.map Expr.fvar) let ctx := {env := ← getEnv, mctx := ← getMCtx, lctx := lctx, opts := {}} let text ← (MessageData.withContext ctx text).toString - return some { text := text, hidden := hint.hidden } + + -- Evaluate the text in the `Hint`'s context to get the old variable names. + let rawText := (← evalHintMessage hint.text) hintFVarsNames + let ctx₂ := {env := ← getEnv, mctx := ← getMCtx, lctx := ← getLCtx, opts := {}} + let rawText ← (MessageData.withContext ctx₂ rawText).toString + + -- Here we map the goal's variable names to the player's variable names. + let mut varNames : Array <| Name × Name := #[] + for (fvar₁, fvar₂) in bij.forward.toArray do + -- get the `userName` of the fvar in the opened local context of the hint. + let name₁ ← fvar₁.getUserName + -- get the `userName` in the player's local context. + let name₂ := (lctx.get! fvar₂).userName + varNames := varNames.push (name₁, name₂) + + return some { + text := text, + hidden := hint.hidden, + rawText := rawText, + varNames := varNames } + else return none else return none diff --git a/server/GameServer/Structures.lean b/server/GameServer/Structures.lean index 7b27917..49ce187 100644 --- a/server/GameServer/Structures.lean +++ b/server/GameServer/Structures.lean @@ -54,8 +54,17 @@ deriving RpcEncodable /-- A hint in the game at the corresponding goal. -/ structure GameHint where + /-- The text with the variable names already inserted. + + Note: This is in theory superfluous and will be completely replaced by `rawText`. We just left + it in for debugging for now. -/ text : String + /-- Flag whether the hint should be hidden initially. -/ hidden : Bool + /-- The text with the variables not inserted yet. -/ + rawText : String + /-- The assignment of variable names in the `rawText` to the ones the player used. -/ + varNames : Array <| Name × Name deriving FromJson, ToJson /-- Bundled `InteractiveGoal` together with an array of hints that apply at this stage. -/