From ec77a5e2dd2cd184496c76349fe9acd0f291215f Mon Sep 17 00:00:00 2001 From: VeryMilkyJoe Date: Sun, 7 Jun 2026 10:41:51 +0200 Subject: [PATCH] Edit .cabal file on rename On WillRename Notification, edit the responsible .cabal file to change the module name entry of the renamed module to the new name. --- .hlint.yaml | 1 + haskell-language-server.cabal | 2 + hls-test-utils/src/Test/Hls/Util.hs | 111 +++++++- .../hls-cabal-plugin/src/Ide/Plugin/Cabal.hs | 62 ++++- .../Ide/Plugin/Cabal/CabalAdd/CodeAction.hs | 24 +- .../src/Ide/Plugin/Cabal/CabalAdd/Rename.hs | 243 ++++++++++++++++++ .../Plugin/Cabal/Completion/CabalFields.hs | 26 +- .../src/Ide/Plugin/Cabal/Definition.hs | 57 ++-- plugins/hls-cabal-plugin/test/Main.hs | 2 + plugins/hls-cabal-plugin/test/Workspace.hs | 93 +++++++ .../test/testdata/rename/Bench.hs | 3 + .../test/testdata/rename/Exe.hs | 3 + .../test/testdata/rename/LibLib.hs | 3 + .../test/testdata/rename/Test.hs | 3 + .../test/testdata/rename/lib/MainLib.hs | 3 + .../test/testdata/rename/rename.cabal | 32 +++ .../src/Ide/Plugin/Rename/ModuleName.hs | 2 +- 17 files changed, 610 insertions(+), 60 deletions(-) create mode 100644 plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/CabalAdd/Rename.hs create mode 100644 plugins/hls-cabal-plugin/test/Workspace.hs create mode 100644 plugins/hls-cabal-plugin/test/testdata/rename/Bench.hs create mode 100644 plugins/hls-cabal-plugin/test/testdata/rename/Exe.hs create mode 100644 plugins/hls-cabal-plugin/test/testdata/rename/LibLib.hs create mode 100644 plugins/hls-cabal-plugin/test/testdata/rename/Test.hs create mode 100644 plugins/hls-cabal-plugin/test/testdata/rename/lib/MainLib.hs create mode 100644 plugins/hls-cabal-plugin/test/testdata/rename/rename.cabal diff --git a/.hlint.yaml b/.hlint.yaml index adc3e992d7..0c3e624d52 100644 --- a/.hlint.yaml +++ b/.hlint.yaml @@ -215,6 +215,7 @@ - Development.IDE.Graph.Internal.Profile - Development.IDE.Graph.Internal.Rules - CodeLensTests #Previously part of GHCIDE Main tests + - Test.Hls.Util # Used in `willRenameFile`: This function is only here temporarily as we are planning to upstream to lsp-test. - name: "Data.Map.!" within: [] diff --git a/haskell-language-server.cabal b/haskell-language-server.cabal index fc67538291..377dcf12f1 100644 --- a/haskell-language-server.cabal +++ b/haskell-language-server.cabal @@ -261,6 +261,7 @@ library hls-cabal-plugin Ide.Plugin.Cabal.CabalAdd.Command Ide.Plugin.Cabal.CabalAdd.CodeAction Ide.Plugin.Cabal.CabalAdd.Types + Ide.Plugin.Cabal.CabalAdd.Rename Ide.Plugin.Cabal.Orphans Ide.Plugin.Cabal.Outline Ide.Plugin.Cabal.Parse @@ -309,6 +310,7 @@ test-suite hls-cabal-plugin-tests Definition Outline Utils + Workspace build-depends: , bytestring , Cabal-syntax >= 3.7 diff --git a/hls-test-utils/src/Test/Hls/Util.hs b/hls-test-utils/src/Test/Hls/Util.hs index cdb3c4de94..abc246d4e6 100644 --- a/hls-test-utils/src/Test/Hls/Util.hs +++ b/hls-test-utils/src/Test/Hls/Util.hs @@ -2,6 +2,7 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} module Test.Hls.Util ( -- * Test Capabilities @@ -37,6 +38,7 @@ module Test.Hls.Util , inspectCommand , inspectDiagnostic , inspectDiagnosticAny + , renameFile , waitForDiagnosticsFrom , waitForDiagnosticsFromSource , waitForDiagnosticsFromSourceWithTimeout @@ -52,7 +54,8 @@ module Test.Hls.Util where import Control.Applicative.Combinators (skipManyTill, (<|>)) -import Control.Exception (catch, throwIO) +import Control.Exception (catch, throw, + throwIO) import Control.Lens (_Just, (&), (.~), (?~), (^.)) import Control.Monad @@ -69,7 +72,7 @@ import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types import qualified Language.LSP.Test as Test -import System.Directory +import qualified System.Directory as Directory import System.FilePath import System.Info.Extra (isMac, isWindows) import qualified System.IO.Extra @@ -80,12 +83,16 @@ import Test.Tasty.ExpectedFailure (expectFailBecause, ignoreTestBecause) import Test.Tasty.HUnit (assertFailure) +import Data.Foldable (traverse_) import qualified Data.List as List +import qualified Data.Map as Map +import Data.Maybe (fromJust) import Data.String.Interpolate (__i) import qualified Data.Text.Internal.Search as T import qualified Data.Text.Utf16.Rope.Mixed as Rope import Development.IDE.Plugin.Completions.Logic (getCompletionPrefixFromRope) import Development.IDE.Plugin.Completions.Types (PosPrefixInfo (..)) +import qualified Language.LSP.Server as LSP noLiteralCaps :: ClientCapabilities noLiteralCaps = def & L.textDocument ?~ textDocumentCaps @@ -187,7 +194,7 @@ onlyRunForGhcVersions vers = withCurrentDirectoryInTmp :: FilePath -> IO a -> IO a withCurrentDirectoryInTmp dir f = withTempCopy ignored dir $ \newDir -> - withCurrentDirectory newDir f + Directory.withCurrentDirectory newDir f where ignored = ["dist", "dist-newstyle", ".stack-work"] @@ -200,7 +207,7 @@ withCurrentDirectoryInTmp dir f = withCurrentDirectoryInTmp' :: [FilePath] -> FilePath -> IO a -> IO a withCurrentDirectoryInTmp' ignored dir f = withTempCopy ignored dir $ \newDir -> - withCurrentDirectory newDir f + Directory.withCurrentDirectory newDir f -- | Example call: @withTempCopy ignored src f@ -- @@ -219,15 +226,15 @@ withTempCopy ignored srcDir f = do -- that are listed in 'ignored'. copyDir :: [FilePath] -> FilePath -> FilePath -> IO () copyDir ignored src dst = do - cnts <- listDirectory src + cnts <- Directory.listDirectory src forM_ cnts $ \file -> do unless (file `elem` ignored) $ do let srcFp = src file dstFp = dst file - isDir <- doesDirectoryExist srcFp + isDir <- Directory.doesDirectoryExist srcFp if isDir - then createDirectory dstFp >> copyDir ignored srcFp dstFp - else copyFile srcFp dstFp + then Directory.createDirectory dstFp >> copyDir ignored srcFp dstFp + else Directory.copyFile srcFp dstFp fromAction :: (Command |? CodeAction) -> CodeAction fromAction (InR action) = action @@ -335,6 +342,92 @@ failIfSessionTimeout action = action `catch` errorHandler errorHandler e@(Test.Timeout _) = assertFailure $ show e errorHandler e = throwIO e +-- --------------------------------------------------------------------- + +-- | Simulate the client action of renaming a file +-- This consists of sending a `WillRenameFiles` request renaming the file from `from` to `to`, +-- and applying the returned `WorkspaceEdit` changes and actually renaming the file. +-- Finally sends a `DidRenameFile` notification to the server. +renameFile :: TextDocumentIdentifier -> TextDocumentIdentifier -> Test.Session () +renameFile from to = do + willRenameFile from to >>= \case + InL we -> do + case LSP.reverseSortEdit we of + (WorkspaceEdit (Just e) _ _) -> + traverse_ + ( \(uri, edits) -> + -- TODO: once we move this to lsp, we can simply send an ApplyWorkspaceEdit + -- request directly and the server will handle it for us + traverse (Test.applyEdit (TextDocumentIdentifier uri)) edits + ) + (Map.assocs e) + (WorkspaceEdit _ (Just docChanges) _) -> do + traverse_ + ( \case + InL edit -> + traverse_ + (\e -> + Test.applyEdit + (TextDocumentIdentifier $ edit ^. L.textDocument . L.uri) + $ mkTextEdit e + ) + (edit ^. L.edits) + InR unsupported -> + error $ "renameFile: Unsupported WorkspaceEdit " <> (show unsupported) + ) + docChanges + _ -> pure () + _ -> pure () + -- We are using the below two operations in place of renameFile, + -- due to a potential race condition on Windows. + liftIO $ Directory.copyFileWithMetadata (getFilePath from) (getFilePath to) + liftIO $ Directory.removeFile $ getFilePath from + didRenameFile from to + where + getFilePath :: TextDocumentIdentifier -> FilePath + getFilePath tId = + case uriToFilePath uri of + Just fp -> fp + Nothing -> error "Failed to parse uri " <> show uri <> " to filepath" + where + uri = tId ^. L.uri + + mkTextEdit :: TextEdit |? AnnotatedTextEdit -> TextEdit + mkTextEdit (InL e) = e + mkTextEdit (InR ate) = + TextEdit (ate ^. L.range) (ate ^. L.newText) + +-- | Sends a WillRenameFiles notification to the server and returns the edit the server produces +-- In case the server returns an error, throws an error as well. +willRenameFile :: TextDocumentIdentifier -> TextDocumentIdentifier -> Test.Session (WorkspaceEdit |? Null) +willRenameFile from to = do + rsp <- Test.request + SMethod_WorkspaceWillRenameFiles + ( + RenameFilesParams [FileRename (docIdToText from) (docIdToText to)] + ) + + case rsp ^. L.result of + Right edit -> return edit + -- Todo: When this function is upstreamed to lsp-test, + -- remove this module from the fromJust entry in `.hlint.yaml`. + Left error -> throw (Test.UnexpectedResponseError (fromJust $ rsp ^. L.id) error) + +-- | Sends a DidRenameFile Notification to the server +-- Currently the server does not handle this notification, +-- but the plan is to reload the session on rename. +didRenameFile :: TextDocumentIdentifier -> TextDocumentIdentifier -> Test.Session () +didRenameFile from to = + Test.sendNotification + SMethod_WorkspaceDidRenameFiles + ( RenameFilesParams + [FileRename (docIdToText from) (docIdToText to) + ] + ) + +docIdToText :: TextDocumentIdentifier -> T.Text +docIdToText tdi = getUri $ tdi ^. L.uri + -- --------------------------------------------------------------------- getCompletionByLabel :: MonadIO m => T.Text -> [CompletionItem] -> m CompletionItem getCompletionByLabel desiredLabel compls = @@ -348,7 +441,7 @@ getCompletionByLabel desiredLabel compls = -- Run with a canonicalized temp dir withCanonicalTempDir :: (FilePath -> IO a) -> IO a withCanonicalTempDir f = System.IO.Extra.withTempDir $ \dir -> do - dir' <- canonicalizePath dir + dir' <- Directory.canonicalizePath dir f dir' -- ---------------------------------------------------------------------------- diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal.hs index dadc5503fc..89d2b23a39 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal.hs @@ -6,13 +6,17 @@ module Ide.Plugin.Cabal (descriptor, haskellInteractionDescriptor, Log (..)) where +import Control.Applicative ((<|>)) import Control.Lens ((^.)) +import Control.Monad.Except (runExceptT) import Control.Monad.Extra import Control.Monad.IO.Class import Control.Monad.Trans.Class (lift) +import Control.Monad.Trans.Except (ExceptT) import Control.Monad.Trans.Maybe (runMaybeT) import Data.HashMap.Strict (HashMap) import qualified Data.List as List +import qualified Data.Map.Strict as Map import qualified Data.Maybe as Maybe import qualified Data.Text () import qualified Data.Text as T @@ -20,6 +24,7 @@ import Development.IDE as D import Development.IDE.Core.FileStore (getVersionedTextDoc) import Development.IDE.Core.PluginUtils import Development.IDE.Core.Shake (restartShakeSession) +import qualified Development.IDE.Core.Shake as Shake import Development.IDE.Graph (Key) import Development.IDE.LSP.HoverDefinition (foundHover) import qualified Development.IDE.Plugin.Completions.Logic as Ghcide @@ -33,6 +38,7 @@ import Distribution.PackageDescription.Configuration (flattenPackageDe import qualified Distribution.Parsec.Position as Syntax import qualified Ide.Plugin.Cabal.CabalAdd.CodeAction as CabalAdd import qualified Ide.Plugin.Cabal.CabalAdd.Command as CabalAdd +import qualified Ide.Plugin.Cabal.CabalAdd.Rename as Rename import Ide.Plugin.Cabal.Completion.CabalFields as CabalFields import qualified Ide.Plugin.Cabal.Completion.Completer.Types as CompleterTypes import qualified Ide.Plugin.Cabal.Completion.Completions as Completions @@ -70,7 +76,11 @@ data Log | LogCompletionContext Types.Context Position | LogCompletions Types.Log | LogCabalAdd CabalAdd.Log - deriving (Show) + | LogDidRename Rename.Log + | LogShake Shake.Log + | LogSessionRestart + | LogNoCabalFile FilePath + | LogCabalRenameFailed T.Text PluginError instance Pretty Log where pretty = \case @@ -95,6 +105,11 @@ instance Pretty Log where <+> pretty position LogCompletions logs -> pretty logs LogCabalAdd logs -> pretty logs + LogDidRename logs -> pretty logs + LogSessionRestart -> "Restarting shake session globally" + LogShake logs -> pretty logs + LogNoCabalFile file -> "Cannot find responsible cabal file for" <+> pretty file + LogCabalRenameFailed file err -> "Rename of file" <+> pretty file <+> "failed with error:" <+> pretty err {- | Some actions in cabal files can be triggered from haskell files. This descriptor allows us to hook into the diagnostics of haskell source files and @@ -128,6 +143,7 @@ descriptor recorder plId = , mkPluginHandler LSP.SMethod_TextDocumentCodeAction $ fieldSuggestCodeAction recorder , mkPluginHandler LSP.SMethod_TextDocumentDefinition gotoDefinition , mkPluginHandler LSP.SMethod_TextDocumentHover hover + , mkPluginHandler LSP.SMethod_WorkspaceWillRenameFiles $ renameModulesHandler recorder ] , pluginNotificationHandlers = mconcat @@ -165,7 +181,6 @@ descriptor recorder plId = log' = logWith recorder ruleRecorder = cmapWithPrio LogRule recorder ofInterestRecorder = cmapWithPrio LogOfInterest recorder - whenUriFile :: Uri -> (NormalizedFilePath -> IO ()) -> IO () whenUriFile uri act = whenJust (uriToFilePath uri) $ act . toNormalizedFilePath' @@ -300,6 +315,38 @@ cabalAddModuleCodeAction recorder state plId (CodeActionParams _ _ (TextDocument pure $ InL $ fmap InR actions Nothing -> pure $ InL [] +renameModulesHandler :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState LSP.Method_WorkspaceWillRenameFiles +renameModulesHandler recorder ideState _plId (RenameFilesParams renames) = do + renamedEdits <- traverse (renameModuleHelper recorder ideState) renames + pure $ InL $ List.foldl' combineTextEdits (WorkspaceEdit mempty mempty mempty) renamedEdits + +renameModuleHelper :: Recorder (WithPriority Log) -> IdeState -> FileRename -> ExceptT PluginError (HandlerM Config) WorkspaceEdit +renameModuleHelper recorder ideState (FileRename oldUri newUri) = do + caps <- lift pluginGetClientCapabilities + renameResult <- runExceptT $ do + oldHaskellFilePath <- uriToFilePathE $ Uri oldUri + newHaskellFilePath <- uriToFilePathE $ Uri newUri + mbCabalFile <- liftIO $ CabalAdd.findResponsibleCabalFile oldHaskellFilePath + case mbCabalFile of + Nothing -> do + logWith recorder Debug $ LogNoCabalFile oldHaskellFilePath + pure mempty + Just cabalFilePath -> + Rename.renameHandler + (cmapWithPrio LogDidRename recorder) + ideState + caps + oldHaskellFilePath + newHaskellFilePath + cabalFilePath + ideState + case renameResult of + Left err -> do + logWith recorder Debug $ LogCabalRenameFailed oldUri err + pure mempty + Right edit -> do + pure edit + {- | Handler for hover messages. If the cursor is hovering on a dependency, add a documentation link to that dependency. @@ -410,3 +457,14 @@ computeCompletionsAt recorder ide prefInfo fp fields matcher = do pos = Types.completionCursorPosition prefInfo context fields = Completions.getContext completerRecorder prefInfo fields completerRecorder = cmapWithPrio LogCompletions recorder + +combineTextEdits :: WorkspaceEdit -> WorkspaceEdit -> WorkspaceEdit +combineTextEdits (WorkspaceEdit c1 dc1 ca1) (WorkspaceEdit c2 dc2 ca2) = + WorkspaceEdit c dc ca + where + c = liftA2 (Map.unionWith (<>)) c1 c2 <|> c1 <|> c2 + dc = dc1 <> dc2 + -- We know this might result in information loss due to the monad instance of map, + -- but we do not expect our use of workspacedit combination to contain two changeAnnotations + -- for the same edit. + ca = ca1 <> ca2 diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/CabalAdd/CodeAction.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/CabalAdd/CodeAction.hs index d5cb1fb137..996a546ea7 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/CabalAdd/CodeAction.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/CabalAdd/CodeAction.hs @@ -248,7 +248,7 @@ addDependencySuggestCodeAction :: GenericPackageDescription -> IO [J.CodeAction] addDependencySuggestCodeAction plId verTxtDocId suggestions haskellFilePath cabalFilePath gpd = do - buildTargets <- liftIO $ getBuildTargets gpd cabalFilePath haskellFilePath + buildTargets <- liftIO $ getBuildTargets (flattenPackageDescription gpd) cabalFilePath haskellFilePath case buildTargets of -- If there are no build targets found, run the `cabal-add` command with default behaviour [] -> pure $ mkCodeActionForDependency cabalFilePath Nothing <$> suggestions @@ -267,17 +267,6 @@ addDependencySuggestCodeAction plId verTxtDocId suggestions haskellFilePath caba -} buildTargetToStringRepr target = render $ CabalPretty.pretty $ buildTargetComponentName target - {- | Finds the build targets that are used in `cabal-add`. - Note the unorthodox usage of `readBuildTargets`: - If the relative path to the haskell file is provided, - `readBuildTargets` will return the build targets, this - module is mentioned in (either exposed-modules or other-modules). - -} - getBuildTargets :: GenericPackageDescription -> FilePath -> FilePath -> IO [BuildTarget] - getBuildTargets gpd cabalFilePath haskellFilePath = do - let haskellFileRelativePath = makeRelative (dropFileName cabalFilePath) haskellFilePath - readBuildTargets (verboseNoStderr silent) (flattenPackageDescription gpd) [haskellFileRelativePath] - mkCodeActionForDependency :: FilePath -> Maybe String -> (T.Text, T.Text) -> J.CodeAction mkCodeActionForDependency cabalFilePath target (suggestedDep, suggestedVersion) = let @@ -300,6 +289,17 @@ addDependencySuggestCodeAction plId verTxtDocId suggestions haskellFilePath caba in J.CodeAction title (Just CodeActionKind_QuickFix) (Just []) Nothing Nothing Nothing (Just command) Nothing +{- | Finds the build targets that are used in `cabal-add`. + Note the unorthodox usage of `readBuildTargets`: + If the relative path to the haskell file is provided, + `readBuildTargets` will return the build targets, this + module is mentioned in (either exposed-modules or other-modules). +-} +getBuildTargets :: PackageDescription -> FilePath -> FilePath -> IO [BuildTarget] +getBuildTargets pd cabalFilePath haskellFilePath = do + let haskellFileRelativePath = makeRelative (dropFileName cabalFilePath) haskellFilePath + readBuildTargets (verboseNoStderr silent) pd [haskellFileRelativePath] + {- | Gives a mentioned number of @(dependency, version)@ pairs found in the "hidden package" diagnostic message. diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/CabalAdd/Rename.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/CabalAdd/Rename.hs new file mode 100644 index 0000000000..ddac9552b2 --- /dev/null +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/CabalAdd/Rename.hs @@ -0,0 +1,243 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} + +module Ide.Plugin.Cabal.CabalAdd.Rename ( + renameHandler, + Log, +) +where + +import Control.Applicative +import Control.Monad (guard) +import qualified Control.Monad.Extra as Maybe +import Control.Monad.Trans +import Control.Monad.Trans.Except (ExceptT, throwE) +import Control.Monad.Trans.Maybe +import Data.ByteString (ByteString) +import qualified Data.Maybe as Maybe +import qualified Data.Text as T +import qualified Data.Text.Encoding as T +import qualified Data.Text.IO as Text +import qualified Data.Text.Utf16.Rope.Mixed as Rope +import Development.IDE.Core.FileStore (getUriContents, + getVersionedTextDoc) +import Development.IDE.Core.PluginUtils (runActionE, + useE) +import qualified Development.IDE.Core.Shake as Shake +import Development.IDE.Types.Location (toNormalizedUri) +import qualified Distribution.Client.Add as Add +import Distribution.Client.Rename (RenameConfig (..), + executeRenameConfig) +import Distribution.Fields (Field) +import qualified Distribution.ModuleName as Cabal +import Distribution.PackageDescription +import Distribution.PackageDescription.Configuration (flattenPackageDescription) +import Distribution.Parsec.Position (Position) +import Distribution.Simple.BuildTarget (buildTargetComponentName) +import Ide.Logger +import Ide.Plugin.Cabal.CabalAdd.CodeAction (buildInfoToHsSourceDirs, + getBuildTargets, + mkRelativeModulePathM) +import Ide.Plugin.Cabal.Completion.Types (ParseCabalFields (..), + ParseCabalFile (..)) +import Ide.Plugin.Cabal.Definition (lookupBuildTargetPackageDescription) +import Ide.Plugin.Error +import Ide.PluginUtils (WithDeletions (IncludeDeletions), + diffText) +import Language.LSP.Protocol.Types (ClientCapabilities, + TextDocumentIdentifier (TextDocumentIdentifier), + VersionedTextDocumentIdentifier, + WorkspaceEdit, + filePathToUri, + toNormalizedFilePath) + +data Log + = LogDidRename FilePath FilePath + | CabalRenameLog T.Text T.Text + deriving (Show) + +instance Pretty Log where + pretty = \case + LogDidRename oldFp newFp -> + "Received rename info from:" <+> pretty oldFp <+> "to:" <+> pretty newFp + CabalRenameLog oldModulePath newModulePath -> + "Executing rename of module from" <+> pretty oldModulePath <+> "to" <+> pretty newModulePath <+> "in cabal file." + +-------------------------------------------- +-- Rename module in cabal file +-------------------------------------------- + +renameHandler :: + forall m. + (MonadIO m) => + Recorder (WithPriority Log) -> + Shake.IdeState -> + ClientCapabilities -> + -- | the old file path, before the rename + FilePath -> + -- | the new file path, after the rename + FilePath -> + -- | the path to the cabal file, responsible for the renamed module + FilePath -> + Shake.IdeState -> + ExceptT PluginError m WorkspaceEdit +renameHandler recorder _ caps oldHaskellFilePath newHaskellFilePath cabalFilePath ideState = do + logWith recorder Info $ LogDidRename oldHaskellFilePath newHaskellFilePath + (contents, fields, gpd, verTxtDocId) <- runActionE "cabal-plugin.getUriContents" ideState $ do + let nuri = toNormalizedUri $ filePathToUri cabalFilePath + nfp = toNormalizedFilePath cabalFilePath + mContent <- lift $ getUriContents nuri + verTxtDocId <- + runActionE "cabalAdd.getVersionedTextDoc" ideState $ + lift $ + getVersionedTextDoc $ + TextDocumentIdentifier (filePathToUri cabalFilePath) + content <- case mContent of + Just content -> pure $ Rope.toText content + Nothing -> liftIO $ Text.readFile cabalFilePath + fields <- useE ParseCabalFields nfp + gpd <- useE ParseCabalFile nfp + pure (content, fields, gpd, verTxtDocId) + cabalFileEdit <- + applyModuleRenameToCabalFile + recorder + (caps, verTxtDocId) + oldHaskellFilePath + newHaskellFilePath + cabalFilePath + (T.encodeUtf8 contents) + fields + gpd + pure cabalFileEdit + +{- | Apply rename to the given cabal file + +Replaces the module name corresponding to the old file path with the +module name corresponding to the new file path in the given cabal file. +Fails if the cabal file cannot be parsed, the file paths cannot be parsed to module names +or no occurrence of the module can be found in the cabal file. +-} +applyModuleRenameToCabalFile :: + forall m. + (MonadIO m) => + Recorder (WithPriority Log) -> + (ClientCapabilities, VersionedTextDocumentIdentifier) -> + -- | the old file path before the rename + FilePath -> + -- | the new file path after the rename + FilePath -> + -- | the path to the cabal file, responsible for the renamed module + FilePath -> + -- | the responsible cabal file's contents + ByteString -> + -- | the responsible cabal file's fields + [Field Position] -> + GenericPackageDescription -> + ExceptT PluginError m WorkspaceEdit +applyModuleRenameToCabalFile recorder (caps, verTxtDocId) oldHaskellFilePath newHaskellFilePath cabalFilePath cnfOrigContents fields gpd = do + let pd = flattenPackageDescription gpd + compName <- resolveFileTargetE pd cabalFilePath oldHaskellFilePath + buildInfo <- resolveBuildInfoE pd compName + newModulePath <- toRelativeModulePathE (buildInfoToHsSourceDirs buildInfo) cabalFilePath newHaskellFilePath + oldModulePath <- toRelativeModulePathE (buildInfoToHsSourceDirs buildInfo) cabalFilePath oldHaskellFilePath + targetField <- resolveTargetFieldForComponentE pd oldModulePath compName buildInfo + newContents <- + maybeToExceptT PluginStaleResolve $ + hoistMaybe $ + executeRenameConfig (Add.validateChanges gpd) (renameConfig (Right $ compName) targetField oldModulePath newModulePath) + logWith recorder Info $ CabalRenameLog oldModulePath newModulePath + pure $ diffText caps (verTxtDocId, T.decodeUtf8 cnfOrigContents) (T.decodeUtf8 newContents) IncludeDeletions + where + -- define renameConfig to pass to cabal-add + renameConfig compName targetField from to = + RenameConfig + { cnfOrigContents = cnfOrigContents + , cnfFields = fields + , cnfComponent = compName + , cnfTargetField = targetField + , cnfRenameFrom = T.encodeUtf8 from + , cnfRenameTo = T.encodeUtf8 to + } + +{- | Determine the `TargetField` of the given module path + +Takes a module path and which component the module is a part of and returns whether the module is in the +exposed- or the other-modules field of the component. +If the module is in neither of the fields, returns Nothing. +-} +findFieldForModule :: T.Text -> ComponentName -> PackageDescription -> BuildInfo -> Maybe Add.TargetField +findFieldForModule modulePath compName pd buildInfo = + let + exposedMods = case compName of + CLibName name -> + case name of + LMainLibName -> + Maybe.maybe [] exposedModules $ library pd + LSubLibName _ -> + concat $ Maybe.concatMapM (getExposedModulesForLib compName) $ subLibraries pd + _ -> [] + otherMods = otherModules buildInfo + in + findInExposedModules exposedMods <|> findInOtherModules otherMods + where + moduleName = Cabal.fromString $ T.unpack modulePath + + findInOtherModules mods = + Add.OtherModules <$ findInModules mods + + findInExposedModules mods = + Add.ExposedModules <$ findInModules mods + + findInModules mods = + guard $ moduleName `elem` mods + + getExposedModulesForLib :: ComponentName -> Library -> Maybe [Cabal.ModuleName] + getExposedModulesForLib compName lib = + case (libName lib, componentNameString compName) of + (LSubLibName lName, Just unqualCompName) + | lName == unqualCompName -> Just $ exposedModules lib + _ -> Nothing + +--------------------------------------------------------- +-- Rule applications with shortcuts to plugin errors +--------------------------------------------------------- + +{- | Returns the `BuildInfo` for the given component name. +If the build info cannot be resolved, throws a PluginError. +-} +resolveBuildInfoE :: (Applicative m) => PackageDescription -> ComponentName -> ExceptT PluginError m BuildInfo +resolveBuildInfoE pd compName = + maybeToExceptT PluginStaleResolve $ hoistMaybe $ lookupBuildTargetPackageDescription pd (Just compName) + +{- | Determines the `ComponentName` of the given file target. +Tries to resolve the file target's component name within the given cabal file. +If the component name cannot be uniquely resolved, throws a PluginError, +-} +resolveFileTargetE :: (MonadIO m) => PackageDescription -> FilePath -> FilePath -> ExceptT PluginError m ComponentName +resolveFileTargetE pd cabalFilePath fileTarget = do + buildTargets <- liftIO $ getBuildTargets pd cabalFilePath fileTarget + case buildTargets of + [buildTarget] -> pure $ buildTargetComponentName buildTarget + [] -> throwE PluginStaleResolve -- todo maybe handle these two cases differently + _ -> throwE PluginStaleResolve + +{- | Returns the field a module name is contained in. + +Takes a module name and a component name and returns whether the module name is in exposed- or other-modules of that component. +If the module name cannot be found in either field, throws a PluginError. +-} +resolveTargetFieldForComponentE :: (Applicative m) => PackageDescription -> T.Text -> ComponentName -> BuildInfo -> ExceptT PluginError m Add.TargetField +resolveTargetFieldForComponentE pd oldModulePath compName buildInfo = + maybeToExceptT PluginStaleResolve $ + hoistMaybe $ + findFieldForModule oldModulePath compName pd buildInfo + +{- | Takes a list of source subdirectories, a cabal source path and a haskell filepath +and returns a path to the module in exposed module syntax. + +The path will be relative to one of the subdirectories, in case the module is contained within one of them. +If no module path can be resolved, throws a PluginError. +-} +toRelativeModulePathE :: (Applicative m) => [FilePath] -> FilePath -> FilePath -> ExceptT PluginError m T.Text +toRelativeModulePathE sourceDirs cabalFilePath haskellFilePath = + maybeToExceptT PluginStaleResolve $ hoistMaybe $ mkRelativeModulePathM sourceDirs cabalFilePath haskellFilePath diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/CabalFields.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/CabalFields.hs index b8cb7ce0d6..b2284c766d 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/CabalFields.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Completion/CabalFields.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE OverloadedStrings #-} + module Ide.Plugin.Cabal.Completion.CabalFields ( findStanzaForColumn , getModulesNames @@ -25,7 +27,10 @@ import qualified Data.Text as T import qualified Data.Text.Encoding as T import Data.Tuple (swap) import qualified Distribution.Fields as Syntax +import Distribution.PackageDescription (LibraryName (LSubLibName), + mkUnqualComponentName) import qualified Distribution.Parsec.Position as Syntax +import Distribution.Types.ComponentName (ComponentName (CBenchName, CExeName, CFLibName, CLibName, CTestName)) import Ide.Plugin.Cabal.Completion.Types import qualified Language.LSP.Protocol.Types as LSP @@ -155,7 +160,6 @@ getOptionalSectionName (x:xs) = case x of Syntax.SecArgName _ name -> Just (T.decodeUtf8 name) _ -> getOptionalSectionName xs -type BuildTargetName = T.Text type ModuleName = T.Text -- | Given a cabal AST returns pairs of all respective target names @@ -186,18 +190,28 @@ type ModuleName = T.Text -- * @getModulesNames@ output: -- -- > [([Just "first-target", Just "second-target"], "Config")] -getModulesNames :: [Syntax.Field any] -> [([Maybe BuildTargetName], ModuleName)] +getModulesNames :: [Syntax.Field any] -> [([Maybe ComponentName], ModuleName)] getModulesNames fields = map swap $ groupSort rawModuleTargetPairs where rawModuleTargetPairs = concatMap getSectionModuleNames sections sections = getSectionsWithModules fields - getSectionModuleNames :: Syntax.Field any -> [(ModuleName, Maybe BuildTargetName)] - getSectionModuleNames (Syntax.Section _ secArgs fields) = map (, getArgsName secArgs) $ concatMap getFieldModuleNames fields + getSectionModuleNames :: Syntax.Field any -> [(ModuleName, Maybe ComponentName)] + getSectionModuleNames (Syntax.Section secName secArgs fields) = map (, getArgsName secName secArgs) $ concatMap getFieldModuleNames fields getSectionModuleNames _ = [] - getArgsName [Syntax.SecArgName _ name] = Just $ T.decodeUtf8 name - getArgsName _ = Nothing -- Can be only a main library, that has no name + getArgsName (Syntax.Name _ secName) [Syntax.SecArgName _ nameBs] = + let + name = mkUnqualComponentName $ T.unpack $ T.decodeUtf8 nameBs + in + case secName of + "library" -> Just $ CLibName $ LSubLibName name + "test-suite" -> Just $ CTestName name + "benchmark" -> Just $ CBenchName name + "executable" -> Just $ CExeName name + "foreign-library" -> Just $ CFLibName name + _ -> Nothing + getArgsName _ _ = Nothing -- Can be only a main library, that has no name -- since it's impossible to have multiple names for a build target getFieldModuleNames field@(Syntax.Field _ modules) = if getFieldName field == T.pack "exposed-modules" || diff --git a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Definition.hs b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Definition.hs index 5137af2b08..7042c6597c 100644 --- a/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Definition.hs +++ b/plugins/hls-cabal-plugin/src/Ide/Plugin/Cabal/Definition.hs @@ -9,25 +9,24 @@ module Ide.Plugin.Cabal.Definition where import Control.Lens ((^.)) import Control.Monad.Extra import Control.Monad.IO.Class +import Data.Foldable (asum) import Data.List (find) import qualified Data.Maybe as Maybe import qualified Data.Text as T import Development.IDE as D import Development.IDE.Core.PluginUtils import qualified Distribution.Fields as Syntax -import Distribution.PackageDescription (Benchmark (..), - BuildInfo (..), - Executable (..), - ForeignLib (..), +import Distribution.PackageDescription (Benchmark (Benchmark, benchmarkBuildInfo, benchmarkName), + BuildInfo (hsSourceDirs), + Executable (Executable, buildInfo, exeName), + ForeignLib (ForeignLib, foreignLibBuildInfo, foreignLibName), GenericPackageDescription, - Library (..), - LibraryName (LMainLibName, LSubLibName), + Library (Library, libBuildInfo, libName), PackageDescription (..), - TestSuite (..), - library, - unUnqualComponentName) + TestSuite (TestSuite, testBuildInfo, testName)) import Distribution.PackageDescription.Configuration (flattenPackageDescription) import qualified Distribution.Parsec.Position as Syntax +import Distribution.Types.ComponentName (ComponentName (..)) import Distribution.Utils.Generic (safeHead) import Distribution.Utils.Path (getSymbolicPath) import Ide.Plugin.Cabal.Completion.CabalFields as CabalFields @@ -142,50 +141,48 @@ gotoModulesDefinition nfp gpd cursor fieldsOfInterest = do isModuleName (Just name) (_, moduleName) = name == moduleName isModuleName _ _ = False --- | Gives all `buildInfo`s given a target name. +-- | Gives all 'BuildInfo's given a target name. -- --- `Maybe buildTargetName` is provided, and if it's --- Nothing we assume, that it's a main library. --- Otherwise looks for the provided name. -lookupBuildTargetPackageDescription :: PackageDescription -> Maybe T.Text -> [BuildInfo] +-- Takes a @'Maybe' 'ComponentName'@ and looks for the coresponding Buildinfo if it is Just. +-- If Nothing is passed we assume that we are looking for a main library. +-- If no main library can be found, returns Nothing. +lookupBuildTargetPackageDescription :: PackageDescription -> Maybe ComponentName -> Maybe BuildInfo lookupBuildTargetPackageDescription (PackageDescription {..}) Nothing = case library of - Nothing -> [] -- Target is a main library but no main library was found - Just (Library {libBuildInfo}) -> [libBuildInfo] + Nothing -> Nothing -- Target is a main library but no main library was found + Just (Library {libBuildInfo}) -> Just libBuildInfo lookupBuildTargetPackageDescription (PackageDescription {..}) (Just buildTargetName) = - Maybe.catMaybes $ + asum $ + foldMap libraryNameLookup library : map executableNameLookup executables <> - map subLibraryNameLookup subLibraries <> + map libraryNameLookup subLibraries <> map foreignLibsNameLookup foreignLibs <> map testSuiteNameLookup testSuites <> map benchmarkNameLookup benchmarks where executableNameLookup :: Executable -> Maybe BuildInfo executableNameLookup (Executable {exeName, buildInfo}) = - if T.pack (unUnqualComponentName exeName) == buildTargetName + if CExeName exeName == buildTargetName then Just buildInfo else Nothing - subLibraryNameLookup :: Library -> Maybe BuildInfo - subLibraryNameLookup (Library {libName, libBuildInfo}) = - case libName of - (LSubLibName name) -> - if T.pack (unUnqualComponentName name) == buildTargetName - then Just libBuildInfo - else Nothing - LMainLibName -> Nothing + libraryNameLookup :: Library -> Maybe BuildInfo + libraryNameLookup (Library {libName, libBuildInfo}) = + if CLibName libName == buildTargetName + then Just libBuildInfo + else Nothing foreignLibsNameLookup :: ForeignLib -> Maybe BuildInfo foreignLibsNameLookup (ForeignLib {foreignLibName, foreignLibBuildInfo}) = - if T.pack (unUnqualComponentName foreignLibName) == buildTargetName + if CFLibName foreignLibName == buildTargetName then Just foreignLibBuildInfo else Nothing testSuiteNameLookup :: TestSuite -> Maybe BuildInfo testSuiteNameLookup (TestSuite {testName, testBuildInfo}) = - if T.pack (unUnqualComponentName testName) == buildTargetName + if CTestName testName == buildTargetName then Just testBuildInfo else Nothing benchmarkNameLookup :: Benchmark -> Maybe BuildInfo benchmarkNameLookup (Benchmark {benchmarkName, benchmarkBuildInfo}) = - if T.pack (unUnqualComponentName benchmarkName) == buildTargetName + if CBenchName benchmarkName == buildTargetName then Just benchmarkBuildInfo else Nothing diff --git a/plugins/hls-cabal-plugin/test/Main.hs b/plugins/hls-cabal-plugin/test/Main.hs index 1a0f890274..3921222038 100644 --- a/plugins/hls-cabal-plugin/test/Main.hs +++ b/plugins/hls-cabal-plugin/test/Main.hs @@ -33,6 +33,7 @@ import System.FilePath import Test.Hls import Test.Hls.FileSystem import Utils +import Workspace (cabalWorkspaceTests) main :: IO () main = do @@ -48,6 +49,7 @@ main = do , gotoDefinitionTests , hoverTests , reloadOnCabalChangeTests + , cabalWorkspaceTests ] -- ------------------------------------------------------------------------ diff --git a/plugins/hls-cabal-plugin/test/Workspace.hs b/plugins/hls-cabal-plugin/test/Workspace.hs new file mode 100644 index 0000000000..a3b5434b6e --- /dev/null +++ b/plugins/hls-cabal-plugin/test/Workspace.hs @@ -0,0 +1,93 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE OverloadedStrings #-} + +module Workspace ( + cabalWorkspaceTests, +) where + +import Control.Lens ((^.)) +import Data.String (IsString (fromString)) +import qualified Data.Text.Encoding as T +import Distribution.PackageDescription (ComponentName (..), + LibraryName (..), + PackageDescription, + benchmarkModules, + exeModules, + explicitLibModules, + foreignLibModules, + getComponent, + showComponentName, + testModules) +import Distribution.PackageDescription.Configuration (flattenPackageDescription) +import Distribution.Types.Component (Component (..)) +import Ide.Plugin.Cabal.Parse (parseCabalFileContents) +import qualified Language.LSP.Protocol.Lens as L +import qualified System.FilePath as FP +import Test.Hls +import Utils + +cabalWorkspaceTests :: TestTree +cabalWorkspaceTests = + testGroup + "Workspace" + [ cabalRenameTests + ] + +cabalRenameTests :: TestTree +cabalRenameTests = + testGroup + "Rename" + [ runHaskellTestCaseSession "Rename in named library" "rename" $ do + let newName = "NewHaskell.hs" + pd <- generateWorkspaceFileRenameTestSession "rename.cabal" "LibLib.hs" newName + checkModuleRenamedIn pd (FP.dropExtension newName) $ CLibName $ LSubLibName "lib" + , runHaskellTestCaseSession "Rename in executable" "rename" $ do + let newName = "ChangesModule.hs" + pd <- generateWorkspaceFileRenameTestSession "rename.cabal" "Exe.hs" newName + checkModuleRenamedIn pd (FP.dropExtension newName) $ CExeName "exe" + , runHaskellTestCaseSession "Rename in main library" "rename" $ do + let newName = "OtherNewHaskell.hs" + pd <- generateWorkspaceFileRenameTestSession "rename.cabal" "lib/MainLib.hs" newName + checkModuleRenamedIn pd (FP.dropExtension newName) $ CLibName LMainLibName + , runHaskellTestCaseSession "Rename in benchmark" "rename" $ do + let newName = "NewHaskell2.hs" + pd <- generateWorkspaceFileRenameTestSession "rename.cabal" "Bench.hs" newName + checkModuleRenamedIn pd (FP.dropExtension newName) $ CBenchName "bench" + , runHaskellTestCaseSession "Rename in test-suite" "rename" $ do + let newName = "NewHaskell.hs" + pd <- generateWorkspaceFileRenameTestSession "rename.cabal" "Test.hs" newName + checkModuleRenamedIn pd (FP.dropExtension newName) $ CTestName "test" + ] + where + generateWorkspaceFileRenameTestSession :: FilePath -> FilePath -> FilePath -> Session PackageDescription + generateWorkspaceFileRenameTestSession cabalFile haskellFile newFileName = do + haskellDoc <- openDoc haskellFile "haskell" + cabalDoc <- openDoc cabalFile "cabal" + _ <- waitForDiagnosticsFrom haskellDoc + let fp = + case uriToFilePath (haskellDoc ^. L.uri) of + Just x -> x + Nothing -> error "Could not parse uri to file path for: " <> haskellFile + let haskellDir = FP.takeDirectory fp + newId = mkTId $ haskellDir FP. newFileName + _ <- renameFile haskellDoc newId + contents <- documentContents cabalDoc + case parseCabalFileContents $ T.encodeUtf8 contents of + (_, Right gpd) -> pure $ flattenPackageDescription gpd + _ -> liftIO $ assertFailure "could not parse cabal file to gpd" + + mkTId :: String -> TextDocumentIdentifier + mkTId s = TextDocumentIdentifier $ filePathToUri s + + checkModuleRenamedIn :: PackageDescription -> String -> ComponentName -> Session () + checkModuleRenamedIn pd newModName compName = do + let comp = getComponent pd compName + compModules = case comp of + CLib lib -> explicitLibModules lib + CFLib fLib -> foreignLibModules fLib + CExe exe -> exeModules exe + CTest test -> testModules test + CBench bench -> benchmarkModules bench + -- todo maybe check that old name is gone + testDescription = newModName <> " was renamed in " <> showComponentName compName + liftIO $ assertBool testDescription $ fromString newModName `elem` compModules diff --git a/plugins/hls-cabal-plugin/test/testdata/rename/Bench.hs b/plugins/hls-cabal-plugin/test/testdata/rename/Bench.hs new file mode 100644 index 0000000000..9b0a828c90 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/rename/Bench.hs @@ -0,0 +1,3 @@ +module Bench where + +baz = 4 diff --git a/plugins/hls-cabal-plugin/test/testdata/rename/Exe.hs b/plugins/hls-cabal-plugin/test/testdata/rename/Exe.hs new file mode 100644 index 0000000000..9f8df7f6cc --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/rename/Exe.hs @@ -0,0 +1,3 @@ +module Exe where + +foo = 5 diff --git a/plugins/hls-cabal-plugin/test/testdata/rename/LibLib.hs b/plugins/hls-cabal-plugin/test/testdata/rename/LibLib.hs new file mode 100644 index 0000000000..78690198c9 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/rename/LibLib.hs @@ -0,0 +1,3 @@ +module LibLib where + +foo = 1 diff --git a/plugins/hls-cabal-plugin/test/testdata/rename/Test.hs b/plugins/hls-cabal-plugin/test/testdata/rename/Test.hs new file mode 100644 index 0000000000..1c0ae80751 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/rename/Test.hs @@ -0,0 +1,3 @@ +module Test where + +bar = "a" diff --git a/plugins/hls-cabal-plugin/test/testdata/rename/lib/MainLib.hs b/plugins/hls-cabal-plugin/test/testdata/rename/lib/MainLib.hs new file mode 100644 index 0000000000..88f2621133 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/rename/lib/MainLib.hs @@ -0,0 +1,3 @@ +module MainLib where + +foo = 8 diff --git a/plugins/hls-cabal-plugin/test/testdata/rename/rename.cabal b/plugins/hls-cabal-plugin/test/testdata/rename/rename.cabal new file mode 100644 index 0000000000..6ffbe8d7d8 --- /dev/null +++ b/plugins/hls-cabal-plugin/test/testdata/rename/rename.cabal @@ -0,0 +1,32 @@ +cabal-version: 3.4 +name: rename +version: 0.1.0.0 +build-type: Simple + +library lib + exposed-modules: LibLib + build-depends: base + +library + hs-source-dirs: lib + other-modules: MainLib + build-depends: base + default-language: Haskell2010 + +executable exe + hs-source-dirs: . + other-modules: Exe + build-depends: base + default-language: Haskell2010 + +test-suite test + type: exitcode-stdio-1.0 + main-is: Main.hs + other-modules: Test + build-depends: base + +benchmark bench + type: exitcode-stdio-1.0 + main-is: Main.hs + build-depends: base + other-modules: Bench diff --git a/plugins/hls-rename-plugin/src/Ide/Plugin/Rename/ModuleName.hs b/plugins/hls-rename-plugin/src/Ide/Plugin/Rename/ModuleName.hs index 530a8e0d85..0a017bb4e7 100644 --- a/plugins/hls-rename-plugin/src/Ide/Plugin/Rename/ModuleName.hs +++ b/plugins/hls-rename-plugin/src/Ide/Plugin/Rename/ModuleName.hs @@ -141,7 +141,7 @@ pathModuleNames recorder state normFilePath filePath let paths = map (normalise . (<> pure pathSeparator)) srcPaths logWith recorder Debug (NormalisedPaths paths) - -- TODO, this can be avoid if the filePath is already absolute, + -- TODO, this can be avoided if the filePath is already absolute, -- we can avoid the toAbsolute call in the future. -- see Note [Root Directory] let mdlPath = (toAbsolute $ rootDir state) filePath