001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.xml.text.completion;
043:
044: import java.util.Enumeration;
045: import javax.swing.text.BadLocationException;
046: import javax.swing.text.Document;
047: import javax.swing.event.DocumentListener;
048: import javax.swing.event.DocumentEvent;
049:
050: import org.netbeans.editor.*;
051: import org.netbeans.editor.ext.*;
052: import org.netbeans.modules.xml.api.model.GrammarQuery;
053: import org.netbeans.modules.xml.api.model.HintContext;
054: import org.netbeans.modules.xml.text.syntax.*;
055: import org.netbeans.modules.xml.text.syntax.dom.*;
056: import org.netbeans.modules.xml.text.api.XMLDefaultTokenContext;
057: import org.w3c.dom.Element;
058: import org.w3c.dom.NamedNodeMap;
059: import org.w3c.dom.Node;
060: import org.openide.util.WeakListeners;
061: import org.openide.ErrorManager;
062:
063: /**
064: * Helper class used in XMLCompletionQuery and other classes that use grammar
065: * Instances of this class must only be constructed and used from the AWT
066: * dispatch thread, because this implementation is not reentrant (see ctx field).
067: *
068: * @author asgeir@dimonsoftware.com
069: */
070: final class SyntaxQueryHelper {
071:
072: public final static int COMPLETION_TYPE_UNKNOWN = 0;
073: public final static int COMPLETION_TYPE_ATTRIBUTE = 1;
074: public final static int COMPLETION_TYPE_VALUE = 2;
075: public final static int COMPLETION_TYPE_ELEMENT = 3;
076: public final static int COMPLETION_TYPE_ENTITY = 4;
077: public final static int COMPLETION_TYPE_NOTATION = 5;
078: public final static int COMPLETION_TYPE_DTD = 6;
079:
080: /** Currect oken or previous one if at token boundary */
081: private TokenItem token = null;
082:
083: private String preText = "";
084:
085: private int erase = 0;
086:
087: private int tunedOffset = 0;
088:
089: private SyntaxElement element;
090:
091: private int completionType = 0;
092:
093: private boolean tokenBoundary;
094:
095: private DefaultContext ctx = new DefaultContext();
096:
097: /** Creates a new instance of SyntaxQueryHelper */
098: public SyntaxQueryHelper(XMLSyntaxSupport sup, int offset)
099: throws BadLocationException, IllegalStateException {
100: tunedOffset = offset;
101: token = sup.getPreviousToken(tunedOffset);
102: if (token != null) { // inside document
103: tokenBoundary = token.getOffset()
104: + token.getImage().length() == tunedOffset;
105: } else {
106: //??? start of document no choice now, but should be prolog if not followed by it
107: throw new BadLocationException(
108: "No token found at current position", offset); // NOI18N
109: }
110:
111: // find out last typed chars that can hint
112:
113: int itemOffset = token.getOffset();
114: preText = "";
115: erase = 0;
116: int eraseRight = 0;
117: int id = token.getTokenID().getNumericID();
118:
119: // determine last typed text, prefix text
120:
121: if (tokenBoundary == false) {
122:
123: preText = token.getImage().substring(0,
124: tunedOffset - token.getOffset());
125: if ("".equals(preText))
126: throw new IllegalStateException(
127: "Cannot get token prefix at " + tunedOffset);
128:
129: // manipulate tunedOffset to delete rest of an old name
130: // for cases where it iseasy to locate original name end
131:
132: if (sup.lastTypedChar() != '<'
133: && sup.lastTypedChar() != '&') {
134: switch (id) {
135:
136: case XMLDefaultTokenContext.TAG_ID:
137: case XMLDefaultTokenContext.CHARACTER_ID:
138: case XMLDefaultTokenContext.ARGUMENT_ID:
139:
140: int i = token.getImage().length();
141: int tail = i - (tunedOffset - itemOffset);
142: tunedOffset += tail;
143: eraseRight = tail;
144: break;
145: }
146: }
147: } else {
148: switch (id) {
149: case XMLDefaultTokenContext.TEXT_ID:
150: case XMLDefaultTokenContext.TAG_ID:
151: case XMLDefaultTokenContext.ARGUMENT_ID:
152: case XMLDefaultTokenContext.CHARACTER_ID:
153: case XMLCompletionQuery.PI_CONTENT_ID:
154: preText = token.getImage();
155: break;
156: }
157: }
158:
159: // adjust how much do you want to erase from the preText
160:
161: switch (id) {
162: case XMLDefaultTokenContext.TAG_ID:
163: // do not erase start delimiters
164: erase = preText.length() - 1 + eraseRight;
165: break;
166: case XMLDefaultTokenContext.CHARACTER_ID:
167: //entity references
168: erase = preText.length() + -1 + eraseRight;
169: break;
170: case XMLDefaultTokenContext.ARGUMENT_ID:
171: erase = preText.length() + eraseRight;
172: break;
173: case XMLDefaultTokenContext.VALUE_ID:
174: erase = preText.length();
175: if (erase > 0
176: && (preText.charAt(0) == '\'' || preText.charAt(0) == '"')) {
177: // Because of attribute values, preText is adjusted in initContext
178: erase--;
179: } else
180: break;
181: }
182:
183: element = sup.getElementChain(tunedOffset);
184:
185: if (element == null)
186: throw new IllegalStateException(
187: "There exists a token therefore a syntax element must exist at "
188: + offset + ", too.");
189:
190: // completion request originates from area covered by DOM,
191: if (element instanceof SyntaxNode
192: && ((SyntaxNode) element).getNodeType() != Node.DOCUMENT_TYPE_NODE) {
193: completionType = initContext();
194: } else {
195: // prolog, internal DTD no completition yet
196: completionType = COMPLETION_TYPE_DTD;
197: }
198: }
199:
200: /**
201: * Find out what to complete: attribute, value, element, entity or notation?
202: * <p>
203: * <pre>
204: * Triggering criteria:
205: *
206: * ELEMENT TOKEN (,=seq) PRETEXT QUERY
207: * -------------------------------------------------------------------
208: * Text text < element name
209: * Text text </ pairing end element
210: * StartTag tag <prefix element name
211: * StartTag ws attribute name
212: * StartTag attr, operator = quoted attribute value
213: * StartTag value 'prefix attribute value
214: * StartTag tag > element value
215: * Text text & entity ref name
216: * StartTag value & entity ref name
217: * </pre>
218: *
219: * @return the type of completion which is one of
220: * COMPLETION_TYPE_UNKNOWN = 0,
221: * COMPLETION_TYPE_ATTRIBUTE = 1,
222: * COMPLETION_TYPE_VALUE = 2,
223: * COMPLETION_TYPE_ELEMENT = 3,
224: * COMPLETION_TYPE_ENTITY = 4,
225: * COMPLETION_TYPE_NOTATION = 5.
226: */
227: private int initContext() {
228: int id = token.getTokenID().getNumericID();
229: SyntaxNode syntaxNode = (SyntaxNode) element;
230:
231: switch (id) {
232: case XMLDefaultTokenContext.TEXT_ID:
233: if (preText.endsWith("<")) {
234: ctx.init(syntaxNode, "");
235: return COMPLETION_TYPE_ELEMENT;
236: } else if (preText.startsWith("&")) {
237: ctx.init(syntaxNode, preText.substring(1));
238: return COMPLETION_TYPE_ENTITY;
239: } else {
240: //??? join all previous texts?
241: // No they are DOM nodes.
242: ctx.init(syntaxNode, preText);
243: return COMPLETION_TYPE_VALUE;
244: }
245: // break;
246:
247: case XMLDefaultTokenContext.TAG_ID:
248: if (StartTag.class.equals(syntaxNode.getClass())
249: || EmptyTag.class.equals(syntaxNode.getClass())) {
250: if (preText.equals("")) {
251: //??? should not occure
252: if (token.getImage().endsWith(">")) {
253: ctx.init(syntaxNode, preText);
254: return COMPLETION_TYPE_VALUE;
255: } else {
256: ctx.init(syntaxNode, preText);
257: return COMPLETION_TYPE_ELEMENT;
258: }
259: } else if (preText.endsWith("/>")) {
260: ctx.init(syntaxNode, "");
261: return COMPLETION_TYPE_VALUE;
262: } else if (preText.endsWith(">")) {
263: ctx.init(syntaxNode, "");
264: return COMPLETION_TYPE_VALUE;
265: } else if (preText.startsWith("</")) {
266: //??? replace immediatelly?
267: ctx.init(syntaxNode, preText.substring(2));
268: return COMPLETION_TYPE_ELEMENT;
269: } else if (preText.startsWith("<")) {
270: ctx.init(syntaxNode, preText.substring(1));
271: return COMPLETION_TYPE_ELEMENT;
272: }
273: } else if (EndTag.class.equals(syntaxNode.getClass())
274: && preText.startsWith("</")) {
275: //endtag
276: ctx.init(syntaxNode, preText.substring(2));
277: return COMPLETION_TYPE_ELEMENT;
278: } else {
279: // pairing tag completion if not at boundary
280: if ("".equals(preText)
281: && token.getImage().endsWith(">")) {
282: ctx.init(syntaxNode, preText);
283: return COMPLETION_TYPE_VALUE;
284: }
285: }
286: break;
287:
288: case XMLDefaultTokenContext.VALUE_ID:
289: if (preText.endsWith("&")) {
290: ctx.init(syntaxNode, "");
291: return COMPLETION_TYPE_ENTITY;
292: } else if ("".equals(preText)) { //??? improve check to addres inner '"'
293: String image = token.getImage();
294: char ch = image.charAt(image.length() - 1);
295:
296: // findout if it is closing '
297:
298: if (ch == '\'' || ch == '"') {
299:
300: if (image.charAt(0) == ch && image.length() > 1) {
301: // we got whole quoted value as single token ("xxx"|)
302: return COMPLETION_TYPE_UNKNOWN;
303: }
304:
305: boolean closing = false;
306: TokenItem prev = token.getPrevious();
307:
308: while (prev != null) {
309: int tid = prev.getTokenID().getNumericID();
310: if (tid == XMLDefaultTokenContext.VALUE_ID) {
311: closing = true;
312: break;
313: } else if (tid == XMLDefaultTokenContext.CHARACTER_ID) {
314: prev = prev.getPrevious();
315: } else {
316: break;
317: }
318: }
319: if (closing == false) {
320: ctx.init(syntaxNode, preText);
321: return COMPLETION_TYPE_VALUE;
322: }
323: } else {
324: ctx.init(syntaxNode, preText);
325: return COMPLETION_TYPE_VALUE;
326: }
327: } else {
328: // This is probably an attribute value
329: // Let's find the matching attribute node and use it to initialize the context
330: NamedNodeMap attrs = syntaxNode.getAttributes();
331: int maxOffsetLessThanCurrent = -1;
332: Node curAttrNode = null;
333: for (int ind = 0; ind < attrs.getLength(); ind++) {
334: AttrImpl attr = (AttrImpl) attrs.item(ind);
335: int attrTokOffset = attr.getFirstToken()
336: .getOffset();
337: if (attrTokOffset > maxOffsetLessThanCurrent
338: && attrTokOffset < token.getOffset()) {
339: maxOffsetLessThanCurrent = attrTokOffset;
340: curAttrNode = attr;
341: }
342: }
343:
344: // eliminate "'",'"' delimiters
345: if (preText.length() > 0) {
346: preText = preText.substring(1);
347: }
348: if (curAttrNode != null) {
349: ctx.init(curAttrNode, preText);
350: } else {
351: ctx.init(syntaxNode, preText);
352: }
353: return COMPLETION_TYPE_VALUE;
354: }
355: break;
356:
357: case XMLDefaultTokenContext.OPERATOR_ID:
358: if ("".equals(preText)) {
359: if ("=".equals(token.getImage())) {
360: ctx.init(syntaxNode, "");
361: return COMPLETION_TYPE_VALUE;
362: }
363: }
364: break;
365:
366: case XMLDefaultTokenContext.WS_ID:
367: if ((StartTag.class.equals(syntaxNode.getClass()) || EmptyTag.class
368: .equals(syntaxNode.getClass()))
369: && !token.getImage().startsWith("/")) {
370: ctx.init((Element) syntaxNode, ""); // GrammarQuery.v2 takes Element ctx
371: return COMPLETION_TYPE_ATTRIBUTE;
372: } else {
373: // end tag no attributes to complete
374: return COMPLETION_TYPE_UNKNOWN;
375: }
376: // break;
377:
378: case XMLDefaultTokenContext.ARGUMENT_ID:
379: if (StartTag.class.equals(syntaxNode.getClass())
380: || EmptyTag.class.equals(syntaxNode.getClass())) {
381: //try to find the current attribute
382: Tag tag = (Tag) syntaxNode;
383: NamedNodeMap nnm = tag.getAttributes();
384: for (int i = 0; i < nnm.getLength(); i++) {
385: AttrImpl attrNode = (AttrImpl) nnm.item(i);
386: if (attrNode.getFirstToken().getOffset() == token
387: .getOffset()) {
388: ctx.init(attrNode, preText);
389: }
390: }
391: if (!ctx.isInitialized()) {
392: ctx.init((Element) syntaxNode, preText); // GrammarQuery.v2 takes Element ctx
393: }
394: return COMPLETION_TYPE_ATTRIBUTE;
395: }
396: break;
397:
398: case XMLDefaultTokenContext.CHARACTER_ID: // entity reference
399: if (preText.startsWith("&#")) {
400: // character ref, ignore
401: return COMPLETION_TYPE_UNKNOWN;
402: } else if (preText.endsWith(";")) {
403: ctx.init(syntaxNode, "");
404: return COMPLETION_TYPE_VALUE;
405: } else if (preText.startsWith("&")) {
406: ctx.init(syntaxNode, preText.substring(1));
407: return COMPLETION_TYPE_ENTITY;
408: } else if ("".equals(preText)) {
409: if (token.getImage().endsWith(";")) {
410: ctx.init(syntaxNode, preText);
411: return COMPLETION_TYPE_VALUE;
412: }
413: }
414: break;
415:
416: default:
417:
418: }
419:
420: // System.err.println("Cannot complete: " + syntaxNode + "\n\t" + token + "\n\t" + preText);
421: return COMPLETION_TYPE_UNKNOWN;
422: }
423:
424: public HintContext getContext() {
425: if (completionType != COMPLETION_TYPE_UNKNOWN
426: && completionType != COMPLETION_TYPE_DTD) {
427: return ctx;
428: } else {
429: return null;
430: }
431: }
432:
433: /** Current token or previous one if at token boundary. */
434: public TokenItem getToken() {
435: return token;
436: }
437:
438: public String getPreText() {
439: return preText;
440: }
441:
442: public int getEraseCount() {
443: return erase;
444: }
445:
446: public int getOffset() {
447: return tunedOffset;
448: }
449:
450: public SyntaxElement getSyntaxElement() {
451: return element;
452: }
453:
454: public int getCompletionType() {
455: return completionType;
456: }
457:
458: /** token boundary */
459: public boolean isBoundary() {
460: return tokenBoundary;
461: }
462:
463: }
|