show local and WSL roots in FileChooser dialog immediately; update WSL roots later...
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / fileChooser / tree / FileTreeModel.java
1 // Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.openapi.fileChooser.tree;
3
4 import com.intellij.execution.wsl.WSLDistribution;
5 import com.intellij.execution.wsl.WSLUtil;
6 import com.intellij.execution.wsl.WslDistributionManager;
7 import com.intellij.openapi.application.ApplicationManager;
8 import com.intellij.openapi.application.Experiments;
9 import com.intellij.openapi.diagnostic.Logger;
10 import com.intellij.openapi.fileChooser.FileChooserDescriptor;
11 import com.intellij.openapi.fileChooser.FileElement;
12 import com.intellij.openapi.util.Disposer;
13 import com.intellij.openapi.util.Pair;
14 import com.intellij.openapi.util.io.FileUtil;
15 import com.intellij.openapi.util.text.StringUtil;
16 import com.intellij.openapi.vfs.*;
17 import com.intellij.openapi.vfs.newvfs.BulkFileListener;
18 import com.intellij.openapi.vfs.newvfs.events.*;
19 import com.intellij.openapi.vfs.newvfs.impl.VirtualFileSystemEntry;
20 import com.intellij.ui.tree.MapBasedTree;
21 import com.intellij.ui.tree.MapBasedTree.Entry;
22 import com.intellij.ui.tree.MapBasedTree.UpdateResult;
23 import com.intellij.util.ReflectionUtil;
24 import com.intellij.util.concurrency.Invoker;
25 import com.intellij.util.concurrency.InvokerSupplier;
26 import com.intellij.util.containers.ContainerUtil;
27 import com.intellij.util.ui.tree.AbstractTreeModel;
28 import it.unimi.dsi.fastutil.ints.IntArrayList;
29 import it.unimi.dsi.fastutil.ints.IntList;
30 import org.jetbrains.annotations.NotNull;
31
32 import javax.swing.*;
33 import javax.swing.tree.TreePath;
34 import java.lang.reflect.Method;
35 import java.nio.file.FileSystems;
36 import java.nio.file.Path;
37 import java.util.Arrays;
38 import java.util.Collections;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.stream.Collectors;
42 import java.util.stream.IntStream;
43
44 public final class FileTreeModel extends AbstractTreeModel implements InvokerSupplier {
45
46   private static final Logger LOG = Logger.getInstance(FileTreeModel.class);
47
48   private final Invoker invoker = Invoker.forBackgroundThreadWithReadAction(this);
49   private final State state;
50   private volatile List<Root> roots;
51
52   public FileTreeModel(@NotNull FileChooserDescriptor descriptor, FileRefresher refresher) {
53     this(descriptor, refresher, true, false);
54   }
55
56   public FileTreeModel(@NotNull FileChooserDescriptor descriptor, FileRefresher refresher, boolean sortDirectories, boolean sortArchives) {
57     if (refresher != null) Disposer.register(this, refresher);
58     state = new State(descriptor, refresher, sortDirectories, sortArchives, this);
59     ApplicationManager.getApplication().getMessageBus().connect(this).subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener() {
60       @Override
61       public void after(@NotNull List<? extends @NotNull VFileEvent> events) {
62         invoker.invoke(() -> process(events));
63       }
64     });
65   }
66
67   public void invalidate() {
68     invoker.invoke(() -> {
69       if (roots != null) {
70         for (Root root : roots) {
71           root.tree.invalidate();
72         }
73       }
74       treeStructureChanged(state.path, null, null);
75     });
76   }
77
78   @NotNull
79   @Override
80   public Invoker getInvoker() {
81     return invoker;
82   }
83
84   @Override
85   public Object getRoot() {
86     if (state.path != null) return state;
87     if (roots == null) roots = state.getRoots();
88     return 1 == roots.size() ? roots.get(0) : null;
89   }
90
91   @Override
92   public Object getChild(Object object, int index) {
93     if (object == state) {
94       if (roots == null) roots = state.getRoots();
95       if (0 <= index && index < roots.size()) return roots.get(index);
96     }
97     else if (object instanceof Node) {
98       Entry<Node> entry = getEntry((Node)object, true);
99       if (entry != null) return entry.getChild(index);
100     }
101     return null;
102   }
103
104   @Override
105   public int getChildCount(Object object) {
106     if (object == state) {
107       if (roots == null) roots = state.getRoots();
108       return roots.size();
109     }
110     else if (object instanceof Node) {
111       Entry<Node> entry = getEntry((Node)object, true);
112       if (entry != null) return entry.getChildCount();
113     }
114     return 0;
115   }
116
117   @Override
118   public boolean isLeaf(Object object) {
119     if (object instanceof Node) {
120       Entry<Node> entry = getEntry((Node)object, false);
121       if (entry != null) return entry.isLeaf();
122     }
123     return false;
124   }
125
126   @Override
127   public int getIndexOfChild(Object object, Object child) {
128     if (object == state) {
129       if (roots == null) roots = state.getRoots();
130       for (int i = 0; i < roots.size(); i++) {
131         if (child == roots.get(i)) return i;
132       }
133     }
134     else if (object instanceof Node && child instanceof Node) {
135       Entry<Node> entry = getEntry((Node)object, true);
136       if (entry != null) return entry.getIndexOf((Node)child);
137     }
138     return -1;
139   }
140
141   private boolean hasEntry(VirtualFile file) {
142     if (file == null) return false;
143     if (roots != null) {
144       for (Root root : roots) {
145         Entry<Node> entry = root.tree.findEntry(file);
146         if (entry != null) return true;
147       }
148     }
149     return false;
150   }
151
152   private Entry<Node> getEntry(Node node, boolean loadChildren) {
153     if (roots != null) {
154       for (Root root : roots) {
155         Entry<Node> entry = root.tree.getEntry(node);
156         if (entry != null) {
157           if (loadChildren && entry.isLoadingRequired()) {
158             root.updateChildren(state, entry);
159             //TODO: update updated
160           }
161           return entry;
162         }
163       }
164     }
165     return null;
166   }
167
168   private void process(List<? extends VFileEvent> events) {
169     if (roots == null) return;
170
171     HashSet<VirtualFile> files = new HashSet<>();
172     HashSet<VirtualFile> parents = new HashSet<>();
173     for (VFileEvent event : events) {
174       if (event instanceof VFilePropertyChangeEvent) {
175         if (hasEntry(event.getFile())) files.add(event.getFile());
176       }
177       else if (event instanceof VFileCreateEvent) {
178         VFileCreateEvent create = (VFileCreateEvent)event;
179         if (hasEntry(create.getParent())) parents.add(create.getParent());
180       }
181       else if (event instanceof VFileCopyEvent) {
182         VFileCopyEvent copy = (VFileCopyEvent)event;
183         if (hasEntry(copy.getNewParent())) parents.add(copy.getNewParent());
184       }
185       else if (event instanceof VFileMoveEvent) {
186         VFileMoveEvent move = (VFileMoveEvent)event;
187         if (hasEntry(move.getNewParent())) parents.add(move.getNewParent());
188         if (hasEntry(move.getOldParent())) parents.add(move.getOldParent());
189       }
190       else if (event instanceof VFileDeleteEvent) {
191         VirtualFile file = event.getFile();
192         if (hasEntry(file)) {
193           files.add(file);
194           //TODO:for all roots
195           file = file.getParent();
196           parents.add(hasEntry(file) ? file : null);
197         }
198       }
199     }
200     for (VirtualFile parent : parents) {
201       for (Root root : roots) {
202         Entry<Node> entry = root.tree.findEntry(parent);
203         if (entry != null) {
204           UpdateResult<Node> update = root.updateChildren(state, entry);
205           //TODO:listeners.isEmpty
206           boolean removed = !update.getRemoved().isEmpty();
207           boolean inserted = !update.getInserted().isEmpty();
208           boolean contained = !update.getContained().isEmpty();
209           if (!removed && !inserted && !contained) continue;
210
211           if (!removed && inserted) {
212             if (listeners.isEmpty()) continue;
213             listeners.treeNodesInserted(update.getEvent(this, entry, update.getInserted()));
214             continue;
215           }
216           if (!inserted && removed) {
217             if (listeners.isEmpty()) continue;
218             listeners.treeNodesRemoved(update.getEvent(this, entry, update.getRemoved()));
219             continue;
220           }
221           treeStructureChanged(entry, null, null);
222         }
223       }
224     }
225     for (VirtualFile file : files) {
226       //TODO:update
227     }
228     //TODO:on valid thread / entry - mark as valid
229   }
230
231   private static VirtualFile findFile(String path) {
232     return LocalFileSystem.getInstance().findFileByPath(FileUtil.toSystemIndependentName(path));
233   }
234
235   private static final class State {
236     private final TreePath path;
237     private final FileChooserDescriptor descriptor;
238     private final FileRefresher refresher;
239     private final boolean sortDirectories;
240     private final boolean sortArchives;
241     private final List<VirtualFile> roots;
242     private final FileTreeModel model;
243
244     private State(FileChooserDescriptor descriptor,
245                   FileRefresher refresher,
246                   boolean sortDirectories,
247                   boolean sortArchives,
248                   FileTreeModel model) {
249       this.descriptor = descriptor;
250       this.refresher = refresher;
251       this.sortDirectories = sortDirectories;
252       this.sortArchives = sortArchives;
253       this.roots = getRoots(descriptor);
254       this.path = roots != null && 1 == roots.size() ? null : new TreePath(this);
255       this.model = model;
256     }
257
258     private int compare(VirtualFile one, VirtualFile two) {
259       if (one == null && two == null) return 0;
260       if (one == null) return -1;
261       if (two == null) return 1;
262       if (sortDirectories) {
263         boolean isDirectory = one.isDirectory();
264         if (isDirectory != two.isDirectory()) return isDirectory ? -1 : 1;
265         if (!isDirectory && sortArchives && descriptor.isChooseJarContents()) {
266           boolean isArchive = FileElement.isArchive(one);
267           if (isArchive != FileElement.isArchive(two)) return isArchive ? -1 : 1;
268         }
269       }
270       return StringUtil.naturalCompare(one.getName(), two.getName());
271     }
272
273     private static boolean isValid(VirtualFile file) {
274       return file != null && file.isValid();
275     }
276
277     private boolean isVisible(VirtualFile file) {
278       return isValid(file) && descriptor.isFileVisible(file, descriptor.isShowHiddenFiles());
279     }
280
281     private boolean isLeaf(VirtualFile file) {
282       if (file == null || file.isDirectory()) return false;
283       return !descriptor.isChooseJarContents() || !FileElement.isArchive(file);
284     }
285
286     private VirtualFile[] getChildren(VirtualFile file) {
287       if (!isValid(file)) return null;
288       if (file.isDirectory()) return file.getChildren();
289       if (!descriptor.isChooseJarContents() || !FileElement.isArchive(file)) return null;
290       String path = file.getPath() + JarFileSystem.JAR_SEPARATOR;
291       VirtualFile jar = JarFileSystem.getInstance().findFileByPath(path);
292       return jar == null ? VirtualFile.EMPTY_ARRAY : jar.getChildren();
293     }
294
295     private List<Root> getRoots() {
296       List<VirtualFile> files = roots;
297       if (files == null) files = getSystemRoots();
298       if (files.isEmpty()) return Collections.emptyList();
299       return ContainerUtil.map(files, file -> new Root(this, file));
300     }
301
302     private static List<VirtualFile> getRoots(FileChooserDescriptor descriptor) {
303       List<VirtualFile> list = ContainerUtil.filter(descriptor.getRoots(), State::isValid);
304       return list.isEmpty() && descriptor.isShowFileSystemRoots() ? null : list;
305     }
306
307     private @NotNull List<VirtualFile> getSystemRoots() {
308       List<WSLDistribution> distributions = List.of();
309       if (WSLUtil.isSystemCompatible() && Experiments.getInstance().isFeatureEnabled("wsl.p9.show.roots.in.file.chooser")) {
310         WslDistributionManager distributionManager = WslDistributionManager.getInstance();
311         List<WSLDistribution> lastDistributions = ContainerUtil.notNullize(distributionManager.getLastInstalledDistributions());
312         distributions = lastDistributions;
313         LOG.debug("WSL distributions: ", distributions);
314         distributionManager.getInstalledDistributionsFuture().thenAccept(newDistributions -> {
315           // called on a background thread without read action
316           if (newDistributions.equals(lastDistributions)) {
317             LOG.debug("WSL distributions are up-to-date");
318             return;
319           }
320           LOG.debug("New WSL distributions: ", newDistributions);
321           model.invoker.invokeLater(() -> {
322             // invokeLater to ensure model.roots are calculated
323             setRoots(getLocalAndWslRoots(newDistributions));
324           });
325         });
326       }
327       return getLocalAndWslRoots(distributions);
328     }
329
330     private static @NotNull List<VirtualFile> getLocalAndWslRoots(@NotNull List<WSLDistribution> distributions) {
331       return toVirtualFiles(ContainerUtil.concat(ContainerUtil.newArrayList(FileSystems.getDefault().getRootDirectories()),
332                                                  ContainerUtil.map(distributions, WSLDistribution::getUNCRootPath)));
333     }
334
335     private void setRoots(@NotNull List<VirtualFile> newRootFiles) {
336       List<Root> oldRoots = model.roots;
337       if (oldRoots == null) {
338         LOG.error("Roots have not been calculated yet, new roots won't be set due to a possible race condition");
339         return;
340       }
341       List<VirtualFile> oldRootFiles = toRootFiles(oldRoots);
342       if (LOG.isDebugEnabled()) {
343         LOG.debug("New roots: " + newRootFiles + ", old roots: " + oldRootFiles);
344       }
345       removeRoots(oldRoots, findNewElementIndices(oldRootFiles, newRootFiles));
346       List<Root> rootsToAdd = newRootFiles.stream().filter(root -> !oldRootFiles.contains(root))
347         .map(root -> new Root(this, root)).collect(Collectors.toList());
348       addRoots(model.roots, rootsToAdd);
349     }
350
351     private static @NotNull List<VirtualFile> toRootFiles(@NotNull List<Root> roots) {
352       return ContainerUtil.map(roots, FileNode::getFile);
353     }
354
355     private void removeRoots(@NotNull List<Root> roots, int[] indicesToRemove) {
356       if (indicesToRemove.length > 0) {
357         List<Root> rootsToRemove = Arrays.stream(indicesToRemove).mapToObj(ind -> roots.get(ind)).collect(Collectors.toList());
358         if (LOG.isDebugEnabled()) {
359           LOG.debug("Removing " + toRootFiles(rootsToRemove));
360         }
361         model.roots = ContainerUtil.filter(roots, root -> !rootsToRemove.contains(root));
362         model.treeNodesRemoved(path, indicesToRemove, rootsToRemove.toArray());
363       }
364     }
365
366     private void addRoots(@NotNull List<Root> roots, @NotNull List<Root> rootsToAdd) {
367       if (rootsToAdd.size() > 0) {
368         if (LOG.isDebugEnabled()) {
369           LOG.debug("Adding " + toRootFiles(rootsToAdd));
370         }
371         model.roots = List.copyOf(ContainerUtil.concat(roots, rootsToAdd));
372         model.treeNodesInserted(path, IntStream.range(roots.size(), roots.size() + rootsToAdd.size()).toArray(), rootsToAdd.toArray());
373       }
374     }
375
376     private static <E> int[] findNewElementIndices(@NotNull List<E> a, @NotNull List<E> b) {
377       IntList newIndices = new IntArrayList();
378       for (int i = 0; i < a.size(); i++) {
379         if (!b.contains(a.get(i))) {
380           newIndices.add(i);
381         }
382       }
383       return newIndices.toIntArray();
384     }
385
386     private static @NotNull List<VirtualFile> toVirtualFiles(@NotNull List<Path> paths) {
387       return paths.stream().map(root -> LocalFileSystem.getInstance().findFileByNioFile(root)).filter(State::isValid).collect(
388         Collectors.toList());
389     }
390
391     @Override
392     public String toString() {
393       return descriptor.getTitle();
394     }
395   }
396
397   private static class Node extends FileNode {
398     private boolean invalid;
399
400     private Node(State state, VirtualFile file) {
401       super(file);
402       if (state.refresher != null && !state.refresher.isRecursive()) state.refresher.register(file);
403       updateContent(state);
404     }
405
406     private boolean updateContent(State state) {
407       VirtualFile file = getFile();
408       if (file == null) return updateName(state.descriptor.getTitle());
409
410       Icon icon = state.descriptor.getIcon(file);
411       String name = state.descriptor.getName(file);
412       String comment = state.descriptor.getComment(file);
413       if (name == null || comment == null) name = file.getPresentableName();
414
415       boolean updated = false;
416       if (updateIcon(icon)) updated = true;
417       if (updateName(name)) updated = true;
418       if (updateComment(comment)) updated = true;
419       if (updateValid(file.isValid())) updated = true;
420       if (updateHidden(FileElement.isFileHidden(file))) updated = true;
421       if (updateSpecial(file.is(VFileProperty.SPECIAL))) updated = true;
422       if (updateSymlink(file.is(VFileProperty.SYMLINK))) updated = true;
423       if (updateWritable(file.isWritable())) updated = true;
424       return updated;
425     }
426
427     @Override
428     public String toString() {
429       return getName();
430     }
431   }
432
433   private static final class Root extends Node {
434     private final MapBasedTree<VirtualFile, Node> tree;
435
436     private Root(State state, VirtualFile file) {
437       super(state, file);
438       if (state.refresher != null && state.refresher.isRecursive()) state.refresher.register(file);
439       tree = new MapBasedTree<>(false, node -> node.getFile(), state.path);
440       tree.onInsert(node -> markDirtyInternal(node.getFile()));
441       tree.updateRoot(Pair.create(this, state.isLeaf(file)));
442     }
443
444     private static void markDirtyInternal(VirtualFile file) {
445       if (file instanceof VirtualFileSystemEntry) {
446         Method method = ReflectionUtil.getDeclaredMethod(VirtualFileSystemEntry.class, "markDirtyInternal");
447         if (method != null) {
448           try {
449             method.invoke(file);
450           }
451           catch (Exception ignore) {
452           }
453         }
454       }
455     }
456
457     private UpdateResult<Node> updateChildren(State state, Entry<Node> parent) {
458       VirtualFile[] children = state.getChildren(parent.getNode().getFile());
459       if (children == null) return tree.update(parent, null);
460       if (children.length == 0) return tree.update(parent, Collections.emptyList());
461       return tree.update(parent, Arrays.stream(children).filter(state::isVisible).sorted(state::compare).map(file -> {
462         Entry<Node> entry = tree.findEntry(file);
463         return entry != null && parent == entry.getParentPath()
464                ? Pair.create(entry.getNode(), entry.isLeaf())
465                : Pair.create(new Node(state, file), state.isLeaf(file));
466       }).collect(Collectors.toList()));
467     }
468   }
469 }