0001: /*
0002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
0003: *
0004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
0005: *
0006: * The contents of this file are subject to the terms of either the GNU
0007: * General Public License Version 2 only ("GPL") or the Common
0008: * Development and Distribution License("CDDL") (collectively, the
0009: * "License"). You may not use this file except in compliance with the
0010: * License. You can obtain a copy of the License at
0011: * http://www.netbeans.org/cddl-gplv2.html
0012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
0013: * specific language governing permissions and limitations under the
0014: * License. When distributing the software, include this License Header
0015: * Notice in each file and include the License file at
0016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
0017: * particular file as subject to the "Classpath" exception as provided
0018: * by Sun in the GPL Version 2 section of the License file that
0019: * accompanied this code. If applicable, add the following below the
0020: * License Header, with the fields enclosed by brackets [] replaced by
0021: * your own identifying information:
0022: * "Portions Copyrighted [year] [name of copyright owner]"
0023: *
0024: * Contributor(s):
0025: *
0026: * The Original Software is NetBeans. The Initial Developer of the Original
0027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
0028: * Microsystems, Inc. All Rights Reserved.
0029: *
0030: * If you wish your version of this file to be governed by only the CDDL
0031: * or only the GPL Version 2, indicate your decision by adding
0032: * "[Contributor] elects to include this software in this distribution
0033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
0034: * single choice of license, a recipient has the option to distribute
0035: * your version of this file under either the CDDL, the GPL Version 2 or
0036: * to extend the choice of license to its licensees as provided above.
0037: * However, if you add GPL Version 2 code and therefore, elected the GPL
0038: * Version 2 license, then the option applies only if the new code is
0039: * made subject to such option by the copyright holder.
0040: */
0041:
0042: package org.netbeans.editor.ext.html;
0043:
0044: import java.util.logging.Level;
0045: import java.util.logging.Logger;
0046: import org.netbeans.editor.ext.html.SyntaxElement;
0047: import java.awt.Component;
0048: import java.awt.Font;
0049: import java.awt.Graphics;
0050: import java.io.IOException;
0051: import java.net.URL;
0052: import java.util.*;
0053: import java.awt.Color;
0054: import java.awt.event.KeyEvent;
0055: import javax.swing.Action;
0056: import javax.swing.text.JTextComponent;
0057: import javax.swing.text.BadLocationException;
0058: import javax.swing.text.Caret;
0059: import javax.swing.text.Document;
0060: import org.netbeans.editor.*;
0061: import org.netbeans.editor.SettingsUtil;
0062: import org.netbeans.editor.Utilities;
0063: import org.netbeans.editor.ext.*;
0064: import org.netbeans.editor.ext.html.dtd.*;
0065: import org.netbeans.editor.ext.html.javadoc.HelpManager;
0066: import org.netbeans.api.editor.completion.Completion;
0067: import org.netbeans.api.html.lexer.HTMLTokenId;
0068: import org.netbeans.api.lexer.Token;
0069: import org.netbeans.api.lexer.TokenHierarchy;
0070: import org.netbeans.api.lexer.TokenId;
0071: import org.netbeans.api.lexer.TokenSequence;
0072: import org.netbeans.modules.editor.indent.api.Reformat;
0073: import org.netbeans.spi.editor.completion.CompletionDocumentation;
0074: import org.netbeans.spi.editor.completion.CompletionItem;
0075: import org.netbeans.spi.editor.completion.CompletionResultSet;
0076: import org.netbeans.spi.editor.completion.CompletionTask;
0077: import org.netbeans.spi.editor.completion.support.AsyncCompletionQuery;
0078: import org.netbeans.spi.editor.completion.support.AsyncCompletionTask;
0079: import org.openide.ErrorManager;
0080:
0081: /**
0082: * HTML completion results finder
0083: *
0084: * @author Petr Nejedly
0085: * @author Marek Fukala
0086: * @version 1.10
0087: */
0088: public class HTMLCompletionQuery {
0089:
0090: private static final String SCRIPT_TAG_NAME = "SCRIPT"; //NOI18N
0091: private static final String STYLE_TAG_NAME = "STYLE"; //NOI18N
0092:
0093: private static boolean lowerCase;
0094:
0095: private static final HTMLCompletionQuery DEFAULT = new HTMLCompletionQuery();
0096:
0097: public static HTMLCompletionQuery getDefault() {
0098: return DEFAULT;
0099: }
0100:
0101: /** Perform the query on the given component. The query usually
0102: * gets the component's document, the caret position and searches back
0103: * to examine surrounding context. Then it returns the result.
0104: * @param component the component to use in this query.
0105: * @param offset position in the component's document to which the query will
0106: * be performed. Usually it's a caret position.
0107: * @param support syntax-support that will be used during resolving of the query.
0108: * @return result of the query or null if there's no result.
0109: */
0110: public List<CompletionItem> query(JTextComponent component,
0111: int offset) {
0112: Class kitClass = Utilities.getKitClass(component);
0113: BaseDocument doc = (BaseDocument) component.getDocument();
0114:
0115: if (kitClass != null) {
0116: lowerCase = SettingsUtil.getBoolean(kitClass,
0117: HTMLSettingsNames.COMPLETION_LOWER_CASE,
0118: HTMLSettingsDefaults.defaultCompletionLowerCase);
0119: }
0120:
0121: if (doc.getLength() == 0)
0122: return null; // nothing to examine
0123: HTMLSyntaxSupport sup = HTMLSyntaxSupport.get(doc);
0124:
0125: if (sup == null)
0126: return null;// No SyntaxSupport for us, no hint for user
0127:
0128: DTD dtd = sup.getDTD();
0129: if (dtd == null)
0130: return null; // We have no knowledge about the structure!
0131:
0132: doc.readLock();
0133: try {
0134: TokenHierarchy hi = TokenHierarchy.get(doc);
0135: TokenSequence ts = hi.tokenSequence(HTMLTokenId.language());
0136: if (ts == null) {
0137: //HTML language is not top level one
0138: ts = hi.tokenSequence();
0139: ts.move(offset);
0140: if (ts.moveNext() || ts.movePrevious()) {
0141: ts = ts.embedded(HTMLTokenId.language());
0142: } else { // no tokens
0143: return null;
0144: }
0145: }
0146:
0147: if (ts == null) {
0148: //no HTML token on the offset
0149: return null;
0150: }
0151:
0152: ts.move(offset);
0153: if (!ts.moveNext() && !ts.movePrevious()) {
0154: return null; //no token found
0155: }
0156:
0157: Token item = ts.token();
0158:
0159: // are we inside token or between tokens
0160: boolean inside = ts.offset() < offset;
0161:
0162: if (!inside) { //use the previous token
0163: if (ts.movePrevious()) {
0164: item = ts.token();
0165: } else {
0166: return null; //no previous token - shouldn't happen
0167: }
0168: }
0169:
0170: Token tok = item;
0171: //scan the token chain before the
0172: while (!(tok.id() == HTMLTokenId.TAG_OPEN || tok.id() == HTMLTokenId.TAG_CLOSE)
0173: && ts.movePrevious()) {
0174: tok = ts.token();
0175: }
0176:
0177: //we found an open or close tag or encountered beginning of the file
0178: if (ts.index() > 0) {
0179: //found the tag
0180: String tagName = tok.text().toString();
0181: for (int i = 0; i < tagName.length(); i++) {
0182: char ch = tagName.charAt(i);
0183: if (Character.isLetter(ch)) {
0184: lowerCase = !Character.isUpperCase(tagName
0185: .charAt(i));
0186: break;
0187: }
0188: }
0189: } //else use the setting value
0190:
0191: //rewind token sequence back
0192: ts.move(item.offset(hi));
0193:
0194: //get text before cursor
0195: int itemOffset = item.offset(hi);
0196: int diff = offset - itemOffset;
0197: String preText = item.text().toString();
0198:
0199: if (diff < preText.length()) {
0200: preText = preText.substring(0, offset - itemOffset);
0201: }
0202: TokenId id = item.id();
0203:
0204: List<CompletionItem> result = null;
0205: int len = 1;
0206:
0207: /* Character reference finder */
0208: if ((id == HTMLTokenId.TEXT || id == HTMLTokenId.VALUE)
0209: && preText.endsWith("&")) { // NOI18N
0210: result = translateCharRefs(offset - len, len, dtd
0211: .getCharRefList(""));
0212: } else if (id == HTMLTokenId.CHARACTER) {
0213: if (inside || !preText.endsWith(";")) { // NOI18N
0214: len = offset - itemOffset;
0215: result = translateCharRefs(offset - len, len, dtd
0216: .getCharRefList(preText.substring(1)));
0217: }
0218: /* Tag finder */
0219: } else if (id == HTMLTokenId.TAG_OPEN) { // NOI18N
0220: len = offset - itemOffset + 1; // minus the < char length
0221: result = translateTags(itemOffset - 1, len, dtd
0222: .getElementList(preText));
0223:
0224: //test whether there is only one item in the CC list
0225: if (result.size() == 1) {
0226: //test whether the CC is trying to complete an already COMPLETE token - the problematic situation
0227: TagItem ti = (TagItem) result.get(0); //there should only one item
0228: String itemText = ti.getItemText();
0229: //itemText = itemText.substring(1, itemText.length() - 1); //remove the < > from the tag name
0230:
0231: if (preText.equals(itemText)) {
0232: //now I have to look ahead to get know whether
0233: //there are some attributes or an end of the tag
0234:
0235: ts.move(offset);
0236: ts.moveNext();
0237: Token t = ts.token();
0238:
0239: //test if next token is a whitespace and the next a tag token or an attribute token
0240: if (t.id() == HTMLTokenId.WS) {
0241: if (ts.moveNext()) {
0242: t = ts.token();
0243: if ((t.id() == HTMLTokenId.TAG_CLOSE || t
0244: .id() == HTMLTokenId.ARGUMENT)) {
0245: //do not put the item into CC - otherwise it will break the completed tag
0246: result = null;
0247: }
0248: }
0249: }
0250: }
0251: }
0252:
0253: } else if (id != HTMLTokenId.BLOCK_COMMENT
0254: && preText.endsWith("<")) { // NOI18N
0255: // There will be lookup for possible StartTags, in SyntaxSupport
0256: // l = translateTags( offset-len, len, sup.getPossibleStartTags ( offset-len, "" ) );
0257: result = translateTags(offset - len, len, dtd
0258: .getElementList(""));
0259:
0260: /* EndTag finder */
0261: } else if (id == HTMLTokenId.TEXT && preText.endsWith("</")) { // NOI18N
0262: len = 2;
0263: result = sup.getPossibleEndTags(offset, "");
0264: } else if (id == HTMLTokenId.TAG_OPEN_SYMBOL
0265: && preText.endsWith("</")) { // NOI18N
0266: len = 2;
0267: result = sup.getPossibleEndTags(offset, "");
0268: } else if (id == HTMLTokenId.TAG_CLOSE) { // NOI18N
0269: len = offset - itemOffset;
0270: result = sup.getPossibleEndTags(offset, preText);
0271:
0272: /*Argument finder */
0273: /* TBD: It is possible to have arg just next to quoted value of previous
0274: * arg, these rules doesn't match start of such arg this case because
0275: * of need for matching starting quote
0276: */
0277: } else if (id == HTMLTokenId.TAG_CLOSE_SYMBOL) {
0278: result = sup.getAutocompletedEndTag(offset);
0279:
0280: } else if (id == HTMLTokenId.WS
0281: || id == HTMLTokenId.ARGUMENT) {
0282: SyntaxElement elem = null;
0283: try {
0284: elem = sup.getElementChain(offset);
0285: // #BUGFIX 25261 At the end of document the element is
0286: // automatically null but that does not mean that the
0287: // completion should return null. Only if element is null
0288: // also for offset-1...
0289: // + bugfix of #52909 - the > is recognized as SyntaxElement.TAG so we need to
0290: // get a syntax element before, when cc is called before > in a tag e.g. <table w|>
0291: if (elem == null
0292: || (elem.getType() == SyntaxElement.TYPE_TAG && ">"
0293: .equals(elem.getText()))) { // NOI18N
0294: elem = sup.getElementChain(offset - 1);
0295: }
0296:
0297: } catch (BadLocationException e) {
0298: return null;
0299: }
0300:
0301: if (elem == null)
0302: return null;
0303:
0304: if (elem.getType() == SyntaxElement.TYPE_TAG) { // not endTags
0305: SyntaxElement.Tag tagElem = (SyntaxElement.Tag) elem;
0306:
0307: String tagName = tagElem.getName().toUpperCase();
0308: DTD.Element tag = dtd.getElement(tagName);
0309:
0310: if (tag == null)
0311: return null; // unknown tag
0312:
0313: String prefix = (id == HTMLTokenId.ARGUMENT) ? preText
0314: : "";
0315: len = prefix.length();
0316: List possible = tag.getAttributeList(prefix); // All attribs of given tag
0317: Collection<SyntaxElement.TagAttribute> existing = tagElem
0318: .getAttributes(); // Attribs already used
0319: Collection<String> existingAttrsNames = new ArrayList<String>(
0320: existing.size());
0321: for (SyntaxElement.TagAttribute ta : existing) {
0322: existingAttrsNames.add(ta.getName());
0323: }
0324:
0325: String wordAtCursor = (item == null) ? null : item
0326: .text().toString();
0327: // #BUGFIX 25261 because of being at the end of document the
0328: // wordAtCursor must be checked for null to prevent NPE
0329: // below
0330: if (wordAtCursor == null) {
0331: wordAtCursor = "";
0332: }
0333:
0334: List<DTD.Attribute> attribs = new ArrayList<DTD.Attribute>();
0335: for (Iterator i = possible.iterator(); i.hasNext();) {
0336: DTD.Attribute attr = (DTD.Attribute) i.next();
0337: String aName = attr.getName();
0338: if (aName.equals(prefix)
0339: || (!existingAttrsNames.contains(aName
0340: .toUpperCase()) && !existingAttrsNames
0341: .contains(aName.toLowerCase()))
0342: || (wordAtCursor.equals(aName) && prefix
0343: .length() > 0)) {
0344: attribs.add(attr);
0345: }
0346: }
0347: result = translateAttribs(offset - len, len,
0348: attribs, tag);
0349: }
0350:
0351: /* Value finder */
0352: /* Suggestion - find special-meaning attributes ( IMG src, A href,
0353: * color,.... - may be better resolved by attr type, may be moved
0354: * to propertysheet
0355: */
0356: } else if (id == HTMLTokenId.VALUE
0357: || id == HTMLTokenId.OPERATOR
0358: || id == HTMLTokenId.WS) {
0359:
0360: if (id == HTMLTokenId.WS) {
0361: //is the token before an operator? '<div color= |red>'
0362: ts.move(item.offset(hi));
0363: ts.movePrevious();
0364: Token t = ts.token();
0365: if (t.id() != HTMLTokenId.OPERATOR) {
0366: return null;
0367: }
0368: }
0369:
0370: SyntaxElement elem = null;
0371: try {
0372: elem = sup.getElementChain(offset);
0373: } catch (BadLocationException e) {
0374: return null;
0375: }
0376:
0377: if (elem == null)
0378: return null;
0379:
0380: // between Tag and error - common state when entering OOTL, e.g. <BDO dir=>
0381: if (elem.getType() == SyntaxElement.TYPE_ERROR) {
0382: elem = elem.getPrevious();
0383: if (elem == null)
0384: return null;
0385: }
0386: if (elem.getType() == SyntaxElement.TYPE_TAG) {
0387: SyntaxElement.Tag tagElem = (SyntaxElement.Tag) elem;
0388:
0389: String tagName = tagElem.getName().toUpperCase();
0390: DTD.Element tag = dtd.getElement(tagName);
0391: if (tag == null)
0392: return null; // unknown tag
0393:
0394: ts.move(item.offset(hi));
0395: ts.moveNext();
0396: Token argItem = ts.token();
0397: while (argItem.id() != HTMLTokenId.ARGUMENT
0398: && ts.movePrevious()) {
0399: argItem = ts.token();
0400: }
0401:
0402: if (argItem.id() != HTMLTokenId.ARGUMENT)
0403: return null; // no ArgItem
0404:
0405: String argName = argItem.text().toString()
0406: .toLowerCase();
0407:
0408: DTD.Attribute arg = tag.getAttribute(argName);
0409: if (arg == null
0410: || arg.getType() != DTD.Attribute.TYPE_SET)
0411: return null;
0412:
0413: if (id != HTMLTokenId.VALUE) {
0414: len = 0;
0415: result = translateValues(offset - len, len, arg
0416: .getValueList(""));
0417: } else {
0418: len = offset - itemOffset;
0419:
0420: String quotationChar = null;
0421: if (preText != null && preText.length() > 0) {
0422: if (preText.substring(0, 1).equals("'"))
0423: quotationChar = "'"; // NOI18N
0424: if (preText.substring(0, 1).equals("\""))
0425: quotationChar = "\""; // NOI18N
0426: }
0427:
0428: result = translateValues(
0429: offset - len,
0430: len,
0431: arg
0432: .getValueList(quotationChar == null ? preText
0433: : preText.substring(1)),
0434: quotationChar);
0435: }
0436: }
0437: } else if (id == HTMLTokenId.SCRIPT) {
0438: result = addEndTag(SCRIPT_TAG_NAME, preText, offset);
0439: } else if (id == HTMLTokenId.STYLE) {
0440: result = addEndTag(STYLE_TAG_NAME, preText, offset);
0441: }
0442:
0443: return result;
0444:
0445: } catch (BadLocationException ble) {
0446: ErrorManager.getDefault().notify(ble);
0447: } finally {
0448: doc.readUnlock();
0449: }
0450:
0451: return null;
0452: }
0453:
0454: private List<CompletionItem> addEndTag(String tagName,
0455: String preText, int offset) {
0456: int commonLength = getLastCommonCharIndex("</" + tagName + ">",
0457: preText.toUpperCase().trim()); //NOI18N
0458: if (commonLength == -1) {
0459: commonLength = 0;
0460: }
0461: if (commonLength == preText.trim().length()) {
0462: ArrayList<CompletionItem> items = new ArrayList<CompletionItem>(
0463: 1);
0464: items.add(new EndTagItem(lowerCase ? tagName.toLowerCase()
0465: : tagName, offset - commonLength, commonLength));
0466: return items;
0467: }
0468: return null;
0469: }
0470:
0471: private int getLastCommonCharIndex(String base, String pattern) {
0472: int i = 0;
0473: for (; i < base.length() && i < pattern.length(); i++) {
0474: if (base.charAt(i) != pattern.charAt(i)) {
0475: i--;
0476: break;
0477: }
0478: }
0479: return i;
0480: }
0481:
0482: List<CompletionItem> translateCharRefs(int offset, int length,
0483: List refs) {
0484: List result = new ArrayList(refs.size());
0485: String name;
0486: for (Iterator i = refs.iterator(); i.hasNext();) {
0487: name = ((DTD.CharRef) i.next()).getName();
0488: result.add(new CharRefItem(name, offset, length, name));
0489: }
0490: return result;
0491: }
0492:
0493: List<CompletionItem> translateTags(int offset, int length, List tags) {
0494: List result = new ArrayList(tags.size());
0495: String name;
0496: for (Iterator i = tags.iterator(); i.hasNext();) {
0497: name = ((DTD.Element) i.next()).getName();
0498: result.add(new TagItem(name, offset, length, name));
0499: }
0500: return result;
0501: }
0502:
0503: List<CompletionItem> translateAttribs(int offset, int length,
0504: List<DTD.Attribute> attribs, DTD.Element tag) {
0505: List<CompletionItem> result = new ArrayList<CompletionItem>(
0506: attribs.size());
0507: String tagName = tag.getName() + "#"; // NOI18N
0508: for (DTD.Attribute attrib : attribs) {
0509: String name = attrib.getName();
0510: switch (attrib.getType()) {
0511: case DTD.Attribute.TYPE_BOOLEAN:
0512: result.add(new BooleanAttribItem(name, offset, length,
0513: attrib.isRequired(), tagName + name));
0514: break;
0515: case DTD.Attribute.TYPE_SET:
0516: result.add(new SetAttribItem(name, offset, length,
0517: attrib.isRequired(), tagName + name));
0518: break;
0519: case DTD.Attribute.TYPE_BASE:
0520: result.add(new PlainAttribItem(name, offset, length,
0521: attrib.isRequired(), tagName + name));
0522: break;
0523: }
0524: }
0525: return result;
0526: }
0527:
0528: List<CompletionItem> translateValues(int offset, int length,
0529: List values) {
0530: return translateValues(offset, length, values, null);
0531: }
0532:
0533: List<CompletionItem> translateValues(int offset, int length,
0534: List values, String quotationChar) {
0535: if (values == null)
0536: return new ArrayList(0);
0537: List result = new ArrayList(values.size());
0538: for (Iterator i = values.iterator(); i.hasNext();) {
0539: result.add(new ValueItem(((DTD.Value) i.next()).getName(),
0540: offset, length, quotationChar));
0541: }
0542: return result;
0543: }
0544:
0545: // Implementation of ResultItems for completion
0546: /** The simple result item operating over an instance of the string,
0547: * it is lightweight in the mean it doesn't allocate any new instances
0548: * of anything and every data creates lazily on request to avoid
0549: * creation of lot of string instances per completion result.
0550: */
0551: public static abstract class HTMLResultItem implements
0552: CompletionItem {
0553:
0554: /** The String on which is this ResultItem defined */
0555: String baseText;
0556: /** the remove and insert point for this item */
0557: int offset;
0558: /** The length of the text to be removed */
0559: int length;
0560:
0561: String helpID;
0562:
0563: boolean shift = false;
0564:
0565: private HTMLCompletionResultItemPaintComponent component;
0566:
0567: private static final int HTML_ITEMS_SORT_PRIORITY = 20;
0568:
0569: public HTMLResultItem(String baseText, int offset, int length) {
0570: this .baseText = lowerCase ? baseText.toLowerCase()
0571: : baseText.toUpperCase();
0572: this .offset = offset;
0573: this .length = length;
0574: this .helpID = null;
0575: }
0576:
0577: public HTMLResultItem(String baseText, int offset, int length,
0578: String helpID) {
0579: this (baseText, offset, length);
0580: this .helpID = helpID;
0581: }
0582:
0583: //-------------
0584: protected int selectionStartOffset = -1;
0585: protected int selectionEndOffset = -1;
0586:
0587: public int getSortPriority() {
0588: return HTML_ITEMS_SORT_PRIORITY;
0589: }
0590:
0591: public CharSequence getSortText() {
0592: return HTMLResultItem.this .getItemText();
0593: }
0594:
0595: public CharSequence getInsertPrefix() {
0596: return getItemText();
0597: }
0598:
0599: public Component getPaintComponent(boolean isSelected) {
0600: //TODO: the paint component should be caches somehow
0601: HTMLCompletionResultItemPaintComponent component = new HTMLCompletionResultItemPaintComponent.StringPaintComponent(
0602: getPaintColor());
0603: component.setSelected(isSelected);
0604: component.setString(getItemText());
0605: return component;
0606: }
0607:
0608: public int getPreferredWidth(Graphics g, Font defaultFont) {
0609: HTMLCompletionResultItemPaintComponent renderComponent = (HTMLCompletionResultItemPaintComponent) getPaintComponent(false);
0610: return renderComponent.getPreferredWidth(g, defaultFont);
0611: }
0612:
0613: public void render(Graphics g, Font defaultFont,
0614: Color defaultColor, Color backgroundColor, int width,
0615: int height, boolean selected) {
0616: Component renderComponent = getPaintComponent(selected);
0617: renderComponent.setFont(defaultFont);
0618: renderComponent.setForeground(defaultColor);
0619: renderComponent.setBackground(backgroundColor);
0620: renderComponent.setBounds(0, 0, width, height);
0621: ((HTMLCompletionResultItemPaintComponent) renderComponent)
0622: .paintComponent(g);
0623: }
0624:
0625: protected Object getAssociatedObject() {
0626: return getItemText();
0627: }
0628:
0629: public static final String COMPLETION_SUBSTITUTE_TEXT = "completion-substitute-text"; //NOI18N
0630:
0631: static int substituteOffset = -1;
0632:
0633: public int getSubstituteOffset() {
0634: return substituteOffset;
0635: }
0636:
0637: public boolean instantSubstitution(JTextComponent c) {
0638: defaultAction(c);
0639: return true;
0640: }
0641:
0642: public CompletionTask createDocumentationTask() {
0643: return new AsyncCompletionTask(new DocQuery(this ));
0644: }
0645:
0646: public CompletionTask createToolTipTask() {
0647: return null;
0648: }
0649:
0650: public int getImportance() {
0651: return 0;
0652: }
0653:
0654: public void processKeyEvent(KeyEvent e) {
0655: shift = (e.getKeyCode() == KeyEvent.VK_ENTER
0656: && e.getID() == KeyEvent.KEY_PRESSED && e
0657: .isShiftDown());
0658: }
0659:
0660: public void defaultAction(JTextComponent component) {
0661: int substOffset = getSubstituteOffset();
0662: if (substOffset == -1)
0663: substOffset = component.getCaretPosition();
0664:
0665: if (!shift)
0666: Completion.get().hideAll();
0667: substituteText(component, substOffset, component
0668: .getCaretPosition()
0669: - substOffset, shift);
0670: }
0671:
0672: boolean replaceText(JTextComponent component, String text) {
0673: BaseDocument doc = (BaseDocument) component.getDocument();
0674: doc.atomicLock();
0675: try {
0676: //test whether we are trying to insert sg. what is already present in the text
0677: String currentText = doc.getText(offset, (doc
0678: .getLength() - offset) < text.length() ? (doc
0679: .getLength() - offset) : text.length());
0680: if (!text.equals(currentText)) {
0681: //remove common part
0682: doc.remove(offset, length);
0683: doc.insertString(offset, text, null);
0684: } else {
0685: int newCaretPos = component.getCaret().getDot()
0686: + text.length() - length;
0687: //#82242 workaround - the problem is that in some situations
0688: //1) result item is created and it remembers the remove length
0689: //2) document is changed
0690: //3) RI is substituted.
0691: //this situation shouldn't happen imho and is a problem of CC infrastructure
0692: component.setCaretPosition(newCaretPos < doc
0693: .getLength() ? newCaretPos : doc
0694: .getLength());
0695: }
0696: } catch (BadLocationException exc) {
0697: return false; //not sucessfull
0698: } finally {
0699: doc.atomicUnlock();
0700: }
0701: return true;
0702: }
0703:
0704: protected void reformat(JTextComponent component, String text) {
0705: //does nothing by default; is overriden in EndTag
0706: }
0707:
0708: public boolean substituteCommonText(JTextComponent c, int a,
0709: int b, int subLen) {
0710: String text = getItemText().substring(0, subLen);
0711: boolean replaced = replaceText(c, text);
0712: reformat(c, text);
0713: return replaced;
0714: }
0715:
0716: public boolean substituteText(JTextComponent c, int a, int b,
0717: boolean shift) {
0718: String text = getItemText();
0719: boolean replaced = replaceText(c, text);
0720: reformat(c, text);
0721: return replaced;
0722: }
0723:
0724: /** @return Properly colored JLabel with text gotten from <CODE>getPaintText()</CODE>. */
0725: public Component getPaintComponent(javax.swing.JList list,
0726: boolean isSelected, boolean cellHasFocus) {
0727: Component ret = getPaintComponent(isSelected);
0728: if (ret == null)
0729: return null;
0730: if (isSelected) {
0731: ret.setBackground(list.getSelectionBackground());
0732: ret.setForeground(list.getSelectionForeground());
0733: } else {
0734: ret.setBackground(list.getBackground());
0735: ret.setForeground(list.getForeground());
0736: }
0737: ret.getAccessibleContext().setAccessibleName(getItemText());
0738: ret.getAccessibleContext().setAccessibleDescription(
0739: getItemText());
0740: return ret;
0741: }
0742:
0743: /** The string used in painting by <CODE>getPaintComponent()</CODE>.
0744: * It defaults to delegate to <CODE>getItemText()</CODE>.
0745: * @return The String to be painted in Completion View.
0746: */
0747: String getPaintText() {
0748: return getItemText();
0749: }
0750:
0751: abstract Color getPaintColor();
0752:
0753: /** @return The String used for looking up the common part of multiple
0754: * items and for default way of replacing the text */
0755: public String getItemText() {
0756: return baseText;
0757: }
0758:
0759: public String getHelpID() {
0760: return helpID;
0761: }
0762:
0763: public String toString() {
0764: StringBuffer sb = new StringBuffer();
0765: String className = this .getClass().getName();
0766: className = className
0767: .substring(className.lastIndexOf('.') + 1); //cut off the package
0768: sb.append(className);
0769: sb.append('(');
0770: sb.append(getItemText());
0771: sb.append(';');
0772: sb.append(getSubstituteOffset());
0773: sb.append(';');
0774: sb.append(getHelpID());
0775: sb.append(')');
0776:
0777: return sb.toString();
0778: }
0779:
0780: }
0781:
0782: public static class AutocompleteEndTagItem extends EndTagItem {
0783: public AutocompleteEndTagItem(String baseText, int offset) {
0784: this (baseText, offset, true);
0785: }
0786:
0787: public AutocompleteEndTagItem(String baseText, int offset,
0788: boolean changeCase) {
0789: super (baseText, offset, 0);
0790: if (!changeCase) {
0791: this .baseText = baseText; //#87218 hotfix - set the original value
0792: }
0793: }
0794:
0795: @Override()
0796: boolean replaceText(JTextComponent component, String text) {
0797: boolean replaced = super .replaceText(component, text);
0798: if (replaced) {
0799: component.setCaretPosition(offset);
0800: }
0801: return replaced;
0802: }
0803:
0804: @Override
0805: protected void reformat(JTextComponent component, String text) {
0806: try {
0807: BaseDocument doc = (BaseDocument) component
0808: .getDocument();
0809: int dotPos = component.getCaretPosition();
0810: Reformat reformat = Reformat.get(doc);
0811: reformat.lock();
0812:
0813: try {
0814: doc.atomicLock();
0815: try {
0816: int startOffset = Utilities.getRowStart(doc,
0817: dotPos);
0818: int endOffset = Utilities
0819: .getRowEnd(doc, dotPos);
0820: reformat.reformat(startOffset, endOffset);
0821: } finally {
0822: doc.atomicUnlock();
0823: }
0824: } finally {
0825: reformat.unlock();
0826: }
0827: } catch (BadLocationException e) {
0828: //ignore
0829: }
0830: }
0831:
0832: @Override
0833: public CharSequence getInsertPrefix() {
0834: //disable instant substitution
0835: return null;
0836: }
0837:
0838: }
0839:
0840: static class EndTagItem extends HTMLResultItem {
0841:
0842: private int order = 0;
0843:
0844: public EndTagItem(String baseText, int offset, int length) {
0845: super (baseText, offset, length);
0846: }
0847:
0848: public EndTagItem(String baseText, int offset, int length,
0849: String helpID) {
0850: super (baseText, offset, length, helpID);
0851: }
0852:
0853: public EndTagItem(String baseText, int offset, int length,
0854: String helpID, int order) {
0855: this (baseText, offset, length, helpID);
0856: this .order = order;
0857: }
0858:
0859: public CharSequence getSortText() {
0860: return getSortText(this .order);
0861: }
0862:
0863: private String getSortText(int index) {
0864: int zeros = index > 100 ? 0 : index > 10 ? 1 : 2;
0865: StringBuffer sb = new StringBuffer();
0866: for (int i = 0; i < zeros; i++) {
0867: sb.append('0');
0868: }
0869: sb.append("" + index);
0870: return sb.toString();
0871: }
0872:
0873: Color getPaintColor() {
0874: return Color.blue;
0875: }
0876:
0877: public String getItemText() {
0878: return "</" + baseText + ">";
0879: } // NOI18N
0880:
0881: public boolean substituteText(JTextComponent c, int a, int b,
0882: boolean shift) {
0883: return super .substituteText(c, a, b, shift);
0884: }
0885:
0886: @Override
0887: protected void reformat(JTextComponent component, String text) {
0888: try {
0889: BaseDocument doc = (BaseDocument) component
0890: .getDocument();
0891: int dotPos = component.getCaretPosition();
0892: Reformat reformat = Reformat.get(doc);
0893: reformat.lock();
0894:
0895: try {
0896: doc.atomicLock();
0897: try {
0898: int startOffset = Utilities.getRowStart(doc,
0899: dotPos);
0900: int endOffset = Utilities
0901: .getRowEnd(doc, dotPos);
0902: reformat.reformat(startOffset, endOffset);
0903: } finally {
0904: doc.atomicUnlock();
0905: }
0906: } finally {
0907: reformat.unlock();
0908: }
0909: } catch (BadLocationException e) {
0910: //ignore
0911: }
0912: }
0913:
0914: }
0915:
0916: private static class CharRefItem extends HTMLResultItem {
0917:
0918: public CharRefItem(String name, int offset, int length) {
0919: super (name, offset, length);
0920: this .baseText = name;
0921: }
0922:
0923: public CharRefItem(String name, int offset, int length,
0924: String helpID) {
0925: super (name, offset, length, helpID);
0926: this .baseText = name;
0927: }
0928:
0929: Color getPaintColor() {
0930: return Color.red.darker();
0931: }
0932:
0933: public CharSequence getSortText() {
0934: String itext = getItemText();
0935: return itext.endsWith(";") ? itext.substring(0, itext
0936: .length() - 1) : itext;
0937: }
0938:
0939: public String getItemText() {
0940: return "&" + baseText + ";";
0941: } // NOI18N
0942: }
0943:
0944: private static class TagItem extends HTMLResultItem {
0945:
0946: public TagItem(String name, int offset, int length) {
0947: super (name, offset, length);
0948: }
0949:
0950: public TagItem(String name, int offset, int length,
0951: String helpID) {
0952: super (name, offset, length, helpID);
0953: }
0954:
0955: public boolean substituteText(JTextComponent c, int a, int b,
0956: boolean shift) {
0957: replaceText(c, "<" + baseText + (shift ? " >" : ">")); // NOI18N
0958: if (shift) {
0959: Caret caret = c.getCaret();
0960: caret.setDot(caret.getDot() - 1);
0961: }
0962: Completion.get().showCompletion(); //show the completion to possibly offer an end tag (end tag autocompletion feature)
0963: return !shift; // flag == false;
0964: }
0965:
0966: Color getPaintColor() {
0967: return Color.blue;
0968: }
0969:
0970: public String getItemText() {
0971: return "<" + baseText + ">";
0972: } // NOI18N
0973: }
0974:
0975: private static class SetAttribItem extends HTMLResultItem {
0976: boolean required;
0977:
0978: public SetAttribItem(String name, int offset, int length,
0979: boolean required) {
0980: super (name, offset, length);
0981: this .required = required;
0982: }
0983:
0984: public SetAttribItem(String name, int offset, int length,
0985: boolean required, String helpID) {
0986: super (name, offset, length, helpID);
0987: this .required = required;
0988: }
0989:
0990: Color getPaintColor() {
0991: return required ? Color.red : Color.green.darker();
0992: }
0993:
0994: String getPaintText() {
0995: return baseText;
0996: }
0997:
0998: public String getItemText() {
0999: return baseText;
1000: } //NOI18N
1001:
1002: public boolean substituteText(JTextComponent c, int a, int b,
1003: boolean shift) {
1004: replaceText(c, baseText + "=\"\""); //NOI18N
1005: Caret caret = c.getCaret();
1006: caret.setDot(caret.getDot() - 1);
1007:
1008: return false; // always refresh
1009: }
1010: }
1011:
1012: private static class BooleanAttribItem extends HTMLResultItem {
1013:
1014: boolean required;
1015:
1016: public BooleanAttribItem(String name, int offset, int length,
1017: boolean required) {
1018: super (name, offset, length);
1019: this .required = required;
1020: }
1021:
1022: public BooleanAttribItem(String name, int offset, int length,
1023: boolean required, String helpID) {
1024: super (name, offset, length, helpID);
1025: this .required = required;
1026: }
1027:
1028: Color getPaintColor() {
1029: return required ? Color.red : Color.green.darker();
1030: }
1031:
1032: public boolean substituteText(JTextComponent c, int a, int b,
1033: boolean shift) {
1034: replaceText(c, shift ? baseText + " " : baseText); // NOI18N
1035: return false; // always refresh
1036: }
1037: }
1038:
1039: private static class PlainAttribItem extends HTMLResultItem {
1040:
1041: boolean required;
1042:
1043: public PlainAttribItem(String name, int offset, int length,
1044: boolean required) {
1045: super (name, offset, length);
1046: this .required = required;
1047: }
1048:
1049: public PlainAttribItem(String name, int offset, int length,
1050: boolean required, String helpID) {
1051: super (name, offset, length, helpID);
1052: this .required = required;
1053: }
1054:
1055: Color getPaintColor() {
1056: return required ? Color.red : Color.green.darker();
1057: }
1058:
1059: public boolean substituteText(JTextComponent c, int a, int b,
1060: boolean shift) {
1061: replaceText(c, baseText + "=\"\""); //NOI18N
1062: Caret caret = c.getCaret();
1063: caret.setDot(caret.getDot() - 1);
1064: return false; // always refresh
1065: }
1066: }
1067:
1068: private static class ValueItem extends HTMLResultItem {
1069:
1070: private String quotationChar = null;
1071:
1072: public ValueItem(String name, int offset, int length,
1073: String quotationChar) {
1074: this (name, offset, length);
1075: this .quotationChar = quotationChar;
1076: }
1077:
1078: public ValueItem(String name, int offset, int length) {
1079: super (name, offset, length);
1080: }
1081:
1082: public CharSequence getInsertPrefix() {
1083: if (quotationChar == null) {
1084: return super .getInsertPrefix();
1085: } else {
1086: return quotationChar + super .getInsertPrefix();
1087: }
1088: }
1089:
1090: Color getPaintColor() {
1091: return Color.magenta;
1092: }
1093:
1094: public boolean substituteText(JTextComponent c, int a, int b,
1095: boolean shift) {
1096: //check whether there is already a " char after the CC offset
1097: BaseDocument doc = (BaseDocument) c.getDocument();
1098: boolean hasQuote = false;
1099: try {
1100: String currentText = doc.getText(c.getCaretPosition(),
1101: 1);
1102: hasQuote = "\"".equals(currentText);
1103: } catch (BadLocationException ble) {
1104: //do nothing
1105: }
1106: String quotedText = ((quotationChar == null) ? baseText
1107: : quotationChar + baseText
1108: + (hasQuote ? "" : quotationChar));
1109: replaceText(c, quotedText);
1110: return !shift;
1111: }
1112: }
1113:
1114: static class DocQuery extends AsyncCompletionQuery {
1115:
1116: private HTMLResultItem item;
1117:
1118: DocQuery(HTMLResultItem item) {
1119: this .item = item;
1120: }
1121:
1122: protected void query(CompletionResultSet resultSet,
1123: Document doc, int caretOffset) {
1124: if (item != null
1125: && item.getHelpID() != null
1126: && HelpManager.getDefault().findHelpItem(
1127: item.getHelpID()) != null) {
1128: resultSet.setDocumentation(new DocItem(item));
1129: }
1130: resultSet.finish();
1131: }
1132:
1133: }
1134:
1135: static class LinkDocItem implements CompletionDocumentation {
1136: private URL url;
1137:
1138: public LinkDocItem(URL url) {
1139: this .url = url;
1140: }
1141:
1142: public String getText() {
1143: return null;
1144: /*
1145: String anchor = HelpManager.getDefault().getAnchorText(url);
1146: if(anchor != null)
1147: return HelpManager.getDefault().getHelpText(url, anchor);
1148: else
1149: return HelpManager.getDefault().getHelpText(url);
1150: */
1151: }
1152:
1153: public URL getURL() {
1154: return url;
1155: }
1156:
1157: public CompletionDocumentation resolveLink(String link) {
1158: return new LinkDocItem(HelpManager.getDefault()
1159: .getRelativeURL(url, link));
1160: }
1161:
1162: public Action getGotoSourceAction() {
1163: return null;
1164: }
1165:
1166: }
1167:
1168: public static class DocItem implements CompletionDocumentation {
1169: private String name;
1170:
1171: public DocItem(HTMLResultItem ri) {
1172: this (ri.getHelpID());
1173: }
1174:
1175: public DocItem(String name) {
1176: this .name = name;
1177: }
1178:
1179: public String getText() {
1180: String help = HelpManager.getDefault().getHelp(name);
1181: return help;
1182: }
1183:
1184: public URL getURL() {
1185: return HelpManager.getDefault().getHelpURL(name);
1186: }
1187:
1188: public CompletionDocumentation resolveLink(String link) {
1189: String currentLink = HelpManager.getDefault().findHelpItem(
1190: name).getFile();
1191: return new LinkDocItem(HelpManager.getDefault()
1192: .getRelativeURL(
1193: HelpManager.getDefault().getHelpURL(name),
1194: link));
1195: }
1196:
1197: public Action getGotoSourceAction() {
1198: return null;
1199: }
1200: }
1201:
1202: public static class HTMLCompletionResult extends
1203: CompletionQuery.DefaultResult {
1204: private int substituteOffset;
1205:
1206: public HTMLCompletionResult(JTextComponent component,
1207: String title, List data, int offset, int len) {
1208: super (component, title, data, offset, len);
1209: substituteOffset = offset - len;
1210: }
1211:
1212: public int getSubstituteOffset() {
1213: return substituteOffset;
1214: }
1215: }
1216:
1217: }
|