php-kilo/src/Row.php

599 lines
15 KiB
PHP
Raw Normal View History

2019-10-24 16:57:27 -04:00
<?php declare(strict_types=1);
namespace Kilo;
/**
* @property-read int size
* @property-read int rsize
*/
class Row {
use MagicProperties;
2019-11-05 12:28:10 -05:00
private string $chars = '';
2019-10-24 16:57:27 -04:00
public string $render = '';
2019-10-25 16:36:03 -04:00
public array $hl = [];
public int $idx;
2019-10-25 10:28:15 -04:00
// This feels dirty...
private Editor $parent;
2019-10-25 16:36:03 -04:00
private bool $hlOpenComment = FALSE;
2019-10-25 10:28:15 -04:00
private array $phpTokenHighlightMap = [
// Delimiters
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,
T_CLOSE_TAG => Highlight::DELIMITER,
T_START_HEREDOC => Highlight::DELIMITER,
T_END_HEREDOC => Highlight::DELIMITER,
// Number literals
T_DNUMBER => Highlight::NUMBER,
T_LNUMBER => Highlight::NUMBER,
2019-11-06 13:57:19 -05:00
// String literals
T_CONSTANT_ENCAPSED_STRING => Highlight::STRING,
2019-11-06 13:57:19 -05:00
T_ENCAPSED_AND_WHITESPACE => Highlight::STRING,
// Simple variables
T_VARIABLE => Highlight::VARIABLE,
T_STRING_VARNAME => 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,
2019-11-06 13:57:19 -05:00
// Type casts
T_ARRAY_CAST => Highlight::KEYWORD2,
T_BOOL_CAST => Highlight::KEYWORD2,
T_DOUBLE_CAST => Highlight::KEYWORD2,
T_INT_CAST => Highlight::KEYWORD2,
T_OBJECT_CAST => Highlight::KEYWORD2,
T_STRING_CAST => Highlight::KEYWORD2,
T_UNSET_CAST => 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,
];
2019-10-25 16:36:03 -04:00
public static function new(Editor $parent, string $chars, int $idx): self
2019-10-24 16:57:27 -04:00
{
2019-10-25 10:28:15 -04:00
$self = new self();
$self->chars = $chars;
$self->parent = $parent;
2019-10-25 16:36:03 -04:00
$self->idx = $idx;
2019-10-24 16:57:27 -04:00
2019-10-25 10:28:15 -04:00
return $self;
2019-10-24 16:57:27 -04:00
}
2019-10-25 10:28:15 -04:00
private function __construct() {}
2019-10-24 16:57:27 -04:00
public function __get(string $name)
{
switch ($name)
{
case 'size':
return strlen($this->chars);
case 'rsize':
return strlen($this->render);
2019-11-05 12:28:10 -05:00
case 'chars':
return $this->chars;
2019-10-24 16:57:27 -04:00
default:
return NULL;
}
}
2019-11-05 12:28:10 -05:00
public function __set(string $key, $value): void
{
if ($key === 'chars')
{
$this->chars = $value;
$this->update();
}
}
2019-10-24 16:57:27 -04:00
public function __toString(): string
{
return $this->chars . "\n";
}
2019-10-25 10:28:15 -04:00
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++;
}
2019-10-24 16:57:27 -04:00
public function update(): void
{
$idx = 0;
2019-11-05 13:51:21 -05:00
// Make sure the render buffer is empty before updating
// otherwise, you will have persistent output where you
// don't want it.
2019-11-05 12:28:10 -05:00
$this->render = '';
2019-10-24 16:57:27 -04:00
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
2019-10-24 16:57:27 -04:00
{
$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;
2019-10-25 16:36:03 -04:00
$mcs = $this->parent->syntax->multiLineCommentStart;
$mce = $this->parent->syntax->multiLineCommentEnd;
$scsLen = strlen($scs);
$mcsLen = strlen($mcs);
$mceLen = strlen($mce);
2019-10-24 16:57:27 -04:00
$prevSep = TRUE;
2019-10-25 11:49:04 -04:00
$inString = '';
2019-10-25 16:36:03 -04:00
$inComment = ($this->idx > 0 && $this->parent->rows[$this->idx - 1]->hlOpenComment);
2019-10-24 16:57:27 -04:00
$i = 0;
while ($i < $this->rsize)
{
$char = $this->render[$i];
$prevHl = ($i > 0) ? $this->hl[$i - 1] : Highlight::NORMAL;
2019-10-25 10:28:15 -04:00
if ($this->parent->syntax === NULL)
{
return;
}
// Single-line comments
2019-10-25 16:36:03 -04:00
if ($scsLen > 0 && $inString === '' && $inComment === FALSE
&& substr($this->render, $i, $scsLen) === $scs)
{
array_replace_range($this->hl, $i, $this->rsize - $i, Highlight::COMMENT);
break;
}
2019-10-25 16:36:03 -04:00
// 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
2019-10-25 11:49:04 -04:00
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
2019-10-25 10:28:15 -04:00
if ($this->parent->syntax->flags & Syntax::HIGHLIGHT_NUMBERS)
2019-10-24 16:57:27 -04:00
{
2019-10-25 10:28:15 -04:00
if (
($char === '.' && $prevHl === Highlight::NUMBER) ||
(($prevSep || $prevHl === Highlight::NUMBER) && is_digit($char))
)
{
$this->hl[$i] = Highlight::NUMBER;
$i++;
$prevSep = FALSE;
continue;
}
2019-10-24 16:57:27 -04:00
}
// Keywords
if ($prevSep)
{
2019-11-05 12:28:10 -05:00
$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) ? "\0" : $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);
}
2019-10-24 16:57:27 -04:00
$prevSep = is_separator($char);
$i++;
}
2019-10-25 16:36:03 -04:00
$changed = $this->hlOpenComment !== $inComment;
$this->hlOpenComment = $inComment;
if ($changed && $this->idx + 1 < $this->parent->numRows)
{
$this->parent->rows[$this->idx + 1]->updateSyntax();
2019-10-25 16:36:03 -04:00
}
2019-10-24 16:57:27 -04:00
}
protected function updateSyntaxPHP():void
{
$rowNum = $this->idx + 1;
if (
( ! isset($this->parent->syntax->tokens))
|| ( ! array_key_exists($rowNum, $this->parent->syntax->tokens))
|| $this->idx > $this->parent->numRows)
2019-11-05 12:56:12 -05:00
{
return;
}
2019-11-05 13:21:37 -05:00
$tokens = $this->parent->syntax->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;
2019-11-06 13:57:19 -05:00
// There will be no tokens if a line is empty, or if in a multi-line comment
if (empty($tokens))
{
if ($inComment && $this->rsize > 0)
{
while ($offset < $this->rsize)
{
if (substr($this->render, $offset, 2) === '*/')
{
array_replace_range($this->hl, $offset, 2, Highlight::ML_COMMENT);
break;
}
$this->hl[$offset] = Highlight::ML_COMMENT;
$offset++;
continue;
}
}
return;
}
foreach ($tokens as $token)
{
if ($offset >= $this->rsize)
{
break;
}
2019-11-06 13:57:19 -05:00
// A multi-line comment can end in the middle of a line...
while ($inComment)
{
2019-11-06 13:57:19 -05:00
if ($token['type'] === T_WHITESPACE)
{
$char = $token['char'];
$charStart = strpos($this->render, $char, $offset) - 2;
$inComment = FALSE;
array_replace_range($this->hl, $charStart, 2, Highlight::ML_COMMENT);
$offset += 2;
}
if (substr($this->render, $offset, 2) === '*/')
{
$inComment = FALSE;
array_replace_range($this->hl, $offset, 2, Highlight::ML_COMMENT);
$offset += 2;
}
$this->hl[$offset] = Highlight::ML_COMMENT;
$offset++;
continue;
}
2019-11-06 13:57:19 -05:00
$char = $token['char'];
$charLen = strlen($char);
if ($charLen === 0 || $offset >= $this->rsize)
{
continue;
}
$charStart = strpos($this->render, $char, $offset);
if ($charStart === FALSE)
{
continue;
}
$charEnd = $charStart + $charLen;
// Probably not great to create a closure in a loop like this, but
// this halves the boilerplate for each type of syntax
$highlightRange = function (int $hl) use ($charLen, $charStart, $charEnd, &$offset) {
array_replace_range($this->hl, $charStart, $charLen, $hl);
$offset = $charEnd;
};
2019-11-06 13:57:19 -05:00
// Start of multiline comment/single line comment
if (in_array($token['type'], [T_DOC_COMMENT, T_COMMENT], TRUE))
{
// Single line comments
if (strpos($token['char'], '//') !== FALSE)
{
$highlightRange(Highlight::COMMENT);
break;
}
// Start of multi-line comment
$start = strpos($this->render, '/*', $offset);
$inComment = strpos($this->render, '*/', $offset) === FALSE;
array_replace_range($this->hl, $start, strlen($char) - $offset, Highlight::ML_COMMENT);
$offset = $start + strlen($char) - $offset;
if ($inComment)
{
break;
}
2019-10-29 17:24:04 -04:00
}
// Highlight specific tokens
2019-11-06 13:57:19 -05:00
if ($token['typeName'] !== 'RAW')
{
2019-11-06 13:57:19 -05:00
if (array_key_exists($token['type'], $this->phpTokenHighlightMap))
{
$hl = $this->phpTokenHighlightMap[$token['type']];
$highlightRange($hl);
2019-11-06 13:57:19 -05:00
continue;
}
// Types/identifiers/keywords that don't have their own token
if ($token['type'] === T_STRING)
{
if (in_array($token['char'], $this->parent->syntax->keywords2, TRUE))
{
$highlightRange(Highlight::KEYWORD2);
2019-11-06 13:57:19 -05:00
continue;
}
}
}
2019-11-06 13:57:19 -05:00
// Highlight raw characters
2019-11-06 13:57:19 -05:00
if (($token['typeName'] === 'RAW') && array_key_exists(trim($token['char']), $this->phpCharacterHighlightMap))
{
2019-11-06 13:57:19 -05:00
$hl = $this->phpCharacterHighlightMap[trim($token['char'])];
$highlightRange($hl);
continue;
}
}
$changed = $this->hlOpenComment !== $inComment;
$this->hlOpenComment = $inComment;
if ($changed && $this->idx + 1 < $this->parent->numRows)
{
$this->parent->rows[$this->idx + 1]->updateSyntax();
}
}
2019-10-24 16:57:27 -04:00
}