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); $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(); } } }