← oracle_full

rich_3061

resolved RESOLVED UNSUBMITTED PASS · None tool calls · 0 s · Textualize/rich

Task input

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 calls (0)

#ToolArgumentsResult
No trace captured.

Patch

--- 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

Test output

show
........................................................................ [ 64%]
........................................                                 [100%]
112 passed in 0.19s