195 lines
7.4 KiB
Haskell
195 lines
7.4 KiB
Haskell
-- | Utilities when writing interactive programs that interpret commands,
|
|
-- e.g. a REPL.
|
|
module UI.Butcher.Monadic.Interactive
|
|
( simpleCompletion
|
|
, shellCompletionWords
|
|
, interactiveHelpDoc
|
|
, partDescStrings
|
|
)
|
|
where
|
|
|
|
|
|
|
|
#include "prelude.inc"
|
|
|
|
import qualified Text.PrettyPrint as PP
|
|
|
|
import UI.Butcher.Monadic.Internal.Types
|
|
import UI.Butcher.Monadic.Internal.Core
|
|
import UI.Butcher.Monadic.Pretty
|
|
|
|
|
|
|
|
-- | Derives a potential completion from a given input string and a given
|
|
-- 'CommandDesc'. Considers potential subcommands and where available the
|
|
-- completion info present in 'PartDesc's.
|
|
simpleCompletion
|
|
:: String -- ^ input string
|
|
-> CommandDesc () -- ^ CommandDesc obtained on that input string
|
|
-> String -- ^ "remaining" input after the last successfully parsed
|
|
-- subcommand. See 'UI.Butcher.Monadic.runCmdParserExt'.
|
|
-> String -- ^ completion, i.e. a string that might be appended
|
|
-- to the current prompt when user presses tab.
|
|
simpleCompletion line cdesc pcRest = List.drop (List.length lastWord)
|
|
$ longestCommonPrefix choices
|
|
where
|
|
longestCommonPrefix [] = ""
|
|
longestCommonPrefix (c1:cr) =
|
|
case find (\s -> List.all (s `isPrefixOf`) cr) $ reverse $ List.inits c1 of
|
|
Nothing -> ""
|
|
Just x -> x
|
|
nameDesc = case _cmd_mParent cdesc of
|
|
Nothing -> cdesc
|
|
Just (_, parent) | null pcRest && not (null lastWord) -> parent
|
|
-- not finished writing a command. if we have commands abc and abcdef,
|
|
-- we may want "def" as a completion after "abc".
|
|
Just{} -> cdesc
|
|
lastWord = reverse $ takeWhile (not . Char.isSpace) $ reverse $ line
|
|
choices :: [String]
|
|
choices = join
|
|
[ [ r
|
|
| (Just r, _) <- Data.Foldable.toList (_cmd_children nameDesc)
|
|
, lastWord `isPrefixOf` r
|
|
, lastWord /= r
|
|
]
|
|
, [ s
|
|
| s <- partDescStrings =<< _cmd_parts nameDesc
|
|
, lastWord `isPrefixOf` s
|
|
, lastWord /= s
|
|
]
|
|
]
|
|
|
|
|
|
-- | Derives a list of completion items from a given input string and a given
|
|
-- 'CommandDesc'. Considers potential subcommands and where available the
|
|
-- completion info present in 'PartDesc's.
|
|
--
|
|
-- See 'addShellCompletion' which uses this.
|
|
shellCompletionWords
|
|
:: String -- ^ input string
|
|
-> CommandDesc () -- ^ CommandDesc obtained on that input string
|
|
-> String -- ^ "remaining" input after the last successfully parsed
|
|
-- subcommand. See 'UI.Butcher.Monadic.runCmdParserExt'.
|
|
-> [CompletionItem]
|
|
shellCompletionWords line cdesc pcRest = choices
|
|
where
|
|
nameDesc = case _cmd_mParent cdesc of
|
|
Nothing -> cdesc
|
|
Just (_, parent) | null pcRest && not (null lastWord) -> parent
|
|
-- not finished writing a command. if we have commands abc and abcdef,
|
|
-- we may want "def" as a completion after "abc".
|
|
Just{} -> cdesc
|
|
lastWord = reverse $ takeWhile (not . Char.isSpace) $ reverse $ line
|
|
choices :: [CompletionItem]
|
|
choices = join
|
|
[ [ CompletionString r
|
|
| (Just r, _) <- Data.Foldable.toList (_cmd_children nameDesc)
|
|
, lastWord `isPrefixOf` r
|
|
, lastWord /= r
|
|
]
|
|
, [ c
|
|
| c <- partDescCompletions =<< _cmd_parts cdesc
|
|
, case c of
|
|
CompletionString s -> lastWord `isPrefixOf` s && lastWord /= s
|
|
_ -> True
|
|
]
|
|
]
|
|
|
|
|
|
-- | Produces a 'PP.Doc' as a hint for the user during interactive command
|
|
-- input. Takes the current (incomplete) prompt line into account. For example
|
|
-- when you have commands (among others) \'config set-email\' and
|
|
-- \'config get-email\', then on empty prompt there will be an item \'config\';
|
|
-- on the partial prompt \'config \' the help doc will contain the
|
|
-- \'set-email\' and \'get-email\' items.
|
|
interactiveHelpDoc
|
|
:: String -- ^ input string
|
|
-> CommandDesc () -- ^ CommandDesc obtained on that input string
|
|
-> String -- ^ "remaining" input after the last successfully parsed
|
|
-- subcommand. See 'UI.Butcher.Monadic.runCmdParserExt'.
|
|
-> Int -- ^ max length of help text
|
|
-> PP.Doc
|
|
interactiveHelpDoc cmdline desc pcRest maxLines = if
|
|
| null cmdline -> helpStrShort
|
|
| List.last cmdline == ' ' -> helpStrShort
|
|
| otherwise -> helpStr
|
|
where
|
|
helpStr = if List.length optionLines > maxLines
|
|
then
|
|
PP.fcat $ List.intersperse (PP.text "|") $ PP.text . fst <$> optionLines
|
|
else PP.vcat $ optionLines <&> \case
|
|
(s, "") -> PP.text s
|
|
(s, h ) -> PP.text s PP.<> PP.text h
|
|
where
|
|
nameDesc = case _cmd_mParent desc of
|
|
Nothing -> desc
|
|
Just (_, parent) | null pcRest -> parent
|
|
Just{} -> desc
|
|
|
|
lastWord = reverse $ takeWhile (not . Char.isSpace) $ reverse $ cmdline
|
|
optionLines :: [(String, String)]
|
|
optionLines = -- a list of potential words that make sense, given
|
|
-- the current input.
|
|
join
|
|
[ [ (s, e)
|
|
| (Just s, c) <- Data.Foldable.toList (_cmd_children nameDesc)
|
|
, lastWord `isPrefixOf` s
|
|
, let e = join $ join
|
|
[ [ " ARGS" | not $ null $ _cmd_parts c ]
|
|
, [ " CMDS" | not $ null $ _cmd_children c ]
|
|
, [ ": " ++ show h | Just h <- [_cmd_help c] ]
|
|
]
|
|
]
|
|
, [ (s, "")
|
|
| s <- partDescStrings =<< _cmd_parts nameDesc
|
|
, lastWord `isPrefixOf` s
|
|
]
|
|
]
|
|
helpStrShort = ppUsageWithHelp desc
|
|
|
|
|
|
-- | Obtains a list of "expected"/potential strings for a command part
|
|
-- described in the 'PartDesc'. In constrast to the 'simpleCompletion'
|
|
-- function this function does not take into account any current input, and
|
|
-- consequently the output elements can in general not be appended to partial
|
|
-- input to form valid input.
|
|
partDescStrings :: PartDesc -> [String]
|
|
partDescStrings = \case
|
|
PartLiteral s -> [s]
|
|
PartVariable _ -> []
|
|
-- TODO: we could handle seq of optional and such much better
|
|
PartOptional x -> partDescStrings x
|
|
PartAlts alts -> alts >>= partDescStrings
|
|
PartSeq [] -> []
|
|
PartSeq (x:_) -> partDescStrings x
|
|
PartDefault _ x -> partDescStrings x
|
|
PartSuggestion ss x -> [ s | CompletionString s <- ss ] ++ partDescStrings x
|
|
PartRedirect _ x -> partDescStrings x
|
|
PartReorder xs -> xs >>= partDescStrings
|
|
PartMany x -> partDescStrings x
|
|
PartWithHelp _h x -> partDescStrings x -- TODO: handle help
|
|
PartHidden{} -> []
|
|
|
|
|
|
-- | Obtains a list of "expected"/potential strings for a command part
|
|
-- described in the 'PartDesc'. In constrast to the 'simpleCompletion'
|
|
-- function this function does not take into account any current input, and
|
|
-- consequently the output elements can in general not be appended to partial
|
|
-- input to form valid input.
|
|
partDescCompletions :: PartDesc -> [CompletionItem]
|
|
partDescCompletions = \case
|
|
PartLiteral s -> [CompletionString s]
|
|
PartVariable _ -> []
|
|
-- TODO: we could handle seq of optional and such much better
|
|
PartOptional x -> partDescCompletions x
|
|
PartAlts alts -> alts >>= partDescCompletions
|
|
PartSeq [] -> []
|
|
PartSeq (x:_) -> partDescCompletions x
|
|
PartDefault _ x -> partDescCompletions x
|
|
PartSuggestion ss x -> ss ++ partDescCompletions x
|
|
PartRedirect _ x -> partDescCompletions x
|
|
PartReorder xs -> xs >>= partDescCompletions
|
|
PartMany x -> partDescCompletions x
|
|
PartWithHelp _h x -> partDescCompletions x -- TODO: handle help
|
|
PartHidden{} -> []
|