55from pathlib import Path
66from typing import TYPE_CHECKING , Callable , ClassVar , Iterable , Iterator
77
8- from ..await_complete import AwaitComplete
9-
10- if TYPE_CHECKING :
11- from typing_extensions import Self
12-
138from rich .style import Style
149from rich .text import Text , TextType
1510
1611from .. import work
12+ from ..await_complete import AwaitComplete
1713from ..message import Message
1814from ..reactive import var
1915from ..worker import Worker , WorkerCancelled , WorkerFailed , get_current_worker
2016from ._tree import TOGGLE_STYLE , Tree , TreeNode
2117
18+ if TYPE_CHECKING :
19+ from typing_extensions import Self
20+
2221
2322@dataclass
2423class DirEntry :
@@ -164,7 +163,7 @@ def _add_to_load_queue(self, node: TreeNode[DirEntry]) -> AwaitComplete:
164163
165164 Returns:
166165 An optionally awaitable object that can be awaited until the
167- load queue has finished processing.
166+ load queue has finished processing.
168167 """
169168 assert node .data is not None
170169 if not node .data .loaded :
@@ -174,16 +173,18 @@ def _add_to_load_queue(self, node: TreeNode[DirEntry]) -> AwaitComplete:
174173 return AwaitComplete (self ._load_queue .join ())
175174
176175 def reload (self ) -> AwaitComplete :
177- """Reload the `DirectoryTree` contents."""
178- self .reset (str (self .path ), DirEntry (self .PATH (self .path )))
176+ """Reload the `DirectoryTree` contents.
177+
178+ Returns:
179+ An optionally awaitable that ensures the tree has finished reloading.
180+ """
179181 # Orphan the old queue...
180182 self ._load_queue = Queue ()
183+ # ... reset the root node ...
184+ processed = self .reload_node (self .root )
181185 # ...and replace the old load with a new one.
182186 self ._loader ()
183- # We have a fresh queue, we have a fresh loader, get the fresh root
184- # loading up.
185- queue_processed = self ._add_to_load_queue (self .root )
186- return queue_processed
187+ return processed
187188
188189 def clear_node (self , node : TreeNode [DirEntry ]) -> Self :
189190 """Clear all nodes under the given node.
@@ -215,6 +216,88 @@ def reset_node(
215216 node .data = data
216217 return self
217218
219+ async def _reload (self , node : TreeNode [DirEntry ]) -> None :
220+ """Reloads the subtree rooted at the given node while preserving state.
221+
222+ After reloading the subtree, nodes that were expanded and still exist
223+ will remain expanded and the highlighted node will be preserved, if it
224+ still exists. If it doesn't, highlighting goes up to the first parent
225+ directory that still exists.
226+
227+ Args:
228+ node: The root of the subtree to reload.
229+ """
230+ async with self .lock :
231+ # Track nodes that were expanded before reloading.
232+ currently_open : set [Path ] = set ()
233+ to_check : list [TreeNode [DirEntry ]] = [node ]
234+ while to_check :
235+ checking = to_check .pop ()
236+ if checking .allow_expand and checking .is_expanded :
237+ if checking .data :
238+ currently_open .add (checking .data .path )
239+ to_check .extend (checking .children )
240+
241+ # Track node that was highlighted before reloading.
242+ highlighted_path : None | Path = None
243+ if self .cursor_line > - 1 :
244+ highlighted_node = self .get_node_at_line (self .cursor_line )
245+ if highlighted_node is not None and highlighted_node .data is not None :
246+ highlighted_path = highlighted_node .data .path
247+
248+ if node .data is not None :
249+ self .reset_node (
250+ node , str (node .data .path .name ), DirEntry (self .PATH (node .data .path ))
251+ )
252+
253+ # Reopen nodes that were expanded and still exist.
254+ to_reopen = [node ]
255+ while to_reopen :
256+ reopening = to_reopen .pop ()
257+ if not reopening .data :
258+ continue
259+ if (
260+ reopening .allow_expand
261+ and (reopening .data .path in currently_open or reopening == node )
262+ and reopening .data .path .exists ()
263+ ):
264+ try :
265+ content = await self ._load_directory (reopening ).wait ()
266+ except (WorkerCancelled , WorkerFailed ):
267+ continue
268+ reopening .data .loaded = True
269+ self ._populate_node (reopening , content )
270+ to_reopen .extend (reopening .children )
271+ reopening .expand ()
272+
273+ if highlighted_path is None :
274+ return
275+
276+ # Restore the highlighted path and consider the parents as fallbacks.
277+ looking = [node ]
278+ highlight_candidates = set (highlighted_path .parents )
279+ highlight_candidates .add (highlighted_path )
280+ best_found : None | TreeNode [DirEntry ] = None
281+ while looking :
282+ checking = looking .pop ()
283+ checking_path = (
284+ checking .data .path if checking .data is not None else None
285+ )
286+ if checking_path in highlight_candidates :
287+ best_found = checking
288+ if checking_path == highlighted_path :
289+ break
290+ if (
291+ checking .allow_expand
292+ and checking .is_expanded
293+ and checking_path in highlighted_path .parents
294+ ):
295+ looking .extend (checking .children )
296+ if best_found is not None :
297+ # We need valid lines. Make sure the tree lines have been computed:
298+ _ = self ._tree_lines
299+ self .cursor_line = best_found .line
300+
218301 def reload_node (self , node : TreeNode [DirEntry ]) -> AwaitComplete :
219302 """Reload the given node's contents.
220303
@@ -223,12 +306,12 @@ def reload_node(self, node: TreeNode[DirEntry]) -> AwaitComplete:
223306 or any other nodes).
224307
225308 Args:
226- node: The node to reload.
309+ node: The root of the subtree to reload.
310+
311+ Returns:
312+ An optionally awaitable that ensures the subtree has finished reloading.
227313 """
228- self .reset_node (
229- node , str (node .data .path .name ), DirEntry (self .PATH (node .data .path ))
230- )
231- return self ._add_to_load_queue (node )
314+ return AwaitComplete (self ._reload (node ))
232315
233316 def validate_path (self , path : str | Path ) -> Path :
234317 """Ensure that the path is of the `Path` type.
@@ -415,28 +498,29 @@ async def _loader(self) -> None:
415498 # this blocks if the queue is empty.
416499 node = await self ._load_queue .get ()
417500 content : list [Path ] = []
418- try :
419- # Spin up a short-lived thread that will load the content of
420- # the directory associated with that node.
421- content = await self ._load_directory (node ).wait ()
422- except WorkerCancelled :
423- # The worker was cancelled, that would suggest we're all
424- # done here and we should get out of the loader in general.
425- break
426- except WorkerFailed :
427- # This particular worker failed to start. We don't know the
428- # reason so let's no-op that (for now anyway).
429- pass
430- else :
431- # We're still here and we have directory content, get it into
432- # the tree.
433- if content :
434- self ._populate_node (node , content )
435- finally :
436- # Mark this iteration as done.
437- self ._load_queue .task_done ()
438-
439- async def _on_tree_node_expanded (self , event : Tree .NodeExpanded ) -> None :
501+ async with self .lock :
502+ try :
503+ # Spin up a short-lived thread that will load the content of
504+ # the directory associated with that node.
505+ content = await self ._load_directory (node ).wait ()
506+ except WorkerCancelled :
507+ # The worker was cancelled, that would suggest we're all
508+ # done here and we should get out of the loader in general.
509+ break
510+ except WorkerFailed :
511+ # This particular worker failed to start. We don't know the
512+ # reason so let's no-op that (for now anyway).
513+ pass
514+ else :
515+ # We're still here and we have directory content, get it into
516+ # the tree.
517+ if content :
518+ self ._populate_node (node , content )
519+ finally :
520+ # Mark this iteration as done.
521+ self ._load_queue .task_done ()
522+
523+ async def _on_tree_node_expanded (self , event : Tree .NodeExpanded [DirEntry ]) -> None :
440524 event .stop ()
441525 dir_entry = event .node .data
442526 if dir_entry is None :
@@ -446,7 +530,7 @@ async def _on_tree_node_expanded(self, event: Tree.NodeExpanded) -> None:
446530 else :
447531 self .post_message (self .FileSelected (event .node , dir_entry .path ))
448532
449- def _on_tree_node_selected (self , event : Tree .NodeSelected ) -> None :
533+ def _on_tree_node_selected (self , event : Tree .NodeSelected [ DirEntry ] ) -> None :
450534 event .stop ()
451535 dir_entry = event .node .data
452536 if dir_entry is None :
0 commit comments