Skip to content

Commit

Permalink
feat: suggest sub commands (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
renancaraujo authored Nov 25, 2022
1 parent ca273fc commit ea3f5db
Show file tree
Hide file tree
Showing 19 changed files with 927 additions and 320 deletions.
11 changes: 7 additions & 4 deletions example/lib/src/command_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ class ExampleCommandRunner extends CompletionCommandRunner<int> {
}) : _logger = logger ?? Logger(),
super(executableName, description) {
// Add root options and flags
argParser.addFlag(
'rootFlag',
help: 'A flag in the root command',
);
argParser
..addFlag(
'rootFlag',
abbr: 'f',
help: 'A flag: in the root command',
)
..addOption('rootOption');

// Add sub commands
addCommand(SomeCommand(_logger));
Expand Down
1 change: 1 addition & 0 deletions example/lib/src/commands/some_commmand.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class SomeCommand extends Command<int> {
},
mandatory: true,
)
..addSeparator('yay')
..addOption(
'hidden',
hide: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,25 @@ void main() {
'some_other_command': 'This is help for some_other_command',
'--help': 'Print this usage information.',
'--rootFlag': r'A flag\: in the root command',
'--rootOption': null,
};

testCompletion(
'basic usage',
forLine: 'example_cli',
suggests: allRootOptionsAndSubcommands,
skip: notImplemmentedYet,
);

testCompletion(
'leading whitespaces',
forLine: ' example_cli',
suggests: allRootOptionsAndSubcommands,
skip: notImplemmentedYet,
);

testCompletion(
'trailing whitespaces',
forLine: 'example_cli ',
suggests: allRootOptionsAndSubcommands,
skip: notImplemmentedYet,
);
});

Expand All @@ -49,6 +47,7 @@ void main() {
suggests: {
'--help': 'Print this usage information.',
'--rootFlag': r'A flag\: in the root command',
'--rootOption': null,
},
skip: notImplemmentedYet,
);
Expand All @@ -58,6 +57,7 @@ void main() {
forLine: 'example_cli --r',
suggests: {
'--rootFlag': r'A flag\: in the root command',
'--rootOption': null,
},
skip: notImplemmentedYet,
);
Expand All @@ -67,6 +67,7 @@ void main() {
forLine: 'example_cli -h --r',
suggests: {
'--rootFlag': r'A flag\: in the root command',
'--rootOption': null,
},
skip: notImplemmentedYet,
);
Expand Down Expand Up @@ -108,7 +109,6 @@ void main() {
'some_command': 'This is help for some_command',
'some_other_command': 'This is help for some_other_command',
},
skip: notImplemmentedYet,
);

testCompletion(
Expand All @@ -118,7 +118,6 @@ void main() {
'some_command': 'This is help for some_command',
'some_other_command': 'This is help for some_other_command',
},
skip: notImplemmentedYet,
);

testCompletion(
Expand All @@ -127,7 +126,6 @@ void main() {
suggests: {
'some_command': 'This is help for some_command',
},
skip: notImplemmentedYet,
);
});

Expand All @@ -138,7 +136,6 @@ void main() {
suggests: {
'melon': 'This is help for some_command',
},
skip: notImplemmentedYet,
);

testCompletion(
Expand All @@ -147,7 +144,6 @@ void main() {
suggests: {
r'disguised\:some_commmand': 'This is help for some_command',
},
skip: notImplemmentedYet,
);
});

Expand All @@ -172,55 +168,50 @@ void main() {
'--continuous': r'A continuous option\: any value is allowed',
'--multi-d': 'An discrete option that can be passed multiple times ',
'--multi-c': 'An continuous option that can be passed multiple times',
'--flag': '',
'--flag': null,
'--inverseflag': 'A flag that the default value is true',
'--trueflag': 'A flag that cannot be negated'
};

final allAbbreviationssInThisLevel = <String, String>{
final allAbbreviationssInThisLevel = <String, String?>{
'-h': 'Print this usage information.',
'-d': 'A discrete option with "allowed" values (mandatory)',
'-m': 'An discrete option that can be passed multiple times ',
'-n': 'An continuous option that can be passed multiple times',
'-f': '',
'-f': null,
'-i': 'A flag that the default value is true',
'-t': 'A flag that cannot be negated'
};

group('empty', () {
group('empty ', () {
testCompletion(
'basic usage',
forLine: 'example_cli some_command',
suggests: allOptionsInThisLevel,
skip: notImplemmentedYet,
);

testCompletion(
'leading spaces',
forLine: ' example_cli some_command',
suggests: allOptionsInThisLevel,
skip: notImplemmentedYet,
);

testCompletion(
'trailing spaces',
forLine: 'example_cli some_command ',
suggests: allOptionsInThisLevel,
skip: notImplemmentedYet,
);

testCompletion(
'options in between',
forLine: 'example_cli -f some_command',
forLine: 'example_cli -f --rootOption yay some_command',
suggests: allOptionsInThisLevel,
skip: notImplemmentedYet,
);

testCompletion(
'lots of spaces in between',
forLine: 'example_cli some_command',
suggests: allOptionsInThisLevel,
skip: notImplemmentedYet,
);
});

Expand All @@ -229,14 +220,12 @@ void main() {
'shows same options for alias sub command',
forLine: 'example_cli melon',
suggests: allOptionsInThisLevel,
skip: notImplemmentedYet,
);

testCompletion(
'shows same options for alias sub command 2',
forLine: 'example_cli disguised:some_commmand',
suggests: allOptionsInThisLevel,
skip: notImplemmentedYet,
);
});

Expand Down Expand Up @@ -470,19 +459,19 @@ void main() {
'subcommand': 'A sub command of some_other_command',
'--help': 'Print this usage information.',
},
skip: notImplemmentedYet,
);
});

group('partially written sub command', () {
testCompletion(
'partially written sub command',
forLine: 'example_cli some_other_command sub',
suggests: {
'subcommand': 'A sub command of some_other_command',
},
skip: notImplemmentedYet,
);
});

group('subcommand', () {
final allOptionsInThisLevel = <String, String?>{
'--help': 'Print this usage information.',
Expand Down Expand Up @@ -516,7 +505,6 @@ void main() {
'basic usage with args in between',
forLine: 'example_cli some_other_command subcommand_alias',
suggests: allOptionsInThisLevel,
skip: notImplemmentedYet,
);
});
});
Expand Down
File renamed without changes.
3 changes: 2 additions & 1 deletion example/test/src/command_runner_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ Usage: example_cli <command> [arguments]
Global options:
-h, --help Print this usage information.
--[no-]rootFlag A flag in the root command
-f, --[no-]rootFlag A flag: in the root command
--rootOption
Available commands:
some_command This is help for some_command
Expand Down
36 changes: 28 additions & 8 deletions lib/src/command_runner/commands/handle_completion_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@ import 'dart:async';

import 'package:args/command_runner.dart';
import 'package:cli_completion/src/command_runner/completion_command_runner.dart';
import 'package:cli_completion/src/handling/completion_level.dart';
import 'package:cli_completion/src/handling/completion_state.dart';
import 'package:cli_completion/src/handling/parser.dart';
import 'package:mason_logger/mason_logger.dart';

/// {@template handle_completion_request_command}
/// A hidden [Command] added by [CompletionCommandRunner] that handles the
/// "completion" sub command.
/// {@endtemplate}
///
/// This is called by a shell function when the user presses "tab".
/// Any output to stdout during this call will be interpreted as suggestions
/// for completions.
/// {@endtemplate}
class HandleCompletionRequestCommand<T> extends Command<T> {
/// {@macro handle_completion_request_command}
HandleCompletionRequestCommand(this.logger);
HandleCompletionRequestCommand();

@override
String get description {
Expand All @@ -31,9 +32,6 @@ class HandleCompletionRequestCommand<T> extends Command<T> {
@override
bool get hidden => true;

/// The [Logger] used to display the completion suggestions
final Logger logger;

@override
CompletionCommandRunner<T> get runner {
return super.runner! as CompletionCommandRunner<T>;
Expand All @@ -42,16 +40,38 @@ class HandleCompletionRequestCommand<T> extends Command<T> {
@override
FutureOr<T>? run() {
try {
// Get completion request params from the environment
final completionState = CompletionState.fromEnvironment(
runner.environmentOverride,
);

// If the parameters in the environment are not supported or invalid,
// do not proceed with completion complete.
if (completionState == null) {
return null;
}

final result = CompletionParser(completionState).parse();
// Find the completion level
final completionLevel = CompletionLevel.find(
completionState.args,
runner.argParser,
runner.commands,
);

// Do not complete if the command structure is not recognized
if (completionLevel == null) {
return null;
}

// Parse the completion level into completion suggestions
final completionResults = CompletionParser(
completionLevel: completionLevel,
).parse();

runner.renderCompletionResult(result);
// Render the completion suggestions
for (final completionResult in completionResults) {
runner.renderCompletionResult(completionResult);
}
} on Exception {
// Do not output any Exception here, since even error messages are
// interpreted as completion suggestions
Expand Down
21 changes: 19 additions & 2 deletions lib/src/command_runner/completion_command_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import 'package:meta/meta.dart';
abstract class CompletionCommandRunner<T> extends CommandRunner<T> {
/// {@macro completion_command_runner}
CompletionCommandRunner(super.executableName, super.description) {
addCommand(HandleCompletionRequestCommand<T>(completionLogger));
addCommand(HandleCompletionRequestCommand<T>());
addCommand(InstallCompletionFilesCommand<T>());
}

Expand Down Expand Up @@ -93,6 +93,23 @@ abstract class CompletionCommandRunner<T> extends CommandRunner<T> {
if (systemShell == null) {
return;
}
completionResult.render(completionLogger, systemShell);

for (final entry in completionResult.completions.entries) {
switch (systemShell) {
case SystemShell.zsh:
// On zsh, colon acts as delimitation between a suggestion and its
// description. Any literal colon should be escaped.
final suggestion = entry.key.replaceAll(':', r'\:');
final description = entry.value?.replaceAll(':', r'\:');

completionLogger.info(
'$suggestion${description != null ? ':$description' : ''}',
);
break;
case SystemShell.bash:
completionLogger.info(entry.key);
break;
}
}
}
}
Loading

0 comments on commit ea3f5db

Please sign in to comment.