2a5ff90be38deda28c0ec487a46f590b0164a125
[idea/community.git] / platform / vcs-impl / src / com / intellij / openapi / vcs / changes / ui / ChangesListView.java
1 // Copyright 2000-2020 JetBrains s.r.o. 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.vcs.changes.ui;
3
4 import com.intellij.ide.dnd.DnDAware;
5 import com.intellij.ide.util.PsiNavigationSupport;
6 import com.intellij.ide.util.treeView.TreeState;
7 import com.intellij.openapi.actionSystem.*;
8 import com.intellij.openapi.fileChooser.actions.VirtualFileDeleteProvider;
9 import com.intellij.openapi.project.Project;
10 import com.intellij.openapi.util.registry.Registry;
11 import com.intellij.openapi.vcs.FilePath;
12 import com.intellij.openapi.vcs.FileStatus;
13 import com.intellij.openapi.vcs.VcsDataKeys;
14 import com.intellij.openapi.vcs.changes.*;
15 import com.intellij.openapi.vfs.VirtualFile;
16 import com.intellij.ui.PopupHandler;
17 import com.intellij.util.containers.ContainerUtil;
18 import com.intellij.util.containers.JBIterable;
19 import com.intellij.util.ui.tree.TreeUtil;
20 import com.intellij.vcsUtil.VcsUtil;
21 import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet;
22 import one.util.streamex.StreamEx;
23 import org.jetbrains.annotations.NonNls;
24 import org.jetbrains.annotations.NotNull;
25 import org.jetbrains.annotations.Nullable;
26
27 import javax.swing.*;
28 import javax.swing.tree.DefaultMutableTreeNode;
29 import javax.swing.tree.DefaultTreeModel;
30 import javax.swing.tree.TreePath;
31 import java.awt.*;
32 import java.awt.event.MouseEvent;
33 import java.util.List;
34 import java.util.*;
35 import java.util.stream.Stream;
36
37 import static com.intellij.openapi.vcs.changes.ChangesUtil.getNavigatableArray;
38 import static com.intellij.openapi.vcs.changes.ChangesUtil.getPathsCaseSensitive;
39 import static com.intellij.openapi.vcs.changes.ui.ChangesBrowserNode.*;
40 import static com.intellij.vcs.commit.ChangesViewCommitPanelKt.subtreeRootObject;
41
42 // TODO: Check if we could extend DnDAwareTree here instead of directly implementing DnDAware
43 public class ChangesListView extends ChangesTree implements DataProvider, DnDAware {
44   @NonNls public static final String HELP_ID = "ideaInterface.changes";
45   @NonNls public static final DataKey<ChangesListView> DATA_KEY = DataKey.create("ChangeListView");
46   @NonNls public static final DataKey<Iterable<FilePath>> UNVERSIONED_FILE_PATHS_DATA_KEY = DataKey.create("ChangeListView.UnversionedFiles");
47   @NonNls public static final DataKey<Iterable<VirtualFile>> EXACTLY_SELECTED_FILES_DATA_KEY = DataKey.create("ChangeListView.ExactlySelectedFiles");
48   @NonNls public static final DataKey<Iterable<FilePath>> IGNORED_FILE_PATHS_DATA_KEY = DataKey.create("ChangeListView.IgnoredFiles");
49   @NonNls public static final DataKey<List<FilePath>> MISSING_FILES_DATA_KEY = DataKey.create("ChangeListView.MissingFiles");
50   @NonNls public static final DataKey<List<LocallyDeletedChange>> LOCALLY_DELETED_CHANGES = DataKey.create("ChangeListView.LocallyDeletedChanges");
51
52   public ChangesListView(@NotNull Project project, boolean showCheckboxes) {
53     super(project, showCheckboxes, true);
54
55     setDragEnabled(true);
56   }
57
58   @NotNull
59   @Override
60   protected ChangesGroupingSupport installGroupingSupport() {
61     return new ChangesGroupingSupport(myProject, this, true);
62   }
63
64   @Override
65   public int getToggleClickCount() {
66     return 2;
67   }
68
69   @Override
70   protected boolean isInclusionVisible(@NotNull ChangesBrowserNode<?> node) {
71     Object subtreeRootObject = subtreeRootObject(node);
72
73     if (subtreeRootObject instanceof LocalChangeList) return !((LocalChangeList)subtreeRootObject).getChanges().isEmpty();
74     if (subtreeRootObject == UNVERSIONED_FILES_TAG) return true;
75     return false;
76   }
77
78   @Override
79   public DefaultTreeModel getModel() {
80     return (DefaultTreeModel)super.getModel();
81   }
82
83   public void updateModel(@NotNull DefaultTreeModel newModel) {
84     TreeState state = TreeState.createOn(this, getRoot());
85     state.setScrollToSelection(false);
86     ChangesBrowserNode<?> oldRoot = getRoot();
87     setModel(newModel);
88     ChangesBrowserNode<?> newRoot = getRoot();
89     state.applyTo(this, newRoot);
90
91     initTreeStateIfNeeded(oldRoot, newRoot);
92   }
93
94   @Override
95   public void rebuildTree() {
96     // currently not used in ChangesListView code flow
97   }
98
99   private void initTreeStateIfNeeded(ChangesBrowserNode<?> oldRoot, ChangesBrowserNode<?> newRoot) {
100     ChangesBrowserNode<?> defaultListNode = getDefaultChangelistNode(newRoot);
101     if (defaultListNode == null) return;
102
103     if (getSelectionCount() == 0) {
104       TreeUtil.selectNode(this, defaultListNode);
105     }
106
107     if (oldRoot.getFileCount() == 0 && TreeUtil.collectExpandedPaths(this).size() == 0) {
108       expandSafe(defaultListNode);
109     }
110   }
111
112   @Nullable
113   private static ChangesBrowserNode<?> getDefaultChangelistNode(@NotNull ChangesBrowserNode<?> root) {
114     @SuppressWarnings({"unchecked", "rawtypes"})
115     Enumeration<ChangesBrowserNode<?>> children = (Enumeration)root.children();
116     Iterator<ChangesBrowserNode<?>> nodes = ContainerUtil.iterate(children);
117     return ContainerUtil.find(nodes, node -> {
118       if (node instanceof ChangesBrowserChangeListNode) {
119         ChangeList list = ((ChangesBrowserChangeListNode)node).getUserObject();
120         return list instanceof LocalChangeList && ((LocalChangeList)list).isDefault();
121       }
122       return false;
123     });
124   }
125
126   @Nullable
127   @Override
128   public Object getData(@NotNull String dataId) {
129     if (DATA_KEY.is(dataId)) {
130       return this;
131     }
132     if (VcsDataKeys.CHANGES.is(dataId)) {
133       return getSelectedChanges().toList().toArray(Change[]::new);
134     }
135     if (VcsDataKeys.CHANGE_LEAD_SELECTION.is(dataId)) {
136       return getLeadSelection().toList().toArray(Change[]::new);
137     }
138     if (VcsDataKeys.CHANGE_LISTS.is(dataId)) {
139       return getSelectedChangeLists().toList().toArray(ChangeList[]::new);
140     }
141     if (CommonDataKeys.VIRTUAL_FILE_ARRAY.is(dataId)) {
142       return getSelectedFiles().toList().toArray(VirtualFile[]::new);
143     }
144     if (VcsDataKeys.VIRTUAL_FILES.is(dataId)) {
145       return getSelectedFiles();
146     }
147     if (VcsDataKeys.FILE_PATHS.is(dataId)) {
148       return getSelectedFilePaths();
149     }
150     if (CommonDataKeys.NAVIGATABLE.is(dataId)) {
151       VirtualFile file = getNavigatableFiles().single();
152       return file != null && !file.isDirectory() ? PsiNavigationSupport.getInstance()
153                                                                        .createNavigatable(myProject, file, 0) : null;
154     }
155     if (CommonDataKeys.NAVIGATABLE_ARRAY.is(dataId)) {
156       return getNavigatableArray(myProject, StreamEx.of(getNavigatableFiles().iterator()));
157     }
158     if (PlatformDataKeys.DELETE_ELEMENT_PROVIDER.is(dataId)) {
159       return getSelectionObjects().find(userObject -> !(userObject instanceof ChangeList)) != null
160              ? new VirtualFileDeleteProvider()
161              : null;
162     }
163     if (UNVERSIONED_FILE_PATHS_DATA_KEY.is(dataId)) {
164       return getSelectedUnversionedFiles();
165     }
166     if (EXACTLY_SELECTED_FILES_DATA_KEY.is(dataId)) {
167       return getExactlySelectedVirtualFiles(this);
168     }
169     if (IGNORED_FILE_PATHS_DATA_KEY.is(dataId)) {
170       return getSelectedIgnoredFiles();
171     }
172     if (VcsDataKeys.MODIFIED_WITHOUT_EDITING_DATA_KEY.is(dataId)) {
173       return getSelectedModifiedWithoutEditing().toList();
174     }
175     if (LOCALLY_DELETED_CHANGES.is(dataId)) {
176       return getSelectedLocallyDeletedChanges().toList();
177     }
178     if (MISSING_FILES_DATA_KEY.is(dataId)) {
179       return getSelectedMissingFiles().toList();
180     }
181     if (VcsDataKeys.HAVE_LOCALLY_DELETED.is(dataId)) {
182       return getSelectedMissingFiles().isNotEmpty();
183     }
184     if (VcsDataKeys.HAVE_MODIFIED_WITHOUT_EDITING.is(dataId)) {
185       return getSelectedModifiedWithoutEditing().isNotEmpty();
186     }
187     if (VcsDataKeys.HAVE_SELECTED_CHANGES.is(dataId)) {
188       return getSelectedChanges().isNotEmpty();
189     }
190     if (PlatformDataKeys.HELP_ID.is(dataId)) {
191       return HELP_ID;
192     }
193     return super.getData(dataId);
194   }
195
196   @NotNull
197   public Stream<FilePath> getUnversionedFiles() {
198     ChangesBrowserUnversionedFilesNode node = TreeUtil.nodeChildren(getRoot())
199       .filter(ChangesBrowserUnversionedFilesNode.class).first();
200     if (node == null) return StreamEx.empty();
201     return node.getFilePathsUnderStream();
202   }
203
204   @NotNull
205   static JBIterable<FilePath> getSelectedUnversionedFiles(@NotNull JTree tree) {
206     return getSelectedFilePaths(tree, UNVERSIONED_FILES_TAG);
207   }
208
209   @NotNull
210   public JBIterable<FilePath> getSelectedUnversionedFiles() {
211     return getSelectedUnversionedFiles(this);
212   }
213
214   @NotNull
215   private JBIterable<FilePath> getSelectedIgnoredFiles() {
216     return getSelectedFilePaths(IGNORED_FILES_TAG);
217   }
218
219   @NotNull
220   private JBIterable<VirtualFile> getSelectedModifiedWithoutEditing() {
221     return getSelectedVirtualFiles(MODIFIED_WITHOUT_EDITING_TAG);
222   }
223
224   @NotNull
225   protected JBIterable<VirtualFile> getSelectedVirtualFiles(@Nullable Object tag) {
226     return getSelectionNodes(this, tag)
227       .flatMap(node -> JBIterable.create(() -> node.getFilesUnderStream().iterator()))
228       .unique();
229   }
230
231   @NotNull
232   protected JBIterable<FilePath> getSelectedFilePaths(@Nullable Object tag) {
233     return getSelectedFilePaths(this, tag);
234   }
235
236   @NotNull
237   private static JBIterable<FilePath> getSelectedFilePaths(@NotNull JTree tree, @Nullable Object tag) {
238     return getSelectionNodes(tree, tag)
239       .flatMap(node -> JBIterable.create(() -> node.getFilePathsUnderStream().iterator()))
240       .unique();
241   }
242
243   @NotNull
244   static JBIterable<VirtualFile> getExactlySelectedVirtualFiles(@NotNull JTree tree) {
245     VcsTreeModelData exactlySelected = VcsTreeModelData.exactlySelected(tree);
246
247     return JBIterable.create(() -> exactlySelected.rawUserObjectsStream().iterator()).map(object -> {
248       if (object instanceof VirtualFile) return (VirtualFile)object;
249       if (object instanceof FilePath) return ((FilePath)object).getVirtualFile();
250       return null;
251     }).filter(Objects::nonNull);
252   }
253
254   @NotNull
255   private JBIterable<ChangesBrowserNode<?>> getSelectionNodes() {
256     return getSelectionNodes(this, null);
257   }
258
259   @NotNull
260   private static JBIterable<ChangesBrowserNode<?>> getSelectionNodes(@NotNull JTree tree, @Nullable Object tag) {
261     return JBIterable.of(tree.getSelectionPaths())
262       .filter(path -> isUnderTag(path, tag))
263       .map(TreePath::getLastPathComponent)
264       .map(node -> ((ChangesBrowserNode<?>)node));
265   }
266
267   @NotNull
268   private JBIterable<Object> getSelectionObjects() {
269     return getSelectionNodes().map(ChangesBrowserNode::getUserObject);
270   }
271
272   @NotNull
273   static JBIterable<VirtualFile> getVirtualFiles(TreePath @Nullable [] paths, @Nullable Object tag) {
274     return JBIterable.of(paths)
275       .filter(path -> isUnderTag(path, tag))
276       .map(TreePath::getLastPathComponent)
277       .map(node -> ((ChangesBrowserNode<?>)node))
278       .flatMap(node -> JBIterable.create(() -> node.getFilesUnderStream().iterator()))
279       .unique();
280   }
281
282   @NotNull
283   static JBIterable<FilePath> getFilePaths(TreePath @Nullable [] paths, @Nullable Object tag) {
284     return JBIterable.of(paths)
285       .filter(path -> isUnderTag(path, tag))
286       .map(TreePath::getLastPathComponent)
287       .map(node -> ((ChangesBrowserNode<?>)node))
288       .flatMap(node -> JBIterable.create(() -> node.getFilePathsUnderStream().iterator()))
289       .unique();
290   }
291
292   static boolean isUnderTag(@NotNull TreePath path, @Nullable Object tag) {
293     boolean result = true;
294
295     if (tag != null) {
296       result = path.getPathCount() > 1 && ((ChangesBrowserNode<?>)path.getPathComponent(1)).getUserObject() == tag;
297     }
298
299     return result;
300   }
301
302   @NotNull
303   static JBIterable<Change> getChanges(@NotNull Project project, TreePath @Nullable [] paths) {
304     JBIterable<Change> changes = JBIterable.of(paths)
305       .map(TreePath::getLastPathComponent)
306       .map(node -> ((ChangesBrowserNode<?>)node))
307       .flatMap(node -> node.traverseObjectsUnder())
308       .filter(Change.class);
309     JBIterable<Change> hijackedChanges = getVirtualFiles(paths, MODIFIED_WITHOUT_EDITING_TAG)
310       .map(file -> toHijackedChange(project, file))
311       .filter(Objects::nonNull);
312
313     return changes.append(hijackedChanges)
314       .filter(new DistinctChangePredicate());
315   }
316
317   @Nullable
318   private static Change toHijackedChange(@NotNull Project project, @NotNull VirtualFile file) {
319     VcsCurrentRevisionProxy before = VcsCurrentRevisionProxy.create(file, project);
320     if (before != null) {
321       ContentRevision afterRevision = new CurrentContentRevision(VcsUtil.getFilePath(file));
322       return new Change(before, afterRevision, FileStatus.HIJACKED);
323     }
324     return null;
325   }
326
327   @NotNull
328   private JBIterable<LocallyDeletedChange> getSelectedLocallyDeletedChanges() {
329     return getSelectionNodes(this, LOCALLY_DELETED_NODE_TAG)
330       .flatMap(node -> node.traverseObjectsUnder())
331       .filter(LocallyDeletedChange.class)
332       .unique();
333   }
334
335   @NotNull
336   private JBIterable<FilePath> getSelectedMissingFiles() {
337     return getSelectedLocallyDeletedChanges().map(LocallyDeletedChange::getPath);
338   }
339
340   @NotNull
341   private JBIterable<FilePath> getSelectedFilePaths() {
342     return JBIterable.<FilePath>empty()
343       .append(getSelectedChanges().map(ChangesUtil::getFilePath))
344       .append(getSelectedVirtualFiles(null).map(VcsUtil::getFilePath))
345       .append(getSelectedFilePaths(null))
346       .unique();
347   }
348
349   @NotNull
350   private JBIterable<VirtualFile> getSelectedFiles() {
351     return JBIterable.<VirtualFile>empty()
352       .append(getSelectedChanges().filterMap(ChangesUtil::getAfterPath).filterMap(FilePath::getVirtualFile))
353       .append(getSelectedVirtualFiles(null))
354       .append(getSelectedFilePaths(null).filterMap(FilePath::getVirtualFile))
355       .unique();
356   }
357
358   @NotNull
359   private JBIterable<VirtualFile> getNavigatableFiles() {
360     return JBIterable.<VirtualFile>empty()
361       .append(getSelectedChanges().flatMap(o -> JBIterable.create(() -> getPathsCaseSensitive(o).iterator())).filterMap(FilePath::getVirtualFile))
362       .append(getSelectedVirtualFiles(null))
363       .append(getSelectedFilePaths(null).filterMap(FilePath::getVirtualFile))
364       .unique();
365   }
366
367   @NotNull
368   private JBIterable<Change> getLeadSelection() {
369     return getSelectionNodes()
370       .filter(node -> node instanceof ChangesBrowserChangeNode)
371       .map(ChangesBrowserChangeNode.class::cast)
372       .map(ChangesBrowserChangeNode::getUserObject)
373       .filter(new DistinctChangePredicate());
374   }
375
376   @NotNull
377   public JBIterable<Change> getChanges() {
378     return getRoot().traverseObjectsUnder().filter(Change.class);
379   }
380
381   @Nullable
382   public List<Change> getAllChangesFromSameChangelist(@NotNull Change change) {
383     DefaultMutableTreeNode node = findNodeInTree(change);
384     while (node != null) {
385       if (node instanceof ChangesBrowserChangeListNode) {
386         return ((ChangesBrowserChangeListNode)node).getAllChangesUnder();
387       }
388       if (node == getRoot()) {
389         if (Registry.is("vcs.skip.single.default.changelist") ||
390             !ChangeListManager.getInstance(myProject).areChangeListsEnabled()) {
391           return getRoot().getAllChangesUnder();
392         }
393       }
394       node = (DefaultMutableTreeNode)node.getParent();
395     }
396     return null;
397   }
398
399   @NotNull
400   public JBIterable<Change> getSelectedChanges() {
401     return getChanges(myProject, getSelectionPaths());
402   }
403
404   @NotNull
405   private JBIterable<ChangeList> getSelectedChangeLists() {
406     return getSelectionObjects()
407       .filter(userObject -> userObject instanceof ChangeList)
408       .map(ChangeList.class::cast)
409       .unique();
410   }
411
412   @Override
413   public void installPopupHandler(@NotNull ActionGroup group) {
414     PopupHandler.installPopupHandler(this, group, ActionPlaces.CHANGES_VIEW_POPUP, ActionManager.getInstance());
415   }
416
417   @Override
418   @NotNull
419   public JComponent getComponent() {
420     return this;
421   }
422
423   @Override
424   public void processMouseEvent(final MouseEvent e) {
425     if (MouseEvent.MOUSE_RELEASED == e.getID() && !isSelectionEmpty() && !e.isShiftDown() && !e.isControlDown()  &&
426         !e.isMetaDown() && !e.isPopupTrigger()) {
427       if (isOverSelection(e.getPoint())) {
428         clearSelection();
429         final TreePath path = getPathForLocation(e.getPoint().x, e.getPoint().y);
430         if (path != null) {
431           setSelectionPath(path);
432         }
433       }
434     }
435
436
437     super.processMouseEvent(e);
438   }
439
440   @Override
441   public boolean isOverSelection(final Point point) {
442     return TreeUtil.isOverSelection(this, point);
443   }
444
445   @Override
446   public void dropSelectionButUnderPoint(final Point point) {
447     TreeUtil.dropSelectionButUnderPoint(this, point);
448   }
449
450   @Nullable
451   public DefaultMutableTreeNode findNodeInTree(Object userObject) {
452     if (userObject instanceof LocalChangeList) {
453       return TreeUtil.nodeChildren(getRoot()).filter(DefaultMutableTreeNode.class).find(node -> userObject.equals(node.getUserObject()));
454     }
455     if (userObject instanceof ChangeListChange) {
456       return TreeUtil.findNode(getRoot(), node -> ChangeListChange.HASHING_STRATEGY.equals(node.getUserObject(), userObject));
457     }
458     return TreeUtil.findNodeWithObject(getRoot(), userObject);
459   }
460
461   @Nullable
462   public TreePath findNodePathInTree(Object userObject) {
463     DefaultMutableTreeNode node = findNodeInTree(userObject);
464     return node != null ? TreeUtil.getPathFromRoot(node) : null;
465   }
466
467   /**
468    * Expands node only if its child count is small enough.
469    * As expanding node with large child count is a slow operation (and result is not very useful).
470    */
471   public void expandSafe(@NotNull DefaultMutableTreeNode node) {
472     if (node.getChildCount() <= 10000) {
473       expandPath(TreeUtil.getPathFromRoot(node));
474     }
475   }
476
477   private static class DistinctChangePredicate extends JBIterable.SCond<Change> {
478     private Set<Object> seen;
479
480     @Override
481     public boolean value(Change change) {
482       if (seen == null) seen = new ObjectOpenCustomHashSet<>(ChangeListChange.HASHING_STRATEGY);
483       return seen.add(change);
484     }
485   }
486 }