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;
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;
28 import javax.swing.tree.DefaultMutableTreeNode;
29 import javax.swing.tree.DefaultTreeModel;
30 import javax.swing.tree.TreePath;
32 import java.awt.event.MouseEvent;
33 import java.util.List;
35 import java.util.stream.Stream;
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;
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");
52 public ChangesListView(@NotNull Project project, boolean showCheckboxes) {
53 super(project, showCheckboxes, true);
60 protected ChangesGroupingSupport installGroupingSupport() {
61 return new ChangesGroupingSupport(myProject, this, true);
65 public int getToggleClickCount() {
70 protected boolean isInclusionVisible(@NotNull ChangesBrowserNode<?> node) {
71 Object subtreeRootObject = subtreeRootObject(node);
73 if (subtreeRootObject instanceof LocalChangeList) return !((LocalChangeList)subtreeRootObject).getChanges().isEmpty();
74 if (subtreeRootObject == UNVERSIONED_FILES_TAG) return true;
80 public HoverIcon getHoverIcon(@NotNull ChangesBrowserNode<?> node) {
85 public DefaultTreeModel getModel() {
86 return (DefaultTreeModel)super.getModel();
89 public void updateModel(@NotNull DefaultTreeModel newModel) {
90 TreeState state = TreeState.createOn(this, getRoot());
91 state.setScrollToSelection(false);
92 ChangesBrowserNode<?> oldRoot = getRoot();
94 ChangesBrowserNode<?> newRoot = getRoot();
95 state.applyTo(this, newRoot);
97 initTreeStateIfNeeded(oldRoot, newRoot);
101 public void rebuildTree() {
102 // currently not used in ChangesListView code flow
105 private void initTreeStateIfNeeded(ChangesBrowserNode<?> oldRoot, ChangesBrowserNode<?> newRoot) {
106 ChangesBrowserNode<?> defaultListNode = getDefaultChangelistNode(newRoot);
107 if (defaultListNode == null) return;
109 if (getSelectionCount() == 0) {
110 TreeUtil.selectNode(this, defaultListNode);
113 if (oldRoot.getFileCount() == 0 && TreeUtil.collectExpandedPaths(this).size() == 0) {
114 expandSafe(defaultListNode);
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();
134 public Object getData(@NotNull String dataId) {
135 if (DATA_KEY.is(dataId)) {
138 if (VcsDataKeys.CHANGES.is(dataId)) {
139 return getSelectedChanges().toList().toArray(Change[]::new);
141 if (VcsDataKeys.CHANGE_LEAD_SELECTION.is(dataId)) {
142 return getLeadSelection().toList().toArray(Change[]::new);
144 if (VcsDataKeys.CHANGE_LISTS.is(dataId)) {
145 return getSelectedChangeLists().toList().toArray(ChangeList[]::new);
147 if (CommonDataKeys.VIRTUAL_FILE_ARRAY.is(dataId)) {
148 return getSelectedFiles().toList().toArray(VirtualFile[]::new);
150 if (VcsDataKeys.VIRTUAL_FILES.is(dataId)) {
151 return getSelectedFiles();
153 if (VcsDataKeys.FILE_PATHS.is(dataId)) {
154 return getSelectedFilePaths();
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;
161 if (CommonDataKeys.NAVIGATABLE_ARRAY.is(dataId)) {
162 return getNavigatableArray(myProject, StreamEx.of(getNavigatableFiles().iterator()));
164 if (PlatformDataKeys.DELETE_ELEMENT_PROVIDER.is(dataId)) {
165 return getSelectionObjects().find(userObject -> !(userObject instanceof ChangeList)) != null
166 ? new VirtualFileDeleteProvider()
169 if (UNVERSIONED_FILE_PATHS_DATA_KEY.is(dataId)) {
170 return getSelectedUnversionedFiles();
172 if (EXACTLY_SELECTED_FILES_DATA_KEY.is(dataId)) {
173 return getExactlySelectedVirtualFiles(this);
175 if (IGNORED_FILE_PATHS_DATA_KEY.is(dataId)) {
176 return getSelectedIgnoredFiles();
178 if (VcsDataKeys.MODIFIED_WITHOUT_EDITING_DATA_KEY.is(dataId)) {
179 return getSelectedModifiedWithoutEditing().toList();
181 if (LOCALLY_DELETED_CHANGES.is(dataId)) {
182 return getSelectedLocallyDeletedChanges().toList();
184 if (MISSING_FILES_DATA_KEY.is(dataId)) {
185 return getSelectedMissingFiles().toList();
187 if (VcsDataKeys.HAVE_LOCALLY_DELETED.is(dataId)) {
188 return getSelectedMissingFiles().isNotEmpty();
190 if (VcsDataKeys.HAVE_MODIFIED_WITHOUT_EDITING.is(dataId)) {
191 return getSelectedModifiedWithoutEditing().isNotEmpty();
193 if (VcsDataKeys.HAVE_SELECTED_CHANGES.is(dataId)) {
194 return getSelectedChanges().isNotEmpty();
196 if (PlatformDataKeys.HELP_ID.is(dataId)) {
199 return super.getData(dataId);
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();
211 static JBIterable<FilePath> getSelectedUnversionedFiles(@NotNull JTree tree) {
212 return getSelectedFilePaths(tree, UNVERSIONED_FILES_TAG);
216 public JBIterable<FilePath> getSelectedUnversionedFiles() {
217 return getSelectedUnversionedFiles(this);
221 private JBIterable<FilePath> getSelectedIgnoredFiles() {
222 return getSelectedFilePaths(IGNORED_FILES_TAG);
226 private JBIterable<VirtualFile> getSelectedModifiedWithoutEditing() {
227 return getSelectedVirtualFiles(MODIFIED_WITHOUT_EDITING_TAG);
231 protected JBIterable<VirtualFile> getSelectedVirtualFiles(@Nullable Object tag) {
232 return getSelectionNodes(this, tag)
233 .flatMap(node -> JBIterable.create(() -> node.getFilesUnderStream().iterator()))
238 protected JBIterable<FilePath> getSelectedFilePaths(@Nullable Object tag) {
239 return getSelectedFilePaths(this, tag);
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()))
250 static JBIterable<VirtualFile> getExactlySelectedVirtualFiles(@NotNull JTree tree) {
251 VcsTreeModelData exactlySelected = VcsTreeModelData.exactlySelected(tree);
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();
257 }).filter(Objects::nonNull);
261 private JBIterable<ChangesBrowserNode<?>> getSelectionNodes() {
262 return getSelectionNodes(this, null);
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));
274 private JBIterable<Object> getSelectionObjects() {
275 return getSelectionNodes().map(ChangesBrowserNode::getUserObject);
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()))
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()))
298 static boolean isUnderTag(@NotNull TreePath path, @Nullable Object tag) {
299 boolean result = true;
302 result = path.getPathCount() > 1 && ((ChangesBrowserNode<?>)path.getPathComponent(1)).getUserObject() == tag;
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);
319 return changes.append(hijackedChanges)
320 .filter(new DistinctChangePredicate());
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);
334 private JBIterable<LocallyDeletedChange> getSelectedLocallyDeletedChanges() {
335 return getSelectionNodes(this, LOCALLY_DELETED_NODE_TAG)
336 .flatMap(node -> node.traverseObjectsUnder())
337 .filter(LocallyDeletedChange.class)
342 private JBIterable<FilePath> getSelectedMissingFiles() {
343 return getSelectedLocallyDeletedChanges().map(LocallyDeletedChange::getPath);
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))
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))
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))
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());
383 public JBIterable<Change> getChanges() {
384 return getRoot().traverseObjectsUnder().filter(Change.class);
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();
394 if (node == getRoot()) {
395 if (Registry.is("vcs.skip.single.default.changelist") ||
396 !ChangeListManager.getInstance(myProject).areChangeListsEnabled()) {
397 return getRoot().getAllChangesUnder();
400 node = (DefaultMutableTreeNode)node.getParent();
406 public JBIterable<Change> getSelectedChanges() {
407 return getChanges(myProject, getSelectionPaths());
411 private JBIterable<ChangeList> getSelectedChangeLists() {
412 return getSelectionObjects()
413 .filter(userObject -> userObject instanceof ChangeList)
414 .map(ChangeList.class::cast)
419 public void installPopupHandler(@NotNull ActionGroup group) {
420 PopupHandler.installPopupHandler(this, group, ActionPlaces.CHANGES_VIEW_POPUP, ActionManager.getInstance());
425 public JComponent getComponent() {
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())) {
435 final TreePath path = getPathForLocation(e.getPoint().x, e.getPoint().y);
437 setSelectionPath(path);
443 super.processMouseEvent(e);
447 public boolean isOverSelection(final Point point) {
448 return TreeUtil.isOverSelection(this, point);
452 public void dropSelectionButUnderPoint(final Point point) {
453 TreeUtil.dropSelectionButUnderPoint(this, point);
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()));
461 if (userObject instanceof ChangeListChange) {
462 return TreeUtil.findNode(getRoot(), node -> ChangeListChange.HASHING_STRATEGY.equals(node.getUserObject(), userObject));
464 return TreeUtil.findNodeWithObject(getRoot(), userObject);
468 public TreePath findNodePathInTree(Object userObject) {
469 DefaultMutableTreeNode node = findNodeInTree(userObject);
470 return node != null ? TreeUtil.getPathFromRoot(node) : null;
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).
477 public void expandSafe(@NotNull DefaultMutableTreeNode node) {
478 if (node.getChildCount() <= 10000) {
479 expandPath(TreeUtil.getPathFromRoot(node));
483 private static class DistinctChangePredicate extends JBIterable.SCond<Change> {
484 private Set<Object> seen;
487 public boolean value(Change change) {
488 if (seen == null) seen = new ObjectOpenCustomHashSet<>(ChangeListChange.HASHING_STRATEGY);
489 return seen.add(change);