Up to step 121
This commit is contained in:
parent
e9cdc17322
commit
34d96e73a6
2
kilo
2
kilo
@ -20,7 +20,7 @@ function main(int $argc, array $argv): int
|
|||||||
$editor->open($argv[1]);
|
$editor->open($argv[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$editor->setStatusMessage('HELP: Ctrl-Q = quit');
|
$editor->setStatusMessage('HELP: Ctrl-S = save | Ctrl-Q = quit');
|
||||||
|
|
||||||
// Input Loop
|
// Input Loop
|
||||||
while (true)
|
while (true)
|
||||||
|
194
src/Editor.php
194
src/Editor.php
@ -22,15 +22,18 @@ trait MagicProperties {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Key {
|
class Key {
|
||||||
|
public const ARROW_DOWN = 'ARROW_DOWN';
|
||||||
public const ARROW_LEFT = 'ARROW_LEFT';
|
public const ARROW_LEFT = 'ARROW_LEFT';
|
||||||
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 ARROW_DOWN = 'ARROW_DOWN';
|
public const BACKSPACE = 'BACKSPACE';
|
||||||
public const DEL_KEY = 'DEL';
|
public const DEL_KEY = 'DEL';
|
||||||
public const HOME_KEY = 'HOME';
|
|
||||||
public const END_KEY = 'END';
|
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_DOWN = 'PAGE_DOWN';
|
||||||
|
public const PAGE_UP = 'PAGE_UP';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -91,6 +94,7 @@ class Editor {
|
|||||||
*/
|
*/
|
||||||
protected array $rows = [];
|
protected array $rows = [];
|
||||||
|
|
||||||
|
protected int $dirty = 0;
|
||||||
protected string $filename = '';
|
protected string $filename = '';
|
||||||
protected string $statusMsg = '';
|
protected string $statusMsg = '';
|
||||||
protected int $statusMsgTime;
|
protected int $statusMsgTime;
|
||||||
@ -135,6 +139,10 @@ class Editor {
|
|||||||
// @TODO Make this more DRY
|
// @TODO Make this more DRY
|
||||||
switch ($c)
|
switch ($c)
|
||||||
{
|
{
|
||||||
|
case "\x7f": return Key::BACKSPACE;
|
||||||
|
|
||||||
|
case "\r": return Key::ENTER;
|
||||||
|
|
||||||
case "\x1b[A": return Key::ARROW_UP;
|
case "\x1b[A": return Key::ARROW_UP;
|
||||||
case "\x1b[B": return Key::ARROW_DOWN;
|
case "\x1b[B": return Key::ARROW_DOWN;
|
||||||
case "\x1b[C": return Key::ARROW_RIGHT;
|
case "\x1b[C": return Key::ARROW_RIGHT;
|
||||||
@ -157,6 +165,9 @@ class Editor {
|
|||||||
case "\x1b[F":
|
case "\x1b[F":
|
||||||
return Key::END_KEY;
|
return Key::END_KEY;
|
||||||
|
|
||||||
|
case "\x1b":
|
||||||
|
return Key::ESCAPE;
|
||||||
|
|
||||||
default: return $c;
|
default: return $c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -248,17 +259,112 @@ class Editor {
|
|||||||
$at = $this->numRows;
|
$at = $this->numRows;
|
||||||
$this->rows[$at] = Row::new($s);
|
$this->rows[$at] = Row::new($s);
|
||||||
$this->updateRow($this->rows[$at]);
|
$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
|
// ! 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
|
public function open(string $filename): void
|
||||||
{
|
{
|
||||||
// Copy filename for display
|
|
||||||
$this->filename = $filename;
|
|
||||||
|
|
||||||
// Determine the full path to the file
|
// Determine the full path to the file
|
||||||
$baseFile = basename($filename);
|
$baseFile = basename($filename);
|
||||||
$basePath = str_replace($baseFile, '', $filename);
|
$basePath = str_replace($baseFile, '', $filename);
|
||||||
@ -266,6 +372,8 @@ class Editor {
|
|||||||
|
|
||||||
$fullname = $path . '/' . $baseFile;
|
$fullname = $path . '/' . $baseFile;
|
||||||
|
|
||||||
|
// Copy filename for display
|
||||||
|
$this->filename = $fullname;
|
||||||
|
|
||||||
// #TODO gracefully handle issues with loading a file
|
// #TODO gracefully handle issues with loading a file
|
||||||
$handle = fopen($fullname, 'rb');
|
$handle = fopen($fullname, 'rb');
|
||||||
@ -273,10 +381,32 @@ class Editor {
|
|||||||
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));
|
$this->appendRow(rtrim($line, "\n\r\0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fclose($handle);
|
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";
|
$this->ab .= "\x1b[7m";
|
||||||
|
|
||||||
$statusFilename = $this->filename !== '' ? $this->filename : '[No Name]';
|
$statusFilename = $this->filename !== '' ? $this->filename : '[No Name]';
|
||||||
$status = sprintf("%.20s - %d lines", $statusFilename, $this->numRows);
|
$isDirty = ($this->dirty > 0) ? '(modified)' : '';
|
||||||
$rstatus = sprintf("%d/%d", $this->cursorY + 1, $this->numRows);
|
$status = sprintf('%.20s - %d lines %s', $statusFilename, $this->numRows, $isDirty);
|
||||||
|
$rstatus = sprintf('%d/%d', $this->cursorY + 1, $this->numRows);
|
||||||
$len = strlen($status);
|
$len = strlen($status);
|
||||||
$rlen = strlen($rstatus);
|
$rlen = strlen($rstatus);
|
||||||
if ($len > $this->screenCols)
|
if ($len > $this->screenCols)
|
||||||
@ -505,6 +636,8 @@ class Editor {
|
|||||||
|
|
||||||
public function processKeypress(): ?string
|
public function processKeypress(): ?string
|
||||||
{
|
{
|
||||||
|
static $quit_times = KILO_QUIT_TIMES;
|
||||||
|
|
||||||
$c = $this->readKey();
|
$c = $this->readKey();
|
||||||
|
|
||||||
if ($c === "\0")
|
if ($c === "\0")
|
||||||
@ -514,6 +647,27 @@ class Editor {
|
|||||||
|
|
||||||
switch ($c)
|
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:
|
case Key::HOME_KEY:
|
||||||
$this->cursorX = 0;
|
$this->cursorX = 0;
|
||||||
break;
|
break;
|
||||||
@ -525,6 +679,16 @@ class Editor {
|
|||||||
}
|
}
|
||||||
break;
|
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_UP:
|
||||||
case Key::PAGE_DOWN:
|
case Key::PAGE_DOWN:
|
||||||
$this->pageUpOrDown($c);
|
$this->pageUpOrDown($c);
|
||||||
@ -537,16 +701,18 @@ class Editor {
|
|||||||
$this->moveCursor($c);
|
$this->moveCursor($c);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case chr(ctrl_key('q')):
|
case chr(ctrl_key('l')):
|
||||||
write_stdout("\x1b[2J"); // Clear the screen
|
case Key::ESCAPE:
|
||||||
write_stdout("\x1b[H"); // Reposition cursor to top-left
|
// Do nothing
|
||||||
return NULL;
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return $c;
|
$this->insertChar($c);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$quit_times = KILO_QUIT_TIMES;
|
||||||
|
|
||||||
return $c;
|
return $c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ namespace Kilo;
|
|||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
define('KILO_VERSION', '0.0.1');
|
define('KILO_VERSION', '0.0.1');
|
||||||
define('KILO_TAB_STOP', 4);
|
define('KILO_TAB_STOP', 4);
|
||||||
|
define('KILO_QUIT_TIMES', 3);
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// ! Misc I/O constants
|
// ! Misc I/O constants
|
||||||
|
Loading…
Reference in New Issue
Block a user