Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
joneugster committed Feb 27, 2025
1 parent c199947 commit a0edece
Show file tree
Hide file tree
Showing 33 changed files with 5,688 additions and 740 deletions.
11 changes: 10 additions & 1 deletion client/src/components/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import '../../css/editor.css'
import { useSelector } from 'react-redux';
import { selectTypewriterMode } from '../../state/progress';
import { Typewriter } from './typewriter';
import { GoalTabs } from './infoview/goal_tabs';
import { GoalTabs } from './goal_tabs';
import { GameInfoview } from './tmp';

export function Editor() {
let { t } = useTranslation()
Expand All @@ -22,6 +23,7 @@ export function Editor() {

const editorRef = useRef<HTMLDivElement>(null)
const infoviewRef = useRef<HTMLDivElement>(null)
const gameInfoviewRef = useRef<HTMLDivElement>(null)
const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor>()
const [leanMonaco, setLeanMonaco] = useState<LeanMonaco>()
const [code, setCode] = useState<string>('')
Expand Down Expand Up @@ -71,11 +73,16 @@ export function Editor() {
useEffect(() => {
console.debug('[LeanGame] Restarting Editor!')
var _leanMonaco = new LeanMonaco()

var leanMonacoEditor = new LeanMonacoEditor()

_leanMonaco.setInfoviewElement(infoviewRef.current!)
;(async () => {
await _leanMonaco.start(options)

// JE: how do I get the editorApi or an RPC session?
//let infoProvider = _leanMonaco.infoProvider.editorApi

console.warn('gameId', gameId)
await leanMonacoEditor.start(editorRef.current!, `/${worldId}/L_${levelId}.lean`, code)

Expand All @@ -98,6 +105,8 @@ export function Editor() {
className={`editor-split ${typewriterMode ? 'hidden' : ''}`} >
<div ref={editorRef} id="editor" />
<div ref={infoviewRef} id="infoview" />
{/* TODO: */}
<GameInfoview editorApi={null}/>
</Split>
{editor && typewriterMode && <Typewriter />}
</div>
Expand Down
28 changes: 15 additions & 13 deletions client/src/components/editor/Typewriter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,16 @@ import { useContext, useRef, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { GameIdContext, InputModeContext, MonacoEditorContext, ProofContext } from '../../state/context'
import { useGetGameInfoQuery } from '../../state/api'
import { AbbreviationRewriter, AbbreviationProvider, AbbreviationConfig, AbbreviationTextSource } from '@leanprover/unicode-input'

import { RpcContext, WithRpcSessions, useRpcSessionAtPos } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions';
import { faWandMagicSparkles } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import '../../css/typewriter.css'
import * as monaco from 'monaco-editor'
import { useRpcSession } from '@leanprover/infoview'

/** The input field */
function TypewriterInput({disabled}: {disabled?: boolean}) {
let { t } = useTranslation()
const rpcSess = React.useContext(RpcContext)
const [typewriterInput, setTypewriterInput] = useState("") // React.useContext(InputModeContext)
const {proof, setProof, interimDiags, setInterimDiags, setCrashed} = React.useContext(ProofContext)

Expand All @@ -24,8 +22,8 @@ function TypewriterInput({disabled}: {disabled?: boolean}) {

/** Reference to the hidden multi-line editor */
const editor = React.useContext(MonacoEditorContext)
const model = editor.getModel()
const uri = model.uri.toString()

// const rpcSess = useRpcSession()

/** Monaco editor requires the code to be set manually. */
function setTypewriterContent (typewriterInput: string) {
Expand All @@ -43,6 +41,15 @@ function TypewriterInput({disabled}: {disabled?: boolean}) {

// const pos = editor.getPosition()
const pos = editor.getModel().getFullModelRange().getEndPosition()

// rpcSess.call('Game.test', pos).then((response) => {
// console.debug('test Rpc call worked')
// console.debug(response)
// }).catch((err) => {
// console.error("failed")
// console.error(err)
// })

if (typewriterInput) {
// setProcessing(true)
editor.executeEdits("typewriter", [{
Expand Down Expand Up @@ -165,12 +172,7 @@ export function Typewriter() {
const uri = model.uri.toString()
const gameInfo = useGetGameInfoQuery({game: gameId})

const rpcSess = useRpcSessionAtPos({uri: uri, line: 0, character: 0})

return <RpcContext.Provider value={rpcSess}>
<div className="typewriter">

<TypewriterInput />
</div>
</RpcContext.Provider>
return <div className="typewriter">
<TypewriterInput />
</div>
}
154 changes: 154 additions & 0 deletions client/src/components/editor/goal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import * as React from 'react';
import { InteractiveGoal, InteractiveHypothesisBundle } from '../Defs';
import { useTranslation } from 'react-i18next';
import { InteractiveCode, InteractiveHypothesisBundle_nonAnonymousNames, LocationsContext, SubexprInfo, TaggedText } from '@leanprover/infoview';
import { SelectableLocation } from '../../../../../node_modules/lean4-infoview/src/infoview/goalLocation';

/** Returns true if `h` is inaccessible according to Lean's default name rendering. */
function isInaccessibleName(h: string): boolean {
return h.indexOf('✝') >= 0;
}

interface GoalFilterState {
/** If true reverse the list of hypotheses, if false present the order received from LSP. */
reverse: boolean,
/** If true show hypotheses that have isType=True, otherwise hide them. */
showType: boolean,
/** If true show hypotheses that have isInstance=True, otherwise hide them. */
showInstance: boolean,
/** If true show hypotheses that contain a dagger in the name, otherwise hide them. */
showHiddenAssumption: boolean
/** If true show the bodies of let-values, otherwise hide them. */
showLetValue: boolean;
}

interface HypProps {
hyp: InteractiveHypothesisBundle
mvarId?: any // MVarId
}

// function Hyp({ hyp: h, mvarId }: HypProps) {
// const locs = React.useContext(LocationsContext)

// const namecls: string = 'mr1 ' +
// (h.isInserted ? 'inserted-text ' : '') +
// (h.isRemoved ? 'removed-text ' : '')

// const names = InteractiveHypothesisBundle_nonAnonymousNames(h as InteractiveHypothesisBundle).map((n, i) =>
// <span className={namecls + (isInaccessibleName(n) ? 'goal-inaccessible ' : '')} key={i}>
// <SelectableLocation
// locs={locs}
// loc={mvarId && h.fvarIds && h.fvarIds.length > i ?
// { mvarId, loc: { hyp: h.fvarIds[i] }} :
// undefined
// }
// alwaysHighlight={false}
// >{n}</SelectableLocation>
// </span>)

// const typeLocs: Locations | undefined = React.useMemo(() =>
// locs && mvarId && h.fvarIds && h.fvarIds.length > 0 ?
// { ...locs, subexprTemplate: { mvarId, loc: { hypType: [h.fvarIds[0], ''] }}} :
// undefined,
// [locs, mvarId, h.fvarIds])

// const valLocs: Locations | undefined = React.useMemo(() =>
// h.val && locs && mvarId && h.fvarIds && h.fvarIds.length > 0 ?
// { ...locs, subexprTemplate: { mvarId, loc: { hypValue: [h.fvarIds[0], ''] }}} :
// undefined,
// [h.val, locs, mvarId, h.fvarIds])

// return <div>
// <strong className="goal-hyp">{names}</strong>
// :&nbsp;
// <LocationsContext.Provider value={typeLocs}>
// <InteractiveCode fmt={h.type} />
// </LocationsContext.Provider>
// {h.val &&
// <LocationsContext.Provider value={valLocs}>
// &nbsp;:=&nbsp;<InteractiveCode fmt={h.val} />
// </LocationsContext.Provider>}
// </div>
// }

function getFilteredHypotheses(hyps: InteractiveHypothesisBundle[], filter: GoalFilterState): InteractiveHypothesisBundle[] {
return hyps.reduce((acc: InteractiveHypothesisBundle[], h) => {
if (h.isInstance && !filter.showInstance) return acc
if (h.isType && !filter.showType) return acc
const names = filter.showHiddenAssumption ? h.names : h.names.filter(n => !isInaccessibleName(n))
const hNew: InteractiveHypothesisBundle = filter.showLetValue ? { ...h, names } : { ...h, names, val: undefined }
if (names.length !== 0) acc.push(hNew)
return acc
}, [])
}

interface GoalProps {
goal: InteractiveGoal
filter: GoalFilterState
showHints?: boolean
typewriter: boolean
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
* provided `filter`. */
export const Goal = React.memo((props: GoalProps) => {
const { goal, filter, showHints, typewriter, unbundle } = props
let { t } = useTranslation()

// TODO: Apparently `goal` can be `undefined`
if (!goal) {return <></>}

const filteredList = getFilteredHypotheses(goal.hyps, filter);
const hyps = filter.reverse ? filteredList.slice().reverse() : filteredList;
const locs = React.useContext(LocationsContext)
const goalLocs = React.useMemo(() =>
locs && goal.mvarId ?
{ ...locs, subexprTemplate: { mvarId: goal.mvarId, loc: { target: '' }}} :
undefined,
[locs, goal.mvarId])
const goalLi = <div key={'goal'} className="goal">
{/* <div className="goal-title">{t("Goal")}:</div> */}
<LocationsContext.Provider value={goalLocs}>
<InteractiveCode fmt={goal.type as TaggedText<SubexprInfo>} />
</LocationsContext.Provider>
</div>

// let cn = 'font-code tl pre-wrap bl bw1 pl1 b--transparent '
// if (props.goal.isInserted) cn += 'b--inserted '
// if (props.goal.isRemoved) cn += 'b--removed '

function unbundleHyps (hyps: InteractiveHypothesisBundle[]) : InteractiveHypothesisBundle[] {
return hyps.flatMap(hyp => (
unbundle ? hyp.names.map(name => {return {...hyp, names: [name]}}) : [hyp]
))
}

// const hints = <Hints hints={goal.hints} key={goal.mvarId} />
const objectHyps = unbundleHyps(hyps.filter(hyp => !hyp.isAssumption))
const assumptionHyps = unbundleHyps(hyps.filter(hyp => hyp.isAssumption))

return <>
{/* {goal.userName && <div><strong className="goal-case">case </strong>{goal.userName}</div>} */}
{filter.reverse && goalLi}
<div className="hypotheses">
{! typewriter && objectHyps.length > 0 &&
<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> }
{!typewriter && assumptionHyps.length > 0 &&
<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> }
</div>
{!filter.reverse && <>
<div className='goal-sign'>
<svg width="100%" height="100%">
<line x1="0%" y1="0%" x2="0%" y2="100%" />
<line x1="0%" y1="50%" x2="100%" y2="50%" />
</svg>
</div>
{goalLi}
</>}
{/* {showHints && hints} */}
</>
})
36 changes: 36 additions & 0 deletions client/src/components/editor/goal_tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from 'react';
import { useTranslation } from "react-i18next";
import { InteractiveGoalWithHints } from "../Defs";
import { Goal } from './goal';

const goalFilter = {
reverse: false,
showType: true,
showInstance: true,
showHiddenAssumption: true,
showLetValue: true
}

/** The tabs of goals that lean has after the command of this step has been processed */
export function GoalTabs({ goals, last, onClick, onGoalChange=(n)=>{}}: { goals: InteractiveGoalWithHints[], last : boolean, onClick? : any, onGoalChange?: (n?: number) => void }) {
let { t } = useTranslation()
const [selectedGoal, setSelectedGoal] = React.useState<number>(0)

if (goals.length == 0) {
return <></>
}

return <div className="goal-tabs" onClick={onClick}>
<div className={`tab-bar ${last ? 'current' : ''}`}>
{goals.map((goal, i) => (
// TODO: Should not use index as key.
<div key={`proof-goal-${i}`} className={`tab ${i == (selectedGoal) ? "active" : ""}`} onClick={(ev) => { onGoalChange(i); setSelectedGoal(i); ev.stopPropagation() }}>
{i ? t("Goal") + ` ${i + 1}` : t("Active Goal")}
</div>
))}
</div>
<div className="goal-tab vscode-light">
<Goal typewriter={false} filter={goalFilter} goal={goals[selectedGoal]?.goal} unbundle={false} />
</div>
</div>
}
47 changes: 47 additions & 0 deletions client/src/components/editor/infoview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { InteractiveDiagnostics_msgToInteractive, MessageData } from '@leanprover/infoview-api'
import { useRpcSession } from './infoview/rpcSessions'
import { InteractiveMessage } from './infoview/traceExplorer'
import { mapRpcError, useAsync } from './infoview/util'

export * from '@leanprover/infoview-api'
export { EditorContext, EnvPosContext, VersionContext } from './infoview/contexts'
export { EditorConnection } from './infoview/editorConnection'
export { GoalLocation, GoalsLocation, LocationsContext } from './infoview/goalLocation'
export { InteractiveCode, /*InteractiveCodeProps*/ } from './infoview/interactiveCode'
export { renderInfoview } from './infoview/main'
export { RpcContext, useRpcSession } from './infoview/rpcSessions'
export { ServerVersion } from './infoview/serverVersion'
export { DynamicComponent, /*DynamicComponentProps, PanelWidgetProps,*/ importWidgetModule } from './infoview/userWidget'
export {
DocumentPosition,
mapRpcError,
useAsync,
useAsyncPersistent,
useAsyncWithTrigger,
useClientNotificationEffect,
useClientNotificationState,
useEvent,
useEventResult,
useServerNotificationEffect,
useServerNotificationState,
} from './infoview/util'
// export { MessageData }

/** Display the given message data as interactive, pretty-printed text. */
export function InteractiveMessageData({ msg }: { msg: MessageData }) {
const rs = useRpcSession()
const interactive = useAsync(() => InteractiveDiagnostics_msgToInteractive(rs, msg, 0), [rs, msg])

if (interactive.state === 'resolved') {
return <InteractiveMessage fmt={interactive.value} />
} else if (interactive.state === 'loading') {
return <>...</>
} else {
return (
<div>
Failed to display message:
{<span>{mapRpcError(interactive.error).message}</span>}
</div>
)
}
}
Loading

0 comments on commit a0edece

Please sign in to comment.