99from typing import TYPE_CHECKING , ClassVar , Iterable , Optional , Sequence , Tuple
1010
1111from rich .console import RenderableType
12+ from rich .segment import Segment
1213from rich .style import Style
1314from rich .text import Text
1415from typing_extensions import Literal
1516
1617from textual ._text_area_theme import TextAreaTheme
1718from textual ._tree_sitter import TREE_SITTER , get_language
19+ from textual .cache import LRUCache
1820from textual .color import Color
1921from textual .document ._document import (
2022 Document ,
@@ -511,6 +513,8 @@ def __init__(
511513 self .set_reactive (TextArea .line_number_start , line_number_start )
512514 self .set_reactive (TextArea .highlight_cursor_line , highlight_cursor_line )
513515
516+ self ._line_cache : LRUCache [tuple , Strip ] = LRUCache (1024 )
517+
514518 self ._set_document (text , language )
515519
516520 self .language = language
@@ -610,6 +614,9 @@ def _get_builtin_highlight_query(language_name: str) -> str:
610614
611615 return highlight_query
612616
617+ def notify_style_update (self ) -> None :
618+ self ._line_cache .clear ()
619+
613620 def check_consume_key (self , key : str , character : str | None = None ) -> bool :
614621 """Check if the widget may consume the given key.
615622
@@ -633,6 +640,7 @@ def check_consume_key(self, key: str, character: str | None = None) -> bool:
633640
634641 def _build_highlight_map (self ) -> None :
635642 """Query the tree for ranges to highlights, and update the internal highlights mapping."""
643+ self ._line_cache .clear ()
636644 highlights = self ._highlights
637645 highlights .clear ()
638646 if not self ._highlight_query :
@@ -1035,6 +1043,7 @@ def wrap_width(self) -> int:
10351043
10361044 def _rewrap_and_refresh_virtual_size (self ) -> None :
10371045 self .wrapped_document .wrap (self .wrap_width , tab_width = self .indent_width )
1046+ self ._line_cache .clear ()
10381047 self ._refresh_size ()
10391048
10401049 @property
@@ -1105,20 +1114,67 @@ def get_line(self, line_index: int) -> Text:
11051114 A `rich.Text` object containing the requested line.
11061115 """
11071116 line_string = self .document .get_line (line_index )
1108- return Text (line_string , end = "" )
1117+ return Text (line_string , end = "" , no_wrap = True )
1118+
1119+ def render_lines (self , crop : Region ) -> list [Strip ]:
1120+ theme = self ._theme
1121+ if theme :
1122+ theme .apply_css (self )
1123+ return super ().render_lines (crop )
11091124
11101125 def render_line (self , y : int ) -> Strip :
11111126 """Render a single line of the TextArea. Called by Textual.
11121127
1128+ Args:
1129+ y: Y Coordinate of line relative to the widget region.
1130+
1131+ Returns:
1132+ A rendered line.
1133+ """
1134+ scroll_x , scroll_y = self .scroll_offset
1135+ absolute_y = scroll_y + y
1136+ selection = self .selection
1137+ cache_key = (
1138+ self .size ,
1139+ scroll_x ,
1140+ absolute_y ,
1141+ (
1142+ selection
1143+ if selection .contains_line (absolute_y )
1144+ else selection .end [0 ] == absolute_y
1145+ ),
1146+ (
1147+ selection .end
1148+ if (
1149+ self ._cursor_visible
1150+ and self .cursor_blink
1151+ and absolute_y == selection .end [0 ]
1152+ )
1153+ else None
1154+ ),
1155+ self .theme ,
1156+ self ._matching_bracket_location ,
1157+ self .match_cursor_bracket ,
1158+ self .soft_wrap ,
1159+ self .show_line_numbers ,
1160+ self .read_only ,
1161+ )
1162+ if (cached_line := self ._line_cache .get (cache_key )) is not None :
1163+ return cached_line
1164+ line = self ._render_line (y )
1165+ self ._line_cache [cache_key ] = line
1166+ return line
1167+
1168+ def _render_line (self , y : int ) -> Strip :
1169+ """Render a single line of the TextArea. Called by Textual.
1170+
11131171 Args:
11141172 y: Y Coordinate of line relative to the widget region.
11151173
11161174 Returns:
11171175 A rendered line.
11181176 """
11191177 theme = self ._theme
1120- if theme :
1121- theme .apply_css (self )
11221178
11231179 wrapped_document = self .wrapped_document
11241180 scroll_x , scroll_y = self .scroll_offset
@@ -1173,7 +1229,8 @@ def render_line(self, y: int) -> Strip:
11731229 if selection_style :
11741230 if line_character_count == 0 and line_index != cursor_row :
11751231 # A simple highlight to show empty lines are included in the selection
1176- line = Text ("▌" , end = "" , style = Style (color = selection_style .bgcolor ))
1232+ line .plain = "▌"
1233+ line .stylize (Style (color = selection_style .bgcolor ))
11771234 else :
11781235 if line_index == selection_top_row == selection_bottom_row :
11791236 # Selection within a single line
@@ -1222,6 +1279,7 @@ def render_line(self, y: int) -> Strip:
12221279 self .has_focus
12231280 and not self .cursor_blink
12241281 or (self .cursor_blink and self ._cursor_visible )
1282+ and not self .read_only
12251283 )
12261284 if draw_matched_brackets :
12271285 matching_bracket_style = theme .bracket_matching_style if theme else None
@@ -1263,13 +1321,11 @@ def render_line(self, y: int) -> Strip:
12631321 gutter_content = (
12641322 str (line_index + self .line_number_start ) if section_offset == 0 else ""
12651323 )
1266- gutter = Text (
1267- f"{ gutter_content :>{gutter_width_no_margin }} " ,
1268- style = gutter_style or "" ,
1269- end = "" ,
1270- )
1324+ gutter = [
1325+ Segment (f"{ gutter_content :>{gutter_width_no_margin }} " , gutter_style )
1326+ ]
12711327 else :
1272- gutter = Text ( "" , end = "" )
1328+ gutter = []
12731329
12741330 # TODO: Lets not apply the division each time through render_line.
12751331 # We should cache sections with the edit counts.
@@ -1304,17 +1360,10 @@ def render_line(self, y: int) -> Strip:
13041360 else max (virtual_width , self .region .size .width )
13051361 )
13061362 target_width = base_width - self .gutter_width
1307- console = self .app .console
1308- gutter_segments = console .render (gutter )
1309-
1310- text_segments = list (
1311- console .render (line , console .options .update_width (target_width ))
1312- )
1313-
1314- gutter_strip = Strip (gutter_segments , cell_length = gutter_width )
1315- text_strip = Strip (text_segments )
13161363
13171364 # Crop the line to show only the visible part (some may be scrolled out of view)
1365+ console = self .app .console
1366+ text_strip = Strip (line .render (console ), cell_length = line .cell_len )
13181367 if not self .soft_wrap :
13191368 text_strip = text_strip .crop (scroll_x , scroll_x + virtual_width )
13201369
@@ -1325,7 +1374,7 @@ def render_line(self, y: int) -> Strip:
13251374 line_style = theme .base_style if theme else None
13261375
13271376 text_strip = text_strip .extend_cell_length (target_width , line_style )
1328- strip = Strip .join ([gutter_strip , text_strip ]). simplify ( )
1377+ strip = Strip .join ([Strip ( gutter , cell_length = gutter_width ), text_strip ])
13291378
13301379 return strip .apply_style (
13311380 theme .base_style
@@ -1645,7 +1694,7 @@ async def _on_mouse_down(self, event: events.MouseDown) -> None:
16451694 # Capture the mouse so that if the cursor moves outside the
16461695 # TextArea widget while selecting, the widget still scrolls.
16471696 self .capture_mouse ()
1648- self ._pause_blink (visible = True )
1697+ self ._pause_blink (visible = False )
16491698 self .history .checkpoint ()
16501699
16511700 async def _on_mouse_move (self , event : events .MouseMove ) -> None :
0 commit comments