478 lines
12 KiB
PHP
478 lines
12 KiB
PHP
<?php declare(strict_types=1);
|
|
|
|
namespace Kilo;
|
|
|
|
/**
|
|
* @property-read int size
|
|
* @property-read int rsize
|
|
*/
|
|
class Row {
|
|
use MagicProperties;
|
|
|
|
public string $chars = '';
|
|
public string $render = '';
|
|
|
|
public array $hl = [];
|
|
|
|
public int $idx;
|
|
|
|
// This feels dirty...
|
|
private Editor $parent;
|
|
private bool $hlOpenComment = FALSE;
|
|
|
|
private array $phpTokenHighlightMap = [
|
|
// Delimiters
|
|
T_CLOSE_TAG => Highlight::DELIMITER,
|
|
T_ARRAY => Highlight::DELIMITER,
|
|
T_CURLY_OPEN => Highlight::DELIMITER,
|
|
T_DOLLAR_OPEN_CURLY_BRACES => Highlight::DELIMITER,
|
|
T_OPEN_TAG => Highlight::DELIMITER,
|
|
T_OPEN_TAG_WITH_ECHO => Highlight::DELIMITER,
|
|
|
|
// Number literals
|
|
T_DNUMBER => Highlight::NUMBER,
|
|
T_LNUMBER => Highlight::NUMBER,
|
|
|
|
// Simple string literals
|
|
T_CONSTANT_ENCAPSED_STRING => Highlight::STRING,
|
|
|
|
// Simple variables
|
|
T_VARIABLE => Highlight::VARIABLE,
|
|
|
|
// Operators
|
|
T_AND_EQUAL => Highlight::OPERATOR,
|
|
T_BOOLEAN_AND => Highlight::OPERATOR,
|
|
T_BOOLEAN_OR => Highlight::OPERATOR,
|
|
T_COALESCE => Highlight::OPERATOR,
|
|
T_CONCAT_EQUAL => Highlight::OPERATOR,
|
|
T_DEC => Highlight::OPERATOR,
|
|
T_DIV_EQUAL => Highlight::OPERATOR,
|
|
T_DOUBLE_ARROW => Highlight::OPERATOR,
|
|
T_DOUBLE_COLON => Highlight::OPERATOR,
|
|
T_ELLIPSIS => Highlight::OPERATOR,
|
|
T_INC => Highlight::OPERATOR,
|
|
T_IS_EQUAL => Highlight::OPERATOR,
|
|
T_IS_GREATER_OR_EQUAL => Highlight::OPERATOR,
|
|
T_IS_IDENTICAL => Highlight::OPERATOR,
|
|
T_IS_NOT_EQUAL => Highlight::OPERATOR,
|
|
T_IS_NOT_IDENTICAL => Highlight::OPERATOR,
|
|
T_IS_SMALLER_OR_EQUAL => Highlight::OPERATOR,
|
|
T_SPACESHIP => Highlight::OPERATOR,
|
|
T_LOGICAL_AND => Highlight::OPERATOR,
|
|
T_LOGICAL_OR => Highlight::OPERATOR,
|
|
T_LOGICAL_XOR => Highlight::OPERATOR,
|
|
T_MINUS_EQUAL => Highlight::OPERATOR,
|
|
T_MOD_EQUAL => Highlight::OPERATOR,
|
|
T_MUL_EQUAL => Highlight::OPERATOR,
|
|
T_NS_SEPARATOR => Highlight::OPERATOR,
|
|
T_OBJECT_OPERATOR => Highlight::OPERATOR,
|
|
T_OR_EQUAL => Highlight::OPERATOR,
|
|
T_PLUS_EQUAL => Highlight::OPERATOR,
|
|
T_POW => Highlight::OPERATOR,
|
|
T_POW_EQUAL => Highlight::OPERATOR,
|
|
T_SL => Highlight::OPERATOR,
|
|
T_SL_EQUAL => Highlight::OPERATOR,
|
|
T_SR => Highlight::OPERATOR,
|
|
T_SR_EQUAL => Highlight::OPERATOR,
|
|
T_XOR_EQUAL => Highlight::OPERATOR,
|
|
|
|
// Keywords1
|
|
T_ABSTRACT => Highlight::KEYWORD1,
|
|
T_AS => Highlight::KEYWORD1,
|
|
T_BREAK => Highlight::KEYWORD1,
|
|
T_CASE => Highlight::KEYWORD1,
|
|
T_CATCH => Highlight::KEYWORD1,
|
|
T_CLASS => Highlight::KEYWORD1,
|
|
T_CLONE => Highlight::KEYWORD1,
|
|
T_CONST => Highlight::KEYWORD1,
|
|
T_CONTINUE => Highlight::KEYWORD1,
|
|
T_DECLARE => Highlight::KEYWORD1,
|
|
T_DEFAULT => Highlight::KEYWORD1,
|
|
T_DO => Highlight::KEYWORD1,
|
|
T_ELSE => Highlight::KEYWORD1,
|
|
T_ELSEIF => Highlight::KEYWORD1,
|
|
T_ENDDECLARE => Highlight::KEYWORD1,
|
|
T_ENDFOR => Highlight::KEYWORD1,
|
|
T_ENDFOREACH => Highlight::KEYWORD1,
|
|
T_ENDIF => Highlight::KEYWORD1,
|
|
T_ENDSWITCH => Highlight::KEYWORD1,
|
|
T_ENDWHILE => Highlight::KEYWORD1,
|
|
T_EXTENDS => Highlight::KEYWORD1,
|
|
T_FINAL => Highlight::KEYWORD1,
|
|
T_FINALLY => Highlight::KEYWORD1,
|
|
T_FOR => Highlight::KEYWORD1,
|
|
T_FOREACH => Highlight::KEYWORD1,
|
|
T_FUNCTION => Highlight::KEYWORD1,
|
|
T_GLOBAL => Highlight::KEYWORD1,
|
|
T_GOTO => Highlight::KEYWORD1,
|
|
T_HALT_COMPILER => Highlight::KEYWORD1,
|
|
T_IF => Highlight::KEYWORD1,
|
|
T_IMPLEMENTS => Highlight::KEYWORD1,
|
|
T_INSTANCEOF => Highlight::KEYWORD1,
|
|
T_INSTEADOF => Highlight::KEYWORD1,
|
|
T_INTERFACE => Highlight::KEYWORD1,
|
|
T_NAMESPACE => Highlight::KEYWORD1,
|
|
T_NEW => Highlight::KEYWORD1,
|
|
T_PRIVATE => Highlight::KEYWORD1,
|
|
T_PUBLIC => Highlight::KEYWORD1,
|
|
T_PROTECTED => Highlight::KEYWORD1,
|
|
T_RETURN => Highlight::KEYWORD1,
|
|
T_STATIC => Highlight::KEYWORD1,
|
|
T_SWITCH => Highlight::KEYWORD1,
|
|
T_THROW => Highlight::KEYWORD1,
|
|
T_TRAIT => Highlight::KEYWORD1,
|
|
T_TRY => Highlight::KEYWORD1,
|
|
T_USE => Highlight::KEYWORD1,
|
|
T_VAR => Highlight::KEYWORD1,
|
|
T_WHILE => Highlight::KEYWORD1,
|
|
T_YIELD => Highlight::KEYWORD1,
|
|
T_YIELD_FROM => Highlight::KEYWORD1,
|
|
|
|
// Not string literals, but identifiers, keywords, etc.
|
|
// T_STRING => Highlight::KEYWORD2,
|
|
];
|
|
|
|
private array $phpCharacterHighlightMap = [
|
|
// Delimiter characters
|
|
'[' => Highlight::DELIMITER,
|
|
']' => Highlight::DELIMITER,
|
|
'{' => Highlight::DELIMITER,
|
|
'}' => Highlight::DELIMITER,
|
|
'(' => Highlight::DELIMITER,
|
|
')' => Highlight::DELIMITER,
|
|
|
|
// Single character operators
|
|
',' => Highlight::OPERATOR,
|
|
';' => Highlight::OPERATOR,
|
|
':' => Highlight::OPERATOR,
|
|
'^' => Highlight::OPERATOR,
|
|
'%' => Highlight::OPERATOR,
|
|
'+' => Highlight::OPERATOR,
|
|
'-' => Highlight::OPERATOR,
|
|
'*' => Highlight::OPERATOR,
|
|
'/' => Highlight::OPERATOR,
|
|
'.' => Highlight::OPERATOR,
|
|
'|' => Highlight::OPERATOR,
|
|
'~' => Highlight::OPERATOR,
|
|
'>' => Highlight::OPERATOR,
|
|
'<' => Highlight::OPERATOR,
|
|
'=' => Highlight::OPERATOR,
|
|
'!' => Highlight::OPERATOR,
|
|
];
|
|
|
|
public static function new(Editor $parent, string $chars, int $idx): self
|
|
{
|
|
$self = new self();
|
|
$self->chars = $chars;
|
|
$self->parent = $parent;
|
|
$self->idx = $idx;
|
|
|
|
return $self;
|
|
}
|
|
|
|
private function __construct() {}
|
|
|
|
public function __get(string $name)
|
|
{
|
|
switch ($name)
|
|
{
|
|
case 'size':
|
|
return strlen($this->chars);
|
|
|
|
case 'rsize':
|
|
return strlen($this->render);
|
|
|
|
default:
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
public function __toString(): string
|
|
{
|
|
return $this->chars . "\n";
|
|
}
|
|
|
|
public function insertChar(int $at, string $c): void
|
|
{
|
|
if ($at < 0 || $at > $this->size)
|
|
{
|
|
$at = $this->size;
|
|
}
|
|
|
|
// 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++;
|
|
}
|
|
|
|
public function appendString(string $s): void
|
|
{
|
|
$this->chars .= $s;
|
|
$this->update();
|
|
|
|
$this->parent->dirty++;
|
|
}
|
|
|
|
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++;
|
|
}
|
|
|
|
public function update(): void
|
|
{
|
|
$idx = 0;
|
|
|
|
for ($i = 0; $i < $this->size; $i++)
|
|
{
|
|
if ($this->chars[$i] === "\t")
|
|
{
|
|
$this->render[$idx++] = ' ';
|
|
while ($idx % KILO_TAB_STOP !== 0)
|
|
{
|
|
$this->render[$idx++] = ' ';
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$this->render[$idx++] = $this->chars[$i];
|
|
}
|
|
}
|
|
|
|
$this->updateSyntax();
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// ! Syntax Highlighting
|
|
// ------------------------------------------------------------------------
|
|
|
|
public function updateSyntax(): void
|
|
{
|
|
$this->hl = array_fill(0, $this->rsize, Highlight::NORMAL);
|
|
|
|
if ($this->parent->syntax->filetype === 'PHP')
|
|
{
|
|
$this->updateSyntaxPHP();
|
|
return;
|
|
}
|
|
|
|
$keywords1 = $this->parent->syntax->keywords1;
|
|
$keywords2 = $this->parent->syntax->keywords2;
|
|
|
|
$scs = $this->parent->syntax->singleLineCommentStart;
|
|
$mcs = $this->parent->syntax->multiLineCommentStart;
|
|
$mce = $this->parent->syntax->multiLineCommentEnd;
|
|
|
|
$scsLen = strlen($scs);
|
|
$mcsLen = strlen($mcs);
|
|
$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;
|
|
|
|
if ($this->parent->syntax === NULL)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// 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->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->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)
|
|
{
|
|
foreach ($keywords1 as $k)
|
|
{
|
|
$klen = strlen($k);
|
|
$nextCharOffset = $i + $klen;
|
|
$isEndOfLine = $nextCharOffset >= $this->rsize;
|
|
$nextChar = ($isEndOfLine) ? "\0" : $this->render[$nextCharOffset];
|
|
|
|
if (substr($this->render, $i, $klen) === $k && is_separator($nextChar))
|
|
{
|
|
array_replace_range($this->hl, $i, $klen, Highlight::KEYWORD1);
|
|
$i += $klen - 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
foreach ($keywords2 as $k)
|
|
{
|
|
$klen = strlen($k);
|
|
$nextCharOffset = $i + $klen;
|
|
$isEndOfLine = $nextCharOffset >= $this->rsize;
|
|
$nextChar = ($isEndOfLine) ? "\0" : $this->render[$nextCharOffset];
|
|
|
|
if (substr($this->render, $i, $klen) === $k && is_separator($nextChar))
|
|
{
|
|
array_replace_range($this->hl, $i, $klen, Highlight::KEYWORD2);
|
|
$i += $klen - 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
$prevSep = is_separator($char);
|
|
$i++;
|
|
}
|
|
|
|
$changed = $this->hlOpenComment !== $inComment;
|
|
$this->hlOpenComment = $inComment;
|
|
if ($changed && $this->idx + 1 < $this->parent->numRows)
|
|
{
|
|
$this->parent->rows[$this->idx + 1]->updateSyntax();
|
|
}
|
|
}
|
|
|
|
protected function updateSyntaxPHP():void
|
|
{
|
|
$tokens = $this->parent->syntax->tokens[$this->idx + 1];
|
|
|
|
// The line is probably just empty
|
|
if ($tokens === NULL)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// $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)
|
|
{
|
|
break;
|
|
}
|
|
|
|
$char = $token['char'];
|
|
$charLen = strlen($char);
|
|
$charStart = strpos($this->render, $char, $offset);
|
|
if ($charStart === FALSE)
|
|
{
|
|
$offset++;
|
|
continue;
|
|
}
|
|
|
|
$charEnd = $charStart + $charLen;
|
|
|
|
// Highlight raw characters
|
|
if (($token['typeName'] === 'RAW') && array_key_exists($token['char'], $this->phpCharacterHighlightMap))
|
|
{
|
|
$hl = $this->phpCharacterHighlightMap[$token['char']];
|
|
array_replace_range($this->hl, $charStart, $charLen, $hl);
|
|
$offset = $charEnd;
|
|
continue;
|
|
}
|
|
|
|
// Highlight specific tokens
|
|
if (array_key_exists($token['type'], $this->phpTokenHighlightMap))
|
|
{
|
|
$hl = $this->phpTokenHighlightMap[$token['type']];
|
|
array_replace_range($this->hl, $charStart, $charLen, $hl);
|
|
$offset = $charEnd;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|