Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 65 additions & 27 deletions src/textual/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from textual._cells import cell_len
from textual._context import active_app
from textual._loop import loop_last
from textual.cache import FIFOCache
from textual.color import Color
from textual.css.types import TextAlign, TextOverflow
from textual.selection import Selection
Expand Down Expand Up @@ -140,6 +141,12 @@ def __init__(
self._optimal_width_cache: int | None = None
self._minimal_width_cache: int | None = None
self._height_cache: tuple[tuple[int, str, bool] | None, int] = (None, 0)
self._divide_cache: (
FIFOCache[Sequence[int], list[tuple[Span, int, int]]] | None
) = None
self._split_cache: FIFOCache[tuple[str, bool, bool], list[Content]] | None = (
None
)

def __str__(self) -> str:
return self._text
Expand Down Expand Up @@ -1194,38 +1201,26 @@ def render_segments(
]
return segments

def divide(self, offsets: Sequence[int]) -> list[Content]:
"""Divide the content at the given offsets.

This will cut the content in to pieces, and return those pieces. Note that the number of pieces
return will be one greater than the number of cuts.
def _divide_spans(self, offsets: tuple[int, ...]) -> list[tuple[Span, int, int]]:
"""Divide content from a list of offset to cut.

Args:
offsets: Sequence of offsets (in characters) of where to apply the cuts.
offsets: A tuple of indices in to the text.

Returns:
List of Content instances which combined would be equal to the whole.
A list of tuples containing Spans and their line offsets.
"""
if not offsets:
return [self]
if self._divide_cache is None:
self._divide_cache = FIFOCache(4)
if (cached_result := self._divide_cache.get(offsets)) is not None:
return cached_result

offsets = sorted(offsets)

text = self.plain
text_length = len(text)
divide_offsets = [0, *offsets, text_length]
line_ranges = list(zip(divide_offsets, divide_offsets[1:]))

new_lines = [Content(text[start:end]) for start, end in line_ranges]

if not self._spans:
return new_lines

_line_appends = [line._spans.append for line in new_lines]
line_ranges = list(zip(offsets, offsets[1:]))
text_length = len(self.plain)
line_count = len(line_ranges)
_Span = Span

for span_start, span_end, style in self._spans:
span_ranges: list[tuple[Span, int, int]] = []
for span in self._spans:
span_start, span_end, _style = span
if span_start >= text_length:
continue
span_end = min(text_length, span_end)
Expand Down Expand Up @@ -1259,7 +1254,44 @@ def divide(self, offsets: Sequence[int]) -> list[Content]:
break
end_line_no = (lower_bound + upper_bound) // 2

for line_no in range(start_line_no, end_line_no + 1):
span_ranges.append((span, start_line_no, end_line_no + 1))
self._divide_cache[offsets] = span_ranges
return span_ranges

def divide(self, offsets: Sequence[int]) -> list[Content]:
"""Divide the content at the given offsets.

This will cut the content in to pieces, and return those pieces. Note that the number of pieces
return will be one greater than the number of cuts.

Args:
offsets: Sequence of offsets (in characters) of where to apply the cuts.

Returns:
List of Content instances which combined would be equal to the whole.
"""
if not offsets:
return [self]

offsets = sorted(offsets)
text = self.plain
divide_offsets = tuple([0, *offsets, len(text)])
line_ranges = list(zip(divide_offsets, divide_offsets[1:]))
line_text = [text[start:end] for start, end in line_ranges]
new_lines = [Content(line, None) for line in line_text]

if not self._spans:
return new_lines

_line_appends = [line._spans.append for line in new_lines]
_Span = Span

for (
(span_start, span_end, style),
start_line,
end_line,
) in self._divide_spans(divide_offsets):
for line_no in range(start_line, end_line):
line_start, line_end = line_ranges[line_no]
new_start = max(0, span_start - line_start)
new_end = min(span_end - line_start, line_end - line_start)
Expand All @@ -1286,11 +1318,16 @@ def split(
List[Content]: A list of Content, one per line of the original.
"""
assert separator, "separator must not be empty"

text = self.plain
if separator not in text:
return [self]

cache_key = (separator, include_separator, allow_blank)
if self._split_cache is None:
self._split_cache = FIFOCache(4)
if (cached_result := self._split_cache.get(cache_key)) is not None:
return cached_result.copy()

if include_separator:
lines = self.divide(
[match.end() for match in re.finditer(re.escape(separator), text)],
Expand All @@ -1310,6 +1347,7 @@ def flatten_spans() -> Iterable[int]:
if not allow_blank and text.endswith(separator):
lines.pop()

self._split_cache[cache_key] = lines
return lines

def rstrip(self, chars: str | None = None) -> Content:
Expand Down
Loading