476383: add an attribute editor for label fields 

Add a text attribute editor that supports single and multi values and
has content assist

Change-Id: I9e53666e713abb1affbb7b6376c5af7192174ddb
Task-Url: https://bugs.eclipse.org/bugs/show_bug.cgi?id=476383
Signed-off-by: chris.poon <chris.poon@tasktop.com>
diff --git a/org.eclipse.mylyn.tasks.tests/src/org/eclipse/mylyn/tasks/tests/AllTasksTests.java b/org.eclipse.mylyn.tasks.tests/src/org/eclipse/mylyn/tasks/tests/AllTasksTests.java
index 4aab2e9..a819eb7 100644
--- a/org.eclipse.mylyn.tasks.tests/src/org/eclipse/mylyn/tasks/tests/AllTasksTests.java
+++ b/org.eclipse.mylyn.tasks.tests/src/org/eclipse/mylyn/tasks/tests/AllTasksTests.java
@@ -108,6 +108,7 @@
 		suite.addTestSuite(TaskDataStoreTest.class);
 		suite.addTestSuite(TaskExportImportTest.class);
 		suite.addTestSuite(PersonProposalProviderTest.class);
+		suite.addTestSuite(OptionsProposalProviderTest.class);
 		suite.addTestSuite(TaskRepositoryLocationTest.class);
 		suite.addTestSuite(TaskRepositoryTest.class);
 		suite.addTestSuite(AttachmentSizeFormatterTest.class);
diff --git a/org.eclipse.mylyn.tasks.tests/src/org/eclipse/mylyn/tasks/tests/OptionsProposalProviderTest.java b/org.eclipse.mylyn.tasks.tests/src/org/eclipse/mylyn/tasks/tests/OptionsProposalProviderTest.java
new file mode 100644
index 0000000..04a24cb
--- /dev/null
+++ b/org.eclipse.mylyn.tasks.tests/src/org/eclipse/mylyn/tasks/tests/OptionsProposalProviderTest.java
@@ -0,0 +1,176 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Tasktop Technologies and others.
+ * 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
+ *
+ * Contributors:
+ *     Tasktop Technologies - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylyn.tasks.tests;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import junit.framework.TestCase;
+
+import org.eclipse.jface.fieldassist.IContentProposal;
+import org.eclipse.mylyn.internal.tasks.ui.OptionsProposalProvider;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+public class OptionsProposalProviderTest extends TestCase {
+
+	@Override
+	protected void setUp() throws Exception {
+		TaskTestUtil.resetTaskListAndRepositories();
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		TaskTestUtil.resetTaskListAndRepositories();
+	}
+
+	@Test
+	public void testEmptyProposals() {
+		OptionsProposalProvider provider = new OptionsProposalProvider(new HashMap<String, String>(), true);
+		assertEquals(0, provider.getProposals("", 0).length);
+		assertEquals(0, provider.getProposals("", 10).length);
+		assertEquals(0, provider.getProposals("test", 0).length);
+	}
+
+	@Test
+	public void testSingleProposal() {
+		final Map<String, String> proposalMap = ImmutableMap.of("aTest", "1");
+		OptionsProposalProvider provider = new OptionsProposalProvider(proposalMap, true);
+
+		List<IContentProposal> proposals = Arrays.asList(provider.getProposals("", 0));
+		assertEquals(1, proposals.size());
+		assertProposal("aTest", "aTest", proposals.get(0));
+	}
+
+	@Test
+	public void testMultipleProposals() {
+		final Map<String, String> proposalMap = ImmutableMap.of("aTest", "1", "bTest", "2", "cTest", "3");
+		OptionsProposalProvider provider = new OptionsProposalProvider(proposalMap, true);
+
+		List<IContentProposal> proposals = Arrays.asList(provider.getProposals("", 0));
+		assertEquals(3, proposals.size());
+		assertProposal("aTest", "aTest", proposals.get(0));
+		assertProposal("bTest", "bTest", proposals.get(1));
+		assertProposal("cTest", "cTest", proposals.get(2));
+	}
+
+	@Test
+	public void testMultipleProposalsSorted() {
+		final Map<String, String> proposalMap = ImmutableMap.of("oneTest", "1", "twoTest", "2", "threeTest", "3");
+		OptionsProposalProvider provider = new OptionsProposalProvider(proposalMap, true);
+
+		List<IContentProposal> proposals = Arrays.asList(provider.getProposals("", 0));
+		assertEquals(3, proposals.size());
+		// NOTE: Sorted alphabetically by contents
+		assertProposal("oneTest", "oneTest", proposals.get(0));
+		assertProposal("threeTest", "threeTest", proposals.get(1));
+		assertProposal("twoTest", "twoTest", proposals.get(2));
+	}
+
+	@Test
+	public void testProposalFilterMultiSelect() {
+		final Map<String, String> proposalMap = ImmutableMap.of("OneTest", "1", "TwoTest", "2", "ThreeTest", "3");
+		OptionsProposalProvider provider = new OptionsProposalProvider(proposalMap, true);
+
+		assertEquals(0, provider.getProposals("ThreeTest", 0).length);
+
+		List<IContentProposal> proposals = Arrays.asList(provider.getProposals("ThreeTest,", 0));
+		assertEquals(2, proposals.size());
+		assertProposal("ThreeTest,OneTest", "OneTest", proposals.get(0));
+		assertProposal("ThreeTest,TwoTest", "TwoTest", proposals.get(1));
+
+		proposals = Arrays.asList(provider.getProposals("ThreeTest, ", 0));
+		assertEquals(2, proposals.size());
+		assertProposal("ThreeTest, OneTest", "OneTest", proposals.get(0));
+		assertProposal("ThreeTest, TwoTest", "TwoTest", proposals.get(1));
+
+		proposals = Arrays.asList(provider.getProposals("o", 0));
+		assertEquals(2, proposals.size());
+		assertProposal("OneTest", "OneTest", proposals.get(0));
+		assertProposal("TwoTest", "TwoTest", proposals.get(1));
+
+		proposals = Arrays.asList(provider.getProposals("O", 0));
+		assertEquals(2, proposals.size());
+		assertProposal("OneTest", "OneTest", proposals.get(0));
+		assertProposal("TwoTest", "TwoTest", proposals.get(1));
+
+		proposals = Arrays.asList(provider.getProposals("one", 0));
+		assertEquals(1, proposals.size());
+		assertProposal("OneTest", "OneTest", proposals.get(0));
+
+		assertEquals(0, provider.getProposals("four", 0).length);
+
+		proposals = Arrays.asList(provider.getProposals("four,", 0));
+		assertEquals(3, proposals.size());
+		assertProposal("four,OneTest", "OneTest", proposals.get(0));
+		assertProposal("four,ThreeTest", "ThreeTest", proposals.get(1));
+		assertProposal("four,TwoTest", "TwoTest", proposals.get(2));
+
+		proposals = Arrays.asList(provider.getProposals("four,   ", 0));
+		assertEquals(3, proposals.size());
+		assertProposal("four,   OneTest", "OneTest", proposals.get(0));
+		assertProposal("four,   ThreeTest", "ThreeTest", proposals.get(1));
+		assertProposal("four,   TwoTest", "TwoTest", proposals.get(2));
+
+		proposals = Arrays.asList(provider.getProposals(",,           ,four,        five     ,   ", 0));
+		assertEquals(3, proposals.size());
+		assertProposal(",,           ,four,        five     ,   OneTest", "OneTest", proposals.get(0));
+		assertProposal(",,           ,four,        five     ,   ThreeTest", "ThreeTest", proposals.get(1));
+		assertProposal(",,           ,four,        five     ,   TwoTest", "TwoTest", proposals.get(2));
+
+		proposals = Arrays.asList(provider.getProposals(",,           ,four,        five     ,   one", 0));
+		assertEquals(1, proposals.size());
+		assertProposal(",,           ,four,        five     ,   OneTest", "OneTest", proposals.get(0));
+	}
+
+	@Test
+	public void testProposalFilterSingleSelect() {
+		final Map<String, String> proposalMap = ImmutableMap.of("OneTest", "1", "TwoTest", "2", "ThreeTest", "3");
+		OptionsProposalProvider provider = new OptionsProposalProvider(proposalMap, false);
+
+		List<IContentProposal> proposals = Arrays.asList(provider.getProposals("ThreeTest", 0));
+		assertEquals(1, proposals.size());
+		assertProposal("ThreeTest", "ThreeTest", proposals.get(0));
+
+		assertEquals(0, provider.getProposals("ThreeTest,", 0).length);
+		assertEquals(0, provider.getProposals("ThreeTest, ", 0).length);
+
+		proposals = Arrays.asList(provider.getProposals("o", 0));
+		assertEquals(2, proposals.size());
+		assertProposal("OneTest", "OneTest", proposals.get(0));
+		assertProposal("TwoTest", "TwoTest", proposals.get(1));
+
+		proposals = Arrays.asList(provider.getProposals("O", 0));
+		assertEquals(2, proposals.size());
+		assertProposal("OneTest", "OneTest", proposals.get(0));
+		assertProposal("TwoTest", "TwoTest", proposals.get(1));
+
+		proposals = Arrays.asList(provider.getProposals("one", 0));
+		assertEquals(1, proposals.size());
+		assertProposal("OneTest", "OneTest", proposals.get(0));
+
+		assertEquals(0, provider.getProposals("four", 0).length);
+		assertEquals(0, provider.getProposals("four,", 0).length);
+		assertEquals(0, provider.getProposals("four,   ", 0).length);
+		assertEquals(0, provider.getProposals(",,           ,four,        five     ,   ", 0).length);
+		assertEquals(0, provider.getProposals(",,           ,four,        five     ,   one", 0).length);
+	}
+
+	private void assertProposal(String content, String label, IContentProposal proposal) {
+		assertEquals(content, proposal.getContent());
+		assertEquals(label, proposal.getLabel());
+		assertNull(proposal.getDescription());
+	}
+}
diff --git a/org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/internal/tasks/ui/editors/LabelsAttributeEditorTest.java b/org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/internal/tasks/ui/editors/LabelsAttributeEditorTest.java
new file mode 100644
index 0000000..36c73ac
--- /dev/null
+++ b/org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/internal/tasks/ui/editors/LabelsAttributeEditorTest.java
@@ -0,0 +1,135 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Tasktop Technologies and others.
+ * 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
+ *
+ * Contributors:
+ *     Tasktop Technologies - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylyn.internal.tasks.ui.editors;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.List;
+
+import org.eclipse.mylyn.internal.tasks.core.data.TaskDataState;
+import org.eclipse.mylyn.tasks.core.TaskRepository;
+import org.eclipse.mylyn.tasks.core.data.TaskAttribute;
+import org.eclipse.mylyn.tasks.core.data.TaskAttributeMapper;
+import org.eclipse.mylyn.tasks.core.data.TaskData;
+import org.eclipse.mylyn.tasks.core.data.TaskDataModel;
+import org.eclipse.mylyn.tasks.tests.TaskTestUtil;
+import org.eclipse.mylyn.tasks.tests.connector.MockTask;
+import org.eclipse.mylyn.tasks.ui.editors.LayoutHint.ColumnSpan;
+import org.eclipse.mylyn.tasks.ui.editors.LayoutHint.RowSpan;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class LabelsAttributeEditorTest {
+
+	private TaskData taskData;
+
+	private TaskDataModel model;
+
+	private LabelsAttributeEditor editor;
+
+	private TaskAttribute singleSelectAttribute;
+
+	private TaskAttribute multiSelectAttribute;
+
+	@Before
+	public void setUp() {
+		TaskRepository repository = TaskTestUtil.createMockRepository();
+		taskData = new TaskData(new TaskAttributeMapper(repository), "kind", "url", "id");
+		MockTask task = new MockTask("taskId");
+		TaskDataState state = new TaskDataState("kind", "url", "taskId");
+		state.setEditsData(taskData);
+		state.setLocalTaskData(taskData);
+		model = new TaskDataModel(repository, task, state);
+	}
+
+	@Test
+	public void testSingleSelectEmpty() {
+		createSingleSelect("");
+		assertSingleSelectValue("");
+		editor.setValue("test");
+		assertSingleSelectValue("test");
+		editor.setValue("");
+		assertSingleSelectValue("");
+	}
+
+	@Test
+	public void testSingleSelect() {
+		createSingleSelect("test option");
+		assertSingleSelectValue("test option");
+		editor.setValue("testing option");
+		assertSingleSelectValue("testing option");
+	}
+
+	@Test
+	public void testSingleSelectWithSeparator() {
+		createSingleSelect("one,two ,      three, four");
+		assertSingleSelectValue("one,two ,      three, four");
+		editor.setValue("one,two ,      three, four ,five     ,");
+		assertSingleSelectValue("one,two ,      three, four ,five     ,");
+	}
+
+	@Test
+	public void testMultiSelectEmpty() {
+		createMultiSelect(ImmutableList.<String> of());
+		assertMultiSelectValue("", ImmutableList.<String> of());
+		editor.setValue("one,two");
+		assertMultiSelectValue("one, two", ImmutableList.of("one", "two"));
+		editor.setValue("");
+		assertMultiSelectValue("", ImmutableList.<String> of());
+	}
+
+	@Test
+	public void testMultiSelect() {
+		createMultiSelect(ImmutableList.of("one", "two"));
+		assertMultiSelectValue("one, two", ImmutableList.of("one", "two"));
+		editor.setValue("one,two,three,four");
+		assertMultiSelectValue("one, two, three, four", ImmutableList.of("one", "two", "three", "four"));
+		editor.setValue("one");
+		assertMultiSelectValue("one", ImmutableList.of("one"));
+		editor.setValue("one,two ,      three,  four  ,five     ,,        ,");
+		assertMultiSelectValue("one, two, three, four, five", ImmutableList.of("one", "two", "three", "four", "five"));
+	}
+
+	private void createSingleSelect(String value) {
+		singleSelectAttribute = taskData.getRoot().createAttribute("singleSelect");
+		singleSelectAttribute.getMetaData().setType(TaskAttribute.TYPE_SINGLE_SELECT);
+		singleSelectAttribute.setValue(value);
+		editor = new LabelsAttributeEditor(model, singleSelectAttribute);
+		assertLayoutHint(false);
+	}
+
+	private void createMultiSelect(List<String> values) {
+		multiSelectAttribute = taskData.getRoot().createAttribute("multiSelect");
+		multiSelectAttribute.getMetaData().setType(TaskAttribute.TYPE_MULTI_SELECT);
+		multiSelectAttribute.setValues(ImmutableList.copyOf(values));
+		editor = new LabelsAttributeEditor(model, multiSelectAttribute);
+		assertLayoutHint(true);
+	}
+
+	private void assertSingleSelectValue(String value) {
+		assertEquals(value, editor.getValue());
+		assertEquals(value, singleSelectAttribute.getValue());
+	}
+
+	private void assertLayoutHint(boolean isMultiSelect) {
+		assertEquals(ColumnSpan.SINGLE, editor.getLayoutHint().columnSpan);
+		assertEquals((isMultiSelect ? RowSpan.MULTIPLE : RowSpan.SINGLE), editor.getLayoutHint().rowSpan);
+	}
+
+	private void assertMultiSelectValue(String stringValue, List<String> values) {
+		assertEquals(stringValue, editor.getValue());
+		assertEquals(values, multiSelectAttribute.getValues());
+	}
+
+}
diff --git a/org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/tasks/ui/editors/AttributeEditorToolkitTest.java b/org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/tasks/ui/editors/AttributeEditorToolkitTest.java
new file mode 100644
index 0000000..1ff7ac1
--- /dev/null
+++ b/org.eclipse.mylyn.tasks.ui.tests/src/org/eclipse/mylyn/tasks/ui/editors/AttributeEditorToolkitTest.java
@@ -0,0 +1,132 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Tasktop Technologies and others.
+ * 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
+ *
+ * Contributors:
+ *     Tasktop Technologies - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylyn.tasks.ui.editors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.eclipse.jface.fieldassist.IContentProposalProvider;
+import org.eclipse.mylyn.commons.workbench.WorkbenchUtil;
+import org.eclipse.mylyn.commons.workbench.editors.CommonTextSupport;
+import org.eclipse.mylyn.internal.tasks.ui.OptionsProposalProvider;
+import org.eclipse.mylyn.internal.tasks.ui.editors.LabelsAttributeEditor;
+import org.eclipse.mylyn.internal.tasks.ui.editors.MultiSelectionAttributeEditor;
+import org.eclipse.mylyn.internal.tasks.ui.editors.SingleSelectionAttributeEditor;
+import org.eclipse.mylyn.tasks.core.data.TaskAttribute;
+import org.eclipse.mylyn.tasks.core.data.TaskData;
+import org.eclipse.mylyn.tasks.core.data.TaskDataModel;
+import org.eclipse.mylyn.tasks.tests.TaskTestUtil;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.fieldassist.ContentAssistCommandAdapter;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+public class AttributeEditorToolkitTest {
+	public class TestAttributeEditorToolkit extends AttributeEditorToolkit {
+
+		private ContentAssistCommandAdapter commandAdapter;
+
+		TestAttributeEditorToolkit(CommonTextSupport textSupport) {
+			super(textSupport);
+		}
+
+		@Override
+		public ContentAssistCommandAdapter createContentAssistCommandAdapter(Control control,
+				IContentProposalProvider proposalProvider) {
+			commandAdapter = super.createContentAssistCommandAdapter(control, proposalProvider);
+			return commandAdapter;
+		}
+
+		@Override
+		public IContentProposalProvider createContentProposalProvider(AbstractAttributeEditor editor) {
+			return super.createContentProposalProvider(editor);
+		}
+	}
+
+	private TestAttributeEditorToolkit toolkit;
+
+	private TaskAttribute taskAttribute;
+
+	private final FormToolkit formToolkit = new FormToolkit(Display.getCurrent());
+
+	private final TaskDataModel taskDataModel = mock(TaskDataModel.class);
+
+	@SuppressWarnings("restriction")
+	@Before
+	public void setUp() {
+		CommonTextSupport textSupport = mock(CommonTextSupport.class);
+		toolkit = spy(new TestAttributeEditorToolkit(textSupport));
+		TaskData taskData = TaskTestUtil.createMockTaskData("1");
+		taskAttribute = taskData.getRoot();
+		when(taskDataModel.getTaskData()).thenReturn(taskData);
+	}
+
+	@After
+	public void tearDown() {
+		formToolkit.dispose();
+	}
+
+	@Test
+	public void testAdaptSingleSelectionAttributeEditor() {
+		SingleSelectionAttributeEditor editor = new SingleSelectionAttributeEditor(taskDataModel, taskAttribute);
+		assertNoOptionsProposalProvider(editor);
+	}
+
+	@Test
+	public void testAdaptMultiSelectionAttributeEditor() {
+		MultiSelectionAttributeEditor editor = new MultiSelectionAttributeEditor(taskDataModel, taskAttribute);
+		assertNoOptionsProposalProvider(editor);
+	}
+
+	private void assertNoOptionsProposalProvider(AbstractAttributeEditor editor) {
+		editor.createControl(WorkbenchUtil.getShell(), formToolkit);
+		toolkit.adapt(editor);
+		verify(toolkit, never()).createContentProposalProvider(any(AbstractAttributeEditor.class));
+		verify(toolkit, never()).createContentAssistCommandAdapter(any(Control.class),
+				any(IContentProposalProvider.class));
+	}
+
+	@Test
+	public void testAdaptLabelsAttributeEditor() {
+		assertOptionsProposalProvider(true);
+		assertOptionsProposalProvider(false);
+	}
+
+	private void assertOptionsProposalProvider(boolean isMultiSelect) {
+		taskAttribute.getMetaData().setType(
+				isMultiSelect ? TaskAttribute.TYPE_MULTI_SELECT : TaskAttribute.TYPE_SINGLE_SELECT);
+		LabelsAttributeEditor editor = new LabelsAttributeEditor(taskDataModel, taskAttribute);
+		editor.createControl(WorkbenchUtil.getShell(), formToolkit);
+		toolkit.adapt(editor);
+
+		verify(toolkit).createContentProposalProvider(editor);
+		ArgumentCaptor<IContentProposalProvider> providerCaptor = ArgumentCaptor.forClass(IContentProposalProvider.class);
+		verify(toolkit).createContentAssistCommandAdapter(eq(editor.getControl()), providerCaptor.capture());
+		IContentProposalProvider proposalProvider = providerCaptor.getValue();
+		assertTrue(proposalProvider instanceof OptionsProposalProvider);
+		assertEquals(isMultiSelect, ((OptionsProposalProvider) proposalProvider).isMultiSelect());
+		assertNull(toolkit.commandAdapter.getAutoActivationCharacters());
+	}
+
+}
diff --git a/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/OptionsProposalProvider.java b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/OptionsProposalProvider.java
new file mode 100644
index 0000000..574b262
--- /dev/null
+++ b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/OptionsProposalProvider.java
@@ -0,0 +1,84 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Tasktop Technologies and others.
+ * 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
+ *
+ * Contributors:
+ *     Tasktop Technologies - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylyn.internal.tasks.ui;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jface.fieldassist.ContentProposal;
+import org.eclipse.jface.fieldassist.IContentProposal;
+import org.eclipse.jface.fieldassist.IContentProposalProvider;
+import org.eclipse.mylyn.internal.tasks.ui.editors.LabelsAttributeEditor;
+
+import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Ordering;
+
+public class OptionsProposalProvider implements IContentProposalProvider {
+
+	private static final String VALUE_SEPARATOR = ","; //$NON-NLS-1$
+
+	private final Set<String> proposals;
+
+	private final boolean isMultiSelect;
+
+	public OptionsProposalProvider(Map<String, String> proposals, boolean isMultiSelect) {
+		this.proposals = proposals.keySet();
+		this.isMultiSelect = isMultiSelect;
+	}
+
+	@Override
+	public IContentProposal[] getProposals(String contents, int position) {
+		Set<String> filteredProposals = new HashSet<>(proposals);
+		filteredProposals.remove(""); //$NON-NLS-1$
+		String lastValue = ""; //$NON-NLS-1$
+		// If the attribute is of type multi-select, filter the past values from the proposals
+		if (isMultiSelect) {
+			String[] contentsArray = contents.split(VALUE_SEPARATOR, -1);
+			if (contentsArray.length > 0) {
+				List<String> trimmedContents = LabelsAttributeEditor.getTrimmedValues(contentsArray);
+				filteredProposals.removeAll(trimmedContents);
+				lastValue = contentsArray[contentsArray.length - 1].trim();
+			}
+		} else {
+			lastValue = contents;
+		}
+
+		// If there is a last value, then filter the remaining the proposals to contain it
+		if (!lastValue.isEmpty()) {
+			for (Iterator<String> iterator = filteredProposals.iterator(); iterator.hasNext();) {
+				String proposal = iterator.next();
+				if (!proposal.toLowerCase().contains(lastValue.toLowerCase())) {
+					iterator.remove();
+				}
+
+			}
+		}
+		// Since the contents of the editor is replaced, we need to include the existing values in the replacement
+		final String existingValues = contents.substring(0, contents.length() - lastValue.length());
+		ImmutableList<String> sortedProposals = FluentIterable.from(filteredProposals).toSortedList(
+				Ordering.from(String.CASE_INSENSITIVE_ORDER));
+		return FluentIterable.from(sortedProposals).transform(new Function<String, IContentProposal>() {
+			public IContentProposal apply(String proposal) {
+				return new ContentProposal(existingValues + proposal, proposal, null);
+			}
+		}).toArray(IContentProposal.class);
+	}
+
+	public boolean isMultiSelect() {
+		return isMultiSelect;
+	}
+}
diff --git a/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/editors/LabelsAttributeEditor.java b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/editors/LabelsAttributeEditor.java
new file mode 100644
index 0000000..d1a58c1
--- /dev/null
+++ b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/editors/LabelsAttributeEditor.java
@@ -0,0 +1,88 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Tasktop Technologies and others.
+ * 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
+ *
+ * Contributors:
+ *     Tasktop Technologies - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.mylyn.internal.tasks.ui.editors;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.mylyn.tasks.core.data.TaskAttribute;
+import org.eclipse.mylyn.tasks.core.data.TaskDataModel;
+import org.eclipse.mylyn.tasks.ui.editors.LayoutHint;
+import org.eclipse.mylyn.tasks.ui.editors.LayoutHint.ColumnSpan;
+import org.eclipse.mylyn.tasks.ui.editors.LayoutHint.RowSpan;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Predicate;
+import com.google.common.base.Strings;
+import com.google.common.collect.FluentIterable;
+
+public class LabelsAttributeEditor extends TextAttributeEditor {
+
+	private static final String VALUE_SEPARATOR = ","; //$NON-NLS-1$
+
+	private final boolean isMultiSelect;
+
+	public LabelsAttributeEditor(TaskDataModel manager, TaskAttribute taskAttribute) {
+		super(manager, taskAttribute);
+		this.isMultiSelect = TaskAttribute.TYPE_MULTI_SELECT.equals(taskAttribute.getMetaData().getType());
+		if (!isReadOnly() && isMultiSelect) {
+			setLayoutHint(new LayoutHint(RowSpan.MULTIPLE, ColumnSpan.SINGLE));
+		}
+	}
+
+	@Override
+	public void createControl(Composite parent, FormToolkit toolkit) {
+		super.createControl(parent, toolkit, (isMultiSelect ? SWT.WRAP : SWT.NONE));
+		if (!isReadOnly() && isMultiSelect) {
+			getText().setToolTipText("Separate multiple values with a comma"); //$NON-NLS-1$
+		}
+	}
+
+	@Override
+	public String getValue() {
+		if (isMultiSelect) {
+			List<String> values = getAttributeMapper().getValues(getTaskAttribute());
+			return Joiner.on(VALUE_SEPARATOR + " ").skipNulls().join(values); //$NON-NLS-1$
+		} else {
+			return getAttributeMapper().getValue(getTaskAttribute());
+		}
+	}
+
+	@Override
+	public void setValue(String text) {
+		if (isMultiSelect) {
+			String[] values = text.split(VALUE_SEPARATOR);
+			getAttributeMapper().setValues(getTaskAttribute(), getTrimmedValues(values));
+		} else {
+			getAttributeMapper().setValue(getTaskAttribute(), text);
+		}
+		attributeChanged();
+	}
+
+	public static List<String> getTrimmedValues(String[] values) {
+		return FluentIterable.from(Arrays.asList(values)).transform(new Function<String, String>() {
+			@Override
+			public String apply(String input) {
+				return Strings.nullToEmpty(input).trim();
+			}
+		}).filter(new Predicate<String>() {
+			@Override
+			public boolean apply(String input) {
+				return !Strings.isNullOrEmpty(input);
+			}
+		}).toList();
+	}
+}
diff --git a/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/editors/TextAttributeEditor.java b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/editors/TextAttributeEditor.java
index 5f4ea1e..6f6ed32 100644
--- a/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/editors/TextAttributeEditor.java
+++ b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/internal/tasks/ui/editors/TextAttributeEditor.java
@@ -45,6 +45,10 @@
 
 	@Override
 	public void createControl(Composite parent, FormToolkit toolkit) {
+		createControl(parent, toolkit, SWT.NONE);
+	}
+
+	protected void createControl(Composite parent, FormToolkit toolkit, int style) {
 		if (isReadOnly()) {
 			text = new Text(parent, SWT.FLAT | SWT.READ_ONLY);
 			text.setFont(EditorUtil.TEXT_FONT);
@@ -52,7 +56,7 @@
 			text.setToolTipText(getDescription());
 			text.setText(getValue());
 		} else {
-			text = toolkit.createText(parent, getValue(), SWT.FLAT);
+			text = toolkit.createText(parent, getValue(), SWT.FLAT | style);
 			text.setFont(EditorUtil.TEXT_FONT);
 			text.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TREE_BORDER);
 			text.setToolTipText(getDescription());
diff --git a/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/tasks/ui/editors/AttributeEditorToolkit.java b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/tasks/ui/editors/AttributeEditorToolkit.java
index 47ac0b3..b2a14c9 100644
--- a/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/tasks/ui/editors/AttributeEditorToolkit.java
+++ b/org.eclipse.mylyn.tasks.ui/src/org/eclipse/mylyn/tasks/ui/editors/AttributeEditorToolkit.java
@@ -28,9 +28,11 @@
 import org.eclipse.jface.viewers.ILabelProvider;
 import org.eclipse.mylyn.commons.ui.compatibility.CommonThemes;
 import org.eclipse.mylyn.commons.workbench.editors.CommonTextSupport;
+import org.eclipse.mylyn.internal.tasks.ui.OptionsProposalProvider;
 import org.eclipse.mylyn.internal.tasks.ui.PersonProposalLabelProvider;
 import org.eclipse.mylyn.internal.tasks.ui.PersonProposalProvider;
 import org.eclipse.mylyn.internal.tasks.ui.editors.EditorUtil;
+import org.eclipse.mylyn.internal.tasks.ui.editors.LabelsAttributeEditor;
 import org.eclipse.mylyn.internal.tasks.ui.editors.Messages;
 import org.eclipse.mylyn.internal.tasks.ui.editors.PersonAttributeEditor;
 import org.eclipse.mylyn.internal.tasks.ui.editors.RepositoryTextViewerConfiguration.Mode;
@@ -81,7 +83,16 @@
 	}
 
 	public void adapt(AbstractAttributeEditor editor) {
-		if (editor.getControl() instanceof Text || editor.getControl() instanceof CCombo
+		if (editor instanceof LabelsAttributeEditor) {
+			Control control = editor.getControl();
+			IContentProposalProvider contentProposalProvider = createContentProposalProvider(editor);
+			if (contentProposalProvider != null) {
+				ContentAssistCommandAdapter adapter = createContentAssistCommandAdapter(control,
+						contentProposalProvider);
+				adapter.setAutoActivationCharacters(null);
+				adapter.setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE);
+			}
+		} else if (editor.getControl() instanceof Text || editor.getControl() instanceof CCombo
 				|| editor instanceof PersonAttributeEditor) {
 			Control control = (editor instanceof PersonAttributeEditor)
 					? ((PersonAttributeEditor) editor).getText()
@@ -91,12 +102,11 @@
 				control = editor.getControl();
 			}
 			if (!editor.isReadOnly() && hasContentAssist(editor.getTaskAttribute())) {
-				IContentProposalProvider contentProposalProvider = createContentProposalProvider(editor.getTaskAttribute());
+				IContentProposalProvider contentProposalProvider = createContentProposalProvider(editor);
 				ILabelProvider labelPropsalProvider = createLabelProposalProvider(editor.getTaskAttribute());
 				if (contentProposalProvider != null && labelPropsalProvider != null) {
-					ContentAssistCommandAdapter adapter = new ContentAssistCommandAdapter(control,
-							getContentAdapter(control), contentProposalProvider,
-							ITextEditorActionDefinitionIds.CONTENT_ASSIST_PROPOSALS, new char[0], true);
+					ContentAssistCommandAdapter adapter = createContentAssistCommandAdapter(control,
+							contentProposalProvider);
 					adapter.setLabelProvider(labelPropsalProvider);
 					adapter.setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE);
 					if (editor instanceof PersonAttributeEditor) {
@@ -127,6 +137,12 @@
 		editor.decorate(getColorIncoming());
 	}
 
+	ContentAssistCommandAdapter createContentAssistCommandAdapter(Control control,
+			IContentProposalProvider proposalProvider) {
+		return new ContentAssistCommandAdapter(control, getContentAdapter(control), proposalProvider,
+				ITextEditorActionDefinitionIds.CONTENT_ASSIST_PROPOSALS, new char[0], true);
+	}
+
 	private IControlContentAdapter getContentAdapter(Control control) {
 		if (control instanceof Combo) {
 			return new ComboContentAdapter();
@@ -171,12 +187,17 @@
 	/**
 	 * Creates an IContentProposalProvider to provide content assist proposals for the given attribute.
 	 * 
-	 * @param attribute
-	 *            attribute for which to provide content assist.
+	 * @param editor
+	 *            editor for which to provide content assist.
 	 * @return the IContentProposalProvider.
 	 */
-	private IContentProposalProvider createContentProposalProvider(TaskAttribute attribute) {
+	IContentProposalProvider createContentProposalProvider(AbstractAttributeEditor editor) {
+		TaskAttribute attribute = editor.getTaskAttribute();
 		Map<String, String> proposals = attribute.getTaskData().getAttributeMapper().getOptions(attribute);
+		if (editor instanceof LabelsAttributeEditor) {
+			return new OptionsProposalProvider(proposals,
+					TaskAttribute.TYPE_MULTI_SELECT.equals(attribute.getMetaData().getType()));
+		}
 		return new PersonProposalProvider(null, attribute.getTaskData(), proposals);
 	}