module Proteome.Filename where import qualified Chronos import qualified Data.Text as Text import Exon (exon) import Path ( Abs, Dir, File, Path, Rel, addExtension, dirname, filename, parent, parseRelDir, reldir, relfile, splitExtension, (), ) import Path.IO (copyFile, doesDirExist, doesFileExist, ensureDir, removeFile) import Ribosome ( Bang (Bang), Handler, LogReport, Rpc, RpcError, logReport, mapReport, pathText, resumeReport, rpcError, ) import Ribosome.Api (bufferSetName, vimCallFunction, vimCommand, vimGetCurrentBuffer, wipeBuffer) import Ribosome.Api.Buffer (currentBufferName, edit) import Ribosome.Api.Path (nvimCwd) import Ribosome.Data.PersistPathError (PersistPathError) import Ribosome.Host.Modify (silent) import Ribosome.Persist (PersistPath, persistPath) import qualified Proteome.Data.FilenameError as FilenameError import Proteome.Data.FilenameError (FilenameError) import Proteome.Path ( absoluteParse, absoluteParseDir, parseAbsDirMaybe, parseAbsFileMaybe, parseRelDirMaybe, parseRelFileMaybe, ) data BufPath = BufPath (Path Abs File) [Text] deriving stock (Eq, Show) bufDir :: BufPath -> Path Abs Dir bufDir (BufPath f _) = parent f data NameSpec = Star (Path Rel File) | Literal (Path Rel File) deriving stock (Eq, Show) rawNameSpec :: NameSpec -> Path Rel File rawNameSpec = \case Star f -> f Literal f -> f data Modification = Filename (Path Rel File) (Path Rel Dir) NameSpec [Text] | Dir (Path Abs Dir) | File (Path Abs File) | Container Int (Path Rel Dir) deriving stock (Eq, Show) nameSpec :: Path Rel File -> NameSpec nameSpec p = if p == [relfile|*|] then Star p else Literal p dotsInPath :: Text -> Int dotsInPath path = Text.length (Text.filter (== '.') (Text.drop 1 path)) absoluteDir :: Text -> Maybe Modification absoluteDir = fmap Dir . parseAbsDirMaybe absoluteFile :: Text -> Maybe Modification absoluteFile = fmap File . parseAbsFileMaybe relativeDir :: Path Abs Dir -> Text -> Maybe Modification relativeDir cwd spec = do rel <- parseRelDirMaybe spec pure (Dir (cwd rel)) relativeFile :: Path Abs Dir -> Text -> Maybe Modification relativeFile cwd spec = do rel <- parseRelFileMaybe spec pure (File (cwd rel)) splitExtensions :: Path b File -> (Path b File, [Text]) splitExtensions = spin [] where spin exts f = case splitExtension f of Just (f', e) -> spin (toText e : exts) f' Nothing -> (f, exts) addExtensions :: Path b File -> [Text] -> Maybe (Path b File) addExtensions name exts = foldlM (flip addExtension) name (toString <$> exts) nameOnly :: Text -> Maybe Modification nameOnly spec = do rel <- parseRelFileMaybe spec let (name, exts) = splitExtensions (filename rel) Just (Filename rel (parent rel) (nameSpec name) exts) maybeDir :: Member (Embed IO) r => Path Abs Dir -> Text -> Sem r Bool maybeDir cwd spec = fromMaybe False <$> traverse doesDirExist (absoluteParseDir cwd spec) regularModification :: Members [Stop FilenameError, Embed IO] r => Path Abs Dir -> Text -> Sem r Modification regularModification cwd spec = do existingDir <- maybeDir cwd spec stopNote (FilenameError.InvalidPathSpec spec) (cons existingDir spec) where cons existingDir | name = nameOnly | absolute = if explicitDir || existingDir then absoluteDir else absoluteFile | explicitDir = relativeDir cwd | otherwise = relativeFile cwd name = not (Text.any ('/' ==) spec) absolute = Text.take 1 spec == "/" explicitDir = Text.takeEnd 1 spec == "/" directorySelector :: Text -> (Int, Text) directorySelector = first Text.length . Text.span ('^' ==) modification :: Members [Stop FilenameError, Embed IO] r => Bool -> Path Abs Dir -> Text -> Sem r Modification modification raw cwd (Text.strip -> spec) = case directorySelector spec of (n, _) | n == 0 || raw -> regularModification cwd spec (n, name) -> do dir <- stopNote (FilenameError.InvalidPathSpec name) (parseRelDir (toString name)) pure (Container n dir) checkBufferPath :: Members [Rpc, Stop FilenameError, Embed IO] r => Path Abs Dir -> Sem r (Path Abs File) checkBufferPath cwd = do name <- currentBufferName path <- stopNote FilenameError.BufferPathInvalid (absoluteParse cwd name) ifM (doesFileExist path) (pure path) (stop FilenameError.BufferPathInvalid) withExtension :: BufPath -> [Text] -> NameSpec -> Maybe (Path Rel File) withExtension (BufPath bufName bufExts) exts = \case Star _ -> addExtensions (filename bufName) (take (length bufExts - length exts) bufExts ++ exts) Literal name -> addExtensions name (exts ++ drop (length exts) bufExts) renameInplace :: Member (Stop FilenameError) r => Bool -> Path Rel File -> BufPath -> Path Rel Dir -> NameSpec -> [Text] -> Sem r (Path Abs File) renameInplace raw spec bufPath destDir newName exts = do rel <- if raw then pure (destDir spec) else stopNote FilenameError.BufferPathInvalid (withExtension bufPath exts newName) pure (bufDir bufPath rel) replaceDir :: Member (Stop FilenameError) r => Int -> Path Rel Dir -> Path Abs File -> Sem r (Path Abs File) replaceDir index name file = do dir <- spin (parent file) index pure (dir filename file) where spin d _ | parent d == d = stop (FilenameError.InvalidPathSpec "not enough directory segments in buffer path") spin d i | i <= 1 = pure (parent d name) spin d i = do sub <- spin (parent d) (i - 1) pure (sub dirname d) assemblePath :: Member (Stop FilenameError) r => Bool -> Path Abs File -> Modification -> Sem r (Path Abs File) assemblePath raw bufPath = \case Filename rawSpec destDir newName exts -> renameInplace raw rawSpec (uncurry BufPath (splitExtensions bufPath)) destDir newName exts Dir dir -> pure (dir filename bufPath) File file -> pure file Container index name -> replaceDir index name bufPath ensureDestinationEmpty :: Members [Stop FilenameError, Embed IO] r => Path Abs File -> Sem r () ensureDestinationEmpty path = whenM (doesFileExist path) (stop (FilenameError.Exists (pathText path))) prepareDestination :: Members [Stop FilenameError, Embed IO] r => Path Abs File -> Sem r () prepareDestination path = ifM exists (ensureDestinationEmpty path) create where exists = doesDirExist dir create = stopTryAny (const (FilenameError.CreateDir (pathText dir))) (ensureDir dir) dir = parent path getCwd :: Members [Stop FilenameError, Rpc !! RpcError] r => Sem r (Path Abs Dir) getCwd = resumeHoistAs FilenameError.BadCwd nvimCwd smartModification :: Members [Stop FilenameError, Rpc !! RpcError, Embed IO] r => Bool -> Text -> Sem r Modification smartModification raw spec = do cwd <- getCwd modification raw cwd spec trashModification :: Members [Stop FilenameError, Rpc, Rpc !! RpcError, PersistPath, Embed IO] r => Sem r Modification trashModification = do cwd <- getCwd bufPath <- checkBufferPath cwd let original = pathText (filename bufPath) Chronos.Time stamp <- liftIO Chronos.now trashFile <- stopNote FilenameError.BufferPathInvalid (parseRelFileMaybe [exon|#{show stamp}_#{original}|]) trashDir <- persistPath (Just [reldir|trash|]) let trashPath = trashDir trashFile stopTryAny (const (FilenameError.CreateDir (pathText trashDir))) (ensureDir trashDir) pure (File trashPath) pathsForMod :: Members [Stop FilenameError, Rpc, Rpc !! RpcError, Embed IO] r => Bool -> Modification -> Sem r (Path Abs File, Path Abs File) pathsForMod raw modi = do cwd <- getCwd bufPath <- checkBufferPath cwd path <- assemblePath raw bufPath modi prepareDestination path pure (bufPath, path) writeBuffer :: Members [Stop FilenameError, Rpc !! RpcError] r => Text -> Sem r () writeBuffer action = err "Couldn't write buffer" $ silent do vimCommand "write!" where err msg = resumeHoist \ e -> FilenameError.ActionFailed action [exon|#{msg}: #{rpcError e}|] updateBuffer :: Member Rpc r => Path Abs File -> Sem r () updateBuffer path = do buf <- vimGetCurrentBuffer bufferSetName buf (pathText path) silent do vimCommand "write!" relocate :: Members [Stop FilenameError, Rpc, Rpc !! RpcError, Embed IO] r => Bool -> Modification -> (Path Abs File -> Path Abs File -> Sem r ()) -> Sem r () relocate raw modi run = do (bufPath, destPath) <- pathsForMod raw modi run bufPath destPath copyOrFail :: Members [Stop FilenameError, Embed IO] r => Path Abs File -> Path Abs File -> Sem r () copyOrFail src dest = stopEitherWith copyFailed =<< tryAny (copyFile src dest) where copyFailed e = FilenameError.ActionFailed "move" [exon|Couldn't copy file: #{e}|] moveFile :: Members [Stop FilenameError, DataLog LogReport, Embed IO] r => Path Abs File -> Path Abs File -> Sem r () moveFile src dest = do copyOrFail src dest leftA (logReport . FilenameError.Remove) =<< tryAny (removeFile src) move :: Members [Stop FilenameError, DataLog LogReport, Rpc, Rpc !! RpcError, Embed IO] r => Bool -> Modification -> Sem r () move raw modi = do relocate raw modi \ buf dest -> do writeBuffer "move" moveFile buf dest updateBuffer dest copy :: Members [Stop FilenameError, Rpc, Rpc !! RpcError, Embed IO] r => Bool -> Modification -> Sem r () copy raw modi = relocate raw modi \ src dest -> do copyOrFail src dest view <- vimCallFunction "winsaveview" [] edit dest vimCallFunction "winrestview" [view] proMove :: Members [DataLog LogReport, Rpc !! RpcError, Embed IO] r => Bang -> Text -> Handler r () proMove bang spec = mapReport @FilenameError $ resumeReport @Rpc do move raw =<< smartModification raw spec where raw = bang == Bang proCopy :: Members [Rpc !! RpcError, Embed IO] r => Bang -> Text -> Handler r () proCopy bang spec = mapReport @FilenameError $ resumeReport @Rpc do copy raw =<< smartModification raw spec where raw = bang == Bang proRemove :: Members [Rpc !! RpcError, PersistPath !! PersistPathError, DataLog LogReport, Embed IO] r => Handler r () proRemove = mapReport @FilenameError $ resumeReport @Rpc $ resumeReport @PersistPath do move False =<< trashModification buf <- vimGetCurrentBuffer resume_ @RpcError do wipeBuffer buf