// /* Partly copied from https://github.com/leanprover/vscode-lean4/blob/master/lean4-infoview/src/infoview/main.tsx */ // import * as React from 'react'; // import type { DidCloseTextDocumentParams, DidChangeTextDocumentParams, Location, DocumentUri } from 'vscode-languageserver-protocol'; // import 'tachyons/css/tachyons.css'; // import '@vscode/codicons/dist/codicon.css'; // import '../../../../node_modules/lean4-infoview/src/infoview/index.css'; // import '../../css/infoview.css' // import { LeanFileProgressParams, LeanFileProgressProcessingInfo, defaultInfoviewConfig, EditorApi, InfoviewApi } from '@leanprover/infoview-api'; // import { useClientNotificationEffect, useServerNotificationEffect, useEventResult, useServerNotificationState } from '../../../../node_modules/lean4-infoview/src/infoview/util'; // import { EditorContext, ConfigContext, ProgressContext, VersionContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts'; // import { RpcContext, WithRpcSessions, useRpcSessionAtPos } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions'; // import { ServerVersion } from '../../../../node_modules/lean4-infoview/src/infoview/serverVersion'; // import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' // import { faDeleteLeft, faHome, faArrowRight, faArrowLeft, faRotateLeft } from '@fortawesome/free-solid-svg-icons' // import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js' // import { useAppDispatch, useAppSelector } from '../../hooks'; // import { LevelInfo, useGetGameInfoQuery, useLoadLevelQuery } from '../../state/api'; // import { changedInventory, levelCompleted, selectCode, selectCompleted, selectInventory } from '../../state/progress'; // import { Infos } from './infos'; // import { AllMessages, Errors, WithLspDiagnosticsContext } from './messages'; // import { Goal, isLastStepWithErrors, lastStepHasErrors, loadGoals } from './goals'; // import { ChatContext, PageContext, PreferencesContext, MonacoEditorContext, ProofContext, GameIdContext } from '../../state/context'; // import { Typewriter, getInteractiveDiagsAt, hasErrors, hasInteractiveErrors } from './typewriter'; // import { InteractiveDiagnostic } from '@leanprover/infoview/*'; // import { CircularProgress } from '@mui/material'; // import { GameHint, InteractiveGoalWithHints, InteractiveGoalsWithHints, ProofState } from './rpc_api'; // import { store } from '../../state/store'; // import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util'; // import { DiagnosticSeverity } from 'vscode-languageclient'; // import { useTranslation } from 'react-i18next'; // import path from 'path'; // import { useContext } from 'react'; // import { Hints, MoreHelpButton, filterHints } from '../chat'; // import { Button, Markdown } from '../utils' /** Wrapper for the two editors. It is important that the `div` with `codeViewRef` is * always present, or the monaco editor cannot start. */ export function DualEditor({ level, codeviewRef, levelId, worldId, worldSize }) { const ec = React.useContext(EditorContext) const { typewriterMode, lockEditorMode } = React.useContext(PageContext) return <>
{/* */}
{ec ? : // TODO: Style this if relevant. <> } } /** The part of the two editors that needs the editor connection first */ function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: string, levelId: number, level: LevelInfo, worldSize: number }) { const ec = React.useContext(EditorContext) const {gameId} = React.useContext(GameIdContext) const { typewriterMode, lockEditorMode } = React.useContext(PageContext) const {proof, setProof} = React.useContext(ProofContext) const dispatch = useAppDispatch() React.useEffect(() => { if (proof?.completed) { dispatch(levelCompleted({ game: gameId, world: worldId, level: levelId })) // On completion, add the names of all new items to the local storage let newTiles = [ ...level?.tactics, ...level?.theorems, ...level?.definitions ].filter((tile) => tile.new).map((tile) => tile.name) // Add the proven statement to the local storage as well. if (level?.statementName != null) { newTiles.push(level?.statementName) } let inv: string[] = selectInventory(gameId)(store.getState()) // add new items and remove duplicates let newInv = [...inv, ...newTiles].filter((item, i, array) => array.indexOf(item) == i) dispatch(changedInventory({ game: gameId, inventory: newInv })) } }, [proof, level]) /* Set up updates to the global infoview state on editor events. */ const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig; const [allProgress, _1] = useServerNotificationState( '$/lean/fileProgress', new Map(), async (params: LeanFileProgressParams) => (allProgress) => { const newProgress = new Map(allProgress); return newProgress.set(params.textDocument.uri, params.processing); }, []) const serverVersion = useEventResult(ec.events.serverRestarted, result => new ServerVersion(result.serverInfo?.version ?? '')) return <> {(typewriterMode && !lockEditorMode) ? :
} } // /** The mathematical formulation of the statement, supporting e.g. Latex // * It takes three forms, depending on the precence of name and description: // * - Theorem xyz: description // * - Theorem xyz // * - Exercises: description // * // * If `showLeanStatement` is true, it will additionally display the lean code. // */ // export function ExerciseStatement({ showLeanStatement = false }) { // let { t } = useTranslation() // const {gameId, worldId, levelId } = React.useContext(GameIdContext) // const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId}) // if (!(levelInfo.data?.descrText || levelInfo.data?.descrFormat)) { return <> } // return <> //
// {levelInfo.data?.descrText ? // // {(levelInfo.data?.displayName ? `**${t("Theorem")}** \`${levelInfo.data?.displayName}\`: ` : '') + t(levelInfo.data?.descrText, {ns: gameId})} // : levelInfo.data?.displayName && // // {`**${t("Theorem")}** \`${levelInfo.data?.displayName}\``} // // } // {levelInfo.data?.descrFormat && showLeanStatement && //

{levelInfo.data?.descrFormat}

// } //
// // } // // TODO: This is only used in `EditorInterface` // // while `TypewriterInterface` has this copy-pasted in. // export function Main(props: { world: string, level: number, data: LevelInfo}) { // let { t } = useTranslation() // const ec = React.useContext(EditorContext); // const {gameId, worldId, levelId} = React.useContext(GameIdContext) // const { proof, setProof } = React.useContext(ProofContext) // const {selectedStep, setSelectedStep, setDeletedChat, showHelp, setShowHelp} = React.useContext(ChatContext) // function toggleSelection(line: number) { // return (ev) => { // console.debug('toggled selection') // if (selectedStep == line) { // setSelectedStep(null) // } else { // setSelectedStep(line) // } // } // } // console.debug(`template: ${props.data?.template}`) // // React.useEffect (() => { // // if (props.data.template) { // // let code: string = selectCode(gameId, worldId, levelId)(store.getState()) // // if (!code.length) { // // //models.push(monaco.editor.createModel(code, 'lean4', uri)) // // } // // } // // }, [props.data.template]) // /* Set up updates to the global infoview state on editor events. */ // const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig; // const [allProgress, _1] = useServerNotificationState( // '$/lean/fileProgress', // new Map(), // async (params: LeanFileProgressParams) => (allProgress) => { // const newProgress = new Map(allProgress); // return newProgress.set(params.textDocument.uri, params.processing); // }, // [] // ); // const curUri = useEventResult(ec.events.changedCursorLocation, loc => loc?.uri); // const curPos: DocumentPosition | undefined = // useEventResult(ec.events.changedCursorLocation, loc => loc ? { uri: loc.uri, ...loc.range.start } : undefined) // // Effect when the cursor changes in the editor // React.useEffect(() => { // // TODO: this is a bit of a hack and will yield unexpected behaviour if lines // // are indented. // let newPos = curPos?.line + (curPos?.character == 0 ? 0 : 1) // // scroll the chat along // setSelectedStep(newPos) // }, [curPos]) // useClientNotificationEffect( // 'textDocument/didClose', // (params: DidCloseTextDocumentParams) => { // if (ec.events.changedCursorLocation.current && // ec.events.changedCursorLocation.current.uri === params.textDocument.uri) { // ec.events.changedCursorLocation.fire(undefined) // } // }, // [] // ); // const serverVersion = // useEventResult(ec.events.serverRestarted, result => new ServerVersion(result.serverInfo?.version ?? '')) // const serverStoppedResult = useEventResult(ec.events.serverStopped); // // NB: the cursor may temporarily become `undefined` when a file is closed. In this case // // it's important not to reconstruct the `WithBlah` wrappers below since they contain state // // that we want to persist. // let ret // if (!serverVersion) { // ret =

{t("Waiting for Lean server to start…")}

// } else if (serverStoppedResult) { // ret =

{serverStoppedResult.message}

{serverStoppedResult.reason}

// } else { // ret =
// {proof?.completedWithWarnings && //
// {proof?.completed ? t("Level completed! πŸŽ‰") : t("Level completed with warnings 🎭")} //
// } // // {/* // */} //
// } // return ret // } // const goalFilter = { // reverse: false, // showType: true, // showInstance: true, // showHiddenAssumption: true, // showLetValue: true // } // /** The display of a single entered lean command */ // function Command({ proof, i, deleteProof }: { proof: ProofState, i: number, deleteProof: any }) { // let {t} = useTranslation() // // The first step will always have an empty command // if (!proof?.steps[i]?.command) { return <> } // if (isLastStepWithErrors(proof, i)) { // // If the last step has errors, we display the command in a different style // // indicating that it will be removed on the next try. // return
// {t("Failed command")}: {proof?.steps[i].command} //
// } else { // return
//
{proof?.steps[i].command}
// //
// } // } // // const MessageView = React.memo(({uri, diag}: MessageViewProps) => { // // const ec = React.useContext(EditorContext); // // const fname = escapeHtml(basename(uri)); // // const {line, character} = diag.range.start; // // const loc: Location = { uri, range: diag.range }; // // const text = TaggedText_stripTags(diag.message); // // const severityClass = diag.severity ? { // // [DiagnosticSeverity.Error]: 'error', // // [DiagnosticSeverity.Warning]: 'warning', // // [DiagnosticSeverity.Information]: 'information', // // [DiagnosticSeverity.Hint]: 'hint', // // }[diag.severity] : ''; // // const title = `Line ${line+1}, Character ${character}`; // // // Hide "unsolved goals" messages // // let message; // // if ("append" in diag.message && "text" in diag.message.append[0] && // // diag.message?.append[0].text === "unsolved goals") { // // message = diag.message.append[0] // // } else { // // message = diag.message // // } // // const { typewriterMode } = React.useContext(PageContext) // // return ( // // //
// // // {title} // // // // // // { e.preventDefault(); void ec.revealLocation(loc); }} // // // title="reveal file location"> // // // {e.preventDefault(); void ec.copyToComment(text)}} // // // title="copy message to comment"> // // // {e.preventDefault(); void ec.api.copyToClipboard(text)}} // // // title="copy message to clipboard"> // // // // // // // //
// // {!typewriterMode &&

{title}

} // //
// //               
// //           
// //
// // //
// // ) // // }, fastIsEqual) // /** The tabs of goals that lean has after the command of this step has been processed */ // export function GoalsTabs({ goals, last, onClick, onGoalChange=(n)=>{}}: { goals: InteractiveGoalWithHints[], last : boolean, onClick? : any, onGoalChange?: (n?: number) => void }) { // let { t } = useTranslation() // const [selectedGoal, setSelectedGoal] = React.useState(0) // if (goals.length == 0) { // return <> // } // return
//
// {goals.map((goal, i) => ( // // TODO: Should not use index as key. //
{ onGoalChange(i); setSelectedGoal(i); ev.stopPropagation() }}> // {i ? t("Goal") + ` ${i + 1}` : t("Active Goal")} //
// ))} //
//
// //
//
// } // // Splitting up Typewriter into two parts is a HACK // export function TypewriterInterfaceWrapper(props: { world: string, level: number, data: LevelInfo, worldSize: number }) { // const ec = React.useContext(EditorContext) // const {gameId} = React.useContext(GameIdContext) // useClientNotificationEffect( // 'textDocument/didClose', // (params: DidCloseTextDocumentParams) => { // if (ec.events.changedCursorLocation.current && // ec.events.changedCursorLocation.current.uri === params.textDocument.uri) { // ec.events.changedCursorLocation.fire(undefined) // } // }, [] // ) // const serverVersion = // useEventResult(ec.events.serverRestarted, result => new ServerVersion(result.serverInfo?.version ?? '')) // const serverStoppedResult = useEventResult(ec.events.serverStopped); // // NB: the cursor may temporarily become `undefined` when a file is closed. In this case // // it's important not to reconstruct the `WithBlah` wrappers below since they contain state // // that we want to persist. // if (!serverVersion) { return <> } // if (serverStoppedResult) { // return
//

{serverStoppedResult.message}

//

{serverStoppedResult.reason}

//
// } // return // } // /** The interface in command line mode */ // export function TypewriterInterface({props}) { // let { t } = useTranslation() // const ec = React.useContext(EditorContext) // const {gameId,worldId, levelId} = React.useContext(GameIdContext) // const editor = React.useContext(MonacoEditorContext) // const model = editor.getModel() // const uri = model.uri.toString() // const gameInfo = useGetGameInfoQuery({game: gameId}) // let image: string = gameInfo.data?.worlds.nodes[worldId].image // const [disableInput, setDisableInput] = React.useState(false) // const [loadingProgress, setLoadingProgress] = React.useState(0) // const { selectedStep, setSelectedStep, setDeletedChat, showHelp, setShowHelp } = React.useContext(ChatContext) // const {mobile} = React.useContext(PreferencesContext) // const { proof, setProof, crashed, setCrashed, interimDiags } = React.useContext(ProofContext) // const { setTypewriterInput } = React.useContext(PageContext) // const proofPanelRef = React.useRef(null) // // const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig; // // const curUri = useEventResult(ec.events.changedCursorLocation, loc => loc?.uri); // const rpcSess = useRpcSessionAtPos({uri: uri, line: 0, character: 0}) // /** Delete all proof lines starting from a given line. // * Note that the first line (i.e. deleting everything) is `1`! // */ // function deleteProof(line: number) { // return (ev) => { // let deletedChat: Array = [] // proof?.steps.slice(line).map((step, i) => { // let filteredHints = filterHints(step.goals[0]?.hints, proof?.steps[i-1]?.goals[0]?.hints) // // Only add these hidden hints to the deletion stack which were visible // deletedChat = [...deletedChat, ...filteredHints.filter(hint => (!hint.hidden || showHelp.has(line + i)))] // }) // setDeletedChat(deletedChat) // // delete showHelp for deleted steps // setShowHelp(new Set(Array.from(showHelp).filter(i => i < line - 1))) // editor.executeEdits("typewriter", [{ // range: monaco.Selection.fromPositions( // { lineNumber: line, column: 1 }, // editor.getModel().getFullModelRange().getEndPosition() // ), // text: '', // forceMoveMarkers: false // }]) // setSelectedStep(null) // setTypewriterInput(proof?.steps[line].command) // // Reload proof on deleting // loadGoals(rpcSess, uri, setProof, setCrashed) // ev.stopPropagation() // } // } // function toggleSelectStep(line: number) { // return (ev) => { // if (mobile) {return} // if (selectedStep == line) { // setSelectedStep(null) // console.debug(`unselected step`) // } else { // setSelectedStep(line) // console.debug(`step ${line} selected`) // } // } // } // // Scroll to the end of the proof if it is updated. // React.useEffect(() => { // if (proof?.steps.length > 1) { // proofPanelRef.current?.lastElementChild?.scrollIntoView() //scrollTo(0,0) // } else { // proofPanelRef.current?.scrollTo(0,0) // } // // also reenable the commandline when the proof changes // // BUG: If selecting 2nd goal on a intermediate proofstep and then delete proof to there, // // the commandline is not displaying disabled even though it should. // setDisableInput(false) // }, [proof]) // // Scroll to element if selection changes // React.useEffect(() => { // if (typeof selectedStep !== 'undefined') { // Array.from(proofPanelRef.current?.getElementsByClassName(`step-${selectedStep}`)).map((elem) => { // elem.scrollIntoView({ block: "center" }) // }) // } // }, [selectedStep]) // // TODO: superfluous, can be replaced with `withErr` from above // let lastStepErrors = proof?.steps.length ? hasInteractiveErrors(getInteractiveDiagsAt(proof, proof?.steps.length)) : false // useServerNotificationEffect("$/game/loading", (params : any) => { // if (params.kind == "loadConstants") { // setLoadingProgress(params.counter/100*50) // } else if (params.kind == "finalizeExtensions") { // setLoadingProgress(50 + params.counter/150*50) // } else { // console.error(`Unknown loading kind: ${params.kind}`) // } // }) // return
// //
// {/*
// {image && // // } //
*/} // {/*
//
//
//
*/} //
// {/* */} // {crashed ?
//

{t("Crashed! Go to editor mode and fix your proof! Last server response:")}

// {interimDiags.map(diag => { // const severityClass = diag.severity ? { // [DiagnosticSeverity.Error]: 'error', // [DiagnosticSeverity.Warning]: 'warning', // [DiagnosticSeverity.Information]: 'information', // [DiagnosticSeverity.Hint]: 'hint', // }[diag.severity] : ''; // return
//
//

{t("Line")} {diag.range.start.line}, {t("Character")} {diag.range.start.character}

//
//                   {diag.message}
//                 
//
//
// })} //
: proof?.steps.length ? // <> // {proof?.steps.map((step, i) => { // let filteredHints = filterHints(step.goals[0]?.hints, proof?.steps[i-1]?.goals[0]?.hints) // // if (i == proof?.steps.length - 1 && hasInteractiveErrors(step.diags)) { // // // if the last command contains an error, we only display the errors but not the // // // entered command as it is still present in the command line. // // // TODO: Should not use index as key. // // return
// // // //
// // } else { // return
// {i > 0 && <> // // // } // {mobile && i == 0 && props.data?.introduction && //
// {props.data?.introduction} //
// } // {mobile && // ({hint: hint, step: i}))} /> // } // {/* setDisableInput(n > 0) : (n) => {}}/> */} // {!(isLastStepWithErrors(proof, i)) && // setDisableInput(n > 0) : (n) => {}}/> // } // {mobile && i == proof?.steps.length - 1 && // // } // {/* Show a message that there are no goals left */} // {/* {!step.goals.length && ( //
// {proof?.completed ? //

Level completed! πŸŽ‰

: //

// no goals left
// This probably means you solved the level with warnings or Lean encountered a parsing error. //

// } //
// )} */} //
// } // //} // )} // {proof?.diagnostics.length > 0 && //
// //
// } // {mobile && proof?.completed && //
// {props.level >= props.worldSize ? // // : // // } //
// } // : // // note: since we don't know the total number of files, // // we use a function which strictly monotonely increases towards `100` as `x β†’ ∞` // // The base is chosen at random s.t. we get roughly 91% for `x = 100`. // } //
//
// //
//
// }