Merge branch 'main' of github.com:leanprover-community/lean4game
This commit is contained in:
@@ -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<HTMLDivElement>(null)
|
||||
const introductionPanelRef = useRef<HTMLDivElement>(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])
|
||||
}
|
||||
|
||||
Generated
+3
-3
@@ -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",
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
+29
-2
@@ -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;
|
||||
|
||||
@@ -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 -/
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 := {} }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
Reference in New Issue
Block a user