Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
13.33% covered (danger)
13.33%
4 / 30
CRAP
11.38% covered (danger)
11.38%
47 / 413
Editor
0.00% covered (danger)
0.00%
0 / 1
13.33% covered (danger)
13.33%
4 / 30
18201.35
11.38% covered (danger)
11.38%
47 / 413
 new
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 __get
0.00% covered (danger)
0.00%
0 / 1
2.15
66.67% covered (warning)
66.67%
2 / 3
 __debugInfo
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
13 / 13
 readKey
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 23
 selectSyntaxHighlight
0.00% covered (danger)
0.00%
0 / 1
6.02
91.67% covered (success)
91.67%
11 / 12
 rowCxToRx
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 6
 rowRxToCx
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 8
 insertRow
0.00% covered (danger)
0.00%
0 / 1
6.97
57.14% covered (warning)
57.14%
8 / 14
 deleteRow
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 8
 insertChar
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 5
 insertNewline
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 10
 deleteChar
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 11
 rowsToString
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 open
0.00% covered (danger)
0.00%
0 / 1
3.07
80.00% covered (warning)
80.00%
8 / 10
 save
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 14
 findCallback
0.00% covered (danger)
0.00%
0 / 1
182
0.00% covered (danger)
0.00%
0 / 42
 find
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 10
 scroll
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 11
 drawRows
0.00% covered (danger)
0.00%
0 / 1
306
0.00% covered (danger)
0.00%
0 / 50
 drawStatusBar
0.00% covered (danger)
0.00%
0 / 1
56
0.00% covered (danger)
0.00%
0 / 19
 drawMessageBar
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 6
 refreshScreen
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 12
 setStatusMessage
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 prompt
0.00% covered (danger)
0.00%
0 / 1
182
0.00% covered (danger)
0.00%
0 / 18
 moveCursor
0.00% covered (danger)
0.00%
0 / 1
306
0.00% covered (danger)
0.00%
0 / 31
 processKeypress
0.00% covered (danger)
0.00%
0 / 1
600
0.00% covered (danger)
0.00%
0 / 53
 pageUpOrDown
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 9
 refreshSyntax
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 refreshPHPSyntax
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
<?php declare(strict_types=1);
namespace Aviat\Kilo;
use Aviat\Kilo\Enum\{Color, KeyCode, KeyType, Highlight};
use Aviat\Kilo\Tokens\PHP;
/**
 * // Don't highlight this!
 * @property-read int numRows
 */
class Editor {
    use Traits\MagicProperties;
    private string $ab = '';
    protected int $cursorX = 0;
    protected int $cursorY = 0;
    protected int $renderX = 0;
    protected int $rowOffset = 0;
    protected int $colOffset = 0;
    protected int $screenRows = 0;
    protected int $screenCols = 0;
    /**
     * Array of Row objects
     */
    public array $rows = [];
    public int $dirty = 0;
    public string $filename = '';
    protected string $statusMsg = '';
    protected int $statusMsgTime;
    public ?Syntax $syntax = NULL;
    // Tokens for highlighting PHP
    public array $tokens = [];
    public static function new(): Editor
    {
        return new self();
    }
    private function __construct()
    {
        $this->statusMsgTime = time();
        [$this->screenRows, $this->screenCols] = get_window_size();
        // Remove a row for the status bar, and one for the message bar
        $this->screenRows -= 2;
    }
    public function __get(string $name)
    {
        if ($name === 'numRows')
        {
            return count($this->rows);
        }
        return NULL;
    }
    public function __debugInfo(): array
    {
        return [
            'colOffset' => $this->colOffset,
            'cursorX' => $this->cursorX,
            'cursorY' => $this->cursorY,
            'dirty' => $this->dirty,
            'filename' => $this->filename,
            'renderX' => $this->renderX,
            'rowOffset' => $this->rowOffset,
            'rows' => $this->rows,
            'screenCols' => $this->screenCols,
            'screenRows' => $this->screenRows,
            'statusMsg' => $this->statusMsg,
            'syntax' => $this->syntax,
            'tokens' => $this->tokens,
        ];
    }
    // ------------------------------------------------------------------------
    // ! Terminal
    // ------------------------------------------------------------------------
    protected function readKey(): string
    {
        $c = read_stdin();
        $map = [
            // Unambiguous mappings
            KeyCode::ARROW_DOWN => KeyType::ARROW_DOWN,
            KeyCode::ARROW_LEFT => KeyType::ARROW_LEFT,
            KeyCode::ARROW_RIGHT => KeyType::ARROW_RIGHT,
            KeyCode::ARROW_UP => KeyType::ARROW_UP,
            KeyCode::DEL_KEY => KeyType::DEL_KEY,
            KeyCode::ENTER => KeyType::ENTER,
            KeyCode::ESCAPE => KeyType::ESCAPE,
            KeyCode::PAGE_DOWN => KeyType::PAGE_DOWN,
            KeyCode::PAGE_UP => KeyType::PAGE_UP,
            // Backspace
            KeyCode::CTRL('h') => KeyType::BACKSPACE,
            KeyCode::BACKSPACE => KeyType::BACKSPACE,
            // Home Key
            "\eOH" => KeyType::HOME_KEY,
            "\e[1~" => KeyType::HOME_KEY,
            "\e[7~" => KeyType::HOME_KEY,
            ANSI::RESET_CURSOR => KeyType::HOME_KEY,
            // End Key
            "\eOF" => KeyType::END_KEY,
            "\e[4~" => KeyType::END_KEY,
            "\e[8~" => KeyType::END_KEY,
            "\e[F" => KeyType::END_KEY,
        ];
        return (array_key_exists($c, $map))
            ? $map[$c]
            : $c;
    }
    protected function selectSyntaxHighlight(): void
    {
        $this->syntax = NULL;
        if (empty($this->filename))
        {
            return;
        }
        // In PHP, `strchr` and `strstr` are the same function
        $ext = (string)strstr(basename($this->filename), '.');
        foreach (get_file_syntax_map() as $syntax)
        {
            if (
                in_array($ext, $syntax->filematch, TRUE) ||
                in_array(basename($this->filename), $syntax->filematch, TRUE)
            ) {
                $this->syntax = $syntax;
                // Pre-tokenize the file
                if ($this->syntax->filetype === 'PHP')
                {
                    $this->tokens = PHP::getFileTokens($this->filename);
                }
                $this->refreshSyntax();
                return;
            }
        }
    }
    // ------------------------------------------------------------------------
    // ! Row Operations
    // ------------------------------------------------------------------------
    protected function rowCxToRx(Row $row, int $cx): int
    {
        $rx = 0;
        for ($i = 0; $i < $cx; $i++)
        {
            if ($row->chars[$i] === KeyCode::TAB)
            {
                $rx += (KILO_TAB_STOP - 1) - ($rx % KILO_TAB_STOP);
            }
            $rx++;
        }
        return $rx;
    }
    protected function rowRxToCx(Row $row, int $rx): int
    {
        $cur_rx = 0;
        for ($cx = 0; $cx < $row->size; $cx++)
        {
            if ($row->chars[$cx] === KeyCode::TAB)
            {
                $cur_rx += (KILO_TAB_STOP - 1) - ($cur_rx % KILO_TAB_STOP);
            }
            $cur_rx++;
            if ($cur_rx > $rx)
            {
                return $cx;
            }
        }
        return $cx;
    }
    protected function insertRow(int $at, string $s, bool $updateSyntax = TRUE): void
    {
        if ($at < 0 || $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),
            ];
        }
        ksort($this->rows);
        $this->rows[$at]->update();
        $this->dirty++;
        // Re-tokenize the file
        if ($updateSyntax)
        {
            $this->refreshPHPSyntax();
        }
    }
    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--;
        }
        // Re-tokenize the file
        $this->refreshPHPSyntax();
        $this->dirty++;
    }
    // ------------------------------------------------------------------------
    // ! Editor Operations
    // ------------------------------------------------------------------------
    protected function insertChar(string $c): void
    {
        if ($this->cursorY === $this->numRows)
        {
            $this->insertRow($this->numRows, '');
        }
        $this->rows[$this->cursorY]->insertChar($this->cursorX, $c);
        // Re-tokenize the file
        $this->refreshPHPSyntax();
        $this->cursorX++;
    }
    protected function insertNewline(): void
    {
        if ($this->cursorX === 0)
        {
            $this->insertRow($this->cursorY, '');
        }
        else
        {
            $row = $this->rows[$this->cursorY];
            $chars = $row->chars;
            $newChars = substr($chars, 0, $this->cursorX);
            // Truncate the previous row
            $row->chars = $newChars;
            // Add a new row, with the contents from the cursor to the end of the line
            $this->insertRow($this->cursorY + 1, substr($chars, $this->cursorX));
        }
        $this->cursorY++;
        $this->cursorX = 0;
        // Re-tokenize the file
        $this->refreshPHPSyntax();
    }
    protected function deleteChar(): void
    {
        if ($this->cursorY === $this->numRows || ($this->cursorX === 0 && $this->cursorY === 0))
        {
            return;
        }
        $row = $this->rows[$this->cursorY];
        if ($this->cursorX > 0)
        {
            $row->deleteChar($this->cursorX - 1);
            $this->cursorX--;
        }
        else
        {
            $this->cursorX = $this->rows[$this->cursorY - 1]->size;
            $this->rows[$this->cursorY -1]->appendString($row->chars);
            $this->deleteRow($this->cursorY);
            $this->cursorY--;
        }
        // Re-tokenize the file
        $this->refreshPHPSyntax();
    }
    // ------------------------------------------------------------------------
    // ! File I/O
    // ------------------------------------------------------------------------
    protected function rowsToString(): string
    {
        $lines = array_map(fn (Row $row) => (string)$row, $this->rows);
        return implode('', $lines);
    }
    public function open(string $filename): void
    {
        // Copy filename for display
        $this->filename = $filename;
        $this->selectSyntaxHighlight();
        $handle = fopen($filename, 'rb');
        if ($handle === FALSE)
        {
            $this->setStatusMessage('Failed to open file: %s', $filename);
            return;
        }
        while (($line = fgets($handle)) !== FALSE)
        {
            // Remove line endings when reading the file
            $this->insertRow($this->numRows, rtrim($line), FALSE);
        }
        fclose($handle);
        $this->dirty = 0;
    }
    protected function save(): void
    {
        if ($this->filename === '')
        {
            $newFilename = $this->prompt('Save as: %s');
            if ($newFilename === '')
            {
                $this->setStatusMessage('Save aborted');
                return;
            }
            $this->filename = $newFilename;
            $this->selectSyntaxHighlight();
        }
        $contents = $this->rowsToString();
        $res = file_put_contents($this->filename, $contents);
        if ($res === strlen($contents))
        {
            $this->setStatusMessage('%d bytes written to disk', strlen($contents));
            $this->dirty = 0;
            return;
        }
        $this->setStatusMessage('Failed to save! I/O error: %s', error_get_last()['message']);
    }
    // ------------------------------------------------------------------------
    // ! Find
    // ------------------------------------------------------------------------
    protected function findCallback(string $query, string $key): void
    {
        static $lastMatch = -1;
        static $direction = 1;
        static $savedHlLine = 0;
        static $savedHl = [];
        if ( ! empty($savedHl))
        {
            $this->rows[$savedHlLine]->hl = $savedHl;
            $savedHl = [];
        }
        switch ($key)
        {
            case KeyCode::ENTER:
            case KeyCode::ESCAPE:
                $lastMatch = -1;
                $direction = 1;
                return;
            case KeyType::ARROW_DOWN:
            case KeyType::ARROW_RIGHT:
                $direction = 1;
            break;
            case KeyType::ARROW_UP:
            case KeyType::ARROW_LEFT:
                $direction = -1;
            break;
            default:
                $lastMatch = -1;
                $direction = 1;
        }
        if ($lastMatch === -1)
        {
            $direction = 1;
        }
        $current = $lastMatch;
        for ($i = 0; $i < $this->numRows; $i++)
        {
            $current += $direction;
            if ($current === -1)
            {
                $current = $this->numRows - 1;
            }
            else if ($current === $this->numRows)
            {
                $current = 0;
            }
            $row =& $this->rows[$current];
            $match = strpos($row->render, $query);
            if ($match !== FALSE)
            {
                $lastMatch = $current;
                $this->cursorY = $current;
                $this->cursorX = $this->rowRxToCx($row, $match);
                $this->rowOffset = $this->numRows;
                $savedHlLine = $current;
                $savedHl = $row->hl;
                // Update the highlight array of the relevant row with the 'MATCH' type
                array_replace_range($row->hl, $match, strlen($query), Highlight::MATCH);
                break;
            }
        }
    }
    protected function find(): void
    {
        $savedCx = $this->cursorX;
        $savedCy = $this->cursorY;
        $savedColOff = $this->colOffset;
        $savedRowOff = $this->rowOffset;
        $query = $this->prompt('Search: %s (Use ESC/Arrows/Enter)', [$this, 'findCallback']);
        // If they pressed escape, the query will be empty,
        // restore original cursor and scroll locations
        if ($query === '')
        {
            $this->cursorX = $savedCx;
            $this->cursorY = $savedCy;
            $this->colOffset = $savedColOff;
            $this->rowOffset = $savedRowOff;
        }
    }
    // ------------------------------------------------------------------------
    // ! Output
    // ------------------------------------------------------------------------
    protected function scroll(): void
    {
        $this->renderX = 0;
        if ($this->cursorY < $this->numRows)
        {
            $this->renderX = $this->rowCxToRx($this->rows[$this->cursorY], $this->cursorX);
        }
        // Vertical Scrolling
        if ($this->cursorY < $this->rowOffset)
        {
            $this->rowOffset = $this->cursorY;
        }
        if ($this->cursorY >= ($this->rowOffset + $this->screenRows))
        {
            $this->rowOffset = $this->cursorY - $this->screenRows + 1;
        }
        // Horizontal Scrolling
        if ($this->renderX < $this->colOffset)
        {
            $this->colOffset = $this->renderX;
        }
        if ($this->renderX >= ($this->colOffset + $this->screenCols))
        {
            $this->colOffset = $this->renderX - $this->screenCols + 1;
        }
    }
    protected function drawRows(): void
    {
        for ($y = 0; $y < $this->screenRows; $y++)
        {
            $filerow = $y + $this->rowOffset;
            if ($filerow >= $this->numRows)
            {
                if ($this->numRows === 0 && $y === (int)($this->screenRows / 2))
                {
                    $welcome = sprintf('PHP Kilo editor -- version %s', KILO_VERSION);
                    $welcomelen = strlen($welcome);
                    if ($welcomelen > $this->screenCols)
                    {
                        $welcomelen = $this->screenCols;
                    }
                    $padding = ($this->screenCols - $welcomelen) / 2;
                    if ($padding > 0)
                    {
                        $this->ab .= '~';
                        $padding--;
                    }
                    for ($i = 0; $i < $padding; $i++)
                    {
                        $this->ab .= ' ';
                    }
                    $this->ab .= substr($welcome, 0, $welcomelen);
                }
                else
                {
                    $this->ab .= '~';
                }
            }
            else
            {
                $len = $this->rows[$filerow]->rsize - $this->colOffset;
                if ($len < 0)
                {
                    $len = 0;
                }
                if ($len > $this->screenCols)
                {
                    $len = $this->screenCols;
                }
                $c = substr($this->rows[$filerow]->render, $this->colOffset, $len);
                $hl = array_slice($this->rows[$filerow]->hl, $this->colOffset, $len);
                $currentColor = -1;
                for ($i = 0; $i < $len; $i++)
                {
                    // Handle 'non-printable' characters
                    if (is_ctrl($c[$i]))
                    {
                        $sym = (ord($c[$i]) <= 26)
                            ? chr(ord('@') + ord($c[$i]))
                            : '?';
                        $this->ab .= ANSI::color(Color::INVERT);
                        $this->ab .= $sym;
                        $this->ab .= ANSI::RESET_TEXT;
                        if ($currentColor !== -1)
                        {
                            $this->ab .= ANSI::color($currentColor);
                        }
                    }
                    else if ($hl[$i] === Highlight::NORMAL)
                    {
                        if ($currentColor !== -1)
                        {
                            $this->ab .= ANSI::RESET_TEXT;
                            $this->ab .= ANSI::color(Color::FG_WHITE);
                            $currentColor = -1;
                        }
                        $this->ab .= $c[$i];
                    }
                    else
                    {
                        $color = syntax_to_color($hl[$i]);
                        if ($color !== $currentColor)
                        {
                            $currentColor = $color;
                            $this->ab .= ANSI::RESET_TEXT;
                            $this->ab .= ANSI::color($color);
                        }
                        $this->ab .= $c[$i];
                    }
                }
                $this->ab .= ANSI::RESET_TEXT;
                $this->ab .= ANSI::color(Color::FG_WHITE);
            }
            $this->ab .= ANSI::CLEAR_LINE;
            $this->ab .= "\r\n";
        }
    }
    protected function drawStatusBar(): void
    {
        $this->ab .= ANSI::color(Color::INVERT);
        $statusFilename = $this->filename !== '' ? $this->filename : '[No Name]';
        $syntaxType = ($this->syntax !== NULL) ? $this->syntax->filetype : 'no ft';
        $isDirty = ($this->dirty > 0) ? '(modified)' : '';
        $status = sprintf('%.20s - %d lines %s', $statusFilename, $this->numRows, $isDirty);
        $rstatus = sprintf('%s | %d/%d', $syntaxType, $this->cursorY + 1, $this->numRows);
        $len = strlen($status);
        $rlen = strlen($rstatus);
        if ($len > $this->screenCols)
        {
            $len = $this->screenCols;
        }
        $this->ab .= substr($status, 0, $len);
        while ($len < $this->screenCols)
        {
            if ($this->screenCols - $len === $rlen)
            {
                $this->ab .= substr($rstatus, 0, $rlen);
                break;
            }
            $this->ab .= ' ';
            $len++;
        }
        $this->ab .= ANSI::RESET_TEXT;
        $this->ab .= "\r\n";
    }
    protected function drawMessageBar(): void
    {
        $this->ab .= ANSI::CLEAR_LINE;
        $len = strlen($this->statusMsg);
        if ($len > $this->screenCols)
        {
            $len = $this->screenCols;
        }
        if ($len > 0 && (time() - $this->statusMsgTime) < 5)
        {
            $this->ab .= substr($this->statusMsg, 0, $len);
        }
    }
    public function refreshScreen(): void
    {
        $this->scroll();
        $this->ab = '';
        $this->ab .= ANSI::HIDE_CURSOR;
        $this->ab .= ANSI::RESET_CURSOR;
        $this->drawRows();
        $this->drawStatusBar();
        $this->drawMessageBar();
        // Specify the current cursor position
        $this->ab .= ANSI::moveCursor(
            ($this->cursorY - $this->rowOffset) + 1,
            ($this->renderX - $this->colOffset) + 1
        );
        $this->ab .= ANSI::SHOW_CURSOR;
        echo $this->ab;
    }
    public function setStatusMessage(string $fmt, ...$args): void
    {
        $this->statusMsg = (count($args) > 0)
            ? sprintf($fmt, ...$args)
            : $fmt;
        $this->statusMsgTime = time();
    }
    // ------------------------------------------------------------------------
    // ! Input
    // ------------------------------------------------------------------------
    protected function prompt(string $prompt, ?callable $callback = NULL): string
    {
        $buffer = '';
        $modifiers = KeyType::getConstList();
        while (TRUE)
        {
            $this->setStatusMessage($prompt, $buffer);
            $this->refreshScreen();
            $c = $this->readKey();
            $isModifier = in_array($c, $modifiers, TRUE);
            if ($c === KeyType::ESCAPE || ($c === KeyType::ENTER && $buffer !== ''))
            {
                $this->setStatusMessage('');
                if ($callback !== NULL)
                {
                    $callback($buffer, $c);
                }
                return '';
            }
            if ($c === KeyType::DEL_KEY || $c === KeyType::BACKSPACE)
            {
                $buffer = substr($buffer, 0, -1);
            }
            else if (is_ascii($c) && ( ! (is_ctrl($c) || $isModifier)))
            {
                $buffer .= $c;
            }
            if ($callback !== NULL)
            {
                $callback($buffer, $c);
            }
        }
    }
    protected function moveCursor(string $key): void
    {
        $row = ($this->cursorY >= $this->numRows)
            ? NULL
            : $this->rows[$this->cursorY];
        switch ($key)
        {
            case KeyType::ARROW_LEFT:
                if ($this->cursorX !== 0)
                {
                    $this->cursorX--;
                }
                else if ($this->cursorX > 0)
                {
                    $this->cursorY--;
                    $this->cursorX = $this->rows[$this->cursorY]->size;
                }
            break;
            case KeyType::ARROW_RIGHT:
                if ($row && $this->cursorX < $row->size)
                {
                    $this->cursorX++;
                }
                else if ($row && $this->cursorX === $row->size)
                {
                    $this->cursorY++;
                    $this->cursorX = 0;
                }
            break;
            case KeyType::ARROW_UP:
                if ($this->cursorY !== 0)
                {
                    $this->cursorY--;
                }
            break;
            case KeyType::ARROW_DOWN:
                if ($this->cursorY < $this->numRows)
                {
                    $this->cursorY++;
                }
            break;
        }
        $row = ($this->cursorY >= $this->numRows)
            ? NULL
            : $this->rows[$this->cursorY];
        $rowlen = $row ? $row->size : 0;
        if ($this->cursorX > $rowlen)
        {
            $this->cursorX = $rowlen;
        }
    }
    public function processKeypress(): ?string
    {
        static $quit_times = KILO_QUIT_TIMES;
        $c = $this->readKey();
        if ($c === KeyCode::NULL || $c === KeyCode::EMPTY)
        {
            return '';
        }
        switch ($c)
        {
            case KeyType::ENTER:
                $this->insertNewline();
            break;
            case KeyCode::CTRL('q'):
                if ($this->dirty > 0 && $quit_times > 0)
                {
                    $this->setStatusMessage('WARNING!!! File has unsaved changes.' .
                        'Press Ctrl-Q %d more times to quit.', $quit_times);
                    $quit_times--;
                    return '';
                }
                write_stdout(ANSI::CLEAR_SCREEN);
                write_stdout(ANSI::RESET_CURSOR);
                return NULL;
            break;
            case KeyCode::CTRL('s'):
                $this->save();
            break;
            case KeyType::HOME_KEY:
                $this->cursorX = 0;
            break;
            case KeyType::END_KEY:
                if ($this->cursorY < $this->numRows)
                {
                    $this->cursorX = $this->rows[$this->cursorY]->size - 1;
                }
            break;
            case KeyCode::CTRL('f'):
                $this->find();
            break;
            case KeyType::BACKSPACE:
            case KeyType::DEL_KEY:
                if ($c === KeyType::DEL_KEY)
                {
                    $this->moveCursor(KeyType::ARROW_RIGHT);
                }
                $this->deleteChar();
            break;
            case KeyType::PAGE_UP:
            case KeyType::PAGE_DOWN:
                $this->pageUpOrDown($c);
            break;
            case KeyType::ARROW_UP:
            case KeyType::ARROW_DOWN:
            case KeyType::ARROW_LEFT:
            case KeyType::ARROW_RIGHT:
                $this->moveCursor($c);
            break;
            case KeyCode::CTRL('l'):
            case KeyType::ESCAPE:
                // Do nothing
            break;
            default:
                $this->insertChar($c);
            break;
        }
        $quit_times = KILO_QUIT_TIMES;
        return $c;
    }
    public function pageUpOrDown(string $c): void
    {
        if ($c === KeyType::PAGE_UP)
        {
            $this->cursorY = $this->rowOffset;
        }
        else if ($c === KeyType::PAGE_DOWN)
        {
            $this->cursorY = $this->rowOffset + $this->screenRows - 1;
            if ($this->cursorY > $this->numRows)
            {
                $this->cursorY = $this->numRows;
            }
        }
        $times = $this->screenRows;
        for (; $times > 0; $times--)
        {
            $this->moveCursor($c === KeyType::PAGE_UP ? KeyType::ARROW_UP : KeyType::ARROW_DOWN);
        }
    }
    protected function refreshSyntax(): void
    {
        // Update the syntax highlighting for all the rows of the file
        array_walk($this->rows, fn (Row $row) => $row->updateSyntax());
    }
    private function refreshPHPSyntax(): void
    {
        if ($this->syntax->filetype !== 'PHP')
        {
            return;
        }
        $this->tokens = PHP::getTokens($this->rowsToString());
        $this->refreshSyntax();
    }
}