Skip to content

Commit f58f800

Browse files
committed
Add skeleton for proper history support
1 parent c5879cd commit f58f800

File tree

8 files changed

+248
-37
lines changed

8 files changed

+248
-37
lines changed

src/History.php

Lines changed: 0 additions & 23 deletions
This file was deleted.

src/Readline.php

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use React\Stream\Util;
99
use Clue\React\Utf8\Sequencer as Utf8Sequencer;
1010
use Clue\React\Term\ControlCodeParser;
11+
use Clue\React\Stdio\Readline\History;
12+
use Clue\React\Stdio\Readline\MemoryHistory;
1113

1214
class Readline extends EventEmitter implements ReadableStreamInterface
1315
{
@@ -17,18 +19,24 @@ class Readline extends EventEmitter implements ReadableStreamInterface
1719
private $echo = true;
1820
private $autocomplete = null;
1921
private $move = true;
20-
private $history = null;
22+
private $history;
2123
private $encoding = 'utf-8';
2224

2325
private $input;
2426
private $output;
2527
private $sequencer;
2628
private $closed = false;
2729

28-
public function __construct(ReadableStreamInterface $input, WritableStreamInterface $output)
30+
public function __construct(ReadableStreamInterface $input, WritableStreamInterface $output, History $history = null)
2931
{
3032
$this->input = $input;
33+
34+
if ($history === null) {
35+
$history = new MemoryHistory();
36+
}
37+
3138
$this->output = $output;
39+
$this->history = $history;
3240

3341
if (!$this->input->isReadable()) {
3442
return $this->close();
@@ -311,21 +319,34 @@ public function getInput()
311319
}
312320

313321
/**
314-
* set history handler to use (or none)
322+
* set history handler to use
315323
*
316324
* The history handler will be called whenever the user hits the UP or DOWN
317325
* arrow keys.
318326
*
319-
* @param HistoryInterface|null $history
327+
* If you do not want to use history support, simply pass a `NullHistory` object.
328+
*
329+
* @param History $history new history handler to use
320330
* @return self
321331
*/
322-
public function setHistory(HistoryInterface $history = null)
332+
public function setHistory(History $history)
323333
{
324334
$this->history = $history;
325335

326336
return $this;
327337
}
328338

339+
/**
340+
* Gets the current history handler in use
341+
*
342+
* @return History
343+
* @see self::setHistory()
344+
*/
345+
public function getHistory()
346+
{
347+
return $this->history;
348+
}
349+
329350
/**
330351
* set autocompletion handler to use (or none)
331352
*
@@ -468,17 +489,13 @@ public function onKeyRight()
468489
/** @internal */
469490
public function onKeyUp()
470491
{
471-
if ($this->history !== null) {
472-
$this->history->up();
473-
}
492+
$this->history->moveUp($this);
474493
}
475494

476495
/** @internal */
477496
public function onKeyDown()
478497
{
479-
if ($this->history !== null) {
480-
$this->history->down();
481-
}
498+
$this->history->moveDown($this);
482499
}
483500

484501
/**
@@ -548,9 +565,7 @@ protected function processLine()
548565
}
549566

550567
// process stored input buffer
551-
if ($this->history !== null) {
552-
$this->history->addLine($line);
553-
}
568+
$this->history->addLine($line);
554569
$this->emit('data', array($line));
555570
}
556571

src/Readline/History.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Clue\React\Stdio\Readline;
4+
5+
use Clue\React\Stdio\Readline;
6+
7+
interface History
8+
{
9+
public function addLine($line);
10+
11+
public function moveUp(Readline $readline);
12+
13+
public function moveDown(Readline $readline);
14+
}

src/Readline/MemoryHistory.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace Clue\React\Stdio\Readline;
4+
5+
use Clue\React\Stdio\Readline;
6+
7+
class MemoryHistory implements History
8+
{
9+
private $lines = array();
10+
private $position = null;
11+
12+
public function addLine($line)
13+
{
14+
if ($line === '') {
15+
return;
16+
}
17+
18+
$this->lines []= $line;
19+
$this->position = null;
20+
}
21+
22+
public function moveUp(Readline $readline)
23+
{
24+
// ignore if already at top or history is empty
25+
if ($this->position === 0 || !$this->lines) {
26+
return;
27+
}
28+
29+
if ($this->position === null) {
30+
// first time up => move to last entry
31+
$this->position = count($this->lines) - 1;
32+
33+
// TODO: buffer current user input
34+
} else {
35+
// somewhere in the list => move by one
36+
$this->position--;
37+
}
38+
39+
$readline->setInput($this->lines[$this->position]);
40+
}
41+
42+
public function moveDown(Readline $readline)
43+
{
44+
if ($this->position === null) {
45+
return;
46+
}
47+
48+
if (($this->position + 1) < count($this->lines)) {
49+
// this is still a valid position => advance by one and apply
50+
$this->position++;
51+
$readline->setInput($this->lines[$this->position]);
52+
} else {
53+
// moved beyond bottom => restore empty input
54+
$readline->setInput('');
55+
$this->position = null;
56+
}
57+
}
58+
}

src/Readline/NullHistory.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Clue\React\Stdio\Readline;
4+
5+
use Clue\React\Stdio\Readline;
6+
7+
class NullHistory implements History
8+
{
9+
public function addLine($line)
10+
{
11+
// NOOP
12+
}
13+
14+
public function moveUp(Readline $readline)
15+
{
16+
// NOOP
17+
}
18+
19+
public function moveDown(Readline $readline)
20+
{
21+
// NOOP
22+
}
23+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
use Clue\React\Stdio\Readline\MemoryHistory;
4+
use Clue\React\Stdio\Readline\History;
5+
class MemoryHistoryTest extends TestCase
6+
{
7+
private $readline;
8+
private $history;
9+
10+
public function setUp()
11+
{
12+
$this->readline = $this->getMockBuilder('Clue\React\Stdio\Readline')->disableOriginalConstructor()->getMock();
13+
$this->history = new MemoryHistory();
14+
}
15+
16+
public function testCanAdd()
17+
{
18+
$this->history->addLine('a');
19+
$this->history->addLine('b');
20+
21+
return $this->history;
22+
}
23+
24+
/**
25+
* @depends testCanAdd
26+
* @param History $history
27+
*/
28+
public function testMovingUpRestoresLastEntry(History $history)
29+
{
30+
$this->readline->expects($this->once())->method('setInput')->with($this->equalTo('b'));
31+
32+
$history->moveUp($this->readline);
33+
34+
return $history;
35+
}
36+
37+
/**
38+
* @depends testMovingUpRestoresLastEntry
39+
* @param History $history
40+
*/
41+
public function testMovingUpMovesToNextEntryWhichIsFirst(History $history)
42+
{
43+
$this->readline->expects($this->once())->method('setInput')->with($this->equalTo('a'));
44+
45+
$history->moveUp($this->readline);
46+
47+
return $history;
48+
}
49+
50+
/**
51+
* @depends testMovingUpMovesToNextEntryWhichIsFirst
52+
* @param History $history
53+
*/
54+
public function testMovingUpWhenAlreadyOnFirstDoesNothing(History $history)
55+
{
56+
$this->readline->expects($this->never())->method('setInput');
57+
58+
$history->moveUp($this->readline);
59+
}
60+
61+
public function testMovingDownDoesNothing()
62+
{
63+
$this->history->addLine('ignored');
64+
65+
$this->readline->expects($this->never())->method('setInput');
66+
67+
$this->history->moveDown($this->readline);
68+
}
69+
70+
public function testMovingInEmptyHistoryDoesNothing()
71+
{
72+
$this->readline->expects($this->never())->method('setInput');
73+
74+
$this->history->moveUp($this->readline);
75+
$this->history->moveDown($this->readline);
76+
}
77+
}

tests/Readline/NullHistoryTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
use Clue\React\Stdio\Readline\NullHistory;
4+
class NullHistoryTest extends TestCase
5+
{
6+
public function testDoesNothing()
7+
{
8+
$readline = $this->getMockBuilder('Clue\React\Stdio\Readline')->disableOriginalConstructor()->getMock();
9+
$readline->expects($this->never())->method('setInput');
10+
11+
$history = new NullHistory();
12+
$history->addLine('a');
13+
$history->addLine('b');
14+
15+
$history->moveUp($readline);
16+
$history->moveDown($readline);
17+
}
18+
}

tests/ReadlineTest.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,35 @@ public function testCursorCellObeysCustomEchoAsterisk(Readline $readline)
495495
$this->assertEquals(3, $readline->getCursorCell());
496496
}
497497

498+
public function testHistoryGetterReturnsSameFromSetter()
499+
{
500+
$history = $this->getMock('Clue\React\Stdio\Readline\History');
501+
502+
$this->assertSame($this->readline, $this->readline->setHistory($history));
503+
504+
$this->assertSame($history, $this->readline->getHistory());
505+
}
506+
507+
public function testKeysCursorUpInvokesHistoryHandler()
508+
{
509+
$history = $this->getMock('Clue\React\Stdio\Readline\History');
510+
$history->expects($this->once())->method('moveUp')->with($this->equalTo($this->readline));
511+
512+
$this->readline->setHistory($history);
513+
514+
$this->readline->onKeyUp($this->readline);
515+
}
516+
517+
public function testKeysCursorDownInvokesHistoryHandler()
518+
{
519+
$history = $this->getMock('Clue\React\Stdio\Readline\History');
520+
$history->expects($this->once())->method('moveDown')->with($this->equalTo($this->readline));
521+
522+
$this->readline->setHistory($history);
523+
524+
$this->readline->onKeyDown($this->readline);
525+
}
526+
498527
public function testEmitEmptyInputOnEnter()
499528
{
500529
$this->readline->on('data', $this->expectCallableOnceWith(''));

0 commit comments

Comments
 (0)