rename message to hint
parent
ecc9a488a2
commit
8c4b995a32
@ -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,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
|
||||
Loading…
Reference in New Issue