33from abc import ABC , abstractmethod
44from dataclasses import dataclass
55from functools import lru_cache
6+ from itertools import zip_longest
67from typing import TYPE_CHECKING , Callable , NamedTuple , Tuple , overload
78
89from typing_extensions import Literal , get_args
910
1011if TYPE_CHECKING :
11- from tree_sitter import Node , Query
12+ from tree_sitter import Query
1213
1314from textual ._cells import cell_len
1415from textual .geometry import Size
@@ -27,6 +28,10 @@ class EditResult:
2728 """The new end Location after the edit is complete."""
2829 replaced_text : str
2930 """The text that was replaced."""
31+ dirty_lines : range | None = None
32+ """The range of lines considered dirty."""
33+ alt_dirty_line : tuple [int , range ] | None = None
34+ """Alternative list of lines considered dirty."""
3035
3136
3237@lru_cache (maxsize = 1024 )
@@ -146,28 +151,6 @@ def clean_up(self) -> None:
146151 The default implementation does nothing.
147152 """
148153
149- def query_syntax_tree (
150- self ,
151- query : "Query" ,
152- start_point : tuple [int , int ] | None = None ,
153- end_point : tuple [int , int ] | None = None ,
154- ) -> dict [str , list ["Node" ]]:
155- """Query the tree-sitter syntax tree.
156-
157- The default implementation always returns an empty list.
158-
159- To support querying in a subclass, this must be implemented.
160-
161- Args:
162- query: The tree-sitter Query to perform.
163- start_point: The (row, column byte) to start the query at.
164- end_point: The (row, column byte) to end the query at.
165-
166- Returns:
167- A dict mapping captured node names to lists of Nodes with that name.
168- """
169- return {}
170-
171154 def set_syntax_tree_update_callback (
172155 callback : Callable [[], None ],
173156 ) -> None :
@@ -262,6 +245,10 @@ def newline(self) -> Newline:
262245 """Get the Newline used in this document (e.g. '\r \n ', '\n '. etc.)"""
263246 return self ._newline
264247
248+ def copy_of_lines (self ):
249+ """Provide a copy of the document's lines."""
250+ return list (self ._lines )
251+
265252 def get_size (self , tab_width : int ) -> Size :
266253 """The Size of the document, taking into account the tab rendering width.
267254
@@ -321,11 +308,40 @@ def replace_range(self, start: Location, end: Location, text: str) -> EditResult
321308 destination_column = len (before_selection )
322309 insert_lines = [before_selection + after_selection ]
323310
311+ try :
312+ prev_top_line = lines [top_row ]
313+ except IndexError :
314+ prev_top_line = None
324315 lines [top_row : bottom_row + 1 ] = insert_lines
325316 destination_row = top_row + len (insert_lines ) - 1
326317
327318 end_location = (destination_row , destination_column )
328- return EditResult (end_location , replaced_text )
319+
320+ n_previous_lines = bottom_row - top_row + 1
321+ dirty_range = None
322+ alt_dirty_line = None
323+ if len (insert_lines ) != n_previous_lines :
324+ dirty_range = range (top_row , len (lines ))
325+ else :
326+ if len (insert_lines ) == 1 and prev_top_line is not None :
327+ rng = self ._build_single_line_range (prev_top_line , insert_lines [0 ])
328+ if rng is not None :
329+ alt_dirty_line = top_row , rng
330+ else :
331+ dirty_range = range (top_row , bottom_row + 1 )
332+
333+ return EditResult (end_location , replaced_text , dirty_range , alt_dirty_line )
334+
335+ @staticmethod
336+ def _build_single_line_range (a , b ):
337+ rng = []
338+ for i , (ca , cb ) in enumerate (zip_longest (a , b )):
339+ if ca != cb :
340+ rng .append (i )
341+ if rng :
342+ return range (rng [0 ], rng [- 1 ] + 1 )
343+ else :
344+ None
329345
330346 def get_text_range (self , start : Location , end : Location ) -> str :
331347 """Get the text that falls between the start and end locations.
0 commit comments