Skip to content

Commit cf832c9

Browse files
committed
Add support to define completions in the definition
1 parent 791daf6 commit cf832c9

4 files changed

Lines changed: 151 additions & 2 deletions

File tree

src/Command.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Circli\Console;
44

5+
use Symfony\Component\Console\Completion\CompletionInput;
6+
use Symfony\Component\Console\Completion\CompletionSuggestions;
57
use Symfony\Component\Console\Input\InputInterface;
68
use Symfony\Component\Console\Output\OutputInterface;
79

@@ -30,6 +32,23 @@ public function __construct(
3032
$this->definition = $definition;
3133
}
3234

35+
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
36+
{
37+
$completions = $this->definition->getCompletions();
38+
$fallback = fn () => parent::complete($input, $suggestions);
39+
if (isset($completions[$input->getCompletionName()])) {
40+
$completionCallable = $this->resolver->createCommand($completions[$input->getCompletionName()]);
41+
$completionCallable($input, $suggestions, $fallback);
42+
}
43+
elseif (isset($completions['__ROOT'])) {
44+
$completionCallable = $this->resolver->createCommand($completions['__ROOT']);
45+
$completionCallable($input, $suggestions, $fallback);
46+
}
47+
else {
48+
parent::complete($input, $suggestions);
49+
}
50+
}
51+
3352
protected function execute(InputInterface $input, OutputInterface $output): int
3453
{
3554
$orgInput = $input;

src/Definition.php

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
namespace Circli\Console;
44

5+
use Symfony\Component\Console\Completion\CompletionInput;
6+
use Symfony\Component\Console\Completion\CompletionSuggestions;
7+
use Symfony\Component\Console\Completion\Suggestion;
58
use Symfony\Component\Console\Exception\InvalidArgumentException;
69
use Symfony\Component\Console\Input\InputArgument;
710
use Symfony\Component\Console\Input\InputDefinition;
@@ -20,6 +23,8 @@ abstract class Definition
2023
private array $usages = [];
2124
/** @var string|callable|null */
2225
private $command;
26+
/** @var array<string|callable> */
27+
private array $completions = [];
2328

2429
public function __construct()
2530
{
@@ -32,6 +37,7 @@ public function __construct()
3237
*
3338
* @param int|null $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
3439
* @param string|bool|int|float|array<mixed>|null $default The default value (for InputArgument::OPTIONAL mode only)
40+
* @param list<string>|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
3541
*
3642
* @return $this
3743
* @throws InvalidArgumentException When argument mode is not valid
@@ -41,8 +47,9 @@ public function addArgument(
4147
int $mode = null,
4248
string $description = '',
4349
string|bool|int|float|array $default = null,
50+
\Closure|array $suggestedValues = [],
4451
): static {
45-
$this->definition->addArgument(new InputArgument($name, $mode, $description, $default));
52+
$this->definition->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));
4653

4754
return $this;
4855
}
@@ -53,6 +60,7 @@ public function addArgument(
5360
* @param string|list<string>|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
5461
* @param int|null $mode The option mode: One of the InputOption::VALUE_* constants
5562
* @param bool|int|string|string[]|null $default The default value (must be null for InputOption::VALUE_NONE)
63+
* @param list<string>|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
5664
*
5765
* @throws InvalidArgumentException If option mode is invalid or incompatible
5866
*/
@@ -62,8 +70,9 @@ public function addOption(
6270
int $mode = null,
6371
string $description = '',
6472
array|bool|int|string $default = null,
73+
\Closure|array $suggestedValues = [],
6574
): static {
66-
$this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default));
75+
$this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
6776

6877
return $this;
6978
}
@@ -164,6 +173,35 @@ public function setCommand(callable|object|string $command): static
164173
return $this;
165174
}
166175

176+
/**
177+
* @param callable(CompletionInput $input, CompletionSuggestions $suggestions, callable $default): void|string $completion
178+
* @return $this
179+
*/
180+
public function setCompletion(callable|string $completion): static
181+
{
182+
$this->completions['__ROOT'] = $completion;
183+
184+
return $this;
185+
}
186+
187+
/**
188+
* @return array<string, callable|string>
189+
*/
190+
public function getCompletions(): array
191+
{
192+
return $this->completions;
193+
}
194+
195+
/**
196+
* @param callable(CompletionInput $input, CompletionSuggestions $suggestions, callable $default): void|string $completion
197+
* @return $this
198+
*/
199+
public function addCompletion(string $argumentOrOption, callable|string $completion): static
200+
{
201+
$this->completions[$argumentOrOption] = $completion;
202+
return $this;
203+
}
204+
167205
public function getDefinition(): InputDefinition
168206
{
169207
return $this->definition;

tests/CommandTest.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
use Circli\Console\Tests\Fixtures\CommandDefinition;
88
use Circli\Console\Tests\Fixtures\COMMANDWithoutNameDefinition;
99
use Circli\Console\Tests\Fixtures\CustomInput;
10+
use Circli\Console\Tests\Fixtures\DefinitionWithCompletion;
1011
use Circli\Console\Tests\Fixtures\InvalidDefinition;
1112
use PHPUnit\Framework\TestCase;
13+
use Symfony\Component\Console\Completion\CompletionInput;
14+
use Symfony\Component\Console\Completion\CompletionSuggestions;
1215
use Symfony\Component\Console\Input\ArrayInput;
1316
use Symfony\Component\Console\Output\NullOutput;
1417

@@ -56,4 +59,56 @@ public function testAutoCreateName(): void
5659

5760
$this->assertSame('command-without-name', $command->getName());
5861
}
62+
63+
public function testCompletionFirstArgument()
64+
{
65+
$definition = new DefinitionWithCompletion();
66+
$command = new Command($definition, new SimpleCommandResolver());
67+
68+
$input = CompletionInput::fromTokens([
69+
'cmd',
70+
'1',
71+
], 1);
72+
$suggestions = new CompletionSuggestions();
73+
74+
$input->bind($command->getDefinition());
75+
76+
$command->complete($input, $suggestions);
77+
$this->assertCount(2, $suggestions->getValueSuggestions());
78+
}
79+
80+
public function testCompletionSecondArgument()
81+
{
82+
$definition = new DefinitionWithCompletion();
83+
$command = new Command($definition, new SimpleCommandResolver());
84+
85+
$input = CompletionInput::fromTokens([
86+
'cmd',
87+
'1',
88+
'f'
89+
], 1);
90+
$suggestions = new CompletionSuggestions();
91+
92+
$input->bind($command->getDefinition());
93+
94+
$command->complete($input, $suggestions);
95+
$this->assertCount(3, $suggestions->getValueSuggestions());
96+
}
97+
98+
public function testCompletionOption()
99+
{
100+
$definition = new DefinitionWithCompletion();
101+
$command = new Command($definition, new SimpleCommandResolver());
102+
103+
$input = CompletionInput::fromTokens([
104+
'cmd',
105+
'--user',
106+
], 1);
107+
$suggestions = new CompletionSuggestions();
108+
109+
$input->bind($command->getDefinition());
110+
111+
$command->complete($input, $suggestions);
112+
$this->assertCount(4, $suggestions->getValueSuggestions());
113+
}
59114
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Circli\Console\Tests\Fixtures;
4+
5+
use Circli\Console\Definition;
6+
use Symfony\Component\Console\Completion\CompletionInput;
7+
use Symfony\Component\Console\Completion\CompletionSuggestions;
8+
use Symfony\Component\Console\Input\InputArgument;
9+
use Symfony\Component\Console\Input\InputOption;
10+
11+
final class DefinitionWithCompletion extends Definition
12+
{
13+
protected function configure(): void
14+
{
15+
$this->setDescription('test description');
16+
$this->addArgument(
17+
'test',
18+
InputArgument::REQUIRED,
19+
suggestedValues: ['1', '2'],
20+
);
21+
$this->addArgument('file');
22+
$this->addOption(
23+
'user',
24+
mode: InputOption::VALUE_REQUIRED,
25+
suggestedValues: ['user1', 'user21', 'random1', 'ext2']
26+
);
27+
28+
$this->addCompletion('file', function (CompletionInput $input, CompletionSuggestions $suggestions, callable $default) {
29+
$suggestions->suggestValues(['file1', 'file2', 'dir1']);
30+
});
31+
32+
$this->setCommand(static function() {
33+
echo 'default definition';
34+
return 0;
35+
});
36+
}
37+
}

0 commit comments

Comments
 (0)