diff --git a/ChangeLog.md b/ChangeLog.md index 68d001175..be5a7add6 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,9 @@ # Revision history for haskell-lsp +## 0.9.0.0 + +* Add `MarkupContent` to `HoverResponse`, and (some) json roundtrip tests. + ## 0.8.2.0 -- 2019-04-11 * Add `applyTextEdit` and `editTextEdit` helpers diff --git a/example/Main.hs b/example/Main.hs index cf7667c6a..c99151d78 100644 --- a/example/Main.hs +++ b/example/Main.hs @@ -238,7 +238,7 @@ reactor lf inp = do let ht = Just $ J.Hover ms (Just range) - ms = J.List [J.CodeString $ J.LanguageString "lsp-hello" "TYPE INFO" ] + ms = J.HoverContentsMS $ J.List [J.CodeString $ J.LanguageString "lsp-hello" "TYPE INFO" ] range = J.Range pos pos reactorSend $ RspHover $ Core.makeResponseMessage req ht diff --git a/haskell-lsp-types/ChangeLog.md b/haskell-lsp-types/ChangeLog.md index 34f4e035d..aafe65116 100644 --- a/haskell-lsp-types/ChangeLog.md +++ b/haskell-lsp-types/ChangeLog.md @@ -1,5 +1,9 @@ # Revision history for haskell-lsp-types +## 0.8.3.0 + +* Add `MarkupContent` to `HoverResponse`, and (some) json roundtrip tests. + ## 0.8.2.0 -- 2019-04-11 * Add `applyTextEdit` and `editTextEdit` helpers diff --git a/haskell-lsp-types/haskell-lsp-types.cabal b/haskell-lsp-types/haskell-lsp-types.cabal index ec5f16973..4a1b85901 100644 --- a/haskell-lsp-types/haskell-lsp-types.cabal +++ b/haskell-lsp-types/haskell-lsp-types.cabal @@ -1,5 +1,5 @@ name: haskell-lsp-types -version: 0.8.2.0 +version: 0.9.0.0 synopsis: Haskell library for the Microsoft Language Server Protocol, data types description: An implementation of the types to allow language implementors to diff --git a/haskell-lsp-types/src/Language/Haskell/LSP/Types/DataTypesJSON.hs b/haskell-lsp-types/src/Language/Haskell/LSP/Types/DataTypesJSON.hs index 7fd2d965e..a1d05e86e 100644 --- a/haskell-lsp-types/src/Language/Haskell/LSP/Types/DataTypesJSON.hs +++ b/haskell-lsp-types/src/Language/Haskell/LSP/Types/DataTypesJSON.hs @@ -5,7 +5,6 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeSynonymInstances #-} @@ -18,14 +17,15 @@ import Data.Aeson.TH import Data.Aeson.Types import Data.Text (Text) import qualified Data.Text as T +import Language.Haskell.LSP.Types.ClientCapabilities import Language.Haskell.LSP.Types.Command import Language.Haskell.LSP.Types.Constants -import Language.Haskell.LSP.Types.ClientCapabilities import Language.Haskell.LSP.Types.Diagnostic import Language.Haskell.LSP.Types.DocumentFilter import Language.Haskell.LSP.Types.List -import Language.Haskell.LSP.Types.Message import Language.Haskell.LSP.Types.Location +import Language.Haskell.LSP.Types.MarkupContent +import Language.Haskell.LSP.Types.Message import Language.Haskell.LSP.Types.Symbol import Language.Haskell.LSP.Types.TextDocument import Language.Haskell.LSP.Types.Uri @@ -499,7 +499,7 @@ interface ServerCapabilities { * * Since 3.10.0 */ - foldingRangeProvider?: boolean | FoldingRangeProviderOptions | (FoldingRangeProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions); + foldingRangeProvider?: boolean | FoldingRangeProviderOptions | (FoldingRangeProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions); /** * The server provides execute command support. */ @@ -544,7 +544,7 @@ data TDS = TDSOptions TextDocumentSyncOptions instance FromJSON TDS where parseJSON x = TDSOptions <$> parseJSON x <|> TDSKind <$> parseJSON x - + instance ToJSON TDS where toJSON (TDSOptions x) = toJSON x toJSON (TDSKind x) = toJSON x @@ -553,7 +553,7 @@ data GotoOptions = GotoOptionsStatic Bool | GotoOptionsDynamic { -- | A document selector to identify the scope of the registration. If set to null -- the document selector provided on the client side will be used. - _documentSelector :: Maybe DocumentSelector + _documentSelector :: Maybe DocumentSelector -- | The id used to register the request. The id can be used to deregister -- the request again. See also Registration#id. , _id :: Maybe Text @@ -624,7 +624,7 @@ data WorkspaceOptions = deriving (Show, Read, Eq) deriveJSON lspOptions ''WorkspaceOptions - + data InitializeResponseCapabilitiesInner = InitializeResponseCapabilitiesInner { -- | Defines how text documents are synced. Is either a detailed structure @@ -1232,19 +1232,19 @@ Request: method: ‘workspace/configuration’ params: ConfigurationParams defined as follows export interface ConfigurationParams { - items: ConfigurationItem[]; + items: ConfigurationItem[]; } export interface ConfigurationItem { - /** - * The scope to get the configuration section for. - */ - scopeUri?: string; - - /** - * The configuration section asked for. - */ - section?: string; + /** + * The scope to get the configuration section for. + */ + scopeUri?: string; + + /** + * The configuration section asked for. + */ + section?: string; } Response: @@ -1731,26 +1731,28 @@ Response result: Hover | null defined as follows: + /** - * The result of a hove request. + * The result of a hover request. */ interface Hover { - /** - * The hover's content - */ - contents: MarkedString | MarkedString[]; + /** + * The hover's content + */ + contents: MarkedString | MarkedString[] | MarkupContent; - /** - * An optional range - */ - range?: Range; + /** + * An optional range is a range inside a text document + * that is used to visualize a hover, e.g. by changing the background color. + */ + range?: Range; } -Where MarkedString is defined as follows: + /** * MarkedString can be used to render human readable text. It is either a markdown string * or a code-block that provides a language and a code snippet. The language identifier - * is sematically equal to the optional language identifier in fenced code blocks in GitHub + * is semantically equal to the optional language identifier in fenced code blocks in GitHub * issues. See https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting * * The pair of a language and a value is an equivalent to markdown: @@ -1759,7 +1761,8 @@ Where MarkedString is defined as follows: * ``` * * Note that markdown strings will be sanitized - that means html will be escaped. - */ +* @deprecated use MarkupContent instead. +*/ type MarkedString = string | { language: string; value: string }; error: code and message set in case an exception happens during the hover @@ -1777,6 +1780,7 @@ data LanguageString = deriveJSON lspOptions ''LanguageString +{-# DEPRECATED MarkedString, PlainString, CodeString "Use MarkupContent instead, since 3.3.0 (11/24/2017)" #-} data MarkedString = PlainString T.Text | CodeString LanguageString @@ -1789,9 +1793,52 @@ instance FromJSON MarkedString where parseJSON (A.String t) = pure $ PlainString t parseJSON o = CodeString <$> parseJSON o +-- ------------------------------------- + +data HoverContents = + HoverContentsMS (List MarkedString) + | HoverContents MarkupContent + | HoverContentsEmpty + deriving (Read,Show,Eq) + +instance ToJSON HoverContents where + toJSON (HoverContentsMS x) = toJSON x + toJSON (HoverContents x) = toJSON x + toJSON (HoverContentsEmpty) = A.Null +instance FromJSON HoverContents where + parseJSON v@(A.String _) = HoverContentsMS <$> parseJSON v + parseJSON (A.Null) = pure HoverContentsEmpty + parseJSON v@(A.Array _) = HoverContentsMS <$> parseJSON v + parseJSON v@(A.Object _) = HoverContents <$> parseJSON v + <|> HoverContentsMS <$> parseJSON v + parseJSON _ = mempty + +-- ------------------------------------- + +#if __GLASGOW_HASKELL__ >= 804 +instance Semigroup HoverContents where + (<>) = mappend +#endif + +instance Monoid HoverContents where + mempty = HoverContentsEmpty + + HoverContentsEmpty `mappend` hc = hc + hc `mappend` HoverContentsEmpty = hc + HoverContents h1 `mappend` HoverContents h2 = HoverContents (h1 `mappend` h2) + HoverContents h1 `mappend` HoverContentsMS (List h2s) = HoverContents (mconcat (h1: (map toMarkupContent h2s))) + HoverContentsMS (List h1s) `mappend` HoverContents h2 = HoverContents (mconcat ((map toMarkupContent h1s) ++ [h2])) + HoverContentsMS (List h1s) `mappend` HoverContentsMS (List h2s) = HoverContentsMS (List (h1s `mappend` h2s)) + +toMarkupContent :: MarkedString -> MarkupContent +toMarkupContent (PlainString s) = unmarkedUpContent s +toMarkupContent (CodeString (LanguageString lang s)) = markedUpContent lang s + +-- ------------------------------------- + data Hover = Hover - { _contents :: List MarkedString + { _contents :: HoverContents , _range :: Maybe Range } deriving (Read,Show,Eq) @@ -2852,4 +2899,3 @@ data TraceNotification = } deriving (Show, Read, Eq) deriveJSON lspOptions ''TraceNotification - diff --git a/haskell-lsp-types/src/Language/Haskell/LSP/Types/MarkupContent.hs b/haskell-lsp-types/src/Language/Haskell/LSP/Types/MarkupContent.hs index 03f3538a7..46e5d97de 100644 --- a/haskell-lsp-types/src/Language/Haskell/LSP/Types/MarkupContent.hs +++ b/haskell-lsp-types/src/Language/Haskell/LSP/Types/MarkupContent.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} @@ -11,6 +12,7 @@ module Language.Haskell.LSP.Types.MarkupContent where import Data.Aeson import Data.Aeson.TH +import Data.Semigroup import Data.Text (Text) import Language.Haskell.LSP.Types.Constants @@ -23,15 +25,15 @@ import Language.Haskell.LSP.Types.Constants * are reserved for internal usage. */ export namespace MarkupKind { - /** - * Plain text is supported as a content format - */ - export const PlainText: 'plaintext' = 'plaintext'; - - /** - * Markdown is supported as a content format - */ - export const Markdown: 'markdown' = 'markdown'; + /** + * Plain text is supported as a content format + */ + export const PlainText: 'plaintext' = 'plaintext'; + + /** + * Markdown is supported as a content format + */ + export const Markdown: 'markdown' = 'markdown'; } export type MarkupKind = 'plaintext' | 'markdown'; -} @@ -78,15 +80,15 @@ instance FromJSON MarkupKind where * remove HTML from the markdown to avoid script execution. */ export interface MarkupContent { - /** - * The type of the Markup - */ - kind: MarkupKind; - - /** - * The content itself - */ - value: string; + /** + * The type of the Markup + */ + kind: MarkupKind; + + /** + * The content itself + */ + value: string; } -} @@ -120,3 +122,37 @@ data MarkupContent = deriving (Read, Show, Eq) deriveJSON lspOptions ''MarkupContent + +-- --------------------------------------------------------------------- + +-- | Create a 'MarkupContent' containing a quoted language string only. +markedUpContent :: Text -> Text -> MarkupContent +markedUpContent lang quote + = MarkupContent MkMarkdown ("```" <> lang <> "\n" <> quote <> "\n```\n") + +-- --------------------------------------------------------------------- + +-- | Create a 'MarkupContent' containing unquoted text +unmarkedUpContent :: Text -> MarkupContent +unmarkedUpContent str = MarkupContent MkPlainText str + +-- --------------------------------------------------------------------- + +-- | Markdown for a section separator in Markdown, being a horizontal line +sectionSeparator :: Text +sectionSeparator = "*\t*\t*\n" + +-- --------------------------------------------------------------------- + +#if __GLASGOW_HASKELL__ >= 804 +instance Semigroup MarkupContent where + (<>) = mappend +#endif + +instance Monoid MarkupContent where + mempty = MarkupContent MkPlainText "" + MarkupContent MkPlainText s1 `mappend` MarkupContent MkPlainText s2 = MarkupContent MkPlainText (s1 `mappend` s2) + MarkupContent MkMarkdown s1 `mappend` MarkupContent _ s2 = MarkupContent MkMarkdown (s1 `mappend` s2) + MarkupContent _ s1 `mappend` MarkupContent MkMarkdown s2 = MarkupContent MkMarkdown (s1 `mappend` s2) + +-- --------------------------------------------------------------------- diff --git a/haskell-lsp.cabal b/haskell-lsp.cabal index 0dd464192..b0e32c3f2 100644 --- a/haskell-lsp.cabal +++ b/haskell-lsp.cabal @@ -1,5 +1,5 @@ name: haskell-lsp -version: 0.8.2.0 +version: 0.9.0.0 synopsis: Haskell library for the Microsoft Language Server Protocol description: An implementation of the types, and basic message server to @@ -44,7 +44,7 @@ library , filepath , hslogger , hashable - , haskell-lsp-types >= 0.8 + , haskell-lsp-types >= 0.8.3 , lens >= 4.15.2 , mtl , network-uri @@ -94,30 +94,34 @@ test-suite haskell-lsp-test main-is: Main.hs other-modules: Spec CapabilitiesSpec + JsonSpec DiagnosticsSpec MethodSpec ServerCapabilitiesSpec + TypesSpec URIFilePathSpec VspSpec WorkspaceEditSpec WorkspaceFoldersSpec build-depends: base + , QuickCheck , aeson , bytestring , containers , data-default , directory , filepath - , hspec , hashable + , haskell-lsp + , hspec -- , hspec-jenkins , lens >= 4.15.2 , network-uri + , quickcheck-instances , sorted-list == 0.2.1.* - , yi-rope - , haskell-lsp - , text , stm + , text + , yi-rope ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall default-language: Haskell2010 diff --git a/test/JsonSpec.hs b/test/JsonSpec.hs new file mode 100644 index 000000000..a69b8d3a3 --- /dev/null +++ b/test/JsonSpec.hs @@ -0,0 +1,71 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeSynonymInstances #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} +-- | Test for JSON serialization +module JsonSpec where + +import Language.Haskell.LSP.Types + +import Data.Aeson +import Test.Hspec +import Test.Hspec.QuickCheck +import Test.QuickCheck hiding (Success) +import Test.QuickCheck.Instances () + +-- import Debug.Trace +-- --------------------------------------------------------------------- + +{-# ANN module ("HLint: ignore Redundant do" :: String) #-} + +main :: IO () +main = hspec spec + +spec :: Spec +spec = describe "dispatcher" jsonSpec + +-- --------------------------------------------------------------------- + +jsonSpec :: Spec +jsonSpec = do + describe "General JSON instances round trip" $ do + -- DataTypesJSON + prop "LanguageString" (propertyJsonRoundtrip :: LanguageString -> Bool) + prop "MarkedString" (propertyJsonRoundtrip :: MarkedString -> Bool) + prop "MarkupContent" (propertyJsonRoundtrip :: MarkupContent -> Bool) + prop "HoverContents" (propertyJsonRoundtrip :: HoverContents -> Bool) + + +-- --------------------------------------------------------------------- + +propertyJsonRoundtrip :: (Eq a, ToJSON a, FromJSON a) => a -> Bool +propertyJsonRoundtrip a = Success a == fromJSON (toJSON a) + +-- --------------------------------------------------------------------- + +instance Arbitrary LanguageString where + arbitrary = LanguageString <$> arbitrary <*> arbitrary + +instance Arbitrary MarkedString where + arbitrary = oneof [PlainString <$> arbitrary, CodeString <$> arbitrary] + +instance Arbitrary MarkupContent where + arbitrary = MarkupContent <$> arbitrary <*> arbitrary + +instance Arbitrary MarkupKind where + arbitrary = oneof [pure MkPlainText,pure MkMarkdown] + +instance Arbitrary HoverContents where + arbitrary = oneof [ HoverContentsMS <$> arbitrary + , HoverContents <$> arbitrary + , pure HoverContentsEmpty] + +-- | make lists of maximum length 3 for test performance +smallList :: Gen a -> Gen [a] +smallList = resize 3 . listOf + +instance (Arbitrary a) => Arbitrary (List a) where + arbitrary = List <$> arbitrary + +-- --------------------------------------------------------------------- diff --git a/test/ServerCapabilitiesSpec.hs b/test/ServerCapabilitiesSpec.hs index 2f2142b4b..6f4790d13 100644 --- a/test/ServerCapabilitiesSpec.hs +++ b/test/ServerCapabilitiesSpec.hs @@ -24,7 +24,7 @@ spec = describe "server capabilities" $ do describe "encodes" $ it "just id" $ encode (FoldingRangeOptionsDynamicDocument Nothing (Just "foo")) `shouldBe` "{\"id\":\"foo\"}" - it "decodes" $ + it "decodes" $ let input = "{\"hoverProvider\": true, \"colorProvider\": {\"id\": \"abc123\", \"documentSelector\": " <> documentFiltersJson <> "}}" Just caps = decode input :: Maybe InitializeResponseCapabilitiesInner in caps ^. colorProvider `shouldBe` Just (ColorOptionsDynamicDocument (Just documentFilters) (Just "abc123")) diff --git a/test/TypesSpec.hs b/test/TypesSpec.hs new file mode 100644 index 000000000..42b473986 --- /dev/null +++ b/test/TypesSpec.hs @@ -0,0 +1,31 @@ +{-# LANGUAGE OverloadedStrings #-} +module TypesSpec where + +import Data.Monoid +import qualified Language.Haskell.LSP.Types as J +import Test.Hspec + +-- --------------------------------------------------------------------- + +main :: IO () +main = hspec spec + +spec :: Spec +spec = diagnosticsSpec + +-- --------------------------------------------------------------------- + +diagnosticsSpec :: Spec +diagnosticsSpec = do + describe "MarkupContent" $ do + it "appends two plainstrings" $ do + J.unmarkedUpContent "string1\n" <> J.unmarkedUpContent "string2\n" + `shouldBe` J.unmarkedUpContent "string1\nstring2\n" + it "appends a marked up and a plain string" $ do + J.markedUpContent "haskell" "foo :: Int" <> J.unmarkedUpContent "string2\n" + `shouldBe` J.MarkupContent J.MkMarkdown "```haskell\nfoo :: Int\n```\nstring2\n" + it "appends a plain string and a marked up string" $ do + J.unmarkedUpContent "string2\n" <> J.markedUpContent "haskell" "foo :: Int" + `shouldBe` J.MarkupContent J.MkMarkdown "string2\n```haskell\nfoo :: Int\n```\n" + +-- --------------------------------------------------------------------- diff --git a/test/WorkspaceEditSpec.hs b/test/WorkspaceEditSpec.hs index 481ca7994..1ae19002c 100644 --- a/test/WorkspaceEditSpec.hs +++ b/test/WorkspaceEditSpec.hs @@ -20,6 +20,6 @@ spec = do describe "editTextEdit" $ it "edits a multiline text edit" $ let orig = TextEdit (Range (Position 1 1) (Position 2 2)) "hello\nworld" - inner = TextEdit (Range (Position 0 3) (Position 1 3)) "ios\ngo" + inner = TextEdit (Range (Position 0 3) (Position 1 3)) "ios\ngo" expected = TextEdit (Range (Position 1 1) (Position 2 2)) "helios\ngold" in editTextEdit orig inner `shouldBe` expected diff --git a/test/WorkspaceFoldersSpec.hs b/test/WorkspaceFoldersSpec.hs index 747904bf1..9f176d6ef 100644 --- a/test/WorkspaceFoldersSpec.hs +++ b/test/WorkspaceFoldersSpec.hs @@ -32,7 +32,7 @@ spec = describe "workspace folders" $ in handleMessage initCb tvarCtx clStr jsonStr let starterWorkspaces = List [wf0] - initParams = InitializeParams + initParams = InitializeParams Nothing Nothing (Just (Uri "/foo")) Nothing fullCaps Nothing (Just starterWorkspaces) initMsg :: InitializeRequest initMsg = RequestMessage "2.0" (IdInt 0) Initialize initParams