-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathcompletion_level.dart
More file actions
139 lines (122 loc) · 4.75 KB
/
completion_level.dart
File metadata and controls
139 lines (122 loc) · 4.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:cli_completion/src/parser/arg_parser_extension.dart';
import 'package:meta/meta.dart';
/// {@template completion_level}
/// The necessary information to produce completion for [CommandRunner]-based
/// cli applications.
/// {@endtemplate}
///
/// The [grammar], [visibleSubcommands] and [visibleOptions] should be derived
/// from a [CommandRunner] and a [Command].
///
/// See also [find] to learn how it is created.
@immutable
class CompletionLevel {
/// {@macro completion_level}
@visibleForTesting
const CompletionLevel({
required this.grammar,
required this.rawArgs,
required this.visibleSubcommands,
required this.visibleOptions,
this.parsedOptions,
});
/// Given a user input [rootArgs] and the [runnerGrammar], it finds the
/// innermost context that needs completion.
///
/// If the user input did not type any sub command, the runner itself
/// will be taken as the completion context.
///
/// Example:
/// ```bash
/// root_command -f command1 command2 -o
/// ```
/// Consider `root_command` the cli executable and command1` a sub command
/// of `root_command` and `command2` a sub command of `command1`.
///
/// In a scenario where the user requests completion for this line, all
/// possible suggestions (options, flags and sub commands) should be declared
/// under the [ArgParser] object belonging to `command2`, all the args
/// preceding `command2` are **not** considered for completion.
///
/// if the user input does not respect the known structure of commands, or if
/// there is any error when parsing the command structure, the
/// [CompletionLevel] will be `null`.
static CompletionLevel? find(
Iterable<String> rootArgs,
ArgParser runnerGrammar,
Map<String, Command<dynamic>> runnerCommands,
) {
// Parse args regarding only commands
final commandsOnlyResults = runnerGrammar.tryParseCommandsOnly(rootArgs);
// If it cannot parse commands, bail out.
if (commandsOnlyResults == null) {
return null;
}
// Find the leaf-most parsed command, starting from the root level
// The user-declared argParser in the current command, starting as the one
// on the runner and substituted by the ones belonging to the
// parsed subcommands, if any.
var originalGrammar = runnerGrammar;
// The available sub commands of the current level, starting as the
// commands declared on the runner and substituted by the
// parsed subcommands, if any.
Map<String, Command<dynamic>>? subcommands = runnerCommands;
var nextLevelResults = commandsOnlyResults.command;
String? commandName;
while (nextLevelResults != null) {
originalGrammar = originalGrammar.commands[nextLevelResults.name]!;
// This can be null if ArgParser.addSubcommand was used directly instead
// of CommandRunner.addCommand or Command.addSubcommand
// In these cases,
subcommands = subcommands?[nextLevelResults.name]?.subcommands;
commandName = nextLevelResults.name;
nextLevelResults = nextLevelResults.command;
}
// rawArgs should be only the args after the last parsed command
final List<String> rawArgs;
if (commandName != null) {
rawArgs = rootArgs
.skipWhile((value) => value != commandName)
.skip(1)
.toList();
} else {
rawArgs = rootArgs.toList();
}
final validOptionsResult = originalGrammar.findValidOptions(rawArgs);
final visibleSubcommands =
subcommands?.values.where((command) {
return !command.hidden;
}).toList() ??
[];
final visibleOptions = originalGrammar.options.values.where((option) {
final wasParsed = validOptionsResult?.wasParsed(option.name) ?? false;
if (wasParsed) {
return option.isMultiple;
}
return !option.hide;
}).toList();
return CompletionLevel(
grammar: originalGrammar,
parsedOptions: validOptionsResult,
rawArgs: rawArgs,
visibleSubcommands: visibleSubcommands,
visibleOptions: visibleOptions,
);
}
/// The [ArgParser] declared in the [CommandRunner] or [Command] that
/// needs completion.
final ArgParser grammar;
/// An [ArgResults] that includes the valid options passed to the command on
/// completion level. Null if no valid options were passed.
final ArgResults? parsedOptions;
/// The user input that needs completion starting from the
/// command/sub_command being completed.
final List<String> rawArgs;
/// The visible commands declared by [grammar] in the form of [Command]
/// instances.
final List<Command<dynamic>> visibleSubcommands;
/// The visible options declared by [grammar].
final List<Option> visibleOptions;
}