{-# LANGUAGE DataKinds #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeOperators #-} module Haskell.Ide.Engine.PluginUtils ( mapEithers , pluginGetFile , makeDiffResult , WithDeletions(..) , makeAdditiveDiffResult , diffText , diffText' , srcSpan2Range , srcSpan2Loc , unpackRealSrcSpan , reverseMapFile , extractRange , fullRange , fileInfo , realSrcSpan2Range , canonicalizeUri , newRangeToOld , oldRangeToNew , newPosToOld , oldPosToNew , unPos , toPos , clientSupportsDocumentChanges , readVFS , getRangeFromVFS , rangeLinesFromVfs , gcatches , ErrorHandler(..) ) where import Control.Monad.IO.Class import Control.Monad.Reader import Control.Monad.Trans.Except import Data.Aeson import Data.Algorithm.Diff import Data.Algorithm.DiffOutput import qualified Data.HashMap.Strict as H import Data.Monoid import qualified Data.Text as T import qualified Data.Text.IO as T import Data.Maybe import FastString import Haskell.Ide.Engine.PluginsIdeMonads import Haskell.Ide.Engine.GhcModuleCache import Haskell.Ide.Engine.MonadFunctions import Haskell.Ide.Engine.ArtifactMap import Language.Haskell.LSP.VFS import Language.Haskell.LSP.Types.Capabilities import qualified Language.Haskell.LSP.Types as J import Prelude hiding (log) import SrcLoc (SrcSpan(..), RealSrcSpan(..)) import Exception import System.Directory import System.FilePath -- --------------------------------------------------------------------- canonicalizeUri :: MonadIO m => Uri -> m Uri canonicalizeUri uri = case uriToFilePath uri of Nothing -> return uri Just fp -> do fp' <- liftIO $ canonicalizePath fp return $ filePathToUri fp' newRangeToOld :: CachedInfo -> Range -> Maybe Range newRangeToOld info (Range start end) = do start' <- newPosToOld info start end' <- newPosToOld info end return (Range start' end') oldRangeToNew :: CachedInfo -> Range -> Maybe Range oldRangeToNew info (Range start end) = do start' <- oldPosToNew info start end' <- oldPosToNew info end return (Range start' end') getRealSrcSpan :: SrcSpan -> Either T.Text RealSrcSpan getRealSrcSpan (RealSrcSpan r) = Right r getRealSrcSpan (UnhelpfulSpan x) = Left $ T.pack $ unpackFS x realSrcSpan2Range :: RealSrcSpan -> Range realSrcSpan2Range = uncurry Range . unpackRealSrcSpan srcSpan2Range :: SrcSpan -> Either T.Text Range srcSpan2Range spn = realSrcSpan2Range <$> getRealSrcSpan spn reverseMapFile :: MonadIO m => (FilePath -> FilePath) -> FilePath -> m FilePath reverseMapFile rfm fp = do fp' <- liftIO $ canonicalizePath fp debugm $ "reverseMapFile: mapped file is " ++ fp' let orig = rfm fp' debugm $ "reverseMapFile: original is " ++ orig orig' <- liftIO $ canonicalizePath orig debugm $ "reverseMapFile: Canonicalized original is " ++ orig return orig' srcSpan2Loc :: (MonadIO m) => (FilePath -> FilePath) -> SrcSpan -> m (Either T.Text Location) srcSpan2Loc revMapp spn = runExceptT $ do let foo :: (Monad m) => Either T.Text RealSrcSpan -> ExceptT T.Text m RealSrcSpan foo (Left e) = throwE e foo (Right v) = pure v rspan <- foo $ getRealSrcSpan spn let fp = unpackFS $ srcSpanFile rspan debugm $ "srcSpan2Loc: mapped file is " ++ fp file <- reverseMapFile revMapp fp debugm $ "srcSpan2Loc: Original file is " ++ file return $ Location (filePathToUri file) (realSrcSpan2Range rspan) -- --------------------------------------------------------------------- -- | Helper function that extracts a filepath from a Uri if the Uri -- is well formed (i.e. begins with a file:// ) -- fails with an IdeResultFail otherwise pluginGetFile :: Monad m => T.Text -> Uri -> (FilePath -> m (IdeResult a)) -> m (IdeResult a) pluginGetFile name uri f = case uriToFilePath uri of Just file -> f file Nothing -> return $ IdeResultFail (IdeError PluginError (name <> "Couldn't resolve uri" <> getUri uri) Null) -- --------------------------------------------------------------------- -- courtesy of http://stackoverflow.com/questions/19891061/mapeithers-function-in-haskell mapEithers :: (a -> Either b c) -> [a] -> Either b [c] mapEithers f (x:xs) = case mapEithers f xs of Left err -> Left err Right ys -> case f x of Left err -> Left err Right y -> Right (y:ys) mapEithers _ _ = Right [] -- --------------------------------------------------------------------- data WithDeletions = IncludeDeletions | SkipDeletions deriving Eq -- | Generate a 'WorkspaceEdit' value from an original file and text to replace it. makeDiffResult :: FilePath -> T.Text -> (FilePath -> FilePath) -> IdeM WorkspaceEdit makeDiffResult orig new fileMap = do origText <- liftIO $ T.readFile orig let fp' = fileMap orig fp <- liftIO $ makeAbsolute fp' diffText (filePathToUri fp,origText) new IncludeDeletions -- | A version of 'makeDiffResult' that has does not insert any deletions makeAdditiveDiffResult :: FilePath -> T.Text -> (FilePath -> FilePath) -> IdeM WorkspaceEdit makeAdditiveDiffResult orig new fileMap = do origText <- liftIO $ T.readFile orig let fp' = fileMap orig fp <- liftIO $ makeAbsolute fp' diffText (filePathToUri fp,origText) new SkipDeletions -- | Generate a 'WorkspaceEdit' value from a pair of source Text -- TODO: Doesn't seem to work with 'editHpackPackage'? diffText :: (Uri,T.Text) -> T.Text -> WithDeletions -> IdeM WorkspaceEdit diffText old new withDeletions = do supports <- clientSupportsDocumentChanges return $ diffText' supports old new withDeletions -- | A pure version of 'diffText' for testing diffText' :: Bool -> (Uri,T.Text) -> T.Text -> WithDeletions -> WorkspaceEdit diffText' supports (f,fText) f2Text withDeletions = if supports then WorkspaceEdit Nothing (Just docChanges) else WorkspaceEdit (Just h) Nothing where d = getGroupedDiff (lines $ T.unpack fText) (lines $ T.unpack f2Text) diffOps = filter (\x -> (withDeletions == IncludeDeletions) || not (isDeletion x)) (diffToLineRanges d) isDeletion (Deletion _ _) = True isDeletion _ = False r = map diffOperationToTextEdit diffOps diff = J.List r h = H.singleton f diff docChanges = J.List [docEdit] docEdit = J.TextDocumentEdit (J.VersionedTextDocumentIdentifier f (Just 0)) diff diffOperationToTextEdit :: DiffOperation LineRange -> J.TextEdit diffOperationToTextEdit (Change fm to) = J.TextEdit range nt where range = calcRange fm nt = T.pack $ init $ unlines $ lrContents to {- In order to replace everything including newline characters, the end range should extend below the last line. From the specification: "If you want to specify a range that contains a line including the line ending character(s) then use an end position denoting the start of the next line" -} diffOperationToTextEdit (Deletion (LineRange (sl, el) _) _) = J.TextEdit range "" where range = J.Range (J.Position (sl - 1) 0) (J.Position el 0) diffOperationToTextEdit (Addition fm l) = J.TextEdit range nt -- fm has a range wrt to the changed file, which starts in the current file at l -- So the range has to be shifted to start at l where range = J.Range (J.Position (l' - 1) 0) (J.Position (l' - 1) 0) l' = max l sl -- Needed to add at the end of the file sl = fst $ lrNumbers fm nt = T.pack $ unlines $ lrContents fm calcRange fm = J.Range s e where sl = fst $ lrNumbers fm sc = 0 s = J.Position (sl - 1) sc -- Note: zero-based lines el = snd $ lrNumbers fm ec = length $ last $ lrContents fm e = J.Position (el - 1) ec -- Note: zero-based lines -- --------------------------------------------------------------------- extractRange :: Range -> T.Text -> T.Text extractRange (Range (Position sl _) (Position el _)) s = newS where focusLines = take (el-sl+1) $ drop sl $ T.lines s newS = T.unlines focusLines -- | Gets the range that covers the entire text fullRange :: T.Text -> Range fullRange s = Range startPos endPos where startPos = Position 0 0 endPos = Position lastLine 0 {- In order to replace everything including newline characters, the end range should extend below the last line. From the specification: "If you want to specify a range that contains a line including the line ending character(s) then use an end position denoting the start of the next line" -} lastLine = length $ T.lines s -- --------------------------------------------------------------------- -- | Returns the directory and file name fileInfo :: T.Text -> (FilePath,FilePath) fileInfo tfileName = let sfileName = T.unpack tfileName dir = takeDirectory sfileName in (dir,sfileName) -- --------------------------------------------------------------------- clientSupportsDocumentChanges :: IdeM Bool clientSupportsDocumentChanges = do ClientCapabilities mwCaps _ _ _ <- getClientCapabilities let supports = do wCaps <- mwCaps WorkspaceEditClientCapabilities mDc <- _workspaceEdit wCaps mDc return $ fromMaybe False supports -- --------------------------------------------------------------------- readVFS :: (MonadIde m, MonadIO m) => Uri -> m (Maybe T.Text) readVFS uri = do mvf <- getVirtualFile uri case mvf of Just vf -> return $ Just (virtualFileText vf) Nothing -> return Nothing getRangeFromVFS :: (MonadIde m, MonadIO m) => Uri -> Range -> m (Maybe T.Text) getRangeFromVFS uri rg = do mvf <- getVirtualFile uri case mvf of Just vfs -> return $ Just $ rangeLinesFromVfs vfs rg Nothing -> return Nothing -- Error catching utilities data ErrorHandler m a = forall e . Exception e => ErrorHandler (e -> m a) gcatches :: forall m a . (MonadIO m, ExceptionMonad m) => m a -> [ErrorHandler m a] -> m a gcatches act handlers = gcatch act h where h :: SomeException -> m a h e = foldr (\(ErrorHandler hand) me -> maybe me hand (fromException e)) (liftIO $ throw e) handlers