Skip to content

Commit dbe64d8

Browse files
committed
feat: suggest negatable flags (#45)
1 parent ef3695d commit dbe64d8

File tree

4 files changed

+127
-4
lines changed

4 files changed

+127
-4
lines changed

example/lib/src/commands/some_commmand.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ class SomeCommand extends Command<int> {
3333
'continuous', // intentionally, this one does not have an abbr
3434
help: 'A continuous option: any value is allowed',
3535
)
36+
..addOption(
37+
'no-option',
38+
help: 'An option that starts with "no" just to make confusion '
39+
'with negated flags',
40+
)
3641
..addMultiOption(
3742
'multi-d',
3843
abbr: 'm',
@@ -56,9 +61,15 @@ class SomeCommand extends Command<int> {
5661
abbr: 'n',
5762
help: 'An continuous option that can be passed multiple times',
5863
)
64+
..addFlag(
65+
'hiddenflag',
66+
hide: true,
67+
help: 'A hidden flag',
68+
)
5969
..addFlag(
6070
'flag',
6171
abbr: 'f',
72+
aliases: ['itIsAFlag'],
6273
)
6374
..addFlag(
6475
'inverseflag',

example/test/integration/completion_integration_test.dart

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ void main() {
1515
'some_other_command': 'This is help for some_other_command',
1616
'--help': 'Print this usage information.',
1717
'--rootFlag': r'A flag\: in the root command',
18+
'--no-rootFlag': r'A flag\: in the root command',
1819
'--rootOption': null,
1920
};
2021

@@ -55,6 +56,7 @@ void main() {
5556
suggests({
5657
'--help': 'Print this usage information.',
5758
'--rootFlag': r'A flag\: in the root command',
59+
'--no-rootFlag': r'A flag\: in the root command',
5860
'--rootOption': null,
5961
}),
6062
);
@@ -190,10 +192,15 @@ void main() {
190192
'--help': 'Print this usage information.',
191193
'--discrete': 'A discrete option with "allowed" values (mandatory)',
192194
'--continuous': r'A continuous option\: any value is allowed',
195+
'--no-option':
196+
'An option that starts with "no" just to make confusion with negated '
197+
'flags',
193198
'--multi-d': 'An discrete option that can be passed multiple times ',
194199
'--multi-c': 'An continuous option that can be passed multiple times',
195200
'--flag': null,
201+
'--no-flag': null,
196202
'--inverseflag': 'A flag that the default value is true',
203+
'--no-inverseflag': 'A flag that the default value is true',
197204
'--trueflag': 'A flag that cannot be negated'
198205
};
199206

@@ -298,6 +305,29 @@ void main() {
298305
);
299306
});
300307

308+
test('suggests negated flags', () async {
309+
await expectLater(
310+
'example_cli some_command --n',
311+
suggests({
312+
'--no-option':
313+
'An option that starts with "no" just to make confusion with '
314+
'negated flags',
315+
'--no-flag': null,
316+
'--no-inverseflag': 'A flag that the default value is true'
317+
}),
318+
);
319+
});
320+
321+
test('suggests negated flags (aliases)', () async {
322+
await expectLater(
323+
'example_cli some_command --no-i',
324+
suggests({
325+
'--no-itIsAFlag': null,
326+
'--no-inverseflag': 'A flag that the default value is true'
327+
}),
328+
);
329+
});
330+
301331
test('suggests only one matching option', () async {
302332
await expectLater(
303333
'example_cli some_command --d',
@@ -544,21 +574,27 @@ void main() {
544574
test('do not include flag after it is specified', () async {
545575
await expectLater(
546576
'example_cli some_command --flag ',
547-
suggests(allOptionsInThisLevel.except('--flag')),
577+
suggests(
578+
allOptionsInThisLevel.except('--flag').except('--no-flag'),
579+
),
548580
);
549581
});
550582

551583
test('do not include flag after it is specified (abbr)', () async {
552584
await expectLater(
553585
'example_cli some_command -f ',
554-
suggests(allOptionsInThisLevel.except('--flag')),
586+
suggests(
587+
allOptionsInThisLevel.except('--flag').except('--no-flag'),
588+
),
555589
);
556590
});
557591

558592
test('do not include negated flag after it is specified', () async {
559593
await expectLater(
560594
'example_cli some_command --no-flag ',
561-
suggests(allOptionsInThisLevel.except('--flag')),
595+
suggests(
596+
allOptionsInThisLevel.except('--flag').except('--no-flag'),
597+
),
562598
);
563599
});
564600

@@ -640,6 +676,7 @@ void main() {
640676
final allOptionsInThisLevel = <String, String?>{
641677
'--help': 'Print this usage information.',
642678
'--flag': 'a flag just to show we are in the subcommand',
679+
'--no-flag': 'a flag just to show we are in the subcommand',
643680
};
644681
group('empty', () {
645682
test('basic usage', () async {

lib/src/parser/completion_result.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ class AllOptionsAndCommandsCompletionResult extends CompletionResult {
4848
}
4949
for (final option in completionLevel.visibleOptions) {
5050
mapCompletions['--${option.name}'] = option.help;
51+
if (option.negatable ?? false) {
52+
mapCompletions['--no-${option.name}'] = option.help;
53+
}
5154
}
5255
return mapCompletions;
5356
}
@@ -146,6 +149,19 @@ class MatchingOptionsCompletionResult extends CompletionResult {
146149
Map<String, String?> get completions {
147150
final mapCompletions = <String, String?>{};
148151
for (final option in completionLevel.visibleOptions) {
152+
final isNegatable = option.negatable ?? false;
153+
if (isNegatable) {
154+
if (option.negatedName.startsWith(pattern)) {
155+
mapCompletions['--${option.negatedName}'] = option.help;
156+
} else {
157+
for (final negatedAlias in option.negatedAliases) {
158+
if (negatedAlias.startsWith(pattern)) {
159+
mapCompletions['--$negatedAlias'] = option.help;
160+
}
161+
}
162+
}
163+
}
164+
149165
if (option.name.startsWith(pattern)) {
150166
mapCompletions['--${option.name}'] = option.help;
151167
} else {
@@ -236,3 +252,9 @@ class OptionValuesCompletionResult extends CompletionResult {
236252
};
237253
}
238254
}
255+
256+
extension on Option {
257+
String get negatedName => 'no-$name';
258+
259+
Iterable<String> get negatedAliases => aliases.map((e) => 'no-$e');
260+
}

test/src/parser/completion_result_test.dart

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ void main() {
2828
() {
2929
final testArgParser = ArgParser()
3030
..addOption('option1')
31-
..addFlag('option2', help: 'yay option 2');
31+
..addFlag('option2', help: 'yay option 2')
32+
..addFlag(
33+
'trueflag',
34+
negatable: false,
35+
);
3236

3337
final completionLevel = CompletionLevel(
3438
grammar: testArgParser,
@@ -59,6 +63,8 @@ void main() {
5963
'command2': 'yay command 2',
6064
'--option1': null,
6165
'--option2': 'yay option 2',
66+
'--no-option2': 'yay option 2',
67+
'--trueflag': null,
6268
},
6369
);
6470
},
@@ -175,6 +181,53 @@ void main() {
175181
);
176182
},
177183
);
184+
185+
test(
186+
'renders suggestions only for negated flags',
187+
() {
188+
final testArgParser = ArgParser()
189+
..addOption('option1')
190+
..addOption(
191+
'noption2',
192+
aliases: ['option2alias'],
193+
help: 'yay noption2',
194+
)
195+
..addFlag('flag', aliases: ['aliasforflag'])
196+
..addFlag('trueflag', negatable: false);
197+
198+
final completionLevel = CompletionLevel(
199+
grammar: testArgParser,
200+
rawArgs: const <String>[],
201+
visibleSubcommands: const [],
202+
visibleOptions: testArgParser.options.values.toList(),
203+
);
204+
205+
final completionResult = MatchingOptionsCompletionResult(
206+
completionLevel: completionLevel,
207+
pattern: 'no',
208+
);
209+
210+
expect(
211+
completionResult.completions,
212+
{
213+
'--noption2': 'yay noption2',
214+
'--no-flag': null,
215+
},
216+
);
217+
218+
final completionResultAlias = MatchingOptionsCompletionResult(
219+
completionLevel: completionLevel,
220+
pattern: 'no-a',
221+
);
222+
223+
expect(
224+
completionResultAlias.completions,
225+
{
226+
'--no-aliasforflag': null,
227+
},
228+
);
229+
},
230+
);
178231
});
179232

180233
group('OptionValuesCompletionResult', () {

0 commit comments

Comments
 (0)