Compare commits

..

No commits in common. "d0817b62c4038742a137cd57bf42ef05dc9e0b32" and "633094ea9ff2349b6ae6deb3c5fce13af81356da" have entirely different histories.

27 changed files with 9710 additions and 10182 deletions

35
composer.lock generated
View File

@ -528,16 +528,16 @@
}, },
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "0.12.81", "version": "0.12.80",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan.git", "url": "https://github.com/phpstan/phpstan.git",
"reference": "0dd5b0ebeff568f7000022ea5f04aa86ad3124b8" "reference": "c6a1b17f22ecf708d434d6bee05092647ec7e686"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/0dd5b0ebeff568f7000022ea5f04aa86ad3124b8", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c6a1b17f22ecf708d434d6bee05092647ec7e686",
"reference": "0dd5b0ebeff568f7000022ea5f04aa86ad3124b8", "reference": "c6a1b17f22ecf708d434d6bee05092647ec7e686",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -568,7 +568,7 @@
"description": "PHPStan - PHP Static Analysis Tool", "description": "PHPStan - PHP Static Analysis Tool",
"support": { "support": {
"issues": "https://github.com/phpstan/phpstan/issues", "issues": "https://github.com/phpstan/phpstan/issues",
"source": "https://github.com/phpstan/phpstan/tree/0.12.81" "source": "https://github.com/phpstan/phpstan/tree/0.12.80"
}, },
"funding": [ "funding": [
{ {
@ -584,7 +584,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2021-03-08T22:03:02+00:00" "time": "2021-02-28T20:22:43+00:00"
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
@ -2994,35 +2994,30 @@
}, },
{ {
"name": "webmozart/assert", "name": "webmozart/assert",
"version": "1.10.0", "version": "1.9.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/webmozarts/assert.git", "url": "https://github.com/webmozarts/assert.git",
"reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
"reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.2 || ^8.0", "php": "^5.3.3 || ^7.0 || ^8.0",
"symfony/polyfill-ctype": "^1.8" "symfony/polyfill-ctype": "^1.8"
}, },
"conflict": { "conflict": {
"phpstan/phpstan": "<0.12.20", "phpstan/phpstan": "<0.12.20",
"vimeo/psalm": "<4.6.1 || 4.6.2" "vimeo/psalm": "<3.9.1"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^8.5.13" "phpunit/phpunit": "^4.8.36 || ^7.5.13"
}, },
"type": "library", "type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.10-dev"
}
},
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Webmozart\\Assert\\": "src/" "Webmozart\\Assert\\": "src/"
@ -3046,9 +3041,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/webmozarts/assert/issues", "issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/1.10.0" "source": "https://github.com/webmozarts/assert/tree/1.9.1"
}, },
"time": "2021-03-09T10:59:23+00:00" "time": "2020-07-08T17:02:28+00:00"
} }
], ],
"aliases": [], "aliases": [],

17
kilo
View File

@ -6,26 +6,9 @@ namespace Aviat\Kilo;
require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/autoload.php';
// Log notices/errors/warnings to file // Log notices/errors/warnings to file
set_error_handler(static function (
$errno,
$errstr,
$errfile,
$errline
) {
$msg = print_r([
'code' => error_code_name($errno),
'message' => $errstr,
'file' => $errfile,
'line' => $errline,
], TRUE);
file_put_contents('kilo.log', $msg, FILE_APPEND);
return true;
}, -1);
set_exception_handler(static function (mixed $e) { set_exception_handler(static function (mixed $e) {
$msg = print_r([ $msg = print_r([
'code' => $e->getCode(), 'code' => $e->getCode(),
'codeName' => error_code_name($e->getCode()),
'message' => $e->getMessage(), 'message' => $e->getMessage(),
'file' => $e->getFile(), 'file' => $e->getFile(),
'line' => $e->getLine(), 'line' => $e->getLine(),

View File

@ -14,7 +14,7 @@
</coverage> </coverage>
<testsuites> <testsuites>
<testsuite name="PHPKilo"> <testsuite name="PHPKilo">
<directory phpVersion="8.0.0" phpVersionOperator="&gt;=">tests</directory> <directory phpVersion="7.4.0" phpVersionOperator="&gt;=">tests</directory>
</testsuite> </testsuite>
</testsuites> </testsuites>
<logging/> <logging/>

View File

@ -27,7 +27,7 @@ class ANSI {
/** /**
* Removes text attributes, such as bold, underline, blink, inverted colors * Removes text attributes, such as bold, underline, blink, inverted colors
*/ */
public const RESET_TEXT = "\e[m"; public const RESET_TEXT = "\e[0m";
public const BOLD_TEXT = "\e[1m"; public const BOLD_TEXT = "\e[1m";

View File

@ -2,9 +2,6 @@
namespace Aviat\Kilo; namespace Aviat\Kilo;
use Aviat\Kilo\Enum\RawKeyCode;
use Aviat\Kilo\Enum\KeyType;
use Aviat\Kilo\Tokens\PHP8;
use Aviat\Kilo\Type\Point; use Aviat\Kilo\Type\Point;
/** /**
@ -13,18 +10,16 @@ use Aviat\Kilo\Type\Point;
* @property-read int $numRows * @property-read int $numRows
*/ */
class Document { class Document {
public FileType $fileType; public ?Syntax $syntax = NULL;
// Tokens for highlighting PHP // Tokens for highlighting PHP
public array $tokens = []; public array $tokens = [];
private function __construct( private function __construct(
public string $filename = '',
public array $rows = [], public array $rows = [],
public bool $dirty = FALSE, public ?string $filename = NULL,
) { private bool $dirty = FALSE,
$this->fileType = FileType::from($this->filename); ) {}
}
public function __get(string $name): ?int public function __get(string $name): ?int
{ {
@ -41,162 +36,21 @@ class Document {
return new self(); return new self();
} }
public function row(int $index): Row public static function open(?string $filename = NULL): self
{ {
return (array_key_exists($index, $this->rows)) // @TODO move logic from Editor
? $this->rows[$index] return new self(filename: $filename);
: Row::default();
} }
public function isEmpty(): bool public function save(): bool
{ {
return empty($this->rows); // @TODO move logic
return false;
} }
// ------------------------------------------------------------------------ public function insertChar(Point $at, string $c): void
// ! File I/O
// ------------------------------------------------------------------------
public function open(string $filename): ?self
{ {
$handle = fopen($filename, 'rb');
if ($handle === FALSE)
{
return NULL;
}
$this->__construct($filename);
while (($line = fgets($handle)) !== FALSE)
{
// Remove line endings when reading the file
$this->rows[] = Row::new($this, rtrim($line), $this->numRows);
}
fclose($handle);
$this->dirty = false;
$this->selectSyntaxHighlight();
return $this;
}
public function save(): int|false
{
$contents = $this->rowsToString();
$res = file_put_contents($this->filename, $contents);
if ($res === strlen($contents))
{
$this->dirty = FALSE;
}
return $res;
}
public function insert(Point $at, string $c): void
{
if ($at->y > $this->numRows)
{
return;
}
if ($c === KeyType::ENTER || $c === RawKeyCode::CARRIAGE_RETURN)
{
$this->insertNewline($at);
$this->dirty = true;
return;
}
$this->rows[$at->y]->insert($at->x, $c);
$this->dirty = true;
}
public function delete(Point $at): void
{
if ($at->y > $this->numRows)
{
return;
}
$row =& $this->rows[$at->y];
if ($at->x === $this->rows[$at->y]->size && $at->y + 1 < $this->numRows)
{
$this->rows[$at->y]->append($this->rows[$at->y + 1]->chars);
$this->deleteRow($at->y + 1);
}
else
{
$row->delete($at->x);
}
$this->dirty = true;
}
public function insertRow(int $at, string $s, bool $updateSyntax = TRUE): void
{
if ($at > $this->numRows)
{
return;
}
$row = Row::new($this, $s, $at);
if ($at === $this->numRows)
{
$this->rows[] = $row;
}
else
{
$this->rows = [
...array_slice($this->rows, 0, $at),
$row,
...array_slice($this->rows, $at),
];
// Update indexes of each row so that correct highlighting is done
for ($idx = $at; $idx < $this->numRows; $idx++)
{
$this->rows[$idx]->idx = $idx;
}
}
ksort($this->rows);
// $this->rows[$at]->highlight();
// Re-tokenize the file
if ($updateSyntax)
{
$this->refreshPHPSyntax();
}
$this->dirty = true;
}
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);
for ($i = $at; $i < $this->numRows; $i++)
{
$this->rows[$i]->idx = $i;
}
// Re-tokenize the file
$this->refreshPHPSyntax();
$this->dirty = true;
} }
public function isDirty(): bool public function isDirty(): bool
@ -204,73 +58,13 @@ class Document {
return $this->dirty; return $this->dirty;
} }
protected function insertNewline(Point $at): void public function deleteChar(Point $at): void
{ {
if ($at->y > $this->numRows)
{
return;
}
if ($at->y === $this->numRows)
{
$this->insertRow($this->numRows, '');
}
else if ($at->x === 1)
{
$this->insertRow($at->y, '');
}
else
{
$row = $this->rows[$at->y];
$chars = $row->chars;
$newChars = substr($chars, 0, $at->x);
// Truncate the previous row
$row->setChars($newChars);
// Add a new row with the contents of the previous row at the point of the split
$this->insertRow($at->y + 1, substr($chars, $at->x));
}
$this->dirty = true;
} }
protected function selectSyntaxHighlight(): void private function insertNewline(Point $at): void
{ {
if (empty($this->filename))
{
return;
}
if ($this->fileType->name === 'PHP')
{
$this->tokens = PHP8::getFileTokens($this->filename);
}
$this->refreshSyntax();
}
protected function rowsToString(): string
{
$lines = array_map(fn (Row $row) => (string)$row, $this->rows);
return implode('', $lines);
}
public function refreshSyntax(): void
{
// Update the syntax highlighting for all the rows of the file
array_walk($this->rows, static fn (Row $row) => $row->update());
}
private function refreshPHPSyntax(): void
{
if ($this->fileType->name !== 'PHP')
{
return;
}
$this->tokens = PHP8::getTokens($this->rowsToString());
$this->refreshSyntax();
} }
} }

View File

@ -3,19 +3,17 @@
namespace Aviat\Kilo; namespace Aviat\Kilo;
use Aviat\Kilo\Type\TerminalSize; use Aviat\Kilo\Type\TerminalSize;
use Aviat\Kilo\Enum\{ use Aviat\Kilo\Enum\{Color, KeyCode, KeyType, Highlight};
Color, use Aviat\Kilo\Tokens\PHP8;
RawKeyCode,
KeyType,
Highlight,
SearchDirection
};
use Aviat\Kilo\Type\{Point, StatusMessage}; use Aviat\Kilo\Type\{Point, StatusMessage};
/** /**
* // Don't highlight this! * // Don't highlight this!
* @property-read int $numRows
*/ */
class Editor { class Editor {
use Traits\MagicProperties;
/** /**
* @var string The screen buffer * @var string The screen buffer
*/ */
@ -61,6 +59,19 @@ class Editor {
*/ */
protected int $quitTimes = KILO_QUIT_TIMES; protected int $quitTimes = KILO_QUIT_TIMES;
/**
* Array of Row objects
*/
public array $rows = [];
public bool $dirty = FALSE;
public string $filename = '';
public ?Syntax $syntax = NULL;
// Tokens for highlighting PHP
public array $tokens = [];
/** /**
* Create the Editor instance with CLI arguments * Create the Editor instance with CLI arguments
* *
@ -89,24 +100,22 @@ class Editor {
$this->cursor = Point::new(); $this->cursor = Point::new();
$this->offset = Point::new(); $this->offset = Point::new();
$this->terminalSize = Terminal::size(); $this->terminalSize = Terminal::size();
$this->document = Document::new();
if (is_string($filename)) if (is_string($filename))
{ {
$maybeDocument = Document::new()->open($filename); $this->open($filename);
if ($maybeDocument === NULL)
{
$this->document = Document::new();
$this->setStatusMessage("ERR: Could not open file: {}", $filename);
}
else
{
$this->document = $maybeDocument;
}
} }
else }
public function __get(string $name): ?int
{
if ($name === 'numRows')
{ {
$this->document = Document::new(); return count($this->rows);
} }
return NULL;
} }
public function __debugInfo(): array public function __debugInfo(): array
@ -115,9 +124,14 @@ class Editor {
'cursor' => $this->cursor, 'cursor' => $this->cursor,
'document' => $this->document, 'document' => $this->document,
'offset' => $this->offset, 'offset' => $this->offset,
'dirty' => $this->dirty,
'filename' => $this->filename,
'renderX' => $this->renderX, 'renderX' => $this->renderX,
'rows' => $this->rows,
'terminalSize' => $this->terminalSize, 'terminalSize' => $this->terminalSize,
'statusMessage' => $this->statusMessage, 'statusMessage' => $this->statusMessage,
'syntax' => $this->syntax,
'tokens' => $this->tokens,
]; ];
} }
@ -133,18 +147,71 @@ class Editor {
} }
} }
/** // ------------------------------------------------------------------------
* Set a status message to be displayed, using printf formatting // ! Terminal
* @param string $fmt // ------------------------------------------------------------------------
* @param mixed ...$args protected function readKey(): string
*/
public function setStatusMessage(string $fmt, mixed ...$args): void
{ {
$text = func_num_args() > 1 $c = Terminal::read();
? sprintf($fmt, ...$args)
: $fmt;
$this->statusMessage = StatusMessage::from($text); return match($c)
{
// 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,
// Backspace
KeyCode::CTRL('h'), KeyCode::BACKSPACE => KeyType::BACKSPACE,
// Escape
KeyCode::CTRL('l'), KeyCode::ESCAPE => KeyType::ESCAPE,
// Home Key
"\eOH", "\e[7~", "\e[1~", ANSI::RESET_CURSOR => KeyType::HOME_KEY,
// End Key
"\eOF", "\e[4~", "\e[8~", "\e[F" => KeyType::END_KEY,
default => $c,
};
}
protected function selectSyntaxHighlight(): void
{
$this->syntax = NULL;
if (empty($this->filename))
{
return;
}
// In PHP, `strchr` and `strstr` are the same function
$ext = (string)strstr(basename($this->filename), '.');
foreach (get_file_syntax_map() as $syntax)
{
if (
in_array($ext, $syntax->filematch, TRUE) ||
in_array(basename($this->filename), $syntax->filematch, TRUE)
) {
$this->syntax = $syntax;
// Pre-tokenize the file
if ($this->syntax->filetype === 'PHP')
{
$this->tokens = PHP8::getFileTokens($this->filename);
}
$this->refreshSyntax();
return;
}
}
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@ -163,7 +230,7 @@ class Editor {
$rx = 0; $rx = 0;
for ($i = 0; $i < $cx; $i++) for ($i = 0; $i < $cx; $i++)
{ {
if ($row->chars[$i] === RawKeyCode::TAB) if ($row->chars[$i] === KeyCode::TAB)
{ {
$rx += (KILO_TAB_STOP - 1) - ($rx % KILO_TAB_STOP); $rx += (KILO_TAB_STOP - 1) - ($rx % KILO_TAB_STOP);
} }
@ -185,7 +252,7 @@ class Editor {
$cur_rx = 0; $cur_rx = 0;
for ($cx = 0; $cx < $row->size; $cx++) for ($cx = 0; $cx < $row->size; $cx++)
{ {
if ($row->chars[$cx] === RawKeyCode::TAB) if ($row->chars[$cx] === KeyCode::TAB)
{ {
$cur_rx += (KILO_TAB_STOP - 1) - ($cur_rx % KILO_TAB_STOP); $cur_rx += (KILO_TAB_STOP - 1) - ($cur_rx % KILO_TAB_STOP);
} }
@ -200,13 +267,180 @@ class Editor {
return $cx; return $cx;
} }
protected function insertRow(int $at, string $s, bool $updateSyntax = TRUE): void
{
if ($at > $this->numRows)
{
return;
}
$row = Row::new($this, $s, $at);
if ($at === $this->numRows)
{
$this->rows[] = $row;
}
else
{
$this->rows = [
...array_slice($this->rows, 0, $at),
$row,
...array_slice($this->rows, $at),
];
// Update indexes of each row so that correct highlighting is done
for ($idx = $at; $idx < $this->numRows; $idx++)
{
$this->rows[$idx]->idx = $idx;
}
}
ksort($this->rows);
$this->rows[$at]->update();
$this->dirty = true;
// Re-tokenize the file
if ($updateSyntax)
{
$this->refreshPHPSyntax();
}
}
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);
for ($i = $at; $i < $this->numRows; $i++)
{
$this->rows[$i]->idx = $i;
}
// Re-tokenize the file
$this->refreshPHPSyntax();
$this->dirty = true;
}
// ------------------------------------------------------------------------
// ! Editor Operations
// ------------------------------------------------------------------------
protected function insertChar(string $c): void
{
if ($this->cursor->y === $this->numRows)
{
$this->insertRow($this->numRows, '');
}
$this->rows[$this->cursor->y]->insertChar($this->cursor->x, $c);
// Re-tokenize the file
$this->refreshPHPSyntax();
$this->cursor->x++;
}
protected function insertNewline(): void
{
// @TODO attempt smart indentation on newline?
if ($this->cursor->x === 0)
{
$this->insertRow($this->cursor->y, '');
}
else
{
$row = $this->rows[$this->cursor->y];
$chars = $row->chars;
$newChars = substr($chars, 0, $this->cursor->x);
// Truncate the previous row
$row->chars = $newChars;
// Add a new row, with the contents from the cursor to the end of the line
$this->insertRow($this->cursor->y + 1, substr($chars, $this->cursor->x));
}
$this->cursor->y++;
$this->cursor->x = 0;
// Re-tokenize the file
$this->refreshPHPSyntax();
}
protected function deleteChar(): void
{
if ($this->cursor->y === $this->numRows || ($this->cursor->x === 0 && $this->cursor->y === 0))
{
return;
}
$row = $this->rows[$this->cursor->y];
if ($this->cursor->x > 0)
{
$row->deleteChar($this->cursor->x - 1);
$this->cursor->x--;
}
else
{
$this->cursor->x = $this->rows[$this->cursor->y - 1]->size;
$this->rows[$this->cursor->y -1]->appendString($row->chars);
$this->deleteRow($this->cursor->y);
$this->cursor->y--;
}
// Re-tokenize the file
$this->refreshPHPSyntax();
}
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// ! File I/O // ! File I/O
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
protected function rowsToString(): string
{
$lines = array_map(fn (Row $row) => (string)$row, $this->rows);
return implode('', $lines);
}
protected function open(string $filename): void
{
// Copy filename for display
$this->filename = $filename;
$this->selectSyntaxHighlight();
$handle = fopen($filename, 'rb');
if ($handle === FALSE)
{
$this->setStatusMessage('Failed to open file: %s', $filename);
return;
}
while (($line = fgets($handle)) !== FALSE)
{
// Remove line endings when reading the file
$this->insertRow($this->numRows, rtrim($line), FALSE);
}
fclose($handle);
$this->dirty = false;
}
protected function save(): void protected function save(): void
{ {
if ($this->document->filename === '') if ($this->filename === '')
{ {
$newFilename = $this->prompt('Save as: %s'); $newFilename = $this->prompt('Save as: %s');
if ($newFilename === '') if ($newFilename === '')
@ -215,14 +449,17 @@ class Editor {
return; return;
} }
$this->document->filename = $newFilename; $this->filename = $newFilename;
$this->selectSyntaxHighlight();
} }
$res = $this->document->save(); $contents = $this->rowsToString();
if ($res !== FALSE) $res = file_put_contents($this->filename, $contents);
if ($res === strlen($contents))
{ {
$this->setStatusMessage('%d bytes written to disk', $res); $this->setStatusMessage('%d bytes written to disk', strlen($contents));
$this->dirty = false;
return; return;
} }
@ -235,72 +472,66 @@ class Editor {
protected function findCallback(string $query, string $key): void protected function findCallback(string $query, string $key): void
{ {
static $lastMatch = NO_MATCH; static $lastMatch = -1;
static $direction = SearchDirection::FORWARD; static $direction = 1;
static $savedHlLine = 0; static $savedHlLine = 0;
static $savedHl = []; static $savedHl = [];
if ( ! empty($savedHl)) if ( ! empty($savedHl))
{ {
$row = $this->document->row($savedHlLine); $this->rows[$savedHlLine]->hl = $savedHl;
if ($row->isValid())
{
$row->hl = $savedHl;
}
$savedHl = []; $savedHl = [];
} }
$direction = match ($key) { switch ($key)
KeyType::ARROW_UP, KeyType::ARROW_LEFT => SearchDirection::BACKWARD,
default => SearchDirection::FORWARD
};
$arrowKeys = [KeyType::ARROW_UP, KeyType::ARROW_DOWN, KeyType::ARROW_LEFT, KeyType::ARROW_RIGHT];
// Reset search state with non arrow-key input
if ( ! in_array($key, $arrowKeys, true))
{ {
$lastMatch = NO_MATCH; case KeyCode::ENTER:
$direction = SearchDirection::FORWARD; case KeyCode::ESCAPE:
$lastMatch = -1;
if ($key === RawKeyCode::ENTER || $key === RawKeyCode::ESCAPE) $direction = 1;
{
return; return;
}
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 === NO_MATCH) if ($lastMatch === -1)
{ {
$direction = SearchDirection::FORWARD; $direction = 1;
} }
$current = (int)$lastMatch; $current = $lastMatch;
if (empty($query)) if (empty($query))
{ {
return; return;
} }
for ($i = 0; $i < $this->document->numRows; $i++) for ($i = 0; $i < $this->numRows; $i++)
{ {
$current += $direction; $current += $direction;
if ($current === -1) if ($current === -1)
{ {
$current = $this->document->numRows - 1; $current = $this->numRows - 1;
} }
else if ($current === $this->document->numRows) else if ($current === $this->numRows)
{ {
$current = 0; $current = 0;
} }
$row = $this->document->row($current); $row =& $this->rows[$current];
if ( ! $row->isValid())
{
break;
}
$match = strpos($row->render, $query); $match = strpos($row->render, $query);
if ($match !== FALSE) if ($match !== FALSE)
@ -308,7 +539,7 @@ class Editor {
$lastMatch = $current; $lastMatch = $current;
$this->cursor->y = (int)$current; $this->cursor->y = (int)$current;
$this->cursor->x = $this->rowRxToCx($row, $match); $this->cursor->x = $this->rowRxToCx($row, $match);
$this->offset->y = $this->document->numRows; $this->offset->y = $this->numRows;
$savedHlLine = $current; $savedHlLine = $current;
$savedHl = $row->hl; $savedHl = $row->hl;
@ -343,15 +574,9 @@ class Editor {
protected function scroll(): void protected function scroll(): void
{ {
$this->renderX = 0; $this->renderX = 0;
if ($this->cursor->y < $this->document->numRows) if ($this->cursor->y < $this->numRows)
{ {
$row = $this->document->row($this->cursor->y); $this->renderX = $this->rowCxToRx($this->rows[$this->cursor->y], $this->cursor->x);
if ($row->isValid())
{
$this->renderX = $this->rowCxToRx($row, $this->cursor->x);
}
} }
// Vertical Scrolling // Vertical Scrolling
@ -379,13 +604,13 @@ class Editor {
{ {
for ($y = 0; $y < $this->terminalSize->rows; $y++) for ($y = 0; $y < $this->terminalSize->rows; $y++)
{ {
$fileRow = $y + $this->offset->y; $filerow = $y + $this->offset->y;
$this->outputBuffer .= ANSI::CLEAR_LINE; $this->outputBuffer .= ANSI::CLEAR_LINE;
($fileRow >= $this->document->numRows) ($filerow >= $this->numRows)
? $this->drawPlaceholderRow($y) ? $this->drawPlaceholderRow($y)
: $this->drawRow($fileRow); : $this->drawRow($filerow);
$this->outputBuffer .= "\r\n"; $this->outputBuffer .= "\r\n";
} }
@ -393,13 +618,7 @@ class Editor {
protected function drawRow(int $rowIdx): void protected function drawRow(int $rowIdx): void
{ {
$row = $this->document->row($rowIdx); $len = $this->rows[$rowIdx]->rsize - $this->offset->x;
if ( ! $row->isValid())
{
return;
}
$len = $row->rsize - $this->offset->x;
if ($len < 0) if ($len < 0)
{ {
$len = 0; $len = 0;
@ -409,8 +628,8 @@ class Editor {
$len = $this->terminalSize->cols; $len = $this->terminalSize->cols;
} }
$chars = substr($row->render, $this->offset->x, (int)$len); $chars = substr($this->rows[$rowIdx]->render, $this->offset->x, (int)$len);
$hl = array_slice($row->hl, $this->offset->x, (int)$len); $hl = array_slice($this->rows[$rowIdx]->hl, $this->offset->x, (int)$len);
$currentColor = -1; $currentColor = -1;
@ -461,7 +680,7 @@ class Editor {
protected function drawPlaceholderRow(int $y): void protected function drawPlaceholderRow(int $y): void
{ {
if ($this->document->numRows === 0 && $y === (int)($this->terminalSize->rows / 2)) if ($this->numRows === 0 && $y === (int)($this->terminalSize->rows / 2))
{ {
$welcome = sprintf('PHP Kilo editor -- version %s', KILO_VERSION); $welcome = sprintf('PHP Kilo editor -- version %s', KILO_VERSION);
$welcomelen = strlen($welcome); $welcomelen = strlen($welcome);
@ -493,11 +712,11 @@ class Editor {
{ {
$this->outputBuffer .= ANSI::color(Color::INVERT); $this->outputBuffer .= ANSI::color(Color::INVERT);
$statusFilename = $this->document->filename !== '' ? $this->document->filename : '[No Name]'; $statusFilename = $this->filename !== '' ? $this->filename : '[No Name]';
$syntaxType = $this->document->fileType->name; $syntaxType = ($this->syntax !== NULL) ? $this->syntax->filetype : 'no ft';
$isDirty = $this->document->isDirty() ? '(modified)' : ''; $isDirty = $this->dirty ? '(modified)' : '';
$status = sprintf('%.20s - %d lines %s', $statusFilename, $this->document->numRows, $isDirty); $status = sprintf('%.20s - %d lines %s', $statusFilename, $this->numRows, $isDirty);
$rstatus = sprintf('%s | %d/%d', $syntaxType, $this->cursor->y + 1, $this->document->numRows); $rstatus = sprintf('%s | %d/%d', $syntaxType, $this->cursor->y + 1, $this->numRows);
$len = strlen($status); $len = strlen($status);
$rlen = strlen($rstatus); $rlen = strlen($rstatus);
if ($len > $this->terminalSize->cols) if ($len > $this->terminalSize->cols)
@ -523,14 +742,12 @@ class Editor {
protected function drawMessageBar(): void protected function drawMessageBar(): void
{ {
$this->outputBuffer .= ANSI::CLEAR_LINE; $this->outputBuffer .= ANSI::CLEAR_LINE;
$len = $this->statusMessage->len; $len = strlen($this->statusMessage->text);
if ($len > $this->terminalSize->cols) if ($len > $this->terminalSize->cols)
{ {
$len = $this->terminalSize->cols; $len = $this->terminalSize->cols;
} }
// If there is a message, and it's been less than 5 seconds since
// last screen update, show the message
if ($len > 0 && (time() - $this->statusMessage->time) < 5) if ($len > 0 && (time() - $this->statusMessage->time) < 5)
{ {
$this->outputBuffer .= substr($this->statusMessage->text, 0, $len); $this->outputBuffer .= substr($this->statusMessage->text, 0, $len);
@ -541,7 +758,10 @@ class Editor {
{ {
$this->scroll(); $this->scroll();
$this->outputBuffer = ANSI::HIDE_CURSOR . ANSI::RESET_CURSOR; $this->outputBuffer = '';
$this->outputBuffer .= ANSI::HIDE_CURSOR;
$this->outputBuffer .= ANSI::RESET_CURSOR;
$this->drawRows(); $this->drawRows();
$this->drawStatusBar(); $this->drawStatusBar();
@ -558,6 +778,11 @@ class Editor {
Terminal::write($this->outputBuffer, strlen($this->outputBuffer)); Terminal::write($this->outputBuffer, strlen($this->outputBuffer));
} }
public function setStatusMessage(string $fmt, mixed ...$args): void
{
$this->statusMessage = StatusMessage::from($fmt, ...$args);
}
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// ! Input // ! Input
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@ -571,20 +796,20 @@ class Editor {
$this->setStatusMessage($prompt, $buffer); $this->setStatusMessage($prompt, $buffer);
$this->refreshScreen(); $this->refreshScreen();
$c = Terminal::readKey(); $c = $this->readKey();
$isModifier = in_array($c, $modifiers, TRUE); $isModifier = in_array($c, $modifiers, TRUE);
if ($c === KeyType::ESCAPE || ($c === RawKeyCode::ENTER && $buffer !== '')) if ($c === KeyType::ESCAPE || ($c === KeyType::ENTER && $buffer !== ''))
{ {
$this->setStatusMessage(''); $this->setStatusMessage('');
if ($callback !== NULL) if ($callback !== NULL)
{ {
$callback($buffer, $c); $callback($buffer, $c);
} }
return ($c === RawKeyCode::ENTER) ? $buffer : ''; return ($c === KeyType::ENTER) ? $buffer : '';
} }
if ($c === KeyType::DELETE || $c === KeyType::BACKSPACE) if ($c === KeyType::DEL_KEY || $c === KeyType::BACKSPACE)
{ {
$buffer = substr($buffer, 0, -1); $buffer = substr($buffer, 0, -1);
} }
@ -600,35 +825,127 @@ class Editor {
} }
} }
/** protected function moveCursor(string $key): void
* Input processing {
*/ $x = $this->cursor->x;
$y = $this->cursor->y;
$row = $this->rows[$y];
switch ($key)
{
case KeyType::ARROW_LEFT:
if ($x !== 0)
{
$x--;
}
else if ($y > 0)
{
// Beginning of a line, go to end of previous line
$y--;
$x = $this->rows[$y]->size - 1;
}
break;
case KeyType::ARROW_RIGHT:
if ($row && $x < $row->size)
{
$x++;
}
else if ($row && $x === $row->size)
{
$y++;
$x = 0;
}
break;
case KeyType::ARROW_UP:
if ($y !== 0)
{
$y--;
}
break;
case KeyType::ARROW_DOWN:
if ($y < $this->numRows)
{
$y++;
}
break;
case KeyType::PAGE_UP:
$y = ($y > $this->terminalSize->rows)
? $y - $this->terminalSize->rows
: 0;
break;
case KeyType::PAGE_DOWN:
$y = ($y + $this->terminalSize->rows < $this->numRows)
? $y + $this->terminalSize->rows
: $this->numRows;
break;
case KeyType::HOME_KEY:
$x = 0;
break;
case KeyType::END_KEY:
if ($y < $this->numRows)
{
$x = $this->rows[$y]->size;
}
break;
default:
// Do nothing
}
// Snap cursor to the end of a row when moving
// from a longer row to a shorter one
$row = $this->rows[$y];
$rowLen = ($row !== NULL) ? $row->size : 0;
if ($x > $rowLen)
{
$x = $rowLen;
}
$this->cursor->x = $x;
$this->cursor->y = $y;
}
protected function processKeypress(): void protected function processKeypress(): void
{ {
$c = Terminal::readKey(); $c = $this->readKey();
if ($c === RawKeyCode::NULL || $c === RawKeyCode::EMPTY) if ($c === KeyCode::NULL || $c === KeyCode::EMPTY)
{ {
return; return;
} }
switch ($c) switch ($c)
{ {
case RawKeyCode::CTRL('q'): case KeyCode::CTRL('q'):
$this->quitAttempt(); $this->quitAttempt();
return; return;
case RawKeyCode::CTRL('s'): case KeyCode::CTRL('s'):
$this->save(); $this->save();
break; break;
case RawKeyCode::CTRL('f'): case KeyCode::CTRL('f'):
$this->find(); $this->find();
break; break;
case KeyType::DELETE: case KeyType::ENTER:
$this->insertNewline();
break;
case KeyType::BACKSPACE: case KeyType::BACKSPACE:
$this->removeChar($c); case KeyType::DEL_KEY:
if ($c === KeyType::DEL_KEY)
{
$this->moveCursor(KeyType::ARROW_RIGHT);
}
$this->deleteChar();
break; break;
case KeyType::ARROW_UP: case KeyType::ARROW_UP:
@ -637,12 +954,12 @@ class Editor {
case KeyType::ARROW_RIGHT: case KeyType::ARROW_RIGHT:
case KeyType::PAGE_UP: case KeyType::PAGE_UP:
case KeyType::PAGE_DOWN: case KeyType::PAGE_DOWN:
case KeyType::HOME: case KeyType::HOME_KEY:
case KeyType::END: case KeyType::END_KEY:
$this->moveCursor($c); $this->moveCursor($c);
break; break;
case RawKeyCode::CTRL('l'): case KeyCode::CTRL('l'):
case KeyType::ESCAPE: case KeyType::ESCAPE:
// Do nothing // Do nothing
break; break;
@ -660,133 +977,14 @@ class Editor {
} }
} }
// ------------------------------------------------------------------------
// ! Editor operation helpers
// ------------------------------------------------------------------------
protected function moveCursor(string $key): void
{
$x = $this->cursor->x;
$y = $this->cursor->y;
$row = $this->document->row($y);
if ( ! $row->isValid())
{
return;
}
switch ($key)
{
case KeyType::ARROW_LEFT:
if ($x !== 0)
{
$x--;
}
else if ($y > 0)
{
// Beginning of a line, go to end of previous line
$y--;
$x = $row->size - 1;
}
break;
case KeyType::ARROW_RIGHT:
if ($x < $row->size)
{
$x++;
}
else if ($x === $row->size)
{
$y++;
$x = 0;
}
break;
case KeyType::ARROW_UP:
if ($y !== 0)
{
$y--;
}
break;
case KeyType::ARROW_DOWN:
if ($y < $this->document->numRows)
{
$y++;
}
break;
case KeyType::PAGE_UP:
$y = saturating_sub($y, $this->terminalSize->rows);
break;
case KeyType::PAGE_DOWN:
$y = saturating_add($y, $this->terminalSize->rows, $this->document->numRows);
break;
case KeyType::HOME:
$x = 0;
break;
case KeyType::END:
if ($y < $this->document->numRows)
{
$x = $row->size;
}
break;
default:
// Do nothing
}
// Snap cursor to the end of a row when moving
// from a longer row to a shorter one
$row = $this->document->row($y);
if ($row->isValid())
{
if ($x > $row->size)
{
$x = $row->size;
}
$this->cursor->x = $x;
$this->cursor->y = $y;
}
}
protected function insertChar(string $c): void
{
$this->document->insert($this->cursor, $c);
$this->moveCursor(KeyType::ARROW_RIGHT);
}
protected function removeChar(string $ch): void
{
if ($ch === KeyType::DELETE)
{
$this->document->delete($this->cursor);
}
if ($ch === KeyType::BACKSPACE && ($this->cursor->x > 0 || $this->cursor->y > 0))
{
$this->moveCursor(KeyType::ARROW_LEFT);
$this->document->delete($this->cursor);
}
}
protected function quitAttempt(): void protected function quitAttempt(): void
{ {
if ($this->document->isDirty() && $this->quitTimes > 0) if ($this->dirty && $this->quitTimes > 0)
{ {
if ($this->quitTimes === KILO_QUIT_TIMES)
{
Terminal::ding();
}
$this->setStatusMessage( $this->setStatusMessage(
'WARNING!!! File has unsaved changes. Press Ctrl-Q %d more times to quit.', 'WARNING!!! File has unsaved changes. Press Ctrl-Q %d more times to quit.',
$this->quitTimes $this->quitTimes
); );
$this->quitTimes--; $this->quitTimes--;
return; return;
} }
@ -795,4 +993,21 @@ class Editor {
$this->shouldQuit = true; $this->shouldQuit = true;
} }
protected function refreshSyntax(): void
{
// Update the syntax highlighting for all the rows of the file
array_walk($this->rows, static fn (Row $row) => $row->update());
}
private function refreshPHPSyntax(): void
{
if ($this->syntax?->filetype !== 'PHP')
{
return;
}
$this->tokens = PHP8::getTokens($this->rowsToString());
$this->refreshSyntax();
}
} }

View File

@ -23,7 +23,6 @@ class Highlight {
public const INVALID = 10; public const INVALID = 10;
public const MATCH = 11; public const MATCH = 11;
public const IDENTIFIER = 12; public const IDENTIFIER = 12;
public const CHARACTER = 13;
/** /**
* Map a PHP syntax token to its associated highlighting type * Map a PHP syntax token to its associated highlighting type

View File

@ -9,7 +9,7 @@ use function Aviat\Kilo\ctrl_key;
* 'Raw' input from stdin * 'Raw' input from stdin
* @enum * @enum
*/ */
class RawKeyCode { class KeyCode {
use Traits\ConstList; use Traits\ConstList;
public const ARROW_DOWN = "\e[B"; public const ARROW_DOWN = "\e[B";
@ -17,9 +17,8 @@ class RawKeyCode {
public const ARROW_RIGHT = "\e[C"; public const ARROW_RIGHT = "\e[C";
public const ARROW_UP = "\e[A"; public const ARROW_UP = "\e[A";
public const BACKSPACE = "\x7f"; public const BACKSPACE = "\x7f";
public const BELL = "\a";
public const CARRIAGE_RETURN = "\r"; public const CARRIAGE_RETURN = "\r";
public const DELETE = "\e[3~"; public const DEL_KEY = "\e[3~";
public const EMPTY = ''; public const EMPTY = '';
public const ENTER = "\r"; public const ENTER = "\r";
public const ESCAPE = "\e"; public const ESCAPE = "\e";

View File

@ -11,16 +11,16 @@ use Aviat\Kilo\Traits;
class KeyType { class KeyType {
use Traits\ConstList; use Traits\ConstList;
public const ARROW_DOWN = 'KEY_ARROW_DOWN'; public const ARROW_DOWN = 'ARROW_DOWN';
public const ARROW_LEFT = 'KEY_ARROW_LEFT'; public const ARROW_LEFT = 'ARROW_LEFT';
public const ARROW_RIGHT = 'KEY_ARROW_RIGHT'; public const ARROW_RIGHT = 'ARROW_RIGHT';
public const ARROW_UP = 'KEY_ARROW_UP'; public const ARROW_UP = 'ARROW_UP';
public const BACKSPACE = 'KEY_BACKSPACE'; public const BACKSPACE = 'BACKSPACE';
public const DELETE = 'KEY_DELETE'; public const DEL_KEY = 'DELETE';
public const END = 'KEY_END'; public const END_KEY = 'END';
public const ENTER = 'KEY_ENTER'; public const ENTER = 'ENTER';
public const ESCAPE = 'KEY_ESCAPE'; public const ESCAPE = 'ESCAPE';
public const HOME = 'KEY_HOME'; public const HOME_KEY = 'HOME';
public const PAGE_DOWN = 'KEY_PAGE_DOWN'; public const PAGE_DOWN = 'PAGE_DOWN';
public const PAGE_UP = 'KEY_PAGE_UP'; public const PAGE_UP = 'PAGE_UP';
} }

View File

@ -1,15 +0,0 @@
<?php declare(strict_types=1);
namespace Aviat\Kilo\Enum;
use Aviat\Kilo\Traits;
/**
* @enum
*/
class SearchDirection {
use Traits\ConstList;
public const FORWARD = 1;
public const BACKWARD = -1;
}

View File

@ -1,108 +0,0 @@
<?php declare(strict_types=1);
namespace Aviat\Kilo;
class FileType {
public static function from(?string $filename): self
{
$syntax = self::getSyntaxFromFilename((string)$filename);
return new self($syntax->filetype, $syntax);
}
private static function getSyntaxFromFilename(string $filename): Syntax
{
$ext = strstr(basename($filename), '.');
$ext = ($ext !== FALSE) ? $ext : '';
return match ($ext) {
'.sh', '.bash' => Syntax::new(
'Shell',
slcs: '#',
mcs: '',
mce: '',
),
'.php', 'kilo' => Syntax::new(
'PHP',
),
'.c', '.h', '.cpp', '.cxx', '.cc', '.hpp' => Syntax::new(
'C',
[
'auto', 'break', 'case', 'const', 'continue', 'default', 'do', 'typedef', 'switch', 'return',
'static', 'while', 'break', 'struct', 'extern', 'union', 'class', 'else', 'enum', 'for', 'case',
'if', 'inline', 'register', 'restrict', 'return', 'sizeof', 'switch', 'typedef', 'union', 'volatile'
],
[
'#include', 'unsigned', '#define', '#ifndef', 'double', 'signed', '#endif',
'#ifdef', 'float', '#error', '#undef', '#elif', 'long', 'char', 'int', 'void', '#if',
'uint32_t', 'wchar_t', 'int32_t', 'int64_t', 'uint64_t', 'int16_t', 'uint16_t',
'uint8_t', 'int8_t',
],
[
'<=>', '<<=', '>>=',
'++', '--', '==', '!=', '>=', '<=', '&&', '||', '<<', '>>',
'+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '->', '::',
],
hasCharType: true,
highlightCharacters: true,
),
'.css', '.less', '.sass', '.scss' => Syntax::new(
'CSS',
slcs: '',
),
'.go' => Syntax::new(
'Go',
[
'break', 'case', 'chan', 'const', 'continue', 'default', 'defer', 'else',
'fallthrough', 'for', 'func', 'go', 'goto', 'if', 'import', 'interface',
'map', 'package', 'range', 'return', 'select', 'struct', 'switch', 'type', 'var',
],
[
'uint8', 'uint16', 'uint32', 'uint64', 'int8', 'int16', 'int32', 'int64', 'float32', 'float64',
'uint', 'int', 'uintptr', 'complex64', 'complex128', 'byte', 'rune', '[]',
],
[
'&^=', '...', '>>=', '<<=', '--', '%=', '>>', ':=', '++', '/=', '<<', '>=',
'<-', '^=', '*=', '<=', '||', '|=', '-=', '!=', '==', '&&', '&=', '+=',
],
hasCharType: true,
highlightCharacters: true,
),
'.js', '.jsx', '.ts', '.tsx', '.jsm', '.mjs', '.es' => Syntax::new(
'JavaScript',
[
'instanceof', 'continue', 'debugger', 'function', 'default', 'extends',
'finally', 'delete', 'export', 'import', 'return', 'switch', 'typeof',
'break', 'catch', 'class', 'const', 'super', 'throw', 'while', 'yield',
'case', 'else', 'this', 'void', 'with', 'from', 'for', 'new', 'try',
'var', 'do', 'if', 'in', 'as',
],
[
'=>', 'Number', 'String', 'Object', 'Math', 'JSON', 'Boolean',
],
),
'.rs' => Syntax::new(
'Rust',
[
'continue', 'return', 'static', 'struct', 'unsafe', 'break', 'const', 'crate',
'extern', 'match', 'super', 'trait', 'where', 'else', 'enum', 'false', 'impl',
'loop', 'move', 'self', 'type', 'while', 'for', 'let', 'mod', 'pub', 'ref', 'true',
'use', 'mut', 'as', 'fn', 'if', 'in',
],
[
'DoubleEndedIterator', 'ExactSizeIterator', 'IntoIterator', 'PartialOrd', 'PartialEq',
'Iterator', 'ToString', 'Default', 'ToOwned', 'Extend', 'FnOnce', 'Option', 'String',
'AsMut', 'AsRef', 'Clone', 'Debug', 'FnMut', 'Sized', 'Unpin', 'array', 'isize',
'usize', '&str', 'Copy', 'Drop', 'From', 'Into', 'None', 'Self', 'Send', 'Some',
'Sync', 'bool', 'char', 'i128', 'u128', 'Box', 'Err', 'Ord', 'Vec', 'dyn', 'f32',
'f64', 'i16', 'i32', 'i64', 'str', 'u16', 'u32', 'u64', 'Eq', 'Fn', 'Ok', 'i8', 'u8',
],
hasCharType: true,
highlightCharacters: true,
),
default => Syntax::default(),
};
}
private function __construct(public string $name, public Syntax $syntax) {}
}

View File

@ -3,7 +3,7 @@
namespace Aviat\Kilo; namespace Aviat\Kilo;
use Aviat\Kilo\Enum\Highlight; use Aviat\Kilo\Enum\Highlight;
use Aviat\Kilo\Enum\RawKeyCode; use Aviat\Kilo\Enum\KeyCode;
/** /**
* @property-read int $size * @property-read int $size
@ -11,71 +11,35 @@ use Aviat\Kilo\Enum\RawKeyCode;
* @property-read string $chars * @property-read string $chars
*/ */
class Row { class Row {
// use Traits\MagicProperties; use Traits\MagicProperties;
/** private string $chars = '';
* The version of the row to be displayed (where tabs are converted to display spaces)
*/
public string $render = ''; public string $render = '';
/**
* The mapping of characters to their highlighting type
*/
public array $hl = []; public array $hl = [];
/** public int $idx;
* Are we in the middle of highlighting a multi-line comment?
*/ // This feels dirty...
private Editor $parent;
private bool $hlOpenComment = FALSE; private bool $hlOpenComment = FALSE;
/** private const T_RAW = -1;
* Create a row in the current document
* public static function new(Editor $parent, string $chars, int $idx): self
* @param Document $parent
* @param string $chars
* @param int $idx
* @return self
*/
public static function new(Document $parent, string $chars, int $idx): self
{ {
return new self( $self = new self();
$parent, $self->chars = $chars;
$chars, $self->parent = $parent;
$idx, $self->idx = $idx;
);
return $self;
} }
/** private function __construct() {
* Create an empty Row // Private in favor of ::new static function
*
* @return self
*/
public static function default(): self
{
return new self(
Document::new(),
'',
0,
);
} }
private function __construct(
/**
* The document that this row belongs to
*/
private Document $parent,
/**
* @var string The raw characters in the row
*/
private string $chars,
/**
* @var int The line number of the current row
*/
public int $idx,
) {}
public function __get(string $name): mixed public function __get(string $name): mixed
{ {
return match ($name) return match ($name)
@ -87,21 +51,20 @@ class Row {
}; };
} }
/** public function __set(string $name, mixed $value): void
* Convert the row contents to a string for saving {
* if ($name === 'chars')
* @return string {
*/ $this->chars = $value;
$this->update();
}
}
public function __toString(): string public function __toString(): string
{ {
return $this->chars . "\n"; return $this->chars . "\n";
} }
/**
* Set the properties to display for var_dump
*
* @return array
*/
public function __debugInfo(): array public function __debugInfo(): array
{ {
return [ return [
@ -114,52 +77,30 @@ class Row {
]; ];
} }
/** public function insertChar(int $at, string $c): void
* Is this row a valid part of a document?
*
* @return bool
*/
public function isValid(): bool
{
return ! $this->parent->isEmpty();
}
/**
* Insert the string or character $c at index $at
*
* @param int $at
* @param string $c
*/
public function insert(int $at, string $c): void
{ {
if ($at < 0 || $at > $this->size) if ($at < 0 || $at > $this->size)
{ {
$this->append($c); $this->appendString($c);
return; return;
} }
// Safely insert into arbitrary position in the existing string // Safely insert into arbitrary position in the existing string
$this->chars = substr($this->chars, 0, $at) . $c . substr($this->chars, $at); $this->chars = substr($this->chars, 0, $at) . $c . substr($this->chars, $at);
$this->update(); $this->update();
$this->parent->dirty = true;
} }
/** public function appendString(string $s): void
* Append $s to the current row
*
* @param string $s
*/
public function append(string $s): void
{ {
$this->chars .= $s; $this->chars .= $s;
$this->update(); $this->update();
$this->parent->dirty = true;
} }
/** public function deleteChar(int $at): void
* Delete the character at the specified index
*
* @param int $at
*/
public function delete(int $at): void
{ {
if ($at < 0 || $at >= $this->size) if ($at < 0 || $at >= $this->size)
{ {
@ -168,53 +109,48 @@ class Row {
$this->chars = substr_replace($this->chars, '', $at, 1); $this->chars = substr_replace($this->chars, '', $at, 1);
$this->update(); $this->update();
$this->parent->dirty = true;
} }
public function setChars(string $chars): void
{
$this->chars = $chars;
$this->update();
}
/**
* Convert tabs to spaces for display, and update syntax highlighting
*/
public function update(): void public function update(): void
{ {
$this->render = tabs_to_spaces($this->chars); $this->render = tabs_to_spaces($this->chars);
$this->highlight();
$this->updateSyntax();
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// ! Syntax Highlighting // ! Syntax Highlighting
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
/** public function updateSyntax(): void
* Parse the current file to apply syntax highlighting
*/
public function highlight(): void
{ {
$this->hl = array_fill(0, $this->rsize, Highlight::NORMAL); $this->hl = array_fill(0, $this->rsize, Highlight::NORMAL);
if ($this->parent->fileType->name === 'PHP') if ($this->parent->syntax === NULL)
{
$this->highlightPHP();
return;
}
if ($this->parent->fileType->name === 'No filetype')
{ {
return; return;
} }
$syntax = $this->parent->fileType->syntax; if ($this->parent->syntax->filetype === 'PHP')
{
$this->updateSyntaxPHP();
return;
}
$mcs = $syntax->multiLineCommentStart; $keywords1 = $this->parent->syntax->keywords1;
$mce = $syntax->multiLineCommentEnd; $keywords2 = $this->parent->syntax->keywords2;
$scs = $this->parent->syntax->singleLineCommentStart;
$mcs = $this->parent->syntax->multiLineCommentStart;
$mce = $this->parent->syntax->multiLineCommentEnd;
$scsLen = strlen($scs);
$mcsLen = strlen($mcs); $mcsLen = strlen($mcs);
$mceLen = strlen($mce); $mceLen = strlen($mce);
$prevSep = TRUE;
$inString = ''; $inString = '';
$inComment = ($this->idx > 0 && $this->parent->rows[$this->idx - 1]->hlOpenComment); $inComment = ($this->idx > 0 && $this->parent->rows[$this->idx - 1]->hlOpenComment);
@ -222,8 +158,19 @@ class Row {
while ($i < $this->rsize) while ($i < $this->rsize)
{ {
$char = $this->render[$i];
$prevHl = ($i > 0) ? $this->hl[$i - 1] : Highlight::NORMAL;
// Single-line comments
if ($scsLen > 0 && $inString === '' && $inComment === FALSE
&& substr($this->render, $i, $scsLen) === $scs)
{
array_replace_range($this->hl, $i, $this->rsize - $i, Highlight::COMMENT);
break;
}
// Multi-line comments // Multi-line comments
if ($syntax->mlComments() && $inString === '') if ($mcsLen > 0 && $mceLen > 0 && $inString === '')
{ {
if ($inComment) if ($inComment)
{ {
@ -233,6 +180,7 @@ class Row {
array_replace_range($this->hl, $i, $mceLen, Highlight::ML_COMMENT); array_replace_range($this->hl, $i, $mceLen, Highlight::ML_COMMENT);
$i += $mceLen; $i += $mceLen;
$inComment = FALSE; $inComment = FALSE;
$prevSep = TRUE;
continue; continue;
} }
@ -249,20 +197,80 @@ class Row {
} }
} }
if ( // String/Char literals
$this->highlightCharacter($i, $syntax) if ($this->parent->syntax->flags & Syntax::HIGHLIGHT_STRINGS)
|| $this->highlightComment($i, $syntax) {
|| $this->highlightPrimaryKeywords($i, $syntax) if ($inString !== '')
|| $this->highlightSecondaryKeywords($i, $syntax) {
|| $this->highlightString($i, $syntax) $this->hl[$i] = Highlight::STRING;
|| $this->highlightOperators($i, $syntax)
|| $this->highlightCommonDelimeters($i) // Check for escaped character
|| $this->highlightCommonOperators($i) if ($char === '\\' && $i+1 < $this->rsize)
|| $this->highlightNumber($i, $syntax) {
) { $this->hl[$i + 1] = Highlight::STRING;
continue; $i += 2;
continue;
}
if ($char === $inString)
{
$inString = '';
}
$i++;
$prevSep = 1;
continue;
}
if ( $char === '"' || $char === '\'')
{
$inString = $char;
$this->hl[$i] = Highlight::STRING;
$i++;
continue;
}
} }
// Numbers, including decimal points
if ($this->parent->syntax->flags & Syntax::HIGHLIGHT_NUMBERS)
{
if (
($char === '.' && $prevHl === Highlight::NUMBER) ||
(($prevSep || $prevHl === Highlight::NUMBER) && is_digit($char))
)
{
$this->hl[$i] = Highlight::NUMBER;
$i++;
$prevSep = FALSE;
continue;
}
}
// Keywords
if ($prevSep)
{
$findKeywords = function (array $keywords, int $syntaxType) use (&$i): void
{
foreach ($keywords as $k)
{
$klen = strlen($k);
$nextCharOffset = $i + $klen;
$isEndOfLine = $nextCharOffset >= $this->rsize;
$nextChar = ($isEndOfLine) ? KeyCode::NULL : $this->render[$nextCharOffset];
if (substr($this->render, $i, $klen) === $k && is_separator($nextChar))
{
array_replace_range($this->hl, $i, $klen, $syntaxType);
$i += $klen - 1;
break;
}
}
};
$findKeywords($keywords1, Highlight::KEYWORD1);
$findKeywords($keywords2, Highlight::KEYWORD2);
}
$prevSep = is_separator($char);
$i++; $i++;
} }
@ -270,227 +278,13 @@ class Row {
$this->hlOpenComment = $inComment; $this->hlOpenComment = $inComment;
if ($changed && $this->idx + 1 < $this->parent->numRows) if ($changed && $this->idx + 1 < $this->parent->numRows)
{ {
$this->parent->rows[$this->idx + 1]->update(); // @codeCoverageIgnoreStart
$this->parent->rows[$this->idx + 1]->updateSyntax();
// @codeCoverageIgnoreEnd
} }
} }
protected function highlightNumber(int &$i, Syntax $opts): bool protected function updateSyntaxPHP():void
{
$char = $this->render[$i];
if ($opts->numbers() && is_digit($char))
{
if ($i > 0)
{
$prevChar = $this->render[$i - 1];
if ( ! is_separator($prevChar))
{
return false;
}
}
while (true)
{
$this->hl[$i] = Highlight::NUMBER;
$i++;
if ($i < strlen($this->render))
{
$nextChar = $this->render[$i];
if ($nextChar !== '.' && ! is_digit($nextChar))
{
return true;
}
continue;
}
break;
}
}
return false;
}
protected function highlightWord(int &$i, array $keywords, int $syntaxType): bool
{
if ($i > 0)
{
$prevChar = $this->render[$i - 1];
if ( ! is_separator($prevChar))
{
return false;
}
}
foreach ($keywords as $k)
{
$klen = strlen($k);
$nextCharOffset = $i + $klen;
$isEndOfLine = $nextCharOffset >= $this->rsize;
$nextChar = ($isEndOfLine) ? RawKeyCode::NULL : $this->render[$nextCharOffset];
if (substr($this->render, $i, $klen) === $k && is_separator($nextChar))
{
array_replace_range($this->hl, $i, $klen, $syntaxType);
$i += $klen;
return true;
}
}
return false;
}
protected function highlightChar(int &$i, array $chars, int $syntaxType): bool
{
if ($this->hl[$i] !== Highlight::NORMAL)
{
return false;
}
$char = $this->render[$i];
if (in_array($char, $chars, TRUE))
{
$this->hl[$i] = $syntaxType;
$i++;
return true;
}
return false;
}
protected function highlightPrimaryKeywords(int &$i, Syntax $opts): bool
{
return $this->highlightWord($i, $opts->keywords1, Highlight::KEYWORD1);
}
protected function highlightSecondaryKeywords(int &$i, Syntax $opts): bool
{
return $this->highlightWord($i, $opts->keywords2, Highlight::KEYWORD2);
}
protected function highlightOperators(int &$i, Syntax $opts): bool
{
return $this->highlightWord($i, $opts->operators, Highlight::OPERATOR);
}
protected function highlightCommonOperators(int &$i): bool
{
return $this->highlightChar(
$i,
['+', '-', '*', '/', '<', '^', '>', '%', '=', ':', ',', ';', '&', '~'],
Highlight::OPERATOR
);
}
protected function highlightCommonDelimeters(int &$i): bool
{
return $this->highlightChar(
$i,
['{', '}', '[', ']', '(', ')'],
Highlight::DELIMITER
);
}
protected function highlightCharacter(int &$i, Syntax $opts): bool
{
if (($i + 1) >= $this->rsize)
{
return false;
}
$char = $this->render[$i];
$nextChar = $this->render[$i + 1];
if ($opts->characters() && $char === "'")
{
$offset = ($nextChar === '\\') ? $i + 2 : $i + 1;
$closingIndex = strpos($this->render, "'", $offset);
if ($closingIndex === false)
{
return false;
}
$closingChar = $this->render[$closingIndex];
if ($closingChar === "'")
{
array_replace_range($this->hl, $i, $closingIndex - $i + 1, Highlight::CHARACTER);
$i = $closingIndex + 1;
return true;
}
}
return false;
}
protected function highlightComment(int &$i, Syntax $opts): bool
{
if ( ! $opts->comments())
{
return false;
}
$scs = $opts->singleLineCommentStart;
$scsLen = strlen($scs);
if ($scsLen > 0 && substr($this->render, $i, $scsLen) === $scs)
{
array_replace_range($this->hl, $i, $this->rsize - $i, Highlight::COMMENT);
$i = $this->rsize;
return true;
}
return false;
}
protected function highlightString(int &$i, Syntax $opts): bool
{
$char = $this->render[$i];
// If there's a separate character type, highlight that separately
if ($opts->hasChar() && $char === "'")
{
return false;
}
if ($opts->strings() && ($char === '"' || $char === '\''))
{
$quote = $char;
$this->hl[$i] = Highlight::STRING;
$i++;
while ($i < $this->rsize)
{
$char = $this->render[$i];
$this->hl[$i] = Highlight::STRING;
// Check for escaped character
if ($char === '\\' && $i+1 < $this->rsize)
{
$this->hl[$i + 1] = Highlight::STRING;
$i += 2;
continue;
}
// End of the string!
$i++;
if ($char === $quote)
{
break;
}
}
return true;
}
return false;
}
protected function highlightPHP(): void
{ {
$rowNum = $this->idx + 1; $rowNum = $this->idx + 1;
@ -501,7 +295,9 @@ class Row {
$this->idx < $this->parent->numRows $this->idx < $this->parent->numRows
)) ))
{ {
// @codeCoverageIgnoreStart
return; return;
// @codeCoverageIgnoreEnd
} }
$tokens = $this->parent->tokens[$rowNum]; $tokens = $this->parent->tokens[$rowNum];
@ -516,7 +312,9 @@ class Row {
{ {
if ($offset >= $this->rsize) if ($offset >= $this->rsize)
{ {
// @codeCoverageIgnoreStart
break; break;
// @codeCoverageIgnoreEnd
} }
// A multi-line comment can end in the middle of a line... // A multi-line comment can end in the middle of a line...
@ -538,11 +336,13 @@ class Row {
break; break;
} }
$char = $token['char']; $char = $token['char']; // ?? '';
$charLen = strlen($char); $charLen = strlen($char);
if ($charLen === 0 || $offset >= $this->rsize) if ($charLen === 0 || $offset >= $this->rsize)
{ {
// @codeCoverageIgnoreStart
continue; continue;
// @codeCoverageIgnoreEnd
} }
$charStart = strpos($this->render, $char, $offset); $charStart = strpos($this->render, $char, $offset);
if ($charStart === FALSE) if ($charStart === FALSE)
@ -592,20 +392,25 @@ class Row {
$tokenHighlight = Highlight::fromPHPToken($token['type']); $tokenHighlight = Highlight::fromPHPToken($token['type']);
$charHighlight = Highlight::fromPHPChar(trim($token['char'])); $charHighlight = Highlight::fromPHPChar(trim($token['char']));
$highlight = match(true) { $hl = match(true) {
// Matches a predefined PHP token // Matches a predefined PHP token
$token['type'] !== T_RAW && $tokenHighlight !== Highlight::NORMAL $token['type'] !== self::T_RAW && $tokenHighlight !== Highlight::NORMAL
=> $tokenHighlight, => $tokenHighlight,
// Matches a specific syntax character // Matches a specific syntax character
$charHighlight !== Highlight::NORMAL => $charHighlight, $charHighlight !== Highlight::NORMAL => $charHighlight,
// Types/identifiers/keywords that don't have their own token, but are
// defined as keywords
in_array($token['char'], $this->parent->syntax->keywords2 ?? [], TRUE)
=> Highlight::KEYWORD2,
default => Highlight::NORMAL, default => Highlight::NORMAL,
}; };
if ($highlight !== Highlight::NORMAL) if ($hl !== Highlight::NORMAL)
{ {
array_replace_range($this->hl, $charStart, $charLen, $highlight); array_replace_range($this->hl, $charStart, $charLen, $hl);
$offset = $charEnd; $offset = $charEnd;
} }
} }
@ -614,7 +419,9 @@ class Row {
$this->hlOpenComment = $inComment; $this->hlOpenComment = $inComment;
if ($changed && ($this->idx + 1) < $this->parent->numRows) if ($changed && ($this->idx + 1) < $this->parent->numRows)
{ {
$this->parent->rows[$this->idx + 1]->highlight(); // @codeCoverageIgnoreStart
$this->parent->rows[$this->idx + 1]->updateSyntax();
// @codeCoverageIgnoreEnd
} }
} }
} }

View File

@ -3,101 +3,42 @@
namespace Aviat\Kilo; namespace Aviat\Kilo;
class Syntax { class Syntax {
public const HIGHLIGHT_NUMBERS = (1 << 0);
public const HIGHLIGHT_STRINGS = (1 << 1);
// Tokens for PHP files // Tokens for PHP files
public array $tokens = []; public array $tokens = [];
public static function new( public static function new(
string $name, string $name,
array $extList = [],
array $keywords1 = [], array $keywords1 = [],
array $keywords2 = [], array $keywords2 = [],
array $operators = [],
string $slcs = '//', string $slcs = '//',
string $mcs = '/*', string $mcs = '/*',
string $mce = '*/', string $mce = '*/',
bool $highlightNumbers = true, int $flags = 0,
bool $highlightStrings = true,
bool $highlightComments = true,
bool $hasCharType = false,
bool $highlightCharacters = false,
): self ): self
{ {
return new self( return new self($name, $extList, $keywords1, $keywords2, $slcs, $mcs, $mce, $flags);
$name,
$keywords1,
$keywords2,
$operators,
$slcs,
$mcs,
$mce,
$highlightNumbers,
$hasCharType,
$highlightCharacters,
$highlightStrings,
$highlightComments,
);
}
public static function default(): self
{
return self::new('No filetype', slcs: '', mcs: '', mce: '', highlightNumbers: false, highlightStrings: false);
} }
private function __construct( private function __construct(
/** The name of the programming language */ /** The name of the programming language */
public string $filetype, public string $filetype,
/** Relevant file extensions for the specified language */
public array $filematch,
/** Primary set of language keywords */ /** Primary set of language keywords */
public array $keywords1, public array $keywords1,
/** Secondary set of language keywords */ /** Secondary set of language keywords */
public array $keywords2, public array $keywords2,
/** Operators for the current language */
public array $operators,
/** Syntax to start a single line comment */ /** Syntax to start a single line comment */
public string $singleLineCommentStart, public string $singleLineCommentStart,
/** Syntax to start a multi-line comment */ /** Syntax to start a multi-line comment */
public string $multiLineCommentStart, public string $multiLineCommentStart,
/** Syntax to end a multi-line commment */ /** Syntax to end a multi-line commment */
public string $multiLineCommentEnd, public string $multiLineCommentEnd,
/** Should we highlight numbers? */ /** Bitflags configuring the specified language syntax */
private bool $highlightNumbers, public int $flags,
/** Does this language have a character type, separate from strings? */
private bool $hasCharType,
/** Should we highlight chars? */
private bool $highlightCharacters,
/** should we highlight Strings? */
private bool $highlightStrings,
/** should we highlight comments? */
private bool $highlightComments,
) {} ) {}
public function numbers(): bool
{
return $this->highlightNumbers;
}
public function strings(): bool
{
return $this->highlightStrings;
}
public function hasChar(): bool
{
return $this->hasCharType;
}
public function characters(): bool
{
return $this->hasCharType && $this->highlightCharacters;
}
public function mlComments(): bool
{
return $this->highlightComments
&& strlen($this->multiLineCommentStart) !== 0
&& strlen($this->multiLineCommentStart) !== 0;
}
public function comments(): bool
{
return $this->highlightComments && strlen($this->singleLineCommentStart) !== 0;
}
} }

View File

@ -2,8 +2,6 @@
namespace Aviat\Kilo; namespace Aviat\Kilo;
use Aviat\Kilo\Enum\RawKeyCode;
use Aviat\Kilo\Enum\KeyType;
use Aviat\Kilo\Type\TerminalSize; use Aviat\Kilo\Type\TerminalSize;
class Terminal { class Terminal {
@ -77,52 +75,6 @@ class Terminal {
return (is_string($input)) ? $input : ''; return (is_string($input)) ? $input : '';
} }
/**
* Get the last key input from the terminal and convert to a
* more useful format
*
* @return string
*/
public static function readKey(): string
{
$c = Terminal::read();
return match($c)
{
// Unambiguous mappings
RawKeyCode::ARROW_DOWN => KeyType::ARROW_DOWN,
RawKeyCode::ARROW_LEFT => KeyType::ARROW_LEFT,
RawKeyCode::ARROW_RIGHT => KeyType::ARROW_RIGHT,
RawKeyCode::ARROW_UP => KeyType::ARROW_UP,
RawKeyCode::DELETE => KeyType::DELETE,
RawKeyCode::ENTER => KeyType::ENTER,
RawKeyCode::PAGE_DOWN => KeyType::PAGE_DOWN,
RawKeyCode::PAGE_UP => KeyType::PAGE_UP,
// Backspace
RawKeyCode::CTRL('h'), RawKeyCode::BACKSPACE => KeyType::BACKSPACE,
// Escape
RawKeyCode::CTRL('l'), RawKeyCode::ESCAPE => KeyType::ESCAPE,
// Home Key
"\eOH", "\e[7~", "\e[1~", ANSI::RESET_CURSOR => KeyType::HOME,
// End Key
"\eOF", "\e[4~", "\e[8~", "\e[F" => KeyType::END,
default => $c,
};
}
/**
* Ring the terminal bell
*/
public static function ding(): void
{
self::write(RawKeyCode::BELL);
}
/** /**
* Write to the stdout stream * Write to the stdout stream
* *

View File

@ -4,8 +4,8 @@ namespace Aviat\Kilo\Tokens;
use PhpToken; use PhpToken;
use function Aviat\Kilo\str_contains;
use function Aviat\Kilo\tabs_to_spaces; use function Aviat\Kilo\tabs_to_spaces;
use const Aviat\Kilo\T_RAW;
class PHP8 extends PhpToken { class PHP8 extends PhpToken {
private array $rawLines = []; private array $rawLines = [];
@ -119,7 +119,7 @@ class PHP8 extends PhpToken {
// Simple characters, usually delimiters or single character operators // Simple characters, usually delimiters or single character operators
$this->tokens[$lineNumber][] = [ $this->tokens[$lineNumber][] = [
'type' => T_RAW, 'type' => -1,
'typeName' => 'RAW', 'typeName' => 'RAW',
'char' => tabs_to_spaces($token), 'char' => tabs_to_spaces($token),
'line' => $lineNumber, 'line' => $lineNumber,

View File

@ -5,12 +5,11 @@ namespace Aviat\Kilo\Type;
class StatusMessage { class StatusMessage {
private function __construct( private function __construct(
public string $text, public string $text,
public int $len,
public int $time, public int $time,
) {} ) {}
public static function from(string $text): self public static function from(string $text, mixed ...$args): self
{ {
return new self($text, strlen($text), time()); return new self(sprintf($text, ...$args), time());
} }
} }

View File

@ -8,6 +8,3 @@ namespace Aviat\Kilo;
const KILO_VERSION = '0.3.0'; const KILO_VERSION = '0.3.0';
const KILO_TAB_STOP = 4; const KILO_TAB_STOP = 4;
const KILO_QUIT_TIMES = 3; const KILO_QUIT_TIMES = 3;
const NO_MATCH = -1;
const T_RAW = -1;

View File

@ -2,7 +2,7 @@
namespace Aviat\Kilo; namespace Aviat\Kilo;
use Aviat\Kilo\Enum\{Color, Highlight, RawKeyCode}; use Aviat\Kilo\Enum\{Color, Highlight, KeyCode};
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// ! C function/macro equivalents // ! C function/macro equivalents
@ -77,12 +77,12 @@ function is_digit(string $char): bool
function is_space(string $char): bool function is_space(string $char): bool
{ {
return match($char) { return match($char) {
RawKeyCode::CARRIAGE_RETURN, KeyCode::CARRIAGE_RETURN,
RawKeyCode::FORM_FEED, KeyCode::FORM_FEED,
RawKeyCode::NEWLINE, KeyCode::NEWLINE,
RawKeyCode::SPACE, KeyCode::SPACE,
RawKeyCode::TAB, KeyCode::TAB,
RawKeyCode::VERTICAL_TAB => true, KeyCode::VERTICAL_TAB => true,
default => false, default => false,
}; };
@ -107,7 +107,7 @@ function is_separator(string $char): bool
$isSep = str_contains(',.()+-/*=~%<>[];', $char); $isSep = str_contains(',.()+-/*=~%<>[];', $char);
return is_space($char) || $char === RawKeyCode::NULL || $isSep; return is_space($char) || $char === KeyCode::NULL || $isSep;
} }
/** /**
@ -138,7 +138,7 @@ function array_replace_range(array &$array, int $offset, int $length, mixed $val
* @param int|null $offset * @param int|null $offset
* @return bool * @return bool
*/ */
function str_has(string $haystack, string $str, ?int $offset = NULL): bool function str_contains(string $haystack, string $str, ?int $offset = NULL): bool
{ {
if (empty($str)) if (empty($str))
{ {
@ -165,7 +165,6 @@ function syntax_to_color(int $hl): int
Highlight::KEYWORD1 => Color::FG_YELLOW, Highlight::KEYWORD1 => Color::FG_YELLOW,
Highlight::KEYWORD2 => Color::FG_GREEN, Highlight::KEYWORD2 => Color::FG_GREEN,
Highlight::STRING => Color::FG_MAGENTA, Highlight::STRING => Color::FG_MAGENTA,
Highlight::CHARACTER => Color::FG_BRIGHT_MAGENTA,
Highlight::NUMBER => Color::FG_BRIGHT_RED, Highlight::NUMBER => Color::FG_BRIGHT_RED,
Highlight::OPERATOR => Color::FG_BRIGHT_GREEN, Highlight::OPERATOR => Color::FG_BRIGHT_GREEN,
Highlight::VARIABLE => Color::FG_BRIGHT_CYAN, Highlight::VARIABLE => Color::FG_BRIGHT_CYAN,
@ -186,41 +185,99 @@ function syntax_to_color(int $hl): int
*/ */
function tabs_to_spaces(string $str, int $number = KILO_TAB_STOP): string function tabs_to_spaces(string $str, int $number = KILO_TAB_STOP): string
{ {
return str_replace(RawKeyCode::TAB, str_repeat(RawKeyCode::SPACE, $number), $str); return str_replace(KeyCode::TAB, str_repeat(KeyCode::SPACE, $number), $str);
} }
function error_code_name(int $code): string /**
* Generate/Get the syntax highlighting objects
*
* @return array
*/
function get_file_syntax_map(): array
{ {
return match ($code) { static $db = [];
E_ERROR => 'Error',
E_WARNING => 'Warning',
E_PARSE => 'Parse Error',
E_NOTICE => 'Notice',
E_CORE_ERROR => 'Core Error',
E_CORE_WARNING => 'Core Warning',
E_COMPILE_ERROR => 'Compile Error',
E_COMPILE_WARNING => 'Compile Warning',
E_USER_ERROR => 'User Error',
E_USER_WARNING => 'User Warning',
E_USER_NOTICE => 'User Notice',
E_RECOVERABLE_ERROR => 'Recoverable Error',
E_DEPRECATED => 'Deprecated',
E_USER_DEPRECATED => 'User Deprecated',
default => 'Unknown',
};
}
function saturating_add(int $a, int $b, int $max): int if (count($db) === 0)
{
return ($a + $b > $max) ? $max : $a + $b;
}
function saturating_sub(int $a, int $b): int
{
if ($b > $a)
{ {
return 0; $db = [
Syntax::new(
'C',
['.c', '.h', '.cpp'],
[
'continue', 'typedef', 'switch', 'return', 'static', 'while', 'break', 'struct',
'union', 'class', 'else', 'enum', 'for', 'case', 'if',
],
[
'#include', 'unsigned', '#define', '#ifndef', 'double', 'signed', '#endif',
'#ifdef', 'float', '#error', '#undef', 'long', 'char', 'int', 'void', '#if',
],
'//',
'/*',
'*/',
Syntax::HIGHLIGHT_NUMBERS | Syntax::HIGHLIGHT_STRINGS,
),
Syntax::new(
'CSS',
['.css', '.less', '.sass', 'scss'],
[],
[],
'',
'/*',
'*/',
Syntax::HIGHLIGHT_NUMBERS | Syntax::HIGHLIGHT_STRINGS,
),
Syntax::new(
'JavaScript',
['.js', '.jsx', '.ts', '.tsx', '.jsm', '.mjs', '.es'],
[
'instanceof', 'continue', 'debugger', 'function', 'default', 'extends',
'finally', 'delete', 'export', 'import', 'return', 'switch', 'typeof',
'break', 'catch', 'class', 'const', 'super', 'throw', 'while', 'yield',
'case', 'else', 'this', 'void', 'with', 'from', 'for', 'new', 'try',
'var', 'do', 'if', 'in', 'as',
],
[
'=>', 'Number', 'String', 'Object', 'Math', 'JSON', 'Boolean',
],
'//',
'/*',
'*/',
Syntax::HIGHLIGHT_NUMBERS | Syntax::HIGHLIGHT_STRINGS,
),
Syntax::new(
'PHP',
['.php', 'kilo'],
[],
[],
'//',
'/*',
'*/',
Syntax::HIGHLIGHT_NUMBERS | Syntax::HIGHLIGHT_STRINGS,
),
Syntax::new(
'Rust',
['.rs'],
[
'continue', 'return', 'static', 'struct', 'unsafe', 'break', 'const', 'crate',
'extern', 'match', 'super', 'trait', 'where', 'else', 'enum', 'false', 'impl',
'loop', 'move', 'self', 'type', 'while', 'for', 'let', 'mod', 'pub', 'ref', 'true',
'use', 'mut', 'as', 'fn', 'if', 'in',
],
[
'DoubleEndedIterator', 'ExactSizeIterator', 'IntoIterator', 'PartialOrd', 'PartialEq',
'Iterator', 'ToString', 'Default', 'ToOwned', 'Extend', 'FnOnce', 'Option', 'String',
'AsMut', 'AsRef', 'Clone', 'Debug', 'FnMut', 'Sized', 'Unpin', 'array', 'isize',
'usize', '&str', 'Copy', 'Drop', 'From', 'Into', 'None', 'Self', 'Send', 'Some',
'Sync', 'bool', 'char', 'i128', 'u128', 'Box', 'Err', 'Ord', 'Vec', 'dyn', 'f32',
'f64', 'i16', 'i32', 'i64', 'str', 'u16', 'u32', 'u64', 'Eq', 'Fn', 'Ok', 'i8', 'u8',
],
'//',
'/*',
'*/',
Syntax::HIGHLIGHT_NUMBERS | Syntax::HIGHLIGHT_STRINGS,
),
];
} }
return $a - $b; return $db;
} }

View File

@ -1,6 +1,6 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace Aviat\Kilo\Tests; namespace Aviat\Kilo\Tests\Traits;
use Aviat\Kilo\ANSI; use Aviat\Kilo\ANSI;
use Aviat\Kilo\Enum\Color; use Aviat\Kilo\Enum\Color;

View File

@ -1,6 +1,6 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace Aviat\Kilo\Tests; namespace Aviat\Kilo\Tests\Traits;
use Aviat\Kilo\Editor; use Aviat\Kilo\Editor;
use Aviat\Kilo\Type\TerminalSize; use Aviat\Kilo\Type\TerminalSize;
@ -29,6 +29,14 @@ class MockEditor extends Editor {
class EditorTest extends TestCase { class EditorTest extends TestCase {
use MatchesSnapshots; use MatchesSnapshots;
public function testSanity(): void
{
$editor = MockEditor::mock();
$this->assertEquals(0, $editor->numRows);
$this->assertNull($editor->syntax);
}
public function test__debugInfo(): void public function test__debugInfo(): void
{ {
$editor = MockEditor::mock(); $editor = MockEditor::mock();
@ -41,7 +49,7 @@ class EditorTest extends TestCase {
{ {
$editor = MockEditor::mock('test.php'); $editor = MockEditor::mock('test.php');
$state = json_encode($editor->__debugInfo(), JSON_THROW_ON_ERROR); $state = json_encode($editor, JSON_THROW_ON_ERROR);
$this->assertMatchesJsonSnapshot($state); $this->assertMatchesJsonSnapshot($state);
} }
@ -49,7 +57,7 @@ class EditorTest extends TestCase {
{ {
$editor = MockEditor::mock('src/ffi.h'); $editor = MockEditor::mock('src/ffi.h');
$state = json_encode($editor->__debugInfo(), JSON_THROW_ON_ERROR); $state = json_encode($editor, JSON_THROW_ON_ERROR);
$this->assertMatchesJsonSnapshot($state); $this->assertMatchesJsonSnapshot($state);
} }
} }

View File

@ -4,7 +4,7 @@ namespace Aviat\Kilo\Tests\Enum;
use function Aviat\Kilo\ctrl_key; use function Aviat\Kilo\ctrl_key;
use Aviat\Kilo\Enum\RawKeyCode; use Aviat\Kilo\Enum\KeyCode;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
class KeyCodeTest extends TestCase { class KeyCodeTest extends TestCase {
@ -16,7 +16,7 @@ class KeyCodeTest extends TestCase {
$ord = $i; $ord = $i;
$expected = chr($ord); $expected = chr($ord);
$actual = RawKeyCode::CTRL($char); $actual = KeyCode::CTRL($char);
$this->assertEquals(ctrl_key($char), $ord, 'chr(ctrl_key) !== CTRL'); $this->assertEquals(ctrl_key($char), $ord, 'chr(ctrl_key) !== CTRL');
$this->assertEquals($expected, $actual, "CTRL+'{$char}' should return chr($ord)"); $this->assertEquals($expected, $actual, "CTRL+'{$char}' should return chr($ord)");
@ -25,13 +25,13 @@ class KeyCodeTest extends TestCase {
public function testNullOnInvalidChar(): void public function testNullOnInvalidChar(): void
{ {
$this->assertNull(RawKeyCode::CTRL("\t")); $this->assertNull(KeyCode::CTRL("\t"));
} }
public function testSameOutputOnUpperOrLower(): void public function testSameOutputOnUpperOrLower(): void
{ {
$lower = RawKeyCode::CTRL('v'); $lower = KeyCode::CTRL('v');
$upper = RawKeyCode::CTRL('V'); $upper = KeyCode::CTRL('V');
$this->assertEquals($lower, $upper); $this->assertEquals($lower, $upper);
} }

View File

@ -8,12 +8,13 @@ use PHPUnit\Framework\TestCase;
use function Aviat\Kilo\array_replace_range; use function Aviat\Kilo\array_replace_range;
use function Aviat\Kilo\ctrl_key; use function Aviat\Kilo\ctrl_key;
use function Aviat\Kilo\get_file_syntax_map;
use function Aviat\Kilo\is_ascii; use function Aviat\Kilo\is_ascii;
use function Aviat\Kilo\is_ctrl; use function Aviat\Kilo\is_ctrl;
use function Aviat\Kilo\is_digit; use function Aviat\Kilo\is_digit;
use function Aviat\Kilo\is_separator; use function Aviat\Kilo\is_separator;
use function Aviat\Kilo\is_space; use function Aviat\Kilo\is_space;
use function Aviat\Kilo\str_has; use function Aviat\Kilo\str_contains;
use function Aviat\Kilo\syntax_to_color; use function Aviat\Kilo\syntax_to_color;
use function Aviat\Kilo\tabs_to_spaces; use function Aviat\Kilo\tabs_to_spaces;
@ -96,23 +97,28 @@ class FunctionTest extends TestCase {
$this->assertNotEquals(syntax_to_color(Highlight::OPERATOR), Color::FG_WHITE); $this->assertNotEquals(syntax_to_color(Highlight::OPERATOR), Color::FG_WHITE);
} }
public function test_get_file_syntax_map(): void
{
$this->assertNotEmpty(get_file_syntax_map());
}
public function test_str_contains(): void public function test_str_contains(): void
{ {
// Search from string offset // Search from string offset
$this->assertTrue(str_has(' vcd', 'vcd', 2)); $this->assertTrue(str_contains(' vcd', 'vcd', 2));
$this->assertFalse(str_has('', "\0")); $this->assertFalse(str_contains('', "\0"));
// An empty search string returns false // An empty search string returns false
$this->assertFalse(str_has('', '')); $this->assertFalse(str_contains('', ''));
$this->assertTrue(str_has('alphabet', 'phab')); $this->assertTrue(str_contains('alphabet', 'phab'));
} }
public function test_tabs_to_spaces(): void public function test_tabs_to_spaces(): void
{ {
$original = "\t\t\t{"; $original = "\t\t\t{";
$this->assertFalse(str_has(tabs_to_spaces($original), "\t")); $this->assertFalse(str_contains(tabs_to_spaces($original), "\t"));
} }
public function test_array_replace_range_length_1(): void public function test_array_replace_range_length_1(): void

View File

@ -1,25 +1,20 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace Aviat\Kilo\Tests; namespace Aviat\Kilo\Tests\Traits;
use Aviat\Kilo\ use Aviat\Kilo\{Editor, Row};
{
Document,
Row};
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
class RowTest extends TestCase { class RowTest extends TestCase {
protected Document $document; protected Editor $editor;
protected Row $row; protected Row $row;
public function setUp(): void public function setUp(): void
{ {
parent::setUp(); parent::setUp();
$this->document = Document::new(); $this->editor = Editor::new();
$this->document->insertRow(0, ''); $this->row = Row::new($this->editor, '', 0);
$this->row = $this->document->rows[0];
} }
public function testSanity(): void public function testSanity(): void
@ -33,14 +28,13 @@ class RowTest extends TestCase {
public function testSetRunsUpdate(): void public function testSetRunsUpdate(): void
{ {
$this->row->setChars('abcde'); $this->row->chars = 'abcde';
$this->assertNotEmpty($this->row->chars);
$this->assertEquals('abcde', $this->row->render); $this->assertEquals('abcde', $this->row->render);
} }
public function test__toString(): void public function test__toString(): void
{ {
$this->row->setChars('abcde'); $this->row->chars = 'abcde';
$this->assertEquals("abcde\n", (string)$this->row); $this->assertEquals("abcde\n", (string)$this->row);
} }
@ -59,41 +53,42 @@ class RowTest extends TestCase {
$this->assertEquals($expected, $actual); $this->assertEquals($expected, $actual);
} }
public function testInsert(): void public function testInsertChar(): void
{ {
$this->row->setChars('abde'); $this->row->chars = 'abde';
$this->row->insert(2, 'c'); $this->row->insertChar(2, 'c');
$this->assertEquals('abcde', $this->row->chars); $this->assertEquals('abcde', $this->row->chars);
$this->assertEquals('abcde', $this->row->render); $this->assertEquals('abcde', $this->row->render);
$this->assertEquals(true, $this->document->dirty); $this->assertEquals(true, $this->editor->dirty);
} }
public function testInsertBadOffset(): void public function testInsertCharBadOffset(): void
{ {
$this->row->setChars('ab'); $this->row->chars = 'ab';
$this->row->insert(5, 'c'); $this->row->insertChar(5, 'c');
$this->assertEquals('abc', $this->row->chars); $this->assertEquals('abc', $this->row->chars);
$this->assertEquals('abc', $this->row->render); $this->assertEquals('abc', $this->row->render);
$this->assertEquals(true, $this->document->dirty); $this->assertEquals(true, $this->editor->dirty);
} }
public function testDelete(): void public function testDeleteChar(): void
{ {
$this->row->setChars('abcdef'); $this->row->chars = 'abcdef';
$this->row->delete(5); $this->row->deleteChar(5);
$this->assertEquals('abcde', $this->row->chars); $this->assertEquals('abcde', $this->row->chars);
$this->assertEquals('abcde', $this->row->render); $this->assertEquals('abcde', $this->row->render);
$this->assertEquals(true, $this->document->dirty); $this->assertEquals(true, $this->editor->dirty);
} }
public function testDeleteBadOffset(): void public function testDeleteCharBadOffset(): void
{ {
$this->row->setChars('ab'); $this->row->chars = 'ab';
$this->row->delete(5); $this->row->deleteChar(5);
$this->assertEquals('ab', $this->row->chars); $this->assertEquals('ab', $this->row->chars);
$this->assertEquals(false, $this->editor->dirty);
} }
} }

View File

@ -1,6 +1,6 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace Aviat\Kilo\Tests; namespace Aviat\Kilo\Tests\Traits;
use Aviat\Kilo\{Termios, TermiosException}; use Aviat\Kilo\{Termios, TermiosException};
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,36 +4,27 @@
"y": 0 "y": 0
}, },
"document": { "document": {
"fileType": { "syntax": null,
"name": "No filetype",
"syntax": {
"tokens": [],
"filetype": "No filetype",
"keywords1": [],
"keywords2": [],
"operators": [],
"singleLineCommentStart": "",
"multiLineCommentStart": "",
"multiLineCommentEnd": ""
}
},
"tokens": [], "tokens": [],
"filename": "",
"rows": [], "rows": [],
"dirty": false "filename": null
}, },
"offset": { "offset": {
"x": 0, "x": 0,
"y": 0 "y": 0
}, },
"dirty": false,
"filename": "",
"renderX": 0, "renderX": 0,
"rows": [],
"terminalSize": { "terminalSize": {
"rows": 21, "rows": 21,
"cols": 80 "cols": 80
}, },
"statusMessage": { "statusMessage": {
"text": "HELP: Ctrl-S = save | Ctrl-Q = quit | Ctrl-F = find", "text": "HELP: Ctrl-S = save | Ctrl-Q = quit | Ctrl-F = find",
"len": 51,
"time": 1234567890 "time": 1234567890
} },
"syntax": null,
"tokens": []
} }