Get syntax highlighting and keyboard natvigation working again...buggy editing functionality
Some checks failed
timw4mail/php-kilo/pipeline/head There was a failure building this commit

This commit is contained in:
Timothy Warren 2021-03-17 13:14:16 -04:00
parent d0aea78ac3
commit 85e96264a8
6 changed files with 176 additions and 194 deletions

18
kilo
View File

@ -6,6 +6,22 @@ namespace Aviat\Kilo;
require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/autoload.php';
// Log notices/errors/warnings to file // Log notices/errors/warnings to file
set_error_handler(static function (
$errno,
$errstr,
$errfile,
$errline
) {
$msg = print_r([
'code' => $errno,
'message' => $errstr,
'file' => $errfile,
'line' => $errline,
], TRUE);
file_put_contents('error.log', $msg, FILE_APPEND);
return true;
}, -1);
set_exception_handler(static function (mixed $e) { set_exception_handler(static function (mixed $e) {
$msg = print_r([ $msg = print_r([
'code' => $e->getCode(), 'code' => $e->getCode(),
@ -14,7 +30,7 @@ set_exception_handler(static function (mixed $e) {
'line' => $e->getLine(), 'line' => $e->getLine(),
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),
], TRUE); ], TRUE);
file_put_contents('kilo.log', $msg, FILE_APPEND); file_put_contents('exception.log', $msg, FILE_APPEND);
}); });
// ! Init with an IIFE // ! Init with an IIFE

View File

@ -17,8 +17,8 @@ class Document {
public array $tokens = []; public array $tokens = [];
private function __construct( private function __construct(
public array $rows = [],
public string $filename = '', public string $filename = '',
public array $rows = [],
public bool $dirty = FALSE, public bool $dirty = FALSE,
) { ) {
$this->fileType = FileType::from($this->filename); $this->fileType = FileType::from($this->filename);
@ -34,7 +34,7 @@ class Document {
return NULL; return NULL;
} }
public static function default(): self public static function new(): self
{ {
return new self(); return new self();
} }
@ -46,7 +46,11 @@ class Document {
return implode('', $lines); return implode('', $lines);
} }
public static function open(string $filename): ?self // ------------------------------------------------------------------------
// ! File I/O
// ------------------------------------------------------------------------
public function open(string $filename): ?self
{ {
$handle = fopen($filename, 'rb'); $handle = fopen($filename, 'rb');
if ($handle === FALSE) if ($handle === FALSE)
@ -54,19 +58,20 @@ class Document {
return NULL; return NULL;
} }
$self = new self(filename: $filename); $this->__construct($filename);
while (($line = fgets($handle)) !== FALSE) while (($line = fgets($handle)) !== FALSE)
{ {
// Remove line endings when reading the file // Remove line endings when reading the file
$self->insertRow($self->numRows, rtrim($line), FALSE); $this->rows[] = Row::new($this, rtrim($line), $this->numRows);
// $this->insertRow($this->numRows, rtrim($line), FALSE);
} }
fclose($handle); fclose($handle);
$self->selectSyntaxHighlight(); $this->selectSyntaxHighlight();
return $self; return $this;
} }
public function save(): int|false public function save(): int|false
@ -85,11 +90,44 @@ class Document {
public function insert(Point $at, string $c): void public function insert(Point $at, string $c): void
{ {
if ($at->y === $this->numRows) if ($at->y > $this->numRows)
{
return;
}
$this->dirty = true;
if ($c === "\n")
{
$this->insertNewline($at);
}
else if ($at->y === $this->numRows)
{ {
$this->insertRow($this->numRows, ''); $this->insertRow($this->numRows, '');
} }
$this->rows[$at->y]->insertChar($at->x, $c);
$this->rows[$at->y]->insert($at->x, $c);
}
public function delete(Point $at): void
{
if ($at->y > $this->numRows)
{
return;
}
$this->dirty = true;
$row =& $this->rows[$at->y];
if ($at->x === $this->rows[$at->y]->size && $at->y + 1 < $this->numRows)
{
$this->rows[$at->y]->append($this->rows[$at->y + 1]->chars);
$this->deleteRow($at->y + 1);
}
else
{
$row->delete($at->x);
}
} }
public function insertRow(int $at, string $s, bool $updateSyntax = TRUE): void public function insertRow(int $at, string $s, bool $updateSyntax = TRUE): void
@ -122,9 +160,7 @@ class Document {
ksort($this->rows); ksort($this->rows);
$this->rows[$at]->update(); // $this->rows[$at]->highlight();
$this->dirty = true;
// Re-tokenize the file // Re-tokenize the file
if ($updateSyntax) if ($updateSyntax)
@ -133,42 +169,61 @@ class Document {
} }
} }
protected function deleteRow(int $at): void
{
if ($at < 0 || $at >= $this->numRows)
{
return;
}
// Remove the row
unset($this->rows[$at]);
// Re-index the array of rows
$this->rows = array_values($this->rows);
for ($i = $at; $i < $this->numRows; $i++)
{
$this->rows[$i]->idx = $i;
}
// Re-tokenize the file
$this->refreshPHPSyntax();
$this->dirty = true;
}
public function isDirty(): bool public function isDirty(): bool
{ {
return $this->dirty; return $this->dirty;
} }
public function deleteChar(Point $at): void protected function insertNewline(Point $at): void
{ {
if ($at->y > $this->numRows)
}
protected function insertNewline(): void
{
// @TODO attempt smart indentation on newline?
if ($this->cursor->x === 0)
{ {
$this->insertRow($this->cursor->y, ''); return;
}
if ($at->y === $this->numRows)
{
$this->insertRow($this->numRows, '');
}
else if ($at->x === 1)
{
$this->insertRow($at->y, '');
} }
else else
{ {
$row = $this->rows[$this->cursor->y]; $row = $this->rows[$at->y];
$chars = $row->chars; $chars = $row->chars;
$newChars = substr($chars, 0, $this->cursor->x); $newChars = substr($chars, 0, $at->x);
// Truncate the previous row // Truncate the previous row
$row->chars = $newChars; $row->chars = $newChars;
// Add a new row, with the contents from the cursor to the end of the line // Add a new row with the contents of the previous row at the point of the split
$this->insertRow($this->cursor->y + 1, substr($chars, $this->cursor->x)); $this->insertRow($at->y + 1, substr($chars, $at->x));
} }
$this->cursor->y++;
$this->cursor->x = 0;
// Re-tokenize the file
$this->refreshPHPSyntax();
} }
public function selectSyntaxHighlight(): void public function selectSyntaxHighlight(): void
@ -189,7 +244,7 @@ class Document {
public function refreshSyntax(): void public function refreshSyntax(): void
{ {
// Update the syntax highlighting for all the rows of the file // Update the syntax highlighting for all the rows of the file
array_walk($this->rows, static fn (Row $row) => $row->update()); array_walk($this->rows, static fn (Row $row) => $row->highlight());
} }
private function refreshPHPSyntax(): void private function refreshPHPSyntax(): void

View File

@ -83,14 +83,13 @@ class Editor {
$this->cursor = Point::new(); $this->cursor = Point::new();
$this->offset = Point::new(); $this->offset = Point::new();
$this->terminalSize = Terminal::size(); $this->terminalSize = Terminal::size();
$this->document = Document::default();
if (is_string($filename)) if (is_string($filename))
{ {
$maybeDocument = Document::open($filename); $maybeDocument = Document::new()->open($filename);
if ($maybeDocument === NULL) if ($maybeDocument === NULL)
{ {
$this->document = Document::default(); $this->document = Document::new();
$this->setStatusMessage("ERR: Could not open file: {}", $filename); $this->setStatusMessage("ERR: Could not open file: {}", $filename);
} }
else else
@ -124,41 +123,6 @@ class Editor {
} }
} }
// ------------------------------------------------------------------------
// ! Terminal
// ------------------------------------------------------------------------
protected function readKey(): string
{
$c = Terminal::read();
return match($c)
{
// Unambiguous mappings
KeyCode::ARROW_DOWN => KeyType::ARROW_DOWN,
KeyCode::ARROW_LEFT => KeyType::ARROW_LEFT,
KeyCode::ARROW_RIGHT => KeyType::ARROW_RIGHT,
KeyCode::ARROW_UP => KeyType::ARROW_UP,
KeyCode::DEL_KEY => KeyType::DEL_KEY,
KeyCode::ENTER => KeyType::ENTER,
KeyCode::PAGE_DOWN => KeyType::PAGE_DOWN,
KeyCode::PAGE_UP => KeyType::PAGE_UP,
// Backspace
KeyCode::CTRL('h'), KeyCode::BACKSPACE => KeyType::BACKSPACE,
// Escape
KeyCode::CTRL('l'), KeyCode::ESCAPE => KeyType::ESCAPE,
// Home Key
"\eOH", "\e[7~", "\e[1~", ANSI::RESET_CURSOR => KeyType::HOME_KEY,
// End Key
"\eOF", "\e[4~", "\e[8~", "\e[F" => KeyType::END_KEY,
default => $c,
};
}
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// ! Row Operations // ! Row Operations
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@ -213,29 +177,6 @@ class Editor {
} }
protected function deleteRow(int $at): void
{
if ($at < 0 || $at >= $this->document->numRows)
{
return;
}
// Remove the row
unset($this->document->rows[$at]);
// Re-index the array of rows
$this->document->rows = array_values($this->document->rows);
for ($i = $at; $i < $this->document->numRows; $i++)
{
$this->document->rows[$i]->idx = $i;
}
// Re-tokenize the file
$this->refreshPHPSyntax();
$this->dirty = true;
}
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// ! Editor Operations // ! Editor Operations
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@ -246,71 +187,10 @@ class Editor {
$this->moveCursor(KeyType::ARROW_RIGHT); $this->moveCursor(KeyType::ARROW_RIGHT);
} }
protected function insertNewline(): void
{
// @TODO attempt smart indentation on newline?
if ($this->cursor->x === 0)
{
$this->document->insert(Point::new(0, $this->cursor->y), '');
}
else
{
$row = $this->document->rows[$this->cursor->y];
$chars = $row->chars;
$newChars = substr($chars, 0, $this->cursor->x);
// Truncate the previous row
$row->chars = $newChars;
// Add a new row, with the contents from the cursor to the end of the line
$this->document->insert(Point::new(0, $this->cursor->y + 1), substr($chars, $this->cursor->x));
// $this->insertRow($this->cursor->y + 1, substr($chars, $this->cursor->x));
}
$this->cursor->y++;
$this->cursor->x = 0;
// Re-tokenize the file
// $this->refreshPHPSyntax();
}
protected function deleteChar(): void
{
if ($this->cursor->y === $this->document->numRows || ($this->cursor->x === 0 && $this->cursor->y === 0))
{
return;
}
$row = $this->document->rows[$this->cursor->y];
if ($this->cursor->x > 0)
{
$row->deleteChar($this->cursor->x - 1);
$this->cursor->x--;
}
else
{
$this->cursor->x = $this->document->rows[$this->cursor->y - 1]->size;
$this->document->rows[$this->cursor->y -1]->appendString($row->chars);
$this->deleteRow($this->cursor->y);
$this->cursor->y--;
}
// Re-tokenize the file
$this->refreshPHPSyntax();
}
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// ! File I/O // ! File I/O
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
protected function rowsToString(): string
{
$lines = array_map(fn (Row $row) => (string)$row, $this->document->rows);
return implode('', $lines);
}
protected function save(): void protected function save(): void
{ {
if ($this->document->filename === '') if ($this->document->filename === '')
@ -474,13 +354,13 @@ class Editor {
{ {
for ($y = 0; $y < $this->terminalSize->rows; $y++) for ($y = 0; $y < $this->terminalSize->rows; $y++)
{ {
$filerow = $y + $this->offset->y; $fileRow = $y + $this->offset->y;
$this->outputBuffer .= ANSI::CLEAR_LINE; $this->outputBuffer .= ANSI::CLEAR_LINE;
($filerow >= $this->document->numRows) ($fileRow >= $this->document->numRows)
? $this->drawPlaceholderRow($y) ? $this->drawPlaceholderRow($y)
: $this->drawRow($filerow); : $this->drawRow($fileRow);
$this->outputBuffer .= "\r\n"; $this->outputBuffer .= "\r\n";
} }
@ -582,9 +462,9 @@ class Editor {
{ {
$this->outputBuffer .= ANSI::color(Color::INVERT); $this->outputBuffer .= ANSI::color(Color::INVERT);
$statusFilename = $this->filename !== '' ? $this->filename : '[No Name]'; $statusFilename = $this->document->filename !== '' ? $this->document->filename : '[No Name]';
$syntaxType = ($this->syntax !== NULL) ? $this->syntax->filetype : 'no ft'; $syntaxType = $this->document->fileType->name;
$isDirty = $this->dirty ? '(modified)' : ''; $isDirty = $this->document->dirty ? '(modified)' : '';
$status = sprintf('%.20s - %d lines %s', $statusFilename, $this->document->numRows, $isDirty); $status = sprintf('%.20s - %d lines %s', $statusFilename, $this->document->numRows, $isDirty);
$rstatus = sprintf('%s | %d/%d', $syntaxType, $this->cursor->y + 1, $this->document->numRows); $rstatus = sprintf('%s | %d/%d', $syntaxType, $this->cursor->y + 1, $this->document->numRows);
$len = strlen($status); $len = strlen($status);
@ -784,7 +664,7 @@ class Editor {
protected function processKeypress(): void protected function processKeypress(): void
{ {
$c = $this->readKey(); $c = Terminal::readKey();
if ($c === KeyCode::NULL || $c === KeyCode::EMPTY) if ($c === KeyCode::NULL || $c === KeyCode::EMPTY)
{ {
@ -805,17 +685,16 @@ class Editor {
$this->find(); $this->find();
break; break;
case KeyType::ENTER: case KeyType::DEL_KEY:
$this->insertNewline(); $this->document->delete($this->cursor);
break; break;
case KeyType::BACKSPACE: case KeyType::BACKSPACE:
case KeyType::DEL_KEY: if ($this->cursor->x > 0 || $this->cursor->y > 0)
if ($c === KeyType::DEL_KEY)
{ {
$this->moveCursor(KeyType::ARROW_RIGHT); $this->moveCursor(KeyType::ARROW_LEFT);
$this->document->delete($this->cursor);
} }
$this->deleteChar();
break; break;
case KeyType::ARROW_UP: case KeyType::ARROW_UP:

View File

@ -18,7 +18,6 @@ class KeyType {
public const BACKSPACE = 'BACKSPACE'; public const BACKSPACE = 'BACKSPACE';
public const DEL_KEY = 'DELETE'; public const DEL_KEY = 'DELETE';
public const END_KEY = 'END'; public const END_KEY = 'END';
public const ENTER = 'ENTER';
public const ESCAPE = 'ESCAPE'; public const ESCAPE = 'ESCAPE';
public const HOME_KEY = 'HOME'; public const HOME_KEY = 'HOME';
public const PAGE_DOWN = 'PAGE_DOWN'; public const PAGE_DOWN = 'PAGE_DOWN';

View File

@ -56,7 +56,7 @@ class Row {
if ($name === 'chars') if ($name === 'chars')
{ {
$this->chars = $value; $this->chars = $value;
$this->update(); $this->highlight();
} }
} }
@ -77,30 +77,30 @@ class Row {
]; ];
} }
public function insertChar(int $at, string $c): void public function insert(int $at, string $c): void
{ {
if ($at < 0 || $at > $this->size) if ($at < 0 || $at > $this->size)
{ {
$this->appendString($c); $this->append($c);
return; return;
} }
// Safely insert into arbitrary position in the existing string // Safely insert into arbitrary position in the existing string
$this->chars = substr($this->chars, 0, $at) . $c . substr($this->chars, $at); $this->chars = substr($this->chars, 0, $at) . $c . substr($this->chars, $at);
$this->update(); $this->highlight();
$this->parent->dirty = true; $this->parent->dirty = true;
} }
public function appendString(string $s): void public function append(string $s): void
{ {
$this->chars .= $s; $this->chars .= $s;
$this->update(); $this->highlight();
$this->parent->dirty = true; $this->parent->dirty = true;
} }
public function deleteChar(int $at): void public function delete(int $at): void
{ {
if ($at < 0 || $at >= $this->size) if ($at < 0 || $at >= $this->size)
{ {
@ -108,28 +108,22 @@ class Row {
} }
$this->chars = substr_replace($this->chars, '', $at, 1); $this->chars = substr_replace($this->chars, '', $at, 1);
$this->update(); $this->highlight();
$this->parent->dirty = true; $this->parent->dirty = true;
} }
public function highlight(): void
{
// $this->update();
$this->highlightGeneral();
}
public function update(): void
{
$this->render = tabs_to_spaces($this->chars);
$this->highlight();
}
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// ! Syntax Highlighting // ! Syntax Highlighting
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
public function highlightGeneral(): void public function highlight(): void
{
$this->render = tabs_to_spaces($this->chars);
$this->highlightGeneral();
}
protected function highlightGeneral(): void
{ {
$this->hl = array_fill(0, $this->rsize, Highlight::NORMAL); $this->hl = array_fill(0, $this->rsize, Highlight::NORMAL);
@ -279,7 +273,7 @@ class Row {
if ($changed && $this->idx + 1 < $this->parent->numRows) if ($changed && $this->idx + 1 < $this->parent->numRows)
{ {
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
$this->parent->rows[$this->idx + 1]->updateSyntax(); $this->parent->rows[$this->idx + 1]->highlight();
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
} }
} }
@ -420,7 +414,7 @@ class Row {
if ($changed && ($this->idx + 1) < $this->parent->numRows) if ($changed && ($this->idx + 1) < $this->parent->numRows)
{ {
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
$this->parent->rows[$this->idx + 1]->updateSyntax(); $this->parent->rows[$this->idx + 1]->highlight();
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
} }
} }

View File

@ -2,6 +2,8 @@
namespace Aviat\Kilo; namespace Aviat\Kilo;
use Aviat\Kilo\Enum\KeyCode;
use Aviat\Kilo\Enum\KeyType;
use Aviat\Kilo\Type\TerminalSize; use Aviat\Kilo\Type\TerminalSize;
class Terminal { class Terminal {
@ -75,6 +77,43 @@ class Terminal {
return (is_string($input)) ? $input : ''; return (is_string($input)) ? $input : '';
} }
/**
* Get the last key input from the terminal and convert to a
* more useful format
*
* @return string
*/
public static function readKey(): string
{
$c = Terminal::read();
return match($c)
{
// Unambiguous mappings
KeyCode::ARROW_DOWN => KeyType::ARROW_DOWN,
KeyCode::ARROW_LEFT => KeyType::ARROW_LEFT,
KeyCode::ARROW_RIGHT => KeyType::ARROW_RIGHT,
KeyCode::ARROW_UP => KeyType::ARROW_UP,
KeyCode::DEL_KEY => KeyType::DEL_KEY,
KeyCode::PAGE_DOWN => KeyType::PAGE_DOWN,
KeyCode::PAGE_UP => KeyType::PAGE_UP,
// Backspace
KeyCode::CTRL('h'), KeyCode::BACKSPACE => KeyType::BACKSPACE,
// Escape
KeyCode::CTRL('l'), KeyCode::ESCAPE => KeyType::ESCAPE,
// Home Key
"\eOH", "\e[7~", "\e[1~", ANSI::RESET_CURSOR => KeyType::HOME_KEY,
// End Key
"\eOF", "\e[4~", "\e[8~", "\e[F" => KeyType::END_KEY,
default => $c,
};
}
/** /**
* Write to the stdout stream * Write to the stdout stream
* *