/* Mostly copied from https://github.com/leanprover/vscode-lean4/blob/master/lean4-infoview/src/infoview/info.tsx */ import * as React from 'react' import { CircularProgress } from '@mui/material' import type { Location, Diagnostic } from 'vscode-languageserver-protocol' import { getInteractiveTermGoal, InteractiveDiagnostic, UserWidgetInstance, Widget_getWidgets, RpcSessionAtPos, isRpcError, RpcErrorCode, getInteractiveDiagnostics, InteractiveTermGoal } from '@leanprover/infoview-api' import { basename, DocumentPosition, RangeHelpers, useEvent, usePausableState, discardMethodNotFound, mapRpcError, useAsyncWithTrigger, PausableProps } from '../../../../node_modules/lean4-infoview/src/infoview/util' import { ConfigContext, EditorContext, LspDiagnosticsContext, ProgressContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts' import { PanelWidgetDisplay } from '../../../../node_modules/lean4-infoview/src/infoview/userWidget' import { RpcContext, useRpcSessionAtPos } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions' import { GoalsLocation, Locations, LocationsContext } from '../../../../node_modules/lean4-infoview/src/infoview/goalLocation' import { AllMessages, lspDiagToInteractive } from './messages' import { goalsToString, Goal, MainAssumptions, OtherGoals, ProofDisplay } from './goals' import { InteractiveGoals } from './rpc_api' import { MonacoEditorContext, ProofStateProps, InfoStatus, ProofContext } from './context' // TODO: All about pinning could probably be removed type InfoKind = 'cursor' | 'pin' interface InfoPinnable { kind: InfoKind /** Takes an argument for caching reasons, but should only ever (un)pin itself. */ onPin: (pos: DocumentPosition) => void } interface InfoStatusBarProps extends InfoPinnable, PausableProps { pos: DocumentPosition status: InfoStatus triggerUpdate: () => Promise } const InfoStatusBar = React.memo((props: InfoStatusBarProps) => { const { kind, onPin, status, pos, isPaused, setPaused, triggerUpdate } = props const ec = React.useContext(EditorContext) const statusColTable: {[T in InfoStatus]: string} = { 'updating': 'gold ', 'error': 'dark-red ', 'ready': '', } const statusColor = statusColTable[status] const locationString = `${basename(pos.uri)}:${pos.line+1}:${pos.character}` const isPinned = kind === 'pin' return ( {locationString} {isPinned && !isPaused && ' (pinned)'} {!isPinned && isPaused && ' (paused)'} {isPinned && isPaused && ' (pinned and paused)'} {isPinned && { e.preventDefault(); void ec.revealPosition(pos); }} title='reveal file location' />} { e.preventDefault(); onPin(pos); }} title={isPinned ? 'unpin' : 'pin'} /> { e.preventDefault(); setPaused(!isPaused) }} title={isPaused ? 'continue updating' : 'pause updating'} /> { e.preventDefault(); void triggerUpdate() }} title='update'/> ) }) interface InfoDisplayContentProps extends PausableProps { pos: DocumentPosition messages: InteractiveDiagnostic[] goals?: InteractiveGoals termGoal?: InteractiveTermGoal error?: string userWidgets: UserWidgetInstance[] triggerUpdate: () => Promise proof? : string } const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => { const {pos, messages, goals, termGoal, error, userWidgets, triggerUpdate, isPaused, setPaused, proof} = props const hasWidget = userWidgets.length > 0 const hasError = !!error const hasMessages = messages.length !== 0 const nothingToShow = !hasError && !goals && !termGoal && !hasMessages && !hasWidget const [selectedLocs, setSelectedLocs] = React.useState([]) React.useEffect(() => setSelectedLocs([]), [pos.uri, pos.line, pos.character]) const locs: Locations = React.useMemo(() => ({ isSelected: (l: GoalsLocation) => selectedLocs.some(v => GoalsLocation.isEqual(v, l)), setSelected: (l, act) => setSelectedLocs(ls => { // We ensure that `selectedLocs` maintains its reference identity if the selection // status of `l` didn't change. const newLocs = ls.filter(v => !GoalsLocation.isEqual(v, l)) const wasSelected = newLocs.length !== ls.length const isSelected = typeof act === 'function' ? act(wasSelected) : act if (isSelected) newLocs.push(l) return wasSelected === isSelected ? ls : newLocs }), subexprTemplate: undefined }), [selectedLocs]) const goalFilter = { reverse: false, showType: true, showInstance: true, showHiddenAssumption: true, showLetValue: true } /* Adding {' '} to manage string literals properly: https://reactjs.org/docs/jsx-in-depth.html#string-literals-1 */ return <> {hasError &&
Error updating:{' '}{error}. { e.preventDefault(); void triggerUpdate() }}>{' '}Try again.
}
{ goals && goals.goals.length > 0 && <> }
{ goals && (goals.goals.length > 0 ? :
No Goals
)}
{userWidgets.map(widget =>
{widget.name} goal) : []} termGoal={termGoal} selectedLocations={selectedLocs} widget={widget}/>
)} {nothingToShow && ( isPaused ? /* Adding {' '} to manage string literals properly: https://reactjs.org/docs/jsx-in-depth.html#string-literals-1 */ Updating is paused.{' '} { e.preventDefault(); void triggerUpdate() }}>Refresh {' '}or { e.preventDefault(); setPaused(false) }}>resume updating {' '}to see information. : <>
Loading goal...
)} {/* {goals && goals.goals.length > 1 &&
Weitere Goals
{goals.goals.slice(1).map((goal, i) =>
)}
}
*/} }) interface InfoDisplayProps { pos: DocumentPosition, status: InfoStatus, messages: InteractiveDiagnostic[], goals?: InteractiveGoals, termGoal?: InteractiveTermGoal, error?: string, userWidgets: UserWidgetInstance[], rpcSess: RpcSessionAtPos, triggerUpdate: () => Promise, } /** Displays goal state and messages. Can be paused. */ function InfoDisplay(props0: ProofStateProps & InfoDisplayProps & InfoPinnable) { // Used to update the paused state *just once* if it is paused, // but a display update is triggered const [shouldRefresh, setShouldRefresh] = React.useState(false) const [{ isPaused, setPaused }, props, propsRef] = usePausableState(false, props0) if (shouldRefresh) { propsRef.current = props0 setShouldRefresh(false) } const triggerDisplayUpdate = async () => { await props0.triggerUpdate() setShouldRefresh(true) } const {kind, goals, rpcSess} = props const ec = React.useContext(EditorContext) // If we are the cursor infoview, then we should subscribe to // some commands from the editor extension const isCursor = kind === 'cursor' useEvent(ec.events.requestedAction, act => { if (!isCursor) return if (act.kind !== 'copyToComment') return if (goals) void ec.copyToComment(goalsToString(goals)) }, [goals]) useEvent(ec.events.requestedAction, act => { if (!isCursor) return if (act.kind !== 'togglePaused') return setPaused(isPaused => !isPaused) }) const editor = React.useContext(MonacoEditorContext) return ( {/*
*/} {/* */}
{/*
*/}
) } /** * 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 * goal states reset to `undefined` on cursor moves. */ export type InfoProps = InfoPinnable & { pos?: DocumentPosition } /** Fetches info from the server and renders an {@link InfoDisplay}. */ export function Info(props: InfoProps) { if (props.kind === 'cursor') return else return } function InfoAtCursor(props: InfoProps) { const ec = React.useContext(EditorContext) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const [curLoc, setCurLoc] = React.useState(ec.events.changedCursorLocation.current!) useEvent(ec.events.changedCursorLocation, loc => loc && setCurLoc(loc), []) const pos = { uri: curLoc.uri, ...curLoc.range.start } return } function useIsProcessingAt(p: DocumentPosition): boolean { const allProgress = React.useContext(ProgressContext) const processing = allProgress.get(p.uri) if (!processing) return false return processing.some(i => RangeHelpers.contains(i.range, p)) } function InfoAux(props: InfoProps) { const proofContext = React.useContext(ProofContext) const config = React.useContext(ConfigContext) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const pos = props.pos! const rpcSess = useRpcSessionAtPos(pos) // 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. const lspDiags = React.useContext(LspDiagnosticsContext) const [lspDiagsHere, setLspDiagsHere] = React.useState([]) React.useEffect(() => { // Note: the curly braces are important. https://medium.com/geekculture/react-uncaught-typeerror-destroy-is-not-a-function-192738a6e79b setLspDiagsHere(diags0 => { const diagPred = (d: Diagnostic) => RangeHelpers.contains(d.range, pos, config.allErrorsOnLine) const newDiags = (lspDiags.get(pos.uri) || []).filter(diagPred) if (newDiags.length === diags0.length && newDiags.every((d, i) => d === diags0[i])) return diags0 return newDiags }) }, [lspDiags, pos.uri, pos.line, pos.character, config.allErrorsOnLine]) const serverIsProcessing = useIsProcessingAt(pos) // 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 // `useAsyncWithTrigger` below, causing the `useEffect` lower down in this component to // make the request. We cannot simply call `triggerUpdateCore` because `useAsyncWithTrigger` // does not support reentrancy like that. const [updaterTick, setUpdaterTick] = React.useState(0) // 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 // that the displayed state cannot ever get out of sync by showing an old reply together // with e.g. a new `pos`. type InfoRequestResult = Omit const [state, triggerUpdateCore] = useAsyncWithTrigger(() => new Promise((resolve, reject) => { const goalsReq = rpcSess.call('Game.getInteractiveGoals', DocumentPosition.toTdpp(pos)) const termGoalReq = getInteractiveTermGoal(rpcSess, DocumentPosition.toTdpp(pos)) const widgetsReq = Widget_getWidgets(rpcSess, pos).catch(discardMethodNotFound) const messagesReq = getInteractiveDiagnostics(rpcSess, {start: pos.line, end: pos.line+1}) // fall back to non-interactive diagnostics when lake fails // (see https://github.com/leanprover/vscode-lean4/issues/90) .then(diags => diags.length === 0 ? lspDiagsHere.map(lspDiagToInteractive) : diags) // While `lake print-paths` is running, the output of Lake is shown as // info diagnostics on line 1. However, all RPC requests block until // 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 // server does not respond within half a second. if (pos.line === 0 && lspDiagsHere.length) { setTimeout(() => resolve({ pos, status: 'updating', messages: lspDiagsHere.map(lspDiagToInteractive), goals: undefined, termGoal: undefined, error: undefined, userWidgets: [], rpcSess }), 500) } // NB: it is important to await await reqs at once, otherwise // if both throw then one exception becomes unhandled. Promise.all([goalsReq, termGoalReq, widgetsReq, messagesReq]).then( ([goals, termGoal, userWidgets, messages]) => resolve({ pos, status: 'ready', messages, goals: goals as any, termGoal, error: undefined, userWidgets: userWidgets?.widgets ?? [], rpcSess }), ex => { if (ex?.code === RpcErrorCode.ContentModified || ex?.code === RpcErrorCode.RpcNeedsReconnect) { // Document has been changed since we made the request, or we need to reconnect // to the RPC sessions. Try again. setUpdaterTick(t => t + 1) reject('retry') } let errorString = '' if (typeof ex === 'string') { errorString = ex } else if (isRpcError(ex)) { errorString = mapRpcError(ex).message } else if (ex instanceof Error) { errorString = ex.toString() } else { errorString = `Unrecognized error: ${JSON.stringify(ex)}` } resolve({ pos, status: 'error', messages: lspDiagsHere.map(lspDiagToInteractive), goals: undefined, termGoal: undefined, error: `Error fetching goals: ${errorString}`, userWidgets: [], rpcSess }) } ) }), [updaterTick, pos.uri, pos.line, pos.character, rpcSess, serverIsProcessing, lspDiagsHere]) // 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 // and schedule just the new one. const updaterTimeout = React.useRef() const clearUpdaterTimeout = () => { if (updaterTimeout.current) { window.clearTimeout(updaterTimeout.current) updaterTimeout.current = undefined } } const triggerUpdate = React.useCallback(() => new Promise(resolve => { clearUpdaterTimeout() const tm = window.setTimeout(() => { void triggerUpdateCore().then(resolve) updaterTimeout.current = undefined }, config.debounceTime) // 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. window.setTimeout(resolve, config.debounceTime) updaterTimeout.current = tm }), [triggerUpdateCore, config.debounceTime]) const [displayProps, setDisplayProps] = React.useState({ pos, status: 'updating', messages: [], goals: undefined, termGoal: undefined, error: undefined, userWidgets: [], rpcSess, triggerUpdate }) // Propagates changes in the state of async info requests to the display props, // and re-requests info if needed. // This effect triggers new requests for info whenever need. It also propagates changes // in the state of the `useAsyncWithTrigger` to the displayed props. React.useEffect(() => { if (state.state === 'notStarted') void triggerUpdate() else if (state.state === 'loading') { setDisplayProps(dp => ({ ...dp, status: 'updating' })) } else if (state.state === 'resolved') { // if (state.value.goals?.goals?.length) { // hintContext.setHints(state.value.goals.goals[0].hints) // } setDisplayProps({ ...state.value, triggerUpdate }) } else if (state.state === 'rejected' && state.error !== 'retry') { // The code inside `useAsyncWithTrigger` may only ever reject with a `retry` exception. console.warn('Unreachable code reached with error: ', state.error) } }, [state]) return }