Compare commits

..
Author SHA1 Message Date
joneugster bea342b2c7 work on network interruption errors 2023-10-27 21:02:20 +02:00
joneugster 940663f640 improve loading of inventory doc popups #138 2023-10-27 18:48:02 +02:00
17 changed files with 834 additions and 1700 deletions
-2
View File
@@ -1,6 +1,4 @@
node_modules node_modules
client/dist client/dist
server/build server/build
server/lakefile.olean
**/lake-packages/ **/lake-packages/
**/.DS_Store
+9
View File
@@ -15,6 +15,9 @@ npm install -g http-server
``` ```
git clone https://github.com/hhu-adam/NNG4.git git clone https://github.com/hhu-adam/NNG4.git
cd NNG4
docker rmi nng4:latest
docker build --pull --rm -f "Dockerfile" -t nng4:latest "."
``` ```
@@ -143,6 +146,12 @@ Activate config:
``` ```
## Install `unzip` for Importing Docker Images
```
sudo apt-get install unzip
```
## Install bubblewrap (bwrap) ## Install bubblewrap (bwrap)
``` ```
sudo apt-get install bubblewrap sudo apt-get install bubblewrap
+1 -1
View File
@@ -33,7 +33,7 @@
</p> </p>
</div> </div>
</noscript> </noscript>
<script type="module" src="/client/src/index.tsx"></script> <script src="bundle.js"></script>
</body> </body>
</html> </html>
+54 -34
View File
@@ -1,6 +1,7 @@
/* Partly copied from https://github.com/leanprover/vscode-lean4/blob/master/lean4-infoview/src/infoview/main.tsx */ /* Partly copied from https://github.com/leanprover/vscode-lean4/blob/master/lean4-infoview/src/infoview/main.tsx */
import * as React from 'react'; import * as React from 'react';
import {useEffect} from 'react';
import type { DidCloseTextDocumentParams, DidChangeTextDocumentParams, Location, DocumentUri } from 'vscode-languageserver-protocol'; import type { DidCloseTextDocumentParams, DidChangeTextDocumentParams, Location, DocumentUri } from 'vscode-languageserver-protocol';
import 'tachyons/css/tachyons.css'; import 'tachyons/css/tachyons.css';
@@ -327,6 +328,17 @@ export function TypewriterInterfaceWrapper(props: { world: string, level: number
// it's important not to reconstruct the `WithBlah` wrappers below since they contain state // it's important not to reconstruct the `WithBlah` wrappers below since they contain state
// that we want to persist. // that we want to persist.
// Catch loss of internet connection
try {
const editor = React.useContext(MonacoEditorContext)
const model = editor.getModel()
if (!model) {
return <p>no internet?</p>
}
} catch {
return <p>no internet??</p>
}
if (!serverVersion) { return <></> } if (!serverVersion) { return <></> }
if (serverStoppedResult) { if (serverStoppedResult) {
return <div> return <div>
@@ -338,19 +350,51 @@ export function TypewriterInterfaceWrapper(props: { world: string, level: number
return <TypewriterInterface props={props} /> return <TypewriterInterface props={props} />
} }
/** Delete all proof lines starting from a given line.
* Note that the first line (i.e. deleting everything) is `1`!
*/
function deleteProof(line: number) {
const editor = React.useContext(MonacoEditorContext)
const { proof } = React.useContext(ProofContext)
const { setSelectedStep } = React.useContext(SelectionContext)
const { setDeletedChat, showHelp } = React.useContext(DeletedChatContext)
const { setTypewriterInput } = React.useContext(InputModeContext)
return (ev) => {
if (editor) {
const model = editor.getModel()
if (model) {
let deletedChat: Array<GameHint> = []
proof.slice(line).map((step, i) => {
// Only add these hidden hints to the deletion stack which were visible
deletedChat = [...deletedChat, ...step.hints.filter(hint => (!hint.hidden || showHelp.has(line + i)))]
})
setDeletedChat(deletedChat)
editor.executeEdits("typewriter", [{
range: monaco.Selection.fromPositions(
{ lineNumber: line, column: 1 },
model.getFullModelRange().getEndPosition()
),
text: '',
forceMoveMarkers: false
}])
setSelectedStep(undefined)
setTypewriterInput(proof[line].command)
ev.stopPropagation()
}
}
}
}
/** The interface in command line mode */ /** The interface in command line mode */
export function TypewriterInterface({props}) { export function TypewriterInterface({props}) {
const ec = React.useContext(EditorContext)
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const editor = React.useContext(MonacoEditorContext) const editor = React.useContext(MonacoEditorContext)
const model = editor.getModel()
const uri = model.uri.toString()
const [disableInput, setDisableInput] = React.useState<boolean>(false) const [disableInput, setDisableInput] = React.useState<boolean>(false)
const { setDeletedChat, showHelp, setShowHelp } = React.useContext(DeletedChatContext) const { showHelp, setShowHelp } = React.useContext(DeletedChatContext)
const {mobile} = React.useContext(MobileContext) const {mobile} = React.useContext(MobileContext)
const { proof } = React.useContext(ProofContext) const { proof } = React.useContext(ProofContext)
const { setTypewriterInput } = React.useContext(InputModeContext)
const { selectedStep, setSelectedStep } = React.useContext(SelectionContext) const { selectedStep, setSelectedStep } = React.useContext(SelectionContext)
const proofPanelRef = React.useRef<HTMLDivElement>(null) const proofPanelRef = React.useRef<HTMLDivElement>(null)
@@ -358,33 +402,9 @@ export function TypewriterInterface({props}) {
// const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig; // const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
// const curUri = useEventResult(ec.events.changedCursorLocation, loc => loc?.uri); // const curUri = useEventResult(ec.events.changedCursorLocation, loc => loc?.uri);
const rpcSess = useRpcSessionAtPos({uri: uri, line: 0, character: 0}) // rpc session
// editor, model or uri might be null if connection is broken
/** Delete all proof lines starting from a given line. const rpcSess = useRpcSessionAtPos({uri: editor?.getModel()?.uri?.toString() ?? '', line: 0, character: 0})
* Note that the first line (i.e. deleting everything) is `1`!
*/
function deleteProof(line: number) {
return (ev) => {
let deletedChat: Array<GameHint> = []
proof.slice(line).map((step, i) => {
// Only add these hidden hints to the deletion stack which were visible
deletedChat = [...deletedChat, ...step.hints.filter(hint => (!hint.hidden || showHelp.has(line + i)))]
})
setDeletedChat(deletedChat)
editor.executeEdits("typewriter", [{
range: monaco.Selection.fromPositions(
{ lineNumber: line, column: 1 },
editor.getModel().getFullModelRange().getEndPosition()
),
text: '',
forceMoveMarkers: false
}])
setSelectedStep(undefined)
setTypewriterInput(proof[line].command)
ev.stopPropagation()
}
}
function toggleSelectStep(line: number) { function toggleSelectStep(line: number) {
return (ev) => { return (ev) => {
@@ -399,8 +419,8 @@ export function TypewriterInterface({props}) {
} }
} }
// Scroll to the end of the proof if it is updated. // Scroll to the end of the proof if it is updated.
React.useEffect(() => { React.useEffect(() => {
if (proof?.length > 1) { if (proof?.length > 1) {
proofPanelRef.current?.lastElementChild?.scrollIntoView() //scrollTo(0,0) proofPanelRef.current?.lastElementChild?.scrollIntoView() //scrollTo(0,0)
} else { } else {
+17 -12
View File
@@ -68,8 +68,8 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
/** Reference to the hidden multi-line editor */ /** Reference to the hidden multi-line editor */
const editor = React.useContext(MonacoEditorContext) const editor = React.useContext(MonacoEditorContext)
const model = editor.getModel() const model = editor?.getModel()
const uri = model.uri.toString() const uri = model?.uri?.toString()
const [oneLineEditor, setOneLineEditor] = useState<monaco.editor.IStandaloneCodeEditor>(null) const [oneLineEditor, setOneLineEditor] = useState<monaco.editor.IStandaloneCodeEditor>(null)
const [processing, setProcessing] = useState(false) const [processing, setProcessing] = useState(false)
@@ -91,11 +91,15 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
*/ */
const loadAllGoals = React.useCallback(() => { const loadAllGoals = React.useCallback(() => {
if (!model || ! uri) {
return
}
let goalCalls = [] let goalCalls = []
let msgCalls = [] let msgCalls = []
// For each line of code ask the server for the goals and the messages on this line // For each line of code ask the server for the goals and the messages on this line
for (let i = 0; i < model.getLineCount(); i++) { for (let i = 0; i < model?.getLineCount(); i++) {
goalCalls.push( goalCalls.push(
rpcSess.call('Game.getInteractiveGoals', DocumentPosition.toTdpp({line: i, character: 0, uri: uri})) rpcSess.call('Game.getInteractiveGoals', DocumentPosition.toTdpp({line: i, character: 0, uri: uri}))
) )
@@ -158,7 +162,7 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
// with no goals there will be no hints. // with no goals there will be no hints.
let hints : GameHint[] = goals.goals.length ? goals.goals[0].hints : [] let hints : GameHint[] = goals.goals.length ? goals.goals[0].hints : []
console.debug(`Command (${i}): `, i ? model.getLineContent(i) : '') console.debug(`Command (${i}): `, i ? model?.getLineContent(i) : '')
console.debug(`Goals: (${i}): `, goalsToString(goals)) // console.debug(`Goals: (${i}): `, goalsToString(goals)) //
console.debug(`Hints: (${i}): `, hints) console.debug(`Hints: (${i}): `, hints)
console.debug(`Errors: (${i}): `, messages) console.debug(`Errors: (${i}): `, messages)
@@ -166,7 +170,7 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
tmpProof.push({ tmpProof.push({
// the command of the line above. Note that `getLineContent` starts counting // the command of the line above. Note that `getLineContent` starts counting
// at `1` instead of `zero`. The first ProofStep will have an empty command. // at `1` instead of `zero`. The first ProofStep will have an empty command.
command: i ? model.getLineContent(i) : '', command: i ? model?.getLineContent(i) : '',
// TODO: store correct data // TODO: store correct data
goals: goals.goals, goals: goals.goals,
// only need the hints of the active goals in chat // only need the hints of the active goals in chat
@@ -185,6 +189,7 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
// Run the command // Run the command
const runCommand = React.useCallback(() => { const runCommand = React.useCallback(() => {
if (processing) {return} if (processing) {return}
if (!uri) {return}
// TODO: Desired logic is to only reset this after a new *error-free* command has been entered // TODO: Desired logic is to only reset this after a new *error-free* command has been entered
setDeletedChat([]) setDeletedChat([])
@@ -195,7 +200,7 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
editor.executeEdits("typewriter", [{ editor.executeEdits("typewriter", [{
range: monaco.Selection.fromPositions( range: monaco.Selection.fromPositions(
pos, pos,
editor.getModel().getFullModelRange().getEndPosition() model.getFullModelRange().getEndPosition()
), ),
text: typewriterInput.trim() + "\n", text: typewriterInput.trim() + "\n",
forceMoveMarkers: false forceMoveMarkers: false
@@ -204,7 +209,7 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
} }
editor.setPosition(pos) editor.setPosition(pos)
}, [typewriterInput, editor]) }, [typewriterInput, editor, model])
useEffect(() => { useEffect(() => {
if (oneLineEditor && oneLineEditor.getValue() !== typewriterInput) { if (oneLineEditor && oneLineEditor.getValue() !== typewriterInput) {
@@ -220,12 +225,14 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
// React when answer from the server comes back // React when answer from the server comes back
useServerNotificationEffect('textDocument/publishDiagnostics', (params: PublishDiagnosticsParams) => { useServerNotificationEffect('textDocument/publishDiagnostics', (params: PublishDiagnosticsParams) => {
if (!uri) {return}
if (params.uri == uri) { if (params.uri == uri) {
setProcessing(false) setProcessing(false)
loadAllGoals() loadAllGoals()
if (!hasErrors(params.diagnostics)) { if (!hasErrors(params.diagnostics)) {
//setTypewriterInput("") //setTypewriterInput("")
editor.setPosition(editor.getModel().getFullModelRange().getEndPosition()) editor.setPosition(model.getFullModelRange().getEndPosition())
} }
} else { } else {
// console.debug(`expected uri: ${uri}, got: ${params.uri}`) // console.debug(`expected uri: ${uri}, got: ${params.uri}`)
@@ -234,7 +241,7 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
// TODO: This is the wrong place apparently. Where do wee need to load them? // TODO: This is the wrong place apparently. Where do wee need to load them?
// TODO: instead of loading all goals every time, we could only load the last one // TODO: instead of loading all goals every time, we could only load the last one
// loadAllGoals() // loadAllGoals()
}, [uri]); }, [uri, editor, model]);
useEffect(() => { useEffect(() => {
const myEditor = monaco.editor.create(inputRef.current!, { const myEditor = monaco.editor.create(inputRef.current!, {
@@ -304,10 +311,8 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
// BUG: Causes `file closed` error // BUG: Causes `file closed` error
//TODO: Intention is to run once when loading, does that work? //TODO: Intention is to run once when loading, does that work?
useEffect(() => { useEffect(() => {
console.debug(`time to update: ${uri} \n ${rpcSess}`)
console.debug(rpcSess)
loadAllGoals() loadAllGoals()
}, [rpcSess]) }, [])
/** Process the entered command */ /** Process the entered command */
const handleSubmit : React.FormEventHandler<HTMLFormElement> = (ev) => { const handleSubmit : React.FormEventHandler<HTMLFormElement> = (ev) => {
+21 -4
View File
@@ -6,6 +6,7 @@ import { faLock, faBan } from '@fortawesome/free-solid-svg-icons'
import { GameIdContext } from '../app'; import { GameIdContext } from '../app';
import Markdown from './markdown'; import Markdown from './markdown';
import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview, useLoadInventoryOverviewQuery } from '../state/api'; import { useLoadDocQuery, InventoryTile, LevelInfo, InventoryOverview, useLoadInventoryOverviewQuery } from '../state/api';
import { QueryStatus } from '@reduxjs/toolkit/query/react'
import { selectDifficulty, selectInventory } from '../state/progress'; import { selectDifficulty, selectInventory } from '../state/progress';
import { store } from '../state/store'; import { store } from '../state/store';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
@@ -114,16 +115,32 @@ function InventoryItem({name, displayName, locked, disabled, newly, showDoc, ena
return <div className={`item ${className}${enableAll ? ' enabled' : ''}`} onClick={handleClick} title={title}>{icon} {displayName}</div> return <div className={`item ${className}${enableAll ? ' enabled' : ''}`} onClick={handleClick} title={title}>{icon} {displayName}</div>
} }
/** Wrapper to catch rejected/pending queries. */
function DocContent({doc}) {
switch(doc.status) {
case QueryStatus.fulfilled:
return <>
<h1 className="doc">{doc.data.displayName}</h1>
<p><code>{doc.data.statement}</code></p>
{/* <code>docstring: {doc.data.docstring}</code> */}
<Markdown>{doc.data.content}</Markdown>
</>
case QueryStatus.rejected:
return <p>Looks like there is a connection problem!</p>
case QueryStatus.pending:
return <p>Loading...</p>
default:
return <></>
}
}
export function Documentation({name, type, handleClose}) { export function Documentation({name, type, handleClose}) {
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const doc = useLoadDocQuery({game: gameId, type: type, name: name}) const doc = useLoadDocQuery({game: gameId, type: type, name: name})
return <div className="documentation"> return <div className="documentation">
<div className="codicon codicon-close modal-close" onClick={handleClose}></div> <div className="codicon codicon-close modal-close" onClick={handleClose}></div>
<h1 className="doc">{doc.data?.displayName}</h1> <DocContent doc={doc} />
<p><code>{doc.data?.statement}</code></p>
{/* <code>docstring: {doc.data?.docstring}</code> */}
<Markdown>{doc.data?.content}</Markdown>
</div> </div>
} }
+107 -121
View File
@@ -237,8 +237,6 @@ function PlayableLevel({impressum, setImpressum}) {
const [inventoryDoc, setInventoryDoc] = useState<{name: string, type: string}>(null) const [inventoryDoc, setInventoryDoc] = useState<{name: string, type: string}>(null)
function closeInventoryDoc () {setInventoryDoc(null)} function closeInventoryDoc () {setInventoryDoc(null)}
const onDidChangeContent = (code) => { const onDidChangeContent = (code) => {
dispatch(codeEdited({game: gameId, world: worldId, level: levelId, code})) dispatch(codeEdited({game: gameId, world: worldId, level: levelId, code}))
} }
@@ -254,30 +252,30 @@ function PlayableLevel({impressum, setImpressum}) {
const {editor, infoProvider, editorConnection} = const {editor, infoProvider, editorConnection} =
useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection) useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection)
/** Unused. Was implementing an undo button, which has been replaced by `deleteProof` inside // /** Unused. Was implementing an undo button, which has been replaced by `deleteProof` inside
* `TypewriterInterface`. // * `TypewriterInterface`.
*/ // */
const handleUndo = () => { // const handleUndo = () => {
const endPos = editor.getModel().getFullModelRange().getEndPosition() // const endPos = editor.getModel().getFullModelRange().getEndPosition()
let range // let range
console.log(endPos.column) // console.log(endPos.column)
if (endPos.column === 1) { // if (endPos.column === 1) {
range = monaco.Selection.fromPositions( // range = monaco.Selection.fromPositions(
new monaco.Position(endPos.lineNumber - 1, 1), // new monaco.Position(endPos.lineNumber - 1, 1),
endPos // endPos
) // )
} else { // } else {
range = monaco.Selection.fromPositions( // range = monaco.Selection.fromPositions(
new monaco.Position(endPos.lineNumber, 1), // new monaco.Position(endPos.lineNumber, 1),
endPos // endPos
) // )
} // }
editor.executeEdits("undo-button", [{ // editor.executeEdits("undo-button", [{
range, // range,
text: "", // text: "",
forceMoveMarkers: false // forceMoveMarkers: false
}]); // }]);
} // }
// Select and highlight proof steps and corresponding hints // Select and highlight proof steps and corresponding hints
// TODO: with the new design, there is no difference between the introduction and // TODO: with the new design, there is no difference between the introduction and
@@ -296,28 +294,31 @@ function PlayableLevel({impressum, setImpressum}) {
setTypewriterMode(false) setTypewriterMode(false)
if (editor) { if (editor) {
let code = editor.getModel().getLinesContent() let model = editor.getModel()
if (model) {
let code = model.getLinesContent()
// console.log(`insert. code: ${code}`) // console.log(`insert. code: ${code}`)
// console.log(`insert. join: ${code.join('')}`) // console.log(`insert. join: ${code.join('')}`)
// console.log(`insert. trim: ${code.join('').trim()}`) // console.log(`insert. trim: ${code.join('').trim()}`)
// console.log(`insert. length: ${code.join('').trim().length}`) // console.log(`insert. length: ${code.join('').trim().length}`)
// console.log(`insert. range: ${editor.getModel().getFullModelRange()}`) // console.log(`insert. range: ${editor.getModel().getFullModelRange()}`)
// TODO: It does seem that the template is always indented by spaces. // TODO: It does seem that the template is always indented by spaces.
// This is a hack, assuming there are exactly two. // This is a hack, assuming there are exactly two.
if (!code.join('').trim().length) { if (!code.join('').trim().length) {
console.debug(`inserting template:\n${level.data.template}`) console.debug(`inserting template:\n${level.data.template}`)
// TODO: This does not work! HERE // TODO: This does not work! HERE
// Probably overwritten by a query to the server // Probably overwritten by a query to the server
editor.executeEdits("template-writer", [{ editor.executeEdits("template-writer", [{
range: editor.getModel().getFullModelRange(), range: model.getFullModelRange(),
text: level.data.template + `\n`, text: level.data.template + `\n`,
forceMoveMarkers: true forceMoveMarkers: true
}]) }])
} else { } else {
console.debug(`not inserting template.`) console.debug(`not inserting template.`)
}
} }
} }
} else { } else {
@@ -335,17 +336,49 @@ function PlayableLevel({impressum, setImpressum}) {
setShowHelp(new Set(selectHelp(gameId, worldId, levelId)(store.getState()))) setShowHelp(new Set(selectHelp(gameId, worldId, levelId)(store.getState())))
}, [gameId, worldId, levelId]) }, [gameId, worldId, levelId])
// switching editor mode
useEffect(() => { useEffect(() => {
if (!typewriterMode) { if (editor) {
// Delete last input attempt from command line let model = editor.getModel()
editor.executeEdits("typewriter", [{ if (model) {
range: editor.getSelection(), if (typewriterMode) {
text: "", // typewriter gets enabled
forceMoveMarkers: false let code = model.getLinesContent().filter(line => line.trim())
}]); editor.executeEdits("typewriter", [{
editor.focus() range: model.getFullModelRange(),
text: code.length ? code.join('\n') + '\n' : '',
forceMoveMarkers: true
}])
// let endPos = model.getFullModelRange().getEndPosition()
// if (model.getLineContent(endPos.lineNumber).trim() !== "") {
// editor.executeEdits("typewriter", [{
// range: monaco.Selection.fromPositions(endPos, endPos),
// text: "\n",
// forceMoveMarkers: true
// }]);
// }
// let endPos = model.getFullModelRange().getEndPosition()
// let currPos = editor.getPosition()
// if (currPos.column != 1 || (currPos.lineNumber != endPos.lineNumber && currPos.lineNumber != endPos.lineNumber - 1)) {
// // This is not a position that would naturally occur from Typewriter, reset:
// editor.setSelection(monaco.Selection.fromPositions(endPos, endPos))
// }
} else {
// typewriter gets disabled
// delete last input attempt from command line
editor.executeEdits("typewriter", [{
range: editor.getSelection(),
text: "",
forceMoveMarkers: false
}]);
editor.focus()
}
}
} }
}, [typewriterMode]) }, [editor, typewriterMode])
useEffect(() => { useEffect(() => {
// Forget whether hidden hints are displayed for steps that don't exist yet // Forget whether hidden hints are displayed for steps that don't exist yet
@@ -363,33 +396,6 @@ function PlayableLevel({impressum, setImpressum}) {
} }
}, [showHelp]) }, [showHelp])
// Effect when command line mode gets enabled
useEffect(() => {
if (editor && typewriterMode) {
let code = editor.getModel().getLinesContent().filter(line => line.trim())
editor.executeEdits("typewriter", [{
range: editor.getModel().getFullModelRange(),
text: code.length ? code.join('\n') + '\n' : '',
forceMoveMarkers: true
}]);
// let endPos = editor.getModel().getFullModelRange().getEndPosition()
// if (editor.getModel().getLineContent(endPos.lineNumber).trim() !== "") {
// editor.executeEdits("typewriter", [{
// range: monaco.Selection.fromPositions(endPos, endPos),
// text: "\n",
// forceMoveMarkers: true
// }]);
// }
// let endPos = editor.getModel().getFullModelRange().getEndPosition()
// let currPos = editor.getPosition()
// if (currPos.column != 1 || (currPos.lineNumber != endPos.lineNumber && currPos.lineNumber != endPos.lineNumber - 1)) {
// // This is not a position that would naturally occur from Typewriter, reset:
// editor.setSelection(monaco.Selection.fromPositions(endPos, endPos))
// }
}
}, [editor, typewriterMode])
return <> return <>
<div style={level.isLoading ? null : {display: "none"}} className="app-content loading"><CircularProgress /></div> <div style={level.isLoading ? null : {display: "none"}} className="app-content loading"><CircularProgress /></div>
<DeletedChatContext.Provider value={{deletedChat, setDeletedChat, showHelp, setShowHelp}}> <DeletedChatContext.Provider value={{deletedChat, setDeletedChat, showHelp, setShowHelp}}>
@@ -483,33 +489,9 @@ function Introduction({impressum, setImpressum}) {
<InventoryPanel levelInfo={inventory?.data} /> <InventoryPanel levelInfo={inventory?.data} />
</Split> </Split>
} }
</> </>
} }
// {mobile?
// // TODO: This is copied from the `Split` component below...
// <>
// <div className={`app-content level-mobile ${level.isLoading ? 'hidden' : ''}`}>
// <ExercisePanel
// impressum={impressum}
// closeImpressum={closeImpressum}
// codeviewRef={codeviewRef}
// visible={pageNumber == 0} />
// <InventoryPanel levelInfo={level?.data} visible={pageNumber == 1} />
// </div>
// </>
// :
// <Split minSize={0} snapOffset={200} sizes={[25, 50, 25]} className={`app-content level ${level.isLoading ? 'hidden' : ''}`}>
// <ChatPanel lastLevel={lastLevel}/>
// <ExercisePanel
// impressum={impressum}
// closeImpressum={closeImpressum}
// codeviewRef={codeviewRef} />
// <InventoryPanel levelInfo={level?.data} />
// </Split>
// }
function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection) { function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection) {
const connection = React.useContext(ConnectionContext) const connection = React.useContext(ConnectionContext)
@@ -604,17 +586,19 @@ function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChange
if (!model) { if (!model) {
model = monaco.editor.createModel(initialCode, 'lean4', uri) model = monaco.editor.createModel(initialCode, 'lean4', uri)
} }
model.onDidChangeContent(() => onDidChangeContent(model.getValue())) if (model) { // in case of broken pipe, this remains null
editor.onDidChangeCursorSelection(() => onDidChangeSelection(editor.getSelections())) model.onDidChangeContent(() => onDidChangeContent(model.getValue()))
editor.setModel(model) editor.onDidChangeCursorSelection(() => onDidChangeSelection(editor.getSelections()))
if (initialSelections) { editor.setModel(model)
console.debug("Initial Selection: ", initialSelections) if (initialSelections) {
// BUG: Somehow I get an `invalid arguments` bug here console.debug("Initial Selection: ", initialSelections)
// editor.setSelections(initialSelections) // BUG: Somehow I get an `invalid arguments` bug here
} // editor.setSelections(initialSelections)
}
return () => { return () => {
editorConnection.api.sendClientNotification(uriStr, "textDocument/didClose", {textDocument: {uri: uriStr}}) editorConnection.api.sendClientNotification(uriStr, "textDocument/didClose", {textDocument: {uri: uriStr}})
model.dispose(); }
} }
} }
}, [editor, levelId, connection, leanClientStarted]) }, [editor, levelId, connection, leanClientStarted])
@@ -624,14 +608,16 @@ function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChange
if (editor && leanClientStarted) { if (editor && leanClientStarted) {
let model = monaco.editor.getModel(uri) let model = monaco.editor.getModel(uri)
infoviewApi.serverRestarted(leanClient.initializeResult) if (model) {
infoviewApi.serverRestarted(leanClient.initializeResult)
infoProvider.openPreview(editor, infoviewApi) infoProvider.openPreview(editor, infoviewApi)
const taskGutter = new LeanTaskGutter(infoProvider.client, editor) const taskGutter = new LeanTaskGutter(infoProvider.client, editor)
const abbrevRewriter = new AbbreviationRewriter(new AbbreviationProvider(), model, editor) const abbrevRewriter = new AbbreviationRewriter(new AbbreviationProvider(), model, editor)
return () => { abbrevRewriter.dispose(); taskGutter.dispose(); } return () => { abbrevRewriter.dispose(); taskGutter.dispose(); }
}
} }
}, [editor, connection, leanClientStarted]) }, [editor, connection, leanClientStarted])
@@ -657,7 +643,7 @@ function useLoadWorldFiles(worldId) {
models.push(monaco.editor.createModel(code, 'lean4', uri)) models.push(monaco.editor.createModel(code, 'lean4', uri))
} }
} }
return () => { for (let model of models) { model.dispose() } } return () => { for (let model of models) { try {model.dispose()} catch {console.log(`failed to dispose model ${model}`)}} }
} }
}, [gameInfo.data, worldId]) }, [gameInfo.data, worldId])
} }
+6
View File
@@ -42,6 +42,12 @@ body {
border-radius: .3em; border-radius: .3em;
} }
/* Hide monaco editor notifications */
.monaco-workbench > .notifications-toasts.visible {
display: none !important;
}
.loading { .loading {
margin: auto; margin: auto;
height: 100%; height: 100%;
+20 -36
View File
@@ -1,45 +1,29 @@
import * as React from 'react' import * as React from 'react';
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client';
import App from './app' import App from './app';
import { ConnectionContext, connection } from './connection' import { ConnectionContext, connection } from './connection'
import { store } from './state/store' import { store } from './state/store';
import { Provider } from 'react-redux' import { Provider } from 'react-redux';
import type { RouteObject } from "react-router" import {
import { createHashRouter, RouterProvider, Route, redirect } from "react-router-dom" createHashRouter,
import ErrorPage from './components/error_page' RouterProvider,
import Welcome from './components/welcome' Route,
import LandingPage from './components/landing_page' } from "react-router-dom";
import Level from './components/level' import ErrorPage from './components/error_page';
import { monacoSetup } from 'lean4web/client/src/monacoSetup' import Welcome from './components/welcome';
import LandingPage from './components/landing_page';
import Level from './components/level';
import { monacoSetup } from 'lean4web/client/src/monacoSetup';
import { redirect } from 'react-router-dom';
monacoSetup() monacoSetup()
// // Do not show the landing page in the dev-container context
// let root_path: RouteObject = (process.env.LEAN4GAME_SINGLE_GAME == "true") ? {
// path: "/",
// loader: () => redirect("/g/local/game")
// } : {
// path: "/",
// element: <LandingPage />,
// }
// If `VITE_LEAN4GAME_SINGLE` is set to true, then `/` should be redirected to
// `/g/local/game`. This is used for the devcontainer setup
let single_game = (import.meta.env.VITE_LEAN4GAME_SINGLE == "true")
let root_object: RouteObject = single_game ? {
path: "/",
loader: () => redirect("/g/local/game")
} : {
path: "/",
element: <LandingPage />,
}
const router = createHashRouter([ const router = createHashRouter([
root_object,
{ {
// For backwards compatibility path: "/",
element: <LandingPage />,
},
{
path: "/game/nng", path: "/game/nng",
loader: () => redirect("/g/hhu-adam/NNG4") loader: () => redirect("/g/hhu-adam/NNG4")
}, },
+55
View File
@@ -0,0 +1,55 @@
// This file is a copy of `index.tsx` where the path "/" is redirected to "/g/local/game".
// It is used for the dev. setup where there is only one game in a folder called `game`.
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import App from './app';
import { ConnectionContext, connection } from './connection'
import { store } from './state/store';
import { Provider } from 'react-redux';
import {
createHashRouter,
RouterProvider,
Route,
} from "react-router-dom";
import ErrorPage from './components/error_page';
import Welcome from './components/welcome';
import LandingPage from './components/landing_page';
import Level from './components/level';
import { monacoSetup } from 'lean4web/client/src/monacoSetup';
import { redirect } from 'react-router-dom';
monacoSetup()
const router = createHashRouter([
{
path: "/",
loader: () => redirect("/g/local/game")
},
{
path: "/g/:owner/:repo",
element: <App />,
errorElement: <ErrorPage />,
children: [
{
path: "/g/:owner/:repo",
element: <Welcome />,
},
{
path: "/g/:owner/:repo/world/:worldId/level/:levelId",
element: <Level />,
},
],
},
]);
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(
<React.StrictMode>
<Provider store={store}>
<ConnectionContext.Provider value={connection}>
<RouterProvider router={router} />
</ConnectionContext.Provider>
</Provider>
</React.StrictMode>
);
Vendored
-10
View File
@@ -1,10 +0,0 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_LEAN4GAME_SINGLE: string
// more env variables...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
+524 -1424
View File
File diff suppressed because it is too large Load Diff
+17 -10
View File
@@ -15,7 +15,6 @@
"@reduxjs/toolkit": "^1.9.1", "@reduxjs/toolkit": "^1.9.1",
"@types/cytoscape": "^3.19.9", "@types/cytoscape": "^3.19.9",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react-swc": "^3.4.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cytoscape": "^3.23.0", "cytoscape": "^3.23.0",
"cytoscape-elk": "^2.1.0", "cytoscape-elk": "^2.1.0",
@@ -37,18 +36,21 @@
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"remark-math": "^5.1.1", "remark-math": "^5.1.1",
"request-progress": "^3.0.0", "request-progress": "^3.0.0",
"vite": "^4.5.0",
"vite-plugin-static-copy": "^0.17.0",
"vite-plugin-svgr": "^4.1.0",
"vscode-ws-jsonrpc": "^2.0.1", "vscode-ws-jsonrpc": "^2.0.1",
"web-worker": "^1.2.0", "web-worker": "^1.2.0",
"ws": "^8.11.0" "ws": "^8.11.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.19.3",
"@babel/core": "^7.20.5",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
"@redux-devtools/core": "^3.13.1", "@redux-devtools/core": "^3.13.1",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@types/debounce": "^1.2.1", "@types/debounce": "^1.2.1",
"babel-loader": "^8.3.0",
"concurrently": "^7.6.0", "concurrently": "^7.6.0",
"css-loader": "^6.7.3", "css-loader": "^6.7.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
@@ -57,16 +59,21 @@
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"ts-loader": "^9.4.2", "ts-loader": "^9.4.2",
"typescript": "^4.9.4", "typescript": "^4.9.4",
"url-loader": "^4.1.1" "url-loader": "^4.1.1",
"webpack": "^5.75.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1",
"webpack-shell-plugin-next": "^2.3.1"
}, },
"scripts": { "scripts": {
"start": "concurrently -n server,client -c blue,green \"npm run start_server\" \"npm run start_client\"", "start": "concurrently -n server,client -c blue,green \"npm run start_server\" \"npm run start_client\"",
"start_server": "cd server && lake build && cross-env NODE_ENV=development nodemon -e mjs --exec \"node ./index.mjs\"", "start_server": "cd server && lake build && cross-env NODE_ENV=development nodemon -e mjs --exec \"node ./index.mjs\"",
"start_client": "cross-env NODE_ENV=development vite --host", "start_client": "cross-env NODE_ENV=development webpack-dev-server --hot",
"build": "npm run build_server && npm run build_client", "build": "cross-env NODE_ENV=production webpack",
"build_server": "cd server && lake build", "production": "cross-env NODE_ENV=production node server/index.mjs",
"build_client": "cross-env NODE_ENV=production vite build", "build_robo": "rm -rf ./Robo && git clone https://github.com/hhu-adam/Robo && docker build ./Robo --file ./Robo/Dockerfile --tag g/hhu-adam/robo && rm -rf ./Robo",
"production": "cross-env NODE_ENV=production node server/index.mjs" "build_nng": "rm -rf ./NNG4 && git clone https://github.com/hhu-adam/NNG4 && docker build ./NNG4 --file ./NNG4/Dockerfile --tag g/hhu-adam/nng4 && rm -rf ./NNG4",
"update_lean": "./UPDATE_LEAN.sh"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
+2 -2
View File
@@ -496,7 +496,7 @@ section NotificationHandling
IO.eprintln s!"Got outdated version number: {newVersion} ≤ {oldDoc.meta.version}" IO.eprintln s!"Got outdated version number: {newVersion} ≤ {oldDoc.meta.version}"
else if ¬ changes.isEmpty then else if ¬ changes.isEmpty then
let newDocText := foldDocumentChanges changes oldDoc.meta.text let newDocText := foldDocumentChanges changes oldDoc.meta.text
updateDocument ⟨docId.uri, newVersion, newDocText, .always⟩ updateDocument ⟨docId.uri, newVersion, newDocText⟩
end NotificationHandling end NotificationHandling
@@ -573,7 +573,7 @@ def initAndRunWorker (i o e : FS.Stream) (opts : Options) : IO UInt32 := do
This is because LSP always refers to characters by (line, column), This is because LSP always refers to characters by (line, column),
so if we get the line number correct it shouldn't matter that there so if we get the line number correct it shouldn't matter that there
is a CR there. -/ is a CR there. -/
let meta : DocumentMeta := ⟨doc.uri, doc.version, doc.text.toFileMap, .always⟩ let meta : DocumentMeta := ⟨doc.uri, doc.version, doc.text.toFileMap⟩
let e := e.withPrefix s!"[{param.textDocument.uri}] " let e := e.withPrefix s!"[{param.textDocument.uri}] "
let _ ← IO.setStderr e let _ ← IO.setStderr e
try try
-4
View File
@@ -1,9 +1,5 @@
import GameServer.FileWorker import GameServer.FileWorker
import GameServer.Watchdog import GameServer.Watchdog
import GameServer.Commands
-- TODO: The only reason we import `Commands` is so that it gets built to on `lake build`
-- should we have a different solution?
unsafe def main : List String → IO UInt32 := fun args => do unsafe def main : List String → IO UInt32 := fun args => do
let e ← IO.getStderr let e ← IO.getStderr
+1 -1
View File
@@ -1 +1 @@
leanprover/lean4:v4.2.0 leanprover/lean4:v4.1.0
-39
View File
@@ -1,39 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import { viteStaticCopy } from 'vite-plugin-static-copy'
import svgr from "vite-plugin-svgr"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
svgr({
svgrOptions: {
// svgr options
},
}),
viteStaticCopy({
targets: [
{
src: 'node_modules/@leanprover/infoview/dist/*.production.min.js',
dest: '.'
}
]
})
],
publicDir: "client/public",
server: {
port: 3000,
proxy: {
'/websocket': {
target: 'ws://localhost:8080',
ws: true
},
}
},
resolve: {
alias: {
path: "path-browserify",
},
},
})