Skip to content

Commit 8fa2d94

Browse files
committed
Indent nested groups when rendering
1 parent 2f15b95 commit 8fa2d94

File tree

6 files changed

+141
-31
lines changed

6 files changed

+141
-31
lines changed

src/Options/Applicative/Builder.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,9 +390,9 @@ option r m = mkParser d g rdr
390390
--
391391
-- will render as "Group Outer.Group Inner".
392392
optPropertiesGroup :: String -> OptProperties -> OptProperties
393-
optPropertiesGroup g o = o { propGroup = OptGroup (g : gs) }
393+
optPropertiesGroup g o = o { propGroup = updateGroupName g oldGroup }
394394
where
395-
OptGroup gs = propGroup o
395+
oldGroup = propGroup o
396396

397397
-- | Prepends a group per 'optPropertiesGroup'.
398398
optionGroup :: String -> Option a -> Option a

src/Options/Applicative/Builder/Internal.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ baseProps = OptProperties
151151
, propShowDefault = Nothing
152152
, propDescMod = Nothing
153153
, propShowGlobal = True
154-
, propGroup = OptGroup []
154+
, propGroup = OptGroup 0 Nothing
155155
}
156156

157157
mkCommand :: Mod CommandFields a -> (Maybe String, [(String, ParserInfo a)])

src/Options/Applicative/Help/Core.hs

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -198,29 +198,93 @@ optionsDesc global pprefs p = vsepChunks
198198
. fmap formatTitle
199199
. fmap tabulateGroup
200200
. groupByTitle
201-
$ mapParser doc p
201+
$ docs
202202
where
203+
docs :: [Maybe (OptGroup, (Doc, Doc))]
204+
docs = mapParser doc p
205+
203206
groupByTitle :: [Maybe (OptGroup, (Doc, Doc))] -> [[(OptGroup, (Doc, Doc))]]
204-
groupByTitle = groupFstAll . catMaybes
207+
groupByTitle xs = groupFstAll . catMaybes $ xs
208+
209+
-- NOTE: [Nested group alignment]
210+
--
211+
-- For nested groups, we want to produce output like:
212+
--
213+
-- Group 1
214+
-- --opt-1 INT Option 1
215+
--
216+
-- - Group 2
217+
-- --opt-2 INT Option 2
218+
--
219+
-- - Group 3
220+
-- - opt-3 INT Option 3
221+
--
222+
-- That is, we have the following constraints:
223+
--
224+
-- 1. Nested groups are prefixed with a hyphen '- ', where the hyphen
225+
-- starts on the same column as the parent group.
226+
--
227+
-- 2. We still want the listed options to be indented twice under the
228+
-- group name, so this means nested options need to be indented
229+
-- again by the standard amount (2), due to the hyphen.
230+
--
231+
-- 3. Help text should be __globally__ aligned.
205232

206233
tabulateGroup :: [(OptGroup, (Doc, Doc))] -> (OptGroup, Chunk Doc)
207-
tabulateGroup l@((title,_):_) = (title, tabulate (prefTabulateFill pprefs) (snd <$> l))
208-
tabulateGroup [] = mempty
234+
tabulateGroup l@((title,_):_) =
235+
(title, tabulate (prefTabulateFill pprefs) (getGroup <$> l))
236+
where
237+
-- Handle NOTE: [Nested group alignment] 3. here i.e. indent the
238+
-- right Doc (help text) according to its indention level and
239+
-- global maxGroupLevel. Notice there is an inverse relationship here,
240+
-- as the further the entire group is indented, the less we need to
241+
-- indent the help text.
242+
getGroup :: (OptGroup, (Doc, Doc)) -> (Doc, Doc)
243+
getGroup o@(_, (x, y)) =
244+
let helpIndent = calcOptHelpIndent o
245+
in (x, indent helpIndent y)
246+
247+
-- Indents the option help text, taking the option's group level and
248+
-- maximum group level into account.
249+
calcOptHelpIndent :: (OptGroup, a) -> Int
250+
calcOptHelpIndent g =
251+
let groupLvl = optGroupToLevel g
252+
in lvlIndent * (maxGroupLevel - groupLvl)
253+
254+
tabulateGroup [] = (OptGroup 0 Nothing, mempty)
209255

210-
-- Note that we treat Global/Available options identically, when it comes
211-
-- to titles.
212256
formatTitle :: (OptGroup, Chunk Doc) -> Chunk Doc
213-
formatTitle (OptGroup groups, opts) =
214-
case groups of
215-
[] -> (pretty defTitle .$.) <$> opts
216-
gs@(_:_) -> (renderGroupStr gs .$.) <$> opts
257+
formatTitle (OptGroup idx mTitle, opts) =
258+
-- Two cases to handle w.r.t group level (i.e. nested groups).
259+
case idx of
260+
-- Group not nested: no indention.
261+
0 -> (\d -> pretty title .$. d) <$> opts
262+
-- Handle NOTE: [Nested group alignment] 1 and 2 here.
263+
n ->
264+
let -- indent entire group based on its level.
265+
indentGroup = indent (lvlIndent * (n - 1))
266+
-- indent opts an extra lvlIndent to account for hyphen
267+
indentOpts = indent lvlIndent
268+
in (\d -> indentGroup $ (pretty $ "- " <> title) .$. indentOpts d)
269+
<$> opts
217270
where
271+
title = case mTitle of
272+
Nothing -> defTitle
273+
Just t -> t
218274
defTitle =
219275
if global
220276
then "Global options:"
221277
else "Available options:"
222278

223-
renderGroupStr = pretty . intercalate "."
279+
maxGroupLevel :: Int
280+
maxGroupLevel = findMaxGroupLevel docs
281+
282+
-- Finds the maxium OptGroup level.
283+
findMaxGroupLevel :: [Maybe (OptGroup, (Doc, Doc))] -> Int
284+
findMaxGroupLevel = foldl' (\acc -> max acc . optGroupToLevel) 0 . catMaybes
285+
286+
optGroupToLevel :: (OptGroup, a) -> Int
287+
optGroupToLevel ((OptGroup i _), _) = i
224288

225289
doc :: ArgumentReachability -> Option a -> Maybe (OptGroup, (Doc, Doc))
226290
doc info opt = do
@@ -238,6 +302,9 @@ optionsDesc global pprefs p = vsepChunks
238302
descGlobal = global
239303
}
240304

305+
lvlIndent :: Int
306+
lvlIndent = 2
307+
241308
errorHelp :: Chunk Doc -> ParserHelp
242309
errorHelp chunk = mempty { helpError = chunk }
243310

src/Options/Applicative/Types.hs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module Options.Applicative.Types (
1212
OptReader(..),
1313
OptProperties(..),
1414
OptGroup(..),
15+
updateGroupName,
1516
OptVisibility(..),
1617
Backtracking(..),
1718
ReadM(..),
@@ -151,15 +152,16 @@ data OptVisibility
151152
-- | Groups for optionals. Can be multiple in the case of nested groups.
152153
--
153154
-- @since 0.19.0.0
154-
newtype OptGroup = OptGroup [String]
155+
data OptGroup = OptGroup !Int (Maybe String)
155156
deriving (Eq, Ord, Show)
156157

157-
instance Semigroup OptGroup where
158-
OptGroup xs <> OptGroup ys = OptGroup (xs ++ ys)
159-
160-
instance Monoid OptGroup where
161-
mempty = OptGroup []
162-
mappend = (<>)
158+
-- | If the group name is not already set, sets the group name to the
159+
-- parameter and leaves the index as-is. If, on the other hand, the group
160+
-- name already exists, we ignore the parameter and increment the index
161+
-- by one.
162+
updateGroupName :: String -> OptGroup -> OptGroup
163+
updateGroupName newName (OptGroup i Nothing) = OptGroup i (Just newName)
164+
updateGroupName _ (OptGroup i (Just oldName)) = OptGroup (i + 1) (Just oldName)
163165

164166
-- | Specification for an individual parser option.
165167
data OptProperties = OptProperties

tests/Examples/ParserGroup/Nested.hs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,22 @@ data LogGroup = LogGroup
1717

1818
data SystemGroup = SystemGroup
1919
{ poll :: Bool,
20+
deepNested :: Nested2,
2021
timeout :: Int
2122
}
2223
deriving (Show)
2324

25+
data Nested2 = Nested2
26+
{ nested2Str :: String,
27+
nested3 :: Nested3
28+
}
29+
deriving (Show)
30+
31+
newtype Nested3 = Nested3
32+
{ nested3Str :: String
33+
}
34+
deriving (Show)
35+
2436
data Sample = Sample
2537
{ hello :: String,
2638
logGroup :: LogGroup,
@@ -97,6 +109,7 @@ sample = do
97109
( long "poll"
98110
<> help "Whether to poll"
99111
)
112+
<*> parseNested2
100113
<*> ( option
101114
auto
102115
( long "timeout"
@@ -105,6 +118,27 @@ sample = do
105118
)
106119
)
107120

121+
parseNested2 = parserOptionGroup "Nested2" $ do
122+
nestedStr2 <-
123+
( option
124+
auto
125+
( long "double-nested"
126+
<> metavar "STR"
127+
<> help "Some nested option"
128+
)
129+
)
130+
nested3 <- parseNested3
131+
pure $ Nested2 nestedStr2 nested3
132+
133+
parseNested3 = parserOptionGroup "Nested3" $
134+
( option
135+
(Nested3 <$> auto)
136+
( long "triple-nested"
137+
<> metavar "STR"
138+
<> help "Another option"
139+
)
140+
)
141+
108142
parseVerbosity =
109143
option
110144
auto

tests/parser_group_nested.err.txt

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
parser_group.nested - a test for optparse-applicative
22

33
Usage: parser_group_nested --hello TARGET [--file-log-path PATH] [--poll]
4-
--timeout INT [--file-log-verbosity INT] [-q|--quiet]
4+
--double-nested STR --triple-nested STR --timeout INT
5+
[--file-log-verbosity INT] [-q|--quiet]
56
(-v|--verbosity ARG) Command
67

78
Nested parser groups
89

910
Available options:
10-
--hello TARGET Target for the greeting
11-
-q,--quiet Whether to be quiet
12-
-v,--verbosity ARG Console verbosity
13-
-h,--help Show this help text
11+
--hello TARGET Target for the greeting
12+
-q,--quiet Whether to be quiet
13+
-v,--verbosity ARG Console verbosity
14+
-h,--help Show this help text
1415

1516
Logging
16-
--file-log-path PATH Log file path
17-
--file-log-verbosity INT File log verbosity
17+
--file-log-path PATH Log file path
18+
--file-log-verbosity INT File log verbosity
1819

19-
Logging.System Options
20-
--poll Whether to poll
21-
--timeout INT Whether to time out
20+
- System Options
21+
--poll Whether to poll
22+
--timeout INT Whether to time out
23+
24+
- Nested2
25+
--double-nested STR Some nested option
26+
27+
- Nested3
28+
--triple-nested STR Another option

0 commit comments

Comments
 (0)