Skip to content

Commit b4cebca

Browse files
committed
Extract completion handler input to CompletionContext
The CompletionHandler class was handling both completion and setting up the command/words to complete for. CompletionHandler now takes a CompletionContext in its constructor, which provides the command and words to complete for. Additionally: * Replaced use of the COMP_WORDBREAKS variable with a set of break characters more suited to parsing Symfony console application command-lines. This was needed for the colon character fix from #1 to work properly, as previously ':' was still in COMP_WORDBREAKS so was used to split a command up. * Added unit tests for CompletionContext
1 parent 3f17fdd commit b4cebca

File tree

6 files changed

+355
-144
lines changed

6 files changed

+355
-144
lines changed

src/Stecman/Component/Symfony/Console/BashCompletion/CompletionCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ protected function execute(InputInterface $input, OutputInterface $output)
5353
if ( $input->getOption('genhook') ) {
5454
$output->write( $handler->generateBashCompletionHook($input->getOption('program')), true );
5555
} else {
56-
$handler->configureFromEnvironment();
56+
$handler->setContext(new EnvironmentCompletionContext());
5757
$output->write($this->runCompletion(), true);
5858
}
5959
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
<?php
2+
3+
4+
namespace Stecman\Component\Symfony\Console\BashCompletion;
5+
6+
7+
class CompletionContext
8+
{
9+
/**
10+
* COMP_WORDS
11+
* An array consisting of the individual words in the current command line.
12+
* @var array|null
13+
*/
14+
protected $words = null;
15+
16+
/**
17+
* COMP_CWORD
18+
* The index in COMP_WORDS of the word containing the current cursor position.
19+
* @var int
20+
*/
21+
protected $wordIndex = null;
22+
23+
/**
24+
* COMP_LINE
25+
* The current contents of the command line.
26+
* @var string
27+
*/
28+
protected $commandLine;
29+
30+
/**
31+
* COMP_POINT
32+
* The index of the current cursor position relative to the beginning of the
33+
* current command. If the current cursor position is at the end of the current
34+
* command, the value of this variable is equal to the length of COMP_LINE.
35+
* @var int
36+
*/
37+
protected $charIndex = 0;
38+
39+
/**
40+
* COMP_WORDBREAKS
41+
* Characters that $commandLine should be split on to get a list of words in a command
42+
* @var string
43+
*/
44+
protected $wordBreaks = "'\"()= \t\n";
45+
46+
/**
47+
* @param string $commandLine
48+
*/
49+
public function setCommandLine($commandLine)
50+
{
51+
$this->commandLine = $commandLine;
52+
$this->reset();
53+
}
54+
55+
/**
56+
* @return string
57+
*/
58+
public function getCommandLine()
59+
{
60+
return $this->commandLine;
61+
}
62+
63+
public function getCurrentWord()
64+
{
65+
if (isset($this->words[$this->wordIndex])) {
66+
return $this->words[$this->wordIndex];
67+
}
68+
69+
return '';
70+
}
71+
72+
public function getWordAtIndex($index)
73+
{
74+
if (isset($this->words[$index])) {
75+
return $this->words[$index];
76+
}
77+
78+
return '';
79+
}
80+
81+
/**
82+
* @return array
83+
*/
84+
public function getWords()
85+
{
86+
if ($this->words === null) {
87+
$this->splitCommand();
88+
}
89+
90+
return $this->words;
91+
}
92+
93+
/**
94+
* @return int
95+
*/
96+
public function getWordIndex()
97+
{
98+
if ($this->wordIndex === null) {
99+
$this->splitCommand();
100+
}
101+
102+
return $this->wordIndex;
103+
}
104+
105+
/**
106+
* @return int
107+
*/
108+
public function getCharIndex()
109+
{
110+
return $this->charIndex;
111+
}
112+
113+
/**
114+
* @param $index
115+
*/
116+
public function setCharIndex($index)
117+
{
118+
$this->charIndex = $index;
119+
$this->reset();
120+
}
121+
122+
/**
123+
* @param string $charList
124+
*/
125+
public function setWordBreaks($charList)
126+
{
127+
$this->wordBreaks = $charList;
128+
}
129+
130+
/**
131+
* Split commandLine into words using wordBreaks
132+
* @return array
133+
*/
134+
protected function splitCommand()
135+
{
136+
$this->words = array();
137+
$this->wordIndex = null;
138+
$cursor = 1;
139+
140+
$breaks = preg_quote($this->wordBreaks);
141+
142+
if (!preg_match_all("/([^$breaks]*)([$breaks]*)/", $this->commandLine, $matches)) {
143+
return;
144+
}
145+
146+
// Groups:
147+
// 1: Word
148+
// 2: Break characters
149+
foreach ($matches[0] as $index => $wholeMatch) {
150+
151+
// Determine which word the cursor is in
152+
$cursor += strlen($wholeMatch);
153+
$word = $matches[1][$index];
154+
155+
if ($this->wordIndex === null && $cursor >= $this->charIndex) {
156+
$this->wordIndex = $index;
157+
158+
// Find the cursor position relative to the end of the word
159+
$cursorWordOffset = $this->charIndex - ($cursor - strlen($matches[2][$index]) - 1);
160+
161+
if ($cursorWordOffset < 0) {
162+
163+
// Cursor is inside the word - truncate the word at the cursor
164+
// (This emulates normal BASH completion behaviour I've observed, though I'm not entirely sure if it's useful)
165+
$word = substr($word, 0, strlen($word) + $cursorWordOffset);
166+
167+
} else if ($cursorWordOffset > 0) {
168+
169+
// Cursor is in the break-space after the word
170+
// Push an empty word at the cursor
171+
$this->wordIndex++;
172+
$this->words[] = $word;
173+
$this->words[] = '';
174+
continue;
175+
}
176+
}
177+
178+
if ($word !== '') {
179+
$this->words[] = $word;
180+
}
181+
}
182+
183+
if ($this->wordIndex > count($this->words) - 1) {
184+
$this->wordIndex = count($this->words) - 1;
185+
}
186+
}
187+
188+
/**
189+
* Reset the computed words so that $this->splitWords is forced to run again
190+
*/
191+
protected function reset()
192+
{
193+
$this->words = null;
194+
$this->wordIndex = null;
195+
}
196+
197+
}

0 commit comments

Comments
 (0)