Skip to content

Commit 78dc807

Browse files
author
Kobayashi
authored
use OsPath in NormalizedFilePath (#446)
* use filepath-compat * make NormalizedFilePath use OsPath * remove filepath-compat, use ShortByteString instead * fix ci * enable OS_PATH * upgrade ghc & don't set OS_PATH for ghc 8.6.5 in CI * remove outdated comment * skip a bad ci combination * extract OsPath related CPP to a standalone module * fix OsPath.Compat * add empty NormalizedUri and NormalizedFilePath * add partial function for compatibility & use decodeFS * run stylish-haskell * lock index-state * bump versions of lsp and lsp-types * fix self version bound * always use utf8 ShortByteString in NormalizedFilePath * revert test changes * remove OsPath.Compat * bump lsp-test version according to pvp * add test cases for OsPath * fix test case * add some doc comments
1 parent 5dc4edd commit 78dc807

File tree

9 files changed

+195
-56
lines changed

9 files changed

+195
-56
lines changed

.github/workflows/haskell.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,17 @@ jobs:
99
strategy:
1010
fail-fast: false
1111
matrix:
12-
ghc: ['9.2.1', '9.0.1', '8.10.7', '8.8.4', '8.6.5']
12+
ghc: ['9.2.4', '9.0.2', '8.10.7', '8.8.4', '8.6.5']
1313
os: [ubuntu-latest, macOS-latest, windows-latest]
14+
ospath: [true, false]
15+
exclude:
16+
# newer 'entropy' doesn't work with old 'unix', and it doesn't have a correct version bound.
17+
- ospath: true
18+
ghc: 8.6.5
19+
# "cabal build" always timeout
20+
- ospath: true
21+
ghc: 8.8.4
22+
os: windows-latest
1423

1524
steps:
1625
- uses: actions/checkout@v2
@@ -41,6 +50,12 @@ jobs:
4150
4251
- name: Cabal update
4352
run: cabal update
53+
- name: Cabal configure
54+
shell: bash
55+
run: |
56+
if [ ${{ matrix.ospath }} = "true" ]; then
57+
cabal configure --constraint="filepath ^>= 1.4.100.0"
58+
fi
4459
- name: Build using cabal
4560
run: cabal build all
4661
- name: Test

cabal.project

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ packages:
66
package lsp
77
flags: +demo
88

9+
index-state: 2022-08-25T22:25:05Z
10+
911
tests: True
1012
benchmarks: True
1113
test-show-details: direct

lsp-test/lsp-test.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
cabal-version: 2.4
22
name: lsp-test
3-
version: 0.14.0.3
3+
version: 0.14.1.0
44
synopsis: Functional test framework for LSP servers.
55
description:
66
A test framework for writing tests against

lsp-types/lsp-types.cabal

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
cabal-version: 2.2
22
name: lsp-types
3-
version: 1.5.0.0
3+
version: 1.5.1.0
44
synopsis: Haskell library for the Microsoft Language Server Protocol, data types
55

66
description: An implementation of the types to allow language implementors to
@@ -68,6 +68,7 @@ library
6868
, Language.LSP.Types.WorkspaceEdit
6969
, Language.LSP.Types.WorkspaceFolders
7070
, Language.LSP.Types.WorkspaceSymbol
71+
, Language.LSP.Types.Uri.OsPath
7172
-- other-extensions:
7273
ghc-options: -Wall
7374
build-depends: base >= 4.11 && < 5
@@ -89,6 +90,9 @@ library
8990
, text
9091
, template-haskell
9192
, unordered-containers
93+
, exceptions
94+
, safe
95+
, bytestring
9296
hs-source-dirs: src
9397
default-language: Haskell2010
9498
default-extensions: StrictData
@@ -106,6 +110,7 @@ test-suite lsp-types-test
106110
TypesSpec
107111
URIFilePathSpec
108112
WorkspaceEditSpec
113+
LocationSpec
109114
build-depends: base
110115
, QuickCheck
111116
-- for instance Arbitrary Value
@@ -117,6 +122,7 @@ test-suite lsp-types-test
117122
, network-uri
118123
, quickcheck-instances
119124
, text
125+
, tuple
120126
build-tool-depends: hspec-discover:hspec-discover
121127
ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall
122128
default-language: Haskell2010

lsp-types/src/Language/LSP/Types.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ module Language.LSP.Types
3737
, module Language.LSP.Types.TextDocument
3838
, module Language.LSP.Types.TypeDefinition
3939
, module Language.LSP.Types.Uri
40+
, module Language.LSP.Types.Uri.OsPath
4041
, module Language.LSP.Types.WatchedFiles
4142
, module Language.LSP.Types.Window
4243
, module Language.LSP.Types.WorkspaceEdit
@@ -69,8 +70,8 @@ import Language.LSP.Types.Initialize
6970
import Language.LSP.Types.Location
7071
import Language.LSP.Types.LspId
7172
import Language.LSP.Types.MarkupContent
72-
import Language.LSP.Types.Method
7373
import Language.LSP.Types.Message
74+
import Language.LSP.Types.Method
7475
import Language.LSP.Types.Parsing
7576
import Language.LSP.Types.Progress
7677
import Language.LSP.Types.References
@@ -83,6 +84,7 @@ import Language.LSP.Types.StaticRegistrationOptions
8384
import Language.LSP.Types.TextDocument
8485
import Language.LSP.Types.TypeDefinition
8586
import Language.LSP.Types.Uri
87+
import Language.LSP.Types.Uri.OsPath
8688
import Language.LSP.Types.WatchedFiles
8789
import Language.LSP.Types.Window
8890
import Language.LSP.Types.WorkspaceEdit

lsp-types/src/Language/LSP/Types/Uri.hs

Lines changed: 73 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
{-# LANGUAGE LambdaCase #-}
1+
{-# LANGUAGE DeriveGeneric #-}
22
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3-
{-# LANGUAGE DeriveGeneric #-}
4-
{-# LANGUAGE RecordWildCards #-}
3+
{-# LANGUAGE InstanceSigs #-}
4+
{-# LANGUAGE OverloadedStrings #-}
5+
{-# LANGUAGE RankNTypes #-}
6+
{-# LANGUAGE RecordWildCards #-}
7+
{-# LANGUAGE TypeSynonymInstances #-}
8+
59
module Language.LSP.Types.Uri
610
( Uri(..)
711
, uriToFilePath
@@ -10,32 +14,39 @@ module Language.LSP.Types.Uri
1014
, toNormalizedUri
1115
, fromNormalizedUri
1216
, NormalizedFilePath
13-
, normalizedFilePath
1417
, toNormalizedFilePath
1518
, fromNormalizedFilePath
1619
, normalizedFilePathToUri
1720
, uriToNormalizedFilePath
21+
, emptyNormalizedFilePath
1822
-- Private functions
1923
, platformAwareUriToFilePath
2024
, platformAwareFilePathToUri
2125
)
2226
where
2327

2428
import Control.DeepSeq
25-
import qualified Data.Aeson as A
26-
import Data.Binary (Binary, Get, put, get)
29+
import qualified Data.Aeson as A
30+
import Data.Binary (Binary, Get, get, put)
31+
import Data.ByteString.Short (ShortByteString)
32+
import qualified Data.ByteString.Short as BS
2733
import Data.Hashable
28-
import Data.List (stripPrefix)
29-
import Data.String (IsString, fromString)
30-
import Data.Text (Text)
31-
import qualified Data.Text as T
34+
import Data.List (stripPrefix)
35+
import Data.String (IsString (fromString))
36+
import Data.Text (Text)
37+
import qualified Data.Text as T
38+
import qualified Data.Text.Encoding as T
39+
import Data.Text.Encoding.Error (UnicodeException)
3240
import GHC.Generics
33-
import Network.URI hiding (authority)
34-
import qualified System.FilePath as FP
35-
import qualified System.FilePath.Posix as FPP
36-
import qualified System.FilePath.Windows as FPW
41+
import GHC.Stack (HasCallStack)
42+
import Network.URI hiding (authority)
43+
import Safe (tailMay)
44+
import qualified System.FilePath as FP
45+
import qualified System.FilePath.Posix as FPP
46+
import qualified System.FilePath.Windows as FPW
3747
import qualified System.Info
3848

49+
3950
newtype Uri = Uri { getUri :: Text }
4051
deriving (Eq,Ord,Read,Show,Generic,A.FromJSON,A.ToJSON,Hashable,A.ToJSONKey,A.FromJSONKey)
4152

@@ -67,7 +78,7 @@ isUnescapedInUriPath systemOS c
6778
normalizeUriEscaping :: String -> String
6879
normalizeUriEscaping uri =
6980
case stripPrefix (fileScheme ++ "//") uri of
70-
Just p -> fileScheme ++ "//" ++ (escapeURIPath $ unEscapeString p)
81+
Just p -> fileScheme ++ "//" ++ escapeURIPath (unEscapeString p)
7182
Nothing -> escapeURIString isUnescapedInURI $ unEscapeString uri
7283
where escapeURIPath = escapeURIString (isUnescapedInUriPath System.Info.os)
7384

@@ -107,17 +118,19 @@ platformAdjustFromUriPath :: SystemOS
107118
-> String -- ^ path
108119
-> FilePath
109120
platformAdjustFromUriPath systemOS authority srcPath =
110-
(maybe id (++) authority) $
111-
if systemOS /= windowsOS || null srcPath then srcPath
112-
else let
113-
firstSegment:rest = (FPP.splitDirectories . tail) srcPath -- Drop leading '/' for absolute Windows paths
114-
drive = if FPW.isDrive firstSegment
115-
then FPW.addTrailingPathSeparator firstSegment
116-
else firstSegment
117-
in FPW.joinDrive drive $ FPW.joinPath rest
121+
maybe id (++) authority $
122+
if systemOS /= windowsOS
123+
then srcPath
124+
else case FPP.splitDirectories <$> tailMay srcPath of
125+
Just (firstSegment:rest) -> -- Drop leading '/' for absolute Windows paths
126+
let drive = if FPW.isDrive firstSegment
127+
then FPW.addTrailingPathSeparator firstSegment
128+
else firstSegment
129+
in FPW.joinDrive drive $ FPW.joinPath rest
130+
_ -> srcPath
118131

119132
filePathToUri :: FilePath -> Uri
120-
filePathToUri = (platformAwareFilePathToUri System.Info.os) . FP.normalise
133+
filePathToUri = platformAwareFilePathToUri System.Info.os . FP.normalise
121134

122135
{-# WARNING platformAwareFilePathToUri "This function is considered private. Use normalizedUriToFilePath instead." #-}
123136
platformAwareFilePathToUri :: SystemOS -> FilePath -> Uri
@@ -151,27 +164,32 @@ platformAdjustToUriPath systemOS srcPath
151164
FPP.addTrailingPathSeparator (init drv)
152165
| otherwise = drv
153166

154-
-- | Newtype wrapper around FilePath that always has normalized slashes.
155-
-- The NormalizedUri and hash of the FilePath are cached to avoided
167+
-- | A file path that is already normalized. It is stored as an UTF-8 encoded 'ShortByteString'
168+
--
169+
-- The 'NormalizedUri' is cached to avoided
156170
-- repeated normalisation when we need to compute them (which is a lot).
157171
--
158172
-- This is one of the most performance critical parts of ghcide, do not
159173
-- modify it without profiling.
160-
data NormalizedFilePath = NormalizedFilePath NormalizedUri !FilePath
174+
data NormalizedFilePath = NormalizedFilePath !NormalizedUri {-# UNPACK #-} !ShortByteString
161175
deriving (Generic, Eq, Ord)
162176

163177
instance NFData NormalizedFilePath
164178

165179
instance Binary NormalizedFilePath where
166180
put (NormalizedFilePath _ fp) = put fp
167181
get = do
168-
v <- Data.Binary.get :: Get FilePath
169-
let nuri = internalNormalizedFilePathToUri v
170-
return (normalizedFilePath nuri v)
182+
v <- Data.Binary.get :: Get ShortByteString
183+
case decodeFilePath v of
184+
Left e -> fail (show e)
185+
Right v' ->
186+
return (NormalizedFilePath (internalNormalizedFilePathToUri v') v)
187+
188+
encodeFilePath :: String -> ShortByteString
189+
encodeFilePath = BS.toShort . T.encodeUtf8 . T.pack
171190

172-
-- | A smart constructor that performs UTF-8 encoding and hash consing
173-
normalizedFilePath :: NormalizedUri -> FilePath -> NormalizedFilePath
174-
normalizedFilePath nuri nfp = NormalizedFilePath nuri nfp
191+
decodeFilePath :: ShortByteString -> Either UnicodeException String
192+
decodeFilePath = fmap T.unpack . T.decodeUtf8' . BS.fromShort
175193

176194
-- | Internal helper that takes a file path that is assumed to
177195
-- already be normalized to a URI. It is up to the caller
@@ -191,20 +209,36 @@ instance Hashable NormalizedFilePath where
191209
hashWithSalt salt (NormalizedFilePath uri _) = hashWithSalt salt uri
192210

193211
instance IsString NormalizedFilePath where
212+
fromString :: String -> NormalizedFilePath
194213
fromString = toNormalizedFilePath
195214

196215
toNormalizedFilePath :: FilePath -> NormalizedFilePath
197-
toNormalizedFilePath fp = normalizedFilePath nuri nfp
216+
toNormalizedFilePath fp = NormalizedFilePath nuri . encodeFilePath $ nfp
198217
where
199-
nfp = FP.normalise fp
200-
nuri = internalNormalizedFilePathToUri nfp
218+
nfp = FP.normalise fp
219+
nuri = internalNormalizedFilePathToUri nfp
201220

202-
fromNormalizedFilePath :: NormalizedFilePath -> FilePath
203-
fromNormalizedFilePath (NormalizedFilePath _ fp) = fp
221+
-- | Extracts 'FilePath' from 'NormalizedFilePath'.
222+
-- The function is total. The 'HasCallStack' constraint is added for debugging purpose only.
223+
fromNormalizedFilePath :: HasCallStack => NormalizedFilePath -> FilePath
224+
fromNormalizedFilePath (NormalizedFilePath _ fp) =
225+
case decodeFilePath fp of
226+
Left e -> error $ show e
227+
Right x -> x
204228

205229
normalizedFilePathToUri :: NormalizedFilePath -> NormalizedUri
206230
normalizedFilePathToUri (NormalizedFilePath uri _) = uri
207231

208232
uriToNormalizedFilePath :: NormalizedUri -> Maybe NormalizedFilePath
209-
uriToNormalizedFilePath nuri = fmap (normalizedFilePath nuri) mbFilePath
233+
uriToNormalizedFilePath nuri = fmap (NormalizedFilePath nuri . encodeFilePath) mbFilePath
210234
where mbFilePath = platformAwareUriToFilePath System.Info.os (fromNormalizedUri nuri)
235+
236+
emptyNormalizedUri :: NormalizedUri
237+
emptyNormalizedUri =
238+
let s = "file://"
239+
in NormalizedUri (hash s) s
240+
241+
-- | 'NormalizedFilePath' that contains an empty file path
242+
emptyNormalizedFilePath :: NormalizedFilePath
243+
emptyNormalizedFilePath = NormalizedFilePath emptyNormalizedUri ""
244+
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{-# LANGUAGE CPP #-}
2+
{-# LANGUAGE ScopedTypeVariables #-}
3+
4+
#if MIN_VERSION_filepath(1,4,100)
5+
#define OS_PATH 1
6+
#endif
7+
8+
module Language.LSP.Types.Uri.OsPath
9+
(
10+
#ifdef OS_PATH
11+
osPathToNormalizedFilePath
12+
, normalizedFilePathToOsPath
13+
#endif
14+
) where
15+
16+
#ifdef OS_PATH
17+
18+
import Control.DeepSeq (NFData, force)
19+
import Control.Exception hiding (try)
20+
import Control.Monad.Catch
21+
import Language.LSP.Types.Uri
22+
import System.IO.Unsafe (unsafePerformIO)
23+
import System.OsPath
24+
25+
{-|
26+
Constructs 'NormalizedFilePath' from 'OsPath'. Throws 'IOException' if the conversion fails.
27+
-}
28+
osPathToNormalizedFilePath :: MonadThrow m => OsPath -> m NormalizedFilePath
29+
osPathToNormalizedFilePath = fmap toNormalizedFilePath . unsafePerformIO' . decodeFS
30+
31+
{-|
32+
Extracts 'OsPath' from 'NormalizedFilePath'. Throws 'IOException' if the conversion fails.
33+
-}
34+
normalizedFilePathToOsPath :: MonadThrow m => NormalizedFilePath -> m OsPath
35+
normalizedFilePathToOsPath = unsafePerformIO' . encodeFS . fromNormalizedFilePath
36+
37+
unsafePerformIO' :: (MonadThrow m, NFData a) => IO a -> m a
38+
unsafePerformIO' action =
39+
case fp of
40+
Left (e :: SomeException) -> throwM e
41+
Right fp' -> pure fp'
42+
where
43+
fp = unsafePerformIO . try $ do
44+
x <- action
45+
evaluate . force $ x
46+
47+
#endif

0 commit comments

Comments
 (0)