Skip to content

Commit eda37c6

Browse files
committed
feat(cli_tools): Support the carapace CLI completions tool
1 parent 8d78a85 commit eda37c6

File tree

3 files changed

+273
-12
lines changed

3 files changed

+273
-12
lines changed

packages/cli_tools/README_completion.md

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,78 @@
33
There is now an experimental feature to generate and install command line
44
completion in bash for commands using `BetterCommandRunner`.
55

6+
### Enable experimental feature
7+
8+
Enable this experimental feature by constructing `BetterCommandRunner`
9+
with the flag `experimentalCompletionCommand` set to `true`.
10+
11+
## Using the tool `carapace`
12+
13+
Carapace supports a lot of shells, including Bash, ZSH, Fish, Elvish,
14+
Powershell, Cmd, and more.
15+
16+
https://carapace.sh/
17+
618
### Prerequisites
719

8-
This requires the tool `completely`, which also requires ruby to be installed.
20+
This requires the tool `carapace`.
21+
22+
```sh
23+
brew install carapace
24+
```
25+
For installing in other environments, see:
26+
https://carapace-sh.github.io/carapace-bin/install.html
27+
28+
### Install completion for the command
29+
30+
When the `carapace` tool is installed, generate the completion for the command.
31+
32+
Note that the YAML file must have the same name as the command executable
33+
before the `.yaml` extension.
34+
35+
```sh
36+
my-command completion -f my-command.yaml -t carapace
37+
cp example.yaml "${UserConfigDir}/carapace/specs/"
38+
```
39+
40+
> Note: ${UserConfigDir} refers to the platform-specific user configuration
41+
directory. On MacOS this is `~/Library/Application Support/` (even though many
42+
other Bash commands use `~/.local/share/`), and on Windows this is `%APPDATA%`.
43+
This can be overridden with the env var `XDG_CONFIG_HOME`, but be aware this
44+
affects lots of applications.
45+
46+
Run the following once for the current shell,
47+
or add to your shell startup script:
48+
49+
Bash:
50+
```bash
51+
source <(carapace my-command)
52+
```
53+
54+
Zsh:
55+
```zsh
56+
zstyle ':completion:*' format $'\e[2;37mCompleting %d\e[m'
57+
source <(carapace my-command)
58+
```
59+
60+
For more information and installing in other shells, see:
61+
https://carapace-sh.github.io/carapace-bin/setup.html
62+
63+
64+
### Distribution
65+
66+
End users will need to install `carapace` and copy the Yaml file to the proper
67+
location, even if the Yaml file is distributed with the command.
68+
69+
70+
## Using the tool `completely`
971

1072
https://github.com/bashly-framework/completely
1173

74+
### Prerequisites
75+
76+
This requires the tool `completely`, which also requires ruby to be installed.
77+
1278
```sh
1379
gem install completely
1480
```
@@ -20,20 +86,15 @@ brew install brew-gem
2086
brew gem install completely
2187
```
2288

23-
### Activate
24-
25-
Construct `BetterCommandRunner` with the flag `experimentalCompletionCommand`
26-
set to `true`.
27-
28-
### Install completion
89+
### Install completion for the command
2990

3091
When the `completely` tool is installed, run e.g:
3192

3293
```sh
33-
my-command completion -f completely.yaml
34-
completely generate completely.yaml completely.bash
94+
my-command completion -f my-command.yaml -t completely
95+
completely generate my-command.yaml my-command.bash
3596
mkdir -p ~/.local/share/bash-completion/completions
36-
cp completely.bash ~/.local/share/bash-completion/completions/my-command.bash
97+
cp my-command.bash ~/.local/share/bash-completion/completions/
3798
```
3899

39100
This will write the completions script to `~/.local/share/bash-completion/completions/`,
@@ -45,5 +106,7 @@ In order to update the completions in the current bash shell, run:
45106
exec bash
46107
```
47108

109+
### Distribution
110+
48111
For end users, the generated bash script can be distributed as a file for them
49-
to install directly.
112+
to install directly in their `~/.local/share/bash-completion/completions/`.
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import 'dart:io' show IOSink;
2+
3+
import 'package:config/config.dart';
4+
5+
import 'usage_representation.dart';
6+
7+
/// Generates usage representation for a command in the YAML format
8+
/// of the `carapace` tool.
9+
/// https://github.com/carapace-sh/carapace
10+
class CarapaceYamlGenerator implements UsageRepresentationGenerator {
11+
@override
12+
void generate(
13+
final IOSink out,
14+
final CommandUsage usage,
15+
) {
16+
out.writeln(
17+
r'# yaml-language-server: $schema=https://carapace.sh/schemas/command.json');
18+
19+
_generateForCommand(out, usage, 0);
20+
}
21+
22+
void _generateForCommand(
23+
final IOSink out,
24+
final CommandUsage usage,
25+
int indentLevel,
26+
) {
27+
if (indentLevel == 0) {
28+
_writeWithIndent(out, 'name: ${usage.commandSequence.last}', indentLevel);
29+
} else {
30+
_writeWithIndent(
31+
out, '- name: ${usage.commandSequence.last}', indentLevel);
32+
indentLevel += 1;
33+
}
34+
35+
if (usage.persistentOptions.isNotEmpty) {
36+
_writeWithIndent(out, 'persistentFlags:', indentLevel);
37+
_declareOptions(out, usage.persistentOptions, indentLevel + 1);
38+
}
39+
40+
if (usage.options.isNotEmpty) {
41+
_writeWithIndent(out, 'flags:', indentLevel);
42+
_declareOptions(out, usage.options, indentLevel + 1);
43+
}
44+
45+
final allOptions = [...usage.persistentOptions, ...usage.options];
46+
47+
final exclusiveGroups = _generateExclusiveOptionGroups(allOptions);
48+
if (exclusiveGroups.isNotEmpty) {
49+
_writeWithIndent(out, 'exclusiveFlags:', indentLevel);
50+
for (final group in exclusiveGroups) {
51+
_writeWithIndent(out, '- [${group.join(', ')}]', indentLevel + 1);
52+
}
53+
}
54+
55+
final specs = _generateOptionCompletionSpecs(allOptions);
56+
if (specs.isNotEmpty) {
57+
_writeWithIndent(out, 'completion:', indentLevel);
58+
_writeWithIndent(out, 'flag:', indentLevel + 1);
59+
for (final spec in specs) {
60+
_writeWithIndent(out, spec, indentLevel + 2);
61+
}
62+
}
63+
64+
out.writeln();
65+
66+
if (usage.subcommands.isNotEmpty) {
67+
_writeWithIndent(out, 'commands:', indentLevel);
68+
for (final subcommand in usage.subcommands) {
69+
_generateForCommand(out, subcommand, indentLevel + 1);
70+
}
71+
}
72+
}
73+
74+
static void _writeWithIndent(
75+
final IOSink out,
76+
final String text,
77+
final int indentLevel,
78+
) {
79+
out.writeln('${_getIndent(indentLevel)}$text');
80+
}
81+
82+
static String _getIndent(final int indentLevel) => ' ' * indentLevel;
83+
84+
static void _declareOptions(
85+
final IOSink out,
86+
final List<OptionDefinition> options,
87+
final int indentLevel,
88+
) {
89+
// options
90+
for (final option in options) {
91+
_declareOption(out, option.option, indentLevel);
92+
}
93+
}
94+
95+
static void _declareOption(
96+
final IOSink out,
97+
final ConfigOptionBase option,
98+
final int indentLevel,
99+
) {
100+
final names = [
101+
if (option.argAbbrev != null) '-${option.argAbbrev}',
102+
if (option.argName != null) '--${option.argName}',
103+
];
104+
105+
String attributes = '';
106+
if (option is! FlagOption) {
107+
attributes += '=';
108+
}
109+
if (option is MultiOption) {
110+
attributes += '*';
111+
}
112+
if (option.mandatory && option is! FlagOption) {
113+
attributes += '!';
114+
}
115+
116+
_writeWithIndent(
117+
out,
118+
'${names.join(', ')}$attributes: ${option.helpText ?? ''}',
119+
indentLevel,
120+
);
121+
122+
if (option case final FlagOption flagOption) {
123+
if (flagOption.negatable && !flagOption.hideNegatedUsage) {
124+
_writeWithIndent(
125+
out,
126+
'--no-${option.argName}$attributes: ${option.helpText ?? ''}',
127+
indentLevel,
128+
);
129+
}
130+
}
131+
}
132+
133+
static List<List<String>> _generateExclusiveOptionGroups(
134+
final List<OptionDefinition> options,
135+
) {
136+
final groups = <List<String>>[];
137+
for (final option in options) {
138+
if (option.option case final FlagOption flagOption) {
139+
final argName = flagOption.argName;
140+
if (argName != null &&
141+
flagOption.negatable &&
142+
!flagOption.hideNegatedUsage) {
143+
groups.add([argName, 'no-$argName']);
144+
}
145+
}
146+
}
147+
return groups;
148+
}
149+
150+
static List<String> _generateOptionCompletionSpecs(
151+
final List<OptionDefinition> options,
152+
) {
153+
final specs = <String>[];
154+
for (final option in options) {
155+
final name = option.option.argName ?? option.option.argAbbrev;
156+
if (name == null) {
157+
continue;
158+
}
159+
160+
final values = _getOptionValues(option.option);
161+
if (values.isNotEmpty) {
162+
final valueSpec = values.map((final v) => '"$v"').join(', ');
163+
specs.add('$name: [$valueSpec]');
164+
}
165+
}
166+
return specs;
167+
}
168+
169+
static List<String> _getOptionValues(
170+
final ConfigOptionBase option,
171+
) {
172+
if (option case final MultiOption multiOption) {
173+
if (multiOption.allowedElementValues case final List allowedValues) {
174+
return allowedValues
175+
.map<String>(multiOption.multiParser.elementParser.format)
176+
.toList();
177+
}
178+
} else if (option.allowedValues case final List allowedValues) {
179+
return allowedValues.map(option.valueParser.format).toList();
180+
}
181+
182+
switch (option.option) {
183+
case EnumOption():
184+
final enumParser = option.option.valueParser as EnumParser;
185+
return enumParser.enumValues.map(enumParser.format).toList();
186+
case FileOption():
187+
return [r'$files'];
188+
case DirOption():
189+
return [r'$directories'];
190+
default:
191+
return [];
192+
}
193+
}
194+
}

packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,20 @@ import 'dart:io' show IOSink, stdout;
33
import 'package:config/config.dart';
44

55
import '../better_command.dart';
6+
import 'carapace_completion.dart';
67
import 'completely_generator.dart';
78
import 'usage_representation.dart';
89

910
enum CompletionTarget {
1011
completely,
12+
carapace,
1113
}
1214

1315
enum CompletionOption<V extends Object> implements OptionDefinition<V> {
1416
target(EnumOption(
1517
enumParser: EnumParser(CompletionTarget.values),
1618
argName: 'target',
1719
argAbbrev: 't',
18-
defaultsTo: CompletionTarget.completely,
1920
)),
2021
execName(StringOption(
2122
argName: 'exec-name',
@@ -66,6 +67,9 @@ class CompletionCommand<T> extends BetterCommand<CompletionOption, T> {
6667
case CompletionTarget.completely:
6768
CompletelyYamlGenerator().generate(out, usage);
6869
break;
70+
case CompletionTarget.carapace:
71+
CarapaceYamlGenerator().generate(out, usage);
72+
break;
6973
}
7074

7175
if (file != null) {

0 commit comments

Comments
 (0)