Skip to content

Commit 2f3e58a

Browse files
tbidneroberth
authored andcommitted
Indent nested groups when rendering
1 parent 26071ef commit 2f3e58a

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
@@ -426,9 +426,9 @@ biOption r r2 m = mkParser d g rdr
426426
-- @
427427
--
428428
optPropertiesGroup :: String -> OptProperties -> OptProperties
429-
optPropertiesGroup g o = o { propGroup = OptGroup (g : gs) }
429+
optPropertiesGroup g o = o { propGroup = updateGroupName g oldGroup }
430430
where
431-
OptGroup gs = propGroup o
431+
oldGroup = propGroup o
432432

433433
-- | Prepends a group per 'optPropertiesGroup'.
434434
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
@@ -166,7 +166,7 @@ baseProps = OptProperties
166166
, propShowDefault = Nothing
167167
, propDescMod = Nothing
168168
, propShowGlobal = True
169-
, propGroup = OptGroup []
169+
, propGroup = OptGroup 0 Nothing
170170
}
171171

172172
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
@@ -201,29 +201,93 @@ optionsDesc global pprefs p = vsepChunks
201201
. fmap formatTitle
202202
. fmap tabulateGroup
203203
. groupByTitle
204-
$ mapParser doc p
204+
$ docs
205205
where
206+
docs :: [Maybe (OptGroup, (Doc, Doc))]
207+
docs = mapParser doc p
208+
206209
groupByTitle :: [Maybe (OptGroup, (Doc, Doc))] -> [[(OptGroup, (Doc, Doc))]]
207-
groupByTitle = groupFstAll . catMaybes
210+
groupByTitle xs = groupFstAll . catMaybes $ xs
211+
212+
-- NOTE: [Nested group alignment]
213+
--
214+
-- For nested groups, we want to produce output like:
215+
--
216+
-- Group 1
217+
-- --opt-1 INT Option 1
218+
--
219+
-- - Group 2
220+
-- --opt-2 INT Option 2
221+
--
222+
-- - Group 3
223+
-- - opt-3 INT Option 3
224+
--
225+
-- That is, we have the following constraints:
226+
--
227+
-- 1. Nested groups are prefixed with a hyphen '- ', where the hyphen
228+
-- starts on the same column as the parent group.
229+
--
230+
-- 2. We still want the listed options to be indented twice under the
231+
-- group name, so this means nested options need to be indented
232+
-- again by the standard amount (2), due to the hyphen.
233+
--
234+
-- 3. Help text should be __globally__ aligned.
208235

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

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

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

228292
doc :: ArgumentReachability -> Option a -> Maybe (OptGroup, (Doc, Doc))
229293
doc info opt = do
@@ -241,6 +305,9 @@ optionsDesc global pprefs p = vsepChunks
241305
descGlobal = global
242306
}
243307

308+
lvlIndent :: Int
309+
lvlIndent = 2
310+
244311
errorHelp :: Chunk Doc -> ParserHelp
245312
errorHelp chunk = mempty { helpError = chunk }
246313

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(..),
@@ -158,15 +159,16 @@ data OptVisibility
158159
-- | Groups for optionals. Can be multiple in the case of nested groups.
159160
--
160161
-- @since 0.19.0.0
161-
newtype OptGroup = OptGroup [String]
162+
data OptGroup = OptGroup !Int (Maybe String)
162163
deriving (Eq, Ord, Show)
163164

164-
instance Semigroup OptGroup where
165-
OptGroup xs <> OptGroup ys = OptGroup (xs ++ ys)
166-
167-
instance Monoid OptGroup where
168-
mempty = OptGroup []
169-
mappend = (<>)
165+
-- | If the group name is not already set, sets the group name to the
166+
-- parameter and leaves the index as-is. If, on the other hand, the group
167+
-- name already exists, we ignore the parameter and increment the index
168+
-- by one.
169+
updateGroupName :: String -> OptGroup -> OptGroup
170+
updateGroupName newName (OptGroup i Nothing) = OptGroup i (Just newName)
171+
updateGroupName _ (OptGroup i (Just oldName)) = OptGroup (i + 1) (Just oldName)
170172

171173
-- | Specification for an individual parser option.
172174
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)