/******************************************************************************* * Copyright (c) 2003, 2004 IBM Corporation 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: * IBM Corporation - initial API and implementation *******************************************************************************/ /* * Created on Mar 9, 2004 * * To change the template for this generated file go to * Window - Preferences - Java - Code Generation - Code and Comments */ package org.eclipse.jst.common.internal.annotations.ui; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.MissingResourceException; import java.util.Set; import java.util.TreeSet; import org.eclipse.core.resources.IFile; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IField; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.ui.JavaUI; import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal; import org.eclipse.jdt.ui.text.java.IJavadocCompletionProcessor; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.contentassist.IContextInformation; import org.eclipse.jst.common.internal.annotations.core.AnnotationTagParser; import org.eclipse.jst.common.internal.annotations.core.TagParseEventHandler; import org.eclipse.jst.common.internal.annotations.core.Token; import org.eclipse.jst.common.internal.annotations.registry.AnnotationTagRegistry; import org.eclipse.jst.common.internal.annotations.registry.AttributeValueProposalHelper; import org.eclipse.jst.common.internal.annotations.registry.AttributeValuesHelper; import org.eclipse.jst.common.internal.annotations.registry.TagAttribSpec; import org.eclipse.jst.common.internal.annotations.registry.TagSpec; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.part.FileEditorInput; /** * @author Pat Kelley * * To change the template for this generated type comment go to Window - Preferences - Java - Code * Generation - Code and Comments */ public class AnnotationTagCompletionProc implements IJavadocCompletionProcessor, TagParseEventHandler { private static final String[] BOOLEAN_VALID_VALUES = new String[]{"false", "true"}; //$NON-NLS-1$ //$NON-NLS-2$ ICompilationUnit m_icu; IDocument m_doc; List m_tags; // Instance variables active when maybeCompleteAttribute is live. Token m_tagName; /** * Set of all attributes names encountered. Only live when maybeCompleteAttribute is live. */ Set m_attSet = new TreeSet(); /** * List of Attribute. Only live when maybeCompleAttribute is live. */ List m_attributes = new ArrayList(); AnnotationTagParser m_parser = new AnnotationTagParser(this); /** * Scope of the tag. TagSpec.TYPE | TagSpec.METHOD | TagSpec.FIELD. Not valid until * getAnnotationArea has been called for a completions request, and only then if * getAnnotationArea() did not return null. */ int m_tagScope; public AnnotationTagCompletionProc() { initTagInfo(); } private void initTagInfo() { if (m_tags == null) m_tags = AnnotationTagRegistry.getAllTagSpecs(); } /* * (non-Javadoc) * * @see org.eclipse.jdt.ui.text.java.IJavadocCompletionProcessor#computeContextInformation(org.eclipse.jdt.core.ICompilationUnit, * int) */ public IContextInformation[] computeContextInformation(ICompilationUnit cu, int offset) { return null; } /* * (non-Javadoc) * * @see org.eclipse.jdt.ui.text.java.IJavadocCompletionProcessor#computeCompletionProposals(org.eclipse.jdt.core.ICompilationUnit, * int, int, int) */ public IJavaCompletionProposal[] computeCompletionProposals(ICompilationUnit cu, int offset, int length, int flags) { if (cu == null) //bug 262362 return null; IEditorInput editorInput = new FileEditorInput((IFile) cu.getResource()); // Set up completion processor state. m_doc = JavaUI.getDocumentProvider().getDocument(editorInput); m_icu = cu; try { AnnotationArea area = getAnnotationArea(offset); if (area == null) { return null; } // Check for tag completion first. ( the easier case ) String tsf = getTagSoFarIfNotCompleted(area.beginOffset, offset); if (tsf != null) { return getTagCompletionsFor(tsf, area, length); } // Ach, have to try the harder case now, where we parse the // annotation return maybeCompleteAttribute(area, offset); } catch (JavaModelException e) { // Silently fail. return null; } catch (BadLocationException ex) { return null; } } private IJavaCompletionProposal[] maybeCompleteAttribute(AnnotationArea area, int cursorPos) throws BadLocationException { m_attSet.clear(); m_attributes.clear(); m_parser.setParserInput(m_doc.get(area.beginOffset, area.length())); m_parser.parse(); TagSpec ts = null; if (m_tagName!=null) ts = getTagSpecForTagName(m_tagName.getText()); // Do we even recognize this tag? if (ts == null) { return null; } // Loop through and determine whether the cursor is within a attribute // assignment, or between assignements. Attribute target = null; Attribute last = null; Attribute before = null; Attribute a = null; boolean between = false; int rCurPos = area.relativeCursorPos(cursorPos); Iterator i = m_attributes.iterator(); while (i.hasNext()) { a = (Attribute) i.next(); if (a.contains(rCurPos)) { target = a; break; } else if (last != null) { // See if the cursor is between, but not directly adjacent to // the last two attributes. if (rCurPos > last.maxExtent() + 1 && rCurPos < a.minExtent() - 1) { between = true; break; } else if (a.immediatelyPrecedes(rCurPos)) { before = a; break; } } last = a; } if (target == null) { if (between) { // If we're between attributes, suggest all possible attributes. return attributeCompletionsFor(ts, cursorPos, 0, "", true); //$NON-NLS-1$ } else if (before != null) { // We're right after the attribute named in 'before', so set the // target to it, and fall // through to the target handling code. target = before; } else { // not between and not immediately after an attribute. We are // past the end of the parsed annotation. // Only offer suggestions if it looks like the last annotation // attribute is valid. if (a == null) { // No annotations attributes, suggest everything. return attributeCompletionsFor(ts, cursorPos, 0, "", true); //$NON-NLS-1$ } else if (rCurPos > a.maxExtent()) { if (a.hasAssignment() && a.hasValue()) { // Last annotation was good, and we're past it, so do // completions for anything return attributeCompletionsFor(ts, cursorPos, 0, "", true); //$NON-NLS-1$ } else if (a.hasAssignment()) return attributeValidValuesFor(ts, a, area, cursorPos); else return attributeCompletionsFor(ts, cursorPos - a.name.length(), 0, a.name.getText(), true); } else { // Didn't match anything, not past the end - we're probably // the first attribute // being added to the tag. return attributeCompletionsFor(ts, cursorPos, 0, "", true); //$NON-NLS-1$ } } } // Completion for a partial attribute name? if (target.name.immediatelyPrecedes(rCurPos)) { return attributeCompletionsFor(ts, area.relativeToAbs(target.name.getBeginning()), target.name.length(), target.name.getText(), !target.hasAssignment()); } // Are we in the middle of a name? if (target.name.contains(rCurPos)) { // We've opted to replace the entire name for this case, which seems // to make the most sense. return attributeCompletionsFor(ts, area.relativeToAbs(target.name.getBeginning()), target.name.length(), target.name.getText().substring(0, rCurPos - target.name.getBeginning()), !target.hasAssignment()); } // If we got this far, we're either in a value, or really confused. // try and return valid values or bail? if (a!= null && a.value != null && (a.value.contains(rCurPos) || (target.hasAssignment() && area.relativeCursorPos(cursorPos) > a.name.getBeginning()))) return attributeValidValuesFor(ts, a, area, cursorPos); return attributeCompletionsFor(ts, cursorPos, 0, "", true); //$NON-NLS-1$ } /** * @return valid values for the attribute */ private IJavaCompletionProposal[] attributeValidValuesFor(TagSpec ts, Attribute a, AnnotationArea area, int cursorPos) { TagAttribSpec tas = ts.attributeNamed(a.name.getText()); if (tas == null) return null; String[] validValues = getValidValues(tas, a, area); String partialValue = calculatePartialValue(a, area, cursorPos); int valueOffset = calculateValueOffset(a, area, cursorPos); if (validValues == null || validValues.length == 0) return createCustomAttributeCompletionProposals(ts, tas, partialValue, valueOffset, a.value.getText(), area.javaElement); return createAttributeCompletionProposals(partialValue, valueOffset, validValues); } /** * @param ts * @param tas * @param partialValue * @param valueOffset * @param value * @param javaElement * @return */ private IJavaCompletionProposal[] createCustomAttributeCompletionProposals(TagSpec ts, TagAttribSpec tas, String partialValue, int valueOffset, String value, IJavaElement javaElement) { AttributeValuesHelper helper = ts.getValidValuesHelper(); if (helper == null) return null; AttributeValueProposalHelper[] proposalHelpers = helper.getAttributeValueProposalHelpers(tas, partialValue, valueOffset, javaElement); if (proposalHelpers == null || proposalHelpers.length == 0) return null; IJavaCompletionProposal[] proposals = new IJavaCompletionProposal[proposalHelpers.length]; AnnotationTagProposal proposal; for (int i = 0; i < proposalHelpers.length; i++) { proposal = new AnnotationTagProposal(proposalHelpers[i]); //proposal.setPartialValueString(partialValue); proposals[i] = proposal; } return proposals; } private IJavaCompletionProposal[] createAttributeCompletionProposals(String partialValue, int valueOffset, String[] validValues) { List resultingValues = new ArrayList(); for (int i = 0; i < validValues.length; i++) { String rplString = validValues[i]; if (partialValue != null && !rplString.startsWith(partialValue)) continue; AnnotationTagProposal prop = new AnnotationTagProposal(rplString, valueOffset, 0, null, rplString, 90); prop.setEnsureQuoted(true); //prop.setPartialValueString(partialValue); resultingValues.add(prop); } if (resultingValues.isEmpty()) return null; return (IJavaCompletionProposal[]) resultingValues.toArray(new IJavaCompletionProposal[resultingValues.size()]); } private String[] getValidValues(TagAttribSpec tas, Attribute a, AnnotationArea area) { String[] validValues = tas.getValidValues(); if (validValues == null || validValues.length == 0) { AttributeValuesHelper helper = tas.getTagSpec().getValidValuesHelper(); if (helper == null) return null; validValues = helper.getValidValues(tas, area.javaElement); if ((validValues == null || validValues.length == 0) && tas.valueIsBool()) validValues = BOOLEAN_VALID_VALUES; } return validValues; } /** * @param a * @param area * @param cursorPos * @return */ private int calculateValueOffset(Attribute a, AnnotationArea area, int cursorPos) { if (a.value == null) return cursorPos; int nameEnd = a.name.getEnd(); int valBeg = a.value.getBeginning(); if (valBeg > nameEnd + 2) return area.relativeToAbs(nameEnd + 2); //Value too far away to be correct. return area.relativeToAbs(valBeg); } /** * @param a * @param area * @param cursorPos * @return */ private String calculatePartialValue(Attribute a, AnnotationArea area, int cursorPos) { if (a.value == null) return null; int nameEnd = a.name.getEnd(); int valueBeg = a.value.getBeginning(); if (valueBeg > nameEnd + 2) return null; //Value is too far away so it must not be part of this attribute. int relativePos = area.relativeCursorPos(cursorPos); if (a.value.contains(relativePos)) { boolean hasBeginQuote = valueBeg - nameEnd == 2; String value = a.value.getText(); int end = relativePos - valueBeg; if (hasBeginQuote) end--; if (end > -1) { int length = value.length(); if (end < length) return value.substring(0, end); else if (end == length) return value; } } return null; } /** * @param tagName * @return */ private TagSpec getTagSpecForTagName(String tagName) { String simpleName = tagName; if (tagName != null && tagName.length() > 0 && tagName.charAt(0) == '@') simpleName = tagName.length() == 2 ? "" : tagName.substring(1); //$NON-NLS-1$ switch (m_tagScope) { case TagSpec.TYPE : return AnnotationTagRegistry.getTypeTag(simpleName); case TagSpec.METHOD : return AnnotationTagRegistry.getMethodTag(simpleName); case TagSpec.FIELD : return AnnotationTagRegistry.getFieldTag(simpleName); } return null; } private IJavaCompletionProposal[] attributeCompletionsFor(TagSpec ts, int replaceOffset, int replaceLength, String partialAttributeName, boolean appendEquals) { Iterator i = ts.getAttributes().iterator(); List props = new ArrayList(); while (i.hasNext()) { TagAttribSpec tas = (TagAttribSpec) i.next(); String aname = tas.getAttribName(); // Don't suggest attributes that have already been specified. if (!m_attSet.contains(aname)) { if (aname.startsWith(partialAttributeName)) { String rtxt = appendEquals ? aname + '=' : aname; AnnotationTagProposal prop = new AnnotationTagProposal(rtxt, replaceOffset, replaceLength, null, aname, 90); prop.setHelpText(lookupAttHelp(tas)); props.add(prop); } } } if (props.isEmpty()) { return null; } return (IJavaCompletionProposal[]) props.toArray(new IJavaCompletionProposal[props.size()]); } /* * (non-Javadoc) * * @see com.ibm.ws.rd.annotations.TagParseEventHandler#annotationTag(com.ibm.ws.rd.annotations.Token) */ public void annotationTag(Token tag) { m_tagName = tag; } /* * (non-Javadoc) * * @see com.ibm.ws.rd.annotations.TagParseEventHandler#endOfTag(int) */ public void endOfTag(int pos) { // Do nothing } /* * (non-Javadoc) * * @see com.ibm.ws.rd.annotations.TagParseEventHandler#attribute(com.ibm.ws.rd.annotations.Token, * int, com.ibm.ws.rd.annotations.Token) */ public void attribute(Token name, int equalsPosition, Token value) { m_attributes.add(new Attribute(name, equalsPosition, value)); m_attSet.add(name.getText()); } private String getReplacementForTag(TagSpec ts, int beginIndex) { StringBuffer bud = new StringBuffer(32); bud.append('@'); bud.append(ts.getTagName()); String prefix = getArrayPrefixForMultipleAttribs(beginIndex); List attributes = ts.getAttributes(); for (int i = 0; i < attributes.size(); i++) { TagAttribSpec tas = (TagAttribSpec) attributes.get(i); if (tas.isRequired()) { bud.append(prefix); bud.append(tas.getAttribName()); bud.append('='); } } return bud.toString(); } private String getArrayPrefixForMultipleAttribs(int beginIndex) { String result = null; String source = null; // Get source from compilation unit try { source = m_icu.getSource(); if (source == null || beginIndex < 0) return result; // trim off everything after our begin index source = source.substring(0, beginIndex + 1); int newLineIndex = source.lastIndexOf('\n'); //if we are on first line... if (newLineIndex == -1) newLineIndex = 0; // Get the current line String currentLine = source.substring(newLineIndex, beginIndex + 1); // Currently we have to have the '@' sign to show our menu int annotationIndex = currentLine.lastIndexOf('@'); result = currentLine.substring(0, annotationIndex); result = result + " "; //$NON-NLS-1$ } catch (Exception e) { // Do nothing } return result; } private IJavaCompletionProposal[] getTagCompletionsFor(String partialTagName, AnnotationArea area, int selectLength) { List found = new ArrayList(); for (int i = 0; i < m_tags.size(); i++) { TagSpec ts = (TagSpec) m_tags.get(i); String tname = ts.getTagName(); if (ts.getScope() == m_tagScope && tname.startsWith(partialTagName)) { String rtxt = getReplacementForTag(ts, area.beginOffset); String labl = '@' + tname; AnnotationTagProposal prop = new AnnotationTagProposal(rtxt, area.beginOffset, Math.max(selectLength, rtxt.length()), null, labl, 90); prop.setHelpText(lookupTagHelp(ts)); found.add(prop); } } if (!found.isEmpty()) { return (IJavaCompletionProposal[]) found.toArray(new IJavaCompletionProposal[found.size()]); } return null; } /* * (non-Javadoc) * * @see org.eclipse.jdt.ui.text.java.IJavadocCompletionProcessor#getErrorMessage() */ public String getErrorMessage() { return null; } private static boolean isWS1(char c) { return c == ' ' || c == '\t' || c == '*' || c == '\r' || c == '\n'; } private String getTagSoFarIfNotCompleted(int startingAt, int cursorAt) throws BadLocationException { if (m_doc.getChar(startingAt) != '@') { return null; } int firstChar = startingAt + 1; if (firstChar == cursorAt) { return ""; //$NON-NLS-1$ } for (int i = firstChar; i < cursorAt; i++) { char c = m_doc.getChar(i); if (isWS1(c)) { return null; } } return m_doc.get(firstChar, cursorAt - firstChar); } /** * Calculates the the area of the annotation we're trying to complete. Also initializes * m_tagScope. * * @param fromOffset * @return * @throws JavaModelException */ private AnnotationArea getAnnotationArea(int fromOffset) throws JavaModelException { // First, roughly calculate the end of the comment. IJavaElement el = m_icu.getElementAt(fromOffset); int absmax, absmin; if (el == null) return null; int ty = el.getElementType(); switch (ty) { case IJavaElement.FIELD : IField f = (IField) el; absmax = f.getNameRange().getOffset(); absmin = f.getSourceRange().getOffset(); m_tagScope = TagSpec.FIELD; break; case IJavaElement.TYPE : IType t = (IType) el; absmax = t.getNameRange().getOffset(); absmin = t.getSourceRange().getOffset(); m_tagScope = TagSpec.TYPE; break; case IJavaElement.METHOD : IMethod m = (IMethod) el; absmax = m.getNameRange().getOffset(); absmin = m.getSourceRange().getOffset(); m_tagScope = TagSpec.METHOD; break; default : m_tagScope = -1; return null; } // Make sure we're not after the name for the member. if (absmax < fromOffset) { return null; } int min = 0, max = 0; try { // Search backwards for the starting '@'. boolean found = false; for (min = fromOffset; min >= absmin; min--) { if (m_doc.getChar(min) == '@') { found = true; break; } } if (!found) { return null; } // Search forwards for the next '@', or the end of the comment. for (max = fromOffset + 1; max < absmax; max++) { if (m_doc.getChar(max) == '@') { break; } } } catch (BadLocationException e) { return null; } return new AnnotationArea(el, min, Math.min(absmax, max)); } private String lookupTagHelp(TagSpec ts) { if (ts != null) try { return ts.lookupTagHelp(); } catch (MissingResourceException e) { // Do nothing, return null } return null; } private String lookupAttHelp(TagAttribSpec tas) { if (tas != null) try { return tas.lookupTagHelp(); } catch (MissingResourceException e) { // Do nothing, return null } return null; } /** * A range that goes from the beginning position up to, but not including, the end position. */ private static class AnnotationArea { /** * Document offset of the beginning of the javadoc annotation. */ int beginOffset; /** * Document offset of the end of the area that could contain an annotation. */ int endOffset; /** * The Java element that this annotation is assigned. * * @param beg * @param end */ IJavaElement javaElement; public AnnotationArea(IJavaElement javaElement, int beg, int end) { this.javaElement = javaElement; beginOffset = beg; endOffset = end; } public int length() { return endOffset - beginOffset; } /** * Returns the cursor position relative to the area. Only valid if * <code>this.contains( absCursorPos )</code> * * @param absCursorPos * @return */ public int relativeCursorPos(int absCursorPos) { return absCursorPos - beginOffset; } public int relativeToAbs(int relPos) { return beginOffset + relPos; } } private static class Attribute { Token name; Token value; int equalsPos; Attribute(Token n, int ep, Token v) { name = n; value = v; equalsPos = ep; } public boolean hasAssignment() { return equalsPos != -1; } public boolean hasValue() { return value.length() != 0; } public boolean contains(int srcPos) { return srcPos >= minExtent() && srcPos <= maxExtent(); } public int minExtent() { return name.getBeginning(); } public int maxExtent() { if (hasAssignment()) { if (hasValue()) return value.getEnd(); return equalsPos; } return name.getEnd(); } public boolean immediatelyPrecedes(int pos) { return maxExtent() + 1 == pos; } } }