diff --git a/benchmarks/benchmarks.py b/benchmarks/benchmarks.py index 032eecf791..f7e7ac0a1d 100644 --- a/benchmarks/benchmarks.py +++ b/benchmarks/benchmarks.py @@ -94,6 +94,71 @@ def _print_with_width(self, width): self.console.print(self.syntax, width) +class SyntaxLineNumbersWrappingSuite: + def setup(self): + self.console = Console( + file=StringIO(), color_system="truecolor", legacy_windows=False + ) + self.syntax = Syntax( + code=snippets.PYTHON_SNIPPET * 120, + lexer="python", + word_wrap=True, + line_numbers=True, + ) + + def time_text_thin_terminal_heavy_wrapping(self): + self.console.print(self.syntax, width=30) + + +class SyntaxLargeWrappingSuite: + def setup(self): + self.console = Console( + file=StringIO(), color_system="truecolor", legacy_windows=False + ) + self.syntax = Syntax( + code=snippets.PYTHON_SNIPPET * 120, + lexer="python", + word_wrap=True, + line_numbers=False, + ) + + def time_text_thin_terminal_heavy_wrapping(self): + self.console.print(self.syntax, width=30) + + +class SyntaxLargeNoWrapSuite: + def setup(self): + self.console = Console( + file=StringIO(), color_system="truecolor", legacy_windows=False + ) + self.syntax = Syntax( + code=snippets.PYTHON_SNIPPET * 120, + lexer="python", + word_wrap=False, + line_numbers=False, + ) + + def time_text_wide_terminal_no_wrapping(self): + self.console.print(self.syntax, width=80) + + +class SyntaxLineRangeSuite: + def setup(self): + self.console = Console( + file=StringIO(), color_system="truecolor", legacy_windows=False + ) + self.syntax = Syntax( + code=snippets.PYTHON_SNIPPET * 120, + lexer="python", + word_wrap=False, + line_numbers=False, + line_range=(3000, 3060), + ) + + def time_text_wide_terminal_line_range(self): + self.console.print(self.syntax, width=80) + + class TableSuite: def time_table_no_wrapping(self): self._print_table(width=100) diff --git a/rich/syntax.py b/rich/syntax.py index 8c8f8315e7..4c2e3deed3 100644 --- a/rich/syntax.py +++ b/rich/syntax.py @@ -526,9 +526,10 @@ def tokens_to_spans() -> Iterable[Tuple[str, Optional[Style]]]: _token_type, token = next(tokens) except StopIteration: break - yield (token, None) if token.endswith("\n"): line_no += 1 + if line_no: + yield ("\n" * line_no, None) # Generate spans until line end for token_type, token in tokens: yield (token, _get_theme_style(token_type)) @@ -693,15 +694,18 @@ def _get_syntax( text, options=options.update(width=code_width) ) else: - syntax_lines = console.render_lines( - text, - options.update(width=code_width, height=None, justify="left"), - style=self.background_style, - pad=True, - new_lines=True, - ) - for syntax_line in syntax_lines: - yield from syntax_line + for line in text.split("\n", allow_blank=True): + line_style = console.get_style(line.style, default=Style.null()) + yield from Segment.adjust_line_length( + list( + Segment.apply_style( + line.render(console), self.background_style + ) + ), + code_width, + style=line_style, + ) + yield Segment.line() return start_line, end_line = self.line_range or (None, None) @@ -743,14 +747,51 @@ def _get_syntax( 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( - line, - render_options.update(height=None, justify="left"), + if self.word_wrap and not self.line_numbers: + text = Text("\n").join(lines) + for wrapped_text_line in text.wrap( + console, + render_options.max_width, + justify="left", + overflow=render_options.overflow, + tab_size=self.tab_size, + no_wrap=render_options.no_wrap, + ): + yield from _Segment.adjust_line_length( + list( + _Segment.apply_style( + wrapped_text_line.render(console), background_style + ) + ), + render_options.max_width, style=background_style, pad=not transparent_background, ) + yield new_line + return + + for line_no, line in enumerate(lines, self.start_line + line_offset): + if self.word_wrap: + wrapped_lines = [ + _Segment.adjust_line_length( + list( + _Segment.apply_style( + wrapped_line.render(console), background_style + ) + ), + render_options.max_width, + style=background_style, + pad=not transparent_background, + ) + for wrapped_line in line.wrap( + console, + render_options.max_width, + justify="left", + overflow=render_options.overflow, + tab_size=self.tab_size, + no_wrap=render_options.no_wrap, + ) + ] else: segments = list(line.render(console, end="")) if options.no_wrap: @@ -769,7 +810,7 @@ def _get_syntax( wrapped_line_left_pad = _Segment( " " * numbers_column_width + " ", background_style ) - for first, wrapped_line in loop_first(wrapped_lines): + for first, wrapped_segments in loop_first(wrapped_lines): if first: line_column = str(line_no).rjust(numbers_column_width - 2) + " " if highlight_line(line_no): @@ -780,11 +821,11 @@ def _get_syntax( yield _Segment(line_column, number_style) else: yield wrapped_line_left_pad - yield from wrapped_line + yield from wrapped_segments yield new_line else: - for wrapped_line in wrapped_lines: - yield from wrapped_line + for wrapped_segments in wrapped_lines: + yield from wrapped_segments yield new_line def _apply_stylized_ranges(self, text: Text) -> None: diff --git a/tests/test_syntax.py b/tests/test_syntax.py index f27227ea6a..2f2d4bdb3d 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -435,6 +435,27 @@ def test_padding_plus_wrap() -> None: assert output == expected +def test_word_wrap_without_line_numbers_with_line_range() -> None: + console = Console(width=14, file=io.StringIO(), legacy_windows=False, record=True) + syntax = Syntax( + "first line should not appear\n" + "second line wraps around here\n" + "third line stays\n" + "fourth line should not appear", + lexer="text", + word_wrap=True, + line_numbers=False, + line_range=(2, 3), + ) + + console.print(syntax) + + assert ( + console.export_text() + == "second line \nwraps around \nhere \nthird line \nstays \n" + ) + + if __name__ == "__main__": syntax = Panel.fit( Syntax(