chore: refactor jsonrpc and lsp handle functions and deal with fallout

This commit is contained in:
Marc Huisinga 2020-12-05 17:30:03 +01:00 committed by Sebastian Ullrich
parent 975b344278
commit 6248e6329e
4 changed files with 212 additions and 168 deletions

View file

@ -75,20 +75,38 @@ inductive Message where
def Batch := Array Message
-- data types for reading expected messages
structure Request (α : Type) where
id : RequestID
param : α
-- Compound type with simplified APIs for passing around
-- jsonrpc data
structure Request (α) where
id : RequestID
method : String
param : α
structure Response (α : Type) where
id : RequestID
instance [ToJson α] : Coe (Request α) Message :=
⟨fun r => Message.request r.id r.method (toStructured? r.param)⟩
structure Notification (α) where
method : String
param : α
instance [ToJson α] : Coe (Notification α) Message :=
⟨fun r => Message.notification r.method (toStructured? r.param)⟩
structure Response (α) where
id : RequestID
result : α
structure Error where
id : RequestID
code : JsonNumber
instance [ToJson α] : Coe (Response α) Message :=
⟨fun r => Message.response r.id (toJson r.result)⟩
structure ResponseError (α) where
id : RequestID
code : ErrorCode
message : String
data? : Option Json
data? : Option α := none
instance [ToJson α] : Coe (ResponseError α) Message :=
⟨fun r => Message.responseError r.id r.code r.message (r.data?.map toJson)⟩
instance : Coe String RequestID := ⟨RequestID.str⟩
instance : Coe JsonNumber RequestID := ⟨RequestID.num⟩
@ -168,62 +186,69 @@ namespace IO.FS.Stream
open Lean
open Lean.JsonRpc
open IO
def readMessage (h : FS.Stream) (nBytes : Nat) : IO Message := do
let j ← h.readJson nBytes
match fromJson? j with
| some m => pure m
| none => throw $ userError ("JSON '" ++ j.compress ++ "' did not have the format of a JSON-RPC message")
section
variables (h : FS.Stream) (nBytes : Nat) (expectedMethod : String) (α) [FromJson α]
def readRequestAs (h : FS.Stream) (nBytes : Nat) (expectedMethod : String) (α : Type) [FromJson α] : IO (Request α) := do
let m ← h.readMessage nBytes
match m with
| Message.request id method params? =>
if method = expectedMethod then
match params? with
| some params =>
let j := toJson params
match fromJson? j with
| some v => pure ⟨id, v⟩
| none => throw $ userError ("unexpected param '" ++ j.compress ++ "' for method '" ++ expectedMethod ++ "'")
| none => throw $ userError ("unexpected lack of param for method '" ++ expectedMethod ++ "'")
else
throw $ userError ("expected method '" ++ expectedMethod ++ "', got method '" ++ method ++ "'")
| _ => throw $ userError "expected request, got other type of message"
def readMessage : IO Message := do
let j ← h.readJson nBytes
match fromJson? j with
| some m => pure m
| none => throw $ userError ("JSON '" ++ j.compress ++ "' did not have the format of a JSON-RPC message")
def readNotificationAs (h : FS.Stream) (nBytes : Nat) (expectedMethod : String) (α : Type) [FromJson α] : IO α := do
let m ← h.readMessage nBytes
match m with
| Message.notification method params? =>
if method = expectedMethod then
match params? with
| some params =>
let j := toJson params
match fromJson? j with
| some v => pure v
| none => throw $ userError ("unexpected param '" ++ j.compress ++ "' for method '" ++ expectedMethod ++ "'")
| none => throw $ userError ("unexpected lack of param for method '" ++ expectedMethod ++ "'")
else
throw $ userError ("expected method '" ++ expectedMethod ++ "', got method '" ++ method ++ "'")
| _ => throw $ userError "expected notification, got other type of message"
def readRequestAs : IO (Request α) := do
let m ← h.readMessage nBytes
match m with
| Message.request id method params? =>
if method = expectedMethod then
match params? with
| some params =>
let j := toJson params
match fromJson? j with
| some v => pure ⟨id, expectedMethod, v⟩
| none => throw $ userError ("unexpected param '" ++ j.compress ++ "' for method '" ++ expectedMethod ++ "'")
| none => throw $ userError ("unexpected lack of param for method '" ++ expectedMethod ++ "'")
else
throw $ userError ("expected method '" ++ expectedMethod ++ "', got method '" ++ method ++ "'")
| _ => throw $ userError "expected request, got other type of message"
def writeMessage (h : FS.Stream) (m : Message) : IO Unit :=
h.writeJson (toJson m)
def readNotificationAs : IO (Notification α) := do
let m ← h.readMessage nBytes
match m with
| Message.notification method params? =>
if method = expectedMethod then
match params? with
| some params =>
let j := toJson params
match fromJson? j with
| some v => pure ⟨expectedMethod, v⟩
| none => throw $ userError ("unexpected param '" ++ j.compress ++ "' for method '" ++ expectedMethod ++ "'")
| none => throw $ userError ("unexpected lack of param for method '" ++ expectedMethod ++ "'")
else
throw $ userError ("expected method '" ++ expectedMethod ++ "', got method '" ++ method ++ "'")
| _ => throw $ userError "expected notification, got other type of message"
end
def writeRequest [ToJson α] (h : FS.Stream) (id : RequestID) (method : String) (params : α) : IO Unit :=
h.writeMessage (Message.request id method (fromJson? (toJson params)))
section
variables [ToJson α] (h : FS.Stream)
def writeNotification [ToJson α] (h : FS.Stream) (method : String) (params : α) : IO Unit :=
h.writeMessage (Message.notification method (fromJson? (toJson params)))
def writeMessage (m : Message) : IO Unit :=
h.writeJson (toJson m)
def writeResponse [ToJson α] (h : FS.Stream) (id : RequestID) (r : α) : IO Unit :=
h.writeMessage (Message.response id (toJson r))
def writeRequest (r : Request α) : IO Unit :=
h.writeMessage r
def writeResponseError (h : FS.Stream) (id : RequestID) (code : ErrorCode) (message : String) : IO Unit :=
h.writeMessage (Message.responseError id code message none)
def writeNotification (n : Notification α) : IO Unit :=
h.writeMessage n
def writeResponseErrorWithData [ToJson α] (h : FS.Stream) (id : RequestID) (code : ErrorCode) (message : String) (data : α) : IO Unit :=
h.writeMessage (Message.responseError id code message (toJson data))
def writeResponse (r : Response α) : IO Unit :=
h.writeMessage r
def writeResponseError (e : ResponseError Unit) : IO Unit :=
h.writeMessage (Message.responseError e.id e.code e.message none)
def writeResponseErrorWithData (e : ResponseError α) : IO Unit :=
h.writeMessage e
end
end IO.FS.Stream

View file

@ -9,76 +9,82 @@ import Lean.Data.JsonRpc
/-! Reading/writing LSP messages from/to IO handles. -/
namespace Lean
namespace Lsp
namespace IO.FS.Stream
open IO
open JsonRpc
open Lean
open Lean.JsonRpc
private def parseHeaderField (s : String) : Option (String × String) := do
guard $ s ≠ "" ∧ s.takeRight 2 = "\r\n"
let xs := (s.dropRight 2).splitOn ": "
match xs with
| [] => none
| [_] => none
| name :: value :: rest =>
let value := ": ".intercalate (value :: rest)
some ⟨name, value⟩
section
variables (h : FS.Stream) (expectedMethod : String) (α) [FromJson α]
private partial def readHeaderFields (h : FS.Stream) : IO (List (String × String)) := do
let l ← h.getLine
if l = "\r\n" then
pure []
else
match parseHeaderField l with
| some hf =>
let tail ← readHeaderFields h
pure (hf :: tail)
| none => throw $ userError ("Invalid header field: " ++ l)
private def parseHeaderField (s : String) : Option (String × String) := do
guard $ s ≠ "" ∧ s.takeRight 2 = "\r\n"
let xs := (s.dropRight 2).splitOn ": "
match xs with
| [] => none
| [_] => none
| name :: value :: rest =>
let value := ": ".intercalate (value :: rest)
some ⟨name, value⟩
/-- Returns the Content-Length. -/
private def readLspHeader (h : FS.Stream) : IO Nat := do
let fields ← readHeaderFields h
match fields.lookup "Content-Length" with
| some length => match length.toNat? with
| some n => pure n
| none => throw $ userError ("Content-Length header value '" ++ length ++ "' is not a Nat")
| none => throw $ userError ("No Content-Length header in header fields: " ++ toString fields)
private partial def readHeaderFields : IO (List (String × String)) := do
let l ← h.getLine
if l = "\r\n" then
pure []
else
match parseHeaderField l with
| some hf =>
let tail ← readHeaderFields
pure (hf :: tail)
| none => throw $ userError ("Invalid header field: " ++ l)
def readLspMessage (h : FS.Stream) : IO Message := do
let nBytes ← readLspHeader h
h.readMessage nBytes
/-- Returns the Content-Length. -/
private def readLspHeader : IO Nat := do
let fields ← readHeaderFields h
match fields.lookup "Content-Length" with
| some length => match length.toNat? with
| some n => pure n
| 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 readLspRequestAs (h : FS.Stream) (expectedMethod : String) (α : Type) [FromJson α] : IO (Request α) := do
let nBytes ← readLspHeader h
h.readRequestAs nBytes expectedMethod α
def readLspMessage : IO Message := do
let nBytes ← readLspHeader h
h.readMessage nBytes
def readLspNotificationAs (h : FS.Stream) (expectedMethod : String) (α : Type) [FromJson α] : IO α := do
let nBytes ← readLspHeader h
h.readNotificationAs nBytes expectedMethod α
def readLspRequestAs : IO (Request α) := do
let nBytes ← readLspHeader h
h.readRequestAs nBytes expectedMethod α
def writeLspMessage (h : FS.Stream) (msg : Message) : IO Unit := do
-- inlined implementation instead of using jsonrpc's writeMessage
-- to maintain the atomicity of putStr
let j := (toJson msg).compress
let header := "Content-Length: " ++ toString j.utf8ByteSize ++ "\r\n\r\n"
h.putStr (header ++ j)
h.flush
def readLspNotificationAs : IO (Notification α) := do
let nBytes ← readLspHeader h
h.readNotificationAs nBytes expectedMethod α
end
def writeLspRequest [ToJson α] (h : FS.Stream) (id : RequestID) (method : String) (params : α) : IO Unit :=
writeLspMessage h (Message.request id method (fromJson? (toJson params)))
section
variables [ToJson α] (h : FS.Stream)
def writeLspNotification [ToJson α] (h : FS.Stream) (method : String) (r : α) : IO Unit :=
writeLspMessage h (Message.notification method (fromJson? (toJson r)))
def writeLspMessage (h : FS.Stream) (msg : Message) : IO Unit := do
-- inlined implementation instead of using jsonrpc's writeMessage
-- to maintain the atomicity of putStr
let j := (toJson msg).compress
let header := "Content-Length: " ++ toString j.utf8ByteSize ++ "\r\n\r\n"
h.putStr (header ++ j)
h.flush
def writeLspResponse [ToJson α] (h : FS.Stream) (id : RequestID) (r : α) : IO Unit :=
writeLspMessage h (Message.response id (toJson r))
def writeLspRequest (r : Request α): IO Unit :=
h.writeLspMessage r
def writeLspResponseError (h : FS.Stream) (id : RequestID) (code : ErrorCode) (message : String) : IO Unit :=
writeLspMessage h (Message.responseError id code message none)
def writeLspNotification (n : Notification α) : IO Unit :=
h.writeLspMessage n
def writeLspResponseErrorWithData [ToJson α] (h : FS.Stream) (id : RequestID) (code : ErrorCode) (message : String) (data : α) : IO Unit :=
writeLspMessage h (Message.responseError id code message (toJson data))
def writeLspResponse (r : Response α) : IO Unit :=
h.writeLspMessage r
end Lsp
end Lean
def writeLspResponseError (e : ResponseError Unit) : IO Unit :=
h.writeLspMessage (Message.responseError e.id e.code e.message none)
def writeLspResponseErrorWithData (e : ResponseError α) : IO Unit :=
h.writeLspMessage e
end
end IO.FS.Stream

View file

@ -47,11 +47,15 @@ open Snapshots
private def sendDiagnostics (h : FS.Stream) (uri : DocumentUri) (version : Nat) (text : FileMap) (log : MessageLog)
: IO Unit := do
let diagnostics ← log.msgs.mapM (msgToDiagnostic text)
Lsp.writeLspNotification h "textDocument/publishDiagnostics"
{ uri := uri,
version? := version,
h.writeLspNotification {
method := "textDocument/publishDiagnostics"
param := {
uri := uri
version? := version
diagnostics := diagnostics.toArray
: PublishDiagnosticsParams }
: PublishDiagnosticsParams
}
}
private def logSnapContent (s : Snapshot) (text : FileMap) : IO Unit :=
IO.eprintln s!"[{s.beginPos}, {s.endPos}]: `{text.source.extract s.beginPos (s.endPos-1)}`"
@ -257,7 +261,7 @@ def handleRequest (id : RequestID) (method : String) (params : Json)
partial def mainLoop : ServerM Unit := do
let st ← read
let msg ← readLspMessage st.hIn
let msg ← st.hIn.readLspMessage
let pendingRequests ← st.pendingRequestsRef.get
let filterFinishedTasks : PendingRequestMap → RequestID → Task (Except IO.Error Unit)
→ ServerM PendingRequestMap := fun acc id task => do
@ -283,8 +287,8 @@ def initAndRunWorker (i o e : FS.Stream) : IO Unit := do
let i ← maybeTee "fwIn.txt" false i
let o ← maybeTee "fwOut.txt" true o
-- TODO(WN): act in accordance with InitializeParams
let _ ← Lsp.readLspRequestAs i "initialize" InitializeParams
let param ← Lsp.readLspNotificationAs i "textDocument/didOpen" DidOpenTextDocumentParams
let _ ← i.readLspRequestAs "initialize" InitializeParams
let ⟨_, param⟩ ← i.readLspNotificationAs "textDocument/didOpen" DidOpenTextDocumentParams
let e ← e.withPrefix s!"[{param.textDocument.uri}] "
let _ ← IO.setStderr e
@ -323,4 +327,4 @@ def workerMain : IO UInt32 := do
end FileWorker
end Server
end Lean
end Lean

View file

@ -69,15 +69,16 @@ open JsonRpc
open System.FilePath
structure OpenDocument where
version : Nat
text : FileMap
version : Nat
text : FileMap
headerAst : Syntax
def workerCfg : Process.StdioConfig :=
{ stdin := Process.Stdio.piped,
stdout := Process.Stdio.piped,
/- We pass workers' stderr through to the editor. -/
stderr := Process.Stdio.inherit }
def workerCfg : Process.StdioConfig := {
stdin := Process.Stdio.piped
stdout := Process.Stdio.piped
/- We pass workers' stderr through to the editor. -/
stderr := Process.Stdio.inherit
}
-- Events that a forwarding task of a worker signals to the main task
inductive WorkerEvent where
@ -98,10 +99,10 @@ inductive WorkerState where
abbrev PendingRequestMap := RBMap RequestID JsonRpc.Message (fun a b => Decidable.decide (a < b))
structure FileWorker where
doc : OpenDocument
proc : Process.Child workerCfg
commTask : Task WorkerEvent
state : WorkerState
doc : OpenDocument
proc : Process.Child workerCfg
commTask : Task WorkerEvent
state : WorkerState
-- NOTE: this should not be mutated outside of namespace FileWorker
pendingRequestsRef : IO.Ref PendingRequestMap
@ -114,7 +115,7 @@ def stdout (fw : FileWorker) : FS.Stream :=
FS.Stream.ofHandle fw.proc.stdout
def readMessage (fw : FileWorker) : IO JsonRpc.Message := do
let msg ← readLspMessage fw.stdout
let msg ← fw.stdout.readLspMessage
match msg with
| Message.request id method params? =>
fw.pendingRequestsRef.modify (fun pendingRequests => pendingRequests.erase id)
@ -122,19 +123,18 @@ def readMessage (fw : FileWorker) : IO JsonRpc.Message := do
msg
def writeMessage (fw : FileWorker) (msg : JsonRpc.Message) : IO Unit :=
writeLspMessage fw.stdin msg
fw.stdin.writeLspMessage msg
def writeNotification [ToJson α] (fw : FileWorker) (method : String) (param : α) : IO Unit :=
writeLspNotification fw.stdin method param
def writeNotification [ToJson α] (fw : FileWorker) (n : Notification α) : IO Unit :=
fw.stdin.writeLspNotification n
def writeRequest [ToJson α] (fw : FileWorker) (id : RequestID) (method : String) (param : α) : IO Unit := do
writeLspRequest fw.stdin id method param
fw.pendingRequestsRef.modify $ fun pendingRequests =>
pendingRequests.insert id (Message.request id method (Json.toStructured? param))
def writeRequest [ToJson α] (fw : FileWorker) (r : Request α) : IO Unit := do
fw.stdin.writeLspRequest r
fw.pendingRequestsRef.modify (fun pendingRequests => pendingRequests.insert r.id r)
def errorPendingRequests (fw : FileWorker) (hOut : FS.Stream) (code : ErrorCode) (msg : String) : IO Unit := do
let pendingRequests ← fw.pendingRequestsRef.modifyGet (fun pendingRequests => (pendingRequests, RBMap.empty))
pendingRequests.forM (fun id _ => writeLspResponseError hOut id code msg)
pendingRequests.forM (fun id _ => hOut.writeLspResponseError {id := id, code := code, message := msg})
end FileWorker
@ -175,7 +175,7 @@ partial def fwdMsgTask (fw : FileWorker) : ServerM (Task WorkerEvent) := do
try
let msg ← fw.readMessage
-- NOTE: Writes to Lean I/O channels are atomic, so these won't trample on each other.
writeLspMessage o msg
o.writeLspMessage msg
loop
catch err =>
-- NOTE: if writeLspMessage from above errors we will block here, but the main task will
@ -218,14 +218,17 @@ def startFileWorker (uri : DocumentUri) (version : Nat) (text : FileMap) : Serve
}
let commTask ← fwdMsgTask commTaskFw
let fw : FileWorker := { commTaskFw with commTask := commTask }
writeLspRequest fw.stdin (0 : Nat) "initialize" st.initParams
fw.writeNotification "textDocument/didOpen" {
textDocument := {
uri := uri
languageId := "lean"
version := version
text := text.source
} : DidOpenTextDocumentParams
fw.stdin.writeLspRequest ⟨0, "initialize", st.initParams⟩
fw.writeNotification {
method := "textDocument/didOpen"
param := {
textDocument := {
uri := uri
languageId := "lean"
version := version
text := text.source
} : DidOpenTextDocumentParams
}
}
updateFileWorkers uri fw
@ -298,7 +301,7 @@ def handleDidChange (p : DidChangeTextDocumentParams) : ServerM Unit := do
match fw.state with
| WorkerState.crashed queuedMsgs => restartCrashedFileWorker doc.uri newFw (queuedMsgs.push msg)
| WorkerState.running =>
try fw.writeNotification "textDocument/didChange" p
try fw.writeNotification "textDocument/didChange", p
catch _ => handleCrash doc.uri #[msg]
def handleDidClose (p : DidCloseTextDocumentParams) : ServerM Unit :=
@ -310,7 +313,7 @@ def handleCancelRequest (p : CancelParams) : ServerM Unit := do
match fw.state with
| WorkerState.crashed queuedMsgs => restartCrashedFileWorker uri fw queuedMsgs
| WorkerState.running =>
try fw.writeNotification "$/cancelRequest" p
try fw.writeNotification "$/cancelRequest", p
catch _ => handleCrash uri #[]
def handleRequest (id : RequestID) (method : String) (params : Json) : ServerM Unit := do
@ -322,7 +325,7 @@ def handleRequest (id : RequestID) (method : String) (params : Json) : ServerM U
match fw.state with
| WorkerState.crashed queuedMsgs => restartCrashedFileWorker uri fw (queuedMsgs.push msg)
| WorkerState.running =>
try fw.writeRequest id method parsedParams
try fw.writeRequest id, method, parsedParams
catch _ => handleCrash uri #[msg]
match method with
| "textDocument/hover" => handle HoverParams
@ -348,7 +351,7 @@ inductive ServerEvent where
| ClientError (e : IO.Error)
def runClientTask : ServerM (Task ServerEvent) := do
let readMsg : IO ServerEvent := ServerEvent.ClientMsg (←readLspMessage (←read).hIn)
let readMsg : IO ServerEvent := ServerEvent.ClientMsg (←(←read).hIn.readLspMessage)
let clientTask ← (←IO.asTask readMsg).map $ fun
| Except.ok ev => ev
| Except.error e => ServerEvent.ClientError e
@ -369,7 +372,7 @@ partial def mainLoop (clientTask : Task ServerEvent) : ServerM Unit := do
match msg with
| Message.request id "shutdown" _ =>
shutdown
writeLspResponse st.hOut id Json.null
st.hOut.writeLspResponse ⟨id, Json.null⟩
| Message.request id method (some params) =>
handleRequest id method (toJson params)
let newClientTask ← runClientTask
@ -399,10 +402,10 @@ def mkLeanServerCapabilities : ServerCapabilities :=
def initAndRunWatchdogAux : ServerM Unit := do
let st ← read
try
let _ ← readLspNotificationAs st.hIn "initialized" InitializedParams
let _ ← st.hIn.readLspNotificationAs "initialized" InitializedParams
let clientTask ← runClientTask
mainLoop clientTask
let Message.notification "exit" none ← readLspMessage st.hIn
let Message.notification "exit" none ← st.hIn.readLspMessage
| throwServerError "Expected an exit notification"
catch err =>
shutdown
@ -416,12 +419,18 @@ def initAndRunWatchdog (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 initRequest ← readLspRequestAs i "initialize" InitializeParams
writeLspResponse o initRequest.id
{ capabilities := mkLeanServerCapabilities,
serverInfo? := some { name := "Lean 4 server"
version? := "0.0.1" }
: InitializeResult }
let initRequest ← i.readLspRequestAs "initialize" InitializeParams
o.writeLspResponse {
id := initRequest.id
result := {
capabilities := mkLeanServerCapabilities
serverInfo? := some {
name := "Lean 4 server"
version? := "0.0.1"
}
: InitializeResult
}
}
ReaderT.run
initAndRunWatchdogAux
{ hIn := i
@ -458,4 +467,4 @@ def watchdogMain : IO UInt32 := do
end Watchdog
end Server
end Lean
end Lean