diff --git a/src/Lean/Data/Lsp/Communication.lean b/src/Lean/Data/Lsp/Communication.lean
index 5f803e7563..5cd50af621 100644
--- a/src/Lean/Data/Lsp/Communication.lean
+++ b/src/Lean/Data/Lsp/Communication.lean
@@ -19,12 +19,12 @@ private def parseHeaderField (s : String) : Option (String × String) :=
if s = "" ∨ s.takeRight 2 ≠ "\r\n" then
none
else
- let xs := (s.dropRight 2).splitOn ": ";
+ let xs := (s.dropRight 2).splitOn ": "
match xs with
| [] => none
| [_] => none
| name :: value :: rest =>
- let value := ": ".intercalate (value :: rest);
+ let value := ": ".intercalate (value :: rest)
some ⟨name, value⟩
private partial def readHeaderFields (h : FS.Stream) : IO (List (String × String)) := do
@@ -47,36 +47,38 @@ private def readLspHeader (h : FS.Stream) : IO Nat := do
| none => throw (userError ("Content-Length header value '" ++ length ++ "' is not a Nat"))
| none => throw (userError ("no Content-Length header in header fields: " ++ toString fields))
-def readLspMessage (h : FS.Stream) : IO Message := do
+variables {m : Type → Type} [Monad m] [MonadIO m]
+
+def readLspMessage (h : FS.Stream) : m Message := do
let nBytes ← readLspHeader h
h.readMessage nBytes
-def readLspRequestAs (h : FS.Stream) (expectedMethod : String) (α : Type) [FromJson α] : IO (Request α) := do
+def readLspRequestAs (h : FS.Stream) (expectedMethod : String) (α : Type) [FromJson α] : m (Request α) := do
let nBytes ← readLspHeader h
h.readRequestAs nBytes expectedMethod α
-def readLspNotificationAs (h : FS.Stream) (expectedMethod : String) (α : Type) [FromJson α] : IO α := do
+def readLspNotificationAs (h : FS.Stream) (expectedMethod : String) (α : Type) [FromJson α] : m α := do
let nBytes ← readLspHeader h
h.readNotificationAs nBytes expectedMethod α
-def writeLspMessage (h : FS.Stream) (m : Message) : IO Unit := do
+def writeLspMessage (h : FS.Stream) (msg : Message) : m Unit := do
-- inlined implementation instead of using jsonrpc's writeMessage
-- to maintain the atomicity of putStr
- let j := (toJson m).compress
+ let j := (toJson msg).compress
let header := "Content-Length: " ++ toString j.utf8ByteSize ++ "\r\n\r\n"
h.putStr (header ++ j)
h.flush
-def writeLspRequest {α : Type} [ToJson α] (h : FS.Stream) (id : RequestID) (method : String) (params : α) : IO Unit :=
+def writeLspRequest {α : Type} [ToJson α] (h : FS.Stream) (id : RequestID) (method : String) (params : α) : m Unit :=
writeLspMessage h (Message.request id method (fromJson? (toJson params)))
-def writeLspNotification {α : Type} [ToJson α] (h : FS.Stream) (method : String) (r : α) : IO Unit :=
+def writeLspNotification {α : Type} [ToJson α] (h : FS.Stream) (method : String) (r : α) : m Unit :=
writeLspMessage h (Message.notification method (fromJson? (toJson r)))
-def writeLspResponse {α : Type} [ToJson α] (h : FS.Stream) (id : RequestID) (r : α) : IO Unit :=
+def writeLspResponse {α : Type} [ToJson α] (h : FS.Stream) (id : RequestID) (r : α) : m Unit :=
writeLspMessage h (Message.response id (toJson r))
-def writeLspResponseError {α : Type} [ToJson α] (h : FS.Stream) (id : RequestID) (code : ErrorCode) (message : String) (data : α) : IO Unit :=
+def writeLspResponseError {α : Type} [ToJson α] (h : FS.Stream) (id : RequestID) (code : ErrorCode) (message : String) (data : α) : m Unit :=
writeLspMessage h (Message.responseError id code message (toJson data))
end Lsp
diff --git a/src/Lean/Server/FileWorker.lean b/src/Lean/Server/FileWorker.lean
index 048170339f..eda90fadd9 100644
--- a/src/Lean/Server/FileWorker.lean
+++ b/src/Lean/Server/FileWorker.lean
@@ -9,6 +9,7 @@ import Std.Data.RBMap
import Lean.Environment
import Lean.Server.Snapshots
+import Lean.Server.Utils
import Lean.Data.Lsp
import Lean.Data.Json.FromToJson
@@ -187,25 +188,11 @@ fun st => st.docRef.get
def updatePendingRequests (map : Array (Task (Except IO.Error Unit)) → Array (Task (Except IO.Error Unit))) : ServerM Unit :=
fun st => st.pendingRequestsRef.modify map
-def readLspMessage : ServerM JsonRpc.Message :=
-fun st => monadLift $ readLspMessage st.hIn
-
-def readLspRequestAs (expectedMethod : String) (α : Type*) [HasFromJson α] : ServerM (Request α) :=
-fun st => monadLift $ readLspRequestAs st.hIn expectedMethod α
-
-def readLspNotificationAs (expectedMethod : String) (α : Type*) [HasFromJson α] : ServerM α :=
-fun st => monadLift $ readLspNotificationAs st.hIn expectedMethod α
-
-def writeLspNotification {α : Type*} [HasToJson α] (method : String) (params : α) : ServerM Unit :=
-fun st => monadLift $ writeLspNotification st.hOut method params
-
-def writeLspResponse {α : Type*} [HasToJson α] (id : RequestID) (params : α) : ServerM Unit :=
-fun st => monadLift $ writeLspResponse st.hOut id params
-
/-- Clears diagnostics for the document version 'version'. -/
-- TODO(WN): how to clear all diagnostics? Sending version 'none' doesn't seem to work
def clearDiagnostics (uri : DocumentUri) (version : Nat) : ServerM Unit :=
-writeLspNotification "textDocument/publishDiagnostics"
+fun st =>
+writeLspNotification st.hOut "textDocument/publishDiagnostics"
{ uri := uri,
version? := version,
diagnostics := #[] : PublishDiagnosticsParams }
@@ -225,13 +212,6 @@ let text := doc.text.toFileMap;
newDoc ← compileDocument h doc.uri doc.version text;
pure newDoc
-private def replaceLspRange (text : FileMap) (r : Lsp.Range) (newText : String) : FileMap :=
-let start := text.lspPosToUtf8Pos r.start;
-let «end» := text.lspPosToUtf8Pos r.«end»;
-let pre := text.source.extract 0 start;
-let post := text.source.extract «end» text.source.bsize;
-(pre ++ newText ++ post).toFileMap
-
def handleDidChange (p : DidChangeTextDocumentParams) : ServerM Unit := do
let docId := p.textDocument;
let changes := p.contentChanges;
@@ -292,8 +272,9 @@ match method with
partial def mainLoop : Unit → ServerM Unit
| () => do
+ st ← read;
-- TODO(MH): gracefully terminate when stdin is closed by watchdog?
- msg ← readLspMessage;
+ msg ← readLspMessage st.hIn;
-- TODO(MH): updatePendingRequests ...: get rid of all requests that are finished
match msg with
| Message.request id method (some params) => do
@@ -304,9 +285,9 @@ partial def mainLoop : Unit → ServerM Unit
mainLoop ()
| _ => throw (userError "got invalid JSON-RPC message")
-def initAndRunServer (i o : FS.Stream) : IO Unit := do
--- ignore InitializeParams for MWE
-initRequest ← Lsp.readLspRequestAs i "initialize" InitializeParams;
+def initAndRunWorker (i o : FS.Stream) : IO Unit := do
+-- TODO(WN): act in accordance with InitializeParams
+_ ← Lsp.readLspRequestAs i "initialize" InitializeParams;
docRequest ← Lsp.readLspRequestAs i "textDocument/didOpen" DidOpenTextDocumentParams;
doc ← openDocument o docRequest.param;
docRef ← IO.mkRef doc;
@@ -315,3 +296,8 @@ runReader (mainLoop ()) (⟨i, o, docRef, pendingRequestsRef⟩ : ServerContext)
end Server
end Lean
+
+def main : IO Unit := do
+hIn ← IO.getStdin;
+hOut ← IO.getStdout;
+Lean.Server.initAndRunWorker hIn hOut
diff --git a/src/Lean/Server/Utils.lean b/src/Lean/Server/Utils.lean
new file mode 100644
index 0000000000..90ce53015d
--- /dev/null
+++ b/src/Lean/Server/Utils.lean
@@ -0,0 +1,15 @@
+import Lean.Data.Position
+import Lean.Data.Lsp
+
+namespace Lean
+namespace Server
+
+def replaceLspRange (text : FileMap) (r : Lsp.Range) (newText : String) : FileMap :=
+let start := text.lspPosToUtf8Pos r.start;
+let «end» := text.lspPosToUtf8Pos r.«end»;
+let pre := text.source.extract 0 start;
+let post := text.source.extract «end» text.source.bsize;
+(pre ++ newText ++ post).toFileMap
+
+end Server
+end Lean
diff --git a/src/Lean/Server/Watchdog.lean b/src/Lean/Server/Watchdog.lean
index 93c0adb925..883841490b 100644
--- a/src/Lean/Server/Watchdog.lean
+++ b/src/Lean/Server/Watchdog.lean
@@ -11,6 +11,7 @@ import Lean.Elab.Import
import Lean.Data.Lsp
import Lean.Server.HasFileSource
+import Lean.Server.Utils
/-!
For general server architecture, see `README.md`. This module implements the watchdog process.
@@ -63,21 +64,7 @@ open Std (RBMap RBMap.empty)
open Lsp
open JsonRpc
-private def replaceLspRange (text : FileMap) (r : Lsp.Range) (newText : String) : FileMap :=
-let start := text.lspPosToUtf8Pos r.start;
-let «end» := text.lspPosToUtf8Pos r.«end»;
-let pre := text.source.extract 0 start;
-let post := text.source.extract «end» text.source.bsize;
-(pre ++ newText ++ post).toFileMap
-
-private def parsedImportsEndPos (input : String) : IO String.Pos := do
-env ← mkEmptyEnvironment;
-let fileName := "";
-let inputCtx := Parser.mkInputContext input fileName;
-let (_, parserState, _) := Parser.parseHeader env inputCtx;
-pure parserState.pos
-
-structure EditableDocument :=
+structure OpenDocument :=
(version : Nat)
(text : FileMap)
(headerEndPos : String.Pos)
@@ -85,17 +72,11 @@ structure EditableDocument :=
def workerCfg : Process.StdioConfig := ⟨Process.Stdio.piped, Process.Stdio.piped, Process.Stdio.piped⟩
structure FileWorker :=
-(doc : EditableDocument)
+(doc : OpenDocument)
(proc : Process.Child workerCfg)
namespace FileWorker
-def spawnArgs : Process.SpawnArgs := {workerCfg with cmd := "fileworker"}
-
-def spawn (doc : EditableDocument) : IO FileWorker := do
-proc ← Process.spawn spawnArgs;
-pure ⟨doc, proc⟩
-
def stdin (fw : FileWorker) : FS.Stream :=
FS.Stream.ofHandle fw.proc.stdin
@@ -105,7 +86,7 @@ FS.Stream.ofHandle fw.proc.stdout
def stderr (fw : FileWorker) : FS.Stream :=
FS.Stream.ofHandle fw.proc.stderr
-def wait (w : FileWorker) : IO Nat := pure 0
+def wait (fw : FileWorker) : IO Nat := pure 0 -- TODO
end FileWorker
@@ -114,69 +95,26 @@ abbrev FileWorkerMap := RBMap DocumentUri FileWorker (fun a b => Decidable.decid
structure ServerContext :=
(hIn hOut : FS.Stream)
(fileWorkersRef : IO.Ref FileWorkerMap)
+/- We store these to pass them to workers. -/
(initParams : InitializeParams)
+(workerPath : String)
abbrev ServerM := ReaderT ServerContext IO
-def updateFileWorkers (key : DocumentUri) (val : FileWorker) : ServerM Unit :=
-fun st => st.fileWorkersRef.modify (fun fileWorkers => fileWorkers.insert key val)
+def spawnFileWorker (doc : OpenDocument) : ServerM FileWorker :=
+fun st => do
+ proc ← Process.spawn {workerCfg with cmd := st.workerPath};
+ pure ⟨doc, proc⟩
-def findFileWorker (key : DocumentUri) : ServerM FileWorker :=
+def updateFileWorkers (uri : DocumentUri) (val : FileWorker) : ServerM Unit :=
+fun st => st.fileWorkersRef.modify (fun fileWorkers => fileWorkers.insert uri val)
+
+def findFileWorker (uri : DocumentUri) : ServerM FileWorker :=
fun st => do
fileWorkers ← st.fileWorkersRef.get;
-match fileWorkers.find? key with
+match fileWorkers.find? uri with
| some fw => pure fw
-| none => throw (userError $ "got unknown document URI (" ++ key ++ ")")
-
-def readWorkerLspMessage (key : DocumentUri) : ServerM JsonRpc.Message :=
-findFileWorker key >>= fun fw => monadLift $ readLspMessage fw.stdout
-
-def readUserLspMessage : ServerM JsonRpc.Message :=
-fun st => monadLift $ readLspMessage st.hIn
-
-def readWorkerLspRequestAs (key : DocumentUri) (expectedMethod : String) (α : Type*) [HasFromJson α] : ServerM (Request α) :=
-findFileWorker key >>= fun fw => monadLift $ readLspRequestAs fw.stdout expectedMethod α
-
-def readUserLspRequestAs (expectedMethod : String) (α : Type*) [HasFromJson α] : ServerM (Request α) :=
-fun st => monadLift $ readLspRequestAs st.hIn expectedMethod α
-
-def readWorkerLspNotificationAs (key : DocumentUri) (expectedMethod : String) (α : Type*) [HasFromJson α] : ServerM α :=
-findFileWorker key >>= fun fw => monadLift $ readLspNotificationAs fw.stdout expectedMethod α
-
-def readUserLspNotificationAs (expectedMethod : String) (α : Type*) [HasFromJson α] : ServerM α :=
-fun st => monadLift $ readLspNotificationAs st.hIn expectedMethod α
-
-def writeWorkerLspMessage (key : DocumentUri) (msg : JsonRpc.Message) : ServerM Unit :=
-findFileWorker key >>= fun fw => monadLift $ writeLspMessage fw.stdin msg
-
-def writeUserLspMessage (msg : JsonRpc.Message) : ServerM Unit :=
-fun st => monadLift $ writeLspMessage st.hOut msg
-
-def writeWorkerLspRequest {α : Type*} [HasToJson α] (key : DocumentUri) (id : RequestID) (method : String) (params : α) : ServerM Unit :=
-findFileWorker key >>= fun fw => monadLift $ writeLspRequest fw.stdin id method params
-
-def writeUserLspRequest {α : Type*} [HasToJson α] (id : RequestID) (method : String) (params : α) : ServerM Unit :=
-fun st => monadLift $ writeLspRequest st.hOut id method params
-
-def writeWorkerLspNotification {α : Type*} [HasToJson α] (key : DocumentUri) (method : String) (params : α) : ServerM Unit :=
-findFileWorker key >>= fun fw => monadLift $ writeLspNotification fw.stdin method params
-
-def writeUserLspNotification {α : Type*} [HasToJson α] (method : String) (params : α) : ServerM Unit :=
-fun st => monadLift $ writeLspNotification st.hOut method params
-
-def writeWorkerLspResponse {α : Type*} [HasToJson α] (key : DocumentUri) (id : RequestID) (params : α) : ServerM Unit :=
-findFileWorker key >>= fun fw => monadLift $ writeLspResponse fw.stdin id params
-
-def writeUserLspResponse {α : Type*} [HasToJson α] (id : RequestID) (params : α) : ServerM Unit :=
-fun st => monadLift $ writeLspResponse st.hOut id params
-
-def writeWorkerInitializeParams (key : DocumentUri) : ServerM Unit := do
-st ← read;
-writeWorkerLspRequest key (0 : Nat) "initialize" st.initParams
-
-def writeWorkerDidOpenNotification (key : DocumentUri) : ServerM Unit := do
-findFileWorker key >>= fun fw => writeWorkerLspNotification key "textDocument/didOpen"
- (DidOpenTextDocumentParams.mk ⟨key, "lean", fw.doc.version, fw.doc.text.source⟩)
+| none => throw (userError $ "got unknown document URI (" ++ uri ++ ")")
def parseParams (paramType : Type*) [HasFromJson paramType] (params : Json) : ServerM paramType :=
match fromJson? params with
@@ -190,17 +128,24 @@ partial def forwardFileWorkerPackets (fw : FileWorker) : Unit → ServerM Unit
| ⟨⟩ => do
-- TODO(MH): detect closed stream somehow and terminate gracefully
-- TODO(MH): potentially catch unintended termination (e.g. due to stack overflow) and restart process
- msg ← monadLift $ readLspMessage fw.stdout;
- -- NOTE: Writes to Lean I/O streams are atomic, so we don't need to synchronise these in principle.
- writeUserLspMessage msg;
+ msg ← readLspMessage fw.stdout;
+ writeLspMessage fw.stdin msg;
forwardFileWorkerPackets ⟨⟩
-def startFileWorker (key : DocumentUri) (version : Nat) (text : FileMap) : ServerM Unit := do
+private def parsedImportsEndPos (input : String) : IO String.Pos := do
+emptyEnv ← mkEmptyEnvironment; -- TODO(WN): a lot of things could be purified if `mkEmptyEnvironment` was just a pure ctr
+let inputCtx := Parser.mkInputContext input "";
+let (_, parserState, _) := Parser.parseHeader emptyEnv inputCtx;
+pure parserState.pos
+
+def startFileWorker (uri : DocumentUri) (version : Nat) (text : FileMap) : ServerM Unit := do
+st ← read;
pos ← monadLift $ parsedImportsEndPos text.source;
-fw ← monadLift $ FileWorker.spawn ⟨version, text, pos⟩;
-updateFileWorkers key fw;
-writeWorkerInitializeParams key;
-writeWorkerDidOpenNotification key;
+fw ← spawnFileWorker ⟨version, text, pos⟩;
+writeLspRequest fw.stdin (0 : Nat) "initialize" st.initParams;
+writeLspNotification fw.stdin "textDocument/didOpen"
+ (DidOpenTextDocumentParams.mk ⟨uri, "lean", fw.doc.version, fw.doc.text.source⟩);
+updateFileWorkers uri fw;
-- TODO(MH): replace with working IO variant
-- TODO(MH): Sebastian said something about this better being implemented as threads
-- (due to the long running nature of these tasks) but i did not yet have time to
@@ -242,9 +187,9 @@ else changes.forM $ fun change =>
terminateFileWorker fw;
startFileWorker doc.uri newVersion newDocText
else
- let newDoc : EditableDocument := ⟨newVersion, newDocText, oldHeaderEndPos⟩;
+ let newDoc : OpenDocument := ⟨newVersion, newDocText, oldHeaderEndPos⟩;
updateFileWorkers doc.uri { fw with doc := newDoc };
- writeWorkerLspNotification doc.uri "textDocument/didChange" p
+ writeLspNotification fw.stdin "textDocument/didChange" p
| TextDocumentContentChangeEvent.fullChange (text : String) =>
throw (userError "TODO impl computing the diff of two sources.")
@@ -258,7 +203,8 @@ st.fileWorkersRef.modify (fun fileWorkers => fileWorkers.erase doc.uri)
def handleRequest (id : RequestID) (method : String) (params : Json) : ServerM Unit := do
let h := (fun α [HasFromJson α] [HasToJson α] [HasFileSource α] => do
parsedParams ← parseParams α params;
- writeWorkerLspRequest (fileSource parsedParams) id method parsedParams);
+ fw ← findFileWorker $ fileSource parsedParams;
+ writeLspRequest fw.stdin id method parsedParams);
match method with
| "textDocument/hover" => h HoverParams
| _ => throw (userError $ "got unsupported request: " ++ method ++
@@ -267,7 +213,8 @@ match method with
def handleNotification (method : String) (params : Json) : ServerM Unit := do
let forward := (fun α [HasFromJson α] [HasToJson α] [HasFileSource α] => do
parsedParams ← parseParams α params;
- writeWorkerLspNotification (fileSource parsedParams) method parsedParams);
+ fw ← findFileWorker $ fileSource parsedParams;
+ writeLspNotification fw.stdin method parsedParams);
let handle := (fun α [HasFromJson α] (handler : α → ServerM Unit) => parseParams α params >>= handler);
match method with
| "textDocument/didOpen" => handle DidOpenTextDocumentParams handleDidOpen
@@ -278,10 +225,11 @@ match method with
partial def mainLoop : Unit → ServerM Unit
| () => do
- msg ← readUserLspMessage;
+ st ← read;
+ msg ← readLspMessage st.hIn;
match msg with
| Message.request id "shutdown" _ =>
- writeUserLspResponse id (Json.null)
+ writeLspResponse st.hOut id (Json.null)
| Message.request id method (some params) => do
handleRequest id method (toJson params);
mainLoop ()
@@ -299,22 +247,26 @@ def mkLeanServerCapabilities : ServerCapabilities :=
save? := none },
hoverProvider := true }
+def initAndRunServerAux : ServerM Unit := do
+st ← read;
+_ ← readLspNotificationAs st.hIn "initialized" InitializedParams;
+mainLoop ();
+Message.notification "exit" none ← readLspMessage st.hIn
+ | throw (userError "Expected an Exit Notification.");
+pure ()
+
def initAndRunServer (i o : FS.Stream) : IO Unit := do
+some workerPath ← IO.getEnv "LEAN_WORKER_PATH"
+ | throw $ userError "You need to specify LEAN_WORKER_PATH in the environment.";
fileWorkersRef ← IO.mkRef (RBMap.empty : FileWorkerMap);
--- ignore InitializeParams for MWE
initRequest ← readLspRequestAs i "initialize" InitializeParams;
+writeLspResponse o initRequest.id
+ { capabilities := mkLeanServerCapabilities,
+ serverInfo? := some { name := "Lean 4 server",
+ version? := "0.0.1" } : InitializeResult };
runReader
- (do
- writeUserLspResponse initRequest.id
- { capabilities := mkLeanServerCapabilities,
- serverInfo? := some { name := "Lean 4 server",
- version? := "0.0.1" } : InitializeResult };
- _ ← readUserLspNotificationAs "initialized" InitializedParams;
- mainLoop ();
- Message.notification "exit" none ← readUserLspMessage
- | throw (userError "Expected an Exit Notification.");
- pure ())
- (⟨i, o, fileWorkersRef, initRequest.param⟩ : ServerContext)
+ initAndRunServerAux
+ (⟨i, o, fileWorkersRef, initRequest.param, workerPath⟩ : ServerContext)
end Server
end Lean