
| Current Path : /var/www/html/strat/vendor/psy/psysh/src/Readline/Interactive/Input/ |
Linux ift1.ift-informatik.de 5.4.0-216-generic #236-Ubuntu SMP Fri Apr 11 19:53:21 UTC 2025 x86_64 |
| Current File : /var/www/html/strat/vendor/psy/psysh/src/Readline/Interactive/Input/Buffer.php |
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2026 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\Readline\Interactive\Input;
use Psy\Readline\Interactive\Helper\BracketPair;
/**
* Manages the text buffer (the text being edited).
*
* Includes PHP-awareness through PHP-Parser integration for understanding
* syntax, detecting complete statements, and providing context-aware features.
*/
class Buffer
{
private string $text = '';
private int $cursor = 0;
private ParseSnapshotCache $parseSnapshotCache;
private StatementCompletenessPolicy $statementCompletenessPolicy;
private IndentationPolicy $indentationPolicy;
private TokenNavigationPolicy $tokenNavigationPolicy;
private WordNavigationPolicy $wordNavigationPolicy;
private VisualNavigationPolicy $visualNavigationPolicy;
private bool $requireSemicolons = false;
private bool $graphemeCacheInitialized = false;
/** @var int[]|null */
private ?array $graphemeBoundaries = null;
/** @var array<int,int>|null */
private ?array $graphemeBoundaryMap = null;
public function __construct(bool $requireSemicolons = false)
{
$this->requireSemicolons = $requireSemicolons;
$this->parseSnapshotCache = new ParseSnapshotCache();
$this->statementCompletenessPolicy = new StatementCompletenessPolicy(
$this->parseSnapshotCache,
$this->requireSemicolons
);
$this->indentationPolicy = new IndentationPolicy();
$this->tokenNavigationPolicy = new TokenNavigationPolicy();
$this->wordNavigationPolicy = new WordNavigationPolicy();
$this->visualNavigationPolicy = new VisualNavigationPolicy();
}
/**
* Get the buffer text.
*/
public function getText(): string
{
return $this->text;
}
/**
* Set the buffer text and move cursor to end.
*/
public function setText(string $text): void
{
$this->text = $text;
$this->cursor = \mb_strlen($text);
$this->invalidateCaches();
}
/**
* Clear the buffer.
*/
public function clear(): void
{
$this->text = '';
$this->cursor = 0;
$this->invalidateCaches();
}
/**
* Get the cursor position.
*/
public function getCursor(): int
{
return $this->cursor;
}
/**
* Set the cursor position.
*/
public function setCursor(int $position): void
{
$this->cursor = \max(0, \min($position, \mb_strlen($this->text)));
}
/**
* Get the length of the text in characters.
*/
public function getLength(): int
{
return \mb_strlen($this->text);
}
/**
* Insert text at the cursor position.
*/
public function insert(string $text): void
{
$before = \mb_substr($this->text, 0, $this->cursor);
$after = \mb_substr($this->text, $this->cursor);
$this->text = $before.$text.$after;
$this->cursor += \mb_strlen($text);
$this->invalidateCaches();
}
/**
* Auto-dedent if typing a closing bracket at start of line.
*
* If the line currently starts with auto-inserted whitespace and
* we're about to insert a closing bracket, remove one indent level.
*
* @param string $char The character being typed
* @param string $context Optional accumulated code context for bracket matching
*/
public function autoDedentIfClosingBracket(string $char, string $context = ''): void
{
$spacesToRemove = $this->indentationPolicy->calculateClosingBracketDedent(
$char, $this->text, $this->cursor, $context
);
if ($spacesToRemove > 0) {
$lines = \explode("\n", $this->text);
$currentLineNum = $this->getCurrentLineNumber();
$lines[$currentLineNum] = \substr($lines[$currentLineNum], $spacesToRemove);
$this->text = \implode("\n", $lines);
$this->cursor -= $spacesToRemove;
$this->invalidateCaches();
}
}
/**
* Delete grapheme clusters backward from cursor.
*
* @return bool True if characters were deleted
*/
public function deleteBackward(int $count = 1): bool
{
if ($this->cursor === 0 || $count <= 0) {
return false;
}
$span = $this->graphemeClusterSpanReverse($count);
$before = \mb_substr($this->text, 0, $this->cursor - $span);
$after = \mb_substr($this->text, $this->cursor);
$this->text = $before.$after;
$this->cursor -= $span;
$this->invalidateCaches();
return true;
}
/**
* Delete grapheme clusters forward from cursor.
*
* @return bool True if characters were deleted
*/
public function deleteForward(int $count = 1): bool
{
if ($this->cursor >= \mb_strlen($this->text) || $count <= 0) {
return false;
}
$span = $this->graphemeClusterSpanForward($count);
$before = \mb_substr($this->text, 0, $this->cursor);
$after = \mb_substr($this->text, $this->cursor + $span);
$this->text = $before.$after;
$this->invalidateCaches();
return true;
}
/**
* Delete from cursor to end of line.
*
* @return string The deleted text
*/
public function deleteToEnd(): string
{
$nextNewline = \mb_strpos($this->text, "\n", $this->cursor);
$end = $nextNewline === false ? \mb_strlen($this->text) : $nextNewline;
$killed = \mb_substr($this->text, $this->cursor, $end - $this->cursor);
$this->text = \mb_substr($this->text, 0, $this->cursor).\mb_substr($this->text, $end);
$this->invalidateCaches();
return $killed;
}
/**
* Delete from start of line to cursor.
*
* @return string The deleted text
*/
public function deleteToStart(): string
{
$beforeCursor = \mb_substr($this->text, 0, $this->cursor);
$lastNewline = \mb_strrpos($beforeCursor, "\n");
$start = $lastNewline === false ? 0 : $lastNewline + 1;
$killed = \mb_substr($this->text, $start, $this->cursor - $start);
$this->text = \mb_substr($this->text, 0, $start).\mb_substr($this->text, $this->cursor);
$this->cursor = $start;
$this->invalidateCaches();
return $killed;
}
/**
* Move cursor left by specified number of grapheme clusters.
*
* @return int Number of code points actually moved
*/
public function moveCursorLeft(int $count = 1): int
{
$oldCursor = $this->cursor;
$span = $this->graphemeClusterSpanReverse($count);
$this->cursor = \max(0, $this->cursor - $span);
return $oldCursor - $this->cursor;
}
/**
* Move cursor right by specified number of grapheme clusters.
*
* @return int Number of code points actually moved
*/
public function moveCursorRight(int $count = 1): int
{
$oldCursor = $this->cursor;
$span = $this->graphemeClusterSpanForward($count);
$this->cursor = \min(\mb_strlen($this->text), $this->cursor + $span);
return $this->cursor - $oldCursor;
}
/**
* Get the grapheme cluster before the cursor.
*
* @return string|null The grapheme cluster, or null if at start
*/
public function getCharBeforeCursor(): ?string
{
if ($this->cursor === 0) {
return null;
}
$span = $this->graphemeClusterSpanReverse(1);
return \mb_substr($this->text, $this->cursor - $span, $span);
}
/**
* Get the grapheme cluster after the cursor.
*
* @return string|null The grapheme cluster, or null if at end
*/
public function getCharAfterCursor(): ?string
{
if ($this->cursor >= \mb_strlen($this->text)) {
return null;
}
$span = $this->graphemeClusterSpanForward(1);
return \mb_substr($this->text, $this->cursor, $span);
}
/**
* Move cursor to start of line.
*/
public function moveCursorToStart(): void
{
$this->cursor = 0;
}
/**
* Move cursor to end of line.
*/
public function moveCursorToEnd(): void
{
$this->cursor = \mb_strlen($this->text);
}
/**
* Move cursor to start of current line (for multi-line buffers).
*/
public function moveCursorToStartOfCurrentLine(): void
{
$beforeCursor = \mb_substr($this->text, 0, $this->cursor);
$lastNewline = \mb_strrpos($beforeCursor, "\n");
$this->cursor = $lastNewline === false ? 0 : $lastNewline + 1;
}
/**
* Move cursor to end of current line (for multi-line buffers).
*/
public function moveCursorToEndOfCurrentLine(): void
{
$nextNewline = \mb_strpos($this->text, "\n", $this->cursor);
$this->cursor = $nextNewline === false ? \mb_strlen($this->text) : $nextNewline;
}
/**
* Get the current line number (0-indexed) based on cursor position.
*/
public function getCurrentLineNumber(): int
{
return \substr_count(\mb_substr($this->text, 0, $this->cursor), "\n");
}
/**
* Get the text of the current line (where cursor is).
*/
public function getCurrentLineText(): string
{
$lines = \explode("\n", $this->text);
$lineNumber = $this->getCurrentLineNumber();
return $lines[$lineNumber] ?? '';
}
/**
* Get the cursor position within the current line (0-indexed).
*/
public function getCursorPositionInLine(): int
{
$lineNumber = $this->getCurrentLineNumber();
$lines = \explode("\n", $this->text);
$charsBeforeLine = 0;
for ($i = 0; $i < $lineNumber; $i++) {
$charsBeforeLine += \mb_strlen($lines[$i]) + 1;
}
return $this->cursor - $charsBeforeLine;
}
/**
* Get the total number of lines in the buffer.
*/
public function getLineCount(): int
{
return \substr_count($this->text, "\n") + 1;
}
/**
* Check if the cursor is on the first line.
*/
public function isOnFirstLine(): bool
{
return $this->getCurrentLineNumber() === 0;
}
/**
* Check if the cursor is on the last line.
*/
public function isOnLastLine(): bool
{
return $this->getCurrentLineNumber() === $this->getLineCount() - 1;
}
/**
* Move cursor to the previous line, maintaining column position if possible.
*
* @return bool True if moved, false if already on first line
*/
public function moveToPreviousLine(): bool
{
if ($this->isOnFirstLine()) {
return false;
}
$beforeCursor = \mb_substr($this->text, 0, $this->cursor);
$lastNewline = \mb_strrpos($beforeCursor, "\n");
if ($lastNewline === false) {
return false;
}
$currentColumn = $this->cursor - $lastNewline - 1;
$beforeCurrentLine = \mb_substr($this->text, 0, $lastNewline);
$prevLineStart = \mb_strrpos($beforeCurrentLine, "\n");
$prevLineStart = $prevLineStart === false ? 0 : $prevLineStart + 1;
$prevLineLength = $lastNewline - $prevLineStart;
$this->cursor = $prevLineStart + \min($currentColumn, $prevLineLength);
return true;
}
/**
* Move cursor to the previous soft-wrapped visual row on the current line.
*
* @param int $terminalWidth Number of terminal columns
* @param int $promptWidth Display width of the active prompt
*
* @return bool True if moved, false if already on first visual row
*/
public function moveToPreviousVisualRow(int $terminalWidth, int $promptWidth): bool
{
return $this->moveVisualRows(-1, $terminalWidth, $promptWidth);
}
/**
* Move cursor to the next soft-wrapped visual row on the current line.
*
* @param int $terminalWidth Number of terminal columns
* @param int $promptWidth Display width of the active prompt
*
* @return bool True if moved, false if already on last visual row
*/
public function moveToNextVisualRow(int $terminalWidth, int $promptWidth): bool
{
return $this->moveVisualRows(1, $terminalWidth, $promptWidth);
}
/**
* Move cursor by soft-wrapped visual rows.
*/
private function moveVisualRows(int $deltaRows, int $terminalWidth, int $promptWidth): bool
{
$target = $this->visualNavigationPolicy->moveByRows(
$this->text,
$this->cursor,
$deltaRows,
$terminalWidth,
$promptWidth
);
if ($target === null) {
return false;
}
$this->cursor = $target;
return true;
}
/**
* Move cursor to the next line, maintaining column position if possible.
*
* @return bool True if moved, false if already on last line
*/
public function moveToNextLine(): bool
{
if ($this->isOnLastLine()) {
return false;
}
$beforeCursor = \mb_substr($this->text, 0, $this->cursor);
$lastNewline = \mb_strrpos($beforeCursor, "\n");
$currentLineStart = $lastNewline === false ? 0 : $lastNewline + 1;
$currentColumn = $this->cursor - $currentLineStart;
$nextNewline = \mb_strpos($this->text, "\n", $this->cursor);
if ($nextNewline === false) {
return false;
}
$nextLineStart = $nextNewline + 1;
$lineAfterNext = \mb_strpos($this->text, "\n", $nextLineStart);
$nextLineEnd = $lineAfterNext === false ? \mb_strlen($this->text) : $lineAfterNext;
$nextLineLength = $nextLineEnd - $nextLineStart;
$this->cursor = $nextLineStart + \min($currentColumn, $nextLineLength);
return true;
}
/**
* Get text before cursor.
*/
public function getBeforeCursor(): string
{
return \mb_substr($this->text, 0, $this->cursor);
}
/**
* Get text after cursor.
*/
public function getAfterCursor(): string
{
return \mb_substr($this->text, $this->cursor);
}
/**
* Find the position of the start of the previous word.
*/
public function findPreviousWord(): int
{
return $this->wordNavigationPolicy->findPreviousWord($this->text, $this->cursor);
}
/**
* Find the position of the start of the next word.
*/
public function findNextWord(): int
{
return $this->wordNavigationPolicy->findNextWord($this->text, $this->cursor);
}
/**
* Delete previous word (from cursor backwards).
*
* @return string The deleted text
*/
public function deletePreviousWord(): string
{
return $this->deleteBackwardTo(fn () => $this->findPreviousWord());
}
/**
* Delete next word (from cursor forwards).
*
* @return string The deleted text
*/
public function deleteNextWord(): string
{
return $this->deleteForwardTo($this->findNextWord());
}
/**
* Check if the buffer is empty.
*/
public function isEmpty(): bool
{
return $this->text === '';
}
/**
* Get the code-point span of N grapheme clusters forward from cursor.
*
* Uses PCRE \X to match extended grapheme clusters (Unicode TR#29).
*/
private function graphemeClusterSpanForward(int $count): int
{
if ($count <= 0) {
return 0;
}
if ($this->initializeGraphemeCache() && $this->graphemeBoundaries !== null && isset($this->graphemeBoundaryMap[$this->cursor])) {
$startIndex = $this->graphemeBoundaryMap[$this->cursor];
$targetIndex = \min($startIndex + $count, \count($this->graphemeBoundaries) - 1);
return $this->graphemeBoundaries[$targetIndex] - $this->cursor;
}
return $this->graphemeClusterSpan(\mb_substr($this->text, $this->cursor), $count, false);
}
/**
* Get the code-point span of N grapheme clusters backward from cursor.
*/
private function graphemeClusterSpanReverse(int $count): int
{
if ($count <= 0) {
return 0;
}
if ($this->initializeGraphemeCache() && $this->graphemeBoundaries !== null && isset($this->graphemeBoundaryMap[$this->cursor])) {
$startIndex = $this->graphemeBoundaryMap[$this->cursor];
$targetIndex = \max(0, $startIndex - $count);
return $this->cursor - $this->graphemeBoundaries[$targetIndex];
}
return $this->graphemeClusterSpan(\mb_substr($this->text, 0, $this->cursor), $count, true);
}
/**
* Build grapheme boundaries for current text.
*/
private function initializeGraphemeCache(): bool
{
if ($this->graphemeCacheInitialized) {
return $this->graphemeBoundaries !== null;
}
$this->graphemeCacheInitialized = true;
if ($this->text === '') {
$this->graphemeBoundaries = [0];
$this->graphemeBoundaryMap = [0 => 0];
return true;
}
if (\preg_match_all('/\X/u', $this->text, $matches) === false || empty($matches[0])) {
$this->graphemeBoundaries = null;
$this->graphemeBoundaryMap = null;
return false;
}
$boundaries = [0];
$boundaryMap = [0 => 0];
$offset = 0;
$index = 0;
foreach ($matches[0] as $cluster) {
$offset += \mb_strlen($cluster);
$boundaries[] = $offset;
$boundaryMap[$offset] = ++$index;
}
$this->graphemeBoundaries = $boundaries;
$this->graphemeBoundaryMap = $boundaryMap;
return true;
}
/**
* Get the code-point span of N grapheme clusters from one end of a string.
*
* @param string $text The text to measure
* @param int $count Number of grapheme clusters
* @param bool $fromEnd Take clusters from the end rather than the start
*/
private function graphemeClusterSpan(string $text, int $count, bool $fromEnd): int
{
if ($count <= 0 || $text === '') {
return 0;
}
// Fall back to code-point count if regex fails (e.g. invalid UTF-8)
if (\preg_match_all('/\X/u', $text, $matches) === false || empty($matches[0])) {
return \min($count, \mb_strlen($text));
}
$offset = $fromEnd ? -$count : 0;
$clusters = \array_slice($matches[0], $offset, $count);
return \array_sum(\array_map('mb_strlen', $clusters));
}
/**
* Delete backward from cursor to a target position, including bracket pairs when applicable.
*
* @param callable $findTarget Returns the target position when called with current cursor state
*
* @return string The deleted text
*/
private function deleteBackwardTo(callable $findTarget): string
{
if (BracketPair::isInsideEmptyBrackets($this)) {
// Include the bracket pair in the deletion
$savedCursor = $this->cursor;
$this->cursor--;
$targetStart = $findTarget();
$this->cursor = $savedCursor;
$killed = \mb_substr($this->text, $targetStart, $this->cursor - $targetStart + 1);
$before = \mb_substr($this->text, 0, $targetStart);
$after = \mb_substr($this->text, $this->cursor + 1);
$this->text = $before.$after;
$this->cursor = $targetStart;
$this->invalidateCaches();
return $killed;
}
$targetStart = $findTarget();
$killed = \mb_substr($this->text, $targetStart, $this->cursor - $targetStart);
$before = \mb_substr($this->text, 0, $targetStart);
$after = \mb_substr($this->text, $this->cursor);
$this->text = $before.$after;
$this->cursor = $targetStart;
$this->invalidateCaches();
return $killed;
}
/**
* Delete forward from cursor to a target position.
*
* @return string The deleted text
*/
private function deleteForwardTo(int $targetEnd): string
{
$killed = \mb_substr($this->text, $this->cursor, $targetEnd - $this->cursor);
$before = \mb_substr($this->text, 0, $this->cursor);
$after = \mb_substr($this->text, $targetEnd);
$this->text = $before.$after;
$this->invalidateCaches();
return $killed;
}
private function invalidateCaches(): void
{
$this->graphemeCacheInitialized = false;
$this->graphemeBoundaries = null;
$this->graphemeBoundaryMap = null;
}
private function getParseSnapshot(): ParseSnapshot
{
return $this->parseSnapshotCache->getSnapshot($this->getText());
}
/**
* @return array Raw token_get_all() tokens
*/
private function getParsedTokens(): array
{
return $this->getParseSnapshot()->getTokens();
}
/**
* @return int[]
*/
private function getParsedTokenPositions(): array
{
return $this->getParseSnapshot()->getTokenPositions();
}
/**
* Check if the buffer contains a complete PHP statement.
*/
public function isCompleteStatement(): bool
{
return $this->statementCompletenessPolicy->isCompleteStatement($this->text);
}
/**
* Check if the buffer has an unrecoverable syntax error.
*/
public function hasUnrecoverableSyntaxError(): bool
{
return $this->statementCompletenessPolicy->hasUnrecoverableSyntaxError($this->text);
}
/**
* Check whether text before the cursor has unclosed brackets.
*/
public function hasUnclosedBracketsBeforeCursor(): bool
{
$textBeforeCursor = $this->getBeforeCursor();
if (\trim($textBeforeCursor) === '') {
return false;
}
return !$this->statementCompletenessPolicy->hasBalancedBrackets($textBeforeCursor);
}
/**
* Calculate the appropriate indentation for the next line.
*
* Analyzes the current line to determine how much whitespace should
* be automatically inserted when continuing to a new line.
*
* @return string The whitespace to insert (spaces or tabs)
*/
public function calculateNextLineIndent(): string
{
return $this->indentationPolicy->calculateNextLineIndent(
$this->text,
$this->getParsedTokens()
);
}
/**
* Calculate indentation using only text before the cursor.
*/
public function calculateIndentBeforeCursor(): string
{
$textBeforeCursor = $this->getBeforeCursor();
$tokens = $this->parseSnapshotCache->getSnapshot($textBeforeCursor)->getTokens();
return $this->indentationPolicy->calculateNextLineIndent($textBeforeCursor, $tokens);
}
/**
* Remove one level of indentation from an indent string.
*/
public function dedent(string $indent): string
{
return $this->indentationPolicy->dedent($indent);
}
/**
* Get the number of spaces needed to reach the next tab stop.
*/
public function spacesToNextTabStop(int $column): int
{
return $this->indentationPolicy->spacesToNextTabStop($column);
}
/**
* Get the number of spaces to remove to reach the previous tab stop.
*/
public function spacesToPreviousTabStop(int $spaces): int
{
return $this->indentationPolicy->spacesToPreviousTabStop($spaces);
}
/**
* Find the start position of the previous token.
*
* Navigates to the start of the token before the cursor position.
* If cursor is inside a token (not at start), goes to start of that token.
* If cursor is at the start of a token, goes to previous token.
*
* @return int Position of previous token start, or 0 if at beginning
*/
public function findPreviousToken(): int
{
return $this->tokenNavigationPolicy->findPreviousToken(
$this->getParsedTokens(),
$this->getParsedTokenPositions(),
$this->cursor
);
}
/**
* Find the start position of the next token.
*
* Navigates to the start of the token after the cursor position.
* If cursor is inside a token, goes to next token after that.
* If cursor is between tokens, goes to next token.
*
* @return int Position of next token start, or end of line if at end
*/
public function findNextToken(): int
{
return $this->tokenNavigationPolicy->findNextToken(
$this->getParsedTokens(),
$this->getParsedTokenPositions(),
$this->cursor,
$this->getLength()
);
}
/**
* Delete from cursor backwards to start of previous token.
*
* @return string The deleted text
*/
public function deletePreviousToken(): string
{
return $this->deleteBackwardTo(fn () => $this->findPreviousToken());
}
/**
* Delete from cursor forwards to start of next token.
*
* @return string The deleted text
*/
public function deleteNextToken(): string
{
return $this->deleteForwardTo($this->findNextToken());
}
}