php-kilo/src/Row.php

657 lines
13 KiB
PHP
Raw Normal View History

2019-10-24 16:57:27 -04:00
<?php declare(strict_types=1);
2019-11-08 16:27:08 -05:00
namespace Aviat\Kilo;
use Aviat\Kilo\Enum\Highlight;
2021-04-09 13:52:01 -04:00
use Aviat\Kilo\Enum\RawKeyCode;
2019-10-24 16:57:27 -04:00
/**
2021-03-03 16:35:58 -05:00
* @property-read int $size
* @property-read int $rsize
2021-03-18 16:26:30 -04:00
* @property string $chars
2019-10-24 16:57:27 -04:00
*/
class Row {
2019-11-19 15:57:51 -05:00
use Traits\MagicProperties;
2019-10-24 16:57:27 -04:00
2021-03-18 16:26:30 -04:00
/**
* The version of the row to be displayed (where tabs are converted to display spaces)
*/
2019-10-24 16:57:27 -04:00
public string $render = '';
2021-03-18 16:26:30 -04:00
/**
* The mapping of characters to their highlighting type
*/
2019-10-25 16:36:03 -04:00
public array $hl = [];
/**
* Are we in the middle of highlighting a multi-line comment?
*/
2019-10-25 16:36:03 -04:00
private bool $hlOpenComment = FALSE;
2019-10-25 10:28:15 -04:00
private const T_RAW = -1;
/**
* 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
2019-10-24 16:57:27 -04:00
{
2021-03-18 16:26:30 -04:00
return new self(
$parent,
$chars,
$idx,
);
2019-10-24 16:57:27 -04:00
}
/**
* Create an empty Row
*
* @return self
*/
2021-03-18 16:26:30 -04:00
public static function default(): self
{
return new self(
Document::new(),
'',
0,
);
2021-03-09 13:37:03 -05:00
}
2019-10-25 10:28:15 -04:00
2021-03-18 16:26:30 -04:00
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,
) {}
2021-03-03 16:35:58 -05:00
public function __get(string $name): mixed
2019-10-24 16:57:27 -04:00
{
2021-03-03 11:50:29 -05:00
return match ($name)
2019-10-24 16:57:27 -04:00
{
2021-03-03 11:50:29 -05:00
'size' => strlen($this->chars),
'rsize' => strlen($this->render),
'chars' => $this->chars,
default => NULL,
};
2019-10-24 16:57:27 -04:00
}
2021-03-03 16:35:58 -05:00
public function __set(string $name, mixed $value): void
2019-11-05 12:28:10 -05:00
{
2021-03-03 16:35:58 -05:00
if ($name === 'chars')
2019-11-05 12:28:10 -05:00
{
$this->chars = $value;
$this->highlight();
2019-11-05 12:28:10 -05:00
}
}
/**
* Convert the row contents to a string for saving
*
* @return string
*/
2019-10-24 16:57:27 -04:00
public function __toString(): string
{
return $this->chars . "\n";
}
/**
* Set the properties to display for var_dump
*
* @return array
*/
2019-11-20 15:03:48 -05:00
public function __debugInfo(): array
2019-11-08 21:48:46 -05:00
{
return [
'size' => $this->size,
'rsize' => $this->rsize,
'chars' => $this->chars,
'render' => $this->render,
'hl' => $this->hl,
2019-11-08 21:48:46 -05:00
'hlOpenComment' => $this->hlOpenComment,
];
}
2021-03-18 16:26:30 -04:00
/**
* Is this row a valid part of a document?
*
* @return bool
*/
public function isValid(): bool
{
return ! $this->parent->isEmpty();
}
/**
* Insert the string or character $c at index $at
*
* @param int $at
* @param string $c
*/
public function insert(int $at, string $c): void
2019-10-25 10:28:15 -04:00
{
if ($at < 0 || $at > $this->size)
{
$this->append($c);
2019-11-20 15:03:48 -05:00
return;
2019-10-25 10:28:15 -04:00
}
// Safely insert into arbitrary position in the existing string
$this->chars = substr($this->chars, 0, $at) . $c . substr($this->chars, $at);
$this->update();
2019-10-25 10:28:15 -04:00
2021-03-10 22:47:57 -05:00
$this->parent->dirty = true;
2019-10-25 10:28:15 -04:00
}
/**
* Append $s to the current row
*
* @param string $s
*/
public function append(string $s): void
2019-10-25 10:28:15 -04:00
{
$this->chars .= $s;
$this->update();
2019-10-25 10:28:15 -04:00
2021-03-10 22:47:57 -05:00
$this->parent->dirty = true;
2019-10-25 10:28:15 -04:00
}
/**
* Delete the character at the specified index
*
* @param int $at
*/
public function delete(int $at): void
2019-10-25 10:28:15 -04:00
{
if ($at < 0 || $at >= $this->size)
{
return;
}
$this->chars = substr_replace($this->chars, '', $at, 1);
$this->update();
2019-10-25 10:28:15 -04:00
2021-03-10 22:47:57 -05:00
$this->parent->dirty = true;
2019-10-25 10:28:15 -04:00
}
/**
* Convert tabs to spaces for display, and update syntax highlighting
*/
public function update(): void
{
$this->render = tabs_to_spaces($this->chars);
$this->highlight();
}
// ------------------------------------------------------------------------
// ! Syntax Highlighting
// ------------------------------------------------------------------------
/**
* Parse the current file to apply syntax highlighting
*/
public function highlight(): void
2019-10-24 16:57:27 -04:00
{
2021-04-14 12:25:41 -04:00
$this->hl = array_fill(0, $this->rsize, Highlight::NORMAL);
if ($this->parent->fileType->name === 'PHP')
{
$this->highlightPHP();
return;
}
$syntax = $this->parent->fileType->syntax;
$mcs = $syntax->multiLineCommentStart;
$mce = $syntax->multiLineCommentEnd;
$mcsLen = strlen($mcs);
$mceLen = strlen($mce);
$inString = '';
$inComment = ($this->idx > 0 && $this->parent->rows[$this->idx - 1]->hlOpenComment);
$i = 0;
while ($i < $this->rsize)
{
// 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;
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;
}
}
if (
$this->highlightComment($i, $syntax)
|| $this->highlightMultilineComments($i, $syntax)
|| $this->highlightPrimaryKeywords($i, $syntax)
|| $this->highlightSecondaryKeywords($i, $syntax)
|| $this->highlightOperators($i, $syntax)
|| $this->highlightCommonOperators($i)
|| $this->highlightCommonDelimeters($i)
|| $this->highlightCharacter($i, $syntax)
|| $this->highlightString($i, $syntax)
|| $this->highlightNumber($i, $syntax)
) {
$i++;
2021-04-14 12:25:41 -04:00
continue;
}
$i++;
}
$changed = $this->hlOpenComment !== $inComment;
$this->hlOpenComment = $inComment;
if ($changed && $this->idx + 1 < $this->parent->numRows)
{
$this->parent->rows[$this->idx + 1]->highlight();
}
2019-10-24 16:57:27 -04:00
}
2021-04-09 19:41:15 -04:00
protected function highlightNumber(int &$i, Syntax $opts): bool
2021-03-18 16:26:30 -04:00
{
2021-04-09 19:41:15 -04:00
$char = $this->render[$i];
if ($opts->numbers() && is_digit($char))
{
if ($i > 0)
{
$prevChar = $this->render[$i - 1];
if ( ! is_separator($prevChar))
{
return false;
}
}
while (true)
{
$this->hl[$i] = Highlight::NUMBER;
$i++;
if ($i < strlen($this->render))
{
$nextChar = $this->render[$i];
if ($nextChar !== '.' && ! is_digit($nextChar))
{
break;
}
}
else
{
break;
}
}
return true;
}
return false;
}
protected function highlightWord(int &$i, array $keywords, int $syntaxType): bool
{
if ($i > 0)
{
$prevChar = $this->render[$i - 1];
if ( ! is_separator($prevChar))
{
return false;
}
}
foreach ($keywords as $k)
{
$klen = strlen($k);
$nextCharOffset = $i + $klen;
$isEndOfLine = $nextCharOffset >= $this->rsize;
$nextChar = ($isEndOfLine) ? RawKeyCode::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;
return true;
}
}
return false;
}
protected function highlightChar(int &$i, array $chars, int $syntaxType): bool
{
$char = $this->render[$i];
if (in_array($char, $chars, TRUE))
{
$this->hl[$i] = $syntaxType;
$i += 1;
return true;
}
return false;
}
2021-04-09 19:41:15 -04:00
protected function highlightPrimaryKeywords(int &$i, Syntax $opts): bool
{
return $this->highlightWord($i, $opts->keywords1, Highlight::KEYWORD1);
}
protected function highlightSecondaryKeywords(int &$i, Syntax $opts): bool
{
return $this->highlightWord($i, $opts->keywords2, Highlight::KEYWORD2);
}
protected function highlightOperators(int &$i, Syntax $opts): bool
{
return $this->highlightWord($i, $opts->operators, Highlight::OPERATOR);
}
protected function highlightCommonOperators(int &$i): bool
{
return $this->highlightChar(
$i,
['+', '-', '*', '/', '<', '^', '>', '%', '=', ':', ',', ';', '&', '~'],
Highlight::OPERATOR
);
}
protected function highlightCommonDelimeters(int &$i): bool
{
return $this->highlightChar(
$i,
['{', '}', '[', ']', '(', ')'],
Highlight::DELIMITER
);
}
protected function highlightCharacter(int &$i, Syntax $opts): bool
2021-04-09 19:41:15 -04:00
{
$char = $this->render[$i];
$nextChar = $this->render[$i + 1];
2021-04-09 19:41:15 -04:00
if ($opts->characters() && $char === "'")
{
$offset = ($nextChar === '\\') ? $i + 2 : $i + 1;
2021-04-14 12:25:41 -04:00
$closingIndex = strpos($this->render, "'", $offset);
if ($closingIndex === false)
{
return false;
}
2021-04-09 19:41:15 -04:00
$closingChar = $this->render[$closingIndex];
if ($closingChar === "'")
{
array_replace_range($this->hl, $i, $closingIndex - $i + 1, Highlight::CHARACTER);
$i = $closingIndex + 1;
2021-04-09 19:41:15 -04:00
return true;
}
}
return false;
}
protected function highlightComment(int &$i, Syntax $opts): bool
{
if ( ! $opts->comments())
{
return false;
}
$scs = $opts->singleLineCommentStart;
2021-03-18 16:26:30 -04:00
$scsLen = strlen($scs);
if ($scsLen > 0 && substr($this->render, $i, $scsLen) === $scs)
{
array_replace_range($this->hl, $i, $this->rsize - $i, Highlight::COMMENT);
2021-04-09 19:41:15 -04:00
$i = $this->rsize;
2021-03-18 16:26:30 -04:00
return true;
}
return false;
}
2021-04-09 19:41:15 -04:00
protected function highlightMultilineComments(int &$i, Syntax $opts): bool
{
if ( ! $opts->mlComments())
2021-04-09 19:41:15 -04:00
{
return false;
}
$mcs = $opts->multiLineCommentStart;
$mce = $opts->multiLineCommentEnd;
$mcsLen = strlen($mcs);
$mceLen = strlen($mce);
if ($i + $mcsLen < $this->rsize && \str_contains($this->render, $mcs))
{
$endix = strpos($this->render, $mcs);
$closingIndex = ($endix !== false)
? $i + $endix + $mcsLen + $mceLen
: $this->rsize;
array_replace_range($this->hl, $i, $closingIndex, Highlight::ML_COMMENT);
$i += $closingIndex;
return true;
}
return false;
}
protected function highlightString(int &$i, Syntax $opts): bool
2021-03-18 16:26:30 -04:00
{
$char = $this->render[$i];
2021-04-09 19:41:15 -04:00
// If there's a separate character type, highlight that separately
if ($opts->hasChar() && $char === "'")
{
return false;
}
if ($opts->strings() && $char === '"' || $char === '\'')
2021-03-18 16:26:30 -04:00
{
$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 highlightPHP(): void
{
$rowNum = $this->idx + 1;
$hasRowTokens = array_key_exists($rowNum, $this->parent->tokens);
2019-11-14 11:12:32 -05:00
if ( ! (
$hasRowTokens &&
2019-11-14 11:12:32 -05:00
$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->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)
{
break;
}
2019-11-06 13:57:19 -05:00
// A multi-line comment can end in the middle of a line...
2019-11-08 12:02:00 -05:00
if ($inComment)
{
2019-11-08 12:02:00 -05:00
// Try looking for the end of the comment first
$commentEnd = strpos($this->render, '*/');
if ($commentEnd !== FALSE)
2019-11-06 13:57:19 -05:00
{
$inComment = FALSE;
array_replace_range($this->hl, 0, $commentEnd + 2, Highlight::ML_COMMENT);
2019-11-14 17:11:10 -05:00
$offset = $commentEnd;
2019-11-08 12:02:00 -05:00
continue;
}
2019-11-08 12:02:00 -05:00
// Otherwise, just set the whole row
$this->hl = array_fill(0, $this->rsize, Highlight::ML_COMMENT);
$this->hl[$offset] = Highlight::ML_COMMENT;
2019-11-08 12:02:00 -05:00
break;
}
2019-11-06 13:57:19 -05:00
2021-04-13 15:03:17 -04:00
$char = $token['char'];
2019-11-06 13:57:19 -05:00
$charLen = strlen($char);
if ($charLen === 0 || $offset >= $this->rsize)
{
continue;
}
$charStart = strpos($this->render, $char, $offset);
if ($charStart === FALSE)
{
continue;
}
2019-11-08 12:02:00 -05:00
$charEnd = $charStart + $charLen;
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
2021-03-03 11:50:29 -05:00
if (str_contains($char, '//') || str_contains($char, '#'))
{
2019-11-08 12:02:00 -05:00
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);
2019-11-14 11:12:32 -05:00
$hasStart = $start !== FALSE;
$hasEnd = $end !== FALSE;
if ($hasStart)
{
2019-11-14 11:12:32 -05:00
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;
}
2019-11-08 12:02:00 -05:00
}
if ($inComment)
{
break;
}
2019-10-29 17:24:04 -04:00
}
2021-03-10 11:10:52 -05:00
$tokenHighlight = Highlight::fromPHPToken($token['type']);
$charHighlight = Highlight::fromPHPChar(trim($token['char']));
2019-11-06 13:57:19 -05:00
$highlight = match(true) {
2021-03-03 16:35:58 -05:00
// Matches a predefined PHP token
$token['type'] !== self::T_RAW && $tokenHighlight !== Highlight::NORMAL
=> $tokenHighlight,
2021-03-03 16:35:58 -05:00
// 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)
2021-03-03 16:35:58 -05:00
=> Highlight::KEYWORD2,
2021-03-03 16:35:58 -05:00
default => Highlight::NORMAL,
};
if ($highlight !== Highlight::NORMAL)
{
array_replace_range($this->hl, $charStart, $charLen, $highlight);
2019-11-08 12:02:00 -05:00
$offset = $charEnd;
}
}
$changed = $this->hlOpenComment !== $inComment;
$this->hlOpenComment = $inComment;
2021-03-03 16:35:58 -05:00
if ($changed && ($this->idx + 1) < $this->parent->numRows)
{
$this->parent->rows[$this->idx + 1]->highlight();
}
}
2019-10-24 16:57:27 -04:00
}