diff --git a/client/src/components/Level.tsx b/client/src/components/Level.tsx index 0159d28..90dd3ef 100644 --- a/client/src/components/Level.tsx +++ b/client/src/components/Level.tsx @@ -22,9 +22,9 @@ import './level.css' import { Button } from './Button' import { ConnectionContext, useLeanClient } from '../connection'; import { useGetGameInfoQuery, useLoadLevelQuery } from '../state/api'; -import { codeEdited, selectCode } from '../state/progress'; -import { useAppDispatch } from '../hooks'; -import { useSelector } from 'react-redux'; +import { codeEdited, selectCode, progressSlice } from '../state/progress'; +import { useAppDispatch, useAppSelector } from '../hooks'; +import { useStore } 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'; @@ -63,7 +63,7 @@ function Level() { const codeviewRef = useRef(null) const introductionPanelRef = useRef(null) - const initialCode = useSelector(selectCode(worldId, levelId)) + const initialCode = useAppSelector(selectCode(worldId, levelId)) const [commandLineMode, setCommandLineMode] = useState(true) const [commandLineInput, setCommandLineInput] = useState("") @@ -114,6 +114,8 @@ function Level() { const gameInfo = useGetGameInfoQuery() + useLoadWorldFiles(worldId) + const level = useLoadLevelQuery({world: worldId, level: levelId}) const dispatch = useAppDispatch() @@ -277,8 +279,8 @@ function useLevelEditor(worldId: string, levelId: number, codeviewRef, initialCo let model = monaco.editor.getModel(uri) if (!model) { model = monaco.editor.createModel(initialCode, 'lean4', uri) - model.onDidChangeContent(() => onDidChangeContent(model.getValue())) } + model.onDidChangeContent(() => onDidChangeContent(model.getValue())) editor.setModel(model) editor.setPosition(model.getFullModelRange().getEndPosition()) @@ -288,9 +290,32 @@ function useLevelEditor(worldId: string, levelId: number, codeviewRef, initialCo const taskGutter = new LeanTaskGutter(infoProvider.client, editor) const abbrevRewriter = new AbbreviationRewriter(new AbbreviationProvider(), model, editor) - return () => { abbrevRewriter.dispose(); taskGutter.dispose(); model.dispose() } + return () => { abbrevRewriter.dispose(); taskGutter.dispose(); } } }, [editor, levelId, connection, leanClientStarted]) return {editor, infoProvider, editorConnection} } + +/** Open all files in this world on the server so that they will load faster when accessed */ +function useLoadWorldFiles(worldId) { + const gameInfo = useGetGameInfoQuery() + const store = useStore() + + useEffect(() => { + if (gameInfo.data) { + const models = [] + for (let levelId = 1; levelId <= gameInfo.data.worldSize[worldId]; levelId++) { + const uri = monaco.Uri.parse(`file:///${worldId}/${levelId}`) + let model = monaco.editor.getModel(uri) + if (model) { + models.push(model) + } else { + const code = selectCode(worldId, levelId)(store.getState()) + models.push(monaco.editor.createModel(code, 'lean4', uri)) + } + } + return () => { for (let model of models) { model.dispose() } } + } + }, [gameInfo.data, worldId]) +} diff --git a/package-lock.json b/package-lock.json index e4f50b9..26c2ec9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "cytoscape-klay": "^3.1.4", "debounce": "^1.2.1", "express": "^4.18.2", - "lean4-infoview": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?master", + "lean4-infoview": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?de0062c", "lean4web": "github:hhu-adam/lean4web", "path-browserify": "^1.0.1", "react": "^18.2.0", @@ -6048,8 +6048,8 @@ "node_modules/lean4-infoview": { "name": "@leanprover/infoview", "version": "0.4.2", - "resolved": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?master", - "integrity": "sha512-XthMPhgRGoP/3lClX5LEQNXQYmzEcIQQBuK6AZ4RC5NKcTU7XUpA0qtZTQcWkPeZbOqz02FX5Dg8f4P1CDMuGw==", + "resolved": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?de0062c", + "integrity": "sha512-xrKjl6/+Myn0UeBmEgkba6LStBfjb4iIC7fSJ3tAJxG0CrsKBmBCeU/sfjah3RNw7ieetLeFGd6YvxtoI4nPhQ==", "license": "Apache-2.0", "dependencies": { "@leanprover/infoview-api": "~0.2.1", diff --git a/package.json b/package.json index 18ca722..9a7bd2f 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "cytoscape-klay": "^3.1.4", "debounce": "^1.2.1", "express": "^4.18.2", - "lean4-infoview": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?master", + "lean4-infoview": "https://gitpkg.now.sh/leanprover/vscode-lean4/lean4-infoview?de0062c", "lean4web": "github:hhu-adam/lean4web", "path-browserify": "^1.0.1", "react": "^18.2.0", diff --git a/server/index.mjs b/server/index.mjs index 30a91de..9c0c394 100644 --- a/server/index.mjs +++ b/server/index.mjs @@ -1,7 +1,7 @@ import { WebSocketServer } from 'ws'; import express from 'express' import path from 'path' -import { spawn } from 'child_process'; +import * as cp from 'child_process'; import * as url from 'url'; import * as rpc from 'vscode-ws-jsonrpc'; import * as jsonrpcserver from 'vscode-ws-jsonrpc/server'; @@ -34,7 +34,34 @@ if (isDevelopment) { cwd = "." } +/** We keep a queue of started Lean Server processes to be ready when a user arrives */ +const queue = [] +const queueLength = 5 + +/** start Lean Server processes to refill the queue */ +function fillQueue() { + while (queue.length < queueLength) { + const serverProcess = cp.spawn(cmd, cmdArgs, { cwd }) + serverProcess.on('error', error => + console.error(`Launching Lean Server failed: ${error}`) + ); + if (serverProcess.stderr !== null) { + serverProcess.stderr.on('data', data => + console.error(`Lean Server: ${data}`) + ); + } + + queue.push(serverProcess) + } +} + +fillQueue() + wss.addListener("connection", function(ws) { + + const ps = queue.shift() // Pick the first Lean process; it's likely to be ready immediately + fillQueue() + const socket = { onMessage: (cb) => { ws.on("message", cb) }, onError: (cb) => { ws.on("error", cb) }, @@ -44,7 +71,7 @@ wss.addListener("connection", function(ws) { const reader = new rpc.WebSocketMessageReader(socket); const writer = new rpc.WebSocketMessageWriter(socket); const socketConnection = jsonrpcserver.createConnection(reader, writer, () => ws.close()) - const serverConnection = jsonrpcserver.createServerProcess('Lean Server', cmd, cmdArgs, { cwd }); + const serverConnection = jsonrpcserver.createProcessStreamConnection(ps); socketConnection.forward(serverConnection, message => { console.log(`CLIENT: ${JSON.stringify(message)}`) return message; diff --git a/server/leanserver/GameServer/Commands.lean b/server/leanserver/GameServer/Commands.lean index 9068e41..8ca11b4 100644 --- a/server/leanserver/GameServer/Commands.lean +++ b/server/leanserver/GameServer/Commands.lean @@ -79,9 +79,12 @@ elab "Statement" statementName:ident ? descr:str sig:declSig val:declVal : comma -- | none => `(lemma $(mkIdent "XX") $sig $val) -- TODO: Make it into an `example` -- | some name => `(lemma $name $sig $val) + let scope ← getScope + elabCommand thmStatement modifyCurLevel fun level => pure {level with goal := sig, + scope := scope, descrText := descr.getString, descrFormat := match statementName with | none => "example " ++ (toString <| reprint sig.raw) ++ " := by" @@ -187,17 +190,28 @@ elab "TacticDoc" name:ident content:str : command => instance : Quote TacticDocEntry `term := ⟨λ entry => Syntax.mkCApp ``TacticDocEntry.mk #[quote entry.name, quote entry.content]⟩ +/-- Throw an error if tactic doc does not exist -/ +def checkTacticDoc (name : Name) : CommandElabM Unit := do + let some _ := (tacticDocExt.getState (← getEnv)).find? (·.name = name) + | throwError "Missing Tactic Documentation: {name}" + /-- Declare tactics that are introduced by this level. -/ elab "NewTactics" args:ident* : command => do - modifyCurLevel fun level => pure {level with newTactics := args.map (·.getId)} + let names := args.map (·.getId) + for name in names do checkTacticDoc name + modifyCurLevel fun level => pure {level with newTactics := names} /-- Declare tactics that are temporarily disabled in this level -/ elab "DisabledTactics" args:ident* : command => do - modifyCurLevel fun level => pure {level with disabledTactics := args.map (·.getId)} + let names := args.map (·.getId) + for name in names do checkTacticDoc name + modifyCurLevel fun level => pure {level with disabledTactics := names} /-- Temporarily disable all tactics except the ones declared here -/ elab "OnlyTactics" args:ident* : command => do - modifyCurLevel fun level => pure {level with onlyTactics := args.map (·.getId)} + let names := args.map (·.getId) + for name in names do checkTacticDoc name + modifyCurLevel fun level => pure {level with onlyTactics := names} /-! ## Lemmas -/ diff --git a/server/leanserver/GameServer/EnvExtensions.lean b/server/leanserver/GameServer/EnvExtensions.lean index 9c1f13d..7c5f57f 100644 --- a/server/leanserver/GameServer/EnvExtensions.lean +++ b/server/leanserver/GameServer/EnvExtensions.lean @@ -161,9 +161,10 @@ deriving ToJson, FromJson, Repr def getCurLevelId [MonadError m] : m LevelId := do return { game := ← getCurGameId, world := ← getCurWorldId, level := ← getCurLevelIdx} -/-- - +/-- Instance to make GameLevel Repr work -/ +instance : Repr Elab.Command.Scope := ⟨fun s _ => repr s.currNamespace⟩ +/-- Fields: - TODO - introduction: Theory block shown all the time. @@ -195,6 +196,7 @@ structure GameLevel where lemmas: Array Availability := default hints: Array GoalHintEntry := default goal : TSyntax `Lean.Parser.Command.declSig := default + scope : Elab.Command.Scope := default descrText: String := default descrFormat : String := default deriving Inhabited, Repr diff --git a/server/leanserver/GameServer/FileWorker.lean b/server/leanserver/GameServer/FileWorker.lean index 75e5166..a82ea56 100644 --- a/server/leanserver/GameServer/FileWorker.lean +++ b/server/leanserver/GameServer/FileWorker.lean @@ -77,13 +77,24 @@ partial def findForbiddenTactics (inputCtx : Parser.InputContext) (level : GameL | .atom info val => -- ignore syntax elements that do not start with a letter -- and ignore "with" keyword - if 0 < val.length ∧ val.data[0]!.isAlpha ∧ val != "with" then + if 0 < val.length ∧ val.data[0]!.isAlpha ∧ val != "with" ∧ val != "at" then if ¬ ((level.tactics.map (·.name.toString))).contains val then addErrorMessage info s!"You have not unlocked the tactic '{val}' yet!" else if level.disabledTactics.contains val ∨ (¬ level.onlyTactics.isEmpty ∧ ¬ level.onlyTactics.contains val)then addErrorMessage info s!"The tactic '{val}' is disabled in this level!" - | .ident info rawVal val preresolved => return () + | .ident info rawVal val preresolved => + let ns ← + try resolveGlobalConst (mkIdent val) + catch | _ => pure [] -- catch "unknown constant" error + for n in ns do + let some (.thmInfo ..) := (← getEnv).find? n + | return () -- not a theroem -> ignore + if ¬ ((level.lemmas.map (·.name))).contains n then + addErrorMessage info s!"You have not unlocked the lemma '{n}' yet!" + else if level.disabledLemmas.contains n + ∨ (¬ level.onlyLemmas.isEmpty ∧ ¬ level.onlyLemmas.contains n)then + addErrorMessage info s!"The lemma '{n}' is disabled in this level!" where addErrorMessage (info : SourceInfo) (s : MessageData) := modify fun st => { st with messages := st.messages.add { @@ -139,10 +150,10 @@ def compileProof (inputCtx : Parser.InputContext) (snap : Snapshot) (hasWidgets let tacticStx := (#[skip] ++ tacticStx.getArgs ++ #[done]).map (⟨.⟩) let tacticStx := ← `(Lean.Parser.Tactic.tacticSeq| $[$(tacticStx)]*) - let cmdStx ← `(command| - set_option tactic.hygienic false in - theorem the_theorem $(level.goal) := by {$(⟨tacticStx⟩)} ) - Elab.Command.elabCommandTopLevel cmdStx) + let cmdStx ← `(command| theorem the_theorem $(level.goal) := by {$(⟨tacticStx⟩)} ) + + Elab.Command.withScope (fun _ => level.scope) do -- use open namespaces and options as in the file + Elab.Command.elabCommandTopLevel cmdStx) cmdCtx cmdStateRef let postNew := (← tacticCacheNew.get).post snap.tacticCache.modify fun _ => { pre := postNew, post := {} } diff --git a/server/leanserver/GameServer/Watchdog.lean b/server/leanserver/GameServer/Watchdog.lean index bc0e09d..8c6fa10 100644 --- a/server/leanserver/GameServer/Watchdog.lean +++ b/server/leanserver/GameServer/Watchdog.lean @@ -102,6 +102,7 @@ def initAndRunWatchdog (args : List String) (i o e : FS.Stream) : IO Unit := do let i ← maybeTee "wdIn.txt" false i let o ← maybeTee "wdOut.txt" true o let e ← maybeTee "wdErr.txt" true e + let state := {env := ← createEnv, game := `TestGame} let initRequest ← i.readLspRequestAs "initialize" InitializeParams o.writeLspResponse { id := initRequest.id @@ -114,7 +115,6 @@ def initAndRunWatchdog (args : List String) (i o e : FS.Stream) : IO Unit := do : InitializeResult } } - let state := {env := ← createEnv, game := `TestGame} let context : ServerContext := { hIn := i hOut := o diff --git a/server/testgame/TestGame/Levels/Predicate/L02_Rewrite.lean b/server/testgame/TestGame/Levels/Predicate/L02_Rewrite.lean index 9e8f068..defa2fa 100644 --- a/server/testgame/TestGame/Levels/Predicate/L02_Rewrite.lean +++ b/server/testgame/TestGame/Levels/Predicate/L02_Rewrite.lean @@ -54,7 +54,7 @@ HiddenHint (a : ℕ) (b : ℕ) (c : ℕ) (d : ℕ) (h₁ : c = d) (h₂ : a = b) "Auch eine Möglichkeit.. Kannst du das Goal so umschreiben, dass es mit einer Annahme übereinstimmt?" Hint (a : ℕ) (b : ℕ) (h : b = a) : a = b => -"Naja auch Umwege führen ans Ziel... Wenn du das Goal zu `a = A` umschreibst, kann man es mit +"Naja auch Umwege führen ans Ziel... Wenn du das Goal zu `a = a` umschreibst, kann man es mit `rfl` beweisen (rsp. das passiert automatisch.)" Hint (a : ℕ) (b : ℕ) (c : ℕ) (d : ℕ) (h₁ : c = d) (h₂ : d = b) (h₃ : d = a) : b = c =>