resolved RESOLVED UNSUBMITTED PASS · None tool calls · 0 s · Textualize/rich
Extend tabs with styles The `Text.extend_tabs` method was previously not taking in to account the spans. Basically it would replace tabs with unstyled spaces. This resolves that issue by extending the style(s) on the tab to cover the new space characters <img width="623" alt="Screenshot 2023-07-28 at 22 00 06" src="https://github.com/Textualize/rich/assets/554369/28c170a0-b154-4a81-af17-a8a334bebd3a">
| # | Tool | Arguments | Result |
|---|---|---|---|
| No trace captured. | |||
--- a/rich/text.py
+++ b/rich/text.py
@@ -97,6 +97,21 @@ def right_crop(self, offset: int) -> "Span":
return self
return Span(start, min(offset, end), style)
+ def extend(self, cells: int) -> "Span":
+ """Extend the span by the given number of cells.
+
+ Args:
+ cells (int): Additional space to add to end of span.
+
+ Returns:
+ Span: A span.
+ """
+ if cells:
+ start, end, style = self
+ return Span(start, end + cells, style)
+ else:
+ return self
+
class Text(JupyterMixin):
"""Text with color / style.
@@ -108,7 +123,7 @@ class Text(JupyterMixin):
overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
no_wrap (bool, optional): Disable text wrapping, or None for default. Defaults to None.
end (str, optional): Character to end text with. Defaults to "\\\\n".
- tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to 8.
+ tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to None.
spans (List[Span], optional). A list of predefined style spans. Defaults to None.
"""
@@ -133,7 +148,7 @@ def __init__(
overflow: Optional["OverflowMethod"] = None,
no_wrap: Optional[bool] = None,
end: str = "\n",
- tab_size: Optional[int] = 8,
+ tab_size: Optional[int] = None,
spans: Optional[List[Span]] = None,
) -> None:
sanitized_text = strip_control_codes(text)
@@ -292,7 +307,7 @@ def from_ansi(
overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
no_wrap (bool, optional): Disable text wrapping, or None for default. Defaults to None.
end (str, optional): Character to end text with. Defaults to "\\\\n".
- tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to 8.
+ tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to None.
"""
from .ansi import AnsiDecoder
@@ -354,7 +369,7 @@ def assemble(
justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.
overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
end (str, optional): Character to end text with. Defaults to "\\\\n".
- tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to 8.
+ tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to None.
meta (Dict[str, Any], optional). Meta data to apply to text, or None for no meta data. Default to None
Returns:
@@ -549,6 +564,27 @@ def get_style_at_offset(self, console: "Console", offset: int) -> Style:
style += get_style(span_style, default="")
return style
+ def extend_style(self, spaces: int) -> None:
+ """Extend the Text given number of spaces where the spaces have the same style as the last character.
+
+ Args:
+ spaces (int): Number of spaces to add to the Text.
+ """
+ if spaces <= 0:
+ return
+ spans = self.spans
+ new_spaces = " " * spaces
+ if spans:
+ end_offset = len(self)
+ self._spans[:] = [
+ span.extend(spaces) if span.end >= end_offset else span
+ for span in spans
+ ]
+ self._text.append(new_spaces)
+ self._length += spaces
+ else:
+ self.plain += new_spaces
+
def highlight_regex(
self,
re_highlight: str,
@@ -646,7 +682,7 @@ def set_length(self, new_length: int) -> None:
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> Iterable[Segment]:
- tab_size: int = console.tab_size or self.tab_size or 8
+ tab_size: int = console.tab_size if self.tab_size is None else self.tab_size
justify = self.justify or options.justify or DEFAULT_JUSTIFY
overflow = self.overflow or options.overflow or DEFAULT_OVERFLOW
@@ -781,27 +817,35 @@ def expand_tabs(self, tab_size: Optional[int] = None) -> None:
"""
if "\t" not in self.plain:
return
- pos = 0
if tab_size is None:
tab_size = self.tab_size
assert tab_size is not None
result = self.blank_copy()
- append = result.append
- _style = self.style
+ new_text: List[Text] = []
+ append = new_text.append
+
for line in self.split("\n", include_separator=True):
- parts = line.split("\t", include_separator=True)
- for part in parts:
- if part.plain.endswith("\t"):
- part._text = [part.plain[:-1] + " "]
- append(part)
- pos += len(part)
- spaces = tab_size - ((pos - 1) % tab_size) - 1
- if spaces:
- append(" " * spaces, _style)
- pos += spaces
- else:
+ if "\t" not in line.plain:
+ append(line)
+ else:
+ cell_position = 0
+ parts = line.split("\t", include_separator=True)
+ for part in parts:
+ if part.plain.endswith("\t"):
+ part._text[-1] = part._text[-1][:-1] + " "
+ cell_position += part.cell_len
+ tab_remainder = cell_position % tab_size
+ if tab_remainder:
+ spaces = tab_size - tab_remainder
+ part.extend_style(spaces)
+ cell_position += spaces
+ else:
+ cell_position += part.cell_len
append(part)
+
+ result = Text("").join(new_text)
+
self._text = [result.plain]
self._length = len(self.plain)
self._spans[:] = result._spans
@@ -932,7 +976,7 @@ def append(
self._text.append(sanitized_text)
offset = len(self)
text_length = len(sanitized_text)
- if style is not None:
+ if style:
self._spans.append(Span(offset, offset + text_length, style))
self._length += text_length
elif isinstance(text, Text):
@@ -942,7 +986,7 @@ def append(
"style must not be set when appending Text instance"
)
text_length = self._length
- if text.style is not None:
+ if text.style:
self._spans.append(
_Span(text_length, text_length + len(text), text.style)
)
@@ -963,7 +1007,7 @@ def append_text(self, text: "Text") -> "Text":
"""
_Span = Span
text_length = self._length
- if text.style is not None:
+ if text.style:
self._spans.append(_Span(text_length, text_length + len(text), text.style))
self._text.append(text.plain)
self._spans.extend(
@@ -990,7 +1034,7 @@ def append_tokens(
offset = len(self)
for content, style in tokens:
append_text(content)
- if style is not None:
+ if style:
append_span(_Span(offset, offset + len(content), style))
offset += len(content)
self._length = offset
@@ -1088,7 +1132,6 @@ def divide(self, offsets: Iterable[int]) -> Lines:
_Span = Span
for span_start, span_end, style in self._spans:
-
lower_bound = 0
upper_bound = line_count
start_line_no = (lower_bound + upper_bound) // 2........................................................................ [ 64%] ........................................ [100%] 112 passed in 0.19s