First big junk of communication refactor

This commit is contained in:
Jon Eugster
2024-02-07 12:50:39 +01:00
parent 72ffab5b46
commit 6472ef5b31
16 changed files with 1038 additions and 483 deletions
+52 -20
View File
@@ -1,7 +1,9 @@
import { GameHint } from "./infoview/rpc_api";
import { GameHint, InteractiveGoalsWithHints, ProofState } from "./infoview/rpc_api";
import * as React from 'react';
import Markdown from './markdown';
import { ProofStep } from "./infoview/context";
import { DeletedChatContext, ProofContext } from "./infoview/context";
import { lastStepHasErrors } from "./infoview/goals";
import { Button } from "./button";
export function Hint({hint, step, selected, toggleSelection, lastLevel} : {hint: GameHint, step: number, selected: number, toggleSelection: any, lastLevel?: boolean}) {
return <div className={`message information step-${step}` + (step == selected ? ' selected' : '') + (lastLevel ? ' recent' : '')} onClick={toggleSelection}>
@@ -16,7 +18,7 @@ export function HiddenHint({hint, step, selected, toggleSelection, lastLevel} :
}
export function Hints({hints, showHidden, step, selected, toggleSelection, lastLevel} : {hints: GameHint[], showHidden: boolean, step: number, selected: number, toggleSelection: any, lastLevel?: boolean}) {
if (!hints) {return <></>}
const openHints = hints.filter(hint => !hint.hidden)
const hiddenHints = hints.filter(hint => hint.hidden)
@@ -46,22 +48,52 @@ export function DeletedHints({hints} : {hints: GameHint[]}) {
}
/** Filter hints to not show consequtive identical hints twice.
*
* This function takes a `ProofStep[]` and extracts the hints in form of an
* element of type `GameHint[][]` where it removes hints that are identical to hints
* appearing in the previous step. Hidden hints are not filtered.
*
* This effectively means we prevent consequtive identical hints from being shown.
* Hidden hints are not filtered.
*/
export function filterHints(proof: ProofStep[]): GameHint[][] {
return proof.map((step, i) => {
if (i == 0){
return step.hints
} else {
// TODO: Writing all fields explicitely is somewhat fragile to changes, is there a
// good way to shallow-compare objects?
return step.hints.filter((hint) => hint.hidden ||
(proof[i-1].hints.find((x) => (x.text == hint.text && x.hidden == hint.hidden)) === undefined))
}
})
export function filterHints(hints: GameHint[], prevHints: GameHint[]): GameHint[] {
if (!hints) {
return []}
else if (!prevHints) {
return hints }
else {
return hints.filter((hint) => hint.hidden ||
(prevHints.find(x => (x.text == hint.text && x.hidden == hint.hidden)) === undefined)
)
}
}
function hasHiddenHints(step: InteractiveGoalsWithHints): boolean {
return step?.goals[0]?.hints.some((hint) => hint.hidden)
}
export function MoreHelpButton() {
const {proof, setProof} = React.useContext(ProofContext)
const {deletedChat, setDeletedChat, showHelp, setShowHelp} = React.useContext(DeletedChatContext)
let k = proof.steps.length - (lastStepHasErrors(proof) ? 2 : 1)
const activateHiddenHints = (ev) => {
// If the last step (`k`) has errors, we want the hidden hints from the
// second-to-last step to be affected
if (!(proof.steps.length)) {return}
// state must not be mutated, therefore we need to clone the set
let tmp = new Set(showHelp)
if (tmp.has(k)) {
tmp.delete(k)
} else {
tmp.add(k)
}
setShowHelp(tmp)
console.debug(`help: ${Array.from(tmp.values())}`)
}
if (hasHiddenHints(proof.steps[k]) && !showHelp.has(k)) {
return <Button to="" onClick={activateHiddenHints}>
Show more help!
</Button>
}
}
+33 -31
View File
@@ -3,8 +3,8 @@
*/
import * as React from 'react';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
import { InteractiveDiagnostic, InteractiveTermGoal } from '@leanprover/infoview-api';
import { GameHint, InteractiveGoal, InteractiveGoals } from './rpc_api';
import { InteractiveDiagnostic } from '@leanprover/infoview-api';
import { GameHint, InteractiveGoal, InteractiveTermGoal,InteractiveGoalsWithHints, ProofState } from './rpc_api';
import { PreferencesState } from '../../state/preferences';
export const MonacoEditorContext = React.createContext<monaco.editor.IStandaloneCodeEditor>(
@@ -12,37 +12,39 @@ export const MonacoEditorContext = React.createContext<monaco.editor.IStandalone
export type InfoStatus = 'updating' | 'error' | 'ready';
/** One step of the proof */
export type ProofStep = {
/** The command in this step */
command : string
/** List of goals *after* this command */
goals: InteractiveGoal[] // TODO: Add correct type
/** Story relevant messages */
hints: GameHint[] // TODO: Add correct type
/** Errors and warnings */
errors: InteractiveDiagnostic[] // TODO: Add correct type
}
// /** One step of the proof */
// export type ProofStep = {
// /** The command in this step */
// command : string
// /** List of goals *after* this command */
// goals: InteractiveGoal[] // TODO: Add correct type
// /** Story relevant messages */
// hints: GameHint[] // TODO: Add correct type
// /** Errors and warnings */
// errors: InteractiveDiagnostic[] // TODO: Add correct type
// }
/** The context storing the proof step-by-step for the command line mode */
export const ProofContext = React.createContext<{
/** 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
* Note that the first step will always have "" as command
*/
proof: ProofStep[],
setProof: React.Dispatch<React.SetStateAction<Array<ProofStep>>>
proof: ProofState,
setProof: React.Dispatch<React.SetStateAction<ProofState>>
}>({
proof: [],
setProof: () => {} // TODO: implement me
proof: {steps: [], diagnostics: [], completed: false},
setProof: () => {}
})
// TODO: Do we still need that?
export interface ProofStateProps {
// pos: DocumentPosition;
status: InfoStatus;
messages: InteractiveDiagnostic[];
goals?: InteractiveGoals;
goals?: InteractiveGoalsWithHints;
termGoal?: InteractiveTermGoal;
error?: string;
// userWidgets: UserWidgetInstance[];
@@ -50,18 +52,18 @@ export interface ProofStateProps {
// triggerUpdate: () => Promise<void>;
}
export const ProofStateContext = React.createContext<{
proofState : ProofStateProps,
setProofState: React.Dispatch<React.SetStateAction<ProofStateProps>>
}>({
proofState : {
status: 'updating',
messages: [],
goals: undefined,
termGoal: undefined,
error: undefined},
setProofState: () => {},
})
// export const ProofStateContext = React.createContext<{
// proofState : ProofStateProps,
// setProofState: React.Dispatch<React.SetStateAction<ProofStateProps>>
// }>({
// proofState : {
// status: 'updating',
// messages: [],
// goals: undefined,
// termGoal: undefined,
// error: undefined},
// setProofState: () => {},
// })
export interface IPreferencesContext extends PreferencesState{
mobile: boolean, // The variables that actually control the page 'layout' can only be changed through layout.
+122 -6
View File
@@ -10,7 +10,10 @@ import { Locations, LocationsContext, SelectableLocation } from '../../../../nod
import { InteractiveCode } from '../../../../node_modules/lean4-infoview/src/infoview/interactiveCode'
import { WithTooltipOnHover } from '../../../../node_modules/lean4-infoview/src/infoview/tooltips';
import { InputModeContext } from './context';
import { InteractiveGoal, InteractiveGoals, InteractiveHypothesisBundle } from './rpc_api';
import { InteractiveGoal, InteractiveGoals, InteractiveGoalsWithHints, InteractiveHypothesisBundle, ProofState } from './rpc_api';
import { RpcSessionAtPos } from '@leanprover/infoview/*';
import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util';
import { DiagnosticSeverity } from 'vscode-languageserver-protocol';
/** Returns true if `h` is inaccessible according to Lean's default name rendering. */
function isInaccessibleName(h: string): boolean {
@@ -39,7 +42,11 @@ function goalToString(g: InteractiveGoal): string {
}
export function goalsToString(goals: InteractiveGoals): string {
return goals.goals.map(goalToString).join('\n\n')
return goals.goals.map(g => goalToString(g)).join('\n\n')
}
export function goalsWithHintsToString(goals: InteractiveGoalsWithHints): string {
return goals.goals.map(g => goalToString(g.goal)).join('\n\n')
}
interface GoalFilterState {
@@ -255,7 +262,7 @@ export const ProofDisplay = React.memo((props : ProofDisplayProps) => {
})
interface GoalsProps {
goals: InteractiveGoals
goals: InteractiveGoalsWithHints
filter: GoalFilterState
}
@@ -264,7 +271,7 @@ export function Goals({ goals, filter }: GoalsProps) {
return <>No goals</>
} else {
return <>
{goals.goals.map((g, i) => <Goal typewriter={false} key={i} goal={g} filter={filter} />)}
{goals.goals.map((g, i) => <Goal typewriter={false} key={i} goal={g.goal} filter={filter} />)}
</>
}
}
@@ -276,7 +283,7 @@ interface FilteredGoalsProps {
* When this is `undefined`, the component will not appear at all but will remember its state
* by virtue of still being mounted in the React tree. When it does appear again, the filter
* settings and collapsed state will be as before. */
goals?: InteractiveGoals
goals?: InteractiveGoalsWithHints
}
/**
@@ -291,7 +298,7 @@ export const FilteredGoals = React.memo(({ headerChildren, goals }: FilteredGoal
data-id="copy-goal-to-comment"
onClick={e => {
e.preventDefault();
if (goals) void ec.copyToComment(goalsToString(goals))
if (goals) void ec.copyToComment(goalsWithHintsToString(goals))
}}
title="copy state to comment" />
@@ -336,3 +343,112 @@ export const FilteredGoals = React.memo(({ headerChildren, goals }: FilteredGoal
</details>
</div>
})
export function loadGoals(
rpcSess: RpcSessionAtPos,
uri: string,
setProof: React.Dispatch<React.SetStateAction<ProofState>>) {
console.info('sending rpc request to load the proof state')
rpcSess.call('Game.getProofState', DocumentPosition.toTdpp({line: 0, character: 0, uri: uri})).then(
(proof : ProofState) => {
console.info(`received a proof state!`)
console.log(proof)
setProof(proof)
// let tmpProof : ProofStep[] = []
// let goalCount = 0
// steps.map((goals, i) => {
// // The first step has an empty command and therefore also no error messages
// // Usually there is a newline at the end of the editors content, so we need to
// // display diagnostics from potentally two lines in the last step.
// let messages = i ? (i == steps.length - 1 ? diagnostics.slice(i-1).flat() : 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")
// })
// if (typeof goals == 'undefined') {
// tmpProof.push({
// command: i ? model.getLineContent(i) : '',
// goals: [],
// hints: [],
// errors: messages
// } as ProofStep)
// console.debug('goals is undefined')
// return
// }
// // If the number of goals reduce, show a message
// if (goals.length && goalCount > goals.length) {
// messages.unshift({
// range: {
// start: {
// line: i-1,
// character: 0,
// },
// end: {
// line: i-1,
// character: 0,
// }},
// severity: DiagnosticSeverity.Information,
// message: {
// text: 'intermediate goal solved 🎉'
// }
// })
// }
// goalCount = goals.length
// // with no goals there will be no hints.
// let hints : GameHint[] = goals.length ? goals[0].hints : []
// console.debug(`Command (${i}): `, i ? model.getLineContent(i) : '')
// console.debug(`Goals: (${i}): `, goalsToString(goals)) //
// console.debug(`Hints: (${i}): `, hints)
// console.debug(`Errors: (${i}): `, messages)
// 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.map(g => g.goal),
// // 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)
}
)
}
export function lastStepHasErrors (proof : ProofState): boolean {
if (!proof?.steps.length) {return false}
let diags = [...proof.steps[proof.steps.length - 1].diags, ...proof.diagnostics]
return diags.some(
(d) => (d.severity == DiagnosticSeverity.Error ) // || d.severity == DiagnosticSeverity.Warning
)
}
export function isLastStepWithErrors (proof : ProofState, i: number): boolean {
if (!proof?.steps.length) {return false}
return (i == proof.steps.length - 1) && lastStepHasErrors(proof)
}
+19 -11
View File
@@ -4,7 +4,7 @@ import * as React from 'react'
import { CircularProgress } from '@mui/material'
import type { Location, Diagnostic } from 'vscode-languageserver-protocol'
import { getInteractiveTermGoal, InteractiveDiagnostic, UserWidgetInstance, Widget_getWidgets, RpcSessionAtPos, isRpcError,
RpcErrorCode, getInteractiveDiagnostics, InteractiveTermGoal } from '@leanprover/infoview-api'
RpcErrorCode, getInteractiveDiagnostics } from '@leanprover/infoview-api'
import { basename, DocumentPosition, RangeHelpers, useEvent, usePausableState, discardMethodNotFound,
mapRpcError, useAsyncWithTrigger, PausableProps } from '../../../../node_modules/lean4-infoview/src/infoview/util'
import { ConfigContext, EditorContext, LspDiagnosticsContext, ProgressContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts'
@@ -14,7 +14,7 @@ import { GoalsLocation, Locations, LocationsContext } from '../../../../node_mod
import { AllMessages, lspDiagToInteractive } from './messages'
import { goalsToString, Goal, MainAssumptions, OtherGoals, ProofDisplay } from './goals'
import { InteractiveGoals } from './rpc_api'
import { InteractiveTermGoal, InteractiveGoalsWithHints, InteractiveGoals, ProofState } from './rpc_api'
import { MonacoEditorContext, ProofStateProps, InfoStatus, ProofContext } from './context'
// TODO: All about pinning could probably be removed
@@ -83,11 +83,11 @@ interface InfoDisplayContentProps extends PausableProps {
error?: string
userWidgets: UserWidgetInstance[]
triggerUpdate: () => Promise<void>
proof? : string
proofString? : string
}
const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
const {pos, messages, goals, termGoal, error, userWidgets, triggerUpdate, isPaused, setPaused, proof} = props
const {pos, messages, goals, termGoal, error, userWidgets, triggerUpdate, isPaused, setPaused, proofString} = props
const hasWidget = userWidgets.length > 0
const hasError = !!error
@@ -114,7 +114,8 @@ const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
const goalFilter = { reverse: false, showType: true, showInstance: true, showHiddenAssumption: true, showLetValue: true }
/* Adding {' '} to manage string literals properly: https://reactjs.org/docs/jsx-in-depth.html#string-literals-1 */
return <>
return <>
{hasError &&
<div className='error' key='errors'>
Error updating:{' '}{error}.
@@ -129,7 +130,7 @@ const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
</div>
<div>
{ goals && (goals.goals.length > 0
? <Goal typewriter={true} filter={goalFilter} key='mainGoal' goal={goals.goals[0]} showHints={true} />
? <Goal typewriter={true} filter={goalFilter} key='mainGoal' goal={goals[0]?.goal} showHints={true} />
: <div className="goals-section-title">No Goals</div>
)}
</div>
@@ -137,7 +138,7 @@ const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
{userWidgets.map(widget =>
<details key={`widget::${widget.id}::${widget.range?.toString()}`} open>
<summary className='mv2 pointer'>{widget.name}</summary>
<PanelWidgetDisplay pos={pos} goals={goals ? goals.goals.map (goal => goal) : []}
<PanelWidgetDisplay pos={pos} goals={goals ? goals.goals : []}
termGoal={termGoal} selectedLocations={selectedLocs} widget={widget}/>
</details>
)}
@@ -166,6 +167,7 @@ interface InfoDisplayProps {
pos: DocumentPosition,
status: InfoStatus,
messages: InteractiveDiagnostic[],
proof?: ProofState,
goals?: InteractiveGoals,
termGoal?: InteractiveTermGoal,
error?: string,
@@ -175,7 +177,7 @@ interface InfoDisplayProps {
}
/** Displays goal state and messages. Can be paused. */
function InfoDisplay(props0: ProofStateProps & InfoDisplayProps & InfoPinnable) {
function InfoDisplay(props0: InfoDisplayProps & InfoPinnable) {
// Used to update the paused state *just once* if it is paused,
// but a display update is triggered
const [shouldRefresh, setShouldRefresh] = React.useState<boolean>(false)
@@ -214,7 +216,7 @@ function InfoDisplay(props0: ProofStateProps & InfoDisplayProps & InfoPinnable)
{/* <details open> */}
{/* <InfoStatusBar {...props} triggerUpdate={triggerDisplayUpdate} isPaused={isPaused} setPaused={setPaused} /> */}
<div className="vscode-light">
<InfoDisplayContent {...props} proof={editor.getValue()} triggerUpdate={triggerDisplayUpdate} isPaused={isPaused} setPaused={setPaused} />
<InfoDisplayContent {...props} proofString={editor.getValue()} triggerUpdate={triggerDisplayUpdate} isPaused={isPaused} setPaused={setPaused} />
</div>
{/* </details> */}
</RpcContext.Provider>
@@ -290,6 +292,8 @@ function InfoAux(props: InfoProps) {
// with e.g. a new `pos`.
type InfoRequestResult = Omit<InfoDisplayProps, 'triggerUpdate'>
const [state, triggerUpdateCore] = useAsyncWithTrigger(() => new Promise<InfoRequestResult>((resolve, reject) => {
const proofReq = rpcSess.call('Game.getProofState', DocumentPosition.toTdpp(pos))
const goalsReq = rpcSess.call('Game.getInteractiveGoals', DocumentPosition.toTdpp(pos))
const termGoalReq = getInteractiveTermGoal(rpcSess, DocumentPosition.toTdpp(pos))
const widgetsReq = Widget_getWidgets(rpcSess, pos).catch(discardMethodNotFound)
@@ -308,6 +312,7 @@ function InfoAux(props: InfoProps) {
pos,
status: 'updating',
messages: lspDiagsHere.map(lspDiagToInteractive),
proof: undefined,
goals: undefined,
termGoal: undefined,
error: undefined,
@@ -318,11 +323,12 @@ function InfoAux(props: InfoProps) {
// NB: it is important to await await reqs at once, otherwise
// if both throw then one exception becomes unhandled.
Promise.all([goalsReq, termGoalReq, widgetsReq, messagesReq]).then(
([goals, termGoal, userWidgets, messages]) => resolve({
Promise.all([proofReq, goalsReq, termGoalReq, widgetsReq, messagesReq]).then(
([proof, goals, termGoal, userWidgets, messages]) => resolve({
pos,
status: 'ready',
messages,
proof : proof as any,
goals: goals as any,
termGoal,
error: undefined,
@@ -353,6 +359,7 @@ function InfoAux(props: InfoProps) {
pos,
status: 'error',
messages: lspDiagsHere.map(lspDiagToInteractive),
proof: undefined,
goals: undefined,
termGoal: undefined,
error: `Error fetching goals: ${errorString}`,
@@ -389,6 +396,7 @@ function InfoAux(props: InfoProps) {
pos,
status: 'updating',
messages: [],
proof: undefined,
goals: undefined,
termGoal: undefined,
error: undefined,
+99 -106
View File
@@ -26,15 +26,16 @@ import Markdown from '../markdown';
import { Infos } from './infos';
import { AllMessages, Errors, WithLspDiagnosticsContext } from './messages';
import { Goal } from './goals';
import { DeletedChatContext, InputModeContext, PreferencesContext, MonacoEditorContext, ProofContext, ProofStep, SelectionContext, WorldLevelIdContext } from './context';
import { Typewriter, hasErrors, hasInteractiveErrors } from './typewriter';
import { Goal, isLastStepWithErrors, lastStepHasErrors, loadGoals } from './goals';
import { DeletedChatContext, InputModeContext, PreferencesContext, MonacoEditorContext, ProofContext, SelectionContext, WorldLevelIdContext } from './context';
import { Typewriter, getInteractiveDiagsAt, hasErrors, hasInteractiveErrors } from './typewriter';
import { InteractiveDiagnostic } from '@leanprover/infoview/*';
import { Button } from '../button';
import { CircularProgress } from '@mui/material';
import { GameHint } from './rpc_api';
import { GameHint, InteractiveGoalsWithHints, ProofState } from './rpc_api';
import { store } from '../../state/store';
import { Hints, filterHints } from '../hints';
import { Hints, MoreHelpButton, filterHints } from '../hints';
import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util';
/** Wrapper for the two editors. It is important that the `div` with `codeViewRef` is
* always present, or the monaco editor cannot start.
@@ -61,36 +62,35 @@ function DualEditorMain({ worldId, levelId, level, worldSize }: { worldId: strin
const gameId = React.useContext(GameIdContext)
const { typewriterMode } = React.useContext(InputModeContext)
// Mark level as completed when server gives notification
const {proof, setProof} = React.useContext(ProofContext)
const dispatch = useAppDispatch()
useServerNotificationEffect(
'$/game/completed',
(params: any) => {
if (ec.events.changedCursorLocation.current &&
ec.events.changedCursorLocation.current.uri === params.uri) {
dispatch(levelCompleted({ game: gameId, world: worldId, level: levelId }))
// On completion, add the names of all new items to the local storage
let newTiles = [
...level?.tactics,
...level?.lemmas,
...level?.definitions
].filter((tile) => tile.new).map((tile) => tile.name)
React.useEffect(() => {
if (proof.completed) {
dispatch(levelCompleted({ game: gameId, world: worldId, level: levelId }))
// Add the proven statement to the local storage as well.
if (level?.statementName != null) {
newTiles.push(level?.statementName)
}
// On completion, add the names of all new items to the local storage
let newTiles = [
...level?.tactics,
...level?.lemmas,
...level?.definitions
].filter((tile) => tile.new).map((tile) => tile.name)
let inv: string[] = selectInventory(gameId)(store.getState())
// add new items and remove duplicates
let newInv = [...inv, ...newTiles].filter((item, i, array) => array.indexOf(item) == i)
dispatch(changedInventory({ game: gameId, inventory: newInv }))
// Add the proven statement to the local storage as well.
if (level?.statementName != null) {
newTiles.push(level?.statementName)
}
}, [level]
)
let inv: string[] = selectInventory(gameId)(store.getState())
// add new items and remove duplicates
let newInv = [...inv, ...newTiles].filter((item, i, array) => array.indexOf(item) == i)
dispatch(changedInventory({ game: gameId, inventory: newInv }))
}
}, [proof, level])
/* Set up updates to the global infoview state on editor events. */
const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
@@ -154,7 +154,7 @@ export function Main(props: { world: string, level: number, data: LevelInfo}) {
const gameId = React.useContext(GameIdContext)
const {worldId, levelId} = React.useContext(WorldLevelIdContext)
const completed = useAppSelector(selectCompleted(gameId, props.world, props.level))
const { proof, setProof } = React.useContext(ProofContext)
console.debug(`template: ${props.data?.template}`)
@@ -206,7 +206,7 @@ export function Main(props: { world: string, level: number, data: LevelInfo}) {
ret = <div><p>{serverStoppedResult.message}</p><p className="error">{serverStoppedResult.reason}</p></div>
} else {
ret = <div className="infoview vscode-light">
{completed && <div className="level-completed">Level completed! 🎉</div>}
{proof.completed && <div className="level-completed">Level completed! 🎉</div>}
<Infos />
</div>
}
@@ -223,15 +223,24 @@ const goalFilter = {
}
/** The display of a single entered lean command */
function Command({ command, deleteProof }: { command: string, deleteProof: any }) {
function Command({ proof, i, deleteProof }: { proof: ProofState, i: number, deleteProof: any }) {
// The first step will always have an empty command
if (!command) { return <></> }
return <div className="command">
<div className="command-text">{command}</div>
<Button to="" className="undo-button btn btn-inverted" title="Retry proof from here" onClick={deleteProof}>
<FontAwesomeIcon icon={faDeleteLeft} />&nbsp;Retry
</Button>
</div>
if (!proof?.steps[i]?.command) { return <></> }
if (isLastStepWithErrors(proof, i)) {
// If the last step has errors, we display the command in a different style
// indicating that it will be removed on the next try.
return <div className="failed-command">
<i>Failed command</i>: {proof.steps[i].command}
</div>
} else {
return <div className="command">
<div className="command-text">{proof.steps[i].command}</div>
<Button to="" className="undo-button btn btn-inverted" title="Retry proof from here" onClick={deleteProof}>
<FontAwesomeIcon icon={faDeleteLeft} />&nbsp;Retry
</Button>
</div>
}
}
// const MessageView = React.memo(({uri, diag}: MessageViewProps) => {
@@ -286,10 +295,14 @@ function Command({ command, deleteProof }: { command: string, deleteProof: any }
// }, fastIsEqual)
/** The tabs of goals that lean ahs after the command of this step has been processed */
function GoalsTabs({ proofStep, last, onClick, onGoalChange=(n)=>{}}: { proofStep: ProofStep, last : boolean, onClick? : any, onGoalChange?: (n?: number) => void }) {
function GoalsTabs({ proofStep, last, onClick, onGoalChange=(n)=>{}}: { proofStep: InteractiveGoalsWithHints, last : boolean, onClick? : any, onGoalChange?: (n?: number) => void }) {
const [selectedGoal, setSelectedGoal] = React.useState<number>(0)
if (proofStep.goals.length == 0) {
return <></>
}
return <div className="goal-tabs" onClick={onClick}>
<div className={`tab-bar ${last ? 'current' : ''}`}>
{proofStep.goals.map((goal, i) => (
@@ -300,7 +313,7 @@ function GoalsTabs({ proofStep, last, onClick, onGoalChange=(n)=>{}}: { proofSte
))}
</div>
<div className="goal-tab vscode-light">
<Goal typewriter={false} filter={goalFilter} goal={proofStep.goals[selectedGoal]} />
<Goal typewriter={false} filter={goalFilter} goal={proofStep.goals[selectedGoal]?.goal} />
</div>
</div>
}
@@ -350,12 +363,11 @@ export function TypewriterInterface({props}) {
const [loadingProgress, setLoadingProgress] = React.useState<number>(0)
const { setDeletedChat, showHelp, setShowHelp } = React.useContext(DeletedChatContext)
const {mobile} = React.useContext(PreferencesContext)
const { proof } = React.useContext(ProofContext)
const { proof, setProof } = React.useContext(ProofContext)
const { setTypewriterInput } = React.useContext(InputModeContext)
const { selectedStep, setSelectedStep } = React.useContext(SelectionContext)
const proofPanelRef = React.useRef<HTMLDivElement>(null)
const completed = useAppSelector(selectCompleted(gameId, props.world, props.level))
// const config = useEventResult(ec.events.changedInfoviewConfig) ?? defaultInfoviewConfig;
// const curUri = useEventResult(ec.events.changedCursorLocation, loc => loc?.uri);
@@ -367,9 +379,11 @@ export function TypewriterInterface({props}) {
function deleteProof(line: number) {
return (ev) => {
let deletedChat: Array<GameHint> = []
filterHints(proof).slice(line).map((hintsAtStep, i) => {
proof.steps.slice(line).map((step, i) => {
let filteredHints = filterHints(step.goals[0]?.hints, proof?.steps[i-1]?.goals[0]?.hints)
// Only add these hidden hints to the deletion stack which were visible
deletedChat = [...deletedChat, ...hintsAtStep.filter(hint => (!hint.hidden || showHelp.has(line + i)))]
deletedChat = [...deletedChat, ...filteredHints.filter(hint => (!hint.hidden || showHelp.has(line + i)))]
})
setDeletedChat(deletedChat)
@@ -382,7 +396,9 @@ export function TypewriterInterface({props}) {
forceMoveMarkers: false
}])
setSelectedStep(undefined)
setTypewriterInput(proof[line].command)
setTypewriterInput(proof.steps[line].command)
// Reload proof on deleting
loadGoals(rpcSess, uri, setProof)
ev.stopPropagation()
}
}
@@ -402,7 +418,7 @@ export function TypewriterInterface({props}) {
// Scroll to the end of the proof if it is updated.
React.useEffect(() => {
if (proof?.length > 1) {
if (proof.steps.length > 1) {
proofPanelRef.current?.lastElementChild?.scrollIntoView() //scrollTo(0,0)
} else {
proofPanelRef.current?.scrollTo(0,0)
@@ -423,38 +439,8 @@ export function TypewriterInterface({props}) {
}
}, [selectedStep])
// TODO: This about hidden hints is all copied from `level.tsx`. Can we move that into `hints.tsx`?
// If the last step has errors, we want to treat it as if it is part of the second-to-last step
let k = proof.length - 1
let withErr = hasInteractiveErrors(proof[k]?.errors) ? 1 : 0
const activateHiddenHints = (ev) => {
// If the last step (`k`) has errors, we want the hidden hints from the
// second-to-last step to be affected
if (!(proof.length)) {return}
// state must not be mutated, therefore we need to clone the set
let tmp = new Set(showHelp)
if (tmp.has(k - withErr)) {
tmp.delete(k - withErr)
} else {
tmp.add(k - withErr)
}
setShowHelp(tmp)
console.debug(`help: ${Array.from(tmp.values())}`)
}
function hasHiddenHints(i : number): boolean {
let step = proof[i]
// For example if the proof isn't loaded yet
if(!step) {return false}
return step.hints.some((hint) => hint.hidden)
}
let lastStepErrors = proof.length ? hasInteractiveErrors(proof[proof.length - 1].errors) : false
// TODO: superfluous, can be replaced with `withErr` from above
let lastStepErrors = proof.steps.length ? hasInteractiveErrors(getInteractiveDiagsAt(proof, proof.steps.length)) : false
useServerNotificationEffect("$/game/loading", (params : any) => {
@@ -474,20 +460,22 @@ export function TypewriterInterface({props}) {
</div>
<div className='proof' ref={proofPanelRef}>
<ExerciseStatement data={props.data} />
{proof.length ?
{proof.steps.length ?
<>
{proof.map((step, i) => {
if (i == proof.length - 1 && lastStepErrors) {
// if the last command contains an error, we only display the errors but not the
// entered command as it is still present in the command line.
// TODO: Should not use index as key.
return <div key={`proof-step-${i}`}>
<Errors errors={step.errors} typewriterMode={true} />
</div>
} else {
{proof.steps.map((step, i) => {
let filteredHints = filterHints(step.goals[0]?.hints, proof?.steps[i-1]?.goals[0]?.hints)
// if (i == proof.steps.length - 1 && hasInteractiveErrors(step.diags)) {
// // if the last command contains an error, we only display the errors but not the
// // entered command as it is still present in the command line.
// // TODO: Should not use index as key.
// return <div key={`proof-step-${i}`} className={`step step-${i}`}>
// <Errors errors={step.diags} typewriterMode={true} />
// </div>
// } else {
return <div key={`proof-step-${i}`} className={`step step-${i}` + (selectedStep == i ? ' selected' : '')}>
<Command command={step.command} deleteProof={deleteProof(i)} />
<Errors errors={step.errors} typewriterMode={true} />
<Command proof={proof} i={i} deleteProof={deleteProof(i)} />
<Errors errors={step.diags} typewriterMode={true} />
{mobile && i == 0 && props.data?.introduction &&
<div className={`message information step-0${selectedStep === 0 ? ' selected' : ''}`} onClick={toggleSelectStep(0)}>
<Markdown>{props.data?.introduction}</Markdown>
@@ -495,22 +483,21 @@ export function TypewriterInterface({props}) {
}
{mobile &&
<Hints key={`hints-${i}`}
hints={step.hints} showHidden={showHelp.has(i)} step={i}
hints={filteredHints} showHidden={showHelp.has(i)} step={i}
selected={selectedStep} toggleSelection={toggleSelectStep(i)}/>
}
<GoalsTabs proofStep={step} last={i == proof.length - (lastStepErrors ? 2 : 1)} onClick={toggleSelectStep(i)} onGoalChange={i == proof.length - 1 - withErr ? (n) => setDisableInput(n > 0) : (n) => {}}/>
{mobile && i == proof.length - 1 &&
hasHiddenHints(proof.length - 1) && !showHelp.has(k - withErr) &&
<Button className="btn btn-help" to="" onClick={activateHiddenHints}>
Show more help!
</Button>
{/* <GoalsTabs proofStep={step} last={i == proof.steps.length - (lastStepErrors ? 2 : 1)} onClick={toggleSelectStep(i)} onGoalChange={i == proof.steps.length - 1 - withErr ? (n) => setDisableInput(n > 0) : (n) => {}}/> */}
{!(isLastStepWithErrors(proof, i)) &&
<GoalsTabs proofStep={step} last={i == proof.steps.length - (lastStepHasErrors(proof) ? 2 : 1)} onClick={toggleSelectStep(i)} onGoalChange={i == proof.steps.length - (lastStepHasErrors(proof) ? 2 : 1) ? (n) => setDisableInput(n > 0) : (n) => {}}/>
}
{mobile && i == proof.steps.length - 1 &&
<MoreHelpButton />
}
{/* Show a message that there are no goals left */}
{!step.goals.length && (
{/* {!step.goals.length && (
<div className="message information">
{completed ?
{proof.completed ?
<p>Level completed! 🎉</p> :
<p>
<b>no goals left</b><br />
@@ -518,11 +505,17 @@ export function TypewriterInterface({props}) {
</p>
}
</div>
)}
)} */}
</div>
}
})}
{mobile && completed &&
//}
)}
{proof.diagnostics.length > 0 &&
<div key={`proof-step-remaining`} className="step step-remaining">
<Errors errors={proof.diagnostics} typewriterMode={true} />
</div>
}
{mobile && proof.completed &&
<div className="button-row mobile">
{props.level >= props.worldSize ?
<Button to={`/${gameId}`}>
@@ -539,7 +532,7 @@ export function TypewriterInterface({props}) {
}
</div>
</div>
<Typewriter hidden={!withErr && proof[proof.length - 1]?.goals.length == 0} disabled={disableInput || !proof.length}/>
<Typewriter disabled={disableInput || !proof.steps.length}/>
</RpcContext.Provider>
</div>
}
+46 -12
View File
@@ -3,46 +3,80 @@
*
* This file is based on `vscode-lean4/vscode-lean4/src/rpcApi.ts`
*/
import { ContextInfo, FVarId, CodeWithInfos, MVarId } from '@leanprover/infoview-api';
export interface GameHint {
text: string;
hidden: boolean;
}
import type { Range } from 'vscode-languageserver-protocol';
import type { ContextInfo, FVarId, CodeWithInfos, MVarId } from '@leanprover/infoview-api';
import { InteractiveDiagnostic, TermInfo } from '@leanprover/infoview/*';
import type { Diagnostic } from 'vscode-languageserver-protocol';
export interface InteractiveHypothesisBundle {
/** The pretty names of the variables in the bundle. Anonymous names are rendered
* as `"[anonymous]"` whereas inaccessible ones have a `✝` appended at the end.
* Use `InteractiveHypothesisBundle_nonAnonymousNames` to filter anonymouse ones out. */
names: string[];
/** Present since server version 1.1.2. */
fvarIds?: FVarId[];
type: CodeWithInfos;
val?: CodeWithInfos;
isInstance?: boolean;
isType?: boolean;
isAssumption?: boolean;
isInserted?: boolean;
isRemoved?: boolean;
isAssumption?: boolean;
}
export interface InteractiveGoalCore {
hyps: InteractiveHypothesisBundle[];
type: CodeWithInfos;
/** Present since server version 1.1.2. */
ctx?: ContextInfo;
}
export interface InteractiveGoal extends InteractiveGoalCore {
userName?: string;
goalPrefix?: string;
/** Present since server version 1.1.2. */
mvarId?: MVarId;
isInserted?: boolean;
isRemoved?: boolean;
}
export interface InteractiveGoals extends InteractiveGoalCore {
goals: InteractiveGoals[];
}
export interface InteractiveTermGoal extends InteractiveGoalCore {
range?: Range;
term?: TermInfo;
}
export interface GameHint {
text: string;
hidden: boolean;
}
export interface InteractiveGoalWithHints {
goal: InteractiveGoal;
hints: GameHint[];
}
export interface InteractiveGoals {
goals: InteractiveGoal[];
export interface InteractiveGoalsWithHints {
goals: InteractiveGoalWithHints[];
command: string;
diags: InteractiveDiagnostic[];
}
/**
* The proof state as it is received from the server.
* Per proof step of the tactic proof, there is one `InteractiveGoalWithHints[]`.
*/
export interface ProofState {
/** The proof steps. step 0 is the state at the beginning of the proof. step one
* contains the goal after the first line has been evaluated.
*
* In particular `step[i]` is the proof step at the beginning of line `i` in vscode.
*/
steps: InteractiveGoalsWithHints[];
/** The remaining diagnostics that are not in the steps. Usually this should only
* be the "unsolved goals" message, I believe.
*/
diagnostics : InteractiveDiagnostic[];
completed : Boolean;
completedWithWarnings : Boolean;
}
+131 -91
View File
@@ -5,7 +5,7 @@ import { faWandMagicSparkles } from '@fortawesome/free-solid-svg-icons'
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
import { Registry } from 'monaco-textmate' // peer dependency
import { wireTmGrammars } from 'monaco-editor-textmate'
import { DiagnosticSeverity, PublishDiagnosticsParams } from 'vscode-languageserver-protocol';
import { DiagnosticSeverity, PublishDiagnosticsParams, DocumentUri } from 'vscode-languageserver-protocol';
import { useServerNotificationEffect } from '../../../../node_modules/lean4-infoview/src/infoview/util';
import { AbbreviationRewriter } from 'lean4web/client/src/editor/abbreviation/rewriter/AbbreviationRewriter';
import { AbbreviationProvider } from 'lean4web/client/src/editor/abbreviation/AbbreviationProvider';
@@ -13,13 +13,21 @@ import * as leanSyntax from 'lean4web/client/src/syntaxes/lean.json'
import * as leanMarkdownSyntax from 'lean4web/client/src/syntaxes/lean-markdown.json'
import * as codeblockSyntax from 'lean4web/client/src/syntaxes/codeblock.json'
import languageConfig from 'lean4/language-configuration.json';
import { InteractiveDiagnostic, getInteractiveDiagnostics } from '@leanprover/infoview-api';
import { InteractiveDiagnostic, RpcSessionAtPos, getInteractiveDiagnostics } from '@leanprover/infoview-api';
import { Diagnostic } from 'vscode-languageserver-types';
import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util';
import { RpcContext } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions';
import { DeletedChatContext, InputModeContext, MonacoEditorContext, ProofContext, ProofStep } from './context'
import { goalsToString } from './goals'
import { GameHint, InteractiveGoals } from './rpc_api'
import { DeletedChatContext, InputModeContext, MonacoEditorContext, ProofContext } from './context'
import { goalsToString, lastStepHasErrors, loadGoals } from './goals'
import { GameHint, ProofState } from './rpc_api'
export interface GameDiagnosticsParams {
uri: DocumentUri;
diagnostics: Diagnostic[];
}
/* We register a new language `leancmd` that looks like lean4, but does not use the lsp server. */
@@ -64,7 +72,7 @@ config.autoClosingPairs = config.autoClosingPairs.map(
monaco.languages.setLanguageConfiguration('lean4cmd', config);
/** The input field */
export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boolean}) {
export function Typewriter({disabled}: {disabled?: boolean}) {
/** Reference to the hidden multi-line editor */
const editor = React.useContext(MonacoEditorContext)
@@ -89,98 +97,98 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
/** Load all goals an messages of the current proof (line-by-line) and save
* the retrieved information into context (`ProofContext`)
*/
const loadAllGoals = React.useCallback(() => {
// const loadAllGoals = React.useCallback(() => {
let goalCalls = []
let msgCalls = []
// 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}).catch((error) => {console.debug("promise broken")})
)
}
// // 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}).catch((error) => {console.debug("promise broken")})
// )
// }
// 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[] = []
// // Wait for all these requests to be processed before saving the results
// Promise.all(goalCalls).then((steps : InteractiveGoalsWithHints[]) => {
// Promise.all(msgCalls).then((diagnostics : [InteractiveDiagnostic[]]) => {
// let tmpProof : ProofStep[] = []
let goalCount = 0
// let goalCount = 0
steps.map((goals, i) => {
// The first step has an empty command and therefore also no error messages
// Usually there is a newline at the end of the editors content, so we need to
// display diagnostics from potentally two lines in the last step.
let messages = i ? (i == steps.length - 1 ? diagnostics.slice(i-1).flat() : diagnostics[i-1]) : []
// steps.map((goals, i) => {
// // The first step has an empty command and therefore also no error messages
// // Usually there is a newline at the end of the editors content, so we need to
// // display diagnostics from potentally two lines in the last step.
// let messages = i ? (i == steps.length - 1 ? diagnostics.slice(i-1).flat() : 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")
})
// // 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")
// })
if (typeof goals == 'undefined') {
tmpProof.push({
command: i ? model.getLineContent(i) : '',
goals: [],
hints: [],
errors: messages
} as ProofStep)
console.debug('goals is undefined')
return
}
// if (typeof goals == 'undefined') {
// tmpProof.push({
// command: i ? model.getLineContent(i) : '',
// goals: [],
// hints: [],
// errors: messages
// } as ProofStep)
// console.debug('goals is undefined')
// return
// }
// If the number of goals reduce, show a message
if (goals.goals.length && goalCount > goals.goals.length) {
messages.unshift({
range: {
start: {
line: i-1,
character: 0,
},
end: {
line: i-1,
character: 0,
}},
severity: DiagnosticSeverity.Information,
message: {
text: 'intermediate goal solved 🎉'
}
})
}
goalCount = goals.goals.length
// // If the number of goals reduce, show a message
// if (goals.length && goalCount > goals.length) {
// messages.unshift({
// range: {
// start: {
// line: i-1,
// character: 0,
// },
// end: {
// line: i-1,
// character: 0,
// }},
// severity: DiagnosticSeverity.Information,
// message: {
// text: 'intermediate goal solved 🎉'
// }
// })
// }
// goalCount = goals.length
// with no goals there will be no hints.
let hints : GameHint[] = goals.goals.length ? goals.goals[0].hints : []
// // with no goals there will be no hints.
// let hints : GameHint[] = goals.length ? goals[0].hints : []
console.debug(`Command (${i}): `, i ? model.getLineContent(i) : '')
console.debug(`Goals: (${i}): `, goalsToString(goals)) //
console.debug(`Hints: (${i}): `, hints)
console.debug(`Errors: (${i}): `, messages)
// console.debug(`Command (${i}): `, i ? model.getLineContent(i) : '')
// console.debug(`Goals: (${i}): `, goalsToString(goals)) //
// console.debug(`Hints: (${i}): `, hints)
// console.debug(`Errors: (${i}): `, messages)
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)
// 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.map(g => g.goal),
// // 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)
}).catch((error) => {console.debug("promise broken")})
}).catch((error) => {console.debug("promise broken")})
}, [editor, rpcSess, uri, model])
// })
// // Save the proof to the context
// setProof(tmpProof)
// }).catch((error) => {console.debug("promise broken")})
// }).catch((error) => {console.debug("promise broken")})
// }, [editor, rpcSess, uri, model])
// Run the command
const runCommand = React.useCallback(() => {
@@ -201,6 +209,8 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
forceMoveMarkers: false
}])
setTypewriterInput('')
// Load proof after executing edits
loadGoals(rpcSess, uri, setProof)
}
editor.setPosition(pos)
@@ -212,9 +222,15 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
}
}, [typewriterInput])
/* Load proof on start/switching to typewriter */
useEffect(() => {
if (proof.length && hasInteractiveErrors(proof[proof.length - 1].errors)) {
setTypewriterInput(proof[proof.length - 1].command)
loadGoals(rpcSess, uri, setProof)
}, [])
/** If the last step has an error, add the command to the typewriter. */
useEffect(() => {
if (lastStepHasErrors(proof)) {
setTypewriterInput(proof.steps[proof.steps.length - 1].command)
}
}, [proof])
@@ -222,7 +238,9 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
useServerNotificationEffect('textDocument/publishDiagnostics', (params: PublishDiagnosticsParams) => {
if (params.uri == uri) {
setProcessing(false)
loadAllGoals()
//loadGoals(rpcSess, uri, setProof)
// TODO: loadAllGoals()
if (!hasErrors(params.diagnostics)) {
//setTypewriterInput("")
editor.setPosition(editor.getModel().getFullModelRange().getEndPosition())
@@ -236,6 +254,15 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
// loadAllGoals()
}, [uri]);
// React when answer from the server comes back
useServerNotificationEffect('$/game/publishDiagnostics', (params: GameDiagnosticsParams) => {
console.log('Received game diagnostics')
console.log(`diag. uri : ${params.uri}`)
console.log(params.diagnostics)
}, [uri]);
useEffect(() => {
const myEditor = monaco.editor.create(inputRef.current!, {
value: typewriterInput,
@@ -306,7 +333,8 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
useEffect(() => {
console.debug(`time to update: ${uri} \n ${rpcSess}`)
console.debug(rpcSess)
loadAllGoals()
// console.debug('LOAD ALL GOALS')
// TODO: loadAllGoals()
}, [rpcSess])
/** Process the entered command */
@@ -315,7 +343,8 @@ export function Typewriter({hidden, disabled}: {hidden?: boolean, disabled?: boo
runCommand()
}
return <div className={`typewriter${hidden ? ' hidden' : ''}${disabled ? ' disabled' : ''}`}>
// do not display if the proof is completed (with potential warnings still present)
return <div className={`typewriter${proof.completedWithWarnings ? ' hidden' : ''}${disabled ? ' disabled' : ''}`}>
<form onSubmit={handleSubmit}>
<div className="typewriter-input-wrapper">
<div ref={inputRef} className="typewriter-input" />
@@ -343,3 +372,14 @@ export function hasInteractiveErrors (diags: InteractiveDiagnostic[]) {
(d) => (d.severity == DiagnosticSeverity.Error ) // || d.severity == DiagnosticSeverity.Warning
)
}
export function getInteractiveDiagsAt (proof: ProofState, k : number) {
if (k == 0) {
return []
} else if (k >= proof.steps.length-1) {
// TODO: Do we need that?
return proof.diagnostics.filter(msg => msg.range.start.line >= proof.steps.length-1)
} else {
return proof.diagnostics.filter(msg => msg.range.start.line == k-1)
}
}
+28 -48
View File
@@ -28,10 +28,10 @@ import Markdown from './markdown'
import {InventoryPanel} from './inventory'
import { hasInteractiveErrors } from './infoview/typewriter'
import { DeletedChatContext, InputModeContext, PreferencesContext, MonacoEditorContext,
ProofContext, ProofStep, SelectionContext, WorldLevelIdContext } from './infoview/context'
ProofContext, SelectionContext, WorldLevelIdContext } from './infoview/context'
import { DualEditor } from './infoview/main'
import { GameHint } from './infoview/rpc_api'
import { DeletedHints, Hint, Hints, filterHints } from './hints'
import { GameHint, InteractiveGoalsWithHints, ProofState } from './infoview/rpc_api'
import { DeletedHints, Hint, Hints, MoreHelpButton, filterHints } from './hints'
import { PrivacyPolicyPopup } from './popup/privacy_policy'
import path from 'path';
@@ -49,6 +49,7 @@ import { WebSocketMessageWriter, toSocket } from 'vscode-ws-jsonrpc'
import { IConnectionProvider } from 'monaco-languageclient'
import { monacoSetup } from 'lean4web/client/src/monacoSetup'
import { onigasmH } from 'onigasm/lib/onigasmH'
import { isLastStepWithErrors, lastStepHasErrors } from './infoview/goals'
monacoSetup()
@@ -83,9 +84,7 @@ function ChatPanel({lastLevel}) {
const {selectedStep, setSelectedStep} = useContext(SelectionContext)
const completed = useAppSelector(selectCompleted(gameId, worldId, levelId))
// If the last step has errors, we want to treat it as if it is part of the second-to-last step
let k = proof.length - 1
let withErr = hasInteractiveErrors(proof[k]?.errors) ? 1 : 0
let k = proof.steps.length - (lastStepHasErrors(proof) ? 2 : 1)
function toggleSelection(line: number) {
return (ev) => {
@@ -98,29 +97,6 @@ function ChatPanel({lastLevel}) {
}
}
function hasHiddenHints(i : number): boolean {
let step = proof[i]
// For example if the proof isn't loaded yet
if(!step) {return false}
return step.hints.some((hint) => hint.hidden)
}
const activateHiddenHints = (ev) => {
// If the last step (`k`) has errors, we want the hidden hints from the
// second-to-last step to be affected
if (!(proof.length)) {return}
// state must not be mutated, therefore we need to clone the set
let tmp = new Set(showHelp)
if (tmp.has(k - withErr)) {
tmp.delete(k - withErr)
} else {
tmp.add(k - withErr)
}
setShowHelp(tmp)
console.debug(`help: ${Array.from(tmp.values())}`)
}
useEffect(() => {
// TODO: For some reason this is always called twice
console.debug('scroll chat')
@@ -146,10 +122,6 @@ function ChatPanel({lastLevel}) {
let introText: Array<string> = level?.data?.introduction.split(/\n(\s*\n)+/)
// experimental: Remove all hints that appeared identically in the previous step
// This effectively prevent consequtive hints being shown.
let modifiedHints : GameHint[][] = filterHints(proof)
return <div className="chat-panel">
<div ref={chatRef} className="chat">
{introText?.filter(t => t.trim()).map(((t, i) =>
@@ -157,18 +129,27 @@ function ChatPanel({lastLevel}) {
<Hint key={`intro-p-${i}`}
hint={{text: t, hidden: false}} step={0} selected={selectedStep} toggleSelection={toggleSelection(0)} />
))}
{modifiedHints.map((step, i) => {
{proof.steps.map((step, i) => {
let filteredHints = filterHints(step.goals[0]?.hints, proof?.steps[i-1]?.goals[0]?.hints)
if (step.goals.length > 0 && !isLastStepWithErrors(proof, i)) {
return <Hints key={`hints-${i}`}
hints={filteredHints} showHidden={showHelp.has(i)} step={i}
selected={selectedStep} toggleSelection={toggleSelection(i)} lastLevel={i == proof.steps.length - 1}/>
}
})}
{/* {modifiedHints.map((step, i) => {
// It the last step has errors, it will have the same hints
// as the second-to-last step. Therefore we should not display them.
if (!(i == proof.length - 1 && withErr)) {
if (!(i == proof.steps.length - 1 && withErr)) {
// TODO: Should not use index as key.
return <Hints key={`hints-${i}`}
hints={step} showHidden={showHelp.has(i)} step={i}
selected={selectedStep} toggleSelection={toggleSelection(i)} lastLevel={i == proof.length - 1}/>
selected={selectedStep} toggleSelection={toggleSelection(i)} lastLevel={i == proof.steps.length - 1}/>
}
})}
})} */}
<DeletedHints hints={deletedChat}/>
{completed &&
{proof.completed &&
<>
<div className={`message information recent step-${k}${selectedStep == k ? ' selected' : ''}`} onClick={toggleSelection(k)}>
Level completed! 🎉
@@ -182,7 +163,7 @@ function ChatPanel({lastLevel}) {
}
</div>
<div className="button-row">
{completed && (lastLevel ?
{proof.completed && (lastLevel ?
<Button to={`/${gameId}`}>
<FontAwesomeIcon icon={faHome} />&nbsp;Leave World
</Button> :
@@ -190,15 +171,12 @@ function ChatPanel({lastLevel}) {
Next&nbsp;<FontAwesomeIcon icon={faArrowRight} />
</Button>)
}
{hasHiddenHints(proof.length - 1) && !showHelp.has(k - withErr) &&
<Button to="" onClick={activateHiddenHints}>
Show more help!
</Button>
}
<MoreHelpButton />
</div>
</div>
}
function ExercisePanel({codeviewRef, visible=true}) {
const gameId = React.useContext(GameIdContext)
const {worldId, levelId} = useContext(WorldLevelIdContext)
@@ -229,7 +207,7 @@ function PlayableLevel({impressum, setImpressum}) {
const level = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
// The state variables for the `ProofContext`
const [proof, setProof] = useState<Array<ProofStep>>([])
const [proof, setProof] = useState<ProofState>({steps: [], diagnostics: [], completed: false, completedWithWarnings: false})
// When deleting the proof, we want to keep to old messages around until
// a new proof has been entered. e.g. to consult messages coming from dead ends
const [deletedChat, setDeletedChat] = useState<Array<GameHint>>([])
@@ -356,15 +334,15 @@ function PlayableLevel({impressum, setImpressum}) {
useEffect(() => {
// Forget whether hidden hints are displayed for steps that don't exist yet
if (proof.length) {
if (proof.steps.length) {
console.debug(Array.from(showHelp))
setShowHelp(new Set(Array.from(showHelp).filter(i => (i < proof.length))))
setShowHelp(new Set(Array.from(showHelp).filter(i => (i < proof.steps.length))))
}
}, [proof])
// save showed help in store
useEffect(() => {
if (proof.length) {
if (proof.steps.length) {
console.debug(`showHelp:\n ${showHelp}`)
dispatch(helpEdited({game: gameId, world: worldId, level: levelId, help: Array.from(showHelp)}))
}
@@ -622,6 +600,8 @@ function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChange
}
// loadRenderInfoview(imports, [infoProvider.getApi(), div], setInfoviewApi)
setInfoProvider(infoProvider)
// TODO: it looks like we get errors "File Changed" here.
client.restart()
const editorApi = infoProvider.getApi()
+12
View File
@@ -188,6 +188,18 @@
flex-direction: row;
}
.exercise .failed-command {
background-color: #eee;
padding: .5em;
border-radius: .2em;
/* TODO: It seems my browsers merge the margings of the proof steps,
so that it only shows once 0.5rem instead of twice. Thus have 1.5 here now.
*/
margin-bottom: 1.5rem;
display: flex;
flex-direction: row;
}
.exercise .command-text {
flex: 1;
background-color: #fff;
+5
View File
@@ -368,3 +368,8 @@ td code {
min-width: 40px;
text-align: center;
}
/* DEBUG */
.proof .step {
border: 2px solid rgb(0, 123, 255);
}