opt-env-conf-test-0.0.0.4: test/OptEnvConf/CompletionSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module OptEnvConf.CompletionSpec (spec) where
import Control.Applicative
import qualified Data.List.NonEmpty as NE
import Data.Maybe
import OptEnvConf.Completer
import OptEnvConf.Completion
import OptEnvConf.Parser
import OptEnvConf.Setting
import OptEnvConf.Test
import Path
import Path.IO
import Test.Syd
spec :: Spec
spec = do
describe "bash" $ do
it "produces the same bash completion script" $
pureGoldenStringFile "test_resources/completion/bash-completion-script.bash" $
bashCompletionScript [absfile|/usr/bin/example-executable|] "example-executable"
describe "zsh" $ do
it "produces the same zsh completion script" $
pureGoldenStringFile "test_resources/completion/zsh-completion-script.zsh" $
zshCompletionScript [absfile|/usr/bin/example-executable|] "example-executable"
describe "fish" $ do
it "produces the same fish completion script" $
pureGoldenStringFile "test_resources/completion/fish-completion-script.fish" $
fishCompletionScript [absfile|/usr/bin/example-executable|] "example-executable"
describe "pureCompletionQuery" $ do
it "can complete a switch from nothing" $
parserCompletionTest
(setting [switch (), short 'e', long "example"])
0
[]
["--example"] -- Only the long version
it "can complete a short switch from a single dash" $
parserCompletionTest
(setting [switch (), short 'e'])
0
["-"]
["-e"]
it "can complete a long switch from a single dash" $
parserCompletionTest
(setting [switch (), long "example"])
0
["-"]
["--example"]
it "can complete a long switch from a double dash" $
parserCompletionTest
(setting [switch (), long "example"])
0
["--"]
["--example"]
it "can complete a short option's dashed" $
parserCompletionTest
(setting [option, short 'e', completer $ listCompleter ["hi"]])
0
[]
["-e"]
it "can complete a long option's dashed" $
parserCompletionTest
(setting [option, long "example", completer $ listCompleter ["hi"]])
0
[]
["--example"]
it "can complete a short option with a separate arg" $
parserCompletionTest
(setting [option, short 'e', completer $ listCompleter ["hi"]])
1
["-e"]
["hi"]
-- Don't think we want to support this.
-- pending "can complete a short option in short-hand mode.
it "can complete a long option" $
parserCompletionTest
(setting [option, long "example", completer $ listCompleter ["hi"]])
1
["--example"]
["hi"]
-- Don't think we want to support this
-- pending "can complete a long option with equals sign"
it "can complete both switches of a tuple" $
parserCompletionTest
((,) <$> setting [switch (), long "foo"] <*> setting [switch (), long "bar"])
0
[]
[ "--foo",
"--bar"
]
it "can complete both switches of a tuple, with a prefix" $
parserCompletionTest
((,) <$> setting [switch (), long "bar"] <*> setting [switch (), long "baz"])
0
["--b"]
[ "--bar",
"--baz"
]
it "can complete both switches of an either" $
parserCompletionTest
(setting [switch (), long "foo"] <|> setting [switch (), long "bar"])
0
[]
[ "--foo",
"--bar"
]
it "can complete both switches of an either wrapped in optionals" $
parserCompletionTest
(optional (setting [switch (), long "foo"]) <|> optional (setting [switch (), long "bar"]))
0
[]
[ "--foo",
"--bar"
]
describe "commands" $ do
let p =
commands
[ command "foo" "1" $ pure (),
command "bar" "2" $ pure (),
command "baz" "3" $ pure ()
]
it "can complete a command argument" $
parserCompletionTest
p
0
[]
[ Completion "foo" (Just "1"),
Completion "bar" (Just "2"),
Completion "baz" (Just "3")
]
it "can complete a command argument when it's been partially provided" $
parserCompletionTest
p
0
["b"]
[ Completion "bar" (Just "2"),
Completion "baz" (Just "3")
]
describe "completion after a command" $ do
it "can complete a command with a switch" $
parserCompletionTest
(commands [command "foo" "1" $ setting [help "ex", switch (), short 'e', long "example"]])
1
["foo"]
[Completion "--example" (Just "ex")] -- Only the long version
it "can complete a command's short switch" $
parserCompletionTest
(commands [command "foo" "1" $ setting [switch (), short 'e']])
1
["foo", "-"]
["-e"]
it "can complete a command's long switch from a single dash" $
parserCompletionTest
(commands [command "foo" "1" $ setting [switch (), long "example"]])
1
["foo", "-"]
["--example"]
it "can complete a command's long switch from a double dash" $
parserCompletionTest
(commands [command "foo" "1" $ setting [switch (), long "example"]])
1
["foo", "--"]
["--example"]
it "can complete a command's short option" $
parserCompletionTest
(commands [command "foo" "1" $ setting [option, short 'e', completer $ listCompleter ["hi"]]])
2
["foo", "-e"]
["hi"]
it "can complete a command's long option" $
parserCompletionTest
(commands [command "foo" "1" $ setting [option, long "example", completer $ listCompleter ["hi"]]])
2
["foo", "--example"]
["hi"]
it "can complete a file argument" $
parserCompletionDescriptionTest
(filePathSetting [help "file arg", argument])
0
[]
["file arg"]
it "can complete a file option" $
parserCompletionDescriptionTest
(filePathSetting [help "file arg", option, long "file"])
1
["--file"]
["file arg"]
it "can complete a directory argument" $
parserCompletionDescriptionTest
(directoryPathSetting [help "dir arg", argument])
0
[]
["dir arg"]
it "can complete a directory option" $
parserCompletionDescriptionTest
(directoryPathSetting [help "dir arg", option, long "file"])
1
["--file"]
["dir arg"]
it "no longer suggests a switch that has already been parsed" $
parserCompletionTest
((,) <$> setting [switch (), long "foo"] <*> setting [switch (), long "bar"])
1
["--foo"]
["--bar"]
it "no longer suggests an option that has already been parsed" $
parserCompletionTest
( (,,)
<$> setting [option, reader (str :: Reader String), long "foo"]
<*> setting [option, reader (str :: Reader String), long "bar"]
<*> setting [switch (), long "quux"]
)
2
["--foo", "foo"]
[ "--bar",
"--quux"
]
it "no longer suggests an argument that has already been parsed" $
parserCompletionDescriptionTest
( (,,)
<$> setting [argument, reader (str :: Reader String), help "hi", completer $ listCompleter ["hi"]]
<*> setting [argument, reader (str :: Reader String), help "ho", completer $ listCompleter ["ho"]]
<*> setting [switch (), long "bar", help "hu"]
)
1
["foo"]
[ "ho",
"hu"
]
describe "commands with a default command" $ do
let p =
commands
[ command "foo" "1" $ pure (),
command "bar" "2" $ pure (),
command "baz" "3" $ pure (),
defaultCommand "bar"
]
it "still lists all commands when there is a default" $
parserCompletionTest
p
0
[]
[ Completion "foo" (Just "1"),
Completion "bar" (Just "2"),
Completion "baz" (Just "3")
]
it "still filters commands by prefix when there is a default" $
parserCompletionTest
p
0
["b"]
[ Completion "bar" (Just "2"),
Completion "baz" (Just "3")
]
it "completes the default command's switch when no command is given" $
parserCompletionTest
( commands
[ command "foo" "1" $ setting [switch (), long "example"],
command "bar" "2" $ pure (),
defaultCommand "foo"
]
)
0
["--"]
["--example"]
it "completes both commands and the default command's switch" $
parserCompletionTest
( commands
[ command "foo" "1" $ setting [switch (), long "example"],
command "bar" "2" $ pure (),
defaultCommand "foo"
]
)
0
[]
[ Completion "foo" (Just "1"),
Completion "bar" (Just "2"),
"--example"
]
it "completes the default command's option when no command is given" $
parserCompletionTest
( commands
[ command "foo" "1" $ setting [option, long "example", completer $ listCompleter ["hi"]],
command "bar" "2" $ pure (),
defaultCommand "foo"
]
)
0
["--"]
["--example"]
it "completes the default command's option value when no command is given" $
parserCompletionTest
( commands
[ command "foo" "1" $ setting [option, long "example", reader (str :: Reader String), completer $ listCompleter ["hi"]],
command "bar" "2" $ pure "bar",
defaultCommand "foo"
]
)
1
["--example"]
["hi"]
it "completes the default command's argument when no command is given" $
parserCompletionDescriptionTest
( commands
[ command "foo" "1" $ setting [argument, reader (str :: Reader String), help "arg help", completer $ listCompleter ["val"]],
command "bar" "2" $ pure "bar",
defaultCommand "foo"
]
)
0
[]
-- Should include the default command's argument completer along with command names
[ "1",
"2",
"arg help"
]
it "completes inside the default command after consuming its switch" $
parserCompletionTest
( commands
[ command "foo" "1" $
(,)
<$> setting [switch (), long "first"]
<*> setting [switch (), long "second"],
command "bar" "2" $ pure ((), ()),
defaultCommand "foo"
]
)
1
["--first"]
["--second"]
it "completes inside the default command after consuming its option" $
parserCompletionTest
( commands
[ command "foo" "1" $
(,)
<$> setting [option, reader (str :: Reader String), long "name"]
<*> setting [switch (), long "verbose"],
command "bar" "2" $ pure ("bar", ()),
defaultCommand "foo"
]
)
2
["--name", "hello"]
["--verbose"]
describe "global options with commands and a default" $ do
it "completes global options and commands together" $
parserCompletionTest
( (,)
<$> setting [switch (), long "verbose"]
<*> commands
[ command "foo" "1" $ pure (),
command "bar" "2" $ pure (),
defaultCommand "foo"
]
)
0
[]
[ "--verbose",
Completion "foo" (Just "1"),
Completion "bar" (Just "2")
]
it "completes commands after a global option" $
parserCompletionTest
( (,)
<$> setting [switch (), long "verbose"]
<*> commands
[ command "foo" "1" $ pure (),
command "bar" "2" $ pure (),
defaultCommand "foo"
]
)
1
["--verbose"]
[ Completion "foo" (Just "1"),
Completion "bar" (Just "2")
]
it "completes the default command's options after a global option" $
parserCompletionTest
( (,)
<$> setting [switch (), long "verbose"]
<*> commands
[ command "foo" "1" $ setting [switch (), long "example"],
command "bar" "2" $ pure (),
defaultCommand "foo"
]
)
1
["--verbose"]
[ Completion "foo" (Just "1"),
Completion "bar" (Just "2"),
"--example"
]
describe "many" $ do
it "can complete repeated switches" $
parserCompletionTest
(length <$> many (setting [switch (), long "verbose", short 'v']))
0
[]
["--verbose"]
it "can still complete after consuming one" $
parserCompletionTest
(length <$> many (setting [switch (), long "verbose", short 'v']))
1
["--verbose"]
["--verbose"]
it "can still complete after consuming two" $
parserCompletionTest
(length <$> many (setting [switch (), long "verbose", short 'v']))
2
["-v", "-v"]
["--verbose"]
it "can complete repeated options" $
parserCompletionTest
( many
( setting
[ option,
reader (str :: Reader String),
long "include",
completer $ listCompleter ["foo", "bar"]
]
)
)
1
["--include"]
[ "foo",
"bar"
]
it "can complete repeated options after one has already been parsed" $
parserCompletionTest
( many
( setting
[ option,
reader (str :: Reader String),
long "include",
completer $ listCompleter ["foo", "bar"]
]
)
)
3
[ "--include",
"foo",
"--include"
]
[ "foo",
"bar"
]
describe "some" $ do
it "can complete at least one switch" $
parserCompletionTest
(NE.length <$> someNonEmpty (setting [switch (), long "verbose", short 'v']))
0
[]
["--verbose"]
it "can still complete after consuming one" $
parserCompletionTest
(NE.length <$> someNonEmpty (setting [switch (), long "verbose", short 'v']))
1
["--verbose"]
["--verbose"]
describe "allOrNothing" $ do
it "completes normally inside allOrNothing" $
parserCompletionTest
( allOrNothing $
(,)
<$> setting [option, reader (str :: Reader String), long "host"]
<*> setting [option, reader (str :: Reader String), long "port"]
)
0
[]
[ "--host",
"--port"
]
it "still suggests the remaining option after one is consumed" $
parserCompletionTest
( allOrNothing $
(,)
<$> setting [option, reader (str :: Reader String), long "host"]
<*> setting [option, reader (str :: Reader String), long "port"]
)
2
["--host", "localhost"]
["--port"]
describe "check" $ do
it "completes through a checkMapEither" $
parserCompletionTest
( checkMapEither
(\s -> if null s then Left "empty" else Right s)
(setting [argument, reader (str :: Reader String), completer $ listCompleter ["hello"]])
)
0
[]
["hello"]
it "completes through a mapIO" $
parserCompletionTest
( mapIO pure $
setting [switch (), long "example"]
)
0
[]
["--example"]
describe "hidden settings" $ do
it "does not suggest hidden switches" $
parserCompletionTest
( (,)
<$> setting [switch (), long "visible"]
<*> setting [switch (), long "secret", hidden]
)
0
[]
["--visible"]
it "does not suggest hidden options" $
parserCompletionTest
( (,)
<$> setting [option, reader (str :: Reader String), long "visible"]
<*> setting [option, reader (str :: Reader String), long "secret", hidden]
)
0
[]
["--visible"]
describe "settings with only env or conf" $ do
it "does not error on env-only settings combined with arg settings" $
parserCompletionTest
( (,)
<$> setting [switch (), long "verbose"]
<*> setting [reader (str :: Reader String), env "MY_VAR"]
)
0
[]
["--verbose"]
describe "enableDisableSwitch" $ do
-- enableDisableSwitch uses a visible "dummy" with the
-- (enable|disable)- prefix for documentation, while the actual
-- --enable-feature and --disable-feature switches are hidden.
it "completes the documented dummy flag" $
parserCompletionTest
( enableDisableSwitch
[ help "enable feature",
name "feature",
value False
]
)
0
[]
[Completion "--(enable|disable)-feature" (Just "enable feature")]
it "filters the documented flag by prefix" $
parserCompletionTest
( enableDisableSwitch
[ help "enable feature",
name "feature",
value False
]
)
0
["--(e"]
[Completion "--(enable|disable)-feature" (Just "enable feature")]
describe "choice" $ do
it "completes across all choices" $
parserCompletionTest
( choice
[ setting [switch (), long "alpha"],
setting [switch (), long "beta"],
setting [switch (), long "gamma"]
]
)
0
[]
[ "--alpha",
"--beta",
"--gamma"
]
describe "withDefault" $ do
it "completes the underlying parser" $
parserCompletionTest
(withDefault "default" $ setting [option, reader (str :: Reader String), long "name", completer $ listCompleter ["alice", "bob"]])
1
["--name"]
[ "alice",
"bob"
]
it "still suggests the option when it has a default" $
parserCompletionTest
(withDefault "default" $ setting [option, reader (str :: Reader String), long "name"])
0
[]
["--name"]
describe "nested commands" $ do
let p =
commands
[ command "top" "top-level" $
commands
[ command "sub-a" "sub a" $ pure (),
command "sub-b" "sub b" $ pure ()
],
command "other" "other" $ pure ()
]
it "completes top-level commands" $
parserCompletionTest
p
0
[]
[ Completion "top" (Just "top-level"),
Completion "other" (Just "other")
]
it "completes sub-commands after selecting a top-level command" $
parserCompletionTest
p
1
["top"]
[ Completion "sub-a" (Just "sub a"),
Completion "sub-b" (Just "sub b")
]
it "filters sub-commands by prefix" $
parserCompletionTest
p
1
["top", "sub-a"]
[Completion "sub-a" (Just "sub a")]
it "completes nested default commands" $
parserCompletionTest
( commands
[ command "top" "top-level" $
commands
[ command "sub-a" "sub a" $ setting [switch (), long "flag"],
command "sub-b" "sub b" $ pure (),
defaultCommand "sub-a"
],
command "other" "other" $ pure ()
]
)
1
["top"]
[ Completion "sub-a" (Just "sub a"),
Completion "sub-b" (Just "sub b"),
"--flag"
]
describe "commands merged via alternative" $ do
it "completes commands from both sides of an alternative" $
parserCompletionTest
( commands [command "foo" "1" $ pure ()]
<|> commands [command "bar" "2" $ pure ()]
)
0
[]
[ Completion "foo" (Just "1"),
Completion "bar" (Just "2")
]
describe "arguments after double dash" $ do
-- A bare -- is consumed as the argument value "--" when it's
-- the only argument remaining, so completion sees nothing left.
it "consumes -- as the argument value" $
parserCompletionTest
(setting [argument, reader (str :: Reader String), completer $ listCompleter ["file1", "file2"]])
1
["--"]
[]
-- After --, the completion should suggest positional arguments
-- rather than switches. Currently it does not: the switch is
-- still suggested because the completion engine does not
-- account for -- separating options from arguments.
it "suggests the switch even after -- (known bug)" $
parserCompletionTest
( (,)
<$> setting [switch (), long "verbose"]
<*> setting [argument, reader (str :: Reader String), completer $ listCompleter ["file1", "file2"]]
)
2
["--", "fi"]
["--verbose"]
describe "mixed arguments and options" $ do
it "can complete an option after a positional argument" $
parserCompletionTest
( (,)
<$> setting [argument, reader (str :: Reader String), completer $ listCompleter ["input"]]
<*> setting [switch (), long "verbose"]
)
1
["input"]
["--verbose"]
it "can complete a positional after an option" $
parserCompletionDescriptionTest
( (,)
<$> setting [switch (), long "verbose"]
<*> setting [argument, reader (str :: Reader String), help "file", completer $ listCompleter ["output"]]
)
1
["--verbose"]
["file"]
describe "multiple arguments" $ do
-- When no args have been typed, both argument completers fire
-- because the completion engine doesn't know which one will be
-- consumed first. This is the documented TODO in Completion.hs.
it "completes both arguments when none have been typed" $
parserCompletionTest
( (,)
<$> setting [argument, reader (str :: Reader String), completer $ listCompleter ["src"]]
<*> setting [argument, reader (str :: Reader String), completer $ listCompleter ["dst"]]
)
0
[]
[ "src",
"dst"
]
it "completes the second argument after the first" $
parserCompletionTest
( (,)
<$> setting [argument, reader (str :: Reader String), completer $ listCompleter ["src"]]
<*> setting [argument, reader (str :: Reader String), completer $ listCompleter ["dst"]]
)
1
["something"]
["dst"]
describe "empty parser" $ do
it "produces no completions for empty" $
parserCompletionTest
(empty :: Parser ())
0
[]
[]
describe "pure parser" $ do
it "produces no completions for pure" $
parserCompletionTest
(pure () :: Parser ())
0
[]
[]
describe "select" $ do
-- Select uses andCompletions: both branches contribute because
-- the completion engine cannot know at completion time whether
-- the first parser will return Left or Right.
it "completes through a select" $
parserCompletionTest
( select
(Left <$> setting [argument, reader (str :: Reader String), completer $ listCompleter ["hello"]])
(const <$> setting [argument, reader (str :: Reader String), completer $ listCompleter ["world"]])
)
0
[]
[ "hello",
"world"
]
describe "withConfig" $ do
it "completes the main parser through withConfig" $
parserCompletionTest
( withConfig (pure Nothing) $
setting [switch (), long "example"]
)
0
[]
["--example"]
it "completes the config file option together with the main parser" $
parserCompletionTest
( withConfig
(Nothing <$ setting [option, reader (str :: Reader String), long "config-file", completer $ listCompleter ["config.yaml"]])
(setting [switch (), long "verbose"])
)
0
[]
-- Main parser completions come first (p2), then config parser completions (p1)
[ "--verbose",
"--config-file"
]
it "completes the config file value along with main parser suggestions" $
parserCompletionTest
( withConfig
(Nothing <$ setting [option, reader (str :: Reader String), long "config-file", completer $ listCompleter ["config.yaml"]])
(setting [switch (), long "verbose"])
)
1
["--config-file"]
-- The main parser (p2) still suggests --verbose since it hasn't
-- consumed anything, and the config parser (p1) offers the completer.
[ "--verbose",
"config.yaml"
]
it "completes the main parser after the config option is consumed" $
parserCompletionTest
( withConfig
(Nothing <$ setting [option, reader (str :: Reader String), long "config-file", completer $ listCompleter ["config.yaml"]])
(setting [switch (), long "verbose"])
)
2
["--config-file", "config.yaml"]
["--verbose"]
describe "optional" $ do
it "completes through an optional parser" $
parserCompletionTest
(optional $ setting [switch (), long "verbose"])
0
[]
["--verbose"]
it "completes an optional with an applicative" $
parserCompletionTest
( (,)
<$> optional (setting [option, reader (str :: Reader String), long "name", completer $ listCompleter ["alice"]])
<*> setting [switch (), long "verbose"]
)
0
[]
[ "--name",
"--verbose"
]
it "completes the second part when the optional is skipped" $
parserCompletionTest
( (,)
<$> optional (setting [option, reader (str :: Reader String), long "name"])
<*> setting [switch (), long "verbose"]
)
0
[]
[ "--name",
"--verbose"
]
describe "many with other parsers" $ do
it "completes both many and a following switch" $
parserCompletionTest
( (,)
<$> many (setting [option, reader (str :: Reader String), long "include", completer $ listCompleter ["foo"]])
<*> setting [switch (), long "verbose"]
)
0
[]
[ "--include",
"--verbose"
]
it "completes both after consuming one many-option" $
parserCompletionTest
( (,)
<$> many (setting [option, reader (str :: Reader String), long "include", completer $ listCompleter ["foo"]])
<*> setting [switch (), long "verbose"]
)
2
["--include", "foo"]
[ "--include",
"--verbose"
]
it "completes many switches combined with a command" $
parserCompletionTest
( (,)
<$> many (setting [switch (), long "verbose", short 'v'])
<*> commands
[ command "run" "run it" $ pure (),
command "build" "build it" $ pure ()
]
)
0
[]
[ "--verbose",
Completion "run" (Just "run it"),
Completion "build" (Just "build it")
]
it "completes commands after consuming many switches" $
parserCompletionTest
( (,)
<$> many (setting [switch (), long "verbose", short 'v'])
<*> commands
[ command "run" "run it" $ pure (),
command "build" "build it" $ pure ()
]
)
2
["-v", "-v"]
[ "--verbose",
Completion "run" (Just "run it"),
Completion "build" (Just "build it")
]
describe "folded short switches" $ do
it "can complete after a folded short switch is consumed" $
parserCompletionTest
( (,)
<$> setting [switch (), short 'v']
<*> setting [switch (), short 'n']
)
1
["-v"]
["-n"]
it "no longer suggests a switch consumed via folding" $
parserCompletionTest
( (,,)
<$> setting [switch (), short 'v']
<*> setting [switch (), short 'n']
<*> setting [switch (), short 'x']
)
1
["-vn"]
["-x"]
describe "short option shorthand" $ do
it "completes after a short option consumed in shorthand" $
parserCompletionTest
( (,)
<$> setting [option, reader (str :: Reader String), short 'f']
<*> setting [switch (), long "verbose"]
)
1
["-ffoo.txt"]
["--verbose"]
describe "deeply nested parsers" $ do
it "completes through optional + many + applicative" $
parserCompletionTest
( (,,)
<$> optional (setting [option, reader (str :: Reader String), long "config"])
<*> many (setting [switch (), long "verbose", short 'v'])
<*> setting [argument, reader (str :: Reader String), completer $ listCompleter ["file.txt"]]
)
0
[]
[ "--config",
"--verbose",
"file.txt"
]
it "completes correctly after all optional+many consumed" $
parserCompletionTest
( (,,)
<$> optional (setting [option, reader (str :: Reader String), long "config"])
<*> many (setting [switch (), long "verbose", short 'v'])
<*> setting [argument, reader (str :: Reader String), completer $ listCompleter ["file.txt"]]
)
3
[ "--config",
"foo",
"--verbose"
]
[ "--verbose",
"file.txt"
]
describe "commands with options inside" $ do
it "completes a command's many options" $
parserCompletionTest
( commands
[ command "run" "run it" $
many (setting [option, reader (str :: Reader String), long "arg", completer $ listCompleter ["val"]])
]
)
2
["run", "--arg"]
["val"]
it "completes a command's many options after one is consumed" $
parserCompletionTest
( commands
[ command "run" "run it" $
many (setting [option, reader (str :: Reader String), long "arg", completer $ listCompleter ["val"]])
]
)
3
[ "run",
"--arg",
"val"
]
["--arg"]
describe "multiple commands with shared options" $ do
it "completes options in the selected command only" $
parserCompletionTest
( commands
[ command "foo" "1" $ setting [switch (), long "alpha"],
command "bar" "2" $ setting [switch (), long "beta"]
]
)
1
["foo"]
["--alpha"]
it "does not leak options from other commands" $
parserCompletionTest
( commands
[ command "foo" "1" $ setting [switch (), long "alpha"],
command "bar" "2" $ setting [switch (), long "beta"]
]
)
1
["bar"]
["--beta"]
describe "alternative branch failures" $ do
-- A parser that is not detected as isEmpty but still fails during
-- completion (andCompletions fails because the inner empty fails).
-- This avoids the (<|>) optimization that removes empty alternatives.
let failingP = (empty :: Parser ()) *> pure ()
it "restores state when first branch fails in an alternative" $
parserCompletionTest
(failingP <|> setting [switch (), long "foo"])
0
[]
["--foo"]
it "returns first branch completions when second branch fails" $
parserCompletionTest
(setting [switch (), long "foo"] <|> failingP)
0
[]
["--foo"]
it "returns no completions when both alternatives fail" $
parserCompletionTest
(failingP <|> failingP)
0
[]
[]
describe "applicative branch failures" $ do
it "returns nothing when first applicative branch fails" $
parserCompletionTest
((,) <$> ((empty :: Parser ()) *> pure ()) <*> setting [switch (), long "foo"])
0
[]
[]
it "returns nothing when second applicative branch fails" $
parserCompletionTest
((,) <$> setting [switch (), long "foo"] <*> ((empty :: Parser String) *> pure ("" :: String)))
0
[]
[]
describe "many edge cases" $ do
it "handles many where the inner parser fails" $
parserCompletionTest
(many ((empty :: Parser ()) *> pure ()))
0
[]
[]
it "returns first iteration completions when recursive call fails" $
parserCompletionTest
(many (commands [command "foo" "1" $ pure ()]))
1
["foo"]
[Completion "foo" (Just "1")]
describe "command not found" $ do
it "returns no completions for an unknown command" $
parserCompletionTest
(commands [command "foo" "1" $ pure (), command "bar" "2" $ pure ()])
1
["unknown"]
[]
describe "argument completer at end" $ do
it "offers the argument completer at the end after a switch is consumed" $
parserCompletionTest
( (,)
<$> setting [switch (), long "verbose"]
<*> setting [argument, reader (str :: Reader String), completer $ listCompleter ["file"]]
)
1
["--verbose"]
["file"]
it "does not offer option completer when not at end of args" $
parserCompletionTest
( (,,)
<$> setting [switch (), long "other"]
<*> setting [option, reader (str :: Reader String), long "name", completer $ listCompleter ["val"]]
<*> setting [switch (), long "extra"]
)
3
[ "--name",
"--other",
"--extra"
]
[]
describe "option with different dashed in args" $ do
it "does not suggest an option when a different dashed is in the args" $
parserCompletionTest
( (,)
<$> setting [option, reader (str :: Reader String), long "output", completer $ listCompleter ["file.txt"]]
<*> setting [option, reader (str :: Reader String), long "input", completer $ listCompleter ["data.csv"]]
)
1
["--input"]
["data.csv"]
describe "default command parser failure" $ do
let emptyS = empty :: Parser String
it "falls back to explicit commands when default command parser fails" $
parserCompletionTest
( commands
[ command "foo" "1" $ (,) <$> setting [argument, reader (str :: Reader String)] <*> emptyS,
command "bar" "2" $ pure ("", ""),
defaultCommand "foo"
]
)
0
[]
[ Completion "foo" (Just "1"),
Completion "bar" (Just "2")
]
describe "prefix filtering" $ do
it "filters switches by typed prefix" $
parserCompletionTest
( (,)
<$> setting [switch (), long "verbose"]
<*> setting [switch (), long "version"]
)
0
["--verb"]
["--verbose"]
it "returns nothing when prefix matches nothing" $
parserCompletionTest
(setting [switch (), long "verbose"])
0
["--xyz"]
[]
it "filters commands by typed prefix" $
parserCompletionDescriptionTest
( commands
[ command "generate" "gen" $ pure (),
command "get" "getter" $ pure (),
command "build" "builder" $ pure ()
]
)
0
["ge"]
[ "gen",
"getter"
]
describe "argument completion with dashed values" $ do
-- When a dashed-looking value is provided as a positional argument,
-- the completion engine treats it as a potential switch (first
-- consumeArgument possibility) instead of consuming it. This
-- causes the argument's completer to fire even though a value was
-- already provided.
it "should not offer the argument completer after a dashed value is consumed" $
parserCompletionTest
(setting [argument, reader (str :: Reader String), completer $ listCompleter ["file"]])
1
["--foo"]
[]
-- Same issue in a tuple: the first argument's completer fires
-- alongside the second's, even though the first argument already
-- has a value.
it "should only complete the second argument after a dashed first argument" $
parserCompletionTest
( (,)
<$> setting [argument, reader (str :: Reader String), completer $ listCompleter ["file"]]
<*> setting [argument, reader (str :: Reader String), completer $ listCompleter ["dest"]]
)
1
["--foo"]
["dest"]
describe "partial option value completion" $ do
it "filters option values by typed prefix" $
parserCompletionTest
( setting
[ option,
reader (str :: Reader String),
long "name",
completer $ listCompleter ["alice", "bob"]
]
)
1
["--name", "al"]
["alice"]
it "returns all option values when prefix is empty" $
parserCompletionTest
( setting
[ option,
reader (str :: Reader String),
long "name",
completer $ listCompleter ["alice", "bob"]
]
)
1
["--name", ""]
[ "alice",
"bob"
]
describe "partial argument completion" $ do
it "filters argument values by typed prefix" $
parserCompletionTest
( setting
[ argument,
reader (str :: Reader String),
completer $ listCompleter ["file1", "file2", "data"]
]
)
0
["fi"]
[ "file1",
"file2"
]
it "filters argument values by a different prefix" $
parserCompletionTest
( setting
[ argument,
reader (str :: Reader String),
completer $ listCompleter ["file1", "file2", "data"]
]
)
0
["da"]
["data"]
describe "partial command name with inner option completion" $ do
it "completes a command's options after selecting by prefix" $
parserCompletionTest
( commands
[ command "deploy" "deploy it" $ setting [switch (), long "force"],
command "debug" "debug it" $ pure ()
]
)
1
["deploy"]
["--force"]
describe "completion at a middle index" $ do
it "completes the value of an option at its value position with a matching prefix" $
parserCompletionTest
( (,)
<$> setting
[ option,
reader (str :: Reader String),
long "foo",
completer $ listCompleter ["val1", "val2"]
]
<*> setting [switch (), long "bar"]
)
1
["--foo", "v", "--bar"]
[ "val1",
"val2"
]
it "filters completions when cursor word narrows the match" $
parserCompletionTest
( (,)
<$> setting [switch (), long "alpha"]
<*> setting [switch (), long "beta"]
)
0
["--alpha", "--beta"]
["--alpha"]
describe "partial long option prefix with multiple matches" $ do
it "filters options by a short prefix matching multiple" $
parserCompletionTest
( (,)
<$> setting [switch (), long "verbose"]
<*> setting [switch (), long "version"]
)
0
["--ver"]
[ "--verbose",
"--version"
]
it "filters options by a longer prefix matching one" $
parserCompletionTest
( (,)
<$> setting [switch (), long "verbose"]
<*> setting [switch (), long "version"]
)
0
["--verb"]
["--verbose"]
describe "partial argument completion after consuming earlier args" $ do
it "filters the second argument's completer by prefix after the first is consumed" $
parserCompletionTest
( (,)
<$> setting [argument, reader (str :: Reader String), completer $ listCompleter ["src"]]
<*> setting [argument, reader (str :: Reader String), completer $ listCompleter ["dst", "data"]]
)
1
["src", "ds"]
["dst"]
describe "commands with partial option value inside" $ do
it "filters a command's option values by prefix" $
parserCompletionTest
( commands
[ command "run" "run it" $
setting
[ option,
reader (str :: Reader String),
long "output",
completer $ listCompleter ["file.txt", "final.out", "data.csv"]
]
]
)
2
["run", "--output", "fi"]
[ "file.txt",
"final.out"
]
describe "many with partial values" $ do
it "filters many option values by prefix" $
parserCompletionTest
( many
( setting
[ option,
reader (str :: Reader String),
long "include",
completer $ listCompleter ["foo", "bar"]
]
)
)
1
["--include", "fo"]
["foo"]
it "filters many option values by prefix after one is consumed" $
parserCompletionTest
( many
( setting
[ option,
reader (str :: Reader String),
long "include",
completer $ listCompleter ["foo", "bar"]
]
)
)
3
["--include", "foo", "--include", "ba"]
["bar"]
sequential . doNotRandomiseExecutionOrder
$ aroundAll
( \func -> withSystemTempDir "completion-spec" $ \tdir -> do
exampleFile <- resolveFile tdir "example.txt"
writeFile (fromAbsFile exampleFile) ""
exampleDir <- resolveDir tdir "exampledir"
createDir exampleDir
nested <- resolveFile exampleDir "nested.txt"
writeFile (fromAbsFile nested) ""
func tdir
)
$ do
let evalQuery p ix ws tdir = withCurrentDir tdir $ do
let arg = fromMaybe "" $ listToMaybe $ drop ix ws
let completions = pureCompletionQuery p ix ws
evalCompletions arg completions
suggestionValues = map (completionResultValue . completionSuggestion)
describe "file and directory completion" $ do
describe "filePathSetting" $ do
itWithOuter "completes files and directories for a file argument" $ \tdir -> do
results <- evalQuery (filePathSetting [help "f", argument]) 0 [] tdir
suggestionValues results `shouldBe` ["example.txt", "exampledir/"]
itWithOuter "completes files and directories after a file option's dashed" $ \tdir -> do
results <- evalQuery (filePathSetting [help "f", option, long "file"]) 1 ["--file"] tdir
suggestionValues results `shouldBe` ["example.txt", "exampledir/"]
itWithOuter "filters by prefix" $ \tdir -> do
results <- evalQuery (filePathSetting [help "f", argument]) 0 ["exampledi"] tdir
suggestionValues results `shouldBe` ["exampledir/"]
-- Directories end in /, files do not. This convention is how
-- shells decide whether to append a trailing space after a
-- completion. The exact values are checked by
-- "completes files and directories for a file argument" above;
-- this test re-states the invariant explicitly.
itWithOuter "directories end in / and files do not" $ \tdir -> do
results <- evalQuery (filePathSetting [help "f", argument]) 0 [] tdir
suggestionValues results `shouldBe` ["example.txt", "exampledir/"]
itWithOuter "files are final and directories are not final" $ \tdir -> do
results <- evalQuery (filePathSetting [help "f", argument]) 0 [] tdir
map (completionResultFinality . completionSuggestion) results
`shouldBe` [CompletionFinal, CompletionNotFinal]
itWithOuter "carries the description through to file completions" $ \tdir -> do
results <- evalQuery (filePathSetting [help "my file", argument]) 0 [] tdir
map completionDescription results `shouldBe` [Just "my file", Just "my file"]
describe "directoryPathSetting" $ do
itWithOuter "completes only directories for a directory argument" $ \tdir -> do
results <- evalQuery (directoryPathSetting [help "d", argument]) 0 [] tdir
suggestionValues results `shouldBe` ["exampledir/"]
itWithOuter "completes only directories after a directory option's dashed" $ \tdir -> do
results <- evalQuery (directoryPathSetting [help "d", option, long "dir"]) 1 ["--dir"] tdir
suggestionValues results `shouldBe` ["exampledir/"]
itWithOuter "never includes files" $ \tdir -> do
results <- evalQuery (directoryPathSetting [help "d", argument]) 0 [] tdir
suggestionValues results `shouldBe` ["exampledir/"]
itWithOuter "directories are not final" $ \tdir -> do
results <- evalQuery (directoryPathSetting [help "d", argument]) 0 [] tdir
map (completionResultFinality . completionSuggestion) results
`shouldBe` [CompletionNotFinal]
itWithOuter "carries the description through to directory completions" $ \tdir -> do
results <- evalQuery (directoryPathSetting [help "my dir", argument]) 0 [] tdir
map completionDescription results `shouldBe` [Just "my dir"]
describe "combined with other settings" $ do
itWithOuter "includes file completions alongside other options" $ \tdir -> do
let parser =
(,)
<$> filePathSetting [help "f", option, long "file"]
<*> setting [switch (), long "verbose"]
results <- evalQuery parser 1 ["--file"] tdir
suggestionValues results `shouldBe` ["example.txt", "exampledir/", "--verbose"]
itWithOuter "includes directory completions alongside other options" $ \tdir -> do
let parser =
(,)
<$> directoryPathSetting [help "d", option, long "dir"]
<*> setting [switch (), long "verbose"]
results <- evalQuery parser 1 ["--dir"] tdir
suggestionValues results `shouldBe` ["exampledir/", "--verbose"]
-- After typing the dashed of an option that takes a value, the
-- cursor is in the value position. The option's completer
-- should fire first, then other options should follow.
--
-- Shell sends index pointing at the empty cursor word:
-- exe --dir <tab> -> index=1, words=["--dir", ""]
itWithOuter "completes directory option value first, then other options" $ \tdir -> do
let parser =
(,)
<$> directoryPathSetting [help "d", option, long "dir"]
<*> setting [switch (), long "verbose"]
results <- evalQuery parser 1 ["--dir", ""] tdir
suggestionValues results `shouldBe` ["exampledir/", "--verbose"]
itWithOuter "completes file option value first, then other options" $ \tdir -> do
let parser =
(,)
<$> filePathSetting [help "f", option, long "file"]
<*> setting [switch (), long "verbose"]
results <- evalQuery parser 1 ["--file", ""] tdir
suggestionValues results `shouldBe` ["example.txt", "exampledir/", "--verbose"]
-- A default command with an argument combined with a
-- directory option causes the directory completer not to fire
-- when completing the option's value.
itWithOuter "completes directory option value alongside default command with argument" $ \tdir -> do
let parser =
(,)
<$> commands
[ command "file" "f" $
setting [help "f", argument, reader (str :: Reader String)],
defaultCommand "file"
]
<*> directoryPathSetting [help "a", option, long "archive-dir"]
results <- evalQuery parser 1 ["--archive-dir", ""] tdir
suggestionValues results `shouldSatisfy` elem "exampledir/"