-
Notifications
You must be signed in to change notification settings - Fork 95
use OsPath in NormalizedFilePath #446
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c64e8be
05c8eb8
bd0b0e3
ec572b1
9d1c18f
8fcb58f
1a3a463
69bd9ef
fa6b362
fa72e57
cd9cec3
1d151e4
f675a60
9945200
ef2a064
43fc9c8
0a88fe8
b4b4b21
7e86651
2fd5f81
2b8d61f
80ebd81
f46250c
488cfe2
eed3a65
c45d471
ee1702b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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,32 +14,39 @@ module Language.LSP.Types.Uri | |
, toNormalizedUri | ||
, fromNormalizedUri | ||
, NormalizedFilePath | ||
, normalizedFilePath | ||
, toNormalizedFilePath | ||
, fromNormalizedFilePath | ||
, normalizedFilePathToUri | ||
, uriToNormalizedFilePath | ||
, emptyNormalizedFilePath | ||
-- Private functions | ||
, platformAwareUriToFilePath | ||
, platformAwareFilePathToUri | ||
) | ||
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.ByteString.Short (ShortByteString) | ||
import qualified Data.ByteString.Short as BS | ||
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 qualified Data.Text.Encoding as T | ||
import Data.Text.Encoding.Error (UnicodeException) | ||
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 GHC.Stack (HasCallStack) | ||
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 } | ||
deriving (Eq,Ord,Read,Show,Generic,A.FromJSON,A.ToJSON,Hashable,A.ToJSONKey,A.FromJSONKey) | ||
|
||
|
@@ -67,7 +78,7 @@ isUnescapedInUriPath systemOS c | |
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 +118,19 @@ platformAdjustFromUriPath :: SystemOS | |
-> 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 | ||
kokobd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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,27 +164,32 @@ platformAdjustToUriPath systemOS srcPath | |
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 | ||
-- | A file path that is already normalized. It is stored as an UTF-8 encoded 'ShortByteString' | ||
-- | ||
-- 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 ghcide, do not | ||
-- modify it without profiling. | ||
data NormalizedFilePath = NormalizedFilePath NormalizedUri !FilePath | ||
data NormalizedFilePath = NormalizedFilePath !NormalizedUri {-# UNPACK #-} !ShortByteString | ||
kokobd marked this conversation as resolved.
Show resolved
Hide resolved
kokobd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
deriving (Generic, Eq, Ord) | ||
|
||
instance NFData NormalizedFilePath | ||
|
||
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) | ||
v <- Data.Binary.get :: Get ShortByteString | ||
case decodeFilePath v of | ||
Left e -> fail (show e) | ||
Right v' -> | ||
return (NormalizedFilePath (internalNormalizedFilePathToUri v') v) | ||
|
||
encodeFilePath :: String -> ShortByteString | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. haddock? |
||
encodeFilePath = BS.toShort . T.encodeUtf8 . T.pack | ||
|
||
-- | A smart constructor that performs UTF-8 encoding and hash consing | ||
normalizedFilePath :: NormalizedUri -> FilePath -> NormalizedFilePath | ||
normalizedFilePath nuri nfp = NormalizedFilePath nuri nfp | ||
decodeFilePath :: ShortByteString -> Either UnicodeException String | ||
kokobd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
decodeFilePath = fmap T.unpack . T.decodeUtf8' . BS.fromShort | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can always speed this up with something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
-- | 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 +209,36 @@ instance Hashable NormalizedFilePath where | |
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 . encodeFilePath $ nfp | ||
where | ||
nfp = FP.normalise fp | ||
nuri = internalNormalizedFilePathToUri nfp | ||
nfp = FP.normalise fp | ||
nuri = internalNormalizedFilePathToUri nfp | ||
|
||
fromNormalizedFilePath :: NormalizedFilePath -> FilePath | ||
fromNormalizedFilePath (NormalizedFilePath _ fp) = fp | ||
-- | Extracts 'FilePath' from 'NormalizedFilePath'. | ||
-- The function is total. The 'HasCallStack' constraint is added for debugging purpose only. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a little confusing as it immediately calls error! I get the argument: the invariant of NFP is that it contains a correctly encoded string so this should never fail. However, I also bet we call this a lot. What would happen if we also cached the decoded filepath in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, memory usage is the point of using I saw most use sites of |
||
fromNormalizedFilePath :: HasCallStack => NormalizedFilePath -> FilePath | ||
fromNormalizedFilePath (NormalizedFilePath _ fp) = | ||
case decodeFilePath fp of | ||
Left e -> error $ show e | ||
Right x -> x | ||
|
||
normalizedFilePathToUri :: NormalizedFilePath -> NormalizedUri | ||
normalizedFilePathToUri (NormalizedFilePath uri _) = uri | ||
|
||
uriToNormalizedFilePath :: NormalizedUri -> Maybe NormalizedFilePath | ||
uriToNormalizedFilePath nuri = fmap (normalizedFilePath nuri) mbFilePath | ||
uriToNormalizedFilePath nuri = fmap (NormalizedFilePath nuri . encodeFilePath) 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 "" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
{-# 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 | ||
#endif | ||
) where | ||
|
||
#ifdef OS_PATH | ||
|
||
import Control.DeepSeq (NFData, force) | ||
import Control.Exception hiding (try) | ||
import Control.Monad.Catch | ||
import Language.LSP.Types.Uri | ||
import System.IO.Unsafe (unsafePerformIO) | ||
import System.OsPath | ||
|
||
{-| | ||
Constructs 'NormalizedFilePath' from 'OsPath'. Throws 'IOException' if the conversion fails. | ||
-} | ||
osPathToNormalizedFilePath :: MonadThrow m => OsPath -> m NormalizedFilePath | ||
osPathToNormalizedFilePath = fmap toNormalizedFilePath . unsafePerformIO' . decodeFS | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm getting a little confused, would you mind writing a comment somewhere explaining the deal with the various encodings? The idea is something like: we always decode OsPaths using the system encoding into a string, and then we re-encode them as UTF8 to store them in NFP? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have missed all the discussion in the PR, so I need a comment explaining why we don't do the obvious thing (which would be storing the
Comment on lines
+28
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Applied in #453. Thank you! |
||
|
||
{-| | ||
Extracts 'OsPath' from 'NormalizedFilePath'. Throws 'IOException' if the conversion fails. | ||
-} | ||
normalizedFilePathToOsPath :: MonadThrow m => NormalizedFilePath -> m OsPath | ||
normalizedFilePathToOsPath = unsafePerformIO' . encodeFS . fromNormalizedFilePath | ||
|
||
unsafePerformIO' :: (MonadThrow m, NFData a) => IO a -> m a | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. doc? |
||
unsafePerformIO' action = | ||
case fp of | ||
Left (e :: SomeException) -> throwM e | ||
Right fp' -> pure fp' | ||
where | ||
fp = unsafePerformIO . try $ do | ||
x <- action | ||
evaluate . force $ x | ||
|
||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should include this as a flag in the package definition?
force-ospath
or something?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice idea!