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 } } }