proteome-0.9.9.9: lib/Proteome/Files.hs
module Proteome.Files where
import Control.Lens (view)
import Control.Monad (foldM)
import Data.Either.Extra (eitherToMaybe)
import Data.List.Extra (dropEnd)
import qualified Data.List.NonEmpty as NonEmpty (toList, zip)
import Data.List.NonEmpty.Extra (maximumOn1)
import qualified Data.Text as Text
import Path (Abs, Dir, File, Path, Rel, parent, parseAbsDir, parseRelDir, parseRelFile, toFilePath, (</>))
import Path.IO (createDirIfMissing, doesDirExist, listDirRel)
import Ribosome (
Handler,
Report,
Rpc,
RpcError,
ScratchId (ScratchId),
SettingError,
Settings,
mapReport,
resumeReport,
)
import Ribosome.Api (nvimGetOption)
import Ribosome.Api.Buffer (edit)
import Ribosome.Api.Path (nvimCwd)
import Ribosome.Data.ScratchOptions (ScratchOptions (filetype, name, syntax))
import Ribosome.Data.Setting (Setting (Setting))
import Ribosome.Host.Data.Args (ArgList (ArgList))
import Ribosome.Menu (
Filter (Fuzzy),
Mappings,
MenuAction,
MenuWidget,
Prompt (..),
PromptConfig (OnlyInsert),
PromptMode,
PromptText (PromptText),
WindowMenus,
menuOk,
menuState,
menuSuccess,
menuUpdatePrompt,
modal,
windowMenu,
withSelection,
(%=),
)
import Ribosome.Menu.Mappings (insert, withInsert)
import Ribosome.Menu.MenuState (mode)
import qualified Ribosome.Settings as Settings
import Text.Regex.PCRE.Light (Regex, compileM)
import Proteome.Data.FilesConfig (FilesConfig (FilesConfig))
import Proteome.Data.FilesError (FilesError)
import qualified Proteome.Data.FilesError as FilesError (FilesError (..))
import qualified Proteome.Data.FilesState as FilesState
import Proteome.Data.FilesState (FilesMode (FilesMode), FilesState, Segment (Full), fileSegments)
import Proteome.Files.Source (files)
import Proteome.Files.Syntax (filesSyntax)
import Proteome.Menu (handleResult)
import qualified Proteome.Settings as Settings
data FileAction =
Create (Path Abs File)
|
Edit (NonEmpty (Path Abs File))
|
NoAction
deriving stock (Eq, Show)
editFile ::
MenuWidget FilesState r FileAction
editFile =
withSelection (pure . Edit . fmap (view #path))
matchingDirs ::
Member (Embed IO) r =>
[Path Abs Dir] ->
Path Rel Dir ->
Sem r [Path Abs Dir]
matchingDirs bases path =
filterM (fmap (fromRight False) . tryAny . doesDirExist) ((</> path) <$> bases)
dirsWithPrefix ::
Member (Embed IO) r =>
Text ->
Path Abs Dir ->
Sem r [Path Rel Dir]
dirsWithPrefix (Text.toLower -> prefix) dir =
filter (Text.isPrefixOf prefix . Text.toLower . toText . toFilePath) . fst <$> listDirRel dir
-- |Search all dirs in @bases@ for relative paths starting with @text@.
-- First, split the last path segment (after /) off and collect the subdirectories of @bases@ that start with the
-- remainder. If there is no / in the text, parsing the remainder fails with 'Nothing' and the @bases@ themselves are
-- used.
-- Then, search the resulting dirs for subdirs starting with the last segment.
-- Return the remainder and the relative subdir paths.
matchingPaths ::
Member (Embed IO) r =>
[Path Abs Dir] ->
Text ->
Sem r (Text, [Path Rel Dir])
matchingPaths bases text' =
(subpath,) . join <$> (traverse (dirsWithPrefix prefix) =<< dirs)
where
subpath =
maybe "" (toText . toFilePath) dir
dirs =
maybe (pure bases) (matchingDirs bases) dir
(dir, prefix) =
first (parseRelDir . toString) $ Text.breakOnEnd "/" text'
commonPrefix :: [Text] -> Maybe Text
commonPrefix (h : t) =
foldM (\ p a -> view _1 <$> Text.commonPrefixes p a) h t
commonPrefix a =
listToMaybe a
tabComplete ::
Member (Embed IO) r =>
[Path Abs Dir] ->
Text ->
Sem r (Maybe Text)
tabComplete bases promptText = do
existingBases <- filterM doesDirExist bases
(subpath, paths) <- matchingPaths existingBases promptText
pure (mappend subpath <$> commonPrefix (toText . toFilePath <$> paths))
tabUpdatePrompt ::
PromptMode ->
Text ->
Prompt
tabUpdatePrompt st prefix =
Prompt (Text.length prefix) st (PromptText prefix)
tab ::
Member (Embed IO) r =>
[Path Abs Dir] ->
MenuWidget FilesState r FileAction
tab bases = do
Prompt _ promptState (PromptText promptText) <- ask
tabComplete bases promptText >>= \case
Just prefix ->
menuUpdatePrompt (tabUpdatePrompt promptState prefix)
Nothing ->
menuOk
createAndEditFile ::
Members [Rpc, Stop FilesError, Embed IO] r =>
Path Abs File ->
Sem r ()
createAndEditFile path = do
stopTryAny (const err) create
edit path
where
err =
FilesError.CouldntCreateDir (toText (toFilePath dir))
create =
createDirIfMissing True dir
dir =
parent path
existingSubdirCount ::
Member (Embed IO) r =>
[Text] ->
Path Abs Dir ->
Sem r Int
existingSubdirCount =
loop 0
where
loop count [] _ =
pure count
loop count (h : t) dir =
case parseRelDir (toString h) of
Right f ->
ifM (doesDirExist sub) (loop (count + 1) t sub) (pure count)
where sub = dir </> f
Left _ ->
pure count
createFile ::
Member (Reader Prompt) r =>
Members [Stop FilesError, Embed IO] r =>
NonEmpty (Path Abs Dir) ->
Sem r (Maybe (MenuAction FileAction))
createFile bases = do
PromptText promptText <- view #text <$> ask
let
parse counts =
(base counts </>) <$> parseRelFile (toString promptText)
subdirCounts <- traverse (existingSubdirCount (dirSegments promptText)) bases
maybe (err promptText) (menuSuccess . Create) (parse subdirCounts)
where
base counts =
fst $ maximumOn1 snd (NonEmpty.zip bases counts)
dirSegments =
dropEnd 1 . Text.splitOn "/"
err =
stop . FilesError.InvalidFilePath
cycleSegment :: MenuWidget FilesState r FileAction
cycleSegment =
menuState do
mode . #segment %= FilesState.cycle
menuOk
actions ::
Members [Stop FilesError, Embed IO] r =>
NonEmpty (Path Abs Dir) ->
Mappings FilesState r FileAction
actions bases =
[
(withInsert "<cr>", editFile),
(insert "<tab>", tab (NonEmpty.toList bases)),
(insert "<c-y>", createFile bases),
(insert "<c-s>", cycleSegment)
]
parsePath :: Path Abs Dir -> Text -> Maybe (Path Abs Dir)
parsePath _ path | Text.take 1 path == "/" =
parseAbsDir (toString path)
parsePath cwd path =
(cwd </>) <$> parseRelDir (toString path)
readRegex ::
Member (Stop FilesError) r =>
Text ->
Text ->
Sem r Regex
readRegex name rgx =
stopNote (FilesError.BadRegex name rgx) (eitherToMaybe (compileM (encodeUtf8 rgx) mempty))
readRegexs ::
Members [Settings, Stop FilesError] r =>
Setting [Text] ->
Sem r [Regex]
readRegexs s@(Setting name _ _) =
traverse (readRegex name) =<< Settings.get s
filesConfig ::
Members [Rpc, Settings, Stop FilesError] r =>
Sem r FilesConfig
filesConfig =
FilesConfig <$> useRg <*> hidden <*> fs <*> dirs <*> wildignore
where
useRg =
Settings.get Settings.filesUseRg
hidden =
Settings.get Settings.filesExcludeHidden
fs =
readRegexs Settings.filesExcludeFiles
dirs =
readRegexs Settings.filesExcludeDirectories
wildignore =
Text.splitOn "," <$> nvimGetOption "wildignore"
fileAction ::
Members [Rpc, Stop FilesError, Stop Report, Embed IO] r =>
FileAction ->
Sem r ()
fileAction = \case
Create path ->
createAndEditFile path
Edit paths ->
traverse_ edit paths
NoAction ->
unit
type FilesStack =
[
WindowMenus () FilesState !! RpcError,
Log,
Async,
Embed IO
]
filesMenu ::
Members FilesStack r =>
Members [Stop FilesError, Stop Report, Settings, Rpc] r =>
Path Abs Dir ->
[Text] ->
Sem r ()
filesMenu cwd pathSpecs = do
mapReport @RpcError do
conf <- filesConfig
items <- fmap (fmap fileSegments) <$> files conf nePaths
result <- windowMenu items (modal (FilesMode Fuzzy Full)) window (actions nePaths)
handleResult fileAction result
where
window =
def & #prompt .~ OnlyInsert & #items .~ opt
opt =
def {
name = ScratchId name,
syntax = [filesSyntax],
filetype = Just name
}
name =
"proteome-files"
nePaths =
fromMaybe (cwd :| []) (nonEmpty absPaths)
absPaths =
mapMaybe (parsePath cwd) pathSpecs
proFiles ::
Members FilesStack r =>
Members [Rpc !! RpcError, Settings !! SettingError] r =>
ArgList ->
Handler r ()
proFiles (ArgList paths) =
mapReport @FilesError $ resumeReport @Rpc $ resumeReport @Settings do
cwd <- resumeHoistAs FilesError.BadCwd nvimCwd
filesMenu cwd paths