Bug 538480 - Offer code completion for selecting the directory in
the workspace selection dialog

ReopenableContentProposalAdapter extends ContentProposalAdapter
instead of ContentAssistCommandAdapter as the latter expects
a WorkBench to be loaded.
This also forces ReopenableContentProposalAdapter to reimplement the
FieldDecoration logic in ContentAssistCommandAdapter.

Tests are deactivated because of Bug 540441 and Bug 275393.

Change-Id: Idbb09dfde30f79c3a723511d75a9bfe21e8740ee
Signed-off-by: Fabian Pfaff <fabian.pfaff@vogella.com>
diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/ChooseWorkspaceDialog.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/ChooseWorkspaceDialog.java
index 14db5af..c383e3e 100644
--- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/ChooseWorkspaceDialog.java
+++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/ChooseWorkspaceDialog.java
@@ -37,6 +37,7 @@
 import org.eclipse.osgi.util.NLS;
 import org.eclipse.osgi.util.TextProcessor;
 import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CLabel;
 import org.eclipse.swt.events.SelectionAdapter;
 import org.eclipse.swt.events.SelectionEvent;
 import org.eclipse.swt.graphics.Point;
@@ -50,7 +51,6 @@
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.DirectoryDialog;
-import org.eclipse.swt.widgets.Label;
 import org.eclipse.swt.widgets.Link;
 import org.eclipse.swt.widgets.Menu;
 import org.eclipse.swt.widgets.MenuItem;
@@ -420,10 +420,12 @@
         panel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
         panel.setFont(parent.getFont());
 
-        Label label = new Label(panel, SWT.NONE);
+		CLabel label = new CLabel(panel, SWT.NONE);
         label.setText(IDEWorkbenchMessages.ChooseWorkspaceDialog_workspaceEntryLabel);
+		label.setMargins(0, 0, 2, 0);
 
-        text = new Combo(panel, SWT.BORDER | SWT.LEAD | SWT.DROP_DOWN);
+		text = new Combo(panel, SWT.BORDER | SWT.LEAD | SWT.DROP_DOWN);
+		new DirectoryProposalContentAssist().apply(text);
         text.setFocus();
         text.setLayoutData(new GridData(400, SWT.DEFAULT));
         text.addModifyListener(e -> {
diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/DirectoryProposalContentAssist.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/DirectoryProposalContentAssist.java
new file mode 100644
index 0000000..094afbb
--- /dev/null
+++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/DirectoryProposalContentAssist.java
@@ -0,0 +1,378 @@
+/*******************************************************************************
+ * Copyright (c) 2018 vogella GmbH 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:
+ *     Fabian Pfaff <fabian.pfaff@vogella.com> - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.ui.internal.ide;
+
+import static java.util.stream.Collectors.toList;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+import org.eclipse.jface.bindings.keys.KeyStroke;
+import org.eclipse.jface.bindings.keys.ParseException;
+import org.eclipse.jface.fieldassist.ComboContentAdapter;
+import org.eclipse.jface.fieldassist.ContentProposal;
+import org.eclipse.jface.fieldassist.ContentProposalAdapter;
+import org.eclipse.jface.fieldassist.ControlDecoration;
+import org.eclipse.jface.fieldassist.FieldDecoration;
+import org.eclipse.jface.fieldassist.FieldDecorationRegistry;
+import org.eclipse.jface.fieldassist.IContentProposal;
+import org.eclipse.jface.fieldassist.IContentProposalListener2;
+import org.eclipse.jface.fieldassist.IContentProposalProvider;
+import org.eclipse.jface.fieldassist.IControlContentAdapter;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.ui.internal.WorkbenchMessages;
+
+/**
+ * Adds content assist to a Combo, that is intended to be used to choose a
+ * directory.
+ */
+public class DirectoryProposalContentAssist {
+
+	private static class FileNameSubstringMatchContentProposalProvider implements IContentProposalProvider {
+
+		private List<String> proposals = Collections.emptyList();
+
+		/**
+		 * Returns an array of valid proposals filtered by substring matching of the
+		 * fileName.
+		 *
+		 * @param contents the current contents of the text field
+		 * @param position the current position of the cursor in the contents
+		 *
+		 * @return the array of {@link IContentProposal} that represent valid proposals
+		 *         for the field.
+		 */
+		@Override
+		public IContentProposal[] getProposals(String contents, int position) {
+			String substring = contents.substring(0, position);
+			// only match fileName as the folder part will be equal anyway and this makes
+			// substring matching easier
+			Pattern pattern = Pattern.compile(substring,
+					Pattern.LITERAL | Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
+			return proposals.stream()
+					.filter(proposal -> proposal.length() >= substring.length() && pattern.matcher(proposal).find())
+					.map(ContentProposal::new)
+					.toArray(IContentProposal[]::new);
+		}
+
+		/**
+		 * Set the Strings to be used as content proposals.
+		 *
+		 * @param proposals the Strings to be used as proposals.
+		 */
+		public void setProposals(List<String> proposals) {
+			this.proposals = proposals;
+		}
+
+	}
+
+	private static class OpenableContentProposalAdapter extends ContentProposalAdapter {
+
+		private static final String CONTENT_ASSIST_DECORATION_ID = "org.eclipse.ui.internal.ide.DirectoryProposalContentAssist$ReopenableContentProposalAdapter"; //$NON-NLS-1$
+
+		public OpenableContentProposalAdapter(Control control, IControlContentAdapter controlContentAdapter,
+				IContentProposalProvider proposalProvider, KeyStroke keyStroke, char[] autoActivationCharacters) {
+			super(control, controlContentAdapter, proposalProvider, keyStroke, autoActivationCharacters);
+			installContentProposalFieldDecoration(control, keyStroke);
+		}
+
+		/**
+		 * Installs a field decoration that shows the user that the control supports
+		 * content assist.
+		 *
+		 * @param control   the control that supports content assist
+		 * @param keyStroke the key stroke to be shown in the field decoration hover
+		 *                  text
+		 */
+		private void installContentProposalFieldDecoration(Control control, KeyStroke keyStroke) {
+			ControlDecoration decoration = new ControlDecoration(control, SWT.TOP | SWT.LEFT);
+			decoration.setShowOnlyOnFocus(true);
+			FieldDecoration dec = getContentAssistFieldDecoration(keyStroke);
+			decoration.setImage(dec.getImage());
+			decoration.setDescriptionText(dec.getDescription());
+		}
+
+		/**
+		 * Return the field decoration that should be used to indicate that content
+		 * assist is available for a field. Ensure that the decoration text includes the
+		 * correct key binding.
+		 *
+		 * @param keyStroke the key stroke to be shown in the hover text
+		 * @return the {@link FieldDecoration} that should be used to show content
+		 *         assist.
+		 **/
+		private FieldDecoration getContentAssistFieldDecoration(KeyStroke keyStroke) {
+			FieldDecorationRegistry registry = FieldDecorationRegistry.getDefault();
+			String decId = CONTENT_ASSIST_DECORATION_ID + keyStroke;
+			FieldDecoration dec = registry.getFieldDecoration(decId);
+
+			// If there is not one, base ours on the standard JFace one.
+			if (dec == null) {
+				FieldDecoration originalDec = registry.getFieldDecoration(FieldDecorationRegistry.DEC_CONTENT_PROPOSAL);
+
+				registry.registerFieldDecoration(decId, null, originalDec.getImage());
+				dec = registry.getFieldDecoration(decId);
+			}
+			dec.setDescription(NLS.bind(WorkbenchMessages.ContentAssist_Cue_Description_Key, keyStroke));
+			return dec;
+		}
+
+		@Override
+		public void openProposalPopup() {
+			super.openProposalPopup();
+		}
+
+	}
+
+	private class DirectoryProposalAutoCompleteField {
+
+		private FileNameSubstringMatchContentProposalProvider proposalProvider;
+		private OpenableContentProposalAdapter adapter;
+
+		public DirectoryProposalAutoCompleteField(Control control, IControlContentAdapter controlContentAdapter) {
+			proposalProvider = new FileNameSubstringMatchContentProposalProvider();
+			KeyStroke triggeringKeyStroke = safeKeyStroke("Ctrl+Space"); //$NON-NLS-1$
+			String backspace = "\b"; //$NON-NLS-1$
+			String delete = "\u007F"; //$NON-NLS-1$
+			char[] autoactivationChars = ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + backspace //$NON-NLS-1$
+					+ delete).toCharArray();
+			adapter = new OpenableContentProposalAdapter(control, controlContentAdapter, proposalProvider,
+					triggeringKeyStroke, autoactivationChars);
+			adapter.setPropagateKeys(true);
+			adapter.setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE);
+		}
+
+		private KeyStroke safeKeyStroke(String keyStrokePattern) {
+			try {
+				return KeyStroke.getInstance(keyStrokePattern);
+			} catch (ParseException e) {
+				return null;
+			}
+		}
+
+		/**
+		 * Sets the given proposals on the content provider and forces the proposal
+		 * pop-up to refresh its content.
+		 *
+		 * @param proposals         the proposals to set
+		 * @param openProposalPopup if the proposal pop-up should be opened
+		 */
+		public void refreshProposals(List<String> proposals, boolean openProposalPopup) {
+			proposalProvider.setProposals(proposals);
+			adapter.refresh();
+			if (openProposalPopup) {
+				adapter.openProposalPopup();
+			}
+		}
+
+	}
+
+	private Path lastDir;
+	private DirectoryProposalAutoCompleteField autoCompleteField;
+	private Combo directoryCombo;
+	/**
+	 * The {@link ContentProposalAdapter} closes the proposal pop-up when the
+	 * proposals are empty. This remembers the popup state and reopen the popup
+	 * after the proposals have been updated by the asynchronous job.
+	 */
+	private boolean popupActivated = false;
+	private List<CompletableFuture<Void>> proposalUpdateFutures = Collections.synchronizedList(new ArrayList<>());
+
+	/**
+	 * Applies auto-completion to a Combo that is intended to be used to choose a
+	 * directory. Proposals are triggered automatically, but can also be triggered
+	 * by pressing Ctrl+Space.
+	 *
+	 * @param combo the Combo that gets the auto-completion applied
+	 */
+	public void apply(Combo combo) {
+		directoryCombo = combo;
+		autoCompleteField = new DirectoryProposalAutoCompleteField(directoryCombo, new ComboContentAdapter());
+
+		getContentProposalAdapter().addContentProposalListener(e -> updateProposals(directoryCombo.getText(), false));
+		getContentProposalAdapter().addContentProposalListener(new IContentProposalListener2() {
+
+			@Override
+			public void proposalPopupOpened(ContentProposalAdapter adapter) {
+				popupActivated = true;
+			}
+
+			@Override
+			public void proposalPopupClosed(ContentProposalAdapter adapter) {
+				// do nothing on purpose
+			}
+		});
+
+		directoryCombo.addModifyListener(
+				e -> updateProposals(directoryCombo.getText().substring(0, directoryCombo.getCaretPosition()), true));
+
+		directoryCombo.addKeyListener(KeyListener.keyPressedAdapter(e -> {
+			if (e.keyCode == SWT.ESC) {
+				popupActivated = false;
+			}
+		}));
+		// use key release because otherwise the caret position is not yet updated
+		directoryCombo.addKeyListener(KeyListener.keyReleasedAdapter(e -> {
+			if (isTraverse(e)) {
+				int caretPosition = directoryCombo.getCaretPosition();
+				updateProposals(directoryCombo.getText().substring(0, caretPosition), popupActivated);
+			}
+		}));
+
+		directoryCombo.addMouseListener(MouseListener.mouseUpAdapter(e -> {
+			int caretPosition = ((Combo) e.getSource()).getCaretPosition();
+			updateProposals(directoryCombo.getText().substring(0, caretPosition), false);
+		}));
+	}
+
+	private boolean isTraverse(KeyEvent e) {
+		return e.keyCode == SWT.ARROW_LEFT || e.keyCode == SWT.ARROW_RIGHT
+				 || e.keyCode == SWT.HOME || e.keyCode == SWT.END;
+	}
+
+	/**
+	 * Checks if the directory in the combo has changed. If the directory has
+	 * changed the proposals get asynchronously updated with the subfolders of the
+	 * new directory. If the current content of the combo is not a valid directory
+	 * the proposals get cleared.
+	 */
+	private void updateProposals(String textFromCombo, boolean openProposalPopup) {
+		Path dir = pathWithoutFileName(textFromCombo);
+		if (dir != null && dir.equals(lastDir)) {
+			if (openProposalPopup) {
+				autoCompleteField.adapter.openProposalPopup();
+			}
+			return;
+		}
+		if (dir == null || !safeIsDirectory(dir)) {
+			updateProposals(Collections.emptyList(), false);
+			lastDir = null;
+			return;
+		}
+
+		lastDir = dir;
+
+		CompletableFuture<Void> completableFuture = CompletableFuture
+				.runAsync(() -> updateProposals(retrieveDirectoriesIn(dir), openProposalPopup));
+
+		proposalUpdateFutures.add(completableFuture);
+		proposalUpdateFutures.removeIf(CompletableFuture::isDone);
+	}
+
+	private List<String> retrieveDirectoriesIn(Path dir) {
+		try (Stream<Path> files = Files.list(dir)) {
+			return filterPaths(files).sorted().collect(toList());
+		} catch (IOException ex) {
+			return new ArrayList<>();
+		}
+	}
+
+	private void updateProposals(List<String> proposals, boolean openProposalPopup) {
+		directoryCombo.getDisplay().syncExec(() -> autoCompleteField.refreshProposals(proposals, openProposalPopup));
+	}
+
+	private Path pathWithoutFileName(String inputPath) {
+		int lastIndex = inputPath.lastIndexOf(File.separatorChar);
+		if (separatorNotFound(lastIndex)) {
+			return null;
+		}
+		return safeGetPath(removeFileName(inputPath, lastIndex));
+	}
+
+	private boolean separatorNotFound(int lastIndex) {
+		return lastIndex < 0;
+	}
+
+	private String removeFileName(String text, int lastIndex) {
+		if (lastIndex == 0) {
+			return File.separator;
+		}
+		return text.substring(0, lastIndex + 1);
+	}
+
+	private Path safeGetPath(String text) {
+		try {
+			return Paths.get(text);
+		} catch (InvalidPathException ex) {
+			return null;
+		}
+	}
+
+	private boolean safeIsDirectory(Path dir) {
+		try {
+			return dir.toFile().isDirectory();
+		} catch (SecurityException ex) {
+			return false;
+		}
+	}
+
+	/**
+	 * Filters out files and hidden directories.
+	 *
+	 * @param paths the Paths to filter
+	 * @return the filtered paths
+	 */
+	private Stream<String> filterPaths(Stream<Path> paths) {
+		return paths.filter(path -> {
+			try {
+				return safeIsDirectory(path) && !Files.isHidden(path);
+			} catch (IOException e) {
+				return false;
+			}
+		}).map(path -> path.toString() + File.separator);
+	}
+
+	public DirectoryProposalAutoCompleteField getAutoCompleteField() {
+		return autoCompleteField;
+	}
+
+	public ContentProposalAdapter getContentProposalAdapter() {
+		return autoCompleteField.adapter;
+	}
+
+	/**
+	 * Wait until the asynchronous proposal refresh is finished. This method is
+	 * intended to by used by tests.
+	 *
+	 * @param timeout timeout in milliseconds
+	 * @throws TimeoutException     if the wait timed out
+	 * @throws ExecutionException   if the future completed exceptionally
+	 * @throws InterruptedException if the current thread was interrupted while
+	 *                              waiting
+	 */
+	protected void wait(int timeout) throws InterruptedException, ExecutionException, TimeoutException {
+		CompletableFuture.allOf(proposalUpdateFutures.toArray(new CompletableFuture[proposalUpdateFutures.size()]))
+				.get(timeout, TimeUnit.MILLISECONDS);
+	}
+
+}
diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/internal/ide/DirectoryProposalContentAssistTest.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/internal/ide/DirectoryProposalContentAssistTest.java
new file mode 100644
index 0000000..ad29fed
--- /dev/null
+++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/internal/ide/DirectoryProposalContentAssistTest.java
@@ -0,0 +1,43 @@
+package org.eclipse.ui.internal.ide;
+
+import java.io.File;
+
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class DirectoryProposalContentAssistTest extends DirectoryProposalContentAssistTestCase {
+
+	@Rule
+	public TemporaryFolder folder = new TemporaryFolder();
+
+	@Ignore // see Bug 540441 and Bug 275393
+	@Test
+	public void fileSeparatorOpensProposalPopup() throws Exception {
+		getFieldAssistWindow().open();
+		sendFocusInToControl();
+
+		sendKeyEventToControl(File.separatorChar);
+		waitForDirectoryContentAssist();
+
+		assertTwoShellsUp();
+	}
+
+	@Ignore // see Bug 540441 and Bug 275393
+	@Test
+	public void opensProposalPopupWithSubfoldersAsProposals() throws Exception {
+		folder.newFolder("foo");
+		folder.newFolder("bar");
+
+		getFieldAssistWindow().open();
+		sendFocusInToControl();
+
+		setControlContent(folder.getRoot().getAbsolutePath());
+		sendKeyEventToControl(File.separatorChar);
+		waitForDirectoryContentAssist();
+
+		assertProposalSize(2);
+	}
+
+}
diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/internal/ide/DirectoryProposalContentAssistTestCase.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/internal/ide/DirectoryProposalContentAssistTestCase.java
new file mode 100644
index 0000000..a8bc2f6
--- /dev/null
+++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/internal/ide/DirectoryProposalContentAssistTestCase.java
@@ -0,0 +1,102 @@
+package org.eclipse.ui.internal.ide;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jface.bindings.keys.KeyStroke;
+import org.eclipse.jface.tests.fieldassist.AbstractFieldAssistTestCase;
+import org.eclipse.jface.tests.fieldassist.AbstractFieldAssistWindow;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableItem;
+
+public class DirectoryProposalContentAssistTestCase extends AbstractFieldAssistTestCase {
+
+	private DirectoryProposalContentAssistWindow directoryContentAssistWindow;
+
+	@Override
+	protected AbstractFieldAssistWindow createFieldAssistWindow() {
+		directoryContentAssistWindow = new DirectoryProposalContentAssistWindow();
+		return directoryContentAssistWindow;
+	}
+
+	public void waitForDirectoryContentAssist() throws InterruptedException, ExecutionException {
+		try {
+			directoryContentAssistWindow.getContentAssist().wait(10000);
+		} catch (TimeoutException e) {
+		}
+		spinEventLoop();
+	}
+
+
+	public void sendKeyEventToControl(char character) {
+		sendKeyDownToControl(character);
+		sendKeyUpToControl(character);
+	}
+
+	private void sendKeyUpToControl(char character) {
+		sendFocusInToControl();
+		Event event = new Event();
+		event.type = SWT.KeyUp;
+		event.character = character;
+		assertTrue("unable to post event to display queue for test case",
+				getFieldAssistWindow().getDisplay().post(event));
+		spinEventLoop();
+	}
+
+	public void sendKeyEventToControl(KeyStroke keyStroke) {
+		sendKeyDownToControl(keyStroke);
+		sendKeyUpToControl(keyStroke);
+	}
+
+	private void sendKeyUpToControl(KeyStroke keyStroke) {
+		sendFocusInToControl();
+		Event event = new Event();
+		event.type = SWT.KeyDown;
+		event.keyCode = keyStroke.getNaturalKey();
+		assertTrue("unable to post event to display queue for test case",
+				getFieldAssistWindow().getDisplay().post(event));
+		spinEventLoop();
+	}
+
+	public void assertProposalSize(int size) {
+		Shell[] shells = getFieldAssistWindow().getDisplay().getShells();
+		Optional<Table> tableOptional = Arrays.stream(shells).map(this::retrieveTable)
+				.filter(Objects::nonNull).findFirst();
+		if (!tableOptional.isPresent()) {
+			fail("Couldn't assert pop-up proposal size - pop-up seems closed.");
+		}
+		TableItem[] proposals = tableOptional.get().getItems();
+
+		assertTrue("Proposal size must be " + size, size == proposals.length);
+	}
+
+	private Table retrieveTable(Shell shell) {
+		Control[] children = shell.getChildren();
+		if (children.length >= 1) {
+			Control control = children[0];
+			if (control instanceof Composite) {
+				Composite composite = (Composite) control;
+				Control[] children2 = composite.getChildren();
+				if (children2.length >= 1) {
+					Control control2 = composite.getChildren()[0];
+					if (control2 instanceof Table) {
+						return (Table) control2;
+					}
+				}
+			}
+		}
+		return null;
+	}
+
+}
diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/internal/ide/DirectoryProposalContentAssistTestSuite.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/internal/ide/DirectoryProposalContentAssistTestSuite.java
new file mode 100644
index 0000000..0bebc52
--- /dev/null
+++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/internal/ide/DirectoryProposalContentAssistTestSuite.java
@@ -0,0 +1,10 @@
+package org.eclipse.ui.internal.ide;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({ DirectoryProposalContentAssistTest.class })
+public class DirectoryProposalContentAssistTestSuite {
+
+}
diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/internal/ide/DirectoryProposalContentAssistWindow.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/internal/ide/DirectoryProposalContentAssistWindow.java
new file mode 100644
index 0000000..f030435
--- /dev/null
+++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/internal/ide/DirectoryProposalContentAssistWindow.java
@@ -0,0 +1,41 @@
+package org.eclipse.ui.internal.ide;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jface.fieldassist.ContentProposalAdapter;
+import org.eclipse.jface.tests.fieldassist.ComboFieldAssistWindow;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Control;
+
+/**
+ * @since 3.5
+ *
+ */
+public class DirectoryProposalContentAssistWindow extends ComboFieldAssistWindow {
+
+	protected static class DirectoryProposalContentAssistTestExtension extends DirectoryProposalContentAssist {
+		@Override
+		public void wait(int timeout) throws InterruptedException, ExecutionException, TimeoutException {
+			super.wait(timeout);
+		}
+	}
+
+	private DirectoryProposalContentAssistTestExtension contentAssist;
+
+	@Override
+	protected ContentProposalAdapter createContentProposalAdapter(Control control) {
+		contentAssist = new DirectoryProposalContentAssistTestExtension();
+		contentAssist.apply((Combo) control);
+		return contentAssist.getContentProposalAdapter();
+	}
+
+	@Override
+	public ContentProposalAdapter getContentProposalAdapter() {
+		return super.getContentProposalAdapter();
+	}
+
+	public DirectoryProposalContentAssistTestExtension getContentAssist() {
+		return contentAssist;
+	}
+}
diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/UiTestSuite.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/UiTestSuite.java
index b87e653..5af5e5c 100644
--- a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/UiTestSuite.java
+++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/UiTestSuite.java
@@ -16,6 +16,7 @@
  *******************************************************************************/
 package org.eclipse.ui.tests;
 
+import org.eclipse.ui.internal.ide.DirectoryProposalContentAssistTestSuite;
 import org.eclipse.ui.tests.activities.ActivitiesTestSuite;
 import org.eclipse.ui.tests.adaptable.AdaptableTestSuite;
 import org.eclipse.ui.tests.api.ApiTestSuite;
@@ -77,7 +78,8 @@
 	StatusHandlingTestSuite.class,
 	MenusTestSuite.class,
 	QuickAccessTestSuite.class,
-	FilteredResourcesSelectionDialogTestSuite.class
+	FilteredResourcesSelectionDialogTestSuite.class,
+	DirectoryProposalContentAssistTestSuite.class
 })
 public class UiTestSuite {