indicate if morphing is possible

IVexWidget.canMorph(QualifiedName) indicates if morphing the current
element into an element with the given name is possible.


Change-Id: I4a2f15124ed9014dbf00f543dbb5b4f1a8b565ca
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/validator/DTDValidatorTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/validator/DTDValidatorTest.java
index 0e7674d..76d60b1 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/validator/DTDValidatorTest.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/validator/DTDValidatorTest.java
@@ -28,7 +28,6 @@
 
 import org.eclipse.core.runtime.QualifiedName;
 import org.eclipse.vex.core.internal.dom.Document;
-import org.eclipse.vex.core.internal.validator.WTPVEXValidator;
 import org.eclipse.vex.core.provisional.dom.AttributeDefinition;
 import org.eclipse.vex.core.provisional.dom.IDocument;
 import org.eclipse.vex.core.provisional.dom.IElement;
@@ -272,6 +271,11 @@
 		assertTrue("isRequired should be true", ad.isRequired());
 	}
 
+	@Test
+	public void givenEmptyElement_shouldBePartiallyValid() throws Exception {
+		assertTrue(validator.isValidSequence(new QualifiedName(null, "section"), Collections.<QualifiedName> emptyList(), true));
+	}
+
 	private Map<QualifiedName, AttributeDefinition> getAttributeMap(final IElement element) {
 		final List<AttributeDefinition> atts = validator.getAttributeDefinitions(element);
 		final Map<QualifiedName, AttributeDefinition> adMap = new HashMap<QualifiedName, AttributeDefinition>();
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2SimpleEditingTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2SimpleEditingTest.java
index 2d4a9db..f8b4864 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2SimpleEditingTest.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2SimpleEditingTest.java
@@ -4,7 +4,7 @@
  * 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:

  * 		Florian Thienel - initial API and implementation

  * 		Carsten Hiesserich - additional tests (bug 407827, 409032)

@@ -14,6 +14,7 @@
 import static org.eclipse.vex.core.internal.widget.VexWidgetTest.PARA;

 import static org.eclipse.vex.core.internal.widget.VexWidgetTest.PRE;

 import static org.eclipse.vex.core.internal.widget.VexWidgetTest.TITLE;

+import static org.eclipse.vex.core.internal.widget.VexWidgetTest.assertCanMorphOnlyTo;

 import static org.eclipse.vex.core.internal.widget.VexWidgetTest.createDocumentWithDTD;

 import static org.eclipse.vex.core.internal.widget.VexWidgetTest.getCurrentXML;

 import static org.eclipse.vex.core.tests.TestResources.TEST_DTD;

@@ -29,6 +30,7 @@
 import org.eclipse.vex.core.internal.css.CssWhitespacePolicy;

 import org.eclipse.vex.core.internal.css.StyleSheet;

 import org.eclipse.vex.core.internal.css.StyleSheetReader;

+import org.eclipse.vex.core.internal.io.XMLFragment;

 import org.eclipse.vex.core.internal.undo.CannotRedoException;

 import org.eclipse.vex.core.provisional.dom.DocumentValidationException;

 import org.eclipse.vex.core.provisional.dom.IDocumentFragment;

@@ -597,6 +599,100 @@
 		assertEquals(expectedXml, getCurrentXML(widget));

 	}

 

+	@Test

+	public void whenReadOnly_cannotMorph() throws Exception {

+		widget.insertElement(TITLE);

+		widget.setReadOnly(true);

+		assertFalse(widget.canMorph(PARA));

+	}

+

+	@Test(expected = ReadOnlyException.class)

+	public void whenReadOnly_shouldNotMorph() throws Exception {

+		widget.insertElement(TITLE);

+		widget.setReadOnly(true);

+		widget.morph(PARA);

+	}

+

+	@Test

+	public void cannotMorphRootElement() throws Exception {

+		assertFalse(widget.canMorph(TITLE));

+	}

+

+	@Test

+	public void morphEmptyElement() throws Exception {

+		widget.insertElement(TITLE);

+

+		assertTrue(widget.canMorph(PARA));

+		assertCanMorphOnlyTo(widget, PARA);

+		widget.morph(PARA);

+	}

+

+	@Test

+	public void givenElementWithText_whenMorphing_shouldPreserveText() throws Exception {

+		widget.insertElement(TITLE);

+		widget.insertText("text");

+

+		assertTrue(widget.canMorph(PARA));

+		widget.morph(PARA);

+

+		widget.selectAll();

+		assertEquals("<section><para>text</para></section>", new XMLFragment(widget.getSelectedFragment()).getXML());

+	}

+

+	@Test

+	public void givenElementWithChildren_whenStructureIsInvalidAfterMorphing_cannotMorph() throws Exception {

+		widget.insertElement(PARA);

+		widget.insertText("before");

+		widget.insertElement(PRE);

+		widget.insertText("within");

+		widget.moveBy(1);

+		widget.insertText("after");

+

+		assertFalse(widget.canMorph(TITLE));

+	}

+

+	@Test

+	public void givenElementWithChildren_whenStructureIsInvalidAfterMorphing_shouldNotProvideElementToMorph() throws Exception {

+		widget.insertElement(PARA);

+		widget.insertText("before");

+		widget.insertElement(PRE);

+		widget.insertText("within");

+		widget.moveBy(1);

+		widget.insertText("after");

+

+		assertCanMorphOnlyTo(widget /* nothing */);

+	}

+

+	@Test(expected = CannotRedoException.class)

+	public void givenElementWithChildren_whenStructureIsInvalidAfterMorphing_shouldNotMorph() throws Exception {

+		widget.insertElement(PARA);

+		widget.insertText("before");

+		widget.insertElement(PRE);

+		widget.insertText("within");

+		widget.moveBy(1);

+		widget.insertText("after");

+

+		widget.morph(TITLE);

+	}

+

+	@Test

+	public void givenAlternativeElement_whenElementIsNotAllowedAtCurrentInsertionPosition_cannotMorph() throws Exception {

+		widget.insertElement(PARA);

+		widget.moveBy(1);

+		widget.insertElement(PARA);

+

+		assertFalse(widget.canMorph(TITLE));

+	}

+

+	@Test

+	public void givenAlternativeElement_whenElementIsNotAllowedAtCurrentInsertionPosition_shouldNotProvideElementToMorph() throws Exception {

+		widget.insertElement(PARA);

+		widget.moveBy(1);

+		widget.insertElement(PARA);

+

+		assertCanMorphOnlyTo(widget /* nothing */);

+	}

+

 	private static StyleSheet readTestStyleSheet() throws IOException {

 		return new StyleSheetReader().read(TestResources.get("test.css"));

 	}

diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/VexWidgetTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/VexWidgetTest.java
index 474fd90..59c53fb 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/VexWidgetTest.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/VexWidgetTest.java
@@ -4,7 +4,7 @@
  * 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:
  *     Florian Thienel - bug 315914, initial implementation
  *******************************************************************************/
@@ -143,12 +143,18 @@
 		return document;
 	}
 
-	public static void assertCanInsertOnly(final IVexWidget widget, final String... elementNames) {
+	public static void assertCanInsertOnly(final IVexWidget widget, final Object... elementNames) {
 		final String[] expected = sortedCopyOf(elementNames);
 		final String[] actual = sortedCopyOf(widget.getValidInsertElements());
 		assertEquals(Arrays.toString(expected), Arrays.toString(actual));
 	}
 
+	public static void assertCanMorphOnlyTo(final IVexWidget widget, final Object... elementNames) {
+		final String[] expected = sortedCopyOf(elementNames);
+		final String[] actual = sortedCopyOf(widget.getValidMorphElements());
+		assertEquals(Arrays.toString(expected), Arrays.toString(actual));
+	}
+
 	public static void assertCannotInsertAnything(final IVexWidget widget) {
 		assertCanInsertOnly(widget /* nothing */);
 	}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/validator/WTPVEXValidator.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/validator/WTPVEXValidator.java
index fdac850..f11f434 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/validator/WTPVEXValidator.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/validator/WTPVEXValidator.java
@@ -364,6 +364,10 @@
 	}
 
 	public boolean isValidSequence(final QualifiedName element, final List<QualifiedName> nodes, final boolean partial) {
+		if (partial && nodes.isEmpty()) {
+			return true;
+		}
+
 		final CMNode parent = getSchema(element.getQualifier()).getElements().getNamedItem(element.getLocalName());
 		if (!(parent instanceof CMElementDeclaration)) {
 			return true;
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BaseVexWidget.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BaseVexWidget.java
index 57c2c74..d9a220d 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BaseVexWidget.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/BaseVexWidget.java
@@ -4,11 +4,11 @@
  * 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:
  *     John Krasnay - initial API and implementation
  *     Igor Jacy Lino Campista - Java 5 warnings fixed (bug 311325)
- *     Holger Voormann - bug 315914: content assist should only show elements 
+ *     Holger Voormann - bug 315914: content assist should only show elements
  *			valid in the current context
  *     Carsten Hiesserich - handling of elements within comments (bug 407801)
  *     Carsten Hiesserich - allow insertion of newline into pre elements (bug 407827)
@@ -297,11 +297,10 @@
 		}
 
 		final IElement parent = document.getElementForInsertionAt(startOffset);
-		final List<QualifiedName> seq1 = Node.getNodeNames(parent.children().before(startOffset));
-		final List<QualifiedName> seq2 = nodeNames;
-		final List<QualifiedName> seq3 = Node.getNodeNames(parent.children().after(endOffset));
+		final List<QualifiedName> nodesBefore = Node.getNodeNames(parent.children().before(startOffset));
+		final List<QualifiedName> nodesAfter = Node.getNodeNames(parent.children().after(endOffset));
 
-		return validator.isValidSequence(parent.getQualifiedName(), seq1, seq2, seq3, true);
+		return validator.isValidSequence(parent.getQualifiedName(), nodesBefore, nodeNames, nodesAfter, true);
 	}
 
 	public boolean canPaste() {
@@ -347,11 +346,11 @@
 			return false;
 		}
 
-		final List<QualifiedName> seq1 = Node.getNodeNames(parent.children().before(element.getStartOffset()));
-		final List<QualifiedName> seq2 = Node.getNodeNames(element.children());
-		final List<QualifiedName> seq3 = Node.getNodeNames(parent.children().after(element.getEndOffset()));
+		final List<QualifiedName> nodesBefore = Node.getNodeNames(parent.children().before(element.getStartOffset()));
+		final List<QualifiedName> newNodes = Node.getNodeNames(element.children());
+		final List<QualifiedName> nodesAfter = Node.getNodeNames(parent.children().after(element.getEndOffset()));
 
-		return validator.isValidSequence(parent.getQualifiedName(), seq1, seq2, seq3, true);
+		return validator.isValidSequence(parent.getQualifiedName(), nodesBefore, newNodes, nodesAfter, true);
 	}
 
 	public void copySelection() {
@@ -670,11 +669,7 @@
 
 		Collections.sort(candidates, new QualifiedNameComparator());
 
-		final ElementName[] result = new ElementName[candidates.size()];
-		int i = 0;
-		for (final QualifiedName candidate : candidates) {
-			result[i++] = new ElementName(candidate, parent.getNamespacePrefix(candidate.getQualifier()));
-		}
+		final ElementName[] result = toElementNames(parent, candidates);
 		return result;
 	}
 
@@ -699,7 +694,7 @@
 			sequence.addAll(nodesBefore);
 			sequence.add(candidate);
 			sequence.addAll(nodesAfter);
-			if (!validator.isValidSequence(parent.getQualifiedName(), sequence, true)) {
+			if (!canContainContent(validator, parent.getQualifiedName(), sequence)) {
 				iterator.remove();
 			}
 		}
@@ -708,7 +703,7 @@
 	private static void filterInvalidSelectionParents(final IValidator validator, final List<QualifiedName> selectedNodes, final List<QualifiedName> candidates) {
 		for (final Iterator<QualifiedName> iter = candidates.iterator(); iter.hasNext();) {
 			final QualifiedName candidate = iter.next();
-			if (!validator.isValidSequence(candidate, selectedNodes, true)) {
+			if (!canContainContent(validator, candidate, selectedNodes)) {
 				iter.remove();
 			}
 		}
@@ -722,48 +717,6 @@
 		return debugging;
 	}
 
-	public ElementName[] getValidMorphElements() {
-		if (readOnly) {
-			return new ElementName[0];
-		}
-
-		if (document == null) {
-			return new ElementName[0];
-		}
-
-		final IValidator validator = document.getValidator();
-		if (validator == null) {
-			return new ElementName[0];
-		}
-
-		final IElement element = document.getElementForInsertionAt(getCaretOffset());
-		final IElement parent = element.getParentElement();
-		if (parent == null) {
-			// can't morph the root
-			return new ElementName[0];
-		}
-
-		final List<QualifiedName> candidates = createCandidatesList(validator, parent, IValidator.PCDATA, element.getQualifiedName());
-
-		// root out those that can't contain the current content
-		final List<QualifiedName> content = Node.getNodeNames(element.children());
-
-		for (final Iterator<QualifiedName> iter = candidates.iterator(); iter.hasNext();) {
-			final QualifiedName candidate = iter.next();
-			if (!validator.isValidSequence(candidate, content, true)) {
-				iter.remove();
-			}
-		}
-
-		Collections.sort(candidates, new QualifiedNameComparator());
-		final ElementName[] result = new ElementName[candidates.size()];
-		int i = 0;
-		for (final QualifiedName candidate : candidates) {
-			result[i++] = new ElementName(candidate, parent.getNamespacePrefix(candidate.getQualifier()));
-		}
-		return result;
-	}
-
 	private int getSelectionEnd() {
 		return selectionEnd;
 	}
@@ -1043,6 +996,97 @@
 		}
 	}
 
+	public ElementName[] getValidMorphElements() {
+		final IElement currentElement = document.getElementForInsertionAt(getCaretOffset());
+		if (!canMorphElement(currentElement)) {
+			return new ElementName[0];
+		}
+
+		final IValidator validator = document.getValidator();
+		final IElement parent = currentElement.getParentElement();
+		final List<QualifiedName> candidates = createCandidatesList(validator, parent, IValidator.PCDATA, currentElement.getQualifiedName());
+		if (candidates.isEmpty()) {
+			return new ElementName[0];
+		}
+
+		final List<QualifiedName> content = Node.getNodeNames(currentElement.children());
+		final List<QualifiedName> nodesBefore = Node.getNodeNames(parent.children().before(currentElement.getStartOffset()));
+		final List<QualifiedName> nodesAfter = Node.getNodeNames(parent.children().after(currentElement.getEndOffset()));
+
+		for (final Iterator<QualifiedName> iter = candidates.iterator(); iter.hasNext();) {
+			final QualifiedName candidate = iter.next();
+			if (!canContainContent(validator, candidate, content)) {
+				iter.remove();
+			} else if (!isValidChild(validator, parent.getQualifiedName(), candidate, nodesBefore, nodesAfter)) {
+				iter.remove();
+			}
+		}
+
+		Collections.sort(candidates, new QualifiedNameComparator());
+		return toElementNames(parent, candidates);
+	}
+
+	private static ElementName[] toElementNames(final IElement parent, final List<QualifiedName> candidates) {
+		final ElementName[] result = new ElementName[candidates.size()];
+		int i = 0;
+		for (final QualifiedName candidate : candidates) {
+			result[i++] = new ElementName(candidate, parent.getNamespacePrefix(candidate.getQualifier()));
+		}
+		return result;
+	}
+
+	private boolean canMorphElement(final IElement element) {
+		if (readOnly) {
+			return false;
+		}
+
+		if (document == null) {
+			return false;
+		}
+
+		if (document.getValidator() == null) {
+			return false;
+		}
+
+		if (element.getParentElement() == null) {
+			return false;
+		}
+
+		if (element == document.getRootElement()) {
+			return false;
+		}
+
+		return true;
+	}
+
+	private static boolean canContainContent(final IValidator validator, final QualifiedName elementName, final List<QualifiedName> content) {
+		return validator.isValidSequence(elementName, content, true);
+	}
+
+	private static boolean isValidChild(final IValidator validator, final QualifiedName parentName, final QualifiedName elementName, final List<QualifiedName> nodesBefore,
+			final List<QualifiedName> nodesAfter) {
+		return validator.isValidSequence(parentName, nodesBefore, Arrays.asList(elementName), nodesAfter, true);
+	}
+
+	public boolean canMorph(final QualifiedName elementName) {
+		final IElement currentElement = document.getElementForInsertionAt(getCaretOffset());
+		if (!canMorphElement(currentElement)) {
+			return false;
+		}
+
+		final IValidator validator = document.getValidator();
+
+		if (!canContainContent(validator, elementName, Node.getNodeNames(currentElement.children()))) {
+			return false;
+		}
+
+		final IElement parent = currentElement.getParentElement();
+		final List<QualifiedName> nodesBefore = Node.getNodeNames(parent.children().before(currentElement.getStartOffset()));
+		final List<QualifiedName> nodesAfter = Node.getNodeNames(parent.children().after(currentElement.getEndOffset()));
+
+		return isValidChild(validator, parent.getQualifiedName(), elementName, nodesBefore, nodesAfter);
+	}
+
 	public void morph(final QualifiedName elementName) throws DocumentValidationException, ReadOnlyException {
 		if (readOnly) {
 			throw new ReadOnlyException(MessageFormat.format("Cannot morph to element {0}, because the editor is read-only.", elementName));
@@ -1542,11 +1586,11 @@
 		final int startOffset = element.getStartOffset();
 		final int endOffset = element.getEndOffset();
 
-		final List<QualifiedName> seq1 = Node.getNodeNames(parent.children().before(startOffset));
-		final List<QualifiedName> seq2 = Arrays.asList(element.getQualifiedName(), element.getQualifiedName());
-		final List<QualifiedName> seq3 = Node.getNodeNames(parent.children().after(endOffset));
+		final List<QualifiedName> nodesBefore = Node.getNodeNames(parent.children().before(startOffset));
+		final List<QualifiedName> newNodes = Arrays.asList(element.getQualifiedName(), element.getQualifiedName());
+		final List<QualifiedName> nodesAfter = Node.getNodeNames(parent.children().after(endOffset));
 
-		return validator.isValidSequence(parent.getQualifiedName(), seq1, seq2, seq3, true);
+		return validator.isValidSequence(parent.getQualifiedName(), nodesBefore, newNodes, nodesAfter, true);
 	}
 
 	public void split() throws DocumentValidationException, ReadOnlyException {
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IVexWidget.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IVexWidget.java
index 5f8393d..ab936e2 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IVexWidget.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/IVexWidget.java
@@ -589,6 +589,15 @@
 	boolean canUnwrap();
 
 	/**
+	 * Indicates whether the current element can be morphed into the given element.
+	 * 
+	 * @param elementName
+	 *            Qualified name of the element to morph the current element into.
+	 * @return true if the current element can be morphed
+	 */
+	boolean canMorph(QualifiedName elementName);
+
+	/**
 	 * Replaces the current element with an element with the given name. The content of the element is preserved.
 	 * 
 	 * @param elementName
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/VexWidget.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/VexWidget.java
index 34b5a8f..359c5cf 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/VexWidget.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/VexWidget.java
@@ -304,6 +304,10 @@
 		return impl.isDebugging();
 	}
 
+	public boolean canMorph(final QualifiedName elementName) {
+		return impl.canMorph(elementName);
+	}
+
 	public void morph(final QualifiedName elementName) throws DocumentValidationException {
 		impl.morph(elementName);
 	}