php-kilo/src/Row.php

428 lines
9.6 KiB
PHP

<?php declare(strict_types=1);
namespace Aviat\Kilo;
use Aviat\Kilo\Enum\Highlight;
use Aviat\Kilo\Enum\KeyCode;
/**
* @property-read int $size
* @property-read int $rsize
* @property-read string $chars
*/
class Row {
use Traits\MagicProperties;
private string $chars = '';
public string $render = '';
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;
}
private function __construct() {
// Private in favor of ::new static function
}
public function __get(string $name): mixed
{
return match ($name)
{
'size' => strlen($this->chars),
'rsize' => strlen($this->render),
'chars' => $this->chars,
default => NULL,
};
}
public function __set(string $name, mixed $value): void
{
if ($name === 'chars')
{
$this->chars = $value;
$this->update();
}
}
public function __toString(): string
{
return $this->chars . "\n";
}
public function __debugInfo(): array
{
return [
'size' => $this->size,
'rsize' => $this->rsize,
'chars' => $this->chars,
'render' => $this->render,
'hl' => $this->hl,
'hlOpenComment' => $this->hlOpenComment,
];
}
public function insertChar(int $at, string $c): void
{
if ($at < 0 || $at > $this->size)
{
$this->appendString($c);
return;
}
// Safely insert into arbitrary position in the existing string
$this->chars = substr($this->chars, 0, $at) . $c . substr($this->chars, $at);
$this->update();
$this->parent->dirty = true;
}
public function appendString(string $s): void
{
$this->chars .= $s;
$this->update();
$this->parent->dirty = true;
}
public function deleteChar(int $at): void
{
if ($at < 0 || $at >= $this->size)
{
return;
}
$this->chars = substr_replace($this->chars, '', $at, 1);
$this->update();
$this->parent->dirty = true;
}
public function highlight(): void
{
// $this->update();
$this->highlightGeneral();
}
public function update(): void
{
$this->render = tabs_to_spaces($this->chars);
$this->highlight();
}
// ------------------------------------------------------------------------
// ! Syntax Highlighting
// ------------------------------------------------------------------------
public function highlightGeneral(): void
{
$this->hl = array_fill(0, $this->rsize, Highlight::NORMAL);
if ($this->parent->fileType->name === 'PHP')
{
$this->highlightPHP();
return;
}
$keywords1 = $this->parent->fileType->syntax->keywords1;
$keywords2 = $this->parent->fileType->syntax->keywords2;
$scs = $this->parent->fileType->syntax->singleLineCommentStart;
$mcs = $this->parent->fileType->syntax->multiLineCommentStart;
$mce = $this->parent->fileType->syntax->multiLineCommentEnd;
$scsLen = strlen($scs);
$mcsLen = strlen($mcs);
$mceLen = strlen($mce);
$prevSep = TRUE;
$inString = '';
$inComment = ($this->idx > 0 && $this->parent->rows[$this->idx - 1]->hlOpenComment);
$i = 0;
while ($i < $this->rsize)
{
$char = $this->render[$i];
$prevHl = ($i > 0) ? $this->hl[$i - 1] : Highlight::NORMAL;
// Single-line comments
if ($scsLen > 0 && $inString === '' && $inComment === FALSE
&& substr($this->render, $i, $scsLen) === $scs)
{
array_replace_range($this->hl, $i, $this->rsize - $i, Highlight::COMMENT);
break;
}
// Multi-line comments
if ($mcsLen > 0 && $mceLen > 0 && $inString === '')
{
if ($inComment)
{
$this->hl[$i] = Highlight::ML_COMMENT;
if (substr($this->render, $i, $mceLen) === $mce)
{
array_replace_range($this->hl, $i, $mceLen, Highlight::ML_COMMENT);
$i += $mceLen;
$inComment = FALSE;
$prevSep = TRUE;
continue;
}
$i++;
continue;
}
if (substr($this->render, $i, $mcsLen) === $mcs)
{
array_replace_range($this->hl, $i, $mcsLen, Highlight::ML_COMMENT);
$i += $mcsLen;
$inComment = TRUE;
continue;
}
}
// 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;
}
}
// Numbers, including decimal points
if ($this->parent->fileType->syntax->flags & Syntax::HIGHLIGHT_NUMBERS)
{
if (
($char === '.' && $prevHl === Highlight::NUMBER) ||
(($prevSep || $prevHl === Highlight::NUMBER) && is_digit($char))
)
{
$this->hl[$i] = Highlight::NUMBER;
$i++;
$prevSep = FALSE;
continue;
}
}
// Keywords
if ($prevSep)
{
$findKeywords = function (array $keywords, int $syntaxType) use (&$i): void
{
foreach ($keywords as $k)
{
$klen = strlen($k);
$nextCharOffset = $i + $klen;
$isEndOfLine = $nextCharOffset >= $this->rsize;
$nextChar = ($isEndOfLine) ? KeyCode::NULL : $this->render[$nextCharOffset];
if (substr($this->render, $i, $klen) === $k && is_separator($nextChar))
{
array_replace_range($this->hl, $i, $klen, $syntaxType);
$i += $klen - 1;
break;
}
}
};
$findKeywords($keywords1, Highlight::KEYWORD1);
$findKeywords($keywords2, Highlight::KEYWORD2);
}
$prevSep = is_separator($char);
$i++;
}
$changed = $this->hlOpenComment !== $inComment;
$this->hlOpenComment = $inComment;
if ($changed && $this->idx + 1 < $this->parent->numRows)
{
// @codeCoverageIgnoreStart
$this->parent->rows[$this->idx + 1]->updateSyntax();
// @codeCoverageIgnoreEnd
}
}
protected function highlightPHP(): void
{
$rowNum = $this->idx + 1;
$hasRowTokens = array_key_exists($rowNum, $this->parent->tokens);
if ( ! (
$hasRowTokens &&
$this->idx < $this->parent->numRows
))
{
// @codeCoverageIgnoreStart
return;
// @codeCoverageIgnoreEnd
}
$tokens = $this->parent->tokens[$rowNum];
$inComment = ($this->idx > 0 && $this->parent->rows[$this->idx - 1]->hlOpenComment);
// Keep track of where you are in the line, so that
// multiples of the same tokens can be effectively matched
$offset = 0;
foreach ($tokens as $token)
{
if ($offset >= $this->rsize)
{
// @codeCoverageIgnoreStart
break;
// @codeCoverageIgnoreEnd
}
// A multi-line comment can end in the middle of a line...
if ($inComment)
{
// Try looking for the end of the comment first
$commentEnd = strpos($this->render, '*/');
if ($commentEnd !== FALSE)
{
$inComment = FALSE;
array_replace_range($this->hl, 0, $commentEnd + 2, Highlight::ML_COMMENT);
$offset = $commentEnd;
continue;
}
// Otherwise, just set the whole row
$this->hl = array_fill(0, $this->rsize, Highlight::ML_COMMENT);
$this->hl[$offset] = Highlight::ML_COMMENT;
break;
}
$char = $token['char']; // ?? '';
$charLen = strlen($char);
if ($charLen === 0 || $offset >= $this->rsize)
{
// @codeCoverageIgnoreStart
continue;
// @codeCoverageIgnoreEnd
}
$charStart = strpos($this->render, $char, $offset);
if ($charStart === FALSE)
{
continue;
}
$charEnd = $charStart + $charLen;
// Start of multiline comment/single line comment
if (in_array($token['type'], [T_DOC_COMMENT, T_COMMENT], TRUE))
{
// Single line comments
if (str_contains($char, '//') || str_contains($char, '#'))
{
array_replace_range($this->hl, $charStart, $charLen, Highlight::COMMENT);
break;
}
// Start of multi-line comment
$start = strpos($this->render, '/*', $offset);
$end = strpos($this->render, '*/', $offset);
$hasStart = $start !== FALSE;
$hasEnd = $end !== FALSE;
if ($hasStart)
{
if ($hasEnd)
{
$len = $end - $start + 2;
array_replace_range($this->hl, $start, $len, Highlight::ML_COMMENT);
$inComment = FALSE;
}
else
{
$inComment = TRUE;
array_replace_range($this->hl, $start, $charLen - $offset, Highlight::ML_COMMENT);
$offset = $start + $charLen - $offset;
}
}
if ($inComment)
{
break;
}
}
$tokenHighlight = Highlight::fromPHPToken($token['type']);
$charHighlight = Highlight::fromPHPChar(trim($token['char']));
$hl = match(true) {
// Matches a predefined PHP token
$token['type'] !== self::T_RAW && $tokenHighlight !== Highlight::NORMAL
=> $tokenHighlight,
// Matches a specific syntax character
$charHighlight !== Highlight::NORMAL => $charHighlight,
// Types/identifiers/keywords that don't have their own token, but are
// defined as keywords
in_array($token['char'], $this->parent->fileType->syntax->keywords2, TRUE)
=> Highlight::KEYWORD2,
default => Highlight::NORMAL,
};
if ($hl !== Highlight::NORMAL)
{
array_replace_range($this->hl, $charStart, $charLen, $hl);
$offset = $charEnd;
}
}
$changed = $this->hlOpenComment !== $inComment;
$this->hlOpenComment = $inComment;
if ($changed && ($this->idx + 1) < $this->parent->numRows)
{
// @codeCoverageIgnoreStart
$this->parent->rows[$this->idx + 1]->updateSyntax();
// @codeCoverageIgnoreEnd
}
}
}