WIP implementing new editor

dev
Jon Eugster 2 years ago
parent 545ac8b0f7
commit 3a885245ae

@ -11,11 +11,11 @@ import { useAppDispatch, useAppSelector } from '../hooks'
import { Button, Markdown } from './utils' import { Button, Markdown } from './utils'
import { ChatContext, GameIdContext, PageContext, PreferencesContext, ProofContext } from '../state/context' import { ChatContext, GameIdContext, PageContext, PreferencesContext, ProofContext } from '../state/context'
import { GameHint, InteractiveGoalsWithHints } from './infoview/rpc_api' import { GameHint, InteractiveGoalsWithHints } from './infoview/rpc_api'
import { lastStepHasErrors } from './infoview/goals' // import { lastStepHasErrors } from './infoview/goals'
import { AllMessages } from '../../../node_modules/@leanprover/infoview/dist/infoview/messages' // import { AllMessages } from '../../../node_modules/@leanprover/infoview/dist/infoview/messages'
import { LeanDiagnostic, RpcErrorCode, getInteractiveDiagnostics, InteractiveDiagnostic, TaggedText_stripTags } from '@leanprover/infoview-api' // import { LeanDiagnostic, RpcErrorCode, getInteractiveDiagnostics, InteractiveDiagnostic, TaggedText_stripTags } from '@leanprover/infoview-api'
import { Location, DocumentUri, Diagnostic, DiagnosticSeverity, PublishDiagnosticsParams } from 'vscode-languageserver-protocol' import { Location, DocumentUri, Diagnostic, DiagnosticSeverity, PublishDiagnosticsParams } from 'vscode-languageserver-protocol'
import { InteractiveMessage } from '../../../node_modules/lean4-infoview/src/infoview/traceExplorer' // import { InteractiveMessage } from '../../../node_modules/lean4-infoview/src/infoview/traceExplorer'
import '../css/chat.css' import '../css/chat.css'
import { faHome } from '@fortawesome/free-solid-svg-icons' import { faHome } from '@fortawesome/free-solid-svg-icons'
@ -39,9 +39,10 @@ export function MoreHelpButton({selected=null} : {selected?: number}) {
const { proof } = React.useContext(ProofContext) const { proof } = React.useContext(ProofContext)
const { showHelp, setShowHelp } = React.useContext(ChatContext) const { showHelp, setShowHelp } = React.useContext(ChatContext)
let k = proof?.steps.length ? let k = 0
((selected === null) ? (proof?.steps.length - (lastStepHasErrors(proof) ? 2 : 1)) : selected) // let k = proof?.steps.length ?
: 0 // ((selected === null) ? (proof?.steps.length - (lastStepHasErrors(proof) ? 2 : 1)) : selected)
// : 0
const activateHiddenHints = (ev) => { const activateHiddenHints = (ev) => {
// If the last step (`k`) has errors, we want the hidden hints from the // If the last step (`k`) has errors, we want the hidden hints from the
@ -204,10 +205,10 @@ export function filterHints(hints: GameHint[], prevHints: GameHint[]): GameHint[
} }
// TODO // TODO
function helper(step, proof, kind, typewriterMode, selectedStep) { // function helper(step, proof, kind, typewriterMode, selectedStep) {
return (step == proof?.steps?.length - (lastStepHasErrors(proof) ? 2 : 1) ? ' recent' : '') + // return (step == proof?.steps?.length - (lastStepHasErrors(proof) ? 2 : 1) ? ' recent' : '') +
(!(kind == HintKind.Conclusion) && step >= (typewriterMode ? proof?.steps?.length : selectedStep+1) ? ' deleted-hint' : '') // (!(kind == HintKind.Conclusion) && step >= (typewriterMode ? proof?.steps?.length : selectedStep+1) ? ' deleted-hint' : '')
} // }
/** A hint as it is displayed in the chat. */ /** A hint as it is displayed in the chat. */
export function Hint({hint, kind, step=null} : GameHintWithStep) { export function Hint({hint, kind, step=null} : GameHintWithStep) {
@ -232,7 +233,7 @@ export function Hint({hint, kind, step=null} : GameHintWithStep) {
// until the next command is submitted; in editor, moving the cursor through the proof will // until the next command is submitted; in editor, moving the cursor through the proof will
// render all hints // render all hints
return <div className={`message kind-${kind} step-${step}` + return <div className={`message kind-${kind} step-${step}` +
((selectedStep !== null && step == selectedStep) ? ' selected' : '') + helper(step, proof, kind, typewriterMode, selectedStep) ((selectedStep !== null && step == selectedStep) ? ' selected' : '') //+ helper(step, proof, kind, typewriterMode, selectedStep)
} onClick={toggleSelection}> } onClick={toggleSelection}>
<Markdown>{getHintText(hint)}</Markdown> <Markdown>{getHintText(hint)}</Markdown>
</div> </div>
@ -350,7 +351,7 @@ export function ChatPanel ({visible = true}) {
chatRef.current!.lastElementChild?.scrollIntoView({block: "center"}) chatRef.current!.lastElementChild?.scrollIntoView({block: "center"})
} else { } else {
// proof currently not completed: first message of last step // proof currently not completed: first message of last step
let lastStep = proof?.steps.length - (lastStepHasErrors(proof) ? 2 : 1) let lastStep = proof?.steps.length //- (lastStepHasErrors(proof) ? 2 : 1)
console.debug(`scroll chat: first message of step ${lastStep}`) console.debug(`scroll chat: first message of step ${lastStep}`)
chatRef.current?.getElementsByClassName(`step-${lastStep}`)[0]?.scrollIntoView({block: "center"}) chatRef.current?.getElementsByClassName(`step-${lastStep}`)[0]?.scrollIntoView({block: "center"})
} }
@ -369,7 +370,7 @@ export function ChatPanel ({visible = true}) {
// Scroll down when new hidden hints are triggered // Scroll down when new hidden hints are triggered
useEffect(() => { useEffect(() => {
if (levelId > 0) { if (levelId > 0) {
let lastStep = proof?.steps.length - (lastStepHasErrors(proof) ? 2 : 1) let lastStep = proof?.steps.length //- (lastStepHasErrors(proof) ? 2 : 1)
if (showHelp.has(lastStep)) { if (showHelp.has(lastStep)) {
console.debug('scroll chat: down to hidden hints') console.debug('scroll chat: down to hidden hints')
// TODO: last element of hidden hints? // TODO: last element of hidden hints?

@ -1,7 +0,0 @@
import * as React from 'react'
export function Editor () {
return <p>
I'm an editor
</p>
}

@ -0,0 +1,93 @@
import * as React from 'react';
import Split from 'react-split'
import { useContext, useEffect, useRef, useState } from 'react'
import { useTranslation } from "react-i18next"
import { GameIdContext } from '../../state/context';
import { useLoadLevelQuery } from '../../state/api';
import { Markdown } from '../utils';
import * as monaco from 'monaco-editor'
import { LeanMonaco, LeanMonacoEditor, LeanMonacoOptions } from 'lean4monaco'
import '../../css/editor.css'
import { useSelector } from 'react-redux';
import { selectTypewriterMode } from '../../state/progress';
export function Editor() {
let { t } = useTranslation()
const {gameId, worldId, levelId } = useContext(GameIdContext)
const typewriterMode = useSelector(selectTypewriterMode(gameId))
const editorRef = useRef<HTMLDivElement>(null)
const infoviewRef = useRef<HTMLDivElement>(null)
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor>()
const [leanMonaco, setLeanMonaco] = useState<LeanMonaco>()
const [code, setCode] = useState<string>('')
const [options, setOptions] = useState<LeanMonacoOptions>({
// placeholder. gets set below
websocket: {
url: ''
}
})
// Update LeanMonaco options when preferences are loaded or change
useEffect(() => {
var socketUrl = ((window.location.protocol === "https:") ? "wss://" : "ws://") +
window.location.host + `/websocket/${gameId}`
console.log(`[LeanGame] socket url: ${socketUrl}`)
var _options: LeanMonacoOptions = {
websocket: {url: socketUrl},
// Restrict monaco's extend (e.g. context menu) to the editor itself
htmlElement: editorRef.current ?? undefined,
vscode: {
/* To add settings here, you can open your settings in VSCode (Ctrl+,), search
* for the desired setting, select "Copy Setting as JSON" from the "More Actions"
* menu next to the selected setting, and paste the copied string here.
*/
// "workbench.colorTheme": preferences.theme,
"editor.tabSize": 2,
// "editor.rulers": [100],
"editor.lightbulb.enabled": "on",
"editor.wordWrap": "on",
"editor.wrappingStrategy": "advanced",
"editor.semanticHighlighting.enabled": true,
"editor.acceptSuggestionOnEnter": "off",
"lean4.input.eagerReplacementEnabled": true,
// "lean4.input.leader": preferences.abbreviationCharacter
}
}
setOptions(_options)
}, [editorRef])
// Setting up the editor and infoview
useEffect(() => {
console.debug('[LeanGame] Restarting Editor!')
var _leanMonaco = new LeanMonaco()
var leanMonacoEditor = new LeanMonacoEditor()
_leanMonaco.setInfoviewElement(infoviewRef.current!)
;(async () => {
await _leanMonaco.start(options)
await leanMonacoEditor.start(editorRef.current!, `${gameId}/${worldId}/Level_${levelId}.lean`, code)
setEditor(leanMonacoEditor.editor)
setLeanMonaco(_leanMonaco)
// Keeping the `code` state up-to-date with the changes in the editor
leanMonacoEditor.editor?.onDidChangeModelContent(() => {
setCode(leanMonacoEditor.editor?.getModel()?.getValue()!)
})
})()
return () => {
leanMonacoEditor.dispose()
_leanMonaco.dispose()
}
}, [options, infoviewRef, editorRef, gameId, worldId, levelId])
return <Split direction='vertical' minSize={200} sizes={[50, 50]}
className={`editor-wrapper ${typewriterMode ? 'hidden' : ''}`} >
<div ref={editorRef} id="editor" />
<div ref={infoviewRef} id="infoview" />
</Split>
}

@ -0,0 +1,37 @@
import * as React from 'react';
import { useContext, useEffect, useRef, useState } from 'react'
import { useTranslation } from "react-i18next"
import { GameIdContext } from '../../state/context';
import { useLoadLevelQuery } from '../../state/api';
import { Markdown } from '../utils';
/** The mathematical formulation of the statement, supporting e.g. Latex
* It takes three forms, depending on the precence of name and description:
* - Theorem xyz: description
* - Theorem xyz
* - Exercises: description
*
* If `showLeanStatement` is true, it will additionally display the lean code.
*/
export function ExerciseStatement({ showLeanStatement = false }) {
let { t } = useTranslation()
const {gameId, worldId, levelId } = useContext(GameIdContext)
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
if (!(levelInfo.data?.descrText || levelInfo.data?.descrFormat)) { return <></> }
return <>
<div className="exercise-statement">
{levelInfo.data?.descrText ?
<Markdown>
{(levelInfo.data?.displayName ? `**${t("Theorem")}** \`${levelInfo.data?.displayName}\`: ` : '') + t(levelInfo.data?.descrText, {ns: gameId})}
</Markdown> : levelInfo.data?.displayName &&
<Markdown>
{`**${t("Theorem")}** \`${levelInfo.data?.displayName}\``}
</Markdown>
}
{levelInfo.data?.descrFormat && showLeanStatement &&
<p><code className="lean-code">{levelInfo.data?.descrFormat}</code></p>
}
</div>
</>
}

@ -11,7 +11,7 @@ import { WorldTreePanel } from './world_tree'
import i18next from 'i18next' import i18next from 'i18next'
import { ChatPanel } from './chat' import { ChatPanel } from './chat'
import { LevelWrapper } from './level' import { NewLevel } from './level'
import { GameHint, ProofState } from './infoview/rpc_api' import { GameHint, ProofState } from './infoview/rpc_api'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { Diagnostic } from 'vscode-languageserver-types' import { Diagnostic } from 'vscode-languageserver-types'
@ -54,7 +54,6 @@ function Game() {
const [interimDiags, setInterimDiags] = useState<Array<Diagnostic>>([]) const [interimDiags, setInterimDiags] = useState<Array<Diagnostic>>([])
const [isCrashed, setIsCrashed] = useState<Boolean>(false) const [isCrashed, setIsCrashed] = useState<Boolean>(false)
const typewriterMode = useSelector(selectTypewriterMode(gameId)) const typewriterMode = useSelector(selectTypewriterMode(gameId))
const setTypewriterMode = (newTypewriterMode: boolean) => dispatch(changeTypewriterMode({game: gameId, typewriterMode: newTypewriterMode})) const setTypewriterMode = (newTypewriterMode: boolean) => dispatch(changeTypewriterMode({game: gameId, typewriterMode: newTypewriterMode}))
@ -83,7 +82,7 @@ function Game() {
{<> {<>
<ChatPanel visible={worldId ? (levelId == 0 && page == 1) :(page == 0)} /> <ChatPanel visible={worldId ? (levelId == 0 && page == 1) :(page == 0)} />
{ worldId ? { worldId ?
<LevelWrapper visible={page == 1} /> : <NewLevel visible={page == 1} /> :
<WorldTreePanel visible={page == 1} /> <WorldTreePanel visible={page == 1} />
} }
<InventoryPanel visible={page == 2} /> <InventoryPanel visible={page == 2} />
@ -95,10 +94,12 @@ function Game() {
<ChatPanel /> <ChatPanel />
<div className="column"> <div className="column">
{/* Note: apparently without this `div` the split panel bugs out. */} {/* Note: apparently without this `div` the split panel bugs out. */}
{worldId ? <LevelWrapper /> : <WorldTreePanel /> } {worldId ?
<NewLevel />
: <WorldTreePanel /> }
</div> </div>
<InventoryPanel /> <InventoryPanel />
</Split> </Split>
} }
</ProofContext.Provider> </ProofContext.Provider>
</ChatContext.Provider> </ChatContext.Provider>

@ -1,382 +1,382 @@
/** // /**
* @fileOverview // * @fileOverview
* // *
* Mostly copied from https://github.com/leanprover/vscode-lean4/blob/master/lean4-infoview/src/infoview/goals.tsx // * Mostly copied from https://github.com/leanprover/vscode-lean4/blob/master/lean4-infoview/src/infoview/goals.tsx
*/ // */
import * as React from 'react' // import * as React from 'react'
import { InteractiveHypothesisBundle_nonAnonymousNames, MVarId, TaggedText_stripTags } from '@leanprover/infoview-api' // // import { InteractiveHypothesisBundle_nonAnonymousNames, MVarId, TaggedText_stripTags } from '@leanprover/infoview-api'
import { EditorContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts'; // // import { EditorContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts';
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 { InteractiveCode } from '../../../../node_modules/lean4-infoview/src/infoview/interactiveCode' // // import { InteractiveCode } from '../../../../node_modules/lean4-infoview/src/infoview/interactiveCode'
import { WithTooltipOnHover } from '../../../../node_modules/lean4-infoview/src/infoview/tooltips'; // // import { WithTooltipOnHover } from '../../../../node_modules/lean4-infoview/src/infoview/tooltips';
import { PageContext } from '../../state/context'; // import { PageContext } from '../../state/context';
import { InteractiveGoal, InteractiveGoals, InteractiveGoalsWithHints, InteractiveHypothesisBundle, ProofState } from './rpc_api'; // import { InteractiveGoal, InteractiveGoals, InteractiveGoalsWithHints, InteractiveHypothesisBundle, ProofState } from './rpc_api';
import { RpcSessionAtPos } from '@leanprover/infoview/*'; // // import { RpcSessionAtPos } from '@leanprover/infoview/*';
import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util'; // // import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util';
import { DiagnosticSeverity } from 'vscode-languageserver-protocol'; // import { DiagnosticSeverity } from 'vscode-languageserver-protocol';
import { useTranslation } from 'react-i18next'; // import { useTranslation } from 'react-i18next';
/** 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: InteractiveGoal): string {
let ret = '' // // let ret = ''
if (g.userName) { // // if (g.userName) {
ret += `case ${g.userName}\n` // // ret += `case ${g.userName}\n`
} // // }
for (const h of g.hyps) { // // for (const h of g.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) {
ret += ` := ${TaggedText_stripTags(h.val)}` // // ret += ` := ${TaggedText_stripTags(h.val)}`
} // // }
ret += '\n' // // ret += '\n'
} // // }
ret += `${TaggedText_stripTags(g.type)}` // // ret += `⊢ ${TaggedText_stripTags(g.type)}`
return ret // // return ret
} // // }
export function goalsToString(goals: InteractiveGoals): string { // // export function goalsToString(goals: InteractiveGoals): string {
return goals.goals.map(g => goalToString(g)).join('\n\n') // // return goals.goals.map(g => goalToString(g)).join('\n\n')
} // // }
export function goalsWithHintsToString(goals: InteractiveGoalsWithHints): string { // // export function goalsWithHintsToString(goals: InteractiveGoalsWithHints): string {
return goals.goals.map(g => goalToString(g.goal)).join('\n\n') // // return goals.goals.map(g => goalToString(g.goal)).join('\n\n')
} // // }
interface GoalFilterState { // interface GoalFilterState {
/** If true reverse the list of hypotheses, if false present the order received from LSP. */ // /** If true reverse the list of hypotheses, if false present the order received from LSP. */
reverse: boolean, // reverse: boolean,
/** If true show hypotheses that have isType=True, otherwise hide them. */ // /** If true show hypotheses that have isType=True, otherwise hide them. */
showType: boolean, // showType: boolean,
/** If true show hypotheses that have isInstance=True, otherwise hide them. */ // /** If true show hypotheses that have isInstance=True, otherwise hide them. */
showInstance: boolean, // showInstance: boolean,
/** If true show hypotheses that contain a dagger in the name, otherwise hide them. */ // /** If true show hypotheses that contain a dagger in the name, otherwise hide them. */
showHiddenAssumption: boolean // showHiddenAssumption: boolean
/** If true show the bodies of let-values, otherwise hide them. */ // /** If true show the bodies of let-values, otherwise hide them. */
showLetValue: boolean; // showLetValue: boolean;
} // }
function getFilteredHypotheses(hyps: InteractiveHypothesisBundle[], filter: GoalFilterState): InteractiveHypothesisBundle[] { // function getFilteredHypotheses(hyps: InteractiveHypothesisBundle[], filter: GoalFilterState): InteractiveHypothesisBundle[] {
return hyps.reduce((acc: InteractiveHypothesisBundle[], h) => { // return hyps.reduce((acc: InteractiveHypothesisBundle[], h) => {
if (h.isInstance && !filter.showInstance) return acc // if (h.isInstance && !filter.showInstance) return acc
if (h.isType && !filter.showType) return acc // if (h.isType && !filter.showType) return acc
const names = filter.showHiddenAssumption ? h.names : h.names.filter(n => !isInaccessibleName(n)) // const names = filter.showHiddenAssumption ? h.names : h.names.filter(n => !isInaccessibleName(n))
const hNew: InteractiveHypothesisBundle = filter.showLetValue ? { ...h, names } : { ...h, names, val: undefined } // const hNew: InteractiveHypothesisBundle = filter.showLetValue ? { ...h, names } : { ...h, names, val: undefined }
if (names.length !== 0) acc.push(hNew) // if (names.length !== 0) acc.push(hNew)
return acc // return acc
}, []) // }, [])
} // }
interface HypProps { // interface HypProps {
hyp: InteractiveHypothesisBundle // hyp: InteractiveHypothesisBundle
mvarId?: MVarId // mvarId?: any // MVarId
} // }
function Hyp({ hyp: h, mvarId }: HypProps) { // function Hyp({ hyp: h, mvarId }: HypProps) {
const locs = React.useContext(LocationsContext) // const locs = React.useContext(LocationsContext)
const namecls: string = 'mr1 ' + // const namecls: string = 'mr1 ' +
(h.isInserted ? 'inserted-text ' : '') + // (h.isInserted ? 'inserted-text ' : '') +
(h.isRemoved ? 'removed-text ' : '') // (h.isRemoved ? 'removed-text ' : '')
const names = InteractiveHypothesisBundle_nonAnonymousNames(h).map((n, i) => // const names = InteractiveHypothesisBundle_nonAnonymousNames(h).map((n, i) =>
<span className={namecls + (isInaccessibleName(n) ? 'goal-inaccessible ' : '')} key={i}> // <span className={namecls + (isInaccessibleName(n) ? 'goal-inaccessible ' : '')} key={i}>
<SelectableLocation // <SelectableLocation
locs={locs} // locs={locs}
loc={mvarId && h.fvarIds && h.fvarIds.length > i ? // loc={mvarId && h.fvarIds && h.fvarIds.length > i ?
{ mvarId, loc: { hyp: h.fvarIds[i] }} : // { mvarId, loc: { hyp: h.fvarIds[i] }} :
undefined // undefined
} // }
alwaysHighlight={false} // alwaysHighlight={false}
>{n}</SelectableLocation> // >{n}</SelectableLocation>
</span>) // </span>)
const typeLocs: Locations | undefined = React.useMemo(() => // const typeLocs: Locations | undefined = React.useMemo(() =>
locs && mvarId && h.fvarIds && h.fvarIds.length > 0 ? // locs && mvarId && h.fvarIds && h.fvarIds.length > 0 ?
{ ...locs, subexprTemplate: { mvarId, loc: { hypType: [h.fvarIds[0], ''] }}} : // { ...locs, subexprTemplate: { mvarId, loc: { hypType: [h.fvarIds[0], ''] }}} :
undefined, // undefined,
[locs, mvarId, h.fvarIds]) // [locs, mvarId, h.fvarIds])
const valLocs: Locations | undefined = React.useMemo(() => // const valLocs: Locations | undefined = React.useMemo(() =>
h.val && locs && mvarId && h.fvarIds && h.fvarIds.length > 0 ? // h.val && locs && mvarId && h.fvarIds && h.fvarIds.length > 0 ?
{ ...locs, subexprTemplate: { mvarId, loc: { hypValue: [h.fvarIds[0], ''] }}} : // { ...locs, subexprTemplate: { mvarId, loc: { hypValue: [h.fvarIds[0], ''] }}} :
undefined, // undefined,
[h.val, locs, mvarId, h.fvarIds]) // [h.val, locs, mvarId, h.fvarIds])
return <div> // return <div>
<strong className="goal-hyp">{names}</strong> // <strong className="goal-hyp">{names}</strong>
:&nbsp; // :&nbsp;
<LocationsContext.Provider value={typeLocs}> // <LocationsContext.Provider value={typeLocs}>
<InteractiveCode fmt={h.type} /> // <InteractiveCode fmt={h.type} />
</LocationsContext.Provider> // </LocationsContext.Provider>
{h.val && // {h.val &&
<LocationsContext.Provider value={valLocs}> // <LocationsContext.Provider value={valLocs}>
&nbsp;:=&nbsp;<InteractiveCode fmt={h.val} /> // &nbsp;:=&nbsp;<InteractiveCode fmt={h.val} />
</LocationsContext.Provider>} // </LocationsContext.Provider>}
</div> // </div>
} // }
interface GoalProps2 { // interface GoalProps2 {
goals: InteractiveGoal[] // goals: InteractiveGoal[]
filter: GoalFilterState // filter: GoalFilterState
} // }
interface GoalProps { // interface GoalProps {
goal: InteractiveGoal // goal: InteractiveGoal
filter: GoalFilterState // filter: GoalFilterState
showHints?: boolean // showHints?: boolean
typewriter: boolean // typewriter: boolean
unbundle?: boolean /** unbundle `x y : Nat` into `x : Nat` and `y : Nat` */ // unbundle?: boolean /** unbundle `x y : Nat` into `x : Nat` and `y : Nat` */
} // }
/** // /**
* Displays the hypotheses, target type and optional case label of a goal according to the // * Displays the hypotheses, target type and optional case label of a goal according to the
* provided `filter`. */ // * provided `filter`. */
export const Goal = React.memo((props: GoalProps) => { // export const Goal = React.memo((props: GoalProps) => {
const { goal, filter, showHints, typewriter, unbundle } = props // const { goal, filter, showHints, typewriter, unbundle } = props
let { t } = useTranslation() // let { t } = useTranslation()
// TODO: Apparently `goal` can be `undefined` // // TODO: Apparently `goal` can be `undefined`
if (!goal) {return <></>} // if (!goal) {return <></>}
const filteredList = getFilteredHypotheses(goal.hyps, filter); // const filteredList = getFilteredHypotheses(goal.hyps, filter);
const hyps = filter.reverse ? filteredList.slice().reverse() : filteredList; // const hyps = filter.reverse ? filteredList.slice().reverse() : filteredList;
const locs = React.useContext(LocationsContext) // const locs = React.useContext(LocationsContext)
const goalLocs = React.useMemo(() => // const goalLocs = React.useMemo(() =>
locs && goal.mvarId ? // locs && goal.mvarId ?
{ ...locs, subexprTemplate: { mvarId: goal.mvarId, loc: { target: '' }}} : // { ...locs, subexprTemplate: { mvarId: goal.mvarId, loc: { target: '' }}} :
undefined, // undefined,
[locs, goal.mvarId]) // [locs, goal.mvarId])
const goalLi = <div key={'goal'} className="goal"> // const goalLi = <div key={'goal'} className="goal">
{/* <div className="goal-title">{t("Goal")}:</div> */} // {/* <div className="goal-title">{t("Goal")}:</div> */}
<LocationsContext.Provider value={goalLocs}> // <LocationsContext.Provider value={goalLocs}>
<InteractiveCode fmt={goal.type} /> // <InteractiveCode fmt={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.isInserted) cn += 'b--inserted '
// if (props.goal.isRemoved) cn += 'b--removed ' // // if (props.goal.isRemoved) cn += 'b--removed '
function unbundleHyps (hyps: InteractiveHypothesisBundle[]) : InteractiveHypothesisBundle[] { // function unbundleHyps (hyps: InteractiveHypothesisBundle[]) : InteractiveHypothesisBundle[] {
return hyps.flatMap(hyp => ( // return hyps.flatMap(hyp => (
unbundle ? hyp.names.map(name => {return {...hyp, names: [name]}}) : [hyp] // unbundle ? hyp.names.map(name => {return {...hyp, names: [name]}}) : [hyp]
)) // ))
} // }
// const hints = <Hints hints={goal.hints} key={goal.mvarId} /> // // const hints = <Hints hints={goal.hints} key={goal.mvarId} />
const objectHyps = unbundleHyps(hyps.filter(hyp => !hyp.isAssumption)) // const objectHyps = unbundleHyps(hyps.filter(hyp => !hyp.isAssumption))
const assumptionHyps = unbundleHyps(hyps.filter(hyp => hyp.isAssumption)) // const assumptionHyps = unbundleHyps(hyps.filter(hyp => hyp.isAssumption))
return <> // return <>
{/* {goal.userName && <div><strong className="goal-case">case </strong>{goal.userName}</div>} */} // {/* {goal.userName && <div><strong className="goal-case">case </strong>{goal.userName}</div>} */}
{filter.reverse && goalLi} // {filter.reverse && goalLi}
<div className="hypotheses"> // <div className="hypotheses">
{! typewriter && objectHyps.length > 0 && // {! typewriter && objectHyps.length > 0 &&
<div className="hyp-group"><div className="hyp-group-title">{t("Objects")}:</div> // <div className="hyp-group"><div className="hyp-group-title">{t("Objects")}:</div>
{objectHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}</div> } // {objectHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}</div> }
{!typewriter && assumptionHyps.length > 0 && // {!typewriter && assumptionHyps.length > 0 &&
<div className="hyp-group"><div className="hyp-group-title">{t("Assumptions")}:</div> // <div className="hyp-group"><div className="hyp-group-title">{t("Assumptions")}:</div>
{assumptionHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}</div> } // {assumptionHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}</div> }
</div> // </div>
{!filter.reverse && <> // {!filter.reverse && <>
<div className='goal-sign'> // <div className='goal-sign'>
<svg width="100%" height="100%"> // <svg width="100%" height="100%">
<line x1="0%" y1="0%" x2="0%" y2="100%" /> // <line x1="0%" y1="0%" x2="0%" y2="100%" />
<line x1="0%" y1="50%" x2="100%" y2="50%" /> // <line x1="0%" y1="50%" x2="100%" y2="50%" />
</svg> // </svg>
</div> // </div>
{goalLi} // {goalLi}
</>} // </>}
{/* {showHints && hints} */} // {/* {showHints && hints} */}
</> // </>
}) // })
export const MainAssumptions = React.memo((props: GoalProps2) => { // export const MainAssumptions = React.memo((props: GoalProps2) => {
let { t } = useTranslation() // let { t } = useTranslation()
const { goals, filter } = props // const { goals, filter } = props
const goal = goals[0] // const goal = goals[0]
const filteredList = getFilteredHypotheses(goal.hyps, filter); // const filteredList = getFilteredHypotheses(goal.hyps, filter);
const hyps = filter.reverse ? filteredList.slice().reverse() : filteredList; // const hyps = filter.reverse ? filteredList.slice().reverse() : filteredList;
const locs = React.useContext(LocationsContext) // const locs = React.useContext(LocationsContext)
const goalLocs = React.useMemo(() => // const goalLocs = React.useMemo(() =>
locs && goal.mvarId ? // locs && goal.mvarId ?
{ ...locs, subexprTemplate: { mvarId: goal.mvarId, loc: { target: '' }}} : // { ...locs, subexprTemplate: { mvarId: goal.mvarId, loc: { target: '' }}} :
undefined, // undefined,
[locs, goal.mvarId]) // [locs, goal.mvarId])
const goalLi = <div key={'goal'}> // const goalLi = <div key={'goal'}>
<div className="goal-title">{t("Goal") + ":"}</div> // <div className="goal-title">{t("Goal") + ":"}</div>
<LocationsContext.Provider value={goalLocs}> // <LocationsContext.Provider value={goalLocs}>
<InteractiveCode fmt={goal.type} /> // <InteractiveCode fmt={goal.type} />
</LocationsContext.Provider> // </LocationsContext.Provider>
</div> // </div>
const objectHyps = hyps.filter(hyp => !hyp.isAssumption) // const objectHyps = hyps.filter(hyp => !hyp.isAssumption)
const assumptionHyps = hyps.filter(hyp => hyp.isAssumption) // const assumptionHyps = hyps.filter(hyp => hyp.isAssumption)
return <div id="main-assumptions"> // return <div id="main-assumptions">
<div className="goals-section-title">{t("Current Goal")}</div> // <div className="goals-section-title">{t("Current Goal")}</div>
{filter.reverse && goalLi} // {filter.reverse && goalLi}
{ objectHyps.length > 0 && // { objectHyps.length > 0 &&
<div className="hyp-group"><div className="hyp-group-title">{t("Objects") + ":"}</div> // <div className="hyp-group"><div className="hyp-group-title">{t("Objects") + ":"}</div>
{objectHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}</div> } // {objectHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}</div> }
{ assumptionHyps.length > 0 && // { assumptionHyps.length > 0 &&
<div className="hyp-group"> // <div className="hyp-group">
<div className="hyp-group-title">{t("Assumptions") + ":"}</div> // <div className="hyp-group-title">{t("Assumptions") + ":"}</div>
{assumptionHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)} // {assumptionHyps.map((h, i) => <Hyp hyp={h} mvarId={goal.mvarId} key={i} />)}
</div> } // </div> }
</div> // </div>
}) // })
export const OtherGoals = React.memo((props: GoalProps2) => { // export const OtherGoals = React.memo((props: GoalProps2) => {
let { t } = useTranslation() // let { t } = useTranslation()
const { goals, filter } = props // const { goals, filter } = props
return <> // return <>
{goals && goals.length > 1 && // {goals && goals.length > 1 &&
<div id="other-goals" className="other-goals"> // <div id="other-goals" className="other-goals">
<div className="goals-section-title">{t("Further Goals")}</div> // <div className="goals-section-title">{t("Further Goals")}</div>
{goals.slice(1).map((goal, i) => // {goals.slice(1).map((goal, i) =>
<details key={i}> // <details key={i}>
<summary> // <summary>
<InteractiveCode fmt={goal.type} /> // <InteractiveCode fmt={goal.type} />
</summary> // </summary>
<Goal typewriter={false} filter={filter} goal={goal} /> // <Goal typewriter={false} filter={filter} goal={goal} />
</details>)} // </details>)}
</div>} // </div>}
</> // </>
}) // })
interface GoalsProps { // interface GoalsProps {
goals: InteractiveGoalsWithHints // goals: InteractiveGoalsWithHints
filter: GoalFilterState // filter: GoalFilterState
} // }
export function Goals({ goals, filter }: GoalsProps) { // export function Goals({ goals, filter }: GoalsProps) {
if (goals.goals.length === 0) { // if (goals.goals.length === 0) {
return <></> // return <></>
} else { // } else {
return <> // return <>
{goals.goals.map((g, i) => <Goal typewriter={false} key={i} goal={g.goal} filter={filter} />)} // {goals.goals.map((g, i) => <Goal typewriter={false} key={i} goal={g.goal} filter={filter} />)}
</> // </>
} // }
} // }
interface FilteredGoalsProps { // interface FilteredGoalsProps {
/** Components to render in the header. */ // /** Components to render in the header. */
headerChildren: React.ReactNode // headerChildren: React.ReactNode
/** // /**
* 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?: InteractiveGoalsWithHints // goals?: InteractiveGoalsWithHints
} // }
/** // /**
* Display goals together with a header containing the provided children as well as buttons // * Display goals together with a header containing the provided children as well as buttons
* to control how the goals are displayed. // * to control how the goals are displayed.
*/ // */
export const FilteredGoals = React.memo(({ headerChildren, goals }: FilteredGoalsProps) => { // export const FilteredGoals = React.memo(({ headerChildren, goals }: FilteredGoalsProps) => {
const ec = React.useContext(EditorContext) // const ec = React.useContext(EditorContext)
const copyToCommentButton = // const copyToCommentButton =
<a className="link pointer mh2 dim codicon codicon-quote" // <a className="link pointer mh2 dim codicon codicon-quote"
data-id="copy-goal-to-comment" // data-id="copy-goal-to-comment"
onClick={e => { // onClick={e => {
e.preventDefault(); // e.preventDefault();
if (goals) void ec.copyToComment(goalsWithHintsToString(goals)) // if (goals) void ec.copyToComment(goalsWithHintsToString(goals))
}} // }}
title="copy state to comment" /> // title="copy state to comment" />
const [goalFilters, setGoalFilters] = React.useState<GoalFilterState>( // const [goalFilters, setGoalFilters] = React.useState<GoalFilterState>(
{ reverse: false, showType: true, showInstance: true, showHiddenAssumption: true, showLetValue: true }); // { reverse: false, showType: true, showInstance: true, showHiddenAssumption: true, showLetValue: true });
const sortClasses = 'link pointer mh2 dim codicon ' + (goalFilters.reverse ? 'codicon-arrow-up ' : 'codicon-arrow-down '); // const sortClasses = 'link pointer mh2 dim codicon ' + (goalFilters.reverse ? 'codicon-arrow-up ' : 'codicon-arrow-down ');
const sortButton = // const sortButton =
<a className={sortClasses} title="reverse list" // <a className={sortClasses} title="reverse list"
onClick={_ => setGoalFilters(s => ({ ...s, reverse: !s.reverse }))} /> // onClick={_ => setGoalFilters(s => ({ ...s, reverse: !s.reverse }))} />
const mkFilterButton = (filterFn: React.SetStateAction<GoalFilterState>, filledFn: (_: GoalFilterState) => boolean, name: string) => // const mkFilterButton = (filterFn: React.SetStateAction<GoalFilterState>, filledFn: (_: GoalFilterState) => boolean, name: string) =>
<a className='link pointer tooltip-menu-content' onClick={_ => { setGoalFilters(filterFn) }}> // <a className='link pointer tooltip-menu-content' onClick={_ => { setGoalFilters(filterFn) }}>
<span className={'tooltip-menu-icon codicon ' + (filledFn(goalFilters) ? 'codicon-check ' : 'codicon-blank ')}>&nbsp;</span> // <span className={'tooltip-menu-icon codicon ' + (filledFn(goalFilters) ? 'codicon-check ' : 'codicon-blank ')}>&nbsp;</span>
<span className='tooltip-menu-text '>{name}</span> // <span className='tooltip-menu-text '>{name}</span>
</a> // </a>
const filterMenu = <span> // const filterMenu = <span>
{mkFilterButton(s => ({ ...s, showType: !s.showType }), gf => gf.showType, 'types')} // {mkFilterButton(s => ({ ...s, showType: !s.showType }), gf => gf.showType, 'types')}
<br/> // <br/>
{mkFilterButton(s => ({ ...s, showInstance: !s.showInstance }), gf => gf.showInstance, 'instances')} // {mkFilterButton(s => ({ ...s, showInstance: !s.showInstance }), gf => gf.showInstance, 'instances')}
<br/> // <br/>
{mkFilterButton(s => ({ ...s, showHiddenAssumption: !s.showHiddenAssumption }), gf => gf.showHiddenAssumption, 'hidden assumptions')} // {mkFilterButton(s => ({ ...s, showHiddenAssumption: !s.showHiddenAssumption }), gf => gf.showHiddenAssumption, 'hidden assumptions')}
<br/> // <br/>
{mkFilterButton(s => ({ ...s, showLetValue: !s.showLetValue }), gf => gf.showLetValue, 'let-values')} // {mkFilterButton(s => ({ ...s, showLetValue: !s.showLetValue }), gf => gf.showLetValue, 'let-values')}
</span> // </span>
const isFiltered = !goalFilters.showInstance || !goalFilters.showType || !goalFilters.showHiddenAssumption || !goalFilters.showLetValue // const isFiltered = !goalFilters.showInstance || !goalFilters.showType || !goalFilters.showHiddenAssumption || !goalFilters.showLetValue
const filterButton = // const filterButton =
<WithTooltipOnHover mkTooltipContent={() => filterMenu}> // <WithTooltipOnHover mkTooltipContent={() => filterMenu}>
<a className={'link pointer mh2 dim codicon ' + (isFiltered ? 'codicon-filter-filled ': 'codicon-filter ')}/> // <a className={'link pointer mh2 dim codicon ' + (isFiltered ? 'codicon-filter-filled ': 'codicon-filter ')}/>
</WithTooltipOnHover> // </WithTooltipOnHover>
return <div style={{display: goals !== undefined ? 'block' : 'none'}}> // return <div style={{display: goals !== undefined ? 'block' : 'none'}}>
<details open> // <details open>
<summary className='mv2 pointer'> // <summary className='mv2 pointer'>
{headerChildren} // {headerChildren}
<span className='fr'>{copyToCommentButton}{sortButton}{filterButton}</span> // <span className='fr'>{copyToCommentButton}{sortButton}{filterButton}</span>
</summary> // </summary>
<div className='ml1'> // <div className='ml1'>
{goals && <Goals goals={goals} filter={goalFilters}></Goals>} // {goals && <Goals goals={goals} filter={goalFilters}></Goals>}
</div> // </div>
</details> // </details>
</div> // </div>
}) // })
export function loadGoals( // export function loadGoals(
rpcSess: RpcSessionAtPos, // rpcSess: RpcSessionAtPos,
uri: string, // uri: string,
setProof: React.Dispatch<React.SetStateAction<ProofState>>, // setProof: React.Dispatch<React.SetStateAction<ProofState>>,
setCrashed: React.Dispatch<React.SetStateAction<Boolean>>) { // setCrashed: React.Dispatch<React.SetStateAction<Boolean>>) {
console.info('sending rpc request to load the proof state') // console.info('sending rpc request to load the proof state')
rpcSess.call('Game.getProofState', DocumentPosition.toTdpp({line: 0, character: 0, uri: uri})).then( // rpcSess.call('Game.getProofState', DocumentPosition.toTdpp({line: 0, character: 0, uri: uri})).then(
(proof : ProofState) => { // (proof : ProofState) => {
if (typeof proof !== 'undefined') { // if (typeof proof !== 'undefined') {
console.info(`received a proof state!`) // console.info(`received a proof state!`)
console.log(proof) // console.log(proof)
setProof(proof) // setProof(proof)
setCrashed(false) // setCrashed(false)
} else { // } else {
console.warn('received undefined proof state!') // console.warn('received undefined proof state!')
setCrashed(true) // setCrashed(true)
// setProof(undefined) // // setProof(undefined)
} // }
} // }
).catch((error) => { // ).catch((error) => {
setCrashed(true) // setCrashed(true)
console.warn(error) // console.warn(error)
}) // })
} // }
export function lastStepHasErrors (proof : ProofState): boolean { // export function lastStepHasErrors (proof : ProofState): boolean {
if (!proof?.steps.length) {return false} // if (!proof?.steps.length) {return false}
let diags = [...proof.steps[proof.steps.length - 1].diags, ...proof.diagnostics] // let diags = [...proof.steps[proof.steps.length - 1].diags, ...proof.diagnostics]
return diags.some( // return diags.some(
(d) => (d.severity == DiagnosticSeverity.Error ) // || d.severity == DiagnosticSeverity.Warning // (d) => (d.severity == DiagnosticSeverity.Error ) // || d.severity == DiagnosticSeverity.Warning
) // )
} // }
export function isLastStepWithErrors (proof : ProofState, i: number): boolean { // export function isLastStepWithErrors (proof : ProofState, i: number): boolean {
if (!proof?.steps.length) {return false} // if (!proof?.steps.length) {return false}
return (i == proof.steps.length - 1) && lastStepHasErrors(proof) // return (i == proof.steps.length - 1) && lastStepHasErrors(proof)
} // }

@ -1,442 +1,442 @@
/* Mostly copied from https://github.com/leanprover/vscode-lean4/blob/master/lean4-infoview/src/infoview/info.tsx */ // /* Mostly copied from https://github.com/leanprover/vscode-lean4/blob/master/lean4-infoview/src/infoview/info.tsx */
import * as React from 'react' // import * as React from 'react'
import { CircularProgress } from '@mui/material' // import { CircularProgress } from '@mui/material'
import type { Location, Diagnostic } from 'vscode-languageserver-protocol' // import type { Location, Diagnostic } from 'vscode-languageserver-protocol'
import { getInteractiveTermGoal, InteractiveDiagnostic, UserWidgetInstance, Widget_getWidgets, RpcSessionAtPos, isRpcError, // import { getInteractiveTermGoal, InteractiveDiagnostic, UserWidgetInstance, Widget_getWidgets, RpcSessionAtPos, isRpcError,
RpcErrorCode, getInteractiveDiagnostics } from '@leanprover/infoview-api' // RpcErrorCode, getInteractiveDiagnostics } from '@leanprover/infoview-api'
import { basename, DocumentPosition, RangeHelpers, useEvent, usePausableState, discardMethodNotFound, // import { basename, DocumentPosition, RangeHelpers, useEvent, usePausableState, discardMethodNotFound,
mapRpcError, useAsyncWithTrigger, PausableProps } from '../../../../node_modules/lean4-infoview/src/infoview/util' // mapRpcError, useAsyncWithTrigger, PausableProps } from '../../../../node_modules/lean4-infoview/src/infoview/util'
import { ConfigContext, EditorContext, LspDiagnosticsContext, ProgressContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts' // import { ConfigContext, EditorContext, LspDiagnosticsContext, ProgressContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts'
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'
import { AllMessages, lspDiagToInteractive } from './messages' // import { AllMessages, lspDiagToInteractive } from './messages'
import { goalsToString, Goal, MainAssumptions, OtherGoals } from './goals' // // import { goalsToString, Goal, MainAssumptions, OtherGoals } from './goals'
import { InteractiveTermGoal, InteractiveGoalsWithHints, InteractiveGoals, ProofState } from './rpc_api' // import { InteractiveTermGoal, InteractiveGoalsWithHints, InteractiveGoals, ProofState } from './rpc_api'
import { MonacoEditorContext, ProofStateProps, InfoStatus, ProofContext } from '../../state/context' // import { MonacoEditorContext, ProofStateProps, InfoStatus, ProofContext } from '../../state/context'
import { useTranslation } from 'react-i18next' // import { useTranslation } from 'react-i18next'
import { GoalsTabs } from './main' // import { GoalsTabs } from './main'
// TODO: All about pinning could probably be removed // // TODO: All about pinning could probably be removed
type InfoKind = 'cursor' | 'pin' // type InfoKind = 'cursor' | 'pin'
interface InfoPinnable { // interface InfoPinnable {
kind: InfoKind // kind: InfoKind
/** Takes an argument for caching reasons, but should only ever (un)pin itself. */ // /** Takes an argument for caching reasons, but should only ever (un)pin itself. */
onPin: (pos: DocumentPosition) => void // onPin: (pos: DocumentPosition) => void
} // }
interface InfoStatusBarProps extends InfoPinnable, PausableProps { // interface InfoStatusBarProps extends InfoPinnable, PausableProps {
pos: DocumentPosition // pos: DocumentPosition
status: InfoStatus // status: InfoStatus
triggerUpdate: () => Promise<void> // triggerUpdate: () => Promise<void>
} // }
const InfoStatusBar = React.memo((props: InfoStatusBarProps) => { // const InfoStatusBar = React.memo((props: InfoStatusBarProps) => {
const { kind, onPin, status, pos, isPaused, setPaused, triggerUpdate } = props // const { kind, onPin, status, pos, isPaused, setPaused, triggerUpdate } = props
const ec = React.useContext(EditorContext) // const ec = React.useContext(EditorContext)
const statusColTable: {[T in InfoStatus]: string} = { // const statusColTable: {[T in InfoStatus]: string} = {
'updating': 'gold ', // 'updating': 'gold ',
'error': 'dark-red ', // 'error': 'dark-red ',
'ready': '', // 'ready': '',
} // }
const statusColor = statusColTable[status] // const statusColor = statusColTable[status]
const locationString = `${basename(pos.uri)}:${pos.line+1}:${pos.character}` // const locationString = `${basename(pos.uri)}:${pos.line+1}:${pos.character}`
const isPinned = kind === 'pin' // const isPinned = kind === 'pin'
return ( // return (
<summary style={{transition: 'color 0.5s ease'}} className={'mv2 pointer ' + statusColor}> // <summary style={{transition: 'color 0.5s ease'}} className={'mv2 pointer ' + statusColor}>
{locationString} // {locationString}
{isPinned && !isPaused && ' (pinned)'} // {isPinned && !isPaused && ' (pinned)'}
{!isPinned && isPaused && ' (paused)'} // {!isPinned && isPaused && ' (paused)'}
{isPinned && isPaused && ' (pinned and paused)'} // {isPinned && isPaused && ' (pinned and paused)'}
<span className='fr'> // <span className='fr'>
{isPinned && // {isPinned &&
<a className='link pointer mh2 dim codicon codicon-go-to-file' // <a className='link pointer mh2 dim codicon codicon-go-to-file'
data-id='reveal-file-location' // data-id='reveal-file-location'
onClick={e => { e.preventDefault(); void ec.revealPosition(pos); }} // onClick={e => { e.preventDefault(); void ec.revealPosition(pos); }}
title='reveal file location' />} // title='reveal file location' />}
<a className={'link pointer mh2 dim codicon ' + (isPinned ? 'codicon-pinned ' : 'codicon-pin ')} // <a className={'link pointer mh2 dim codicon ' + (isPinned ? 'codicon-pinned ' : 'codicon-pin ')}
data-id='toggle-pinned' // data-id='toggle-pinned'
onClick={e => { e.preventDefault(); onPin(pos); }} // onClick={e => { e.preventDefault(); onPin(pos); }}
title={isPinned ? 'unpin' : 'pin'} /> // title={isPinned ? 'unpin' : 'pin'} />
<a className={'link pointer mh2 dim codicon ' + (isPaused ? 'codicon-debug-continue ' : 'codicon-debug-pause ')} // <a className={'link pointer mh2 dim codicon ' + (isPaused ? 'codicon-debug-continue ' : 'codicon-debug-pause ')}
data-id='toggle-paused' // data-id='toggle-paused'
onClick={e => { e.preventDefault(); setPaused(!isPaused) }} // onClick={e => { e.preventDefault(); setPaused(!isPaused) }}
title={isPaused ? 'continue updating' : 'pause updating'} /> // title={isPaused ? 'continue updating' : 'pause updating'} />
<a className='link pointer mh2 dim codicon codicon-refresh' // <a className='link pointer mh2 dim codicon codicon-refresh'
data-id='update' // data-id='update'
onClick={e => { e.preventDefault(); void triggerUpdate() }} // onClick={e => { e.preventDefault(); void triggerUpdate() }}
title='update'/> // title='update'/>
</span> // </span>
</summary> // </summary>
) // )
}) // })
interface InfoDisplayContentProps extends PausableProps { // interface InfoDisplayContentProps extends PausableProps {
pos: DocumentPosition // pos: DocumentPosition
messages: InteractiveDiagnostic[] // messages: InteractiveDiagnostic[]
goals?: InteractiveGoals // goals?: InteractiveGoals
termGoal?: InteractiveTermGoal // termGoal?: InteractiveTermGoal
error?: string // error?: string
userWidgets: UserWidgetInstance[] // userWidgets: UserWidgetInstance[]
triggerUpdate: () => Promise<void> // triggerUpdate: () => Promise<void>
proofString? : string // proofString? : string
} // }
const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => { // const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => {
let { t } = useTranslation() // let { t } = useTranslation()
const {pos, messages, goals, termGoal, error, userWidgets, triggerUpdate, isPaused, setPaused, proofString} = props // const {pos, messages, goals, termGoal, error, userWidgets, triggerUpdate, isPaused, setPaused, proofString} = props
const hasWidget = userWidgets.length > 0 // const hasWidget = userWidgets.length > 0
const hasError = !!error // const hasError = !!error
const hasMessages = messages.length !== 0 // const hasMessages = messages.length !== 0
const nothingToShow = !hasError && !goals && !termGoal && !hasMessages && !hasWidget // const nothingToShow = !hasError && !goals && !termGoal && !hasMessages && !hasWidget
const [selectedLocs, setSelectedLocs] = React.useState<GoalsLocation[]>([]) // const [selectedLocs, setSelectedLocs] = React.useState<GoalsLocation[]>([])
React.useEffect(() => setSelectedLocs([]), [pos.uri, pos.line, pos.character]) // React.useEffect(() => setSelectedLocs([]), [pos.uri, pos.line, pos.character])
const locs: Locations = React.useMemo(() => ({ // const locs: Locations = React.useMemo(() => ({
isSelected: (l: GoalsLocation) => selectedLocs.some(v => GoalsLocation.isEqual(v, l)), // isSelected: (l: GoalsLocation) => selectedLocs.some(v => GoalsLocation.isEqual(v, l)),
setSelected: (l, act) => setSelectedLocs(ls => { // setSelected: (l, act) => setSelectedLocs(ls => {
// We ensure that `selectedLocs` maintains its reference identity if the selection // // We ensure that `selectedLocs` maintains its reference identity if the selection
// status of `l` didn't change. // // status of `l` didn't change.
const newLocs = ls.filter(v => !GoalsLocation.isEqual(v, l)) // const newLocs = ls.filter(v => !GoalsLocation.isEqual(v, l))
const wasSelected = newLocs.length !== ls.length // const wasSelected = newLocs.length !== ls.length
const isSelected = typeof act === 'function' ? act(wasSelected) : act // const isSelected = typeof act === 'function' ? act(wasSelected) : act
if (isSelected) newLocs.push(l) // if (isSelected) newLocs.push(l)
return wasSelected === isSelected ? ls : newLocs // return wasSelected === isSelected ? ls : newLocs
}), // }),
subexprTemplate: undefined // subexprTemplate: undefined
}), [selectedLocs]) // }), [selectedLocs])
const goalFilter = { reverse: false, showType: true, showInstance: true, showHiddenAssumption: true, showLetValue: true } // 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 */ // /* Adding {' '} to manage string literals properly: https://reactjs.org/docs/jsx-in-depth.html#string-literals-1 */
return <> // return <>
{hasError && // {hasError &&
<div className='error' key='errors'> // <div className='error' key='errors'>
Error updating:{' '}{error}. // Error updating:{' '}{error}.
<a className='link pointer dim' onClick={e => { e.preventDefault(); void triggerUpdate() }}>{' '}Try again.</a> // <a className='link pointer dim' onClick={e => { e.preventDefault(); void triggerUpdate() }}>{' '}Try again.</a>
</div>} // </div>}
<AllMessages /> {/* TODO: Move error messages to Chat instead */} // <AllMessages /> {/* TODO: Move error messages to Chat instead */}
<LocationsContext.Provider value={locs}> // <LocationsContext.Provider value={locs}>
<div className="goals-section"> // <div className="goals-section">
{ goals && goals.goals.length > 0 && <> // { goals && goals.goals.length > 0 && <>
<GoalsTabs goals={goals.goals.map(goal => ({goal: goal, hints: []}))} last={false} onClick={() => {}} onGoalChange={() => {}}/> // <GoalsTabs goals={goals.goals.map(goal => ({goal: goal, hints: []}))} last={false} onClick={() => {}} onGoalChange={() => {}}/>
{/* <MainAssumptions filter={goalFilter} key='mainGoal' goals={goals.goals} /> // {/* <MainAssumptions filter={goalFilter} key='mainGoal' goals={goals.goals} />
<OtherGoals filter={goalFilter} goals={goals.goals} /> */} // <OtherGoals filter={goalFilter} goals={goals.goals} /> */}
</>} // </>}
</div> // </div>
{/* <div> // {/* <div>
{ goals && (goals.goals.length > 0 // { 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.goals[0]} showHints={true} />
: <div className="goals-section-title">{t("No Goals")}</div> // : <div className="goals-section-title">{t("No Goals")}</div>
)} // )}
</div> */} // </div> */}
</LocationsContext.Provider> // </LocationsContext.Provider>
{/* {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 : []} // <PanelWidgetDisplay pos={pos} goals={goals ? goals.goals : []}
termGoal={termGoal} selectedLocations={selectedLocs} widget={widget}/> // termGoal={termGoal} selectedLocations={selectedLocs} widget={widget}/>
</details> // </details>
)} */} // )} */}
{/* {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 * /
<span>Updating is paused.{' '} // <span>Updating is paused.{' '}
<a className='link pointer dim' onClick={e => { e.preventDefault(); void triggerUpdate() }}>Refresh</a> // <a className='link pointer dim' onClick={e => { e.preventDefault(); void triggerUpdate() }}>Refresh</a>
{' '}or <a className='link pointer dim' onClick={e => { e.preventDefault(); setPaused(false) }}>resume updating</a> // {' '}or <a className='link pointer dim' onClick={e => { e.preventDefault(); setPaused(false) }}>resume updating</a>
{' '}to see information. // {' '}to see information.
</span> : // </span> :
<><CircularProgress /><div>{t("Loading goal…")}</div></>)} */} // <><CircularProgress /><div>{t("Loading goal…")}</div></>)} */}
{/* <LocationsContext.Provider value={locs}> // {/* <LocationsContext.Provider value={locs}>
{goals && goals.goals.length > 1 && <div className="goals-section other-goals"> // {goals && goals.goals.length > 1 && <div className="goals-section other-goals">
<div className="goals-section-title">Weitere Goals</div> // <div className="goals-section-title">Weitere Goals</div>
{goals.goals.slice(1).map((goal, i) => // {goals.goals.slice(1).map((goal, i) =>
<details key={i}><summary><InteractiveCode fmt={goal.type} /></summary> <Goal typewriter={false} filter={goalFilter} goal={goal} /></details>)} // <details key={i}><summary><InteractiveCode fmt={goal.type} /></summary> <Goal typewriter={false} filter={goalFilter} goal={goal} /></details>)}
</div>} // </div>}
</LocationsContext.Provider> */} // </LocationsContext.Provider> */}
</> // </>
}) // })
interface InfoDisplayProps { // interface InfoDisplayProps {
pos: DocumentPosition, // pos: DocumentPosition,
status: InfoStatus, // status: InfoStatus,
messages: InteractiveDiagnostic[], // messages: InteractiveDiagnostic[],
proof?: ProofState, // proof?: ProofState,
goals?: InteractiveGoals, // goals?: InteractiveGoals,
termGoal?: InteractiveTermGoal, // termGoal?: InteractiveTermGoal,
error?: string, // error?: string,
userWidgets: UserWidgetInstance[], // userWidgets: UserWidgetInstance[],
rpcSess: RpcSessionAtPos, // rpcSess: RpcSessionAtPos,
triggerUpdate: () => Promise<void>, // triggerUpdate: () => Promise<void>,
} // }
/** Displays goal state and messages. Can be paused. */ // /** Displays goal state and messages. Can be paused. */
function InfoDisplay(props0: InfoDisplayProps & InfoPinnable) { // function InfoDisplay(props0: InfoDisplayProps & InfoPinnable) {
// Used to update the paused state *just once* if it is paused, // // Used to update the paused state *just once* if it is paused,
// but a display update is triggered // // but a display update is triggered
const [shouldRefresh, setShouldRefresh] = React.useState<boolean>(false) // const [shouldRefresh, setShouldRefresh] = React.useState<boolean>(false)
const [{ isPaused, setPaused }, props, propsRef] = usePausableState(false, props0) // const [{ isPaused, setPaused }, props, propsRef] = usePausableState(false, props0)
if (shouldRefresh) { // if (shouldRefresh) {
propsRef.current = props0 // propsRef.current = props0
setShouldRefresh(false) // setShouldRefresh(false)
} // }
const triggerDisplayUpdate = async () => { // const triggerDisplayUpdate = async () => {
await props0.triggerUpdate() // await props0.triggerUpdate()
setShouldRefresh(true) // setShouldRefresh(true)
} // }
const {kind, goals, rpcSess} = props // const {kind, goals, rpcSess} = props
const ec = React.useContext(EditorContext) // const ec = React.useContext(EditorContext)
// If we are the cursor infoview, then we should subscribe to // // If we are the cursor infoview, then we should subscribe to
// some commands from the editor extension // // some commands from the editor extension
const isCursor = kind === 'cursor' // const isCursor = kind === 'cursor'
useEvent(ec.events.requestedAction, act => { // useEvent(ec.events.requestedAction, act => {
if (!isCursor) return // if (!isCursor) return
if (act.kind !== 'copyToComment') return // if (act.kind !== 'copyToComment') return
if (goals) void ec.copyToComment(goalsToString(goals)) // if (goals) void ec.copyToComment(goalsToString(goals))
}, [goals]) // }, [goals])
useEvent(ec.events.requestedAction, act => { // useEvent(ec.events.requestedAction, act => {
if (!isCursor) return // if (!isCursor) return
if (act.kind !== 'togglePaused') return // if (act.kind !== 'togglePaused') return
setPaused(isPaused => !isPaused) // setPaused(isPaused => !isPaused)
}) // })
const editor = React.useContext(MonacoEditorContext) // const editor = React.useContext(MonacoEditorContext)
return ( // return (
<RpcContext.Provider value={rpcSess}> // <RpcContext.Provider value={rpcSess}>
{/* <details open> */} // {/* <details open> */}
{/* <InfoStatusBar {...props} triggerUpdate={triggerDisplayUpdate} isPaused={isPaused} setPaused={setPaused} /> */} // {/* <InfoStatusBar {...props} triggerUpdate={triggerDisplayUpdate} isPaused={isPaused} setPaused={setPaused} /> */}
<div className="vscode-light"> // <div className="vscode-light">
<InfoDisplayContent {...props} proofString={editor.getValue()} triggerUpdate={triggerDisplayUpdate} isPaused={isPaused} setPaused={setPaused} /> // <InfoDisplayContent {...props} proofString={editor.getValue()} triggerUpdate={triggerDisplayUpdate} isPaused={isPaused} setPaused={setPaused} />
</div> // </div>
{/* </details> */} // {/* </details> */}
</RpcContext.Provider> // </RpcContext.Provider>
) // )
} // }
/** // /**
* Note: in the cursor view, we have to keep the cursor position as part of the component state // * Note: in the cursor view, we have to keep the cursor position as part of the component state
* to avoid flickering when the cursor moved. Otherwise, the component is re-initialised and the // * to avoid flickering when the cursor moved. Otherwise, the component is re-initialised and the
* goal states reset to `undefined` on cursor moves. // * goal states reset to `undefined` on cursor moves.
*/ // */
export type InfoProps = InfoPinnable & { pos?: DocumentPosition } // export type InfoProps = InfoPinnable & { pos?: DocumentPosition }
/** Fetches info from the server and renders an {@link InfoDisplay}. */ // /** Fetches info from the server and renders an {@link InfoDisplay}. */
export function Info(props: InfoProps) { // export function Info(props: InfoProps) {
if (props.kind === 'cursor') return <InfoAtCursor {...props} /> // if (props.kind === 'cursor') return <InfoAtCursor {...props} />
else return <InfoAux {...props} pos={props.pos} /> // else return <InfoAux {...props} pos={props.pos} />
} // }
function InfoAtCursor(props: InfoProps) { // function InfoAtCursor(props: InfoProps) {
const ec = React.useContext(EditorContext) // const ec = React.useContext(EditorContext)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const [curLoc, setCurLoc] = React.useState<Location>(ec.events.changedCursorLocation.current!) // const [curLoc, setCurLoc] = React.useState<Location>(ec.events.changedCursorLocation.current!)
useEvent(ec.events.changedCursorLocation, loc => loc && setCurLoc(loc), []) // useEvent(ec.events.changedCursorLocation, loc => loc && setCurLoc(loc), [])
const pos = { uri: curLoc.uri, ...curLoc.range.start } // const pos = { uri: curLoc.uri, ...curLoc.range.start }
return <InfoAux {...props} pos={pos} /> // return <InfoAux {...props} pos={pos} />
} // }
function useIsProcessingAt(p: DocumentPosition): boolean { // function useIsProcessingAt(p: DocumentPosition): boolean {
const allProgress = React.useContext(ProgressContext) // const allProgress = React.useContext(ProgressContext)
const processing = allProgress.get(p.uri) // const processing = allProgress.get(p.uri)
if (!processing) return false // if (!processing) return false
return processing.some(i => RangeHelpers.contains(i.range, p)) // return processing.some(i => RangeHelpers.contains(i.range, p))
} // }
function InfoAux(props: InfoProps) { // function InfoAux(props: InfoProps) {
const { setProof } = React.useContext(ProofContext) // const { setProof } = React.useContext(ProofContext)
const config = React.useContext(ConfigContext) // const config = React.useContext(ConfigContext)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const pos = props.pos! // const pos = props.pos!
const rpcSess = useRpcSessionAtPos(pos) // const rpcSess = useRpcSessionAtPos(pos)
// Compute the LSP diagnostics at this info's position. We try to ensure that if these remain // // Compute the LSP diagnostics at this info's position. We try to ensure that if these remain
// the same, then so does the identity of `lspDiagsHere` so that it can be used as a dep. // // the same, then so does the identity of `lspDiagsHere` so that it can be used as a dep.
const lspDiags = React.useContext(LspDiagnosticsContext) // const lspDiags = React.useContext(LspDiagnosticsContext)
const [lspDiagsHere, setLspDiagsHere] = React.useState<Diagnostic[]>([]) // const [lspDiagsHere, setLspDiagsHere] = React.useState<Diagnostic[]>([])
React.useEffect(() => { // React.useEffect(() => {
// Note: the curly braces are important. https://medium.com/geekculture/react-uncaught-typeerror-destroy-is-not-a-function-192738a6e79b // // Note: the curly braces are important. https://medium.com/geekculture/react-uncaught-typeerror-destroy-is-not-a-function-192738a6e79b
setLspDiagsHere(diags0 => { // setLspDiagsHere(diags0 => {
const diagPred = (d: Diagnostic) => // const diagPred = (d: Diagnostic) =>
RangeHelpers.contains(d.range, pos, config.allErrorsOnLine) // RangeHelpers.contains(d.range, pos, config.allErrorsOnLine)
const newDiags = (lspDiags.get(pos.uri) || []).filter(diagPred) // const newDiags = (lspDiags.get(pos.uri) || []).filter(diagPred)
if (newDiags.length === diags0.length && newDiags.every((d, i) => d === diags0[i])) return diags0 // if (newDiags.length === diags0.length && newDiags.every((d, i) => d === diags0[i])) return diags0
return newDiags // return newDiags
}) // })
}, [lspDiags, pos.uri, pos.line, pos.character, config.allErrorsOnLine]) // }, [lspDiags, pos.uri, pos.line, pos.character, config.allErrorsOnLine])
const serverIsProcessing = useIsProcessingAt(pos) // const serverIsProcessing = useIsProcessingAt(pos)
// This is a virtual dep of the info-requesting function. It is bumped whenever the Lean server // // This is a virtual dep of the info-requesting function. It is bumped whenever the Lean server
// indicates that another request should be made. Bumping it dirties the dep state of // // indicates that another request should be made. Bumping it dirties the dep state of
// `useAsyncWithTrigger` below, causing the `useEffect` lower down in this component to // // `useAsyncWithTrigger` below, causing the `useEffect` lower down in this component to
// make the request. We cannot simply call `triggerUpdateCore` because `useAsyncWithTrigger` // // make the request. We cannot simply call `triggerUpdateCore` because `useAsyncWithTrigger`
// does not support reentrancy like that. // // does not support reentrancy like that.
const [updaterTick, setUpdaterTick] = React.useState<number>(0) // const [updaterTick, setUpdaterTick] = React.useState<number>(0)
// For atomicity, we use a single update function that fetches all the info at `pos` at once. // // For atomicity, we use a single update function that fetches all the info at `pos` at once.
// Besides what the server replies with, we also include the inputs (deps) in this type so // // Besides what the server replies with, we also include the inputs (deps) in this type so
// that the displayed state cannot ever get out of sync by showing an old reply together // // that the displayed state cannot ever get out of sync by showing an old reply together
// 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 proofReq = rpcSess.call('Game.getProofState', DocumentPosition.toTdpp(pos)).catch((error) => { // const proofReq = rpcSess.call('Game.getProofState', DocumentPosition.toTdpp(pos)).catch((error) => {
console.warn(error) // console.warn(error)
}) // })
const goalsReq = rpcSess.call('Game.getInteractiveGoals', 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})
// fall back to non-interactive diagnostics when lake fails // // fall back to non-interactive diagnostics when lake fails
// (see https://github.com/leanprover/vscode-lean4/issues/90) // // (see https://github.com/leanprover/vscode-lean4/issues/90)
.then(diags => diags.length === 0 ? lspDiagsHere.map(lspDiagToInteractive) : diags) // .then(diags => diags.length === 0 ? lspDiagsHere.map(lspDiagToInteractive) : diags)
// While `lake print-paths` is running, the output of Lake is shown as // // While `lake print-paths` is running, the output of Lake is shown as
// info diagnostics on line 1. However, all RPC requests block until // // info diagnostics on line 1. However, all RPC requests block until
// Lake is finished, so we don't see these diagnostics while Lake is // // Lake is finished, so we don't see these diagnostics while Lake is
// building. Therefore we show the LSP diagnostics on line 1 if the // // building. Therefore we show the LSP diagnostics on line 1 if the
// server does not respond within half a second. // // server does not respond within half a second.
if (pos.line === 0 && lspDiagsHere.length) { // if (pos.line === 0 && lspDiagsHere.length) {
setTimeout(() => resolve({ // setTimeout(() => resolve({
pos, // pos,
status: 'updating', // status: 'updating',
messages: lspDiagsHere.map(lspDiagToInteractive), // messages: lspDiagsHere.map(lspDiagToInteractive),
proof: undefined, // proof: undefined,
goals: undefined, // goals: undefined,
termGoal: undefined, // termGoal: undefined,
error: undefined, // error: undefined,
userWidgets: [], // userWidgets: [],
rpcSess // rpcSess
}), 500) // }), 500)
} // }
// NB: it is important to await await reqs at once, otherwise // // NB: it is important to await await reqs at once, otherwise
// if both throw then one exception becomes unhandled. // // if both throw then one exception becomes unhandled.
Promise.all([proofReq, goalsReq, termGoalReq, widgetsReq, messagesReq]).then( // Promise.all([proofReq, goalsReq, termGoalReq, widgetsReq, messagesReq]).then(
([proof, goals, termGoal, userWidgets, messages]) => resolve({ // ([proof, goals, termGoal, userWidgets, messages]) => resolve({
pos, // pos,
status: 'ready', // status: 'ready',
messages, // messages,
proof : proof as any, // proof : proof as any,
goals: goals as any, // goals: goals as any,
termGoal, // termGoal,
error: undefined, // error: undefined,
userWidgets: userWidgets?.widgets ?? [], // userWidgets: userWidgets?.widgets ?? [],
rpcSess // rpcSess
}), // }),
ex => { // ex => {
if (ex?.code === RpcErrorCode.ContentModified || // if (ex?.code === RpcErrorCode.ContentModified ||
ex?.code === RpcErrorCode.RpcNeedsReconnect) { // ex?.code === RpcErrorCode.RpcNeedsReconnect) {
// Document has been changed since we made the request, or we need to reconnect // // Document has been changed since we made the request, or we need to reconnect
// to the RPC sessions. Try again. // // to the RPC sessions. Try again.
setUpdaterTick(t => t + 1) // setUpdaterTick(t => t + 1)
reject('retry') // reject('retry')
} // }
let errorString = '' // let errorString = ''
if (typeof ex === 'string') { // if (typeof ex === 'string') {
errorString = ex // errorString = ex
} else if (isRpcError(ex)) { // } else if (isRpcError(ex)) {
errorString = mapRpcError(ex).message // errorString = mapRpcError(ex).message
} else if (ex instanceof Error) { // } else if (ex instanceof Error) {
errorString = ex.toString() // errorString = ex.toString()
} else { // } else {
errorString = `Unrecognized error: ${JSON.stringify(ex)}` // errorString = `Unrecognized error: ${JSON.stringify(ex)}`
} // }
resolve({ // resolve({
pos, // pos,
status: 'error', // status: 'error',
messages: lspDiagsHere.map(lspDiagToInteractive), // messages: lspDiagsHere.map(lspDiagToInteractive),
proof: undefined, // proof: undefined,
goals: undefined, // goals: undefined,
termGoal: undefined, // termGoal: undefined,
error: `Error fetching goals: ${errorString}`, // error: `Error fetching goals: ${errorString}`,
userWidgets: [], // userWidgets: [],
rpcSess // rpcSess
}) // })
} // }
) // )
}), [updaterTick, pos.uri, pos.line, pos.character, rpcSess, serverIsProcessing, lspDiagsHere]) // }), [updaterTick, pos.uri, pos.line, pos.character, rpcSess, serverIsProcessing, lspDiagsHere])
// We use a timeout to debounce info requests. Whenever a request is already scheduled // // We use a timeout to debounce info requests. Whenever a request is already scheduled
// but something happens that warrants a request for newer info, we cancel the old request // // but something happens that warrants a request for newer info, we cancel the old request
// and schedule just the new one. // // and schedule just the new one.
const updaterTimeout = React.useRef<number>() // const updaterTimeout = React.useRef<number>()
const clearUpdaterTimeout = () => { // const clearUpdaterTimeout = () => {
if (updaterTimeout.current) { // if (updaterTimeout.current) {
window.clearTimeout(updaterTimeout.current) // window.clearTimeout(updaterTimeout.current)
updaterTimeout.current = undefined // updaterTimeout.current = undefined
} // }
} // }
const triggerUpdate = React.useCallback(() => new Promise<void>(resolve => { // const triggerUpdate = React.useCallback(() => new Promise<void>(resolve => {
clearUpdaterTimeout() // clearUpdaterTimeout()
const tm = window.setTimeout(() => { // const tm = window.setTimeout(() => {
void triggerUpdateCore().then(resolve) // void triggerUpdateCore().then(resolve)
updaterTimeout.current = undefined // updaterTimeout.current = undefined
}, config.debounceTime) // }, config.debounceTime)
// Hack: even if the request is cancelled, the promise should resolve so that no `await` // // Hack: even if the request is cancelled, the promise should resolve so that no `await`
// is left waiting forever. We ensure this happens in a simple way. // // is left waiting forever. We ensure this happens in a simple way.
window.setTimeout(resolve, config.debounceTime) // window.setTimeout(resolve, config.debounceTime)
updaterTimeout.current = tm // updaterTimeout.current = tm
}), [triggerUpdateCore, config.debounceTime]) // }), [triggerUpdateCore, config.debounceTime])
const [displayProps, setDisplayProps] = React.useState<InfoDisplayProps>({ // const [displayProps, setDisplayProps] = React.useState<InfoDisplayProps>({
pos, // pos,
status: 'updating', // status: 'updating',
messages: [], // messages: [],
proof: undefined, // proof: undefined,
goals: undefined, // goals: undefined,
termGoal: undefined, // termGoal: undefined,
error: undefined, // error: undefined,
userWidgets: [], // userWidgets: [],
rpcSess, // rpcSess,
triggerUpdate // triggerUpdate
}) // })
// Propagates changes in the state of async info requests to the display props, // // Propagates changes in the state of async info requests to the display props,
// and re-requests info if needed. // // and re-requests info if needed.
// This effect triggers new requests for info whenever need. It also propagates changes // // This effect triggers new requests for info whenever need. It also propagates changes
// in the state of the `useAsyncWithTrigger` to the displayed props. // // in the state of the `useAsyncWithTrigger` to the displayed props.
React.useEffect(() => { // React.useEffect(() => {
if (state.state === 'notStarted') // if (state.state === 'notStarted')
void triggerUpdate() // void triggerUpdate()
else if (state.state === 'loading') { // else if (state.state === 'loading') {
setDisplayProps(dp => ({ ...dp, status: 'updating' })) // setDisplayProps(dp => ({ ...dp, status: 'updating' }))
} // }
else if (state.state === 'resolved') { // else if (state.state === 'resolved') {
// if (state.value.goals?.goals?.length) { // // if (state.value.goals?.goals?.length) {
// hintContext.setHints(state.value.goals.goals[0].hints) // // hintContext.setHints(state.value.goals.goals[0].hints)
// } // // }
setDisplayProps({ ...state.value, triggerUpdate }) // setDisplayProps({ ...state.value, triggerUpdate })
// Update the game's proof state // // Update the game's proof state
console.info('updating proof from editor mode.') // console.info('updating proof from editor mode.')
setProof(state.value.proof) // setProof(state.value.proof)
} else if (state.state === 'rejected' && state.error !== 'retry') { // } else if (state.state === 'rejected' && state.error !== 'retry') {
// The code inside `useAsyncWithTrigger` may only ever reject with a `retry` exception. // // The code inside `useAsyncWithTrigger` may only ever reject with a `retry` exception.
console.warn('Unreachable code reached with error: ', state.error) // console.warn('Unreachable code reached with error: ', state.error)
} // }
}, [state]) // }, [state])
return <InfoDisplay kind={props.kind} onPin={props.onPin} {...displayProps} /> // return <InfoDisplay kind={props.kind} onPin={props.onPin} {...displayProps} />
} // }

@ -1,133 +1,133 @@
/* Mostly copied from https://github.com/leanprover/vscode-lean4/blob/master/lean4-infoview/src/infoview/infos.tsx */ // /* Mostly copied from https://github.com/leanprover/vscode-lean4/blob/master/lean4-infoview/src/infoview/infos.tsx */
import * as React from 'react'; // import * as React from 'react';
import { DidChangeTextDocumentParams, DidCloseTextDocumentParams, TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; // import { DidChangeTextDocumentParams, DidCloseTextDocumentParams, TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol';
import { EditorContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts'; // import { EditorContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts';
import { DocumentPosition, Keyed, PositionHelpers, useClientNotificationEffect, useClientNotificationState, useEvent, useEventResult } from '../../../../node_modules/lean4-infoview/src/infoview/util'; // import { DocumentPosition, Keyed, PositionHelpers, useClientNotificationEffect, useClientNotificationState, useEvent, useEventResult } from '../../../../node_modules/lean4-infoview/src/infoview/util';
import { Info, InfoProps } from './info'; // import { Info, InfoProps } from './info';
import { useTranslation } from 'react-i18next'; // import { useTranslation } from 'react-i18next';
/** Manages and displays pinned infos, as well as info for the current location. */ // /** Manages and displays pinned infos, as well as info for the current location. */
export function Infos() { // export function Infos() {
let { t } = useTranslation() // let { t } = useTranslation()
const ec = React.useContext(EditorContext); // const ec = React.useContext(EditorContext);
// Update pins when the document changes. In particular, when edits are made // // Update pins when the document changes. In particular, when edits are made
// earlier in the text such that a pin has to move up or down. // // earlier in the text such that a pin has to move up or down.
const [pinnedPositions, setPinnedPositions] = useClientNotificationState( // const [pinnedPositions, setPinnedPositions] = useClientNotificationState(
'textDocument/didChange', // 'textDocument/didChange',
new Array<Keyed<DocumentPosition>>(), // new Array<Keyed<DocumentPosition>>(),
(pinnedPositions, params: DidChangeTextDocumentParams) => { // (pinnedPositions, params: DidChangeTextDocumentParams) => {
if (pinnedPositions.length === 0) return pinnedPositions; // if (pinnedPositions.length === 0) return pinnedPositions;
let changed: boolean = false; // let changed: boolean = false;
const newPins = pinnedPositions.map(pin => { // const newPins = pinnedPositions.map(pin => {
if (pin.uri !== params.textDocument.uri) return pin; // if (pin.uri !== params.textDocument.uri) return pin;
// NOTE(WN): It's important to make a clone here, otherwise this // // NOTE(WN): It's important to make a clone here, otherwise this
// actually mutates the pin. React state updates must be pure. // // actually mutates the pin. React state updates must be pure.
// See https://github.com/facebook/react/issues/12856 // // See https://github.com/facebook/react/issues/12856
const newPin: Keyed<DocumentPosition> = { ...pin }; // const newPin: Keyed<DocumentPosition> = { ...pin };
for (const chg of params.contentChanges) { // for (const chg of params.contentChanges) {
if (!TextDocumentContentChangeEvent.isIncremental(chg)) { // if (!TextDocumentContentChangeEvent.isIncremental(chg)) {
changed = true; // changed = true;
return null; // return null;
} // }
if (PositionHelpers.isLessThanOrEqual(newPin, chg.range.start)) continue; // if (PositionHelpers.isLessThanOrEqual(newPin, chg.range.start)) continue;
// We can assume chg.range.start < pin // // We can assume chg.range.start < pin
// If the pinned position is replaced with new text, just delete the pin. // // If the pinned position is replaced with new text, just delete the pin.
if (PositionHelpers.isLessThanOrEqual(newPin, chg.range.end)) { // if (PositionHelpers.isLessThanOrEqual(newPin, chg.range.end)) {
changed = true; // changed = true;
return null; // return null;
} // }
const oldPin = { ...newPin }; // const oldPin = { ...newPin };
// How many lines before the pin position were added by the change. // // How many lines before the pin position were added by the change.
// Can be negative when more lines are removed than added. // // Can be negative when more lines are removed than added.
let additionalLines = 0; // let additionalLines = 0;
let lastLineLen = chg.range.start.character; // let lastLineLen = chg.range.start.character;
for (const c of chg.text) // for (const c of chg.text)
if (c === '\n') { // if (c === '\n') {
additionalLines++; // additionalLines++;
lastLineLen = 0; // lastLineLen = 0;
} else lastLineLen++; // } else lastLineLen++;
// Subtract lines that were already present // // Subtract lines that were already present
additionalLines -= (chg.range.end.line - chg.range.start.line) // additionalLines -= (chg.range.end.line - chg.range.start.line)
newPin.line += additionalLines; // newPin.line += additionalLines;
if (oldPin.line < chg.range.end.line) { // if (oldPin.line < chg.range.end.line) {
// Should never execute by the <= check above. // // Should never execute by the <= check above.
throw new Error('unreachable code reached') // throw new Error('unreachable code reached')
} else if (oldPin.line === chg.range.end.line) { // } else if (oldPin.line === chg.range.end.line) {
newPin.character = lastLineLen + (oldPin.character - chg.range.end.character); // newPin.character = lastLineLen + (oldPin.character - chg.range.end.character);
} // }
} // }
if (!DocumentPosition.isEqual(newPin, pin)) changed = true; // if (!DocumentPosition.isEqual(newPin, pin)) changed = true;
// NOTE(WN): We maintain the `key` when a pin is moved around to maintain // // NOTE(WN): We maintain the `key` when a pin is moved around to maintain
// its component identity and minimise flickering. // // its component identity and minimise flickering.
return newPin; // return newPin;
}); // });
if (changed) return newPins.filter(p => p !== null) as Keyed<DocumentPosition>[]; // if (changed) return newPins.filter(p => p !== null) as Keyed<DocumentPosition>[];
return pinnedPositions; // return pinnedPositions;
}, // },
[] // []
); // );
// Remove pins for closed documents // // Remove pins for closed documents
useClientNotificationEffect( // useClientNotificationEffect(
'textDocument/didClose', // 'textDocument/didClose',
(params: DidCloseTextDocumentParams) => { // (params: DidCloseTextDocumentParams) => {
setPinnedPositions(pinnedPositions => pinnedPositions.filter(p => p.uri !== params.textDocument.uri)); // setPinnedPositions(pinnedPositions => pinnedPositions.filter(p => p.uri !== params.textDocument.uri));
}, // },
[] // []
); // );
const curPos: DocumentPosition | undefined = // const curPos: DocumentPosition | undefined =
useEventResult(ec.events.changedCursorLocation, loc => loc ? { uri: loc.uri, ...loc.range.start } : undefined) // useEventResult(ec.events.changedCursorLocation, loc => loc ? { uri: loc.uri, ...loc.range.start } : undefined)
// Update pins on UI actions // // Update pins on UI actions
const pinKey = React.useRef<number>(0); // const pinKey = React.useRef<number>(0);
const isPinned = (pinnedPositions: DocumentPosition[], pos: DocumentPosition) => { // const isPinned = (pinnedPositions: DocumentPosition[], pos: DocumentPosition) => {
return pinnedPositions.some(p => DocumentPosition.isEqual(p, pos)); // return pinnedPositions.some(p => DocumentPosition.isEqual(p, pos));
} // }
const pin = React.useCallback((pos: DocumentPosition) => { // const pin = React.useCallback((pos: DocumentPosition) => {
setPinnedPositions(pinnedPositions => { // setPinnedPositions(pinnedPositions => {
if (isPinned(pinnedPositions, pos)) return pinnedPositions; // if (isPinned(pinnedPositions, pos)) return pinnedPositions;
pinKey.current += 1; // pinKey.current += 1;
return [ ...pinnedPositions, { ...pos, key: pinKey.current.toString() } ]; // return [ ...pinnedPositions, { ...pos, key: pinKey.current.toString() } ];
}); // });
}, []); // }, []);
const unpin = React.useCallback((pos: DocumentPosition) => { // const unpin = React.useCallback((pos: DocumentPosition) => {
setPinnedPositions(pinnedPositions => { // setPinnedPositions(pinnedPositions => {
if (!isPinned(pinnedPositions, pos)) return pinnedPositions; // if (!isPinned(pinnedPositions, pos)) return pinnedPositions;
return pinnedPositions.filter(p => !DocumentPosition.isEqual(p, pos)); // return pinnedPositions.filter(p => !DocumentPosition.isEqual(p, pos));
}); // });
}, []); // }, []);
// Toggle pin at current position when the editor requests it // // Toggle pin at current position when the editor requests it
useEvent(ec.events.requestedAction, act => { // useEvent(ec.events.requestedAction, act => {
if (act.kind !== 'togglePin') return // if (act.kind !== 'togglePin') return
if (!curPos) return // if (!curPos) return
setPinnedPositions(pinnedPositions => { // setPinnedPositions(pinnedPositions => {
if (isPinned(pinnedPositions, curPos)) { // if (isPinned(pinnedPositions, curPos)) {
return pinnedPositions.filter(p => !DocumentPosition.isEqual(p, curPos)); // return pinnedPositions.filter(p => !DocumentPosition.isEqual(p, curPos));
} else { // } else {
pinKey.current += 1; // pinKey.current += 1;
return [ ...pinnedPositions, { ...curPos, key: pinKey.current.toString() } ]; // return [ ...pinnedPositions, { ...curPos, key: pinKey.current.toString() } ];
} // }
}); // });
}, [curPos?.uri, curPos?.line, curPos?.character]); // }, [curPos?.uri, curPos?.line, curPos?.character]);
const infoProps: Keyed<InfoProps>[] = pinnedPositions.map(pos => ({ kind: 'pin', onPin: unpin, pos, key: pos.key })); // const infoProps: Keyed<InfoProps>[] = pinnedPositions.map(pos => ({ kind: 'pin', onPin: unpin, pos, key: pos.key }));
if (curPos) infoProps.push({ kind: 'cursor', onPin: pin, key: 'cursor' }); // if (curPos) infoProps.push({ kind: 'cursor', onPin: pin, key: 'cursor' });
return <div> // return <div>
{infoProps.map (ps => <Info {...ps} />)} // {infoProps.map (ps => <Info {...ps} />)}
{!curPos && <p>{t("Click somewhere in the Lean file to enable the infoview.")}</p> } // {!curPos && <p>{t("Click somewhere in the Lean file to enable the infoview.")}</p> }
</div>; // </div>;
} // }

File diff suppressed because it is too large Load Diff

@ -1,240 +1,240 @@
import * as React from 'react' // import * as React from 'react'
import fastIsEqual from 'react-fast-compare' // import fastIsEqual from 'react-fast-compare'
import { Location, DocumentUri, Diagnostic, DiagnosticSeverity, PublishDiagnosticsParams } from 'vscode-languageserver-protocol' // import { Location, DocumentUri, Diagnostic, DiagnosticSeverity, PublishDiagnosticsParams } from 'vscode-languageserver-protocol'
import { LeanDiagnostic, RpcErrorCode, getInteractiveDiagnostics, InteractiveDiagnostic, TaggedText_stripTags } from '@leanprover/infoview-api' // import { LeanDiagnostic, RpcErrorCode, getInteractiveDiagnostics, InteractiveDiagnostic, TaggedText_stripTags } from '@leanprover/infoview-api'
import { basename, escapeHtml, usePausableState, useEvent, addUniqueKeys, DocumentPosition, useServerNotificationState, useEventResult } from '../../../../node_modules/lean4-infoview/src/infoview/util' // import { basename, escapeHtml, usePausableState, useEvent, addUniqueKeys, DocumentPosition, useServerNotificationState, useEventResult } from '../../../../node_modules/lean4-infoview/src/infoview/util'
import { ConfigContext, EditorContext, LspDiagnosticsContext, VersionContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts' // import { ConfigContext, EditorContext, LspDiagnosticsContext, VersionContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts'
import { Details } from '../../../../node_modules/lean4-infoview/src/infoview/collapsing' // import { Details } from '../../../../node_modules/lean4-infoview/src/infoview/collapsing'
import { InteractiveMessage } from '../../../../node_modules/lean4-infoview/src/infoview/traceExplorer' // import { InteractiveMessage } from '../../../../node_modules/lean4-infoview/src/infoview/traceExplorer'
import { RpcContext, useRpcSessionAtPos } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions' // import { RpcContext, useRpcSessionAtPos } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions'
import { PageContext } from '../../state/context' // import { PageContext } from '../../state/context'
import { useTranslation } from 'react-i18next' // import { useTranslation } from 'react-i18next'
interface MessageViewProps { // interface MessageViewProps {
uri: DocumentUri; // uri: DocumentUri;
diag: InteractiveDiagnostic; // diag: InteractiveDiagnostic;
} // }
/** A list of messages (info/warning/error) that are produced after this command */ // /** A list of messages (info/warning/error) that are produced after this command */
function Error({error, typewriterMode} : {error : InteractiveDiagnostic, typewriterMode : boolean}) { // function Error({error, typewriterMode} : {error : InteractiveDiagnostic, typewriterMode : boolean}) {
// The first step will always have an empty command // // The first step will always have an empty command
const severityClass = error.severity ? { // const severityClass = error.severity ? {
[DiagnosticSeverity.Error]: 'error', // [DiagnosticSeverity.Error]: 'error',
[DiagnosticSeverity.Warning]: 'warning', // [DiagnosticSeverity.Warning]: 'warning',
[DiagnosticSeverity.Information]: 'information', // [DiagnosticSeverity.Information]: 'information',
[DiagnosticSeverity.Hint]: 'hint', // [DiagnosticSeverity.Hint]: 'hint',
}[error.severity] : ''; // }[error.severity] : '';
const {line, character} = error.range.start; // const {line, character} = error.range.start;
const title = `Line ${line+1}, Character ${character}`; // const title = `Line ${line+1}, Character ${character}`;
// Hide "unsolved goals" messages // // Hide "unsolved goals" messages
let message; // let message;
if ("append" in error.message && "text" in error.message.append[0] && // if ("append" in error.message && "text" in error.message.append[0] &&
error.message?.append[0].text === "unsolved goals") { // error.message?.append[0].text === "unsolved goals") {
message = error.message.append[0] // message = error.message.append[0]
} else { // } else {
message = error.message // message = error.message
} // }
return <div className={severityClass + ' ml1 message'}> // return <div className={severityClass + ' ml1 message'}>
{!typewriterMode && <p className="mv2">{title}</p>} // {!typewriterMode && <p className="mv2">{title}</p>}
<pre className="font-code pre-wrap"> // <pre className="font-code pre-wrap">
<InteractiveMessage fmt={message} /> // <InteractiveMessage fmt={message} />
</pre> // </pre>
</div> // </div>
} // }
// TODO: Should not use index as key. // // TODO: Should not use index as key.
/** A list of messages (info/warning/error) that are produced after this command */ // /** A list of messages (info/warning/error) that are produced after this command */
export function Errors ({errors, typewriterMode} : {errors : InteractiveDiagnostic[], typewriterMode : boolean}) { // export function Errors ({errors, typewriterMode} : {errors : InteractiveDiagnostic[], typewriterMode : boolean}) {
return <div> // return <div>
{errors.map((err, i) => (<Error key={`error-${i}`} error={err} typewriterMode={typewriterMode}/>))} // {errors.map((err, i) => (<Error key={`error-${i}`} error={err} typewriterMode={typewriterMode}/>))}
</div> // </div>
} // }
const MessageView = React.memo(({uri, diag}: MessageViewProps) => { // const MessageView = React.memo(({uri, diag}: MessageViewProps) => {
const ec = React.useContext(EditorContext); // const ec = React.useContext(EditorContext);
const fname = escapeHtml(basename(uri)); // const fname = escapeHtml(basename(uri));
const {line, character} = diag.range.start; // const {line, character} = diag.range.start;
const loc: Location = { uri, range: diag.range }; // const loc: Location = { uri, range: diag.range };
const text = TaggedText_stripTags(diag.message); // const text = TaggedText_stripTags(diag.message);
const severityClass = diag.severity ? { // const severityClass = diag.severity ? {
[DiagnosticSeverity.Error]: 'error', // [DiagnosticSeverity.Error]: 'error',
[DiagnosticSeverity.Warning]: 'warning', // [DiagnosticSeverity.Warning]: 'warning',
[DiagnosticSeverity.Information]: 'information', // [DiagnosticSeverity.Information]: 'information',
[DiagnosticSeverity.Hint]: 'hint', // [DiagnosticSeverity.Hint]: 'hint',
}[diag.severity] : ''; // }[diag.severity] : '';
const title = `Line ${line+1}, Character ${character}`; // const title = `Line ${line+1}, Character ${character}`;
// Hide "unsolved goals" messages // // Hide "unsolved goals" messages
let message; // let message;
if ("append" in diag.message && "text" in diag.message.append[0] && // if ("append" in diag.message && "text" in diag.message.append[0] &&
diag.message?.append[0].text === "unsolved goals") { // diag.message?.append[0].text === "unsolved goals") {
message = diag.message.append[0] // message = diag.message.append[0]
} else { // } else {
message = diag.message // message = diag.message
} // }
const { typewriterMode, lockEditorMode } = React.useContext(PageContext) // const { typewriterMode, lockEditorMode } = React.useContext(PageContext)
return ( // return (
// <details open> // // <details open>
// <summary className={severityClass + ' mv2 pointer'}>{title} // // <summary className={severityClass + ' mv2 pointer'}>{title}
// <span className="fr"> // // <span className="fr">
// <a className="link pointer mh2 dim codicon codicon-go-to-file" // // <a className="link pointer mh2 dim codicon codicon-go-to-file"
// onClick={e => { e.preventDefault(); void ec.revealLocation(loc); }} // // onClick={e => { e.preventDefault(); void ec.revealLocation(loc); }}
// title="reveal file location"></a> // // title="reveal file location"></a>
// <a className="link pointer mh2 dim codicon codicon-quote" // // <a className="link pointer mh2 dim codicon codicon-quote"
// data-id="copy-to-comment" // // data-id="copy-to-comment"
// onClick={e => {e.preventDefault(); void ec.copyToComment(text)}} // // onClick={e => {e.preventDefault(); void ec.copyToComment(text)}}
// title="copy message to comment"></a> // // title="copy message to comment"></a>
// <a className="link pointer mh2 dim codicon codicon-clippy" // // <a className="link pointer mh2 dim codicon codicon-clippy"
// onClick={e => {e.preventDefault(); void ec.api.copyToClipboard(text)}} // // onClick={e => {e.preventDefault(); void ec.api.copyToClipboard(text)}}
// title="copy message to clipboard"></a> // // title="copy message to clipboard"></a>
// </span> // // </span>
// </summary> // // </summary>
<div className={severityClass + ' ml1 message'}> // <div className={severityClass + ' ml1 message'}>
{!(typewriterMode && !lockEditorMode) && <p className="mv2">{title}</p>} // {!(typewriterMode && !lockEditorMode) && <p className="mv2">{title}</p>}
<pre className="font-code pre-wrap"> // <pre className="font-code pre-wrap">
<InteractiveMessage fmt={message} /> // <InteractiveMessage fmt={message} />
</pre> // </pre>
</div> // </div>
// </details> // // </details>
) // )
}, fastIsEqual) // }, fastIsEqual)
function mkMessageViewProps(uri: DocumentUri, messages: InteractiveDiagnostic[]): MessageViewProps[] { // function mkMessageViewProps(uri: DocumentUri, messages: InteractiveDiagnostic[]): MessageViewProps[] {
const views: MessageViewProps[] = messages // const views: MessageViewProps[] = messages
.sort((msga, msgb) => { // .sort((msga, msgb) => {
const a = msga.fullRange?.end || msga.range.end; // const a = msga.fullRange?.end || msga.range.end;
const b = msgb.fullRange?.end || msgb.range.end; // const b = msgb.fullRange?.end || msgb.range.end;
return a.line === b.line ? a.character - b.character : a.line - b.line // return a.line === b.line ? a.character - b.character : a.line - b.line
}).map(m => { // }).map(m => {
return { uri, diag: m }; // return { uri, diag: m };
}); // });
return addUniqueKeys(views, v => DocumentPosition.toString({uri: v.uri, ...v.diag.range.start})); // return addUniqueKeys(views, v => DocumentPosition.toString({uri: v.uri, ...v.diag.range.start}));
} // }
/** Shows the given messages assuming they are for the given file. */ // /** Shows the given messages assuming they are for the given file. */
export const MessagesList = React.memo(({uri, messages}: {uri: DocumentUri, messages: InteractiveDiagnostic[]}) => { // export const MessagesList = React.memo(({uri, messages}: {uri: DocumentUri, messages: InteractiveDiagnostic[]}) => {
const should_hide = messages.length === 0; // const should_hide = messages.length === 0;
if (should_hide) { return <></> } // if (should_hide) { return <></> }
return ( // return (
<div> // <div>
{mkMessageViewProps(uri, messages).map(m => <MessageView {...m} />)} // {mkMessageViewProps(uri, messages).map(m => <MessageView {...m} />)}
</div> // </div>
); // );
}) // })
function lazy<T>(f: () => T): () => T { // function lazy<T>(f: () => T): () => T {
let state: {t: T} | undefined // let state: {t: T} | undefined
return () => { // return () => {
if (!state) state = {t: f()} // if (!state) state = {t: f()}
return state.t // return state.t
} // }
} // }
/** Displays all messages for the specified file. Can be paused. */ // /** Displays all messages for the specified file. Can be paused. */
export function AllMessages() { // export function AllMessages() {
const ec = React.useContext(EditorContext); // const ec = React.useContext(EditorContext);
const sv = React.useContext(VersionContext); // const sv = React.useContext(VersionContext);
const curPos: DocumentPosition | undefined = // const curPos: DocumentPosition | undefined =
useEventResult(ec.events.changedCursorLocation, loc => loc ? { uri: loc.uri, ...loc.range.start } : undefined) // useEventResult(ec.events.changedCursorLocation, loc => loc ? { uri: loc.uri, ...loc.range.start } : undefined)
const rs0 = useRpcSessionAtPos({ uri: curPos.uri, line: 0, character: 0 }); // const rs0 = useRpcSessionAtPos({ uri: curPos.uri, line: 0, character: 0 });
const dc = React.useContext(LspDiagnosticsContext); // const dc = React.useContext(LspDiagnosticsContext);
const config = React.useContext(ConfigContext); // const config = React.useContext(ConfigContext);
const diags0 = dc.get(curPos.uri) || []; // const diags0 = dc.get(curPos.uri) || [];
const iDiags0 = React.useMemo(() => lazy(async () => { // const iDiags0 = React.useMemo(() => lazy(async () => {
try { // try {
const diags = await getInteractiveDiagnostics(rs0); // const diags = await getInteractiveDiagnostics(rs0);
if (diags.length > 0) { // if (diags.length > 0) {
return diags // return diags
} // }
} catch (err: any) { // } catch (err: any) {
if (err?.code === RpcErrorCode.ContentModified) { // if (err?.code === RpcErrorCode.ContentModified) {
// Document has been changed since we made the request. This can happen // // Document has been changed since we made the request. This can happen
// while typing quickly. When the server catches up on next edit, it will // // while typing quickly. When the server catches up on next edit, it will
// send new diagnostics to which the infoview responds by calling // // send new diagnostics to which the infoview responds by calling
// `getInteractiveDiagnostics` again. // // `getInteractiveDiagnostics` again.
} else { // } else {
console.log('getInteractiveDiagnostics error ', err) // console.log('getInteractiveDiagnostics error ', err)
} // }
} // }
return diags0.map(d => ({ ...(d as LeanDiagnostic), message: { text: d.message } })); // return diags0.map(d => ({ ...(d as LeanDiagnostic), message: { text: d.message } }));
}), [sv, rs0, curPos.uri, diags0]); // }), [sv, rs0, curPos.uri, diags0]);
const [{ isPaused, setPaused }, [uri, rs, diags, iDiags], _] = usePausableState(false, [curPos.uri, rs0, diags0, iDiags0]); // const [{ isPaused, setPaused }, [uri, rs, diags, iDiags], _] = usePausableState(false, [curPos.uri, rs0, diags0, iDiags0]);
// Fetch interactive diagnostics when we're entering the paused state // // Fetch interactive diagnostics when we're entering the paused state
// (if they haven't already been fetched before) // // (if they haven't already been fetched before)
React.useEffect(() => { if (isPaused) { void iDiags() } }, [iDiags, isPaused]); // React.useEffect(() => { if (isPaused) { void iDiags() } }, [iDiags, isPaused]);
const setOpenRef = React.useRef<React.Dispatch<React.SetStateAction<boolean>>>(); // const setOpenRef = React.useRef<React.Dispatch<React.SetStateAction<boolean>>>();
useEvent(ec.events.requestedAction, act => { // useEvent(ec.events.requestedAction, act => {
if (act.kind === 'toggleAllMessages' && setOpenRef.current !== undefined) { // if (act.kind === 'toggleAllMessages' && setOpenRef.current !== undefined) {
setOpenRef.current(t => !t); // setOpenRef.current(t => !t);
} // }
}); // });
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">
<a className={'link pointer mh2 dim codicon ' + (isPaused ? 'codicon-debug-continue' : 'codicon-debug-pause')} // <a className={'link pointer mh2 dim codicon ' + (isPaused ? 'codicon-debug-continue' : 'codicon-debug-pause')}
onClick={e => { e.preventDefault(); setPaused(p => !p); }} // onClick={e => { e.preventDefault(); setPaused(p => !p); }}
title={isPaused ? 'continue updating' : 'pause updating'}> // title={isPaused ? 'continue updating' : 'pause updating'}>
</a> // </a>
</span> // </span>
</summary> */} // </summary> */}
<AllMessagesBody uri={curPos.uri} key={curPos.uri} messages={iDiags0} curPos={curPos} /> // <AllMessagesBody uri={curPos.uri} key={curPos.uri} messages={iDiags0} curPos={curPos} />
{/* </Details> */} // {/* </Details> */}
</RpcContext.Provider> // </RpcContext.Provider>
) // )
} // }
/** We factor out the body of {@link AllMessages} which lazily fetches its contents only when expanded. */ // /** We factor out the body of {@link AllMessages} which lazily fetches its contents only when expanded. */
function AllMessagesBody({uri, curPos, messages}: {uri: DocumentUri, curPos: DocumentPosition | undefined , messages: () => Promise<InteractiveDiagnostic[]>}) { // function AllMessagesBody({uri, curPos, messages}: {uri: DocumentUri, curPos: DocumentPosition | undefined , messages: () => Promise<InteractiveDiagnostic[]>}) {
let { t } = useTranslation() // let { t } = useTranslation()
const [msgs, setMsgs] = React.useState<InteractiveDiagnostic[] | undefined>(undefined) // const [msgs, setMsgs] = React.useState<InteractiveDiagnostic[] | undefined>(undefined)
React.useEffect(() => { void messages().then( // React.useEffect(() => { void messages().then(
msgs => setMsgs(msgs.filter( // msgs => setMsgs(msgs.filter(
(d)=>{ // (d)=>{
//console.log(`message start: ${d.range.start.line}. CurPos: ${curPos.line}`) // //console.log(`message start: ${d.range.start.line}. CurPos: ${curPos.line}`)
// Only show the messages from the line where the cursor is. // // Only show the messages from the line where the cursor is.
return d.range.start.line == curPos.line // return d.range.start.line == curPos.line
})) // }))
) }, [messages, curPos]) // ) }, [messages, curPos])
if (msgs === undefined) return <div>{t("Loading messages…")}</div> // if (msgs === undefined) return <div>{t("Loading messages…")}</div>
else return <MessagesList uri={uri} messages={msgs}/> // else return <MessagesList uri={uri} messages={msgs}/>
} // }
/** // /**
* Provides a `LspDiagnosticsContext` which stores the latest version of the // * Provides a `LspDiagnosticsContext` which stores the latest version of the
* diagnostics as sent by the publishDiagnostics notification. // * diagnostics as sent by the publishDiagnostics notification.
*/ // */
export function WithLspDiagnosticsContext({children}: React.PropsWithChildren<{}>) { // export function WithLspDiagnosticsContext({children}: React.PropsWithChildren<{}>) {
const [allDiags, _0] = useServerNotificationState( // const [allDiags, _0] = useServerNotificationState(
'textDocument/publishDiagnostics', // 'textDocument/publishDiagnostics',
new Map<DocumentUri, Diagnostic[]>(), // new Map<DocumentUri, Diagnostic[]>(),
async (params: PublishDiagnosticsParams) => diags => // async (params: PublishDiagnosticsParams) => diags =>
new Map(diags).set(params.uri, params.diagnostics), // new Map(diags).set(params.uri, params.diagnostics),
[] // []
) // )
return <LspDiagnosticsContext.Provider value={allDiags}>{children}</LspDiagnosticsContext.Provider> // return <LspDiagnosticsContext.Provider value={allDiags}>{children}</LspDiagnosticsContext.Provider>
} // }
/** Embeds a non-interactive diagnostic into the type `InteractiveDiagnostic`. */ // /** Embeds a non-interactive diagnostic into the type `InteractiveDiagnostic`. */
export function lspDiagToInteractive(diag: Diagnostic): InteractiveDiagnostic { // export function lspDiagToInteractive(diag: Diagnostic): InteractiveDiagnostic {
return { ...(diag as LeanDiagnostic), message: { text: diag.message } }; // return { ...(diag as LeanDiagnostic), message: { text: diag.message } };
} // }

@ -4,8 +4,8 @@
* This file is based on `vscode-lean4/vscode-lean4/src/rpcApi.ts` * This file is based on `vscode-lean4/vscode-lean4/src/rpcApi.ts`
*/ */
import type { Range } from 'vscode-languageserver-protocol'; import type { Range } from 'vscode-languageserver-protocol';
import type { ContextInfo, FVarId, CodeWithInfos, MVarId } from '@leanprover/infoview-api'; // import type { ContextInfo, FVarId, CodeWithInfos, MVarId } from '@leanprover/infoview-api';
import { InteractiveDiagnostic, TermInfo } from '@leanprover/infoview/*'; // import { InteractiveDiagnostic, TermInfo } from '@leanprover/infoview/*';
import type { Diagnostic } from 'vscode-languageserver-protocol'; import type { Diagnostic } from 'vscode-languageserver-protocol';
export interface InteractiveHypothesisBundle { export interface InteractiveHypothesisBundle {
@ -13,9 +13,9 @@ export interface InteractiveHypothesisBundle {
* as `"[anonymous]"` whereas inaccessible ones have a `` appended at the end. * as `"[anonymous]"` whereas inaccessible ones have a `` appended at the end.
* Use `InteractiveHypothesisBundle_nonAnonymousNames` to filter anonymouse ones out. */ * Use `InteractiveHypothesisBundle_nonAnonymousNames` to filter anonymouse ones out. */
names: string[]; names: string[];
fvarIds?: FVarId[]; fvarIds?: any // FVarId[];
type: CodeWithInfos; type: any // CodeWithInfos;
val?: CodeWithInfos; val?: any // CodeWithInfos;
isInstance?: boolean; isInstance?: boolean;
isType?: boolean; isType?: boolean;
isInserted?: boolean; isInserted?: boolean;
@ -25,14 +25,14 @@ export interface InteractiveHypothesisBundle {
export interface InteractiveGoalCore { export interface InteractiveGoalCore {
hyps: InteractiveHypothesisBundle[]; hyps: InteractiveHypothesisBundle[];
type: CodeWithInfos; type: any // CodeWithInfos;
ctx?: ContextInfo; ctx?: any // ContextInfo;
} }
export interface InteractiveGoal extends InteractiveGoalCore { export interface InteractiveGoal extends InteractiveGoalCore {
userName?: string; userName?: string;
goalPrefix?: string; goalPrefix?: string;
mvarId?: MVarId; mvarId?: any // MVarId;
isInserted?: boolean; isInserted?: boolean;
isRemoved?: boolean; isRemoved?: boolean;
} }
@ -43,7 +43,7 @@ export interface InteractiveGoals extends InteractiveGoalCore {
export interface InteractiveTermGoal extends InteractiveGoalCore { export interface InteractiveTermGoal extends InteractiveGoalCore {
range?: Range; range?: Range;
term?: TermInfo; term?: any //TermInfo;
} }
export interface GameHint { export interface GameHint {
@ -61,7 +61,7 @@ export interface InteractiveGoalWithHints {
export interface InteractiveGoalsWithHints { export interface InteractiveGoalsWithHints {
goals: InteractiveGoalWithHints[]; goals: InteractiveGoalWithHints[];
command: string; command: string;
diags: InteractiveDiagnostic[]; diags: any //InteractiveDiagnostic[];
} }
/** /**
@ -78,7 +78,7 @@ export interface ProofState {
/** The remaining diagnostics that are not in the steps. Usually this should only /** The remaining diagnostics that are not in the steps. Usually this should only
* be the "unsolved goals" message, I believe. * be the "unsolved goals" message, I believe.
*/ */
diagnostics : InteractiveDiagnostic[]; diagnostics : any // InteractiveDiagnostic[];
completed : Boolean; completed : Boolean;
completedWithWarnings : Boolean; completedWithWarnings : Boolean;
} }

@ -1,304 +1,304 @@
import * as React from 'react' // import * as React from 'react'
import { useRef, useState, useEffect } from 'react' // import { useRef, useState, useEffect } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' // import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faWandMagicSparkles } from '@fortawesome/free-solid-svg-icons' // import { faWandMagicSparkles } from '@fortawesome/free-solid-svg-icons'
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js' // import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
import { Registry } from 'monaco-textmate' // peer dependency // // import { Registry } from 'monaco-textmate' // peer dependency
import { wireTmGrammars } from 'monaco-editor-textmate' // // import { wireTmGrammars } from 'monaco-editor-textmate'
import { DiagnosticSeverity, PublishDiagnosticsParams, DocumentUri } from 'vscode-languageserver-protocol'; // import { DiagnosticSeverity, PublishDiagnosticsParams, DocumentUri } from 'vscode-languageserver-protocol';
import { useServerNotificationEffect } from '../../../../node_modules/lean4-infoview/src/infoview/util'; // import { useServerNotificationEffect } from '../../../../node_modules/lean4-infoview/src/infoview/util';
import { AbbreviationRewriter } from 'lean4web/client/src/editor/abbreviation/rewriter/AbbreviationRewriter'; // // import { AbbreviationRewriter } from 'lean4web/client/src/editor/abbreviation/rewriter/AbbreviationRewriter';
import { AbbreviationProvider } from 'lean4web/client/src/editor/abbreviation/AbbreviationProvider'; // // import { AbbreviationProvider } from 'lean4web/client/src/editor/abbreviation/AbbreviationProvider';
import * as leanSyntax from 'lean4web/client/src/syntaxes/lean.json' // // import * as leanSyntax from 'lean4web/client/src/syntaxes/lean.json'
import * as leanMarkdownSyntax from 'lean4web/client/src/syntaxes/lean-markdown.json' // // import * as leanMarkdownSyntax from 'lean4web/client/src/syntaxes/lean-markdown.json'
import * as codeblockSyntax from 'lean4web/client/src/syntaxes/codeblock.json' // // import * as codeblockSyntax from 'lean4web/client/src/syntaxes/codeblock.json'
import languageConfig from 'lean4/language-configuration.json'; // // import languageConfig from 'lean4/language-configuration.json';
import { InteractiveDiagnostic, RpcSessionAtPos, getInteractiveDiagnostics } from '@leanprover/infoview-api'; // import { InteractiveDiagnostic, RpcSessionAtPos, getInteractiveDiagnostics } from '@leanprover/infoview-api';
import { Diagnostic } from 'vscode-languageserver-types'; // import { Diagnostic } from 'vscode-languageserver-types';
import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util'; // import { DocumentPosition } from '../../../../node_modules/lean4-infoview/src/infoview/util';
import { RpcContext } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions'; // import { RpcContext } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions';
import { ChatContext, PageContext, MonacoEditorContext, ProofContext, GameIdContext } from '../../state/context' // import { ChatContext, PageContext, MonacoEditorContext, ProofContext, GameIdContext } from '../../state/context'
import { goalsToString, lastStepHasErrors, loadGoals } from './goals' // import { goalsToString, lastStepHasErrors, loadGoals } from './goals'
import { GameHint, ProofState } from './rpc_api' // import { GameHint, ProofState } from './rpc_api'
import { useTranslation } from 'react-i18next' // import { useTranslation } from 'react-i18next'
import { InputAbbreviationRewriter } from '@leanprover/unicode-input-component' // import { InputAbbreviationRewriter } from '@leanprover/unicode-input-component'
import ContentEditable from 'react-contenteditable' // import ContentEditable from 'react-contenteditable'
export interface GameDiagnosticsParams { // export interface GameDiagnosticsParams {
uri: DocumentUri; // uri: DocumentUri;
diagnostics: Diagnostic[]; // diagnostics: Diagnostic[];
} // }
/* We register a new language `leancmd` that looks like lean4, but does not use the lsp server. */ // /* We register a new language `leancmd` that looks like lean4, but does not use the lsp server. */
// register Monaco languages // // register Monaco languages
monaco.languages.register({ // monaco.languages.register({
id: 'lean4cmd', // id: 'lean4cmd',
extensions: ['.leancmd'] // extensions: ['.leancmd']
}) // })
// register Monaco languages // TODO: JE. I dont understand why I suddenly had to add this when it worked without before. // // register Monaco languages // TODO: JE. I dont understand why I suddenly had to add this when it worked without before.
monaco.languages.register({ // monaco.languages.register({
id: 'lean4', // id: 'lean4',
extensions: ['.lean'] // extensions: ['.lean']
}) // })
// map of monaco "language id's" to TextMate scopeNames // // map of monaco "language id's" to TextMate scopeNames
const grammars = new Map() // const grammars = new Map()
grammars.set('lean4', 'source.lean') // grammars.set('lean4', 'source.lean')
grammars.set('lean4cmd', 'source.lean') // grammars.set('lean4cmd', 'source.lean')
const registry = new Registry({ // // const registry = new Registry({
getGrammarDefinition: async (scopeName) => { // // getGrammarDefinition: async (scopeName) => {
if (scopeName === 'source.lean') { // // if (scopeName === 'source.lean') {
return { // // return {
format: 'json', // // format: 'json',
content: JSON.stringify(leanSyntax) // // content: JSON.stringify(leanSyntax)
} // // }
} else if (scopeName === 'source.lean.markdown') { // // } else if (scopeName === 'source.lean.markdown') {
return { // // return {
format: 'json', // // format: 'json',
content: JSON.stringify(leanMarkdownSyntax) // // content: JSON.stringify(leanMarkdownSyntax)
} // // }
} else { // // } else {
return { // // return {
format: 'json', // // format: 'json',
content: JSON.stringify(codeblockSyntax) // // content: JSON.stringify(codeblockSyntax)
} // // }
} // // }
} // // }
}); // // });
wireTmGrammars(monaco, registry, grammars) // // wireTmGrammars(monaco, registry, grammars)
let config: any = { ...languageConfig } // // let config: any = { ...languageConfig }
config.autoClosingPairs = config.autoClosingPairs.map( // // config.autoClosingPairs = config.autoClosingPairs.map(
pair => { return {'open': pair[0], 'close': pair[1]} } // // pair => { return {'open': pair[0], 'close': pair[1]} }
) // // )
monaco.languages.setLanguageConfiguration('lean4cmd', config); // // monaco.languages.setLanguageConfiguration('lean4cmd', config);
/** The input field */ // /** The input field */
export function Typewriter({disabled}: {disabled?: boolean}) { // export function Typewriter({disabled}: {disabled?: boolean}) {
let { t } = useTranslation() // let { t } = useTranslation()
/** Reference to the hidden multi-line editor */ // /** Reference to the hidden multi-line editor */
const editor = React.useContext(MonacoEditorContext) // const editor = React.useContext(MonacoEditorContext)
const model = editor?.getModel() // const model = editor?.getModel()
const uri = model?.uri.toString() // const uri = model?.uri.toString()
const [oneLineEditor, setOneLineEditor] = useState<monaco.editor.IStandaloneCodeEditor>(null) // const [oneLineEditor, setOneLineEditor] = useState<monaco.editor.IStandaloneCodeEditor>(null)
const [processing, setProcessing] = useState(false) // const [processing, setProcessing] = useState(false)
const {typewriterInput, setTypewriterInput} = React.useContext(PageContext) // const {typewriterInput, setTypewriterInput} = React.useContext(PageContext)
const inputRef = useRef() // const inputRef = useRef()
// The context storing all information about the current proof // // The context storing all information about the current proof
const {proof, setProof, interimDiags, setInterimDiags, setCrashed} = React.useContext(ProofContext) // const {proof, setProof, interimDiags, setInterimDiags, setCrashed} = React.useContext(ProofContext)
const {gameId, worldId, levelId} = React.useContext(GameIdContext) // const {gameId, worldId, levelId} = React.useContext(GameIdContext)
// state to store the last batch of deleted messages // // state to store the last batch of deleted messages
const {setDeletedChat} = React.useContext(ChatContext) // const {setDeletedChat} = React.useContext(ChatContext)
const rpcSess = React.useContext(RpcContext) // const rpcSess = React.useContext(RpcContext)
// Run the command // // Run the command
const runCommand = React.useCallback(() => { // const runCommand = React.useCallback(() => {
if (processing) {return} // if (processing) {return}
// TODO: Desired logic is to only reset this after a new *error-free* command has been entered // // TODO: Desired logic is to only reset this after a new *error-free* command has been entered
setDeletedChat([]) // setDeletedChat([])
const pos = editor?.getPosition() // const pos = editor?.getPosition()
if (typewriterInput) { // if (typewriterInput) {
setProcessing(true) // setProcessing(true)
editor?.executeEdits("typewriter", [{ // editor?.executeEdits("typewriter", [{
range: monaco.Selection.fromPositions( // range: monaco.Selection.fromPositions(
pos, // pos,
editor?.getModel()?.getFullModelRange()?.getEndPosition() // editor?.getModel()?.getFullModelRange()?.getEndPosition()
), // ),
text: typewriterInput.trim() + "\n", // text: typewriterInput.trim() + "\n",
forceMoveMarkers: false // forceMoveMarkers: false
}]) // }])
setTypewriterInput('') // setTypewriterInput('')
// Load proof after executing edits // // Load proof after executing edits
loadGoals(rpcSess, uri, setProof, setCrashed) // loadGoals(rpcSess, uri, setProof, setCrashed)
} // }
editor?.setPosition(pos) // editor?.setPosition(pos)
}, [typewriterInput, editor]) // }, [typewriterInput, editor])
useEffect(() => { // useEffect(() => {
if (oneLineEditor && oneLineEditor.getValue() !== typewriterInput) { // if (oneLineEditor && oneLineEditor.getValue() !== typewriterInput) {
oneLineEditor.setValue(typewriterInput) // oneLineEditor.setValue(typewriterInput)
} // }
}, [typewriterInput]) // }, [typewriterInput])
/* Load proof on start/switching to typewriter */ // /* Load proof on start/switching to typewriter */
useEffect(() => { // useEffect(() => {
setProof(null) // setProof(null)
loadGoals(rpcSess, uri, setProof, setCrashed) // loadGoals(rpcSess, uri, setProof, setCrashed)
}, [gameId, worldId, levelId]) // }, [gameId, worldId, levelId])
/** If the last step has an error, add the command to the typewriter. */ // /** If the last step has an error, add the command to the typewriter. */
useEffect(() => { // useEffect(() => {
if (lastStepHasErrors(proof)) { // if (lastStepHasErrors(proof)) {
setTypewriterInput(proof?.steps[proof?.steps.length - 1].command) // setTypewriterInput(proof?.steps[proof?.steps.length - 1].command)
} // }
}, [proof]) // }, [proof])
// React when answer from the server comes back // // React when answer from the server comes back
useServerNotificationEffect('textDocument/publishDiagnostics', (params: PublishDiagnosticsParams) => { // useServerNotificationEffect('textDocument/publishDiagnostics', (params: PublishDiagnosticsParams) => {
if (params.uri == uri) { // if (params.uri == uri) {
setProcessing(false) // setProcessing(false)
console.log('Received lean diagnostics') // console.log('Received lean diagnostics')
console.log(params.diagnostics) // console.log(params.diagnostics)
setInterimDiags(params.diagnostics) // setInterimDiags(params.diagnostics)
//loadGoals(rpcSess, uri, setProof) // //loadGoals(rpcSess, uri, setProof)
// TODO: loadAllGoals() // // TODO: loadAllGoals()
if (!hasErrors(params.diagnostics)) { // if (!hasErrors(params.diagnostics)) {
//setTypewriterInput("") // //setTypewriterInput("")
editor?.setPosition(editor?.getModel()?.getFullModelRange()?.getEndPosition()) // editor?.setPosition(editor?.getModel()?.getFullModelRange()?.getEndPosition())
} // }
} else { // } else {
// console.debug(`expected uri: ${uri}, got: ${params.uri}`) // // console.debug(`expected uri: ${uri}, got: ${params.uri}`)
// console.debug(params) // // console.debug(params)
} // }
// TODO: This is the wrong place apparently. Where do wee need to load them? // // TODO: This is the wrong place apparently. Where do wee need to load them?
// TODO: instead of loading all goals every time, we could only load the last one // // TODO: instead of loading all goals every time, we could only load the last one
// loadAllGoals() // // loadAllGoals()
}, [uri]); // }, [uri]);
// // React when answer from the server comes back // // // React when answer from the server comes back
// useServerNotificationEffect('$/game/publishDiagnostics', (params: GameDiagnosticsParams) => { // // useServerNotificationEffect('$/game/publishDiagnostics', (params: GameDiagnosticsParams) => {
// console.log('Received game diagnostics') // // console.log('Received game diagnostics')
// console.log(`diag. uri : ${params.uri}`) // // console.log(`diag. uri : ${params.uri}`)
// console.log(params.diagnostics) // // console.log(params.diagnostics)
// }, [uri]); // // }, [uri]);
useEffect(() => { // useEffect(() => {
const myEditor = monaco.editor.create(inputRef.current!, { // const myEditor = monaco.editor.create(inputRef.current!, {
value: typewriterInput, // value: typewriterInput,
language: "lean4cmd", // language: "lean4cmd",
quickSuggestions: false, // quickSuggestions: false,
lightbulb: { // lightbulb: {
enabled: true // enabled: true
}, // },
unicodeHighlight: { // unicodeHighlight: {
ambiguousCharacters: false, // ambiguousCharacters: false,
}, // },
automaticLayout: true, // automaticLayout: true,
minimap: { // minimap: {
enabled: false // enabled: false
}, // },
lineNumbers: 'off', // lineNumbers: 'off',
tabSize: 2, // tabSize: 2,
glyphMargin: false, // glyphMargin: false,
folding: false, // folding: false,
lineDecorationsWidth: 0, // lineDecorationsWidth: 0,
lineNumbersMinChars: 0, // lineNumbersMinChars: 0,
'semanticHighlighting.enabled': true, // 'semanticHighlighting.enabled': true,
overviewRulerLanes: 0, // overviewRulerLanes: 0,
hideCursorInOverviewRuler: true, // hideCursorInOverviewRuler: true,
scrollbar: { // scrollbar: {
vertical: 'hidden', // vertical: 'hidden',
horizontalScrollbarSize: 3 // horizontalScrollbarSize: 3
}, // },
overviewRulerBorder: false, // overviewRulerBorder: false,
theme: 'vs-code-theme-converted', // theme: 'vs-code-theme-converted',
fontFamily: "JuliaMono", // fontFamily: "JuliaMono",
contextmenu: false // contextmenu: false
}) // })
setOneLineEditor(myEditor) // setOneLineEditor(myEditor)
const abbrevRewriter = new AbbreviationRewriter(new AbbreviationProvider(), myEditor.getModel(), myEditor) // const abbrevRewriter = new AbbreviationRewriter(new AbbreviationProvider(), myEditor.getModel(), myEditor)
return () => {abbrevRewriter.dispose(); myEditor.dispose()} // return () => {abbrevRewriter.dispose(); myEditor.dispose()}
}, []) // }, [])
useEffect(() => { // useEffect(() => {
if (!oneLineEditor) return // if (!oneLineEditor) return
// Ensure that our one-line editor can only have a single line // // Ensure that our one-line editor can only have a single line
const l = oneLineEditor.getModel().onDidChangeContent((e) => { // const l = oneLineEditor.getModel().onDidChangeContent((e) => {
const value = oneLineEditor.getValue() // const value = oneLineEditor.getValue()
setTypewriterInput(value) // setTypewriterInput(value)
const newValue = value.replace(/[\n\r]/g, '') // const newValue = value.replace(/[\n\r]/g, '')
if (value != newValue) { // if (value != newValue) {
oneLineEditor.setValue(newValue) // oneLineEditor.setValue(newValue)
} // }
}) // })
return () => { l.dispose() } // return () => { l.dispose() }
}, [oneLineEditor, setTypewriterInput]) // }, [oneLineEditor, setTypewriterInput])
useEffect(() => { // useEffect(() => {
if (!oneLineEditor) return // if (!oneLineEditor) return
// Run command when pressing enter // // Run command when pressing enter
const l = oneLineEditor.onKeyUp((ev) => { // const l = oneLineEditor.onKeyUp((ev) => {
if (ev.code === "Enter" || ev.code === "NumpadEnter") { // if (ev.code === "Enter" || ev.code === "NumpadEnter") {
runCommand() // runCommand()
} // }
}) // })
return () => { l.dispose() } // return () => { l.dispose() }
}, [oneLineEditor, runCommand]) // }, [oneLineEditor, runCommand])
// BUG: Causes `file closed` error // // BUG: Causes `file closed` error
//TODO: Intention is to run once when loading, does that work? // //TODO: Intention is to run once when loading, does that work?
useEffect(() => { // useEffect(() => {
console.debug(`time to update: ${uri} \n ${rpcSess}`) // console.debug(`time to update: ${uri} \n ${rpcSess}`)
console.debug(rpcSess) // console.debug(rpcSess)
// console.debug('LOAD ALL GOALS') // // console.debug('LOAD ALL GOALS')
// TODO: loadAllGoals() // // TODO: loadAllGoals()
}, [rpcSess]) // }, [rpcSess])
/** Process the entered command */ // /** Process the entered command */
const handleSubmit : React.FormEventHandler<HTMLFormElement> = (ev) => { // const handleSubmit : React.FormEventHandler<HTMLFormElement> = (ev) => {
ev.preventDefault() // ev.preventDefault()
runCommand() // runCommand()
} // }
// do not display if the proof is completed (with potential warnings still present) // // do not display if the proof is completed (with potential warnings still present)
return <div className={`typewriter${proof?.completedWithWarnings ? ' hidden' : ''}${disabled ? ' disabled' : ''}`}> // return <div className={`typewriter${proof?.completedWithWarnings ? ' hidden' : ''}${disabled ? ' disabled' : ''}`}>
<form onSubmit={handleSubmit}> // <form onSubmit={handleSubmit}>
<div className="typewriter-input-wrapper"> // <div className="typewriter-input-wrapper">
<div ref={inputRef} className="typewriter-input" /> // <div ref={inputRef} className="typewriter-input" />
</div> // </div>
<button type="submit" disabled={processing} className="btn btn-inverted"> // <button type="submit" disabled={processing} className="btn btn-inverted">
<FontAwesomeIcon icon={faWandMagicSparkles} />&nbsp;{t("Execute")} // <FontAwesomeIcon icon={faWandMagicSparkles} />&nbsp;{t("Execute")}
</button> // </button>
</form> // </form>
</div> // </div>
} // }
/** Checks whether the diagnostics contain any errors or warnings to check whether the level has // /** Checks whether the diagnostics contain any errors or warnings to check whether the level has
been completed.*/ // been completed.*/
export function hasErrors(diags: Diagnostic[]) { // export function hasErrors(diags: Diagnostic[]) {
return diags.some( // return diags.some(
(d) => // (d) =>
!d.message.startsWith("unsolved goals") && // !d.message.startsWith("unsolved goals") &&
(d.severity == DiagnosticSeverity.Error ) // || d.severity == DiagnosticSeverity.Warning // (d.severity == DiagnosticSeverity.Error ) // || d.severity == DiagnosticSeverity.Warning
) // )
} // }
// TODO: Didn't manage to unify this with the one above // // TODO: Didn't manage to unify this with the one above
export function hasInteractiveErrors (diags: InteractiveDiagnostic[]) { // export function hasInteractiveErrors (diags: InteractiveDiagnostic[]) {
return (typeof diags !== 'undefined') && diags.some( // return (typeof diags !== 'undefined') && diags.some(
(d) => (d.severity == DiagnosticSeverity.Error ) // || d.severity == DiagnosticSeverity.Warning // (d) => (d.severity == DiagnosticSeverity.Error ) // || d.severity == DiagnosticSeverity.Warning
) // )
} // }
export function getInteractiveDiagsAt (proof: ProofState, k : number) { // export function getInteractiveDiagsAt (proof: ProofState, k : number) {
if (k == 0) { // if (k == 0) {
return [] // return []
} else if (k >= proof?.steps.length-1) { // } else if (k >= proof?.steps.length-1) {
// TODO: Do we need that? // // TODO: Do we need that?
return proof?.diagnostics.filter(msg => msg.range.start.line >= proof?.steps.length-1) // return proof?.diagnostics.filter(msg => msg.range.start.line >= proof?.steps.length-1)
} else { // } else {
return proof?.diagnostics.filter(msg => msg.range.start.line == k-1) // return proof?.diagnostics.filter(msg => msg.range.start.line == k-1)
} // }
} // }

@ -7,17 +7,18 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faHome, faArrowRight, faCode, faTerminal } from '@fortawesome/free-solid-svg-icons' import { faHome, faArrowRight, faCode, faTerminal } from '@fortawesome/free-solid-svg-icons'
import { CircularProgress } from '@mui/material' import { CircularProgress } from '@mui/material'
import type { Location } from 'vscode-languageserver-protocol' import type { Location } from 'vscode-languageserver-protocol'
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js' import * as monaco from 'monaco-editor'
import { AbbreviationProvider } from 'lean4web/client/src/editor/abbreviation/AbbreviationProvider' // import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
import { AbbreviationRewriter } from 'lean4web/client/src/editor/abbreviation/rewriter/AbbreviationRewriter' // import { AbbreviationProvider } from 'lean4web/client/src/editor/abbreviation/AbbreviationProvider'
import { InfoProvider } from 'lean4web/client/src/editor/infoview' // import { AbbreviationRewriter } from 'lean4web/client/src/editor/abbreviation/rewriter/AbbreviationRewriter'
import { LeanTaskGutter } from 'lean4web/client/src/editor/taskgutter' // import { InfoProvider } from 'lean4web/client/src/editor/infoview'
import { InfoviewApi } from '@leanprover/infoview' // import { LeanTaskGutter } from 'lean4web/client/src/editor/taskgutter'
import { EditorContext } from '../../../node_modules/lean4-infoview/src/infoview/contexts' // import { InfoviewApi } from '@leanprover/infoview'
import { EditorConnection, EditorEvents } from '../../../node_modules/lean4-infoview/src/infoview/editorConnection' // import { EditorContext } from '../../../node_modules/lean4-infoview/src/infoview/contexts'
import { EventEmitter } from '../../../node_modules/lean4-infoview/src/infoview/event' // import { EditorConnection, EditorEvents } from '../../../node_modules/lean4-infoview/src/infoview/editorConnection'
import { Diagnostic } from 'vscode-languageserver-types' // import { EventEmitter } from '../../../node_modules/lean4-infoview/src/infoview/event'
import { DiagnosticSeverity } from 'vscode-languageclient'; // import { Diagnostic } from 'vscode-languageserver-types'
// import { DiagnosticSeverity } from 'vscode-languageclient';
import { useAppDispatch, useAppSelector } from '../hooks' import { useAppDispatch, useAppSelector } from '../hooks'
import { useGetGameInfoQuery, useLoadInventoryOverviewQuery, useLoadLevelQuery } from '../state/api' import { useGetGameInfoQuery, useLoadInventoryOverviewQuery, useLoadLevelQuery } from '../state/api'
@ -25,11 +26,10 @@ import { changedSelection, codeEdited, selectCode, selectSelections, selectCompl
selectHelp, selectDifficulty, selectInventory, selectTypewriterMode, changeTypewriterMode } from '../state/progress' selectHelp, selectDifficulty, selectInventory, selectTypewriterMode, changeTypewriterMode } from '../state/progress'
import { store } from '../state/store' import { store } from '../state/store'
import {InventoryPanel} from './inventory' import {InventoryPanel} from './inventory'
import { Editor } from './editor'
import { Typewriter } from './typewriter' import { Typewriter } from './typewriter'
import { ChatContext, InputModeContext, PreferencesContext, MonacoEditorContext, import { ChatContext, InputModeContext, PreferencesContext, MonacoEditorContext,
ProofContext, PageContext, GameIdContext } from '../state/context' ProofContext, PageContext, GameIdContext } from '../state/context'
import { DualEditor, ExerciseStatement } from './infoview/main' // import { DualEditor, ExerciseStatement } from './infoview/main'
import { GameHint, InteractiveGoalsWithHints, ProofState } from './infoview/rpc_api' import { GameHint, InteractiveGoalsWithHints, ProofState } from './infoview/rpc_api'
import path from 'path'; import path from 'path';
@ -37,25 +37,92 @@ import '@fontsource/roboto/300.css'
import '@fontsource/roboto/400.css' import '@fontsource/roboto/400.css'
import '@fontsource/roboto/500.css' import '@fontsource/roboto/500.css'
import '@fontsource/roboto/700.css' import '@fontsource/roboto/700.css'
import 'lean4web/client/src/editor/infoview.css' // import 'lean4web/client/src/editor/infoview.css'
import 'lean4web/client/src/editor/vscode.css' // import 'lean4web/client/src/editor/vscode.css'
import '../css/level.css' import '../css/level.css'
import { LeanClient } from 'lean4web/client/src/editor/leanclient' // import { LeanClient } from 'lean4web/client/src/editor/leanclient'
import { DisposingWebSocketMessageReader } from 'lean4web/client/src/reader' // import { DisposingWebSocketMessageReader } from 'lean4web/client/src/reader'
import { WebSocketMessageWriter, toSocket } from 'vscode-ws-jsonrpc' // import { WebSocketMessageWriter, toSocket } from 'vscode-ws-jsonrpc'
import { IConnectionProvider } from 'monaco-languageclient' // import { IConnectionProvider } from 'monaco-languageclient'
import { monacoSetup } from 'lean4web/client/src/monacoSetup' // import { monacoSetup } from 'lean4web/client/src/monacoSetup'
import { onigasmH } from 'onigasm/lib/onigasmH' // import { onigasmH } from 'onigasm/lib/onigasmH'
import { isLastStepWithErrors, lastStepHasErrors } from './infoview/goals' // import { isLastStepWithErrors, lastStepHasErrors } from './infoview/goals'
import { InfoPopup } from './popup/info' import { InfoPopup } from './popup/info'
import { PreferencesPopup } from './popup/preferences' import { PreferencesPopup } from './popup/preferences'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import i18next from 'i18next' import i18next from 'i18next'
import { ChatButtons } from './chat' import { ChatButtons } from './chat'
import { NavButton } from './navigation' import { NavButton } from './navigation'
import { ExerciseStatement } from './editor/ExcerciseStatement'
import { Editor } from './editor/Editor'
monacoSetup() export function NewLevel({visible = true}) {
const dispatch = useAppDispatch()
let { t } = useTranslation()
const {gameId, worldId, levelId} = React.useContext(GameIdContext)
const { mobile } = useContext(PreferencesContext)
const [lockEditorMode, setLockEditorMode] = useState(false)
const gameInfo = useGetGameInfoQuery({game: gameId})
const levelInfo = useLoadLevelQuery({game: gameId, world: worldId, level: levelId})
const typewriterMode = useSelector(selectTypewriterMode(gameId))
const proofPanelRef = React.useRef<HTMLDivElement>(null)
const setTypewriterMode = (newTypewriterMode: boolean) => dispatch(changeTypewriterMode({
game: gameId,
typewriterMode: newTypewriterMode
}))
// Load the namespace of the game
i18next.loadNamespaces(gameId).catch(err => {
console.warn(`translations for ${gameId} do not exist.`)
})
/** toggle input mode if allowed */
function toggleInputMode(ev: React.MouseEvent) {
if (!lockEditorMode) {
setTypewriterMode(!typewriterMode)
console.log('test')
}
}
return <div className="exercise">
{ // Display world image if it exists.
gameInfo.data?.worlds.nodes[worldId].image &&
<div className='world-image'>
<img className="contain" src={path.join("data", gameId, gameInfo.data?.worlds.nodes[worldId].image)} alt="" />
</div>
}
{ levelId > 0 &&
<div className={`exercise-content ${(typewriterMode && !lockEditorMode) ? 'typewriter-mode': 'editor-mode' }`}>
<NavButton
className="btn-input-mode"
icon={(typewriterMode && !lockEditorMode) ? faCode : faTerminal}
inverted={true}
disabled={levelId == 0 || lockEditorMode}
onClick={(ev) => toggleInputMode(ev)}
title={lockEditorMode ? t("Editor mode is enforced!") : typewriterMode ? t("Editor mode") : t("Typewriter mode")} />
<div className="tmp-pusher" />
<div className='proof' ref={proofPanelRef}>
<ExerciseStatement showLeanStatement={!typewriterMode} />
<Editor />
{/* <div ref={editorRef} className="new-editor-test"/> */}
{/* <div ref={infoviewRef} className="new-infoview-test"/> */}
{/* <Level/> */}
{/* { typewriterMode ? <Typewriter /> : <Editor /> } */}
</div>
</div>
}
</div>
}
// monacoSetup()
export function Level({visible = true}) { export function Level({visible = true}) {
let { t } = useTranslation() let { t } = useTranslation()
@ -114,33 +181,33 @@ export function Level({visible = true}) {
} }
const {editor, infoProvider, editorConnection} = // const {editor, infoProvider, editorConnection} =
useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection) // useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection)
/** Unused. Was implementing an undo button, which has been replaced by `deleteProof` inside // /** Unused. Was implementing an undo button, which has been replaced by `deleteProof` inside
* `TypewriterInterface`. // * `TypewriterInterface`.
*/ // */
const handleUndo = () => { // const handleUndo = () => {
const endPos = editor.getModel().getFullModelRange().getEndPosition() // const endPos = editor.getModel().getFullModelRange().getEndPosition()
let range // let range
console.log(endPos.column) // console.log(endPos.column)
if (endPos.column === 1) { // if (endPos.column === 1) {
range = monaco.Selection.fromPositions( // range = monaco.Selection.fromPositions(
new monaco.Position(endPos.lineNumber - 1, 1), // new monaco.Position(endPos.lineNumber - 1, 1),
endPos // endPos
) // )
} else { // } else {
range = monaco.Selection.fromPositions( // range = monaco.Selection.fromPositions(
new monaco.Position(endPos.lineNumber, 1), // new monaco.Position(endPos.lineNumber, 1),
endPos // endPos
) // )
} // }
editor.executeEdits("undo-button", [{ // editor.executeEdits("undo-button", [{
range, // range,
text: "", // text: "",
forceMoveMarkers: false // forceMoveMarkers: false
}]); // }]);
} // }
// Select and highlight proof steps and corresponding hints // Select and highlight proof steps and corresponding hints
// TODO: with the new design, there is no difference between the introduction and // TODO: with the new design, there is no difference between the introduction and
@ -148,62 +215,62 @@ export function Level({visible = true}) {
const [selectedStep, setSelectedStep] = useState<number>() const [selectedStep, setSelectedStep] = useState<number>()
useEffect (() => { // useEffect (() => {
// Lock editor mode // // Lock editor mode
if (levelInfo.data?.template) { // if (levelInfo.data?.template) {
setLockEditorMode(true) // setLockEditorMode(true)
if (editor) { // if (editor) {
let code = editor.getModel().getLinesContent() // let code = editor.getModel().getLinesContent()
// console.log(`insert. code: ${code}`) // // console.log(`insert. code: ${code}`)
// console.log(`insert. join: ${code.join('')}`) // // console.log(`insert. join: ${code.join('')}`)
// console.log(`insert. trim: ${code.join('').trim()}`) // // console.log(`insert. trim: ${code.join('').trim()}`)
// console.log(`insert. length: ${code.join('').trim().length}`) // // console.log(`insert. length: ${code.join('').trim().length}`)
// console.log(`insert. range: ${editor.getModel().getFullModelRange()}`) // // console.log(`insert. range: ${editor.getModel().getFullModelRange()}`)
// TODO: It does seem that the template is always indented by spaces. // // TODO: It does seem that the template is always indented by spaces.
// This is a hack, assuming there are exactly two. // // This is a hack, assuming there are exactly two.
if (!code.join('').trim().length) { // if (!code.join('').trim().length) {
console.debug(`inserting template:\n${levelInfo.data.template}`) // console.debug(`inserting template:\n${levelInfo.data.template}`)
// TODO: This does not work! HERE // // TODO: This does not work! HERE
// Probably overwritten by a query to the server // // Probably overwritten by a query to the server
editor.executeEdits("template-writer", [{ // editor.executeEdits("template-writer", [{
range: editor.getModel().getFullModelRange(), // range: editor.getModel().getFullModelRange(),
text: levelInfo.data.template + `\n`, // text: levelInfo.data.template + `\n`,
forceMoveMarkers: true // forceMoveMarkers: true
}]) // }])
} else { // } else {
console.debug(`not inserting template.`) // console.debug(`not inserting template.`)
} // }
} // }
} else { // } else {
setLockEditorMode(false) // setLockEditorMode(false)
} // }
}, [levelInfo, levelId, worldId, gameId, editor]) // }, [levelInfo, levelId, worldId, gameId, editor])
useEffect(() => { // useEffect(() => {
// TODO: That's a problem if the saved proof contains an error // // TODO: That's a problem if the saved proof contains an error
// Reset command line input when loading a new level // // Reset command line input when loading a new level
setTypewriterInput("") // setTypewriterInput("")
// Load the selected help steps from the store // // Load the selected help steps from the store
setShowHelp(new Set(selectHelp(gameId, worldId, levelId)(store.getState()))) // setShowHelp(new Set(selectHelp(gameId, worldId, levelId)(store.getState())))
}, [gameId, worldId, levelId]) // }, [gameId, worldId, levelId])
useEffect(() => { // useEffect(() => {
if (!(typewriterMode && !lockEditorMode) && editor) { // if (!(typewriterMode && !lockEditorMode) && editor) {
// Delete last input attempt from command line // // Delete last input attempt from command line
editor.executeEdits("typewriter", [{ // editor.executeEdits("typewriter", [{
range: editor.getSelection(), // range: editor.getSelection(),
text: "", // text: "",
forceMoveMarkers: false // forceMoveMarkers: false
}]); // }]);
editor.focus() // editor.focus()
} // }
}, [typewriterMode, lockEditorMode]) // }, [typewriterMode, lockEditorMode])
useEffect(() => { useEffect(() => {
// Forget whether hidden hints are displayed for steps that don't exist yet // Forget whether hidden hints are displayed for steps that don't exist yet
@ -222,14 +289,15 @@ export function Level({visible = true}) {
}, [showHelp]) }, [showHelp])
// Effect when command line mode gets enabled // Effect when command line mode gets enabled
useEffect(() => { // useEffect(() => {
if (onigasmH && editor && (typewriterMode && !lockEditorMode)) { // if (//onigasmH &&
let code = editor.getModel().getLinesContent().filter(line => line.trim()) // editor && (typewriterMode && !lockEditorMode)) {
editor.executeEdits("typewriter", [{ // let code = editor.getModel().getLinesContent().filter(line => line.trim())
range: editor.getModel().getFullModelRange(), // editor.executeEdits("typewriter", [{
text: code.length ? code.join('\n') + '\n' : '', // range: editor.getModel().getFullModelRange(),
forceMoveMarkers: true // text: code.length ? code.join('\n') + '\n' : '',
}]); // forceMoveMarkers: true
// }]);
// let endPos = editor.getModel().getFullModelRange().getEndPosition() // let endPos = editor.getModel().getFullModelRange().getEndPosition()
// if (editor.getModel().getLineContent(endPos.lineNumber).trim() !== "") { // if (editor.getModel().getLineContent(endPos.lineNumber).trim() !== "") {
@ -245,18 +313,18 @@ export function Level({visible = true}) {
// // This is not a position that would naturally occur from Typewriter, reset: // // This is not a position that would naturally occur from Typewriter, reset:
// editor.setSelection(monaco.Selection.fromPositions(endPos, endPos)) // editor.setSelection(monaco.Selection.fromPositions(endPos, endPos))
// } // }
} // }
}, [editor, typewriterMode, lockEditorMode, onigasmH == null]) // }, [editor, typewriterMode, lockEditorMode, ])//onigasmH == null])
return <div className={visible?'':'hidden'}> return <div className={visible?'':'hidden'}>
{/* <div style={levelInfo.isLoading ? null : {display: "none"}} className="app-content loading"><CircularProgress /></div> */} {/* <div style={levelInfo.isLoading ? null : {display: "none"}} className="app-content loading"><CircularProgress /></div> */}
<EditorContext.Provider value={editorConnection}> {/* <EditorContext.Provider value={editorConnection}>
<MonacoEditorContext.Provider value={editor}> <MonacoEditorContext.Provider value={editor}> */}
{levelId > 0 && {levelId > 0 &&
<ExercisePanel codeviewRef={codeviewRef} />} <ExercisePanel codeviewRef={codeviewRef} />}
</MonacoEditorContext.Provider> {/* </MonacoEditorContext.Provider>
</EditorContext.Provider> </EditorContext.Provider> */}
</div> </div>
} }
@ -286,18 +354,6 @@ export function LevelWrapper({visible = true}) {
const lastLevel = levelId >= gameInfo.data?.worldSize[worldId] const lastLevel = levelId >= gameInfo.data?.worldSize[worldId]
const { proof } = useContext(ProofContext) const { proof } = useContext(ProofContext)
// // impressum pop-up
// function toggleImpressum() {setImpressum(!impressum)}
// function togglePrivacy() {setPrivacy(!privacy)}
// When clicking on an inventory item, the inventory is overlayed by the item's doc.
// If this state is set to a pair `(name, type)` then the according doc will be open.
// Set `inventoryDoc` to `null` to close the doc
const [inventoryDoc, setInventoryDoc] = useState<{name: string, type: string}>(null)
function closeInventoryDoc () {setInventoryDoc(null)}
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const typewriterMode = useSelector(selectTypewriterMode(gameId)) const typewriterMode = useSelector(selectTypewriterMode(gameId))
@ -537,7 +593,7 @@ export function ExercisePanel({codeviewRef, visible=true}: {codeviewRef: React.M
const gameInfo = useGetGameInfoQuery({game: gameId}) const gameInfo = useGetGameInfoQuery({game: gameId})
return <div className={`exercise-panel ${visible ? '' : 'hidden'}`}> return <div className={`exercise-panel ${visible ? '' : 'hidden'}`}>
<div className=""> <div className="">
<DualEditor level={level?.data} codeviewRef={codeviewRef} levelId={levelId} worldId={worldId} worldSize={gameInfo.data?.worldSize[worldId]}/> {/* <DualEditor level={level?.data} codeviewRef={codeviewRef} levelId={levelId} worldId={worldId} worldSize={gameInfo.data?.worldSize[worldId]}/> */}
</div> </div>
</div> </div>
} }
@ -632,170 +688,170 @@ export function ExercisePanel({codeviewRef, visible=true}: {codeviewRef: React.M
// </Split> // </Split>
// } // }
function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection) { // function useLevelEditor(codeviewRef, initialCode, initialSelections, onDidChangeContent, onDidChangeSelection) {
const {gameId, worldId, levelId} = React.useContext(GameIdContext) // const {gameId, worldId, levelId} = React.useContext(GameIdContext)
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 [editorConnection, setEditorConnection] = useState<null|EditorConnection>(null) // const [editorConnection, setEditorConnection] = useState<null|EditorConnection>(null)
const uriStr = `file:///${worldId}/${levelId}` // const uriStr = `file:///${worldId}/${levelId}`
const uri = monaco.Uri.parse(uriStr) // const uri = monaco.Uri.parse(uriStr)
const inventory: Array<String> = useSelector(selectInventory(gameId)) // const inventory: Array<String> = useSelector(selectInventory(gameId))
const difficulty: number = useSelector(selectDifficulty(gameId)) // const difficulty: number = useSelector(selectDifficulty(gameId))
useEffect(() => { // useEffect(() => {
// monaco.editor.getModels().forEach(model => model.dispose()); // // monaco.editor.getModels().forEach(model => model.dispose());
console.info(`trying to create model: ${gameId} ${worldId} ${levelId} ${uri}`) // console.info(`trying to create model: ${gameId} ${worldId} ${levelId} ${uri}`)
const model = monaco.editor.createModel(initialCode ?? '', 'lean4', uri) // const model = monaco.editor.createModel(initialCode ?? '', 'lean4', uri)
if (onDidChangeContent) { // if (onDidChangeContent) {
model.onDidChangeContent(() => onDidChangeContent(model.getValue())) // model.onDidChangeContent(() => onDidChangeContent(model.getValue()))
} // }
const editor = monaco.editor.create(codeviewRef.current!, { // const editor = monaco.editor.create(codeviewRef.current!, {
model, // model,
glyphMargin: true, // glyphMargin: true,
quickSuggestions: false, // quickSuggestions: false,
lineDecorationsWidth: 5, // lineDecorationsWidth: 5,
folding: false, // folding: false,
lineNumbers: 'on', // lineNumbers: 'on',
lightbulb: { // lightbulb: {
enabled: true // enabled: true
}, // },
unicodeHighlight: { // unicodeHighlight: {
ambiguousCharacters: false, // ambiguousCharacters: false,
}, // },
automaticLayout: true, // automaticLayout: true,
minimap: { // minimap: {
enabled: false // enabled: false
}, // },
lineNumbersMinChars: 3, // lineNumbersMinChars: 3,
tabSize: 2, // tabSize: 2,
'semanticHighlighting.enabled': true, // 'semanticHighlighting.enabled': true,
fontFamily: "JuliaMono", // fontFamily: "JuliaMono",
theme: 'vs-code-theme-converted' // theme: 'vs-code-theme-converted'
}) // })
if (onDidChangeSelection) { // if (onDidChangeSelection) {
editor.onDidChangeCursorSelection(() => onDidChangeSelection(editor.getSelections())) // editor.onDidChangeCursorSelection(() => onDidChangeSelection(editor.getSelections()))
} // }
if (initialSelections) { // if (initialSelections) {
console.debug("Initial Selection: ", initialSelections) // console.debug("Initial Selection: ", initialSelections)
// BUG: Somehow I get an `invalid arguments` bug here // // BUG: Somehow I get an `invalid arguments` bug here
// editor.setSelections(initialSelections) // // editor.setSelections(initialSelections)
} // }
setEditor(editor) // setEditor(editor)
const abbrevRewriter = new AbbreviationRewriter(new AbbreviationProvider(), model, editor) // const abbrevRewriter = new AbbreviationRewriter(new AbbreviationProvider(), model, editor)
const socketUrl = ((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + '/websocket/' + gameId // const socketUrl = ((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + '/websocket/' + gameId
const connectionProvider : IConnectionProvider = { // const connectionProvider : IConnectionProvider = {
get: async () => { // get: async () => {
return await new Promise((resolve, reject) => { // return await new Promise((resolve, reject) => {
console.log(`connecting ${socketUrl}`) // console.log(`connecting ${socketUrl}`)
const websocket = new WebSocket(socketUrl) // const websocket = new WebSocket(socketUrl)
websocket.addEventListener('error', (ev) => { // websocket.addEventListener('error', (ev) => {
reject(ev) // reject(ev)
}) // })
websocket.addEventListener('message', (msg) => { // websocket.addEventListener('message', (msg) => {
// console.log(msg.data) // // console.log(msg.data)
}) // })
websocket.addEventListener('open', () => { // websocket.addEventListener('open', () => {
const socket = toSocket(websocket) // const socket = toSocket(websocket)
const reader = new DisposingWebSocketMessageReader(socket) // const reader = new DisposingWebSocketMessageReader(socket)
const writer = new WebSocketMessageWriter(socket) // const writer = new WebSocketMessageWriter(socket)
resolve({ // resolve({
reader, // reader,
writer // writer
}) // })
}) // })
}) // })
} // }
} // }
// Following `vscode-lean4/webview/index.ts` // // Following `vscode-lean4/webview/index.ts`
const client = new LeanClient(connectionProvider, showRestartMessage, {inventory, difficulty}) // const client = new LeanClient(connectionProvider, showRestartMessage, {inventory, difficulty})
const infoProvider = new InfoProvider(client) // const infoProvider = new InfoProvider(client)
// const div: HTMLElement = infoviewRef.current! // // const div: HTMLElement = infoviewRef.current!
const imports = { // const imports = {
'@leanprover/infoview': `${window.location.origin}/index.production.min.js`, // '@leanprover/infoview': `${window.location.origin}/index.production.min.js`,
'react': `${window.location.origin}/react.production.min.js`, // 'react': `${window.location.origin}/react.production.min.js`,
'react/jsx-runtime': `${window.location.origin}/react-jsx-runtime.production.min.js`, // 'react/jsx-runtime': `${window.location.origin}/react-jsx-runtime.production.min.js`,
'react-dom': `${window.location.origin}/react-dom.production.min.js`, // 'react-dom': `${window.location.origin}/react-dom.production.min.js`,
'react-popper': `${window.location.origin}/react-popper.production.min.js` // 'react-popper': `${window.location.origin}/react-popper.production.min.js`
} // }
// loadRenderInfoview(imports, [infoProvider.getApi(), div], setInfoviewApi) // // loadRenderInfoview(imports, [infoProvider.getApi(), div], setInfoviewApi)
setInfoProvider(infoProvider) // setInfoProvider(infoProvider)
// TODO: it looks like we get errors "File Changed" here. // // TODO: it looks like we get errors "File Changed" here.
client.restart("Lean4Game") // client.restart("Lean4Game")
const editorApi = infoProvider.getApi() // const editorApi = infoProvider.getApi()
const editorEvents: EditorEvents = { // const editorEvents: EditorEvents = {
initialize: new EventEmitter(), // initialize: new EventEmitter(),
gotServerNotification: new EventEmitter(), // gotServerNotification: new EventEmitter(),
sentClientNotification: new EventEmitter(), // sentClientNotification: new EventEmitter(),
serverRestarted: new EventEmitter(), // serverRestarted: new EventEmitter(),
serverStopped: new EventEmitter(), // serverStopped: new EventEmitter(),
changedCursorLocation: new EventEmitter(), // changedCursorLocation: new EventEmitter(),
changedInfoviewConfig: new EventEmitter(), // changedInfoviewConfig: new EventEmitter(),
runTestScript: new EventEmitter(), // runTestScript: new EventEmitter(),
requestedAction: new EventEmitter(), // requestedAction: new EventEmitter(),
}; // };
// Challenge: write a type-correct fn from `Eventify<T>` to `T` without using `any` // // Challenge: write a type-correct fn from `Eventify<T>` to `T` without using `any`
const infoviewApi: InfoviewApi = { // const infoviewApi: InfoviewApi = {
initialize: async l => editorEvents.initialize.fire(l), // initialize: async l => editorEvents.initialize.fire(l),
gotServerNotification: async (method, params) => { // gotServerNotification: async (method, params) => {
editorEvents.gotServerNotification.fire([method, params]); // editorEvents.gotServerNotification.fire([method, params]);
}, // },
sentClientNotification: async (method, params) => { // sentClientNotification: async (method, params) => {
editorEvents.sentClientNotification.fire([method, params]); // editorEvents.sentClientNotification.fire([method, params]);
}, // },
serverRestarted: async r => editorEvents.serverRestarted.fire(r), // serverRestarted: async r => editorEvents.serverRestarted.fire(r),
serverStopped: async serverStoppedReason => { // serverStopped: async serverStoppedReason => {
editorEvents.serverStopped.fire(serverStoppedReason) // editorEvents.serverStopped.fire(serverStoppedReason)
}, // },
changedCursorLocation: async loc => editorEvents.changedCursorLocation.fire(loc), // changedCursorLocation: async loc => editorEvents.changedCursorLocation.fire(loc),
changedInfoviewConfig: async conf => editorEvents.changedInfoviewConfig.fire(conf), // changedInfoviewConfig: async conf => editorEvents.changedInfoviewConfig.fire(conf),
requestedAction: async action => editorEvents.requestedAction.fire(action), // requestedAction: async action => editorEvents.requestedAction.fire(action),
// See https://rollupjs.org/guide/en/#avoiding-eval // // See https://rollupjs.org/guide/en/#avoiding-eval
// eslint-disable-next-line @typescript-eslint/no-implied-eval // // eslint-disable-next-line @typescript-eslint/no-implied-eval
runTestScript: async script => new Function(script)(), // runTestScript: async script => new Function(script)(),
getInfoviewHtml: async () => document.body.innerHTML, // getInfoviewHtml: async () => document.body.innerHTML,
}; // };
const ec = new EditorConnection(editorApi, editorEvents); // const ec = new EditorConnection(editorApi, editorEvents);
setEditorConnection(ec) // setEditorConnection(ec)
editorEvents.initialize.on((loc: Location) => ec.events.changedCursorLocation.fire(loc)) // editorEvents.initialize.on((loc: Location) => ec.events.changedCursorLocation.fire(loc))
setEditor(editor) // setEditor(editor)
setInfoProvider(infoProvider) // setInfoProvider(infoProvider)
infoProvider.openPreview(editor, infoviewApi) // infoProvider.openPreview(editor, infoviewApi)
const taskgutter = new LeanTaskGutter(infoProvider.client, editor) // const taskgutter = new LeanTaskGutter(infoProvider.client, editor)
// TODO: // // TODO:
// setRestart(() => restart) // // setRestart(() => restart)
return () => { // return () => {
editor.dispose(); // editor.dispose();
model.dispose(); // model.dispose();
abbrevRewriter.dispose(); // abbrevRewriter.dispose();
taskgutter.dispose(); // taskgutter.dispose();
infoProvider.dispose(); // infoProvider.dispose();
client.dispose(); // client.dispose();
} // }
}, [gameId, worldId, levelId]) // }, [gameId, worldId, levelId])
const showRestartMessage = () => { // const showRestartMessage = () => {
// setRestartMessage(true) // // setRestartMessage(true)
console.log("TODO: SHOW RESTART MESSAGE") // console.log("TODO: SHOW RESTART MESSAGE")
} // }
return {editor, infoProvider, editorConnection} // return {editor, infoProvider, editorConnection}
} // }

@ -5,7 +5,7 @@ import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css'; import '@fontsource/roboto/700.css';
import { Button, Dialog, DialogContent, DialogContentText, DialogActions } from '@mui/material'; import { Button, Dialog, DialogContent, DialogContentText, DialogActions } from '@mui/material';
import Markdown from './markdown'; import { Markdown } from './utils';
function Message({ isOpen, content, close }) { function Message({ isOpen, content, close }) {

@ -11,6 +11,10 @@
/* General styling */ /* General styling */
html, body, #root, .app {
height: 100%;
}
body { body {
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
@ -92,12 +96,7 @@ em {
/* App Bar */ /* App Bar */
#root {
height: 100%;
}
.app { .app {
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }

@ -0,0 +1,17 @@
.editor-wrapper {
flex: 1;
}
#editor {
height: 400px;
}
#infoview {
height: 400px;
}
#infoview iframe {
width: 100%;
height: 95%; /* TODO: setting this to 100% makes it a few pixels too high... */
border: unset;
}

@ -27,7 +27,6 @@
overflow: auto; overflow: auto;
position: relative; position: relative;
scroll-behavior: smooth; scroll-behavior: smooth;
} }
.slider .column { .slider .column {

@ -313,6 +313,8 @@ td code {
.editor-mode .proof { .editor-mode .proof {
height: 100%; height: 100%;
display: flex;
flex-direction: column;
} }
.editor-mode .tmp-pusher { .editor-mode .tmp-pusher {

Loading…
Cancel
Save