Skip to content

Commit

Permalink
Another attempt at using the lsp API for some progress reporting (#4218)
Browse files Browse the repository at this point in the history
* Another attempt at using the lsp API for some progress reporting

* Fixing tests

* Remove trace

* Make splice plugin tests not depend on progress

* More test fixing

* Switch to hackage

* stack

* warnings

* more

* Put tests back

---------

Co-authored-by: Patrick <fwy996602672@gmail.com>
  • Loading branch information
michaelpj and soulomoon committed May 19, 2024
1 parent b43dcbb commit 0651c5c
Show file tree
Hide file tree
Showing 19 changed files with 120 additions and 168 deletions.
2 changes: 1 addition & 1 deletion cabal.project
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ packages:
./hls-plugin-api
./hls-test-utils

index-state: 2024-04-30T10:44:19Z
index-state: 2024-05-10T00:00:00Z

tests: True
test-show-details: direct
Expand Down
2 changes: 1 addition & 1 deletion ghcide/ghcide.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ library
, implicit-hie >= 0.1.4.0 && < 0.1.5
, lens
, list-t
, lsp ^>=2.5.0.0
, lsp ^>=2.6.0.0
, lsp-types ^>=2.2.0.0
, mtl
, opentelemetry >=0.6.1
Expand Down
134 changes: 39 additions & 95 deletions ghcide/src/Development/IDE/Core/ProgressReporting.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module Development.IDE.Core.ProgressReporting
( ProgressEvent(..)
, ProgressReporting(..)
, noProgressReporting
, delayedProgressReporting
, progressReporting
-- utilities, reexported for use in Core.Shake
, mRunLspT
, mRunLspTCallback
Expand All @@ -12,31 +12,28 @@ module Development.IDE.Core.ProgressReporting
)
where

import Control.Concurrent.Async
import Control.Concurrent.STM.Stats (TVar, atomicallyNamed,
modifyTVar', newTVarIO,
readTVarIO)
import Control.Concurrent.Strict
import Control.Concurrent.STM.Stats (TVar, atomically,
atomicallyNamed, modifyTVar',
newTVarIO, readTVar, retry)
import Control.Concurrent.Strict (modifyVar_, newVar,
threadDelay)
import Control.Monad.Extra hiding (loop)
import Control.Monad.IO.Class
import Control.Monad.Trans.Class (lift)
import Data.Aeson (ToJSON (toJSON))
import Data.Foldable (for_)
import Data.Functor (($>))
import qualified Data.Text as T
import Data.Unique
import Development.IDE.GHC.Orphans ()
import Development.IDE.Graph hiding (ShakeValue)
import Development.IDE.Types.Location
import Development.IDE.Types.Options
import qualified Focus
import Language.LSP.Protocol.Message
import Language.LSP.Protocol.Types
import qualified Language.LSP.Protocol.Types as LSP
import Language.LSP.Server (ProgressAmount (..),
ProgressCancellable (..),
withProgress)
import qualified Language.LSP.Server as LSP
import qualified StmContainers.Map as STM
import System.Time.Extra
import UnliftIO.Exception (bracket_)
import UnliftIO (Async, async, cancel)

data ProgressEvent
= KickStarted
Expand Down Expand Up @@ -64,14 +61,14 @@ data State
-- | State transitions used in 'delayedProgressReporting'
data Transition = Event ProgressEvent | StopProgress

updateState :: IO (Async ()) -> Transition -> State -> IO State
updateState _ _ Stopped = pure Stopped
updateState start (Event KickStarted) NotStarted = Running <$> start
updateState start (Event KickStarted) (Running a) = cancel a >> Running <$> start
updateState _ (Event KickCompleted) (Running a) = cancel a $> NotStarted
updateState _ (Event KickCompleted) st = pure st
updateState _ StopProgress (Running a) = cancel a $> Stopped
updateState _ StopProgress st = pure st
updateState :: IO () -> Transition -> State -> IO State
updateState _ _ Stopped = pure Stopped
updateState start (Event KickStarted) NotStarted = Running <$> async start
updateState start (Event KickStarted) (Running job) = cancel job >> Running <$> async start
updateState _ (Event KickCompleted) (Running job) = cancel job $> NotStarted
updateState _ (Event KickCompleted) st = pure st
updateState _ StopProgress (Running job) = cancel job $> Stopped
updateState _ StopProgress st = pure st

-- | Data structure to track progress across the project
data InProgressState = InProgressState
Expand All @@ -93,7 +90,7 @@ recordProgress InProgressState{..} file shift = do
(Just 0, 0) -> pure ()
(Just 0, _) -> modifyTVar' doneVar pred
(Just _, 0) -> modifyTVar' doneVar (+1)
(Just _, _) -> pure()
(Just _, _) -> pure ()
where
alterPrevAndNew = do
prev <- Focus.lookup
Expand All @@ -102,91 +99,38 @@ recordProgress InProgressState{..} file shift = do
return (prev, new)
alter x = let x' = maybe (shift 0) shift x in Just x'

-- | A 'ProgressReporting' that enqueues Begin and End notifications in a new
-- thread, with a grace period (nothing will be sent if 'KickCompleted' arrives
-- before the end of the grace period).
delayedProgressReporting
:: Seconds -- ^ Grace period before starting
-> Seconds -- ^ sampling delay
-> Maybe (LSP.LanguageContextEnv c)
progressReporting
:: Maybe (LSP.LanguageContextEnv c)
-> ProgressReportingStyle
-> IO ProgressReporting
delayedProgressReporting _before _after Nothing _optProgressStyle = noProgressReporting
delayedProgressReporting before after (Just lspEnv) optProgressStyle = do
progressReporting Nothing _optProgressStyle = noProgressReporting
progressReporting (Just lspEnv) optProgressStyle = do
inProgressState <- newInProgress
progressState <- newVar NotStarted
let progressUpdate event = updateStateVar $ Event event
progressStop = updateStateVar StopProgress
updateStateVar = modifyVar_ progressState . updateState (lspShakeProgress inProgressState)

progressStop = updateStateVar StopProgress
updateStateVar = modifyVar_ progressState . updateState (lspShakeProgressNew inProgressState)
inProgress = updateStateForFile inProgressState
return ProgressReporting{..}
where
lspShakeProgress InProgressState{..} = do
-- first sleep a bit, so we only show progress messages if it's going to take
-- a "noticable amount of time" (we often expect a thread kill to arrive before the sleep finishes)
liftIO $ sleep before
u <- ProgressToken . InR . T.pack . show . hashUnique <$> liftIO newUnique

b <- liftIO newBarrier
void $ LSP.runLspT lspEnv $ LSP.sendRequest SMethod_WindowWorkDoneProgressCreate
LSP.WorkDoneProgressCreateParams { _token = u } $ liftIO . signalBarrier b
liftIO $ async $ do
ready <- waitBarrier b
LSP.runLspT lspEnv $ for_ ready $ const $ bracket_ (start u) (stop u) (loop u 0)
lspShakeProgressNew :: InProgressState -> IO ()
lspShakeProgressNew InProgressState{..} =
LSP.runLspT lspEnv $ withProgress "Processing" Nothing NotCancellable $ \update -> loop update 0
where
start token = LSP.sendNotification SMethod_Progress $
LSP.ProgressParams
{ _token = token
, _value = toJSON $ WorkDoneProgressBegin
{ _kind = AString @"begin"
, _title = "Processing"
, _cancellable = Nothing
, _message = Nothing
, _percentage = Nothing
}
}
stop token = LSP.sendNotification SMethod_Progress
LSP.ProgressParams
{ _token = token
, _value = toJSON $ WorkDoneProgressEnd
{ _kind = AString @"end"
, _message = Nothing
}
}
loop _ _ | optProgressStyle == NoProgress =
forever $ liftIO $ threadDelay maxBound
loop token prevPct = do
done <- liftIO $ readTVarIO doneVar
todo <- liftIO $ readTVarIO todoVar
liftIO $ sleep after
if todo == 0 then loop token 0 else do
let
nextFrac :: Double
nextFrac = fromIntegral done / fromIntegral todo
loop _ _ | optProgressStyle == NoProgress = forever $ liftIO $ threadDelay maxBound
loop update prevPct = do
(todo, done, nextPct) <- liftIO $ atomically $ do
todo <- readTVar todoVar
done <- readTVar doneVar
let nextFrac :: Double
nextFrac = if todo == 0 then 0 else fromIntegral done / fromIntegral todo
nextPct :: UInt
nextPct = floor $ 100 * nextFrac
when (nextPct /= prevPct) $
LSP.sendNotification SMethod_Progress $
LSP.ProgressParams
{ _token = token
, _value = case optProgressStyle of
Explicit -> toJSON $ WorkDoneProgressReport
{ _kind = AString @"report"
, _cancellable = Nothing
, _message = Just $ T.pack $ show done <> "/" <> show todo
, _percentage = Nothing
}
Percentage -> toJSON $ WorkDoneProgressReport
{ _kind = AString @"report"
, _cancellable = Nothing
, _message = Nothing
, _percentage = Just nextPct
}
NoProgress -> error "unreachable"
}
loop token nextPct
when (nextPct == prevPct) retry
pure (todo, done, nextPct)

update (ProgressAmount (Just nextPct) (Just $ T.pack $ show done <> "/" <> show todo))
loop update nextPct
updateStateForFile inProgress file = actionBracket (f succ) (const $ f pred) . const
-- This functions are deliberately eta-expanded to avoid space leaks.
-- Do not remove the eta-expansion without profiling a session with at
Expand Down
5 changes: 2 additions & 3 deletions ghcide/src/Development/IDE/Core/Shake.hs
Original file line number Diff line number Diff line change
Expand Up @@ -660,10 +660,9 @@ shakeOpen recorder lspEnv defaultConfig idePlugins debouncer
atomically $ modifyTVar' exportsMap (<> em)
logWith recorder Debug $ LogCreateHieDbExportsMapFinish (ExportsMap.size em)

progress <- do
let (before, after) = if testing then (0,0.1) else (0.1,0.1)
progress <-
if reportProgress
then delayedProgressReporting before after lspEnv optProgressStyle
then progressReporting lspEnv optProgressStyle
else noProgressReporting
actionQueue <- newQueue

Expand Down
1 change: 1 addition & 0 deletions ghcide/src/Development/IDE/LSP/LanguageServer.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE NumericUnderscores #-}
-- WARNING: A copy of DA.Daml.LanguageServer, try to keep them in sync
-- This version removes the daml: handling
module Development.IDE.LSP.LanguageServer
Expand Down
14 changes: 12 additions & 2 deletions ghcide/src/Development/IDE/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,15 @@ defaultArguments recorder plugins = Arguments
{ optCheckProject = pure $ checkProject config
, optCheckParents = pure $ checkParents config
}
, argsLspOptions = def {LSP.optCompletionTriggerCharacters = Just "."}
, argsLspOptions = def
{ LSP.optCompletionTriggerCharacters = Just "."
-- Generally people start to notice that something is taking a while at about 1s, so
-- that's when we start reporting progress
, LSP.optProgressStartDelay = 1_00_000
-- Once progress is being reported, it's nice to see that it's moving reasonably quickly,
-- but not so fast that it's ugly. This number is a bit made up
, LSP.optProgressUpdateDelay = 1_00_000
}
, argsDefaultHlsConfig = def
, argsGetHieDbLoc = getHieDbLoc
, argsDebouncer = newAsyncDebouncer
Expand Down Expand Up @@ -266,7 +274,7 @@ defaultArguments recorder plugins = Arguments
testing :: Recorder (WithPriority Log) -> IdePlugins IdeState -> Arguments
testing recorder plugins =
let
arguments@Arguments{ argsHlsPlugins, argsIdeOptions } =
arguments@Arguments{ argsHlsPlugins, argsIdeOptions, argsLspOptions } =
defaultArguments recorder plugins
hlsPlugins = pluginDescToIdePlugins $
idePluginsToPluginDesc argsHlsPlugins
Expand All @@ -276,10 +284,12 @@ testing recorder plugins =
defOptions = argsIdeOptions config sessionLoader
in
defOptions{ optTesting = IdeTesting True }
lspOptions = argsLspOptions { LSP.optProgressStartDelay = 0, LSP.optProgressUpdateDelay = 0 }
in
arguments
{ argsHlsPlugins = hlsPlugins
, argsIdeOptions = ideOptions
, argsLspOptions = lspOptions
}

defaultMain :: Recorder (WithPriority Log) -> Arguments -> IO ()
Expand Down
3 changes: 1 addition & 2 deletions ghcide/test/exe/THTests.hs
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,7 @@ thLinkingTest unboxed = testCase name $ runWithExtraFiles dir $ \dir -> do
-- modify b too
let bSource' = T.unlines $ init (T.lines bSource) ++ ["$th"]
changeDoc bdoc [TextDocumentContentChangeEvent . InR $ TextDocumentContentChangeWholeDocument bSource']
waitForProgressBegin
waitForAllProgressDone
waitForDiagnostics

Check warning on line 183 in ghcide/test/exe/THTests.hs

View workflow job for this annotation

GitHub Actions / caching (9.6, macOS-latest)

A do-notation statement discarded a result of type ‘[Diagnostic]’

Check warning on line 183 in ghcide/test/exe/THTests.hs

View workflow job for this annotation

GitHub Actions / caching (9.6, ubuntu-latest)

A do-notation statement discarded a result of type ‘[Diagnostic]’

Check warning on line 183 in ghcide/test/exe/THTests.hs

View workflow job for this annotation

GitHub Actions / caching (9.6, windows-latest)

A do-notation statement discarded a result of type ‘[Diagnostic]’

Check warning on line 183 in ghcide/test/exe/THTests.hs

View workflow job for this annotation

GitHub Actions / caching (9.6, ubuntu-latest)

A do-notation statement discarded a result of type ‘[Diagnostic]’

Check warning on line 183 in ghcide/test/exe/THTests.hs

View workflow job for this annotation

GitHub Actions / caching (9.6, macOS-latest)

A do-notation statement discarded a result of type ‘[Diagnostic]’

Check warning on line 183 in ghcide/test/exe/THTests.hs

View workflow job for this annotation

GitHub Actions / caching (9.6, windows-latest)

A do-notation statement discarded a result of type ‘[Diagnostic]’

Check warning on line 183 in ghcide/test/exe/THTests.hs

View workflow job for this annotation

GitHub Actions / caching (9.6, ubuntu-latest)

A do-notation statement discarded a result of type ‘[Diagnostic]’

Check warning on line 183 in ghcide/test/exe/THTests.hs

View workflow job for this annotation

GitHub Actions / caching (9.6, macOS-latest)

A do-notation statement discarded a result of type ‘[Diagnostic]’

Check warning on line 183 in ghcide/test/exe/THTests.hs

View workflow job for this annotation

GitHub Actions / caching (9.6, windows-latest)

A do-notation statement discarded a result of type ‘[Diagnostic]’

Check warning on line 183 in ghcide/test/exe/THTests.hs

View workflow job for this annotation

GitHub Actions / caching (9.6, ubuntu-latest)

A do-notation statement discarded a result of type ‘[Diagnostic]’

Check warning on line 183 in ghcide/test/exe/THTests.hs

View workflow job for this annotation

GitHub Actions / caching (9.6, macOS-latest)

A do-notation statement discarded a result of type ‘[Diagnostic]’

Check warning on line 183 in ghcide/test/exe/THTests.hs

View workflow job for this annotation

GitHub Actions / caching (9.6, windows-latest)

A do-notation statement discarded a result of type ‘[Diagnostic]’

expectCurrentDiagnostics bdoc [(DiagnosticSeverity_Warning, (4,1), "Top-level binding")]

Expand Down
14 changes: 7 additions & 7 deletions haskell-language-server.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ library hls-cabal-plugin
, hls-plugin-api == 2.8.0.0
, hls-graph == 2.8.0.0
, lens
, lsp ^>=2.5
, lsp ^>=2.6
, lsp-types ^>=2.2
, regex-tdfa ^>=1.3.1
, text
Expand Down Expand Up @@ -389,7 +389,7 @@ library hls-call-hierarchy-plugin
, hiedb ^>= 0.6.0.0
, hls-plugin-api == 2.8.0.0
, lens
, lsp >=2.5
, lsp >=2.6
, sqlite-simple
, text

Expand Down Expand Up @@ -1002,7 +1002,7 @@ library hls-alternate-number-format-plugin
, hls-graph
, hls-plugin-api == 2.8.0.0
, lens
, lsp ^>=2.5
, lsp ^>=2.6
, mtl
, regex-tdfa
, syb
Expand Down Expand Up @@ -1232,7 +1232,7 @@ library hls-gadt-plugin
, hls-plugin-api == 2.8.0.0
, haskell-language-server:hls-refactor-plugin
, lens
, lsp >=2.5
, lsp >=2.6
, mtl
, text
, transformers
Expand Down Expand Up @@ -1281,7 +1281,7 @@ library hls-explicit-fixity-plugin
, ghcide == 2.8.0.0
, hashable
, hls-plugin-api == 2.8.0.0
, lsp >=2.5
, lsp >=2.6
, text

default-extensions: DataKinds
Expand Down Expand Up @@ -1736,7 +1736,7 @@ library hls-semantic-tokens-plugin
, ghcide == 2.8.0.0
, hls-plugin-api == 2.8.0.0
, lens
, lsp >=2.5
, lsp >=2.6
, text
, transformers
, bytestring
Expand Down Expand Up @@ -1804,7 +1804,7 @@ library hls-notes-plugin
, hls-graph == 2.8.0.0
, hls-plugin-api == 2.8.0.0
, lens
, lsp >=2.5
, lsp >=2.6
, mtl >= 2.2
, regex-tdfa >= 1.3.1
, text
Expand Down
2 changes: 1 addition & 1 deletion hls-plugin-api/hls-plugin-api.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ library
, hls-graph == 2.8.0.0
, lens
, lens-aeson
, lsp ^>=2.5
, lsp ^>=2.6
, megaparsec >=9.0
, mtl
, opentelemetry >=0.4
Expand Down
5 changes: 2 additions & 3 deletions plugins/hls-change-type-signature-plugin/test/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ import Test.Hls (CodeAction (..), Command,
mkPluginTestDescriptor',
openDoc, runSessionWithServer,
testCase, testGroup, toEither,
type (|?),
waitForAllProgressDone,
type (|?), waitForBuildQueue,
waitForDiagnostics, (@?=))
import Text.Regex.TDFA ((=~))

Expand Down Expand Up @@ -96,7 +95,7 @@ goldenChangeSignature fp = goldenWithHaskellDoc def changeTypeSignaturePlugin (f
codeActionTest :: FilePath -> Int -> Int -> TestTree
codeActionTest fp line col = goldenChangeSignature fp $ \doc -> do
void waitForDiagnostics -- code actions are triggered from Diagnostics
void waitForAllProgressDone -- apparently some tests need this to get the CodeAction to show up
void waitForBuildQueue -- apparently some tests need this to get the CodeAction to show up
actions <- getCodeActions doc (pointRange line col)
foundActions <- findChangeTypeActions actions
liftIO $ length foundActions @?= 1
Expand Down
5 changes: 4 additions & 1 deletion plugins/hls-eval-plugin/test/testdata/TIO.expected.hs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
-- IO expressions are supported, stdout/stderr output is ignored
module TIO where

import Control.Concurrent (threadDelay)

{-
Does not capture stdout, returns value.
Has a delay in order to show progress reporting.
>>> print "ABC" >> return "XYZ"
>>> threadDelay 2000000 >> print "ABC" >> return "XYZ"
"XYZ"
-}
Loading

0 comments on commit 0651c5c

Please sign in to comment.