packages feed

haskell-language-server-2.10.0.0: ghcide-test/exe/ResolveTests.hs

{-# LANGUAGE DeriveAnyClass     #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE OverloadedStrings  #-}
module ResolveTests (tests) where

import           Config
import           Control.Lens
import           Data.Aeson
import qualified Data.Maybe                    as Maybe
import           Data.Text                     (Text)
import qualified Data.Text                     as T
import           GHC.Generics
import           Ide.Logger
import           Ide.Types                     (PluginDescriptor (..), PluginId,
                                                defaultPluginDescriptor,
                                                mkPluginHandler,
                                                mkResolveHandler)
import qualified Language.LSP.Protocol.Lens    as J
import qualified Language.LSP.Protocol.Lens    as JL
import           Language.LSP.Protocol.Message (SomeMethod (..))
import qualified Language.LSP.Protocol.Message as LSP
import           Language.LSP.Protocol.Types
import           Language.LSP.Test             hiding (resolveCompletion)
import           Test.Hls                      (IdeState, SMethod (..), liftIO,
                                                mkPluginTestDescriptor,
                                                someMethodToMethodString,
                                                waitForAllProgressDone)
import qualified Test.Hls.FileSystem           as FS
import           Test.Tasty
import           Test.Tasty.HUnit

tests :: TestTree
tests = testGroup "resolve"
  [ testGroup "with and without data" resolveRequests
  ]

removeData :: JL.HasData_ s (Maybe a) => s -> s
removeData param = param & JL.data_ .~ Nothing

simpleTestSession :: TestName -> Session () -> TestTree
simpleTestSession name act =
    testCase name $ runWithResolvePlugin (mkIdeTestFs [FS.directCradle ["A.hs"]]) (const act)

runWithResolvePlugin :: FS.VirtualFileTree -> (FilePath -> Session a) -> IO a
runWithResolvePlugin fs =
    testSessionWithPlugin fs
        (mkPluginTestDescriptor resolvePluginDescriptor "resolve-plugin")

data CompletionItemResolveData = CompletionItemResolveData
    { completionItemResolve_number :: Int
    }
    deriving stock (Show, Eq, Ord, Generic)
    deriving anyclass (ToJSON, FromJSON)

data CodeActionResolve = CodeActionResolve
    { codeActionResolve_number :: Int
    }
    deriving stock (Show, Eq, Ord, Generic)
    deriving anyclass (ToJSON, FromJSON)

data CodeLensResolve = CodeLensResolve
    { codeLensResolve_number :: Int
    }
    deriving stock (Show, Eq, Ord, Generic)
    deriving anyclass (ToJSON, FromJSON)

resolvePluginDescriptor :: Recorder (WithPriority Text) -> PluginId -> PluginDescriptor IdeState
resolvePluginDescriptor recorder pid = (defaultPluginDescriptor pid "Test Plugin for Resolve Requests")
  { pluginHandlers = mconcat
      [ mkResolveHandler LSP.SMethod_CompletionItemResolve $ \_ _ param _ CompletionItemResolveData{} -> pure param
      , mkPluginHandler LSP.SMethod_TextDocumentCompletion $ \_ _ _ -> do
          pure $ InL
            [ defCompletionItem "test item without data"
            , defCompletionItem "test item with data"
                & J.data_ .~ Just (toJSON $ CompletionItemResolveData 100)
            ]
      , mkResolveHandler LSP.SMethod_CodeActionResolve $ \_ _ param _ CodeActionResolve{} -> pure param
      , mkPluginHandler LSP.SMethod_TextDocumentCodeAction $ \_ _ _ -> do
          logWith recorder Debug "Why is the handler not called?"
          pure $ InL
            [ InR $ defCodeAction "test item without data"
            , InR $ defCodeAction "test item with data"
                & J.data_ .~ Just (toJSON $ CodeActionResolve 70)
            ]
      , mkResolveHandler LSP.SMethod_CodeLensResolve $ \_ _ param _ CodeLensResolve{} -> pure param
      , mkPluginHandler LSP.SMethod_TextDocumentCodeLens $ \_ _ _ -> do
          pure $ InL
            [ defCodeLens "test item without data"
            , defCodeLens "test item with data"
                & J.data_ .~ Just (toJSON $ CodeLensResolve 50)
            ]
      ]
  }

resolveRequests :: [TestTree]
resolveRequests =
  [ simpleTestSession "completion resolve" $ do
      doc <- createDoc "A.hs" "haskell" $ T.unlines
        [ "module A where"
        , "data Foo = Foo { foo :: Int }"
        , "bar = Foo 4"
        ]
      waitForAllProgressDone
      items <- getCompletions doc (Position 2 7)
      let resolveCompItems = filter (\i -> "test item" `T.isPrefixOf` (i ^. J.label)) items
      liftIO $ assertEqual "There must be exactly two results" 2 (length resolveCompItems)
      -- This must not throw an error.
      _ <- traverse (resolveCompletion . removeData) resolveCompItems
      pure ()
  , simpleTestSession "codeAction resolve" $ do
      doc <- createDoc "A.hs" "haskell" $ T.unlines
        [ "module A where"
        , "data Foo = Foo { foo :: Int }"
        , "bar = Foo 4"
        ]
      waitForAllProgressDone
      -- Cant use 'getAllCodeActions', as this lsp-test function queries the diagnostic
      -- locations and we don't have diagnostics in these tests.
      cas <- Maybe.mapMaybe (preview _R) <$> getCodeActions doc (Range (Position 0 0) (Position 1 0))
      let resolveCas = filter (\i -> "test item" `T.isPrefixOf` (i ^. J.title)) cas
      liftIO $ assertEqual "There must be exactly two results" 2 (length resolveCas)
      -- This must not throw an error.
      _ <- traverse (resolveCodeAction . removeData) resolveCas
      pure ()
  , simpleTestSession "codelens resolve" $ do
      doc <- createDoc "A.hs" "haskell" $ T.unlines
        [ "module A where"
        , "data Foo = Foo { foo :: Int }"
        , "bar = Foo 4"
        ]
      waitForAllProgressDone
      cd <- getCodeLenses doc
      let resolveCodeLenses = filter (\i -> case i ^. J.command of
            Just cmd -> "test item" `T.isPrefixOf` (cmd ^. J.title)
            Nothing  -> False
            ) cd
      liftIO $ assertEqual "There must be exactly two results" 2 (length resolveCodeLenses)
      -- This must not throw an error.
      _ <- traverse (resolveCodeLens . removeData) resolveCodeLenses
      pure ()
  ]

defCompletionItem :: T.Text -> CompletionItem
defCompletionItem lbl = CompletionItem
    { _label = lbl
    , _labelDetails = Nothing
    , _kind = Nothing
    , _tags = Nothing
    , _detail = Nothing
    , _documentation = Nothing
    , _deprecated = Nothing
    , _preselect = Nothing
    , _sortText = Nothing
    , _filterText = Nothing
    , _insertText = Just "insertion"
    , _insertTextFormat = Nothing
    , _insertTextMode = Nothing
    , _textEdit = Nothing
    , _textEditText = Nothing
    , _additionalTextEdits = Nothing
    , _commitCharacters = Nothing
    , _command = Nothing
    , _data_ = Nothing
    }

defCodeAction :: T.Text -> CodeAction
defCodeAction lbl = CodeAction
    { _title = lbl
    , _kind = Just CodeActionKind_Refactor
    , _diagnostics = Nothing
    , _isPreferred = Nothing
    , _disabled = Nothing
    , _edit = Nothing
    , _command = Just $ Command
        { _title = lbl
        , _command = lbl
        , _arguments = Nothing
        }
    , _data_ = Nothing
    }

defCodeLens :: T.Text -> CodeLens
defCodeLens lbl = CodeLens
    { _range = mkRange 0 0 1 0
    , _command = Just $ Command
        { _title = lbl
        , _command = lbl
        , _arguments = Nothing
        }
    , _data_ = Nothing
    }

-- TODO: expose this from lsp-test
resolveCompletion :: CompletionItem -> Session CompletionItem
resolveCompletion item = do
  rsp <- request SMethod_CompletionItemResolve item
  case rsp ^. JL.result of
      Left err -> liftIO $ assertFailure (someMethodToMethodString (SomeMethod SMethod_CompletionItemResolve) <> " failed with: " <> show err)
      Right x -> pure x