/** * @fileOverview * * Mostly copied from https://github.com/leanprover/vscode-lean4/blob/master/lean4-infoview/src/infoview/goals.tsx */ import * as React from 'react' import { InteractiveHypothesisBundle_nonAnonymousNames, MVarId, TaggedText_stripTags } from '@leanprover/infoview-api' import { EditorContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts'; import { Locations, LocationsContext, SelectableLocation } from '../../../../node_modules/lean4-infoview/src/infoview/goalLocation'; import { InteractiveCode } from '../../../../node_modules/lean4-infoview/src/infoview/interactiveCode' import { WithTooltipOnHover } from '../../../../node_modules/lean4-infoview/src/infoview/tooltips'; import { InputModeContext } from './context'; import { InteractiveGoal, InteractiveGoals, InteractiveGoalsWithHints, InteractiveHypothesisBundle, ProofState } from './rpc_api'; import { RpcSessionAtPos } from '@leanprover/infoview/*'; import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util'; import { DiagnosticSeverity } from 'vscode-languageserver-protocol'; /** Returns true if `h` is inaccessible according to Lean's default name rendering. */ function isInaccessibleName(h: string): boolean { return h.indexOf('✝') >= 0; } function goalToString(g: InteractiveGoal): string { let ret = '' if (g.userName) { ret += `case ${g.userName}\n` } for (const h of g.hyps) { const names = InteractiveHypothesisBundle_nonAnonymousNames(h).join(' ') ret += `${names} : ${TaggedText_stripTags(h.type)}` if (h.val) { ret += ` := ${TaggedText_stripTags(h.val)}` } ret += '\n' } ret += `⊢ ${TaggedText_stripTags(g.type)}` return ret } export function goalsToString(goals: InteractiveGoals): string { return goals.goals.map(g => goalToString(g)).join('\n\n') } export function goalsWithHintsToString(goals: InteractiveGoalsWithHints): string { return goals.goals.map(g => goalToString(g.goal)).join('\n\n') } interface GoalFilterState { /** If true reverse the list of hypotheses, if false present the order received from LSP. */ reverse: boolean, /** If true show hypotheses that have isType=True, otherwise hide them. */ showType: boolean, /** If true show hypotheses that have isInstance=True, otherwise hide them. */ showInstance: boolean, /** If true show hypotheses that contain a dagger in the name, otherwise hide them. */ showHiddenAssumption: boolean /** If true show the bodies of let-values, otherwise hide them. */ showLetValue: boolean; } function getFilteredHypotheses(hyps: InteractiveHypothesisBundle[], filter: GoalFilterState): InteractiveHypothesisBundle[] { return hyps.reduce((acc: InteractiveHypothesisBundle[], h) => { if (h.isInstance && !filter.showInstance) return acc if (h.isType && !filter.showType) return acc const names = filter.showHiddenAssumption ? h.names : h.names.filter(n => !isInaccessibleName(n)) const hNew: InteractiveHypothesisBundle = filter.showLetValue ? { ...h, names } : { ...h, names, val: undefined } if (names.length !== 0) acc.push(hNew) return acc }, []) } interface HypProps { hyp: InteractiveHypothesisBundle mvarId?: MVarId } function Hyp({ hyp: h, mvarId }: HypProps) { const locs = React.useContext(LocationsContext) const namecls: string = 'mr1 ' + (h.isInserted ? 'inserted-text ' : '') + (h.isRemoved ? 'removed-text ' : '') const names = InteractiveHypothesisBundle_nonAnonymousNames(h).map((n, i) => i ? { mvarId, loc: { hyp: h.fvarIds[i] }} : undefined } alwaysHighlight={false} >{n} ) const typeLocs: Locations | undefined = React.useMemo(() => locs && mvarId && h.fvarIds && h.fvarIds.length > 0 ? { ...locs, subexprTemplate: { mvarId, loc: { hypType: [h.fvarIds[0], ''] }}} : undefined, [locs, mvarId, h.fvarIds]) const valLocs: Locations | undefined = React.useMemo(() => h.val && locs && mvarId && h.fvarIds && h.fvarIds.length > 0 ? { ...locs, subexprTemplate: { mvarId, loc: { hypValue: [h.fvarIds[0], ''] }}} : undefined, [h.val, locs, mvarId, h.fvarIds]) return
{names} {h.val &&  :=  }
} interface GoalProps2 { goals: InteractiveGoal[] filter: GoalFilterState } interface GoalProps { goal: InteractiveGoal filter: GoalFilterState showHints?: boolean typewriter: boolean } interface ProofDisplayProps { proof: string } /** * Displays the hypotheses, target type and optional case label of a goal according to the * provided `filter`. */ export const Goal = React.memo((props: GoalProps) => { const { goal, filter, showHints, typewriter } = props // TODO: Apparently `goal` can be `undefined` if (!goal) {return <>} 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.mvarId ? { ...locs, subexprTemplate: { mvarId: goal.mvarId, loc: { target: '' }}} : undefined, [locs, goal.mvarId]) const goalLi =
Goal:
// let cn = 'font-code tl pre-wrap bl bw1 pl1 b--transparent ' // if (props.goal.isInserted) cn += 'b--inserted ' // if (props.goal.isRemoved) cn += 'b--removed ' // const hints = const objectHyps = hyps.filter(hyp => !hyp.isAssumption) const assumptionHyps = hyps.filter(hyp => hyp.isAssumption) const {typewriterMode} = React.useContext(InputModeContext) return
{/* {goal.userName &&
case {goal.userName}
} */} {filter.reverse && goalLi} {! typewriter && objectHyps.length > 0 &&
Objects:
{objectHyps.map((h, i) => )}
} {!typewriter && assumptionHyps.length > 0 &&
Assumptions:
{assumptionHyps.map((h, i) => )}
} {/* {typewriter && typewriterMode && } */} {!filter.reverse && goalLi} {/* {showHints && hints} */}
}) export const MainAssumptions = React.memo((props: GoalProps2) => { const { goals, filter } = props const goal = goals[0] 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.mvarId ? { ...locs, subexprTemplate: { mvarId: goal.mvarId, loc: { target: '' }}} : undefined, [locs, goal.mvarId]) const goalLi =
Goal:
const objectHyps = hyps.filter(hyp => !hyp.isAssumption) const assumptionHyps = hyps.filter(hyp => hyp.isAssumption) return
Current Goal
{filter.reverse && goalLi} { objectHyps.length > 0 &&
Objects:
{objectHyps.map((h, i) => )}
} { assumptionHyps.length > 0 &&
Assumptions:
{assumptionHyps.map((h, i) => )}
}
}) export const OtherGoals = React.memo((props: GoalProps2) => { const { goals, filter } = props return <> {goals && goals.length > 1 &&
Further Goals
{goals.slice(1).map((goal, i) =>
)}
} }) // TODO: deprecated export const ProofDisplay = React.memo((props : ProofDisplayProps) => { const { proof } = props const steps = proof.match(/.+/g) return <> { steps &&
Proof history
{steps.map((s) =>
{s}
)}
} }) interface GoalsProps { goals: InteractiveGoalsWithHints filter: GoalFilterState } export function Goals({ goals, filter }: GoalsProps) { if (goals.goals.length === 0) { return <>No goals } else { return <> {goals.goals.map((g, i) => )} } } interface FilteredGoalsProps { /** Components to render in the header. */ headerChildren: React.ReactNode /** * 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?: InteractiveGoalsWithHints } /** * Display goals together with a header containing the provided children as well as buttons * to control how the goals are displayed. */ export const FilteredGoals = React.memo(({ headerChildren, goals }: FilteredGoalsProps) => { const ec = React.useContext(EditorContext) const copyToCommentButton = { e.preventDefault(); if (goals) void ec.copyToComment(goalsWithHintsToString(goals)) }} title="copy state to comment" /> const [goalFilters, setGoalFilters] = React.useState( { reverse: false, showType: true, showInstance: true, showHiddenAssumption: true, showLetValue: true }); const sortClasses = 'link pointer mh2 dim codicon ' + (goalFilters.reverse ? 'codicon-arrow-up ' : 'codicon-arrow-down '); const sortButton = setGoalFilters(s => ({ ...s, reverse: !s.reverse }))} /> const mkFilterButton = (filterFn: React.SetStateAction, filledFn: (_: GoalFilterState) => boolean, name: string) => { setGoalFilters(filterFn) }}>   {name} const filterMenu = {mkFilterButton(s => ({ ...s, showType: !s.showType }), gf => gf.showType, 'types')}
{mkFilterButton(s => ({ ...s, showInstance: !s.showInstance }), gf => gf.showInstance, 'instances')}
{mkFilterButton(s => ({ ...s, showHiddenAssumption: !s.showHiddenAssumption }), gf => gf.showHiddenAssumption, 'hidden assumptions')}
{mkFilterButton(s => ({ ...s, showLetValue: !s.showLetValue }), gf => gf.showLetValue, 'let-values')}
const isFiltered = !goalFilters.showInstance || !goalFilters.showType || !goalFilters.showHiddenAssumption || !goalFilters.showLetValue const filterButton = filterMenu}> return
{headerChildren} {copyToCommentButton}{sortButton}{filterButton}
{goals && }
}) export function loadGoals( rpcSess: RpcSessionAtPos, uri: string, setProof: React.Dispatch>) { console.info('sending rpc request to load the proof state') rpcSess.call('Game.getProofState', DocumentPosition.toTdpp({line: 0, character: 0, uri: uri})).then( (proof : ProofState) => { console.info(`received a proof state!`) console.log(proof) setProof(proof) // let tmpProof : ProofStep[] = [] // let goalCount = 0 // steps.map((goals, i) => { // // The first step has an empty command and therefore also no error messages // // Usually there is a newline at the end of the editors content, so we need to // // display diagnostics from potentally two lines in the last step. // let messages = i ? (i == steps.length - 1 ? diagnostics.slice(i-1).flat() : diagnostics[i-1]) : [] // // Filter out the 'unsolved goals' message // messages = messages.filter((msg) => { // return !("append" in msg.message && // "text" in msg.message.append[0] && // msg.message.append[0].text === "unsolved goals") // }) // if (typeof goals == 'undefined') { // tmpProof.push({ // command: i ? model.getLineContent(i) : '', // goals: [], // hints: [], // errors: messages // } as ProofStep) // console.debug('goals is undefined') // return // } // // If the number of goals reduce, show a message // if (goals.length && goalCount > goals.length) { // messages.unshift({ // range: { // start: { // line: i-1, // character: 0, // }, // end: { // line: i-1, // character: 0, // }}, // severity: DiagnosticSeverity.Information, // message: { // text: 'intermediate goal solved 🎉' // } // }) // } // goalCount = goals.length // // with no goals there will be no hints. // let hints : GameHint[] = goals.length ? goals[0].hints : [] // console.debug(`Command (${i}): `, i ? model.getLineContent(i) : '') // console.debug(`Goals: (${i}): `, goalsToString(goals)) // // console.debug(`Hints: (${i}): `, hints) // console.debug(`Errors: (${i}): `, messages) // tmpProof.push({ // // the command of the line above. Note that `getLineContent` starts counting // // at `1` instead of `zero`. The first ProofStep will have an empty command. // command: i ? model.getLineContent(i) : '', // // TODO: store correct data // goals: goals.map(g => g.goal), // // only need the hints of the active goals in chat // hints: hints, // // errors and messages from the server // errors: messages // } as ProofStep) // }) // // Save the proof to the context // setProof(tmpProof) } ) } export function lastStepHasErrors (proof : ProofState): boolean { if (!proof?.steps.length) {return false} let diags = [...proof.steps[proof.steps.length - 1].diags, ...proof.diagnostics] return diags.some( (d) => (d.severity == DiagnosticSeverity.Error ) // || d.severity == DiagnosticSeverity.Warning ) } export function isLastStepWithErrors (proof : ProofState, i: number): boolean { if (!proof?.steps.length) {return false} return (i == proof.steps.length - 1) && lastStepHasErrors(proof) }