{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE DataKinds #-}
module Language.LSP.VFS
(
VFS(..)
, VirtualFile(..)
, virtualFileText
, virtualFileVersion
, initVFS
, openVFS
, changeFromClientVFS
, changeFromServerVFS
, persistFileVFS
, closeVFS
, updateVFS
, rangeLinesFromVfs
, PosPrefixInfo(..)
, getCompletionPrefix
, applyChanges
, applyChange
, changeChars
) where
import Control.Lens hiding ( parts )
import Control.Monad
import Data.Char (isUpper, isAlphaNum)
import Data.Text ( Text )
import qualified Data.Text as T
import Data.List
import Data.Ord
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Map as Map
import Data.Maybe
import Data.Rope.UTF16 ( Rope )
import qualified Data.Rope.UTF16 as Rope
import qualified Language.LSP.Types as J
import qualified Language.LSP.Types.Lens as J
import System.FilePath
import Data.Hashable
import System.Directory
import System.IO
import System.IO.Temp
import System.Log.Logger
{-# ANN module ("hlint: ignore Eta reduce" :: String) #-}
{-# ANN module ("hlint: ignore Redundant do" :: String) #-}
data VirtualFile =
VirtualFile {
_lsp_version :: !Int
, _file_version :: !Int
, _text :: Rope
} deriving (Show)
type VFSMap = Map.Map J.NormalizedUri VirtualFile
data VFS = VFS { vfsMap :: Map.Map J.NormalizedUri VirtualFile
, vfsTempDir :: FilePath
} deriving Show
virtualFileText :: VirtualFile -> Text
virtualFileText vf = Rope.toText (_text vf)
virtualFileVersion :: VirtualFile -> Int
virtualFileVersion vf = _lsp_version vf
initVFS :: (VFS -> IO r) -> IO r
initVFS k = withSystemTempDirectory "haskell-lsp" $ \temp_dir -> k (VFS mempty temp_dir)
openVFS :: VFS -> J.Message 'J.TextDocumentDidOpen -> (VFS, [String])
openVFS vfs (J.NotificationMessage _ _ params) =
let J.DidOpenTextDocumentParams
(J.TextDocumentItem uri _ version text) = params
in (updateVFS (Map.insert (J.toNormalizedUri uri) (VirtualFile version 0 (Rope.fromText text))) vfs
, [])
changeFromClientVFS :: VFS -> J.Message 'J.TextDocumentDidChange -> (VFS,[String])
changeFromClientVFS vfs (J.NotificationMessage _ _ params) =
let
J.DidChangeTextDocumentParams vid (J.List changes) = params
J.VersionedTextDocumentIdentifier (J.toNormalizedUri -> uri) version = vid
in
case Map.lookup uri (vfsMap vfs) of
Just (VirtualFile _ file_ver str) ->
let str' = applyChanges str changes
in (updateVFS (Map.insert uri (VirtualFile (fromMaybe 0 version) (file_ver + 1) str')) vfs, [])
Nothing ->
(vfs, ["haskell-lsp:changeVfs:can't find uri:" ++ show uri])
updateVFS :: (VFSMap -> VFSMap) -> VFS -> VFS
updateVFS f vfs@VFS{vfsMap} = vfs { vfsMap = f vfsMap }
changeFromServerVFS :: VFS -> J.Message 'J.WorkspaceApplyEdit -> IO VFS
changeFromServerVFS initVfs (J.RequestMessage _ _ _ params) = do
let J.ApplyWorkspaceEditParams _label edit = params
J.WorkspaceEdit mChanges mDocChanges = edit
case mDocChanges of
Just (J.List textDocEdits) -> applyEdits textDocEdits
Nothing -> case mChanges of
Just cs -> applyEdits $ HashMap.foldlWithKey' changeToTextDocumentEdit [] cs
Nothing -> do
debugM "haskell-lsp.changeVfs" "No changes"
return initVfs
where
changeToTextDocumentEdit acc uri edits =
acc ++ [J.TextDocumentEdit (J.VersionedTextDocumentIdentifier uri (Just 0)) edits]
applyEdits :: [J.TextDocumentEdit] -> IO VFS
applyEdits = foldM f initVfs . sortOn (^. J.textDocument . J.version)
f :: VFS -> J.TextDocumentEdit -> IO VFS
f vfs (J.TextDocumentEdit vid (J.List edits)) = do
let sortedEdits = sortOn (Down . (^. J.range)) edits
changeEvents = map editToChangeEvent sortedEdits
ps = J.DidChangeTextDocumentParams vid (J.List changeEvents)
notif = J.NotificationMessage "" J.STextDocumentDidChange ps
let (vfs',ls) = changeFromClientVFS vfs notif
mapM_ (debugM "haskell-lsp.changeFromServerVFS") ls
return vfs'
editToChangeEvent (J.TextEdit range text) = J.TextDocumentContentChangeEvent (Just range) Nothing text
virtualFileName :: FilePath -> J.NormalizedUri -> VirtualFile -> FilePath
virtualFileName prefix uri (VirtualFile _ file_ver _) =
let uri_raw = J.fromNormalizedUri uri
basename = maybe "" takeFileName (J.uriToFilePath uri_raw)
padLeft :: Int -> Int -> String
padLeft n num =
let numString = show num
in replicate (n - length numString) '0' ++ numString
in prefix </> basename ++ "-" ++ padLeft 5 file_ver ++ "-" ++ show (hash uri_raw) ++ ".hs"
persistFileVFS :: VFS -> J.NormalizedUri -> Maybe (FilePath, IO ())
persistFileVFS vfs uri =
case Map.lookup uri (vfsMap vfs) of
Nothing -> Nothing
Just vf ->
let tfn = virtualFileName (vfsTempDir vfs) uri vf
action = do
exists <- doesFileExist tfn
unless exists $ do
let contents = Rope.toString (_text vf)
writeRaw h = do
hSetNewlineMode h noNewlineTranslation
hSetEncoding h utf8
hPutStr h contents
debugM "haskell-lsp.persistFileVFS" $ "Writing virtual file: "
++ "uri = " ++ show uri ++ ", virtual file = " ++ show tfn
withFile tfn WriteMode writeRaw
in Just (tfn, action)
closeVFS :: VFS -> J.Message 'J.TextDocumentDidClose -> (VFS, [String])
closeVFS vfs (J.NotificationMessage _ _ params) =
let J.DidCloseTextDocumentParams (J.TextDocumentIdentifier uri) = params
in (updateVFS (Map.delete (J.toNormalizedUri uri)) vfs,["Closed: " ++ show uri])
applyChanges :: Rope -> [J.TextDocumentContentChangeEvent] -> Rope
applyChanges = foldl' applyChange
applyChange :: Rope -> J.TextDocumentContentChangeEvent -> Rope
applyChange _ (J.TextDocumentContentChangeEvent Nothing Nothing str)
= Rope.fromText str
applyChange str (J.TextDocumentContentChangeEvent (Just (J.Range (J.Position sl sc) _to)) (Just len) txt)
= changeChars str start len txt
where
start = Rope.rowColumnCodeUnits (Rope.RowColumn sl sc) str
applyChange str (J.TextDocumentContentChangeEvent (Just (J.Range (J.Position sl sc) (J.Position el ec))) Nothing txt)
= changeChars str start len txt
where
start = Rope.rowColumnCodeUnits (Rope.RowColumn sl sc) str
end = Rope.rowColumnCodeUnits (Rope.RowColumn el ec) str
len = end - start
applyChange str (J.TextDocumentContentChangeEvent Nothing (Just _) _txt)
= str
changeChars :: Rope -> Int -> Int -> Text -> Rope
changeChars str start len new = mconcat [before, Rope.fromText new, after']
where
(before, after) = Rope.splitAt start str
after' = Rope.drop len after
data PosPrefixInfo = PosPrefixInfo
{ fullLine :: T.Text
, prefixModule :: T.Text
, prefixText :: T.Text
, cursorPos :: J.Position
} deriving (Show,Eq)
getCompletionPrefix :: (Monad m) => J.Position -> VirtualFile -> m (Maybe PosPrefixInfo)
getCompletionPrefix pos@(J.Position l c) (VirtualFile _ _ ropetext) =
return $ Just $ fromMaybe (PosPrefixInfo "" "" "" pos) $ do
let headMaybe [] = Nothing
headMaybe (x:_) = Just x
lastMaybe [] = Nothing
lastMaybe xs = Just $ last xs
curLine <- headMaybe $ T.lines $ Rope.toText
$ fst $ Rope.splitAtLine 1 $ snd $ Rope.splitAtLine l ropetext
let beforePos = T.take c curLine
curWord <-
if | T.null beforePos -> Just ""
| T.last beforePos == ' ' -> Just ""
| otherwise -> lastMaybe (T.words beforePos)
let parts = T.split (=='.')
$ T.takeWhileEnd (\x -> isAlphaNum x || x `elem` ("._'"::String)) curWord
case reverse parts of
[] -> Nothing
(x:xs) -> do
let modParts = dropWhile (not . isUpper . T.head)
$ reverse $ filter (not .T.null) xs
modName = T.intercalate "." modParts
return $ PosPrefixInfo curLine modName x pos
rangeLinesFromVfs :: VirtualFile -> J.Range -> T.Text
rangeLinesFromVfs (VirtualFile _ _ ropetext) (J.Range (J.Position lf _cf) (J.Position lt _ct)) = r
where
(_ ,s1) = Rope.splitAtLine lf ropetext
(s2, _) = Rope.splitAtLine (lt - lf) s1
r = Rope.toText s2