import * as React from 'react'; import { useEffect, useState, useRef } from 'react'; import '@fontsource/roboto/300.css'; import '@fontsource/roboto/400.css'; import '@fontsource/roboto/500.css'; import '@fontsource/roboto/700.css'; import { InfoviewApi } from '@leanprover/infoview' import { Link, useParams } from 'react-router-dom'; import { Box, CircularProgress, FormControlLabel, FormGroup, Switch, IconButton } from '@mui/material'; import MuiDrawer from '@mui/material/Drawer'; import Grid from '@mui/material/Unstable_Grid2'; import Inventory from './Inventory'; import { LeanTaskGutter } from 'lean4web/client/src/editor/taskgutter'; import { AbbreviationProvider } from 'lean4web/client/src/editor/abbreviation/AbbreviationProvider'; import 'lean4web/client/src/editor/vscode.css'; import 'lean4web/client/src/editor/infoview.css'; import { AbbreviationRewriter } from 'lean4web/client/src/editor/abbreviation/rewriter/AbbreviationRewriter'; import { InfoProvider } from 'lean4web/client/src/editor/infoview'; import 'lean4web/client/src/editor/infoview.css' import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js' import './level.css' import { Button } from './Button' import { ConnectionContext, useLeanClient } from '../connection'; import { useGetGameInfoQuery, useLoadLevelQuery } from '../state/api'; import { codeEdited, selectCode, progressSlice, selectCompleted } from '../state/progress'; import { useAppDispatch, useAppSelector } from '../hooks'; import { useStore } from 'react-redux'; import { EditorContext, ConfigContext, ProgressContext, VersionContext } from '../../../node_modules/lean4-infoview/src/infoview/contexts'; import { EditorConnection, EditorEvents } from '../../../node_modules/lean4-infoview/src/infoview/editorConnection'; import { EventEmitter } from '../../../node_modules/lean4-infoview/src/infoview/event'; import { Main } from './infoview/main' import type { Location } from 'vscode-languageserver-protocol'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faHome, faArrowRight, faArrowLeft, faRotateLeft } from '@fortawesome/free-solid-svg-icons' import { styled, useTheme, Theme, CSSObject } from '@mui/material/styles'; import Markdown from './Markdown'; import Split from 'react-split' import { Alert } from '@mui/material'; export const MonacoEditorContext = React.createContext(null as any); export const InputModeContext = React.createContext<{ commandLineMode: boolean, setCommandLineMode: React.Dispatch>, commandLineInput: string, setCommandLineInput: React.Dispatch> }>({ commandLineMode: true, setCommandLineMode: () => {}, commandLineInput: "", setCommandLineInput: () => {}, }); function Level() { const params = useParams(); const levelId = parseInt(params.levelId) const worldId = params.worldId const codeviewRef = useRef(null) const introductionPanelRef = useRef(null) const initialCode = useAppSelector(selectCode(worldId, levelId)) const [commandLineMode, setCommandLineMode] = useState(true) const [commandLineInput, setCommandLineInput] = useState("") const [canUndo, setCanUndo] = useState(initialCode.trim() !== "") const theme = useTheme(); useEffect(() => { // Scroll to top when loading a new level introductionPanelRef.current!.scrollTo(0,0) }, [levelId]) React.useEffect(() => { if (!commandLineMode) { // Delete last input attempt from command line editor.executeEdits("command-line", [{ range: editor.getSelection(), text: "", forceMoveMarkers: false }]); editor.focus() } }, [commandLineMode]) const handleUndo = () => { const endPos = editor.getModel().getFullModelRange().getEndPosition() let range console.log(endPos.column) if (endPos.column === 1) { range = monaco.Selection.fromPositions( new monaco.Position(endPos.lineNumber - 1, 1), endPos ) } else { range = monaco.Selection.fromPositions( new monaco.Position(endPos.lineNumber, 1), endPos ) } editor.executeEdits("undo-button", [{ range, text: "", forceMoveMarkers: false }]); } const connection = React.useContext(ConnectionContext) const gameInfo = useGetGameInfoQuery() useLoadWorldFiles(worldId) const level = useLoadLevelQuery({world: worldId, level: levelId}) const dispatch = useAppDispatch() const onDidChangeContent = (code) => { dispatch(codeEdited({world: worldId, level: levelId, code})) setCanUndo(code.trim() !== "") } const completed = useAppSelector(selectCompleted(worldId, levelId)) const {editor, infoProvider, editorConnection} = useLevelEditor(worldId, levelId, codeviewRef, initialCode, onDidChangeContent) // TODO: This is a hack for having an introduction (i.e. level 0) // for each world. if (levelId == 0) { return <>
{gameInfo.data?.worlds.nodes[worldId].title && `World: ${gameInfo.data?.worlds.nodes[worldId].title}`}
{`Einführung`}
{gameInfo.data?.worlds.nodes[worldId].introduction}
{levelId >= gameInfo.data?.worldSize[worldId] ? : }
} return <>
{gameInfo.data?.worlds.nodes[worldId].title && `World: ${gameInfo.data?.worlds.nodes[worldId].title}`}
{levelId && `Level ${levelId}`}{level?.data?.title && `: ${level?.data?.title}`}
{/*
{level?.data?.introduction}
*/}
{level?.data?.introduction}
{/*

Aufgabe:

*/} {level?.data?.descrText}
{level?.data?.descrFormat}
{commandLineMode && } { setCommandLineMode(!commandLineMode) }} />} label="Editor mode" />
{editorConnection &&
} {completed &&
{level?.data?.conclusion} {levelId >= gameInfo.data?.worldSize[worldId] ? : }
}
{!level.isLoading && }

Display Tactic documentation here?

} export default Level function useLevelEditor(worldId: string, levelId: number, codeviewRef, initialCode, onDidChangeContent) { const connection = React.useContext(ConnectionContext) const [editor, setEditor] = useState(null) const [infoProvider, setInfoProvider] = useState(null) const [infoviewApi, setInfoviewApi] = useState(null) const [editorConnection, setEditorConnection] = useState(null) // Create Editor useEffect(() => { const editor = monaco.editor.create(codeviewRef.current!, { glyphMargin: true, quickSuggestions: false, lightbulb: { enabled: true }, unicodeHighlight: { ambiguousCharacters: false, }, automaticLayout: true, minimap: { enabled: false }, lineNumbersMinChars: 3, 'semanticHighlighting.enabled': true, theme: 'vs-code-theme-converted' }) const infoProvider = new InfoProvider(connection.getLeanClient()) const editorApi = infoProvider.getApi() const editorEvents: EditorEvents = { initialize: new EventEmitter(), gotServerNotification: new EventEmitter(), sentClientNotification: new EventEmitter(), serverRestarted: new EventEmitter(), serverStopped: new EventEmitter(), changedCursorLocation: new EventEmitter(), changedInfoviewConfig: new EventEmitter(), runTestScript: new EventEmitter(), requestedAction: new EventEmitter(), }; // Challenge: write a type-correct fn from `Eventify` to `T` without using `any` const infoviewApi: InfoviewApi = { initialize: async l => editorEvents.initialize.fire(l), gotServerNotification: async (method, params) => { editorEvents.gotServerNotification.fire([method, params]); }, sentClientNotification: async (method, params) => { editorEvents.sentClientNotification.fire([method, params]); }, serverRestarted: async r => editorEvents.serverRestarted.fire(r), serverStopped: async serverStoppedReason => { editorEvents.serverStopped.fire(serverStoppedReason) }, changedCursorLocation: async loc => editorEvents.changedCursorLocation.fire(loc), changedInfoviewConfig: async conf => editorEvents.changedInfoviewConfig.fire(conf), requestedAction: async action => editorEvents.requestedAction.fire(action), // See https://rollupjs.org/guide/en/#avoiding-eval // eslint-disable-next-line @typescript-eslint/no-implied-eval runTestScript: async script => new Function(script)(), getInfoviewHtml: async () => document.body.innerHTML, }; const ec = new EditorConnection(editorApi, editorEvents); setEditorConnection(ec) editorEvents.initialize.on((loc: Location) => ec.events.changedCursorLocation.fire(loc)) setEditor(editor) setInfoProvider(infoProvider) setInfoviewApi(infoviewApi) return () => { infoProvider.dispose(); editor.dispose() } }, []) const {leanClient, leanClientStarted} = useLeanClient() // Create model when level changes useEffect(() => { if (editor && leanClientStarted) { const uri = monaco.Uri.parse(`file:///${worldId}/${levelId}`) let model = monaco.editor.getModel(uri) if (!model) { model = monaco.editor.createModel(initialCode, 'lean4', uri) } model.onDidChangeContent(() => onDidChangeContent(model.getValue())) editor.setModel(model) editor.setPosition(model.getFullModelRange().getEndPosition()) infoviewApi.serverRestarted(leanClient.initializeResult) infoProvider.openPreview(editor, infoviewApi) const taskGutter = new LeanTaskGutter(infoProvider.client, editor) const abbrevRewriter = new AbbreviationRewriter(new AbbreviationProvider(), model, editor) return () => { abbrevRewriter.dispose(); taskGutter.dispose(); } } }, [editor, levelId, connection, leanClientStarted]) return {editor, infoProvider, editorConnection} } /** Open all files in this world on the server so that they will load faster when accessed */ function useLoadWorldFiles(worldId) { const gameInfo = useGetGameInfoQuery() const store = useStore() useEffect(() => { if (gameInfo.data) { const models = [] for (let levelId = 1; levelId <= gameInfo.data.worldSize[worldId]; levelId++) { const uri = monaco.Uri.parse(`file:///${worldId}/${levelId}`) let model = monaco.editor.getModel(uri) if (model) { models.push(model) } else { const code = selectCode(worldId, levelId)(store.getState()) models.push(monaco.editor.createModel(code, 'lean4', uri)) } } return () => { for (let model of models) { model.dispose() } } } }, [gameInfo.data, worldId]) }