Complete chapter 5
This commit is contained in:
parent
1522544fc9
commit
98a3b8b691
@ -7,4 +7,6 @@ due to requiring the `FFI` extension.
|
|||||||
|
|
||||||
* The `editor` prefix has been removed from all the relevant functions, instead they are methods on the `Editor` class.
|
* The `editor` prefix has been removed from all the relevant functions, instead they are methods on the `Editor` class.
|
||||||
* Enums are faked with class constants
|
* Enums are faked with class constants
|
||||||
|
* Properties that must be manually updated in the C version (like counts/string length) are implemented with magic methods,
|
||||||
|
so they are essentially calculated on read.
|
||||||
* Generally, if a function exists in PHP, with the same name as the C function, the PHP version will be used.
|
* Generally, if a function exists in PHP, with the same name as the C function, the PHP version will be used.
|
136
src/Editor.php
136
src/Editor.php
@ -27,7 +27,7 @@ class Key {
|
|||||||
public const ARROW_RIGHT = 'ARROW_RIGHT';
|
public const ARROW_RIGHT = 'ARROW_RIGHT';
|
||||||
public const ARROW_UP = 'ARROW_UP';
|
public const ARROW_UP = 'ARROW_UP';
|
||||||
public const BACKSPACE = 'BACKSPACE';
|
public const BACKSPACE = 'BACKSPACE';
|
||||||
public const DEL_KEY = 'DEL';
|
public const DEL_KEY = 'DELETE';
|
||||||
public const END_KEY = 'END';
|
public const END_KEY = 'END';
|
||||||
public const ENTER = 'ENTER';
|
public const ENTER = 'ENTER';
|
||||||
public const ESCAPE = 'ESCAPE';
|
public const ESCAPE = 'ESCAPE';
|
||||||
@ -70,6 +70,27 @@ class Row {
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function update(): void
|
||||||
|
{
|
||||||
|
$idx = 0;
|
||||||
|
|
||||||
|
for ($i = 0; $i < $this->size; $i++)
|
||||||
|
{
|
||||||
|
if ($this->chars[$i] === "\t")
|
||||||
|
{
|
||||||
|
$this->render[$idx++] = ' ';
|
||||||
|
while ($idx % KILO_TAB_STOP !== 0)
|
||||||
|
{
|
||||||
|
$this->render[$idx++] = ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$this->render[$idx++] = $this->chars[$i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -233,32 +254,29 @@ class Editor {
|
|||||||
return $rx;
|
return $rx;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function updateRow(Row $row): void
|
protected function insertRow(int $at, string $s): void
|
||||||
{
|
{
|
||||||
$idx = 0;
|
if ($at < 0 || $at > $this->numRows)
|
||||||
|
|
||||||
for ($i = 0; $i < $row->size; $i++)
|
|
||||||
{
|
{
|
||||||
if ($row->chars[$i] === "\t")
|
return;
|
||||||
{
|
|
||||||
$row->render[$idx++] = ' ';
|
|
||||||
while ($idx % KILO_TAB_STOP !== 0)
|
|
||||||
{
|
|
||||||
$row->render[$idx++] = ' ';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$row = Row::new($s);
|
||||||
|
|
||||||
|
if ($at === $this->numRows)
|
||||||
|
{
|
||||||
|
$this->rows[] = $row;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$row->render[$idx++] = $row->chars[$i];
|
$this->rows = [
|
||||||
}
|
...array_slice($this->rows, 0, $at),
|
||||||
}
|
$row,
|
||||||
|
...array_slice($this->rows, $at),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function appendRow(string $s): void
|
$this->rows[$at]->update();
|
||||||
{
|
|
||||||
$at = $this->numRows;
|
|
||||||
$this->rows[$at] = Row::new($s);
|
|
||||||
$this->updateRow($this->rows[$at]);
|
|
||||||
|
|
||||||
$this->dirty++;
|
$this->dirty++;
|
||||||
}
|
}
|
||||||
@ -288,15 +306,14 @@ class Editor {
|
|||||||
|
|
||||||
// Safely insert into arbitrary position in the existing string
|
// Safely insert into arbitrary position in the existing string
|
||||||
$row->chars = substr($row->chars, 0, $at) . $c . substr($row->chars, $at);
|
$row->chars = substr($row->chars, 0, $at) . $c . substr($row->chars, $at);
|
||||||
|
$row->update();
|
||||||
$this->updateRow($row);
|
|
||||||
$this->dirty++;
|
$this->dirty++;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function rowAppendString(Row $row, string $s): void
|
protected function rowAppendString(Row $row, string $s): void
|
||||||
{
|
{
|
||||||
$row->chars .= $s;
|
$row->chars .= $s;
|
||||||
$this->updateRow($row);
|
$row->update();
|
||||||
$this->dirty++;
|
$this->dirty++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,7 +325,7 @@ class Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$row->chars = substr_replace($row->chars, '', $at, 1);
|
$row->chars = substr_replace($row->chars, '', $at, 1);
|
||||||
$this->updateRow($row);
|
$row->update();
|
||||||
$this->dirty++;
|
$this->dirty++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,12 +337,34 @@ class Editor {
|
|||||||
{
|
{
|
||||||
if ($this->cursorY === $this->numRows)
|
if ($this->cursorY === $this->numRows)
|
||||||
{
|
{
|
||||||
$this->appendRow('');
|
$this->insertRow($this->numRows, '');
|
||||||
}
|
}
|
||||||
$this->rowInsertChar($this->rows[$this->cursorY], $this->cursorX, $c);
|
$this->rowInsertChar($this->rows[$this->cursorY], $this->cursorX, $c);
|
||||||
$this->cursorX++;
|
$this->cursorX++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function insertNewline(): void
|
||||||
|
{
|
||||||
|
if ($this->cursorX === 0)
|
||||||
|
{
|
||||||
|
$this->insertRow($this->cursorY, '');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$row = $this->rows[$this->cursorY];
|
||||||
|
|
||||||
|
// Add a new row, with the contents from the cursor to the end of the line
|
||||||
|
$this->insertRow($this->cursorY + 1, substr($row->chars, $this->cursorX));
|
||||||
|
|
||||||
|
// Update the (now previous) row
|
||||||
|
$row->chars = substr($row->chars, 0, $this->cursorX);
|
||||||
|
$row->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->cursorY++;
|
||||||
|
$this->cursorX = 0;
|
||||||
|
}
|
||||||
|
|
||||||
protected function deleteChar(): void
|
protected function deleteChar(): void
|
||||||
{
|
{
|
||||||
if ($this->cursorY === $this->numRows || ($this->cursorX === 0 && $this->cursorY === 0))
|
if ($this->cursorY === $this->numRows || ($this->cursorX === 0 && $this->cursorY === 0))
|
||||||
@ -377,11 +416,17 @@ class Editor {
|
|||||||
|
|
||||||
// #TODO gracefully handle issues with loading a file
|
// #TODO gracefully handle issues with loading a file
|
||||||
$handle = fopen($fullname, 'rb');
|
$handle = fopen($fullname, 'rb');
|
||||||
|
if ($handle === FALSE)
|
||||||
|
{
|
||||||
|
disableRawMode();
|
||||||
|
print_r(error_clear_last());
|
||||||
|
die();
|
||||||
|
}
|
||||||
|
|
||||||
while (($line = fgets($handle)) !== FALSE)
|
while (($line = fgets($handle)) !== FALSE)
|
||||||
{
|
{
|
||||||
// Remove line endings when reading the file
|
// Remove line endings when reading the file
|
||||||
$this->appendRow(rtrim($line, "\n\r\0"));
|
$this->insertRow($this->numRows, rtrim($line));
|
||||||
}
|
}
|
||||||
|
|
||||||
fclose($handle);
|
fclose($handle);
|
||||||
@ -393,8 +438,13 @@ class Editor {
|
|||||||
{
|
{
|
||||||
if ($this->filename === '')
|
if ($this->filename === '')
|
||||||
{
|
{
|
||||||
|
$this->filename = $this->prompt('Save as: %s');
|
||||||
|
if ($this->filename === '')
|
||||||
|
{
|
||||||
|
$this->setStatusMessage('Save aborted');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$contents = $this->rowsToString();
|
$contents = $this->rowsToString();
|
||||||
|
|
||||||
@ -577,6 +627,40 @@ class Editor {
|
|||||||
// ! Input
|
// ! Input
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
protected function prompt(string $prompt): string
|
||||||
|
{
|
||||||
|
$buffer = '';
|
||||||
|
while (TRUE)
|
||||||
|
{
|
||||||
|
$this->setStatusMessage($prompt, $buffer);
|
||||||
|
$this->refreshScreen();
|
||||||
|
|
||||||
|
$c = $this->readKey();
|
||||||
|
$cord = ord($c);
|
||||||
|
|
||||||
|
if ($c === Key::ESCAPE)
|
||||||
|
{
|
||||||
|
$this->setStatusMessage('');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($c === Key::ENTER && $buffer !== '')
|
||||||
|
{
|
||||||
|
$this->setStatusMessage('');
|
||||||
|
return $buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($c === Key::DEL_KEY || $c === Key::BACKSPACE || $c === chr(ctrl_key('h')))
|
||||||
|
{
|
||||||
|
$buffer = substr($buffer, 0, -1);
|
||||||
|
}
|
||||||
|
else if ($cord < 128 && $this->ffi->iscntrl($cord) === 0)
|
||||||
|
{
|
||||||
|
$buffer .= $c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected function moveCursor(string $key): void
|
protected function moveCursor(string $key): void
|
||||||
{
|
{
|
||||||
$row = ($this->cursorY >= $this->numRows)
|
$row = ($this->cursorY >= $this->numRows)
|
||||||
@ -648,7 +732,7 @@ class Editor {
|
|||||||
switch ($c)
|
switch ($c)
|
||||||
{
|
{
|
||||||
case Key::ENTER:
|
case Key::ENTER:
|
||||||
// TODO
|
$this->insertNewline();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case chr(ctrl_key('q')):
|
case chr(ctrl_key('q')):
|
||||||
|
@ -57,8 +57,6 @@ function read_stdin(int $len = 128): string
|
|||||||
{
|
{
|
||||||
$handle = fopen('php://stdin', 'rb');
|
$handle = fopen('php://stdin', 'rb');
|
||||||
$input = fread($handle, $len);
|
$input = fread($handle, $len);
|
||||||
|
|
||||||
$input = rtrim($input);
|
|
||||||
fclose($handle);
|
fclose($handle);
|
||||||
|
|
||||||
return $input;
|
return $input;
|
||||||
@ -87,7 +85,16 @@ function read_stdout(int $len = 128): string
|
|||||||
return $input;
|
return $input;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do bit twiddling to convert a letter into
|
||||||
|
* its Ctrl-letter equivalent
|
||||||
|
*
|
||||||
|
* @param string $char
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
function ctrl_key(string $char): int
|
function ctrl_key(string $char): int
|
||||||
{
|
{
|
||||||
|
// b1,100,001 (a) & b0,011,111 = b0,000,001
|
||||||
|
// b1,100,010 (b) & b0,011,111 = b0,000,010
|
||||||
return ord($char) & 0x1f;
|
return ord($char) & 0x1f;
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user