php-kilo/src/Editor.php

921 lines
19 KiB
PHP
Raw Normal View History

2019-10-14 16:21:41 -04:00
<?php declare(strict_types=1);
2019-11-08 16:27:08 -05:00
namespace Aviat\Kilo;
use Aviat\Kilo\Enum\{Color, KeyCode, KeyType, Highlight};
2021-03-03 11:50:29 -05:00
use Aviat\Kilo\Tokens\PHP8;
2019-10-14 16:21:41 -04:00
/**
2019-10-25 16:36:03 -04:00
* // Don't highlight this!
2021-03-03 16:35:58 -05:00
* @property-read int $numRows
*/
2019-10-14 16:21:41 -04:00
class Editor {
2019-11-19 15:57:51 -05:00
use Traits\MagicProperties;
private string $ab = '';
2019-10-14 16:21:41 -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
*/
2019-10-25 16:36:03 -04:00
public array $rows = [];
2019-10-15 13:23:25 -04:00
2019-10-25 10:28:15 -04:00
public int $dirty = 0;
public string $filename = '';
protected string $statusMsg = '';
protected int $statusMsgTime;
2020-01-23 13:41:35 -05:00
public ?Syntax $syntax = NULL;
2019-10-25 10:28:15 -04:00
// Tokens for highlighting PHP
public array $tokens = [];
public static function new(): Editor
2019-10-15 13:23:25 -04:00
{
return new self();
2019-10-15 13:23:25 -04:00
}
private function __construct()
2019-10-14 16:21:41 -04:00
{
$this->statusMsgTime = time();
2019-10-14 16:21:41 -04:00
[$this->screenRows, $this->screenCols] = get_window_size();
2019-11-19 15:57:51 -05:00
// Remove a row for the status bar, and one for the message bar
$this->screenRows -= 2;
}
2020-12-04 11:18:21 -05:00
public function __get(string $name): ?int
{
if ($name === 'numRows')
{
return count($this->rows);
}
return NULL;
2019-10-14 16:21:41 -04:00
}
2019-11-20 15:03:48 -05:00
public function __debugInfo(): array
{
return [
'colOffset' => $this->colOffset,
'cursorX' => $this->cursorX,
'cursorY' => $this->cursorY,
'dirty' => $this->dirty,
'filename' => $this->filename,
'renderX' => $this->renderX,
'rowOffset' => $this->rowOffset,
'rows' => $this->rows,
'screenCols' => $this->screenCols,
'screenRows' => $this->screenRows,
'statusMsg' => $this->statusMsg,
'syntax' => $this->syntax,
'tokens' => $this->tokens,
];
}
2019-10-14 16:21:41 -04:00
// ------------------------------------------------------------------------
// ! Terminal
// ------------------------------------------------------------------------
protected function readKey(): string
{
$c = read_stdin();
2019-10-14 16:21:41 -04:00
2021-03-03 11:50:29 -05:00
return match($c)
{
2020-02-05 16:32:17 -05:00
// 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,
2020-02-05 16:32:17 -05:00
// Backspace
2021-03-03 11:50:29 -05:00
KeyCode::CTRL('h'), KeyCode::BACKSPACE => KeyType::BACKSPACE,
2020-02-05 16:32:17 -05:00
2020-12-04 11:18:21 -05:00
// Escape
2021-03-03 11:50:29 -05:00
KeyCode::CTRL('l'), KeyCode::ESCAPE => KeyType::ESCAPE,
2020-12-04 11:18:21 -05:00
2020-02-05 16:32:17 -05:00
// Home Key
2021-03-03 11:50:29 -05:00
"\eOH", "\e[7~", "\e[1~", ANSI::RESET_CURSOR => KeyType::HOME_KEY,
2019-10-14 16:21:41 -04:00
2020-02-05 16:32:17 -05:00
// End Key
2021-03-03 11:50:29 -05:00
"\eOF", "\e[4~", "\e[8~", "\e[F" => KeyType::END_KEY,
2019-10-14 16:21:41 -04:00
2021-03-03 11:50:29 -05:00
default => $c,
};
2019-10-14 16:21:41 -04:00
}
2019-10-25 10:28:15 -04:00
protected function selectSyntaxHighlight(): void
{
$this->syntax = NULL;
if (empty($this->filename))
{
return;
}
// In PHP, `strchr` and `strstr` are the same function
2020-02-05 14:50:31 -05:00
$ext = (string)strstr(basename($this->filename), '.');
2019-10-25 10:28:15 -04:00
2019-11-08 21:48:46 -05:00
foreach (get_file_syntax_map() as $syntax)
2019-10-25 10:28:15 -04:00
{
2020-02-05 14:50:31 -05:00
if (
in_array($ext, $syntax->filematch, TRUE) ||
in_array(basename($this->filename), $syntax->filematch, TRUE)
) {
$this->syntax = $syntax;
2020-02-05 14:50:31 -05:00
// Pre-tokenize the file
if ($this->syntax->filetype === 'PHP')
{
2021-03-03 11:50:29 -05:00
$this->tokens = PHP8::getFileTokens($this->filename);
2019-10-25 10:28:15 -04:00
}
2020-02-05 14:50:31 -05:00
$this->refreshSyntax();
return;
2019-10-25 10:28:15 -04:00
}
}
}
2019-10-15 13:23:25 -04:00
// ------------------------------------------------------------------------
// ! Row Operations
// ------------------------------------------------------------------------
protected function rowCxToRx(Row $row, int $cx): int
{
$rx = 0;
for ($i = 0; $i < $cx; $i++)
{
2020-02-05 14:50:31 -05:00
if ($row->chars[$i] === KeyCode::TAB)
{
$rx += (KILO_TAB_STOP - 1) - ($rx % KILO_TAB_STOP);
}
$rx++;
}
return $rx;
}
protected function rowRxToCx(Row $row, int $rx): int
{
$cur_rx = 0;
for ($cx = 0; $cx < $row->size; $cx++)
{
2020-02-05 14:50:31 -05:00
if ($row->chars[$cx] === KeyCode::TAB)
{
$cur_rx += (KILO_TAB_STOP - 1) - ($cur_rx % KILO_TAB_STOP);
}
$cur_rx++;
if ($cur_rx > $rx)
{
return $cx;
}
}
return $cx;
}
protected function insertRow(int $at, string $s, bool $updateSyntax = TRUE): void
{
2019-10-22 16:16:28 -04:00
if ($at < 0 || $at > $this->numRows)
{
return;
}
2019-10-25 16:36:03 -04:00
$row = Row::new($this, $s, $at);
2019-10-22 16:16:28 -04:00
if ($at === $this->numRows)
{
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),
];
}
ksort($this->rows);
2019-11-05 12:28:10 -05:00
$this->rows[$at]->update();
$this->dirty++;
// Re-tokenize the file
2019-11-19 15:57:51 -05:00
if ($updateSyntax)
{
$this->refreshPHPSyntax();
}
2019-10-22 12:09:11 -04:00
}
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);
2019-10-25 16:36:03 -04:00
for ($i = $at; $i < $this->numRows; $i++)
{
$this->rows[$i]->idx--;
}
2019-10-22 12:09:11 -04:00
// Re-tokenize the file
2019-11-19 15:57:51 -05:00
$this->refreshPHPSyntax();
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
}
2019-10-25 10:28:15 -04:00
$this->rows[$this->cursorY]->insertChar($this->cursorX, $c);
2019-10-22 12:09:11 -04:00
// Re-tokenize the file
2019-11-19 15:57:51 -05:00
$this->refreshPHPSyntax();
$this->cursorX++;
}
protected function insertNewline(): void
{
2020-12-04 11:18:21 -05:00
// @TODO attempt smart indentation on newline?
2019-10-22 16:16:28 -04:00
if ($this->cursorX === 0)
{
2019-11-05 12:28:10 -05:00
$this->insertRow($this->cursorY, '');
2019-10-22 16:16:28 -04:00
}
else
{
$row = $this->rows[$this->cursorY];
$chars = $row->chars;
2019-11-05 12:28:10 -05:00
$newChars = substr($chars, 0, $this->cursorX);
2019-10-22 16:16:28 -04:00
2019-11-05 12:28:10 -05:00
// Truncate the previous row
$row->chars = $newChars;
2019-10-22 16:16:28 -04:00
2019-11-05 12:28:10 -05:00
// Add a new row, with the contents from the cursor to the end of the line
$this->insertRow($this->cursorY + 1, substr($chars, $this->cursorX));
}
2019-11-05 12:28:10 -05:00
$this->cursorY++;
$this->cursorX = 0;
// Re-tokenize the file
2019-11-19 15:57:51 -05:00
$this->refreshPHPSyntax();
2019-10-22 16:16:28 -04:00
}
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)
{
2019-10-25 10:28:15 -04:00
$row->deleteChar($this->cursorX - 1);
2019-10-22 12:09:11 -04:00
$this->cursorX--;
}
else
{
$this->cursorX = $this->rows[$this->cursorY - 1]->size;
2019-10-25 10:28:15 -04:00
$this->rows[$this->cursorY -1]->appendString($row->chars);
2019-10-22 12:09:11 -04:00
$this->deleteRow($this->cursorY);
$this->cursorY--;
}
// Re-tokenize the file
2019-11-19 15:57:51 -05:00
$this->refreshPHPSyntax();
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
{
$lines = array_map(fn (Row $row) => (string)$row, $this->rows);
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-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-25 10:28:15 -04:00
$this->selectSyntaxHighlight();
2019-10-15 13:23:25 -04:00
2019-10-22 16:44:55 -04:00
$handle = fopen($filename, 'rb');
2019-10-22 16:16:28 -04:00
if ($handle === FALSE)
{
2020-02-05 14:50:31 -05:00
$this->setStatusMessage('Failed to open file: %s', $filename);
return;
2019-10-22 16:16:28 -04:00
}
2019-10-15 13:23:25 -04:00
while (($line = fgets($handle)) !== FALSE)
{
// Remove line endings when reading the file
$this->insertRow($this->numRows, rtrim($line), FALSE);
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-25 10:28:15 -04:00
$this->selectSyntaxHighlight();
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
}
// ------------------------------------------------------------------------
// ! Find
// ------------------------------------------------------------------------
protected function findCallback(string $query, string $key): void
{
static $lastMatch = -1;
static $direction = 1;
static $savedHlLine = 0;
static $savedHl = [];
if ( ! empty($savedHl))
{
$this->rows[$savedHlLine]->hl = $savedHl;
$savedHl = [];
}
2020-02-05 14:50:31 -05:00
switch ($key)
{
2020-02-05 14:50:31 -05:00
case KeyCode::ENTER:
case KeyCode::ESCAPE:
$lastMatch = -1;
$direction = 1;
return;
2020-02-05 14:50:31 -05:00
case KeyType::ARROW_DOWN:
case KeyType::ARROW_RIGHT:
$direction = 1;
break;
case KeyType::ARROW_UP:
case KeyType::ARROW_LEFT:
$direction = -1;
break;
default:
$lastMatch = -1;
$direction = 1;
}
if ($lastMatch === -1)
{
$direction = 1;
}
2019-10-25 16:36:03 -04:00
$current = $lastMatch;
2020-12-04 11:18:21 -05:00
if (empty($query))
{
return;
}
for ($i = 0; $i < $this->numRows; $i++)
{
$current += $direction;
if ($current === -1)
{
$current = $this->numRows - 1;
}
else if ($current === $this->numRows)
{
$current = 0;
}
2020-02-05 16:32:17 -05:00
$row =& $this->rows[$current];
$match = strpos($row->render, $query);
if ($match !== FALSE)
{
$lastMatch = $current;
$this->cursorY = $current;
2020-02-05 16:32:17 -05:00
$this->cursorX = $this->rowRxToCx($row, $match);
$this->rowOffset = $this->numRows;
$savedHlLine = $current;
2020-02-05 16:32:17 -05:00
$savedHl = $row->hl;
// Update the highlight array of the relevant row with the 'MATCH' type
2020-02-05 16:32:17 -05:00
array_replace_range($row->hl, $match, strlen($query), Highlight::MATCH);
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
{
$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
{
$this->rowOffset = $this->cursorY;
}
2019-10-18 16:20:34 -04:00
if ($this->cursorY >= ($this->rowOffset + $this->screenRows))
{
$this->rowOffset = $this->cursorY - $this->screenRows + 1;
2019-10-15 13:23:25 -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
{
$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++)
{
$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-11-05 13:51:21 -05:00
if ($this->numRows === 0 && $y === (int)($this->screenRows / 2))
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;
if ($padding > 0)
2019-10-15 13:23:25 -04:00
{
$this->ab .= '~';
$padding--;
}
2019-10-25 16:36:03 -04:00
for ($i = 0; $i < $padding; $i++)
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
{
$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);
$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-25 15:34:56 -04:00
// Handle 'non-printable' characters
2020-02-05 14:50:31 -05:00
if (is_ctrl($c[$i]))
2019-10-25 15:34:56 -04:00
{
$sym = (ord($c[$i]) <= 26)
? chr(ord('@') + ord($c[$i]))
: '?';
$this->ab .= ANSI::color(Color::INVERT);
2019-10-25 15:34:56 -04:00
$this->ab .= $sym;
$this->ab .= ANSI::RESET_TEXT;
2019-10-25 15:34:56 -04:00
if ($currentColor !== -1)
{
$this->ab .= ANSI::color($currentColor);
2019-10-25 15:34:56 -04:00
}
}
else if ($hl[$i] === Highlight::NORMAL)
2019-10-23 13:36:16 -04:00
{
if ($currentColor !== -1)
{
$this->ab .= ANSI::RESET_TEXT;
$this->ab .= ANSI::color(Color::FG_WHITE);
$currentColor = -1;
}
2019-10-23 13:36:16 -04:00
$this->ab .= $c[$i];
}
else
{
$color = syntax_to_color($hl[$i]);
if ($color !== $currentColor)
{
$currentColor = $color;
$this->ab .= ANSI::RESET_TEXT;
$this->ab .= ANSI::color($color);
}
2019-10-23 13:36:16 -04:00
$this->ab .= $c[$i];
}
}
$this->ab .= ANSI::RESET_TEXT;
$this->ab .= ANSI::color(Color::FG_WHITE);
2019-10-14 16:21:41 -04:00
}
$this->ab .= ANSI::CLEAR_LINE;
$this->ab .= "\r\n";
}
}
protected function drawStatusBar(): void
{
$this->ab .= ANSI::color(Color::INVERT);
$statusFilename = $this->filename !== '' ? $this->filename : '[No Name]';
2019-10-25 10:28:15 -04:00
$syntaxType = ($this->syntax !== NULL) ? $this->syntax->filetype : 'no ft';
2019-10-22 12:09:11 -04:00
$isDirty = ($this->dirty > 0) ? '(modified)' : '';
$status = sprintf('%.20s - %d lines %s', $statusFilename, $this->numRows, $isDirty);
2019-10-25 10:28:15 -04:00
$rstatus = sprintf('%s | %d/%d', $syntaxType, $this->cursorY + 1, $this->numRows);
$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
{
$this->ab .= substr($rstatus, 0, $rlen);
break;
2019-10-14 16:21:41 -04:00
}
$this->ab .= ' ';
$len++;
}
$this->ab .= ANSI::RESET_TEXT;
$this->ab .= "\r\n";
}
protected function drawMessageBar(): void
{
$this->ab .= ANSI::CLEAR_LINE;
$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 .= ANSI::HIDE_CURSOR;
$this->ab .= ANSI::RESET_CURSOR;
2019-10-14 16:21:41 -04:00
$this->drawRows();
$this->drawStatusBar();
$this->drawMessageBar();
2019-10-14 16:21:41 -04:00
// Specify the current cursor position
$this->ab .= ANSI::moveCursor(
($this->cursorY - $this->rowOffset) + 1,
($this->renderX - $this->colOffset) + 1
);
2019-10-14 16:21:41 -04:00
$this->ab .= ANSI::SHOW_CURSOR;
2019-10-14 16:21:41 -04:00
echo $this->ab;
}
2021-03-03 11:50:29 -05:00
public function setStatusMessage(string $fmt, mixed ...$args): void
{
$this->statusMsg = (count($args) > 0)
? sprintf($fmt, ...$args)
: $fmt;
$this->statusMsgTime = time();
2019-10-14 16:21:41 -04:00
}
// ------------------------------------------------------------------------
// ! Input
// ------------------------------------------------------------------------
protected function prompt(string $prompt, ?callable $callback = NULL): string
2019-10-22 16:16:28 -04:00
{
$buffer = '';
$modifiers = KeyType::getConstList();
2019-10-22 16:16:28 -04:00
while (TRUE)
{
$this->setStatusMessage($prompt, $buffer);
$this->refreshScreen();
$c = $this->readKey();
2020-02-05 16:32:17 -05:00
$isModifier = in_array($c, $modifiers, TRUE);
2019-10-22 16:16:28 -04:00
2020-02-05 14:50:31 -05:00
if ($c === KeyType::ESCAPE || ($c === KeyType::ENTER && $buffer !== ''))
2019-10-22 16:16:28 -04:00
{
$this->setStatusMessage('');
if ($callback !== NULL)
{
$callback($buffer, $c);
}
2020-12-04 11:18:21 -05:00
return ($c === KeyType::ENTER) ? $buffer : '';
2019-10-22 16:16:28 -04:00
}
2020-02-05 16:32:17 -05:00
if ($c === KeyType::DEL_KEY || $c === KeyType::BACKSPACE)
2019-10-22 16:16:28 -04:00
{
$buffer = substr($buffer, 0, -1);
}
2020-02-05 16:32:17 -05:00
else if (is_ascii($c) && ( ! (is_ctrl($c) || $isModifier)))
2019-10-22 16:16:28 -04:00
{
$buffer .= $c;
}
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
{
$row = ($this->cursorY >= $this->numRows)
? NULL
: $this->rows[$this->cursorY];
2019-10-14 16:21:41 -04:00
switch ($key)
{
case KeyType::ARROW_LEFT:
if ($this->cursorX !== 0)
2019-10-14 16:21:41 -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 KeyType::ARROW_RIGHT:
if ($row && $this->cursorX < $row->size)
{
$this->cursorX++;
}
else if ($row && $this->cursorX === $row->size)
2019-10-14 16:21:41 -04:00
{
$this->cursorY++;
$this->cursorX = 0;
2019-10-14 16:21:41 -04:00
}
break;
case KeyType::ARROW_UP:
if ($this->cursorY !== 0)
2019-10-14 16:21:41 -04:00
{
$this->cursorY--;
2019-10-14 16:21:41 -04:00
}
break;
case KeyType::ARROW_DOWN:
if ($this->cursorY < $this->numRows)
2019-10-14 16:21:41 -04:00
{
$this->cursorY++;
2019-10-14 16:21:41 -04:00
}
break;
}
$row = ($this->cursorY >= $this->numRows)
? NULL
: $this->rows[$this->cursorY];
2020-12-04 11:18:21 -05:00
$rowlen = $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
2020-02-05 14:50:31 -05:00
if ($c === KeyCode::NULL || $c === KeyCode::EMPTY)
2019-10-16 22:14:30 -04:00
{
return '';
}
switch ($c)
2019-10-14 16:21:41 -04:00
{
case KeyType::ENTER:
2019-10-22 16:16:28 -04:00
$this->insertNewline();
2019-10-22 12:09:11 -04:00
break;
2020-02-05 16:32:17 -05:00
case KeyCode::CTRL('q'):
2019-10-22 12:09:11 -04:00
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(ANSI::CLEAR_SCREEN);
write_stdout(ANSI::RESET_CURSOR);
2019-10-22 12:09:11 -04:00
return NULL;
break;
2020-02-05 16:32:17 -05:00
case KeyCode::CTRL('s'):
2019-10-22 12:09:11 -04:00
$this->save();
break;
case KeyType::HOME_KEY:
$this->cursorX = 0;
2019-10-14 16:21:41 -04:00
break;
case KeyType::END_KEY:
if ($this->cursorY < $this->numRows)
{
$this->cursorX = $this->rows[$this->cursorY]->size - 1;
}
2019-10-14 16:21:41 -04:00
break;
2020-02-05 16:32:17 -05:00
case KeyCode::CTRL('f'):
$this->find();
break;
case KeyType::BACKSPACE:
case KeyType::DEL_KEY:
if ($c === KeyType::DEL_KEY)
2019-10-22 12:09:11 -04:00
{
$this->moveCursor(KeyType::ARROW_RIGHT);
2019-10-22 12:09:11 -04:00
}
$this->deleteChar();
break;
case KeyType::PAGE_UP:
case KeyType::PAGE_DOWN:
2019-10-15 13:23:25 -04:00
$this->pageUpOrDown($c);
2019-10-14 16:21:41 -04:00
break;
case KeyType::ARROW_UP:
case KeyType::ARROW_DOWN:
case KeyType::ARROW_LEFT:
case KeyType::ARROW_RIGHT:
2019-10-14 16:21:41 -04:00
$this->moveCursor($c);
break;
2020-02-05 16:32:17 -05:00
case KeyCode::CTRL('l'):
case KeyType::ESCAPE:
2019-10-22 12:09:11 -04:00
// Do nothing
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
2020-02-05 14:50:31 -05:00
public function pageUpOrDown(string $c): void
2019-10-15 13:23:25 -04:00
{
if ($c === KeyType::PAGE_UP)
{
$this->cursorY = $this->rowOffset;
}
else if ($c === KeyType::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 === KeyType::PAGE_UP ? KeyType::ARROW_UP : KeyType::ARROW_DOWN);
2019-10-15 13:23:25 -04:00
}
}
2020-02-05 14:50:31 -05:00
protected function refreshSyntax(): void
{
// Update the syntax highlighting for all the rows of the file
2021-03-03 16:35:58 -05:00
// array_walk($this->rows, static fn (Row $row) => $row->updateSyntax());
2020-02-05 14:50:31 -05:00
}
private function refreshPHPSyntax(): void
{
2021-03-03 16:35:58 -05:00
if ($this->syntax?->filetype !== 'PHP')
2019-11-19 15:57:51 -05:00
{
return;
}
2021-03-03 11:50:29 -05:00
$this->tokens = PHP8::getTokens($this->rowsToString());
2020-02-05 14:50:31 -05:00
$this->refreshSyntax();
}
2019-10-24 20:21:44 -04:00
}