Merge branch 'main' of github.com:leanprover-community/lean4game

pull/43/head
Jon Eugster 2 years ago
commit eff64d9713

@ -5,7 +5,6 @@ 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 } from '@leanprover/infoview'
import { renderInfoview } from './infoview/main'
import { Link as RouterLink } from 'react-router-dom'; import { Link as RouterLink } from 'react-router-dom';
import { Box, Button, CircularProgress, FormControlLabel, FormGroup, Switch, IconButton } from '@mui/material'; import { Box, Button, CircularProgress, FormControlLabel, FormGroup, Switch, IconButton } from '@mui/material';
import MuiDrawer from '@mui/material/Drawer'; import MuiDrawer from '@mui/material/Drawer';
@ -28,6 +27,12 @@ import { codeEdited, selectCode } from '../state/progress';
import { useAppDispatch } from '../hooks'; import { useAppDispatch } from '../hooks';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { EditorContext, ConfigContext, ProgressContext, VersionContext } from '../../../node_modules/lean4-infoview/src/infoview/contexts';
import { EditorConnection, EditorEvents } from '../../../node_modules/lean4-infoview/src/infoview/editorConnection';
import { EventEmitter } from '../../../node_modules/lean4-infoview/src/infoview/event';
import { Main } from './infoview/main'
import type { Location } from 'vscode-languageserver-protocol';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUpload, faArrowRotateRight, faChevronLeft, faChevronRight, faBook, faHome, faArrowRight, faArrowLeft } from '@fortawesome/free-solid-svg-icons' import { faUpload, faArrowRotateRight, faChevronLeft, faChevronRight, faBook, faHome, faArrowRight, faArrowLeft } from '@fortawesome/free-solid-svg-icons'
@ -104,8 +109,7 @@ function Level() {
const worldId = params.worldId const worldId = params.worldId
const codeviewRef = useRef<HTMLDivElement>(null) const codeviewRef = useRef<HTMLDivElement>(null)
const infoviewRef = useRef<HTMLDivElement>(null) const introductionPanelRef = useRef<HTMLDivElement>(null)
const messagePanelRef = useRef<HTMLDivElement>(null)
const [showSidePanel, setShowSidePanel] = useState(true) const [showSidePanel, setShowSidePanel] = useState(true)
@ -117,7 +121,7 @@ function Level() {
useEffect(() => { useEffect(() => {
// Scroll to top when loading a new level // Scroll to top when loading a new level
messagePanelRef.current!.scrollTo(0,0) introductionPanelRef.current!.scrollTo(0,0)
}, [levelId]) }, [levelId])
const connection = React.useContext(ConnectionContext) const connection = React.useContext(ConnectionContext)
@ -134,8 +138,8 @@ function Level() {
const initialCode = useSelector(selectCode(worldId, levelId)) const initialCode = useSelector(selectCode(worldId, levelId))
const {editor, infoProvider} = const {editor, infoProvider, editorConnection} =
useLevelEditor(worldId, levelId, codeviewRef, infoviewRef, initialCode, onDidChangeContent) useLevelEditor(worldId, levelId, codeviewRef, initialCode, onDidChangeContent)
const {setTitle, setSubtitle} = React.useContext(SetTitleContext); const {setTitle, setSubtitle} = React.useContext(SetTitleContext);
@ -165,7 +169,7 @@ function Level() {
</Drawer> </Drawer>
<Grid container columnSpacing={{ xs: 1, sm: 2, md: 3 }} sx={{ flexGrow: 1, p: 3 }} className="main-grid"> <Grid container columnSpacing={{ xs: 1, sm: 2, md: 3 }} sx={{ flexGrow: 1, p: 3 }} className="main-grid">
<Grid xs={8} className="main-panel"> <Grid xs={8} className="main-panel">
<div ref={messagePanelRef} className="message-panel"> <div ref={introductionPanelRef} className="introduction-panel">
<Markdown>{level?.data?.introduction}</Markdown> <Markdown>{level?.data?.introduction}</Markdown>
</div> </div>
<div className="exercise"> <div className="exercise">
@ -189,8 +193,10 @@ function Level() {
component={RouterLink} to={`/`} component={RouterLink} to={`/`}
sx={{ ml: 3, mt: 2, mb: 2 }} disableFocusRipple><FontAwesomeIcon icon={faHome}></FontAwesomeIcon></Button> sx={{ ml: 3, mt: 2, mb: 2 }} disableFocusRipple><FontAwesomeIcon icon={faHome}></FontAwesomeIcon></Button>
<div ref={infoviewRef} className="infoview vscode-light"></div>
{/* <Infoview key={worldId + "/Level" + levelId} worldId={worldId} levelId={levelId} editor={editor} editorApi={infoProvider?.getApi()} /> */} <EditorContext.Provider value={editorConnection}>
{editorConnection ? <Main /> : null}
</EditorContext.Provider>
</Grid> </Grid>
</Grid> </Grid>
</div> </div>
@ -200,13 +206,14 @@ function Level() {
export default Level export default Level
function useLevelEditor(worldId: string, levelId: number, codeviewRef, infoviewRef, initialCode, onDidChangeContent) { function useLevelEditor(worldId: string, levelId: number, codeviewRef, initialCode, onDidChangeContent) {
const connection = React.useContext(ConnectionContext) const connection = React.useContext(ConnectionContext)
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor|null>(null) const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor|null>(null)
const [infoProvider, setInfoProvider] = useState<null|InfoProvider>(null) const [infoProvider, setInfoProvider] = useState<null|InfoProvider>(null)
const [infoviewApi, setInfoviewApi] = useState<null|InfoviewApi>(null) const [infoviewApi, setInfoviewApi] = useState<null|InfoviewApi>(null)
const [editorConnection, setEditorConnection] = useState<null|EditorConnection>(null)
// Create Editor // Create Editor
useEffect(() => { useEffect(() => {
@ -228,8 +235,47 @@ function useLevelEditor(worldId: string, levelId: number, codeviewRef, infoviewR
}) })
const infoProvider = new InfoProvider(connection.getLeanClient()) const infoProvider = new InfoProvider(connection.getLeanClient())
const div: HTMLElement = infoviewRef.current!
const infoviewApi = renderInfoview(infoProvider.getApi(), div) const editorApi = infoProvider.getApi()
const editorEvents: EditorEvents = {
initialize: new EventEmitter(),
gotServerNotification: new EventEmitter(),
sentClientNotification: new EventEmitter(),
serverRestarted: new EventEmitter(),
serverStopped: new EventEmitter(),
changedCursorLocation: new EventEmitter(),
changedInfoviewConfig: new EventEmitter(),
runTestScript: new EventEmitter(),
requestedAction: new EventEmitter(),
};
// Challenge: write a type-correct fn from `Eventify<T>` to `T` without using `any`
const infoviewApi: InfoviewApi = {
initialize: async l => editorEvents.initialize.fire(l),
gotServerNotification: async (method, params) => {
editorEvents.gotServerNotification.fire([method, params]);
},
sentClientNotification: async (method, params) => {
editorEvents.sentClientNotification.fire([method, params]);
},
serverRestarted: async r => editorEvents.serverRestarted.fire(r),
serverStopped: async serverStoppedReason => {
editorEvents.serverStopped.fire(serverStoppedReason)
},
changedCursorLocation: async loc => editorEvents.changedCursorLocation.fire(loc),
changedInfoviewConfig: async conf => editorEvents.changedInfoviewConfig.fire(conf),
requestedAction: async action => editorEvents.requestedAction.fire(action),
// See https://rollupjs.org/guide/en/#avoiding-eval
// eslint-disable-next-line @typescript-eslint/no-implied-eval
runTestScript: async script => new Function(script)(),
getInfoviewHtml: async () => document.body.innerHTML,
};
const ec = new EditorConnection(editorApi, editorEvents);
setEditorConnection(ec)
editorEvents.initialize.on((loc: Location) => ec.events.changedCursorLocation.fire(loc))
setEditor(editor) setEditor(editor)
setInfoProvider(infoProvider) setInfoProvider(infoProvider)
@ -263,5 +309,5 @@ function useLevelEditor(worldId: string, levelId: number, codeviewRef, infoviewR
} }
}, [editor, levelId, connection, leanClientStarted]) }, [editor, levelId, connection, leanClientStarted])
return {editor, infoProvider} return {editor, infoProvider, editorConnection}
} }

@ -6,20 +6,21 @@ import { InteractiveGoal, InteractiveGoals, InteractiveHypothesisBundle, Interac
import { WithTooltipOnHover } from '../../../../node_modules/lean4-infoview/src/infoview/tooltips'; import { WithTooltipOnHover } from '../../../../node_modules/lean4-infoview/src/infoview/tooltips';
import { EditorContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts'; import { EditorContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts';
import { Locations, LocationsContext, SelectableLocation } from '../../../../node_modules/lean4-infoview/src/infoview/goalLocation'; import { Locations, LocationsContext, SelectableLocation } from '../../../../node_modules/lean4-infoview/src/infoview/goalLocation';
import { GameInteractiveGoal, GameInteractiveGoals } from './rpcApi';
/** 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;
} }
function goalToString(g: InteractiveGoal): string { function goalToString(g: GameInteractiveGoal): string {
let ret = '' let ret = ''
if (g.userName) { if (g.goal.userName) {
ret += `case ${g.userName}\n` ret += `case ${g.goal.userName}\n`
} }
for (const h of g.hyps) { for (const h of g.goal.hyps) {
const names = InteractiveHypothesisBundle_nonAnonymousNames(h).join(' ') const names = InteractiveHypothesisBundle_nonAnonymousNames(h).join(' ')
ret += `${names} : ${TaggedText_stripTags(h.type)}` ret += `${names} : ${TaggedText_stripTags(h.type)}`
if (h.val) { if (h.val) {
@ -28,12 +29,12 @@ function goalToString(g: InteractiveGoal): string {
ret += '\n' ret += '\n'
} }
ret += `${TaggedText_stripTags(g.type)}` ret += `${TaggedText_stripTags(g.goal.type)}`
return ret return ret
} }
export function goalsToString(goals: InteractiveGoals): string { export function goalsToString(goals: GameInteractiveGoals): string {
return goals.goals.map(goalToString).join('\n\n') return goals.goals.map(goalToString).join('\n\n')
} }
@ -111,7 +112,7 @@ function Hyp({ hyp: h, mvarId }: HypProps) {
} }
interface GoalProps { interface GoalProps {
goal: InteractiveGoal goal: GameInteractiveGoal
filter: GoalFilterState filter: GoalFilterState
} }
@ -121,44 +122,49 @@ interface GoalProps {
export const Goal = React.memo((props: GoalProps) => { export const Goal = React.memo((props: GoalProps) => {
const { goal, filter } = props const { goal, filter } = props
const prefix = goal.goalPrefix ?? 'Prove: ' const prefix = goal.goal.goalPrefix ?? 'Prove: '
const filteredList = getFilteredHypotheses(goal.hyps, filter); const filteredList = getFilteredHypotheses(goal.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)
const goalLocs = React.useMemo(() => const goalLocs = React.useMemo(() =>
locs && goal.mvarId ? locs && goal.goal.mvarId ?
{ ...locs, subexprTemplate: { mvarId: goal.mvarId, loc: { target: '' }}} : { ...locs, subexprTemplate: { mvarId: goal.goal.mvarId, loc: { target: '' }}} :
undefined, undefined,
[locs, goal.mvarId]) [locs, goal.goal.mvarId])
const goalLi = <div key={'goal'}> const goalLi = <div key={'goal'}>
<strong className="goal-vdash">Prove: </strong> <strong className="goal-vdash">Prove: </strong>
<LocationsContext.Provider value={goalLocs}> <LocationsContext.Provider value={goalLocs}>
<InteractiveCode fmt={goal.type} /> <InteractiveCode fmt={goal.goal.type} />
</LocationsContext.Provider> </LocationsContext.Provider>
</div> </div>
let cn = 'font-code tl pre-wrap bl bw1 pl1 b--transparent ' let cn = 'font-code tl pre-wrap bl bw1 pl1 b--transparent '
if (props.goal.isInserted) cn += 'b--inserted ' if (props.goal.goal.isInserted) cn += 'b--inserted '
if (props.goal.isRemoved) cn += 'b--removed ' if (props.goal.goal.isRemoved) cn += 'b--removed '
if (goal.userName) { // TODO: make this prettier
const hints = goal.messages.map((m) => <div>{m.message}</div>)
if (goal.goal.userName) {
return <details open className={cn}> return <details open className={cn}>
<summary className='mv1 pointer'> <summary className='mv1 pointer'>
<strong className="goal-case">case </strong>{goal.userName} <strong className="goal-case">case </strong>{goal.goal.userName}
</summary> </summary>
{filter.reverse && goalLi} {filter.reverse && goalLi}
{hyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)} {hyps.map((h, i) => <Hyp hyp={h} mvarId={goal.goal.mvarId} key={i} />)}
{!filter.reverse && goalLi} {!filter.reverse && goalLi}
{hints}
</details> </details>
} else return <div className={cn}> } else return <div className={cn}>
{filter.reverse && goalLi} {filter.reverse && goalLi}
{hyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)} {hyps.map((h, i) => <Hyp hyp={h} mvarId={goal.goal.mvarId} key={i} />)}
{!filter.reverse && goalLi} {!filter.reverse && goalLi}
{hints}
</div> </div>
}) })
interface GoalsProps { interface GoalsProps {
goals: InteractiveGoals goals: GameInteractiveGoals
filter: GoalFilterState filter: GoalFilterState
} }
@ -179,7 +185,7 @@ interface FilteredGoalsProps {
* When this is `undefined`, the component will not appear at all but will remember its state * 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 * 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. */ * settings and collapsed state will be as before. */
goals?: InteractiveGoals goals?: GameInteractiveGoals
} }
/** /**

@ -11,6 +11,7 @@ import { lspDiagToInteractive, MessagesList } from './messages';
import { getInteractiveGoals, getInteractiveTermGoal, InteractiveDiagnostic, import { getInteractiveGoals, getInteractiveTermGoal, InteractiveDiagnostic,
InteractiveGoals, UserWidgetInstance, Widget_getWidgets, RpcSessionAtPos, isRpcError, InteractiveGoals, UserWidgetInstance, Widget_getWidgets, RpcSessionAtPos, isRpcError,
RpcErrorCode, getInteractiveDiagnostics, InteractiveTermGoal } from '@leanprover/infoview-api'; RpcErrorCode, getInteractiveDiagnostics, InteractiveTermGoal } from '@leanprover/infoview-api';
import { GameInteractiveGoal, GameInteractiveGoals } from './rpcApi';
import { PanelWidgetDisplay } from '../../../../node_modules/lean4-infoview/src/infoview/userWidget' import { PanelWidgetDisplay } from '../../../../node_modules/lean4-infoview/src/infoview/userWidget'
import { RpcContext, useRpcSessionAtPos } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions'; import { RpcContext, useRpcSessionAtPos } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions';
import { GoalsLocation, Locations, LocationsContext } from '../../../../node_modules/lean4-infoview/src/infoview/goalLocation'; import { GoalsLocation, Locations, LocationsContext } from '../../../../node_modules/lean4-infoview/src/infoview/goalLocation';
@ -76,7 +77,7 @@ const InfoStatusBar = React.memo((props: InfoStatusBarProps) => {
interface InfoDisplayContentProps extends PausableProps { interface InfoDisplayContentProps extends PausableProps {
pos: DocumentPosition; pos: DocumentPosition;
messages: InteractiveDiagnostic[]; messages: InteractiveDiagnostic[];
goals?: InteractiveGoals; goals?: GameInteractiveGoals;
termGoal?: InteractiveTermGoal; termGoal?: InteractiveTermGoal;
error?: string; error?: string;
userWidgets: UserWidgetInstance[]; userWidgets: UserWidgetInstance[];
@ -120,22 +121,22 @@ const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
{goals && <Goals filter={{ reverse: false, showType: true, showInstance: true, showHiddenAssumption: true, showLetValue: true }} key='goals' goals={goals} />} {goals && <Goals filter={{ reverse: false, showType: true, showInstance: true, showHiddenAssumption: true, showLetValue: true }} key='goals' goals={goals} />}
</LocationsContext.Provider> </LocationsContext.Provider>
<FilteredGoals headerChildren='Expected type' key='term-goal' <FilteredGoals headerChildren='Expected type' key='term-goal'
goals={termGoal !== undefined ? {goals: [termGoal]} : undefined} /> goals={termGoal !== undefined ? {goals: [{goal:termGoal, messages: []}]} : undefined} />
{userWidgets.map(widget => {userWidgets.map(widget =>
<details key={`widget::${widget.id}::${widget.range?.toString()}`} open> <details key={`widget::${widget.id}::${widget.range?.toString()}`} open>
<summary className='mv2 pointer'>{widget.name}</summary> <summary className='mv2 pointer'>{widget.name}</summary>
<PanelWidgetDisplay pos={pos} goals={goals ? goals.goals : []} termGoal={termGoal} <PanelWidgetDisplay pos={pos} goals={goals ? goals.goals.map (goal => goal.goal) : []} termGoal={termGoal}
selectedLocations={selectedLocs} widget={widget}/> selectedLocations={selectedLocs} widget={widget}/>
</details> </details>
)} )}
<div style={{display: hasMessages ? 'block' : 'none'}} key='messages'> {/* <div style={{display: hasMessages ? 'block' : 'none'}} key='messages'>
{/* <details key='messages' open> <details key='messages' open>
<summary className='mv2 pointer'>Messages ({messages.length})</summary> */} <summary className='mv2 pointer'>Messages ({messages.length})</summary>
<div className='ml1'> <div className='ml1'>
<MessagesList uri={pos.uri} messages={messages} /> <MessagesList uri={pos.uri} messages={messages} />
</div> </div>
{/* </details> */} </details>
</div> </div> */}
{nothingToShow && ( {nothingToShow && (
isPaused ? isPaused ?
/* Adding {' '} to manage string literals properly: https://reactjs.org/docs/jsx-in-depth.html#string-literals-1 */ /* Adding {' '} to manage string literals properly: https://reactjs.org/docs/jsx-in-depth.html#string-literals-1 */
@ -152,7 +153,7 @@ interface InfoDisplayProps {
pos: DocumentPosition; pos: DocumentPosition;
status: InfoStatus; status: InfoStatus;
messages: InteractiveDiagnostic[]; messages: InteractiveDiagnostic[];
goals?: InteractiveGoals; goals?: GameInteractiveGoals;
termGoal?: InteractiveTermGoal; termGoal?: InteractiveTermGoal;
error?: string; error?: string;
userWidgets: UserWidgetInstance[]; userWidgets: UserWidgetInstance[];
@ -271,7 +272,7 @@ function InfoAux(props: InfoProps) {
// with e.g. a new `pos`. // with e.g. a new `pos`.
type InfoRequestResult = Omit<InfoDisplayProps, 'triggerUpdate'> type InfoRequestResult = Omit<InfoDisplayProps, 'triggerUpdate'>
const [state, triggerUpdateCore] = useAsyncWithTrigger(() => new Promise<InfoRequestResult>((resolve, reject) => { const [state, triggerUpdateCore] = useAsyncWithTrigger(() => new Promise<InfoRequestResult>((resolve, reject) => {
const goalsReq = getInteractiveGoals(rpcSess, DocumentPosition.toTdpp(pos)); const goalsReq = rpcSess.call('Game.getInteractiveGoals', DocumentPosition.toTdpp(pos));
const termGoalReq = getInteractiveTermGoal(rpcSess, DocumentPosition.toTdpp(pos)) const termGoalReq = getInteractiveTermGoal(rpcSess, DocumentPosition.toTdpp(pos))
const widgetsReq = Widget_getWidgets(rpcSess, pos).catch(discardMethodNotFound) const widgetsReq = Widget_getWidgets(rpcSess, pos).catch(discardMethodNotFound)
const messagesReq = getInteractiveDiagnostics(rpcSess, {start: pos.line, end: pos.line+1}) const messagesReq = getInteractiveDiagnostics(rpcSess, {start: pos.line, end: pos.line+1})
@ -304,7 +305,7 @@ function InfoAux(props: InfoProps) {
pos, pos,
status: 'ready', status: 'ready',
messages, messages,
goals, goals: goals as any,
termGoal, termGoal,
error: undefined, error: undefined,
userWidgets: userWidgets?.widgets ?? [], userWidgets: userWidgets?.widgets ?? [],

@ -1,7 +1,6 @@
/* Partly copied from https://github.com/leanprover/vscode-lean4/blob/master/lean4-infoview/src/infoview/main.tsx */ /* Partly copied from https://github.com/leanprover/vscode-lean4/blob/master/lean4-infoview/src/infoview/main.tsx */
import * as React from 'react'; import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import type { DidCloseTextDocumentParams, Location, DocumentUri } from 'vscode-languageserver-protocol'; import type { DidCloseTextDocumentParams, Location, DocumentUri } from 'vscode-languageserver-protocol';
import 'tachyons/css/tachyons.css'; import 'tachyons/css/tachyons.css';
@ -13,16 +12,14 @@ import './infoview.css'
import { LeanFileProgressParams, LeanFileProgressProcessingInfo, defaultInfoviewConfig, EditorApi, InfoviewApi } from '@leanprover/infoview-api'; import { LeanFileProgressParams, LeanFileProgressProcessingInfo, defaultInfoviewConfig, EditorApi, InfoviewApi } from '@leanprover/infoview-api';
import { Infos } from './infos'; import { Infos } from './infos';
import { AllMessages, WithLspDiagnosticsContext } from '../../../../node_modules/lean4-infoview/src/infoview/messages'; import { AllMessages, WithLspDiagnosticsContext } from './messages';
import { useClientNotificationEffect, useEventResult, useServerNotificationState } from '../../../../node_modules/lean4-infoview/src/infoview/util'; import { useClientNotificationEffect, useEventResult, useServerNotificationState } from '../../../../node_modules/lean4-infoview/src/infoview/util';
import { EditorContext, ConfigContext, ProgressContext, VersionContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts'; import { EditorContext, ConfigContext, ProgressContext, VersionContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts';
import { WithRpcSessions } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions'; import { WithRpcSessions } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions';
import { EditorConnection, EditorEvents } from '../../../../node_modules/lean4-infoview/src/infoview/editorConnection';
import { EventEmitter } from '../../../../node_modules/lean4-infoview/src/infoview/event';
import { ServerVersion } from '../../../../node_modules/lean4-infoview/src/infoview/serverVersion'; import { ServerVersion } from '../../../../node_modules/lean4-infoview/src/infoview/serverVersion';
function Main(props: {}) { export function Main(props: {}) {
const ec = React.useContext(EditorContext); const ec = React.useContext(EditorContext);
/* Set up updates to the global infoview state on editor events. */ /* Set up updates to the global infoview state on editor events. */
@ -63,11 +60,11 @@ function Main(props: {}) {
} else if (serverStoppedResult){ } else if (serverStoppedResult){
ret = <div><p>{serverStoppedResult.message}</p><p className="error">{serverStoppedResult.reason}</p></div> ret = <div><p>{serverStoppedResult.message}</p><p className="error">{serverStoppedResult.reason}</p></div>
} else { } else {
ret = <div className="ma1"> ret = <div className="ma1 infoview vscode-light">
<Infos /> <Infos />
{/* {curUri && <div className="mv2"> {curUri && <div className="mv2">
<AllMessages uri={curUri} /> <AllMessages uri={curUri} />
</div>} */} </div>}
</div> </div>
} }
@ -85,59 +82,3 @@ function Main(props: {}) {
</ConfigContext.Provider> </ConfigContext.Provider>
); );
} }
/**
* Render the Lean infoview into the DOM element `uiElement`.
*
* @param editorApi is a collection of methods which the infoview needs to be able to invoke
* on the editor in order to function correctly (such as inserting text or moving the cursor).
* @returns a collection of methods which must be invoked when the relevant editor events occur.
*/
export function renderInfoview(editorApi: EditorApi, uiElement: HTMLElement): InfoviewApi {
const editorEvents: EditorEvents = {
initialize: new EventEmitter(),
gotServerNotification: new EventEmitter(),
sentClientNotification: new EventEmitter(),
serverRestarted: new EventEmitter(),
serverStopped: new EventEmitter(),
changedCursorLocation: new EventEmitter(),
changedInfoviewConfig: new EventEmitter(),
runTestScript: new EventEmitter(),
requestedAction: new EventEmitter(),
};
// Challenge: write a type-correct fn from `Eventify<T>` to `T` without using `any`
const infoviewApi: InfoviewApi = {
initialize: async l => editorEvents.initialize.fire(l),
gotServerNotification: async (method, params) => {
editorEvents.gotServerNotification.fire([method, params]);
},
sentClientNotification: async (method, params) => {
editorEvents.sentClientNotification.fire([method, params]);
},
serverRestarted: async r => editorEvents.serverRestarted.fire(r),
serverStopped: async serverStoppedReason => {
editorEvents.serverStopped.fire(serverStoppedReason)
},
changedCursorLocation: async loc => editorEvents.changedCursorLocation.fire(loc),
changedInfoviewConfig: async conf => editorEvents.changedInfoviewConfig.fire(conf),
requestedAction: async action => editorEvents.requestedAction.fire(action),
// See https://rollupjs.org/guide/en/#avoiding-eval
// eslint-disable-next-line @typescript-eslint/no-implied-eval
runTestScript: async script => new Function(script)(),
getInfoviewHtml: async () => document.body.innerHTML,
};
const ec = new EditorConnection(editorApi, editorEvents);
editorEvents.initialize.on((loc: Location) => ec.events.changedCursorLocation.fire(loc))
const root = ReactDOM.createRoot(uiElement)
root.render(<React.StrictMode>
<EditorContext.Provider value={ec}>
<Main/>
</EditorContext.Provider>
</React.StrictMode>)
return infoviewApi;
}

@ -130,7 +130,7 @@ export function AllMessages({uri: uri0}: { uri: DocumentUri }) {
return ( return (
<RpcContext.Provider value={rs}> <RpcContext.Provider value={rs}>
<Details setOpenRef={setOpenRef as any} initiallyOpen={!config.autoOpenShowsGoal}> {/* <Details setOpenRef={setOpenRef as any} initiallyOpen={!config.autoOpenShowsGoal}>
<summary className="mv2 pointer"> <summary className="mv2 pointer">
All Messages ({diags.length}) All Messages ({diags.length})
<span className="fr"> <span className="fr">
@ -139,9 +139,9 @@ export function AllMessages({uri: uri0}: { uri: DocumentUri }) {
title={isPaused ? 'continue updating' : 'pause updating'}> title={isPaused ? 'continue updating' : 'pause updating'}>
</a> </a>
</span> </span>
</summary> </summary> */}
<AllMessagesBody uri={uri} messages={iDiags} /> <AllMessagesBody uri={uri} messages={iDiags} />
</Details> {/* </Details> */}
</RpcContext.Provider> </RpcContext.Provider>
) )
} }

@ -0,0 +1,15 @@
import { InteractiveGoals, InteractiveGoal } from '@leanprover/infoview-api';
export interface GameMessage {
message: string;
spoiler: boolean;
}
export interface GameInteractiveGoal {
goal: InteractiveGoal;
messages: GameMessage[];
}
export interface GameInteractiveGoals {
goals: GameInteractiveGoal[];
}

@ -2,6 +2,7 @@
height: 100%; height: 100%;
flex: 1; flex: 1;
min-height: 0; min-height: 0;
display: flex;
} }
.main-panel, .info-panel { .main-panel, .info-panel {
@ -14,7 +15,7 @@
flex-flow: column; flex-flow: column;
} }
.message-panel { .introduction-panel {
width: 100%; width: 100%;
} }
@ -55,12 +56,12 @@ mjx-container[jax="CHTML"][display="true"] {
/* Styling tables for Markdown */ /* Styling tables for Markdown */
.message-panel table, .message-panel th, .message-panel td { .introduction-panel table, .introduction-panel th, .introduction-panel td {
/* border: 1px solid rgb(0, 0, 0, 0.12); */ /* border: 1px solid rgb(0, 0, 0, 0.12); */
border-collapse: collapse; border-collapse: collapse;
} }
.message-panel th, .message-panel td { .introduction-panel th, .introduction-panel td {
padding-left: .5em; padding-left: .5em;
padding-right: .5em; padding-right: .5em;
} }
@ -96,7 +97,7 @@ td code {
border: 1px solid rgb(230, 122, 0); border: 1px solid rgb(230, 122, 0);
} }
.message-panel { .introduction-panel {
border: 1px solid rgb(192, 18, 178); border: 1px solid rgb(192, 18, 178);
} }

@ -100,7 +100,7 @@ def matchDecls (declMvars : Array Expr) (declFvars : Array Expr) : MetaM Bool :=
open Meta in open Meta in
/-- Find all messages whose trigger matches the current goal -/ /-- Find all messages whose trigger matches the current goal -/
def findMessages (goal : MVarId) (doc : FileWorker.EditableDocument) (hLog : IO.FS.Stream) : MetaM (Array GameMessage) := do def findMessages (goal : MVarId) (doc : FileWorker.EditableDocument) : MetaM (Array GameMessage) := do
goal.withContext do goal.withContext do
let level ← getLevelByFileName doc.meta.mkInputContext.fileName let level ← getLevelByFileName doc.meta.mkInputContext.fileName
let messages ← level.messages.filterMapM fun message => do let messages ← level.messages.filterMapM fun message => do
@ -109,7 +109,6 @@ def findMessages (goal : MVarId) (doc : FileWorker.EditableDocument) (hLog : IO.
if ← isDefEq messageGoal (← inferType $ mkMVar goal) -- TODO: also check assumptions if ← isDefEq messageGoal (← inferType $ mkMVar goal) -- TODO: also check assumptions
then then
let lctx ← getLCtx -- Local context of the `goal` let lctx ← getLCtx -- Local context of the `goal`
hLog.putStr s!"{← declMvars.mapM inferType} =?= {← lctx.getFVars.mapM inferType}"
if ← matchDecls declMvars lctx.getFVars if ← matchDecls declMvars lctx.getFVars
then then
return some { message := message.message, spoiler := message.spoiler } return some { message := message.message, spoiler := message.spoiler }
@ -117,11 +116,23 @@ def findMessages (goal : MVarId) (doc : FileWorker.EditableDocument) (hLog : IO.
else return none else return none
return messages return messages
structure GameInteractiveGoal where
goal : InteractiveGoal
messages: Array GameMessage
deriving RpcEncodable
structure GameInteractiveGoals where
goals : Array GameInteractiveGoal
deriving RpcEncodable
def GameInteractiveGoals.append (l r : GameInteractiveGoals) : GameInteractiveGoals where
goals := l.goals ++ r.goals
/-- Get goals and messages at a given position -/ instance : Append GameInteractiveGoals := ⟨GameInteractiveGoals.append⟩
def getGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Option PlainGoal)) := do
open RequestM in
def getInteractiveGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Option GameInteractiveGoals)) := do
let doc ← readDoc let doc ← readDoc
let hLog := (← read).hLog
let text := doc.meta.text let text := doc.meta.text
let hoverPos := text.lspPosToUtf8Pos p.position let hoverPos := text.lspPosToUtf8Pos p.position
-- TODO: I couldn't find a good condition to find the correct snap. So we are looking -- TODO: I couldn't find a good condition to find the correct snap. So we are looking
@ -129,24 +140,35 @@ def getGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Option PlainGoal
withWaitFindSnap doc (fun s => ¬ (s.infoTree.goalsAt? doc.meta.text hoverPos).isEmpty) withWaitFindSnap doc (fun s => ¬ (s.infoTree.goalsAt? doc.meta.text hoverPos).isEmpty)
(notFoundX := return none) fun snap => do (notFoundX := return none) fun snap => do
if let rs@(_ :: _) := snap.infoTree.goalsAt? doc.meta.text hoverPos then if let rs@(_ :: _) := snap.infoTree.goalsAt? doc.meta.text hoverPos then
let goals ← rs.mapM fun { ctxInfo := ci, tacticInfo := ti, useAfter := useAfter, .. } => do let goals : List GameInteractiveGoals ← rs.mapM fun { ctxInfo := ci, tacticInfo := ti, useAfter := useAfter, .. } => do
let ci := if useAfter then { ci with mctx := ti.mctxAfter } else { ci with mctx := ti.mctxBefore } let ciAfter := { ci with mctx := ti.mctxAfter }
let goals := List.toArray <| if useAfter then ti.goalsAfter else ti.goalsBefore let ci := if useAfter then ciAfter else { ci with mctx := ti.mctxBefore }
let goals ← ci.runMetaM {} $ goals.mapM fun goal => do -- compute the interactive goals
let messages ← findMessages goal doc hLog let goals ← ci.runMetaM {} do
return ← goal.toGameGoal messages return List.toArray <| if useAfter then ti.goalsAfter else ti.goalsBefore
return goals let goals ← ci.runMetaM {} do
return some { goals := goals.foldl (· ++ ·) ∅ } goals.mapM fun goal => do
let messages ← findMessages goal doc
return {goal := ← Widget.goalToInteractive goal, messages}
-- compute the goal diff
-- let goals ← ciAfter.runMetaM {} (do
-- try
-- Widget.diffInteractiveGoals useAfter ti goals
-- catch _ =>
-- -- fail silently, since this is just a bonus feature
-- return goals
-- )
return {goals}
return some <| goals.foldl (· ++ ·) ⟨#[]⟩
else else
return none return none
builtin_initialize builtin_initialize
registerBuiltinRpcProcedure registerBuiltinRpcProcedure
`Game.getGoals `Game.getInteractiveGoals
Lsp.PlainGoalParams Lsp.PlainGoalParams
(Option PlainGoal) (Option GameInteractiveGoals)
getGoals getInteractiveGoals
structure Diagnostic where structure Diagnostic where
severity : Option Lean.Lsp.DiagnosticSeverity severity : Option Lean.Lsp.DiagnosticSeverity

Loading…
Cancel
Save