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
Some checks failed
timw4mail/php-kilo/pipeline/head There was a failure building this commit
This commit is contained in:
parent
d0aea78ac3
commit
85e96264a8
18
kilo
18
kilo
@ -6,6 +6,22 @@ namespace Aviat\Kilo;
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
// 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) {
|
||||
$msg = print_r([
|
||||
'code' => $e->getCode(),
|
||||
@ -14,7 +30,7 @@ set_exception_handler(static function (mixed $e) {
|
||||
'line' => $e->getLine(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
], TRUE);
|
||||
file_put_contents('kilo.log', $msg, FILE_APPEND);
|
||||
file_put_contents('exception.log', $msg, FILE_APPEND);
|
||||
});
|
||||
|
||||
// ! Init with an IIFE
|
||||
|
115
src/Document.php
115
src/Document.php
@ -17,8 +17,8 @@ class Document {
|
||||
public array $tokens = [];
|
||||
|
||||
private function __construct(
|
||||
public array $rows = [],
|
||||
public string $filename = '',
|
||||
public array $rows = [],
|
||||
public bool $dirty = FALSE,
|
||||
) {
|
||||
$this->fileType = FileType::from($this->filename);
|
||||
@ -34,7 +34,7 @@ class Document {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
public static function default(): self
|
||||
public static function new(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
@ -46,7 +46,11 @@ class Document {
|
||||
return implode('', $lines);
|
||||
}
|
||||
|
||||
public static function open(string $filename): ?self
|
||||
// ------------------------------------------------------------------------
|
||||
// ! File I/O
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
public function open(string $filename): ?self
|
||||
{
|
||||
$handle = fopen($filename, 'rb');
|
||||
if ($handle === FALSE)
|
||||
@ -54,19 +58,20 @@ class Document {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$self = new self(filename: $filename);
|
||||
$this->__construct($filename);
|
||||
|
||||
while (($line = fgets($handle)) !== FALSE)
|
||||
{
|
||||
// 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);
|
||||
|
||||
$self->selectSyntaxHighlight();
|
||||
$this->selectSyntaxHighlight();
|
||||
|
||||
return $self;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function save(): int|false
|
||||
@ -85,11 +90,44 @@ class Document {
|
||||
|
||||
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->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
|
||||
@ -122,9 +160,7 @@ class Document {
|
||||
|
||||
ksort($this->rows);
|
||||
|
||||
$this->rows[$at]->update();
|
||||
|
||||
$this->dirty = true;
|
||||
// $this->rows[$at]->highlight();
|
||||
|
||||
// Re-tokenize the file
|
||||
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
|
||||
{
|
||||
return $this->dirty;
|
||||
}
|
||||
|
||||
public function deleteChar(Point $at): void
|
||||
protected function insertNewline(Point $at): void
|
||||
{
|
||||
|
||||
if ($at->y > $this->numRows)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
protected function insertNewline(): void
|
||||
if ($at->y === $this->numRows)
|
||||
{
|
||||
// @TODO attempt smart indentation on newline?
|
||||
|
||||
if ($this->cursor->x === 0)
|
||||
$this->insertRow($this->numRows, '');
|
||||
}
|
||||
else if ($at->x === 1)
|
||||
{
|
||||
$this->insertRow($this->cursor->y, '');
|
||||
$this->insertRow($at->y, '');
|
||||
}
|
||||
else
|
||||
{
|
||||
$row = $this->rows[$this->cursor->y];
|
||||
$row = $this->rows[$at->y];
|
||||
$chars = $row->chars;
|
||||
$newChars = substr($chars, 0, $this->cursor->x);
|
||||
$newChars = substr($chars, 0, $at->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->insertRow($this->cursor->y + 1, substr($chars, $this->cursor->x));
|
||||
// Add a new row with the contents of the previous row at the point of the split
|
||||
$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
|
||||
@ -189,7 +244,7 @@ class Document {
|
||||
public function refreshSyntax(): void
|
||||
{
|
||||
// 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
|
||||
|
149
src/Editor.php
149
src/Editor.php
@ -83,14 +83,13 @@ class Editor {
|
||||
$this->cursor = Point::new();
|
||||
$this->offset = Point::new();
|
||||
$this->terminalSize = Terminal::size();
|
||||
$this->document = Document::default();
|
||||
|
||||
if (is_string($filename))
|
||||
{
|
||||
$maybeDocument = Document::open($filename);
|
||||
$maybeDocument = Document::new()->open($filename);
|
||||
if ($maybeDocument === NULL)
|
||||
{
|
||||
$this->document = Document::default();
|
||||
$this->document = Document::new();
|
||||
$this->setStatusMessage("ERR: Could not open file: {}", $filename);
|
||||
}
|
||||
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
|
||||
// ------------------------------------------------------------------------
|
||||
@ -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
|
||||
// ------------------------------------------------------------------------
|
||||
@ -246,71 +187,10 @@ class Editor {
|
||||
$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
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
protected function rowsToString(): string
|
||||
{
|
||||
$lines = array_map(fn (Row $row) => (string)$row, $this->document->rows);
|
||||
|
||||
return implode('', $lines);
|
||||
}
|
||||
|
||||
protected function save(): void
|
||||
{
|
||||
if ($this->document->filename === '')
|
||||
@ -474,13 +354,13 @@ class Editor {
|
||||
{
|
||||
for ($y = 0; $y < $this->terminalSize->rows; $y++)
|
||||
{
|
||||
$filerow = $y + $this->offset->y;
|
||||
$fileRow = $y + $this->offset->y;
|
||||
|
||||
$this->outputBuffer .= ANSI::CLEAR_LINE;
|
||||
|
||||
($filerow >= $this->document->numRows)
|
||||
($fileRow >= $this->document->numRows)
|
||||
? $this->drawPlaceholderRow($y)
|
||||
: $this->drawRow($filerow);
|
||||
: $this->drawRow($fileRow);
|
||||
|
||||
$this->outputBuffer .= "\r\n";
|
||||
}
|
||||
@ -582,9 +462,9 @@ class Editor {
|
||||
{
|
||||
$this->outputBuffer .= ANSI::color(Color::INVERT);
|
||||
|
||||
$statusFilename = $this->filename !== '' ? $this->filename : '[No Name]';
|
||||
$syntaxType = ($this->syntax !== NULL) ? $this->syntax->filetype : 'no ft';
|
||||
$isDirty = $this->dirty ? '(modified)' : '';
|
||||
$statusFilename = $this->document->filename !== '' ? $this->document->filename : '[No Name]';
|
||||
$syntaxType = $this->document->fileType->name;
|
||||
$isDirty = $this->document->dirty ? '(modified)' : '';
|
||||
$status = sprintf('%.20s - %d lines %s', $statusFilename, $this->document->numRows, $isDirty);
|
||||
$rstatus = sprintf('%s | %d/%d', $syntaxType, $this->cursor->y + 1, $this->document->numRows);
|
||||
$len = strlen($status);
|
||||
@ -784,7 +664,7 @@ class Editor {
|
||||
|
||||
protected function processKeypress(): void
|
||||
{
|
||||
$c = $this->readKey();
|
||||
$c = Terminal::readKey();
|
||||
|
||||
if ($c === KeyCode::NULL || $c === KeyCode::EMPTY)
|
||||
{
|
||||
@ -805,17 +685,16 @@ class Editor {
|
||||
$this->find();
|
||||
break;
|
||||
|
||||
case KeyType::ENTER:
|
||||
$this->insertNewline();
|
||||
case KeyType::DEL_KEY:
|
||||
$this->document->delete($this->cursor);
|
||||
break;
|
||||
|
||||
case KeyType::BACKSPACE:
|
||||
case KeyType::DEL_KEY:
|
||||
if ($c === KeyType::DEL_KEY)
|
||||
if ($this->cursor->x > 0 || $this->cursor->y > 0)
|
||||
{
|
||||
$this->moveCursor(KeyType::ARROW_RIGHT);
|
||||
$this->moveCursor(KeyType::ARROW_LEFT);
|
||||
$this->document->delete($this->cursor);
|
||||
}
|
||||
$this->deleteChar();
|
||||
break;
|
||||
|
||||
case KeyType::ARROW_UP:
|
||||
|
@ -18,7 +18,6 @@ class KeyType {
|
||||
public const BACKSPACE = 'BACKSPACE';
|
||||
public const DEL_KEY = 'DELETE';
|
||||
public const END_KEY = 'END';
|
||||
public const ENTER = 'ENTER';
|
||||
public const ESCAPE = 'ESCAPE';
|
||||
public const HOME_KEY = 'HOME';
|
||||
public const PAGE_DOWN = 'PAGE_DOWN';
|
||||
|
40
src/Row.php
40
src/Row.php
@ -56,7 +56,7 @@ class Row {
|
||||
if ($name === 'chars')
|
||||
{
|
||||
$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)
|
||||
{
|
||||
$this->appendString($c);
|
||||
$this->append($c);
|
||||
return;
|
||||
}
|
||||
|
||||
// Safely insert into arbitrary position in the existing string
|
||||
$this->chars = substr($this->chars, 0, $at) . $c . substr($this->chars, $at);
|
||||
$this->update();
|
||||
$this->highlight();
|
||||
|
||||
$this->parent->dirty = true;
|
||||
}
|
||||
|
||||
public function appendString(string $s): void
|
||||
public function append(string $s): void
|
||||
{
|
||||
$this->chars .= $s;
|
||||
$this->update();
|
||||
$this->highlight();
|
||||
|
||||
$this->parent->dirty = true;
|
||||
}
|
||||
|
||||
public function deleteChar(int $at): void
|
||||
public function delete(int $at): void
|
||||
{
|
||||
if ($at < 0 || $at >= $this->size)
|
||||
{
|
||||
@ -108,28 +108,22 @@ class Row {
|
||||
}
|
||||
|
||||
$this->chars = substr_replace($this->chars, '', $at, 1);
|
||||
$this->update();
|
||||
$this->highlight();
|
||||
|
||||
$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
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
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);
|
||||
|
||||
@ -279,7 +273,7 @@ class Row {
|
||||
if ($changed && $this->idx + 1 < $this->parent->numRows)
|
||||
{
|
||||
// @codeCoverageIgnoreStart
|
||||
$this->parent->rows[$this->idx + 1]->updateSyntax();
|
||||
$this->parent->rows[$this->idx + 1]->highlight();
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
}
|
||||
@ -420,7 +414,7 @@ class Row {
|
||||
if ($changed && ($this->idx + 1) < $this->parent->numRows)
|
||||
{
|
||||
// @codeCoverageIgnoreStart
|
||||
$this->parent->rows[$this->idx + 1]->updateSyntax();
|
||||
$this->parent->rows[$this->idx + 1]->highlight();
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace Aviat\Kilo;
|
||||
|
||||
use Aviat\Kilo\Enum\KeyCode;
|
||||
use Aviat\Kilo\Enum\KeyType;
|
||||
use Aviat\Kilo\Type\TerminalSize;
|
||||
|
||||
class Terminal {
|
||||
@ -75,6 +77,43 @@ class Terminal {
|
||||
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
|
||||
*
|
||||
|
Loading…
Reference in New Issue
Block a user