/******************************************************************************* * Copyright (C) 2016, Thomas Wolf <thomas.wolf@paranor.ch> * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html *******************************************************************************/ package org.eclipse.egit.ui.internal.commit; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.stream.Collectors; import org.eclipse.core.runtime.Path; import org.eclipse.egit.ui.internal.UIText; import org.eclipse.egit.ui.internal.commit.DiffRegionFormatter.FileDiffRegion; import org.eclipse.egit.ui.internal.history.FileDiff; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.resource.LocalResourceManager; import org.eclipse.jface.resource.ResourceManager; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.util.SafeRunnable; import org.eclipse.jface.viewers.IOpenListener; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.OpenEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Menu; import org.eclipse.ui.ISharedImages; import org.eclipse.ui.PlatformUI; /** * A {@link NestedContentOutlinePage} for the {DiffEditorPage}, displaying an * outline for {@link DiffDocument}s. */ public class DiffEditorOutlinePage extends NestedContentOutlinePage { private IDocument input; private CopyOnWriteArrayList<IOpenListener> openListeners = new CopyOnWriteArrayList<>(); private ISelection selection; @Override public void createControl(Composite parent) { super.createControl(parent); TreeViewer viewer = getTreeViewer(); viewer.setAutoExpandLevel(2); viewer.setContentProvider(new DiffContentProvider()); viewer.setLabelProvider(new DiffLabelProvider()); viewer.addDoubleClickListener( event -> openFolder(event.getSelection())); viewer.addOpenListener(event -> fireOpenEvent(event)); if (input != null) { viewer.setInput(input); } createContextMenu(viewer); if (selection != null) { viewer.setSelection(selection); } } /** * Sets the input of the page to the given {@link IDocument}. * * @param input * to set for the page */ public void setInput(IDocument input) { this.input = input; TreeViewer viewer = getTreeViewerChecked(); if (viewer != null) { viewer.setInput(input); } } @Override public void setSelection(ISelection selection) { this.selection = selection; TreeViewer viewer = getTreeViewerChecked(); if (viewer != null) { super.setSelection(selection); } } private TreeViewer getTreeViewerChecked() { TreeViewer viewer = getTreeViewer(); if (viewer == null || viewer.getControl() == null || viewer.getControl().isDisposed()) { return null; } return viewer; } /** * Adds a listener for selection-open in this page's viewer. Has no effect * if an identical listener is already registered. * * @param listener * to add to the page'sviewer */ public void addOpenListener(IOpenListener listener) { openListeners.addIfAbsent(listener); } /** * Removes the given open listener from this page's viewer. Has no effect if * the listener is not registered. * * @param listener * to remove from this page's viewer. */ public void removeOpenListener(IOpenListener listener) { openListeners.remove(listener); } private void openFolder(ISelection currentSelection) { if (currentSelection instanceof IStructuredSelection) { Object currentNode = ((IStructuredSelection) currentSelection) .getFirstElement(); if (currentNode instanceof DiffContentProvider.Folder) { TreeViewer viewer = getTreeViewerChecked(); if (viewer != null) { viewer.setExpandedState(currentNode, !viewer.getExpandedState(currentNode)); } } } } private void fireOpenEvent(OpenEvent event) { for (IOpenListener listener : openListeners) { SafeRunnable.run(new SafeRunnable() { @Override public void run() { listener.open(event); } }); } } private void createContextMenu(TreeViewer viewer) { MenuManager contextMenu = new MenuManager(); contextMenu.setRemoveAllWhenShown(true); contextMenu.addMenuListener(menuManager -> { setFocus(); Collection<FileDiffRegion> selected = getSelectedFileDiffs(); if (selected.isEmpty()) { return; } Collection<FileDiffRegion> haveNew = selected.stream() .filter(diff -> !diff.getDiff().getChange() .equals(DiffEntry.ChangeType.DELETE)) .collect(Collectors.toList()); Collection<FileDiffRegion> haveOld = selected.stream() .filter(diff -> !diff.getDiff().getChange() .equals(DiffEntry.ChangeType.ADD)) .collect(Collectors.toList()); Collection<FileDiffRegion> existing = haveNew.stream() .filter(diff -> new Path(diff.getRepository().getWorkTree() .getAbsolutePath()) .append(diff.getDiff().getNewPath()) .toFile().exists()) .collect(Collectors.toList()); if (!existing.isEmpty()) { menuManager.add(new Action( UIText.CommitFileDiffViewer_OpenWorkingTreeVersionInEditorMenuLabel) { @Override public void run() { for (FileDiffRegion fileDiff : existing) { File file = new Path(fileDiff.getRepository() .getWorkTree().getAbsolutePath()).append( fileDiff.getDiff().getNewPath()) .toFile(); DiffViewer.openFileInEditor(file, -1); } } }); } if (!haveNew.isEmpty()) { menuManager.add(new Action( UIText.CommitFileDiffViewer_OpenInEditorMenuLabel) { @Override public void run() { for (FileDiffRegion fileDiff : haveNew) { DiffViewer.openInEditor(fileDiff.getRepository(), fileDiff.getDiff(), DiffEntry.Side.NEW, -1); } } }); } if (!haveOld.isEmpty()) { menuManager.add(new Action( UIText.CommitFileDiffViewer_OpenPreviousInEditorMenuLabel) { @Override public void run() { for (FileDiffRegion fileDiff : haveOld) { DiffViewer.openInEditor(fileDiff.getRepository(), fileDiff.getDiff(), DiffEntry.Side.OLD, -1); } } }); } if (selected.size() == 1) { menuManager.add(new Separator()); menuManager.add(new Action( UIText.CommitFileDiffViewer_CompareMenuLabel) { @Override public void run() { FileDiffRegion fileDiff = selected.iterator().next(); DiffViewer.showTwoWayFileDiff(fileDiff.getRepository(), fileDiff.getDiff()); } }); } }); Menu menu = contextMenu.createContextMenu(viewer.getTree()); viewer.getTree().setMenu(menu); } private Collection<FileDiffRegion> getSelectedFileDiffs() { ISelection currentSelection = getSelection(); List<FileDiffRegion> result = new ArrayList<>(); if (!currentSelection.isEmpty() && currentSelection instanceof StructuredSelection) { for (Object selected : ((StructuredSelection) currentSelection).toList()) { if (selected instanceof FileDiffRegion && !((FileDiffRegion) selected).getDiff() .isSubmodule()) { result.add((FileDiffRegion) selected); } } } return result; } private static class DiffContentProvider implements ITreeContentProvider { private static final Object[] NOTHING = new Object[0]; public static class Folder { public String name; public List<FileDiffRegion> files; } private HashMap<String, Folder> folders = new LinkedHashMap<>(); private Map<FileDiffRegion, Folder> parents = new HashMap<>(); @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { folders.clear(); parents.clear(); if (newInput instanceof DiffDocument) { computeFolders(((DiffDocument) newInput).getFileRegions()); } } @Override public void dispose() { folders.clear(); parents.clear(); } @Override public Object[] getElements(Object inputElement) { if (inputElement instanceof DiffDocument) { return folders.values().toArray(); } return NOTHING; } @Override public Object[] getChildren(Object parentElement) { if (parentElement instanceof Folder) { return ((Folder) parentElement).files.toArray(); } return NOTHING; } @Override public Object getParent(Object element) { if (element instanceof FileDiffRegion) { return parents.get(element); } return null; } @Override public boolean hasChildren(Object element) { return (element instanceof Folder); } private void computeFolders(FileDiffRegion[] ranges) { for (FileDiffRegion range : ranges) { String path = range.getDiff().getPath(); int i = path.lastIndexOf('/'); if (i > 0) { path = path.substring(0, i); } else { path = "/"; //$NON-NLS-1$ } Folder folder = folders.get(path); if (folder == null) { folder = new Folder(); folder.name = path; folder.files = new ArrayList<>(); folders.put(path, folder); } folder.files.add(range); parents.put(range, folder); } } } private static class DiffLabelProvider extends LabelProvider { private final Image FOLDER = PlatformUI.getWorkbench().getSharedImages() .getImage(ISharedImages.IMG_OBJ_FOLDER); private final ResourceManager resourceManager = new LocalResourceManager( JFaceResources.getResources()); public DiffLabelProvider() { super(); } @Override public Image getImage(Object element) { if (element instanceof DiffContentProvider.Folder) { return FOLDER; } if (element instanceof FileDiffRegion) { FileDiff diff = ((FileDiffRegion) element).getDiff(); return (Image) resourceManager .get(diff.getImageDescriptor(diff)); } return super.getImage(element); } @Override public String getText(Object element) { if (element instanceof DiffContentProvider.Folder) { return ((DiffContentProvider.Folder) element).name; } if (element instanceof FileDiffRegion) { FileDiff diff = ((FileDiffRegion) element).getDiff(); String path = diff.getPath(); int i = path.lastIndexOf('/'); return path.substring(i + 1); } return super.getText(element); } @Override public void dispose() { resourceManager.dispose(); super.dispose(); } } }