001: /*
002: * Sun Public License Notice
003: *
004: * The contents of this file are subject to the Sun Public License
005: * Version 1.0 (the "License"). You may not use this file except in
006: * compliance with the License. A copy of the License is available at
007: * http://www.sun.com/
008: *
009: * The Original Code is NetBeans. The Initial Developer of the Original
010: * Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun
011: * Microsystems, Inc. All Rights Reserved.
012: */
013:
014: package org.netbeans.editor.ext.html;
015:
016: import java.awt.Color;
017: import java.util.ArrayList;
018: import java.util.Collection;
019: import java.util.Iterator;
020: import java.util.List;
021:
022: import javax.swing.text.BadLocationException;
023: import javax.swing.text.Caret;
024: import javax.swing.text.JTextComponent;
025:
026: import org.netbeans.editor.BaseDocument;
027: import org.netbeans.editor.SyntaxSupport;
028: import org.netbeans.editor.TokenID;
029: import org.netbeans.editor.TokenItem;
030: import org.netbeans.editor.Utilities;
031: import org.netbeans.editor.ext.CompletionQuery;
032: import org.netbeans.editor.ext.html.dtd.DTD;
033:
034: /**
035: * HTML completion results finder
036: *
037: * @author Petr Nejedly
038: * @version 1.00
039: */
040: public class HTMLCompletionQuery implements CompletionQuery {
041:
042: /**
043: * Perform the query on the given component. The query usually gets the
044: * component's document, the caret position and searches back to examine
045: * surrounding context. Then it returns the result.
046: *
047: * @param component
048: * the component to use in this query.
049: * @param offset
050: * position in the component's document to which the query will
051: * be performed. Usually it's a caret position.
052: * @param support
053: * syntax-support that will be used during resolving of the
054: * query.
055: * @return result of the query or null if there's no result.
056: */
057: public CompletionQuery.Result query(JTextComponent component,
058: int offset, SyntaxSupport support) {
059: BaseDocument doc = (BaseDocument) component.getDocument();
060: if (doc.getLength() == 0)
061: return null; // nothing to examine
062: HTMLSyntaxSupport sup = (HTMLSyntaxSupport) support
063: .get(HTMLSyntaxSupport.class);
064: if (sup == null)
065: return null;// No SyntaxSupport for us, no hint for user
066: DTD dtd = sup.getDTD();
067: if (dtd == null)
068: return null; // We have no knowledge about the structure!
069:
070: try {
071: TokenItem item = null;
072: TokenItem prev = null;
073: // are we inside token or between tokens
074: boolean inside = false;
075:
076: item = sup.getTokenChain(offset, offset + 1);
077: if (item != null) { // inside document
078: prev = item.getPrevious();
079: inside = item.getOffset() < offset;
080: } else { // @ end of document
081: prev = sup.getTokenChain(offset - 1, offset); // !!!
082: }
083: boolean begin = (prev == null && !inside);
084: /*
085: * if( prev == null && !inside ) System.err.println( "Beginning of
086: * document, first token = " + item.getTokenID() ); else if( item ==
087: * null ) System.err.println( "End of document, last token = " +
088: * prev.getTokenID() ); else if( ! inside ) System.err.println(
089: * "Between tokens " + prev.getTokenID() + " and " +
090: * item.getTokenID() ); else System.err.println( "Inside token " +
091: * item.getTokenID() );
092: */
093:
094: if (begin)
095: return null;
096:
097: TokenID id = null;
098: List l = null;
099: int len = 1;
100: int itemOffset = 0;
101: String preText = null;
102:
103: if (inside) {
104: id = item.getTokenID();
105: preText = item.getImage().substring(0,
106: offset - item.getOffset());
107: itemOffset = item.getOffset();
108: } else {
109: id = prev.getTokenID();
110: preText = prev.getImage().substring(0,
111: offset - prev.getOffset());
112: itemOffset = prev.getOffset();
113: }
114:
115: /*
116: * Here are completion finders, each have its own set of rules and
117: * source of results They are now written just for testing rules, I
118: * will rewrite them to more compact and faster, tree form, as soon
119: * as i'll have them all.
120: */
121:
122: /* Character reference finder */
123: if ((id == HTMLTokenContext.TEXT || id == HTMLTokenContext.VALUE)
124: && preText.endsWith("&")) {
125: l = translateCharRefs(offset - len, len, dtd
126: .getCharRefList(""));
127: } else if (id == HTMLTokenContext.CHARACTER) {
128: if (inside || !preText.endsWith(";")) {
129: len = offset - itemOffset;
130: l = translateCharRefs(offset - len, len, dtd
131: .getCharRefList(preText.substring(1)));
132: }
133: /* Tag finder */
134: } else if (id == HTMLTokenContext.TEXT
135: && preText.endsWith("<")) {
136: // There will be lookup for possible StartTags, in SyntaxSupport
137: // l = translateTags( offset-len, len, sup.getPossibleStartTags
138: // ( offset-len, "" ) );
139: l = translateTags(offset - len, len, dtd
140: .getElementList(""));
141:
142: // System.err.println("l = " + l );
143: } else if (id == HTMLTokenContext.TAG
144: && preText.startsWith("<")
145: && !preText.startsWith("</")) {
146: len = offset - itemOffset;
147: l = translateTags(offset - len, len, dtd
148: .getElementList(preText.substring(1)));
149: // l = translateTags( offset-len, len, sup.getPossibleStartTags
150: // ( offset-len, preText.substring( 1 ) ) );
151: /* EndTag finder */
152: } else if (id == HTMLTokenContext.TEXT
153: && preText.endsWith("</")) {
154: len = 2;
155: l = sup.getPossibleEndTags(offset, "");
156: } else if (id == HTMLTokenContext.TAG
157: && preText.startsWith("</")) {
158: len = offset - itemOffset;
159: l = sup
160: .getPossibleEndTags(offset, preText
161: .substring(2));
162:
163: /* Argument finder */
164: /*
165: * TBD: It is possible to have arg just next to quoted value of
166: * previous arg, these rules doesn't match start of such arg
167: * this case because of need for matching starting quote
168: */
169: } else if (id == HTMLTokenContext.WS
170: || id == HTMLTokenContext.ARGUMENT) {
171: SyntaxElement elem = null;
172: try {
173: elem = sup.getElementChain(offset);
174: } catch (BadLocationException e) {
175: return null;
176: }
177:
178: if (elem == null)
179: return null;
180:
181: if (elem.getType() == SyntaxElement.TYPE_TAG) { // not endTags
182: SyntaxElement.Tag tagElem = (SyntaxElement.Tag) elem;
183:
184: String tagName = tagElem.getName().toUpperCase();
185: DTD.Element tag = dtd.getElement(tagName);
186:
187: if (tag == null)
188: return null; // unknown tag
189:
190: String prefix = (id == HTMLTokenContext.ARGUMENT) ? preText
191: : "";
192: len = prefix.length();
193:
194: List possible = tag.getAttributeList(prefix); // All
195: // attribs
196: // of given
197: // tag
198:
199: Collection existing = tagElem.getAttributes(); // Attribs
200: // already
201: // used
202:
203: String wordAtCursor = "";
204: try {
205: wordAtCursor = Utilities.getWord(doc, Utilities
206: .getWordStart(doc, offset));
207: } catch (BadLocationException e) {
208: }
209:
210: l = new ArrayList();
211: for (Iterator i = possible.iterator(); i.hasNext();) {
212: DTD.Attribute attr = (DTD.Attribute) i.next();
213: String aName = attr.getName();
214:
215: if (aName.equals(prefix)
216: || !existing.contains(aName)
217: || wordAtCursor.equals(aName))
218: l.add(attr);
219: }
220: l = translateAttribs(offset - len, len, l);
221: }
222:
223: /* Value finder */
224: /*
225: * Suggestion - find special-meaning attributes ( IMG src, A
226: * href, color,.... - may be better resolved by attr type, may
227: * be moved to propertysheet
228: */
229: } else if (id == HTMLTokenContext.VALUE
230: || id == HTMLTokenContext.OPERATOR
231: || id == HTMLTokenContext.WS
232: && (inside ? prev : prev.getPrevious())
233: .getTokenID() == HTMLTokenContext.OPERATOR) {
234: SyntaxElement elem = null;
235: try {
236: elem = sup.getElementChain(offset);
237: } catch (BadLocationException e) {
238: return null;
239: }
240:
241: if (elem == null)
242: return null;
243:
244: // between Tag and error - common state when entering OOTL, e.g.
245: // <BDO dir=>
246: if (elem.getType() == SyntaxElement.TYPE_ERROR) {
247: elem = elem.getPrevious();
248: if (elem == null)
249: return null;
250: }
251:
252: if (elem.getType() == SyntaxElement.TYPE_TAG) {
253: SyntaxElement.Tag tagElem = (SyntaxElement.Tag) elem;
254:
255: String tagName = tagElem.getName().toUpperCase();
256: DTD.Element tag = dtd.getElement(tagName);
257: if (tag == null)
258: return null; // unknown tag
259:
260: TokenItem argItem = prev;
261: while (argItem != null
262: && argItem.getTokenID() != HTMLTokenContext.ARGUMENT)
263: argItem = argItem.getPrevious();
264: if (argItem == null)
265: return null; // no ArgItem
266: String argName = argItem.getImage();
267:
268: DTD.Attribute arg = tag.getAttribute(argName);
269: if (arg == null
270: || arg.getType() != DTD.Attribute.TYPE_SET)
271: return null;
272:
273: if (id != HTMLTokenContext.VALUE) {
274: len = 0;
275: l = translateValues(offset - len, len, arg
276: .getValueList(""));
277: } else {
278: len = offset - itemOffset;
279: l = translateValues(offset - len, len, arg
280: .getValueList(preText));
281: }
282: }
283: }
284:
285: // System.err.println("l = " + l );
286: if (l == null)
287: return null;
288: else
289: return new CompletionQuery.DefaultResult(component,
290: "Results for DOCTYPE " + dtd.getIdentifier(),
291: l, offset, len);
292:
293: } catch (BadLocationException e) {
294: e.printStackTrace();
295: }
296: return null;
297: }
298:
299: List translateCharRefs(int offset, int length, List refs) {
300: List result = new ArrayList(refs.size());
301: for (Iterator i = refs.iterator(); i.hasNext();) {
302: result.add(new CharRefItem(((DTD.CharRef) i.next())
303: .getName(), offset, length));
304: }
305: return result;
306: }
307:
308: List translateTags(int offset, int length, List tags) {
309: List result = new ArrayList(tags.size());
310: for (Iterator i = tags.iterator(); i.hasNext();) {
311: result.add(new TagItem(((DTD.Element) i.next()).getName(),
312: offset, length));
313: }
314: return result;
315: }
316:
317: List translateAttribs(int offset, int length, List attribs) {
318: List result = new ArrayList(attribs.size());
319: for (Iterator i = attribs.iterator(); i.hasNext();) {
320: DTD.Attribute attrib = (DTD.Attribute) i.next();
321: String name = attrib.getName();
322: switch (attrib.getType()) {
323: case DTD.Attribute.TYPE_BOOLEAN:
324: result.add(new BooleanAttribItem(name, offset, length,
325: attrib.isRequired()));
326: break;
327: case DTD.Attribute.TYPE_SET:
328: result.add(new SetAttribItem(name, offset, length,
329: attrib.isRequired()));
330: break;
331: case DTD.Attribute.TYPE_BASE:
332: result.add(new PlainAttribItem(name, offset, length,
333: attrib.isRequired()));
334: break;
335: }
336: }
337: return result;
338: }
339:
340: List translateValues(int offset, int length, List values) {
341: if (values == null)
342: return new ArrayList(0);
343: List result = new ArrayList(values.size());
344: for (Iterator i = values.iterator(); i.hasNext();) {
345: result.add(new ValueItem(((DTD.Value) i.next()).getName(),
346: offset, length));
347: }
348: return result;
349: }
350:
351: // Implementation of ResultItems for completion
352: /**
353: * The simple result item operating over an instance of the string, it is
354: * lightweight in the mean it doesn't allocate any new instances of anything
355: * and every data creates lazily on request to avoid creation of lot of
356: * string instances per completion result.
357: */
358: private static abstract class HTMLResultItem implements
359: CompletionQuery.ResultItem {
360: /** The Component used as a rubberStamp for painting items */
361: static javax.swing.JLabel rubberStamp = new javax.swing.JLabel();
362: static {
363: rubberStamp.setOpaque(true);
364: }
365:
366: /** The String on which is this ResultItem defined */
367: String baseText;
368: /** the remove and insert point for this item */
369: int offset;
370: /** The length of the text to be removed */
371: int length;
372:
373: public HTMLResultItem(String baseText, int offset, int length) {
374: this .baseText = baseText;
375: this .offset = offset;
376: this .length = length;
377: }
378:
379: boolean replaceText(JTextComponent component, String text) {
380: BaseDocument doc = (BaseDocument) component.getDocument();
381: doc.atomicLock();
382: try {
383: doc.remove(offset, length);
384: doc.insertString(offset, text, null);
385: } catch (BadLocationException exc) {
386: return false; // not sucessfull
387: } finally {
388: doc.atomicUnlock();
389: }
390: return true;
391: }
392:
393: public boolean substituteCommonText(JTextComponent c, int a,
394: int b, int subLen) {
395: return replaceText(c, getItemText().substring(0, subLen));
396: }
397:
398: public boolean substituteText(JTextComponent c, int a, int b,
399: boolean shift) {
400: return replaceText(c, getItemText());
401: }
402:
403: /** @return Properly colored JLabel with text gotten from <CODE>getPaintText()</CODE>. */
404: public java.awt.Component getPaintComponent(
405: javax.swing.JList list, boolean isSelected,
406: boolean cellHasFocus) {
407: // The space is prepended to avoid interpretation as HTML Label
408: rubberStamp.setText(" " + getPaintText()); // NOI18N
409: if (isSelected) {
410: rubberStamp
411: .setBackground(list.getSelectionBackground());
412: rubberStamp
413: .setForeground(list.getSelectionForeground());
414: } else {
415: rubberStamp.setBackground(list.getBackground());
416: rubberStamp.setForeground(getPaintColor());
417: }
418: return rubberStamp;
419: }
420:
421: /**
422: * The string used in painting by <CODE>getPaintComponent()</CODE>.
423: * It defaults to delegate to <CODE>getItemText()</CODE>.
424: *
425: * @return The String to be painted in Completion View.
426: */
427: String getPaintText() {
428: return getItemText();
429: }
430:
431: abstract Color getPaintColor();
432:
433: /**
434: * @return The String used for looking up the common part of multiple
435: * items and for default way of replacing the text
436: */
437: public String getItemText() {
438: return baseText;
439: }
440: }
441:
442: static class EndTagItem extends HTMLResultItem {
443:
444: public EndTagItem(String baseText, int offset, int length) {
445: super (baseText, offset, length);
446: }
447:
448: Color getPaintColor() {
449: return Color.blue;
450: }
451:
452: public String getItemText() {
453: return "</" + baseText + ">";
454: } // NOI18N
455:
456: public boolean substituteText(JTextComponent c, int a, int b,
457: boolean shift) {
458: return super .substituteText(c, a, b, shift);
459: }
460: }
461:
462: private static class CharRefItem extends HTMLResultItem {
463:
464: public CharRefItem(String name, int offset, int length) {
465: super (name, offset, length);
466: }
467:
468: Color getPaintColor() {
469: return Color.red.darker();
470: }
471:
472: public String getItemText() {
473: return "&" + baseText + ";";
474: } // NOI18N
475: }
476:
477: private static class TagItem extends HTMLResultItem {
478:
479: public TagItem(String name, int offset, int length) {
480: super (name, offset, length);
481: }
482:
483: public boolean substituteText(JTextComponent c, int a, int b,
484: boolean shift) {
485: replaceText(c, "<" + baseText + (shift ? " >" : ">")); // NOI18N
486: if (shift) {
487: Caret caret = c.getCaret();
488: caret.setDot(caret.getDot() - 1);
489: }
490: return !shift; // flag == false;
491: }
492:
493: Color getPaintColor() {
494: return Color.blue;
495: }
496:
497: public String getItemText() {
498: return "<" + baseText + ">";
499: } // NOI18N
500: }
501:
502: private static class SetAttribItem extends HTMLResultItem {
503: boolean required;
504:
505: public SetAttribItem(String name, int offset, int length,
506: boolean required) {
507: super (name, offset, length);
508: this .required = required;
509: }
510:
511: Color getPaintColor() {
512: return required ? Color.red : Color.green.darker();
513: }
514:
515: String getPaintText() {
516: return baseText;
517: }
518:
519: public String getItemText() {
520: return baseText + "=";
521: } // NOI18N
522:
523: public boolean substituteText(JTextComponent c, int a, int b,
524: boolean shift) {
525: super .substituteText(c, 0, 0, shift);
526: return false; // always refresh
527: }
528: }
529:
530: private static class BooleanAttribItem extends HTMLResultItem {
531:
532: boolean required;
533:
534: public BooleanAttribItem(String name, int offset, int length,
535: boolean required) {
536: super (name, offset, length);
537: this .required = required;
538: }
539:
540: Color getPaintColor() {
541: return required ? Color.red : Color.green.darker();
542: }
543:
544: public boolean substituteText(JTextComponent c, int a, int b,
545: boolean shift) {
546: replaceText(c, shift ? baseText + " " : baseText);
547: return false; // always refresh
548: }
549: }
550:
551: private static class PlainAttribItem extends HTMLResultItem {
552:
553: boolean required;
554:
555: public PlainAttribItem(String name, int offset, int length,
556: boolean required) {
557: super (name, offset, length);
558: this .required = required;
559: }
560:
561: Color getPaintColor() {
562: return required ? Color.red : Color.green.darker();
563: }
564:
565: public boolean substituteText(JTextComponent c, int a, int b,
566: boolean shift) {
567: replaceText(c, baseText + "=''"); // NOI18N
568: if (shift) {
569: Caret caret = c.getCaret();
570: caret.setDot(caret.getDot() - 1);
571: }
572: return false; // always refresh
573: }
574: }
575:
576: private static class ValueItem extends HTMLResultItem {
577:
578: public ValueItem(String name, int offset, int length) {
579: super (name, offset, length);
580: }
581:
582: Color getPaintColor() {
583: return Color.magenta;
584: }
585:
586: public boolean substituteText(JTextComponent c, int a, int b,
587: boolean shift) {
588: replaceText(c, shift ? baseText + " " : baseText); // NOI18N
589: return !shift;
590: }
591: }
592: }
|