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); default: return NULL; } } public function __toString(): string { return $this->chars . "\n"; } 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 { $idx = 0; for ($i = 0; $i < $this->size; $i++) { if ($this->chars[$i] === "\t") { $this->render[$idx++] = ' '; while ($idx % KILO_TAB_STOP !== 0) { $this->render[$idx++] = ' '; } } else { $this->render[$idx++] = $this->chars[$i]; } } $this->updateSyntax(); } // ------------------------------------------------------------------------ // ! Syntax Highlighting // ------------------------------------------------------------------------ protected function updateSyntax(): void { $this->hl = array_fill(0, $this->rsize, Highlight::NORMAL); 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) { foreach ($keywords1 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, Highlight::KEYWORD1); $i += $klen - 1; break; } } foreach ($keywords2 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, Highlight::KEYWORD2); $i += $klen - 1; break; } } } $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]->update(); } } protected function updateSyntaxPHP():void { $tokens = $this->parent->syntax->tokens[$this->idx + 1]; $inComment = ($this->idx > 0 && $this->parent->rows[$this->idx - 1]->hlOpenComment); // The line is probably just empty if ($tokens === NULL) { return; } // 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) { $char = $token['char']; $charLen = strlen($char); $charStart = strpos($this->render, $char, $offset); $charEnd = $charStart + $charLen; switch ($token['type']) { case T_LNUMBER: array_replace_range($this->hl, $charStart, $charLen, Highlight::NUMBER); $offset = $charEnd; continue 2; case T_CONSTANT_ENCAPSED_STRING: array_replace_range($this->hl, $charStart, $charLen, Highlight::STRING); $offset = $charEnd; continue 2; // Operators case T_AND_EQUAL: case T_BOOLEAN_AND: case T_BOOLEAN_OR: case T_COALESCE: case T_CONCAT_EQUAL: case T_DIV_EQUAL: case T_DOUBLE_ARROW: case T_DOUBLE_COLON: case T_ELLIPSIS: case T_INC: case T_IS_EQUAL: case T_IS_GREATER_OR_EQUAL: case T_IS_IDENTICAL: case T_IS_NOT_EQUAL: case T_IS_NOT_IDENTICAL: case T_IS_SMALLER_OR_EQUAL: case T_SPACESHIP: case T_LOGICAL_AND: case T_LOGICAL_OR: case T_LOGICAL_XOR: case T_MINUS_EQUAL: case T_MOD_EQUAL: case T_MUL_EQUAL: case T_OBJECT_OPERATOR: case T_OR_EQUAL: case T_PAAMAYIM_NEKUDOTAYIM: case T_PLUS_EQUAL: case T_POW: case T_POW_EQUAL: case T_SL: case T_SL_EQUAL: case T_SR: case T_SR_EQUAL: case T_XOR_EQUAL: array_replace_range($this->hl, $charStart, $charLen, Highlight::OPERATOR); $offset = $charEnd; continue 2; case T_VARIABLE: array_replace_range($this->hl, $charStart, $charLen, Highlight::VARIABLE); $offset = $charEnd; continue 2; case T_DOC_COMMENT: // TODO break; // Keywords1 case T_ABSTRACT: case T_AS: case T_BREAK: case T_CASE: case T_DO: array_replace_range($this->hl, $charStart, $charLen, Highlight::KEYWORD1); // $keyword = $this->getKeywordFromToken($token['type']); $offset = $charEnd; continue 2; break; // Keywords 2 } } } private function getKeywordFromToken(int $token): ?string { $map = [ T_ABSTRACT => 'abstract', T_AS => 'as', T_BREAK => 'break', T_CASE => 'case', T_DO => 'do', ]; return $map[$token] ?? NULL; } }