Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
11 / 11
CRAP
100.00% covered (success)
100.00%
182 / 182
Row
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
11 / 11
79
100.00% covered (success)
100.00%
182 / 182
 new
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
5 / 5
 __construct
n/a
0 / 0
1
n/a
0 / 0
 __get
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
5 / 5
 __set
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 __toString
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 __debugInfo
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
6 / 6
 insertChar
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
6 / 6
 appendString
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 deleteChar
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
5 / 5
 update
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 updateSyntax
100.00% covered (success)
100.00%
1 / 1
36
100.00% covered (success)
100.00%
83 / 83
 updateSyntaxPHP
100.00% covered (success)
100.00%
1 / 1
25
100.00% covered (success)
100.00%
63 / 63
<?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
 */
class Row {
    use Traits\MagicProperties;
    private string $chars = '';
    public string $render = '';
    public array $hl = [];
    public int $idx;
    // This feels dirty...
    private Editor $parent;
    private bool $hlOpenComment = FALSE;
    private const T_RAW = -1;
    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,
        // String literals
        T_CONSTANT_ENCAPSED_STRING => Highlight::STRING,
        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,
        // 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,
        // Invalid syntax
        T_BAD_CHARACTER => Highlight::INVALID,
    ];
    private array $phpCharacterHighlightMap = [
        // Delimiter characters
        '[' => Highlight::DELIMITER,
        ']' => Highlight::DELIMITER,
        '{' => 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,
        '!' => 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);
            case 'chars': return $this->chars;
            default: return NULL;
        }
    }
    public function __set(string $key, $value): void
    {
        if ($key === '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++;
    }
    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
    {
        $this->render = tabs_to_spaces($this->chars);
        $this->updateSyntax();
    }
    // ------------------------------------------------------------------------
    // ! Syntax Highlighting
    // ------------------------------------------------------------------------
    public function updateSyntax(): void
    {
        $this->hl = array_fill(0, $this->rsize, Highlight::NORMAL);
        if ($this->parent->syntax === NULL)
        {
            return;
        }
        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;
            // 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)
            {
                $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 updateSyntaxPHP():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 (strpos($char, '//') !== FALSE || strpos($char, '#') !== FALSE)
                {
                    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;
                }
            }
            // Highlight specific tokens
            if ($token['typeName'] !== 'RAW')
            {
                if (array_key_exists($token['type'], $this->phpTokenHighlightMap))
                {
                    $hl = $this->phpTokenHighlightMap[$token['type']];
                    array_replace_range($this->hl, $charStart, $charLen, $hl);
                    $offset = $charEnd;
                    continue;
                }
                // Types/identifiers/keywords that don't have their own token
                if ($token['type'] === T_STRING &&
                    in_array($token['char'], $this->parent->syntax->keywords2, TRUE))
                {
                    array_replace_range($this->hl, $charStart, $charLen, Highlight::KEYWORD2);
                    $offset = $charEnd;
                    continue;
                }
            }
            // Highlight raw characters
            if (($token['type'] === self::T_RAW) && array_key_exists(trim($token['char']), $this->phpCharacterHighlightMap))
            {
                $hl = $this->phpCharacterHighlightMap[trim($token['char'])];
                array_replace_range($this->hl, $charStart, $charLen, $hl);
                $offset = $charEnd;
                continue;
            }
        }
        $changed = $this->hlOpenComment !== $inComment;
        $this->hlOpenComment = $inComment;
        if ($changed && $this->idx + 1 < $this->parent->numRows)
        {
            // @codeCoverageIgnoreStart
            $this->parent->rows[$this->idx + 1]->updateSyntax();
            // @codeCoverageIgnoreEnd
        }
    }
}