blob: 4df8b40c1b8b2337d13d6b4d9b353bb2b264b993 [file] [log] [blame]
/*******************************************************************************
*Copyright (c) 2008 Standards for Technology in Automotive Retail 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:
* David Carver (STAR) - bug 244978 - initial API and implementation
*******************************************************************************/
package org.eclipse.wst.xsl.ui.internal.contentassist;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.swt.graphics.Image;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
import org.eclipse.wst.sse.core.internal.util.Debug;
import org.eclipse.wst.sse.ui.internal.contentassist.CustomCompletionProposal;
import org.eclipse.wst.xml.core.internal.contentmodel.CMElementDeclaration;
import org.eclipse.wst.xml.core.internal.contentmodel.CMNode;
import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.ModelQuery;
import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.ModelQueryAction;
import org.eclipse.wst.xml.core.internal.modelquery.ModelQueryUtil;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
import org.eclipse.wst.xml.ui.internal.contentassist.XMLRelevanceConstants;
import org.eclipse.wst.xml.ui.internal.editor.CMImageUtil;
import org.eclipse.wst.xml.ui.internal.editor.XMLEditorPluginImageHelper;
import org.eclipse.wst.xml.ui.internal.editor.XMLEditorPluginImages;
import org.eclipse.wst.xml.ui.internal.taginfo.MarkupTagInfoProvider;
import org.eclipse.wst.xml.xpath.core.util.XSLTXPathHelper;
import org.eclipse.wst.xsl.ui.internal.contentassist.contentmodel.XSLContentModelGenerator;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* This class provides content assistance proposals outside of the XSL namespace. Normal
* XML editor content assistance only provides proposals for items within the same namespace
* or if an element has children elements. This class extends this functionality by checking
* for the first XSL ancestor and uses that to determine what proposals should be
* provided in the way of xsl elements.
*
* @author David Carver
* @since 1.0
*/
public class ElementContentAssistRequest extends
AbstractXSLContentAssistRequest {
private XSLContentModelGenerator contentModel;
private static final String XPATH_FIRST_XSLANCESTOR_NODE = "ancestor::xsl:*[1]";
private MarkupTagInfoProvider infoProvider = null;
/**
* @param node
* @param parent
* @param documentRegion
* @param completionRegion
* @param begin
* @param length
* @param filter
* @param textViewer
*/
public ElementContentAssistRequest(Node node,
IStructuredDocumentRegion documentRegion,
ITextRegion completionRegion, int begin, int length, String filter,
ITextViewer textViewer) {
super(node, documentRegion, completionRegion, begin, length,
filter, textViewer);
contentModel = new XSLContentModelGenerator();
}
/**
* Provides a list of possible proposals for the XSL Elements within the current
* scope.
*/
@Override
public ArrayList<ICompletionProposal> getCompletionProposals() {
if (region.getType() == DOMRegionContext.XML_TAG_OPEN) {
computeTagOpenProposals();
} else if (region.getType() == DOMRegionContext.XML_TAG_NAME) {
computeTagNameProposals();
}
return getAllCompletionProposals();
}
/**
* Calculate proposals for open content regions.
*/
protected void computeTagOpenProposals() {
if (replacementBeginPosition == documentRegion.getStartOffset(region)) {
if (node.getNodeType() == Node.ELEMENT_NODE) {
// at the start of an existing tag, right before the '<'
computeTagNameProposals();
}
} else {
// within the white space
ITextRegion name = getNameRegion(((IDOMNode) node)
.getStartStructuredDocumentRegion());
if ((name != null)
&& ((documentRegion.getStartOffset(name) <= replacementBeginPosition) && (documentRegion
.getEndOffset(name) >= replacementBeginPosition))) {
// replace the existing name
replacementBeginPosition = documentRegion.getStartOffset(name);
replacementLength = name.getTextLength();
} else {
// insert a valid new name, or possibly an end tag
// addEndTagProposals(contentAssistRequest);
setReplacementLength(0);
}
addTagNameProposals(getElementPosition(node));
}
}
/**
* Calculates the proposals for the XML Tag Name Region.
*/
protected void computeTagNameProposals() {
// completing the *first* tag in "<tagname1 |<tagname2"
// Ignore attributes
if (inAttributeRegion()) {
return;
}
IDOMNode actualNode = (IDOMNode) node;
addTagNameProposals(this.getElementPosition(node));
// addEndTagNameProposals();
}
/**
* Check to see if the current position is in an Attribute Region if so,
* return true otherwise false
* @return True if in attribute region, false otherwise.
*/
protected boolean inAttributeRegion() {
return replacementBeginPosition > documentRegion.getStartOffset(region) + region.getLength();
}
/**
* Adds proposals for the XML_TAG_NAME region.
* @param position
*/
protected void addTagNameProposals(int position) {
Node ancestorNode = null;
try {
ancestorNode = XSLTXPathHelper.selectSingleNode(getNode(),
XPATH_FIRST_XSLANCESTOR_NODE);
} catch (Exception ex) {
return;
}
List<CMNode> cmnodes = null;
if (ancestorNode.getNodeType() == Node.ELEMENT_NODE) {
cmnodes = getAvailableChildElementDeclarations(
(Element) ancestorNode, 0);
Iterator<CMNode> nodeIterator = cmnodes.iterator();
// chop off any leading <'s and whitespace from the matchstring
while ((matchString.length() > 0)
&& (Character.isWhitespace(matchString.charAt(0)) || beginsWith(
matchString, "<"))) {
//$NON-NLS-1$
matchString = matchString.substring(1);
}
if (!nodeIterator.hasNext()) {
return;
}
while (nodeIterator.hasNext()) {
CMNode elementDecl = (CMNode) nodeIterator.next();
if (elementDecl != null) {
// only add proposals for the child element's that begin
// with the matchstring
String proposedText = null;
proposedText = contentModel.getRequiredName(ancestorNode,
elementDecl);
int cursorAdjustment = proposedText.length();
if (elementDecl instanceof CMElementDeclaration) {
CMElementDeclaration ed = (CMElementDeclaration) elementDecl;
if (ed.getContentType() == CMElementDeclaration.EMPTY) {
proposedText += contentModel.getStartTagClose(
ancestorNode, ed);
cursorAdjustment = proposedText.length();
} else {
StringBuffer sb = new StringBuffer();
contentModel.generateTag(ancestorNode, ed, sb);
// since it's a name proposal, assume '<' is
// already there
// only return the rest of the tag
proposedText = sb.toString().substring(1);
cursorAdjustment = getCursorPositionForProposedText(proposedText);
}
}
if (beginsWith(proposedText, matchString)) {
Image image = CMImageUtil.getImage(elementDecl);
if (image == null) {
image = XMLEditorPluginImageHelper
.getInstance()
.getImage(
XMLEditorPluginImages.IMG_OBJ_TAG_GENERIC);
}
String proposedInfo = getAdditionalInfo(
getCMElementDeclaration(getParent()), elementDecl);
CustomCompletionProposal proposal = new CustomCompletionProposal(
proposedText, getReplacementBeginPosition(),
getReplacementLength(), cursorAdjustment,
image, contentModel.getRequiredName(getParent(),
elementDecl), null, proposedInfo,
XMLRelevanceConstants.R_TAG_NAME);
addProposal(proposal);
}
}
}
}
}
/** Returns a list of CMNodes that are available within this parent context
* Given the grammar shown below and a snippet of XML code (where the '|'
* indicated the cursor position)
* the list would return all of the element declarations that are
* potential child elements of Foo.
*
* grammar : Foo -> (A, B, C)
* snippet : <Foo><A>|
* result : {A, B, C}
*
* @param parent
* @param childPosition
* @return
*/
protected List<CMNode> getAvailableChildElementDeclarations(Element parent,
int childPosition) {
List modelQueryActions = getAvailableChildrenAtIndex(parent,
childPosition, ModelQuery.VALIDITY_NONE);
Iterator iterator = modelQueryActions.iterator();
List<CMNode> cmnodes = new Vector();
while (iterator.hasNext()) {
ModelQueryAction action = (ModelQueryAction) iterator.next();
if ((childPosition < 0)
|| (((action.getStartIndex() <= childPosition) && (childPosition <= action
.getEndIndex())))) {
CMNode actionCMNode = action.getCMNode();
if ((actionCMNode != null) && !cmnodes.contains(actionCMNode)) {
cmnodes.add(actionCMNode);
}
}
}
return cmnodes;
}
// returns a list of ModelQueryActions
protected List getAvailableChildrenAtIndex(Element parent, int index,
int validityChecking) {
List list = new ArrayList();
CMElementDeclaration parentDecl = getCMElementDeclaration(parent);
if (parentDecl != null) {
ModelQuery modelQuery = ModelQueryUtil.getModelQuery(parent
.getOwnerDocument());
// taken from ActionManagers
// int editMode = modelQuery.getEditMode();
int editMode = ModelQuery.EDIT_MODE_UNCONSTRAINED;
int ic = (editMode == ModelQuery.EDIT_MODE_CONSTRAINED_STRICT) ? ModelQuery.INCLUDE_CHILD_NODES
| ModelQuery.INCLUDE_SEQUENCE_GROUPS
: ModelQuery.INCLUDE_CHILD_NODES;
modelQuery.getInsertActions(parent, parentDecl, index, ic,
validityChecking, list);
}
return list;
}
protected CMElementDeclaration getCMElementDeclaration(Node node) {
CMElementDeclaration result = null;
if (node.getNodeType() == Node.ELEMENT_NODE) {
ModelQuery modelQuery = ModelQueryUtil.getModelQuery(node
.getOwnerDocument());
if (modelQuery != null) {
result = modelQuery.getCMElementDeclaration((Element) node);
}
}
return result;
}
protected int getElementPosition(Node child) {
Node parent = child.getParentNode();
if (parent == null) {
return 0;
}
NodeList children = parent.getChildNodes();
if (children == null) {
return 0;
}
int count = 0;
for (int i = 0; i < children.getLength(); i++) {
if (children.item(i) == child) {
return count;
} else {
// if (children.item(i).getNodeType() == Node.ELEMENT_NODE)
count++;
}
}
return 0;
}
/**
* Retreives cmnode's documentation to display in the completion proposal's
* additional info. If no documentation exists for cmnode, try displaying
* parentOrOwner's documentation
*
* String any documentation information to display for cmnode.
* <code>null</code> if there is nothing to display.
*/
protected String getAdditionalInfo(CMNode parentOrOwner, CMNode cmnode) {
String addlInfo = null;
if (cmnode == null) {
if (Debug.displayWarnings) {
new IllegalArgumentException("Null declaration!").printStackTrace(); //$NON-NLS-1$
}
return null;
}
addlInfo = getInfoProvider().getInfo(cmnode);
if ((addlInfo == null) && (parentOrOwner != null)) {
addlInfo = getInfoProvider().getInfo(parentOrOwner);
}
return addlInfo;
}
/**
* Gets the infoProvider.
*
* fInfoProvider and if fInfoProvider was <code>null</code> create a new
* instance
*/
public MarkupTagInfoProvider getInfoProvider() {
if (infoProvider == null) {
infoProvider = new MarkupTagInfoProvider();
}
return infoProvider;
}
protected boolean beginsWith(String aString, String prefix) {
if ((aString == null) || (prefix == null)) {
return true;
}
return aString.toLowerCase().startsWith(prefix.toLowerCase());
}
/**
* This is the position the cursor should be in after the proposal is
* applied
*
* @param proposedText
* @return the position the cursor should be in after the proposal is
* applied
*/
private int getCursorPositionForProposedText(String proposedText) {
int cursorAdjustment;
cursorAdjustment = proposedText.indexOf("\"\"") + 1; //$NON-NLS-1$
// otherwise, after the first tag
if (cursorAdjustment == 0) {
cursorAdjustment = proposedText.indexOf('>') + 1;
}
if (cursorAdjustment == 0) {
cursorAdjustment = proposedText.length() + 1;
}
return cursorAdjustment;
}
protected ITextRegion getNameRegion(IStructuredDocumentRegion flatNode) {
if (flatNode == null) {
return null;
}
Iterator regionList = flatNode.getRegions().iterator();
while (regionList.hasNext()) {
ITextRegion region = (ITextRegion) regionList.next();
if (isNameRegion(region)) {
return region;
}
}
return null;
}
/**
* Checks to the see if the element is in the correct region.
* @param region
* @return
*/
protected boolean isNameRegion(ITextRegion region) {
String type = region.getType();
return ((type == DOMRegionContext.XML_TAG_NAME)
|| (type == DOMRegionContext.XML_ELEMENT_DECL_NAME)
|| (type == DOMRegionContext.XML_DOCTYPE_NAME) || (type == DOMRegionContext.XML_ATTLIST_DECL_NAME));
}
}