2019-10-14 16:21:41 -04:00
|
|
|
<?php declare(strict_types=1);
|
|
|
|
|
|
|
|
namespace Kilo;
|
|
|
|
|
|
|
|
|
2019-10-15 13:23:25 -04:00
|
|
|
|
2019-10-16 16:43:15 -04:00
|
|
|
/**
|
|
|
|
* @property-read int numRows
|
|
|
|
*/
|
2019-10-14 16:21:41 -04:00
|
|
|
class Editor {
|
2019-10-16 16:43:15 -04:00
|
|
|
use MagicProperties;
|
|
|
|
|
|
|
|
private string $ab = '';
|
2019-10-14 16:21:41 -04:00
|
|
|
|
2019-10-16 16:43:15 -04:00
|
|
|
protected int $cursorX = 0;
|
|
|
|
protected int $cursorY = 0;
|
|
|
|
protected int $renderX = 0;
|
|
|
|
protected int $rowOffset = 0;
|
|
|
|
protected int $colOffset = 0;
|
2019-10-14 16:21:41 -04:00
|
|
|
protected int $screenRows = 0;
|
|
|
|
protected int $screenCols = 0;
|
|
|
|
|
2019-10-15 13:23:25 -04:00
|
|
|
/**
|
|
|
|
* Array of Row objects
|
|
|
|
*/
|
|
|
|
protected array $rows = [];
|
|
|
|
|
2019-10-22 12:09:11 -04:00
|
|
|
protected int $dirty = 0;
|
2019-10-16 16:43:15 -04:00
|
|
|
protected string $filename = '';
|
|
|
|
protected string $statusMsg = '';
|
|
|
|
protected int $statusMsgTime;
|
|
|
|
|
2019-10-24 10:58:38 -04:00
|
|
|
public static function new(): Editor
|
2019-10-15 13:23:25 -04:00
|
|
|
{
|
2019-10-24 10:58:38 -04:00
|
|
|
return new self();
|
2019-10-15 13:23:25 -04:00
|
|
|
}
|
|
|
|
|
2019-10-24 10:58:38 -04:00
|
|
|
private function __construct()
|
2019-10-14 16:21:41 -04:00
|
|
|
{
|
2019-10-16 16:43:15 -04:00
|
|
|
$this->statusMsgTime = time();
|
2019-10-14 16:21:41 -04:00
|
|
|
|
2019-10-24 10:58:38 -04:00
|
|
|
[$this->screenRows, $this->screenCols] = get_window_size();
|
2019-10-16 16:43:15 -04:00
|
|
|
|
|
|
|
$this->screenRows -= 2;
|
|
|
|
|
|
|
|
// print_r($this); die();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function __get(string $name)
|
|
|
|
{
|
|
|
|
if ($name === 'numRows')
|
|
|
|
{
|
|
|
|
return count($this->rows);
|
|
|
|
}
|
|
|
|
|
|
|
|
return NULL;
|
2019-10-14 16:21:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// ! Terminal
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
protected function readKey(): string
|
|
|
|
{
|
2019-10-21 15:37:20 -04:00
|
|
|
$c = read_stdin();
|
2019-10-14 16:21:41 -04:00
|
|
|
|
2019-10-21 15:37:20 -04:00
|
|
|
// @TODO Make this more DRY
|
|
|
|
switch ($c)
|
2019-10-14 16:21:41 -04:00
|
|
|
{
|
2019-10-22 12:09:11 -04:00
|
|
|
case "\x7f": return Key::BACKSPACE;
|
|
|
|
|
|
|
|
case "\r": return Key::ENTER;
|
|
|
|
|
2019-10-21 15:37:20 -04:00
|
|
|
case "\x1b[A": return Key::ARROW_UP;
|
|
|
|
case "\x1b[B": return Key::ARROW_DOWN;
|
|
|
|
case "\x1b[C": return Key::ARROW_RIGHT;
|
|
|
|
case "\x1b[D": return Key::ARROW_LEFT;
|
2019-10-14 16:21:41 -04:00
|
|
|
|
2019-10-21 15:37:20 -04:00
|
|
|
case "\x1b[3~": return Key::DEL_KEY;
|
2019-10-16 16:43:15 -04:00
|
|
|
|
2019-10-21 15:37:20 -04:00
|
|
|
case "\x1b[5~": return Key::PAGE_UP;
|
|
|
|
case "\x1b[6~": return Key::PAGE_DOWN;
|
2019-10-16 16:43:15 -04:00
|
|
|
|
2019-10-21 15:37:20 -04:00
|
|
|
case "\x1bOH":
|
|
|
|
case "\x1b[1~":
|
|
|
|
case "\x1b[7~":
|
|
|
|
case "\x1b[H":
|
|
|
|
return Key::HOME_KEY;
|
2019-10-14 16:21:41 -04:00
|
|
|
|
2019-10-21 15:37:20 -04:00
|
|
|
case "\x1bOF":
|
|
|
|
case "\x1b[4~":
|
|
|
|
case "\x1b[8~":
|
|
|
|
case "\x1b[F":
|
|
|
|
return Key::END_KEY;
|
2019-10-14 16:21:41 -04:00
|
|
|
|
2019-10-22 12:09:11 -04:00
|
|
|
case "\x1b":
|
|
|
|
return Key::ESCAPE;
|
|
|
|
|
2019-10-21 15:37:20 -04:00
|
|
|
default: return $c;
|
|
|
|
}
|
2019-10-14 16:21:41 -04:00
|
|
|
}
|
|
|
|
|
2019-10-15 13:23:25 -04:00
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// ! Row Operations
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
|
2019-10-16 16:43:15 -04:00
|
|
|
protected function rowCxToRx(Row $row, int $cx): int
|
|
|
|
{
|
|
|
|
$rx = 0;
|
|
|
|
for ($i = 0; $i < $cx; $i++)
|
|
|
|
{
|
|
|
|
if ($row->chars[$i] === "\t")
|
|
|
|
{
|
|
|
|
$rx += (KILO_TAB_STOP - 1) - ($rx % KILO_TAB_STOP);
|
|
|
|
}
|
|
|
|
$rx++;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $rx;
|
|
|
|
}
|
|
|
|
|
2019-10-23 10:36:04 -04:00
|
|
|
protected function rowRxToCx(Row $row, int $rx): int
|
|
|
|
{
|
|
|
|
$cur_rx = 0;
|
|
|
|
for ($cx = 0; $cx < $row->size; $cx++)
|
|
|
|
{
|
|
|
|
if ($row->chars[$cx] === "\t")
|
|
|
|
{
|
|
|
|
$cur_rx += (KILO_TAB_STOP - 1) - ($cur_rx % KILO_TAB_STOP);
|
|
|
|
}
|
|
|
|
$cur_rx++;
|
|
|
|
|
|
|
|
if ($cur_rx > $rx)
|
|
|
|
{
|
|
|
|
return $cx;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $cx;
|
|
|
|
}
|
|
|
|
|
2019-10-22 16:16:28 -04:00
|
|
|
protected function insertRow(int $at, string $s): void
|
2019-10-16 16:43:15 -04:00
|
|
|
{
|
2019-10-22 16:16:28 -04:00
|
|
|
if ($at < 0 || $at > $this->numRows)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$row = Row::new($s);
|
2019-10-16 16:43:15 -04:00
|
|
|
|
2019-10-22 16:16:28 -04:00
|
|
|
if ($at === $this->numRows)
|
2019-10-16 16:43:15 -04:00
|
|
|
{
|
2019-10-22 16:16:28 -04:00
|
|
|
$this->rows[] = $row;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
$this->rows = [
|
|
|
|
...array_slice($this->rows, 0, $at),
|
|
|
|
$row,
|
|
|
|
...array_slice($this->rows, $at),
|
|
|
|
];
|
2019-10-16 16:43:15 -04:00
|
|
|
}
|
|
|
|
|
2019-10-22 16:16:28 -04:00
|
|
|
$this->rows[$at]->update();
|
2019-10-22 12:09:11 -04:00
|
|
|
|
|
|
|
$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);
|
2019-10-22 16:16:28 -04:00
|
|
|
$row->update();
|
2019-10-22 12:09:11 -04:00
|
|
|
$this->dirty++;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function rowAppendString(Row $row, string $s): void
|
|
|
|
{
|
|
|
|
$row->chars .= $s;
|
2019-10-22 16:16:28 -04:00
|
|
|
$row->update();
|
2019-10-22 12:09:11 -04:00
|
|
|
$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);
|
2019-10-22 16:16:28 -04:00
|
|
|
$row->update();
|
2019-10-22 12:09:11 -04:00
|
|
|
$this->dirty++;
|
|
|
|
}
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// ! Editor Operations
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
|
|
|
|
protected function insertChar(string $c): void
|
|
|
|
{
|
|
|
|
if ($this->cursorY === $this->numRows)
|
|
|
|
{
|
2019-10-22 16:16:28 -04:00
|
|
|
$this->insertRow($this->numRows, '');
|
2019-10-22 12:09:11 -04:00
|
|
|
}
|
|
|
|
$this->rowInsertChar($this->rows[$this->cursorY], $this->cursorX, $c);
|
|
|
|
$this->cursorX++;
|
|
|
|
}
|
|
|
|
|
2019-10-22 16:16:28 -04:00
|
|
|
protected function insertNewline(): void
|
|
|
|
{
|
|
|
|
if ($this->cursorX === 0)
|
|
|
|
{
|
|
|
|
$this->insertRow($this->cursorY, '');
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
$row = $this->rows[$this->cursorY];
|
|
|
|
|
|
|
|
// Add a new row, with the contents from the cursor to the end of the line
|
|
|
|
$this->insertRow($this->cursorY + 1, substr($row->chars, $this->cursorX));
|
|
|
|
|
|
|
|
// Update the (now previous) row
|
|
|
|
$row->chars = substr($row->chars, 0, $this->cursorX);
|
|
|
|
$row->update();
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->cursorY++;
|
|
|
|
$this->cursorX = 0;
|
|
|
|
}
|
|
|
|
|
2019-10-22 12:09:11 -04:00
|
|
|
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--;
|
|
|
|
}
|
2019-10-15 13:23:25 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// ! File I/O
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
|
2019-10-22 12:09:11 -04:00
|
|
|
protected function rowsToString(): string
|
2019-10-15 13:23:25 -04:00
|
|
|
{
|
2019-10-22 16:44:55 -04:00
|
|
|
$lines = [];
|
2019-10-22 12:09:11 -04:00
|
|
|
foreach ($this->rows as $row)
|
|
|
|
{
|
2019-10-22 16:44:55 -04:00
|
|
|
$lines[] = (string)$row;
|
2019-10-22 12:09:11 -04:00
|
|
|
}
|
|
|
|
|
2019-10-22 16:44:55 -04:00
|
|
|
return implode('', $lines);
|
2019-10-22 12:09:11 -04:00
|
|
|
}
|
2019-10-16 16:43:15 -04:00
|
|
|
|
2019-10-22 12:09:11 -04:00
|
|
|
public function open(string $filename): void
|
|
|
|
{
|
2019-10-22 16:44:55 -04:00
|
|
|
// Copy filename for display
|
|
|
|
$this->filename = $filename;
|
|
|
|
|
2019-10-16 16:43:15 -04:00
|
|
|
// Determine the full path to the file
|
2019-10-22 16:44:55 -04:00
|
|
|
/* $baseFile = basename($filename);
|
2019-10-15 13:23:25 -04:00
|
|
|
$basePath = str_replace($baseFile, '', $filename);
|
|
|
|
$path = (is_dir($basePath)) ? $basePath : getcwd();
|
|
|
|
|
2019-10-22 16:44:55 -04:00
|
|
|
$fullname = $path . '/' . $baseFile; */
|
2019-10-15 13:23:25 -04:00
|
|
|
|
2019-10-16 16:43:15 -04:00
|
|
|
// #TODO gracefully handle issues with loading a file
|
2019-10-22 16:44:55 -04:00
|
|
|
$handle = fopen($filename, 'rb');
|
2019-10-22 16:16:28 -04:00
|
|
|
if ($handle === FALSE)
|
|
|
|
{
|
2019-10-22 16:44:55 -04:00
|
|
|
write_stdout("\x1b[2J"); // Clear the screen
|
|
|
|
write_stdout("\x1b[H"); // Reposition cursor to top-left
|
2019-10-22 16:16:28 -04:00
|
|
|
disableRawMode();
|
2019-10-22 16:44:55 -04:00
|
|
|
print_r(error_get_last());
|
2019-10-22 16:16:28 -04:00
|
|
|
die();
|
|
|
|
}
|
2019-10-15 13:23:25 -04:00
|
|
|
|
|
|
|
while (($line = fgets($handle)) !== FALSE)
|
|
|
|
{
|
2019-10-21 15:37:20 -04:00
|
|
|
// Remove line endings when reading the file
|
2019-10-22 16:16:28 -04:00
|
|
|
$this->insertRow($this->numRows, rtrim($line));
|
2019-10-15 13:23:25 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
fclose($handle);
|
2019-10-22 12:09:11 -04:00
|
|
|
|
|
|
|
$this->dirty = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function save(): void
|
|
|
|
{
|
|
|
|
if ($this->filename === '')
|
|
|
|
{
|
2019-10-22 16:44:55 -04:00
|
|
|
$newFilename = $this->prompt('Save as: %s');
|
|
|
|
if ($newFilename === '')
|
2019-10-22 16:16:28 -04:00
|
|
|
{
|
|
|
|
$this->setStatusMessage('Save aborted');
|
|
|
|
return;
|
|
|
|
}
|
2019-10-22 16:44:55 -04:00
|
|
|
|
|
|
|
$this->filename = $newFilename;
|
2019-10-22 12:09:11 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
$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']);
|
2019-10-15 13:23:25 -04:00
|
|
|
}
|
|
|
|
|
2019-10-23 10:36:04 -04:00
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// ! Find
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
|
|
|
|
protected function findCallback(string $query, string $key): void
|
|
|
|
{
|
|
|
|
static $lastMatch = -1;
|
|
|
|
static $direction = 1;
|
|
|
|
|
2019-10-24 16:22:52 -04:00
|
|
|
static $savedHlLine = 0;
|
|
|
|
static $savedHl = [];
|
|
|
|
|
|
|
|
if ( ! empty($savedHl))
|
|
|
|
{
|
|
|
|
$this->rows[$savedHlLine]->hl = $savedHl;
|
|
|
|
$savedHl = [];
|
|
|
|
}
|
|
|
|
|
2019-10-23 10:36:04 -04:00
|
|
|
if ($key === "\r" || $key === "\x1b")
|
|
|
|
{
|
|
|
|
$lastMatch = -1;
|
|
|
|
$direction = 1;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($key === Key::ARROW_RIGHT || $key === Key::ARROW_DOWN)
|
|
|
|
{
|
|
|
|
$direction = 1;
|
|
|
|
}
|
|
|
|
else if ($key === Key::ARROW_LEFT || $key === Key::ARROW_UP)
|
|
|
|
{
|
|
|
|
$direction = -1;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
$lastMatch = -1;
|
|
|
|
$direction = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($lastMatch === -1)
|
|
|
|
{
|
|
|
|
$direction = 1;
|
|
|
|
}
|
|
|
|
$current = $lastMatch;
|
|
|
|
|
|
|
|
for ($i = 0; $i < $this->numRows; $i++)
|
|
|
|
{
|
|
|
|
$current += $direction;
|
|
|
|
if ($current === -1)
|
|
|
|
{
|
|
|
|
$current = $this->numRows - 1;
|
|
|
|
}
|
|
|
|
else if ($current === $this->numRows)
|
|
|
|
{
|
|
|
|
$current = 0;
|
|
|
|
}
|
|
|
|
|
2019-10-24 16:22:52 -04:00
|
|
|
$row = $this->rows[$current];
|
|
|
|
|
|
|
|
$match = strpos($row->render, $query);
|
2019-10-23 10:36:04 -04:00
|
|
|
if ($match !== FALSE)
|
|
|
|
{
|
|
|
|
$lastMatch = $current;
|
|
|
|
$this->cursorY = $current;
|
2019-10-24 16:22:52 -04:00
|
|
|
$this->cursorX = $this->rowRxToCx($row, $match);
|
2019-10-23 10:36:04 -04:00
|
|
|
$this->rowOffset = $this->numRows;
|
2019-10-24 12:01:00 -04:00
|
|
|
|
2019-10-24 16:22:52 -04:00
|
|
|
$savedHlLine = $current;
|
|
|
|
$savedHl = $row->hl;
|
2019-10-24 12:01:00 -04:00
|
|
|
// Update the highlight array of the relevant row with the 'MATCH' type
|
2019-10-24 16:22:52 -04:00
|
|
|
array_replace_range($row->hl, $match, strlen($query), Highlight::MATCH);
|
2019-10-24 12:01:00 -04:00
|
|
|
|
2019-10-23 10:36:04 -04:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function find(): void
|
|
|
|
{
|
|
|
|
$savedCx = $this->cursorX;
|
|
|
|
$savedCy = $this->cursorY;
|
|
|
|
$savedColOff = $this->colOffset;
|
|
|
|
$savedRowOff = $this->rowOffset;
|
|
|
|
|
|
|
|
$query = $this->prompt('Search: %s (Use ESC/Arrows/Enter)', [$this, 'findCallback']);
|
|
|
|
|
|
|
|
// If they pressed escape, the query will be empty,
|
|
|
|
// restore original cursor and scroll locations
|
|
|
|
if ($query === '')
|
|
|
|
{
|
|
|
|
$this->cursorX = $savedCx;
|
|
|
|
$this->cursorY = $savedCy;
|
|
|
|
$this->colOffset = $savedColOff;
|
|
|
|
$this->rowOffset = $savedRowOff;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-14 16:21:41 -04:00
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// ! Output
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
|
2019-10-15 13:23:25 -04:00
|
|
|
protected function scroll(): void
|
|
|
|
{
|
2019-10-16 16:43:15 -04:00
|
|
|
$this->renderX = 0;
|
|
|
|
if ($this->cursorY < $this->numRows)
|
|
|
|
{
|
|
|
|
$this->renderX = $this->rowCxToRx($this->rows[$this->cursorY], $this->cursorX);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Vertical Scrolling
|
|
|
|
if ($this->cursorY < $this->rowOffset)
|
2019-10-15 13:23:25 -04:00
|
|
|
{
|
2019-10-16 16:43:15 -04:00
|
|
|
$this->rowOffset = $this->cursorY;
|
|
|
|
}
|
2019-10-18 16:20:34 -04:00
|
|
|
if ($this->cursorY >= ($this->rowOffset + $this->screenRows))
|
2019-10-16 16:43:15 -04:00
|
|
|
{
|
|
|
|
$this->rowOffset = $this->cursorY - $this->screenRows + 1;
|
2019-10-15 13:23:25 -04:00
|
|
|
}
|
|
|
|
|
2019-10-16 16:43:15 -04:00
|
|
|
// Horizontal Scrolling
|
|
|
|
if ($this->renderX < $this->colOffset)
|
|
|
|
{
|
|
|
|
$this->colOffset = $this->renderX;
|
|
|
|
}
|
2019-10-18 16:20:34 -04:00
|
|
|
if ($this->renderX >= ($this->colOffset + $this->screenCols))
|
2019-10-15 13:23:25 -04:00
|
|
|
{
|
2019-10-16 16:43:15 -04:00
|
|
|
$this->colOffset = $this->renderX - $this->screenCols + 1;
|
2019-10-15 13:23:25 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-14 16:21:41 -04:00
|
|
|
protected function drawRows(): void
|
|
|
|
{
|
|
|
|
for ($y = 0; $y < $this->screenRows; $y++)
|
|
|
|
{
|
2019-10-16 16:43:15 -04:00
|
|
|
$filerow = $y + $this->rowOffset;
|
2019-10-18 16:20:34 -04:00
|
|
|
if ($filerow >= $this->numRows)
|
2019-10-14 16:21:41 -04:00
|
|
|
{
|
2019-10-16 16:43:15 -04:00
|
|
|
if ($this->numRows === 0 && $y === $this->screenRows / 3)
|
2019-10-14 16:21:41 -04:00
|
|
|
{
|
2019-10-15 13:23:25 -04:00
|
|
|
$welcome = sprintf('PHP Kilo editor -- version %s', KILO_VERSION);
|
|
|
|
$welcomelen = strlen($welcome);
|
|
|
|
if ($welcomelen > $this->screenCols)
|
|
|
|
{
|
|
|
|
$welcomelen = $this->screenCols;
|
|
|
|
}
|
2019-10-14 16:21:41 -04:00
|
|
|
|
2019-10-15 13:23:25 -04:00
|
|
|
$padding = ($this->screenCols - $welcomelen) / 2;
|
2019-10-16 16:43:15 -04:00
|
|
|
if ($padding > 0)
|
2019-10-15 13:23:25 -04:00
|
|
|
{
|
|
|
|
$this->ab .= '~';
|
|
|
|
$padding--;
|
|
|
|
}
|
2019-10-18 16:20:34 -04:00
|
|
|
for (; $padding >= 0; $padding--)
|
2019-10-15 13:23:25 -04:00
|
|
|
{
|
|
|
|
$this->ab .= ' ';
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->ab .= substr($welcome, 0, $welcomelen);
|
2019-10-14 16:21:41 -04:00
|
|
|
}
|
2019-10-15 13:23:25 -04:00
|
|
|
else
|
2019-10-14 16:21:41 -04:00
|
|
|
{
|
2019-10-15 13:23:25 -04:00
|
|
|
$this->ab .= '~';
|
2019-10-14 16:21:41 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2019-10-16 16:43:15 -04:00
|
|
|
$len = $this->rows[$filerow]->rsize - $this->colOffset;
|
2019-10-15 13:23:25 -04:00
|
|
|
if ($len < 0)
|
|
|
|
{
|
|
|
|
$len = 0;
|
|
|
|
}
|
|
|
|
if ($len > $this->screenCols)
|
|
|
|
{
|
|
|
|
$len = $this->screenCols;
|
|
|
|
}
|
|
|
|
|
2019-10-23 13:36:16 -04:00
|
|
|
$c = substr($this->rows[$filerow]->render, $this->colOffset, $len);
|
2019-10-23 16:10:56 -04:00
|
|
|
$hl = array_slice($this->rows[$filerow]->hl, $this->colOffset, $len);
|
|
|
|
|
|
|
|
$currentColor = -1;
|
2019-10-23 13:36:16 -04:00
|
|
|
|
|
|
|
for ($i = 0; $i < $len; $i++)
|
|
|
|
{
|
2019-10-23 16:10:56 -04:00
|
|
|
if ($hl[$i] === Highlight::NORMAL)
|
2019-10-23 13:36:16 -04:00
|
|
|
{
|
2019-10-23 16:10:56 -04:00
|
|
|
if ($currentColor !== -1)
|
|
|
|
{
|
2019-10-24 16:22:52 -04:00
|
|
|
$this->ab .= "\x1b[39m"; // Reset foreground color
|
|
|
|
$this->ab .= "\x1b[0m"; // Reset background color
|
2019-10-23 16:10:56 -04:00
|
|
|
$currentColor = -1;
|
|
|
|
}
|
2019-10-23 13:36:16 -04:00
|
|
|
$this->ab .= $c[$i];
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2019-10-24 10:58:38 -04:00
|
|
|
$color = syntax_to_color($hl[$i]);
|
2019-10-23 16:10:56 -04:00
|
|
|
if ($color !== $currentColor)
|
|
|
|
{
|
|
|
|
$currentColor = $color;
|
2019-10-24 16:22:52 -04:00
|
|
|
$this->ab .= "\x1b[0m"; // Reset background color
|
2019-10-23 16:10:56 -04:00
|
|
|
$this->ab .= sprintf("\x1b[%dm", $color);
|
|
|
|
}
|
2019-10-23 13:36:16 -04:00
|
|
|
$this->ab .= $c[$i];
|
|
|
|
}
|
|
|
|
}
|
2019-10-23 16:10:56 -04:00
|
|
|
|
|
|
|
$this->ab .= "\x1b[39m";
|
2019-10-24 16:22:52 -04:00
|
|
|
$this->ab .= "\x1b[0m";
|
2019-10-14 16:21:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
$this->ab .= "\x1b[K"; // Clear the current line
|
2019-10-16 16:43:15 -04:00
|
|
|
$this->ab .= "\r\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function drawStatusBar(): void
|
|
|
|
{
|
|
|
|
$this->ab .= "\x1b[7m";
|
|
|
|
|
|
|
|
$statusFilename = $this->filename !== '' ? $this->filename : '[No Name]';
|
2019-10-22 12:09:11 -04:00
|
|
|
$isDirty = ($this->dirty > 0) ? '(modified)' : '';
|
|
|
|
$status = sprintf('%.20s - %d lines %s', $statusFilename, $this->numRows, $isDirty);
|
|
|
|
$rstatus = sprintf('%d/%d', $this->cursorY + 1, $this->numRows);
|
2019-10-16 16:43:15 -04:00
|
|
|
$len = strlen($status);
|
|
|
|
$rlen = strlen($rstatus);
|
|
|
|
if ($len > $this->screenCols)
|
|
|
|
{
|
|
|
|
$len = $this->screenCols;
|
|
|
|
}
|
|
|
|
$this->ab .= substr($status, 0, $len);
|
|
|
|
while ($len < $this->screenCols)
|
|
|
|
{
|
|
|
|
if ($this->screenCols - $len === $rlen)
|
2019-10-14 16:21:41 -04:00
|
|
|
{
|
2019-10-16 16:43:15 -04:00
|
|
|
$this->ab .= substr($rstatus, 0, $rlen);
|
|
|
|
break;
|
2019-10-14 16:21:41 -04:00
|
|
|
}
|
2019-10-16 16:43:15 -04:00
|
|
|
|
|
|
|
$this->ab .= ' ';
|
|
|
|
$len++;
|
|
|
|
}
|
|
|
|
$this->ab .= "\x1b[m";
|
|
|
|
$this->ab .= "\r\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function drawMessageBar(): void
|
|
|
|
{
|
|
|
|
$this->ab .= "\x1b[K";
|
|
|
|
$len = strlen($this->statusMsg);
|
|
|
|
if ($len > $this->screenCols)
|
|
|
|
{
|
|
|
|
$len = $this->screenCols;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($len > 0 && (time() - $this->statusMsgTime) < 5)
|
|
|
|
{
|
|
|
|
$this->ab .= substr($this->statusMsg, 0, $len);
|
2019-10-14 16:21:41 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function refreshScreen(): void
|
|
|
|
{
|
2019-10-15 13:23:25 -04:00
|
|
|
$this->scroll();
|
|
|
|
|
2019-10-14 16:21:41 -04:00
|
|
|
$this->ab = '';
|
|
|
|
|
|
|
|
$this->ab .= "\x1b[?25l"; // Hide the cursor
|
|
|
|
$this->ab .= "\x1b[H"; // Reposition cursor to top-left
|
|
|
|
|
|
|
|
$this->drawRows();
|
2019-10-16 16:43:15 -04:00
|
|
|
$this->drawStatusBar();
|
|
|
|
$this->drawMessageBar();
|
2019-10-14 16:21:41 -04:00
|
|
|
|
|
|
|
// Specify the current cursor position
|
2019-10-16 16:43:15 -04:00
|
|
|
$this->ab .= sprintf("\x1b[%d;%dH",
|
|
|
|
($this->cursorY - $this->rowOffset) + 1,
|
|
|
|
($this->renderX - $this->colOffset) + 1
|
|
|
|
);
|
2019-10-14 16:21:41 -04:00
|
|
|
|
|
|
|
$this->ab .= "\x1b[?25h"; // Show the cursor
|
|
|
|
|
2019-10-16 16:43:15 -04:00
|
|
|
echo $this->ab;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function setStatusMessage(string $fmt, ...$args): void
|
|
|
|
{
|
|
|
|
$this->statusMsg = (count($args) > 0)
|
|
|
|
? sprintf($fmt, ...$args)
|
|
|
|
: $fmt;
|
|
|
|
$this->statusMsgTime = time();
|
2019-10-14 16:21:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// ! Input
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
|
2019-10-23 10:36:04 -04:00
|
|
|
protected function prompt(string $prompt, ?callable $callback = NULL): string
|
2019-10-22 16:16:28 -04:00
|
|
|
{
|
|
|
|
$buffer = '';
|
2019-10-23 10:36:04 -04:00
|
|
|
$modifiers = Key::getConstList();
|
2019-10-22 16:16:28 -04:00
|
|
|
while (TRUE)
|
|
|
|
{
|
|
|
|
$this->setStatusMessage($prompt, $buffer);
|
|
|
|
$this->refreshScreen();
|
|
|
|
|
|
|
|
$c = $this->readKey();
|
|
|
|
$cord = ord($c);
|
|
|
|
|
|
|
|
if ($c === Key::ESCAPE)
|
|
|
|
{
|
|
|
|
$this->setStatusMessage('');
|
2019-10-23 10:36:04 -04:00
|
|
|
if ($callback !== NULL)
|
|
|
|
{
|
|
|
|
$callback($buffer, $c);
|
|
|
|
}
|
2019-10-22 16:16:28 -04:00
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($c === Key::ENTER && $buffer !== '')
|
|
|
|
{
|
|
|
|
$this->setStatusMessage('');
|
2019-10-23 10:36:04 -04:00
|
|
|
if ($callback !== NULL)
|
|
|
|
{
|
|
|
|
$callback($buffer, $c);
|
|
|
|
}
|
2019-10-22 16:16:28 -04:00
|
|
|
return $buffer;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($c === Key::DEL_KEY || $c === Key::BACKSPACE || $c === chr(ctrl_key('h')))
|
|
|
|
{
|
|
|
|
$buffer = substr($buffer, 0, -1);
|
|
|
|
}
|
2019-10-24 10:58:38 -04:00
|
|
|
else if (is_ascii($c) && ( ! is_cntrl($c)) && ! in_array($c, $modifiers, TRUE))
|
2019-10-22 16:16:28 -04:00
|
|
|
{
|
|
|
|
$buffer .= $c;
|
|
|
|
}
|
2019-10-23 10:36:04 -04:00
|
|
|
|
|
|
|
if ($callback !== NULL)
|
|
|
|
{
|
|
|
|
$callback($buffer, $c);
|
|
|
|
}
|
2019-10-22 16:16:28 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-14 16:21:41 -04:00
|
|
|
protected function moveCursor(string $key): void
|
|
|
|
{
|
2019-10-16 16:43:15 -04:00
|
|
|
$row = ($this->cursorY >= $this->numRows)
|
|
|
|
? NULL
|
|
|
|
: $this->rows[$this->cursorY];
|
|
|
|
|
2019-10-14 16:21:41 -04:00
|
|
|
switch ($key)
|
|
|
|
{
|
|
|
|
case Key::ARROW_LEFT:
|
2019-10-16 16:43:15 -04:00
|
|
|
if ($this->cursorX !== 0)
|
2019-10-14 16:21:41 -04:00
|
|
|
{
|
2019-10-16 16:43:15 -04:00
|
|
|
$this->cursorX--;
|
|
|
|
}
|
|
|
|
else if ($this->cursorX > 0)
|
|
|
|
{
|
|
|
|
$this->cursorY--;
|
|
|
|
$this->cursorX = $this->rows[$this->cursorY]->size;
|
2019-10-14 16:21:41 -04:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Key::ARROW_RIGHT:
|
2019-10-16 16:43:15 -04:00
|
|
|
if ($row && $this->cursorX < $row->size)
|
|
|
|
{
|
|
|
|
$this->cursorX++;
|
|
|
|
}
|
|
|
|
else if ($row && $this->cursorX === $row->size)
|
2019-10-14 16:21:41 -04:00
|
|
|
{
|
2019-10-16 16:43:15 -04:00
|
|
|
$this->cursorY++;
|
|
|
|
$this->cursorX = 0;
|
2019-10-14 16:21:41 -04:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Key::ARROW_UP:
|
2019-10-16 16:43:15 -04:00
|
|
|
if ($this->cursorY !== 0)
|
2019-10-14 16:21:41 -04:00
|
|
|
{
|
2019-10-16 16:43:15 -04:00
|
|
|
$this->cursorY--;
|
2019-10-14 16:21:41 -04:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case Key::ARROW_DOWN:
|
2019-10-16 16:43:15 -04:00
|
|
|
if ($this->cursorY < $this->numRows)
|
2019-10-14 16:21:41 -04:00
|
|
|
{
|
2019-10-16 16:43:15 -04:00
|
|
|
$this->cursorY++;
|
2019-10-14 16:21:41 -04:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2019-10-16 16:43:15 -04:00
|
|
|
|
|
|
|
$row = ($this->cursorY >= $this->numRows)
|
|
|
|
? NULL
|
|
|
|
: $this->rows[$this->cursorY];
|
|
|
|
$rowlen = $row ? $row->size : 0;
|
|
|
|
if ($this->cursorX > $rowlen)
|
|
|
|
{
|
|
|
|
$this->cursorX = $rowlen;
|
|
|
|
}
|
2019-10-14 16:21:41 -04:00
|
|
|
}
|
|
|
|
|
2019-10-16 22:14:30 -04:00
|
|
|
public function processKeypress(): ?string
|
2019-10-14 16:21:41 -04:00
|
|
|
{
|
2019-10-22 12:09:11 -04:00
|
|
|
static $quit_times = KILO_QUIT_TIMES;
|
|
|
|
|
2019-10-14 16:21:41 -04:00
|
|
|
$c = $this->readKey();
|
2019-10-16 22:14:30 -04:00
|
|
|
|
|
|
|
if ($c === "\0")
|
|
|
|
{
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
2019-10-16 16:43:15 -04:00
|
|
|
switch ($c)
|
2019-10-14 16:21:41 -04:00
|
|
|
{
|
2019-10-22 12:09:11 -04:00
|
|
|
case Key::ENTER:
|
2019-10-22 16:16:28 -04:00
|
|
|
$this->insertNewline();
|
2019-10-22 12:09:11 -04:00
|
|
|
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;
|
|
|
|
|
2019-10-14 16:21:41 -04:00
|
|
|
case Key::HOME_KEY:
|
2019-10-16 16:43:15 -04:00
|
|
|
$this->cursorX = 0;
|
2019-10-14 16:21:41 -04:00
|
|
|
break;
|
|
|
|
|
|
|
|
case Key::END_KEY:
|
2019-10-16 16:43:15 -04:00
|
|
|
if ($this->cursorY < $this->numRows)
|
|
|
|
{
|
2019-10-21 15:37:20 -04:00
|
|
|
$this->cursorX = $this->rows[$this->cursorY]->size - 1;
|
2019-10-16 16:43:15 -04:00
|
|
|
}
|
2019-10-14 16:21:41 -04:00
|
|
|
break;
|
|
|
|
|
2019-10-23 10:36:04 -04:00
|
|
|
case chr(ctrl_key('f')):
|
|
|
|
$this->find();
|
|
|
|
break;
|
|
|
|
|
2019-10-22 12:09:11 -04:00
|
|
|
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;
|
|
|
|
|
2019-10-14 16:21:41 -04:00
|
|
|
case Key::PAGE_UP:
|
|
|
|
case Key::PAGE_DOWN:
|
2019-10-15 13:23:25 -04:00
|
|
|
$this->pageUpOrDown($c);
|
2019-10-14 16:21:41 -04:00
|
|
|
break;
|
|
|
|
|
|
|
|
case Key::ARROW_UP:
|
|
|
|
case Key::ARROW_DOWN:
|
|
|
|
case Key::ARROW_LEFT:
|
|
|
|
case Key::ARROW_RIGHT:
|
|
|
|
$this->moveCursor($c);
|
|
|
|
break;
|
2019-10-16 16:43:15 -04:00
|
|
|
|
2019-10-22 12:09:11 -04:00
|
|
|
case chr(ctrl_key('l')):
|
|
|
|
case Key::ESCAPE:
|
|
|
|
// Do nothing
|
2019-10-16 16:43:15 -04:00
|
|
|
break;
|
2019-10-18 16:20:34 -04:00
|
|
|
|
|
|
|
default:
|
2019-10-22 12:09:11 -04:00
|
|
|
$this->insertChar($c);
|
|
|
|
break;
|
2019-10-14 16:21:41 -04:00
|
|
|
}
|
|
|
|
|
2019-10-22 12:09:11 -04:00
|
|
|
$quit_times = KILO_QUIT_TIMES;
|
|
|
|
|
2019-10-14 16:21:41 -04:00
|
|
|
return $c;
|
|
|
|
}
|
2019-10-15 13:23:25 -04:00
|
|
|
|
2019-10-16 16:43:15 -04:00
|
|
|
private function pageUpOrDown(string $c): void
|
2019-10-15 13:23:25 -04:00
|
|
|
{
|
2019-10-16 16:43:15 -04:00
|
|
|
if ($c === Key::PAGE_UP)
|
|
|
|
{
|
|
|
|
$this->cursorY = $this->rowOffset;
|
|
|
|
}
|
|
|
|
else if ($c === Key::PAGE_DOWN)
|
|
|
|
{
|
|
|
|
$this->cursorY = $this->rowOffset + $this->screenRows - 1;
|
|
|
|
if ($this->cursorY > $this->numRows)
|
|
|
|
{
|
|
|
|
$this->cursorY = $this->numRows;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-15 13:23:25 -04:00
|
|
|
$times = $this->screenRows;
|
2019-10-18 16:20:34 -04:00
|
|
|
for (; $times > 0; $times--)
|
2019-10-15 13:23:25 -04:00
|
|
|
{
|
|
|
|
$this->moveCursor($c === Key::PAGE_UP ? Key::ARROW_UP : Key::ARROW_DOWN);
|
|
|
|
}
|
|
|
|
}
|
2019-10-14 16:21:41 -04:00
|
|
|
}
|