File manager - Edit - /var/www/html/homologBancodetalentos/vendor/psy/psysh/src/Readline/Interactive/Actions/TabAction.php
Back
<?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\Actions; use Psy\Completion\CompletionEngine; use Psy\Completion\CompletionRequest; use Psy\Completion\FuzzyMatcher; use Psy\Readline\Interactive\Helper\CompletionRenderer; use Psy\Readline\Interactive\Helper\CurrentWord; use Psy\Readline\Interactive\Input\Buffer; use Psy\Readline\Interactive\Readline; use Psy\Readline\Interactive\Terminal; /** * Tab completion action. * * Triggers context-aware tab completion when the Tab key is pressed. */ class TabAction implements ActionInterface { private ?CompletionEngine $completer = null; private bool $smartBrackets = false; /** @var string[] */ private array $currentMatches = []; /** @var string[] */ private array $filteredMatches = []; private int $selectedIndex = 0; private string $filterText = ''; private bool $inInteractiveMode = false; private ?bool $interactiveSelectionEnabled = null; /** @var int Viewport: first visible row when scrolling. */ private int $scrollOffset = 0; /** @var bool Viewport: whether the menu has expanded to full height. */ private bool $expanded = false; private int $totalRows = 0; private int $totalColumns = 0; public function __construct(?CompletionEngine $completer = null, bool $smartBrackets = false) { $this->completer = $completer; $this->smartBrackets = $smartBrackets; } /** * Set the CompletionEngine instance. */ public function setCompleter(CompletionEngine $completer): void { $this->completer = $completer; } /** * Force-enable or disable interactive menu mode (for tests). * * @internal */ public function setInteractiveSelectionEnabled(?bool $enabled): void { $this->interactiveSelectionEnabled = $enabled; } /** * {@inheritdoc} */ public function execute(Buffer $buffer, Terminal $terminal, Readline $readline): bool { if ($this->completer === null) { $terminal->bell(); return true; } $text = $buffer->getText(); $cursor = $buffer->getCursor(); $matches = $this->completer->getCompletions( new CompletionRequest($text, $cursor, CompletionRequest::MODE_TAB) ); if (empty($matches)) { return true; } if (\count($matches) === 1) { $this->insertMatch($buffer, $matches[0]); return true; } $commonPrefix = $this->getCommonPrefix($matches); $currentWord = CurrentWord::extract($text, $cursor); if (!empty($commonPrefix) && $commonPrefix !== $currentWord) { $this->insertMatch($buffer, $commonPrefix); $currentWord = $commonPrefix; } $this->currentMatches = \array_values($matches); $this->filterText = $currentWord; $this->filteredMatches = $this->currentMatches; $this->selectedIndex = 0; $this->scrollOffset = 0; $this->expanded = false; $this->inInteractiveMode = true; $readline->enterMenuMode(); try { $this->updateOverlay($buffer, $terminal, $readline); return $this->handleInteractiveSelection($buffer, $terminal, $readline); } finally { $readline->exitMenuMode(); } } /** * {@inheritdoc} */ public function getName(): string { return 'tab-completion'; } /** * Insert a match at the cursor position. */ private function insertMatch(Buffer $buffer, string $match): void { $text = $buffer->getText(); $cursor = $buffer->getCursor(); $textLen = \mb_strlen($text); $start = $cursor; $hasVarPrefix = false; while ($start > 0) { $char = \mb_substr($text, $start - 1, 1); if (\ctype_space($char)) { break; } if ($char === '>' && $start >= 2 && \mb_substr($text, $start - 2, 1) === '-') { break; } if ($char === ':' && $start >= 2 && \mb_substr($text, $start - 2, 1) === ':') { break; } if ($char === '$') { $hasVarPrefix = true; $start--; break; } $start--; } $end = $cursor; while ($end < $textLen) { $char = \mb_substr($text, $end, 1); if (\ctype_space($char) || (!\ctype_alnum($char) && $char !== '_')) { break; } $end++; } if ($hasVarPrefix && $match[0] !== '$') { $match = '$'.$match; } $addParens = $this->smartBrackets && $this->shouldAddParentheses($match); $before = \mb_substr($text, 0, $start); $after = \mb_substr($text, $end); if ($addParens) { $buffer->setText($before.$match.'()'.$after); // Place cursor after parens for zero-arg functions, inside for others $offset = $this->functionHasParameters($match) ? 1 : 2; $buffer->setCursor($start + \mb_strlen($match) + $offset); } else { $buffer->setText($before.$match.$after); $buffer->setCursor($start + \mb_strlen($match)); } } /** * Determine if parentheses should be added for this completion. */ private function shouldAddParentheses(string $match): bool { if (\substr($match, 0, 1) === '$') { return false; } if (\strtoupper($match) === $match && \ctype_upper($match[0])) { return false; } return \function_exists($match); } /** * Check whether a function accepts any parameters (required or optional). */ private function functionHasParameters(string $name): bool { try { return (new \ReflectionFunction($name))->getNumberOfParameters() > 0; } catch (\ReflectionException $e) { // If we can't reflect it, assume it has parameters return true; } } /** * Handle interactive selection with arrow keys and filtering. */ private function handleInteractiveSelection(Buffer $buffer, Terminal $terminal, Readline $readline): bool { if ($this->interactiveSelectionEnabled !== null) { $isTTY = $this->interactiveSelectionEnabled; } else { $isTTY = @\stream_isatty(\STDIN); } if (!$isTTY) { $this->inInteractiveMode = false; return true; } while ($this->inInteractiveMode) { $key = $readline->readNextKey(); $keyStr = (string) $key; $keyValue = $key->getValue(); if ($keyStr === 'escape:[A' || $keyValue === "\x10") { // Up or Ctrl-P $this->moveSelection(-1, $readline); $this->updateOverlay($buffer, $terminal, $readline); } elseif ($keyStr === 'escape:[B' || $keyValue === "\x0e") { // Down or Ctrl-N $this->moveSelection(1, $readline); $this->updateOverlay($buffer, $terminal, $readline); } elseif ($keyStr === 'escape:[C' || $keyValue === "\x06") { // Right or Ctrl-F $this->moveSelectionHorizontal(1, $readline); $this->updateOverlay($buffer, $terminal, $readline); } elseif ($keyStr === 'escape:[D' || $keyValue === "\x02") { // Left or Ctrl-B $this->moveSelectionHorizontal(-1, $readline); $this->updateOverlay($buffer, $terminal, $readline); } elseif ($keyValue === "\t") { // Tab cycles to the next match $this->moveSelection(1, $readline); $this->updateOverlay($buffer, $terminal, $readline); } elseif ($keyValue === "\r" || $keyValue === "\n") { $this->inInteractiveMode = false; if (!empty($this->filteredMatches)) { $selected = $this->filteredMatches[$this->selectedIndex]; $this->insertMatch($buffer, $selected); } $readline->clearOverlay($buffer); break; } elseif ($keyValue === "\e" || $keyValue === "\x03") { // Escape or Ctrl-C $this->inInteractiveMode = false; $readline->clearOverlay($buffer); break; } elseif ($keyValue === "\x7f" || $keyValue === "\x08") { // Backspace if ($buffer->getCursor() > 0) { $buffer->deleteBackward(); $this->filterText = CurrentWord::extract($buffer->getText(), $buffer->getCursor()); if ($this->filterText === '') { $this->inInteractiveMode = false; $readline->clearOverlay($buffer); break; } $this->updateFilter($terminal); $this->updateOverlay($buffer, $terminal, $readline); } } elseif ($key->isChar() && \strlen($keyValue) === 1 && $keyValue >= ' ' && $keyValue <= '~') { $buffer->insert($keyValue); if ($keyValue === ' ') { $this->inInteractiveMode = false; $readline->clearOverlay($buffer); break; } $this->filterText = CurrentWord::extract($buffer->getText(), $buffer->getCursor()); $this->updateFilter($terminal); $this->updateOverlay($buffer, $terminal, $readline); } else { $this->inInteractiveMode = false; $readline->clearOverlay($buffer); $readline->replayKey($key); break; } } return true; } /** * Generate overlay lines from matches and render the frame. */ private function updateOverlay(Buffer $buffer, Terminal $terminal, Readline $readline): void { // Terminal width may change while the menu is open. $this->updateLayout($terminal); $maxRows = $this->getMaxRows($readline); if ($maxRows === null) { $this->scrollOffset = 0; } else { $maxOffset = \max(0, $this->totalRows - $maxRows); $this->scrollOffset = \max(0, \min($this->scrollOffset, $maxOffset)); } $renderer = new CompletionRenderer($terminal); $lines = $renderer->render($this->filteredMatches, $this->selectedIndex, $maxRows, $this->scrollOffset, !$this->expanded); // Prepend a blank separator line above the menu $readline->renderOverlay($buffer, \array_merge([''], $lines)); } /** * Update filtered matches based on current filter text. */ private function updateFilter(?Terminal $terminal = null): void { $previousSelection = $this->filteredMatches[$this->selectedIndex] ?? null; if ($this->filterText === '') { $this->filteredMatches = $this->currentMatches; } else { $this->filteredMatches = FuzzyMatcher::filter($this->filterText, $this->currentMatches); } // Keep the same item selected if it survived filtering, otherwise // reset to the top where the best matches are. $newIndex = \array_search($previousSelection, $this->filteredMatches, true); $this->selectedIndex = $newIndex !== false ? $newIndex : 0; // Reset viewport when filter changes $this->scrollOffset = 0; $this->expanded = false; if ($terminal !== null) { $this->updateLayout($terminal); } } /** * Recalculate total row count for the current matches. */ private function updateLayout(Terminal $terminal): void { if (empty($this->filteredMatches)) { $this->totalRows = 0; $this->totalColumns = 0; return; } $renderer = new CompletionRenderer($terminal); $layout = $renderer->calculateLayout($this->filteredMatches); $this->totalRows = $layout['rows']; $this->totalColumns = $layout['columns']; } /** * Get the maximum visible rows for the current viewport state. * * Returns null if all rows fit without truncation. */ private function getMaxRows(Readline $readline): ?int { $available = $readline->getOverlayAvailableRows(!$this->expanded); // Account for the blank separator line above the menu $menuBudget = \max(1, $available - 1); if ($this->totalRows <= $menuBudget) { // No truncation needed if everything fits (including the +1 off-by-one: // don't show a status line when the last row would fit in its place) return null; } // When truncated, reserve one row for the status line return \max(1, $menuBudget - 1); } /** * Move selection up or down, adjusting viewport as needed. */ private function moveSelection(int $delta, Readline $readline): void { $count = \count($this->filteredMatches); if ($count === 0) { return; } $newIndex = $this->selectedIndex + $delta; // Wrap around if ($newIndex < 0) { $newIndex = $count - 1; } elseif ($newIndex >= $count) { $newIndex = 0; } $this->selectedIndex = $newIndex; $this->adjustViewport($readline); } /** * Adjust scroll offset so the selected item is visible. */ private function adjustViewport(Readline $readline): void { $maxRows = $this->getMaxRows($readline); // Everything fits, no scrolling needed if ($maxRows === null) { $this->scrollOffset = 0; return; } $selectedRow = $this->selectedIndex % $this->totalRows; // Selection is above the visible window if ($selectedRow < $this->scrollOffset) { $this->scrollOffset = $selectedRow; return; } // Selection is below the visible window if ($selectedRow >= $this->scrollOffset + $maxRows) { if (!$this->expanded) { $this->expanded = true; $maxRows = $this->getMaxRows($readline); // After expanding, everything might fit if ($maxRows === null) { $this->scrollOffset = 0; return; } } // Scroll to keep selection visible at the bottom if ($selectedRow >= $this->scrollOffset + $maxRows) { $this->scrollOffset = $selectedRow - $maxRows + 1; } } // Clamp scroll offset $this->scrollOffset = \max(0, \min($this->scrollOffset, $this->totalRows - $maxRows)); } /** * Move selection left or right (horizontal between columns). */ private function moveSelectionHorizontal(int $delta, Readline $readline): void { $count = \count($this->filteredMatches); if ($count === 0) { return; } $rows = $this->totalRows; $columns = $this->totalColumns; $currentRow = $this->selectedIndex % $rows; $currentCol = \intdiv($this->selectedIndex, $rows); $newCol = $currentCol + $delta; // Wrap to next/previous row at column boundaries if ($newCol >= $columns || ($currentRow + ($newCol * $rows)) >= $count) { $newCol = 0; $currentRow++; if ($currentRow >= $rows) { $currentRow = 0; } } elseif ($newCol < 0) { $newCol = $columns - 1; $currentRow--; if ($currentRow < 0) { $currentRow = $rows - 1; } } $newIndex = $currentRow + ($newCol * $rows); // If we landed past the end (sparse last row), fall back to last column on this row if ($newIndex >= $count) { while ($newCol > 0 && $newIndex >= $count) { $newCol--; $newIndex = $currentRow + ($newCol * $rows); } } $this->selectedIndex = $newIndex; $this->adjustViewport($readline); } /** * Get the common prefix of all matches. * * @todo Should this be grapheme cluster aware? * * @param string[] $matches */ private function getCommonPrefix(array $matches): string { if (empty($matches)) { return ''; } $first = \array_shift($matches); $prefix = ''; for ($i = 0; $i < \strlen($first); $i++) { $char = $first[$i]; foreach ($matches as $match) { if (!isset($match[$i]) || $match[$i] !== $char) { return $prefix; } } $prefix .= $char; } return $prefix; } }
| ver. 1.4 |
Github
|
.
| PHP 8.3.28 | Generation time: 0.02 |
proxy
|
phpinfo
|
Settings