packages feed

lsp-types 1.5.0.0 → 1.6.0.0

raw patch · 20 files changed

+1041/−58 lines, 20 filesdep +QuickCheckdep +exceptionsdep +hspecdep ~aesondep ~basedep ~filepathPVP ok

version bump matches the API change (PVP)

Dependencies added: QuickCheck, exceptions, hspec, lsp-types, quickcheck-instances, safe, tuple

Dependency ranges changed: aeson, base, filepath, network-uri

API changes (from Hackage documentation)

- Data.IxMap: type family Base f;
- Language.LSP.Types: normalizedFilePath :: NormalizedUri -> FilePath -> NormalizedFilePath
+ Data.IxMap: type Base f;
+ Language.LSP.Types: ErrorCodeCustom :: Int32 -> ErrorCode
+ Language.LSP.Types: RequestFailed :: ErrorCode
+ Language.LSP.Types: ServerCancelled :: ErrorCode
+ Language.LSP.Types: emptyNormalizedFilePath :: NormalizedFilePath
+ Language.LSP.Types: isSubrangeOf :: Range -> Range -> Bool
+ Language.LSP.Types: positionInRange :: Position -> Range -> Bool
- Language.LSP.Types: [CustomEq] :: (m1 ~ (CustomMethod :: Method f t1), m2 ~ (CustomMethod :: Method f t2)) => {runCustomEq :: t1 ~ t2 => m1 :~~: m2} -> CustomEq m1 m2
+ Language.LSP.Types: [CustomEq] :: (m1 ~ (CustomMethod :: Method f t1), m2 ~ (CustomMethod :: Method f t2)) => (t1 ~ t2 => m1 :~~: m2) -> CustomEq m1 m2
- Language.LSP.Types: [WorkspaceSemanticTokensRefresh] :: Method FromClient Request
+ Language.LSP.Types: [WorkspaceSemanticTokensRefresh] :: Method FromServer Request
- Language.LSP.Types: regHelper :: forall m_a648B x_a648C. SMethod m_a648B -> (Show (RegistrationOptions m_a648B) => ToJSON (RegistrationOptions m_a648B) => FromJSON (RegistrationOptions m_a648B) => x_a648C) -> x_a648C
+ Language.LSP.Types: regHelper :: forall m_a6IB6 x_a6IB7. SMethod m_a6IB6 -> (Show (RegistrationOptions m_a6IB6) => ToJSON (RegistrationOptions m_a6IB6) => FromJSON (RegistrationOptions m_a6IB6) => x_a6IB7) -> x_a6IB7
- Language.LSP.Types.Lens: _InL :: forall a_a6XhT b_aarW a_aarV. Prism ((|?) a_a6XhT b_aarW) ((|?) a_aarV b_aarW) a_a6XhT a_aarV
+ Language.LSP.Types.Lens: _InL :: forall a_a7FUY b_iqS3 a_iqS2. Prism ((|?) a_a7FUY b_iqS3) ((|?) a_iqS2 b_iqS3) a_a7FUY a_iqS2
- Language.LSP.Types.Lens: _InR :: forall a_aarV b_a6XhY b_aarW. Prism ((|?) a_aarV b_a6XhY) ((|?) a_aarV b_aarW) b_a6XhY b_aarW
+ Language.LSP.Types.Lens: _InR :: forall a_iqS2 b_a7FV3 b_iqS3. Prism ((|?) a_iqS2 b_a7FV3) ((|?) a_iqS2 b_iqS3) b_a7FV3 b_iqS3

Files

ChangeLog.md view
@@ -1,4 +1,12 @@ # Revision history for lsp-types++## 1.6.0.0++* Add `isSubRangeOf` and `positionInRange` helper functions+* Add `ServerCancelled`, `RequestFailed` and `ErrorCodeCustom` server error types+* Fix "workspace/semanticTokens/refresh" to be a server method instead of a client method+* Use a packed representation for `NormalizedFilePath`+* Add converions from `OsPath` to `NormalizedFilePath` in `Language.LSP.Types.Uri.OsPath` when using new enough `filepath`   ## 1.5.0.0 
lsp-types.cabal view
@@ -1,6 +1,6 @@ cabal-version:       2.2 name:                lsp-types-version:             1.5.0.0+version:             1.6.0.0 synopsis:            Haskell library for the Microsoft Language Server Protocol, data types  description:         An implementation of the types to allow language implementors to@@ -16,6 +16,11 @@ build-type:          Simple extra-source-files:  ChangeLog.md, README.md +flag force-ospath+  default: False+  manual: False+  description: Force a version bound on filepath library, to enable 'OsPath'.+ library   exposed-modules:     Language.LSP.Types                      , Language.LSP.Types.Capabilities@@ -68,6 +73,7 @@                      , Language.LSP.Types.WorkspaceEdit                      , Language.LSP.Types.WorkspaceFolders                      , Language.LSP.Types.WorkspaceSymbol+                     , Language.LSP.Types.Uri.OsPath  -- other-extensions:   ghc-options:         -Wall   build-depends:       base >= 4.11 && < 5@@ -78,7 +84,6 @@                      , deepseq                      , Diff >= 0.2                      , dlist-                     , filepath                      , hashable                      , lens >= 4.15.2                      , mtl < 2.4@@ -89,9 +94,45 @@                      , text                      , template-haskell                      , unordered-containers+                     , exceptions+                     , safe+  if flag(force-ospath)+    build-depends: filepath ^>= 1.4.100.0+  else+    build-depends: filepath   hs-source-dirs:      src   default-language:    Haskell2010   default-extensions: StrictData++test-suite lsp-types-test+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             Main.hs+  other-modules:       Spec+                       CapabilitiesSpec+                       JsonSpec+                       MethodSpec+                       ServerCapabilitiesSpec+                       SemanticTokensSpec+                       TypesSpec+                       URIFilePathSpec+                       WorkspaceEditSpec+                       LocationSpec+  build-depends:       base+                     , QuickCheck+                     -- for instance Arbitrary Value+                     , aeson >= 2.0.3.0+                     , filepath+                     , hspec+                     , lsp-types+                     , lens >= 4.15.2+                     , network-uri+                     , quickcheck-instances+                     , text+                     , tuple+  build-tool-depends:  hspec-discover:hspec-discover+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N -Wall+  default-language:    Haskell2010  source-repository head   type:     git
src/Language/LSP/Types.hs view
@@ -37,6 +37,7 @@   , module Language.LSP.Types.TextDocument   , module Language.LSP.Types.TypeDefinition   , module Language.LSP.Types.Uri+  , module Language.LSP.Types.Uri.OsPath   , module Language.LSP.Types.WatchedFiles   , module Language.LSP.Types.Window   , module Language.LSP.Types.WorkspaceEdit@@ -69,8 +70,8 @@ import           Language.LSP.Types.Location import           Language.LSP.Types.LspId import           Language.LSP.Types.MarkupContent-import           Language.LSP.Types.Method import           Language.LSP.Types.Message+import           Language.LSP.Types.Method import           Language.LSP.Types.Parsing import           Language.LSP.Types.Progress import           Language.LSP.Types.References@@ -83,6 +84,7 @@ import           Language.LSP.Types.TextDocument import           Language.LSP.Types.TypeDefinition import           Language.LSP.Types.Uri+import           Language.LSP.Types.Uri.OsPath import           Language.LSP.Types.WatchedFiles import           Language.LSP.Types.Window import           Language.LSP.Types.WorkspaceEdit
src/Language/LSP/Types/Location.hs view
@@ -1,11 +1,11 @@-{-# LANGUAGE TemplateHaskell            #-}-{-# LANGUAGE DeriveGeneric              #-}+{-# LANGUAGE DeriveGeneric   #-}+{-# LANGUAGE TemplateHaskell #-} module Language.LSP.Types.Location where  import           Control.DeepSeq import           Data.Aeson.TH import           Data.Hashable-import           GHC.Generics hiding (UInt)+import           GHC.Generics              hiding (UInt) import           Language.LSP.Types.Common import           Language.LSP.Types.Uri import           Language.LSP.Types.Utils@@ -33,8 +33,8 @@  data Range =   Range-    { _start :: Position -- ^ The range's start position.-    , _end   :: Position -- ^ The range's end position.+    { _start :: Position -- ^ The range's start position. (inclusive)+    , _end   :: Position -- ^ The range's end position. (exclusive, see: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#range )     } deriving (Show, Read, Eq, Ord, Generic)  instance NFData Range@@ -65,12 +65,12 @@     -- range at the mouse position.     _originSelectionRange :: Maybe Range     -- | The target resource identifier of this link.-  , _targetUri :: Uri+  , _targetUri            :: Uri     -- | The full target range of this link. If the target for example is a     -- symbol then target range is the range enclosing this symbol not including     -- leading/trailing whitespace but everything else like comments. This     -- information is typically used to highlight the range in the editor.-  , _targetRange :: Range+  , _targetRange          :: Range     -- | The range that should be selected and revealed when this link is being     -- followed, e.g the name of a function. Must be contained by the the     -- 'targetRange'. See also @DocumentSymbol._range@@@ -84,3 +84,11 @@ -- prop> mkRange l c l' c' = Range (Position l c) (Position l' c') mkRange :: UInt -> UInt -> UInt -> UInt -> Range mkRange l c l' c' = Range (Position l c) (Position l' c')++-- | 'isSubrangeOf' returns true if for every 'Position' in the first 'Range', it's also in the second 'Range'.+isSubrangeOf :: Range -> Range -> Bool+isSubrangeOf smallRange range = _start smallRange >= _start range && _end smallRange <= _end range++-- | 'positionInRange' returns true if the given 'Position' is in the 'Range'.+positionInRange :: Position -> Range -> Bool+positionInRange p (Range sp ep) = sp <= p && p < ep -- Range's end position is exclusive.
src/Language/LSP/Types/Message.hs view
@@ -60,6 +60,7 @@ import Data.Aeson import Data.Aeson.TH import Data.Text (Text)+import Data.Scientific import Data.String import GHC.Generics @@ -318,6 +319,9 @@                | UnknownErrorCode                | RequestCancelled                | ContentModified+               | ServerCancelled+               | RequestFailed+               | ErrorCodeCustom Int32                -- ^ Note: server error codes are reserved from -32099 to -32000                deriving (Read,Show,Eq) @@ -333,6 +337,9 @@   toJSON UnknownErrorCode     = Number (-32001)   toJSON RequestCancelled     = Number (-32800)   toJSON ContentModified      = Number (-32801)+  toJSON ServerCancelled      = Number (-32802)+  toJSON RequestFailed        = Number (-32803)+  toJSON (ErrorCodeCustom n)  = Number (fromIntegral n)  instance FromJSON ErrorCode where   parseJSON (Number (-32700)) = pure ParseError@@ -346,7 +353,12 @@   parseJSON (Number (-32001)) = pure UnknownErrorCode   parseJSON (Number (-32800)) = pure RequestCancelled   parseJSON (Number (-32801)) = pure ContentModified-  parseJSON _                 = fail "ErrorCode"+  parseJSON (Number (-32802)) = pure ServerCancelled+  parseJSON (Number (-32803)) = pure RequestFailed+  parseJSON (Number n       ) = case toBoundedInteger n of+    Just i -> pure (ErrorCodeCustom i)+    Nothing -> fail "Couldn't convert ErrorCode to bounded integer."+  parseJSON _                 = fail "Couldn't parse ErrorCode"  -- ------------------------------------- 
src/Language/LSP/Types/Method.hs view
@@ -87,7 +87,6 @@   TextDocumentSemanticTokensFull     :: Method FromClient Request   TextDocumentSemanticTokensFullDelta :: Method FromClient Request   TextDocumentSemanticTokensRange    :: Method FromClient Request-  WorkspaceSemanticTokensRefresh     :: Method FromClient Request  -- ServerMethods   -- Window@@ -108,6 +107,7 @@   WorkspaceWorkspaceFolders          :: Method FromServer Request   WorkspaceConfiguration             :: Method FromServer Request   WorkspaceApplyEdit                 :: Method FromServer Request+  WorkspaceSemanticTokensRefresh     :: Method FromServer Request   -- Document   TextDocumentPublishDiagnostics     :: Method FromServer Notification @@ -287,7 +287,6 @@   parseJSON (A.String "workspace/didChangeWatchedFiles")     = pure $ SomeClientMethod SWorkspaceDidChangeWatchedFiles   parseJSON (A.String "workspace/symbol")                    = pure $ SomeClientMethod SWorkspaceSymbol   parseJSON (A.String "workspace/executeCommand")            = pure $ SomeClientMethod SWorkspaceExecuteCommand-  parseJSON (A.String "workspace/semanticTokens/refresh")    = pure $ SomeClientMethod SWorkspaceSemanticTokensRefresh  -- Document   parseJSON (A.String "textDocument/didOpen")                = pure $ SomeClientMethod STextDocumentDidOpen   parseJSON (A.String "textDocument/didChange")              = pure $ SomeClientMethod STextDocumentDidChange@@ -351,6 +350,7 @@   parseJSON (A.String "workspace/workspaceFolders")          = pure $ SomeServerMethod SWorkspaceWorkspaceFolders   parseJSON (A.String "workspace/configuration")             = pure $ SomeServerMethod SWorkspaceConfiguration   parseJSON (A.String "workspace/applyEdit")                 = pure $ SomeServerMethod SWorkspaceApplyEdit+  parseJSON (A.String "workspace/semanticTokens/refresh")    = pure $ SomeServerMethod SWorkspaceSemanticTokensRefresh   -- Document   parseJSON (A.String "textDocument/publishDiagnostics")     = pure $ SomeServerMethod STextDocumentPublishDiagnostics @@ -388,7 +388,6 @@   toJSON SWorkspaceDidChangeWatchedFiles     = A.String "workspace/didChangeWatchedFiles"   toJSON SWorkspaceSymbol                    = A.String "workspace/symbol"   toJSON SWorkspaceExecuteCommand            = A.String "workspace/executeCommand"-  toJSON SWorkspaceSemanticTokensRefresh     = A.String "workspace/semanticTokens/refresh"   -- Document   toJSON STextDocumentDidOpen                = A.String "textDocument/didOpen"   toJSON STextDocumentDidChange              = A.String "textDocument/didChange"@@ -445,6 +444,7 @@   toJSON SWorkspaceWorkspaceFolders          = A.String "workspace/workspaceFolders"   toJSON SWorkspaceConfiguration             = A.String "workspace/configuration"   toJSON SWorkspaceApplyEdit                 = A.String "workspace/applyEdit"+  toJSON SWorkspaceSemanticTokensRefresh     = A.String "workspace/semanticTokens/refresh"   -- Document   toJSON STextDocumentPublishDiagnostics     = A.String "textDocument/publishDiagnostics"   -- Cancelling
src/Language/LSP/Types/Parsing.hs view
@@ -259,12 +259,12 @@ splitClientMethod STextDocumentSemanticTokensFull = IsClientReq splitClientMethod STextDocumentSemanticTokensFullDelta = IsClientReq splitClientMethod STextDocumentSemanticTokensRange = IsClientReq-splitClientMethod SWorkspaceSemanticTokensRefresh  = IsClientReq splitClientMethod SCancelRequest = IsClientNot splitClientMethod SCustomMethod{} = IsClientEither  {-# INLINE splitServerMethod #-} splitServerMethod :: SServerMethod m -> ServerNotOrReq m+-- Window splitServerMethod SWindowShowMessage = IsServerNot splitServerMethod SWindowShowMessageRequest = IsServerReq splitServerMethod SWindowShowDocument = IsServerReq@@ -272,13 +272,19 @@ splitServerMethod SWindowWorkDoneProgressCreate = IsServerReq splitServerMethod SProgress = IsServerNot splitServerMethod STelemetryEvent = IsServerNot+-- Client splitServerMethod SClientRegisterCapability = IsServerReq splitServerMethod SClientUnregisterCapability = IsServerReq+-- Workspace splitServerMethod SWorkspaceWorkspaceFolders = IsServerReq splitServerMethod SWorkspaceConfiguration = IsServerReq splitServerMethod SWorkspaceApplyEdit = IsServerReq+splitServerMethod SWorkspaceSemanticTokensRefresh = IsServerReq+-- Document splitServerMethod STextDocumentPublishDiagnostics = IsServerNot+-- Cancelling splitServerMethod SCancelRequest = IsServerNot+-- Custom splitServerMethod SCustomMethod{} = IsServerEither  -- | Given a witness that two custom methods are of the same type, produce a witness that the methods are the same
src/Language/LSP/Types/Uri.hs view
@@ -1,7 +1,11 @@-{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE DeriveGeneric              #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE InstanceSigs               #-}+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE RankNTypes                 #-}+{-# LANGUAGE RecordWildCards            #-}+{-# LANGUAGE TypeSynonymInstances       #-}+ module Language.LSP.Types.Uri   ( Uri(..)   , uriToFilePath@@ -10,11 +14,11 @@   , toNormalizedUri   , fromNormalizedUri   , NormalizedFilePath-  , normalizedFilePath   , toNormalizedFilePath   , fromNormalizedFilePath   , normalizedFilePathToUri   , uriToNormalizedFilePath+  , emptyNormalizedFilePath   -- Private functions   , platformAwareUriToFilePath   , platformAwareFilePathToUri@@ -22,18 +26,19 @@   where  import           Control.DeepSeq-import qualified Data.Aeson                                 as A-import           Data.Binary                                (Binary, Get, put, get)+import qualified Data.Aeson              as A+import           Data.Binary             (Binary, Get, get, put) import           Data.Hashable-import           Data.List                                  (stripPrefix)-import           Data.String                                (IsString, fromString)-import           Data.Text                                  (Text)-import qualified Data.Text                                  as T+import           Data.List               (stripPrefix)+import           Data.String             (IsString (fromString))+import           Data.Text               (Text)+import qualified Data.Text               as T import           GHC.Generics-import           Network.URI hiding (authority)-import qualified System.FilePath                            as FP-import qualified System.FilePath.Posix                      as FPP-import qualified System.FilePath.Windows                    as FPW+import           Network.URI             hiding (authority)+import           Safe                    (tailMay)+import qualified System.FilePath         as FP+import qualified System.FilePath.Posix   as FPP+import qualified System.FilePath.Windows as FPW import qualified System.Info  newtype Uri = Uri { getUri :: Text }@@ -67,7 +72,7 @@ normalizeUriEscaping :: String -> String normalizeUriEscaping uri =   case stripPrefix (fileScheme ++ "//") uri of-    Just p -> fileScheme ++ "//" ++ (escapeURIPath $ unEscapeString p)+    Just p  -> fileScheme ++ "//" ++ escapeURIPath (unEscapeString p)     Nothing -> escapeURIString isUnescapedInURI $ unEscapeString uri   where escapeURIPath = escapeURIString (isUnescapedInUriPath System.Info.os) @@ -107,17 +112,19 @@                           -> String -- ^ path                           -> FilePath platformAdjustFromUriPath systemOS authority srcPath =-  (maybe id (++) authority) $-  if systemOS /= windowsOS || null srcPath then srcPath-    else let-      firstSegment:rest = (FPP.splitDirectories . tail) srcPath  -- Drop leading '/' for absolute Windows paths-      drive = if FPW.isDrive firstSegment-              then FPW.addTrailingPathSeparator firstSegment-              else firstSegment-      in FPW.joinDrive drive $ FPW.joinPath rest+  maybe id (++) authority $+  if systemOS /= windowsOS+  then srcPath+  else case FPP.splitDirectories <$> tailMay srcPath of+      Just (firstSegment:rest) -> -- Drop leading '/' for absolute Windows paths+        let drive = if FPW.isDrive firstSegment+                    then FPW.addTrailingPathSeparator firstSegment+                    else firstSegment+         in FPW.joinDrive drive $ FPW.joinPath rest+      _ -> srcPath  filePathToUri :: FilePath -> Uri-filePathToUri = (platformAwareFilePathToUri System.Info.os) . FP.normalise+filePathToUri = platformAwareFilePathToUri System.Info.os . FP.normalise  {-# WARNING platformAwareFilePathToUri "This function is considered private. Use normalizedUriToFilePath instead." #-} platformAwareFilePathToUri :: SystemOS -> FilePath -> Uri@@ -151,13 +158,30 @@           FPP.addTrailingPathSeparator (init drv)       | otherwise = drv --- | Newtype wrapper around FilePath that always has normalized slashes.--- The NormalizedUri and hash of the FilePath are cached to avoided--- repeated normalisation when we need to compute them (which is a lot).------ This is one of the most performance critical parts of ghcide, do not--- modify it without profiling.-data NormalizedFilePath = NormalizedFilePath NormalizedUri !FilePath+{-| A file path that is already normalized.++The 'NormalizedUri' is cached to avoided+repeated normalisation when we need to compute them (which is a lot).++This is one of the most performance critical parts of HLS, do not+modify it without profiling.++== Adoption Plan of OsPath++Currently we store 'Text'. We may change it to OsPath in the future if+the following steps are executed.++1. In the client codebase, use 'osPathToNormalizedFilePath' and 'normalizedFilePathToOsPath' instead of 'fromNormalizedFilePath'+  and 'toNormalizedFilePath'. For HLS, we could wait until GHC 9.6 becomes the oldest+  GHC we support, then change 'FilePath' to OsPath everywhere in the codebase.+2. Deprecate and remove 'fromNormalizedFilePath' and 'toNormalizedFilePath'.+3. Change 'Text' to OsPath and benchmark it to make sure performance doesn't go down. Don't forget to check Windows,+  as OsPath on Windows uses UTF-16, which may consume more memory.++See [#453](https://github.com/haskell/lsp/pull/453) and [#446](https://github.com/haskell/lsp/pull/446)+for more discussions on this topic.+-}+data NormalizedFilePath = NormalizedFilePath !NormalizedUri {-# UNPACK #-} !Text     deriving (Generic, Eq, Ord)  instance NFData NormalizedFilePath@@ -165,13 +189,8 @@ instance Binary NormalizedFilePath where   put (NormalizedFilePath _ fp) = put fp   get = do-    v <- Data.Binary.get :: Get FilePath-    let nuri = internalNormalizedFilePathToUri v-    return (normalizedFilePath nuri v)---- | A smart constructor that performs UTF-8 encoding and hash consing-normalizedFilePath :: NormalizedUri -> FilePath -> NormalizedFilePath-normalizedFilePath nuri nfp = NormalizedFilePath nuri nfp+    v <- Data.Binary.get :: Get Text+    return (NormalizedFilePath (internalNormalizedFilePathToUri (T.unpack v)) v)  -- | Internal helper that takes a file path that is assumed to -- already be normalized to a URI. It is up to the caller@@ -191,20 +210,32 @@   hashWithSalt salt (NormalizedFilePath uri _) = hashWithSalt salt uri  instance IsString NormalizedFilePath where+    fromString :: String -> NormalizedFilePath     fromString = toNormalizedFilePath  toNormalizedFilePath :: FilePath -> NormalizedFilePath-toNormalizedFilePath fp = normalizedFilePath nuri nfp+toNormalizedFilePath fp = NormalizedFilePath nuri . T.pack $ nfp   where-      nfp = FP.normalise fp-      nuri = internalNormalizedFilePathToUri nfp+    nfp = FP.normalise fp+    nuri = internalNormalizedFilePathToUri nfp +-- | Extracts 'FilePath' from 'NormalizedFilePath'. fromNormalizedFilePath :: NormalizedFilePath -> FilePath-fromNormalizedFilePath (NormalizedFilePath _ fp) = fp+fromNormalizedFilePath (NormalizedFilePath _ fp) = T.unpack fp  normalizedFilePathToUri :: NormalizedFilePath -> NormalizedUri normalizedFilePathToUri (NormalizedFilePath uri _) = uri  uriToNormalizedFilePath :: NormalizedUri -> Maybe NormalizedFilePath-uriToNormalizedFilePath nuri = fmap (normalizedFilePath nuri) mbFilePath+uriToNormalizedFilePath nuri = fmap (NormalizedFilePath nuri . T.pack) mbFilePath   where mbFilePath = platformAwareUriToFilePath System.Info.os (fromNormalizedUri nuri)++emptyNormalizedUri :: NormalizedUri+emptyNormalizedUri =+    let s = "file://"+    in NormalizedUri (hash s) s++-- | 'NormalizedFilePath' that contains an empty file path+emptyNormalizedFilePath :: NormalizedFilePath+emptyNormalizedFilePath = NormalizedFilePath emptyNormalizedUri ""+
+ src/Language/LSP/Types/Uri/OsPath.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE CPP                 #-}+{-# LANGUAGE ScopedTypeVariables #-}++#if MIN_VERSION_filepath(1,4,100)+#define OS_PATH 1+#endif++module Language.LSP.Types.Uri.OsPath+  (+#ifdef OS_PATH+    osPathToNormalizedFilePath+  , normalizedFilePathToOsPath+  , EncodingException+#endif+  ) where++#ifdef OS_PATH++import           Control.Exception      hiding (try)+import           Control.Monad.Catch+import           GHC.IO.Encoding        (getFileSystemEncoding)+import           Language.LSP.Types.Uri+import           System.IO+import           System.IO.Unsafe       (unsafePerformIO)+import           System.OsPath+import           System.OsPath.Encoding (EncodingException)++{-|+Constructs 'NormalizedFilePath' from 'OsPath'. Throws 'EncodingException' if the conversion fails.++We store a 'Text' in 'NormalizedFilePath', which is UTF-16 or UTF-8 depending on the verion of text library.+'OsPath' may have a different encoding than 'Text', so this function may fail.+But DO NOTE THAT encoding mismatch doesn't always mean an exception will be thrown.+[Possibly your encoding simply won't throw exception on failure](https://hackage.haskell.org/package/base-4.17.0.0/docs/src/GHC.IO.Encoding.html#initFileSystemEncoding).+Possibly the conversion function can't find any invalid byte sequence, giving a sucessful but wrong result.+-}+osPathToNormalizedFilePath :: MonadThrow m => OsPath -> m NormalizedFilePath+osPathToNormalizedFilePath = fmap toNormalizedFilePath . liftException . decodeWith systemEnc utf16le++{-|+Extracts 'OsPath' from 'NormalizedFilePath'. Throws 'EncodingException' if the conversion fails.+-}+normalizedFilePathToOsPath :: MonadThrow m => NormalizedFilePath -> m OsPath+normalizedFilePathToOsPath = liftException . encodeWith systemEnc utf16le . fromNormalizedFilePath++liftException :: (MonadThrow m, Exception e) => Either e a -> m a+liftException (Right x)  = pure x+liftException (Left err) = throwM err++systemEnc :: TextEncoding+systemEnc = unsafePerformIO getFileSystemEncoding++#endif
+ test/CapabilitiesSpec.hs view
@@ -0,0 +1,16 @@+module CapabilitiesSpec where++import Language.LSP.Types+import Language.LSP.Types.Capabilities+import Test.Hspec++spec :: Spec+spec = describe "capabilities" $ do+  it "gives 3.10 capabilities" $+    let ClientCapabilities _ (Just tdcs) _ _ _ = capsForVersion (LSPVersion 3 10)+        Just (DocumentSymbolClientCapabilities _ _ mHierarchical _ _ ) = _documentSymbol tdcs+      in mHierarchical `shouldBe` Just True+  it "gives pre 3.10 capabilities" $+      let ClientCapabilities _ (Just tdcs) _ _ _ = capsForVersion (LSPVersion 3 9)+          Just (DocumentSymbolClientCapabilities _ _ mHierarchical _ _) = _documentSymbol tdcs+        in mHierarchical `shouldBe` Nothing
+ test/JsonSpec.hs view
@@ -0,0 +1,173 @@+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE FlexibleContexts     #-}+{-# LANGUAGE TypeInType           #-}+{-# LANGUAGE FlexibleInstances    #-}+{-# LANGUAGE OverloadedStrings    #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+-- For the use of MarkedString+{-# OPTIONS_GHC -fno-warn-deprecations #-}+-- | Test for JSON serialization+module JsonSpec where++import           Language.LSP.Types++import qualified Data.Aeson                    as J+import           Data.List(isPrefixOf)+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 = do+  describe "dispatcher" jsonSpec+  describe "ResponseMessage"  responseMessageSpec++-- ---------------------------------------------------------------------++jsonSpec :: Spec+jsonSpec = do+  describe "General JSON instances round trip" $ do+  -- DataTypesJSON+    prop "LanguageString" (propertyJsonRoundtrip :: LanguageString -> Property)+    prop "MarkedString"   (propertyJsonRoundtrip :: MarkedString -> Property)+    prop "MarkupContent"  (propertyJsonRoundtrip :: MarkupContent -> Property)+    prop "HoverContents"  (propertyJsonRoundtrip :: HoverContents -> Property)+    prop "ResponseError"  (propertyJsonRoundtrip :: ResponseError -> Property)+    prop "WatchedFiles"   (propertyJsonRoundtrip :: DidChangeWatchedFilesRegistrationOptions -> Property)+    prop "ResponseMessage Initialize"+         (propertyJsonRoundtrip :: ResponseMessage 'TextDocumentHover -> Property)+    -- prop "ResponseMessage JSON value"+        --  (propertyJsonRoundtrip :: ResponseMessage J.Value -> Property)+  describe "JSON decoding regressions" $+    it "CompletionItem" $+      (J.decode "{\"jsonrpc\":\"2.0\",\"result\":[{\"label\":\"raisebox\"}],\"id\":1}" :: Maybe (ResponseMessage 'TextDocumentCompletion))+        `shouldNotBe` Nothing+++responseMessageSpec :: Spec+responseMessageSpec = do+  describe "edge cases" $ do+    it "decodes result = null" $ do+      let input = "{\"jsonrpc\": \"2.0\", \"id\": 123, \"result\": null}"+        in  J.decode input `shouldBe` Just+              ((ResponseMessage "2.0" (Just (IdInt 123)) (Right J.Null)) :: ResponseMessage 'WorkspaceExecuteCommand)+    it "handles missing params field" $ do+      J.eitherDecode "{ \"jsonrpc\": \"2.0\", \"id\": 15, \"method\": \"shutdown\"}"+        `shouldBe` Right (RequestMessage "2.0" (IdInt 15) SShutdown Empty)+  describe "invalid JSON" $ do+    it "throws if neither result nor error is present" $ do+      (J.eitherDecode "{\"jsonrpc\":\"2.0\",\"id\":1}" :: Either String (ResponseMessage 'Initialize))+        `shouldBe` Left ("Error in $: both error and result cannot be Nothing")+    it "throws if both result and error are present" $ do+      (J.eitherDecode+        "{\"jsonrpc\":\"2.0\",\"id\": 1,\"result\":{\"capabilities\": {}},\"error\":{\"code\":-32700,\"message\":\"\",\"data\":null}}"+        :: Either String (ResponseMessage 'Initialize))+        `shouldSatisfy`+          (either (\err -> "Error in $: both error and result cannot be present" `isPrefixOf` err) (\_ -> False))++-- ---------------------------------------------------------------------++propertyJsonRoundtrip :: (Eq a, Show a, J.ToJSON a, J.FromJSON a) => a -> Property+propertyJsonRoundtrip a = J.Success a === J.fromJSON (J.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+                    ]++instance Arbitrary UInt where+  arbitrary = fromInteger <$> arbitrary++instance Arbitrary Uri where+  arbitrary = Uri <$> arbitrary++instance Arbitrary Position where+  arbitrary = Position <$> arbitrary <*> arbitrary++instance Arbitrary Location where+  arbitrary = Location <$> arbitrary <*> arbitrary++instance Arbitrary Range where+  arbitrary = Range <$> arbitrary <*> arbitrary++instance Arbitrary Hover where+  arbitrary = Hover <$> arbitrary <*> arbitrary++instance Arbitrary (ResponseResult m) => Arbitrary (ResponseMessage m) where+  arbitrary =+    oneof+      [ ResponseMessage+          <$> arbitrary+          <*> arbitrary+          <*> (Right <$> arbitrary)+      , ResponseMessage+          <$> arbitrary+          <*> arbitrary+          <*> (Left <$> arbitrary)+      ]++instance Arbitrary (LspId m) where+  arbitrary = oneof [IdInt <$> arbitrary, IdString <$> arbitrary]++instance Arbitrary ResponseError where+  arbitrary = ResponseError <$> arbitrary <*> arbitrary <*> pure Nothing++instance Arbitrary ErrorCode where+  arbitrary =+    elements+      [ ParseError+      , InvalidRequest+      , MethodNotFound+      , InvalidParams+      , InternalError+      , ServerErrorStart+      , ServerErrorEnd+      , ServerNotInitialized+      , UnknownErrorCode+      , RequestCancelled+      , ContentModified+      ]++-- | 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++-- ---------------------------------------------------------------------++instance Arbitrary DidChangeWatchedFilesRegistrationOptions where+  arbitrary = DidChangeWatchedFilesRegistrationOptions <$> arbitrary++instance Arbitrary FileSystemWatcher where+  arbitrary = FileSystemWatcher <$> arbitrary <*> arbitrary++instance Arbitrary WatchKind where+  arbitrary = WatchKind <$> arbitrary <*> arbitrary <*> arbitrary++-- ---------------------------------------------------------------------
+ test/LocationSpec.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE OverloadedStrings #-}++module LocationSpec where++import           Language.LSP.Types+import           Test.Hspec++main :: IO ()+main = hspec spec++spec :: Spec+spec = do+  describe "isSubrangeOf" $ do+    it "is true if the first range is totally inside the second range" $+      isSubrangeOf (mkRange 1 2 1 5) (mkRange 1 1 1 6) `shouldBe` True+    it "is true if two ranges equal" $+      isSubrangeOf (mkRange 1 2 1 5) (mkRange 1 2 1 5) `shouldBe` True+    it "is false if the first range is outside of the second" $+      isSubrangeOf (mkRange 1 1 1 5) (mkRange 1 2 1 5) `shouldBe` False++  describe "positionInRange" $ do+    it "is false if position is after the end of a single line range" $+      positionInRange (Position 1 10) (Range (Position 1 1) (Position 1 3)) `shouldBe` False+    it "is false if position is before the begining of a single line range" $+      positionInRange (Position 1 0) (Range (Position 1 1) (Position 1 6)) `shouldBe` False+    it "is true if position is in a single line range" $+      positionInRange (Position 1 5) (Range (Position 1 1) (Position 1 6)) `shouldBe` True+    it "is false if position is right at the end of the range" $+      positionInRange (Position 1 5) (Range (Position 1 1) (Position 1 5)) `shouldBe` False+    it "is true if position is in the middle of a multiline range" $+      positionInRange (Position 3 5) (Range (Position 1 1) (Position 5 6)) `shouldBe` True+    it "is false if position is before the beginning of a multiline range" $+      positionInRange (Position 3 5) (Range (Position 3 6) (Position 4 10)) `shouldBe` False+    it "is false if position is right at the end of a multiline range" $+      positionInRange (Position 4 10) (Range (Position 3 6) (Position 4 10)) `shouldBe` False
+ test/Main.hs view
@@ -0,0 +1,7 @@+module Main where++import Test.Hspec.Runner+import qualified Spec++main :: IO ()+main = hspec Spec.spec
+ test/MethodSpec.hs view
@@ -0,0 +1,99 @@+{-# LANGUAGE OverloadedStrings, DataKinds #-}+module MethodSpec where+++import           Control.Monad+import qualified Data.Aeson as J+import qualified Language.LSP.Types            as J+import           Test.Hspec+import qualified Data.Text as T++-- ---------------------------------------------------------------------++main :: IO ()+main = hspec spec++spec :: Spec+spec = describe "Method enum aeson instance consistency" diagnosticsSpec++-- ---------------------------------------------------------------------++clientMethods :: [T.Text]+clientMethods = [+  -- General+   "initialize"+  ,"initialized"+  ,"shutdown"+  ,"exit"+  ,"$/cancelRequest"+ -- Workspace+  ,"workspace/didChangeConfiguration"+  ,"workspace/didChangeWatchedFiles"+  ,"workspace/symbol"+  ,"workspace/executeCommand"+  ,"workspace/semanticTokens/refresh"+ -- Document+  ,"textDocument/didOpen"+  ,"textDocument/didChange"+  ,"textDocument/willSave"+  ,"textDocument/willSaveWaitUntil"+  ,"textDocument/didSave"+  ,"textDocument/didClose"+  ,"textDocument/completion"+  ,"completionItem/resolve"+  ,"textDocument/hover"+  ,"textDocument/signatureHelp"+  ,"textDocument/references"+  ,"textDocument/documentHighlight"+  ,"textDocument/documentSymbol"+  ,"textDocument/formatting"+  ,"textDocument/rangeFormatting"+  ,"textDocument/onTypeFormatting"+  ,"textDocument/definition"+  ,"textDocument/codeAction"+  ,"textDocument/codeLens"+  ,"codeLens/resolve"+  ,"textDocument/documentLink"+  ,"documentLink/resolve"+  ,"textDocument/rename"+  ,"textDocument/prepareRename"+  ,"textDocument/prepareCallHierarchy"+  ,"callHierarchy/incomingCalls"+  ,"callHierarchy/outgoingCalls"++  ,"textDocument/semanticTokens"+  ,"textDocument/semanticTokens/full"+  ,"textDocument/semanticTokens/full/delta"+  ,"textDocument/semanticTokens/range"+  ]++serverMethods :: [T.Text]+serverMethods = [+  -- Window+   "window/showMessage"+  ,"window/showMessageRequest"+  ,"window/logMessage"+  ,"telemetry/event"+  -- Client+  ,"client/registerCapability"+  ,"client/unregisterCapability"+  -- Workspace+  ,"workspace/applyEdit"+  -- Document+  ,"textDocument/publishDiagnostics"+  ]++diagnosticsSpec :: Spec+diagnosticsSpec = do+  describe "Client Methods" $ do+    it "maintains roundtrip consistency" $ do+      forM_ clientMethods $ \m -> do+        (J.toJSON <$> (J.fromJSON (J.String m) :: J.Result (J.SomeClientMethod)))+          `shouldBe` (J.Success $ J.String m)+  describe "Server Methods" $ do+    it "maintains roundtrip consistency" $ do+      forM_ serverMethods $ \m -> do+        (J.toJSON <$> (J.fromJSON (J.String m) :: J.Result (J.SomeServerMethod)))+          `shouldBe` (J.Success $ J.String m)++    -- ---------------------------------
+ test/SemanticTokensSpec.hs view
@@ -0,0 +1,74 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+module SemanticTokensSpec where++import Test.Hspec+import Language.LSP.Types+import Data.List (unfoldr)+import Data.Either (isRight)++spec :: Spec+spec = do+  let exampleLegend = SemanticTokensLegend (List [SttProperty, SttType, SttClass]) (List [StmUnknown "private", StmStatic])+      exampleTokens1 = [+        SemanticTokenAbsolute 2 5 3 SttProperty [StmUnknown "private", StmStatic]+        , SemanticTokenAbsolute 2 10 4 SttType []+        , SemanticTokenAbsolute 5 2 7 SttClass []+        ]+      exampleTokens2 = [+        SemanticTokenAbsolute 3 5 3 SttProperty [StmUnknown "private", StmStatic]+        , SemanticTokenAbsolute 3 10 4 SttType []+        , SemanticTokenAbsolute 6 2 7 SttClass []+        ]++      bigNumber :: UInt+      bigNumber = 100000+      bigTokens =+        unfoldr (\i -> if i == bigNumber then Nothing else Just (SemanticTokenAbsolute i 1 1 SttType [StmUnknown "private", StmStatic], i+1)) 0+      -- Relativized version of bigTokens+      bigTokensRel =+        unfoldr (\i -> if i == bigNumber then Nothing else Just (SemanticTokenRelative (if i == 0 then 0 else 1) 1 1 SttType [StmUnknown "private", StmStatic], i+1)) 0++      -- One more order of magnitude makes diffing more-or-less hang - possibly we need a better diffing algorithm, since this is only ~= 200 tokens at 5 ints per token+      -- (I checked and it is the diffing that's slow, not turning it into edits)+      smallerBigNumber :: UInt+      smallerBigNumber = 1000+      bigInts :: [UInt]+      bigInts =+        unfoldr (\i -> if i == smallerBigNumber then Nothing else Just (1, i+1)) 0+      bigInts2 :: [UInt]+      bigInts2 =+        unfoldr (\i -> if i == smallerBigNumber then Nothing else Just (if even i then 2 else 1, i+1)) 0++  describe "relativize/absolutizeTokens" $ do+    it "round-trips" $ do+      absolutizeTokens (relativizeTokens exampleTokens1) `shouldBe` exampleTokens1+      absolutizeTokens (relativizeTokens exampleTokens2) `shouldBe` exampleTokens2+    it "handles big tokens" $ relativizeTokens bigTokens `shouldBe` bigTokensRel++  describe "encodeTokens" $ do+    context "when running the LSP examples" $ do+      it "encodes example 1 correctly" $+        let encoded = encodeTokens exampleLegend (relativizeTokens exampleTokens1)+        in encoded `shouldBe` Right [{- token 1 -}2,5,3,0,3,{- token 2 -}0,5,4,1,0,{- token 3 -}3,2,7,2,0]+      it "encodes example 2 correctly" $+        let encoded = encodeTokens exampleLegend (relativizeTokens exampleTokens2)+        in encoded `shouldBe` Right [{- token 1 -}3,5,3,0,3,{- token 2 -}0,5,4,1,0,{- token 3 -}3,2,7,2,0]+    it "handles big tokens" $ encodeTokens exampleLegend bigTokensRel `shouldSatisfy` isRight++  describe "computeEdits" $ do+    it "handles an edit in the middle" $+      computeEdits @Int [1,2,3] [1,4,5,3] `shouldBe` [Edit 1 1 [4,5]]+    it "handles an edit at the end" $+      computeEdits @Int [1,2,3] [1,2,4,5] `shouldBe` [Edit 2 1 [4,5]]+    it "handles an edit at the beginning" $+      computeEdits @Int [1,2,3] [4,5,2,3] `shouldBe` [Edit 0 1 [4,5]]+    it "handles an ambiguous edit" $+      computeEdits @Int [1,2,3] [1,3,4,3] `shouldBe` [Edit 1 1 [], Edit 3 0 [4,3]]+    it "handles a long edit" $+      computeEdits @Int [1,2,3,4,5] [1,7,7,7,7,7,5] `shouldBe` [Edit 1 3 [7,7,7,7,7]]+    it "handles multiple edits" $+      computeEdits @Int [1,2,3,4,5] [1,6,3,7,7,5] `shouldBe` [Edit 1 1 [6], Edit 3 1 [7,7]]+    it "handles big tokens" $+      -- It's a little hard to specify a useful predicate here, the main point is that it should not take too long+      computeEdits @UInt bigInts bigInts2 `shouldSatisfy` (not . null)
+ test/ServerCapabilitiesSpec.hs view
@@ -0,0 +1,40 @@+{-# LANGUAGE OverloadedStrings #-}+module ServerCapabilitiesSpec where++import Control.Lens.Operators+import Data.Aeson+import Language.LSP.Types+import Language.LSP.Types.Capabilities+import Language.LSP.Types.Lens+import Test.Hspec++spec :: Spec+spec = describe "server capabilities" $ do+  describe "folding range options" $ do+    describe "decodes" $ do+      it "just id" $+        let input = "{\"id\": \"abc123\"}"+          in decode input `shouldBe` Just (FoldingRangeRegistrationOptions Nothing Nothing (Just "abc123"))+      it "id and document selector" $+        let input = "{\"id\": \"foo\", \"documentSelector\": " <> documentFiltersJson <> "}"+          in decode input `shouldBe` Just (FoldingRangeRegistrationOptions (Just documentFilters) Nothing (Just "foo"))+      it "static boolean" $+        let input = "true"+          in decode input `shouldBe` Just True+    describe "encodes" $+      it "just id" $+        encode (FoldingRangeRegistrationOptions Nothing Nothing (Just "foo")) `shouldBe` "{\"id\":\"foo\"}"+  it "decodes" $+    let input = "{\"hoverProvider\": true, \"colorProvider\": {\"id\": \"abc123\", \"documentSelector\": " <> documentFiltersJson <> "}}"+        Just caps = decode input :: Maybe ServerCapabilities+      in caps ^. colorProvider `shouldBe` Just (InR $ InR $ DocumentColorRegistrationOptions (Just documentFilters) (Just "abc123") Nothing)+  describe "client/registerCapability" $+    it "allows empty registerOptions" $+      let input = "{\"registrations\":[{\"registerOptions\":{},\"method\":\"workspace/didChangeConfiguration\",\"id\":\"4a56f5ca-7188-4f4c-a366-652d6f9d63aa\"}]}"+          Just registrationParams = decode input :: Maybe RegistrationParams+        in registrationParams ^. registrations `shouldBe`+             List [SomeRegistration $ Registration "4a56f5ca-7188-4f4c-a366-652d6f9d63aa"+                                      SWorkspaceDidChangeConfiguration Empty]+  where+    documentFilters = List [DocumentFilter (Just "haskell") Nothing Nothing]+    documentFiltersJson = "[{\"language\": \"haskell\"}]"
+ test/Spec.hs view
@@ -0,0 +1,9 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec #-}+{-++See https://github.com/hspec/hspec/tree/master/hspec-discover#readme+to understand this module++Or http://hspec.github.io/hspec-discover.html++-}
+ test/TypesSpec.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE OverloadedStrings #-}+module TypesSpec where++import qualified Language.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\nstring3\n"+        `shouldBe` J.MarkupContent J.MkMarkdown "\n```haskell\nfoo :: Int\n```\nstring2  \nstring3  \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\n```haskell\nfoo :: Int\n```\n"++-- ---------------------------------------------------------------------
+ test/URIFilePathSpec.hs view
@@ -0,0 +1,311 @@+{-# LANGUAGE CPP                 #-}+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE ScopedTypeVariables #-}++#if MIN_VERSION_filepath(1,4,100)+#define OS_PATH+#endif++module URIFilePathSpec where++#ifdef OS_PATH+import qualified System.OsPath           as OsPath+#endif++import           Control.Monad           (when)+import           Data.List+import           Data.Text               (Text, pack)+import           Language.LSP.Types++import           Control.Exception       (IOException, throwIO)+import           Data.Maybe              (fromJust)+import           GHC.IO.Encoding         (setFileSystemEncoding)+import           Network.URI+import           System.FilePath         (normalise)+import qualified System.FilePath.Windows as FPW+import qualified System.Info+import           System.IO+import           Test.Hspec+import           Test.QuickCheck++-- ---------------------------------------------------------------------++isWindows :: Bool+isWindows = System.Info.os == "mingw32"++main :: IO ()+main = hspec spec++spec :: Spec+spec = do+  describe "Platform aware URI file path functions" platformAwareUriFilePathSpec+  describe "URI file path functions" uriFilePathSpec+  describe "URI normalization functions" uriNormalizeSpec+  describe "Normalized file path functions" normalizedFilePathSpec++windowsOS :: String+windowsOS = "mingw32"++testPosixUri :: Uri+testPosixUri = Uri $ pack "file:///home/myself/example.hs"++testPosixFilePath :: FilePath+testPosixFilePath = "/home/myself/example.hs"++relativePosixFilePath :: FilePath+relativePosixFilePath = "myself/example.hs"++testWindowsUri :: Uri+testWindowsUri = Uri $ pack "file:///c:/Users/myself/example.hs"++testWindowsFilePath :: FilePath+testWindowsFilePath = "c:\\Users\\myself\\example.hs"++platformAwareUriFilePathSpec :: Spec+platformAwareUriFilePathSpec = do+  it "converts a URI to a POSIX file path" $ do+    let theFilePath = platformAwareUriToFilePath "posix" testPosixUri+    theFilePath `shouldBe` Just testPosixFilePath++  it "converts a POSIX file path to a URI" $ do+    let theUri = platformAwareFilePathToUri "posix" testPosixFilePath+    theUri `shouldBe` testPosixUri++  it "converts a URI to a Windows file path" $ do+    let theFilePath = platformAwareUriToFilePath windowsOS testWindowsUri+    theFilePath `shouldBe` Just testWindowsFilePath++  it "converts a Windows file path to a URI" $ do+    let theUri = platformAwareFilePathToUri windowsOS testWindowsFilePath+    theUri `shouldBe` testWindowsUri++  it "converts a POSIX file path to a URI" $ do+    let theFilePath = platformAwareFilePathToUri "posix" "./Functional.hs"+    theFilePath `shouldBe` (Uri "file://./Functional.hs")++  it "converts a Windows file path to a URI" $ do+    let theFilePath = platformAwareFilePathToUri windowsOS "./Functional.hs"+    theFilePath `shouldBe` (Uri "file:///./Functional.hs")++  it "converts a Windows file path to a URI" $ do+    let theFilePath = platformAwareFilePathToUri windowsOS "c:/Functional.hs"+    theFilePath `shouldBe` (Uri "file:///c:/Functional.hs")++  it "converts a POSIX file path to a URI and back" $ do+    let theFilePath = platformAwareFilePathToUri "posix" "./Functional.hs"+    theFilePath `shouldBe` (Uri "file://./Functional.hs")+    let Just (URI scheme' auth' path' query' frag') =  parseURI "file://./Functional.hs"+    (scheme',auth',path',query',frag') `shouldBe`+      ("file:"+      ,Just (URIAuth {uriUserInfo = "", uriRegName = ".", uriPort = ""}) -- AZ: Seems odd+      ,"/Functional.hs"+      ,""+      ,"")+    Just "./Functional.hs" `shouldBe` platformAwareUriToFilePath "posix" theFilePath++  it "converts a Posix file path to a URI and back" $ property $ forAll genPosixFilePath $ \fp -> do+      let uri = platformAwareFilePathToUri "posix" fp+      platformAwareUriToFilePath "posix" uri `shouldBe` Just fp++  it "converts a Windows file path to a URI and back" $ property $ forAll genWindowsFilePath $ \fp -> do+      let uri = platformAwareFilePathToUri windowsOS fp+      -- We normalise to account for changes in the path separator.+      -- But driver letters are *not* normalized so we skip them+      when (not $ "c:" `isPrefixOf` fp) $+        platformAwareUriToFilePath windowsOS uri `shouldBe` Just (FPW.normalise fp)++  it "converts a relative POSIX file path to a URI and back" $ do+    let uri = platformAwareFilePathToUri "posix" relativePosixFilePath+    uri `shouldBe` Uri "file://myself/example.hs"+    let back = platformAwareUriToFilePath "posix" uri+    back `shouldBe` Just relativePosixFilePath+++testUri :: Uri+testUri | isWindows = Uri "file:///C:/Users/myself/example.hs"+        | otherwise = Uri "file:///home/myself/example.hs"++testFilePath :: FilePath+testFilePath | isWindows = "C:\\Users\\myself\\example.hs"+             | otherwise = "/home/myself/example.hs"++withCurrentDirFilePath :: FilePath+withCurrentDirFilePath | isWindows = "C:\\Users\\.\\myself\\.\\.\\example.hs"+                       | otherwise = "/home/./myself/././example.hs"++fromRelativefilePathUri :: Uri+fromRelativefilePathUri | isWindows = Uri  "file:///myself/example.hs"+                        | otherwise = Uri "file://myself/example.hs"++relativeFilePath :: FilePath+relativeFilePath | isWindows = "myself\\example.hs"+                 | otherwise = "myself/example.hs"++withLowerCaseDriveLetterFilePath :: FilePath+withLowerCaseDriveLetterFilePath = "c:\\Users\\.\\myself\\.\\.\\example.hs"++withInitialCurrentDirUriStr :: String+withInitialCurrentDirUriStr | isWindows = "file:///Functional.hs"+                            | otherwise = "file://Functional.hs"++withInitialCurrentDirUriParts :: (String, Maybe URIAuth,  String, String, String)+withInitialCurrentDirUriParts+  | isWindows =+    ("file:"+    ,Just (URIAuth {uriUserInfo = "", uriRegName = "", uriPort = ""}) -- JNS: And asymmetrical+    ,"/Functional.hs","","")+  | otherwise =+     ("file:"+    ,Just (URIAuth {uriUserInfo = "", uriRegName = "Functional.hs", uriPort = ""}) -- AZ: Seems odd+    ,"","","")++withInitialCurrentDirFilePath :: FilePath+withInitialCurrentDirFilePath | isWindows = ".\\Functional.hs"+                              | otherwise = "./Functional.hs"++noNormalizedUriTxt :: Text+noNormalizedUriTxt | isWindows = "file:///c:/Users/./myself/././example.hs"+                   | otherwise = "file:///home/./myself/././example.hs"++noNormalizedUri :: Uri+noNormalizedUri = Uri noNormalizedUriTxt++uriFilePathSpec :: Spec+uriFilePathSpec = do+  it "converts a URI to a file path" $ do+    let theFilePath = uriToFilePath testUri+    theFilePath `shouldBe` Just testFilePath++  it "converts a file path to a URI" $ do+    let theUri = filePathToUri testFilePath+    theUri `shouldBe` testUri++  it "removes unnecessary current directory paths" $ do+    let theUri = filePathToUri withCurrentDirFilePath+    theUri `shouldBe` testUri++  when isWindows $+    it "make the drive letter upper case when converting a Windows file path to a URI" $ do+      let theUri = filePathToUri withLowerCaseDriveLetterFilePath+      theUri `shouldBe` testUri++  it "converts a file path to a URI and back" $ property $ forAll genFilePath $ \fp -> do+      let uri = filePathToUri fp+      uriToFilePath uri `shouldBe` Just (normalise fp)++  it "converts a relative file path to a URI and back" $ do+    let uri = filePathToUri relativeFilePath+    uri `shouldBe` fromRelativefilePathUri+    let back = uriToFilePath uri+    back `shouldBe` Just relativeFilePath++  it "converts a file path with initial current dir to a URI and back" $ do+    let uri = filePathToUri withInitialCurrentDirFilePath+    uri `shouldBe` (Uri (pack withInitialCurrentDirUriStr))+    let Just (URI scheme' auth' path' query' frag') =  parseURI withInitialCurrentDirUriStr+    (scheme',auth',path',query',frag') `shouldBe` withInitialCurrentDirUriParts+    Just "Functional.hs" `shouldBe` uriToFilePath uri++uriNormalizeSpec :: Spec+uriNormalizeSpec = do++  it "ignores differences in percent-encoding" $ property $ \uri ->+    toNormalizedUri (Uri $ pack $ escapeURIString isUnescapedInURI uri) `shouldBe`+        toNormalizedUri (Uri $ pack $ escapeURIString (const False) uri)++  it "ignores differences in percent-encoding (examples)" $ do+    toNormalizedUri (Uri $ pack "http://server/path%C3%B1?param=%C3%B1") `shouldBe`+        toNormalizedUri (Uri $ pack "http://server/path%c3%b1?param=%c3%b1")+    toNormalizedUri (Uri $ pack "file:///path%2A") `shouldBe`+        toNormalizedUri (Uri $ pack "file:///path%2a")++  it "normalizes uri file path when converting from uri to normalized uri" $ do+    let (NormalizedUri _ uri) = toNormalizedUri noNormalizedUri+    let (Uri nuri) = testUri+    uri `shouldBe` nuri++  it "converts a file path with reserved uri chars to a normalized URI and back" $ do+    let start = if isWindows then "C:\\" else "/"+    let fp = start ++ "path;part#fragmen?param=val"+    let nuri = toNormalizedUri (filePathToUri fp)+    uriToFilePath (fromNormalizedUri nuri) `shouldBe` Just fp++  it "converts a file path with substrings that looks like uri escaped chars and back" $ do+    let start = if isWindows then "C:\\" else "/"+    let fp = start ++ "ca%C3%B1a"+    let nuri = toNormalizedUri (filePathToUri fp)+    uriToFilePath (fromNormalizedUri nuri) `shouldBe` Just fp++  it "converts a file path to a normalized URI and back" $ property $ forAll genFilePath $ \fp -> do+    let nuri = toNormalizedUri (filePathToUri fp)+    case uriToFilePath (fromNormalizedUri nuri) of+      Just nfp -> nfp `shouldBe` (normalise fp)+      Nothing  -> return () -- Some unicode paths creates invalid uris, ignoring for now++genFilePath :: Gen FilePath+genFilePath | isWindows = genWindowsFilePath+            | otherwise = genPosixFilePath++genWindowsFilePath :: Gen FilePath+genWindowsFilePath = do+    segments <- listOf1 pathSegment+    pathSep <- elements ['/', '\\']+    driveLetter <- elements ["C:", "c:"]+    pure (driveLetter <> [pathSep] <> intercalate [pathSep] segments)+  where pathSegment = listOf1 (genValidUnicodeChar `suchThat` (`notElem` ['/', '\\', ':']))++genPosixFilePath :: Gen FilePath+genPosixFilePath = do+    segments <- listOf1 pathSegment+    pure ("/" <> intercalate "/" segments)+  where pathSegment = listOf1 (genValidUnicodeChar `suchThat` (`notElem` ['/']))++genValidUnicodeChar :: Gen Char+genValidUnicodeChar = arbitraryUnicodeChar `suchThat` isCharacter+  where isCharacter x = x /= '\65534' && x /= '\65535'++normalizedFilePathSpec :: Spec+normalizedFilePathSpec = beforeAll (setFileSystemEncoding utf8) $ do+  it "makes file path normalized" $ property $ forAll genFilePath $ \fp -> do+    let nfp = toNormalizedFilePath fp+    fromNormalizedFilePath nfp `shouldBe` (normalise fp)++  it "converts to a normalized uri and back" $ property $ forAll genFilePath $ \fp -> do+    let nuri = normalizedFilePathToUri (toNormalizedFilePath fp)+    case uriToNormalizedFilePath nuri of+      Just nfp -> fromNormalizedFilePath nfp `shouldBe` (normalise fp)+      Nothing  -> return () -- Some unicode paths creates invalid uris, ignoring for now++  it "converts a file path with reserved uri chars to a normalized URI and back" $ do+    let start = if isWindows then "C:\\" else "/"+    let fp = start ++ "path;part#fragmen?param=val"+    let nuri = normalizedFilePathToUri (toNormalizedFilePath fp)+    fmap fromNormalizedFilePath (uriToNormalizedFilePath nuri) `shouldBe` Just fp++  it "converts a file path with substrings that looks like uri escaped chars and back" $ do+    let start = if isWindows then "C:\\" else "/"+    let fp = start ++ "ca%C3%B1a"+    let nuri = normalizedFilePathToUri (toNormalizedFilePath fp)+    fmap fromNormalizedFilePath (uriToNormalizedFilePath nuri) `shouldBe` Just fp++  it "creates the same NormalizedUri than the older implementation" $ property $ forAll genFilePath $ \fp -> do+    let nuri = normalizedFilePathToUri (toNormalizedFilePath fp)+    let oldNuri = toNormalizedUri (filePathToUri fp)+    nuri `shouldBe` oldNuri++#ifdef OS_PATH+  it "converts to NormalizedFilePath and back sucessfully" $ property $ forAll genFilePath $ \fp -> do+    let osPath = fromJust (OsPath.encodeUtf fp)+    osPath' <- osPathToNormalizedFilePath osPath >>= normalizedFilePathToOsPath+    osPath' `shouldBe` OsPath.normalise osPath++  it "can not convert OsPath in non-standard encoding to NormalizedFilePath" $+    -- Windows always use UTF16LE, the following test case doesn't apply+    when (not isWindows) $+      -- \184921 is an example that the raw bytes of UTF16 is not valid UTF8.+      -- Case like this is not very common. I found it with the help of QuickCheck.+      case OsPath.encodeWith utf16be utf16be "\184921" of+        Left err -> throwIO err+        Right osPath  -> do+          osPathToNormalizedFilePath osPath `shouldThrow` \(_ :: EncodingException) -> True+#endif
+ test/WorkspaceEditSpec.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE OverloadedStrings #-}+module WorkspaceEditSpec where++import Test.Hspec+import Language.LSP.Types++spec :: Spec+spec = do+  describe "applyTextEdit" $ do+    it "inserts text" $+      let te = TextEdit (Range (Position 1 2) (Position 1 2)) "foo"+        in applyTextEdit te "lorem\nipsum\ndolor" `shouldBe` "lorem\nipfoosum\ndolor"+    it "deletes text" $+      let te = TextEdit (Range (Position 0 2) (Position 1 2)) ""+        in applyTextEdit te "lorem\nipsum\ndolor" `shouldBe` "losum\ndolor"+    it "edits a multiline text" $+      let te = TextEdit (Range (Position 1 0) (Position 2 0)) "slorem"+        in applyTextEdit te "lorem\nipsum\ndolor" `shouldBe` "lorem\nsloremdolor"+    it "inserts text past the last line" $+      let te = TextEdit (Range (Position 3 2) (Position 3 2)) "foo"+        in applyTextEdit te "lorem\nipsum\ndolor" `shouldBe` "lorem\nipsum\ndolorfoo"++  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"+          expected = TextEdit (Range (Position 1 1) (Position 2 2)) "helios\ngold"+         in editTextEdit orig inner `shouldBe` expected