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->highlight(); } } 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, ]; } /** * Is this row a valid part of a document? * * @return bool */ public function isValid(): bool { return ! $this->parent->isEmpty(); } public function insert(int $at, string $c): void { if ($at < 0 || $at > $this->size) { $this->append($c); return; } // Safely insert into arbitrary position in the existing string $this->chars = substr($this->chars, 0, $at) . $c . substr($this->chars, $at); $this->highlight(); $this->parent->dirty = true; } public function append(string $s): void { $this->chars .= $s; $this->highlight(); $this->parent->dirty = true; } public function delete(int $at): void { if ($at < 0 || $at >= $this->size) { return; } $this->chars = substr_replace($this->chars, '', $at, 1); $this->highlight(); $this->parent->dirty = true; } // ------------------------------------------------------------------------ // ! Syntax Highlighting // ------------------------------------------------------------------------ public function highlight(): void { $this->render = tabs_to_spaces($this->chars); $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) ) { continue; } $i++; } $changed = $this->hlOpenComment !== $inComment; $this->hlOpenComment = $inComment; if ($changed && $this->idx + 1 < $this->parent->numRows) { $this->parent->rows[$this->idx + 1]->highlight(); } } protected function highlightNumber(int &$i, Syntax $opts): bool { $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; } 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 { $char = $this->render[$i]; $nextChar = $this->render[$i + 1]; if ($opts->characters() && $char === "'") { $offset = ($nextChar === '\\') ? $i + 2 : $i + 1; $closingIndex = strpos($this->render, "'", $offset); if ($closingIndex === false) { return false; } $closingChar = $this->render[$closingIndex]; if ($closingChar === "'") { array_replace_range($this->hl, $i, $closingIndex - $i + 1, Highlight::CHARACTER); $i = $closingIndex + 1; return true; } } return false; } protected function highlightComment(int &$i, Syntax $opts): bool { if ( ! $opts->comments()) { return false; } $scs = $opts->singleLineCommentStart; $scsLen = strlen($scs); if ($scsLen > 0 && substr($this->render, $i, $scsLen) === $scs) { array_replace_range($this->hl, $i, $this->rsize - $i, Highlight::COMMENT); $i = $this->rsize; return true; } return false; } protected function highlightMultilineComments(int &$i, Syntax $opts): bool { if ( ! $opts->comments()) { 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 { $char = $this->render[$i]; // If there's a separate character type, highlight that separately if ($opts->hasChar() && $char === "'") { return false; } if ($opts->strings() && $char === '"' || $char === '\'') { $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); if ( ! ( $hasRowTokens && $this->idx < $this->parent->numRows )) { return; } $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; } // 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) { continue; } $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) { $this->parent->rows[$this->idx + 1]->highlight(); } } }