3
3
from abc import ABC , abstractmethod
4
4
from dataclasses import dataclass
5
5
from functools import lru_cache
6
+ from itertools import zip_longest
6
7
from typing import TYPE_CHECKING , Callable , NamedTuple , Tuple , overload
7
8
8
9
from typing_extensions import Literal , get_args
9
10
10
11
if TYPE_CHECKING :
11
- from tree_sitter import Node , Query
12
+ from tree_sitter import Query
12
13
13
14
from textual ._cells import cell_len
14
15
from textual .geometry import Size
@@ -27,6 +28,10 @@ class EditResult:
27
28
"""The new end Location after the edit is complete."""
28
29
replaced_text : str
29
30
"""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."""
30
35
31
36
32
37
@lru_cache (maxsize = 1024 )
@@ -146,28 +151,6 @@ def clean_up(self) -> None:
146
151
The default implementation does nothing.
147
152
"""
148
153
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
-
171
154
def set_syntax_tree_update_callback (
172
155
callback : Callable [[], None ],
173
156
) -> None :
@@ -262,6 +245,10 @@ def newline(self) -> Newline:
262
245
"""Get the Newline used in this document (e.g. '\r \n ', '\n '. etc.)"""
263
246
return self ._newline
264
247
248
+ def copy_of_lines (self ):
249
+ """Provide a copy of the document's lines."""
250
+ return list (self ._lines )
251
+
265
252
def get_size (self , tab_width : int ) -> Size :
266
253
"""The Size of the document, taking into account the tab rendering width.
267
254
@@ -321,11 +308,40 @@ def replace_range(self, start: Location, end: Location, text: str) -> EditResult
321
308
destination_column = len (before_selection )
322
309
insert_lines = [before_selection + after_selection ]
323
310
311
+ try :
312
+ prev_top_line = lines [top_row ]
313
+ except IndexError :
314
+ prev_top_line = None
324
315
lines [top_row : bottom_row + 1 ] = insert_lines
325
316
destination_row = top_row + len (insert_lines ) - 1
326
317
327
318
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
329
345
330
346
def get_text_range (self , start : Location , end : Location ) -> str :
331
347
"""Get the text that falls between the start and end locations.
0 commit comments