Start splitting render methods
All checks were successful
timw4mail/php-kilo/pipeline/head This commit looks good
All checks were successful
timw4mail/php-kilo/pipeline/head This commit looks good
This commit is contained in:
parent
d92ca27880
commit
6c568dfaac
@ -40,12 +40,17 @@ class Document {
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function row(int $index): ?Row
|
||||
public function row(int $index): Row
|
||||
{
|
||||
return (array_key_exists($index, $this->rows)) ? $this->rows[$index] : null;
|
||||
return (array_key_exists($index, $this->rows))
|
||||
? $this->rows[$index]
|
||||
: Row::default();
|
||||
}
|
||||
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->rows);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// ! File I/O
|
||||
|
241
src/Editor.php
241
src/Editor.php
@ -3,7 +3,13 @@
|
||||
namespace Aviat\Kilo;
|
||||
|
||||
use Aviat\Kilo\Type\TerminalSize;
|
||||
use Aviat\Kilo\Enum\{Color, KeyCode, KeyType, Highlight};
|
||||
use Aviat\Kilo\Enum\{
|
||||
Color,
|
||||
KeyCode,
|
||||
KeyType,
|
||||
Highlight,
|
||||
SearchDirection
|
||||
};
|
||||
use Aviat\Kilo\Type\{Point, StatusMessage};
|
||||
|
||||
/**
|
||||
@ -127,6 +133,20 @@ class Editor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a status message to be displayed, using printf formatting
|
||||
* @param string $fmt
|
||||
* @param mixed ...$args
|
||||
*/
|
||||
public function setStatusMessage(string $fmt, mixed ...$args): void
|
||||
{
|
||||
$text = func_num_args() > 1
|
||||
? sprintf($fmt, ...$args)
|
||||
: $fmt;
|
||||
|
||||
$this->statusMessage = StatusMessage::from($text);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// ! Row Operations
|
||||
// ------------------------------------------------------------------------
|
||||
@ -180,17 +200,6 @@ class Editor {
|
||||
return $cx;
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// ! Editor Operations
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
protected function insertChar(string $c): void
|
||||
{
|
||||
$this->document->insert($this->cursor, $c);
|
||||
$this->moveCursor(KeyType::ARROW_RIGHT);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// ! File I/O
|
||||
// ------------------------------------------------------------------------
|
||||
@ -226,8 +235,8 @@ class Editor {
|
||||
|
||||
protected function findCallback(string $query, string $key): void
|
||||
{
|
||||
static $lastMatch = -1;
|
||||
static $direction = 1;
|
||||
static $lastMatch = NO_MATCH;
|
||||
static $direction = SearchDirection::FORWARD;
|
||||
|
||||
static $savedHlLine = 0;
|
||||
static $savedHl = [];
|
||||
@ -236,7 +245,7 @@ class Editor {
|
||||
{
|
||||
$row = $this->document->row($savedHlLine);
|
||||
|
||||
if ($row !== null)
|
||||
if ($row->isValid())
|
||||
{
|
||||
$row->hl = $savedHl;
|
||||
}
|
||||
@ -244,35 +253,31 @@ class Editor {
|
||||
$savedHl = [];
|
||||
}
|
||||
|
||||
switch ($key)
|
||||
$direction = match ($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;
|
||||
$direction = SearchDirection::FORWARD;
|
||||
|
||||
if ($key === KeyCode::ENTER || $key === KeyCode::ESCAPE)
|
||||
{
|
||||
case KeyCode::ENTER:
|
||||
case KeyCode::ESCAPE:
|
||||
$lastMatch = -1;
|
||||
$direction = 1;
|
||||
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 === -1)
|
||||
if ($lastMatch === NO_MATCH)
|
||||
{
|
||||
$direction = 1;
|
||||
$direction = SearchDirection::FORWARD;
|
||||
}
|
||||
|
||||
$current = $lastMatch;
|
||||
$current = (int)$lastMatch;
|
||||
|
||||
if (empty($query))
|
||||
{
|
||||
@ -292,7 +297,7 @@ class Editor {
|
||||
}
|
||||
|
||||
$row = $this->document->row($current);
|
||||
if ($row === null)
|
||||
if ( ! $row->isValid())
|
||||
{
|
||||
break;
|
||||
}
|
||||
@ -342,7 +347,7 @@ class Editor {
|
||||
{
|
||||
$row = $this->document->row($this->cursor->y);
|
||||
|
||||
if ($row !== null)
|
||||
if ($row->isValid())
|
||||
{
|
||||
$this->renderX = $this->rowCxToRx($row, $this->cursor->x);
|
||||
}
|
||||
@ -389,7 +394,7 @@ class Editor {
|
||||
protected function drawRow(int $rowIdx): void
|
||||
{
|
||||
$row = $this->document->row($rowIdx);
|
||||
if ($row === null)
|
||||
if ( ! $row->isValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -556,15 +561,6 @@ class Editor {
|
||||
Terminal::write($this->outputBuffer, strlen($this->outputBuffer));
|
||||
}
|
||||
|
||||
public function setStatusMessage(string $fmt, mixed ...$args): void
|
||||
{
|
||||
$text = func_num_args() > 1
|
||||
? sprintf($fmt, ...$args)
|
||||
: $fmt;
|
||||
|
||||
$this->statusMessage = StatusMessage::from($text);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// ! Input
|
||||
// ------------------------------------------------------------------------
|
||||
@ -607,12 +603,87 @@ class Editor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Input processing
|
||||
*/
|
||||
protected function processKeypress(): void
|
||||
{
|
||||
$c = Terminal::readKey();
|
||||
|
||||
if ($c === KeyCode::NULL || $c === KeyCode::EMPTY)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($c)
|
||||
{
|
||||
case KeyCode::CTRL('q'):
|
||||
$this->quitAttempt();
|
||||
return;
|
||||
|
||||
case KeyCode::CTRL('s'):
|
||||
$this->save();
|
||||
break;
|
||||
|
||||
case KeyCode::CTRL('f'):
|
||||
$this->find();
|
||||
break;
|
||||
|
||||
case KeyType::DEL_KEY:
|
||||
$this->document->delete($this->cursor);
|
||||
break;
|
||||
|
||||
case KeyType::BACKSPACE:
|
||||
if ($this->cursor->x > 0 || $this->cursor->y > 0)
|
||||
{
|
||||
$this->moveCursor(KeyType::ARROW_LEFT);
|
||||
$this->document->delete($this->cursor);
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyType::ARROW_UP:
|
||||
case KeyType::ARROW_DOWN:
|
||||
case KeyType::ARROW_LEFT:
|
||||
case KeyType::ARROW_RIGHT:
|
||||
case KeyType::PAGE_UP:
|
||||
case KeyType::PAGE_DOWN:
|
||||
case KeyType::HOME_KEY:
|
||||
case KeyType::END_KEY:
|
||||
$this->moveCursor($c);
|
||||
break;
|
||||
|
||||
case KeyCode::CTRL('l'):
|
||||
case KeyType::ESCAPE:
|
||||
// Do nothing
|
||||
break;
|
||||
|
||||
case KeyType::ENTER:
|
||||
$this->insertChar("\n");
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->insertChar($c);
|
||||
break;
|
||||
}
|
||||
|
||||
// Reset quit confirmation timer on different keypress
|
||||
if ($this->quitTimes < KILO_QUIT_TIMES)
|
||||
{
|
||||
$this->quitTimes = KILO_QUIT_TIMES;
|
||||
$this->setStatusMessage('');
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// ! Editor operation helpers
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
protected function moveCursor(string $key): void
|
||||
{
|
||||
$x = $this->cursor->x;
|
||||
$y = $this->cursor->y;
|
||||
$row = $this->document->row($y);
|
||||
if ($row === NULL)
|
||||
if ( ! $row->isValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -688,7 +759,7 @@ class Editor {
|
||||
// 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 !== null)
|
||||
if ($row->isValid())
|
||||
{
|
||||
if ($x > $row->size)
|
||||
{
|
||||
@ -700,72 +771,10 @@ class Editor {
|
||||
}
|
||||
}
|
||||
|
||||
protected function processKeypress(): void
|
||||
protected function insertChar(string $c): void
|
||||
{
|
||||
$c = Terminal::readKey();
|
||||
|
||||
if ($c === KeyCode::NULL || $c === KeyCode::EMPTY)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($c)
|
||||
{
|
||||
case KeyCode::CTRL('q'):
|
||||
$this->quitAttempt();
|
||||
return;
|
||||
|
||||
case KeyCode::CTRL('s'):
|
||||
$this->save();
|
||||
break;
|
||||
|
||||
case KeyCode::CTRL('f'):
|
||||
$this->find();
|
||||
break;
|
||||
|
||||
case KeyType::DEL_KEY:
|
||||
$this->document->delete($this->cursor);
|
||||
break;
|
||||
|
||||
case KeyType::BACKSPACE:
|
||||
if ($this->cursor->x > 0 || $this->cursor->y > 0)
|
||||
{
|
||||
$this->moveCursor(KeyType::ARROW_LEFT);
|
||||
$this->document->delete($this->cursor);
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyType::ARROW_UP:
|
||||
case KeyType::ARROW_DOWN:
|
||||
case KeyType::ARROW_LEFT:
|
||||
case KeyType::ARROW_RIGHT:
|
||||
case KeyType::PAGE_UP:
|
||||
case KeyType::PAGE_DOWN:
|
||||
case KeyType::HOME_KEY:
|
||||
case KeyType::END_KEY:
|
||||
$this->moveCursor($c);
|
||||
break;
|
||||
|
||||
case KeyCode::CTRL('l'):
|
||||
case KeyType::ESCAPE:
|
||||
// Do nothing
|
||||
break;
|
||||
|
||||
case KeyType::ENTER:
|
||||
$this->insertChar("\n");
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->insertChar($c);
|
||||
break;
|
||||
}
|
||||
|
||||
// Reset quit confirmation timer on different keypress
|
||||
if ($this->quitTimes < KILO_QUIT_TIMES)
|
||||
{
|
||||
$this->quitTimes = KILO_QUIT_TIMES;
|
||||
$this->setStatusMessage('');
|
||||
}
|
||||
$this->document->insert($this->cursor, $c);
|
||||
$this->moveCursor(KeyType::ARROW_RIGHT);
|
||||
}
|
||||
|
||||
protected function quitAttempt(): void
|
||||
|
15
src/Enum/SearchDirection.php
Normal file
15
src/Enum/SearchDirection.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?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;
|
||||
}
|
146
src/Row.php
146
src/Row.php
@ -8,38 +8,60 @@ use Aviat\Kilo\Enum\KeyCode;
|
||||
/**
|
||||
* @property-read int $size
|
||||
* @property-read int $rsize
|
||||
* @property-read string $chars
|
||||
* @property string $chars
|
||||
*/
|
||||
class Row {
|
||||
use Traits\MagicProperties;
|
||||
|
||||
private string $chars = '';
|
||||
/**
|
||||
* The version of the row to be displayed (where tabs are converted to display spaces)
|
||||
*/
|
||||
public string $render = '';
|
||||
|
||||
/**
|
||||
* The mapping of characters to their highlighting type
|
||||
*/
|
||||
public array $hl = [];
|
||||
|
||||
public int $idx;
|
||||
|
||||
// This feels dirty...
|
||||
private Document $parent;
|
||||
private bool $hlOpenComment = FALSE;
|
||||
|
||||
private const T_RAW = -1;
|
||||
|
||||
public static function new(Document $parent, string $chars, int $idx): self
|
||||
{
|
||||
$self = new self();
|
||||
$self->chars = $chars;
|
||||
$self->parent = $parent;
|
||||
$self->idx = $idx;
|
||||
|
||||
return $self;
|
||||
return new self(
|
||||
$parent,
|
||||
$chars,
|
||||
$idx,
|
||||
);
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
// Private in favor of ::new static function
|
||||
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
|
||||
{
|
||||
return match ($name)
|
||||
@ -77,6 +99,16 @@ class Row {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this row a valid part of a document?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
return ! $this->parent->isEmpty();
|
||||
}
|
||||
|
||||
public function insert(int $at, string $c): void
|
||||
{
|
||||
if ($at < 0 || $at > $this->size)
|
||||
@ -123,6 +155,60 @@ class Row {
|
||||
$this->highlightGeneral();
|
||||
}
|
||||
|
||||
protected function highlightComment(int $i, string $char): bool
|
||||
{
|
||||
$scs = $this->parent->fileType->syntax->singleLineCommentStart;
|
||||
$scsLen = strlen($scs);
|
||||
|
||||
if ($scsLen > 0 && substr($this->render, $i, $scsLen) === $scs)
|
||||
{
|
||||
array_replace_range($this->hl, $i, $this->rsize - $i, Highlight::COMMENT);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function highlightString(int &$i): bool
|
||||
{
|
||||
$char = $this->render[$i];
|
||||
$enabled = $this->parent->fileType->syntax->flags & Syntax::HIGHLIGHT_STRINGS;
|
||||
|
||||
if ($enabled && $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!
|
||||
if ($char === $quote)
|
||||
{
|
||||
$i++;
|
||||
break;
|
||||
}
|
||||
|
||||
$i++;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function highlightGeneral(): void
|
||||
{
|
||||
$this->hl = array_fill(0, $this->rsize, Highlight::NORMAL);
|
||||
@ -192,37 +278,7 @@ class Row {
|
||||
}
|
||||
|
||||
// String/Char literals
|
||||
if ($this->parent->fileType->syntax->flags & Syntax::HIGHLIGHT_STRINGS)
|
||||
{
|
||||
if ($inString !== '')
|
||||
{
|
||||
$this->hl[$i] = Highlight::STRING;
|
||||
|
||||
// Check for escaped character
|
||||
if ($char === '\\' && $i+1 < $this->rsize)
|
||||
{
|
||||
$this->hl[$i + 1] = Highlight::STRING;
|
||||
$i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($char === $inString)
|
||||
{
|
||||
$inString = '';
|
||||
}
|
||||
$i++;
|
||||
$prevSep = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $char === '"' || $char === '\'')
|
||||
{
|
||||
$inString = $char;
|
||||
$this->hl[$i] = Highlight::STRING;
|
||||
$i++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$this->highlightString($i);
|
||||
|
||||
// Numbers, including decimal points
|
||||
if ($this->parent->fileType->syntax->flags & Syntax::HIGHLIGHT_NUMBERS)
|
||||
|
@ -8,3 +8,5 @@ namespace Aviat\Kilo;
|
||||
const KILO_VERSION = '0.3.0';
|
||||
const KILO_TAB_STOP = 4;
|
||||
const KILO_QUIT_TIMES = 3;
|
||||
|
||||
const NO_MATCH = -1;
|
||||
|
Loading…
Reference in New Issue
Block a user