package com.anjlab.tapestry5.webtools.contentassist; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import org.apache.commons.lang.StringUtils; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.wst.sse.ui.contentassist.CompletionProposalInvocationContext; import org.eclipse.wst.xml.ui.internal.contentassist.ContentAssistRequest; import org.eclipse.wst.xml.ui.internal.contentassist.DefaultXMLCompletionProposalComputer; import org.eclipse.wst.xml.ui.internal.contentassist.MarkupCompletionProposal; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import com.anjlab.eclipse.tapestry5.Activator; import com.anjlab.eclipse.tapestry5.Component; import com.anjlab.eclipse.tapestry5.EclipseUtils; import com.anjlab.eclipse.tapestry5.LibraryMapping; import com.anjlab.eclipse.tapestry5.Parameter; import com.anjlab.eclipse.tapestry5.Property; import com.anjlab.eclipse.tapestry5.TapestryComponentSpecification; import com.anjlab.eclipse.tapestry5.TapestryContext; import com.anjlab.eclipse.tapestry5.TapestryContextScope; import com.anjlab.eclipse.tapestry5.TapestryModule; import com.anjlab.eclipse.tapestry5.TapestryProject; import com.anjlab.eclipse.tapestry5.TapestryUtils; @SuppressWarnings({ "restriction" }) public class TapestryCompletionProposalComputer extends DefaultXMLCompletionProposalComputer { private static interface ProposalCallback { void newProposal(TapestryContext tapestryContext, String tagName, String displayString); } // TODO Remove addTagInsertionProposals // @Override // protected void addTagInsertionProposals(final ContentAssistRequest contentAssistRequest, // int childPosition, // CompletionProposalInvocationContext context) // { // enumProposals(contentAssistRequest, context, new ProposalCallback() // { // @Override // public void newProposal(TapestryContext tapestryContext, // String tagName, // String displayString) // { // // TODO Generate required attributes in addTagNameProposals too // // StringBuilder tagTemplate = new StringBuilder(); // // tagTemplate.append("<") // .append(tagName); // // // XXX How to check if component may have/has content? // boolean mayHaveContent = true; // // for (Parameter parameter : tapestryContext.getSpecification().getParameters()) // { // if (parameter.isRequired()) // { // tagTemplate.append(" ") // .append(parameter.getName()) // .append("=\"") // .append(StringUtils.isEmpty(parameter.getValue()) ? "" : parameter.getValue()) // .append("\""); // } // } // // if (mayHaveContent) // { // tagTemplate.append(">") // .append("</") // .append(tagName) // .append(">"); // } // else // { // tagTemplate.append(" />"); // } // // addProposal(contentAssistRequest, // tapestryContext, // tagTemplate.toString(), // displayString); // } // }); // } @Override protected void addTagNameProposals(final ContentAssistRequest request, int childPosition, CompletionProposalInvocationContext context) { enumProposals(request, context, new ProposalCallback() { @Override public void newProposal(TapestryContext tapestryContext, String tagName, String displayString) { addProposal(request, tapestryContext, tagName, displayString); } }); } private void addProposal(ContentAssistRequest request, TapestryContext tapestryContext, String replacementString, String displayString) { request.addProposal(new MarkupCompletionProposal( replacementString, // replacementString request.getReplacementBeginPosition(), request.getReplacementLength(), replacementString.length(), Activator.getTapestryLogoIcon(), // image displayString, // displayString null, // contextInfo tapestryContext.getJavadoc(), // additionalProposalInfo 3000 - (StringUtils.countMatches(replacementString, ".") > 0 ? 1 : 0), // relevance true // updateReplacementLengthOnValidate )); } private void enumProposals(final ContentAssistRequest request, CompletionProposalInvocationContext context, ProposalCallback proposalCallback) { TapestryProject tapestryProject = getTapestryProject(context); if (tapestryProject == null) { // No tapestry project available for the context return; } Map<String, String> xmlnsMappings = findXmlnsMappingsRelativeTo(request.getNode()); for (TapestryModule tapestryModule : tapestryProject.modules()) { for (TapestryContext tapestryContext : tapestryModule.getComponents()) { // If tapestry-library: prefix defined, try to use it first for completion proposals String tagName = getComponentTagName(tapestryModule, tapestryContext, xmlnsMappings, true); String displayString = tagName; String userInput = request.getMatchString(); if (!isComponentNameProposalMatches(tagName, userInput, xmlnsMappings)) { // ... and fall back to full component name if library prefixed name doesn't match user input: // maybe the user is entering full component name String fullTagName = getComponentTagName(tapestryModule, tapestryContext, xmlnsMappings, false); if (!isComponentNameProposalMatches(fullTagName, userInput, xmlnsMappings)) { continue; } // User tries to input full component name, but prefix is available for this library. // Force using prefixed tag name for completion proposal, otherwise why would user defined xmlns for it? displayString = fullTagName; } proposalCallback.newProposal(tapestryContext, tagName, displayString); } } } @Override protected void addAttributeNameProposals( ContentAssistRequest request, CompletionProposalInvocationContext context) { // Display Page/Component parameters proposals TapestryContextScope scope = getCurrentTagSpecification(request, context); if (scope == null) { return; } NamedNodeMap attributes = request.getNode().getAttributes(); // TODO Add parameters of applied t:mixins // Current component may also have embedded definition via @Component annotation Component embeddedDefinition = null; String componentId = TapestryUtils.findTapestryAttribute(request.getNode(), "id"); if (StringUtils.isNotEmpty(componentId)) { TapestryContextScope scope2 = getCurrentTapestryContextSpecification(request, context); for (Component component : scope2.specification.getComponents()) { if (StringUtils.equals(componentId, component.getId())) { embeddedDefinition = component; break; } } } for (Parameter parameter : scope.specification.getParameters(scope.project)) { if (!parameter.getName().startsWith(request.getMatchString())) { continue; } if (parameterBoundUsingAttribute(parameter, attributes) || parameterBoundUsingChildNode(parameter, request.getNode()) || parameterBoundUsingEmbeddedComponent(parameter, embeddedDefinition)) { continue; } String replacementString = parameter.getName() + "=\"\""; request.addProposal(new MarkupCompletionProposal( replacementString, request.getReplacementBeginPosition(), request.getReplacementLength(), replacementString.length() - 1, Activator.getTapestryLogoIcon(), // image parameter.getName(), // displayString null, // contextInfo parameter.getJavadoc(), // additionalProposalInfo 3000, // relevance true // updateReplacementLengthOnValidate )); } } private boolean parameterBoundUsingAttribute(Parameter parameter, NamedNodeMap attributes) { return attributes.getNamedItem(parameter.getName()) != null; } private boolean parameterBoundUsingChildNode(Parameter parameter, Node node) { NodeList childNodes = node.getChildNodes(); if (childNodes != null) { for (int i = 0; i < childNodes.getLength(); i++) { Node child = childNodes.item(i); if (StringUtils.equals("tapestry:parameter", child.getNamespaceURI()) && StringUtils.equalsIgnoreCase(parameter.getName(), child.getLocalName())) { return true; } } } return false; } private boolean parameterBoundUsingEmbeddedComponent(Parameter parameter, Component embeddedDefinition) { if (embeddedDefinition == null) { return false; } for (String param : embeddedDefinition.getParameters()) { // TODO Only check within list of publishParameters String[] nameValue = param.split("="); if (nameValue.length == 2 && StringUtils.equalsIgnoreCase(nameValue[0], parameter.getName())) { return true; } } return false; } @Override protected void addAttributeValueProposals( ContentAssistRequest request, CompletionProposalInvocationContext context) { // Display Page/Component properties TapestryContextScope scope = getCurrentTapestryContextSpecification(request, context); if (scope == null) { return; } // TODO Support comma-separated lists (like for t:mixins), maps, and different binding prefixes // TODO Support properties in dot-notation, like: user.firstName for (Property property : scope.specification.getProperties()) { if (!property.getName().startsWith(request.getMatchString().replaceAll("\"|'", ""))) { continue; } String replacementString = '"' + property.getName() + '"'; request.addProposal(new MarkupCompletionProposal( replacementString, request.getReplacementBeginPosition(), request.getReplacementLength(), replacementString.length() - 1, Activator.getTapestryLogoIcon(), // image property.getName(), // displayString null, // contextInfo property.getJavadoc(), // additionalProposalInfo 3000, // relevance true // updateReplacementLengthOnValidate )); } } private TapestryProject getTapestryProject(CompletionProposalInvocationContext context) { Shell shell = context.getViewer().getTextWidget().getShell(); IWorkbenchWindow window = EclipseUtils.getWorkbenchWindow(shell); if (window == null) { return null; } TapestryProject tapestryProject = Activator.getDefault().getTapestryProject(window); return tapestryProject; } private boolean isComponentNameProposalMatches(String proposal, String userInput, Map<String, String> xmlnsMappings) { if (StringUtils.isEmpty(userInput) || proposal.startsWith(userInput)) { return true; } for (Entry<String, String> xmlnsMapping : xmlnsMappings.entrySet()) { if (TapestryUtils.isTapestryComponentsNamespace(xmlnsMapping.getValue()) && proposal.startsWith(xmlnsMapping.getKey() + ":" + userInput)) { return true; } } return false; } private String getComponentTagName(TapestryModule tapestryModule, TapestryContext tapestryContext, Map<String, String> xmlnsMappings, boolean useLibraryPrefix) { if (useLibraryPrefix) { String packageName = tapestryContext.getPackageName(); // 1. Check if this component is from some library for (LibraryMapping library : tapestryModule.libraryMappings()) { if (packageName.startsWith(library.getRootPackage())) { // 2. Check if there are prefix defined for this library in xmlnsMappings for (Entry<String, String> xmlnsMapping : xmlnsMappings.entrySet()) { if (xmlnsMapping.getValue().equals("tapestry-library:" + library.getPathPrefix())) { // 3. Use this prefix for tag name return xmlnsMapping.getKey() + ":" + tapestryModule.getComponentName(tapestryContext) .substring(library.getPathPrefix().length() + ".".length()); } } } } } // 4. Use default tapestry NS prefix for (Entry<String, String> xmlnsMapping : xmlnsMappings.entrySet()) { if (TapestryUtils.isTapestryDefaultNamespace(xmlnsMapping.getValue())) { return xmlnsMapping.getKey() + ":" + tapestryModule.getComponentName(tapestryContext); } } // 5. Something went wrong -- probably document is not well formed yet, use "de-facto" default NS prefix return "t:" + tapestryModule.getComponentName(tapestryContext); } private Map<String, String> findXmlnsMappingsRelativeTo(Node node) { Map<String, String> mappings = new HashMap<String, String>(); findXmlnsMappingsAt(node, mappings); return mappings; } private void findXmlnsMappingsAt(Node node, Map<String, String> mappings) { if (node == null) { return; } NamedNodeMap attributes = node.getAttributes(); if (attributes != null) { for (int i = 0; i < attributes.getLength(); i++) { Node item = attributes.item(i); if (!item.getNodeName().startsWith("xmlns:")) { continue; } String prefix = item.getNodeName().substring("xmlns:".length()); mappings.put(prefix, item.getNodeValue()); } } findXmlnsMappingsAt(node.getParentNode(), mappings); } private TapestryContextScope getCurrentTagSpecification( ContentAssistRequest request, CompletionProposalInvocationContext context) { Shell shell = context.getViewer().getTextWidget().getShell(); IWorkbenchWindow window = EclipseUtils.getWorkbenchWindow(shell); if (window == null) { return null; } String componentName = TapestryUtils.getComponentName(window, request.getNode()); if (componentName == null) { return null; } TapestryContextScope scope = TapestryUtils.getTapestryContext(window, componentName); if (scope == null) { return null; } TapestryComponentSpecification specification = scope.context.getSpecification(); return new TapestryContextScope(window, scope.project, scope.context, specification); } private TapestryContextScope getCurrentTapestryContextSpecification( ContentAssistRequest request, CompletionProposalInvocationContext context) { Shell shell = context.getViewer().getTextWidget().getShell(); IWorkbenchWindow window = EclipseUtils.getWorkbenchWindow(shell); if (window == null) { return null; } TapestryContext tapestryContext = Activator.getDefault().getTapestryContext(window); if (tapestryContext == null) { return null; } TapestryProject tapestryProject = Activator.getDefault().getTapestryProject(window); TapestryComponentSpecification specification = tapestryContext.getSpecification(); return new TapestryContextScope(window, tapestryProject, tapestryContext, specification); } }