Skip to content

Commit ec0c050

Browse files
committed
line cache
1 parent 5f8cbcd commit ec0c050

File tree

2 files changed

+51
-7
lines changed

2 files changed

+51
-7
lines changed

src/textual/document/_document.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,3 +466,8 @@ def is_empty(self) -> bool:
466466
"""Return True if the selection has 0 width, i.e. it's just a cursor."""
467467
start, end = self
468468
return start == end
469+
470+
def contains_line(self, y: int) -> bool:
471+
"""Check if the given line is within the selection."""
472+
top, bottom = sorted((self.start[0], self.end[0]))
473+
return y >= top and y <= bottom

src/textual/widgets/_text_area.py

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from textual._text_area_theme import TextAreaTheme
1818
from textual._tree_sitter import TREE_SITTER, get_language
19+
from textual.cache import LRUCache
1920
from textual.color import Color
2021
from textual.document._document import (
2122
Document,
@@ -512,6 +513,8 @@ def __init__(
512513
self.set_reactive(TextArea.line_number_start, line_number_start)
513514
self.set_reactive(TextArea.highlight_cursor_line, highlight_cursor_line)
514515

516+
self._line_cache: LRUCache[tuple, Strip] = LRUCache(1024)
517+
515518
self._set_document(text, language)
516519

517520
self.language = language
@@ -611,6 +614,9 @@ def _get_builtin_highlight_query(language_name: str) -> str:
611614

612615
return highlight_query
613616

617+
def notify_style_update(self) -> None:
618+
self._line_cache.clear()
619+
614620
def check_consume_key(self, key: str, character: str | None = None) -> bool:
615621
"""Check if the widget may consume the given key.
616622
@@ -634,6 +640,7 @@ def check_consume_key(self, key: str, character: str | None = None) -> bool:
634640

635641
def _build_highlight_map(self) -> None:
636642
"""Query the tree for ranges to highlights, and update the internal highlights mapping."""
643+
self._line_cache.clear()
637644
highlights = self._highlights
638645
highlights.clear()
639646
if not self._highlight_query:
@@ -838,6 +845,8 @@ def _set_theme(self, theme: str) -> None:
838845
if background:
839846
self.styles.background = Color.from_rich_color(background)
840847

848+
theme_object.apply_css(self)
849+
841850
@property
842851
def available_themes(self) -> set[str]:
843852
"""A list of the names of the themes available to the `TextArea`.
@@ -1108,15 +1117,42 @@ def get_line(self, line_index: int) -> Text:
11081117
line_string = self.document.get_line(line_index)
11091118
return Text(line_string, end="")
11101119

1111-
def render_lines(self, crop: Region) -> list[Strip]:
1112-
theme = self._theme
1113-
if theme:
1114-
theme.apply_css(self)
1115-
return super().render_lines(crop)
1116-
11171120
def render_line(self, y: int) -> Strip:
11181121
"""Render a single line of the TextArea. Called by Textual.
11191122
1123+
Args:
1124+
y: Y Coordinate of line relative to the widget region.
1125+
1126+
Returns:
1127+
A rendered line.
1128+
"""
1129+
scroll_x, scroll_y = self.scroll_offset
1130+
absolute_y = scroll_y + y
1131+
selection = self.selection
1132+
cache_key = (
1133+
self.size,
1134+
scroll_x,
1135+
absolute_y,
1136+
(
1137+
selection
1138+
if selection.contains_line(absolute_y)
1139+
else selection.end[0] == absolute_y
1140+
),
1141+
self._cursor_visible,
1142+
self.cursor_blink,
1143+
self.theme,
1144+
self._matching_bracket_location,
1145+
self.match_cursor_bracket,
1146+
)
1147+
if (cached_line := self._line_cache.get(cache_key)) is not None:
1148+
return cached_line
1149+
line = self._render_line(y)
1150+
self._line_cache[cache_key] = line
1151+
return line
1152+
1153+
def _render_line(self, y: int) -> Strip:
1154+
"""Render a single line of the TextArea. Called by Textual.
1155+
11201156
Args:
11211157
y: Y Coordinate of line relative to the widget region.
11221158
@@ -1310,7 +1346,10 @@ def render_line(self, y: int) -> Strip:
13101346

13111347
# Crop the line to show only the visible part (some may be scrolled out of view)
13121348
console = self.app.console
1313-
text_strip = Strip(console.render(line), cell_length=line.cell_len)
1349+
text_strip = Strip(
1350+
console.render(line, options=console.options.update_width(line.cell_len)),
1351+
cell_length=line.cell_len,
1352+
)
13141353
if not self.soft_wrap:
13151354
text_strip = text_strip.crop(scroll_x, scroll_x + virtual_width)
13161355

0 commit comments

Comments
 (0)