Skip to content

Commit 79a41db

Browse files
authored
Add padding support to Syntax (#2247)
* Add padding to Syntax constructor * Add padding to from_path constructor, use correct type * Padding for `Syntax` - support vertical padding * Extract logic for create base syntax without padding * Add test for padded `Syntax` * Small refactor of Syntax padding * Small refactor of Syntax padding * Remove some dead code * Update changelog
1 parent 5a999f4 commit 79a41db

File tree

3 files changed

+76
-13
lines changed

3 files changed

+76
-13
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Change SVG export to create a simpler SVG
1313
- Fix render_lines crash when render height was negative https://github.com/Textualize/rich/pull/2246
1414

15+
### Added
16+
17+
- Add `padding` to Syntax constructor https://github.com/Textualize/rich/pull/2247
18+
1519
## [12.3.0] - 2022-04-26
1620

1721
### Added

rich/syntax.py

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@
2323
from pygments.util import ClassNotFound
2424

2525
from rich.containers import Lines
26+
from rich.padding import Padding, PaddingDimensions
2627

2728
from ._loop import loop_first
2829
from .color import Color, blend_rgb
2930
from .console import Console, ConsoleOptions, JustifyMethod, RenderResult
3031
from .jupyter import JupyterMixin
3132
from .measure import Measurement
32-
from .segment import Segment
33+
from .segment import Segment, Segments
3334
from .style import Style
3435
from .text import Text
3536

@@ -100,6 +101,7 @@
100101
}
101102

102103
RICH_SYNTAX_THEMES = {"ansi_light": ANSI_LIGHT, "ansi_dark": ANSI_DARK}
104+
NUMBERS_COLUMN_DEFAULT_PADDING = 2
103105

104106

105107
class SyntaxTheme(ABC):
@@ -209,6 +211,7 @@ class Syntax(JupyterMixin):
209211
word_wrap (bool, optional): Enable word wrapping.
210212
background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.
211213
indent_guides (bool, optional): Show indent guides. Defaults to False.
214+
padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding).
212215
"""
213216

214217
_pygments_style_class: Type[PygmentsStyle]
@@ -242,6 +245,7 @@ def __init__(
242245
word_wrap: bool = False,
243246
background_color: Optional[str] = None,
244247
indent_guides: bool = False,
248+
padding: PaddingDimensions = 0,
245249
) -> None:
246250
self.code = code
247251
self._lexer = lexer
@@ -258,6 +262,7 @@ def __init__(
258262
Style(bgcolor=background_color) if background_color else Style()
259263
)
260264
self.indent_guides = indent_guides
265+
self.padding = padding
261266

262267
self._theme = self.get_theme(theme)
263268

@@ -278,6 +283,7 @@ def from_path(
278283
word_wrap: bool = False,
279284
background_color: Optional[str] = None,
280285
indent_guides: bool = False,
286+
padding: PaddingDimensions = 0,
281287
) -> "Syntax":
282288
"""Construct a Syntax object from a file.
283289
@@ -296,6 +302,7 @@ def from_path(
296302
word_wrap (bool, optional): Enable word wrapping of code.
297303
background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.
298304
indent_guides (bool, optional): Show indent guides. Defaults to False.
305+
padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding).
299306
300307
Returns:
301308
[Syntax]: A Syntax object that may be printed to the console
@@ -320,6 +327,7 @@ def from_path(
320327
word_wrap=word_wrap,
321328
background_color=background_color,
322329
indent_guides=indent_guides,
330+
padding=padding,
323331
)
324332

325333
@classmethod
@@ -498,7 +506,10 @@ def _numbers_column_width(self) -> int:
498506
"""Get the number of characters used to render the numbers column."""
499507
column_width = 0
500508
if self.line_numbers:
501-
column_width = len(str(self.start_line + self.code.count("\n"))) + 2
509+
column_width = (
510+
len(str(self.start_line + self.code.count("\n")))
511+
+ NUMBERS_COLUMN_DEFAULT_PADDING
512+
)
502513
return column_width
503514

504515
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]:
527538
def __rich_measure__(
528539
self, console: "Console", options: "ConsoleOptions"
529540
) -> "Measurement":
541+
_, right, _, left = Padding.unpack(self.padding)
530542
if self.code_width is not None:
531-
width = self.code_width + self._numbers_column_width
543+
width = self.code_width + self._numbers_column_width + right + left
532544
return Measurement(self._numbers_column_width, width)
533545
return Measurement(self._numbers_column_width, options.max_width)
534546

535547
def __rich_console__(
536548
self, console: Console, options: ConsoleOptions
537549
) -> RenderResult:
550+
segments = Segments(self._get_syntax(console, options))
551+
if self.padding:
552+
yield Padding(
553+
segments, style=self._theme.get_background_style(), pad=self.padding
554+
)
555+
else:
556+
yield segments
538557

558+
def _get_syntax(
559+
self,
560+
console: Console,
561+
options: ConsoleOptions,
562+
) -> Iterable[Segment]:
563+
"""
564+
Get the Segments for the Syntax object, excluding any vertical/horizontal padding
565+
"""
539566
transparent_background = self._get_base_style().transparent_background
540567
code_width = (
541568
(
@@ -553,12 +580,6 @@ def __rich_console__(
553580
code = code.expandtabs(self.tab_size)
554581
text = self.highlight(code, self.line_range)
555582

556-
(
557-
background_style,
558-
number_style,
559-
highlight_number_style,
560-
) = self._get_number_styles(console)
561-
562583
if not self.line_numbers and not self.word_wrap and not self.line_range:
563584
if not ends_on_nl:
564585
text.remove_suffix("\n")
@@ -615,11 +636,16 @@ def __rich_console__(
615636

616637
highlight_line = self.highlight_lines.__contains__
617638
_Segment = Segment
618-
padding = _Segment(" " * numbers_column_width + " ", background_style)
619639
new_line = _Segment("\n")
620640

621641
line_pointer = "> " if options.legacy_windows else "❱ "
622642

643+
(
644+
background_style,
645+
number_style,
646+
highlight_number_style,
647+
) = self._get_number_styles(console)
648+
623649
for line_no, line in enumerate(lines, self.start_line + line_offset):
624650
if self.word_wrap:
625651
wrapped_lines = console.render_lines(
@@ -628,7 +654,6 @@ def __rich_console__(
628654
style=background_style,
629655
pad=not transparent_background,
630656
)
631-
632657
else:
633658
segments = list(line.render(console, end=""))
634659
if options.no_wrap:
@@ -642,7 +667,11 @@ def __rich_console__(
642667
pad=not transparent_background,
643668
)
644669
]
670+
645671
if self.line_numbers:
672+
wrapped_line_left_pad = _Segment(
673+
" " * numbers_column_width + " ", background_style
674+
)
646675
for first, wrapped_line in loop_first(wrapped_lines):
647676
if first:
648677
line_column = str(line_no).rjust(numbers_column_width - 2) + " "
@@ -653,7 +682,7 @@ def __rich_console__(
653682
yield _Segment(" ", highlight_number_style)
654683
yield _Segment(line_column, number_style)
655684
else:
656-
yield padding
685+
yield wrapped_line_left_pad
657686
yield from wrapped_line
658687
yield new_line
659688
else:
@@ -739,6 +768,16 @@ def __rich_console__(
739768
dest="lexer_name",
740769
help="Lexer name",
741770
)
771+
parser.add_argument(
772+
"-p", "--padding", type=int, default=0, dest="padding", help="Padding"
773+
)
774+
parser.add_argument(
775+
"--highlight-line",
776+
type=int,
777+
default=None,
778+
dest="highlight_line",
779+
help="The line number (not index!) to highlight",
780+
)
742781
args = parser.parse_args()
743782

744783
from rich.console import Console
@@ -755,6 +794,8 @@ def __rich_console__(
755794
theme=args.theme,
756795
background_color=args.background_color,
757796
indent_guides=args.indent_guides,
797+
padding=args.padding,
798+
highlight_lines={args.highlight_line},
758799
)
759800
else:
760801
syntax = Syntax.from_path(
@@ -765,5 +806,7 @@ def __rich_console__(
765806
theme=args.theme,
766807
background_color=args.background_color,
767808
indent_guides=args.indent_guides,
809+
padding=args.padding,
810+
highlight_lines={args.highlight_line},
768811
)
769812
console.print(syntax, soft_wrap=args.soft_wrap)

tests/test_syntax.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# coding=utf-8
2-
2+
import io
33
import os
44
import sys
55
import tempfile
@@ -303,6 +303,22 @@ def test_syntax_guess_lexer():
303303
assert Syntax.guess_lexer("banana.html", "{{something|filter:3}}") == "html+django"
304304

305305

306+
def test_syntax_padding():
307+
syntax = Syntax("x = 1", lexer="python", padding=(1, 3))
308+
console = Console(
309+
width=20,
310+
file=io.StringIO(),
311+
color_system="truecolor",
312+
legacy_windows=False,
313+
record=True,
314+
)
315+
console.print(syntax)
316+
output = console.export_text()
317+
assert (
318+
output == " \n x = 1 \n \n"
319+
)
320+
321+
306322
if __name__ == "__main__":
307323
syntax = Panel.fit(
308324
Syntax(

0 commit comments

Comments
 (0)