Fix arrow key movement and use Position class for cursor and offset

This commit is contained in:
Timothy Warren 2021-03-09 12:46:30 -05:00
parent e4ffe8eb98
commit fdd90e289e
3 changed files with 141 additions and 120 deletions

View File

@ -12,14 +12,34 @@ use Aviat\Kilo\Tokens\PHP8;
class Editor { class Editor {
use Traits\MagicProperties; use Traits\MagicProperties;
/**
* @var string The screen buffer
*/
private string $outputBuffer = ''; private string $outputBuffer = '';
protected int $cursorX = 0; /**
protected int $cursorY = 0; * @var Position The 0-based location of the cursor in the current viewport
*/
protected Position $cursor;
/**
* @var Position The scroll offset of the file in the current viewport
*/
protected Position $offset;
/**
* @var int The rendered cursor position
*/
protected int $renderX = 0; protected int $renderX = 0;
protected int $rowOffset = 0;
protected int $colOffset = 0; /**
* @var int The size of the current terminal in rows
*/
protected int $screenRows = 0; protected int $screenRows = 0;
/**
* @var int The size of the current terminal in columns
*/
protected int $screenCols = 0; protected int $screenCols = 0;
/** /**
@ -45,6 +65,8 @@ class Editor {
private function __construct() private function __construct()
{ {
$this->statusMsgTime = time(); $this->statusMsgTime = time();
$this->cursor = Position::default();
$this->offset = Position::default();
[$this->screenRows, $this->screenCols] = Terminal::getWindowSize(); [$this->screenRows, $this->screenCols] = Terminal::getWindowSize();
@ -65,13 +87,11 @@ class Editor {
public function __debugInfo(): array public function __debugInfo(): array
{ {
return [ return [
'colOffset' => $this->colOffset, 'cursor' => $this->cursor,
'cursorX' => $this->cursorX, 'offset' => $this->offset,
'cursorY' => $this->cursorY,
'dirty' => $this->dirty, 'dirty' => $this->dirty,
'filename' => $this->filename, 'filename' => $this->filename,
'renderX' => $this->renderX, 'renderX' => $this->renderX,
'rowOffset' => $this->rowOffset,
'rows' => $this->rows, 'rows' => $this->rows,
'screenCols' => $this->screenCols, 'screenCols' => $this->screenCols,
'screenRows' => $this->screenRows, 'screenRows' => $this->screenRows,
@ -257,41 +277,41 @@ class Editor {
protected function insertChar(string $c): void protected function insertChar(string $c): void
{ {
if ($this->cursorY === $this->numRows) if ($this->cursor->y === $this->numRows)
{ {
$this->insertRow($this->numRows, ''); $this->insertRow($this->numRows, '');
} }
$this->rows[$this->cursorY]->insertChar($this->cursorX, $c); $this->rows[$this->cursor->y]->insertChar($this->cursor->x, $c);
// Re-tokenize the file // Re-tokenize the file
$this->refreshPHPSyntax(); $this->refreshPHPSyntax();
$this->cursorX++; $this->cursor->x++;
} }
protected function insertNewline(): void protected function insertNewline(): void
{ {
// @TODO attempt smart indentation on newline? // @TODO attempt smart indentation on newline?
if ($this->cursorX === 0) if ($this->cursor->x === 0)
{ {
$this->insertRow($this->cursorY, ''); $this->insertRow($this->cursor->y, '');
} }
else else
{ {
$row = $this->rows[$this->cursorY]; $row = $this->rows[$this->cursor->y];
$chars = $row->chars; $chars = $row->chars;
$newChars = substr($chars, 0, $this->cursorX); $newChars = substr($chars, 0, $this->cursor->x);
// Truncate the previous row // Truncate the previous row
$row->chars = $newChars; $row->chars = $newChars;
// Add a new row, with the contents from the cursor to the end of the line // 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->insertRow($this->cursor->y + 1, substr($chars, $this->cursor->x));
} }
$this->cursorY++; $this->cursor->y++;
$this->cursorX = 0; $this->cursor->x = 0;
// Re-tokenize the file // Re-tokenize the file
$this->refreshPHPSyntax(); $this->refreshPHPSyntax();
@ -299,23 +319,23 @@ class Editor {
protected function deleteChar(): void protected function deleteChar(): void
{ {
if ($this->cursorY === $this->numRows || ($this->cursorX === 0 && $this->cursorY === 0)) if ($this->cursor->y === $this->numRows || ($this->cursor->x === 0 && $this->cursor->y === 0))
{ {
return; return;
} }
$row = $this->rows[$this->cursorY]; $row = $this->rows[$this->cursor->y];
if ($this->cursorX > 0) if ($this->cursor->x > 0)
{ {
$row->deleteChar($this->cursorX - 1); $row->deleteChar($this->cursor->x - 1);
$this->cursorX--; $this->cursor->x--;
} }
else else
{ {
$this->cursorX = $this->rows[$this->cursorY - 1]->size; $this->cursor->x = $this->rows[$this->cursor->y - 1]->size;
$this->rows[$this->cursorY -1]->appendString($row->chars); $this->rows[$this->cursor->y -1]->appendString($row->chars);
$this->deleteRow($this->cursorY); $this->deleteRow($this->cursor->y);
$this->cursorY--; $this->cursor->y--;
} }
// Re-tokenize the file // Re-tokenize the file
@ -457,9 +477,9 @@ class Editor {
if ($match !== FALSE) if ($match !== FALSE)
{ {
$lastMatch = $current; $lastMatch = $current;
$this->cursorY = (int)$current; $this->cursor->y = (int)$current;
$this->cursorX = $this->rowRxToCx($row, $match); $this->cursor->x = $this->rowRxToCx($row, $match);
$this->rowOffset = $this->numRows; $this->offset->y = $this->numRows;
$savedHlLine = $current; $savedHlLine = $current;
$savedHl = $row->hl; $savedHl = $row->hl;
@ -473,10 +493,10 @@ class Editor {
protected function find(): void protected function find(): void
{ {
$savedCx = $this->cursorX; $savedCx = $this->cursor->x;
$savedCy = $this->cursorY; $savedCy = $this->cursor->y;
$savedColOff = $this->colOffset; $savedColOff = $this->offset->x;
$savedRowOff = $this->rowOffset; $savedRowOff = $this->offset->y;
$query = $this->prompt('Search: %s (Use ESC/Arrows/Enter)', [$this, 'findCallback']); $query = $this->prompt('Search: %s (Use ESC/Arrows/Enter)', [$this, 'findCallback']);
@ -484,10 +504,10 @@ class Editor {
// restore original cursor and scroll locations // restore original cursor and scroll locations
if ($query === '') if ($query === '')
{ {
$this->cursorX = $savedCx; $this->cursor->x = $savedCx;
$this->cursorY = $savedCy; $this->cursor->y = $savedCy;
$this->colOffset = $savedColOff; $this->offset->x = $savedColOff;
$this->rowOffset = $savedRowOff; $this->offset->y = $savedRowOff;
} }
} }
@ -498,29 +518,29 @@ class Editor {
protected function scroll(): void protected function scroll(): void
{ {
$this->renderX = 0; $this->renderX = 0;
if ($this->cursorY < $this->numRows) if ($this->cursor->y < $this->numRows)
{ {
$this->renderX = $this->rowCxToRx($this->rows[$this->cursorY], $this->cursorX); $this->renderX = $this->rowCxToRx($this->rows[$this->cursor->y], $this->cursor->x);
} }
// Vertical Scrolling // Vertical Scrolling
if ($this->cursorY < $this->rowOffset) if ($this->cursor->y < $this->offset->y)
{ {
$this->rowOffset = $this->cursorY; $this->offset->y = $this->cursor->y;
} }
if ($this->cursorY >= ($this->rowOffset + $this->screenRows)) else if ($this->cursor->y >= ($this->offset->y + $this->screenRows))
{ {
$this->rowOffset = $this->cursorY - $this->screenRows + 1; $this->offset->y = $this->cursor->y - $this->screenRows + 1;
} }
// Horizontal Scrolling // Horizontal Scrolling
if ($this->renderX < $this->colOffset) if ($this->renderX < $this->offset->x)
{ {
$this->colOffset = $this->renderX; $this->offset->x = $this->renderX;
} }
if ($this->renderX >= ($this->colOffset + $this->screenCols)) else if ($this->renderX >= ($this->offset->x + $this->screenCols))
{ {
$this->colOffset = $this->renderX - $this->screenCols + 1; $this->offset->x = $this->renderX - $this->screenCols + 1;
} }
} }
@ -528,7 +548,7 @@ class Editor {
{ {
for ($y = 0; $y < $this->screenRows; $y++) for ($y = 0; $y < $this->screenRows; $y++)
{ {
$filerow = $y + $this->rowOffset; $filerow = $y + $this->offset->y;
($filerow >= $this->numRows) ($filerow >= $this->numRows)
? $this->drawPlaceholderRow($y) ? $this->drawPlaceholderRow($y)
@ -541,7 +561,7 @@ class Editor {
protected function drawRow(int $rowIdx): void protected function drawRow(int $rowIdx): void
{ {
$len = $this->rows[$rowIdx]->rsize - $this->colOffset; $len = $this->rows[$rowIdx]->rsize - $this->offset->x;
if ($len < 0) if ($len < 0)
{ {
$len = 0; $len = 0;
@ -551,8 +571,8 @@ class Editor {
$len = $this->screenCols; $len = $this->screenCols;
} }
$chars = substr($this->rows[$rowIdx]->render, $this->colOffset, (int)$len); $chars = substr($this->rows[$rowIdx]->render, $this->offset->x, (int)$len);
$hl = array_slice($this->rows[$rowIdx]->hl, $this->colOffset, (int)$len); $hl = array_slice($this->rows[$rowIdx]->hl, $this->offset->x, (int)$len);
$currentColor = -1; $currentColor = -1;
@ -639,7 +659,7 @@ class Editor {
$syntaxType = ($this->syntax !== NULL) ? $this->syntax->filetype : 'no ft'; $syntaxType = ($this->syntax !== NULL) ? $this->syntax->filetype : 'no ft';
$isDirty = ($this->dirty > 0) ? '(modified)' : ''; $isDirty = ($this->dirty > 0) ? '(modified)' : '';
$status = sprintf('%.20s - %d lines %s', $statusFilename, $this->numRows, $isDirty); $status = sprintf('%.20s - %d lines %s', $statusFilename, $this->numRows, $isDirty);
$rstatus = sprintf('%s | %d/%d', $syntaxType, $this->cursorY + 1, $this->numRows); $rstatus = sprintf('%s | %d/%d', $syntaxType, $this->cursor->y + 1, $this->numRows);
$len = strlen($status); $len = strlen($status);
$rlen = strlen($rstatus); $rlen = strlen($rstatus);
if ($len > $this->screenCols) if ($len > $this->screenCols)
@ -692,8 +712,8 @@ class Editor {
// Specify the current cursor position // Specify the current cursor position
$this->outputBuffer .= ANSI::moveCursor( $this->outputBuffer .= ANSI::moveCursor(
$this->cursorY - $this->rowOffset, $this->cursor->y - $this->offset->y,
$this->renderX - $this->colOffset $this->renderX - $this->offset->x
); );
$this->outputBuffer .= ANSI::SHOW_CURSOR; $this->outputBuffer .= ANSI::SHOW_CURSOR;
@ -753,58 +773,78 @@ class Editor {
protected function moveCursor(string $key): void protected function moveCursor(string $key): void
{ {
$row = ($this->cursorY >= $this->numRows) $row = $this->rows[$this->cursor->y];
? NULL
: $this->rows[$this->cursorY];
switch ($key) switch ($key)
{ {
case KeyType::ARROW_LEFT: case KeyType::ARROW_LEFT:
if ($this->cursorX !== 0) if ($this->cursor->x !== 0)
{ {
$this->cursorX--; $this->cursor->x--;
} }
else if ($this->cursorY > 0) else if ($this->cursor->y > 0)
{ {
$this->cursorY--; $this->cursor->y--;
$this->cursorX = $this->rows[$this->cursorY]->size; $this->cursor->x = $this->rows[$this->cursor->y]->size;
} }
break; break;
case KeyType::ARROW_RIGHT: case KeyType::ARROW_RIGHT:
if ($row && $this->cursorX < $row->size) if ($row && $this->cursor->x < $row->size)
{ {
$this->cursorX++; $this->cursor->x++;
} }
else if ($row && $this->cursorX === $row->size) else if ($row && $this->cursor->x === $row->size)
{ {
$this->cursorY++; $this->cursor->y++;
$this->cursorX = 0; $this->cursor->x = 0;
} }
break; break;
case KeyType::ARROW_UP: case KeyType::ARROW_UP:
if ($this->cursorY !== 0) if ($this->cursor->y !== 0)
{ {
$this->cursorY--; $this->cursor->y--;
} }
break; break;
case KeyType::ARROW_DOWN: case KeyType::ARROW_DOWN:
if ($this->cursorY < $this->numRows) if ($this->cursor->y < $this->numRows)
{ {
$this->cursorY++; $this->cursor->y++;
} }
break; break;
case KeyType::PAGE_UP:
$this->cursor->y = ($this->cursor->y > $this->screenRows)
? $this->cursor->y - $this->screenRows
: 0;
break;
case KeyType::PAGE_DOWN:
$this->cursor->y = ($this->cursor->y + $this->screenRows < $this->numRows)
? $this->cursor->y + $this->screenRows
: $this->numRows;
break;
case KeyType::HOME_KEY:
$this->cursor->x = 0;
break;
case KeyType::END_KEY:
if ($this->cursor->y < $this->numRows)
{
$this->cursor->x = $this->rows[$this->cursor->y]->size - 1;
}
break;
default:
// Do nothing
} }
$row = ($this->cursorY >= $this->numRows) if ($this->cursor->x > $row->size)
? NULL
: $this->rows[$this->cursorY];
$rowlen = $row->size ?? 0;
if ($this->cursorX > $rowlen)
{ {
$this->cursorX = $rowlen; $this->cursor->x = $row->size;
} }
} }
@ -834,25 +874,12 @@ class Editor {
return ''; return '';
} }
Terminal::clear(); Terminal::clear();
return NULL;
return NULL;
break;
case KeyCode::CTRL('s'): case KeyCode::CTRL('s'):
$this->save(); $this->save();
break; 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'): case KeyCode::CTRL('f'):
$this->find(); $this->find();
break; break;
@ -866,15 +893,14 @@ class Editor {
$this->deleteChar(); $this->deleteChar();
break; break;
case KeyType::PAGE_UP:
case KeyType::PAGE_DOWN:
$this->pageUpOrDown($c);
break;
case KeyType::ARROW_UP: case KeyType::ARROW_UP:
case KeyType::ARROW_DOWN: case KeyType::ARROW_DOWN:
case KeyType::ARROW_LEFT: case KeyType::ARROW_LEFT:
case KeyType::ARROW_RIGHT: case KeyType::ARROW_RIGHT:
case KeyType::PAGE_UP:
case KeyType::PAGE_DOWN:
case KeyType::HOME_KEY:
case KeyType::END_KEY:
$this->moveCursor($c); $this->moveCursor($c);
break; break;
@ -893,28 +919,6 @@ class Editor {
return $c; 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 protected function refreshSyntax(): void
{ {
// Update the syntax highlighting for all the rows of the file // Update the syntax highlighting for all the rows of the file

17
src/Position.php Normal file
View File

@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace Aviat\Kilo;
class Position {
private function __construct(public int $x, public int $y) {}
public static function new(int $x, int $y): self
{
return new Position($x, $y);
}
public static function default(): self
{
return new Position(0, 0);
}
}

View File

@ -118,7 +118,7 @@ function is_separator(string $char): bool
* @param int $length The number of indices to update * @param int $length The number of indices to update
* @param mixed $value The value to replace in the range * @param mixed $value The value to replace in the range
*/ */
function array_replace_range(array &$array, int $offset, int $length, $value):void function array_replace_range(array &$array, int $offset, int $length, mixed $value):void
{ {
if ($length === 1) if ($length === 1)
{ {