Compare commits

..

2 Commits

Author SHA1 Message Date
228576ccf1 Fix page down behavior, resolves #1
All checks were successful
timw4mail/php-kilo/pipeline/head This commit looks good
2021-04-16 12:51:53 -04:00
4812d548a7 Work on line numbers
All checks were successful
timw4mail/php-kilo/pipeline/head This commit looks good
2021-04-15 18:35:03 -04:00
39 changed files with 7521 additions and 8124 deletions

View File

@ -1,4 +1,4 @@
FROM php:8.1-alpine FROM php:8-alpine3.13
RUN apk add --no-cache --virtual .persistent-deps libffi-dev \ RUN apk add --no-cache --virtual .persistent-deps libffi-dev \
&& docker-php-ext-configure ffi --with-ffi \ && docker-php-ext-configure ffi --with-ffi \

4
Jenkinsfile vendored
View File

@ -5,9 +5,9 @@ pipeline {
} }
} }
stages { stages {
stage('PHP 8.1') { stage('PHP 7.4') {
steps { steps {
sh 'apk add --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/testing php-phpdbg' sh 'apk add --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/testing php8-phpdbg'
sh 'curl -sS https://getcomposer.org/installer | php' sh 'curl -sS https://getcomposer.org/installer | php'
sh 'php composer.phar install' sh 'php composer.phar install'
sh 'phpdbg -dffi.enable=1 -qrr -- ./vendor/bin/phpunit --coverage-text --coverage-clover clover.xml --colors=never -c phpunit.xml tests' sh 'phpdbg -dffi.enable=1 -qrr -- ./vendor/bin/phpunit --coverage-text --coverage-clover clover.xml --colors=never -c phpunit.xml tests'

View File

@ -5,7 +5,7 @@
A reimplementation of the [Kilo](https://viewsourcecode.org/snaptoken/kilo/index.html) tutorial in PHP. Also has some inspiration from the [Hecto](https://www.philippflenker.com/hecto/) text editor tutorial. Requires PHP 8 and FFI. A reimplementation of the [Kilo](https://viewsourcecode.org/snaptoken/kilo/index.html) tutorial in PHP. Also has some inspiration from the [Hecto](https://www.philippflenker.com/hecto/) text editor tutorial. Requires PHP 8 and FFI.
## Requirements ## Requirements
* PHP 8.1 * PHP 8
* FFI enabled * FFI enabled
## Implementation notes: ## Implementation notes:

View File

@ -1,7 +1,8 @@
{ {
"autoload": { "autoload": {
"files": [ "files": [
"src/Kilo.php" "src/constants.php",
"src/functions.php"
], ],
"psr-4": { "psr-4": {
"Aviat\\Kilo\\": "src/" "Aviat\\Kilo\\": "src/"
@ -15,8 +16,8 @@
"require-dev": { "require-dev": {
"ext-json": "*", "ext-json": "*",
"phpunit/phpunit": "^9.5.0", "phpunit/phpunit": "^9.5.0",
"phpstan/phpstan": "^1.8.2", "phpstan/phpstan": "^0.12.19",
"spatie/phpunit-snapshot-assertions": "^4.2.15" "spatie/phpunit-snapshot-assertions": "^4.2.0"
}, },
"scripts": { "scripts": {
"coverage": "phpdbg -qrr -- vendor/bin/phpunit -c phpunit.xml tests", "coverage": "phpdbg -qrr -- vendor/bin/phpunit -c phpunit.xml tests",
@ -26,6 +27,6 @@
}, },
"require": { "require": {
"ext-ffi": "*", "ext-ffi": "*",
"php": ">= 8.1.0" "php": ">= 8.0.0"
} }
} }

1014
composer.lock generated

File diff suppressed because it is too large Load Diff

30
kilo
View File

@ -3,34 +3,14 @@
namespace Aviat\Kilo; namespace Aviat\Kilo;
use Aviat\Kilo\Terminal\Termios; require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/src/Kilo.php';
// Remove the composer install requirement by
// manually handling autoloading
spl_autoload_register(static function (string $class): void {
$nsParts = explode('\\', $class);
array_shift($nsParts);
array_shift($nsParts);
array_unshift($nsParts, __DIR__, 'src');
$file = implode(DIRECTORY_SEPARATOR, $nsParts) . '.php';
if (file_exists($file))
{
require_once($file);
return;
}
});
// Log notices/errors/warnings to file // Log notices/errors/warnings to file
set_error_handler(static function ( set_error_handler(static function (
int $errno, $errno,
string $errstr, $errstr,
string $errfile, $errfile,
int $errline, $errline
) { ) {
$msg = print_r([ $msg = print_r([
'code' => error_code_name($errno), 'code' => error_code_name($errno),

View File

@ -1,9 +1,6 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace Aviat\Kilo\Terminal; namespace Aviat\Kilo;
use Aviat\Kilo\Terminal\Enum\Color;
use Aviat\Kilo\Terminal\Enum\Color256;
/** /**
* ANSI * ANSI
@ -34,18 +31,8 @@ class ANSI {
public const BOLD_TEXT = "\e[1m"; public const BOLD_TEXT = "\e[1m";
public const DIM_TEXT = "\e[2m";
public const ITALIC_TEXT = "\d[3m";
public const UNDERLINE_TEXT = "\e[4m"; public const UNDERLINE_TEXT = "\e[4m";
public const BLINKING_TEXT = "\e[5m";
public const INVERSE_TEXT = "\e[7m";
public const STRIKE_TEXT = "\e[9m";
/** /**
* Move the cursor to position 0,0 which is the top left * Move the cursor to position 0,0 which is the top left
*/ */
@ -59,48 +46,14 @@ class ANSI {
public const SHOW_CURSOR = "\e[?25h"; public const SHOW_CURSOR = "\e[?25h";
/** /**
* Generate the ascii sequence for basic foreground text color * Generate the ascii sequence for basic text color
* *
* @param Color|Color256|int $color * @param int $color
* @param Color $ground
* @return string * @return string
*/ */
public static function color(Color|Color256| int $color, Color $ground = Color::Fg): string public static function color(int $color): string
{ {
if ( ! $color instanceof Color) return self::escapeSequence('%dm', $color);
{
return self::color256($color, $ground);
}
return self::escapeSequence('%dm', $color->value);
}
/**
* Generate the ANSI sequence for a 256 mode color
*
* @param Color256 | int $color
* @param Color $ground
* @return string
*/
public static function color256(Color256| int $color, Color $ground = Color::Fg): string
{
if ($color instanceof Color256)
{
$color = $color->value;
}
return self::escapeSequence('%d;5;%dm', $ground->value, $color);
}
/**
* Invert the foreground/background on a text segment
*
* @param string $text
* @return string
*/
public static function invert(string $text): string
{
return self::INVERSE_TEXT . $text . self::RESET_TEXT;
} }
/** /**

View File

@ -78,12 +78,6 @@ class Document {
$this->dirty = false; $this->dirty = false;
$this->selectSyntaxHighlight(); $this->selectSyntaxHighlight();
// Add a row to empty files so it can be properly edited
if ($this->isEmpty())
{
$this->rows[] = Row::new($this, "", 0);
}
return $this; return $this;
} }
@ -101,14 +95,14 @@ class Document {
return $res; return $res;
} }
public function insert(Point $at, string|KeyType $c): void public function insert(Point $at, string $c): void
{ {
if ($at->y > $this->numRows) if ($at->y > $this->numRows)
{ {
return; return;
} }
if ($c === KeyType::Enter || $c === RawKeyCode::CARRIAGE_RETURN) if ($c === KeyType::ENTER || $c === RawKeyCode::CARRIAGE_RETURN)
{ {
$this->insertNewline($at); $this->insertNewline($at);
$this->dirty = true; $this->dirty = true;
@ -119,7 +113,7 @@ class Document {
$this->dirty = true; $this->dirty = true;
} }
public function delete(Point $at, int $gutter = 0): void public function delete(Point $at): void
{ {
if ($at->y > $this->numRows) if ($at->y > $this->numRows)
{ {
@ -128,7 +122,7 @@ class Document {
$row =& $this->rows[$at->y]; $row =& $this->rows[$at->y];
if (($at->x - $gutter) === $this->rows[$at->y]->size && $at->y + 1 < $this->numRows) 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->rows[$at->y]->append($this->rows[$at->y + 1]->chars);
$this->deleteRow($at->y + 1); $this->deleteRow($at->y + 1);
@ -236,6 +230,10 @@ class Document {
// Add a new row with the contents of the previous row at the point of the split // 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->insertRow($at->y + 1, substr($chars, $at->x));
// Highlight the two changed rows
$row->update();
$this->rows[$at->y + 1]->update();
} }
$this->dirty = true; $this->dirty = true;

View File

@ -2,25 +2,20 @@
namespace Aviat\Kilo; namespace Aviat\Kilo;
use Aviat\Kilo\Enum\{Highlight, KeyType, RawKeyCode, SearchDirection};
use Aviat\Kilo\Terminal\ANSI;
use Aviat\Kilo\Terminal\Enum\Color;
use Aviat\Kilo\Terminal\Terminal;
use Aviat\Kilo\Type\{Point, StatusMessage};
use Aviat\Kilo\Type\TerminalSize; use Aviat\Kilo\Type\TerminalSize;
use Aviat\Kilo\Enum\{
Color,
RawKeyCode,
KeyType,
Highlight,
SearchDirection
};
use Aviat\Kilo\Type\{Point, StatusMessage};
/** /**
* // Don't highlight this! * // Don't highlight this!
*/ */
class Editor { class Editor {
/**
* @var bool Whether to render line numbers
*/
public bool $showLineNumbers = true;
/**
* @var int The size of the line number Gutter in characters
*/
public int $numberGutter = 5;
/** /**
* @var string The screen buffer * @var string The screen buffer
*/ */
@ -61,6 +56,11 @@ class Editor {
*/ */
protected bool $shouldQuit = false; protected bool $shouldQuit = false;
/**
* @var int The amount to move the cursor off the left
*/
protected int $cursorLeftOffset = 0;
/** /**
* @var int The number of times to confirm you wish to quit * @var int The number of times to confirm you wish to quit
*/ */
@ -68,6 +68,10 @@ class Editor {
/** /**
* Create the Editor instance with CLI arguments * Create the Editor instance with CLI arguments
*
* @param int $argc
* @param array $argv
* @return Editor
*/ */
public static function new(int $argc = 0, array $argv = []): Editor public static function new(int $argc = 0, array $argv = []): Editor
{ {
@ -81,11 +85,15 @@ class Editor {
/** /**
* The real constructor, ladies and gentlemen * The real constructor, ladies and gentlemen
*
* @param string|null $filename
*/ */
private function __construct(?string $filename = NULL) private function __construct(?string $filename = NULL)
{ {
$this->statusMessage = StatusMessage::from('HELP: Ctrl-S = save | Ctrl-Q = quit | Ctrl-F = find'); $this->statusMessage = StatusMessage::from('HELP: Ctrl-S = save | Ctrl-Q = quit | Ctrl-F = find');
$this->cursor = Point::new(); $this->cursor = Point::new();
$this->cursor->x += $this->cursorLeftOffset;
$this->offset = Point::new(); $this->offset = Point::new();
$this->terminalSize = Terminal::size(); $this->terminalSize = Terminal::size();
@ -125,20 +133,17 @@ class Editor {
*/ */
public function run(): void public function run(): void
{ {
$this->refreshScreen();
while ( ! $this->shouldQuit) while ( ! $this->shouldQuit)
{ {
// Don't redraw unless the screen actually needs to update! $this->refreshScreen();
if ($this->processKeypress() !== false) $this->processKeypress();
{
$this->refreshScreen();
}
} }
} }
/** /**
* Set a status message to be displayed, using printf formatting * Set a status message to be displayed, using printf formatting
* @param string $fmt
* @param mixed ...$args
*/ */
public function setStatusMessage(string $fmt, mixed ...$args): void public function setStatusMessage(string $fmt, mixed ...$args): void
{ {
@ -155,6 +160,10 @@ class Editor {
/** /**
* Cursor X to Render X * Cursor X to Render X
*
* @param Row $row
* @param int $cx
* @return int
*/ */
protected function rowCxToRx(Row $row, int $cx): int protected function rowCxToRx(Row $row, int $cx): int
{ {
@ -173,6 +182,10 @@ class Editor {
/** /**
* Render X to Cursor X * Render X to Cursor X
*
* @param Row $row
* @param int $rx
* @return int
*/ */
protected function rowRxToCx(Row $row, int $rx): int protected function rowRxToCx(Row $row, int $rx): int
{ {
@ -227,7 +240,7 @@ class Editor {
// ! Find // ! Find
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
protected function findCallback(string $query, string|KeyType $key): void protected function findCallback(string $query, string $key): void
{ {
static $lastMatch = NO_MATCH; static $lastMatch = NO_MATCH;
static $direction = SearchDirection::FORWARD; static $direction = SearchDirection::FORWARD;
@ -248,11 +261,11 @@ class Editor {
} }
$direction = match ($key) { $direction = match ($key) {
KeyType::ArrowUp, KeyType::ArrowLeft => SearchDirection::BACKWARD, KeyType::ARROW_UP, KeyType::ARROW_LEFT => SearchDirection::BACKWARD,
default => SearchDirection::FORWARD default => SearchDirection::FORWARD
}; };
$arrowKeys = [KeyType::ArrowUp, KeyType::ArrowDown, KeyType::ArrowLeft, KeyType::ArrowRight]; $arrowKeys = [KeyType::ARROW_UP, KeyType::ARROW_DOWN, KeyType::ARROW_LEFT, KeyType::ARROW_RIGHT];
// Reset search state with non arrow-key input // Reset search state with non arrow-key input
if ( ! in_array($key, $arrowKeys, true)) if ( ! in_array($key, $arrowKeys, true))
@ -280,7 +293,7 @@ class Editor {
for ($i = 0; $i < $this->document->numRows; $i++) for ($i = 0; $i < $this->document->numRows; $i++)
{ {
$current += $direction->value; $current += $direction;
if ($current === -1) if ($current === -1)
{ {
$current = $this->document->numRows - 1; $current = $this->document->numRows - 1;
@ -307,7 +320,7 @@ class Editor {
$savedHlLine = $current; $savedHlLine = $current;
$savedHl = $row->hl; $savedHl = $row->hl;
// Update the highlight array of the relevant row with the 'MATCH' type // Update the highlight array of the relevant row with the 'MATCH' type
array_replace_range($row->hl, $match, strlen($query), Highlight::SearchMatch); array_replace_range($row->hl, $match, strlen($query), Highlight::MATCH);
break; break;
} }
@ -379,13 +392,13 @@ class Editor {
($fileRow >= $this->document->numRows) ($fileRow >= $this->document->numRows)
? $this->drawPlaceholderRow($y) ? $this->drawPlaceholderRow($y)
: $this->drawRow($fileRow); : $this->drawRow($fileRow, true);
$this->outputBuffer .= "\r\n"; $this->outputBuffer .= "\r\n";
} }
} }
protected function drawRow(int $rowIdx): void protected function drawRow(int $rowIdx, bool $lineNumbers): void
{ {
$row = $this->document->row($rowIdx); $row = $this->document->row($rowIdx);
if ( ! $row->isValid()) if ( ! $row->isValid())
@ -403,13 +416,46 @@ class Editor {
$len = $this->terminalSize->cols; $len = $this->terminalSize->cols;
} }
if ($this->showLineNumbers) if ($lineNumbers)
{ {
$this->outputBuffer .= sprintf("%-{$this->numberGutter}s", ($rowIdx+1)); $this->drawLineNumbers($rowIdx);
} }
$chars = substr($row->render, $this->offset->x, (int)$len); $this->drawRowHighlighting($row, $len);
$hl = array_slice($row->hl, $this->offset->x, (int)$len);
$this->outputBuffer .= ANSI::RESET_TEXT;
$this->outputBuffer .= ANSI::color(Color::FG_WHITE);
}
protected function drawLineNumbers(int $rowIdx): void
{
$rawColCount = match (true) {
$this->document->numRows > 50 => 3,
$this->document->numRows > 500 => 4,
$this->document->numRows > 5_000 => 5,
$this->document->numRows > 50_000 => 6,
$this->document->numRows > 500_000 => 7,
default => 2,
};
$rowNumber = $rowIdx + 1;
$digitLen = strlen("$rowNumber");
$frontPadding = str_repeat(' ', $rawColCount - $digitLen);
$rearPadding = str_repeat(' ', 2);
$output = sprintf("%s%d%s", $frontPadding, $rowNumber, $rearPadding);
$this->outputBuffer .= ANSI::RESET_TEXT;
$this->outputBuffer .= ANSI::color(Color::FG_WHITE);
$this->outputBuffer .= $output;
$this->cursorLeftOffset = strlen($output);
}
protected function drawRowHighlighting(Row $row, int $len): void
{
$chars = substr($row->render, $this->offset->x, $len);
$hl = array_slice($row->hl, $this->offset->x, $len);
$currentColor = -1; $currentColor = -1;
@ -423,13 +469,15 @@ class Editor {
$sym = (ord($ch) <= 26) $sym = (ord($ch) <= 26)
? chr(ord('@') + ord($ch)) ? chr(ord('@') + ord($ch))
: '?'; : '?';
$this->outputBuffer .= ANSI::invert($sym); $this->outputBuffer .= ANSI::color(Color::INVERT);
$this->outputBuffer .= $sym;
$this->outputBuffer .= ANSI::RESET_TEXT;
if ($currentColor !== -1) if ($currentColor !== -1)
{ {
$this->outputBuffer .= ANSI::color($currentColor); $this->outputBuffer .= ANSI::color($currentColor);
} }
} }
else if ($hl[$i] === Highlight::Normal) else if ($hl[$i] === Highlight::NORMAL)
{ {
if ($currentColor !== -1) if ($currentColor !== -1)
{ {
@ -441,7 +489,7 @@ class Editor {
} }
else else
{ {
$color = get_syntax_color($hl[$i]); $color = syntax_to_color($hl[$i]);
if ($color !== $currentColor) if ($color !== $currentColor)
{ {
$currentColor = $color; $currentColor = $color;
@ -451,9 +499,6 @@ class Editor {
$this->outputBuffer .= $ch; $this->outputBuffer .= $ch;
} }
} }
$this->outputBuffer .= ANSI::RESET_TEXT;
$this->outputBuffer .= ANSI::color(Color::FG_WHITE);
} }
protected function drawPlaceholderRow(int $y): void protected function drawPlaceholderRow(int $y): void
@ -467,14 +512,17 @@ class Editor {
$welcomelen = $this->terminalSize->cols; $welcomelen = $this->terminalSize->cols;
} }
$padding = (int)floor(($this->terminalSize->cols - $welcomelen) / 2); $padding = ($this->terminalSize->cols - $welcomelen) / 2;
if ($padding > 0) if ($padding > 0)
{ {
$this->outputBuffer .= '~'; $this->outputBuffer .= '~';
$padding--; $padding--;
} }
for ($i = 0; $i < $padding; $i++)
{
$this->outputBuffer .= ' ';
}
$this->outputBuffer .= str_repeat(' ', $padding);
$this->outputBuffer .= substr($welcome, 0, $welcomelen); $this->outputBuffer .= substr($welcome, 0, $welcomelen);
} }
else else
@ -485,7 +533,7 @@ class Editor {
protected function drawStatusBar(): void protected function drawStatusBar(): void
{ {
$this->outputBuffer .= ANSI::INVERSE_TEXT; $this->outputBuffer .= ANSI::color(Color::INVERT);
$statusFilename = $this->document->filename !== '' ? $this->document->filename : '[No Name]'; $statusFilename = $this->document->filename !== '' ? $this->document->filename : '[No Name]';
$syntaxType = $this->document->fileType->name; $syntaxType = $this->document->fileType->name;
@ -541,12 +589,11 @@ class Editor {
$this->drawStatusBar(); $this->drawStatusBar();
$this->drawMessageBar(); $this->drawMessageBar();
$gutter = ($this->showLineNumbers) ? $this->numberGutter : 0;
// Specify the current cursor position // Specify the current cursor position
// The current 'x' position takes into account the length of the line number column
$this->outputBuffer .= ANSI::moveCursor( $this->outputBuffer .= ANSI::moveCursor(
$this->cursor->y - $this->offset->y, $this->cursor->y - $this->offset->y,
($this->renderX - $this->offset->x) + $gutter, $this->renderX - $this->offset->x + $this->cursorLeftOffset,
); );
$this->outputBuffer .= ANSI::SHOW_CURSOR; $this->outputBuffer .= ANSI::SHOW_CURSOR;
@ -570,21 +617,21 @@ class Editor {
$c = Terminal::readKey(); $c = Terminal::readKey();
$isModifier = in_array($c, $modifiers, TRUE); $isModifier = in_array($c, $modifiers, TRUE);
if ($c === KeyType::Escape || ($c === KeyType::Enter && $buffer !== '')) if ($c === KeyType::ESCAPE || ($c === RawKeyCode::ENTER && $buffer !== ''))
{ {
$this->setStatusMessage(''); $this->setStatusMessage('');
if ($callback !== NULL) if ($callback !== NULL)
{ {
$callback($buffer, $c); $callback($buffer, $c);
} }
return ($c === KeyType::Enter) ? $buffer : ''; return ($c === RawKeyCode::ENTER) ? $buffer : '';
} }
if ($c === KeyType::Delete || $c === KeyType::Backspace) if ($c === KeyType::DELETE || $c === KeyType::BACKSPACE)
{ {
$buffer = substr($buffer, 0, -1); $buffer = substr($buffer, 0, -1);
} }
else if (is_string($c) && is_ascii($c) && ( ! (is_ctrl($c) || $isModifier))) else if (is_ascii($c) && ( ! (is_ctrl($c) || $isModifier)))
{ {
$buffer .= $c; $buffer .= $c;
} }
@ -598,23 +645,21 @@ class Editor {
/** /**
* Input processing * Input processing
*
* Returns `false` on no keypress
*/ */
protected function processKeypress(): bool|null protected function processKeypress(): void
{ {
$c = Terminal::readKey(); $c = Terminal::readKey();
if ($c === RawKeyCode::NULL || $c === RawKeyCode::EMPTY) if ($c === RawKeyCode::NULL || $c === RawKeyCode::EMPTY)
{ {
return false; return;
} }
switch ($c) switch ($c)
{ {
case RawKeyCode::CTRL('q'): case RawKeyCode::CTRL('q'):
$this->quitAttempt(); $this->quitAttempt();
return true; return;
case RawKeyCode::CTRL('s'): case RawKeyCode::CTRL('s'):
$this->save(); $this->save();
@ -624,24 +669,24 @@ class Editor {
$this->find(); $this->find();
break; break;
case KeyType::Delete: case KeyType::DELETE:
case KeyType::Backspace: case KeyType::BACKSPACE:
$this->removeChar($c); $this->removeChar($c);
break; break;
case KeyType::ArrowUp: case KeyType::ARROW_UP:
case KeyType::ArrowDown: case KeyType::ARROW_DOWN:
case KeyType::ArrowLeft: case KeyType::ARROW_LEFT:
case KeyType::ArrowRight: case KeyType::ARROW_RIGHT:
case KeyType::PageUp: case KeyType::PAGE_UP:
case KeyType::PageDown: case KeyType::PAGE_DOWN:
case KeyType::Home: case KeyType::HOME:
case KeyType::End: case KeyType::END:
$this->moveCursor($c); $this->moveCursor($c);
break; break;
case RawKeyCode::CTRL('l'): case RawKeyCode::CTRL('l'):
case KeyType::Escape: case KeyType::ESCAPE:
// Do nothing // Do nothing
break; break;
@ -656,17 +701,15 @@ class Editor {
$this->quitTimes = KILO_QUIT_TIMES; $this->quitTimes = KILO_QUIT_TIMES;
$this->setStatusMessage(''); $this->setStatusMessage('');
} }
return true;
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// ! Editor operation helpers // ! Editor operation helpers
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
protected function moveCursor(KeyType $key): void protected function moveCursor(string $key): void
{ {
$x = $this->cursor->x; $x = saturating_sub($this->cursor->x, $this->cursorLeftOffset);
$y = $this->cursor->y; $y = $this->cursor->y;
$row = $this->document->row($y); $row = $this->document->row($y);
if ( ! $row->isValid()) if ( ! $row->isValid())
@ -676,7 +719,7 @@ class Editor {
switch ($key) switch ($key)
{ {
case KeyType::ArrowLeft: case KeyType::ARROW_LEFT:
if ($x !== 0) if ($x !== 0)
{ {
$x--; $x--;
@ -685,11 +728,11 @@ class Editor {
{ {
// Beginning of a line, go to end of previous line // Beginning of a line, go to end of previous line
$y--; $y--;
$x = $row->size - 1; $x = $this->document->row($y)->size - 1;
} }
break; break;
case KeyType::ArrowRight: case KeyType::ARROW_RIGHT:
if ($x < $row->size) if ($x < $row->size)
{ {
$x++; $x++;
@ -699,43 +742,43 @@ class Editor {
$y++; $y++;
$x = 0; $x = 0;
} }
break; break;
case KeyType::ArrowUp: case KeyType::ARROW_UP:
if ($y !== 0) if ($y !== 0)
{ {
$y--; $y--;
} }
break; break;
case KeyType::ArrowDown: case KeyType::ARROW_DOWN:
if ($y < $this->document->numRows) if ($y < $this->document->numRows)
{ {
$y++; $y++;
} }
break; break;
case KeyType::PageUp: case KeyType::PAGE_UP:
$y = saturating_sub($y, $this->terminalSize->rows); $y = saturating_sub($y, $this->terminalSize->rows);
break; break;
case KeyType::PageDown: case KeyType::PAGE_DOWN:
$y = saturating_add($y, $this->terminalSize->rows, $this->document->numRows); $y = saturating_add($y, $this->terminalSize->rows, $this->document->numRows);
break; break;
case KeyType::Home: case KeyType::HOME:
$x = 0; $x = 0;
break; break;
case KeyType::End: case KeyType::END:
if ($y < $this->document->numRows) if ($y < $this->document->numRows)
{ {
$x = $row->size; $x = $row->size;
} }
break; break;
default: default:
// Do nothing return;
} }
// Snap cursor to the end of a row when moving // Snap cursor to the end of a row when moving
@ -749,27 +792,28 @@ class Editor {
} }
$this->cursor->x = $x; $this->cursor->x = $x;
$this->cursor->y = $y;
} }
$this->cursor->y = ($y >= $this->document->numRows) ? $this->document->numRows - 1 : $y;
} }
protected function insertChar(string|KeyType $c): void protected function insertChar(string $c): void
{ {
$this->document->insert($this->cursor, $c); $this->document->insert($this->cursor, $c);
$this->moveCursor(KeyType::ArrowRight); $this->moveCursor(KeyType::ARROW_RIGHT);
} }
protected function removeChar(string|KeyType $ch): void protected function removeChar(string $ch): void
{ {
if ($ch === KeyType::Delete) if ($ch === KeyType::DELETE)
{ {
$this->document->delete($this->cursor, $this->numberGutter); $this->document->delete($this->cursor);
} }
if ($ch === KeyType::Backspace && (($this->cursor->x >= $this->numberGutter) || $this->cursor->y > 0)) if ($ch === KeyType::BACKSPACE && ($this->cursor->x > $this->cursorLeftOffset || $this->cursor->y > 0))
{ {
$this->moveCursor(KeyType::ArrowLeft); $this->moveCursor(KeyType::ARROW_LEFT);
$this->document->delete($this->cursor, $this->numberGutter); $this->document->delete($this->cursor);
} }
} }

150
src/Enum/C.php Normal file
View File

@ -0,0 +1,150 @@
<?php declare(strict_types=1);
namespace Aviat\Kilo\Enum;
use Aviat\Kilo\Traits;
/**
* Just a namespace for C language constants
*/
class C {
use Traits\ConstList;
// ------------------------------------------------------------------------
// ! Misc I/O constants
// ------------------------------------------------------------------------
public const STDIN_FILENO = 0;
public const STDOUT_FILENO = 1;
public const STDERR_FILENO = 2;
public const TCSAFLUSH = 2;
// ------------------------------------------------------------------------
// ! Termios flags and constants
// ------------------------------------------------------------------------
/* Input modes */
public const IGNBRK = (1 << 0); /* Ignore break condition. */
public const BRKINT = (1 << 1); /* Signal interrupt on break. */
public const IGNPAR = (1 << 2); /* Ignore characters with parity errors. */
public const PARMRK = (1 << 3); /* Mark parity and framing errors. */
public const INPCK = (1 << 4); /* Enable input parity check. */
public const ISTRIP = (1 << 5); /* Strip 8th bit off characters. */
public const INLCR = (1 << 6); /* Map NL to CR on input. */
public const IGNCR = (1 << 7); /* Ignore CR. */
public const ICRNL = (1 << 8); /* Map CR to NL on input. */
public const IXON = (1 << 9); /* Enable start/stop output control. */
public const IXOFF = (1 << 10); /* Enable start/stop input control. */
public const IXANY = (1 << 11); /* Any character will restart after stop. */
public const IMAXBEL = (1 << 13); /* Ring bell when input queue is full. */
public const IUCLC = (1 << 14); /* Translate upper case input to lower case. */
/* Output modes */
public const OPOST = (1 << 0); /* Perform output processing. */
public const ONLCR = (1 << 1); /* Map NL to CR-NL on output. */
public const OXTABS = (1 << 2); /* Expand tabs to spaces. */
public const ONOEOT = (1 << 3); /* Discard EOT (^D) on output. */
public const OCRNL = (1 << 4); /* Map CR to NL. */
public const ONOCR = (1 << 5); /* Discard CR's when on column 0. */
public const ONLRET = (1 << 6); /* Move to column 0 on NL. */
public const NLDLY = (3 << 8); /* NL delay. */
public const NL0 = (0 << 8); /* NL type 0. */
public const NL1 = (1 << 8); /* NL type 1. */
public const TABDLY = (3 << 10 | 1 << 2); /* TAB delay. */
public const TAB0 = (0 << 10); /* TAB delay type 0. */
public const TAB1 = (1 << 10); /* TAB delay type 1. */
public const TAB2 = (2 << 10); /* TAB delay type 2. */
public const TAB3 = (1 << 2); /* Expand tabs to spaces. */
public const CRDLY = (3 << 12); /* CR delay. */
public const CR0 = (0 << 12); /* CR delay type 0. */
public const CR1 = (1 << 12); /* CR delay type 1. */
public const CR2 = (2 << 12); /* CR delay type 2. */
public const CR3 = (3 << 12); /* CR delay type 3. */
public const FFDLY = (1 << 14); /* FF delay. */
public const FF0 = (0 << 14); /* FF delay type 0. */
public const FF1 = (1 << 14); /* FF delay type 1. */
public const BSDLY = (1 << 15); /* BS delay. */
public const BS0 = (0 << 15); /* BS delay type 0. */
public const BS1 = (1 << 15); /* BS delay type 1. */
public const VTDLY = (1 << 16); /* VT delay. */
public const VT0 = (0 << 16); /* VT delay type 0. */
public const VT1 = (1 << 16); /* VT delay type 1. */
public const OLCUC = (1 << 17); /* Translate lower case output to upper case */
public const OFILL = (1 << 18); /* Send fill characters for delays. */
public const OFDEL = (1 << 19); /* Fill is DEL. */
/* Control modes */
public const CIGNORE = (1 << 0); /* Ignore these control flags. */
public const CS5 = 0; /* 5 bits per byte. */
public const CS6 = (1 << 8); /* 6 bits per byte. */
public const CS7 = (1 << 9); /* 7 bits per byte. */
public const CS8 = (C::CS6|C::CS7); /* 8 bits per byte. */
public const CSIZE = (C::CS5|C::CS6|C::CS7|C::CS8); /* Number of bits per byte (mask). */
public const CSTOPB = (1 << 10); /* Two stop bits instead of one. */
public const CREAD = (1 << 11); /* Enable receiver. */
public const PARENB = (1 << 12); /* Parity enable. */
public const PARODD = (1 << 13); /* Odd parity instead of even. */
public const HUPCL = (1 << 14); /* Hang up on last close. */
public const CLOCAL = (1 << 15); /* Ignore modem status lines. */
public const CRTSCTS = (1 << 16); /* RTS/CTS flow control. */
public const CRTS_IFLOW = C::CRTSCTS; /* Compatibility. */
public const CCTS_OFLOW = C::CRTSCTS; /* Compatibility. */
public const CDTRCTS = (1 << 17); /* DTR/CTS flow control. */
public const MDMBUF = (1 << 20); /* DTR/DCD flow control. */
public const CHWFLOW = (C::MDMBUF|C::CRTSCTS|C::CDTRCTS); /* All types of flow control. */
/* Local modes */
public const ECHOKE = (1 << 0); /* Visual erase for KILL. */
public const _ECHOE = (1 << 1); /* Visual erase for ERASE. */
public const ECHOE = C::_ECHOE;
public const _ECHOK = (1 << 2); /* Echo NL after KILL. */
public const ECHOK = C::_ECHOK;
public const _ECHO = (1 << 3); /* Enable echo. */
public const ECHO = C::_ECHO;
public const _ECHONL = (1 << 4); /* Echo NL even if ECHO is off. */
public const ECHONL = C::_ECHONL;
public const ECHOPRT = (1 << 5); /* Hardcopy visual erase. */
public const ECHOCTL = (1 << 6); /* Echo control characters as ^X. */
public const _ISIG = (1 << 7); /* Enable signals. */
public const ISIG = C::_ISIG;
public const _ICANON = (1 << 8); /* Do erase and kill processing. */
public const ICANON = C::_ICANON;
public const ALTWERASE = (1 << 9); /* Alternate WERASE algorithm. */
public const _IEXTEN = (1 << 10); /* Enable DISCARD and LNEXT. */
public const IEXTEN = C::_IEXTEN;
public const EXTPROC = (1 << 11); /* External processing. */
public const _TOSTOP = (1 << 22); /* Send SIGTTOU for background output. */
public const TOSTOP = C::_TOSTOP;
public const FLUSHO = (1 << 23); /* Output being flushed (state). */
public const XCASE = (1 << 24); /* Canonical upper/lower case. */
public const NOKERNINFO = (1 << 25); /* Disable VSTATUS. */
public const PENDIN = (1 << 29); /* Retype pending input (state). */
public const _NOFLSH = (1 << 31); /* Disable flush after interrupt. */
public const NOFLSH = C::_NOFLSH;
/* Control characters */
public const VEOF = 0; /* End-of-file character [ICANON]. */
public const VEOL = 1; /* End-of-line character [ICANON]. */
public const VEOL2 = 2; /* Second EOL character [ICANON]. */
public const VERASE = 3; /* Erase character [ICANON]. */
public const VWERASE = 4; /* Word-erase character [ICANON]. */
public const VKILL = 5; /* Kill-line character [ICANON]. */
public const VREPRINT = 6; /* Reprint-line character [ICANON]. */
public const VINTR = 8; /* Interrupt character [ISIG]. */
public const VQUIT = 9; /* Quit character [ISIG]. */
public const VSUSP = 10; /* Suspend character [ISIG]. */
public const VDSUSP = 11; /* Delayed suspend character [ISIG]. */
public const VSTART = 12; /* Start (X-ON) character [IXON, IXOFF]. */
public const VSTOP = 13; /* Stop (X-OFF) character [IXON, IXOFF]. */
public const VLNEXT = 14; /* Literal-next character [IEXTEN]. */
public const VDISCARD = 15; /* Discard character [IEXTEN]. */
public const VMIN = 16; /* Minimum number of bytes read at once [!ICANON]. */
public const VTIME = 17; /* Time-out value (tenths of a second) [!ICANON]. */
public const VSTATUS = 18; /* Status character [ICANON]. */
public const NCCS = 20; /* Value duplicated in <hurd/tioctl.defs>. */
// ------------------------------------------------------------------------
// ! IOCTL constants
// ------------------------------------------------------------------------
public const TIOCGWINSZ = 0x5413;
}

51
src/Enum/Color.php Normal file
View File

@ -0,0 +1,51 @@
<?php declare(strict_types=1);
namespace Aviat\Kilo\Enum;
use Aviat\Kilo\Traits;
/**
* ANSI Color escape sequences
* @enum
*/
class Color {
use Traits\ConstList;
// Foreground colors
public const FG_BLACK = 30;
public const FG_RED = 31;
public const FG_GREEN = 32;
public const FG_YELLOW = 33;
public const FG_BLUE = 34;
public const FG_MAGENTA = 35;
public const FG_CYAN = 36;
public const FG_WHITE = 37;
public const FG_BRIGHT_BLACK = 90;
public const FG_BRIGHT_RED = 91;
public const FG_BRIGHT_GREEN = 92;
public const FG_BRIGHT_YELLOW = 93;
public const FG_BRIGHT_BLUE = 94;
public const FG_BRIGHT_MAGENTA = 95;
public const FG_BRIGHT_CYAN = 96;
public const FG_BRIGHT_WHITE = 97;
// Background colors
public const BG_BLACK = 40;
public const BG_RED = 41;
public const BG_GREEN = 42;
public const BG_YELLOW = 43;
public const BG_BLUE = 44;
public const BG_MAGENTA = 45;
public const BG_CYAN = 46;
public const BG_WHITE = 47;
public const BG_BRIGHT_BLACK = 100;
public const BG_BRIGHT_RED = 101;
public const BG_BRIGHT_GREEN = 102;
public const BG_BRIGHT_YELLOW = 103;
public const BG_BRIGHT_BLUE = 104;
public const BG_BRIGHT_MAGENTA = 105;
public const BG_BRIGHT_CYAN = 106;
public const BG_BRIGHT_WHITE = 107;
public const INVERT = 7;
}

View File

@ -3,49 +3,46 @@
namespace Aviat\Kilo\Enum; namespace Aviat\Kilo\Enum;
use Aviat\Kilo\Traits; use Aviat\Kilo\Traits;
use JsonSerializable;
/** /**
* @enum * @enum
*/ */
enum Highlight implements JsonSerializable { class Highlight {
use Traits\EnumTrait; use Traits\ConstList;
case Normal; public const NORMAL = 0;
case Comment; public const COMMENT = 1;
case MultiLineComment; public const ML_COMMENT = 2;
case Keyword1; public const KEYWORD1 = 3;
case Keyword2; public const KEYWORD2 = 4;
case String; public const STRING = 5;
case Number; public const NUMBER = 6;
case Operator; public const OPERATOR = 7;
case Variable; public const VARIABLE = 8;
case Delimiter; public const DELIMITER = 9;
case Invalid; public const INVALID = 10;
case SearchMatch; public const MATCH = 11;
case Identifier; public const IDENTIFIER = 12;
case Character; public const CHARACTER = 13;
case Embed;
/** /**
* Map a PHP syntax token to its associated highlighting type * Map a PHP syntax token to its associated highlighting type
* *
* @param int $token * @param int $token
* @return Highlight * @return int
*/ */
public static function fromPHPToken(int $token): self public static function fromPHPToken(int $token): int
{ {
return match($token) { return match($token) {
// Delimiters // Delimiters
T_ARRAY, T_ARRAY,
T_ATTRIBUTE,
T_CURLY_OPEN, T_CURLY_OPEN,
T_DOLLAR_OPEN_CURLY_BRACES, T_DOLLAR_OPEN_CURLY_BRACES,
T_OPEN_TAG, T_OPEN_TAG,
T_OPEN_TAG_WITH_ECHO, T_OPEN_TAG_WITH_ECHO,
T_CLOSE_TAG, T_CLOSE_TAG,
T_START_HEREDOC, T_START_HEREDOC,
T_END_HEREDOC => Highlight::Delimiter, T_END_HEREDOC => Highlight::DELIMITER,
// Number literals and magic constants // Number literals and magic constants
T_CLASS_C, T_CLASS_C,
@ -58,18 +55,16 @@ enum Highlight implements JsonSerializable {
T_METHOD_C, T_METHOD_C,
T_NS_C, T_NS_C,
T_NUM_STRING, T_NUM_STRING,
T_TRAIT_C => Highlight::Number, T_TRAIT_C => Highlight::NUMBER,
// String literals // String literals
T_CONSTANT_ENCAPSED_STRING, T_ENCAPSED_AND_WHITESPACE => Highlight::String, T_CONSTANT_ENCAPSED_STRING, T_ENCAPSED_AND_WHITESPACE => Highlight::STRING,
// Simple variables // Simple variables
T_VARIABLE, T_STRING_VARNAME => Highlight::Variable, T_VARIABLE, T_STRING_VARNAME => Highlight::VARIABLE,
// Operators // Operators
T_AS, T_AS,
T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG,
T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG,
T_AND_EQUAL, T_AND_EQUAL,
T_BOOLEAN_AND, T_BOOLEAN_AND,
T_BOOLEAN_OR, T_BOOLEAN_OR,
@ -108,7 +103,7 @@ enum Highlight implements JsonSerializable {
T_SL_EQUAL, T_SL_EQUAL,
T_SR, T_SR,
T_SR_EQUAL, T_SR_EQUAL,
T_XOR_EQUAL => Highlight::Operator, T_XOR_EQUAL => Highlight::OPERATOR,
// Keywords1 // Keywords1
T_ABSTRACT, T_ABSTRACT,
@ -171,10 +166,10 @@ enum Highlight implements JsonSerializable {
T_VAR, T_VAR,
T_WHILE, T_WHILE,
T_YIELD, T_YIELD,
T_YIELD_FROM => Highlight::Keyword1, T_YIELD_FROM => Highlight::KEYWORD1,
// Not string literals, but identifiers, keywords, etc. // Not string literals, but identifiers, keywords, etc.
T_STRING => Highlight::Identifier, T_STRING => Highlight::IDENTIFIER,
// Types and casts // Types and casts
T_ARRAY_CAST, T_ARRAY_CAST,
@ -184,14 +179,12 @@ enum Highlight implements JsonSerializable {
T_INT_CAST, T_INT_CAST,
T_OBJECT_CAST, T_OBJECT_CAST,
T_STRING_CAST, T_STRING_CAST,
T_UNSET_CAST => Highlight::Keyword2, T_UNSET_CAST => Highlight::KEYWORD2,
// Comments // Invalid syntax
T_DOC_COMMENT => Highlight::MultiLineComment, T_BAD_CHARACTER => Highlight::INVALID,
T_INLINE_HTML => Highlight::Embed, default => Highlight::NORMAL,
default => Highlight::Normal,
}; };
} }
@ -200,19 +193,19 @@ enum Highlight implements JsonSerializable {
* highlighting type * highlighting type
* *
* @param string $char * @param string $char
* @return Highlight * @return int
*/ */
public static function fromPHPChar(string $char): self public static function fromPHPChar(string $char): int
{ {
return match ($char) { return match ($char) {
// Delimiter characters // Delimiter characters
'[', ']', '{', '}', '(', ')', '"', "'" => Highlight::Delimiter, '[', ']', '{', '}', '(', ')', '"', "'" => Highlight::DELIMITER,
// Single character operators // Single character operators
'?', ',', ';', ':', '^', '%', '+', '-', '?', ',', ';', ':', '^', '%', '+', '-',
'*', '/', '.', '|', '~', '>', '<', '=', '!' => Highlight::Operator, '*', '/', '.', '|', '~', '>', '<', '=', '!' => Highlight::OPERATOR,
default => Highlight::Normal, default => Highlight::NORMAL,
}; };
} }
} }

View File

@ -3,34 +3,24 @@
namespace Aviat\Kilo\Enum; namespace Aviat\Kilo\Enum;
use Aviat\Kilo\Traits; use Aviat\Kilo\Traits;
use JsonSerializable;
/** /**
* Enum representing various control keys * Constants representing various control keys
* @enum
*/ */
enum KeyType implements JsonSerializable { class KeyType {
use Traits\EnumTrait;
use Traits\ConstList; use Traits\ConstList;
// ------------------------------------------------------------------------ public const ARROW_DOWN = 'KEY_ARROW_DOWN';
// Movement Keys public const ARROW_LEFT = 'KEY_ARROW_LEFT';
// ------------------------------------------------------------------------ public const ARROW_RIGHT = 'KEY_ARROW_RIGHT';
case ArrowUp; public const ARROW_UP = 'KEY_ARROW_UP';
case ArrowDown; public const BACKSPACE = 'KEY_BACKSPACE';
case ArrowLeft; public const DELETE = 'KEY_DELETE';
case ArrowRight; public const END = 'KEY_END';
case Home; public const ENTER = 'KEY_ENTER';
case End; public const ESCAPE = 'KEY_ESCAPE';
case PageUp; public const HOME = 'KEY_HOME';
case PageDown; public const PAGE_DOWN = 'KEY_PAGE_DOWN';
// ------------------------------------------------------------------------ public const PAGE_UP = 'KEY_PAGE_UP';
// Editing Keys
// ------------------------------------------------------------------------
case Backspace;
case Delete;
case Enter;
// ------------------------------------------------------------------------
// Others
// ------------------------------------------------------------------------
case Escape;
} }

View File

@ -7,9 +7,9 @@ use Aviat\Kilo\Traits;
/** /**
* @enum * @enum
*/ */
enum SearchDirection: int { class SearchDirection {
use Traits\ConstList; use Traits\ConstList;
case FORWARD = 1; public const FORWARD = 1;
case BACKWARD = -1; public const BACKWARD = -1;
} }

View File

@ -1,13 +0,0 @@
<?php declare(strict_types=1);
namespace Aviat\Kilo\Enum;
use Aviat\Kilo\Traits;
use JsonSerializable;
enum SyntaxFamily implements JsonSerializable {
use Traits\EnumTrait;
case C;
case XML;
}

View File

@ -2,11 +2,12 @@
namespace Aviat\Kilo; namespace Aviat\Kilo;
use Aviat\Kilo\Enum\SyntaxFamily;
class FileType { class FileType {
/** /**
* Create the FileType object from the filename * Create the FileType object from the filename
*
* @param string|null $filename
* @return self
*/ */
public static function from(?string $filename): self public static function from(?string $filename): self
{ {
@ -17,6 +18,9 @@ class FileType {
/** /**
* Create the Syntax object from the filename * Create the Syntax object from the filename
*
* @param string $filename
* @return Syntax
*/ */
private static function getSyntaxFromFilename(string $filename): Syntax private static function getSyntaxFromFilename(string $filename): Syntax
{ {
@ -111,7 +115,7 @@ class FileType {
'usize', '&str', 'Copy', 'Drop', 'From', 'Into', 'None', 'Self', 'Send', 'Some', 'usize', '&str', 'Copy', 'Drop', 'From', 'Into', 'None', 'Self', 'Send', 'Some',
'Sync', 'bool', 'char', 'i128', 'u128', 'Box', 'Err', 'Ord', 'Vec', 'dyn', 'f32', 'Sync', 'bool', 'char', 'i128', 'u128', 'Box', 'Err', 'Ord', 'Vec', 'dyn', 'f32',
'f64', 'i16', 'i32', 'i64', 'str', 'u16', 'u32', 'u64', 'Eq', 'Fn', 'Ok', 'i8', 'u8', 'f64', 'i16', 'i32', 'i64', 'str', 'u16', 'u32', 'u64', 'Eq', 'Fn', 'Ok', 'i8', 'u8',
'&mut self', '&mut', '&self', 'self', '&mut self', '&mut', '&self', 'self', '()',
], ],
[ [
'...', '=>', '..', '>>=', '<<=', '--', '%=', '>>', ':=', '++', '/=', '<<', '>=', '...', '=>', '..', '>>=', '<<=', '--', '%=', '>>', ':=', '++', '/=', '<<', '>=',
@ -121,89 +125,9 @@ class FileType {
hasCharType: true, hasCharType: true,
highlightCharacters: true, highlightCharacters: true,
), ),
'.html', '.htm', '.shtml' => Syntax::new(
'Html',
[
'!doctype', '!DOCTYPE', 'html', 'base', 'head', 'link', 'meta', 'style', 'title', 'body', 'address', 'article',
'aside', 'footer', 'header', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'main', 'nav', 'section',
'blockquote', 'dd', 'div', 'dl', 'dt', 'figcaption', 'figure', 'hr', 'li', 'ol', 'p', 'pre',
'ul', 'a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'dfn', 'em', 'i', 'kbd',
'mark', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'time',
'u', 'var', 'wbr', 'area', 'audio', 'img', 'map', 'track', 'video', 'embed', 'iframe', 'object',
'param', 'picture', 'portal', 'source', 'svg', 'math', 'canvas', 'noscript', 'script', 'del',
'ins', 'caption', 'col', 'colgroup', 'table', 'td', 'tbody', 'tfoot', 'th', 'thead', 'tr',
'button', 'datalist', 'fieldset', 'form', 'input', 'label', 'legend', 'meter', 'optgroup',
'option', 'output', 'progress', 'select', 'textarea', 'details', 'dialog', 'menu', 'summary',
'slot', 'template', 'frameset', 'frame', 'noframes', 'marquee', 'font', 'center',
],
[
'id', 'class', 'for', 'style', 'charset'
],
operators: ['</', '<', '=', '>'],
slcs: '',
mcs: '<!--',
mce: '-->',
highlightNumbers: false,
hasCommonOperators: false,
syntaxFamily: SyntaxFamily::XML,
),
'.xml' => Syntax::new(
'XML',
[
'!doctype', '!element', '<?xml', '?>',
],
operators: ['<', '=', '>'],
slcs: '',
mcs: '<!--',
mce: '-->',
highlightNumbers: false,
hasCommonOperators: false,
syntaxFamily: SyntaxFamily::XML,
),
'.zig' => Syntax::new(
'Zig',
[
'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',
'pub', 'fn', 'orelse', 'catch', 'and', 'or', 'comptime', 'test', 'var', 'opaque',
'usingnamespace', 'errdefer', 'callconv', 'unreachable', 'defer', 'inline',
'@addrSpaceCast', '@addWithOverflow', '@alignCast', '@alignOf', '@as', '@atomicLoad', '@atomicRmw',
'@atomicStore', '@bitCast', '@bitOffsetOf', '@bitSizeOf', '@breakpoint', '@mulAdd', '@byteSwap',
'@bitReverse', '@offsetOf', '@call', '@cDefine', '@cImport', '@cInclude', '@clz', '@cmpxchgStrong',
'@cmpxchgWeak', '@compileError', '@compileLog', '@constCast', '@ctz', '@cUndef', '@cVaArg', '@cVaCopy',
'@cVaEnd', '@cVaStart', '@divExact', '@divFloor', '@divTrunc', '@embedFile', '@enumFromInt', '@errorFromInt',
'@errorName', '@errorReturnTrace', '@errorCast', '@export', '@extern', '@fence', '@field', '@fieldParentPtr',
'@floatCast', '@floatFromInt', '@frameAddress', '@hasDecl', '@hasField', '@import', '@inComptime',
'@intCast', '@intFromBool', '@intFromEnum', '@intFromError', '@intFromFloat', '@intFromPtr', '@max',
'@memcpy', '@memset', '@min', "@wasmMemorySize", '@wasmMemoryGrow', '@mod', '@mulWithOverflow', '@panic',
'@popCount', '@prefetch', '@ptrCast', '@ptrFromInt', '@rem', '@returnAddress', '@select', '@setAlignStack',
'@setCold', '@setEvalBranchQuota', '@setFloatMode', '@setRuntimeSafety', '@shlExact', '@shlWithOverflow',
'@shrExact', '@shuffle', '@sizeOf', '@splat', '@reduce', '@src', '@sqrt', '@sin', '@cos', '@tan', '@exp',
'@exp2', '@log', '@log2', '@log10', '@abs', '@floor', '@ceil', '@trunc', '@round', '@subWithOverflow',
'@tagName', '@This', '@trap', '@truncate', '@Type', '@typeInfo', '@typeName', '@TypeOf', '@unionint',
'@Vector', '@volatileCast', '@workGroupId', '@workGroupSize', '@workItemId',
],
[
'i8', 'u8', 'i16', 'u16', 'i32', 'u32', 'i64', 'u64', 'i128', 'u128', 'isize', 'usize',
'c_char', 'c_short', 'c_ushort', 'c_int', 'c_uint', 'c_long', 'c_ulong', 'c_longlong',
'c_ulonglong', 'c_longdouble',
'f16', 'f32', 'f64', 'f80', 'f128', 'bool', 'anyopaque', 'void', 'noreturn', 'type',
'anyerror', 'comptime_init', 'comptime_float',
],
[
'<=>', '<<=', '>>=', '...',
'++', '--', '==', '!=', '>=', '<=', '&&', '||', '<<', '>>',
'+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '->', '::',
'+%', '+%=', '+|', '+|=', '-%', '-%=', '*%', '*%=', '*|', '*|=',
'.?', '++', '**', '.*',
],
mcs: '',
mce: '',
),
default => Syntax::default(), default => Syntax::default(),
}; };
} }
private function __construct(public string $name, public Syntax $syntax) {} private function __construct(public string $name, public Syntax $syntax) {}
} }

View File

@ -28,6 +28,11 @@ class Row {
/** /**
* Create a row in the current document * Create a row in the current document
*
* @param Document $parent
* @param string $chars
* @param int $idx
* @return self
*/ */
public static function new(Document $parent, string $chars, int $idx): self public static function new(Document $parent, string $chars, int $idx): self
{ {
@ -40,6 +45,8 @@ class Row {
/** /**
* Create an empty Row * Create an empty Row
*
* @return self
*/ */
public static function default(): self public static function default(): self
{ {
@ -54,22 +61,19 @@ class Row {
/** /**
* The document that this row belongs to * The document that this row belongs to
*/ */
private readonly Document $parent, private Document $parent,
/** /**
* The raw characters in the row * @var string The raw characters in the row
*/ */
private string $chars, private string $chars,
/** /**
* The line number of the current row * @var int The line number of the current row
*/ */
public int $idx, public int $idx,
) {} ) {}
/**
* Set up dynamically generated properties
*/
public function __get(string $name): mixed public function __get(string $name): mixed
{ {
return match ($name) return match ($name)
@ -83,6 +87,8 @@ class Row {
/** /**
* Convert the row contents to a string for saving * Convert the row contents to a string for saving
*
* @return string
*/ */
public function __toString(): string public function __toString(): string
{ {
@ -91,6 +97,8 @@ class Row {
/** /**
* Set the properties to display for var_dump * Set the properties to display for var_dump
*
* @return array
*/ */
public function __debugInfo(): array public function __debugInfo(): array
{ {
@ -106,6 +114,8 @@ class Row {
/** /**
* Is this row a valid part of a document? * Is this row a valid part of a document?
*
* @return bool
*/ */
public function isValid(): bool public function isValid(): bool
{ {
@ -115,8 +125,8 @@ class Row {
/** /**
* Insert the string or character $c at index $at * Insert the string or character $c at index $at
* *
* @param int $at The point to insert at * @param int $at
* @param string $c The string to insert * @param string $c
*/ */
public function insert(int $at, string $c): void public function insert(int $at, string $c): void
{ {
@ -133,6 +143,8 @@ class Row {
/** /**
* Append $s to the current row * Append $s to the current row
*
* @param string $s
*/ */
public function append(string $s): void public function append(string $s): void
{ {
@ -143,7 +155,7 @@ class Row {
/** /**
* Delete the character at the specified index * Delete the character at the specified index
* *
* @param int $at The index to delete * @param int $at
*/ */
public function delete(int $at): void public function delete(int $at): void
{ {
@ -158,6 +170,8 @@ class Row {
/** /**
* Set the contents of the Row * Set the contents of the Row
*
* @param string $chars
*/ */
public function setChars(string $chars): void public function setChars(string $chars): void
{ {
@ -183,7 +197,7 @@ class Row {
*/ */
public function highlight(): void 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->fileType->name === 'PHP')
{ {
@ -216,10 +230,10 @@ class Row {
{ {
if ($inComment) if ($inComment)
{ {
$this->hl[$i] = Highlight::MultiLineComment; $this->hl[$i] = Highlight::ML_COMMENT;
if (substr($this->render, $i, $mceLen) === $mce) if (substr($this->render, $i, $mceLen) === $mce)
{ {
array_replace_range($this->hl, $i, $mceLen, Highlight::MultiLineComment); array_replace_range($this->hl, $i, $mceLen, Highlight::ML_COMMENT);
$i += $mceLen; $i += $mceLen;
$inComment = FALSE; $inComment = FALSE;
continue; continue;
@ -231,7 +245,7 @@ class Row {
if (substr($this->render, $i, $mcsLen) === $mcs) if (substr($this->render, $i, $mcsLen) === $mcs)
{ {
array_replace_range($this->hl, $i, $mcsLen, Highlight::MultiLineComment); array_replace_range($this->hl, $i, $mcsLen, Highlight::ML_COMMENT);
$i += $mcsLen; $i += $mcsLen;
$inComment = TRUE; $inComment = TRUE;
continue; continue;
@ -245,11 +259,18 @@ class Row {
|| $this->highlightSecondaryKeywords($i, $syntax) || $this->highlightSecondaryKeywords($i, $syntax)
|| $this->highlightString($i, $syntax) || $this->highlightString($i, $syntax)
|| $this->highlightOperators($i, $syntax) || $this->highlightOperators($i, $syntax)
|| $this->highlightCommonDelimiters($i) || $this->highlightCommonDelimeters($i)
|| $this->highlightCommonOperators($i, $syntax) || $this->highlightCommonOperators($i, $syntax)
|| $this->highlightNumber($i, $syntax) || $this->highlightNumber($i, $syntax)
) { ) {
continue; if ($i >= $this->rsize)
{
break;
}
else
{
continue;
}
} }
$i++; $i++;
@ -265,11 +286,15 @@ class Row {
/** /**
* Highlight number literals * Highlight number literals
*
* @param int $i
* @param Syntax $opts
* @return bool
*/ */
protected function highlightNumber(int &$i, Syntax $opts): bool protected function highlightNumber(int &$i, Syntax $opts): bool
{ {
$char = $this->render[$i]; $char = $this->render[$i];
if ($opts->numbers() && is_digit($char) && $this->hl[$i] === Highlight::Normal) if ($opts->numbers() && is_digit($char) && $this->hl[$i] === Highlight::NORMAL)
{ {
if ($i > 0) if ($i > 0)
{ {
@ -282,7 +307,7 @@ class Row {
while (true) while (true)
{ {
$this->hl[$i] = Highlight::Number; $this->hl[$i] = Highlight::NUMBER;
$i++; $i++;
if ($i < strlen($this->render)) if ($i < strlen($this->render))
@ -305,8 +330,14 @@ class Row {
/** /**
* Highlight keywords and/or operators * Highlight keywords and/or operators
*
* @param int $i
* @param array $keywords
* @param int $syntaxType
* @param bool $requireSeparator
* @return bool
*/ */
protected function highlightWord(int &$i, array $keywords, Highlight $syntaxType, bool $requireSeparator = true): bool protected function highlightWord(int &$i, array $keywords, int $syntaxType, bool $requireSeparator = true): bool
{ {
if ($i > 0 && $requireSeparator) if ($i > 0 && $requireSeparator)
{ {
@ -338,10 +369,15 @@ class Row {
/** /**
* Highlight a single-char character from a list of provided keywords * Highlight a single-char character from a list of provided keywords
*
* @param int $i
* @param array $chars
* @param int $syntaxType
* @return bool
*/ */
protected function highlightChar(int &$i, array $chars, Highlight $syntaxType): bool protected function highlightChar(int &$i, array $chars, int $syntaxType): bool
{ {
if ($this->hl[$i] !== Highlight::Normal) if ($this->hl[$i] !== Highlight::NORMAL)
{ {
return false; return false;
} }
@ -361,30 +397,46 @@ class Row {
/** /**
* Highlight primary keywords * Highlight primary keywords
*
* @param int $i
* @param Syntax $opts
* @return bool
*/ */
protected function highlightPrimaryKeywords(int &$i, Syntax $opts): bool protected function highlightPrimaryKeywords(int &$i, Syntax $opts): bool
{ {
return $this->highlightWord($i, $opts->keywords1, Highlight::Keyword1); return $this->highlightWord($i, $opts->keywords1, Highlight::KEYWORD1);
} }
/** /**
* Highlight secondary keywords * Highlight secondary keywords
*
* @param int $i
* @param Syntax $opts
* @return bool
*/ */
protected function highlightSecondaryKeywords(int &$i, Syntax $opts): bool protected function highlightSecondaryKeywords(int &$i, Syntax $opts): bool
{ {
return $this->highlightWord($i, $opts->keywords2, Highlight::Keyword2); return $this->highlightWord($i, $opts->keywords2, Highlight::KEYWORD2);
} }
/** /**
* Highlight language-specific operators * Highlight language-specific operators
*
* @param int $i
* @param Syntax $opts
* @return bool
*/ */
protected function highlightOperators(int &$i, Syntax $opts): bool protected function highlightOperators(int &$i, Syntax $opts): bool
{ {
return $this->highlightWord($i, $opts->operators, Highlight::Operator, false); return $this->highlightWord($i, $opts->operators, Highlight::OPERATOR, false);
} }
/** /**
* Highlight common single-character operators * Highlight common single-character operators
*
* @param int $i
* @param Syntax $opts
* @return bool
*/ */
protected function highlightCommonOperators(int &$i, Syntax $opts): bool protected function highlightCommonOperators(int &$i, Syntax $opts): bool
{ {
@ -396,24 +448,31 @@ class Row {
return $this->highlightChar( return $this->highlightChar(
$i, $i,
['+', '-', '*', '/', '<', '^', '>', '%', '=', ':', ',', ';', '&', '~', '!', '|', '.'], ['+', '-', '*', '/', '<', '^', '>', '%', '=', ':', ',', ';', '&', '~', '!', '|', '.'],
Highlight::Operator Highlight::OPERATOR
); );
} }
/** /**
* Highlight brackets and braces * Highlight brackets and braces
*
* @param int $i
* @return bool
*/ */
protected function highlightCommonDelimiters(int &$i): bool protected function highlightCommonDelimeters(int &$i): bool
{ {
return $this->highlightChar( return $this->highlightChar(
$i, $i,
['{', '}', '[', ']', '(', ')'], ['{', '}', '[', ']', '(', ')'],
Highlight::Delimiter Highlight::DELIMITER
); );
} }
/** /**
* Highlight character literals * Highlight character literals
*
* @param int $i
* @param Syntax $opts
* @return bool
*/ */
protected function highlightCharacter(int &$i, Syntax $opts): bool protected function highlightCharacter(int &$i, Syntax $opts): bool
{ {
@ -437,7 +496,7 @@ class Row {
$closingChar = $this->render[$closingIndex]; $closingChar = $this->render[$closingIndex];
if ($closingChar === "'") if ($closingChar === "'")
{ {
array_replace_range($this->hl, $i, $closingIndex - $i + 1, Highlight::Character); array_replace_range($this->hl, $i, $closingIndex - $i + 1, Highlight::CHARACTER);
$i = $closingIndex + 1; $i = $closingIndex + 1;
return true; return true;
@ -449,6 +508,10 @@ class Row {
/** /**
* Highlight single-line comments * Highlight single-line comments
*
* @param int $i
* @param Syntax $opts
* @return bool
*/ */
protected function highlightComment(int &$i, Syntax $opts): bool protected function highlightComment(int &$i, Syntax $opts): bool
{ {
@ -462,7 +525,7 @@ class Row {
if ($scsLen > 0 && substr($this->render, $i, $scsLen) === $scs) if ($scsLen > 0 && substr($this->render, $i, $scsLen) === $scs)
{ {
array_replace_range($this->hl, $i, $this->rsize - $i, Highlight::Comment); array_replace_range($this->hl, $i, $this->rsize - $i, Highlight::COMMENT);
$i = $this->rsize; $i = $this->rsize;
return true; return true;
@ -473,6 +536,10 @@ class Row {
/** /**
* Highlight quote-delimited string literals * Highlight quote-delimited string literals
*
* @param int $i
* @param Syntax $opts
* @return bool
*/ */
protected function highlightString(int &$i, Syntax $opts): bool protected function highlightString(int &$i, Syntax $opts): bool
{ {
@ -487,18 +554,18 @@ class Row {
if ($opts->strings() && ($char === '"' || $char === '\'')) if ($opts->strings() && ($char === '"' || $char === '\''))
{ {
$quote = $char; $quote = $char;
$this->hl[$i] = Highlight::String; $this->hl[$i] = Highlight::STRING;
$i++; $i++;
while ($i < $this->rsize) while ($i < $this->rsize)
{ {
$char = $this->render[$i]; $char = $this->render[$i];
$this->hl[$i] = Highlight::String; $this->hl[$i] = Highlight::STRING;
// Check for escaped character // Check for escaped character
if ($char === '\\' && $i+1 < $this->rsize) if ($char === '\\' && $i+1 < $this->rsize)
{ {
$this->hl[$i + 1] = Highlight::String; $this->hl[$i + 1] = Highlight::STRING;
$i += 2; $i += 2;
continue; continue;
} }
@ -557,20 +624,20 @@ class Row {
if ($commentEnd !== FALSE) if ($commentEnd !== FALSE)
{ {
$inComment = FALSE; $inComment = FALSE;
array_replace_range($this->hl, 0, $commentEnd + 2, Highlight::MultiLineComment); array_replace_range($this->hl, 0, $commentEnd + 2, Highlight::ML_COMMENT);
$offset = $commentEnd; $offset = $commentEnd;
continue; continue;
} }
// Otherwise, just set the whole row // Otherwise, just set the whole row
$this->hl = array_fill(0, $this->rsize, Highlight::MultiLineComment); $this->hl = array_fill(0, $this->rsize, Highlight::ML_COMMENT);
$this->hl[$offset] = Highlight::MultiLineComment; $this->hl[$offset] = Highlight::ML_COMMENT;
break; break;
} }
$char = $token['char']; $char = $token['char'];
$charLen = strlen($char); $charLen = strlen($char);
if ($charLen === 0) if ($charLen === 0 || $offset >= $this->rsize)
{ {
continue; continue;
} }
@ -587,7 +654,7 @@ class Row {
// Single line comments // Single line comments
if (str_contains($char, '//') || str_contains($char, '#')) if (str_contains($char, '//') || str_contains($char, '#'))
{ {
array_replace_range($this->hl, $charStart, $charLen, Highlight::Comment); array_replace_range($this->hl, $charStart, $charLen, Highlight::COMMENT);
break; break;
} }
@ -602,13 +669,13 @@ class Row {
if ($hasEnd) if ($hasEnd)
{ {
$len = $end - $start + 2; $len = $end - $start + 2;
array_replace_range($this->hl, $start, $len, Highlight::MultiLineComment); array_replace_range($this->hl, $start, $len, Highlight::ML_COMMENT);
$inComment = FALSE; $inComment = FALSE;
} }
else else
{ {
$inComment = TRUE; $inComment = TRUE;
array_replace_range($this->hl, $start, $charLen - $offset, Highlight::MultiLineComment); array_replace_range($this->hl, $start, $charLen - $offset, Highlight::ML_COMMENT);
$offset = $start + $charLen - $offset; $offset = $start + $charLen - $offset;
} }
} }
@ -624,16 +691,16 @@ class Row {
$highlight = match(true) { $highlight = match(true) {
// Matches a predefined PHP token // Matches a predefined PHP token
$token['type'] !== T_RAW && $tokenHighlight !== Highlight::Normal $token['type'] !== 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,
default => Highlight::Normal, default => Highlight::NORMAL,
}; };
if ($highlight !== Highlight::Normal) if ($highlight !== Highlight::NORMAL)
{ {
array_replace_range($this->hl, $charStart, $charLen, $highlight); array_replace_range($this->hl, $charStart, $charLen, $highlight);
$offset = $charEnd; $offset = $charEnd;

View File

@ -2,11 +2,6 @@
namespace Aviat\Kilo; namespace Aviat\Kilo;
use Aviat\Kilo\Enum\SyntaxFamily;
/**
* A representation of the syntax of a programming language
*/
class Syntax { class Syntax {
// Tokens for PHP files // Tokens for PHP files
public array $tokens = []; public array $tokens = [];
@ -25,7 +20,6 @@ class Syntax {
bool $hasCharType = false, bool $hasCharType = false,
bool $highlightCharacters = false, bool $highlightCharacters = false,
bool $hasCommonOperators = true, bool $hasCommonOperators = true,
SyntaxFamily $syntaxFamily = SyntaxFamily::C,
): self ): self
{ {
return new self( return new self(
@ -42,17 +36,16 @@ class Syntax {
$highlightStrings, $highlightStrings,
$highlightComments, $highlightComments,
$hasCommonOperators, $hasCommonOperators,
$syntaxFamily,
); );
} }
public static function default(): self public static function default(): self
{ {
return self::new( return self::new(
'No filetype', 'No filetype', slcs:
slcs: '', '', mcs:
mcs: '', '', mce:
mce: '', '',
highlightNumbers: false, highlightNumbers: false,
highlightStrings: false, highlightStrings: false,
hasCommonOperators: false, hasCommonOperators: false,
@ -86,8 +79,6 @@ class Syntax {
private bool $highlightComments, private bool $highlightComments,
/** should we highlight common operators? */ /** should we highlight common operators? */
private bool $hasCommonOperators, private bool $hasCommonOperators,
/** What kind of general syntax family does this file belong to? */
private SyntaxFamily $syntaxFamily,
) {} ) {}
public function numbers(): bool public function numbers(): bool
@ -114,7 +105,7 @@ class Syntax {
{ {
return $this->highlightComments return $this->highlightComments
&& strlen($this->multiLineCommentStart) !== 0 && strlen($this->multiLineCommentStart) !== 0
&& strlen($this->multiLineCommentEnd) !== 0; && strlen($this->multiLineCommentStart) !== 0;
} }
public function comments(): bool public function comments(): bool
@ -126,9 +117,4 @@ class Syntax {
{ {
return $this->hasCommonOperators; return $this->hasCommonOperators;
} }
public function syntaxFamily(): SyntaxFamily
{
return $this->syntaxFamily;
}
} }

View File

@ -1,9 +1,9 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace Aviat\Kilo\Terminal; namespace Aviat\Kilo;
use Aviat\Kilo\Enum\KeyType;
use Aviat\Kilo\Enum\RawKeyCode; use Aviat\Kilo\Enum\RawKeyCode;
use Aviat\Kilo\Enum\KeyType;
use Aviat\Kilo\Type\TerminalSize; use Aviat\Kilo\Type\TerminalSize;
class Terminal { class Terminal {
@ -81,35 +81,35 @@ class Terminal {
* Get the last key input from the terminal and convert to a * Get the last key input from the terminal and convert to a
* more useful format * more useful format
* *
* @return string|KeyType * @return string
*/ */
public static function readKey(): string|KeyType public static function readKey(): string
{ {
$c = Terminal::read(); $c = Terminal::read();
return match($c) return match($c)
{ {
// Unambiguous mappings // Unambiguous mappings
RawKeyCode::ARROW_DOWN => KeyType::ArrowDown, RawKeyCode::ARROW_DOWN => KeyType::ARROW_DOWN,
RawKeyCode::ARROW_LEFT => KeyType::ArrowLeft, RawKeyCode::ARROW_LEFT => KeyType::ARROW_LEFT,
RawKeyCode::ARROW_RIGHT => KeyType::ArrowRight, RawKeyCode::ARROW_RIGHT => KeyType::ARROW_RIGHT,
RawKeyCode::ARROW_UP => KeyType::ArrowUp, RawKeyCode::ARROW_UP => KeyType::ARROW_UP,
RawKeyCode::DELETE => KeyType::Delete, RawKeyCode::DELETE => KeyType::DELETE,
RawKeyCode::ENTER => KeyType::Enter, RawKeyCode::ENTER => KeyType::ENTER,
RawKeyCode::PAGE_DOWN => KeyType::PageDown, RawKeyCode::PAGE_DOWN => KeyType::PAGE_DOWN,
RawKeyCode::PAGE_UP => KeyType::PageUp, RawKeyCode::PAGE_UP => KeyType::PAGE_UP,
// Backspace // Backspace
RawKeyCode::CTRL('h'), RawKeyCode::BACKSPACE => KeyType::Backspace, RawKeyCode::CTRL('h'), RawKeyCode::BACKSPACE => KeyType::BACKSPACE,
// Escape // Escape
RawKeyCode::CTRL('l'), RawKeyCode::ESCAPE => KeyType::Escape, RawKeyCode::CTRL('l'), RawKeyCode::ESCAPE => KeyType::ESCAPE,
// Home Key // Home Key
"\eOH", "\e[7~", "\e[1~", ANSI::RESET_CURSOR => KeyType::Home, "\eOH", "\e[7~", "\e[1~", ANSI::RESET_CURSOR => KeyType::HOME,
// End Key // End Key
"\eOF", "\e[4~", "\e[8~", "\e[F" => KeyType::End, "\eOF", "\e[4~", "\e[8~", "\e[F" => KeyType::END,
default => $c, default => $c,
}; };
@ -126,6 +126,7 @@ class Terminal {
/** /**
* Write to the stdout stream * Write to the stdout stream
* *
* @codeCoverageIgnore
* @param string $str * @param string $str
* @param int|NULL $len * @param int|NULL $len
* @return int|false * @return int|false
@ -153,6 +154,7 @@ class Terminal {
* See if tput exists for fallback terminal size detection * See if tput exists for fallback terminal size detection
* *
* @return bool * @return bool
* @codeCoverageIgnore
*/ */
private static function has_tput(): bool private static function has_tput(): bool
{ {

View File

@ -1,27 +0,0 @@
<?php declare(strict_types=1);
namespace Aviat\Kilo\Terminal;
use Aviat\Kilo\Traits;
/**
* Just a namespace for C language constants
*/
class C {
use Traits\ConstList;
// ------------------------------------------------------------------------
// ! Misc I/O constants
// ------------------------------------------------------------------------
public const STDIN_FILENO = 0;
public const STDOUT_FILENO = 1;
public const STDERR_FILENO = 2;
public const TCSANOW = 0;
public const TCSAFLUSH = 2;
// ------------------------------------------------------------------------
// ! IOCTL constants
// ------------------------------------------------------------------------
public const TIOCGWINSZ = 0x5413;
}

View File

@ -1,55 +0,0 @@
<?php declare(strict_types=1);
namespace Aviat\Kilo\Terminal\Enum;
use Aviat\Kilo\Traits;
/**
* ANSI Color escape sequences
* @enum
*/
enum Color: int {
use Traits\ConstList;
// Foreground/Background
case Fg = 38;
case Bg = 48;
// Foreground colors
case FG_BLACK = 30;
case FG_RED = 31;
case FG_GREEN = 32;
case FG_YELLOW = 33;
case FG_BLUE = 34;
case FG_MAGENTA = 35;
case FG_CYAN = 36;
case FG_WHITE = 37;
case FG_BRIGHT_BLACK = 90;
case FG_BRIGHT_RED = 91;
case FG_BRIGHT_GREEN = 92;
case FG_BRIGHT_YELLOW = 93;
case FG_BRIGHT_BLUE = 94;
case FG_BRIGHT_MAGENTA = 95;
case FG_BRIGHT_CYAN = 96;
case FG_BRIGHT_WHITE = 97;
// Background colors
case BG_BLACK = 40;
case BG_RED = 41;
case BG_GREEN = 42;
case BG_YELLOW = 43;
case BG_BLUE = 44;
case BG_MAGENTA = 45;
case BG_CYAN = 46;
case BG_WHITE = 47;
case BG_BRIGHT_BLACK = 100;
case BG_BRIGHT_RED = 101;
case BG_BRIGHT_GREEN = 102;
case BG_BRIGHT_YELLOW = 103;
case BG_BRIGHT_BLUE = 104;
case BG_BRIGHT_MAGENTA = 105;
case BG_BRIGHT_CYAN = 106;
case BG_BRIGHT_WHITE = 107;
case INVERT = 7;
}

View File

@ -1,31 +0,0 @@
<?php declare(strict_types=1);
namespace Aviat\Kilo\Terminal\Enum;
use Aviat\Kilo\Traits;
/**
* ANSI 256 Color escape sequences
* @enum
*/
enum Color256: int {
use Traits\ConstList;
// Base colors for 256-color setup
case Black = 0;
case Red = 1;
case Green = 2;
case Yellow = 3;
case Blue = 4;
case Magenta = 5;
case Cyan = 6;
case White = 7;
case BrightBlack = 8;
case BrightRed = 9;
case BrightGreen = 10;
case BrightYellow = 11;
case BrightBlue = 12;
case BrightMagenta = 13;
case BrightCyan = 14;
case BrightWhite = 15;
}

View File

@ -1,11 +0,0 @@
<?php declare(strict_types=1);
namespace Aviat\Kilo\Terminal\Enum;
use Aviat\Kilo\Traits;
enum LibType: string {
use Traits\ConstList;
case GLIBC = "glibc";
case MUSL = "musl";
}

View File

@ -1,70 +0,0 @@
/**
* Interfaces for PHP FFI
*
* Most of the structure code is cribbed from GLib
*
* Defines are not (generally) recognized by the FFI integration
*/
// PHP 'constants' for FFI integration
// These seem to be the only define statements supported by the FFI integration
#define FFI_SCOPE "terminal"
#define FFI_LIB "libc.so"
// Nonsense for a test with a single quote
// Ignored by PHP due to the octothorpe (#)
#if 0
# char* x = "String with \" escape char";
# char y = 'q';
#endif
// -----------------------------------------------------------------------------
//! <termios.h>
// -----------------------------------------------------------------------------
/* Type of terminal control flag masks. */
typedef unsigned long int tcflag_t;
/* Type of control characters. */
typedef unsigned char cc_t;
/* Type of baud rate specifiers. */
typedef long int speed_t;
/* Terminal control structure. */
struct termios
{
/* Input modes. */
tcflag_t c_iflag;
/* Output modes. */
tcflag_t c_oflag;
/* Control modes. */
tcflag_t c_cflag;
/* Local modes. */
tcflag_t c_lflag;
/* Control characters. */
cc_t c_cc[20];
/* Input and output baud rates. */
speed_t __ispeed, __ospeed;
};
int tcgetattr (int fd, struct termios *termios_p);
int tcsetattr (int fd, int optional_actions, const struct termios *termios_p);
void cfmakeraw(struct termios *);
int tcgetwinsize(int fd, struct winsize *);
// -----------------------------------------------------------------------------
//! <sys/ioctl.h>
// -----------------------------------------------------------------------------
struct winsize {
unsigned short ws_row;
unsigned short ws_col;
unsigned short ws_xpixel;
unsigned short ws_ypixel;
};
int ioctl (int, int, ...);

View File

@ -1,11 +1,12 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace Aviat\Kilo\Terminal; namespace Aviat\Kilo;
use Aviat\Kilo\Terminal\Enum\LibType;
use FFI; use FFI;
use FFI\CData; use FFI\CData;
use Aviat\Kilo\Enum\C;
/** /**
* An implicit singleton wrapper around terminal settings to simplify enabling/disabling raw mode * An implicit singleton wrapper around terminal settings to simplify enabling/disabling raw mode
*/ */
@ -58,13 +59,16 @@ class Termios {
register_shutdown_function([static::class, 'disableRawMode']); register_shutdown_function([static::class, 'disableRawMode']);
$termios = clone $instance->originalTermios; $termios = clone $instance->originalTermios;
$termios->c_iflag &= ~(C::BRKINT | C::ICRNL | C::INPCK | C::ISTRIP | C::IXON);
$termios->c_oflag = 0; // &= ~(C::OPOST);
$termios->c_cflag |= (C::CS8);
$termios->c_lflag &= ~( C::ECHO | C::ICANON | C::IEXTEN | C::ISIG );
$termios->c_cc[C::VMIN] = 0;
$termios->c_cc[C::VTIME] = 1;
// Use the stdlib func to set raw mode parameters // Turn on raw mode
self::ffi()->cfmakeraw(FFI::addr($termios));
// Apply raw mode to the terminal
$res = self::ffi() $res = self::ffi()
->tcsetattr(C::STDIN_FILENO, C::TCSANOW, FFI::addr($termios)); ->tcsetattr(C::STDIN_FILENO, C::TCSAFLUSH, FFI::addr($termios));
return $res !== -1; return $res !== -1;
} }
@ -93,21 +97,17 @@ class Termios {
/** /**
* Get the size of the current terminal window * Get the size of the current terminal window
* *
* @codeCoverageIgnore
* @return array|null * @return array|null
*/ */
public static function getWindowSize(): ?array public static function getWindowSize(): ?array
{ {
$res = NULL;
// First, try to get the answer from ioctl // First, try to get the answer from ioctl
$ffi = self::ffi(); $ffi = self::ffi();
$ws = $ffi->new('struct winsize'); $ws = $ffi->new('struct winsize');
if ($ws !== NULL) if ($ws !== NULL)
{ {
$res = (self::getLibType() === LibType::MUSL) $res = $ffi->ioctl(C::STDOUT_FILENO, C::TIOCGWINSZ, FFI::addr($ws));
? $ffi->tcgetwinsize(C::STDOUT_FILENO, FFI::addr($ws))
: $ffi->ioctl(C::STDOUT_FILENO, C::TIOCGWINSZ, FFI::addr($ws));
if ($res === 0 && $ws->ws_col !== 0 && $ws->ws_row !== 0) if ($res === 0 && $ws->ws_col !== 0 && $ws->ws_row !== 0)
{ {
return [$ws->ws_row, $ws->ws_col]; return [$ws->ws_row, $ws->ws_col];
@ -117,33 +117,14 @@ class Termios {
return null; return null;
} }
private static function getLibType(): LibType
{
static $type;
if ($type === NULL)
{
$maybeLibc = "/usr/lib/libc.so";
if (file_exists($maybeLibc))
{
$rawLibInfo = (string)shell_exec($maybeLibc);
if (str_contains(strtolower($rawLibInfo), "musl"))
{
$type = LibType::MUSL;
}
}
$type = LibType::GLIBC;
}
return $type;
}
private static function getInstance(): self private static function getInstance(): self
{ {
static $instance; static $instance;
$instance ??= new self(); if ($instance === NULL)
{
$instance = new self();
}
return $instance; return $instance;
} }
@ -159,12 +140,6 @@ class Termios {
if ($ffi === NULL) if ($ffi === NULL)
{ {
if (self::getLibType() === LibType::MUSL)
{
$ffi = FFI::load(__DIR__ . '/ffi_musl.h');
return $ffi;
}
$ffi = FFI::load(__DIR__ . '/ffi.h'); $ffi = FFI::load(__DIR__ . '/ffi.h');
} }

View File

@ -1,14 +1,11 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace Aviat\Kilo\Terminal; namespace Aviat\Kilo;
use Throwable; use Throwable;
class TermiosException extends \UnexpectedValueException { class TermiosException extends \UnexpectedValueException {
public function __construct( public function __construct($message = 'Failed to apply terminal settings', $code = 0, Throwable $previous = NULL)
string $message = 'Failed to apply terminal settings',
int $code = 0,
Throwable $previous = NULL)
{ {
parent::__construct($message, $code, $previous); parent::__construct($message, $code, $previous);
} }

View File

@ -13,7 +13,7 @@ class PHP8 extends PhpToken {
private array $tokens = []; private array $tokens = [];
/** /**
* Use 'PhpToken' to get the tokens for a file, * Use 'token_get_all' to get the tokens for a file,
* organized by row number * organized by row number
* *
* @param string $code * @param string $code
@ -66,7 +66,7 @@ class PHP8 extends PhpToken {
return $this->tokens; return $this->tokens;
} }
protected function processObjectToken(PhpToken $token): void protected function processObjectToken(\PhpToken $token): void
{ {
$currentLine = $token->line; $currentLine = $token->line;
$char = tabs_to_spaces($token->text); $char = tabs_to_spaces($token->text);
@ -144,4 +144,4 @@ class PHP8 extends PhpToken {
return NULL; return NULL;
} }
} }

View File

@ -1,20 +0,0 @@
<?php declare(strict_types=1);
namespace Aviat\Kilo\Traits;
trait EnumTrait {
public function jsonSerialize(): mixed
{
if (property_exists($this, 'value'))
{
return $this->value;
}
if (property_exists($this, 'name'))
{
return $this->name;
}
return print_r($this, true);
}
}

13
src/constants.php Normal file
View File

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace Aviat\Kilo;
// -----------------------------------------------------------------------------
// ! App Constants
// -----------------------------------------------------------------------------
const KILO_VERSION = '0.3.0';
const KILO_TAB_STOP = 4;
const KILO_QUIT_TIMES = 3;
const NO_MATCH = -1;
const T_RAW = -1;

View File

@ -9,7 +9,7 @@
// PHP 'constants' for FFI integration // PHP 'constants' for FFI integration
// These seem to be the only define statements supported by the FFI integration // These seem to be the only define statements supported by the FFI integration
#define FFI_SCOPE "terminal" #define FFI_SCOPE "terminal"
#define FFI_LIB "libc.so" #define FFI_LIB "libc.so.6"
// Nonsense for a test with a single quote // Nonsense for a test with a single quote
// Ignored by PHP due to the octothorpe (#) // Ignored by PHP due to the octothorpe (#)
@ -55,7 +55,6 @@ struct termios
int tcgetattr (int fd, struct termios *termios_p); int tcgetattr (int fd, struct termios *termios_p);
int tcsetattr (int fd, int optional_actions, const struct termios *termios_p); int tcsetattr (int fd, int optional_actions, const struct termios *termios_p);
void cfmakeraw(struct termios *);
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
//! <sys/ioctl.h> //! <sys/ioctl.h>

View File

@ -2,48 +2,7 @@
namespace Aviat\Kilo; namespace Aviat\Kilo;
use Aviat\Kilo\Enum\Highlight; use Aviat\Kilo\Enum\{Color, Highlight, RawKeyCode};
use Aviat\Kilo\Enum\RawKeyCode;
use Aviat\Kilo\Terminal\Enum\Color;
use Aviat\Kilo\Terminal\Enum\Color256;
// -----------------------------------------------------------------------------
// ! App Constants
// -----------------------------------------------------------------------------
const KILO_VERSION = '0.3.0';
const KILO_TAB_STOP = 4;
const KILO_QUIT_TIMES = 3;
const NO_MATCH = -1;
const T_RAW = -1;
// -----------------------------------------------------------------------------
// ! App Config
// -----------------------------------------------------------------------------
/**
* Configure syntax highlighting colors
*/
function get_syntax_color(Highlight $hl): Color | Color256 | int {
return match ($hl)
{
Highlight::Comment => Color::FG_CYAN,
Highlight::MultiLineComment => Color::FG_BRIGHT_BLACK,
Highlight::Keyword1 => Color::FG_YELLOW,
Highlight::Keyword2 => Color::FG_GREEN,
Highlight::String => Color::FG_MAGENTA,
Highlight::Character => Color::FG_BRIGHT_MAGENTA,
Highlight::Number => Color::FG_BRIGHT_RED,
Highlight::Operator => Color::FG_BRIGHT_GREEN,
Highlight::Variable => Color::FG_BRIGHT_CYAN,
Highlight::Delimiter => Color::FG_BLUE,
Highlight::Invalid => Color::BG_BRIGHT_RED,
Highlight::SearchMatch => Color::INVERT,
Highlight::Identifier => Color::FG_BRIGHT_WHITE,
default => Color::FG_WHITE,
};
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// ! C function/macro equivalents // ! C function/macro equivalents
@ -52,6 +11,9 @@ function get_syntax_color(Highlight $hl): Color | Color256 | int {
/** /**
* Do bit twiddling to convert a letter into * Do bit twiddling to convert a letter into
* its Ctrl-letter equivalent ordinal ascii value * its Ctrl-letter equivalent ordinal ascii value
*
* @param string $char
* @return int
*/ */
function ctrl_key(string $char): int function ctrl_key(string $char): int
{ {
@ -68,6 +30,9 @@ function ctrl_key(string $char): int
/** /**
* Does the one-character string contain an ascii ordinal value? * Does the one-character string contain an ascii ordinal value?
*
* @param string $single_char
* @return bool
*/ */
function is_ascii(string $single_char): bool function is_ascii(string $single_char): bool
{ {
@ -81,6 +46,9 @@ function is_ascii(string $single_char): bool
/** /**
* Does the one-character string contain an ascii control character? * Does the one-character string contain an ascii control character?
*
* @param string $char
* @return bool
*/ */
function is_ctrl(string $char): bool function is_ctrl(string $char): bool
{ {
@ -90,6 +58,9 @@ function is_ctrl(string $char): bool
/** /**
* Does the one-character string contain an ascii number? * Does the one-character string contain an ascii number?
*
* @param string $char
* @return bool
*/ */
function is_digit(string $char): bool function is_digit(string $char): bool
{ {
@ -99,6 +70,9 @@ function is_digit(string $char): bool
/** /**
* Does the one-character string contain ascii whitespace? * Does the one-character string contain ascii whitespace?
*
* @param string $char
* @return bool
*/ */
function is_space(string $char): bool function is_space(string $char): bool
{ {
@ -120,6 +94,9 @@ function is_space(string $char): bool
/** /**
* Does the one-character string contain a character that separates tokens? * Does the one-character string contain a character that separates tokens?
*
* @param string $char
* @return bool
*/ */
function is_separator(string $char): bool function is_separator(string $char): bool
{ {
@ -154,7 +131,12 @@ function array_replace_range(array &$array, int $offset, int $length, mixed $val
} }
/** /**
* Does the string $haystack contain $str, optionally searching from $offset * Does the string $haystack contain $str, optionally searching from $offset?
*
* @param string $haystack
* @param string $str
* @param int|null $offset
* @return bool
*/ */
function str_has(string $haystack, string $str, ?int $offset = NULL): bool function str_has(string $haystack, string $str, ?int $offset = NULL): bool
{ {
@ -168,14 +150,51 @@ function str_has(string $haystack, string $str, ?int $offset = NULL): bool
: \str_contains($haystack, $str); : \str_contains($haystack, $str);
} }
/**
* Get the ASCII color escape number for the specified syntax type
*
* @param int $hl
* @return int
*/
function syntax_to_color(int $hl): int
{
return match ($hl)
{
Highlight::COMMENT => Color::FG_CYAN,
Highlight::ML_COMMENT => Color::FG_BRIGHT_BLACK,
Highlight::KEYWORD1 => Color::FG_YELLOW,
Highlight::KEYWORD2 => Color::FG_GREEN,
Highlight::STRING => Color::FG_MAGENTA,
Highlight::CHARACTER => Color::FG_BRIGHT_MAGENTA,
Highlight::NUMBER => Color::FG_BRIGHT_RED,
Highlight::OPERATOR => Color::FG_BRIGHT_GREEN,
Highlight::VARIABLE => Color::FG_BRIGHT_CYAN,
Highlight::DELIMITER => Color::FG_BLUE,
Highlight::INVALID => Color::BG_BRIGHT_RED,
Highlight::MATCH => Color::INVERT,
Highlight::IDENTIFIER => Color::FG_BRIGHT_WHITE,
default => Color::FG_WHITE,
};
}
/** /**
* Replace tabs with the specified number of spaces. * Replace tabs with the specified number of spaces.
*
* @param string $str
* @param int $number
* @return string
*/ */
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(RawKeyCode::TAB, str_repeat(RawKeyCode::SPACE, $number), $str);
} }
/**
* Get an understandable name for the type of error code
*
* @param int $code
* @return string
*/
function error_code_name(int $code): string function error_code_name(int $code): string
{ {
return match ($code) { return match ($code) {
@ -198,7 +217,13 @@ function error_code_name(int $code): string
} }
/** /**
* Adds two numbers to at most the $max value * Add two integers, returning the sum, or if the
* sum is greater than $max, returning $max
*
* @param int $a
* @param int $b
* @param int $max
* @return int
*/ */
function saturating_add(int $a, int $b, int $max): int function saturating_add(int $a, int $b, int $max): int
{ {
@ -206,7 +231,12 @@ function saturating_add(int $a, int $b, int $max): int
} }
/** /**
* Delete one number from another, down to zero at the least * Subtract $b from $a, returning the difference
* or 0, whichever is greater
*
* @param int $a
* @param int $b
* @return int
*/ */
function saturating_sub(int $a, int $b): int function saturating_sub(int $a, int $b): int
{ {
@ -216,4 +246,4 @@ function saturating_sub(int $a, int $b): int
} }
return $a - $b; return $a - $b;
} }

View File

@ -2,7 +2,6 @@
interface Ifoo {} interface Ifoo {}
// Let's see emoji! 🕯️😸⛩⛪
abstract class Foo implements Ifoo { abstract class Foo implements Ifoo {
/** /**
* @param int $a * @param int $a
@ -14,12 +13,9 @@ abstract class Foo implements Ifoo {
*/ */
abstract public function bar(int $a, float $b, array $c, callable $d, string $e): string; abstract public function bar(int $a, float $b, array $c, callable $d, string $e): string;
#[ReturnTypeWillChange]
protected function doNothing(): void {} protected function doNothing(): void {}
} }
#[Attribute]
#[AllowDynamicProperties]
class Test { class Test {
public function __construct(public string $foo, public string $bar) {} public function __construct(public string $foo, public string $bar) {}
} }
@ -28,17 +24,14 @@ class Test {
* Docblock comment * Docblock comment
*/ */
class FooBar extends Foo implements Ifoo { class FooBar extends Foo implements Ifoo {
public function bar(int $a, float $b, array $c, callable $d, #[SensitiveParameter] string $e = 'default'): string public function bar(int $a, float $b, array $c, callable $d, string $e = 'default'): string
{ {
$cstr = print_r($c, TRUE); $cstr = print_r($c, TRUE);
$d(); $d();
$r = $this->operations($a, (int)$b); return "{$a}, ${b}, " . $cstr;
return "{$a}, ${b}, " . $cstr . " = {$r}";
} }
#[Test('a', 'b')]
private function operations(int $a, int $b): int private function operations(int $a, int $b): int
{ {
$this?->x?->bar(); $this?->x?->bar();
@ -71,10 +64,6 @@ trait Baz {
} }
} }
class BazFoo {
use Baz;
}
$square = fn (int $x) => $x ** 2; $square = fn (int $x) => $x ** 2;
foreach ([-1, 0, 1, 2] as $x) foreach ([-1, 0, 1, 2] as $x)
@ -111,7 +100,7 @@ $q = ($x !== 2)
/* /*
Heredoc Heredoc
*/$z = $x + $y[0]; */$z = $x + $y;
$sql = <<<SQL $sql = <<<SQL
SELECT * FROM "foo" WHERE "bar"='baz' AND id={$x}; SELECT * FROM "foo" WHERE "bar"='baz' AND id={$x};
SQL; SQL;
@ -128,8 +117,7 @@ TEMPLATE;
<title>HTML</title> <title>HTML</title>
</head> </head>
<body> <body>
<h1>Test</h1> <h1><?= $_SERVER['HTTP_HOST'] ?></h1>
<div><?php $bf = new BazFoo(); print_r($bf->about()) ?></div>
</body> </body>
</html> </html>
<?php exit(); ?> <?php exit(); ?>

View File

@ -2,8 +2,8 @@
namespace Aviat\Kilo\Tests; namespace Aviat\Kilo\Tests;
use Aviat\Kilo\Terminal\ANSI; use Aviat\Kilo\ANSI;
use Aviat\Kilo\Terminal\Enum\Color; use Aviat\Kilo\Enum\Color;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
class ANSITest extends TestCase { class ANSITest extends TestCase {

View File

@ -47,7 +47,7 @@ class EditorTest extends TestCase {
public function testOpen(): void public function testOpen(): void
{ {
$editor = MockEditor::mock('src/Terminal/ffi.h'); $editor = MockEditor::mock('src/ffi.h');
$state = json_encode($editor->__debugInfo(), JSON_THROW_ON_ERROR); $state = json_encode($editor->__debugInfo(), JSON_THROW_ON_ERROR);
$this->assertMatchesJsonSnapshot($state); $this->assertMatchesJsonSnapshot($state);

View File

@ -2,6 +2,8 @@
namespace Aviat\Kilo\Tests; namespace Aviat\Kilo\Tests;
use Aviat\Kilo\Enum\Color;
use Aviat\Kilo\Enum\Highlight;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use function Aviat\Kilo\array_replace_range; use function Aviat\Kilo\array_replace_range;
@ -12,6 +14,7 @@ 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_has;
use function Aviat\Kilo\syntax_to_color;
use function Aviat\Kilo\tabs_to_spaces; use function Aviat\Kilo\tabs_to_spaces;
class FunctionTest extends TestCase { class FunctionTest extends TestCase {
@ -85,6 +88,14 @@ class FunctionTest extends TestCase {
$this->assertTrue(is_separator("\0")); $this->assertTrue(is_separator("\0"));
} }
public function test_syntax_to_color(): void
{
// Nonsense input returns FG::White
$this->assertEquals(syntax_to_color(999), Color::FG_WHITE);
$this->assertNotEquals(syntax_to_color(Highlight::OPERATOR), Color::FG_WHITE);
}
public function test_str_contains(): void public function test_str_contains(): void
{ {
// Search from string offset // Search from string offset

View File

@ -2,9 +2,10 @@
namespace Aviat\Kilo\Tests; namespace Aviat\Kilo\Tests;
use Aviat\Kilo\Terminal\Terminal;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Aviat\Kilo\Terminal;
class TerminalTest extends TestCase { class TerminalTest extends TestCase {
public function test_getWindowSize(): void public function test_getWindowSize(): void

View File

@ -2,7 +2,7 @@
namespace Aviat\Kilo\Tests; namespace Aviat\Kilo\Tests;
use Aviat\Kilo\{Terminal\Termios, Terminal\TermiosException}; use Aviat\Kilo\{Termios, TermiosException};
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
class TermiosTest extends TestCase { class TermiosTest extends TestCase {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff