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