From 34d96e73a628032aa1084bac70bb4f4b51be41c8 Mon Sep 17 00:00:00 2001 From: Timothy J Warren Date: Tue, 22 Oct 2019 12:09:11 -0400 Subject: [PATCH] Up to step 121 --- kilo | 2 +- src/Editor.php | 194 ++++++++++++++++++++++++++++++++++++++++++---- src/constants.php | 1 + 3 files changed, 182 insertions(+), 15 deletions(-) diff --git a/kilo b/kilo index 93239c3..2b8ca1b 100755 --- a/kilo +++ b/kilo @@ -20,7 +20,7 @@ function main(int $argc, array $argv): int $editor->open($argv[1]); } - $editor->setStatusMessage('HELP: Ctrl-Q = quit'); + $editor->setStatusMessage('HELP: Ctrl-S = save | Ctrl-Q = quit'); // Input Loop while (true) diff --git a/src/Editor.php b/src/Editor.php index e3849d7..a21c093 100644 --- a/src/Editor.php +++ b/src/Editor.php @@ -22,15 +22,18 @@ trait MagicProperties { } class Key { + public const ARROW_DOWN = 'ARROW_DOWN'; public const ARROW_LEFT = 'ARROW_LEFT'; public const ARROW_RIGHT = 'ARROW_RIGHT'; public const ARROW_UP = 'ARROW_UP'; - public const ARROW_DOWN = 'ARROW_DOWN'; + public const BACKSPACE = 'BACKSPACE'; public const DEL_KEY = 'DEL'; - public const HOME_KEY = 'HOME'; public const END_KEY = 'END'; - public const PAGE_UP = 'PAGE_UP'; + public const ENTER = 'ENTER'; + public const ESCAPE = 'ESCAPE'; + public const HOME_KEY = 'HOME'; public const PAGE_DOWN = 'PAGE_DOWN'; + public const PAGE_UP = 'PAGE_UP'; } /** @@ -91,6 +94,7 @@ class Editor { */ protected array $rows = []; + protected int $dirty = 0; protected string $filename = ''; protected string $statusMsg = ''; protected int $statusMsgTime; @@ -135,6 +139,10 @@ class Editor { // @TODO Make this more DRY switch ($c) { + case "\x7f": return Key::BACKSPACE; + + case "\r": return Key::ENTER; + case "\x1b[A": return Key::ARROW_UP; case "\x1b[B": return Key::ARROW_DOWN; case "\x1b[C": return Key::ARROW_RIGHT; @@ -157,6 +165,9 @@ class Editor { case "\x1b[F": return Key::END_KEY; + case "\x1b": + return Key::ESCAPE; + default: return $c; } } @@ -248,17 +259,112 @@ class Editor { $at = $this->numRows; $this->rows[$at] = Row::new($s); $this->updateRow($this->rows[$at]); + + $this->dirty++; + } + + 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); + + $this->dirty++; + } + + protected function rowInsertChar(Row $row, int $at, string $c): void + { + if ($at < 0 || $at > $row->size) + { + $at = $row->size; + } + + // Safely insert into arbitrary position in the existing string + $row->chars = substr($row->chars, 0, $at) . $c . substr($row->chars, $at); + + $this->updateRow($row); + $this->dirty++; + } + + protected function rowAppendString(Row $row, string $s): void + { + $row->chars .= $s; + $this->updateRow($row); + $this->dirty++; + } + + protected function rowDeleteChar(Row $row, int $at): void + { + if ($at < 0 || $at >= $row->size) + { + return; + } + + $row->chars = substr_replace($row->chars, '', $at, 1); + $this->updateRow($row); + $this->dirty++; + } + + // ------------------------------------------------------------------------ + // ! Editor Operations + // ------------------------------------------------------------------------ + + protected function insertChar(string $c): void + { + if ($this->cursorY === $this->numRows) + { + $this->appendRow(''); + } + $this->rowInsertChar($this->rows[$this->cursorY], $this->cursorX, $c); + $this->cursorX++; + } + + protected function deleteChar(): void + { + if ($this->cursorY === $this->numRows || ($this->cursorX === 0 && $this->cursorY === 0)) + { + return; + } + + $row = $this->rows[$this->cursorY]; + if ($this->cursorX > 0) + { + $this->rowDeleteChar($row, $this->cursorX - 1); + $this->cursorX--; + } + else + { + $this->cursorX = $this->rows[$this->cursorY - 1]->size; + $this->rowAppendString($this->rows[$this->cursorY -1], $row->chars); + $this->deleteRow($this->cursorY); + $this->cursorY--; + } } // ------------------------------------------------------------------------ // ! File I/O // ------------------------------------------------------------------------ + protected function rowsToString(): string + { + $str = ''; + foreach ($this->rows as $row) + { + $str .= $row->chars . "\n"; + } + + return $str; + } + public function open(string $filename): void { - // Copy filename for display - $this->filename = $filename; - // Determine the full path to the file $baseFile = basename($filename); $basePath = str_replace($baseFile, '', $filename); @@ -266,6 +372,8 @@ class Editor { $fullname = $path . '/' . $baseFile; + // Copy filename for display + $this->filename = $fullname; // #TODO gracefully handle issues with loading a file $handle = fopen($fullname, 'rb'); @@ -273,10 +381,32 @@ class Editor { while (($line = fgets($handle)) !== FALSE) { // Remove line endings when reading the file - $this->appendRow(rtrim($line)); + $this->appendRow(rtrim($line, "\n\r\0")); } fclose($handle); + + $this->dirty = 0; + } + + protected function save(): void + { + if ($this->filename === '') + { + return; + } + + $contents = $this->rowsToString(); + + $res = file_put_contents($this->filename, $contents); + if ($res === strlen($contents)) + { + $this->setStatusMessage('%d bytes written to disk', strlen($contents)); + $this->dirty = 0; + return; + } + + $this->setStatusMessage('Failed to save! I/O error: %s', error_get_last()['message']); } // ------------------------------------------------------------------------ @@ -371,8 +501,9 @@ class Editor { $this->ab .= "\x1b[7m"; $statusFilename = $this->filename !== '' ? $this->filename : '[No Name]'; - $status = sprintf("%.20s - %d lines", $statusFilename, $this->numRows); - $rstatus = sprintf("%d/%d", $this->cursorY + 1, $this->numRows); + $isDirty = ($this->dirty > 0) ? '(modified)' : ''; + $status = sprintf('%.20s - %d lines %s', $statusFilename, $this->numRows, $isDirty); + $rstatus = sprintf('%d/%d', $this->cursorY + 1, $this->numRows); $len = strlen($status); $rlen = strlen($rstatus); if ($len > $this->screenCols) @@ -505,6 +636,8 @@ class Editor { public function processKeypress(): ?string { + static $quit_times = KILO_QUIT_TIMES; + $c = $this->readKey(); if ($c === "\0") @@ -514,6 +647,27 @@ class Editor { switch ($c) { + case Key::ENTER: + // TODO + break; + + case chr(ctrl_key('q')): + if ($this->dirty > 0 && $quit_times > 0) + { + $this->setStatusMessage('WARNING!!! File has unsaved changes.' . + 'Press Ctrl-Q %d more times to quit.', $quit_times); + $quit_times--; + return ''; + } + write_stdout("\x1b[2J"); // Clear the screen + write_stdout("\x1b[H"); // Reposition cursor to top-left + return NULL; + break; + + case chr(ctrl_key('s')): + $this->save(); + break; + case Key::HOME_KEY: $this->cursorX = 0; break; @@ -525,6 +679,16 @@ class Editor { } break; + case Key::BACKSPACE: + case chr(ctrl_key('h')): + case Key::DEL_KEY: + if ($c === Key::DEL_KEY) + { + $this->moveCursor(Key::ARROW_RIGHT); + } + $this->deleteChar(); + break; + case Key::PAGE_UP: case Key::PAGE_DOWN: $this->pageUpOrDown($c); @@ -537,16 +701,18 @@ class Editor { $this->moveCursor($c); break; - case chr(ctrl_key('q')): - write_stdout("\x1b[2J"); // Clear the screen - write_stdout("\x1b[H"); // Reposition cursor to top-left - return NULL; + case chr(ctrl_key('l')): + case Key::ESCAPE: + // Do nothing break; default: - return $c; + $this->insertChar($c); + break; } + $quit_times = KILO_QUIT_TIMES; + return $c; } diff --git a/src/constants.php b/src/constants.php index 85bbff1..97d576b 100644 --- a/src/constants.php +++ b/src/constants.php @@ -7,6 +7,7 @@ namespace Kilo; // ----------------------------------------------------------------------------- define('KILO_VERSION', '0.0.1'); define('KILO_TAB_STOP', 4); +define('KILO_QUIT_TIMES', 3); // ----------------------------------------------------------------------------- // ! Misc I/O constants