diff --git a/README.md b/README.md index 4c2e84a..fb17350 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,18 @@ [![Build Status](https://jenkins.timshome.page/buildStatus/icon?job=timw4mail%2Fphp-kilo%2Fmaster)](https://jenkins.timshome.page/job/timw4mail/job/php-kilo/job/master/) -A reimplementation of the [Kilo](https://viewsourcecode.org/snaptoken/kilo/index.html) tutorial in PHP. Requires PHP 8 and FFI. +A reimplementation of the [Kilo](https://viewsourcecode.org/snaptoken/kilo/index.html) tutorial in PHP. Also has some inspiration from the [Hecto](https://www.philippflenker.com/hecto/) text editor tutorial. Requires PHP 8 and FFI. + +## Requirements +* PHP 8 +* FFI enabled ## Implementation notes: -* 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 one of the implementation classes. * Enums are faked with class constants * Composer is used for autoloading * 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. \ No newline at end of file +* Generally, if a function exists in PHP, with the same name as the C function, the PHP version will be used. +* Classes are used to modularize functionality, and reduce the amount of functions with global side effects diff --git a/kilo b/kilo index 390ecf8..79a105d 100755 --- a/kilo +++ b/kilo @@ -22,23 +22,7 @@ return (static function (int $argc, array $argv): int { Termios::enableRawMode(); register_shutdown_function([Termios::class, 'disableRawMode']); - $editor = Editor::new(); - $editor->setStatusMessage('HELP: Ctrl-S = save | Ctrl-Q = quit | Ctrl-F = find'); - - if ($argc >= 2) - { - $editor->open($argv[1]); - } - - // Input Loop - while (true) - { - $editor->refreshScreen(); - if ($editor->processKeypress() === NULL) - { - break; - } - } + Editor::new($argc, $argv)->run(); return 0; })($argc, $argv); diff --git a/src/Document.php b/src/Document.php index a0fed04..e484aef 100644 --- a/src/Document.php +++ b/src/Document.php @@ -2,17 +2,64 @@ namespace Aviat\Kilo; +/** + * The representation of the current document being edited + * + * @property-read int $numRows + */ class Document { + public ?Syntax $syntax = NULL; + private function __construct( public array $rows = [], public ?string $filename = NULL, private bool $dirty = FALSE, ) {} + public function __get(string $name): ?int + { + if ($name === 'numRows') + { + return count($this->rows); + } + + return NULL; + } + + public static function new(): self + { + return new self(); + } + public static function open(?string $filename = NULL): self { // @TODO move logic from Editor return new self(filename: $filename); } -} \ No newline at end of file + public function save(): bool + { + // @TODO move logic + return false; + } + + public function insertChar(Point $at, string $c): void + { + + } + + public function isDirty(): bool + { + return $this->dirty; + } + + public function deleteChar(Point $at): void + { + + } + + private function insertNewline(Point $at): void + { + + } +} diff --git a/src/Editor.php b/src/Editor.php index fe648ff..2955b72 100644 --- a/src/Editor.php +++ b/src/Editor.php @@ -2,8 +2,10 @@ namespace Aviat\Kilo; +use Aviat\Kilo\Type\TerminalSize; use Aviat\Kilo\Enum\{Color, KeyCode, KeyType, Highlight}; use Aviat\Kilo\Tokens\PHP8; +use Aviat\Kilo\Type\{Point, StatusMessage}; /** * // Don't highlight this! @@ -27,20 +29,30 @@ class Editor { */ protected Point $offset; + /** + * @var Document The document being edited + */ + protected Document $document; + + /** + * @var StatusMessage A disappearing status message + */ + protected StatusMessage $statusMessage; + + /** + * @var TerminalSize The size of the terminal in rows and columns + */ + protected TerminalSize $terminalSize; + /** * @var int The rendered cursor position */ protected int $renderX = 0; /** - * @var int The size of the current terminal in rows + * @var bool Should we stop the rendering loop? */ - protected int $screenRows = 0; - - /** - * @var int The size of the current terminal in columns - */ - protected int $screenCols = 0; + protected bool $shouldQuit = false; /** * @var int The number of times to confirm you wish to quit @@ -54,29 +66,45 @@ class Editor { public bool $dirty = FALSE; public string $filename = ''; - protected string $statusMsg = ''; - protected int $statusMsgTime; public ?Syntax $syntax = NULL; // Tokens for highlighting PHP public array $tokens = []; - public static function new(): Editor + /** + * Create the Editor instance with CLI arguments + * + * @param int $argc + * @param array $argv + * @return Editor + */ + public static function new(int $argc = 0, array $argv = []): Editor { + if ($argc >= 2 && ! empty($argv[1])) + { + return new self($argv[1]); + } + return new self(); } - private function __construct() + /** + * The real constructor, ladies and gentlemen + * + * @param string|null $filename + */ + private function __construct(?string $filename = NULL) { - $this->statusMsgTime = time(); + $this->statusMessage = StatusMessage::from('HELP: Ctrl-S = save | Ctrl-Q = quit | Ctrl-F = find'); $this->cursor = Point::new(); $this->offset = Point::new(); + $this->terminalSize = Terminal::size(); - [$this->screenRows, $this->screenCols] = Terminal::getWindowSize(); - - // Remove a row for the status bar, and one for the message bar - $this->screenRows -= 2; + if (is_string($filename)) + { + $this->open($filename); + } } public function __get(string $name): ?int @@ -93,19 +121,31 @@ class Editor { { return [ 'cursor' => $this->cursor, + 'document' => $this->document, 'offset' => $this->offset, 'dirty' => $this->dirty, 'filename' => $this->filename, 'renderX' => $this->renderX, 'rows' => $this->rows, - 'screenCols' => $this->screenCols, - 'screenRows' => $this->screenRows, - 'statusMsg' => $this->statusMsg, + 'terminalSize' => $this->terminalSize, + 'statusMessage' => $this->statusMessage, 'syntax' => $this->syntax, 'tokens' => $this->tokens, ]; } + /** + * Start the input loop + */ + public function run(): void + { + while ( ! $this->shouldQuit) + { + $this->refreshScreen(); + $this->processKeypress(); + } + } + // ------------------------------------------------------------------------ // ! Terminal // ------------------------------------------------------------------------ @@ -372,7 +412,7 @@ class Editor { return implode('', $lines); } - public function open(string $filename): void + protected function open(string $filename): void { // Copy filename for display $this->filename = $filename; @@ -422,7 +462,7 @@ class Editor { return; } - $this->setStatusMessage('Failed to save! I/O error: %s', error_get_last()['message']); + $this->setStatusMessage('Failed to save! I/O error: %s', error_get_last()['message'] ?? ''); } // ------------------------------------------------------------------------ @@ -543,9 +583,9 @@ class Editor { { $this->offset->y = $this->cursor->y; } - else if ($this->cursor->y >= ($this->offset->y + $this->screenRows)) + else if ($this->cursor->y >= ($this->offset->y + $this->terminalSize->rows)) { - $this->offset->y = $this->cursor->y - $this->screenRows + 1; + $this->offset->y = $this->cursor->y - $this->terminalSize->rows + 1; } // Horizontal Scrolling @@ -553,15 +593,15 @@ class Editor { { $this->offset->x = $this->renderX; } - else if ($this->renderX >= ($this->offset->x + $this->screenCols)) + else if ($this->renderX >= ($this->offset->x + $this->terminalSize->cols)) { - $this->offset->x = $this->renderX - $this->screenCols + 1; + $this->offset->x = $this->renderX - $this->terminalSize->cols + 1; } } protected function drawRows(): void { - for ($y = 0; $y < $this->screenRows; $y++) + for ($y = 0; $y < $this->terminalSize->rows; $y++) { $filerow = $y + $this->offset->y; @@ -582,9 +622,9 @@ class Editor { { $len = 0; } - if ($len > $this->screenCols) + if ($len > $this->terminalSize->cols) { - $len = $this->screenCols; + $len = $this->terminalSize->cols; } $chars = substr($this->rows[$rowIdx]->render, $this->offset->x, (int)$len); @@ -639,16 +679,16 @@ class Editor { protected function drawPlaceholderRow(int $y): void { - if ($this->numRows === 0 && $y === (int)($this->screenRows / 2)) + if ($this->numRows === 0 && $y === (int)($this->terminalSize->rows / 2)) { $welcome = sprintf('PHP Kilo editor -- version %s', KILO_VERSION); $welcomelen = strlen($welcome); - if ($welcomelen > $this->screenCols) + if ($welcomelen > $this->terminalSize->cols) { - $welcomelen = $this->screenCols; + $welcomelen = $this->terminalSize->cols; } - $padding = ($this->screenCols - $welcomelen) / 2; + $padding = ($this->terminalSize->cols - $welcomelen) / 2; if ($padding > 0) { $this->outputBuffer .= '~'; @@ -678,14 +718,14 @@ class Editor { $rstatus = sprintf('%s | %d/%d', $syntaxType, $this->cursor->y + 1, $this->numRows); $len = strlen($status); $rlen = strlen($rstatus); - if ($len > $this->screenCols) + if ($len > $this->terminalSize->cols) { - $len = $this->screenCols; + $len = $this->terminalSize->cols; } $this->outputBuffer .= substr($status, 0, $len); - while ($len < $this->screenCols) + while ($len < $this->terminalSize->cols) { - if ($this->screenCols - $len === $rlen) + if ($this->terminalSize->cols - $len === $rlen) { $this->outputBuffer .= substr($rstatus, 0, $rlen); break; @@ -701,19 +741,19 @@ class Editor { protected function drawMessageBar(): void { $this->outputBuffer .= ANSI::CLEAR_LINE; - $len = strlen($this->statusMsg); - if ($len > $this->screenCols) + $len = strlen($this->statusMessage->text); + if ($len > $this->terminalSize->cols) { - $len = $this->screenCols; + $len = $this->terminalSize->cols; } - if ($len > 0 && (time() - $this->statusMsgTime) < 5) + if ($len > 0 && (time() - $this->statusMessage->time) < 5) { - $this->outputBuffer .= substr($this->statusMsg, 0, $len); + $this->outputBuffer .= substr($this->statusMessage->text, 0, $len); } } - public function refreshScreen(): void + protected function refreshScreen(): void { $this->scroll(); @@ -739,10 +779,7 @@ class Editor { public function setStatusMessage(string $fmt, mixed ...$args): void { - $this->statusMsg = (count($args) > 0) - ? sprintf($fmt, ...$args) - : $fmt; - $this->statusMsgTime = time(); + $this->statusMessage = StatusMessage::from($fmt, ...$args); } // ------------------------------------------------------------------------ @@ -835,14 +872,14 @@ class Editor { break; case KeyType::PAGE_UP: - $y = ($y > $this->screenRows) - ? $y - $this->screenRows + $y = ($y > $this->terminalSize->rows) + ? $y - $this->terminalSize->rows : 0; break; case KeyType::PAGE_DOWN: - $y = ($y + $this->screenRows < $this->numRows) - ? $y + $this->screenRows + $y = ($y + $this->terminalSize->rows < $this->numRows) + ? $y + $this->terminalSize->rows : $this->numRows; break; @@ -874,23 +911,20 @@ class Editor { $this->cursor->y = $y; } - public function processKeypress(): ?string + protected function processKeypress(): void { $c = $this->readKey(); if ($c === KeyCode::NULL || $c === KeyCode::EMPTY) { - return ''; + return; } switch ($c) { case KeyCode::CTRL('q'): - return $this->quitAttempt(); - - case KeyType::ENTER: - $this->insertNewline(); - break; + $this->quitAttempt(); + return; case KeyCode::CTRL('s'): $this->save(); @@ -900,6 +934,10 @@ class Editor { $this->find(); break; + case KeyType::ENTER: + $this->insertNewline(); + break; + case KeyType::BACKSPACE: case KeyType::DEL_KEY: if ($c === KeyType::DEL_KEY) @@ -931,12 +969,14 @@ class Editor { } // Reset quit confirmation timer on different keypress - $this->quitTimes = KILO_QUIT_TIMES; - - return $c; + if ($this->quitTimes < KILO_QUIT_TIMES) + { + $this->quitTimes = KILO_QUIT_TIMES; + $this->setStatusMessage(''); + } } - protected function quitAttempt(): ?string + protected function quitAttempt(): void { if ($this->dirty && $this->quitTimes > 0) { @@ -945,11 +985,12 @@ class Editor { $this->quitTimes ); $this->quitTimes--; - return KeyCode::CTRL('q'); + return; } Terminal::clear(); - return NULL; + + $this->shouldQuit = true; } protected function refreshSyntax(): void diff --git a/src/Terminal.php b/src/Terminal.php index 9ca646b..0cb9cbf 100644 --- a/src/Terminal.php +++ b/src/Terminal.php @@ -2,7 +2,20 @@ namespace Aviat\Kilo; +use Aviat\Kilo\Type\TerminalSize; + class Terminal { + /** + * Get the size of the current terminal window + * + * @codeCoverageIgnore + * @return TerminalSize + */ + public static function size(): TerminalSize + { + return new TerminalSize(...self::getWindowSize()); + } + /** * Get the size of the current terminal window * diff --git a/src/Point.php b/src/Type/Point.php similarity index 95% rename from src/Point.php rename to src/Type/Point.php index 9c890a9..6b049a5 100644 --- a/src/Point.php +++ b/src/Type/Point.php @@ -1,6 +1,6 @@ rows -= 2; + } +} \ No newline at end of file