From f2a31d2baa928e3abcd827971b50dbeeb637f5e4 Mon Sep 17 00:00:00 2001 From: Alexander Bentkamp Date: Tue, 24 Jan 2023 10:35:31 +0100 Subject: [PATCH 1/7] fix css --- client/src/components/level.css | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/level.css b/client/src/components/level.css index 4c5a069..8fed3d6 100644 --- a/client/src/components/level.css +++ b/client/src/components/level.css @@ -2,6 +2,7 @@ height: 100%; flex: 1; min-height: 0; + display: flex; } .main-panel, .info-panel { From 9527bee77edbe9d3b80164a857d5cae106151c2c Mon Sep 17 00:00:00 2001 From: Alexander Bentkamp Date: Tue, 24 Jan 2023 10:40:10 +0100 Subject: [PATCH 2/7] rename message-panel to introduction-panel --- client/src/components/Level.tsx | 6 +++--- client/src/components/level.css | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/src/components/Level.tsx b/client/src/components/Level.tsx index 468acd0..aa3cf78 100644 --- a/client/src/components/Level.tsx +++ b/client/src/components/Level.tsx @@ -105,7 +105,7 @@ function Level() { const codeviewRef = useRef(null) const infoviewRef = useRef(null) - const messagePanelRef = useRef(null) + const introductionPanelRef = useRef(null) const [showSidePanel, setShowSidePanel] = useState(true) @@ -117,7 +117,7 @@ function Level() { useEffect(() => { // Scroll to top when loading a new level - messagePanelRef.current!.scrollTo(0,0) + introductionPanelRef.current!.scrollTo(0,0) }, [levelId]) const connection = React.useContext(ConnectionContext) @@ -165,7 +165,7 @@ function Level() { -
+
{level?.data?.introduction}
diff --git a/client/src/components/level.css b/client/src/components/level.css index 8fed3d6..5d70d46 100644 --- a/client/src/components/level.css +++ b/client/src/components/level.css @@ -15,7 +15,7 @@ flex-flow: column; } -.message-panel { +.introduction-panel { width: 100%; } @@ -56,12 +56,12 @@ mjx-container[jax="CHTML"][display="true"] { /* Styling tables for Markdown */ -.message-panel table, .message-panel th, .message-panel td { +.introduction-panel table, .introduction-panel th, .introduction-panel td { /* border: 1px solid rgb(0, 0, 0, 0.12); */ border-collapse: collapse; } -.message-panel th, .message-panel td { +.introduction-panel th, .introduction-panel td { padding-left: .5em; padding-right: .5em; } @@ -97,7 +97,7 @@ td code { border: 1px solid rgb(230, 122, 0); } -.message-panel { +.introduction-panel { border: 1px solid rgb(192, 18, 178); } From 45bdc22600ba4ca0307259c314b365d65fc6a1e8 Mon Sep 17 00:00:00 2001 From: Alexander Bentkamp Date: Tue, 24 Jan 2023 10:50:51 +0100 Subject: [PATCH 3/7] show all messages, independent of cursor position --- client/src/components/infoview/info.tsx | 10 +++++----- client/src/components/infoview/main.tsx | 6 +++--- client/src/components/infoview/messages.tsx | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/client/src/components/infoview/info.tsx b/client/src/components/infoview/info.tsx index b344be4..9745245 100644 --- a/client/src/components/infoview/info.tsx +++ b/client/src/components/infoview/info.tsx @@ -128,14 +128,14 @@ const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => { selectedLocations={selectedLocs} widget={widget}/> )} -
- {/*
- Messages ({messages.length}) */} + {/*
+
+ Messages ({messages.length})
- {/*
*/} -
+
+
*/} {nothingToShow && ( isPaused ? /* Adding {' '} to manage string literals properly: https://reactjs.org/docs/jsx-in-depth.html#string-literals-1 */ diff --git a/client/src/components/infoview/main.tsx b/client/src/components/infoview/main.tsx index 892d584..0623b3f 100644 --- a/client/src/components/infoview/main.tsx +++ b/client/src/components/infoview/main.tsx @@ -13,7 +13,7 @@ import './infoview.css' import { LeanFileProgressParams, LeanFileProgressProcessingInfo, defaultInfoviewConfig, EditorApi, InfoviewApi } from '@leanprover/infoview-api'; import { Infos } from './infos'; -import { AllMessages, WithLspDiagnosticsContext } from '../../../../node_modules/lean4-infoview/src/infoview/messages'; +import { AllMessages, WithLspDiagnosticsContext } from './messages'; import { useClientNotificationEffect, useEventResult, useServerNotificationState } from '../../../../node_modules/lean4-infoview/src/infoview/util'; import { EditorContext, ConfigContext, ProgressContext, VersionContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts'; import { WithRpcSessions } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions'; @@ -65,9 +65,9 @@ function Main(props: {}) { } else { ret =
- {/* {curUri &&
+ {curUri &&
-
} */} +
}
} diff --git a/client/src/components/infoview/messages.tsx b/client/src/components/infoview/messages.tsx index f64b495..12bdaed 100644 --- a/client/src/components/infoview/messages.tsx +++ b/client/src/components/infoview/messages.tsx @@ -130,7 +130,7 @@ export function AllMessages({uri: uri0}: { uri: DocumentUri }) { return ( -
+ {/*
All Messages ({diags.length}) @@ -139,9 +139,9 @@ export function AllMessages({uri: uri0}: { uri: DocumentUri }) { title={isPaused ? 'continue updating' : 'pause updating'}> - + */} -
+ {/*
*/}
) } From 32bacf8b7ce2c6202b24d4fb4bbed8201b831e8f Mon Sep 17 00:00:00 2001 From: Alexander Bentkamp Date: Tue, 24 Jan 2023 11:03:02 +0100 Subject: [PATCH 4/7] move renderInfoview function --- client/src/components/Level.tsx | 58 +++++++++++++++++++++-- client/src/components/infoview/main.tsx | 61 +------------------------ 2 files changed, 56 insertions(+), 63 deletions(-) diff --git a/client/src/components/Level.tsx b/client/src/components/Level.tsx index aa3cf78..35e02f1 100644 --- a/client/src/components/Level.tsx +++ b/client/src/components/Level.tsx @@ -5,7 +5,7 @@ import '@fontsource/roboto/400.css'; import '@fontsource/roboto/500.css'; import '@fontsource/roboto/700.css'; import { InfoviewApi } from '@leanprover/infoview' -import { renderInfoview } from './infoview/main' +import * as ReactDOM from 'react-dom/client'; import { Link as RouterLink } from 'react-router-dom'; import { Box, Button, CircularProgress, FormControlLabel, FormGroup, Switch, IconButton } from '@mui/material'; import MuiDrawer from '@mui/material/Drawer'; @@ -28,6 +28,12 @@ import { codeEdited, selectCode } from '../state/progress'; import { useAppDispatch } from '../hooks'; import { useSelector } from 'react-redux'; +import { EditorContext, ConfigContext, ProgressContext, VersionContext } from '../../../node_modules/lean4-infoview/src/infoview/contexts'; +import { EditorConnection, EditorEvents } from '../../../node_modules/lean4-infoview/src/infoview/editorConnection'; +import { EventEmitter } from '../../../node_modules/lean4-infoview/src/infoview/event'; +import { Main } from './infoview/main' +import type { Location } from 'vscode-languageserver-protocol'; + import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faUpload, faArrowRotateRight, faChevronLeft, faChevronRight, faBook, faHome, faArrowRight, faArrowLeft } from '@fortawesome/free-solid-svg-icons' @@ -228,8 +234,54 @@ function useLevelEditor(worldId: string, levelId: number, codeviewRef, infoviewR }) const infoProvider = new InfoProvider(connection.getLeanClient()) - const div: HTMLElement = infoviewRef.current! - const infoviewApi = renderInfoview(infoProvider.getApi(), div) + const uiElement: HTMLElement = infoviewRef.current! + + const editorApi = infoProvider.getApi() + + const editorEvents: EditorEvents = { + initialize: new EventEmitter(), + gotServerNotification: new EventEmitter(), + sentClientNotification: new EventEmitter(), + serverRestarted: new EventEmitter(), + serverStopped: new EventEmitter(), + changedCursorLocation: new EventEmitter(), + changedInfoviewConfig: new EventEmitter(), + runTestScript: new EventEmitter(), + requestedAction: new EventEmitter(), + }; + + // Challenge: write a type-correct fn from `Eventify` to `T` without using `any` + const infoviewApi: InfoviewApi = { + initialize: async l => editorEvents.initialize.fire(l), + gotServerNotification: async (method, params) => { + editorEvents.gotServerNotification.fire([method, params]); + }, + sentClientNotification: async (method, params) => { + editorEvents.sentClientNotification.fire([method, params]); + }, + serverRestarted: async r => editorEvents.serverRestarted.fire(r), + serverStopped: async serverStoppedReason => { + editorEvents.serverStopped.fire(serverStoppedReason) + }, + changedCursorLocation: async loc => editorEvents.changedCursorLocation.fire(loc), + changedInfoviewConfig: async conf => editorEvents.changedInfoviewConfig.fire(conf), + requestedAction: async action => editorEvents.requestedAction.fire(action), + // See https://rollupjs.org/guide/en/#avoiding-eval + // eslint-disable-next-line @typescript-eslint/no-implied-eval + runTestScript: async script => new Function(script)(), + getInfoviewHtml: async () => document.body.innerHTML, + }; + + const ec = new EditorConnection(editorApi, editorEvents); + + editorEvents.initialize.on((loc: Location) => ec.events.changedCursorLocation.fire(loc)) + + const root = ReactDOM.createRoot(uiElement) + root.render( + +
+ + ) setEditor(editor) setInfoProvider(infoProvider) diff --git a/client/src/components/infoview/main.tsx b/client/src/components/infoview/main.tsx index 0623b3f..4728820 100644 --- a/client/src/components/infoview/main.tsx +++ b/client/src/components/infoview/main.tsx @@ -1,7 +1,6 @@ /* Partly copied from https://github.com/leanprover/vscode-lean4/blob/master/lean4-infoview/src/infoview/main.tsx */ import * as React from 'react'; -import * as ReactDOM from 'react-dom/client'; import type { DidCloseTextDocumentParams, Location, DocumentUri } from 'vscode-languageserver-protocol'; import 'tachyons/css/tachyons.css'; @@ -17,12 +16,10 @@ import { AllMessages, WithLspDiagnosticsContext } from './messages'; import { useClientNotificationEffect, useEventResult, useServerNotificationState } from '../../../../node_modules/lean4-infoview/src/infoview/util'; import { EditorContext, ConfigContext, ProgressContext, VersionContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts'; import { WithRpcSessions } from '../../../../node_modules/lean4-infoview/src/infoview/rpcSessions'; -import { EditorConnection, EditorEvents } from '../../../../node_modules/lean4-infoview/src/infoview/editorConnection'; -import { EventEmitter } from '../../../../node_modules/lean4-infoview/src/infoview/event'; import { ServerVersion } from '../../../../node_modules/lean4-infoview/src/infoview/serverVersion'; -function Main(props: {}) { +export function Main(props: {}) { const ec = React.useContext(EditorContext); /* Set up updates to the global infoview state on editor events. */ @@ -85,59 +82,3 @@ function Main(props: {}) { ); } - -/** - * Render the Lean infoview into the DOM element `uiElement`. - * - * @param editorApi is a collection of methods which the infoview needs to be able to invoke - * on the editor in order to function correctly (such as inserting text or moving the cursor). - * @returns a collection of methods which must be invoked when the relevant editor events occur. - */ -export function renderInfoview(editorApi: EditorApi, uiElement: HTMLElement): InfoviewApi { - const editorEvents: EditorEvents = { - initialize: new EventEmitter(), - gotServerNotification: new EventEmitter(), - sentClientNotification: new EventEmitter(), - serverRestarted: new EventEmitter(), - serverStopped: new EventEmitter(), - changedCursorLocation: new EventEmitter(), - changedInfoviewConfig: new EventEmitter(), - runTestScript: new EventEmitter(), - requestedAction: new EventEmitter(), - }; - - // Challenge: write a type-correct fn from `Eventify` to `T` without using `any` - const infoviewApi: InfoviewApi = { - initialize: async l => editorEvents.initialize.fire(l), - gotServerNotification: async (method, params) => { - editorEvents.gotServerNotification.fire([method, params]); - }, - sentClientNotification: async (method, params) => { - editorEvents.sentClientNotification.fire([method, params]); - }, - serverRestarted: async r => editorEvents.serverRestarted.fire(r), - serverStopped: async serverStoppedReason => { - editorEvents.serverStopped.fire(serverStoppedReason) - }, - changedCursorLocation: async loc => editorEvents.changedCursorLocation.fire(loc), - changedInfoviewConfig: async conf => editorEvents.changedInfoviewConfig.fire(conf), - requestedAction: async action => editorEvents.requestedAction.fire(action), - // See https://rollupjs.org/guide/en/#avoiding-eval - // eslint-disable-next-line @typescript-eslint/no-implied-eval - runTestScript: async script => new Function(script)(), - getInfoviewHtml: async () => document.body.innerHTML, - }; - - const ec = new EditorConnection(editorApi, editorEvents); - - editorEvents.initialize.on((loc: Location) => ec.events.changedCursorLocation.fire(loc)) - - const root = ReactDOM.createRoot(uiElement) - root.render( - -
- - ) - - return infoviewApi; -} From 1aba4162e4668b49cac954ac21984f5da85e3f79 Mon Sep 17 00:00:00 2001 From: Alexander Bentkamp Date: Tue, 24 Jan 2023 11:22:43 +0100 Subject: [PATCH 5/7] integrate infoview into react component tree --- client/src/components/Level.tsx | 26 ++++++++++--------------- client/src/components/infoview/main.tsx | 2 +- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/client/src/components/Level.tsx b/client/src/components/Level.tsx index 35e02f1..43939c6 100644 --- a/client/src/components/Level.tsx +++ b/client/src/components/Level.tsx @@ -5,7 +5,6 @@ import '@fontsource/roboto/400.css'; import '@fontsource/roboto/500.css'; import '@fontsource/roboto/700.css'; import { InfoviewApi } from '@leanprover/infoview' -import * as ReactDOM from 'react-dom/client'; import { Link as RouterLink } from 'react-router-dom'; import { Box, Button, CircularProgress, FormControlLabel, FormGroup, Switch, IconButton } from '@mui/material'; import MuiDrawer from '@mui/material/Drawer'; @@ -110,7 +109,6 @@ function Level() { const worldId = params.worldId const codeviewRef = useRef(null) - const infoviewRef = useRef(null) const introductionPanelRef = useRef(null) const [showSidePanel, setShowSidePanel] = useState(true) @@ -140,8 +138,8 @@ function Level() { const initialCode = useSelector(selectCode(worldId, levelId)) - const {editor, infoProvider} = - useLevelEditor(worldId, levelId, codeviewRef, infoviewRef, initialCode, onDidChangeContent) + const {editor, infoProvider, editorConnection} = + useLevelEditor(worldId, levelId, codeviewRef, initialCode, onDidChangeContent) const {setTitle, setSubtitle} = React.useContext(SetTitleContext); @@ -195,8 +193,10 @@ function Level() { component={RouterLink} to={`/`} sx={{ ml: 3, mt: 2, mb: 2 }} disableFocusRipple> -
- {/* */} + + + {editorConnection ?
: null} +
@@ -206,13 +206,14 @@ function Level() { export default Level -function useLevelEditor(worldId: string, levelId: number, codeviewRef, infoviewRef, initialCode, onDidChangeContent) { +function useLevelEditor(worldId: string, levelId: number, codeviewRef, initialCode, onDidChangeContent) { const connection = React.useContext(ConnectionContext) const [editor, setEditor] = useState(null) const [infoProvider, setInfoProvider] = useState(null) const [infoviewApi, setInfoviewApi] = useState(null) + const [editorConnection, setEditorConnection] = useState(null) // Create Editor useEffect(() => { @@ -234,7 +235,6 @@ function useLevelEditor(worldId: string, levelId: number, codeviewRef, infoviewR }) const infoProvider = new InfoProvider(connection.getLeanClient()) - const uiElement: HTMLElement = infoviewRef.current! const editorApi = infoProvider.getApi() @@ -273,16 +273,10 @@ function useLevelEditor(worldId: string, levelId: number, codeviewRef, infoviewR }; const ec = new EditorConnection(editorApi, editorEvents); + setEditorConnection(ec) editorEvents.initialize.on((loc: Location) => ec.events.changedCursorLocation.fire(loc)) - const root = ReactDOM.createRoot(uiElement) - root.render( - -
- - ) - setEditor(editor) setInfoProvider(infoProvider) setInfoviewApi(infoviewApi) @@ -315,5 +309,5 @@ function useLevelEditor(worldId: string, levelId: number, codeviewRef, infoviewR } }, [editor, levelId, connection, leanClientStarted]) - return {editor, infoProvider} + return {editor, infoProvider, editorConnection} } diff --git a/client/src/components/infoview/main.tsx b/client/src/components/infoview/main.tsx index 4728820..7c4db9f 100644 --- a/client/src/components/infoview/main.tsx +++ b/client/src/components/infoview/main.tsx @@ -60,7 +60,7 @@ export function Main(props: {}) { } else if (serverStoppedResult){ ret =

{serverStoppedResult.message}

{serverStoppedResult.reason}

} else { - ret =
+ ret =
{curUri &&
From 8175d32a3c70385f41967b74fa7123d18119a5c6 Mon Sep 17 00:00:00 2001 From: Alexander Bentkamp Date: Tue, 24 Jan 2023 12:15:47 +0100 Subject: [PATCH 6/7] custom getInteractiveGoals RPC --- client/src/components/infoview/info.tsx | 4 +- server/leanserver/GameServer/RpcHandlers.lean | 42 ++++++++++++------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/client/src/components/infoview/info.tsx b/client/src/components/infoview/info.tsx index 9745245..3bed742 100644 --- a/client/src/components/infoview/info.tsx +++ b/client/src/components/infoview/info.tsx @@ -271,7 +271,7 @@ function InfoAux(props: InfoProps) { // with e.g. a new `pos`. type InfoRequestResult = Omit const [state, triggerUpdateCore] = useAsyncWithTrigger(() => new Promise((resolve, reject) => { - const goalsReq = getInteractiveGoals(rpcSess, DocumentPosition.toTdpp(pos)); + const goalsReq = rpcSess.call('Game.getInteractiveGoals', DocumentPosition.toTdpp(pos)); const termGoalReq = getInteractiveTermGoal(rpcSess, DocumentPosition.toTdpp(pos)) const widgetsReq = Widget_getWidgets(rpcSess, pos).catch(discardMethodNotFound) const messagesReq = getInteractiveDiagnostics(rpcSess, {start: pos.line, end: pos.line+1}) @@ -304,7 +304,7 @@ function InfoAux(props: InfoProps) { pos, status: 'ready', messages, - goals, + goals: goals as any, termGoal, error: undefined, userWidgets: userWidgets?.widgets ?? [], diff --git a/server/leanserver/GameServer/RpcHandlers.lean b/server/leanserver/GameServer/RpcHandlers.lean index a3e2f9f..1ce3777 100644 --- a/server/leanserver/GameServer/RpcHandlers.lean +++ b/server/leanserver/GameServer/RpcHandlers.lean @@ -117,11 +117,9 @@ def findMessages (goal : MVarId) (doc : FileWorker.EditableDocument) (hLog : IO. else return none return messages - -/-- Get goals and messages at a given position -/ -def getGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Option PlainGoal)) := do +open RequestM in +def getInteractiveGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Option Widget.InteractiveGoals)) := do let doc ← readDoc - let hLog := (← read).hLog let text := doc.meta.text let hoverPos := text.lspPosToUtf8Pos p.position -- TODO: I couldn't find a good condition to find the correct snap. So we are looking @@ -129,24 +127,38 @@ def getGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Option PlainGoal withWaitFindSnap doc (fun s => ¬ (s.infoTree.goalsAt? doc.meta.text hoverPos).isEmpty) (notFoundX := return none) fun snap => do if let rs@(_ :: _) := snap.infoTree.goalsAt? doc.meta.text hoverPos then - let goals ← rs.mapM fun { ctxInfo := ci, tacticInfo := ti, useAfter := useAfter, .. } => do - let ci := if useAfter then { ci with mctx := ti.mctxAfter } else { ci with mctx := ti.mctxBefore } - let goals := List.toArray <| if useAfter then ti.goalsAfter else ti.goalsBefore - let goals ← ci.runMetaM {} $ goals.mapM fun goal => do - let messages ← findMessages goal doc hLog - return ← goal.toGameGoal messages + let goals : List Widget.InteractiveGoals ← rs.mapM fun { ctxInfo := ci, tacticInfo := ti, useAfter := useAfter, .. } => do + let ciAfter := { ci with mctx := ti.mctxAfter } + let ci := if useAfter then ciAfter else { ci with mctx := ti.mctxBefore } + -- compute the interactive goals + let goals ← ci.runMetaM {} (do + let goals := List.toArray <| if useAfter then ti.goalsAfter else ti.goalsBefore + let goals ← goals.mapM Widget.goalToInteractive + return {goals} + ) + -- compute the goal diff + let goals ← ciAfter.runMetaM {} (do + try + Widget.diffInteractiveGoals useAfter ti goals + catch _ => + -- fail silently, since this is just a bonus feature + return goals + ) + -- TODO: add hints + -- let goals ← ci.runMetaM {} $ goals.mapM fun goal => do + -- let messages ← findMessages goal doc hLog + -- return ← goal.toGameGoal messages return goals - return some { goals := goals.foldl (· ++ ·) ∅ } + return some <| goals.foldl (· ++ ·) ∅ else return none builtin_initialize registerBuiltinRpcProcedure - `Game.getGoals + `Game.getInteractiveGoals Lsp.PlainGoalParams - (Option PlainGoal) - getGoals - + (Option InteractiveGoals) + getInteractiveGoals structure Diagnostic where severity : Option Lean.Lsp.DiagnosticSeverity From b7cc5aaf57a8b3de6c8f532dfe45c6b52df5d539 Mon Sep 17 00:00:00 2001 From: Alexander Bentkamp Date: Tue, 24 Jan 2023 14:09:34 +0100 Subject: [PATCH 7/7] show hints --- client/src/components/infoview/goals.tsx | 48 +++++++++------- client/src/components/infoview/info.tsx | 9 +-- client/src/components/infoview/rpcApi.ts | 15 +++++ server/leanserver/GameServer/RpcHandlers.lean | 56 +++++++++++-------- 4 files changed, 80 insertions(+), 48 deletions(-) create mode 100644 client/src/components/infoview/rpcApi.ts diff --git a/client/src/components/infoview/goals.tsx b/client/src/components/infoview/goals.tsx index d40fbb1..1a290bf 100644 --- a/client/src/components/infoview/goals.tsx +++ b/client/src/components/infoview/goals.tsx @@ -6,20 +6,21 @@ import { InteractiveGoal, InteractiveGoals, InteractiveHypothesisBundle, Interac import { WithTooltipOnHover } from '../../../../node_modules/lean4-infoview/src/infoview/tooltips'; import { EditorContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts'; import { Locations, LocationsContext, SelectableLocation } from '../../../../node_modules/lean4-infoview/src/infoview/goalLocation'; +import { GameInteractiveGoal, GameInteractiveGoals } from './rpcApi'; /** Returns true if `h` is inaccessible according to Lean's default name rendering. */ function isInaccessibleName(h: string): boolean { return h.indexOf('✝') >= 0; } -function goalToString(g: InteractiveGoal): string { +function goalToString(g: GameInteractiveGoal): string { let ret = '' - if (g.userName) { - ret += `case ${g.userName}\n` + if (g.goal.userName) { + ret += `case ${g.goal.userName}\n` } - for (const h of g.hyps) { + for (const h of g.goal.hyps) { const names = InteractiveHypothesisBundle_nonAnonymousNames(h).join(' ') ret += `${names} : ${TaggedText_stripTags(h.type)}` if (h.val) { @@ -28,12 +29,12 @@ function goalToString(g: InteractiveGoal): string { ret += '\n' } - ret += `⊢ ${TaggedText_stripTags(g.type)}` + ret += `⊢ ${TaggedText_stripTags(g.goal.type)}` return ret } -export function goalsToString(goals: InteractiveGoals): string { +export function goalsToString(goals: GameInteractiveGoals): string { return goals.goals.map(goalToString).join('\n\n') } @@ -111,7 +112,7 @@ function Hyp({ hyp: h, mvarId }: HypProps) { } interface GoalProps { - goal: InteractiveGoal + goal: GameInteractiveGoal filter: GoalFilterState } @@ -121,44 +122,49 @@ interface GoalProps { export const Goal = React.memo((props: GoalProps) => { const { goal, filter } = props - const prefix = goal.goalPrefix ?? 'Prove: ' - const filteredList = getFilteredHypotheses(goal.hyps, filter); + const prefix = goal.goal.goalPrefix ?? 'Prove: ' + const filteredList = getFilteredHypotheses(goal.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: '' }}} : + locs && goal.goal.mvarId ? + { ...locs, subexprTemplate: { mvarId: goal.goal.mvarId, loc: { target: '' }}} : undefined, - [locs, goal.mvarId]) + [locs, goal.goal.mvarId]) const goalLi =
Prove: - +
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 ' + if (props.goal.goal.isInserted) cn += 'b--inserted ' + if (props.goal.goal.isRemoved) cn += 'b--removed ' - if (goal.userName) { + // TODO: make this prettier + const hints = goal.messages.map((m) =>
{m.message}
) + + if (goal.goal.userName) { return
- case {goal.userName} + case {goal.goal.userName} {filter.reverse && goalLi} - {hyps.map((h, i) => )} + {hyps.map((h, i) => )} {!filter.reverse && goalLi} + {hints}
} else return
{filter.reverse && goalLi} - {hyps.map((h, i) => )} + {hyps.map((h, i) => )} {!filter.reverse && goalLi} + {hints}
}) interface GoalsProps { - goals: InteractiveGoals + goals: GameInteractiveGoals filter: GoalFilterState } @@ -179,7 +185,7 @@ interface FilteredGoalsProps { * 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 * settings and collapsed state will be as before. */ - goals?: InteractiveGoals + goals?: GameInteractiveGoals } /** diff --git a/client/src/components/infoview/info.tsx b/client/src/components/infoview/info.tsx index 3bed742..8899f82 100644 --- a/client/src/components/infoview/info.tsx +++ b/client/src/components/infoview/info.tsx @@ -11,6 +11,7 @@ import { lspDiagToInteractive, MessagesList } from './messages'; import { getInteractiveGoals, getInteractiveTermGoal, InteractiveDiagnostic, InteractiveGoals, UserWidgetInstance, Widget_getWidgets, RpcSessionAtPos, isRpcError, RpcErrorCode, getInteractiveDiagnostics, InteractiveTermGoal } from '@leanprover/infoview-api'; +import { GameInteractiveGoal, GameInteractiveGoals } from './rpcApi'; 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'; @@ -76,7 +77,7 @@ const InfoStatusBar = React.memo((props: InfoStatusBarProps) => { interface InfoDisplayContentProps extends PausableProps { pos: DocumentPosition; messages: InteractiveDiagnostic[]; - goals?: InteractiveGoals; + goals?: GameInteractiveGoals; termGoal?: InteractiveTermGoal; error?: string; userWidgets: UserWidgetInstance[]; @@ -120,11 +121,11 @@ const InfoDisplayContent = React.memo((props: InfoDisplayContentProps) => { {goals && } + goals={termGoal !== undefined ? {goals: [{goal:termGoal, messages: []}]} : undefined} /> {userWidgets.map(widget =>
{widget.name} - goal.goal) : []} termGoal={termGoal} selectedLocations={selectedLocs} widget={widget}/>
)} @@ -152,7 +153,7 @@ interface InfoDisplayProps { pos: DocumentPosition; status: InfoStatus; messages: InteractiveDiagnostic[]; - goals?: InteractiveGoals; + goals?: GameInteractiveGoals; termGoal?: InteractiveTermGoal; error?: string; userWidgets: UserWidgetInstance[]; diff --git a/client/src/components/infoview/rpcApi.ts b/client/src/components/infoview/rpcApi.ts new file mode 100644 index 0000000..f07cece --- /dev/null +++ b/client/src/components/infoview/rpcApi.ts @@ -0,0 +1,15 @@ +import { InteractiveGoals, InteractiveGoal } from '@leanprover/infoview-api'; + +export interface GameMessage { + message: string; + spoiler: boolean; +} + +export interface GameInteractiveGoal { + goal: InteractiveGoal; + messages: GameMessage[]; +} + +export interface GameInteractiveGoals { + goals: GameInteractiveGoal[]; +} diff --git a/server/leanserver/GameServer/RpcHandlers.lean b/server/leanserver/GameServer/RpcHandlers.lean index 1ce3777..791ef7f 100644 --- a/server/leanserver/GameServer/RpcHandlers.lean +++ b/server/leanserver/GameServer/RpcHandlers.lean @@ -100,7 +100,7 @@ def matchDecls (declMvars : Array Expr) (declFvars : Array Expr) : MetaM Bool := open Meta in /-- Find all messages whose trigger matches the current goal -/ -def findMessages (goal : MVarId) (doc : FileWorker.EditableDocument) (hLog : IO.FS.Stream) : MetaM (Array GameMessage) := do +def findMessages (goal : MVarId) (doc : FileWorker.EditableDocument) : MetaM (Array GameMessage) := do goal.withContext do let level ← getLevelByFileName doc.meta.mkInputContext.fileName let messages ← level.messages.filterMapM fun message => do @@ -109,7 +109,6 @@ def findMessages (goal : MVarId) (doc : FileWorker.EditableDocument) (hLog : IO. if ← isDefEq messageGoal (← inferType $ mkMVar goal) -- TODO: also check assumptions then let lctx ← getLCtx -- Local context of the `goal` - hLog.putStr s!"{← declMvars.mapM inferType} =?= {← lctx.getFVars.mapM inferType}" if ← matchDecls declMvars lctx.getFVars then return some { message := message.message, spoiler := message.spoiler } @@ -117,8 +116,22 @@ def findMessages (goal : MVarId) (doc : FileWorker.EditableDocument) (hLog : IO. else return none return messages +structure GameInteractiveGoal where + goal : InteractiveGoal + messages: Array GameMessage + deriving RpcEncodable + +structure GameInteractiveGoals where + goals : Array GameInteractiveGoal + deriving RpcEncodable + +def GameInteractiveGoals.append (l r : GameInteractiveGoals) : GameInteractiveGoals where + goals := l.goals ++ r.goals + +instance : Append GameInteractiveGoals := ⟨GameInteractiveGoals.append⟩ + open RequestM in -def getInteractiveGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Option Widget.InteractiveGoals)) := do +def getInteractiveGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Option GameInteractiveGoals)) := do let doc ← readDoc let text := doc.meta.text let hoverPos := text.lspPosToUtf8Pos p.position @@ -127,29 +140,26 @@ def getInteractiveGoals (p : Lsp.PlainGoalParams) : RequestM (RequestTask (Optio withWaitFindSnap doc (fun s => ¬ (s.infoTree.goalsAt? doc.meta.text hoverPos).isEmpty) (notFoundX := return none) fun snap => do if let rs@(_ :: _) := snap.infoTree.goalsAt? doc.meta.text hoverPos then - let goals : List Widget.InteractiveGoals ← rs.mapM fun { ctxInfo := ci, tacticInfo := ti, useAfter := useAfter, .. } => do + let goals : List GameInteractiveGoals ← rs.mapM fun { ctxInfo := ci, tacticInfo := ti, useAfter := useAfter, .. } => do let ciAfter := { ci with mctx := ti.mctxAfter } let ci := if useAfter then ciAfter else { ci with mctx := ti.mctxBefore } -- compute the interactive goals - let goals ← ci.runMetaM {} (do - let goals := List.toArray <| if useAfter then ti.goalsAfter else ti.goalsBefore - let goals ← goals.mapM Widget.goalToInteractive - return {goals} - ) + let goals ← ci.runMetaM {} do + return List.toArray <| if useAfter then ti.goalsAfter else ti.goalsBefore + let goals ← ci.runMetaM {} do + goals.mapM fun goal => do + let messages ← findMessages goal doc + return {goal := ← Widget.goalToInteractive goal, messages} -- compute the goal diff - let goals ← ciAfter.runMetaM {} (do - try - Widget.diffInteractiveGoals useAfter ti goals - catch _ => - -- fail silently, since this is just a bonus feature - return goals - ) - -- TODO: add hints - -- let goals ← ci.runMetaM {} $ goals.mapM fun goal => do - -- let messages ← findMessages goal doc hLog - -- return ← goal.toGameGoal messages - return goals - return some <| goals.foldl (· ++ ·) ∅ + -- let goals ← ciAfter.runMetaM {} (do + -- try + -- Widget.diffInteractiveGoals useAfter ti goals + -- catch _ => + -- -- fail silently, since this is just a bonus feature + -- return goals + -- ) + return {goals} + return some <| goals.foldl (· ++ ·) ⟨#[]⟩ else return none @@ -157,7 +167,7 @@ builtin_initialize registerBuiltinRpcProcedure `Game.getInteractiveGoals Lsp.PlainGoalParams - (Option InteractiveGoals) + (Option GameInteractiveGoals) getInteractiveGoals structure Diagnostic where