diff --git a/CHANGELOG.md b/CHANGELOG.md index e14a3690c..eb65a5316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Change SVG export to create a simpler SVG - Fix render_lines crash when render height was negative https://github.com/Textualize/rich/pull/2246 +### Added + +- Add `padding` to Syntax constructor https://github.com/Textualize/rich/pull/2247 + ## [12.3.0] - 2022-04-26 ### Added diff --git a/rich/syntax.py b/rich/syntax.py index 6a337e407..cb34855ac 100644 --- a/rich/syntax.py +++ b/rich/syntax.py @@ -23,13 +23,14 @@ from pygments.util import ClassNotFound from rich.containers import Lines +from rich.padding import Padding, PaddingDimensions from ._loop import loop_first from .color import Color, blend_rgb from .console import Console, ConsoleOptions, JustifyMethod, RenderResult from .jupyter import JupyterMixin from .measure import Measurement -from .segment import Segment +from .segment import Segment, Segments from .style import Style from .text import Text @@ -100,6 +101,7 @@ } RICH_SYNTAX_THEMES = {"ansi_light": ANSI_LIGHT, "ansi_dark": ANSI_DARK} +NUMBERS_COLUMN_DEFAULT_PADDING = 2 class SyntaxTheme(ABC): @@ -209,6 +211,7 @@ class Syntax(JupyterMixin): word_wrap (bool, optional): Enable word wrapping. background_color (str, optional): Optional background color, or None to use theme color. Defaults to None. indent_guides (bool, optional): Show indent guides. Defaults to False. + padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding). """ _pygments_style_class: Type[PygmentsStyle] @@ -242,6 +245,7 @@ def __init__( word_wrap: bool = False, background_color: Optional[str] = None, indent_guides: bool = False, + padding: PaddingDimensions = 0, ) -> None: self.code = code self._lexer = lexer @@ -258,6 +262,7 @@ def __init__( Style(bgcolor=background_color) if background_color else Style() ) self.indent_guides = indent_guides + self.padding = padding self._theme = self.get_theme(theme) @@ -278,6 +283,7 @@ def from_path( word_wrap: bool = False, background_color: Optional[str] = None, indent_guides: bool = False, + padding: PaddingDimensions = 0, ) -> "Syntax": """Construct a Syntax object from a file. @@ -296,6 +302,7 @@ def from_path( word_wrap (bool, optional): Enable word wrapping of code. background_color (str, optional): Optional background color, or None to use theme color. Defaults to None. indent_guides (bool, optional): Show indent guides. Defaults to False. + padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding). Returns: [Syntax]: A Syntax object that may be printed to the console @@ -320,6 +327,7 @@ def from_path( word_wrap=word_wrap, background_color=background_color, indent_guides=indent_guides, + padding=padding, ) @classmethod @@ -498,7 +506,10 @@ def _numbers_column_width(self) -> int: """Get the number of characters used to render the numbers column.""" column_width = 0 if self.line_numbers: - column_width = len(str(self.start_line + self.code.count("\n"))) + 2 + column_width = ( + len(str(self.start_line + self.code.count("\n"))) + + NUMBERS_COLUMN_DEFAULT_PADDING + ) return column_width def _get_number_styles(self, console: Console) -> Tuple[Style, Style, Style]: @@ -527,15 +538,31 @@ def _get_number_styles(self, console: Console) -> Tuple[Style, Style, Style]: def __rich_measure__( self, console: "Console", options: "ConsoleOptions" ) -> "Measurement": + _, right, _, left = Padding.unpack(self.padding) if self.code_width is not None: - width = self.code_width + self._numbers_column_width + width = self.code_width + self._numbers_column_width + right + left return Measurement(self._numbers_column_width, width) return Measurement(self._numbers_column_width, options.max_width) def __rich_console__( self, console: Console, options: ConsoleOptions ) -> RenderResult: + segments = Segments(self._get_syntax(console, options)) + if self.padding: + yield Padding( + segments, style=self._theme.get_background_style(), pad=self.padding + ) + else: + yield segments + def _get_syntax( + self, + console: Console, + options: ConsoleOptions, + ) -> Iterable[Segment]: + """ + Get the Segments for the Syntax object, excluding any vertical/horizontal padding + """ transparent_background = self._get_base_style().transparent_background code_width = ( ( @@ -553,12 +580,6 @@ def __rich_console__( code = code.expandtabs(self.tab_size) text = self.highlight(code, self.line_range) - ( - background_style, - number_style, - highlight_number_style, - ) = self._get_number_styles(console) - if not self.line_numbers and not self.word_wrap and not self.line_range: if not ends_on_nl: text.remove_suffix("\n") @@ -615,11 +636,16 @@ def __rich_console__( highlight_line = self.highlight_lines.__contains__ _Segment = Segment - padding = _Segment(" " * numbers_column_width + " ", background_style) new_line = _Segment("\n") line_pointer = "> " if options.legacy_windows else "❱ " + ( + background_style, + number_style, + highlight_number_style, + ) = self._get_number_styles(console) + for line_no, line in enumerate(lines, self.start_line + line_offset): if self.word_wrap: wrapped_lines = console.render_lines( @@ -628,7 +654,6 @@ def __rich_console__( style=background_style, pad=not transparent_background, ) - else: segments = list(line.render(console, end="")) if options.no_wrap: @@ -642,7 +667,11 @@ def __rich_console__( pad=not transparent_background, ) ] + if self.line_numbers: + wrapped_line_left_pad = _Segment( + " " * numbers_column_width + " ", background_style + ) for first, wrapped_line in loop_first(wrapped_lines): if first: line_column = str(line_no).rjust(numbers_column_width - 2) + " " @@ -653,7 +682,7 @@ def __rich_console__( yield _Segment(" ", highlight_number_style) yield _Segment(line_column, number_style) else: - yield padding + yield wrapped_line_left_pad yield from wrapped_line yield new_line else: @@ -739,6 +768,16 @@ def __rich_console__( dest="lexer_name", help="Lexer name", ) + parser.add_argument( + "-p", "--padding", type=int, default=0, dest="padding", help="Padding" + ) + parser.add_argument( + "--highlight-line", + type=int, + default=None, + dest="highlight_line", + help="The line number (not index!) to highlight", + ) args = parser.parse_args() from rich.console import Console @@ -755,6 +794,8 @@ def __rich_console__( theme=args.theme, background_color=args.background_color, indent_guides=args.indent_guides, + padding=args.padding, + highlight_lines={args.highlight_line}, ) else: syntax = Syntax.from_path( @@ -765,5 +806,7 @@ def __rich_console__( theme=args.theme, background_color=args.background_color, indent_guides=args.indent_guides, + padding=args.padding, + highlight_lines={args.highlight_line}, ) console.print(syntax, soft_wrap=args.soft_wrap) diff --git a/tests/test_syntax.py b/tests/test_syntax.py index e5d904f36..a88ce0a33 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -1,5 +1,5 @@ # coding=utf-8 - +import io import os import sys import tempfile @@ -303,6 +303,22 @@ def test_syntax_guess_lexer(): assert Syntax.guess_lexer("banana.html", "{{something|filter:3}}") == "html+django" +def test_syntax_padding(): + syntax = Syntax("x = 1", lexer="python", padding=(1, 3)) + console = Console( + width=20, + file=io.StringIO(), + color_system="truecolor", + legacy_windows=False, + record=True, + ) + console.print(syntax) + output = console.export_text() + assert ( + output == " \n x = 1 \n \n" + ) + + if __name__ == "__main__": syntax = Panel.fit( Syntax(