Merge branch 'main' of github.com:leanprover-community/lean4game
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
/-- Get goals and messages at a given position -/
|
structure GameInteractiveGoals where
|
||||||
def getGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Option PlainGoal)) := do
|
goals : Array GameInteractiveGoal
|
||||||
|
deriving RpcEncodable
|
||||||
|
|
||||||
|
def GameInteractiveGoals.append (l r : GameInteractiveGoals) : GameInteractiveGoals where
|
||||||
|
goals := l.goals ++ r.goals
|
||||||
|
|
||||||
|
instance : Append GameInteractiveGoals := ⟨GameInteractiveGoals.append⟩
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user