rename message to hint

This commit is contained in:
Alexander Bentkamp
2023-01-24 16:37:18 +01:00
parent ecc9a488a2
commit 8c4b995a32
44 changed files with 187 additions and 527 deletions
-152
View File
@@ -1,152 +0,0 @@
import * as React from 'react';
import { useEffect, useState, useRef } from 'react';
import { EditorApi } from '@leanprover/infoview-api'
import { LeanClient } from 'lean4web/client/src/editor/leanclient';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
import * as ls from 'vscode-languageserver-protocol'
import TacticState from './TacticState';
import { useLeanClient } from '../connection';
import { useAppDispatch } from '../hooks';
import { levelCompleted, selectCompleted } from '../state/progress';
// TODO: move into Lean 4 web
function toLanguageServerPosition (pos: monaco.Position): ls.Position {
return {line : pos.lineNumber - 1, character: pos.column - 1}
}
function Infoview({ worldId, levelId, editor, editorApi } : {worldId: string, levelId: number, editor: monaco.editor.IStandaloneCodeEditor, editorApi: EditorApi}) {
const dispatch = useAppDispatch()
const [rpcSession, setRpcSession] = useState<string>()
const [goals, setGoals] = useState<any[]>(null)
const [completed, setCompleted] = useState<boolean>(false)
const [diagnostics, setDiagnostics] = useState<any[]>(undefined)
/* `globalDiagnostics` is a work-around to show something when the proof is complete
but had previous subgoals with `sorry` or errors.
It is displayed whenever there are no goals and no (error)-messages present and the proof
isn't complete. */
const [globalDiagnostics, setGlobalDiagnostics] = useState<any[]>(undefined)
const {uri} = useEditorUri(editor)
const {leanClient, leanClientStarted} = useLeanClient()
const fetchInteractiveGoals = () => {
if (editor && rpcSession && editor.getPosition()) {
const pos = toLanguageServerPosition(editor.getPosition())
leanClient.sendRequest("$/lean/rpc/call", {"method":"Game.getGoals",
"params":{"textDocument":{uri}, "position": pos},
"sessionId":rpcSession,
"textDocument":{uri},
"position": pos
}).then((res) => {
setGoals(res ? res.goals : null)
console.log(goals)
}).catch((err) => {
console.error(err)
})
leanClient.sendRequest("$/lean/rpc/call", {"method":"Game.getDiagnostics",
"params":{"textDocument":{uri}, "lineRange": {start: pos.line, end: pos.line + 1}},
"sessionId":rpcSession,
"textDocument":{uri},
"position": pos
}).then((res) => {
/* Workaround to not display the error `unsolved goals` */
if (res) {res = res.filter(x => x.message.trim() !== 'unsolved goals')}
setDiagnostics(res ? res : undefined)
console.log(res)
}).catch((err) => {
console.error(err)
})
}
}
const checkCompleted = () => {
if (editor && rpcSession && editor.getPosition()) {
const pos = toLanguageServerPosition(editor.getPosition())
// Get all diagnostics independent of cursor position
leanClient.sendRequest("$/lean/rpc/call", {"method":"Game.getDiagnostics",
"params":{"textDocument":{uri},},
"sessionId":rpcSession,
"textDocument":{uri},
"position": pos
}).then((res) => {
// Check that there are no errors and no warnings
const completed = !res.some(({severity}) => severity <= 2)
if (completed) {
dispatch(levelCompleted({world: worldId, level: levelId}))
}
setCompleted(completed)
setGlobalDiagnostics(res)
}).catch((err) => {
console.error(err)
})
}
}
useEffect(() => {
if (editor) {
fetchInteractiveGoals()
checkCompleted()
}
}, [editor, uri, rpcSession]);
useEffect(() => {
if (editorApi && leanClientStarted && uri) {
editorApi.closeRpcSession(rpcSession)
setRpcSession(undefined)
console.log(uri)
editorApi.createRpcSession(uri).then((rpcSession) => {
setRpcSession(rpcSession)
})
}
}, [editorApi, uri]);
useEffect(() => {
if (editor) {
const t = editor.onDidChangeCursorPosition(async (ev) => {
fetchInteractiveGoals()
})
return () => { t.dispose() }
}
}, [editor, rpcSession])
useEffect(() => {
const t = leanClient.didChange(async (ev) => {
fetchInteractiveGoals()
checkCompleted()
})
return () => { t.dispose() }
}, [editor, leanClient, rpcSession])
return (<div>
<TacticState goals={goals} diagnostics={diagnostics} completed={completed}
globalDiagnostics={globalDiagnostics}></TacticState>
</div>)
}
export default Infoview
const useEditorUri = (editor: monaco.editor.IStandaloneCodeEditor) => {
const [uri, setUri] = useState<string>(undefined)
useEffect(() => {
if (editor) {
const t = editor.onDidChangeModel((ev) => {
if (ev.newModelUrl) {
setUri(ev.newModelUrl.toString())
} else {
setUri(undefined)
}
})
return () => {t.dispose()}
}
}, [editor])
return {uri}
}
-1
View File
@@ -20,7 +20,6 @@ import 'lean4web/client/src/editor/infoview.css'
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
import './level.css'
import { ConnectionContext, useLeanClient } from '../connection';
import Infoview from './Infoview';
import { useParams } from 'react-router-dom';
import { useGetGameInfoQuery, useLoadLevelQuery } from '../state/api';
import { codeEdited, selectCode } from '../state/progress';
-142
View File
@@ -1,142 +0,0 @@
import * as React from 'react';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import { Paper, Box, Typography, Alert, FormControlLabel, FormGroup, Switch, Collapse, CircularProgress } from '@mui/material';
import { Accordion, AccordionSummary, AccordionDetails, Divider } from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faArrowPointer } from '@fortawesome/free-solid-svg-icons'
import Markdown from './Markdown';
// TODO: Dead variables (x✝) are not displayed correctly.
function Goal({ goal }) {
const [showHints, setShowHints] = React.useState(false);
const handleHintsChange = () => {
setShowHints((prev) => !prev);
};
const hasObject = typeof goal.objects === "object" && goal.objects.length > 0
const hasAssumption = typeof goal.assumptions === "object" && goal.assumptions.length > 0
const openMessages = typeof goal.messages === "object" ? goal.messages.filter((msg) => ! msg.spoiler) : []
const hints = typeof goal.messages === "object" ? goal.messages.filter((msg) => msg.spoiler) : []
const hasHints = hints.length > 0
return (
<Box sx={{ pl: 2 }}>
{hasObject && <Box><Typography>Objects</Typography>
<List>
{goal.objects.map((item, index) =>
<ListItem key={index}>
<Typography color="primary" sx={{ mr: 1 }}>{item.userName}</Typography> :
<Typography color="secondary" sx={{ ml: 1 }}>{item.type}</Typography>
</ListItem>)}
</List></Box>}
{hasAssumption && <Box><Typography>Assumptions</Typography>
<List>
{goal.assumptions.map((item, index) => <ListItem key={index}><Typography color="primary" sx={{ mr: 1 }}>{item.userName}</Typography> :
<Typography color="secondary" sx={{ ml: 1 }}>{item.type}</Typography></ListItem>)}
</List></Box>}
<Typography>Prove:</Typography>
<Typography color="primary" sx={{ ml: 2 }}>{goal.goal}</Typography>
{openMessages.map((message) => <Alert severity="info" sx={{ mt: 1 }}><Markdown>{message.message}</Markdown></Alert>)}
{hasHints &&
<FormControlLabel
control={<Switch checked={showHints} onChange={handleHintsChange} />}
label="More Help?"
/>}
{hints.map((hint, index) => <Collapse key={index} in={showHints}><Alert severity="info" sx={{ mt: 1 }}><Markdown>{hint.message}</Markdown></Alert></Collapse>)}
</Box>)
}
/* Function to display a goal that is not the main goal. */
function OtherGoal({ goal }) {
const hasObject = typeof goal.objects === "object" && goal.objects.length > 0
const hasAssumption = typeof goal.assumptions === "object" && goal.assumptions.length > 0
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography color="primary" sx={{ ml: 0 }}>⊢ {goal.goal}</Typography>
</AccordionSummary>
<AccordionDetails sx={{ backgroundColor: "aliceblue" }}>
{hasObject &&
<Box>
<Typography>Objects</Typography>
<List>
{goal.objects.map((item, index) =>
<ListItem key={index}>
<Typography color="primary" sx={{ mr: 1 }}>{item.userName}</Typography> :
<Typography color="secondary" sx={{ ml: 1 }}>{item.type}</Typography>
</ListItem>)}
</List>
</Box>}
{hasAssumption &&
<Box>
<Typography>Assumptions</Typography>
<List>
{goal.assumptions.map((item, index) =>
<ListItem key={index}>
<Typography color="primary" sx={{ mr: 1 }}>{item.userName}</Typography> :
<Typography color="secondary" sx={{ ml: 1 }}>{item.type}</Typography>
</ListItem>)}
</List>
</Box>}
<Typography>Prove:</Typography>
<Typography color="primary" sx={{ ml: 2 }}>{goal.goal}</Typography>
</AccordionDetails>
</Accordion>)
}
function TacticState({ goals, diagnostics, completed, globalDiagnostics }) {
const isLoading = goals === null
const hasGoal = goals !== null && goals.length > 0
const hasManyGoal = hasGoal && goals.length > 1
if (!(hasGoal || completed) && globalDiagnostics) {
diagnostics = [{"severity" : 4, "message": "Showing global messages:"}, ...globalDiagnostics]
}
const hasError = typeof diagnostics === "object" && diagnostics.length > 0
return (
<Box sx={{ height: "100%" }}>
{completed && <Typography variant="h6">Level completed ! 🎉</Typography>}
{hasGoal &&
<Paper sx={{ pt: 1, pl: 2, pr: 3, pb: 1, height: "100%" }}>
<Typography variant="h5">Main Goal
(at <FontAwesomeIcon icon={faArrowPointer}></FontAwesomeIcon>)
</Typography>
<Goal goal={goals[0]} />
</Paper>}
{isLoading && <CircularProgress />}
{!(hasGoal || completed || isLoading) &&
<Typography variant="h6">
No goals
(at <FontAwesomeIcon icon={faArrowPointer}></FontAwesomeIcon>)
</Typography>}
{hasError &&
<Paper sx={{ pt: 1, pl: 2, pr: 3, pb: 1, height: "100%" }}>
<Typography variant="h5" sx={{ mb: 2 }}>Lean says</Typography>
{diagnostics.map(({severity, message}, index) =>
<Alert key={index} severity={{1: "error", 2:"warning", 3:"info", 4:"success"}[severity]} sx={{ mt: 1 }}>{message}</Alert>
// TODO: When does Lean give severity=4? Had colour `gray` before. Make custom theme for that
)}
</Paper>}
{hasManyGoal && <Paper sx={{ pt: 1, pl: 2, pr: 3, pb: 1, mt: 1 }}>
<Typography variant="h5" sx={{ mb: 2 }}>Further Goals</Typography>
{goals.slice(1).map((goal, index) => <Paper><OtherGoal key={index} goal={goal} /></Paper>)}
</Paper>}
</Box>
)
}
export default TacticState
+1 -1
View File
@@ -143,7 +143,7 @@ export const Goal = React.memo((props: GoalProps) => {
if (props.goal.goal.isRemoved) cn += 'b--removed '
// TODO: make this prettier
const hints = goal.messages.map((m) => <div>{m.message}</div>)
const hints = goal.hints.map((m) => <div>{m.text}</div>)
if (goal.goal.userName) {
return <details open className={cn}>
+1 -1
View File
@@ -121,7 +121,7 @@ const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
{goals && <Goals filter={{ reverse: false, showType: true, showInstance: true, showHiddenAssumption: true, showLetValue: true }} key='goals' goals={goals} />}
</LocationsContext.Provider>
<FilteredGoals headerChildren='Expected type' key='term-goal'
goals={termGoal !== undefined ? {goals: [{goal:termGoal, messages: []}]} : undefined} />
goals={termGoal !== undefined ? {goals: [{goal:termGoal, hints: []}]} : undefined} />
{userWidgets.map(widget =>
<details key={`widget::${widget.id}::${widget.range?.toString()}`} open>
<summary className='mv2 pointer'>{widget.name}</summary>
+4 -4
View File
@@ -1,13 +1,13 @@
import { InteractiveGoals, InteractiveGoal } from '@leanprover/infoview-api';
export interface GameMessage {
message: string;
spoiler: boolean;
export interface GameHint {
text: string;
hidden: boolean;
}
export interface GameInteractiveGoal {
goal: InteractiveGoal;
messages: GameMessage[];
hints: GameHint[];
}
export interface GameInteractiveGoals {