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, ]; 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() { return [ 'size' => $this->size, 'rsize' => $this->rsize, 'chars' => $this->chars, 'render' => $this->render, 'hlOpenComment' => $this->hlOpenComment, ]; } 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++; } 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 ( ! isset($this->parent->syntax)) { 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; if ($this->parent->syntax === NULL) { return; } // 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) ? "\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); } $prevSep = is_separator($char); $i++; } $changed = $this->hlOpenComment !== $inComment; $this->hlOpenComment = $inComment; if ($changed && $this->idx + 1 < $this->parent->numRows) { $this->parent->rows[$this->idx + 1]->updateSyntax(); } } protected function updateSyntaxPHP():void { $rowNum = $this->idx + 1; $hasTokens = isset($this->parent->syntax->tokens); $hasRow = $hasTokens && array_key_exists($rowNum, $this->parent->syntax->tokens); if ( ! ( $hasTokens && $hasRow && $this->idx < $this->parent->numRows )) { return; } $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; 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 (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) { if (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['typeName'] === '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) { $this->parent->rows[$this->idx + 1]->updateSyntax(); } } }