packages feed

proteome-0.9.9.9: lib/Proteome/Filename.hs

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