display all proof steps in command line modus

pull/118/head
Jon Eugster 3 years ago
parent 3ce83734f4
commit be039b5de3

@ -13,8 +13,13 @@ import * as leanSyntax from 'lean4web/client/src/syntaxes/lean.json'
import * as leanMarkdownSyntax from 'lean4web/client/src/syntaxes/lean-markdown.json' import * as leanMarkdownSyntax from 'lean4web/client/src/syntaxes/lean-markdown.json'
import * as codeblockSyntax from 'lean4web/client/src/syntaxes/codeblock.json' import * as codeblockSyntax from 'lean4web/client/src/syntaxes/codeblock.json'
import languageConfig from 'lean4/language-configuration.json'; import languageConfig from 'lean4/language-configuration.json';
import { InteractiveDiagnostic, getInteractiveDiagnostics } from '@leanprover/infoview-api';
import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util';
import { useRpcSessionAtPos } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions';
import { InputModeContext, MonacoEditorContext } from './context' import { InputModeContext, MonacoEditorContext, ProofContext, ProofStep } from './context'
import { goalsToString } from './goals'
import { InteractiveGoals } from './rpc_api'
/* We register a new language `leancmd` that looks like lean4, but does not use the lsp server. */ /* We register a new language `leancmd` that looks like lean4, but does not use the lsp server. */
@ -58,10 +63,14 @@ config.autoClosingPairs = config.autoClosingPairs.map(
) )
monaco.languages.setLanguageConfiguration('lean4cmd', config); monaco.languages.setLanguageConfiguration('lean4cmd', config);
/** The input field */
export function CommandLine() { export function CommandLine() {
/** 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 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)
@ -69,26 +78,108 @@ export function CommandLine() {
const inputRef = useRef() const inputRef = useRef()
// The context storing all information about the current proof
const {proof, setProof} = React.useContext(ProofContext)
// TODO: does the position matter at all?
const rpcSess = useRpcSessionAtPos({uri: uri, line: 1, character: 1})
/** Load all goals an messages of the current proof (line-by-line) and save
* the retrieved information into context (`ProofContext`)
*/
const loadAllGoals = React.useCallback(() => {
let goalCalls = []
let msgCalls = []
// 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++) {
goalCalls.push(
rpcSess.call('Game.getInteractiveGoals', DocumentPosition.toTdpp({line: i, character: 0, uri: uri}))
)
msgCalls.push(
getInteractiveDiagnostics(rpcSess, {start: i, end: i+1})
)
}
// Wait for all these requests to be processed before saving the results
Promise.all(goalCalls).then((steps : InteractiveGoals[]) => {
Promise.all(msgCalls).then((diagnostics : [InteractiveDiagnostic[]]) => {
let tmpProof : ProofStep[] = []
steps.map((goals, i) => {
// The first step has an empty command and therefore also no error messages
let messages = i ? diagnostics[i-1] : []
// Filter out the 'unsolved goals' message
messages = messages.filter((msg) => {
return !("append" in msg.message &&
"text" in msg.message.append[0] &&
msg.message.append[0].text === "unsolved goals")
})
// TODO: Check what happens if the code gets into a bad state and no goals are available
if (!goals) {
tmpProof.push({
command: i ? model.getLineContent(i) : '',
goals: [],
hints: [],
errors: messages
} as ProofStep)
} else {
console.debug(`Command (${i}): `, i ? model.getLineContent(i) : '')
console.debug(`Goals: (${i}): `, goalsToString(goals)) //
console.debug(`Hints: (${i}): `, goals.goals[0]?.hints)
console.debug(`Errors: (${i}): `, messages)
// with no goals there will be no hints
let hints = goals.goals.length ? goals.goals[0].hints : []
tmpProof.push({
// the command of the line above. Note that `getLineContent` starts counting
// at `1` instead of `zero`. The first ProofStep will have an empty command.
command: i ? model.getLineContent(i) : '',
// TODO: store correct data
goals: goals.goals || [],
// only need the hints of the active goals in chat
hints: hints,
// errors and messages from the server
errors: messages
} as ProofStep)
}
})
// Save the proof to the context
setProof(tmpProof)
})
})
}, [commandLineInput, editor])
// Run the command // Run the command
const runCommand = React.useCallback(() => { const runCommand = React.useCallback(() => {
if (processing) return; if (processing) {return}
const pos = editor.getPosition() const pos = editor.getPosition()
editor.executeEdits("command-line", [{ if (commandLineInput) {
range: monaco.Selection.fromPositions( setProcessing(true)
pos, editor.executeEdits("command-line", [{
editor.getModel().getFullModelRange().getEndPosition() range: monaco.Selection.fromPositions(
), pos,
text: commandLineInput + "\n", editor.getModel().getFullModelRange().getEndPosition()
forceMoveMarkers: false ),
}]); text: commandLineInput + "\n",
forceMoveMarkers: false
}])
}
editor.setPosition(pos) editor.setPosition(pos)
setProcessing(true)
}, [commandLineInput, editor]) }, [commandLineInput, editor])
useEffect(() => { useEffect(() => {
if (oneLineEditor && oneLineEditor.getValue() !== commandLineInput) { if (oneLineEditor && oneLineEditor.getValue() !== commandLineInput) {
oneLineEditor.setValue(commandLineInput) oneLineEditor.setValue(commandLineInput)
} }
// TODO: Is this the right place to load the goals once initially?
loadAllGoals()
}, [commandLineInput]) }, [commandLineInput])
// React when answer from the server comes back // React when answer from the server comes back
@ -118,6 +209,7 @@ export function CommandLine() {
enabled: false enabled: false
}, },
lineNumbers: 'off', lineNumbers: 'off',
tabSize: 2,
glyphMargin: false, glyphMargin: false,
folding: false, folding: false,
lineDecorationsWidth: 0, lineDecorationsWidth: 0,
@ -146,7 +238,7 @@ export function CommandLine() {
// Ensure that our one-line editor can only have a single line // Ensure that our one-line editor can only have a single line
const l = oneLineEditor.getModel().onDidChangeContent((e) => { const l = oneLineEditor.getModel().onDidChangeContent((e) => {
const value = oneLineEditor.getValue() const value = oneLineEditor.getValue()
setCommandLineInput(value) setCommandLineInput(value.trim())
const newValue = value.replace(/[\n\r]/g, '') const newValue = value.replace(/[\n\r]/g, '')
if (value != newValue) { if (value != newValue) {
oneLineEditor.setValue(newValue) oneLineEditor.setValue(newValue)
@ -161,14 +253,17 @@ export function CommandLine() {
const l = oneLineEditor.onKeyUp((ev) => { const l = oneLineEditor.onKeyUp((ev) => {
if (ev.code === "Enter") { if (ev.code === "Enter") {
runCommand() runCommand()
loadAllGoals() // TODO: instead of loading all goals every time, we could only load the last one
} }
}) })
return () => { l.dispose() } return () => { l.dispose() }
}, [oneLineEditor, runCommand]) }, [oneLineEditor, runCommand])
/** Process the entered command */
const handleSubmit : React.FormEventHandler<HTMLFormElement> = (ev) => { const handleSubmit : React.FormEventHandler<HTMLFormElement> = (ev) => {
ev.preventDefault() ev.preventDefault()
runCommand() runCommand()
loadAllGoals() // TODO: instead of loading all goals every time, we could only load the last one
} }
return <div className="command-line"> return <div className="command-line">
@ -176,7 +271,9 @@ export function CommandLine() {
<div className="command-line-input-wrapper"> <div className="command-line-input-wrapper">
<div ref={inputRef} className="command-line-input" /> <div ref={inputRef} className="command-line-input" />
</div> </div>
<button type="submit" disabled={processing} className="btn btn-inverted"><FontAwesomeIcon icon={faWandMagicSparkles} /> Execute</button> <button type="submit" disabled={processing} className="btn btn-inverted">
<FontAwesomeIcon icon={faWandMagicSparkles} /> Execute
</button>
</form> </form>
</div> </div>
} }

@ -4,39 +4,54 @@
import * as React from 'react'; import * as React from 'react';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js' import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
import { InteractiveDiagnostic, InteractiveTermGoal } from '@leanprover/infoview-api'; import { InteractiveDiagnostic, InteractiveTermGoal } from '@leanprover/infoview-api';
import { InteractiveGoals } from './rpc_api'; import { InteractiveGoal, InteractiveGoals } from './rpc_api';
export const MonacoEditorContext = React.createContext<monaco.editor.IStandaloneCodeEditor>( export const MonacoEditorContext = React.createContext<monaco.editor.IStandaloneCodeEditor>(
null as any) null as any)
// TODO: Is this still used?
export const HintContext = React.createContext<{
showHiddenHints : boolean,
setShowHiddenHints: React.Dispatch<React.SetStateAction<boolean>>
}>({
showHiddenHints: true,
setShowHiddenHints: () => {},
});
export type InfoStatus = 'updating' | 'error' | 'ready'; export type InfoStatus = 'updating' | 'error' | 'ready';
/** One step of the proof */
export type ProofStep = { export type ProofStep = {
// TODO: Add correct types /** The command in this step */
command : string command : string
goals: string /** List of goals *after* this command */
hints: string goals: InteractiveGoal[] // TODO: Add correct type
errors: string /** Story relevant messages */
hints: any // TODO: Add correct type
/** Errors and warnings */
errors: any // TODO: Add correct type
} }
/** The context storing the proof step-by-step for the command line mode */
export const ProofContext = React.createContext<{ export const ProofContext = React.createContext<{
// The first entry will always have an empty/undefined command /** The proof consists of multiple steps that are processed one after the other.
* In particular multi-line terms like `match`-statements will not be supported.
*
* Note that the first step will always have `null` as command
*/
proof: ProofStep[], proof: ProofStep[],
setProof: React.Dispatch<React.SetStateAction<Array<ProofStep>>> setProof: React.Dispatch<React.SetStateAction<Array<ProofStep>>>
}>({ }>({
proof: [], proof: [],
setProof: () => {} setProof: () => {} // TODO: implement me
}) })
// TODO: Is this still used?
export const HintContext = React.createContext<{
showHiddenHints : boolean,
setShowHiddenHints: React.Dispatch<React.SetStateAction<boolean>>
}>({
showHiddenHints: true,
setShowHiddenHints: () => {},
});
export interface ProofStateProps { export interface ProofStateProps {
// pos: DocumentPosition; // pos: DocumentPosition;
status: InfoStatus; status: InfoStatus;

@ -2,8 +2,7 @@
* @fileOverview * @fileOverview
* *
* Mostly copied from https://github.com/leanprover/vscode-lean4/blob/master/lean4-infoview/src/infoview/goals.tsx * Mostly copied from https://github.com/leanprover/vscode-lean4/blob/master/lean4-infoview/src/infoview/goals.tsx
*/ */
import * as React from 'react' import * as React from 'react'
import { InteractiveHypothesisBundle_nonAnonymousNames, MVarId, TaggedText_stripTags } from '@leanprover/infoview-api' import { InteractiveHypothesisBundle_nonAnonymousNames, MVarId, TaggedText_stripTags } from '@leanprover/infoview-api'
import { EditorContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts'; import { EditorContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts';
@ -13,7 +12,6 @@ import { WithTooltipOnHover } from '../../../../node_modules/lean4-infoview/src/
import { InputModeContext } from './context'; import { InputModeContext } from './context';
import { InteractiveGoal, InteractiveGoals, InteractiveHypothesisBundle } from './rpc_api'; import { InteractiveGoal, InteractiveGoals, InteractiveHypothesisBundle } from './rpc_api';
/** Returns true if `h` is inaccessible according to Lean's default name rendering. */ /** Returns true if `h` is inaccessible according to Lean's default name rendering. */
function isInaccessibleName(h: string): boolean { function isInaccessibleName(h: string): boolean {
return h.indexOf('✝') >= 0; return h.indexOf('✝') >= 0;
@ -140,6 +138,9 @@ interface ProofDisplayProps {
export const Goal = React.memo((props: GoalProps) => { export const Goal = React.memo((props: GoalProps) => {
const { goal, filter, showHints, commandLine } = props const { goal, filter, showHints, commandLine } = props
// TODO: Apparently `goal` can be `undefined`
if (!goal) {return <></>}
const filteredList = getFilteredHypotheses(goal.hyps, filter); const filteredList = getFilteredHypotheses(goal.hyps, filter);
const hyps = filter.reverse ? filteredList.slice().reverse() : filteredList; const hyps = filter.reverse ? filteredList.slice().reverse() : filteredList;
const locs = React.useContext(LocationsContext) const locs = React.useContext(LocationsContext)
@ -182,6 +183,8 @@ export const Goal = React.memo((props: GoalProps) => {
export const MainAssumptions = React.memo((props: GoalProps2) => { export const MainAssumptions = React.memo((props: GoalProps2) => {
const { goals, filter } = props const { goals, filter } = props
const goal = goals[0] const goal = goals[0]
const filteredList = getFilteredHypotheses(goal.hyps, filter); const filteredList = getFilteredHypotheses(goal.hyps, filter);
const hyps = filter.reverse ? filteredList.slice().reverse() : filteredList; const hyps = filter.reverse ? filteredList.slice().reverse() : filteredList;

@ -21,11 +21,77 @@ import { levelCompleted, selectCompleted } from '../../state/progress';
import Markdown from '../markdown'; import Markdown from '../markdown';
import { Infos } from './infos'; import { Infos } from './infos';
import { AllMessages, WithLspDiagnosticsContext } from './messages'; import { AllMessages, Errors, WithLspDiagnosticsContext } from './messages';
import { Goal } from './goals'; import { Goal } from './goals';
import { InputModeContext, ProofStateContext } from './context'; import { InputModeContext, ProofContext, ProofStateContext, ProofStep } from './context';
import { CommandLine } from './command_line'; import { CommandLine } from './command_line';
import { InteractiveDiagnostic } from '@leanprover/infoview/*';
/** 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, commandLineMode}) {
const ec = React.useContext(EditorContext)
// const { commandLineMode, setCommandLineMode } = React.useContext(InputModeContext)
// return <div className={hidden ? 'hidden' : ''}>
// <ExerciseStatement data={data} />
// <div className={`statement ${commandLineMode ? 'hidden' : ''}`}><code>{data?.descrFormat}</code></div>
// <div ref={codeviewRef} className={'codeview'}></div>
// {ec && <Main key={`${worldId}/${levelId}`} world={worldId} level={levelId} />}
// </div>
// }
return <>
<div className={commandLineMode ? ' hidden' : ''}>
<ExerciseStatement data={level?.data} />
<div ref={codeviewRef} className={'codeview'}></div>
</div>
{ec && <DualEditorMain worldId={worldId} levelId={levelId} level={level} commandLineMode={commandLineMode}/>}
</>
}
/* The part of the two editors that can needs the editor connection first */
function DualEditorMain({worldId, levelId, level, commandLineMode}) {
const ec = React.useContext(EditorContext)
const gameId = React.useContext(GameIdContext)
/* 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<DocumentUri, LeanFileProgressProcessingInfo[]>(),
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 <>
<ConfigContext.Provider value={config}>
<VersionContext.Provider value={serverVersion}>
<WithRpcSessions>
<WithLspDiagnosticsContext>
<ProgressContext.Provider value={allProgress}>
{/* We need the editor to always there and hidden because
the command line edits its content */}
{ // TODO: Is there any possibility that the editor connection takes a while
// and we should show a circular progress here?
}
{commandLineMode ?
<CommandLineInterface world={worldId} level={levelId} data={level?.data}/>
:
<Main key={`${worldId}/${levelId}`} world={worldId} level={levelId} />
}
</ProgressContext.Provider>
</WithLspDiagnosticsContext>
</WithRpcSessions>
</VersionContext.Provider>
</ConfigContext.Provider>
</>
}
// The mathematical formulation of the statement, supporting e.g. Latex // The mathematical formulation of the statement, supporting e.g. Latex
// It takes three forms, depending on the precence of name and description: // It takes three forms, depending on the precence of name and description:
@ -106,43 +172,101 @@ export function Main(props: {world: string, level: number}) {
</div> </div>
} }
return ( return ret
<ConfigContext.Provider value={config}>
<VersionContext.Provider value={serverVersion}>
<WithRpcSessions>
<WithLspDiagnosticsContext>
<ProgressContext.Provider value={allProgress}>
{ret}
</ProgressContext.Provider>
</WithLspDiagnosticsContext>
</WithRpcSessions>
</VersionContext.Provider>
</ConfigContext.Provider>
);
} }
// `codeviewRef`: the codeViewRef. Used to edit the editor's content even if not visible const goalFilter = {
export function EditorInterface({data, codeviewRef, hidden, worldId, levelId, editorConnection}) { reverse: false,
showType: true,
showInstance: true,
showHiddenAssumption: true,
showLetValue: true
}
const { commandLineMode, setCommandLineMode } = React.useContext(InputModeContext) /** The display of a single entered lean command */
function Command({command} : {command: string}) {
// The first step will always have an empty command
if (!command) {return <></>}
return <div className="command">
{command}
</div>
}
return <div className={hidden ? 'hidden' : ''}> // const MessageView = React.memo(({uri, diag}: MessageViewProps) => {
<ExerciseStatement data={data} /> // const ec = React.useContext(EditorContext);
<div className={`statement ${commandLineMode ? 'hidden' : ''}`}><code>{data?.descrFormat}</code></div> // const fname = escapeHtml(basename(uri));
<div ref={codeviewRef} className={'codeview'}></div> // const {line, character} = diag.range.start;
{editorConnection && <Main key={`${worldId}/${levelId}`} world={worldId} level={levelId} />} // 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 { commandLineMode } = React.useContext(InputModeContext)
// return (
// // <details open>
// // <summary className={severityClass + ' mv2 pointer'}>{title}
// // <span className="fr">
// // <a className="link pointer mh2 dim codicon codicon-go-to-file"
// // onClick={e => { e.preventDefault(); void ec.revealLocation(loc); }}
// // title="reveal file location"></a>
// // <a className="link pointer mh2 dim codicon codicon-quote"
// // data-id="copy-to-comment"
// // onClick={e => {e.preventDefault(); void ec.copyToComment(text)}}
// // title="copy message to comment"></a>
// // <a className="link pointer mh2 dim codicon codicon-clippy"
// // onClick={e => {e.preventDefault(); void ec.api.copyToClipboard(text)}}
// // title="copy message to clipboard"></a>
// // </span>
// // </summary>
// <div className={severityClass + ' ml1 message'}>
// {!commandLineMode && <p className="mv2">{title}</p>}
// <pre className="font-code pre-wrap">
// <InteractiveMessage fmt={message} />
// </pre>
// </div>
// // </details>
// )
// }, fastIsEqual)
/** The tabs of goals that lean ahs after the command of this step has been processed */
function GoalsTab({proofStep} : {proofStep: ProofStep}) {
const [selectedGoal, setSelectedGoal] = React.useState<number>(0)
return <div>
<div className="tab-bar">
{proofStep.goals.map((goal, i) => (
<div className={`tab ${i == (selectedGoal) ? "active": ""}`} onClick={() => { setSelectedGoal(i) }}>
{i ? `Goal ${i+1}` : "Active Goal"}
</div>
))}
</div>
<div className="goal-tab vscode-light">
<Goal commandLine={false} filter={goalFilter} goal={proofStep.goals[selectedGoal]} />
</div>
</div> </div>
} }
/** The interface in command line mode */
export function CommandLineInterface(props: {world: string, level: number, data: LevelInfo}) { export function CommandLineInterface(props: {world: string, level: number, data: LevelInfo}) {
const ec = React.useContext(EditorContext); const ec = React.useContext(EditorContext)
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const {proof} = React.useContext(ProofContext)
const proofStateContext = React.useContext(ProofStateContext)
const [selectedGoal, setSelectedGoal] = React.useState<number>(0)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@ -187,8 +311,6 @@ export function CommandLineInterface(props: {world: string, level: number, data:
[] []
); );
const goalFilter = { reverse: false, showType: true, showInstance: true, showHiddenAssumption: true, showLetValue: true }
const serverVersion = const serverVersion =
useEventResult(ec.events.serverRestarted, result => new ServerVersion(result.serverInfo?.version ?? '')) useEventResult(ec.events.serverRestarted, result => new ServerVersion(result.serverInfo?.version ?? ''))
const serverStoppedResult = useEventResult(ec.events.serverStopped); const serverStoppedResult = useEventResult(ec.events.serverStopped);
@ -206,39 +328,28 @@ export function CommandLineInterface(props: {world: string, level: number, data:
{/* {completed && <div className="level-completed">Level completed! 🎉</div>} */} {/* {completed && <div className="level-completed">Level completed! 🎉</div>} */}
<div className="content"> <div className="content">
<ExerciseStatement data={props.data} /> <ExerciseStatement data={props.data} />
<Infos /> {/* <Infos /> */}
<div className="tmp-pusher"></div> <div className="tmp-pusher"></div>
<div className="tab-bar"> {proof.map((step, i) => {
{proofStateContext.proofState.goals?.goals.map((goal, i) => if (i == proof.length - 1 && step.errors.length) {
<div className={`tab ${i == (selectedGoal) ? "active": ""}`} // if the last command contains an error, we should not display it
onClick={() => { setSelectedGoal(i) }}> // as it will be overwritten by the next entered command
{i ? `Goal ${i+1}` : "Active Goal"} return <div>
</div>)} <Errors errors={step.errors} commandLineMode={true}/>
</div> </div>
<div className="goal-tab vscode-light"> }
{proofStateContext.proofState.goals?.goals?.length && else {
<Goal commandLine={false} filter={goalFilter} goal={proofStateContext.proofState.goals.goals[selectedGoal]} /> return <div className="step">
<Command command={step.command} />
<Errors errors={step.errors} commandLineMode={true}/>
<GoalsTab proofStep={step} />
</div>
} }
</div> })}
</div> </div>
<CommandLine /> <CommandLine />
</div> </div>
} }
return <> return ret
{/* <button className="btn" onClick={handleUndo} disabled={!canUndo}><FontAwesomeIcon icon={faRotateLeft} /> Undo</button> */}
<ConfigContext.Provider value={config}>
<VersionContext.Provider value={serverVersion}>
<WithRpcSessions>
<WithLspDiagnosticsContext>
<ProgressContext.Provider value={allProgress}>
{ret}
</ProgressContext.Provider>
</WithLspDiagnosticsContext>
</WithRpcSessions>
</VersionContext.Provider>
</ConfigContext.Provider>
</>
} }

@ -17,6 +17,44 @@ interface MessageViewProps {
diag: InteractiveDiagnostic; diag: InteractiveDiagnostic;
} }
/** A list of messages (info/warning/error) that are produced after this command */
function Error({error, commandLineMode} : {error : InteractiveDiagnostic, commandLineMode : boolean}) {
// The first step will always have an empty command
const severityClass = error.severity ? {
[DiagnosticSeverity.Error]: 'error',
[DiagnosticSeverity.Warning]: 'warning',
[DiagnosticSeverity.Information]: 'information',
[DiagnosticSeverity.Hint]: 'hint',
}[error.severity] : '';
const {line, character} = error.range.start;
const title = `Line ${line+1}, Character ${character}`;
// Hide "unsolved goals" messages
let message;
if ("append" in error.message && "text" in error.message.append[0] &&
error.message?.append[0].text === "unsolved goals") {
message = error.message.append[0]
} else {
message = error.message
}
return <div className={severityClass + ' ml1 message'}>
{!commandLineMode && <p className="mv2">{title}</p>}
<pre className="font-code pre-wrap">
<InteractiveMessage fmt={message} />
</pre>
</div>
}
/** A list of messages (info/warning/error) that are produced after this command */
export function Errors ({errors, commandLineMode} : {errors : InteractiveDiagnostic[], commandLineMode : boolean}) {
return <div>
{errors.map((err) => (<Error error={err} commandLineMode={commandLineMode}/>))}
</div>
}
const MessageView = React.memo(({uri, diag}: MessageViewProps) => { const MessageView = React.memo(({uri, diag}: MessageViewProps) => {
const ec = React.useContext(EditorContext); const ec = React.useContext(EditorContext);
const fname = escapeHtml(basename(uri)); const fname = escapeHtml(basename(uri));

@ -211,3 +211,15 @@ td code {
overflow-y: scroll; overflow-y: scroll;
padding: 1em; padding: 1em;
} }
/* TODO: Create a nice style and move this to a better place */
.exercise .command {
background-color: rgb(167, 167, 167);
}
.exercise .step {
background-color: #ffeb91;
margin-top: 5px;
margin-bottom: 5px;
}

@ -6,7 +6,7 @@ import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css'; import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css'; import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css'; import '@fontsource/roboto/700.css';
import { InfoviewApi } from '@leanprover/infoview' import { InfoviewApi, defaultInfoviewConfig } from '@leanprover/infoview'
import { Link, useParams } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
import { Box, CircularProgress, FormControlLabel, FormGroup, Switch, IconButton } from '@mui/material'; import { Box, CircularProgress, FormControlLabel, FormGroup, Switch, IconButton } from '@mui/material';
import MuiDrawer from '@mui/material/Drawer'; import MuiDrawer from '@mui/material/Drawer';
@ -28,7 +28,6 @@ import type { Location } from 'vscode-languageserver-protocol';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faHome, faArrowRight, faArrowLeft, faRotateLeft } from '@fortawesome/free-solid-svg-icons' import { faHome, faArrowRight, faArrowLeft, faRotateLeft } from '@fortawesome/free-solid-svg-icons'
import { styled, useTheme, Theme, CSSObject } from '@mui/material/styles'; import { styled, useTheme, Theme, CSSObject } from '@mui/material/styles';
import { DocumentPosition } from '../../../node_modules/lean4-infoview/src/infoview/util';
import { GameIdContext } from '../app'; import { GameIdContext } from '../app';
import { ConnectionContext, useLeanClient } from '../connection'; import { ConnectionContext, useLeanClient } from '../connection';
@ -38,7 +37,7 @@ import Markdown from './markdown';
import {Inventory, Documentation} from './inventory'; import {Inventory, Documentation} from './inventory';
import { useGetGameInfoQuery, useLoadLevelQuery } from '../state/api'; import { useGetGameInfoQuery, useLoadLevelQuery } from '../state/api';
import { changedSelection, codeEdited, selectCode, selectSelections, progressSlice, selectCompleted } from '../state/progress'; import { changedSelection, codeEdited, selectCode, selectSelections, progressSlice, selectCompleted } from '../state/progress';
import { EditorInterface, CommandLineInterface } from './infoview/main' import { DualEditor } from './infoview/main'
import { Hints } from './hints'; import { Hints } from './hints';
import { HintContext, InputModeContext, MonacoEditorContext, ProofContext, ProofStateContext, ProofStateProps, ProofStep } from './infoview/context'; import { HintContext, InputModeContext, MonacoEditorContext, ProofContext, ProofStateContext, ProofStateProps, ProofStep } from './infoview/context';
@ -62,6 +61,10 @@ function PlayableLevel({worldId, levelId}) {
const chatPanelRef = useRef<HTMLDivElement>(null) const chatPanelRef = useRef<HTMLDivElement>(null)
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
// The state variables for the `ProofContext`
const [proof, setProof] = useState<Array<ProofStep>>([])
const initialCode = useAppSelector(selectCode(gameId, worldId, levelId)) const initialCode = useAppSelector(selectCode(gameId, worldId, levelId))
const initialSelections = useAppSelector(selectSelections(gameId, worldId, levelId)) const initialSelections = useAppSelector(selectSelections(gameId, worldId, levelId))
@ -71,7 +74,6 @@ function PlayableLevel({worldId, levelId}) {
const [showHiddenHints, setShowHiddenHints] = useState(false) const [showHiddenHints, setShowHiddenHints] = useState(false)
const [proof, setProof] = useState<Array<ProofStep>>([])
const [proofState, setProofState] = React.useState<ProofStateProps>({ const [proofState, setProofState] = React.useState<ProofStateProps>({
status: 'updating', status: 'updating',
@ -79,7 +81,7 @@ function PlayableLevel({worldId, levelId}) {
goals: undefined, goals: undefined,
termGoal: undefined, termGoal: undefined,
error: undefined, error: undefined,
}) })
const theme = useTheme(); const theme = useTheme();
@ -182,6 +184,9 @@ function PlayableLevel({worldId, levelId}) {
const levelTitle = <>{levelId && `Level ${levelId}`}{level?.data?.title && `: ${level?.data?.title}`}</> const levelTitle = <>{levelId && `Level ${levelId}`}{level?.data?.title && `: ${level?.data?.title}`}</>
// TODO: with the new design, there is no difference between the introduction and
// a hint at the beginning of the proof...
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>
<ProofStateContext.Provider value={{proofState, setProofState}}> <ProofStateContext.Provider value={{proofState, setProofState}}>
@ -197,9 +202,9 @@ function PlayableLevel({worldId, levelId}) {
<Markdown>{level?.data?.introduction}</Markdown> <Markdown>{level?.data?.introduction}</Markdown>
</div> </div>
} }
{proofState.goals?.goals.length && {proof.map((step, i) => {
<Hints hints={proofState.goals?.goals[0].hints} showHidden={showHiddenHints}/> return <Hints hints={step.hints} showHidden={showHiddenHints}/>
} })}
{completed && {completed &&
<> <>
<div className="message info"> <div className="message info">
@ -216,10 +221,6 @@ function PlayableLevel({worldId, levelId}) {
Next&nbsp;<FontAwesomeIcon icon={faArrowRight} /></Button>} Next&nbsp;<FontAwesomeIcon icon={faArrowRight} /></Button>}
</> </>
} }
{/* { hints.map(hint => <div className="message info"><Markdown>{hint.text}</Markdown></div>) } */}
{/* TODO: Remove this debugging message: */}
{showHiddenHints && <p>Hidden Hints are displayed</p>}
</div> </div>
<div className="toggle-hidden-hints"> <div className="toggle-hidden-hints">
<FormControlLabel <FormControlLabel
@ -230,19 +231,11 @@ function PlayableLevel({worldId, levelId}) {
</div> </div>
<div className="exercise-panel"> <div className="exercise-panel">
<EditorContext.Provider value={editorConnection}> <EditorContext.Provider value={editorConnection}>
<MonacoEditorContext.Provider value={editor}> <MonacoEditorContext.Provider value={editor}>
<div className="exercise"> <div className="exercise">
{/* We need the editor to always there and hidden because <DualEditor level={level} codeviewRef={codeviewRef} levelId={levelId} worldId={worldId} commandLineMode={commandLineMode} />
the command line edits its content */} </div>
<EditorInterface data={level?.data} codeviewRef={codeviewRef} hidden={commandLineMode} </MonacoEditorContext.Provider>
worldId={worldId} levelId={levelId} editorConnection={editorConnection}/>
{ // TODO: Is there any possibility that the editor connection takes a while
// and we should show a circular progress here?
commandLineMode && editorConnection &&
<CommandLineInterface world={worldId} level={levelId} data={level?.data}/>
}
</div>
</MonacoEditorContext.Provider>
</EditorContext.Provider> </EditorContext.Provider>
</div> </div>
<div className="inventory-panel"> <div className="inventory-panel">

Loading…
Cancel
Save