fix: periodically refresh semantic tokens (#3619)

This PR fixes an issue where the file worker would not provide the
client with semantic tokens until the file had been elaborated
completely. The file worker now also tells the client to refresh its
semantic tokens after running "Restart File". This PR is based on #3271.
This commit is contained in:
Marc Huisinga 2024-03-14 18:10:04 +01:00 committed by GitHub
parent 78a72741c6
commit 4e3a8468c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 58 additions and 25 deletions

View file

@ -62,21 +62,22 @@ open JsonRpc
structure WorkerContext where
/-- Synchronized output channel for LSP messages. Notifications for outdated versions are
discarded on read. -/
chanOut : IO.Channel JsonRpc.Message
chanOut : IO.Channel JsonRpc.Message
/--
Latest document version received by the client, used for filtering out notifications from
previous versions.
-/
maxDocVersionRef : IO.Ref Int
hLog : FS.Stream
initParams : InitializeParams
processor : Parser.InputContext → BaseIO Lean.Language.Lean.InitialSnapshot
clientHasWidgets : Bool
maxDocVersionRef : IO.Ref Int
freshRequestIdRef : IO.Ref Int
hLog : FS.Stream
initParams : InitializeParams
processor : Parser.InputContext → BaseIO Lean.Language.Lean.InitialSnapshot
clientHasWidgets : Bool
/--
Options defined on the worker cmdline (i.e. not including options from `setup-file`), used for
context-free tasks such as editing delay.
-/
cmdlineOpts : Options
cmdlineOpts : Options
/-! # Asynchronous snapshot elaboration -/
@ -222,7 +223,6 @@ structure WorkerState where
/-- A map of RPC session IDs. We allow asynchronous elab tasks and request handlers
to modify sessions. A single `Ref` ensures atomic transactions. -/
rpcSessions : RBMap UInt64 (IO.Ref RpcSession) compare
freshRequestID : Nat
abbrev WorkerM := ReaderT WorkerContext <| StateRefT WorkerState IO
@ -288,6 +288,7 @@ section Initialization
mainModuleName ← moduleNameOfFileName path none
catch _ => pure ()
let maxDocVersionRef ← IO.mkRef 0
let freshRequestIdRef ← IO.mkRef 0
let chanOut ← mkLspOutputChannel maxDocVersionRef
let srcSearchPathPromise ← IO.Promise.new
@ -301,6 +302,7 @@ section Initialization
processor
clientHasWidgets
maxDocVersionRef
freshRequestIdRef
cmdlineOpts := opts
}
let doc : EditableDocumentCore := {
@ -316,7 +318,6 @@ section Initialization
pendingRequests := RBMap.empty
rpcSessions := RBMap.empty
importCachingTask? := none
freshRequestID := 0
})
where
/-- Creates an LSP message output channel along with a reader that sends out read messages on
@ -345,6 +346,18 @@ section Initialization
return chanOut
end Initialization
section ServerRequests
def sendServerRequest [ToJson α]
(ctx : WorkerContext)
(method : String)
(param : α)
: IO Unit := do
let freshRequestId ← ctx.freshRequestIdRef.modifyGet fun freshRequestId =>
(freshRequestId, freshRequestId + 1)
let r : JsonRpc.Request α := ⟨freshRequestId, method, param⟩
ctx.chanOut.send r
end ServerRequests
section Updates
def updatePendingRequests (map : PendingRequestMap → PendingRequestMap) : WorkerM Unit := do
modify fun st => { st with pendingRequests := map st.pendingRequests }
@ -541,8 +554,8 @@ section MessageHandling
ctx.chanOut.send <| e.toLspResponseError id
queueRequest id t
def handleResponse (_ : RequestID) (result : Json) : WorkerM Unit :=
throwServerError s!"Unknown response kind: {result}"
def handleResponse (_ : RequestID) (_ : Json) : WorkerM Unit :=
return -- The only response that we currently expect here is always empty
end MessageHandling
@ -589,6 +602,13 @@ section MainLoop
| _ => throwServerError "Got invalid JSON-RPC message"
end MainLoop
def runRefreshTask : WorkerM (Task (Except IO.Error Unit)) := do
let ctx ← read
IO.asTask do
while ! (←IO.checkCanceled) do
sendServerRequest ctx "workspace/semanticTokens/refresh" (none : Option Nat)
IO.sleep 2000
def initAndRunWorker (i o e : FS.Stream) (opts : Options) : IO UInt32 := do
let i ← maybeTee "fwIn.txt" false i
let o ← maybeTee "fwOut.txt" true o
@ -605,7 +625,11 @@ def initAndRunWorker (i o e : FS.Stream) (opts : Options) : IO UInt32 := do
let _ ← IO.setStderr e
try
let (ctx, st) ← initializeWorker meta o e initParams.param opts
let _ ← StateRefT'.run (s := st) <| ReaderT.run (r := ctx) (mainLoop i)
let _ ← StateRefT'.run (s := st) <| ReaderT.run (r := ctx) do
let refreshTask ← runRefreshTask
let exitCode ← mainLoop i
IO.cancel refreshTask
pure exitCode
return (0 : UInt32)
catch err =>
IO.eprintln err

View file

@ -430,10 +430,10 @@ def noHighlightKinds : Array SyntaxNodeKind := #[
``Lean.Parser.Command.moduleDoc]
structure SemanticTokensContext where
beginPos : String.Pos
endPos : String.Pos
text : FileMap
snap : Snapshot
beginPos : String.Pos
endPos? : Option String.Pos
text : FileMap
snap : Snapshot
structure SemanticTokensState where
data : Array Nat
@ -447,20 +447,29 @@ def keywordSemanticTokenMap : RBMap String SemanticTokenType compare :=
|>.insert "stop" .leanSorryLike
|>.insert "#exit" .leanSorryLike
partial def handleSemanticTokens (beginPos endPos : String.Pos)
partial def handleSemanticTokens (beginPos : String.Pos) (endPos? : Option String.Pos)
: RequestM (RequestTask SemanticTokens) := do
let doc ← readDoc
let text := doc.meta.text
let t := doc.cmdSnaps.waitUntil (·.endPos >= endPos)
mapTask t fun (snaps, _) =>
match endPos? with
| none =>
-- Only grabs the finished prefix so that we do not need to wait for elaboration to complete
-- for the full file before sending a response. This means that the response will be incomplete,
-- which we mitigate by regularly sending `workspace/semanticTokens/refresh` requests in the
-- `FileWorker` to tell the client to re-compute the semantic tokens.
let (snaps, _) ← doc.cmdSnaps.getFinishedPrefix
asTask <| run doc snaps
| some endPos =>
let t := doc.cmdSnaps.waitUntil (·.endPos >= endPos)
mapTask t fun (snaps, _) => run doc snaps
where
run doc snaps : RequestM SemanticTokens :=
StateT.run' (s := { data := #[], lastLspPos := ⟨0, 0⟩ : SemanticTokensState }) do
for s in snaps do
if s.endPos <= beginPos then
continue
ReaderT.run (r := SemanticTokensContext.mk beginPos endPos text s) <|
ReaderT.run (r := SemanticTokensContext.mk beginPos endPos? doc.meta.text s) <|
go s.stx
return { data := (← get).data }
where
go (stx : Syntax) := do
match stx with
| `($e.$id:ident) => go e; addToken id SemanticTokenType.property
@ -506,9 +515,9 @@ where
(val.length > 1 && val.front == '#' && (val.get ⟨1⟩).isAlpha) then
addToken stx (keywordSemanticTokenMap.findD val .keyword)
addToken stx type := do
let ⟨beginPos, endPos, text, _⟩ ← read
let ⟨beginPos, endPos?, text, _⟩ ← read
if let (some pos, some tailPos) := (stx.getPos?, stx.getTailPos?) then
if beginPos <= pos && pos < endPos then
if beginPos <= pos && endPos?.all (pos < ·) then
let lspPos := (← get).lastLspPos
let lspPos' := text.utf8PosToLspPos pos
let deltaLine := lspPos'.line - lspPos.line
@ -523,7 +532,7 @@ where
def handleSemanticTokensFull (_ : SemanticTokensParams)
: RequestM (RequestTask SemanticTokens) := do
handleSemanticTokens 0 ⟨1 <<< 31⟩
handleSemanticTokens 0 none
def handleSemanticTokensRange (p : SemanticTokensRangeParams)
: RequestM (RequestTask SemanticTokens) := do