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