package com.google.dart.tools.wst.ui.contentassist;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.dart.engine.ast.AstNode;
import com.google.dart.engine.ast.Expression;
import com.google.dart.engine.context.AnalysisContext;
import com.google.dart.engine.element.CompilationUnitElement;
import com.google.dart.engine.element.Element;
import com.google.dart.engine.element.HtmlElement;
import com.google.dart.engine.element.angular.AngularComponentElement;
import com.google.dart.engine.element.angular.AngularDecoratorElement;
import com.google.dart.engine.element.angular.AngularElement;
import com.google.dart.engine.element.angular.AngularHasAttributeSelectorElement;
import com.google.dart.engine.element.angular.AngularPropertyElement;
import com.google.dart.engine.element.angular.AngularSelectorElement;
import com.google.dart.engine.element.angular.AngularTagSelectorElement;
import com.google.dart.engine.html.ast.HtmlUnit;
import com.google.dart.engine.html.ast.HtmlUnitUtils;
import com.google.dart.engine.html.ast.XmlTagNode;
import com.google.dart.engine.index.Index;
import com.google.dart.engine.internal.element.angular.AngularApplication;
import com.google.dart.engine.search.SearchEngineFactory;
import com.google.dart.engine.services.assist.AssistContext;
import com.google.dart.tools.core.DartCore;
import com.google.dart.tools.deploy.Activator;
import com.google.dart.tools.ui.DartToolsPlugin;
import com.google.dart.tools.ui.internal.text.dart.DartCompletionProposalComputer;
import com.google.dart.tools.ui.text.dart.ContentAssistInvocationContext;
import com.google.dart.tools.ui.text.dart.DartContentAssistInvocationContext;
import com.google.dart.tools.wst.ui.HtmlReconcilerHook;
import com.google.dart.tools.wst.ui.HtmlReconcilerManager;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.contentassist.CompletionProposal;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
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.ui.contentassist.CompletionProposalInvocationContext;
import org.eclipse.wst.sse.ui.contentassist.ICompletionProposalComputer;
import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
import static org.eclipse.wst.sse.ui.internal.contentassist.ContentAssistUtils.getStructuredDocumentRegion;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* {@link ICompletionProposalComputer} for Angular HTML.
*/
@SuppressWarnings("restriction")
public class AngularCompletionProposalComputer implements ICompletionProposalComputer {
/**
* Container with information about an attribute completion.
*/
private static class AttributeCompletion {
final String name;
public AttributeCompletion(String name) {
this.name = name;
}
}
private static final List<ICompletionProposal> EMPTY_PROPOSALS = ImmutableList.of();
private static List<AngularPropertyElement> getSortedProperties(AngularComponentElement component) {
List<AngularPropertyElement> properties = Lists.newArrayList(component.getProperties());
Collections.sort(properties, new Comparator<AngularPropertyElement>() {
@Override
public int compare(AngularPropertyElement o1, AngularPropertyElement o2) {
String name1 = o1.getName();
String name2 = o2.getName();
return name1.compareTo(name2);
}
});
return properties;
}
private final DartCompletionProposalComputer proposalComputer = new DartCompletionProposalComputer();
private ITextViewer viewer;
private IStructuredDocument document;
private int offset;
private HtmlUnit htmlUnit;
private HtmlElement htmlElement;
private AngularApplication application;
private List<ICompletionProposal> proposals;
private boolean doExpressionCompletion;
@Override
public List<ICompletionProposal> computeCompletionProposals(
CompletionProposalInvocationContext completionContext, IProgressMonitor monitor) {
try {
viewer = completionContext.getViewer();
document = (IStructuredDocument) viewer.getDocument();
offset = completionContext.getInvocationOffset();
proposals = Lists.newArrayList();
// wait for Angular resolution
if (!waitForResolution()) {
return proposals;
}
// try to complete as Angular specific HTML entity
doExpressionCompletion = true;
completeAttribute();
completeTag();
// maybe complete as Dart
if (doExpressionCompletion) {
completeExpression(monitor);
}
// done
return proposals;
} catch (Throwable e) {
DartToolsPlugin.log(e);
} finally {
viewer = null;
document = null;
offset = 0;
}
return EMPTY_PROPOSALS;
}
@Override
public List<ICompletionProposal> computeContextInformation(
CompletionProposalInvocationContext context, IProgressMonitor monitor) {
List<ICompletionProposal> proposals = Lists.newArrayList();
return proposals;
}
@Override
public String getErrorMessage() {
return null;
}
@Override
public void sessionEnded() {
}
@Override
public void sessionStarted() {
}
private void addEmptyAttributeProposal(int offset, int length, String name) {
String replacement = name + "=" + '"';
int cursorPosition = replacement.length();
replacement += '"';
proposals.add(new CompletionProposal(
replacement,
offset,
length,
cursorPosition,
Activator.getImage("icons/full/dart16/angular_16_blue.png"),
null,
null,
null));
}
private void completeAttribute() throws Exception {
// prepare tag region in document
IStructuredDocumentRegion tagRegion = getStructuredDocumentRegion(viewer, offset);
if (tagRegion == null) {
return;
}
if (!tagRegion.isEnded()) {
return;
}
int tagOffset = tagRegion.getStartOffset();
// prepare possible attribute completions
List<AttributeCompletion> attributeCompletions = Lists.newArrayList();
// add component properties
{
AngularComponentElement component = getAngularComponent();
if (component != null) {
List<AngularPropertyElement> componentProperties = getSortedProperties(component);
for (AngularPropertyElement property : componentProperties) {
String propertyName = property.getName();
AttributeCompletion completion = new AttributeCompletion(propertyName);
attributeCompletions.add(completion);
}
}
}
// add directives
for (AngularElement angularElement : application.getElements()) {
if (angularElement instanceof AngularDecoratorElement) {
AngularDecoratorElement directive = (AngularDecoratorElement) angularElement;
AngularSelectorElement selector = directive.getSelector();
if (selector instanceof AngularHasAttributeSelectorElement) {
AngularHasAttributeSelectorElement attributeSelector = (AngularHasAttributeSelectorElement) selector;
attributeCompletions.add(new AttributeCompletion(attributeSelector.getName()));
}
}
}
// prepare text region
ITextRegion textRegion = tagRegion.getRegionAtCharacterOffset(offset);
if (textRegion == null) {
return;
}
String textType = textRegion.getType();
// "<tag attrNamePrefix!>"
if (DOMRegionContext.XML_TAG_CLOSE.equals(textType)) {
ITextRegion prevTextRegion = tagRegion.getRegionAtCharacterOffset(offset - 1);
String prevTextType = prevTextRegion.getType();
if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(prevTextType)) {
textRegion = prevTextRegion;
textType = prevTextType;
}
}
// "<tag attrNamePrefix! otherAttr='bar'>"
if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(textType)) {
int attrOffset = tagOffset + textRegion.getStart();
int attrLength = textRegion.getTextLength();
String attrPrefix = document.get(attrOffset, attrLength);
// propose component properties
for (AttributeCompletion completion : attributeCompletions) {
String propertyName = completion.name;
if (propertyName.startsWith(attrPrefix)) {
addEmptyAttributeProposal(attrOffset, attrLength, propertyName);
}
}
// done
doExpressionCompletion = false;
return;
}
// after tag name or other attribute value
if (DOMRegionContext.XML_TAG_NAME.equals(textType)) {
if (textRegion.getLength() > 1 + textRegion.getTextLength()) {
for (AttributeCompletion completion : attributeCompletions) {
String propertyName = completion.name;
addEmptyAttributeProposal(offset, 0, propertyName);
}
doExpressionCompletion = false;
}
// done
return;
}
// "<tag !>" or "<tag foo='value'! >"
if (DOMRegionContext.XML_TAG_CLOSE.equals(textType)
|| DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(textType)) {
if (hasWhitespaceBetweenLastAndOffset(tagRegion)) {
for (AttributeCompletion completion : attributeCompletions) {
String propertyName = completion.name;
addEmptyAttributeProposal(offset, 0, propertyName);
}
doExpressionCompletion = false;
}
// done
return;
}
}
private void completeExpression(IProgressMonitor monitor) {
// find Expression
final Expression expression = HtmlUnitUtils.getExpression(htmlUnit, offset);
if (expression == null) {
return;
}
// prepare AssistContext
final AssistContext assistContext;
{
AnalysisContext analysisContext = htmlElement.getContext();
Index index = DartCore.getProjectManager().getIndex();
assistContext = new AssistContext(
SearchEngineFactory.createSearchEngine(index),
analysisContext,
null,
null,
null,
offset,
0) {
@Override
public CompilationUnitElement getCompilationUnitElement() {
return htmlElement.getAngularCompilationUnit();
}
@Override
public AstNode getCoveredNode() {
return expression;
}
};
}
// use Dart completion
ContentAssistInvocationContext completionContext = new DartContentAssistInvocationContext(
viewer,
offset,
null,
assistContext,
null);
List<ICompletionProposal> dartProposals = proposalComputer.computeCompletionProposals(
completionContext,
monitor);
proposals.addAll(dartProposals);
}
private void completeTag() throws Exception {
int nameOffset = 0;
int nameLength = 0;
String namePrefix = null;
// prepare region in document
IStructuredDocumentRegion tagRegion = getStructuredDocumentRegion(viewer, offset);
if (tagRegion == null) {
return;
}
String tagType = tagRegion.getType();
// "<prefix!" completion
if (DOMRegionContext.XML_TAG_NAME.equals(tagType)) {
int tagOffset = tagRegion.getStartOffset();
// we want a tag completion for "<prefix! <some-other-tag>" but not for "<closed-tag !>"
if (tagRegion.isEnded()) {
return;
}
// prepare text region
ITextRegion textRegion = tagRegion.getRegionAtCharacterOffset(offset);
if (textRegion == null) {
return;
}
String textType = textRegion.getType();
// should be XML_TAG_NAME completion
if (!DOMRegionContext.XML_TAG_NAME.equals(textType)) {
return;
}
// prepare name information
nameOffset = tagOffset + textRegion.getStart();
nameLength = textRegion.getTextLength();
namePrefix = document.get(nameOffset, nameLength);
}
// "<!" completion
if (DOMRegionContext.XML_CONTENT.equals(tagType)) {
if (offset != 0 && document.get(offset - 1, 1).equals("<")) {
nameOffset = offset;
nameLength = 0;
namePrefix = "";
}
}
// check if completion is activated at a supported position
if (namePrefix == null) {
return;
}
// cannot be a Dart expression
doExpressionCompletion = false;
// complete the tag name
for (AngularElement element : application.getElements()) {
if (element instanceof AngularComponentElement) {
AngularComponentElement component = (AngularComponentElement) element;
AngularSelectorElement selector = component.getSelector();
if (selector instanceof AngularTagSelectorElement) {
AngularTagSelectorElement tagSelector = (AngularTagSelectorElement) selector;
String tagName = tagSelector.getName();
if (tagName.startsWith(namePrefix)) {
String replacement = tagName + ">";
int cursorPosition = replacement.length();
replacement += "</" + tagName + ">";
proposals.add(new CompletionProposal(
replacement,
nameOffset,
nameLength,
cursorPosition,
Activator.getImage("icons/full/dart16/angular_16_blue.png"),
tagName,
null,
null));
}
}
}
}
}
/**
* Return an {@link AngularComponentElement} that encloses given offset.
*/
private AngularComponentElement getAngularComponent() {
// prepare enclosing tag
XmlTagNode tagNode = HtmlUnitUtils.getEnclosingTagNode(htmlUnit, offset);
if (tagNode == null) {
return null;
}
// prepare Angular selector
Element tagElement = tagNode.getElement();
if (!(tagElement instanceof AngularTagSelectorElement)) {
return null;
}
AngularTagSelectorElement selector = (AngularTagSelectorElement) tagElement;
// prepare Angular component
if (!(selector.getEnclosingElement() instanceof AngularComponentElement)) {
return null;
}
return (AngularComponentElement) selector.getEnclosingElement();
}
private boolean hasWhitespaceBetweenLastAndOffset(IStructuredDocumentRegion tagRegion) {
ITextRegion textRegion = tagRegion.getRegionAtCharacterOffset(offset);
// tag closing is not interesting, we need to know what is before it: "<tag !>" or "<tag!>"
if (DOMRegionContext.XML_TAG_CLOSE.equals(textRegion.getType())) {
textRegion = tagRegion.getRegionAtCharacterOffset(offset - 1);
}
// we need a whitespace: "<tag !" or "<tag foo='bar' !"
return offset > tagRegion.getStartOffset() + textRegion.getTextEnd();
}
private void prepareResolution() {
HtmlReconcilerHook reconciler = HtmlReconcilerManager.getInstance().reconcilerFor(document);
htmlUnit = reconciler.getResolvedUnit();
application = reconciler.getApplication();
if (htmlUnit != null) {
htmlElement = htmlUnit.getElement();
}
}
private boolean waitForResolution() {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 300) {
prepareResolution();
if (htmlUnit != null && htmlElement != null && application != null) {
return true;
}
Thread.yield();
}
return false;
}
}