diff --git a/AGENTS.md b/AGENTS.md index 6cce18e..cd5a7a3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,6 +32,7 @@ src/ EventBus.php Pub/sub used for key/mouse/custom events CommandInvoker.php Thin wrapper around php-di's Invoker (see Commands note below) MessageFormatter.php JSON syntax highlighting for log payloads + PhpHighlighter.php PHP syntax highlighting (native token_get_all) for the code excerpt in Details CommandAttributes/ #[KeyPressed] #[Mouse] #[OnEvent] #[Periodic] #[FirstRender] Commands/Command.php Abstract marker class — currently has zero implementations (see known-issues.md) Components/ Header, LogList, LogItem, Details, Navigation, Splash, Filter (the `/` filter input line, see typingMode below) diff --git a/src/Console/Components/Details.php b/src/Console/Components/Details.php index 3896706..76aeebc 100644 --- a/src/Console/Components/Details.php +++ b/src/Console/Components/Details.php @@ -27,6 +27,7 @@ use Tapper\Console\Component; use Tapper\Console\MessageFormatter; use Tapper\Console\Palette; +use Tapper\Console\PhpHighlighter; use Tapper\Console\Support\Scroll; class Details extends Component @@ -83,21 +84,32 @@ public function pageDown(): void private function parseCode(array $code): array { - return array_map(function ($line) { - $style = Style::default()->darkGray(); + $numberWidth = array_reduce( + $code, + fn (int $width, array $line): int => max($width, strlen((string) $line['number'])), + 1, + ); - if ($line['active']) { - $style = $style->lightGreen(); - } + return array_map(function ($line) use ($numberWidth) { + $active = $line['active']; + + $prefixStyle = $active + ? Style::default()->fg(RgbColor::fromHex(Palette::ACCENT)) + : Style::default()->darkGray(); - $codeLine = sprintf( - ' %s %s│ %s', - $line['number'], - $line['active'] ? '➔' : ' ', - $line['line'], + $prefix = Span::styled( + sprintf(' %s %s│ ', str_pad((string) $line['number'], $numberWidth, ' ', STR_PAD_LEFT), $active ? '➔' : ' '), + $prefixStyle, ); - return ListItem::fromString($codeLine)->style($style); + $lineSpans = [$prefix, ...PhpHighlighter::highlightLine($line['line'])]; + $item = ListItem::new(Text::fromLine(new Line($lineSpans))); + + if ($active) { + $item = $item->style(Style::default()->bg(RgbColor::fromHex(Palette::SELECTION_BG))); + } + + return $item; }, $code); } diff --git a/src/Console/PhpHighlighter.php b/src/Console/PhpHighlighter.php new file mode 100644 index 0000000..4d308b5 --- /dev/null +++ b/src/Console/PhpHighlighter.php @@ -0,0 +1,141 @@ + $token) { + if (is_string($token)) { + $spans[] = Span::fromString($token); + + continue; + } + + [$id, $text] = $token; + $spans[] = Span::styled($text, self::styleFor($id, $text, $tokens, $i)); + } + + return $spans; + } + + /** + * @param array $tokens + */ + private static function styleFor(int $id, string $text, array $tokens, int $index): Style + { + if (in_array($id, self::COMMENT_TOKENS, true)) { + return Style::default()->fg(RgbColor::fromHex(self::COMMENT_COLOR)); + } + + if (in_array($id, self::KEYWORD_TOKENS, true)) { + return Style::default()->fg(RgbColor::fromHex(self::KEYWORD_COLOR)); + } + + if ($id === T_VARIABLE) { + return Style::default()->fg(RgbColor::fromHex(self::VARIABLE_COLOR)); + } + + if (in_array($id, self::STRING_TOKENS, true)) { + return Style::default()->fg(RgbColor::fromHex(self::STRING_COLOR)); + } + + if ($id === T_LNUMBER || $id === T_DNUMBER) { + return Style::default()->fg(RgbColor::fromHex(self::NUMBER_COLOR)); + } + + if ($id === T_STRING) { + if (in_array(strtolower($text), self::BARE_KEYWORDS, true)) { + return Style::default()->fg(RgbColor::fromHex(self::KEYWORD_COLOR)); + } + + if (self::nextSignificant($tokens, $index) === '(') { + return Style::default()->fg(RgbColor::fromHex(self::FUNCTION_COLOR)); + } + } + + return Style::default()->fg(RgbColor::fromHex(Palette::TEXT_DEFAULT)); + } + + /** + * @param array $tokens + */ + private static function nextSignificant(array $tokens, int $index): ?string + { + for ($i = $index + 1; $i < count($tokens); $i++) { + $token = $tokens[$i]; + + if (is_string($token)) { + return $token; + } + + if ($token[0] === T_WHITESPACE) { + continue; + } + + return $token[1]; + } + + return null; + } +}