Bug 569373: Use ViewFilter to filter launch configs

The content provider is only called when the launch configuration
tree is initialized. Any following add/remove of a launch
configuration is handled in LaunchConfigurationView without
fetching the new tree from the content provider.
Forcibly refresh the list in the viewer when the added launch
configuration is shared. To avoid having dangling launch
configurations in the tree.

Change-Id: Id67d550dddc6df583229013c1c71bcfc5f320f38
Signed-off-by: Torbjörn Svensson <azoff@svenskalinuxforeningen.se>
diff --git a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationTreeContentProvider.java b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationTreeContentProvider.java
index ae4c31c..d8fe82e 100644
--- a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationTreeContentProvider.java
+++ b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationTreeContentProvider.java
@@ -18,12 +18,7 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
 
 import org.eclipse.core.resources.ResourcesPlugin;
 import org.eclipse.core.runtime.CoreException;
@@ -98,7 +93,6 @@
 						configs.add(launchConfig);
 					}
 				}
-				configs = filterUniqueLaunchConfigurations(configs);
 
 				ILaunchConfiguration[] prototypes = getLaunchManager().getLaunchConfigurations(type, ILaunchConfiguration.PROTOTYPE);
 				Collections.addAll(configs, prototypes);
@@ -230,56 +224,4 @@
 	private Shell getShell() {
 		return fShell;
 	}
-
-	/**
-	 * Returns unique launch configurations from the input list Launch
-	 * configurations are unique if the following criteria are fulfilled:
-	 * <ol>
-	 * <li>The name is unique within the list</li>
-	 * <li>The set of attributes (key and value) for the launch configuration is
-	 * unique within the list</li>
-	 * </ol>
-	 *
-	 * @param launchConfigurations The list of launch configurations to filter
-	 * @return List of unique launch configurations based on criteria above
-	 * @throws CoreException If unable to fetch attributes for launch configuration
-	 */
-	private List<ILaunchConfiguration> filterUniqueLaunchConfigurations(List<ILaunchConfiguration> launchConfigurations) throws CoreException {
-		List<ILaunchConfiguration> configs = new ArrayList<>();
-
-		// Create map between name and list of launch configurations with same
-		// name
-		Map<String, List<ILaunchConfiguration>> configNameMap = new HashMap<>();
-
-		for (ILaunchConfiguration launchConfig : launchConfigurations) {
-			String name = launchConfig.getName();
-			if (!configNameMap.containsKey(name)) {
-				configNameMap.put(name, new ArrayList<>());
-			}
-			configNameMap.get(name).add(launchConfig);
-		}
-
-		// Identify unique configurations
-		for (Entry<String, List<ILaunchConfiguration>> entry : configNameMap.entrySet()) {
-			List<ILaunchConfiguration> configsWithSameName = entry.getValue();
-			if (configsWithSameName.size() == 1) {
-				// Only one configuration, add it
-				configs.add(configsWithSameName.get(0));
-			} else if (configsWithSameName.size() > 1) {
-				// More than one configuration detected with same name,
-				// verify that they are unique
-				Set<Map<String, Object>> seenConfigContent = new HashSet<>();
-				for (ILaunchConfiguration config : configsWithSameName) {
-					Map<String, Object> content = config.getAttributes();
-					if (!seenConfigContent.contains(content)) {
-						// Only add the first one with the same content
-						seenConfigContent.add(content);
-						configs.add(config);
-					}
-				}
-			}
-		}
-
-		return configs;
-	}
 }
diff --git a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationView.java b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationView.java
index 9081204..415a6f7 100644
--- a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationView.java
+++ b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationView.java
@@ -319,11 +319,11 @@
 		if (viewer != null) {
 			try {
 				viewer.getControl().setRedraw(false);
-				if (configuration.getPrototype() != null) {
-					viewer.add(configuration.getPrototype(), configuration);
-				} else {
-					viewer.add(configuration.getType(), configuration);
+				Object parentElement = configuration.getPrototype();
+				if (parentElement == null) {
+					parentElement = configuration.getType();
 				}
+				viewer.add(parentElement, configuration);
 				// if moved, remove original now
 				if (from != null) {
 					viewer.remove(from);
@@ -332,6 +332,10 @@
 					viewer.setSelection(new StructuredSelection(configuration), true);
 				}
 				updateFilterLabel();
+				// Need to refresh here, bug 559758
+				if (!configuration.isLocal()) {
+					viewer.refresh(parentElement);
+				}
 			}
 			catch (CoreException e) {}
 			finally {
diff --git a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationsDialog.java b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationsDialog.java
index c6550db..af6d76e 100644
--- a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationsDialog.java
+++ b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/LaunchConfigurationsDialog.java
@@ -233,6 +233,7 @@
 	private DeletedProjectFilter fDeletedProjectFilter;
 	private LaunchConfigurationTypeFilter fLCTFilter;
 	private WorkingSetsFilter fWorkingSetsFilter;
+	private UniqueLaunchConfigurationFileFilter fUniqueLaunchConfugurationFileFilter;
 
 	/**
 	 * set of reserved names that should not be considered when generating a new name for a launch configuration
@@ -645,6 +646,8 @@
 		if(DebugUIPlugin.getDefault().getPreferenceStore().getBoolean(IInternalDebugUIConstants.PREF_FILTER_WORKING_SETS)) {
 			filters.add(fWorkingSetsFilter);
 		}
+		fUniqueLaunchConfugurationFileFilter = new UniqueLaunchConfigurationFileFilter();
+		filters.add(fUniqueLaunchConfugurationFileFilter);
 		return filters.toArray(new ViewerFilter[filters.size()]);
 	}
 
diff --git a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/UniqueLaunchConfigurationFileFilter.java b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/UniqueLaunchConfigurationFileFilter.java
new file mode 100644
index 0000000..9880db0
--- /dev/null
+++ b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/launchConfigurations/UniqueLaunchConfigurationFileFilter.java
@@ -0,0 +1,102 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Torbjörn Svensson and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Torbjörn Svensson - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.debug.internal.ui.launchConfigurations;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerFilter;
+
+/**
+ * Filters out the instance of ILaunchCOnfiguration where the, possibly nested,
+ * launch configuration files are stored. If the launch configuration is local,
+ * it's simply returned.
+ */
+public class UniqueLaunchConfigurationFileFilter extends ViewerFilter {
+	@Override
+	public Object[] filter(Viewer viewer, Object parent, Object[] elements) {
+		int size = elements.length;
+		if (size == 0) {
+			return elements;
+		}
+
+		List<Object> filteredElements = new ArrayList<>(size);
+
+		Map<String, List<ILaunchConfiguration>> configPathMap = new HashMap<>();
+		for (Object element : elements) {
+			if (element instanceof ILaunchConfiguration) {
+				ILaunchConfiguration config = (ILaunchConfiguration) element;
+
+				String path = toLaunchFileLocation(config);
+				if (!configPathMap.containsKey(path)) {
+					configPathMap.put(path, new ArrayList<>());
+				}
+				configPathMap.get(path).add(config);
+			} else {
+				filteredElements.add(element);
+			}
+		}
+
+		for (Entry<String, List<ILaunchConfiguration>> entry : configPathMap.entrySet()) {
+			List<ILaunchConfiguration> configsWithSamePath = entry.getValue();
+			if (entry.getKey() == null) {
+				// Local configurations ends up here, add them without further filter
+				filteredElements.addAll(configsWithSamePath);
+			} else if (configsWithSamePath.size() == 1) {
+				// Only one configuration, add it
+				filteredElements.add(configsWithSamePath.get(0));
+			} else if (configsWithSamePath.size() > 1) {
+				// More than one config with same path.
+				// Order them with the shortest project relative path first
+				configsWithSamePath.sort((o1, o2) -> {
+					IPath path1 = o1.getFile().getProjectRelativePath();
+					IPath path2 = o2.getFile().getProjectRelativePath();
+					return Integer.compare(path1.segmentCount(), path2.segmentCount());
+				});
+				// Use the configuration with the shortest path.
+				// The other configurations in the list are "identical" but with
+				// the container set to one of the parent project(s) in the
+				// workspace hierarchy.
+				filteredElements.add(configsWithSamePath.get(0));
+			}
+		}
+
+		return filteredElements.toArray();
+	}
+
+	private String toLaunchFileLocation(ILaunchConfiguration config) {
+		if (!config.isLocal()) {
+			IFile file = config.getFile();
+			if (file != null) {
+				IPath path = file.getLocation();
+				if (path != null) {
+					return path.toOSString();
+				}
+			}
+		}
+		return null;
+	}
+
+	@Override
+	public boolean select(Viewer viewer, Object parentElement, Object element) {
+		return true;
+	}
+}