/-
Copyright (c) 2021 Wojciech Nawrocki. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.

Authors: Wojciech Nawrocki, Marc Huisinga
-/
module

prelude


public import Lean.Server.RequestCancellation

public import Lean.Server.FileSource
public import Lean.Server.FileWorker.Utils


public import Std.Sync.Mutex

public section

/-- Checks whether `r` contains `hoverPos`, taking into account EOF according to `text`. -/
def Lean.FileMap.rangeContainsHoverPos (text : Lean.FileMap) (r : String.Range)
    (hoverPos : String.Pos.Raw) (includeStop := false) : Bool :=
  -- When `hoverPos` is at the very end of the file, it is *after* the last position in `text`.
  -- However, for `includeStop = false`, all ranges stop at the last position in `text`,
  -- which always excludes a `hoverPos` at the very end of the file.
  -- For the purposes of the language server, we generally assume that ranges that extend to
  -- the end of the file also include a `hoverPos` at the very end of the file.
  let isRangeAtEOF := r.stop == text.source.endPos
  r.contains hoverPos (includeStop := includeStop || isRangeAtEOF)

def Lean.FileMap.rangeOverlapsRequestedRange
    (text : Lean.FileMap)
    (documentRange : String.Range)
    (requestedRange : String.Range)
    (includeDocumentRangeStop := false)
    (includeRequestedRangeStop := false)
    : Bool :=
  let isDocumentRangeAtEOF := documentRange.stop == text.source.endPos
  documentRange.overlaps requestedRange
    (includeFirstStop := includeDocumentRangeStop || isDocumentRangeAtEOF)
    (includeSecondStop := includeRequestedRangeStop)

def Lean.FileMap.rangeIncludesRequestedRange
    (text : Lean.FileMap)
    (documentRange : String.Range)
    (requestedRange : String.Range)
    (includeDocumentRangeStop := false)
    (includeRequestedRangeStop := false)
    : Bool :=
  let isDocumentRangeAtEOF := documentRange.stop == text.source.endPos
  documentRange.includes requestedRange
    (includeSuperStop := includeDocumentRangeStop || isDocumentRangeAtEOF)
    (includeSubStop := includeRequestedRangeStop)

namespace Lean.Language

open Lean.Server

inductive SnapshotTree.foldSnaps.Control where
  | done
  | proceed (foldChildren : Bool)

partial def SnapshotTree.foldSnaps (tree : SnapshotTree) (init : α)
    (f : SnapshotTask SnapshotTree → α → ServerTask (α × foldSnaps.Control)) : ServerTask α :=
  let t := traverseTree init tree
  t.mapCheap (·.1)
where
  traverseTree (acc : α) (tree : SnapshotTree) : ServerTask (α × Bool) :=
    traverseChildren acc tree.children.toList

  traverseChildren (acc : α) : List (SnapshotTask SnapshotTree) → ServerTask (α × Bool)
    | [] => .pure (acc, false)
    | child::otherChildren =>
      f child acc |>.bindCheap fun (acc, control) => Id.run do
        let .proceed foldChildrenOfChild := control
          | return .pure (acc, true)
        if ! foldChildrenOfChild then
          return traverseChildren acc otherChildren
        let subtreeTask := child.task.asServerTask.bindCheap fun tree =>
          traverseTree acc tree
        return subtreeTask.bindCheap fun (acc, done) => Id.run do
          if done then
            return .pure (acc, done)
          return traverseChildren acc otherChildren

/--
Finds the first (in pre-order) snapshot task in `tree` that contains `hoverPos`
(including whitespace) and which contains an info tree, and then returns that info tree,
waiting for any snapshot tasks on the way.
Subtrees that do not contain the position are skipped without forcing their tasks.
If the caller of this function needs the correct snapshot when the cursor is on whitespace,
then this function is likely the wrong one to call, as it simply yields the first snapshot
that contains `hoverPos` in its whitespace, which is not necessarily the correct one
(e.g. it may be indentation-sensitive).
-/
partial def SnapshotTree.findInfoTreeAtPos (text : FileMap) (tree : SnapshotTree)
    (hoverPos : String.Pos.Raw) (includeStop : Bool) : ServerTask (Option Elab.InfoTree) :=
  tree.foldSnaps (init := none) fun snap _ => Id.run do
    let some stx := snap.stx?
      -- One of the invariants of the snapshot tree is that `stx? = none` implies that
      -- this entire subtree has no relevant `InfoTree` information, so we can safely discard it
      -- here.
      | return .pure (none, .proceed (foldChildren := false))
    let some range := stx.getRangeWithTrailing? (canonicalOnly := true)
      -- In the worst case, the `infoTreeSnap` of the `CommandParsedSnap` will have canonical
      -- syntax that we can use here, so ignoring snapshots with non-canonical syntax can only
      -- at worst break incrementality in request handlers.
      | return .pure (none, .proceed (foldChildren := true))
    if ! text.rangeContainsHoverPos range hoverPos includeStop then
      -- Subtrees of the snapshot tree always have syntax ranges that are contained in those of
      -- their parents, so we can terminate early here.
      return .pure (none, .proceed (foldChildren := false))
    return snap.task.asServerTask.mapCheap fun tree => Id.run do
      let some infoTree := tree.element.infoTree?
        | return (none, .proceed (foldChildren := true))
      return (infoTree, .done)

partial def SnapshotTree.foldInfosInRange (tree : SnapshotTree) (requestedRange : String.Range)
    (init : α) (f : Elab.ContextInfo → Elab.Info → α → α) : ServerTask α :=
  tree.foldSnaps (init := init) fun snap acc => Id.run do
    let some stx := snap.stx?
      | return .pure (acc, .proceed (foldChildren := false))
    let some range := stx.getRangeWithTrailing? (canonicalOnly := true)
      | return .pure (acc, .proceed (foldChildren := true))
    if ! range.overlaps requestedRange (includeFirstStop := true) (includeSecondStop := true) then
      return .pure (acc, .proceed (foldChildren := false))
    return snap.task.asServerTask.mapCheap fun tree => Id.run do
      let some infoTree := tree.element.infoTree?
        | return (acc, .proceed (foldChildren := true))
      let acc := infoTree.foldInfo (init := acc) fun ctx i acc => Id.run do
        let some r := i.range?
          | return acc
        if ! r.overlaps requestedRange (includeFirstStop := true) (includeSecondStop := true) then
          return acc
        return f ctx i acc
      return (acc, .proceed (foldChildren := true))

partial def SnapshotTree.collectMessagesInRange (tree : SnapshotTree)
    (requestedRange : String.Range) : ServerTask MessageLog :=
  tree.foldSnaps (init := .empty) fun snap log => Id.run do
    let some stx := snap.stx?
      | return .pure (log, .proceed (foldChildren := true))
    let some range := stx.getRangeWithTrailing? (canonicalOnly := true)
      | return .pure (log, .proceed (foldChildren := true))
    if ! range.overlaps requestedRange (includeFirstStop := true) (includeSecondStop := true) then
      return .pure (log, .proceed (foldChildren := false))
    return snap.task.asServerTask.mapCheap fun tree => Id.run do
      return (log ++ tree.element.diagnostics.msgLog, .proceed (foldChildren := true))

end Lean.Language

namespace Lean.Server

structure RequestError where
  code    : JsonRpc.ErrorCode
  message : String
  deriving Inhabited

namespace RequestError
open JsonRpc

def fileChanged : RequestError :=
  { code := ErrorCode.contentModified
    message := "File changed." }

def methodNotFound (method : String) : RequestError :=
  { code := ErrorCode.methodNotFound
    message := s!"No request handler found for '{method}'" }

def invalidParams (message : String) : RequestError :=
  { code := ErrorCode.invalidParams, message }

def internalError (message : String) : RequestError :=
  { code := ErrorCode.internalError, message }

def requestCancelled : RequestError :=
  { code := ErrorCode.requestCancelled, message := "" }

def rpcNeedsReconnect : RequestError :=
  { code := ErrorCode.rpcNeedsReconnect, message := "Outdated RPC session" }

def ofException (e : Lean.Exception) : IO RequestError :=
  return internalError (← e.toMessageData.toString)

def ofIoError (e : IO.Error) : RequestError :=
  internalError (toString e)

def toLspResponseError (id : RequestID) (e : RequestError) : ResponseError Unit :=
  { id := id
    code := e.code
    message := e.message }

end RequestError

def parseRequestParams (paramType : Type) [FromJson paramType] (params : Json)
    : Except RequestError paramType :=
  fromJson? params |>.mapError fun inner => {
    code := JsonRpc.ErrorCode.invalidParams
    message := s!"Cannot parse request params: {params.compress}\n{inner}"
  }

inductive ServerRequestResponse (α : Type) where
  | success (response : α)
  | failure (code : JsonRpc.ErrorCode) (message : String)
  deriving Inhabited

abbrev ServerRequestEmitter := (method : String) → (param : Json)
  → BaseIO (ServerTask (ServerRequestResponse Json))

structure RequestContext where
  rpcSessions          : Std.TreeMap UInt64 (IO.Ref FileWorker.RpcSession)
  doc                  : FileWorker.EditableDocument
  hLog                 : IO.FS.Stream
  initParams           : Lsp.InitializeParams
  cancelTk             : RequestCancellationToken
  serverRequestEmitter : ServerRequestEmitter

abbrev RequestTask α := ServerTask (Except RequestError α)
abbrev RequestT m := ReaderT RequestContext <| ExceptT RequestError m
/-- Workers execute request handlers in this monad. -/
abbrev RequestM := ReaderT RequestContext <| EIO RequestError

def RequestM.run (act : RequestM α) (rc : RequestContext) : EIO RequestError α :=
  act rc

abbrev RequestTask.pure (a : α) : RequestTask α := ServerTask.pure (.ok a)

instance : MonadLift IO RequestM where
  monadLift x := do
    match ←  x.toBaseIO with
    | .error e => throw <| RequestError.ofIoError e
    | .ok v => return v

instance : MonadLift (EIO Exception) RequestM where
  monadLift x := do
    match ←  x.toBaseIO with
    | .error e => throw <| ← RequestError.ofException e
    | .ok v => return v

instance : MonadLift CancellableM RequestM where
  monadLift x := do
    let ctx ← read
    let r ← x.run ctx.cancelTk
    match r with
    | .error _ => throw RequestError.requestCancelled
    | .ok v    => return v

namespace RequestM
open FileWorker
open Snapshots

def runInIO (x : RequestM α) (ctx : RequestContext) : IO α := do
  x.run ctx |>.adaptExcept (IO.userError ·.message)

def readDoc [Monad m] [MonadReaderOf RequestContext m] : m EditableDocument := do
  let rc ← readThe RequestContext
  return rc.doc

def asTask (t : RequestM α) : RequestM (RequestTask α) := do
  let rc ← readThe RequestContext
  ServerTask.EIO.asTask <| t.run rc

def pureTask (t : RequestM α) : RequestM (RequestTask α) := do
  let r ← t.run
  return ServerTask.pure <| .ok r

def mapTaskCheap (t : ServerTask α) (f : α → RequestM β) : RequestM (RequestTask β) := do
  let rc ← readThe RequestContext
  ServerTask.EIO.mapTaskCheap (f · rc) t

def mapTaskCostly (t : ServerTask α) (f : α → RequestM β) : RequestM (RequestTask β) := do
  let rc ← readThe RequestContext
  ServerTask.EIO.mapTaskCostly (f · rc) t

def bindTaskCheap (t : ServerTask α) (f : α → RequestM (RequestTask β)) : RequestM (RequestTask β) := do
  let rc ← readThe RequestContext
  ServerTask.EIO.bindTaskCheap t (f · rc)

def bindTaskCostly (t : ServerTask α) (f : α → RequestM (RequestTask β)) : RequestM (RequestTask β) := do
  let rc ← readThe RequestContext
  ServerTask.EIO.bindTaskCostly t (f · rc)

def mapRequestTaskCheap (t : RequestTask α) (f : α → RequestM β) : RequestM (RequestTask β) := do
  mapTaskCheap (t := t) fun
    | .error e => throw e
    | .ok r => f r

def mapRequestTaskCostly (t : RequestTask α) (f : α → RequestM β) : RequestM (RequestTask β) := do
  mapTaskCostly (t := t) fun
    | .error e => throw e
    | .ok r => f r

def bindRequestTaskCheap (t : RequestTask α) (f : α → RequestM (RequestTask β)) : RequestM (RequestTask β) := do
  bindTaskCheap (t := t) fun
    | .error e => throw e
    | .ok r => f r

def bindRequestTaskCostly (t : RequestTask α) (f : α → RequestM (RequestTask β)) : RequestM (RequestTask β) := do
  bindTaskCostly (t := t) fun
    | .error e => throw e
    | .ok r => f r

def parseRequestParams (paramType : Type) [FromJson paramType] (params : Json)
    : RequestM paramType :=
  EIO.ofExcept <| Server.parseRequestParams paramType params

def checkCancelled : RequestM Unit := do
  let rc ← readThe RequestContext
  if ← rc.cancelTk.wasCancelledByCancelRequest then
    throw .requestCancelled

def sendServerRequest
    paramType [ToJson paramType] responseType [FromJson responseType] [Inhabited responseType]
    (method : String)
    (param  : paramType)
    : RequestM (ServerTask (ServerRequestResponse responseType)) := do
  let ctx ← read
  let task ← ctx.serverRequestEmitter method (toJson param)
  return task.mapCheap fun
    | ServerRequestResponse.success response =>
      match fromJson? response with
      | .ok (response : responseType) => ServerRequestResponse.success response
      | .error err => ServerRequestResponse.failure .parseError s!"Cannot parse server request response: {response.compress}\n{err}"
    | ServerRequestResponse.failure code msg => ServerRequestResponse.failure code msg

def waitFindSnapAux (notFoundX : RequestM α) (x : Snapshot → RequestM α)
    : Except IO.Error (Option Snapshot) → RequestM α
  /- The elaboration task that we're waiting for may be aborted if the file contents change.
  In that case, we reply with the `fileChanged` error by default. Thanks to this, the server doesn't
  get bogged down in requests for an old state of the document. -/
  | Except.error e => throw (RequestError.ofIoError e)
  | Except.ok none => notFoundX
  | Except.ok (some snap) => x snap

/-- Create a task which waits for the first snapshot matching `p`, handles various errors,
and if a matching snapshot was found executes `x` with it. If not found, the task executes
`notFoundX`. -/
def withWaitFindSnap (doc : EditableDocument) (p : Snapshot → Bool)
    (notFoundX : RequestM β)
    (x : Snapshot → RequestM β)
    : RequestM (RequestTask β) := do
  let findTask := doc.cmdSnaps.waitFind? p
  mapTaskCostly findTask <| waitFindSnapAux notFoundX x

/-- See `withWaitFindSnap`. -/
def bindWaitFindSnap (doc : EditableDocument) (p : Snapshot → Bool)
    (notFoundX : RequestM (RequestTask β))
    (x : Snapshot → RequestM (RequestTask β))
    : RequestM (RequestTask β) := do
  let findTask := doc.cmdSnaps.waitFind? p
  bindTaskCostly findTask <| waitFindSnapAux notFoundX x

/-- Create a task which waits for the snapshot containing `lspPos` and executes `f` with it.
If no such snapshot exists, the request fails with an error. -/
def withWaitFindSnapAtPos
    (lspPos : Lsp.Position)
    (f : Snapshots.Snapshot → RequestM α)
    : RequestM (RequestTask α) := do
  let doc ← readDoc
  let pos := doc.meta.text.lspPosToUtf8Pos lspPos
  withWaitFindSnap doc (fun s => s.endPos >= pos)
    (notFoundX := throw ⟨.invalidParams, s!"no snapshot found at {lspPos}"⟩)
    (x := f)

open Language.Lean in
/-- Finds the first `CommandParsedSnapshot` containing `hoverPos`, asynchronously. -/
partial def findCmdParsedSnap (doc : EditableDocument) (hoverPos : String.Pos.Raw)
    : ServerTask (Option CommandParsedSnapshot) := Id.run do
  let some headerParsed := doc.initSnap.result?
    | .pure none
  headerParsed.processedSnap.task.asServerTask.bindCheap fun headerProcessed => Id.run do
    let some headerSuccess := headerProcessed.result?
      | return .pure none
    let firstCmdSnapTask : ServerTask CommandParsedSnapshot := headerSuccess.firstCmdSnap.task
    firstCmdSnapTask.bindCheap go
where
  go (cmdParsed : CommandParsedSnapshot) : ServerTask (Option CommandParsedSnapshot) := Id.run do
    if containsHoverPos cmdParsed then
      return .pure (some cmdParsed)
    if isAfterHoverPos cmdParsed then
      -- This should never happen in principle
      -- (commands + trailing ws are consecutive and there is no unassigned space between them),
      -- but it's always good to eliminate one additional assumption.
      return .pure none
    match cmdParsed.nextCmdSnap? with
    | some next =>
      next.task.asServerTask.bindCheap go
    | none => .pure none

  containsHoverPos (cmdParsed : CommandParsedSnapshot) : Bool := Id.run do
    let some range := cmdParsed.stx.getRangeWithTrailing? (canonicalOnly := true)
      | return false
    return doc.meta.text.rangeContainsHoverPos range hoverPos (includeStop := false)

  isAfterHoverPos (cmdParsed : CommandParsedSnapshot) : Bool := Id.run do
    let some startPos := cmdParsed.stx.getPos? (canonicalOnly := true)
      | return false
    return hoverPos < startPos

open Language in
/--
Finds the command syntax and info tree of the first snapshot task containing `pos`, asynchronously.
The info tree may be from a nested snapshot, such as a single tactic.

See `SnapshotTree.findInfoTreeAtPos` for details on how the search is done.
-/
def findCmdDataAtPos
    (doc : EditableDocument)
    (hoverPos : String.Pos.Raw)
    (includeStop : Bool)
    : ServerTask (Option (Syntax × Elab.InfoTree)) :=
  findCmdParsedSnap doc hoverPos |>.bindCheap fun
    | some cmdParsed => toSnapshotTree cmdParsed.elabSnap |>.findInfoTreeAtPos doc.meta.text hoverPos includeStop |>.bindCheap fun
      | some infoTree => .pure <| some (cmdParsed.stx, infoTree)
      | none          => cmdParsed.elabSnap.infoTreeSnap.task.asServerTask.mapCheap fun s =>
        assert! s.infoTree?.isSome
        some (cmdParsed.stx, s.infoTree?.get!)
    | none => .pure none

open Language in
/--
Finds the info tree of the first snapshot task containing `pos`, asynchronously.
The info tree may be from a nested snapshot, such as a single tactic.

See `SnapshotTree.findInfoTreeAtPos` for details on how the search is done.
-/
partial def findInfoTreeAtPos
    (doc : EditableDocument)
    (hoverPos : String.Pos.Raw)
    (includeStop : Bool)
    : ServerTask (Option Elab.InfoTree) :=
  findCmdDataAtPos doc hoverPos includeStop |>.mapCheap (·.map (·.2))

open Elab.Command in
def runCommandElabM (snap : Snapshot) (c : RequestT CommandElabM α) : RequestM α := do
  let rc ← readThe RequestContext
  match ← snap.runCommandElabM rc.doc.meta (c.run rc) with
  | .ok v => return v
  | .error e => throw e

def runCoreM (snap : Snapshot) (c : RequestT CoreM α) : RequestM α := do
  let rc ← readThe RequestContext
  match ← snap.runCoreM rc.doc.meta (c.run rc) with
  | .ok v => return v
  | .error e => throw e

open Elab.Term in
def runTermElabM (snap : Snapshot) (c : RequestT TermElabM α) : RequestM α := do
  let rc ← readThe RequestContext
  match ← snap.runTermElabM rc.doc.meta (c.run rc) with
  | .ok v => return v
  | .error e => throw e

end RequestM

/-! # The global request handlers table

We maintain a global map of LSP request handlers. This allows user code such as plugins
to register its own handlers, for example to support ITP functionality such as goal state
visualization.

For details of how to register one, see `registerLspRequestHandler`. -/
section HandlerTable
open Lsp

structure LspResponse (α : Type) where
  response   : α
  isComplete : Bool

structure SerializedLspResponse where
  response?  : Option Json
  serialized : String
  isComplete : Bool

-- Custom serialization that uses `r.serialized` in place of `response`.
-- Identical to `Json.compress` for the JSON of `JsonRpc.Message.response ..`.
def SerializedLspResponse.toSerializedMessage
    (id : JsonRpc.RequestID) (r : SerializedLspResponse) : String :=
  "{" ++
    s!"\"id\":{toJson id |>.compress}," ++
    s!"\"jsonrpc\":\"2.0\"," ++
    s!"\"result\":{r.serialized}"
    ++ "}"

structure RequestHandler where
  fileSource : Json → Except RequestError FileIdent
  handle : Json → RequestM (RequestTask SerializedLspResponse)

builtin_initialize requestHandlers : IO.Ref (PersistentHashMap String RequestHandler) ←
  IO.mkRef {}

/-- NB: This method may only be called in `initialize` blocks (user or builtin).

A registration consists of:
- a type of JSON-parsable request data `paramType`
- a `FileSource` instance for it so the system knows where to route requests
- a type of JSON-serializable response data `respType`
- an actual `handler` which runs in the `RequestM` monad and is expected
  to produce an asynchronous `RequestTask` which does any waiting/computation
- an optional `serialize` function which can be used to serialize `respType` directly to a string
  for performance reasons, skipping `Json`

A handler task may be cancelled at any time, so it should check the cancellation token when possible
to handle this cooperatively. Any exceptions thrown in a request handler will be reported to the client
as LSP error responses. -/
def registerLspRequestHandler (method : String)
    paramType [FromJson paramType] [FileSource paramType]
    respType [ToJson respType]
    (handler : paramType → RequestM (RequestTask respType))
    (serialize? : Option (respType → String) := none) :
    IO Unit := do
  if !(← Lean.initializing) then
    throw <| IO.userError s!"Failed to register LSP request handler for '{method}': only possible during initialization"
  if (← requestHandlers.get).contains method then
    throw <| IO.userError s!"Failed to register LSP request handler for '{method}': already registered"
  let fileSource := fun j =>
    parseRequestParams paramType j |>.map Lsp.fileSource
  let handle := fun j => do
    let params ← RequestM.parseRequestParams paramType j
    let t ← handler params
    pure <| t.mapCheap <| Except.map fun r =>
      if let some serialize := serialize? then
        {
          response?  := none
          serialized := serialize r
          isComplete := true
        }
      else
        let j := toJson r
        {
          response? := some j
          serialized := j.compress
          isComplete := true
        }

  requestHandlers.modify fun rhs => rhs.insert method { fileSource, handle }

def lookupLspRequestHandler (method : String) : IO (Option RequestHandler) :=
  return (← requestHandlers.get).find? method

/-- NB: This method may only be called in `initialize` blocks (user or builtin).

Register another handler to invoke after the last one registered for a method.
At least one handler for the method must have already been registered to perform
chaining.

For more details on the registration of a handler, see `registerLspRequestHandler`. -/
def chainLspRequestHandler (method : String)
    paramType [FromJson paramType]
    respType [FromJson respType] [ToJson respType]
    (handler : paramType → RequestTask respType → RequestM (RequestTask respType)) : IO Unit := do
  if !(← Lean.initializing) then
    throw <| IO.userError s!"Failed to chain LSP request handler for '{method}': only possible during initialization"
  if let some oldHandler ← lookupLspRequestHandler method then
    let handle := fun j => do
      let t ← oldHandler.handle j
      let t := t.mapCheap fun x =>
        x.bind fun j => do
          let response ←
            match j.response? with
            | none =>
              Json.parse j.serialized |>.mapError fun e =>
                .internalError s!"Failed to parse original LSP response JSON for `{method}` when chaining: {e}"
            | some response =>
              pure response
          FromJson.fromJson? response |>.mapError fun e =>
            .internalError s!"Failed to parse original LSP response for `{method}` when chaining: {e}"
      let params ← RequestM.parseRequestParams paramType j
      let t ← handler params t
      pure <| t.mapCheap <| Except.map fun r =>
        let j := toJson r
        {
          response? := some j
          serialized := j.compress
          isComplete := true
        }

    requestHandlers.modify fun rhs => rhs.insert method {oldHandler with handle}
  else
    throw <| IO.userError s!"Failed to chain LSP request handler for '{method}': no initial handler registered"

inductive RequestHandlerCompleteness where
  | complete
  /--
  A request handler is partial if the LSP spec states that the request method implemented by
  the handler should be responded to with the full state of the document, but our implementation
  of the handler only returns a partial result for the document
  (e.g. only for the processed regions of the document, to reduce latency after a `didChange`).
  A request handler can only be partial if LSP also specifies a corresponding `refresh`
  server-to-client request, e.g. `workspace/inlayHint/refresh` for `textDocument/inlayHint`.
  This is necessary because we use the `refresh` request to prompt the client to re-request the
  data for a partial request if we returned a partial response for that request in the past,
  so that the client eventually converges to a complete set of information for the full document.
  -/
  | «partial» (refreshMethod : String) (refreshIntervalMs : Nat)

structure StatefulRequestHandler where
  fileSource      : Json → Except RequestError FileIdent
  /--
  `handle` with explicit state management for chaining request handlers.
  This function is pure w.r.t. `lastTaskMutex` and `stateRef`, but not `RequestM`.
  -/
  pureHandle      : Json → Dynamic → RequestM (SerializedLspResponse × Dynamic)
  handle          : Json → RequestM (RequestTask SerializedLspResponse)
  /--
  `onDidChange` with explicit state management for chaining request handlers.
  This function is pure w.r.t. `lastTaskMutex` and `stateRef`, but not `RequestM`.
  -/
  pureOnDidChange : DidChangeTextDocumentParams → (StateT Dynamic RequestM) Unit
  onDidChange     : DidChangeTextDocumentParams → RequestM Unit
  lastTaskMutex   : Std.Mutex (ServerTask Unit)
  initState       : Dynamic
  /--
  `stateRef` is synchronized in `registerStatefulLspRequestHandler` by using `lastTaskMutex` to
  ensure that stateful request tasks for the same handler are executed sequentially (in order of arrival).
  -/
  stateRef        : IO.Ref Dynamic
  completeness    : RequestHandlerCompleteness

builtin_initialize statefulRequestHandlers : IO.Ref (PersistentHashMap String StatefulRequestHandler) ←
  IO.mkRef {}

private def getState! (method : String) (state : Dynamic) stateType [TypeName stateType] : RequestM stateType := do
  let some state := state.get? stateType
    | throw <| .internalError s!"Got invalid state type in stateful LSP request handler for {method}"
  return state

private def getIOState! (method : String) (state : Dynamic) stateType [TypeName stateType] : IO stateType := do
  let some state := state.get? stateType
    | throw <| .userError s!"Got invalid state type in stateful LSP request handler for {method}"
  return state

private def overrideStatefulLspRequestHandler
    (method : String) (completeness : RequestHandlerCompleteness)
    paramType [FromJson paramType] [FileSource paramType]
    respType [ToJson respType]
    stateType [TypeName stateType]
    (initState : stateType)
    (handler : paramType → stateType → RequestM (LspResponse respType × stateType))
    (onDidChange : DidChangeTextDocumentParams → StateT stateType RequestM Unit)
    : IO Unit := do
  if !(← Lean.initializing) then
    throw <| IO.userError s!"Failed to register stateful LSP request handler for '{method}': only possible during initialization"
  let fileSource := fun j =>
    parseRequestParams paramType j |>.map Lsp.fileSource
  let lastTaskMutex ← Std.Mutex.new <| ServerTask.pure ()
  let initState := Dynamic.mk initState
  let stateRef ← IO.mkRef initState

  let pureHandle : Json → Dynamic → RequestM (SerializedLspResponse × Dynamic) := fun param state => do
    let param ← RequestM.parseRequestParams paramType param
    let state ← getState! method state stateType
    let (r, state') ← handler param state
    let response := toJson r.response
    return ({ r with response? := some response, serialized := response.compress }, Dynamic.mk state')

  let handle : Json → RequestM (RequestTask SerializedLspResponse) := fun param => lastTaskMutex.atomically do
    let lastTask ← get
    let requestTask ← RequestM.mapTaskCostly lastTask fun () => do
      let state ← stateRef.get
      let (r, state') ← pureHandle param state
      stateRef.set state'
      return r
    set <| requestTask.mapCheap <| fun _ => ()
    return requestTask

  let pureOnDidChange : DidChangeTextDocumentParams → (StateT Dynamic RequestM) Unit := fun param => do
    let state ← getState! method (← get) stateType
    let ((), state') ← onDidChange param |>.run state
    set <| Dynamic.mk state'

  let onDidChange : DidChangeTextDocumentParams → RequestM Unit := fun param => lastTaskMutex.atomically do
      let lastTask ← get
      let didChangeTask ← RequestM.mapTaskCostly (t := lastTask) fun () => do
        let state ← stateRef.get
        let ((), state') ← pureOnDidChange param |>.run state
        stateRef.set state'
      set <| didChangeTask.mapCheap <| fun _ => ()

  statefulRequestHandlers.modify fun rhs => rhs.insert method {
    fileSource,
    pureHandle,
    handle,
    pureOnDidChange,
    onDidChange,
    lastTaskMutex,
    initState,
    stateRef,
    completeness
  }

private def registerStatefulLspRequestHandler
    (method : String) (completeness : RequestHandlerCompleteness)
    paramType [FromJson paramType] [FileSource paramType]
    respType [ToJson respType]
    stateType [TypeName stateType]
    (initState : stateType)
    (handler : paramType → stateType → RequestM (LspResponse respType × stateType))
    (onDidChange : DidChangeTextDocumentParams → StateT stateType RequestM Unit)
    : IO Unit := do
  if (← requestHandlers.get).contains method then
    throw <| IO.userError s!"Failed to register stateful LSP request handler for '{method}': already registered"
  overrideStatefulLspRequestHandler method completeness paramType respType stateType initState handler onDidChange

def registerCompleteStatefulLspRequestHandler (method : String)
    paramType [FromJson paramType] [FileSource paramType]
    respType [ToJson respType]
    stateType [TypeName stateType]
    (initState : stateType)
    (handler : paramType → stateType → RequestM (respType × stateType))
    (onDidChange : DidChangeTextDocumentParams → StateT stateType RequestM Unit)
    : IO Unit :=
  let handler : paramType → stateType → RequestM (LspResponse respType × stateType) := fun p s => do
    let (response, s) ← handler p s
    return ({ response, isComplete := true }, s)
  registerStatefulLspRequestHandler method .complete paramType respType stateType initState handler onDidChange

def registerPartialStatefulLspRequestHandler (method refreshMethod : String) (refreshIntervalMs : Nat)
    paramType [FromJson paramType] [FileSource paramType]
    respType [ToJson respType]
    stateType [TypeName stateType]
    (initState : stateType)
    (handler : paramType → stateType → RequestM (LspResponse respType × stateType))
    (onDidChange : DidChangeTextDocumentParams → StateT stateType RequestM Unit) :=
  registerStatefulLspRequestHandler method (.partial refreshMethod refreshIntervalMs) paramType respType stateType initState handler onDidChange

def isStatefulLspRequestMethod (method : String) : BaseIO Bool := do
  return (← statefulRequestHandlers.get).contains method

def lookupStatefulLspRequestHandler (method : String) : BaseIO (Option StatefulRequestHandler) := do
  return (← statefulRequestHandlers.get).find? method

def partialLspRequestHandlerMethods : IO (Array (String × String × Nat)) := do
  return (← statefulRequestHandlers.get).toArray.filterMap fun (method, h) => do
    let .partial refreshMethod refreshIntervalMs := h.completeness
      | none
    return (method, refreshMethod, refreshIntervalMs)

def chainStatefulLspRequestHandler (method : String)
    paramType [FromJson paramType] [ToJson paramType] [FileSource paramType]
    respType [FromJson respType] [ToJson respType]
    stateType [TypeName stateType]
    (handler : paramType → LspResponse respType → stateType → RequestM (LspResponse respType × stateType))
    (onDidChange : DidChangeTextDocumentParams → StateT stateType RequestM Unit) : IO Unit := do
  if ! (← Lean.initializing) then
    throw <| IO.userError s!"Failed to chain stateful LSP request handler for '{method}': only possible during initialization"
  let some oldHandler ← lookupStatefulLspRequestHandler method
    | throw <| IO.userError s!"Failed to chain stateful LSP request handler for '{method}': no initial handler registered"
  let oldHandle := oldHandler.pureHandle
  let oldOnDidChange := oldHandler.pureOnDidChange
  let initState ← getIOState! method oldHandler.initState stateType
  let handle (p : paramType) (s : stateType) : RequestM (LspResponse respType × stateType) := do
    let (r, s) ← oldHandle (toJson p) (Dynamic.mk s)
    let .ok response :=
        match r.response? with
        | none => Json.parse r.serialized
        | some response => .ok response
      | throw <| RequestError.internalError "Failed to parse response of previous request handler when chaining stateful LSP request handlers"
    let .ok response := fromJson? response
      | throw <| RequestError.internalError "Failed to convert response of previous request handler when chaining stateful LSP request handlers"
    let r := { r with response := response }
    let s ← getState! method s stateType
    handler p r s
  let onDidChange (p : DidChangeTextDocumentParams) : StateT stateType RequestM Unit := do
    let s ← get
    let ((), s) ← oldOnDidChange p |>.run (Dynamic.mk s)
    let s ← getState! method s stateType
    let ((), s) ← onDidChange p |>.run s
    set <| s
  overrideStatefulLspRequestHandler method oldHandler.completeness paramType respType stateType initState
    handle onDidChange

def handleOnDidChange (p : DidChangeTextDocumentParams) : RequestM Unit := do
  (← statefulRequestHandlers.get).forM fun _ handler => do
    handler.onDidChange p

def handleLspRequest (method : String) (params : Json) : RequestM (RequestTask SerializedLspResponse) := do
  if ← isStatefulLspRequestMethod method then
    match ← lookupStatefulLspRequestHandler method with
    | none =>
      throw <| .internalError
        s!"request '{method}' routed through watchdog but unknown in worker; are both using the same plugins?"
    | some rh => rh.handle params
  else
    match ← lookupLspRequestHandler method with
    | none =>
      throw <| .internalError
        s!"request '{method}' routed through watchdog but unknown in worker; are both using the same plugins?"
    | some rh => rh.handle params

def routeLspRequest (method : String) (params : Json) : IO (Except RequestError FileIdent) := do
  if ← isStatefulLspRequestMethod method then
    match ← lookupStatefulLspRequestHandler method with
    | none => return Except.error <| RequestError.methodNotFound method
    | some rh => return rh.fileSource params
  else
    match ← lookupLspRequestHandler method with
    | none => return Except.error <| RequestError.methodNotFound method
    | some rh => return rh.fileSource params

end HandlerTable
end Lean.Server
