Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add padding support to Syntax #2247

Merged
merged 14 commits into from May 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
67 changes: 55 additions & 12 deletions rich/syntax.py
Expand Up @@ -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

Expand Down Expand Up @@ -100,6 +101,7 @@
}

RICH_SYNTAX_THEMES = {"ansi_light": ANSI_LIGHT, "ansi_dark": ANSI_DARK}
NUMBERS_COLUMN_DEFAULT_PADDING = 2


class SyntaxTheme(ABC):
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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.

Expand All @@ -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
Expand All @@ -320,6 +327,7 @@ def from_path(
word_wrap=word_wrap,
background_color=background_color,
indent_guides=indent_guides,
padding=padding,
)

@classmethod
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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 = (
(
Expand All @@ -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")
Expand Down Expand Up @@ -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(
Expand All @@ -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:
Expand All @@ -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) + " "
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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)
18 changes: 17 additions & 1 deletion tests/test_syntax.py
@@ -1,5 +1,5 @@
# coding=utf-8

import io
import os
import sys
import tempfile
Expand Down Expand Up @@ -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(
Expand Down