fileType = FileType::from($this->filename); } public function __get(string $name): ?int { if ($name === 'numRows') { return count($this->rows); } return NULL; } public static function new(): self { return new self(); } public function row(int $index): Row { return (array_key_exists($index, $this->rows)) ? $this->rows[$index] : Row::default(); } public function isEmpty(): bool { return empty($this->rows); } // ------------------------------------------------------------------------ // ! File I/O // ------------------------------------------------------------------------ public function open(string $filename): ?self { $handle = fopen($filename, 'rb'); if ($handle === FALSE) { return NULL; } $this->__construct($filename); while (($line = fgets($handle)) !== FALSE) { // Remove line endings when reading the file $this->rows[] = Row::new($this, rtrim($line), $this->numRows); } fclose($handle); $this->dirty = false; $this->selectSyntaxHighlight(); return $this; } public function save(): int|false { $contents = $this->rowsToString(); $res = file_put_contents($this->filename, $contents); if ($res === strlen($contents)) { $this->dirty = FALSE; } return $res; } public function insert(Point $at, string $c): void { if ($at->y > $this->numRows) { return; } if ($c === KeyType::ENTER || $c === RawKeyCode::CARRIAGE_RETURN) { $this->insertNewline($at); $this->dirty = true; return; } $this->rows[$at->y]->insert($at->x, $c); $this->dirty = true; } public function delete(Point $at): void { if ($at->y > $this->numRows) { return; } $row =& $this->rows[$at->y]; if ($at->x === $this->rows[$at->y]->size && $at->y + 1 < $this->numRows) { $this->rows[$at->y]->append($this->rows[$at->y + 1]->chars); $this->deleteRow($at->y + 1); } else { $row->delete($at->x); } $this->dirty = true; } public function insertRow(int $at, string $s, bool $updateSyntax = TRUE): void { if ($at > $this->numRows) { return; } $row = Row::new($this, $s, $at); if ($at === $this->numRows) { $this->rows[] = $row; } else { $this->rows = [ ...array_slice($this->rows, 0, $at), $row, ...array_slice($this->rows, $at), ]; // Update indexes of each row so that correct highlighting is done for ($idx = $at; $idx < $this->numRows; $idx++) { $this->rows[$idx]->idx = $idx; } } ksort($this->rows); // $this->rows[$at]->highlight(); // Re-tokenize the file if ($updateSyntax) { $this->refreshPHPSyntax(); } $this->dirty = true; } protected function deleteRow(int $at): void { if ($at < 0 || $at >= $this->numRows) { return; } // Remove the row unset($this->rows[$at]); // Re-index the array of rows $this->rows = array_values($this->rows); for ($i = $at; $i < $this->numRows; $i++) { $this->rows[$i]->idx = $i; } // Re-tokenize the file $this->refreshPHPSyntax(); $this->dirty = true; } public function isDirty(): bool { return $this->dirty; } protected function insertNewline(Point $at): void { if ($at->y > $this->numRows) { return; } if ($at->y === $this->numRows) { $this->insertRow($this->numRows, ''); } else if ($at->x === 1) { $this->insertRow($at->y, ''); } else { $row = $this->rows[$at->y]; $chars = $row->chars; $newChars = substr($chars, 0, $at->x); // Truncate the previous row $row->setChars($newChars); // Add a new row with the contents of the previous row at the point of the split $this->insertRow($at->y + 1, substr($chars, $at->x)); } $this->dirty = true; } protected function selectSyntaxHighlight(): void { if (empty($this->filename)) { return; } if ($this->fileType->name === 'PHP') { $this->tokens = PHP8::getFileTokens($this->filename); } $this->refreshSyntax(); } protected function rowsToString(): string { $lines = array_map(fn (Row $row) => (string)$row, $this->rows); return implode('', $lines); } public function refreshSyntax(): void { // Update the syntax highlighting for all the rows of the file array_walk($this->rows, static fn (Row $row) => $row->update()); } private function refreshPHPSyntax(): void { if ($this->fileType->name !== 'PHP') { return; } $this->tokens = PHP8::getTokens($this->rowsToString()); $this->refreshSyntax(); } }