package tk.eclipse.plugin.htmleditor.assist; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Stack; import jp.aonir.fuzzyxml.FuzzyXMLAttribute; import jp.aonir.fuzzyxml.FuzzyXMLDocument; import jp.aonir.fuzzyxml.FuzzyXMLElement; import jp.aonir.fuzzyxml.FuzzyXMLNode; import jp.aonir.fuzzyxml.FuzzyXMLParser; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.contentassist.CompletionProposal; import org.eclipse.jface.text.contentassist.ContextInformation; import org.eclipse.jface.text.contentassist.ContextInformationValidator; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.text.contentassist.IContextInformation; import org.eclipse.jface.text.contentassist.IContextInformationValidator; import org.eclipse.swt.graphics.Image; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IFileEditorInput; import org.objectstyle.wolips.bindings.utils.BindingReflectionUtils; import org.objectstyle.wolips.templateeditor.InlineWodTagInfo; import org.objectstyle.wolips.wodclipse.core.Activator; import org.objectstyle.wolips.wodclipse.core.preferences.PreferenceConstants; import tk.eclipse.plugin.htmleditor.HTMLPlugin; import tk.eclipse.plugin.htmleditor.HTMLUtil; import tk.eclipse.plugin.htmleditor.IFileAssistProcessor; import tk.eclipse.plugin.htmleditor.editors.HTMLSourceEditor; import tk.eclipse.plugin.htmleditor.template.HTMLTemplateAssistProcessor; /** * An implementation of <code>IContentAssistProcessor</code>. * This processor provides code-completion for the <code>HTMLSourceEditor</code>. * * @author Naoki Takezoe */ public class HTMLAssistProcessor extends HTMLTemplateAssistProcessor { /*implements IContentAssistProcessor {*/ private boolean _xhtmlMode = false; private char[] _chars = {}; private Image _tagImage; private Image _attrImage; private Image _valueImage; private boolean _assistCloseTag = true; private List<CustomAttribute> _customAttrs = CustomAttribute.loadFromPreference(false); private List<CustomElement> _customElems = CustomElement.loadFromPreference(false); private Set<String> _customElemNames = new HashSet<String>(); protected CSSAssistProcessor _cssAssist = new CSSAssistProcessor(); protected IFileAssistProcessor[] _fileAssistProcessors; private int _offset; private FuzzyXMLDocument _doc; private ITextViewer _textViewer; /** * The constructor. */ public HTMLAssistProcessor() { _tagImage = HTMLPlugin.getDefault().getImageRegistry().get(HTMLPlugin.ICON_TAG); _attrImage = HTMLPlugin.getDefault().getImageRegistry().get(HTMLPlugin.ICON_ATTR); _valueImage = HTMLPlugin.getDefault().getImageRegistry().get(HTMLPlugin.ICON_VALUE); _fileAssistProcessors = HTMLPlugin.getDefault().getFileAssistProcessors(); for (int i = 0; i < _customElems.size(); i++) { _customElemNames.add(_customElems.get(i).getDisplayName()); } } public boolean enableTemplate() { return true; } public void setXHTMLMode(boolean xhtmlMode) { this._xhtmlMode = xhtmlMode; } public void setAutoAssistChars(char[] chars) { if (chars != null) { this._chars = chars; } } public void setAssistCloseTag(boolean assistCloseTag) { this._assistCloseTag = assistCloseTag; } /** * Returns an array of attribute value proposals. * * @param tagName the tag name * @param value the attribute value * @param attrInfo the attribute information * @return the array of attribute value proposals */ protected AssistInfo[] getAttributeValues(String tagName, String value, TagInfo tagInfo, AttributeInfo attrInfo) { // CSS if (attrInfo.getAttributeType() == AttributeInfo.CSS) { return _cssAssist.getAssistInfo(tagName, value); } // FILE if (attrInfo.getAttributeType() == AttributeInfo.FILE) { ArrayList<AssistInfo> list = new ArrayList<AssistInfo>(); for (int i = 0; i < _fileAssistProcessors.length; i++) { AssistInfo[] assists = _fileAssistProcessors[i].getAssistInfo(value); for (int j = 0; j < assists.length; j++) { list.add(assists[j]); } } return list.toArray(new AssistInfo[list.size()]); } // IDREF if (attrInfo.getAttributeType() == AttributeInfo.IDREF) { ArrayList<AssistInfo> list = new ArrayList<AssistInfo>(); String[] ids = getIDs(); for (int i = 0; i < ids.length; i++) { list.add(new AssistInfo(ids[i])); } return list.toArray(new AssistInfo[list.size()]); } // IDREFS if (attrInfo.getAttributeType() == AttributeInfo.IDREFS) { ArrayList<AssistInfo> list = new ArrayList<AssistInfo>(); String[] ids = getIDs(); String prefix = value; if (prefix.length() != 0 && !prefix.endsWith(" ")) { prefix = prefix + " "; } for (int i = 0; i < ids.length; i++) { list.add(new AssistInfo(prefix + ids[i], ids[i])); } return list.toArray(new AssistInfo[list.size()]); } // ETC String[] values = AttributeValueDefinition.getAttributeValues(attrInfo.getAttributeType()); AssistInfo[] infos = new AssistInfo[values.length]; for (int i = 0; i < infos.length; i++) { infos[i] = new AssistInfo(values[i]); } return infos; } /** * Returns ID attribute values. * * @return the array which contans ID attribute values */ protected String[] getIDs() { FuzzyXMLDocument doc = getDocument(); List<String> list = new ArrayList<String>(); if (doc != null) { FuzzyXMLElement element = doc.getDocumentElement(); extractID(element, list); } return list.toArray(new String[list.size()]); } private void extractID(FuzzyXMLElement element, List<String> list) { FuzzyXMLAttribute[] attrs = element.getAttributes(); for (int i = 0; i < attrs.length; i++) { TagInfo tagInfo = getTagInfo(element.getName()); if (tagInfo != null) { AttributeInfo attrInfo = tagInfo.getAttributeInfo(attrs[i].getName()); if (attrInfo != null) { if (attrInfo.getAttributeType() == AttributeInfo.ID) { list.add(attrs[i].getValue()); } } } } FuzzyXMLNode[] nodes = element.getChildren(); for (int i = 0; i < nodes.length; i++) { if (nodes[i] instanceof FuzzyXMLElement) { extractID((FuzzyXMLElement) nodes[i], list); } } } /** * Returns the <code>List</code> which contains * all <code>TagInfo</code>s. * * @return the <code>List</code> which contains * all <code>TagInfo</code>s */ protected List<TagInfo> getTagList() { return TagDefinition.getTagInfoAsList(); } /** * Returns the <code>TagInfo</code> which has the specified name. * * @param name a tag name * @return the <code>TagInfo</code> */ protected TagInfo getTagInfo(String name) { List tagList = TagDefinition.getTagInfoAsList(); for (int i = 0; i < tagList.size(); i++) { TagInfo info = (TagInfo) tagList.get(i); if (info.getTagName().equals(name)) { return info; } } return null; } /** * Returns the <code>FuzzyXMLElement</code> by the offset. * * @return the <code>FuzzyXMLElement</code> */ protected FuzzyXMLElement getOffsetElement() { return _doc.getElementByOffset(_offset); } /** * Returns the <code>FuzzyXMLDocument</code>. * * @return the <code>FuzzyXMLDocument</code> */ protected FuzzyXMLDocument getDocument() { return _doc; } protected ITextViewer getTextViewer() { return _textViewer; } @Override public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int documentOffset) { _textViewer = viewer; IDocument document = viewer.getDocument(); String text = document.get().substring(0, documentOffset); String[] dim = getLastWord(text); String word = dim[0].toLowerCase(); String prev = dim[1].toLowerCase(); String last = dim[2]; String attr = dim[3]; boolean inTag = false; boolean spacesAroundEquals = Activator.getDefault().getPreferenceStore().getBoolean(PreferenceConstants.SPACES_AROUND_EQUALS); String equals = spacesAroundEquals?" = ":"="; try { for (int i = documentOffset; i < document.getLength(); i++) { char ch = document.getChar(i); if (ch == '<') { break; } else if (ch == '>') { inTag = true; break; } } } catch (BadLocationException e) { // ignore; e.printStackTrace(); } String next = document.get().substring(documentOffset); this._offset = documentOffset; this._doc = new FuzzyXMLParser(false).parse(document.get()); List<ICompletionProposal> list = new ArrayList<ICompletionProposal>(); List<TagInfo> tagList = getTagList(); // attribute value if ((word.startsWith("\"") && (word.length() == 1 || !word.endsWith("\""))) || (word.startsWith("'") && (word.length() == 1 || !word.endsWith("\'")))) { String value = dim[0].substring(1); TagInfo tagInfo = getTagInfo(last.toLowerCase()); if (tagInfo != null) { AttributeInfo attrInfo = tagInfo.getAttributeInfo(attr); if (attrInfo == null) { attrInfo = new AttributeInfo(attr, true); } if (attrInfo != null) { AssistInfo[] keywords = getAttributeValues(last, dim[0].substring(1), tagInfo, attrInfo); for (int i = 0; i < keywords.length; i++) { if (keywords[i].getOffset() > 0 || keywords[i].getReplaceString().toLowerCase().startsWith(value.toLowerCase())) { // list.add(new CompletionProposal( // keywords[i].getReplaceString(), // documentOffset - value.length(), value.length(), // keywords[i].getReplaceString().length(), // keywords[i].getImage()==null ? valueImage : keywords[i].getImage(), // keywords[i].getDisplayString(), null, null)); list.add(keywords[i].toCompletionProposal(documentOffset, value, _valueImage)); } } } } // tag } else if (word.startsWith("<") && !word.startsWith("</")) { if (supportTagRelation()) { TagInfo parent = getTagInfo(last); tagList = new ArrayList<TagInfo>(); if (parent != null) { String[] childNames = parent.getChildTagNames(); for (int i = 0; i < childNames.length; i++) { tagList.add(getTagInfo(childNames[i])); } } } List<TagInfo> dynamicTagInfo = getDynamicTagInfo(word.substring(1)); if (dynamicTagInfo != null) { tagList.addAll(dynamicTagInfo); } for (int i = 0; i < tagList.size(); i++) { TagInfo tagInfo = tagList.get(i); if (tagInfo instanceof TextInfo) { TextInfo textInfo = (TextInfo) tagInfo; if ((textInfo.getText().toLowerCase()).indexOf(word) == 0) { list.add(new CompletionProposal(textInfo.getText(), documentOffset - word.length(), word.length(), textInfo.getPosition(), _tagImage, textInfo.getDisplayString(), null, tagInfo.getDescription())); } continue; } String tagName = tagInfo.getTagName(); String tagNameMatch = "<" + tagName.toLowerCase(); if (tagNameMatch.startsWith(word) && !(tagNameMatch.equals(word) && next.startsWith(">"))) { String assistKeyword = tagName; int position = 0; // required attributes AttributeInfo[] requireAttrs; if (inTag) { requireAttrs = new AttributeInfo[0]; position = tagName.length() + 1; } else { requireAttrs = tagInfo.getRequiredAttributeInfo(); for (int j = 0; j < requireAttrs.length; j++) { assistKeyword = assistKeyword + " " + requireAttrs[j].getAttributeName(); if (requireAttrs[j].hasValue()) { assistKeyword = assistKeyword + equals + "\"\""; if (j == 0) { position = tagName.length() + requireAttrs[j].getAttributeName().length() + (spacesAroundEquals ? 5 : 3); } } } } boolean forceAttributePosition = (requireAttrs.length == 0 && tagInfo.requiresAttributes()); if (!inTag) { if (tagInfo.hasBody()) { assistKeyword = assistKeyword + ">"; if (_assistCloseTag) { if (position == 0) { position = assistKeyword.length(); } assistKeyword = assistKeyword + "</" + tagName + ">"; } } else { if (tagInfo.isEmptyTag() && _xhtmlMode == false) { assistKeyword = assistKeyword + ">"; } else { assistKeyword = assistKeyword + "/>"; } } } if (position == 0) { position = assistKeyword.length(); } if (forceAttributePosition && position > 0) { if (tagInfo.hasBody()) { position--; } else if (tagInfo.isEmptyTag()) { if (_xhtmlMode) { position -= 2; } else { position--; } } } try { if (tagInfo instanceof InlineWodTagInfo && BindingReflectionUtils.memberIsDeprecated(((InlineWodTagInfo)tagInfo).getElementType())) { list.add(new HTMLDeprecatedCompletionProposal(assistKeyword, documentOffset - word.length() + 1, word.length() - 1, position, _tagImage, tagName, null, tagInfo.getDescription())); } else { list.add(new CompletionProposal(assistKeyword, documentOffset - word.length() + 1, word.length() - 1, position, _tagImage, tagName, null, tagInfo.getDescription())); } } catch (Exception ex) { ex.printStackTrace(); } } } // custom elements for (int i = 0; i < _customElems.size(); i++) { CustomElement element = _customElems.get(i); if ((element.getAssistString().toLowerCase()).indexOf(word) == 0) { int position = element.getAssistString().indexOf('"'); if (position == -1) { position = element.getAssistString().indexOf("><"); } if (position == -1) { position = element.getAssistString().length(); } list.add(new CompletionProposal(element.getAssistString(), documentOffset - word.length(), word.length(), position + 1, _tagImage, element.getDisplayName(), null, null)); } } // attribute } else if (!prev.equals("")) { String tagName = prev; TagInfo tagInfo = getTagInfo(tagName); if (tagInfo != null) { AttributeInfo[] attrList = tagInfo.getAttributeInfo(); for (int j = 0; j < attrList.length; j++) { if (attrList[j].getAttributeName().toLowerCase().indexOf(word) == 0) { String assistKeyword = null; int position = 0; if (attrList[j].hasValue()) { assistKeyword = attrList[j].getAttributeName() + equals + "\"\""; position = equals.length() + 1; } else { assistKeyword = attrList[j].getAttributeName(); position = 0; } list.add(new CompletionProposal(assistKeyword, documentOffset - word.length(), word.length(), attrList[j].getAttributeName().length() + position, _attrImage, attrList[j].getAttributeName(), null, attrList[j].getDescription())); } } } // custom attributes for (int i = 0; i < _customAttrs.size(); i++) { CustomAttribute attrInfo = _customAttrs.get(i); if (attrInfo.getTargetTag().equals("*") || attrInfo.getTargetTag().equals(tagName)) { if (tagName.indexOf(":") < 0 || _customElemNames.contains(tagName)) { list.add(new CompletionProposal(attrInfo.getAttributeName() + equals + "\"\"", documentOffset - word.length(), word.length(), attrInfo.getAttributeName().length() + 2, _attrImage, attrInfo.getAttributeName(), null, null)); } } } // close tag } else if (!last.equals("")) { TagInfo info = getTagInfo(last); if (info == null || _xhtmlMode == true || info.hasBody() || !info.isEmptyTag()) { String assistKeyword = "</" + last + ">"; int length = 0; if (assistKeyword.toLowerCase().startsWith(word)) { length = word.length(); } list.add(new CompletionProposal(assistKeyword, documentOffset - length, length, assistKeyword.length(), _tagImage, assistKeyword, null, null)); } } HTMLUtil.sortCompilationProposal(list); if (enableTemplate()) { ICompletionProposal[] templates = super.computeCompletionProposals(viewer, documentOffset); for (int i = 0; i < templates.length; i++) { list.add(templates[i]); } } ICompletionProposal[] prop = list.toArray(new ICompletionProposal[list.size()]); return prop; } protected List<TagInfo> getDynamicTagInfo(String tagName) { return null; } /** * Returns true if this processor support parent and child relation. * In the default, this method returns false. */ protected boolean supportTagRelation() { return false; } /** * Returns same informations for code completion from calet position. * * @return * <ul> * <li>0 - last word from calet position (if it's tag, it contains <)</li> * <li>1 - target of attribute completion (only tag name, not contains <)</li> * <li>2 - target of close tag completion (only tag name, not contains <)</li> * <li>3 - previous attribute name</li> * </ul> */ protected String[] getLastWord(String text) { // TODO It's dirty... StringBuffer sb = new StringBuffer(); Stack<String> stack = new Stack<String>(); String word = ""; String prevTag = ""; String lastTag = ""; String attr = ""; String temp1 = ""; // temporary String temp2 = ""; // temporary for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); // skip scriptlet if (c == '<' && text.length() > i + 1 && text.charAt(i + 1) == '%') { i = text.indexOf("%>", i + 2); if (i == -1) { i = text.length(); } continue; } // skip XML declaration if (c == '<' && text.length() > i + 1 && text.charAt(i + 1) == '?') { i = text.indexOf("?>", i + 2); if (i == -1) { i = text.length(); } continue; } if (isDelimiter(c)) { temp1 = sb.toString(); // skip whitespaces in the attribute value if (temp1.length() > 1 && ((temp1.startsWith("\"") && !temp1.endsWith("\"") && c != '"') || (temp1.startsWith("'") && !temp1.endsWith("'") && c != '\''))) { sb.append(c); continue; } if (!temp1.equals("")) { temp2 = temp1; if (temp2.endsWith("=") && !prevTag.equals("") && !temp2.equals("=")) { attr = temp2.substring(0, temp2.length() - 1); } } if (temp1.startsWith("<") && !temp1.startsWith("</") && !temp1.startsWith("<!")) { prevTag = temp1.substring(1); if (!temp1.endsWith("/")) { stack.push(prevTag); } } else if (temp1.startsWith("</") && stack.size() != 0) { stack.pop(); } else if (temp1.endsWith("/") && stack.size() != 0) { stack.pop(); } sb.setLength(0); if (c == '<') { sb.append(c); } else if (c == '"' || c == '\'') { if (temp1.startsWith("\"") || temp1.startsWith("'")) { sb.append(temp1); } sb.append(c); } else if (c == '>') { prevTag = ""; attr = ""; } } else { if (c == '=' && !prevTag.equals("")) { attr = temp2.trim(); } temp1 = sb.toString(); if (temp1.length() > 1 && (temp1.startsWith("\"") && temp1.endsWith("\"")) || (temp1.startsWith("'") && temp1.endsWith("'"))) { sb.setLength(0); } sb.append(c); } } if (stack.size() != 0) { lastTag = stack.pop(); } // Hmm... it's not perfect... if (attr.endsWith("=")) { attr = attr.substring(0, attr.length() - 1); } word = sb.toString(); return new String[] { word, prevTag, lastTag, attr }; } /** * Tests a character is delimiter or not delimiter. */ protected boolean isDelimiter(char c) { if (c == ' ' || c == '(' || c == ')' || c == ',' //|| c == '.' || c == ';' || c == '\n' || c == '\r' || c == '\t' || c == '+' || c == '>' || c == '<' || c == '*' || c == '^' //|| c == '{' //|| c == '}' /*|| c == '[' || c == ']'*/|| c == '"' || c == '\'') { return true; } else { return false; } } @Override public IContextInformation[] computeContextInformation(ITextViewer viewer, int documentOffset) { ContextInformation[] info = new ContextInformation[0]; return info; } @Override public char[] getCompletionProposalAutoActivationCharacters() { return _chars; } @Override public char[] getContextInformationAutoActivationCharacters() { return _chars; } @Override public IContextInformationValidator getContextInformationValidator() { return new ContextInformationValidator(this); } @Override public String getErrorMessage() { return "Error"; } /** * Updates internal informations. * * @param editor the <code>HTMLSourceEditor</code> instance * @param source editing source code */ public void update(HTMLSourceEditor editor, String source) { IEditorInput editorInput = editor.getEditorInput(); if (editorInput instanceof IFileEditorInput) { IFileEditorInput input = (IFileEditorInput) editorInput; _cssAssist.reload(input.getFile(), source); _customAttrs = CustomAttribute.loadFromPreference(false); _customElems = CustomElement.loadFromPreference(false); _customElemNames.clear(); for (int i = 0; i < _customElems.size(); i++) { _customElemNames.add(_customElems.get(i).getDisplayName()); } for (int i = 0; i < _fileAssistProcessors.length; i++) { _fileAssistProcessors[i].reload(input.getFile()); } } } }